core 1.5.0
Shared MPI block decomposition, asynchronous ghost-layer exchange, SDF geometry, load balancing (header-only C++20)
Loading...
Searching...
No Matches
particle_migrator.hpp
Go to the documentation of this file.
1// core — Lagrangian particle migration over the same block decomposition.
2//
3// After particles move, each one may now belong to a different rank's block. migrate() reassigns
4// every particle to the rank that owns the block containing its (periodically wrapped) position,
5// shipping departing particles to their new owner via the NBX engine — the dynamic/sparse exchange
6// the consensus protocol is built for. Positions and an opaque fixed-stride payload travel
7// together, so the caller can carry velocity, orientation, id, etc. without this layer knowing the
8// schema.
9//
10// This is the Lagrangian counterpart to peclet::core::halo::GridHaloTopology (Eulerian ghost
11// cells): same decomposition, same async engine, different payload — the field-agnostic design in
12// action.
13#ifndef PECLET_CORE_HALO_PARTICLE_MIGRATOR_HPP
14#define PECLET_CORE_HALO_PARTICLE_MIGRATOR_HPP
15
16#include <algorithm>
17#include <cmath>
18#include <cstring>
19#include <limits>
20#include <map>
21#include <utility>
22#include <vector>
23
28
29namespace peclet::core::halo {
30
32template <int Dim>
33struct DomainMap {
36 std::array<bool, Dim> periodic{};
37};
38
39template <int Dim>
41 public:
44 dec_ = &dec;
45 rank_ = rank;
46 map_ = map;
47 comm_ = comm;
48 }
49
53 IVec<Dim> cellOf(const Vec<Dim>& x) const {
54 IVec<Dim> g{};
55 const IVec<Dim>& gsize = dec_->globalSize();
56 for (int i = 0; i < Dim; ++i) {
57 double rel = (x[i] - map_.origin[i]) / map_.cellSize[i];
58 Index c = static_cast<Index>(std::floor(rel));
59 if (map_.periodic[i]) {
60 c = wrap(c, gsize[i]);
61 } else {
62 c = std::clamp<Index>(c, 0, gsize[i] - 1);
63 }
64 g[i] = c;
65 }
66 return g;
67 }
68
70 int ownerOf(const Vec<Dim>& x) const { return dec_->ownerOf(cellOf(x)); }
71
74 const IVec<Dim>& gsize = dec_->globalSize();
75 for (int i = 0; i < Dim; ++i) {
76 if (!map_.periodic[i])
77 continue;
78 double L = map_.cellSize[i] * static_cast<double>(gsize[i]);
79 double rel = x[i] - map_.origin[i];
80 rel = std::fmod(std::fmod(rel, L) + L, L);
81 x[i] = map_.origin[i] + rel;
82 }
83 return x;
84 }
85
89 std::size_t migrate(std::vector<Vec<Dim>>& pos, std::vector<char>& payload, std::size_t stride) {
90 const std::size_t n = pos.size();
91 const std::size_t posBytes = Dim * sizeof(double);
92 const std::size_t recBytes = posBytes + stride;
93
94 std::map<int, std::vector<char>> outbox;
95 std::size_t keep = 0;
96 sent_ = 0;
97 for (std::size_t i = 0; i < n; ++i) {
99 int owner = ownerOf(wx);
100 if (owner == rank_) {
101 pos[keep] = wx;
102 if (stride)
103 std::memmove(&payload[keep * stride], &payload[i * stride], stride);
104 ++keep;
105 } else {
106 auto& buf = outbox[owner];
107 std::size_t off = buf.size();
108 buf.resize(off + recBytes);
109 std::memcpy(buf.data() + off, wx.data(), posBytes);
110 if (stride)
111 std::memcpy(buf.data() + off + posBytes, &payload[i * stride], stride);
112 ++sent_;
113 }
114 }
115 pos.resize(keep);
116 payload.resize(keep * stride);
117
118 // Ship departing particles to their owners and absorb arrivals (NBX).
119 std::vector<std::pair<int, const std::vector<char>*>> queue;
120 queue.reserve(outbox.size());
121 for (auto& [r, buf] : outbox)
122 queue.emplace_back(r, &buf);
123
124 received_ = 0;
125 NbxEngine nbx(comm_);
126 std::size_t q = 0;
127 auto packNext = [&](std::vector<char>& out) -> int {
128 if (q >= queue.size())
129 return -1;
130 int dest = queue[q].first;
131 out = *queue[q].second;
132 ++q;
133 return dest;
134 };
135 auto onRecv = [&](int /*src*/, std::vector<char>& msg) {
136 std::size_t cnt = msg.size() / recBytes;
137 for (std::size_t i = 0; i < cnt; ++i) {
138 const char* rec = msg.data() + i * recBytes;
139 Vec<Dim> x{};
140 std::memcpy(x.data(), rec, posBytes);
141 pos.push_back(x);
142 if (stride) {
143 std::size_t off = payload.size();
144 payload.resize(off + stride);
145 std::memcpy(&payload[off], rec + posBytes, stride);
146 }
147 ++received_;
148 }
149 };
150 nbx.exchange(packNext, onRecv, /*tag=*/7401);
151 return pos.size();
152 }
153
154 std::size_t lastSent() const { return sent_; }
155 std::size_t lastReceived() const { return received_; }
156
157 int rank() const { return rank_; }
158 MPI_Comm comm() const { return comm_; }
159 const decomp::BlockDecomposer<Dim>& decomposer() const { return *dec_; }
160
163 bool withinRcutOfBlock(const Vec<Dim>& x, int r, double rcut, Vec<Dim>& img) const {
164 const auto& o = dec_->origins()[r];
165 const auto& s = dec_->sizes()[r];
166 const IVec<Dim>& gsize = dec_->globalSize();
167 double d2 = 0.0;
168 for (int i = 0; i < Dim; ++i) {
169 double lo = map_.origin[i] + o[i] * map_.cellSize[i];
170 double hi = map_.origin[i] + (o[i] + s[i]) * map_.cellSize[i];
171 double L = map_.cellSize[i] * static_cast<double>(gsize[i]);
172 double cands[3];
173 int nc = 0;
174 cands[nc++] = x[i];
175 if (map_.periodic[i]) {
176 cands[nc++] = x[i] - L;
177 cands[nc++] = x[i] + L;
178 }
179 double bestGap = std::numeric_limits<double>::infinity();
180 double bestImg = x[i];
181 for (int c = 0; c < nc; ++c) {
182 double p = cands[c];
183 double gap = (p < lo) ? (lo - p) : (p > hi) ? (p - hi) : 0.0;
184 if (gap < bestGap) {
185 bestGap = gap;
186 bestImg = p;
187 }
188 }
189 img[i] = bestImg;
190 d2 += bestGap * bestGap;
191 }
192 return d2 < rcut * rcut;
193 }
194
206 bool boxWithinRcutOfBlock(const Vec<Dim>& bmin, const Vec<Dim>& bmax, int r, double rcut) const {
207 const auto& o = dec_->origins()[r];
208 const auto& s = dec_->sizes()[r];
209 const IVec<Dim>& gsize = dec_->globalSize();
210 double d2 = 0.0;
211 for (int d = 0; d < Dim; ++d) {
212 const double lo = map_.origin[d] + o[d] * map_.cellSize[d];
213 const double hi = map_.origin[d] + (o[d] + s[d]) * map_.cellSize[d];
214 auto gap = [&](double shift) {
215 const double a = bmin[d] + shift, b = bmax[d] + shift;
216 return std::max(0.0, std::max(lo - b, a - hi));
217 };
218 double g = gap(0.0);
219 if (map_.periodic[d]) {
220 const double L = map_.cellSize[d] * static_cast<double>(gsize[d]);
221 g = std::min(g, std::min(gap(-L), gap(L)));
222 }
223 d2 += g * g;
224 }
225 return d2 < rcut * rcut;
226 }
227
228 void imagesWithinRcutOfBlock(const Vec<Dim>& x, int r, double rcut, bool allowIdentity,
229 std::vector<Vec<Dim>>& outShifts) const {
230 const auto& o = dec_->origins()[r];
231 const auto& s = dec_->sizes()[r];
232 const IVec<Dim>& gsize = dec_->globalSize();
233 double lo[Dim], hi[Dim], cand[Dim][3];
234 int nc[Dim];
235 for (int d = 0; d < Dim; ++d) {
236 lo[d] = map_.origin[d] + o[d] * map_.cellSize[d];
237 hi[d] = map_.origin[d] + (o[d] + s[d]) * map_.cellSize[d];
238 const double L = map_.cellSize[d] * static_cast<double>(gsize[d]);
239 cand[d][0] = 0.0;
240 nc[d] = 1;
241 if (map_.periodic[d]) {
242 cand[d][1] = -L;
243 cand[d][2] = L;
244 nc[d] = 3;
245 }
246 }
247 // Per-axis prefilter (exact): an image qualifies only if its total squared gap is below rc2,
248 // so every one of its per-axis gaps is -- keep, per axis and in candidate order, the shifts
249 // whose own gap passes, then enumerate their product in the same nesting (axis 0 fastest) as
250 // the full 3^Dim walk. Same set, same order, typically one candidate per axis instead of 27
251 // combinations (this runs for every owned particle x every rank at every topology build).
252 const double rc2 = rcut * rcut;
253 int keep[Dim][3];
254 double keepGap2[Dim][3];
255 int nk[Dim];
256 for (int d = 0; d < Dim; ++d) {
257 nk[d] = 0;
258 for (int k = 0; k < nc[d]; ++k) {
259 const double p = x[d] + cand[d][k];
260 const double gap = (p < lo[d]) ? (lo[d] - p) : (p > hi[d]) ? (p - hi[d]) : 0.0;
261 if (gap * gap < rc2) {
262 keep[d][nk[d]] = k;
263 keepGap2[d][nk[d]] = gap * gap;
264 ++nk[d];
265 }
266 }
267 if (nk[d] == 0)
268 return;
269 }
270 int total = 1;
271 for (int d = 0; d < Dim; ++d)
272 total *= nk[d];
273 for (int idx = 0; idx < total; ++idx) {
274 Vec<Dim> shift{};
275 int t = idx;
276 bool identity = true;
277 double d2 = 0.0;
278 for (int d = 0; d < Dim; ++d) {
279 const int j = t % nk[d];
280 t /= nk[d];
281 const int k = keep[d][j];
282 shift[d] = cand[d][k];
283 if (k != 0)
284 identity = false;
285 d2 += keepGap2[d][j];
286 }
287 if (d2 < rc2 && (allowIdentity || !identity))
288 outShifts.push_back(shift);
289 }
290 }
291
298 std::size_t gatherGhosts(const std::vector<Vec<Dim>>& pos, const std::vector<char>& payload,
299 std::size_t stride, double rcut, std::vector<Vec<Dim>>& ghostPos,
300 std::vector<char>& ghostPayload) {
301 ghostPos.clear();
302 ghostPayload.clear();
303 const std::size_t posBytes = Dim * sizeof(double);
304 const std::size_t recBytes = posBytes + stride;
305 int nranks = 0;
306 MPI_Comm_size(comm_, &nranks);
307
308 std::map<int, std::vector<char>> outbox;
310 for (std::size_t i = 0; i < pos.size(); ++i) {
311 for (int r = 0; r < nranks; ++r) {
312 if (r == rank_)
313 continue;
314 if (!withinRcutOfBlock(pos[i], r, rcut, img))
315 continue;
316 auto& buf = outbox[r];
317 std::size_t off = buf.size();
318 buf.resize(off + recBytes);
319 std::memcpy(buf.data() + off, img.data(), posBytes);
320 if (stride)
321 std::memcpy(buf.data() + off + posBytes, &payload[i * stride], stride);
322 }
323 }
324
325 std::vector<std::pair<int, const std::vector<char>*>> queue;
326 queue.reserve(outbox.size());
327 for (auto& kv : outbox)
328 queue.emplace_back(kv.first, &kv.second);
329 std::size_t q = 0, recv = 0;
330 NbxEngine nbx(comm_);
331 auto packNext = [&](std::vector<char>& out) -> int {
332 if (q >= queue.size())
333 return -1;
334 int dest = queue[q].first;
335 out = *queue[q].second;
336 ++q;
337 return dest;
338 };
339 auto onRecv = [&](int /*src*/, std::vector<char>& msg) {
340 std::size_t cnt = msg.size() / recBytes;
341 for (std::size_t i = 0; i < cnt; ++i) {
342 const char* rec = msg.data() + i * recBytes;
343 Vec<Dim> x{};
344 std::memcpy(x.data(), rec, posBytes);
345 ghostPos.push_back(x);
346 if (stride) {
347 std::size_t off = ghostPayload.size();
348 ghostPayload.resize(off + stride);
349 std::memcpy(&ghostPayload[off], rec + posBytes, stride);
350 }
351 ++recv;
352 }
353 };
354 nbx.exchange(packNext, onRecv, /*tag=*/7402);
355 return recv;
356 }
357
358 private:
359 const decomp::BlockDecomposer<Dim>* dec_ = nullptr;
360 int rank_ = 0;
361 DomainMap<Dim> map_{};
362 MPI_Comm comm_ = MPI_COMM_WORLD;
363 std::size_t sent_ = 0, received_ = 0;
364};
365
366} // namespace peclet::core::halo
367
368#endif // PECLET_CORE_HALO_PARTICLE_MIGRATOR_HPP
Orthogonal recursive bisection of a Dim-dimensional global cell grid into numBlocks axis-aligned bloc...
IVec< Dim > cellOf(const Vec< Dim > &x) const
Global decomposition cell containing position x (after periodic wrap / boundary clamp).
std::size_t gatherGhosts(const std::vector< Vec< Dim > > &pos, const std::vector< char > &payload, std::size_t stride, double rcut, std::vector< Vec< Dim > > &ghostPos, std::vector< char > &ghostPayload)
Gather ghost copies of particles within rcut of this rank's block boundary.
const decomp::BlockDecomposer< Dim > & decomposer() const
Vec< Dim > wrapPosition(Vec< Dim > x) const
Wrap a position into the periodic box (no-op on non-periodic axes).
std::size_t migrate(std::vector< Vec< Dim > > &pos, std::vector< char > &payload, std::size_t stride)
Reassign every particle to its owning rank.
bool boxWithinRcutOfBlock(const Vec< Dim > &bmin, const Vec< Dim > &bmax, int r, double rcut) const
Append the shift (= image − x) of EVERY periodic image of x that comes within rcut of block r's AABB ...
void imagesWithinRcutOfBlock(const Vec< Dim > &x, int r, double rcut, bool allowIdentity, std::vector< Vec< Dim > > &outShifts) const
bool withinRcutOfBlock(const Vec< Dim > &x, int r, double rcut, Vec< Dim > &img) const
True if x (via its closest periodic image) comes within rcut of block r's physical AABB.
void init(const decomp::BlockDecomposer< Dim > &dec, int rank, DomainMap< Dim > map, MPI_Comm comm=MPI_COMM_WORLD)
int ownerOf(const Vec< Dim > &x) const
Rank that owns the block containing position x (after periodic wrap / boundary clamp).
int MPI_Comm_size(MPI_Comm, int *s)
Definition mpi_stub.hpp:70
int MPI_Comm
Definition mpi_stub.hpp:17
#define MPI_COMM_WORLD
Definition mpi_stub.hpp:27
Index wrap(Index x, Index n)
Periodic wrap of a coordinate into [0, n): wrap(x, n) = (x % n + n) % n.
Definition types.hpp:37
Kokkos::View< T *, MemSpace > View
1D device array.
Definition view.hpp:26
std::int64_t Index
Signed index type for grids and particles (supersedes block_decomposer's long int IndxT).
Definition types.hpp:15
Physical-space layout that maps a position to a global cell of the decomposition.
Vec< Dim > origin
physical coordinate of global cell (0,...)
std::array< bool, Dim > periodic
per-axis periodicity
Vec< Dim > cellSize
physical size of one global cell