peclet-dem
Performance-portable XPBD Discrete Element Method (Kokkos + ArborX)
Loading...
Searching...
No Matches
test_particle_migration.cpp
Go to the documentation of this file.
1// First MPI step for packing-gpu (mirrors cfd-gpu's test_mac_halo): validate that transport-core's
2// ParticleMigrator migrates packing-style particles across rank-owned blocks of the domain.
3//
4// packing-gpu stores particles as SoA float4 arrays (d_pos[.xyz,.w=inv_mass], d_vel, d_quat,
5// d_ang_vel, d_inv_inertia, d_scale, d_shape_ids) in a periodic box [domain_min, domain_min+size).
6// Here each particle's full per-particle record is the migrator's opaque payload; its position
7// drives ownership. We decompose the domain (ORB over a cell grid covering the box), then over
8// several random-walk + migrate steps require, globally and every step: particle count conserved,
9// every particle on its owning rank, and the id multiset exactly preserved (sum + xor reductions).
10//
11// Host-side: migration is infrequent and host-staged (CUDA-aware MPI is unavailable on this box),
12// so a real integration would download particles, migrate, and upload — the same pattern validated
13// here.
14#include <mpi.h>
15
16#include <array>
17#include <cstdint>
18#include <cstdio>
19#include <cstring>
20#include <vector>
21
22#include "peclet/core/common/types.hpp"
23#include "peclet/core/decomp/block_decomposer.hpp"
24#include "peclet/core/halo/particle_migrator.hpp"
25
26using namespace tpx;
27using peclet::core::decomp::BlockDecomposer;
28using peclet::core::halo::DomainMap;
29using peclet::core::halo::ParticleMigrator;
30
31// packing-gpu per-particle record (the migrator payload). Mirrors the SoA float4 state arrays.
33 float pos[4]; // .xyz position, .w inv_mass
34 float vel[4]; // .xyz velocity, .w type/phase
35 float quat[4]; // orientation
36 float ang_vel[4]; // angular velocity
37 float inv_inertia[4]; // diagonal inertia + pad
38 float scale;
40 std::int64_t id; // unique id for conservation checks
41};
42
43static double frac(std::uint64_t x) {
44 x ^= x >> 33;
45 x *= 0xff51afd7ed558ccdULL;
46 x ^= x >> 33;
47 return static_cast<double>(x & 0xFFFFFF) / static_cast<double>(0x1000000);
48}
49
50int main(int argc, char** argv) {
51 MPI_Init(&argc, &argv);
52 int rank = 0, size = 1;
53 MPI_Comm_rank(MPI_COMM_WORLD, &rank);
54 MPI_Comm_size(MPI_COMM_WORLD, &size);
55
56 // packing-style domain: a periodic box. Decompose a cell grid covering it.
57 const double dmin[3] = {0.0, 0.0, 0.0};
58 const double dsize[3] = {10.0, 8.0, 6.0};
59 IVec<3> gsize{40, 32, 24}; // decomposition resolution
60 BlockDecomposer<3> dec(static_cast<std::size_t>(size), gsize);
61
62 DomainMap<3> map;
63 for (int i = 0; i < 3; ++i) {
64 map.origin[i] = dmin[i];
65 map.cellSize[i] = dsize[i] / static_cast<double>(gsize[i]);
66 map.periodic[i] = true;
67 }
68 ParticleMigrator<3> mig;
69 mig.init(dec, rank, map, MPI_COMM_WORLD);
70
71 // Generate N particles deterministically, scattered across ranks by id%size.
72 const std::int64_t N = 5000;
73 const std::size_t stride = sizeof(PackingParticle);
74 std::vector<Vec<3>> pos;
75 std::vector<char> payload;
76 for (std::int64_t id = rank; id < N; id += size) {
77 Vec<3> x{dmin[0] + frac((std::uint64_t)id * 3 + 0) * dsize[0],
78 dmin[1] + frac((std::uint64_t)id * 3 + 1) * dsize[1],
79 dmin[2] + frac((std::uint64_t)id * 3 + 2) * dsize[2]};
80 pos.push_back(x);
82 p.pos[0] = (float)x[0];
83 p.pos[1] = (float)x[1];
84 p.pos[2] = (float)x[2];
85 p.pos[3] = 1.0f;
86 p.quat[3] = 1.0f;
87 p.scale = 1.0f;
88 p.shape_id = (int)(id % 4);
89 p.id = id;
90 std::size_t off = payload.size();
91 payload.resize(off + stride);
92 std::memcpy(&payload[off], &p, stride);
93 }
94
95 std::int64_t expectSum = N * (N - 1) / 2, expectXor = 0;
96 for (std::int64_t i = 0; i < N; ++i)
97 expectXor ^= i;
98
99 int fail = 0;
100 for (int step = 0; step < 6; ++step) {
101 mig.migrate(pos, payload, stride);
102
103 std::int64_t lcount = (std::int64_t)pos.size(), lsum = 0, lxor = 0;
104 for (std::size_t k = 0; k < pos.size(); ++k) {
105 if (mig.ownerOf(pos[k]) != rank)
106 ++fail;
108 std::memcpy(&p, &payload[k * stride], stride);
109 lsum += p.id;
110 lxor ^= p.id;
111 }
112 std::int64_t gcount = 0, gsum = 0, gxor = 0;
113 MPI_Allreduce(&lcount, &gcount, 1, MPI_INT64_T, MPI_SUM, MPI_COMM_WORLD);
114 MPI_Allreduce(&lsum, &gsum, 1, MPI_INT64_T, MPI_SUM, MPI_COMM_WORLD);
115 MPI_Allreduce(&lxor, &gxor, 1, MPI_INT64_T, MPI_BXOR, MPI_COMM_WORLD);
116 if (gcount != N || gsum != expectSum || gxor != expectXor) {
117 if (rank == 0)
118 std::fprintf(stderr, " step %d FAILED: count=%lld sum=%lld xor=%lld\n", step,
119 (long long)gcount, (long long)gsum, (long long)gxor);
120 ++fail;
121 }
122
123 // random walk for the next step
124 for (std::size_t k = 0; k < pos.size(); ++k) {
126 std::memcpy(&p, &payload[k * stride], stride);
127 for (int i = 0; i < 3; ++i)
128 pos[k][i] += (frac((std::uint64_t)p.id * 7 + i + step * 131) - 0.5) * 0.25 * dsize[i];
129 }
130 }
131
132 // Ghost particles: gather copies within one interaction radius of the block boundary, so a real
133 // integration would run cuBQL broadphase locally over owned + ghost particles per rank.
134 const double rcut = 0.5;
135 std::vector<Vec<3>> gpos;
136 std::vector<char> gpay;
137 std::size_t nghost = mig.gatherGhosts(pos, payload, stride, rcut, gpos, gpay);
138 // Invariant: every received ghost image lies within rcut of this rank's block.
139 Vec<3> img;
140 for (std::size_t k = 0; k < gpos.size(); ++k)
141 if (!mig.withinRcutOfBlock(gpos[k], rank, rcut, img))
142 ++fail;
143 long long lg = (long long)nghost, gghost = 0;
144 MPI_Reduce(&lg, &gghost, 1, MPI_LONG_LONG, MPI_SUM, 0, MPI_COMM_WORLD);
145
146 int total = 0;
147 MPI_Allreduce(&fail, &total, 1, MPI_INT, MPI_SUM, MPI_COMM_WORLD);
148 if (rank == 0) {
149 std::printf("# %lld ghost particles gathered (rcut=%.2f)\n", gghost, rcut);
150 if (total == 0)
151 std::printf("OK (np=%d): %lld packing particles migrate+ghost correctly over 6 steps\n", size,
152 (long long)N);
153 else
154 std::fprintf(stderr, "FAILED (np=%d): %d\n", size, total);
155 }
156 MPI_Finalize();
157 return total == 0 ? 0 : 1;
158}
static const double dmin[3]
static double frac(std::uint64_t x)
int main(int argc, char **argv)