chombo-discharge
Loading...
Searching...
No Matches
CD_KDParticleMergeImplem.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_KDPARTICLEMERGEIMPLEM_H
14#define CD_KDPARTICLEMERGEIMPLEM_H
15
16// Std includes
17#include <algorithm>
18#include <cmath>
19#include <functional>
20#include <utility>
21#include <vector>
22
23// Chombo includes
24#include <BaseFab.H>
25#include <Box.H>
26#include <CH_Timer.H>
27#include <MayDay.H>
28#include <SPMD.H>
29#include <DataIterator.H>
30
31// Our includes
32#include <CD_LevelTiles.H>
33#include <CD_KDParticleMerge.H>
34#include <CD_NamespaceHeader.H>
35
36namespace ParticleManagement {
37
38namespace detail {
39
40template <typename T>
41inline std::vector<T>
42kdExchangeByRank(const std::vector<std::vector<T>>& a_sendByRank)
43{
44 const int numRanks = static_cast<int>(a_sendByRank.size());
45
46#ifdef CH_MPI
47 if (numRanks <= 1) {
48 return a_sendByRank.empty() ? std::vector<T>() : a_sendByRank[0];
49 }
50
51 std::vector<int> sendCounts(numRanks, 0);
52
53 for (int r = 0; r < numRanks; r++) {
54 sendCounts[r] = static_cast<int>(a_sendByRank[r].size());
55 }
56
57 std::vector<int> recvCounts(numRanks, 0);
58 MPI_Alltoall(sendCounts.data(), 1, MPI_INT, recvCounts.data(), 1, MPI_INT, Chombo_MPI::comm);
59
60 std::vector<int> sdispl(numRanks, 0);
61 std::vector<int> rdispl(numRanks, 0);
62
63 long stot = 0;
64 long rtot = 0;
65
66 for (int r = 0; r < numRanks; r++) {
67 sdispl[r] = static_cast<int>(stot);
68 stot += sendCounts[r];
69
70 rdispl[r] = static_cast<int>(rtot);
71 rtot += recvCounts[r];
72 }
73
74 std::vector<T> sflat(stot);
75
76 for (int r = 0; r < numRanks; r++) {
77 if (sendCounts[r] > 0) {
78 std::copy(a_sendByRank[r].begin(), a_sendByRank[r].end(), sflat.begin() + sdispl[r]);
79 }
80 }
81
82 std::vector<T> rflat(rtot);
83
84 // Scale record counts/displacements up to bytes -- MPI only knows bytes.
85 std::vector<int> sendBytes(numRanks);
86 std::vector<int> recvBytes(numRanks);
87 std::vector<int> sdisplBytes(numRanks);
88 std::vector<int> rdisplBytes(numRanks);
89
90 for (int r = 0; r < numRanks; r++) {
91 sendBytes[r] = sendCounts[r] * static_cast<int>(sizeof(T));
92 recvBytes[r] = recvCounts[r] * static_cast<int>(sizeof(T));
93 sdisplBytes[r] = sdispl[r] * static_cast<int>(sizeof(T));
94 rdisplBytes[r] = rdispl[r] * static_cast<int>(sizeof(T));
95 }
96
97 MPI_Alltoallv(sflat.data(),
98 sendBytes.data(),
99 sdisplBytes.data(),
100 MPI_BYTE,
101 rflat.data(),
102 recvBytes.data(),
103 rdisplBytes.data(),
104 MPI_BYTE,
105 Chombo_MPI::comm);
106
107 return rflat;
108#else
109 return a_sendByRank.empty() ? std::vector<T>() : a_sendByRank[0];
110#endif
111}
112
121template <typename Packed>
122inline void
123kdBBox(RealVect& a_boxLo,
124 RealVect& a_boxHi,
125 const std::vector<MergeParticle<Packed>>& a_particles,
126 const std::size_t a_lo,
127 const std::size_t a_hi) noexcept
128{
129 CH_assert(a_hi > a_lo);
130 CH_assert(a_hi <= a_particles.size());
131
132 a_boxLo = a_particles[a_lo].position;
133 a_boxHi = a_particles[a_lo].position;
134
135 for (std::size_t i = a_lo + 1; i < a_hi; i++) {
136 const RealVect& pos = a_particles[i].position;
137
138 for (int dir = 0; dir < SpaceDim; dir++) {
139 a_boxLo[dir] = std::min(a_boxLo[dir], pos[dir]);
140 a_boxHi[dir] = std::max(a_boxHi[dir], pos[dir]);
141 }
142 }
143}
144
151static constexpr Real s_kdMaxLeafExtent = 1.0;
152
168inline Real
169kdMaxAxisSpan(const RealVect& a_boxLo, const RealVect& a_boxHi, const RealVect& a_dx) noexcept
170{
171 Real maxAxisSpan = 0.0;
172
173 for (int dir = 0; dir < SpaceDim; dir++) {
174 maxAxisSpan = std::max(maxAxisSpan, (a_boxHi[dir] - a_boxLo[dir]) / a_dx[dir]);
175 }
176
177 return maxAxisSpan;
178}
179
189inline IntVect
190kdCellKeyOf(const RealVect& a_position, const RealVect& a_probLo, const RealVect& a_dx) noexcept
191{
192 IntVect iv;
193
194 for (int dir = 0; dir < SpaceDim; dir++) {
195 iv[dir] = static_cast<int>(std::floor((a_position[dir] - a_probLo[dir]) / a_dx[dir]));
196 }
197
198 return iv;
199}
200
209inline bool
210kdIntVectLess(const IntVect& a_lhs, const IntVect& a_rhs) noexcept
211{
212 for (int dir = 0; dir < SpaceDim; dir++) {
213 if (a_lhs[dir] != a_rhs[dir]) {
214 return a_lhs[dir] < a_rhs[dir];
215 }
216 }
217
218 return false;
219}
220
241template <typename Packed>
242inline void
243kdFillCellHistogram(FArrayBox& a_counts,
244 const std::vector<MergeParticle<Packed>>& a_particles,
245 const RealVect& a_probLo,
246 const RealVect& a_dx) noexcept
247{
248 a_counts.setVal(0);
249
250 for (const MergeParticle<Packed>& p : a_particles) {
251 const IntVect cell = kdCellKeyOf(p.position, a_probLo, a_dx);
252
253 // Every gathered particle is resident in this patch or a ghost within one cell of it, so its
254 // cell lies inside a_counts' box. The previous map tolerated any key silently; a cell-indexed
255 // holder does not, which is the point -- the invariant is now checked instead of assumed.
256 CH_assert(a_counts.box().contains(cell));
257
258 a_counts(cell, 0)++;
259 }
260}
261
277template <typename Packed>
278inline std::size_t
280 const std::size_t a_lo,
281 const std::size_t a_hi,
282 const int a_axis) noexcept
283{
284 CH_assert(a_hi - a_lo >= 2);
285
286 const std::size_t mid = a_lo + (a_hi - a_lo) / 2;
287
288 std::nth_element(a_particles.begin() + a_lo,
289 a_particles.begin() + mid,
290 a_particles.begin() + a_hi,
291 [a_axis](const MergeParticle<Packed>& a_p1, const MergeParticle<Packed>& a_p2) noexcept -> bool {
292 return a_p1.position[a_axis] < a_p2.position[a_axis];
293 });
294
295 return mid;
296}
297
322template <typename Packed>
323inline std::size_t
325 const std::size_t a_lo,
326 const std::size_t a_hi,
327 const int a_axis) noexcept
328{
329 CH_assert(a_hi - a_lo >= 2);
330
331 std::sort(a_particles.begin() + a_lo,
332 a_particles.begin() + a_hi,
333 [a_axis](const MergeParticle<Packed>& a_p1, const MergeParticle<Packed>& a_p2) noexcept -> bool {
334 return a_p1.position[a_axis] < a_p2.position[a_axis];
335 });
336
337 Real totalWeight = 0.0;
338
339 for (std::size_t idx = a_lo; idx < a_hi; idx++) {
340 totalWeight += a_particles[idx].weight;
341 }
342
343 const Real half = 0.5 * totalWeight;
344
345 Real acc = 0.0;
346 std::size_t mid = a_lo + 1;
347
348 for (std::size_t idx = a_lo; idx < a_hi; idx++) {
349 acc += a_particles[idx].weight;
350
351 if (acc >= half) {
352 mid = idx + 1;
353
354 break;
355 }
356 }
357
358 const std::size_t clamped = std::max(a_lo + 1, std::min(a_hi - 1, mid));
359
360 // Both children must be non-empty or the caller's recursion cannot terminate.
361 CH_assert(clamped > a_lo && clamped < a_hi);
362
363 return clamped;
364}
365
366template <typename Packed>
367inline void
369 FArrayBox& a_used,
370 std::vector<KDLeaf>& a_leaves,
371 const int a_ppc,
372 const Real a_splitWeightLeafDx,
373 const RealVect& a_dx,
374 const RealVect& a_probLo,
375 const FArrayBox& a_cellCounts) noexcept
376{
377 CH_TIME("ParticleManagement::buildKDQuotaLeaves");
378
379 a_leaves.clear();
380
381 if (a_particles.empty() || a_ppc <= 0) {
382 return;
383 }
384
385 // Partition into the particles that may be merged (those in TRUE-crowded cells) and those that
386 // may not, moving the mergeable ones to the front so every leaf below is still a contiguous range.
387 // Particles in uncrowded cells are emitted as singleton leaves: the merge threshold is 2 members,
388 // so a singleton is never merged, and an uncrowded cell can therefore never be drained below
389 // target by a neighbour's merge.
390 const auto firstUnmergeable = std::stable_partition(a_particles.begin(),
391 a_particles.end(),
392 [&](const MergeParticle<Packed>& a_p) {
393 const IntVect cell = kdCellKeyOf(a_p.position, a_probLo, a_dx);
394
395 // A cell outside the histogram box reads as 0
396 // (empty) and is therefore never crowded.
397 return a_cellCounts.box().contains(cell)
398 ? static_cast<int>(a_cellCounts(cell, 0)) > a_ppc
399 : false;
400 });
401
402 const std::size_t numMergeable = static_cast<std::size_t>(firstUnmergeable - a_particles.begin());
403
404 for (std::size_t idx = numMergeable; idx < a_particles.size(); idx++) {
405 RealVect boxLo, boxHi;
406 kdBBox(boxLo, boxHi, a_particles, idx, idx + 1);
407
408 a_leaves.push_back(KDLeaf{idx, idx + 1, boxLo, boxHi});
409 }
410
411 if (numMergeable == 0) {
412 return;
413 }
414
415 // LIVE PER-CELL LEAF BUDGET, enforced as a hard constraint at split time rather than computed
416 // per node and hoped for afterwards.
417 //
418 // Every leaf becomes exactly ONE super-particle at its own weighted centroid, so the number of
419 // leaves whose centroid falls in a cell is that cell's post-merge population. Track that count
420 // live and refuse any split that would push a cell past a_ppc. The per-cell ceiling is a shared
421 // resource and cannot be expressed by any test a node applies to itself alone.
422 //
423 // Splits are handed out heaviest-first: the quota is claimed first-come-first-served, so a
424 // lighter ordering lets light leaves take a cell's slots before a heavy leaf is considered, and
425 // the heavy one is then refused and retired whole as a single very heavy super-particle.
426 // Returns both the centroid's cell AND the range's total weight. The weight sum is a byproduct of
427 // the centroid accumulation, so returning it here removes the separate per-child rangeWeight() pass
428 // the heap loop used to make (same summation order, so the Real is bit-identical).
429 auto centroidCellOf = [&](const std::size_t a_lo, const std::size_t a_hi) -> std::pair<IntVect, Real> {
430 RealVect centroid = RealVect::Zero;
431 Real totalW = 0.0;
432
433 for (std::size_t idx = a_lo; idx < a_hi; idx++) {
434 centroid += a_particles[idx].weight * a_particles[idx].position;
435 totalW += a_particles[idx].weight;
436 }
437
438 if (totalW > 0.0) {
439 centroid /= totalW;
440 }
441 else {
442 centroid = a_particles[a_lo].position;
443 }
444
445 return {kdCellKeyOf(centroid, a_probLo, a_dx), totalW};
446 };
447
448 struct KDCand
449 {
450 Real weight;
451 std::size_t count;
452 std::size_t lo;
453 std::size_t hi;
454 IntVect cell;
455 };
456
457 // Hand out each cell's finite quota to its HEAVIEST candidate leaf first, not its most populous.
458 // The quota is a scarce resource claimed first-come-first-served, so ordering by count lets light
459 // leaves consume a cell's slots before a heavy leaf is ever considered -- the heavy one is then
460 // refused and retired whole, becoming a single very heavy super-particle sitting next to the light
461 // ones that took its slots. Ordering by weight makes the leaves that most need subdividing claim
462 // slots first.
463 const auto byWeight = [](const KDCand& a_lhs, const KDCand& a_rhs) noexcept -> bool {
464 return a_lhs.weight < a_rhs.weight;
465 };
466
467 // Live per-cell leaf count: how many leaves are currently centred in each cell. Caller-owned mesh
468 // data, zeroed here; its ghost cell must cover every cell a leaf centroid can fall in, since
469 // gathered ghosts sit up to one cell outside this patch.
470 FArrayBox& used = a_used;
471 used.setVal(0.0);
472
473 // Reserved to numMergeable, a safe upper bound for both: leaves partition [0,numMergeable) with at
474 // least one member each, so #leaves <= numMergeable, and the split frontier can never exceed the
475 // leaf count. One allocation apiece instead of the push_back reallocation ladder, every patch.
476 std::vector<KDCand> heap;
477 std::vector<KDLeaf> finalLeaves;
478
479 heap.reserve(numMergeable);
480 finalLeaves.reserve(numMergeable);
481
482 {
483 const auto [rootCell, rootWeight] = centroidCellOf(0, numMergeable);
484
485 // A centroid of positions gathered for this patch cannot fall outside the patch grown by the
486 // ghost width. Asserted rather than assumed: the map this replaced would have accepted any cell
487 // silently, which is exactly the class of bug a bounded holder is meant to surface.
488 CH_assert(used.box().contains(rootCell));
489
490 used(rootCell, 0)++;
491 heap.push_back(KDCand{rootWeight, numMergeable, 0, numMergeable, rootCell});
492 }
493
494 while (!heap.empty()) {
495 std::pop_heap(heap.begin(), heap.end(), byWeight);
496
497 const KDCand node = heap.back();
498 heap.pop_back();
499
500 if (node.count < 2) {
501 RealVect boxLo, boxHi;
502 kdBBox(boxLo, boxHi, a_particles, node.lo, node.hi);
503
504 finalLeaves.push_back(KDLeaf{node.lo, node.hi, boxLo, boxHi});
505
506 continue;
507 }
508
509 // One bounding box per node, computed once here and reused three ways: the span test, the split
510 // axis (formerly re-derived from a second box inside each split helper), and -- if the split is
511 // refused below -- the emitted leaf (formerly re-derived a third time in the emission pass). A
512 // split only reorders within [lo,hi), so the box is unchanged by it either way.
513 RealVect nodeBoxLo, nodeBoxHi;
514 kdBBox(nodeBoxLo, nodeBoxHi, a_particles, node.lo, node.hi);
515
516 const Real nodeSpan = kdMaxAxisSpan(nodeBoxLo, nodeBoxHi, a_dx);
517 const int splitAxis = (nodeBoxHi - nodeBoxLo).maxDir(true);
518
519 // While a node spans several cells a split apportions leaves BETWEEN cells, which is a question
520 // of counts; once it is narrow the weight median is what drives the super-particles toward equal
521 // weight. This is an extent test, not a "both corners in one cell" test, so a narrow node
522 // straddling a face still qualifies.
523 const std::size_t mid = (nodeSpan <= a_splitWeightLeafDx)
524 ? kdSplitWeightMedian(a_particles, node.lo, node.hi, splitAxis)
525 : kdSplitCountMedian(a_particles, node.lo, node.hi, splitAxis);
526
527 const auto [cellLeft, weightLeft] = centroidCellOf(node.lo, mid);
528 const auto [cellRight, weightRight] = centroidCellOf(mid, node.hi);
529
530 CH_assert(used.box().contains(cellLeft));
531 CH_assert(used.box().contains(cellRight));
532
533 used(node.cell, 0)--;
534 used(cellLeft, 0)++;
535 used(cellRight, 0)++;
536
537 // The quota constrains only leaves that can actually merge. A node wider than
538 // s_kdMaxLeafExtent is vetoed by the caller and leaves every member behind
539 // individually, so it costs its cell its full member count rather than one particle; splitting
540 // it further lowers that cell's final population even though it raises the leaf count.
541 if (nodeSpan <= s_kdMaxLeafExtent && (used(cellLeft, 0) > a_ppc || used(cellRight, 0) > a_ppc)) {
542 used(cellLeft, 0)--;
543 used(cellRight, 0)--;
544 used(node.cell, 0)++;
545
546 finalLeaves.push_back(KDLeaf{node.lo, node.hi, nodeBoxLo, nodeBoxHi});
547
548 continue;
549 }
550
551 heap.push_back(KDCand{weightLeft, mid - node.lo, node.lo, mid, cellLeft});
552 std::push_heap(heap.begin(), heap.end(), byWeight);
553
554 heap.push_back(KDCand{weightRight, node.hi - mid, mid, node.hi, cellRight});
555 std::push_heap(heap.begin(), heap.end(), byWeight);
556 }
557
558 for (const KDLeaf& leaf : finalLeaves) {
559 a_leaves.push_back(leaf);
560 }
561
562 // The leaves must partition the input exactly: every particle in exactly one leaf, no overlap and
563 // nothing dropped. Callers rely on this both for the merge itself and for the invariant that
564 // total weight is conserved.
565#ifndef NDEBUG
566 std::size_t coveredMembers = 0;
567
568 for (const KDLeaf& bl : a_leaves) {
569 CH_assert(bl.hi > bl.lo);
570 CH_assert(bl.hi <= a_particles.size());
571
572 coveredMembers += bl.hi - bl.lo;
573 }
574
575 CH_assert(coveredMembers == a_particles.size());
576#endif
577}
578
590inline void
591kdBoxRealBounds(RealVect& a_boxLo,
592 RealVect& a_boxHi,
593 const Box& a_box,
594 const RealVect& a_dx,
595 const RealVect& a_probLo) noexcept
596{
597 for (int dir = 0; dir < SpaceDim; dir++) {
598 a_boxLo[dir] = a_probLo[dir] + a_box.smallEnd(dir) * a_dx[dir];
599 a_boxHi[dir] = a_probLo[dir] + (a_box.bigEnd(dir) + 1) * a_dx[dir];
600 }
601}
602
617inline void
618kdCheckCentroid(const RealVect& a_centroid,
619 const Real a_totalWeight,
620 const RealVect& a_boxLo,
621 const RealVect& a_boxHi) noexcept
622{
623#ifndef NDEBUG
624 CH_assert(a_totalWeight > 0.0);
625
626 for (int dir = 0; dir < SpaceDim; dir++) {
627 const Real tol = 1.0e-9 * (std::abs(a_boxLo[dir]) + std::abs(a_boxHi[dir]) + 1.0);
628
629 CH_assert(a_centroid[dir] >= a_boxLo[dir] - tol);
630 CH_assert(a_centroid[dir] <= a_boxHi[dir] + tol);
631 }
632#else
633 (void)a_centroid;
634 (void)a_totalWeight;
635 (void)a_boxLo;
636 (void)a_boxHi;
637#endif
638}
639
640} // namespace detail
641
642template <typename P,
643 typename Packed,
644 typename Traits,
645 typename Gather,
646 typename Combine,
647 typename Scatter,
648 typename Allocator,
649 typename PosValid>
650inline void
652 EBAMRFAB& a_cellHistogram,
653 EBAMRFAB& a_leafQuota,
654 const AmrMesh& a_amr,
655 const int a_ppc,
656 const Real a_splitWeightLeafDx,
657 const Gather& a_gather,
658 const Combine& a_combine,
659 const Scatter& a_scatter,
660 const Allocator& a_allocateID,
661 const PosValid& a_isPositionValid)
662{
663 using namespace detail;
664
665 // Carve-protocol types. Local to this function because nothing else constructs or consumes them:
666 // they describe one pass of the claim/verdict/commit exchange and have no meaning outside it.
667
668 // Box ranking key. Tightest box wins a contested particle; the anchor id is the deterministic
669 // tiebreak, since AABB volume alone is not a strict total order.
670 struct KDBoxKey
671 {
672 Real volume;
673 ParticleID anchor;
674
675 bool
676 operator<(const KDBoxKey& a_rhs) const noexcept
677 {
678 return (volume != a_rhs.volume) ? (volume < a_rhs.volume) : (anchor < a_rhs.anchor);
679 }
680 };
681
682 // One member of a boundary box: the particle and the rank that owns it.
683 struct KDMember
684 {
685 ParticleID id;
686 RankID owner;
687 };
688
689 // Phase 1 wire type: a box's claim on one member, routed to that member's owner.
690 struct KDClaim
691 {
692 ParticleID memberID;
693 KDBoxKey key;
694 RankID proposerRank;
695 int proposerBoxIdx; // which of the proposer's own boxes claimed it
696 };
697
698 // Phase 2 wire type: the owner's nominal argmin winner, answered per claiming box.
699 struct KDVerdict
700 {
701 ParticleID memberID;
702 int claimantBoxIdx;
703 bool won;
704 };
705
706 // Phase 3 wire type: the proposer's actual outcome. Only this authorizes a deletion.
707 struct KDCommit
708 {
709 ParticleID memberID;
710 bool committed;
711 };
712
713 CH_TIMERS("ParticleManagement::mergeKDCarve");
714 CH_TIMER("ParticleManagement::mergeKDCarve::build_classify", t_build);
715 CH_TIMER("ParticleManagement::mergeKDCarve::carve_exchange", t_carve);
716 CH_TIMER("ParticleManagement::mergeKDCarve::remove_consumed", t_remove);
717 CH_TIMER("ParticleManagement::mergeKDCarve::place_results", t_place);
718
719 const std::string realm = a_particles.getRealm();
720 const int finestLevel = a_amr.getFinestLevel();
721 const RealVect probLo = a_amr.getProbLo();
722
723 // Per-cell scratch, as mesh data on this realm rather than per-patch buffers: both quantities are
724 // cell data, and this is what the rest of the code uses for cell data. The caller owns the holders
725 // so they are allocated once per regrid rather than once per call -- AmrMesh::allocate() builds a
726 // Copier per level, which is cheap in isolation but adds up at scale. Their one ghost cell is what
727 // the gathered ghosts and any just-outside leaf centroid need.
728 EBAMRFAB& histogram = a_cellHistogram;
729 EBAMRFAB& leafQuota = a_leafQuota;
730
731 CH_assert(histogram[0]->nComp() == 1);
732 CH_assert(leafQuota[0]->nComp() == 1);
733 CH_assert(histogram[0]->ghostVect() >= IntVect::Unit);
734 CH_assert(leafQuota[0]->ghostVect() >= IntVect::Unit);
735
736 const int myRank = procID();
737 const int numRanks = numProc();
738
739 // ---- Per-patch bookkeeping shared across the whole rank ----
740 struct PatchWork
741 {
742 int level;
743 DataIndex din;
744 RealVect dx;
745 Box box;
746 };
747
748 struct RuntimeBox
749 {
750 KDBoxKey key;
751 int patchIdx;
752 std::vector<KDMember> members;
753 };
754
755 // particle: the merged super-particle itself.
756 // patchIdx: which patch's leaf produced it, used as the cheap first guess when placing it.
757 struct MergedResult
758 {
759 MergeParticle<Packed> particle;
760 int patchIdx;
761 };
762
763 // One entry per (locally-owned, boundary-exposed particle) x (one of MY OWN boxes that lists it),
764 // appended during STEP 1 and sorted by id once STEP 1 finishes -- see selfClaims' declaration
765 // below for why an id can have more than one entry, and boxIdx < 0 for what a sentinel entry means.
766 // id: the locally-owned, boundary-exposed particle this entry concerns.
767 // key: the claiming box's key. Meaningless when boxIdx is negative.
768 // boxIdx: index into myBoxes, or -1 for a listen-only sentinel. A sentinel carries no claim from
769 // any of this rank's own boxes; it records that this id must still be processed against
770 // foreign claims in STEP 3, which is the case for an unmergeable leaf's exposed member or
771 // a boundary leaf holding fewer than two members.
772 struct SelfClaimEntry
773 {
774 ParticleID id;
775 KDBoxKey key;
776 int boxIdx;
777 };
778
779 std::vector<PatchWork> patchWork;
780
781 // gatheredParticles: every particle this rank gathered, locals and ghosts alike, in gather order.
782 // Append-only and never reordered, so an index into it is a stable handle. Holds the
783 // full payloads particlesByID used to carry inline.
784 // particlesByID: one (id, slot-into-gatheredParticles) entry per (id, patch-occurrence), appended
785 // during STEP 1, then deduplicated once STEP 1 finishes by sorting and collapsing
786 // runs, preferring the non-ghost copy. Sorting these light (id, slot) keys instead of
787 // whole particles is what keeps the dedup off the fat-struct memory path. Point
788 // lookups after that go through findParticle() below, which binary-searches it and
789 // dereferences the surviving slot.
790 // myBoxes: this rank's own boundary boxes, indexed by the boxIdx carried in claims.
791 // consumedIDs: ids whose particle has been merged away and must be deleted. Sorted once,
792 // before STEP 7 consumes it by binary search.
793 // mergedResults: super-particles produced this pass, placed in STEP 8.
794 std::vector<MergeParticle<Packed>> gatheredParticles;
795 std::vector<std::pair<ParticleID, std::size_t>> particlesByID;
796 std::vector<RuntimeBox> myBoxes;
797 std::vector<ParticleID> consumedIDs;
798 std::vector<MergedResult> mergedResults;
799
800 // A particle can be listed by more than one of MY OWN boxes at once -- it is gathered once into
801 // its own home patch's build (as a local member there) but can ALSO be gathered as a ghost into a
802 // NEIGHBORING patch's build on this SAME rank, and that patch's build is entirely independent -- it
803 // may propose its own, different box that also lists this particle. Treating "owner == myRank" as
804 // "no contest possible" (collapsing to a single entry per id) is exactly the bug this vector-of-
805 // entries shape exists to prevent: two of my own boxes can genuinely compete for the same particle
806 // with no other rank involved at all, and collapsing the earlier entry double-counts the particle's
807 // weight if both boxes go on to commit. Sorted by id once STEP 1 finishes (see below); STEP 3
808 // processes it as grouped runs of equal id rather than hashing.
809 std::vector<SelfClaimEntry> selfClaims;
810
811 // Cheap pre-pass (patch sizes only, no per-particle work) so the containers below can be sized
812 // once up front instead of growing repeatedly as STEP 1 fills them. combinedCountUpperBound
813 // includes ghosts, so it's also a safe (if slightly generous) upper bound for the local-only ones.
814 std::size_t combinedCountUpperBound = 0;
815
816 for (int lvl = 0; lvl <= finestLevel; lvl++) {
817 const DisjointBoxLayout& dbl = a_amr.getGrids(realm)[lvl];
818 const DataIterator& dit = dbl.dataIterator();
819 const int nbox = dit.size();
820
821#pragma omp parallel for schedule(runtime) reduction(+ : combinedCountUpperBound)
822 for (int mybox = 0; mybox < nbox; mybox++) {
823 combinedCountUpperBound += a_particles[lvl][dit[mybox]].size();
824 }
825 }
826
827 gatheredParticles.reserve(combinedCountUpperBound);
828 particlesByID.reserve(combinedCountUpperBound);
829 selfClaims.reserve(combinedCountUpperBound);
830 consumedIDs.reserve(combinedCountUpperBound);
831
832 // Per-patch leaf/member scratch, declared once and reused across every patch below rather than
833 // reallocated inside the loop -- the same reuse pattern as the histogram/leafQuota holders above.
834 // buildKDQuotaLeaves() clear()s leaves on entry and members is cleared per leaf, so each reuse
835 // retains the heap capacity grown by earlier patches instead of starting from nothing.
836 std::vector<KDLeaf> leaves;
837 std::vector<KDMember> members;
838
839 // Which cells hold a particle that some other box can also see. Read from the realm, which derives it
840 // from the three ghost masks when they are built; see Realm::m_particleGhostExposure.
841 const AMRMask& exposure = a_amr.getParticleGhostExposure(realm, 1);
842
843 // ==== STEP 1: gather + build + classify + commit interior, per patch ====
844 CH_START(t_build);
845
846 for (int lvl = 0; lvl <= finestLevel; lvl++) {
847 const DisjointBoxLayout& dbl = a_amr.getGrids(realm)[lvl];
848 const DataIterator& dit = dbl.dataIterator();
849
850 const RealVect dx = a_amr.getDx()[lvl] * RealVect::Unit;
851
852 const int nbox = dit.size();
853
854 // Serial (no omp): every patch appends to the same gatheredParticles/particlesByID/patchWork/
855 // selfClaims/consumedIDs buffers, reuses the shared leaves/members scratch declared above, and
856 // draws ids from the single a_allocateID() counter. Parallelising the box loop would race on all
857 // of them, and the id race would mint duplicates.
858 for (int mybox = 0; mybox < nbox; mybox++) {
859 const DataIndex& din = dit[mybox];
860
861 ParticleSoA<P, Traits>& leaf = a_particles[lvl][din];
862
863 const BaseFab<bool>& exposureDin = (*exposure[lvl])[din];
864
865 std::vector<MergeParticle<Packed>> combined;
866 combined.reserve(leaf.size());
867
868 for (std::size_t i = 0; i < leaf.size(); i++) {
870
871 p.position = leaf.position(i);
872 p.weight = leaf.weight(i);
873 p.globalID = leaf.particleID(i);
874 p.ownerRank = leaf.rankID(i);
875 p.isGhost = leaf.isGhost(i);
876 p.payload = a_gather(leaf, i);
877
878 combined.push_back(p);
879
880 // Append unconditionally -- deduplicated by id (preferring the non-ghost copy, per
881 // MergeParticle::isGhost's own docs) in the one pass below once STEP 1 finishes gathering,
882 // rather than via a per-particle hash lookup here. The full payload lives in gatheredParticles;
883 // particlesByID carries only (id, slot) so the dedup sort stays off the fat-struct path.
884 gatheredParticles.push_back(p);
885 particlesByID.emplace_back(p.globalID, gatheredParticles.size() - 1);
886 }
887
888 patchWork.push_back(PatchWork{lvl, din, dx, dbl[din]});
889 const int patchIdx = static_cast<int>(patchWork.size()) - 1;
890
891 if (combined.empty()) {
892 continue;
893 }
894
895 // Ground truth for how crowded each cell really is -- see kdFillCellHistogram(). Both this and
896 // the live quota are cell data, so they live in the mesh holders allocated once above rather
897 // than in per-patch scratch. Their ghost cell covers the gathered ghosts, which sit up to one
898 // cell outside this patch, and any leaf centroid that lands just outside it.
899 FArrayBox& cellCounts = (*histogram[lvl])[din];
900 FArrayBox& quota = (*leafQuota[lvl])[din];
901
902 kdFillCellHistogram(cellCounts, combined, probLo, dx);
903
904 buildKDQuotaLeaves(combined, quota, leaves, a_ppc, a_splitWeightLeafDx, dx, probLo, cellCounts);
905
906 for (const KDLeaf& bl : leaves) {
907 members.clear();
908 members.reserve(bl.hi - bl.lo);
909
910 bool anyLocal = false;
911
912 // Running minimum over ALL members; bl.hi > bl.lo always holds for a leaf. Folded into the
913 // loop below so the boundary branch does not need a separate pass just to compute it.
914 ParticleID anchor = combined[bl.lo].globalID;
915
916 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
917 members.push_back(KDMember{combined[idx].globalID, combined[idx].ownerRank});
918 anchor = std::min(anchor, combined[idx].globalID);
919
920 // "Local" here means physically resident in THIS patch, not merely owned by this rank --
921 // a particle owned by this rank via a DIFFERENT one of its own patches, appearing here
922 // only as a ghost, gives this leaf no standing (see MergeParticle::isGhost's own docs).
923 if (!combined[idx].isGhost) {
924 anyLocal = true;
925 }
926 }
927
928 if (!anyLocal) {
929 // Nothing physically resident here, so this rank has no standing to act on it.
930 continue;
931 }
932
933 Real leafVolume = 1.0;
934
935 for (int dir = 0; dir < SpaceDim; dir++) {
936 leafVolume *= (bl.boxHi[dir] - bl.boxLo[dir]);
937 }
938
939 // Per-axis extent, not aggregate volume -- see kdMaxAxisSpan()'s own docs for why a
940 // volume ratio alone cannot be trusted here (a long, thin leaf can have volume <=
941 // one cell while still spanning several cells along one axis). leafVolume itself
942 // is still needed below, unrelated to this check -- it's the box key's tie-break priority.
943 const bool unmergeable = kdMaxAxisSpan(bl.boxLo, bl.boxHi, dx) > s_kdMaxLeafExtent;
944
945 // Per-resident-member exposure -- needed either way (leaf-wide OR for the interior/
946 // boundary decision below; per-particle for the unmergeable-leaf listen-only case). Ghosts
947 // are skipped entirely, not just because they have no standing (above) but because their
948 // position lies outside this patch's own box -- the exposure mask carries no ghost cells, so
949 // a ghost's cell is not addressable in it at all. Filtering on isGhost (not ownerRank) is what
950 // makes this safe: a particle owned by this rank via a different patch is exactly as much
951 // a ghost here, position-wise, as one owned by a different rank entirely.
952 std::vector<bool> exposed(bl.hi - bl.lo, false);
953 bool anyExposed = false;
954 bool anyGhost = false;
955
956 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
957 if (combined[idx].isGhost) {
958 anyGhost = true;
959
960 continue;
961 }
962
963 const IntVect cell = kdCellKeyOf(combined[idx].position, probLo, dx);
964
965 CH_assert(exposureDin.box().contains(cell));
966
967 const bool isExposed = exposureDin(cell, 0);
968
969 exposed[idx - bl.lo] = isExposed;
970 anyExposed = anyExposed || isExposed;
971 }
972
973 if (unmergeable) {
974 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
975 // exposed[] is already false for every ghost by construction (the exposure loop above
976 // skips them), so this is equivalent to also checking !isGhost -- kept explicit anyway
977 // as the actual invariant being relied on, not an implicit one.
978 if (!combined[idx].isGhost && exposed[idx - bl.lo]) {
979 // Listen-only sentinel; see SelfClaimEntry.
980 selfClaims.push_back(SelfClaimEntry{combined[idx].globalID, KDBoxKey{}, -1});
981 }
982 }
983
984 continue;
985 }
986
987 if (!anyExposed && !anyGhost) {
988 // Interior: commit immediately, zero communication, direct insert -- safe ONLY because
989 // anyGhost is false, i.e. this leaf genuinely contains no foreign data at all. Explicitly
990 // requiring !anyGhost (not just !anyExposed) matters whenever a leaf spans cells: the old
991 // argument ("a leaf whose members are all non-exposed can never contain a ghost, since
992 // ghost-fill is symmetric at width 1") only holds when a leaf's own extent is <= 1 cell --
993 // every member is then automatically within ghost-reach of every other member, so a ghost's
994 // presence would force at least one resident's own cell to be exposed too. Once a leaf can
995 // span SEVERAL cells, a ghost near one edge and a resident several
996 // cells away at the other edge can coexist in the SAME leaf without that resident itself
997 // ever being within ghost-width-1 of anything -- anyExposed alone would then be false even
998 // though the leaf holds a foreign particle, and committing it here would merge that
999 // particle's weight locally while its true home patch independently wins the SAME
1000 // particle's argmin via its own boundary box, double-counting it. Falling through to the
1001 // boundary-candidate branch below instead (rather than committing) is what lets a real
1002 // self-claim be raised for that foreign member, so the argmin actually arbitrates it.
1003 if (bl.hi - bl.lo >= 2) {
1004 std::vector<Packed> payloads;
1005 std::vector<Real> weights;
1006 RealVect centroid = RealVect::Zero;
1007 Real totalW = 0.0;
1008
1009 payloads.reserve(bl.hi - bl.lo);
1010 weights.reserve(bl.hi - bl.lo);
1011
1012 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
1013 const MergeParticle<Packed>& p = combined[idx];
1014
1015 CH_assert(!p.isGhost);
1016
1017 payloads.push_back(p.payload);
1018 weights.push_back(p.weight);
1019 centroid += p.weight * p.position;
1020 totalW += p.weight;
1021 }
1022
1023 centroid /= totalW;
1024
1025 kdCheckCentroid(centroid, totalW, bl.boxLo, bl.boxHi);
1026
1027 if (a_isPositionValid(centroid)) {
1028 MergeParticle<Packed> merged;
1029
1030 merged.position = centroid;
1031 merged.weight = totalW;
1032 merged.globalID = a_allocateID();
1033 merged.ownerRank = myRank;
1034 merged.payload = a_combine(payloads.data(), weights.data(), payloads.size());
1035
1036 mergedResults.push_back(MergedResult{merged, patchIdx});
1037
1038 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
1039 consumedIDs.push_back(combined[idx].globalID);
1040 }
1041 }
1042 }
1043
1044 continue;
1045 }
1046
1047 // Boundary candidate.
1048 if (members.size() >= 2) {
1049 const KDBoxKey key{leafVolume, anchor};
1050
1051 const int boxIdx = static_cast<int>(myBoxes.size());
1052 myBoxes.push_back(RuntimeBox{key, patchIdx, members});
1053
1054 for (const KDMember& m : members) {
1055 if (m.owner == myRank) {
1056 // Append, never collapse -- see selfClaims' own docs: this same particle can already
1057 // carry an entry from a DIFFERENT one of my own boxes (a different patch's independent
1058 // build), and both must survive to be argmin'd together in step 3.
1059 selfClaims.push_back(SelfClaimEntry{m.id, key, boxIdx});
1060 }
1061 }
1062 }
1063 else {
1064 // Exactly one member, and it must be the resident, exposed one (members.size()>=1 and
1065 // anyLocal both hold) -- can never reach the merge threshold alone, listen only.
1066 selfClaims.push_back(SelfClaimEntry{members[0].id, KDBoxKey{}, -1});
1067 }
1068 }
1069 }
1070 }
1071
1072 // particlesByID was appended to once per (id, patch-occurrence) above -- an id can appear more
1073 // than once (its home patch, plus any neighboring patch that gathered it as a ghost). Sort once
1074 // and collapse each run to a single entry, preferring the non-ghost copy when the run has one
1075 // (ghost-vs-ghost or non-ghost-vs-non-ghost duplicates carry identical data, so it doesn't matter
1076 // which of those survives) -- this is the dedup rule the old per-particle hash lookup used to
1077 // enforce inline.
1078 std::sort(particlesByID.begin(), particlesByID.end(), [](const auto& a_lhs, const auto& a_rhs) {
1079 return a_lhs.first < a_rhs.first;
1080 });
1081 {
1082 std::size_t writeIdx = 0;
1083
1084 for (std::size_t readIdx = 0; readIdx < particlesByID.size();) {
1085 std::size_t runEnd = readIdx + 1;
1086
1087 while (runEnd < particlesByID.size() && particlesByID[runEnd].first == particlesByID[readIdx].first) {
1088 runEnd++;
1089 }
1090
1091 std::size_t chosen = readIdx;
1092 for (std::size_t k = readIdx; k < runEnd; k++) {
1093 if (!gatheredParticles[particlesByID[k].second].isGhost) {
1094 chosen = k;
1095
1096 break;
1097 }
1098 }
1099
1100 if (writeIdx != chosen) {
1101 particlesByID[writeIdx] = particlesByID[chosen];
1102 }
1103 writeIdx++;
1104 readIdx = runEnd;
1105 }
1106
1107 particlesByID.resize(writeIdx);
1108 }
1109
1110 // Binary-search lookup into the now-sorted, deduplicated particlesByID -- replaces the old
1111 // unordered_map's .at().
1112 auto findParticle = [&particlesByID, &gatheredParticles](const ParticleID a_id) -> const MergeParticle<Packed>& {
1113 const auto it = std::lower_bound(particlesByID.begin(),
1114 particlesByID.end(),
1115 a_id,
1116 [](const std::pair<ParticleID, std::size_t>& a_entry, const ParticleID a_key) {
1117 return a_entry.first < a_key;
1118 });
1119
1120 CH_assert(it != particlesByID.end() && it->first == a_id);
1121
1122 return gatheredParticles[it->second];
1123 };
1124
1125 // selfClaims was appended to during STEP 1 -- sort by id once so STEP 3 can process it as grouped
1126 // runs of equal id (a particle's possibly-several self-claim entries, see SelfClaimEntry's own
1127 // docs) instead of hashing.
1128 std::sort(selfClaims.begin(), selfClaims.end(), [](const SelfClaimEntry& a_lhs, const SelfClaimEntry& a_rhs) {
1129 return a_lhs.id < a_rhs.id;
1130 });
1131
1132 CH_STOP(t_build);
1133
1134 // ==== STEP 2: Phase 1 -- claims to owners ====
1135 CH_START(t_carve);
1136 std::vector<std::vector<KDClaim>> claimSendByRank(numRanks);
1137
1138 for (std::size_t boxIdx = 0; boxIdx < myBoxes.size(); boxIdx++) {
1139 const RuntimeBox& box = myBoxes[boxIdx];
1140
1141 for (const KDMember& m : box.members) {
1142 if (m.owner != myRank) {
1143 claimSendByRank[m.owner].push_back(KDClaim{m.id, box.key, myRank, static_cast<int>(boxIdx)});
1144 }
1145 }
1146 }
1147
1148 // Sorted by memberID once received, so STEP 3's point lookups (claimsRange, below) and STEP 4's
1149 // full pass (grouped runs) don't need a hash map either.
1150 std::vector<KDClaim> incomingClaims = kdExchangeByRank(claimSendByRank);
1151 std::sort(incomingClaims.begin(), incomingClaims.end(), [](const KDClaim& a_lhs, const KDClaim& a_rhs) {
1152 return a_lhs.memberID < a_rhs.memberID;
1153 });
1154
1155 auto claimsRange = [&incomingClaims](const ParticleID a_id) {
1156 const auto lo = std::lower_bound(incomingClaims.begin(),
1157 incomingClaims.end(),
1158 a_id,
1159 [](const KDClaim& a_c, const ParticleID a_key) {
1160 return a_c.memberID < a_key;
1161 });
1162 const auto hi = std::upper_bound(incomingClaims.begin(),
1163 incomingClaims.end(),
1164 a_id,
1165 [](const ParticleID a_key, const KDClaim& a_c) {
1166 return a_key < a_c.memberID;
1167 });
1168
1169 return std::make_pair(lo, hi);
1170 };
1171
1172 // ==== STEP 3: local argmin -- nominal winner only, no deletion yet ====
1173 // The winner of a particle is either one of MY OWN boxes (rank == myRank, boxIdx identifies
1174 // WHICH one -- see selfClaims' own docs for why this can't just be "myRank", plural competing
1175 // boxes on this same rank are exactly the bug that distinction exists to prevent) or a foreign
1176 // rank (boxIdx meaningless).
1177 // rank: the rank owning the winning box.
1178 // boxIdx: that box's index within the winning rank's own box list.
1179 // key: the winning key itself, needed in STEP 4 to answer each incoming claim individually --
1180 // a claim won iff its own key is not strictly worse than this one.
1181 struct Winner
1182 {
1183 RankID rank;
1184 int boxIdx;
1185 KDBoxKey key;
1186 };
1187
1188 struct WinnerEntry
1189 {
1190 ParticleID id;
1191 Winner winner;
1192 };
1193
1194 // Built in ascending-id order for free -- selfClaims is sorted by id, and its grouped runs are
1195 // visited in that same order below -- so no separate sort is needed before findWinner's binary
1196 // search.
1197 std::vector<WinnerEntry> nominalWinner;
1198 nominalWinner.reserve(selfClaims.size());
1199
1200 for (std::size_t idx = 0; idx < selfClaims.size();) {
1201 const ParticleID id = selfClaims[idx].id;
1202
1203 std::size_t idxEnd = idx + 1;
1204
1205 while (idxEnd < selfClaims.size() && selfClaims[idxEnd].id == id) {
1206 idxEnd++;
1207 }
1208
1209 bool haveWinner = false;
1210 KDBoxKey bestKey{};
1211 RankID bestRank = myRank;
1212 int bestBoxIdx = -1;
1213
1214 for (std::size_t k = idx; k < idxEnd; k++) {
1215 if (selfClaims[k].boxIdx < 0) {
1216 // Listen-only sentinel, not an actual self-claim.
1217 continue;
1218 }
1219 if (!haveWinner || selfClaims[k].key < bestKey) {
1220 haveWinner = true;
1221 bestKey = selfClaims[k].key;
1222 bestRank = myRank;
1223 bestBoxIdx = selfClaims[k].boxIdx;
1224 }
1225 }
1226
1227 const auto range = claimsRange(id);
1228
1229 for (auto cit = range.first; cit != range.second; ++cit) {
1230 const KDClaim& c = *cit;
1231
1232 if (!haveWinner || c.key < bestKey) {
1233 haveWinner = true;
1234 bestKey = c.key;
1235 bestRank = c.proposerRank;
1236 // The winning claim's OWN box index (on ITS rank), not a sentinel -- step 4 compares
1237 // (rank, boxIdx) as the winning claim's IDENTITY, not just its key value, precisely so
1238 // that an exact key tie between two INDEPENDENT claims doesn't tell both of them "you
1239 // won" (see step 4's own comment). This must be the real index even for a foreign
1240 // winner, or that identity comparison could never match a foreign claim at all.
1241 bestBoxIdx = c.proposerBoxIdx;
1242 }
1243 }
1244
1245 if (haveWinner) {
1246 nominalWinner.push_back(WinnerEntry{id, Winner{bestRank, bestBoxIdx, bestKey}});
1247 }
1248
1249 idx = idxEnd;
1250 }
1251
1252 // Binary-search lookup into the now-sorted nominalWinner -- replaces the old unordered_map's
1253 // .find()/.at().
1254 auto findWinner = [&nominalWinner](const ParticleID a_id) -> const Winner* {
1255 const auto it = std::lower_bound(nominalWinner.begin(),
1256 nominalWinner.end(),
1257 a_id,
1258 [](const WinnerEntry& a_entry, const ParticleID a_key) {
1259 return a_entry.id < a_key;
1260 });
1261
1262 return (it != nominalWinner.end() && it->id == a_id) ? &it->winner : nullptr;
1263 };
1264
1265 // ==== STEP 4: Phase 2 -- verdicts to proposers, foreign members only ====
1266 // One verdict PER INCOMING CLAIM, not per rank: two of the SAME rank's own boxes can both claim
1267 // this particle (from two different patches that rank owns), and each needs its own, correct
1268 // answer -- a rank-level "your rank won" cannot tell them apart. See KDClaim::proposerBoxIdx.
1269
1270 std::vector<std::vector<KDVerdict>> verdictSendByRank(numRanks);
1271
1272 for (std::size_t idx = 0; idx < incomingClaims.size();) {
1273 const ParticleID id = incomingClaims[idx].memberID;
1274
1275 std::size_t idxEnd = idx + 1;
1276
1277 while (idxEnd < incomingClaims.size() && incomingClaims[idxEnd].memberID == id) {
1278 idxEnd++;
1279 }
1280
1281 const Winner* winner = findWinner(id);
1282
1283 // Every id that shows up in incomingClaims was claimed on one of MY OWN, locally-owned,
1284 // boundary-exposed particles -- STEP 1's classification is exhaustive over exposed members, so
1285 // it always left a (possibly listen-only) selfClaims entry for it, which always yields a
1286 // nominalWinner entry (haveWinner requires only ONE candidate, and the incoming claim itself
1287 // is one). This is a correctness invariant, not a defensive fallback.
1288 CH_assert(winner != nullptr);
1289
1290 for (std::size_t k = idx; k < idxEnd; k++) {
1291 const KDClaim& c = incomingClaims[k];
1292
1293 // Compare IDENTITY (which specific claim was chosen), not key VALUE: an exact key tie
1294 // between two INDEPENDENT claims is real and not vanishingly rare here (BrownianWalker's
1295 // regular initial distribution can make two independently-built boxes end up with the exact
1296 // same volume), and comparing key value alone would tell BOTH tied claimants "you won" --
1297 // exactly the kind of double-count this whole protocol exists to prevent. Step 3 already
1298 // deterministically picked exactly one winner among any tie; only that specific claim,
1299 // identified by (rank, box index), is told so.
1300 const bool won = (c.proposerRank == winner->rank) && (c.proposerBoxIdx == winner->boxIdx);
1301
1302 verdictSendByRank[c.proposerRank].push_back(KDVerdict{id, c.proposerBoxIdx, won});
1303 }
1304
1305 idx = idxEnd;
1306 }
1307
1308 const std::vector<KDVerdict> incomingVerdicts = kdExchangeByRank(verdictSendByRank);
1309
1310 // wonForeignByBox[boxIdx] = every foreign member THAT SPECIFIC one of my own boxes was told it won.
1311 // Indexed directly by boxIdx (a dense range over myBoxes) rather than hashed -- each box's own
1312 // list is bounded by its own membership (at most ppc), so sorting it once and using binary_search
1313 // at the read site below avoids both a per-box hash-table allocation (unordered_set would add one
1314 // per box, of which there can be thousands) and, for large ppc, the O(box size^2) cost a linear
1315 // scan would have across all of a box's foreign-member lookups.
1316
1317 std::vector<std::vector<ParticleID>> wonForeignByBox(myBoxes.size());
1318
1319 for (const KDVerdict& v : incomingVerdicts) {
1320 if (v.won) {
1321 wonForeignByBox[v.claimantBoxIdx].push_back(v.memberID);
1322 }
1323 }
1324
1325 for (std::vector<ParticleID>& won : wonForeignByBox) {
1326 std::sort(won.begin(), won.end());
1327 }
1328
1329 // ==== STEP 5: assemble each of my boxes' provisional membership, commit or not ====
1330 std::vector<std::vector<KDCommit>> commitSendByRank(numRanks);
1331
1332 for (std::size_t boxIdx = 0; boxIdx < myBoxes.size(); boxIdx++) {
1333 const RuntimeBox& box = myBoxes[boxIdx];
1334
1335 std::vector<ParticleID> survivingLocal;
1336 std::vector<ParticleID> survivingForeign;
1337
1338 for (const KDMember& m : box.members) {
1339 if (m.owner == myRank) {
1340 const Winner* winner = findWinner(m.id);
1341
1342 if (winner != nullptr && winner->rank == myRank && winner->boxIdx == static_cast<int>(boxIdx)) {
1343 survivingLocal.push_back(m.id);
1344 }
1345 }
1346 else {
1347 const std::vector<ParticleID>& won = wonForeignByBox[boxIdx];
1348
1349 if (std::binary_search(won.begin(), won.end(), m.id)) {
1350 survivingForeign.push_back(m.id);
1351 }
1352 }
1353 }
1354
1355 const std::size_t totalSurvivors = survivingLocal.size() + survivingForeign.size();
1356
1357 bool committed = false;
1358
1359 if (totalSurvivors >= 2) {
1360 std::vector<Packed> payloads;
1361 std::vector<Real> weights;
1362 RealVect centroid = RealVect::Zero;
1363 Real totalW = 0.0;
1364 RealVect surviveBoxLo, surviveBoxHi;
1365 bool haveSurviveBox = false;
1366
1367 payloads.reserve(totalSurvivors);
1368 weights.reserve(totalSurvivors);
1369
1370 auto accumulate = [&](const ParticleID a_id) {
1371 const MergeParticle<Packed>& p = findParticle(a_id);
1372
1373 payloads.push_back(p.payload);
1374 weights.push_back(p.weight);
1375 centroid += p.weight * p.position;
1376 totalW += p.weight;
1377
1378 if (!haveSurviveBox) {
1379 surviveBoxLo = p.position;
1380 surviveBoxHi = p.position;
1381 haveSurviveBox = true;
1382 }
1383 else {
1384 for (int dir = 0; dir < SpaceDim; dir++) {
1385 surviveBoxLo[dir] = std::min(surviveBoxLo[dir], p.position[dir]);
1386 surviveBoxHi[dir] = std::max(surviveBoxHi[dir], p.position[dir]);
1387 }
1388 }
1389 };
1390
1391 for (const ParticleID id : survivingLocal) {
1392 accumulate(id);
1393 }
1394
1395 for (const ParticleID id : survivingForeign) {
1396 accumulate(id);
1397 }
1398
1399 centroid /= totalW;
1400
1401 kdCheckCentroid(centroid, totalW, surviveBoxLo, surviveBoxHi);
1402
1403 if (a_isPositionValid(centroid)) {
1404 committed = true;
1405
1406 MergeParticle<Packed> merged;
1407
1408 merged.position = centroid;
1409 merged.weight = totalW;
1410 merged.globalID = a_allocateID();
1411 merged.ownerRank = myRank;
1412 merged.payload = a_combine(payloads.data(), weights.data(), payloads.size());
1413
1414 mergedResults.push_back(MergedResult{merged, box.patchIdx});
1415
1416 for (const ParticleID id : survivingLocal) {
1417 // This rank owns these, and the outcome is already known, so nothing need be awaited.
1418 consumedIDs.push_back(id);
1419 }
1420 }
1421 }
1422
1423 // Phase 3 outgoing: every foreign member this box nominally won needs to hear the outcome,
1424 // whether this box committed or not, since its owner is otherwise left not knowing whether to
1425 // delete it.
1426 //
1427 // This round cannot be collapsed into Phase 2. Winning a member's nominal argmin does not mean
1428 // the winning box will actually commit: it can independently lose OTHER members to OTHER,
1429 // unrelated lower-keyed boxes and end up under the merge threshold itself. Deleting on the
1430 // Phase 2 verdict alone then destroys that particle's weight, because no merged particle
1431 // anywhere ends up holding it. Concretely, with three ranks and box keys Kx < Ky < Kz:
1432 //
1433 // Bx{p1(X), p2(Y)}, By{p2(Y), p3(Z)}, Bz{p3(Z), p4(X)}
1434 //
1435 // Argmin gives p1,p2 -> Bx (Bx beats By on p2) and p3 -> By (By beats Bz). Assembling actual
1436 // memberships: Bx = {p1,p2} commits; By has lost p2 to Bx and is left with {p3} alone, so it
1437 // does NOT commit; Bz has lost p3 to By and is left with {p4} alone, so it does not either.
1438 // Z learns from Phase 2 only that By nominally won p3 -- deleting p3 there would lose it
1439 // outright. Phase 3 is what tells Z to release it instead. The same holds for chains of any
1440 // length.
1441 for (const ParticleID id : survivingForeign) {
1442 commitSendByRank[findParticle(id).ownerRank].push_back(KDCommit{id, committed});
1443 }
1444 }
1445
1446 // ==== STEP 6: Phase 3 -- commit/release, foreign members only ====
1447 const std::vector<KDCommit> incomingCommits = kdExchangeByRank(commitSendByRank);
1448
1449 for (const KDCommit& c : incomingCommits) {
1450 if (c.committed) {
1451 consumedIDs.push_back(c.memberID);
1452 }
1453 }
1454
1455 std::sort(consumedIDs.begin(), consumedIDs.end());
1456
1457 // STEP 7 below finds entries by binary search, which silently misses on an unsorted range.
1458 CH_assert(std::is_sorted(consumedIDs.begin(), consumedIDs.end()));
1459
1460 CH_STOP(t_carve);
1461
1462 // ==== STEP 7: remove consumed particles ====
1463 CH_START(t_remove);
1464 for (const PatchWork& pw : patchWork) {
1465 ParticleSoA<P, Traits>& leaf = a_particles[pw.level][pw.din];
1466
1467 std::size_t i = 0;
1468
1469 while (i < leaf.size()) {
1470 if (!leaf.isGhost(i) && std::binary_search(consumedIDs.begin(), consumedIDs.end(), leaf.particleID(i))) {
1471 leaf.remove(i);
1472 }
1473 else {
1474 i++;
1475 }
1476 }
1477 }
1478
1479 a_particles.clearGhostParticles();
1480
1481 CH_STOP(t_remove);
1482
1483 // ==== STEP 8: place merged results ====
1484 CH_START(t_place);
1485 std::vector<std::vector<MergeParticle<Packed>>> scatterByDestRank(numRanks);
1486
1487 auto insertHere = [&](const MergeParticle<Packed>& a_p, const int a_level, const DataIndex& a_din) {
1488 ParticleSoA<P, Traits>& leaf = a_particles[a_level][a_din];
1489
1490 a_scatter(leaf, a_p);
1491 };
1492
1493 for (const MergedResult& mr : mergedResults) {
1494 const PatchWork& pw = patchWork[mr.patchIdx];
1495
1496 RealVect boxRealLo, boxRealHi;
1497
1498 kdBoxRealBounds(boxRealLo, boxRealHi, pw.box, pw.dx, probLo);
1499
1500 bool inOwnBox = true;
1501
1502 for (int dir = 0; dir < SpaceDim && inOwnBox; dir++) {
1503 if (mr.particle.position[dir] < boxRealLo[dir] || mr.particle.position[dir] >= boxRealHi[dir]) {
1504 inOwnBox = false;
1505 }
1506 }
1507
1508 if (inOwnBox) {
1509 insertHere(mr.particle, pw.level, pw.din);
1510
1511 continue;
1512 }
1513
1514 const auto dst = a_particles.findDestination(mr.particle.position);
1515
1516 if (!dst.valid) {
1517 MayDay::Error("ParticleManagement::mergeKDCarve -- merged particle not found in any patch");
1518 }
1519
1520 if (dst.rank == myRank) {
1521 const DataIndex din = a_amr.getLevelTiles(realm)[dst.level]->getMyGrids().at(dst.gridIndex);
1522
1523 insertHere(mr.particle, dst.level, din);
1524 }
1525 else {
1526 MergeParticle<Packed> corrected = mr.particle;
1527 corrected.ownerRank = dst.rank;
1528
1529 scatterByDestRank[dst.rank].push_back(corrected);
1530 }
1531 }
1532
1533 const std::vector<MergeParticle<Packed>> incomingScattered = kdExchangeByRank(scatterByDestRank);
1534
1535 for (const MergeParticle<Packed>& p : incomingScattered) {
1536 const auto dst = a_particles.findDestination(p.position);
1537
1538 if (!dst.valid || dst.rank != myRank) {
1539 MayDay::Error("ParticleManagement::mergeKDCarve -- incoming scattered particle not "
1540 "found in any of this rank's own patches");
1541 }
1542
1543 const DataIndex din = a_amr.getLevelTiles(realm)[dst.level]->getMyGrids().at(dst.gridIndex);
1544
1545 MergeParticle<Packed> corrected = p;
1546 corrected.ownerRank = myRank;
1547
1548 insertHere(corrected, dst.level, din);
1549 }
1550
1551 CH_STOP(t_place);
1552}
1553
1554template <typename P,
1555 typename Packed,
1556 typename Traits,
1557 typename Gather,
1558 typename Combine,
1559 typename Scatter,
1560 typename Allocator,
1561 typename PosValid>
1562inline void
1564 EBAMRFAB& a_cellHistogram,
1565 EBAMRFAB& a_leafQuota,
1566 const AmrMesh& a_amr,
1567 const int a_ppc,
1568 const Real a_splitWeightLeafDx,
1569 const Gather& a_gather,
1570 const Combine& a_combine,
1571 const Scatter& a_scatter,
1572 const Allocator& a_allocateID,
1573 const PosValid& a_isPositionValid)
1574{
1575 // The patch-local merge is the ghost-free case of mergeKDInterior() writing back into its own
1576 // container, so it delegates rather than repeating the body.
1577 //
1578 // mergeKDInterior() commits a leaf only when the leaf holds no ghost. The caller here must not
1579 // have filled a ghost halo -- a ghost would be merged locally while its true owner merges it too --
1580 // so every gathered particle is resident, every leaf is ghost-free, and that test admits them all.
1581 // Passing a_particles as its own destination puts the super-particles back where the patch merge
1582 // has always put them.
1583 mergeKDInterior<P, Packed, Traits>(a_particles,
1584 a_particles,
1585 a_cellHistogram,
1586 a_leafQuota,
1587 a_amr,
1588 a_ppc,
1589 a_splitWeightLeafDx,
1590 a_gather,
1591 a_combine,
1592 a_scatter,
1593 a_allocateID,
1594 a_isPositionValid);
1595}
1596
1597template <typename P,
1598 typename Packed,
1599 typename Traits,
1600 typename Gather,
1601 typename Combine,
1602 typename Scatter,
1603 typename Allocator,
1604 typename PosValid>
1605inline void
1607 ParticleContainer<P, Traits>& a_interior,
1608 EBAMRFAB& a_cellHistogram,
1609 EBAMRFAB& a_leafQuota,
1610 const AmrMesh& a_amr,
1611 const int a_ppc,
1612 const Real a_splitWeightLeafDx,
1613 const Gather& a_gather,
1614 const Combine& a_combine,
1615 const Scatter& a_scatter,
1616 const Allocator& a_allocateID,
1617 const PosValid& a_isPositionValid)
1618{
1619 using namespace detail;
1620
1621 CH_TIMERS("ParticleManagement::mergeKDInterior");
1622 CH_TIMER("ParticleManagement::mergeKDInterior::build", t_build);
1623 CH_TIMER("ParticleManagement::mergeKDInterior::commit", t_commit);
1624 CH_TIMER("ParticleManagement::mergeKDInterior::remove_place", t_place);
1625
1626 const std::string realm = a_particles.getRealm();
1627 const int finestLevel = a_amr.getFinestLevel();
1628 const RealVect probLo = a_amr.getProbLo();
1629
1630 // A leaf committed from patch (lvl,din) is written to the SAME patch of a_interior, which is only
1631 // meaningful when the two containers share a layout.
1632 CH_assert(a_interior.getRealm() == realm);
1633
1634 // Caller-owned per-cell scratch; see mergeKDCarve() for why the holders are not allocated here.
1635 EBAMRFAB& histogram = a_cellHistogram;
1636 EBAMRFAB& leafQuota = a_leafQuota;
1637
1638 CH_assert(histogram[0]->nComp() == 1);
1639 CH_assert(leafQuota[0]->nComp() == 1);
1640 CH_assert(histogram[0]->ghostVect() >= IntVect::Unit);
1641 CH_assert(leafQuota[0]->ghostVect() >= IntVect::Unit);
1642
1643 const int myRank = procID();
1644
1645 // Per-patch leaf scratch, declared once and reused across every patch below rather than reallocated
1646 // inside the loop -- the same reuse pattern as the histogram/leafQuota holders above.
1647 // buildKDQuotaLeaves() clear()s it on entry, so each reuse retains the heap capacity grown by
1648 // earlier patches instead of starting from nothing.
1649 std::vector<KDLeaf> leaves;
1650
1651 for (int lvl = 0; lvl <= finestLevel; lvl++) {
1652 const DisjointBoxLayout& dbl = a_amr.getGrids(realm)[lvl];
1653 const DataIterator& dit = dbl.dataIterator();
1654
1655 const RealVect dx = a_amr.getDx()[lvl] * RealVect::Unit;
1656
1657 const int nbox = dit.size();
1658
1659 // Serial (no omp): the leaves scratch is shared across patches by design, consumedIDs is appended
1660 // to from every patch, and a_allocateID() hands out ids from one counter.
1661 for (int mybox = 0; mybox < nbox; mybox++) {
1662 const DataIndex& din = dit[mybox];
1663
1664 ParticleSoA<P, Traits>& leaf = a_particles[lvl][din];
1665
1666 // Ghosts ARE gathered, unlike mergeKDPatch(). They are never merged here, but they must take
1667 // part in the partition: a ghost is what marks the leaf it lands in as contested, and the
1668 // per-cell quota has to see the cell's true occupancy to hand out the right number of slots.
1669 std::vector<MergeParticle<Packed>> combined;
1670 combined.reserve(leaf.size());
1671
1672 for (std::size_t i = 0; i < leaf.size(); i++) {
1674
1675 p.position = leaf.position(i);
1676 p.weight = leaf.weight(i);
1677 p.globalID = leaf.particleID(i);
1678 p.ownerRank = leaf.rankID(i);
1679 p.isGhost = leaf.isGhost(i);
1680 p.payload = a_gather(leaf, i);
1681
1682 combined.push_back(p);
1683 }
1684
1685 if (combined.empty()) {
1686 continue;
1687 }
1688
1689 CH_START(t_build);
1690
1691 // Ground truth for how crowded each cell really is -- see kdFillCellHistogram(). Both this and
1692 // the live quota are cell data, so they live in the mesh holders allocated once above rather
1693 // than in per-patch scratch. Their ghost cell covers the gathered ghosts, which sit up to one
1694 // cell outside this patch, and any leaf centroid that lands just outside it.
1695 FArrayBox& cellCounts = (*histogram[lvl])[din];
1696 FArrayBox& quota = (*leafQuota[lvl])[din];
1697
1698 kdFillCellHistogram(cellCounts, combined, probLo, dx);
1699
1700 buildKDQuotaLeaves(combined, quota, leaves, a_ppc, a_splitWeightLeafDx, dx, probLo, cellCounts);
1701
1702 CH_STOP(t_build);
1703 CH_START(t_commit);
1704
1705 std::vector<ParticleID> consumedIDs;
1706 std::vector<MergeParticle<Packed>> mergedResults;
1707
1708 for (const KDLeaf& bl : leaves) {
1709 if (bl.hi - bl.lo < 2) {
1710 continue;
1711 }
1712
1713 // Cheap geometric test before the O(members) ghost scan below.
1714 if (kdMaxAxisSpan(bl.boxLo, bl.boxHi, dx) > s_kdMaxLeafExtent) {
1715 continue;
1716 }
1717
1718 // THE rule for this tier: commit iff the leaf holds no ghost. Every member is then a
1719 // particle physically resident in this patch, so no other patch can commit any of them --
1720 // a patch that draws one of them into a leaf of its own necessarily sees it as a ghost, and
1721 // that leaf is disqualified here by this very test. Committed leaves therefore never share
1722 // a member across patches, and no weight is counted twice. Boundary exposure is deliberately
1723 // NOT consulted: an exposed leaf that happens to hold no ghost is uncontested and merges
1724 // here, which is exactly what shrinks the skin relative to the carve.
1725 bool anyGhost = false;
1726
1727 for (std::size_t idx = bl.lo; idx < bl.hi && !anyGhost; idx++) {
1728 anyGhost = combined[idx].isGhost;
1729 }
1730
1731 if (anyGhost) {
1732 continue;
1733 }
1734
1735 std::vector<Packed> payloads;
1736 std::vector<Real> weights;
1737 RealVect centroid = RealVect::Zero;
1738 Real totalW = 0.0;
1739
1740 payloads.reserve(bl.hi - bl.lo);
1741 weights.reserve(bl.hi - bl.lo);
1742
1743 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
1744 const MergeParticle<Packed>& p = combined[idx];
1745
1746 // Guaranteed by the anyGhost test above; merging a ghost here would double-count its
1747 // weight against its true owner.
1748 CH_assert(!p.isGhost);
1749
1750 payloads.push_back(p.payload);
1751 weights.push_back(p.weight);
1752 centroid += p.weight * p.position;
1753 totalW += p.weight;
1754 }
1755
1756 centroid /= totalW;
1757
1758 kdCheckCentroid(centroid, totalW, bl.boxLo, bl.boxHi);
1759
1760 if (!a_isPositionValid(centroid)) {
1761 continue;
1762 }
1763
1764 MergeParticle<Packed> merged;
1765
1766 merged.position = centroid;
1767 merged.weight = totalW;
1768 merged.globalID = a_allocateID();
1769 merged.ownerRank = myRank;
1770 merged.isGhost = false;
1771 merged.payload = a_combine(payloads.data(), weights.data(), payloads.size());
1772
1773 mergedResults.push_back(merged);
1774
1775 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
1776 consumedIDs.push_back(combined[idx].globalID);
1777 }
1778 }
1779
1780 CH_STOP(t_commit);
1781
1782 if (mergedResults.empty()) {
1783 continue;
1784 }
1785
1786 CH_START(t_place);
1787
1788 // Consumed originals leave a_particles; the super-particles they became are written to
1789 // a_interior instead of back here. What is left behind in a_particles is precisely the skin.
1790 std::sort(consumedIDs.begin(), consumedIDs.end());
1791
1792 // Removal below finds entries by binary search, which silently misses on an unsorted range.
1793 CH_assert(std::is_sorted(consumedIDs.begin(), consumedIDs.end()));
1794
1795 {
1796 std::size_t i = 0;
1797
1798 while (i < leaf.size()) {
1799 if (!leaf.isGhost(i) && std::binary_search(consumedIDs.begin(), consumedIDs.end(), leaf.particleID(i))) {
1800 leaf.remove(i);
1801 }
1802 else {
1803 i++;
1804 }
1805 }
1806 }
1807
1808 RealVect boxRealLo, boxRealHi;
1809
1810 kdBoxRealBounds(boxRealLo, boxRealHi, dbl[din], dx, probLo);
1811
1812 for (const MergeParticle<Packed>& mr : mergedResults) {
1813 bool inOwnBox = true;
1814
1815 for (int dir = 0; dir < SpaceDim && inOwnBox; dir++) {
1816 if (mr.position[dir] < boxRealLo[dir] || mr.position[dir] >= boxRealHi[dir]) {
1817 inOwnBox = false;
1818 }
1819 }
1820
1821 if (inOwnBox) {
1822 a_scatter(a_interior[lvl][din], mr);
1823
1824 continue;
1825 }
1826
1827 // Every member was resident in this patch, so their weighted centroid is too -- this branch
1828 // is unreachable in exact arithmetic and exists only for round-off at a patch face. It must
1829 // still land on this rank: there is no exchange here to carry a particle anywhere else.
1830 const auto dst = a_particles.findDestination(mr.position);
1831
1832 if (!dst.valid || dst.rank != myRank) {
1833 MayDay::Error("ParticleManagement::mergeKDInterior -- merged particle left this rank's own patches");
1834 }
1835
1836 const DataIndex dstDin = a_amr.getLevelTiles(realm)[dst.level]->getMyGrids().at(dst.gridIndex);
1837
1838 a_scatter(a_interior[dst.level][dstDin], mr);
1839 }
1840
1841 CH_STOP(t_place);
1842 }
1843 }
1844}
1845
1846} // namespace ParticleManagement
1847
1848#include <CD_NamespaceFooter.H>
1849
1850#endif
Declaration of distributed, MPI-safe whole-patch kd-tree super-particle merge algorithms.
Declaration of LevelTiles.
std::int32_t RankID
Owning-rank identifier type (container-owned metadata column; fixed-width for I/O).
Definition CD_ParticleSoA.H:166
std::int64_t ParticleID
Global particle identifier type (container-owned metadata column; fixed-width for I/O).
Definition CD_ParticleSoA.H:161
Vector< RefCountedPtr< LevelData< BaseFab< bool > > > > AMRMask
Alias for cutting down on the typic of booleans defined over AMR grids.
Definition CD_Realm.H:33
Class for handling spatial operations.
Definition CD_AmrMesh.H:45
const Vector< RefCountedPtr< LevelTiles > > & getLevelTiles(const std::string &a_realm) const
Get the tiled space representation.
Definition CD_AmrMesh.cpp:3439
const Vector< DisjointBoxLayout > & getGrids(const std::string &a_realm) const
Get the grids.
Definition CD_AmrMesh.cpp:3271
const AMRMask & getParticleGhostExposure(const std::string &a_realm, const int a_width) const
Get the particle boundary-exposure mask on a realm for a registered width.
Definition CD_AmrMesh.cpp:3534
RealVect getProbLo() const
Get lower-left corner of computational domain.
Definition CD_AmrMesh.cpp:3042
const Vector< Real > & getDx() const
Get spatial resolutions.
Definition CD_AmrMesh.cpp:3215
int getFinestLevel() const
Get finest grid level.
Definition CD_AmrMesh.cpp:3064
AMR-hierarchy container of computational particles, stored per patch in Struct-of-Arrays form.
Definition CD_ParticleContainer.H:123
void clearGhostParticles()
Remove every ghost particle (any non-Valid GhostType) from all levels and patches.
Definition CD_ParticleContainerImplem.H:598
std::string getRealm() const
Realm label.
Definition CD_ParticleContainer.H:300
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
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
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
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
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
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
Real kdMaxAxisSpan(const RealVect &a_boxLo, const RealVect &a_boxHi, const RealVect &a_dx) noexcept
Largest per-axis extent of [a_boxLo,a_boxHi], expressed as a fraction of that axis's own cell width.
Definition CD_KDParticleMergeImplem.H:169
void kdCheckCentroid(const RealVect &a_centroid, const Real a_totalWeight, const RealVect &a_boxLo, const RealVect &a_boxHi) noexcept
Debug-only sanity check on a merged particle's position: a weighted centroid of positions that all li...
Definition CD_KDParticleMergeImplem.H:618
void kdBBox(RealVect &a_boxLo, RealVect &a_boxHi, const std::vector< MergeParticle< Packed > > &a_particles, const std::size_t a_lo, const std::size_t a_hi) noexcept
Axis-aligned bounding box of particles[a_lo, a_hi).
Definition CD_KDParticleMergeImplem.H:123
std::size_t kdSplitCountMedian(std::vector< MergeParticle< Packed > > &a_particles, const std::size_t a_lo, const std::size_t a_hi, const int a_axis) noexcept
Split particles[a_lo,a_hi) in place by the longest axis at the count-median – the sole split rule use...
Definition CD_KDParticleMergeImplem.H:279
bool kdIntVectLess(const IntVect &a_lhs, const IntVect &a_rhs) noexcept
Strict weak ordering over IntVect, lexicographic component-wise.
Definition CD_KDParticleMergeImplem.H:210
std::vector< T > kdExchangeByRank(const std::vector< std::vector< T > > &a_sendByRank)
Generic Alltoallv-style exchange for a trivially-copyable record type: send a per-destination-rank bu...
Definition CD_KDParticleMergeImplem.H:42
void kdBoxRealBounds(RealVect &a_boxLo, RealVect &a_boxHi, const Box &a_box, const RealVect &a_dx, const RealVect &a_probLo) noexcept
Real-space bounds of a box: the half-open region [lo, hi) that its cells cover.
Definition CD_KDParticleMergeImplem.H:591
void buildKDQuotaLeaves(std::vector< MergeParticle< Packed > > &a_particles, FArrayBox &a_used, std::vector< KDLeaf > &a_leaves, const int a_ppc, const Real a_splitWeightLeafDx, const RealVect &a_dx, const RealVect &a_probLo, const FArrayBox &a_cellCounts) noexcept
Build one whole-patch kd tree: partition a_particles by position into leaves, each of which becomes e...
Definition CD_KDParticleMergeImplem.H:368
IntVect kdCellKeyOf(const RealVect &a_position, const RealVect &a_probLo, const RealVect &a_dx) noexcept
The unclamped, position-derived cell index a physical position falls in.
Definition CD_KDParticleMergeImplem.H:190
void kdFillCellHistogram(FArrayBox &a_counts, const std::vector< MergeParticle< Packed > > &a_particles, const RealVect &a_probLo, const RealVect &a_dx) noexcept
Tally a per-cell particle-count histogram over one patch's gathered particles.
Definition CD_KDParticleMergeImplem.H:243
std::size_t kdSplitWeightMedian(std::vector< MergeParticle< Packed > > &a_particles, const std::size_t a_lo, const std::size_t a_hi, const int a_axis) noexcept
Split particles[a_lo,a_hi) in place by the longest axis at the WEIGHT median – the plane with half th...
Definition CD_KDParticleMergeImplem.H:324
Namespace for various particle management tools.
Definition CD_KDParticleMerge.H:33
void mergeKDInterior(ParticleContainer< P, Traits > &a_particles, ParticleContainer< P, Traits > &a_interior, EBAMRFAB &a_cellHistogram, EBAMRFAB &a_leafQuota, const AmrMesh &a_amr, const int a_ppc, const Real a_splitWeightLeafDx, const Gather &a_gather, const Combine &a_combine, const Scatter &a_scatter, const Allocator &a_allocateID, const PosValid &a_isPositionValid)
Run the uncontested tier of the kd merge, splitting the input into merged and leftover.
Definition CD_KDParticleMergeImplem.H:1606
void mergeKDPatch(ParticleContainer< P, Traits > &a_particles, EBAMRFAB &a_cellHistogram, EBAMRFAB &a_leafQuota, const AmrMesh &a_amr, const int a_ppc, const Real a_splitWeightLeafDx, const Gather &a_gather, const Combine &a_combine, const Scatter &a_scatter, const Allocator &a_allocateID, const PosValid &a_isPositionValid)
Run one patch-local kd-tree merge over every patch this rank owns.
Definition CD_KDParticleMergeImplem.H:1563
void mergeKDCarve(ParticleContainer< P, Traits > &a_particles, EBAMRFAB &a_cellHistogram, EBAMRFAB &a_leafQuota, const AmrMesh &a_amr, const int a_ppc, const Real a_splitWeightLeafDx, const Gather &a_gather, const Combine &a_combine, const Scatter &a_scatter, const Allocator &a_allocateID, const PosValid &a_isPositionValid)
Run one non-iterative pass of the kd-tree carve merge over every patch this rank owns.
Definition CD_KDParticleMergeImplem.H:651
Minimal, payload-agnostic description of one particle as input to a distributed merge.
Definition CD_ParticleManagement.H:430
bool isGhost
True iff this is a ghost copy in the patch currently holding it, i.e. not physically stored there....
Definition CD_ParticleManagement.H:462
RealVect position
Particle position.
Definition CD_ParticleManagement.H:434
Packed payload
Opaque, caller-defined payload. Never inspected by the merge logic – only carried through to the comb...
Definition CD_ParticleManagement.H:474
Real weight
Particle weight. Not required to be an integer.
Definition CD_ParticleManagement.H:439
int level
The AMR level this particle lives on. Used by the nearest-neighbor mergers, whose cell keys are level...
Definition CD_ParticleManagement.H:468
ParticleID globalID
Globally unique particle id, unique within one merge round across every rank.
Definition CD_ParticleManagement.H:448
RankID ownerRank
The rank owning this particle. Always read from the particle's own data, never inferred from MPI tran...
Definition CD_ParticleManagement.H:454
One leaf of a kd tree: a contiguous index range into the (in-place reordered) particle buffer,...
Definition CD_KDParticleMerge.H:68