core
Shared MPI block decomposition + asynchronous ghost-layer exchange (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
202 void imagesWithinRcutOfBlock(const Vec<Dim>& x, int r, double rcut, bool allowIdentity,
203 std::vector<Vec<Dim>>& outShifts) const {
204 const auto& o = dec_->origins()[r];
205 const auto& s = dec_->sizes()[r];
206 const IVec<Dim>& gsize = dec_->globalSize();
207 double lo[Dim], hi[Dim], cand[Dim][3];
208 int nc[Dim];
209 for (int d = 0; d < Dim; ++d) {
210 lo[d] = map_.origin[d] + o[d] * map_.cellSize[d];
211 hi[d] = map_.origin[d] + (o[d] + s[d]) * map_.cellSize[d];
212 const double L = map_.cellSize[d] * static_cast<double>(gsize[d]);
213 cand[d][0] = 0.0;
214 nc[d] = 1;
215 if (map_.periodic[d]) {
216 cand[d][1] = -L;
217 cand[d][2] = L;
218 nc[d] = 3;
219 }
220 }
221 int total = 1;
222 for (int d = 0; d < Dim; ++d)
223 total *= nc[d];
224 const double rc2 = rcut * rcut;
225 for (int idx = 0; idx < total; ++idx) {
226 Vec<Dim> shift{};
227 int t = idx;
228 bool identity = true;
229 double d2 = 0.0;
230 for (int d = 0; d < Dim; ++d) {
231 const int k = t % nc[d];
232 t /= nc[d];
233 shift[d] = cand[d][k];
234 if (k != 0)
235 identity = false;
236 const double p = x[d] + shift[d];
237 const double gap = (p < lo[d]) ? (lo[d] - p) : (p > hi[d]) ? (p - hi[d]) : 0.0;
238 d2 += gap * gap;
239 }
240 if (d2 < rc2 && (allowIdentity || !identity))
241 outShifts.push_back(shift);
242 }
243 }
244
251 std::size_t gatherGhosts(const std::vector<Vec<Dim>>& pos, const std::vector<char>& payload,
252 std::size_t stride, double rcut, std::vector<Vec<Dim>>& ghostPos,
253 std::vector<char>& ghostPayload) {
254 ghostPos.clear();
255 ghostPayload.clear();
256 const std::size_t posBytes = Dim * sizeof(double);
257 const std::size_t recBytes = posBytes + stride;
258 int nranks = 0;
259 MPI_Comm_size(comm_, &nranks);
260
261 std::map<int, std::vector<char>> outbox;
263 for (std::size_t i = 0; i < pos.size(); ++i) {
264 for (int r = 0; r < nranks; ++r) {
265 if (r == rank_)
266 continue;
267 if (!withinRcutOfBlock(pos[i], r, rcut, img))
268 continue;
269 auto& buf = outbox[r];
270 std::size_t off = buf.size();
271 buf.resize(off + recBytes);
272 std::memcpy(buf.data() + off, img.data(), posBytes);
273 if (stride)
274 std::memcpy(buf.data() + off + posBytes, &payload[i * stride], stride);
275 }
276 }
277
278 std::vector<std::pair<int, const std::vector<char>*>> queue;
279 queue.reserve(outbox.size());
280 for (auto& kv : outbox)
281 queue.emplace_back(kv.first, &kv.second);
282 std::size_t q = 0, recv = 0;
283 NbxEngine nbx(comm_);
284 auto packNext = [&](std::vector<char>& out) -> int {
285 if (q >= queue.size())
286 return -1;
287 int dest = queue[q].first;
288 out = *queue[q].second;
289 ++q;
290 return dest;
291 };
292 auto onRecv = [&](int /*src*/, std::vector<char>& msg) {
293 std::size_t cnt = msg.size() / recBytes;
294 for (std::size_t i = 0; i < cnt; ++i) {
295 const char* rec = msg.data() + i * recBytes;
296 Vec<Dim> x{};
297 std::memcpy(x.data(), rec, posBytes);
298 ghostPos.push_back(x);
299 if (stride) {
300 std::size_t off = ghostPayload.size();
301 ghostPayload.resize(off + stride);
302 std::memcpy(&ghostPayload[off], rec + posBytes, stride);
303 }
304 ++recv;
305 }
306 };
307 nbx.exchange(packNext, onRecv, /*tag=*/7402);
308 return recv;
309 }
310
311 private:
312 const decomp::BlockDecomposer<Dim>* dec_ = nullptr;
313 int rank_ = 0;
314 DomainMap<Dim> map_{};
315 MPI_Comm comm_ = MPI_COMM_WORLD;
316 std::size_t sent_ = 0, received_ = 0;
317};
318
319} // namespace peclet::core::halo
320
321#endif // PECLET_CORE_HALO_PARTICLE_MIGRATOR_HPP
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.
void imagesWithinRcutOfBlock(const Vec< Dim > &x, int r, double rcut, bool allowIdentity, std::vector< Vec< Dim > > &outShifts) const
Append the shift (= image − x) of EVERY periodic image of x that comes within rcut of block r's AABB ...
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:69
int MPI_Comm
Definition mpi_stub.hpp:16
#define MPI_COMM_WORLD
Definition mpi_stub.hpp:26
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