chombo-discharge
Loading...
Searching...
No Matches
CD_ParticleContainerImplem.H
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2021-2026 SINTEF Energy Research
3 *
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
13#ifndef CD_PARTICLECONTAINERIMPLEM_H
14#define CD_PARTICLECONTAINERIMPLEM_H
15
16// Std includes
17#include <cmath>
18#include <cstddef>
19#include <cstdint>
20#include <cstdlib>
21#include <cstring>
22#include <limits>
23#include <map>
24#include <unordered_map>
25#include <utility>
26#include <vector>
27
28// Chombo includes
29#include <CH_Timer.H>
30#include <MayDay.H>
31#include <CH_assert.H>
32#include <BoxIterator.H>
33#include <parstream.H>
34
35// Our includes
37#include <CD_ParticleMemory.H>
38#include <CD_NamespaceHeader.H>
39
40template <typename P, typename Traits>
43{
44 return LevelTiles::findDestination(a_pos, m_probLo, m_dx, m_minBlockSize, m_levelTiles, m_finestLevel);
45}
46
47template <typename P, typename Traits>
50 const Vector<DisjointBoxLayout>& a_sourceGrids)
51{
52 CH_TIME("ParticleContainer::gatherToPool");
53
54 MoverPool pool(numProc());
55
56 // Bin every particle of one leaf into a destination pool, returning the number of off-domain
57 // (outcast) particles. Shared by the serial and OpenMP paths so the binning logic lives once.
58 // findDestination() is const and reads only the (immutable-during-gather) tile maps, so it is
59 // safe to call concurrently; each thread bins into its own thread-local pool.
60 auto binLeaf = [this](MoverPool& a_target, const Leaf& a_leaf) -> unsigned long long {
61 unsigned long long outcast = 0;
62
63 for (std::size_t i = 0; i < a_leaf.size(); i++) {
64 const RealVect pos = a_leaf.position(i);
65 const auto dst = this->findDestination(pos);
66
67 if (!dst.valid) {
68 outcast++;
69 continue;
70 }
71
72 Leaf& bin = a_target[dst.rank][PoolKey(dst.level, dst.gridIndex)];
73
74 bin.append(pos, a_leaf.weight(i), a_leaf.gather(i));
75
76 const std::size_t k = bin.size() - 1;
77
78 bin.particleID(k) = a_leaf.particleID(i); // preserve id
79 bin.rankID(k) = dst.rank; // new owner
80 }
81
82 return outcast;
83 };
84
85 unsigned long long numOutcast = 0;
86
87 for (int lvl = 0; lvl < a_source.size(); lvl++) {
88 LevelParticles& level = *a_source[lvl];
89 const DataIterator& dit = a_sourceGrids[lvl].dataIterator();
90 const int nbox = dit.size();
91
92#ifdef _OPENMP
93#pragma omp parallel reduction(+ : numOutcast)
94 {
95 MoverPool threadPool(numProc()); // thread-local; no locking while binning
96
97#pragma omp for schedule(runtime)
98 for (int mybox = 0; mybox < nbox; mybox++) {
99 Leaf& leaf = level[dit[mybox]];
100
101 numOutcast += binLeaf(threadPool, leaf);
102
103 // The pool now holds this leaf's particles, and the source is discarded by every caller, so
104 // release the arena here rather than at the end. Otherwise the source and the pool are both
105 // fully live until the caller returns, which doubles the particle footprint at exactly the
106 // point in a regrid where it is already highest. Each thread owns its own boxes, so this is
107 // race-free.
108 leaf.clear();
109 leaf.shrinkToFit();
110 }
111
112 // Merge the thread-local pool into the shared pool. First writer to a (rank,key) slot gets the
113 // O(1) arena swap inside catenate(); later writers append.
114#pragma omp critical(ParticleContainer_gatherMerge)
115 {
116 for (int r = 0; r < numProc(); r++) {
117 for (auto& kv : threadPool[r]) {
118 pool[r][kv.first].catenate(kv.second);
119 }
120 }
121 }
122 }
123#else
124 for (int mybox = 0; mybox < nbox; mybox++) {
125 Leaf& leaf = level[dit[mybox]];
126
127 numOutcast += binLeaf(pool, leaf);
128
129 // See the OpenMP branch: drain as we go so the source and the pool are never both fully live.
130 leaf.clear();
131 leaf.shrinkToFit();
132 }
133#endif
134 }
135
136 m_numOutcastLocal = numOutcast;
137
138 return pool;
139}
140
141template <typename P, typename Traits>
144{
145 CH_TIME("ParticleContainer::gatherMoversToPool");
146
147 CH_assert(m_validCells != nullptr);
148
149 MoverPool pool(numProc());
150 unsigned long long numOutcast = 0;
151
152 for (int lvl = 0; lvl <= m_finestLevel; lvl++) {
153 LevelParticles& level = *m_particles[lvl];
154 const LevelData<BaseFab<bool>>* maskLevel = (*m_validCells)[lvl].isNull() ? nullptr : &(*(*m_validCells)[lvl]);
155 const DataIterator& dit = m_grids[lvl].dataIterator();
156 const int nbox = dit.size();
157
158 // Partition one leaf in place: keep stayers (still inside their box AND in a cell flagged valid =
159 // not covered by a finer level), pool the movers and swap-pop them out. a_mask == nullptr forces
160 // every particle to be treated as a mover (no usable mask for this leaf). Returns outcast movers.
161 // findDestination()/cellIndex() are const reads, so this is safe to call from many threads as long
162 // as each leaf is touched by a single thread (the omp-for partitions boxes disjointly).
163 auto partitionLeaf = [this, lvl](MoverPool& a_target,
164 [[maybe_unused]] const DataIndex& a_din,
165 Leaf& a_leaf,
166 const BaseFab<bool>* a_mask) -> unsigned long long {
167 unsigned long long outcast = 0;
168
169 const Box maskBox = (a_mask != nullptr) ? a_mask->box() : Box();
170
171 std::size_t i = 0;
172
173 while (i < a_leaf.size()) {
174 const RealVect pos = a_leaf.position(i);
175 if (a_mask != nullptr) {
176 const IntVect iv = this->cellIndex(lvl, pos);
177
178 if (maskBox.contains(iv) && (*a_mask)(iv, 0)) {
179 // Stayer: in its own box and not under a finer grid, so it cannot have changed owner.
180 // Debug self-check of that invariant -- the destination the gather-all path would compute
181 // must be exactly this (level, grid, rank). If this ever fires, the skip is unsound.
182#ifndef NDEBUG
183 const auto chk = this->findDestination(pos);
184 CH_assert(chk.valid && chk.level == lvl && chk.rank == procID() &&
185 m_levelTiles[lvl]->getMyGrids().at(chk.gridIndex) == a_din);
186#endif
187 i++;
188
189 continue;
190 }
191 }
192
193 const auto dst = this->findDestination(pos); // mover: route it
194
195 if (dst.valid) {
196 Leaf& bin = a_target[dst.rank][PoolKey(dst.level, dst.gridIndex)];
197
198 bin.append(pos, a_leaf.weight(i), a_leaf.gather(i));
199
200 const std::size_t k = bin.size() - 1;
201
202 bin.particleID(k) = a_leaf.particleID(i); // preserve id
203 bin.rankID(k) = dst.rank; // new owner
204 }
205 else {
206 outcast++;
207 }
208
209 a_leaf.remove(i); // swap-pop; do NOT advance i (a new particle now occupies slot i)
210 }
211
212 return outcast;
213 };
214
215#ifdef _OPENMP
216#pragma omp parallel reduction(+ : numOutcast)
217 {
218 MoverPool threadPool(numProc());
219
220#pragma omp for schedule(runtime)
221 for (int mybox = 0; mybox < nbox; mybox++) {
222 const DataIndex& din = dit[mybox];
223 const BaseFab<bool>* mask = (maskLevel != nullptr) ? &(*maskLevel)[din] : nullptr;
224
225 if (mask != nullptr && !mask->isUsable()) {
226 mask = nullptr;
227 }
228
229 numOutcast += partitionLeaf(threadPool, din, level[din], mask);
230 }
231
232#pragma omp critical(ParticleContainer_gatherMerge)
233 {
234 for (int r = 0; r < numProc(); r++) {
235 for (auto& kv : threadPool[r]) {
236 pool[r][kv.first].catenate(kv.second);
237 }
238 }
239 }
240 }
241#else
242 for (int mybox = 0; mybox < nbox; mybox++) {
243 const DataIndex& din = dit[mybox];
244 const BaseFab<bool>* mask = (maskLevel != nullptr) ? &(*maskLevel)[din] : nullptr;
245
246 if (mask != nullptr && !mask->isUsable()) {
247 mask = nullptr;
248 }
249
250 numOutcast += partitionLeaf(pool, din, level[din], mask);
251 }
252#endif
253 }
254
255 m_numOutcastLocal = numOutcast;
256
257 return pool;
258}
259
260template <typename P, typename Traits>
261inline void
262ParticleContainer<P, Traits>::distributeFromPool(MoverPool& a_pool, const bool a_setOwnerToReceiver)
263{
264 CH_TIME("ParticleContainer::distributeFromPool");
265
266 const int myRank = procID();
267 const int numRanks = numProc();
268
269 // ---- same-rank movers: catenate each pooled leaf into its destination leaf ----
270 // On the gather-all path the destination leaves are empty (remap clears them; regrid allocates them
271 // fresh) so catenate() takes the O(1) arena-swap fast path. On the mask fast path the destinations
272 // still hold their stayers, so catenate() appends (O(movers)); a mover never returns to its own
273 // origin leaf, so there is no self-append.
274 for (auto& kv : a_pool[myRank]) {
275 const int lvl = kv.first.first;
276 const unsigned int gidx = kv.first.second;
277 const DataIndex din = m_levelTiles[lvl]->getMyGrids().at(gidx);
278
279 (*m_particles[lvl])[din].catenate(kv.second);
280 }
281
282#ifdef CH_MPI
283 // ---- cross-rank movers: one Alltoallv of linearized particles ----
284 constexpr std::size_t pp = Leaf::bytesPerParticle();
285 constexpr std::size_t headerLen = 2 * sizeof(std::uint32_t) + sizeof(std::uint64_t);
286
287 // Size the send side before packing anything. A destination's byte count is arithmetic over the
288 // pool -- a header plus the payload for each pooled leaf -- so computing it up front costs nothing,
289 // and knowing it lets every particle be linearized straight into the flat buffer that MPI reads.
290 // Packing into per-destination buffers first and copying those into the flat one afterwards would
291 // hold a second full copy of every outbound particle for the duration of the copy.
292 std::vector<int> sendCounts(numRanks, 0);
293 std::vector<int> recvCounts(numRanks, 0);
294 std::vector<std::int64_t> sendBytes(numRanks, 0);
295
296 std::int64_t stot = 0;
297
298 for (int r = 0; r < numRanks; r++) {
299 if (r == myRank) {
300 continue;
301 }
302
303 std::int64_t bytes = 0;
304
305 for (const auto& kv : a_pool[r]) {
306 bytes += static_cast<std::int64_t>(headerLen + kv.second.size() * pp);
307 }
308
309 sendBytes[r] = bytes;
310 stot += bytes;
311 }
312
313 // MPI counts and displacements are int, so the byte totals have to fit in one. This is a real limit
314 // at scale rather than a formality -- a rank holding 2 GB of outbound particles reaches it -- and it
315 // has to be enforced in every build. CH_assert compiles out under OPT, which is precisely where the
316 // scale is, so a guard that used it would let the displacements overflow and corrupt the exchange
317 // silently instead of stopping it.
318 if (stot > static_cast<std::int64_t>(std::numeric_limits<int>::max())) {
319 MayDay::Abort("ParticleContainer::distributeFromPool - outbound particle bytes exceed MPI's int limit");
320 }
321
322 for (int r = 0; r < numRanks; r++) {
323 sendCounts[r] = static_cast<int>(sendBytes[r]);
324 }
325
326 MPI_Alltoall(sendCounts.data(), 1, MPI_INT, recvCounts.data(), 1, MPI_INT, Chombo_MPI::comm);
327
328 std::vector<int> sdispl(numRanks, 0);
329 std::vector<int> rdispl(numRanks, 0);
330
331 std::int64_t sacc = 0;
332 std::int64_t rtot = 0;
333
334 for (int r = 0; r < numRanks; r++) {
335 sdispl[r] = static_cast<int>(sacc);
336 sacc += sendCounts[r];
337
338 rdispl[r] = static_cast<int>(rtot);
339 rtot += recvCounts[r];
340 }
341
342 if (rtot > static_cast<std::int64_t>(std::numeric_limits<int>::max())) {
343 MayDay::Abort("ParticleContainer::distributeFromPool - inbound particle bytes exceed MPI's int limit");
344 }
345
346 std::vector<char> sflat(stot);
347 std::vector<char> rflat(rtot);
348
349 // The flat buffers are particle payload in flight, so account for them alongside the arenas. They
350 // are part of the peak this rank has to fit, and on a particle-heavy step they are comparable in
351 // size to the arenas themselves -- leaving them out would report a peak that understates the point
352 // in the step where an out-of-memory failure actually happens.
353 const std::size_t flatBytes = static_cast<std::size_t>(stot) + static_cast<std::size_t>(rtot);
354
356
357 // Each destination's slice of sflat is disjoint and linearizeParticle is a const read, so packing
358 // different destinations in parallel is race-free. A pooled leaf is spent once it has been
359 // linearized, so release it there and then: holding it until the pool goes out of scope would keep
360 // a second copy of every outbound particle alive across the exchange itself, which is the point in
361 // the step where the footprint peaks.
362#ifdef _OPENMP
363#pragma omp parallel for schedule(runtime)
364#endif
365 for (int r = 0; r < numRanks; r++) {
366 if (r == myRank) {
367 continue;
368 }
369
370 char* p = sflat.data() + sdispl[r];
371
372 for (auto& kv : a_pool[r]) {
373 Leaf& src = kv.second;
374
375 const std::uint32_t lvl = static_cast<std::uint32_t>(kv.first.first);
376 const std::uint32_t gidx = static_cast<std::uint32_t>(kv.first.second);
377 const std::uint64_t count = static_cast<std::uint64_t>(src.size());
378
379 std::memcpy(p, &lvl, sizeof(std::uint32_t));
380 p += sizeof(std::uint32_t);
381
382 std::memcpy(p, &gidx, sizeof(std::uint32_t));
383 p += sizeof(std::uint32_t);
384
385 std::memcpy(p, &count, sizeof(std::uint64_t));
386 p += sizeof(std::uint64_t);
387
388 for (std::size_t i = 0; i < count; i++) {
389 src.linearizeParticle(p, i);
390 p += pp;
391 }
392
393 src.clear();
394 src.shrinkToFit();
395 }
396 }
397
398 MPI_Alltoallv(sflat.data(),
399 sendCounts.data(),
400 sdispl.data(),
401 MPI_BYTE,
402 rflat.data(),
403 recvCounts.data(),
404 rdispl.data(),
405 MPI_BYTE,
406 Chombo_MPI::comm);
407
408 // ---- unpack: group received chunks by destination leaf, then append in parallel ----
409 // Pass 1 (serial, header-only walk): record each chunk's payload pointer + count, grouped by its
410 // destination (level, gridIndex). Pass 2 appends in parallel with one destination leaf per thread,
411 // so no two threads ever touch the same leaf (delinearizeAndAppend mutates the leaf arena).
412 struct RecvChunk
413 {
414 const char* payload; // first particle of the chunk within rflat
415 std::uint64_t count;
416 };
417 struct DestGroup
418 {
419 int level;
420 unsigned int gridIndex;
421 std::uint64_t total;
422 std::vector<RecvChunk> chunks;
423 };
424
425 std::vector<DestGroup> groups;
426 std::map<PoolKey, std::size_t> keyToGroup;
427 {
428 const char* p = rflat.data();
429 const char* end = p + rtot;
430
431 while (p < end) {
432 std::uint32_t lvl;
433 std::uint32_t gidx;
434 std::uint64_t count;
435
436 std::memcpy(&lvl, p, sizeof(std::uint32_t));
437 p += sizeof(std::uint32_t);
438
439 std::memcpy(&gidx, p, sizeof(std::uint32_t));
440 p += sizeof(std::uint32_t);
441
442 std::memcpy(&count, p, sizeof(std::uint64_t));
443 p += sizeof(std::uint64_t);
444
445 const PoolKey key(static_cast<int>(lvl), gidx);
446 const auto it = keyToGroup.find(key);
447
448 if (it == keyToGroup.end()) {
449 keyToGroup.emplace(key, groups.size());
450 groups.push_back(DestGroup{static_cast<int>(lvl), gidx, count, {RecvChunk{p, count}}});
451 }
452 else {
453 DestGroup& g = groups[it->second];
454 g.total += count;
455 g.chunks.push_back(RecvChunk{p, count});
456 }
457
458 p += count * pp;
459 }
460 }
461
462 const int numGroups = static_cast<int>(groups.size());
463
464#ifdef _OPENMP
465#pragma omp parallel for schedule(runtime)
466#endif
467 for (int d = 0; d < numGroups; d++) {
468 const DestGroup& g = groups[d];
469 const DataIndex din = m_levelTiles[g.level]->getMyGrids().at(g.gridIndex);
470
471 Leaf& tgt = (*m_particles[g.level])[din];
472
473 tgt.reserve(tgt.size() + g.total); // one allocation; avoids geometric regrow during the appends
474 for (const RecvChunk& chunk : g.chunks) {
475 const char* q = chunk.payload;
476
477 for (std::uint64_t c = 0; c < chunk.count; c++) {
479 q += pp;
480 if (a_setOwnerToReceiver) {
481 tgt.rankID(tgt.size() - 1) = myRank; // remap/regrid: owner is the receiving rank
482 }
483 // else: keep the rankID carried in the buffer (ghost particles record their true owner)
484 }
485 }
486 }
487
489#endif
490}
491
492template <typename P, typename Traits>
493inline void
495{
496 CH_TIME("ParticleContainer::remap");
497
498 CH_assert(m_isDefined);
499
500 // Ghost particles are transient, non-owned copies; drop them so they are never routed as if owned.
501 this->clearGhostParticles();
502
503 if (m_validCells != nullptr && static_cast<int>(m_validCells->size()) >= m_finestLevel + 1) {
504 // Mask fast path: a particle still in its box and not under a finer grid cannot change owner, so we
505 // leave those "stayers" in place and pool only the movers. Destinations therefore keep their stayers
506 // -- do NOT clearParticles() here; distributeFromPool() appends the movers onto the existing leaves.
507 MoverPool pool = this->gatherMoversToPool();
508
509 this->distributeFromPool(pool, true); // remap: the receiving rank becomes the new owner
510 }
511 else {
512 // Fallback: re-pool every valid particle (used when no mask is available, e.g. addParticlesDestructive
513 // staging, or before the realm's valid-cell mask has been built).
514 MoverPool pool = this->gatherToPool(m_particles, m_grids);
515
516 this->clearParticles();
517 this->distributeFromPool(pool, true); // remap: the receiving rank becomes the new owner
518 }
519
520 m_isOrganizedByCell = false;
521}
522
523template <typename P, typename Traits>
524inline void
526{
527 CH_TIME("ParticleContainer::preRegrid");
528
529 CH_assert(m_isDefined);
530
531 // Ghost particles are transient, non-owned copies; drop them so they are never cached/regridded.
532 this->clearGhostParticles();
533
534 m_cacheParticles = m_particles;
535 m_cacheGrids = m_grids;
536}
537
538template <typename P, typename Traits>
539inline void
540ParticleContainer<P, Traits>::regrid(const Vector<DisjointBoxLayout>& a_grids,
541 const Vector<ProblemDomain>& a_domains,
542 const Vector<Real>& a_dx,
543 const Vector<int>& a_refRat,
544 const int a_minBlockSize,
545 const Vector<RefCountedPtr<LevelTiles>>& a_levelTiles,
546 const int a_newFinestLevel)
547{
548 CH_TIME("ParticleContainer::regrid");
549
550 CH_assert(m_isDefined);
551 CH_assert(a_levelTiles.size() > a_newFinestLevel);
552
553 // ---- adopt the new layout ----
554 m_grids = a_grids;
555 m_domains = a_domains;
556 m_refRat = a_refRat;
557 m_minBlockSize = a_minBlockSize;
558 m_finestLevel = a_newFinestLevel;
559
560 // Alias the Realm's per-level tile->box maps (the single source of truth); never build our own.
561 m_levelTiles = a_levelTiles;
562
563 m_dx.resize(a_newFinestLevel + 1);
564 for (int lvl = 0; lvl <= a_newFinestLevel; lvl++) {
565 m_dx[lvl] = a_dx[lvl] * RealVect::Unit;
566 }
567
568 // ---- re-allocate the (empty) holders over the new grids ----
569 m_particles.resize(a_newFinestLevel + 1);
570 m_maskParticles.resize(a_newFinestLevel + 1);
571
572 for (int lvl = 0; lvl <= a_newFinestLevel; lvl++) {
573 m_particles[lvl] = RefCountedPtr<LevelParticles>(new LevelParticles(a_grids[lvl]));
574 m_maskParticles[lvl] = RefCountedPtr<LevelParticles>(new LevelParticles(a_grids[lvl]));
575 }
576
577 this->setupGrownGrids();
578
579 m_bufferParticles.resize(a_newFinestLevel + 1);
580
581 for (int lvl = 0; lvl <= a_newFinestLevel; lvl++) {
582 m_bufferParticles[lvl] = RefCountedPtr<LevelParticles>(new LevelParticles(m_grownGrids[lvl]));
583 }
584
585 // ---- redistribute the cached particles onto the new layout ----
586 MoverPool pool = this->gatherToPool(m_cacheParticles, m_cacheGrids);
587 this->distributeFromPool(pool, true); // regrid: the receiving rank becomes the new owner
589 // ---- release the cache ----
590 m_cacheParticles.resize(0);
591 m_cacheGrids.resize(0);
592
593 m_isOrganizedByCell = false;
594}
595
596template <typename P, typename Traits>
597inline void
599{
600 CH_TIME("ParticleContainer::clearGhostParticles");
601
602 CH_assert(m_isDefined);
603
604 for (int lvl = 0; lvl <= m_finestLevel; lvl++) {
605 LevelParticles& level = *m_particles[lvl];
606 const DisjointBoxLayout& dbl = m_grids[lvl];
607 const DataIterator& dit = dbl.dataIterator();
608
609 const int nbox = dit.size();
610
611#pragma omp parallel for schedule(runtime)
612 for (int mybox = 0; mybox < nbox; mybox++) {
613 const DataIndex& din = dit[mybox];
614 Leaf& leaf = level[din];
615
616 // Swap-and-pop every ghost; the swapped-in particle now occupies index i, so do NOT advance on a hit.
617 std::size_t i = 0;
618 while (i < leaf.size()) {
619 if (leaf.isGhost(i)) {
620 leaf.remove(i);
621 }
622 else {
623 i++;
624 }
625 }
626 }
627 }
628
629 m_isOrganizedByCell = false; // swap-pop reorders the surviving valid particles
630}
631
632template <typename P, typename Traits>
633inline void
635{
636 CH_TIME("ParticleContainer::claimOwnership");
637
638 CH_assert(m_isDefined);
639
640 const RankID myRank = static_cast<RankID>(procID());
641
642 // Ghosts are left untouched (they carry their true owner rank). Each non-ghost particle in the valid
643 // region (or all non-ghost particles when !a_onlyValidRegion) is stamped with this rank. Per-particle
644 // and box-local -> embarrassingly parallel.
645 for (int lvl = 0; lvl <= m_finestLevel; lvl++) {
646 LevelParticles& level = *m_particles[lvl];
647 const DisjointBoxLayout& dbl = m_grids[lvl];
648 const DataIterator& dit = dbl.dataIterator();
649 const int nbox = dit.size();
650
651#pragma omp parallel for schedule(runtime)
652 for (int mybox = 0; mybox < nbox; mybox++) {
653 const DataIndex& din = dit[mybox];
654 const Box box = dbl[din];
655 Leaf& leaf = level[din];
656
657 for (std::size_t i = 0; i < leaf.size(); i++) {
658 if (leaf.isGhost(i)) {
659 continue;
660 }
661 if (!a_onlyValidRegion || this->inValidBox(lvl, box, leaf.position(i))) {
662 leaf.rankID(i) = myRank;
663 }
664 }
665 }
666 }
667}
668
669template <typename P, typename Traits>
670inline long long
671ParticleContainer<P, Traits>::resetParticleIDs(const bool a_onlyValidRegion, const ParticleID a_startID)
672{
673 CH_TIME("ParticleContainer::resetParticleIDs");
674
675 CH_assert(m_isDefined);
676
677 // A non-ghost particle qualifies for a new id iff (!a_onlyValidRegion) or its cell lies in the leaf's
678 // grid box. Ghosts never qualify (and are left untouched); non-qualifying non-ghost strays get
679 // s_invalidID. The predicate must be identical in the count and assign passes below.
680
681 // Flatten the local boxes in (level, box) order -- this fixes the local id ordering.
682 std::vector<std::pair<int, DataIndex>> boxes;
683 for (int lvl = 0; lvl <= m_finestLevel; lvl++) {
684 const DataIterator& dit = m_grids[lvl].dataIterator();
685 const int nbox = dit.size();
686 for (int mybox = 0; mybox < nbox; mybox++) {
687 boxes.emplace_back(lvl, dit[mybox]);
688 }
689 }
690 const int numBoxes = static_cast<int>(boxes.size());
691
692 // Pass 1: count qualifying particles per box (parallel; each box independent).
693 std::vector<long long> boxCount(numBoxes, 0);
694#pragma omp parallel for schedule(runtime)
695 for (int b = 0; b < numBoxes; b++) {
696 const int lvl = boxes[b].first;
697 const DataIndex& din = boxes[b].second;
698 const Box box = m_grids[lvl][din];
699 const Leaf& leaf = (*m_particles[lvl])[din];
700
701 long long count = 0;
702 for (std::size_t i = 0; i < leaf.size(); i++) {
703 if (!leaf.isGhost(i) && (!a_onlyValidRegion || this->inValidBox(lvl, box, leaf.position(i)))) {
704 count++;
705 }
706 }
707 boxCount[b] = count;
708 }
709
710 // Local exclusive prefix over boxes + local total.
711 std::vector<long long> boxLocalOffset(numBoxes, 0);
712 long long localTotal = 0;
713 for (int b = 0; b < numBoxes; b++) {
714 boxLocalOffset[b] = localTotal;
715 localTotal += boxCount[b];
716 }
717
718 // Per-rank exclusive offset: all-gather each rank's local total, then sum the ranks before this one.
719 const Vector<long long> perRankCount = ParallelOps::gather(localTotal);
720
721 ParticleID rankOffset = a_startID;
722 for (int r = 0; r < procID(); r++) {
723 rankOffset += perRankCount[r];
724 }
725
726 // Pass 2: assign ids (parallel; each box starts at its own offset). Ghosts untouched; qualifying get a
727 // contiguous id; non-qualifying non-ghost strays get s_invalidID.
728#pragma omp parallel for schedule(runtime)
729 for (int b = 0; b < numBoxes; b++) {
730 const int lvl = boxes[b].first;
731 const DataIndex& din = boxes[b].second;
732 const Box box = m_grids[lvl][din];
733 Leaf& leaf = (*m_particles[lvl])[din];
734
735 ParticleID id = rankOffset + static_cast<ParticleID>(boxLocalOffset[b]);
736 for (std::size_t i = 0; i < leaf.size(); i++) {
737 if (leaf.isGhost(i)) {
738 continue;
739 }
740 if (!a_onlyValidRegion || this->inValidBox(lvl, box, leaf.position(i))) {
741 leaf.particleID(i) = id++;
742 }
743 else {
745 }
746 }
747 }
748
749 return ParallelOps::sum(localTotal);
750}
751
752template <typename P, typename Traits>
755 const AMRParticleGhostMask& a_maskCoarToFine,
756 const AMRParticleGhostMask& a_maskFineToCoar)
757{
758 CH_TIME("ParticleContainer::gatherGhostsFromMasks");
759
760 MoverPool pool(numProc());
761
762 // Bin one source leaf's valid particles as ghosts of every target box listed by its cell's masks. The
763 // three masks are all indexed by the SOURCE cell on the source level, so a single cell lookup drives all
764 // directions. GhostType is from the RECEIVER's view: same level -> SameLevel, coarse source scattered to
765 // a finer patch -> Coarse, fine source scattered to a coarser patch -> Fine.
766 auto binLeaf = [&](MoverPool& a_target, const Leaf& a_leaf, const int a_srcLvl, const DataIndex& a_din) {
767 const bool haveSame = (a_srcLvl < a_maskSame.size()) && !a_maskSame[a_srcLvl].isNull();
768 const bool haveC2F = (a_srcLvl < m_finestLevel) && (a_srcLvl < a_maskCoarToFine.size()) &&
769 !a_maskCoarToFine[a_srcLvl].isNull();
770 const bool haveF2C = (a_srcLvl >= 1) && (a_srcLvl < a_maskFineToCoar.size()) &&
771 !a_maskFineToCoar[a_srcLvl].isNull();
772
773 const ParticleGhostMask* same = haveSame ? &((*a_maskSame[a_srcLvl])[a_din]) : nullptr;
774 const ParticleGhostMask* c2f = haveC2F ? &((*a_maskCoarToFine[a_srcLvl])[a_din]) : nullptr;
775 const ParticleGhostMask* f2c = haveF2C ? &((*a_maskFineToCoar[a_srcLvl])[a_din]) : nullptr;
776
777 if (same == nullptr && c2f == nullptr && f2c == nullptr) {
778 return;
779 }
780
781 for (std::size_t i = 0; i < a_leaf.size(); i++) {
782 const RealVect pos = a_leaf.position(i);
783 const IntVect iv = this->cellIndex(a_srcLvl, pos);
784
785 auto scatter =
786 [&](const ParticleGhostMask* a_mask, const int a_dstLvl, const GhostType a_gt, const bool a_pruneByTargetBox) {
787 if (a_mask == nullptr || !a_mask->box().contains(iv)) {
788 return;
789 }
790 const int nt = a_mask->numTargets(iv);
791
792 // Coarse-to-fine only: the source (coarse) cell is coarser than the fine ghost width, so a whole
793 // coarse cell would over-scatter. Keep the particle for a target only if its DESTINATION (fine)
794 // cell lies in that target's acceptance box (the exact fine ghost shell).
795 const IntVect dstCell = a_pruneByTargetBox ? this->cellIndex(a_dstLvl, pos) : IntVect::Zero;
796
797 for (int k = 0; k < nt; k++) {
798 if (a_pruneByTargetBox && !a_mask->targetBox(iv, k).contains(dstCell)) {
799 continue;
800 }
801
802 Leaf& bin = a_target[a_mask->targetRank(iv, k)][PoolKey(a_dstLvl, a_mask->targetGridIndex(iv, k))];
803
804 bin.append(pos, a_leaf.weight(i), a_leaf.gather(i));
805
806 const std::size_t j = bin.size() - 1;
807
808 bin.particleID(j) = a_leaf.particleID(i); // preserve id
809 bin.rankID(j) = procID(); // owner is this rank (it owns its valid particles)
810 bin.ghost(j) = a_gt;
811 }
812 };
813
814 scatter(same, a_srcLvl, GhostType::SameLevel, false);
815 scatter(c2f, a_srcLvl + 1, GhostType::Coarse, true);
816 scatter(f2c, a_srcLvl - 1, GhostType::Fine, false);
817 }
818 };
819
820 for (int lvl = 0; lvl <= m_finestLevel; lvl++) {
821 const LevelParticles& level = *m_particles[lvl];
822 const DataIterator& dit = m_grids[lvl].dataIterator();
823 const int nbox = dit.size();
824
825#ifdef _OPENMP
826#pragma omp parallel
827 {
828 MoverPool threadPool(numProc()); // thread-local; no locking while binning
829
830#pragma omp for schedule(runtime)
831 for (int mybox = 0; mybox < nbox; mybox++) {
832 binLeaf(threadPool, level[dit[mybox]], lvl, dit[mybox]);
833 }
834
835#pragma omp critical(ParticleContainer_gatherGhostsFromMasksMerge)
836 {
837 for (int r = 0; r < numProc(); r++) {
838 for (auto& kv : threadPool[r]) {
839 pool[r][kv.first].catenate(kv.second);
840 }
841 }
842 }
843 }
844#else
845 for (int mybox = 0; mybox < nbox; mybox++) {
846 binLeaf(pool, level[dit[mybox]], lvl, dit[mybox]);
847 }
848#endif
849 }
850
851 return pool;
852}
853
854template <typename P, typename Traits>
855inline void
857 const AMRParticleGhostMask& a_maskCoarToFine,
858 const AMRParticleGhostMask& a_maskFineToCoar)
859{
860 CH_TIME("ParticleContainer::fillGhostParticles");
861
862 CH_assert(m_isDefined);
863
864 // Refill from a clean slate: drop any existing ghost halo first.
865 this->clearGhostParticles();
866
867 MoverPool pool = this->gatherGhostsFromMasks(a_maskSame, a_maskCoarToFine, a_maskFineToCoar);
868
869 // a_setOwnerToReceiver == false: ghost copies keep their true owner's rankID (not the receiver's).
870 this->distributeFromPool(pool, false);
871
872 m_isOrganizedByCell = false; // ghosts appended to the leaves invalidate any cell sort
873}
874
875template <typename P, typename Traits>
876inline void
878{
879 CH_TIME("ParticleContainer::setupGrownGrids");
880
881 m_grownGrids.resize(m_finestLevel + 1);
882 for (int lvl = 0; lvl <= m_finestLevel; lvl++) {
883 const DisjointBoxLayout& dbl = m_grids[lvl];
884
885 Vector<Box> boxes = dbl.boxArray();
886
887 if (lvl > 0) {
888 const Box domainBox = m_domains[lvl].domainBox();
889 for (int i = 0; i < boxes.size(); i++) {
890 boxes[i].grow(m_refRat[lvl - 1]);
891 boxes[i] &= domainBox;
892 }
893 }
895 m_grownGrids[lvl] = BoxLayout(boxes, dbl.procIDs());
896 }
897}
898
899template <typename P, typename Traits>
900inline void
901ParticleContainer<P, Traits>::copyMaskParticles(const Vector<RefCountedPtr<LevelData<BaseFab<bool>>>>& a_mask)
902{
903 CH_TIME("ParticleContainer::copyMaskParticles(amr)");
904
905 CH_assert(m_isDefined);
906
907 for (int lvl = 0; lvl <= m_finestLevel; lvl++) {
908 if (!a_mask[lvl].isNull()) {
909 this->copyMaskParticles(lvl, *a_mask[lvl]);
910 }
911 }
912}
913
914template <typename P, typename Traits>
915inline void
916ParticleContainer<P, Traits>::copyMaskParticles(const int a_level, const LevelData<BaseFab<bool>>& a_mask)
917{
918 CH_TIME("ParticleContainer::copyMaskParticles(level)");
919
920 CH_assert(m_isDefined);
921
922 LevelParticles& maskLevel = *m_maskParticles[a_level];
923 const LevelParticles& srcLevel = *m_particles[a_level];
924
925 const DisjointBoxLayout& dbl = m_grids[a_level];
926 const DataIterator& dit = dbl.dataIterator();
927
928 const int nbox = dit.size();
929
930#pragma omp parallel for schedule(runtime)
931 for (int mybox = 0; mybox < nbox; mybox++) {
932 const DataIndex& din = dit[mybox];
933
934 maskLevel[din].clear();
935
936 const BaseFab<bool>& mask = a_mask[din];
937 if (!mask.isUsable()) {
938 continue;
939 }
940 const Box maskBox = mask.box();
941
942 const Leaf& src = srcLevel[din];
943 Leaf& dst = maskLevel[din];
944
945 for (std::size_t i = 0; i < src.size(); i++) {
946 const IntVect iv = this->cellIndex(a_level, src.position(i));
947
948 if (maskBox.contains(iv) && mask(iv, 0)) {
949 dst.append(src.position(i), src.weight(i), src.gather(i));
951 const std::size_t k = dst.size() - 1;
952
953 dst.particleID(k) = src.particleID(i);
954 dst.rankID(k) = src.rankID(i);
955 }
956 }
957 }
958}
959
960template <typename P, typename Traits>
961inline void
962ParticleContainer<P, Traits>::transferMaskParticles(const Vector<RefCountedPtr<LevelData<BaseFab<bool>>>>& a_mask)
964 CH_TIME("ParticleContainer::transferMaskParticles(amr)");
965
966 CH_assert(m_isDefined);
967
968 for (int lvl = 0; lvl <= m_finestLevel; lvl++) {
969 if (!a_mask[lvl].isNull()) {
970 this->transferMaskParticles(lvl, *a_mask[lvl]);
971 }
972 }
973}
974
975template <typename P, typename Traits>
976inline void
977ParticleContainer<P, Traits>::transferMaskParticles(const int a_level, const LevelData<BaseFab<bool>>& a_mask)
978{
979 CH_TIME("ParticleContainer::transferMaskParticles(level)");
980
981 CH_assert(m_isDefined);
982
983 LevelParticles& maskLevel = *m_maskParticles[a_level];
984 LevelParticles& srcLevel = *m_particles[a_level];
985
986 const DisjointBoxLayout& dbl = m_grids[a_level];
987 const DataIterator& dit = dbl.dataIterator();
988
989 const int nbox = dit.size();
990
991#pragma omp parallel for schedule(runtime)
992 for (int mybox = 0; mybox < nbox; mybox++) {
993 const DataIndex& din = dit[mybox];
994
995 const BaseFab<bool>& mask = a_mask[din];
996 if (!mask.isUsable()) {
997 continue;
998 }
999 const Box maskBox = mask.box();
1000
1001 Leaf& src = srcLevel[din];
1002 Leaf& dst = maskLevel[din];
1003
1004 std::size_t i = 0;
1005 while (i < src.size()) {
1006 const IntVect iv = this->cellIndex(a_level, src.position(i));
1007
1008 if (maskBox.contains(iv) && mask(iv, 0)) {
1009 dst.append(src.position(i), src.weight(i), src.gather(i));
1010
1011 const std::size_t k = dst.size() - 1;
1012
1013 dst.particleID(k) = src.particleID(i);
1014 dst.rankID(k) = src.rankID(i);
1015
1016 src.remove(i); // swap-and-pop; the swapped-in particle now occupies index i
1017 }
1018 else {
1019 ++i;
1020 }
1021 }
1022 }
1023}
1024
1025#include <CD_NamespaceFooter.H>
1026
1027#endif
Declaration of an AMR-hierarchy container that owns per-patch ParticleSoA leaves.
Vector< RefCountedPtr< LayoutData< ParticleSoA< P, Traits > > > > AMRParticlesSoA
Per-level holder vector: one ParticleSoA leaf container per box, per level.
Definition CD_ParticleContainer.H:56
Vector< RefCountedPtr< LayoutData< ParticleGhostMask > > > AMRParticleGhostMask
AMR-wide particle ghost-target lookup: one per-box CSR table per level, indexed by SOURCE level.
Definition CD_ParticleGhostMask.H:210
Process-wide accounting of the memory held by particle data.
std::int32_t RankID
Owning-rank identifier type (container-owned metadata column; fixed-width for I/O).
Definition CD_ParticleSoA.H:166
GhostType
Ghost designation of a particle (container-owned metadata column).
Definition CD_ParticleSoA.H:177
@ SameLevel
Ghost from an adjacent patch on the same level.
@ Coarse
Ghost from the next-coarser level (level l-1).
@ Fine
Ghost from the next-finer level (level l+1).
std::int64_t ParticleID
Global particle identifier type (container-owned metadata column; fixed-width for I/O).
Definition CD_ParticleSoA.H:161
static LevelAndBox findDestination(const RealVect &a_pos, const RealVect &a_probLo, const Vector< RealVect > &a_dx, const int a_minBlockSize, const Vector< RefCountedPtr< LevelTiles > > &a_levelTiles, const int a_finestLevel) noexcept
Map a physical position to its owning (level, grid index, rank) via the finest containing tile.
Definition CD_LevelTiles.H:161
void fillGhostParticles(const AMRParticleGhostMask &a_maskSame, const AMRParticleGhostMask &a_maskCoarToFine, const AMRParticleGhostMask &a_maskFineToCoar)
Fill each patch's ghost halo from prebuilt Realm particle ghost masks.
Definition CD_ParticleContainerImplem.H:856
void regrid(const Vector< DisjointBoxLayout > &a_grids, const Vector< ProblemDomain > &a_domains, const Vector< Real > &a_dx, const Vector< int > &a_refRat, const int a_minBlockSize, const Vector< RefCountedPtr< LevelTiles > > &a_levelTiles, const int a_newFinestLevel)
Rebuild over a new AMR layout and redistribute the preRegrid()-cached particles onto it.
Definition CD_ParticleContainerImplem.H:540
void transferMaskParticles(const Vector< RefCountedPtr< LevelData< BaseFab< bool > > > > &a_mask)
Move the valid particles selected by a per-cell mask into the mask holder, all levels.
Definition CD_ParticleContainerImplem.H:962
void setupGrownGrids()
Build the grown grids (buffer-particle layout): boxes grown by the refinement factor.
Definition CD_ParticleContainerImplem.H:877
MoverPool gatherMoversToPool()
Mask fast path for remap(): pool ONLY the movers, leaving stayers in place.
Definition CD_ParticleContainerImplem.H:143
void clearGhostParticles()
Remove every ghost particle (any non-Valid GhostType) from all levels and patches.
Definition CD_ParticleContainerImplem.H:598
std::vector< std::map< PoolKey, Leaf > > MoverPool
Per-rank pool of movers: pool[rank] maps a destination patch to a small SoA of particles.
Definition CD_ParticleContainer.H:911
void preRegrid()
Cache the current valid particles ahead of a regrid.
Definition CD_ParticleContainerImplem.H:525
void claimOwnership(const bool a_onlyValidRegion)
Claim ownership of this rank's particles by stamping their owner rank (rankID = procID()).
Definition CD_ParticleContainerImplem.H:634
void distributeFromPool(MoverPool &a_pool, const bool a_setOwnerToReceiver)
Assign a pool into the valid holders: local appends + MPI scatter.
Definition CD_ParticleContainerImplem.H:262
long long resetParticleIDs(const bool a_onlyValidRegion, const ParticleID a_startID)
Assign globally-unique, contiguous particle IDs to the owned particles.
Definition CD_ParticleContainerImplem.H:671
void remap()
Redistribute every valid particle to the patch/level/rank that owns its cell.
Definition CD_ParticleContainerImplem.H:494
MoverPool gatherToPool(AMRParticlesSoA< P, Traits > &a_source, const Vector< DisjointBoxLayout > &a_sourceGrids)
Gather every particle of a source holder into a per-rank, per-destination-patch pool.
Definition CD_ParticleContainerImplem.H:49
LayoutData< Leaf > LevelParticles
The per-level holder type (one Leaf per box on the level's DisjointBoxLayout).
Definition CD_ParticleContainer.H:133
LevelTiles::LevelAndBox findDestination(const RealVect &a_pos) const
Map a position to its owning (level, grid index, rank) via the finest containing tile.
Definition CD_ParticleContainerImplem.H:42
void copyMaskParticles(const Vector< RefCountedPtr< LevelData< BaseFab< bool > > > > &a_mask)
Copy the valid particles selected by a per-cell mask into the mask holder, all levels.
Definition CD_ParticleContainerImplem.H:901
std::pair< int, unsigned int > PoolKey
Pool key identifying a destination patch: (level, grid index within the level).
Definition CD_ParticleContainer.H:906
MoverPool gatherGhostsFromMasks(const AMRParticleGhostMask &a_maskSame, const AMRParticleGhostMask &a_maskCoarToFine, const AMRParticleGhostMask &a_maskFineToCoar)
Gather ghost copies into a per-rank pool using prebuilt particle ghost masks.
Definition CD_ParticleContainerImplem.H:754
Per-box CSR lookup from a source cell to the destination boxes whose ghosted region covers it.
Definition CD_ParticleGhostMask.H:53
const Box & box() const noexcept
Get the box that the source cells live on.
Definition CD_ParticleGhostMaskImplem.H:90
unsigned int targetGridIndex(const IntVect &a_iv, const int a_i) const noexcept
Get the destination grid index of the i-th target for a cell.
Definition CD_ParticleGhostMaskImplem.H:116
const Box & targetBox(const IntVect &a_iv, const int a_i) const noexcept
Get the acceptance box of the i-th target for a cell.
Definition CD_ParticleGhostMaskImplem.H:128
int numTargets(const IntVect &a_iv) const noexcept
Get the number of scatter targets for a cell.
Definition CD_ParticleGhostMaskImplem.H:100
unsigned int targetRank(const IntVect &a_iv, const int a_i) const noexcept
Get the receiving rank of the i-th target for a cell.
Definition CD_ParticleGhostMaskImplem.H:122
Arena-backed Struct-of-Arrays particle container for a single grid patch.
Definition CD_ParticleSoA.H:655
RankID & rankID(const std::size_t a_index) noexcept
Owning rank of particle i (container-owned metadata).
Definition CD_ParticleSoA.H:1279
void append(const RealVect &a_position, const double a_weight)
Append one particle with a default-constructed payload.
Definition CD_ParticleSoA.H:955
void shrinkToFit()
Reclaim unused capacity by reallocating the arena down to the current size.
Definition CD_ParticleSoAImplem.H:187
bool isGhost(const std::size_t a_index) const noexcept
Whether particle i is a ghost particle (any non-Valid designation).
Definition CD_ParticleSoA.H:1331
void delinearizeAndAppend(const void *a_buffer)
Append a particle delinearized from a byte buffer (all columns, for MPI receive).
Definition CD_ParticleSoAImplem.H:243
void linearizeParticle(void *a_buffer, const std::size_t a_index) const noexcept
Linearize particle i into a byte buffer (all columns, for MPI send).
Definition CD_ParticleSoAImplem.H:232
RealVect position(const std::size_t a_index) const noexcept
Position of particle i as a RealVect (by value, assembled from the scalar columns).
Definition CD_ParticleSoA.H:1188
GhostType & ghost(const std::size_t a_index) noexcept
Ghost designation of particle i (container-owned metadata).
Definition CD_ParticleSoA.H:1305
double & weight(const std::size_t a_index) noexcept
Weight of particle i.
Definition CD_ParticleSoA.H:1222
std::size_t size() const noexcept
Number of particles currently stored.
Definition CD_ParticleSoA.H:882
P gather(const std::size_t a_index) const
Gather particle i's payload back into the AoS payload view.
Definition CD_ParticleSoA.H:1021
ParticleID & particleID(const std::size_t a_index) noexcept
Global id of particle i (container-owned metadata).
Definition CD_ParticleSoA.H:1253
void remove(const std::size_t a_index) noexcept
Remove particle i using swap-and-pop (O(1), does NOT preserve order).
Definition CD_ParticleSoA.H:1033
void clear() noexcept
Drop all particles (keeps the arena; invalidates the cell sort).
Definition CD_ParticleSoA.H:911
void reserve(const std::size_t a_capacity)
Ensure capacity for at least a_capacity particles (reallocates + moves on growth).
Definition CD_ParticleSoAImplem.H:88
Vector< int > gather(const int &a_localValue) noexcept
Gather local values – return a vector of local values.
Definition CD_ParallelOpsImplem.H:540
Real sum(const Real &a_value) noexcept
Compute the sum across all MPI ranks.
Definition CD_ParallelOpsImplem.H:354
void removeBytes(const Kind a_kind, const std::size_t a_bytes) noexcept
Record a release.
Definition CD_ParticleMemory.H:140
void addBytes(const Kind a_kind, const std::size_t a_bytes) noexcept
Record an allocation.
Definition CD_ParticleMemory.H:124
@ Buffer
Flat exchange buffers: payload in flight.
Result of a point->block query. See findDestination.
Definition CD_LevelTiles.H:123