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_Random.H>
33#include <CD_LevelTiles.H>
34#include <CD_KDParticleMerge.H>
35#include <CD_NamespaceHeader.H>
36
37namespace ParticleManagement {
38
39namespace detail {
40
41template <typename T>
42inline std::vector<T>
43kdExchangeByRank(const std::vector<std::vector<T>>& a_sendByRank)
44{
45 const int numRanks = static_cast<int>(a_sendByRank.size());
46
47#ifdef CH_MPI
48 if (numRanks <= 1) {
49 return a_sendByRank.empty() ? std::vector<T>() : a_sendByRank[0];
50 }
51
52 std::vector<int> sendCounts(numRanks, 0);
53
54 for (int r = 0; r < numRanks; r++) {
55 sendCounts[r] = static_cast<int>(a_sendByRank[r].size());
56 }
57
58 std::vector<int> recvCounts(numRanks, 0);
59 MPI_Alltoall(sendCounts.data(), 1, MPI_INT, recvCounts.data(), 1, MPI_INT, Chombo_MPI::comm);
60
61 std::vector<int> sdispl(numRanks, 0);
62 std::vector<int> rdispl(numRanks, 0);
63
64 long stot = 0;
65 long rtot = 0;
66
67 for (int r = 0; r < numRanks; r++) {
68 sdispl[r] = static_cast<int>(stot);
69 stot += sendCounts[r];
70
71 rdispl[r] = static_cast<int>(rtot);
72 rtot += recvCounts[r];
73 }
74
75 std::vector<T> sflat(stot);
76
77 for (int r = 0; r < numRanks; r++) {
78 if (sendCounts[r] > 0) {
79 std::copy(a_sendByRank[r].begin(), a_sendByRank[r].end(), sflat.begin() + sdispl[r]);
80 }
81 }
82
83 std::vector<T> rflat(rtot);
84
85 // Scale record counts/displacements up to bytes -- MPI only knows bytes.
86 std::vector<int> sendBytes(numRanks);
87 std::vector<int> recvBytes(numRanks);
88 std::vector<int> sdisplBytes(numRanks);
89 std::vector<int> rdisplBytes(numRanks);
90
91 for (int r = 0; r < numRanks; r++) {
92 sendBytes[r] = sendCounts[r] * static_cast<int>(sizeof(T));
93 recvBytes[r] = recvCounts[r] * static_cast<int>(sizeof(T));
94 sdisplBytes[r] = sdispl[r] * static_cast<int>(sizeof(T));
95 rdisplBytes[r] = rdispl[r] * static_cast<int>(sizeof(T));
96 }
97
98 MPI_Alltoallv(sflat.data(),
99 sendBytes.data(),
100 sdisplBytes.data(),
101 MPI_BYTE,
102 rflat.data(),
103 recvBytes.data(),
104 rdisplBytes.data(),
105 MPI_BYTE,
106 Chombo_MPI::comm);
107
108 return rflat;
109#else
110 return a_sendByRank.empty() ? std::vector<T>() : a_sendByRank[0];
111#endif
112}
113
122template <typename Packed>
123inline void
124kdBBox(RealVect& a_boxLo,
125 RealVect& a_boxHi,
126 const std::vector<MergeParticle<Packed>>& a_particles,
127 const std::size_t a_lo,
128 const std::size_t a_hi) noexcept
129{
130 CH_assert(a_hi > a_lo);
131 CH_assert(a_hi <= a_particles.size());
132
133 a_boxLo = a_particles[a_lo].position;
134 a_boxHi = a_particles[a_lo].position;
135
136 for (std::size_t i = a_lo + 1; i < a_hi; i++) {
137 const RealVect& pos = a_particles[i].position;
138
139 for (int dir = 0; dir < SpaceDim; dir++) {
140 a_boxLo[dir] = std::min(a_boxLo[dir], pos[dir]);
141 a_boxHi[dir] = std::max(a_boxHi[dir], pos[dir]);
142 }
143 }
144}
145
152static constexpr Real s_kdMaxLeafExtent = 1.0;
153
169inline Real
170kdMaxAxisSpan(const RealVect& a_boxLo, const RealVect& a_boxHi, const RealVect& a_dx) noexcept
171{
172 Real maxAxisSpan = 0.0;
173
174 for (int dir = 0; dir < SpaceDim; dir++) {
175 maxAxisSpan = std::max(maxAxisSpan, (a_boxHi[dir] - a_boxLo[dir]) / a_dx[dir]);
176 }
177
178 return maxAxisSpan;
179}
180
190inline IntVect
191kdCellKeyOf(const RealVect& a_position, const RealVect& a_probLo, const RealVect& a_dx) noexcept
192{
193 IntVect iv;
194
195 for (int dir = 0; dir < SpaceDim; dir++) {
196 iv[dir] = static_cast<int>(std::floor((a_position[dir] - a_probLo[dir]) / a_dx[dir]));
197 }
198
199 return iv;
200}
201
210inline bool
211kdIntVectLess(const IntVect& a_lhs, const IntVect& a_rhs) noexcept
212{
213 for (int dir = 0; dir < SpaceDim; dir++) {
214 if (a_lhs[dir] != a_rhs[dir]) {
215 return a_lhs[dir] < a_rhs[dir];
216 }
217 }
218
219 return false;
220}
221
242template <typename Packed>
243inline void
244kdFillCellHistogram(FArrayBox& a_counts,
245 const std::vector<MergeParticle<Packed>>& a_particles,
246 const RealVect& a_probLo,
247 const RealVect& a_dx) noexcept
248{
249 a_counts.setVal(0);
250
251 for (const MergeParticle<Packed>& p : a_particles) {
252 const IntVect cell = kdCellKeyOf(p.position, a_probLo, a_dx);
253
254 // Every gathered particle is resident in this patch or a ghost within one cell of it, so its
255 // cell lies inside a_counts' box. The previous map tolerated any key silently; a cell-indexed
256 // holder does not, which is the point -- the invariant is now checked instead of assumed.
257 CH_assert(a_counts.box().contains(cell));
258
259 a_counts(cell, 0)++;
260 }
261}
262
295template <typename Packed, typename PosValid>
296inline void
297kdCapCellWeights(std::vector<MergeParticle<Packed>>& a_particles,
298 const FArrayBox& a_cellCounts,
299 const FArrayBox& a_cellWeights,
300 const int a_ppc,
301 const KDSplitPlacement a_splitPlacement,
302 const RealVect& a_probLo,
303 const RealVect& a_dx,
304 const PosValid& a_isPositionValid) noexcept
305{
306 if (a_ppc < 1) {
307 return;
308 }
309
310 const Real spacing = a_dx[0] / std::pow(static_cast<Real>(a_ppc), 1.0 / SpaceDim);
311
312 const std::size_t numOriginal = a_particles.size();
313
314 for (std::size_t i = 0; i < numOriginal; i++) {
315 if (a_particles[i].isGhost) {
316 continue;
317 }
318
319 const IntVect cell = kdCellKeyOf(a_particles[i].position, a_probLo, a_dx);
320
321 if (!a_cellCounts.box().contains(cell) || static_cast<int>(a_cellCounts(cell, 0)) <= a_ppc) {
322 continue;
323 }
324
325 const long long cap = std::max(
326 1LL,
327 static_cast<long long>(std::ceil(a_cellWeights(cell, 0) / static_cast<Real>(a_ppc))));
328 const long long w = static_cast<long long>(std::llround(a_particles[i].weight));
329
330 if (w <= cap) {
331 continue;
332 }
333
334 const long long numPieces = (w + cap - 1) / cap;
335 const std::vector<long long> pieces = ParticleManagement::partitionParticleWeights<long long>(w, numPieces);
336
337 if (pieces.empty()) {
338 continue;
339 }
340
341 RealVect cellLo;
342 for (int dir = 0; dir < SpaceDim; dir++) {
343 cellLo[dir] = a_probLo[dir] + cell[dir] * a_dx[dir];
344 }
345
346 const MergeParticle<Packed> parent = a_particles[i];
347
348 a_particles[i].weight = static_cast<Real>(pieces[0]);
349
350 for (std::size_t k = 1; k < pieces.size(); k++) {
351 MergeParticle<Packed> piece = parent;
352
353 piece.weight = static_cast<Real>(pieces[k]);
354
355 const RealVect displaced = ParticleManagement::detail::kdSplitDisplace(a_splitPlacement,
356 parent.position,
357 cellLo,
358 a_dx,
359 spacing);
360
361 piece.position = a_isPositionValid(displaced) ? displaced : parent.position;
362
363 a_particles.push_back(piece);
364 }
365 }
366}
367
383template <typename Packed>
384inline std::size_t
386 const std::size_t a_lo,
387 const std::size_t a_hi,
388 const int a_axis) noexcept
389{
390 CH_assert(a_hi - a_lo >= 2);
391
392 const std::size_t mid = a_lo + (a_hi - a_lo) / 2;
393
394 std::nth_element(a_particles.begin() + a_lo,
395 a_particles.begin() + mid,
396 a_particles.begin() + a_hi,
397 [a_axis](const MergeParticle<Packed>& a_p1, const MergeParticle<Packed>& a_p2) noexcept -> bool {
398 return a_p1.position[a_axis] < a_p2.position[a_axis];
399 });
400
401 return mid;
402}
403
428template <typename Packed>
429inline std::size_t
431 const std::size_t a_lo,
432 const std::size_t a_hi,
433 const int a_axis) noexcept
434{
435 CH_assert(a_hi - a_lo >= 2);
436
437 std::sort(a_particles.begin() + a_lo,
438 a_particles.begin() + a_hi,
439 [a_axis](const MergeParticle<Packed>& a_p1, const MergeParticle<Packed>& a_p2) noexcept -> bool {
440 return a_p1.position[a_axis] < a_p2.position[a_axis];
441 });
442
443 Real totalWeight = 0.0;
444
445 for (std::size_t idx = a_lo; idx < a_hi; idx++) {
446 totalWeight += a_particles[idx].weight;
447 }
448
449 const Real half = 0.5 * totalWeight;
450
451 Real acc = 0.0;
452 std::size_t mid = a_lo + 1;
453
454 for (std::size_t idx = a_lo; idx < a_hi; idx++) {
455 acc += a_particles[idx].weight;
456
457 if (acc >= half) {
458 mid = idx + 1;
459
460 break;
461 }
462 }
463
464 const std::size_t clamped = std::max(a_lo + 1, std::min(a_hi - 1, mid));
465
466 // Both children must be non-empty or the caller's recursion cannot terminate.
467 CH_assert(clamped > a_lo && clamped < a_hi);
468
469 return clamped;
470}
471
472template <typename Packed>
473inline void
475 FArrayBox& a_used,
476 std::vector<KDLeaf>& a_leaves,
477 const int a_ppc,
478 const Real a_weightMedianCellWidths,
479 const RealVect& a_dx,
480 const RealVect& a_probLo,
481 const FArrayBox& a_cellCounts) noexcept
482{
483 CH_TIME("ParticleManagement::buildKDQuotaLeaves");
484
485 a_leaves.clear();
486
487 if (a_particles.empty() || a_ppc <= 0) {
488 return;
489 }
490
491 // Partition into the particles that may be merged (those in TRUE-crowded cells) and those that
492 // may not, moving the mergeable ones to the front so every leaf below is still a contiguous range.
493 // Particles in uncrowded cells are emitted as singleton leaves: the merge threshold is 2 members,
494 // so a singleton is never merged, and an uncrowded cell can therefore never be drained below
495 // target by a neighbour's merge.
496 const auto firstUnmergeable = std::stable_partition(a_particles.begin(),
497 a_particles.end(),
498 [&](const MergeParticle<Packed>& a_p) {
499 const IntVect cell = kdCellKeyOf(a_p.position, a_probLo, a_dx);
500
501 // A cell outside the histogram box reads as 0
502 // (empty) and is therefore never crowded.
503 return a_cellCounts.box().contains(cell)
504 ? static_cast<int>(a_cellCounts(cell, 0)) > a_ppc
505 : false;
506 });
507
508 const std::size_t numMergeable = static_cast<std::size_t>(firstUnmergeable - a_particles.begin());
509
510 for (std::size_t idx = numMergeable; idx < a_particles.size(); idx++) {
511 RealVect boxLo, boxHi;
512 kdBBox(boxLo, boxHi, a_particles, idx, idx + 1);
513
514 a_leaves.push_back(KDLeaf{idx, idx + 1, boxLo, boxHi});
515 }
516
517 if (numMergeable == 0) {
518 return;
519 }
520
521 // LIVE PER-CELL LEAF BUDGET, enforced as a hard constraint at split time rather than computed
522 // per node and hoped for afterwards.
523 //
524 // Every leaf becomes exactly ONE super-particle at its own weighted centroid, so the number of
525 // leaves whose centroid falls in a cell is that cell's post-merge population. Track that count
526 // live and refuse any split that would push a cell past a_ppc. The per-cell ceiling is a shared
527 // resource and cannot be expressed by any test a node applies to itself alone.
528 //
529 // Splits are handed out heaviest-first: the quota is claimed first-come-first-served, so a
530 // lighter ordering lets light leaves take a cell's slots before a heavy leaf is considered, and
531 // the heavy one is then refused and retired whole as a single very heavy super-particle.
532 // Returns both the centroid's cell AND the range's total weight. The weight sum is a byproduct of
533 // the centroid accumulation, so returning it here removes the separate per-child rangeWeight() pass
534 // the heap loop used to make (same summation order, so the Real is bit-identical).
535 auto centroidCellOf = [&](const std::size_t a_lo, const std::size_t a_hi) -> std::pair<IntVect, Real> {
536 RealVect centroid = RealVect::Zero;
537 Real totalW = 0.0;
538
539 for (std::size_t idx = a_lo; idx < a_hi; idx++) {
540 centroid += a_particles[idx].weight * a_particles[idx].position;
541 totalW += a_particles[idx].weight;
542 }
543
544 if (totalW > 0.0) {
545 centroid /= totalW;
546 }
547 else {
548 centroid = a_particles[a_lo].position;
549 }
550
551 return {kdCellKeyOf(centroid, a_probLo, a_dx), totalW};
552 };
553
554 struct KDCand
555 {
556 Real weight;
557 std::size_t count;
558 std::size_t lo;
559 std::size_t hi;
560 IntVect cell;
561 };
562
563 // Hand out each cell's finite quota to its HEAVIEST candidate leaf first, not its most populous.
564 // The quota is a scarce resource claimed first-come-first-served, so ordering by count lets light
565 // leaves consume a cell's slots before a heavy leaf is ever considered -- the heavy one is then
566 // refused and retired whole, becoming a single very heavy super-particle sitting next to the light
567 // ones that took its slots. Ordering by weight makes the leaves that most need subdividing claim
568 // slots first.
569 const auto byWeight = [](const KDCand& a_lhs, const KDCand& a_rhs) noexcept -> bool {
570 return a_lhs.weight < a_rhs.weight;
571 };
572
573 // Live per-cell leaf count: how many leaves are currently centred in each cell. Caller-owned mesh
574 // data, zeroed here; its ghost cell must cover every cell a leaf centroid can fall in, since
575 // gathered ghosts sit up to one cell outside this patch.
576 FArrayBox& used = a_used;
577 used.setVal(0.0);
578
579 // Reserved to numMergeable, a safe upper bound for both: leaves partition [0,numMergeable) with at
580 // least one member each, so #leaves <= numMergeable, and the split frontier can never exceed the
581 // leaf count. One allocation apiece instead of the push_back reallocation ladder, every patch.
582 std::vector<KDCand> heap;
583 std::vector<KDLeaf> finalLeaves;
584
585 heap.reserve(numMergeable);
586 finalLeaves.reserve(numMergeable);
587
588 {
589 const auto [rootCell, rootWeight] = centroidCellOf(0, numMergeable);
590
591 // A centroid of positions gathered for this patch cannot fall outside the patch grown by the
592 // ghost width. Asserted rather than assumed: the map this replaced would have accepted any cell
593 // silently, which is exactly the class of bug a bounded holder is meant to surface.
594 CH_assert(used.box().contains(rootCell));
595
596 used(rootCell, 0)++;
597 heap.push_back(KDCand{rootWeight, numMergeable, 0, numMergeable, rootCell});
598 }
599
600 while (!heap.empty()) {
601 std::pop_heap(heap.begin(), heap.end(), byWeight);
602
603 const KDCand node = heap.back();
604 heap.pop_back();
605
606 if (node.count < 2) {
607 RealVect boxLo, boxHi;
608 kdBBox(boxLo, boxHi, a_particles, node.lo, node.hi);
609
610 finalLeaves.push_back(KDLeaf{node.lo, node.hi, boxLo, boxHi});
611
612 continue;
613 }
614
615 // One bounding box per node, computed once here and reused three ways: the span test, the split
616 // axis (formerly re-derived from a second box inside each split helper), and -- if the split is
617 // refused below -- the emitted leaf (formerly re-derived a third time in the emission pass). A
618 // split only reorders within [lo,hi), so the box is unchanged by it either way.
619 RealVect nodeBoxLo, nodeBoxHi;
620 kdBBox(nodeBoxLo, nodeBoxHi, a_particles, node.lo, node.hi);
621
622 const Real nodeSpan = kdMaxAxisSpan(nodeBoxLo, nodeBoxHi, a_dx);
623 const int splitAxis = (nodeBoxHi - nodeBoxLo).maxDir(true);
624
625 // While a node spans several cells a split apportions leaves BETWEEN cells, which is a question
626 // of counts; once it is narrow the weight median is what drives the super-particles toward equal
627 // weight. This is an extent test, not a "both corners in one cell" test, so a narrow node
628 // straddling a face still qualifies.
629 const std::size_t mid = (nodeSpan <= a_weightMedianCellWidths)
630 ? kdSplitWeightMedian(a_particles, node.lo, node.hi, splitAxis)
631 : kdSplitCountMedian(a_particles, node.lo, node.hi, splitAxis);
632
633 const auto [cellLeft, weightLeft] = centroidCellOf(node.lo, mid);
634 const auto [cellRight, weightRight] = centroidCellOf(mid, node.hi);
635
636 CH_assert(used.box().contains(cellLeft));
637 CH_assert(used.box().contains(cellRight));
638
639 used(node.cell, 0)--;
640 used(cellLeft, 0)++;
641 used(cellRight, 0)++;
642
643 // The quota constrains only leaves that can actually merge. A node wider than
644 // s_kdMaxLeafExtent is vetoed by the caller and leaves every member behind
645 // individually, so it costs its cell its full member count rather than one particle; splitting
646 // it further lowers that cell's final population even though it raises the leaf count.
647 if (nodeSpan <= s_kdMaxLeafExtent && (used(cellLeft, 0) > a_ppc || used(cellRight, 0) > a_ppc)) {
648 used(cellLeft, 0)--;
649 used(cellRight, 0)--;
650 used(node.cell, 0)++;
651
652 finalLeaves.push_back(KDLeaf{node.lo, node.hi, nodeBoxLo, nodeBoxHi});
653
654 continue;
655 }
656
657 heap.push_back(KDCand{weightLeft, mid - node.lo, node.lo, mid, cellLeft});
658 std::push_heap(heap.begin(), heap.end(), byWeight);
659
660 heap.push_back(KDCand{weightRight, node.hi - mid, mid, node.hi, cellRight});
661 std::push_heap(heap.begin(), heap.end(), byWeight);
662 }
663
664 for (const KDLeaf& leaf : finalLeaves) {
665 a_leaves.push_back(leaf);
666 }
667
668 // The leaves must partition the input exactly: every particle in exactly one leaf, no overlap and
669 // nothing dropped. Callers rely on this both for the merge itself and for the invariant that
670 // total weight is conserved.
671#ifndef NDEBUG
672 std::size_t coveredMembers = 0;
673
674 for (const KDLeaf& bl : a_leaves) {
675 CH_assert(bl.hi > bl.lo);
676 CH_assert(bl.hi <= a_particles.size());
677
678 coveredMembers += bl.hi - bl.lo;
679 }
680
681 CH_assert(coveredMembers == a_particles.size());
682#endif
683}
684
696inline void
697kdBoxRealBounds(RealVect& a_boxLo,
698 RealVect& a_boxHi,
699 const Box& a_box,
700 const RealVect& a_dx,
701 const RealVect& a_probLo) noexcept
702{
703 for (int dir = 0; dir < SpaceDim; dir++) {
704 a_boxLo[dir] = a_probLo[dir] + a_box.smallEnd(dir) * a_dx[dir];
705 a_boxHi[dir] = a_probLo[dir] + (a_box.bigEnd(dir) + 1) * a_dx[dir];
706 }
707}
708
723inline void
724kdCheckCentroid(const RealVect& a_centroid,
725 const Real a_totalWeight,
726 const RealVect& a_boxLo,
727 const RealVect& a_boxHi) noexcept
728{
729#ifndef NDEBUG
730 CH_assert(a_totalWeight > 0.0);
731
732 for (int dir = 0; dir < SpaceDim; dir++) {
733 const Real tol = 1.0e-9 * (std::abs(a_boxLo[dir]) + std::abs(a_boxHi[dir]) + 1.0);
734
735 CH_assert(a_centroid[dir] >= a_boxLo[dir] - tol);
736 CH_assert(a_centroid[dir] <= a_boxHi[dir] + tol);
737 }
738#else
739 (void)a_centroid;
740 (void)a_totalWeight;
741 (void)a_boxLo;
742 (void)a_boxHi;
743#endif
744}
745
772template <typename Packed>
773inline RealVect
774kdPlaceMerged(const KDPlacement a_placement,
775 const RealVect& a_centroid,
776 const Real a_totalWeight,
777 const RealVect& a_boxLo,
778 const RealVect& a_boxHi,
779 const std::vector<Real>& a_weights,
780 const std::vector<RealVect>& a_positions) noexcept
781{
782 switch (a_placement) {
784 return a_centroid;
785 }
786 case KDPlacement::Sample: {
787 // This is the one branch that reads a_positions, so it is also the one that needs it filled.
788 CH_assert(a_positions.size() == a_weights.size());
789
790 // Inverse-CDF draw over the members' weights: member i is picked with probability w_i / W.
791 const Real threshold = Random::getUniformReal01() * a_totalWeight;
792
793 Real accumulated = 0.0;
794
795 for (std::size_t i = 0; i < a_weights.size(); i++) {
796 accumulated += a_weights[i];
797
798 if (accumulated >= threshold) {
799 return a_positions[i];
800 }
801 }
802
803 // Only reachable if the accumulation falls short of the threshold by round-off.
804 return a_positions.empty() ? a_centroid : a_positions.back();
805 }
806 case KDPlacement::Random: {
807 RealVect x;
808
809 for (int dir = 0; dir < SpaceDim; dir++) {
810 x[dir] = a_boxLo[dir] + Random::getUniformReal01() * (a_boxHi[dir] - a_boxLo[dir]);
811 }
812
813 return x;
814 }
815 }
816
817 return a_centroid;
818}
819
820} // namespace detail
821
822template <typename P,
823 typename Packed,
824 typename Traits,
825 typename Gather,
826 typename Combine,
827 typename Scatter,
828 typename Allocator,
829 typename PosValid,
830 typename PatchRegular>
831inline void
833 EBAMRFAB& a_cellHistogram,
834 EBAMRFAB& a_leafQuota,
835 const AmrMesh& a_amr,
836 const int a_ppc,
837 const Real a_weightMedianCellWidths,
838 const KDPlacement a_placement,
839 const bool a_capWeights,
840 const KDSplitPlacement a_splitPlacement,
841 const Gather& a_gather,
842 const Combine& a_combine,
843 const Scatter& a_scatter,
844 const Allocator& a_allocateID,
845 const PosValid& a_isPositionValid,
846 const PatchRegular& a_isPatchRegular)
847{
848 using namespace detail;
849
850 // Carve-protocol types. Local to this function because nothing else constructs or consumes them:
851 // they describe one pass of the claim/verdict/commit exchange and have no meaning outside it.
852
853 // Box ranking key. Tightest box wins a contested particle; the anchor id is the deterministic
854 // tiebreak, since AABB volume alone is not a strict total order.
855 struct KDBoxKey
856 {
857 Real volume;
858 ParticleID anchor;
859
860 bool
861 operator<(const KDBoxKey& a_rhs) const noexcept
862 {
863 return (volume != a_rhs.volume) ? (volume < a_rhs.volume) : (anchor < a_rhs.anchor);
864 }
865 };
866
867 // One member of a boundary box: the particle and the rank that owns it.
868 struct KDMember
869 {
870 ParticleID id;
871 RankID owner;
872 };
873
874 // Phase 1 wire type: a box's claim on one member, routed to that member's owner.
875 struct KDClaim
876 {
877 ParticleID memberID;
878 KDBoxKey key;
879 RankID proposerRank;
880 int proposerBoxIdx; // which of the proposer's own boxes claimed it
881 };
882
883 // Phase 2 wire type: the owner's nominal argmin winner, answered per claiming box.
884 struct KDVerdict
885 {
886 ParticleID memberID;
887 int claimantBoxIdx;
888 bool won;
889 };
890
891 // Phase 3 wire type: the proposer's actual outcome. Only this authorizes a deletion.
892 struct KDCommit
893 {
894 ParticleID memberID;
895 bool committed;
896 };
897
898 CH_TIMERS("ParticleManagement::mergeKDCarve");
899 CH_TIMER("ParticleManagement::mergeKDCarve::build_classify", t_build);
900 CH_TIMER("ParticleManagement::mergeKDCarve::carve_exchange", t_carve);
901 CH_TIMER("ParticleManagement::mergeKDCarve::remove_consumed", t_remove);
902 CH_TIMER("ParticleManagement::mergeKDCarve::place_results", t_place);
903
904 const std::string realm = a_particles.getRealm();
905 const int finestLevel = a_amr.getFinestLevel();
906 const RealVect probLo = a_amr.getProbLo();
907
908 // Per-cell scratch, as mesh data on this realm rather than per-patch buffers: both quantities are
909 // cell data, and this is what the rest of the code uses for cell data. The caller owns the holders
910 // so they are allocated once per regrid rather than once per call -- AmrMesh::allocate() builds a
911 // Copier per level, which is cheap in isolation but adds up at scale. Their one ghost cell is what
912 // the gathered ghosts and any just-outside leaf centroid need.
913 EBAMRFAB& histogram = a_cellHistogram;
914 EBAMRFAB& leafQuota = a_leafQuota;
915
916 CH_assert(histogram[0]->nComp() == 1);
917 CH_assert(leafQuota[0]->nComp() == 1);
918 CH_assert(histogram[0]->ghostVect() >= IntVect::Unit);
919 CH_assert(leafQuota[0]->ghostVect() >= IntVect::Unit);
920
921 const int myRank = procID();
922 const int numRanks = numProc();
923
924 // ---- Per-patch bookkeeping shared across the whole rank ----
925 struct PatchWork
926 {
927 int level;
928 DataIndex din;
929 RealVect dx;
930 Box box;
931 };
932
933 struct RuntimeBox
934 {
935 KDBoxKey key;
936 int patchIdx;
937 std::vector<KDMember> members;
938 };
939
940 // particle: the merged super-particle itself.
941 // patchIdx: which patch's leaf produced it, used as the cheap first guess when placing it.
942 struct MergedResult
943 {
944 MergeParticle<Packed> particle;
945 int patchIdx;
946 };
947
948 // One entry per (locally-owned, boundary-exposed particle) x (one of MY OWN boxes that lists it),
949 // appended during STEP 1 and sorted by id once STEP 1 finishes -- see selfClaims' declaration
950 // below for why an id can have more than one entry, and boxIdx < 0 for what a sentinel entry means.
951 // id: the locally-owned, boundary-exposed particle this entry concerns.
952 // key: the claiming box's key. Meaningless when boxIdx is negative.
953 // boxIdx: index into myBoxes, or -1 for a listen-only sentinel. A sentinel carries no claim from
954 // any of this rank's own boxes; it records that this id must still be processed against
955 // foreign claims in STEP 3, which is the case for an unmergeable leaf's exposed member or
956 // a boundary leaf holding fewer than two members.
957 struct SelfClaimEntry
958 {
959 ParticleID id;
960 KDBoxKey key;
961 int boxIdx;
962 };
963
964 std::vector<PatchWork> patchWork;
965
966 // gatheredParticles: every particle this rank gathered, locals and ghosts alike, in gather order.
967 // Append-only and never reordered, so an index into it is a stable handle. Holds the
968 // full payloads particlesByID used to carry inline.
969 // particlesByID: one (id, slot-into-gatheredParticles) entry per (id, patch-occurrence), appended
970 // during STEP 1, then deduplicated once STEP 1 finishes by sorting and collapsing
971 // runs, preferring the non-ghost copy. Sorting these light (id, slot) keys instead of
972 // whole particles is what keeps the dedup off the fat-struct memory path. Point
973 // lookups after that go through findParticle() below, which binary-searches it and
974 // dereferences the surviving slot.
975 // myBoxes: this rank's own boundary boxes, indexed by the boxIdx carried in claims.
976 // consumedIDs: ids whose particle has been merged away and must be deleted. Sorted once,
977 // before STEP 7 consumes it by binary search.
978 // mergedResults: super-particles produced this pass, placed in STEP 8.
979 std::vector<MergeParticle<Packed>> gatheredParticles;
980 std::vector<std::pair<ParticleID, std::size_t>> particlesByID;
981 std::vector<RuntimeBox> myBoxes;
982 std::vector<ParticleID> consumedIDs;
983 std::vector<MergedResult> mergedResults;
984
985 // A particle can be listed by more than one of MY OWN boxes at once -- it is gathered once into
986 // its own home patch's build (as a local member there) but can ALSO be gathered as a ghost into a
987 // NEIGHBORING patch's build on this SAME rank, and that patch's build is entirely independent -- it
988 // may propose its own, different box that also lists this particle. Treating "owner == myRank" as
989 // "no contest possible" (collapsing to a single entry per id) is exactly the bug this vector-of-
990 // entries shape exists to prevent: two of my own boxes can genuinely compete for the same particle
991 // with no other rank involved at all, and collapsing the earlier entry double-counts the particle's
992 // weight if both boxes go on to commit. Sorted by id once STEP 1 finishes (see below); STEP 3
993 // processes it as grouped runs of equal id rather than hashing.
994 std::vector<SelfClaimEntry> selfClaims;
995
996 // Cheap pre-pass (patch sizes only, no per-particle work) so the containers below can be sized
997 // once up front instead of growing repeatedly as STEP 1 fills them. combinedCountUpperBound
998 // includes ghosts, so it's also a safe (if slightly generous) upper bound for the local-only ones.
999 std::size_t combinedCountUpperBound = 0;
1000
1001 for (int lvl = 0; lvl <= finestLevel; lvl++) {
1002 const DisjointBoxLayout& dbl = a_amr.getGrids(realm)[lvl];
1003 const DataIterator& dit = dbl.dataIterator();
1004 const int nbox = dit.size();
1005
1006#pragma omp parallel for schedule(runtime) reduction(+ : combinedCountUpperBound)
1007 for (int mybox = 0; mybox < nbox; mybox++) {
1008 combinedCountUpperBound += a_particles[lvl][dit[mybox]].size();
1009 }
1010 }
1011
1012 gatheredParticles.reserve(combinedCountUpperBound);
1013 particlesByID.reserve(combinedCountUpperBound);
1014 selfClaims.reserve(combinedCountUpperBound);
1015 consumedIDs.reserve(combinedCountUpperBound);
1016
1017 // Per-patch leaf/member scratch, declared once and reused across every patch below rather than
1018 // reallocated inside the loop -- the same reuse pattern as the histogram/leafQuota holders above.
1019 // buildKDQuotaLeaves() clear()s leaves on entry and members is cleared per leaf, so each reuse
1020 // retains the heap capacity grown by earlier patches instead of starting from nothing.
1021 std::vector<KDLeaf> leaves;
1022 std::vector<KDMember> members;
1023
1024 // Commit scratch, hoisted for the same reason and cleared per committed leaf. Declared per leaf
1025 // these cost a malloc/free apiece every time a leaf commits, and a leaf commits about as often as
1026 // a super-particle is produced -- the busiest count in the whole merge. The interior tier below and
1027 // the boundary tier further down are never live at the same time, so both share these.
1028 std::vector<Packed> payloads;
1029 std::vector<Real> weights;
1030 std::vector<RealVect> positions;
1031
1032 // Only KDPlacement::Sample reads the members' positions; filling them for the other placements is a
1033 // store per member per leaf that nothing goes on to read.
1034 const bool needPositions = (a_placement == KDPlacement::Sample);
1035
1036 // Which cells hold a particle that some other box can also see. Read from the realm, which derives it
1037 // from the three ghost masks when they are built; see Realm::m_particleGhostExposure.
1038 const AMRMask& exposure = a_amr.getParticleGhostExposure(realm, 1);
1039
1040 // ==== STEP 1: gather + build + classify + commit interior, per patch ====
1041 CH_START(t_build);
1042
1043 for (int lvl = 0; lvl <= finestLevel; lvl++) {
1044 const DisjointBoxLayout& dbl = a_amr.getGrids(realm)[lvl];
1045 const DataIterator& dit = dbl.dataIterator();
1046
1047 const RealVect dx = a_amr.getDx()[lvl] * RealVect::Unit;
1048
1049 const int nbox = dit.size();
1050
1051 // Serial (no omp): every patch appends to the same gatheredParticles/particlesByID/patchWork/
1052 // selfClaims/consumedIDs buffers, reuses the shared leaves/members scratch declared above, and
1053 // draws ids from the single a_allocateID() counter. Parallelising the box loop would race on all
1054 // of them, and the id race would mint duplicates.
1055 for (int mybox = 0; mybox < nbox; mybox++) {
1056 const DataIndex& din = dit[mybox];
1057
1058 ParticleSoA<P, Traits>& leaf = a_particles[lvl][din];
1059
1060 // Asked once per patch, then short-circuited into every position test below. Where the patch holds
1061 // no cut or covered cell there is no solid for a merged particle to land in, and the implicit
1062 // function evaluation -- the expensive term in the commit loop, paid once per super-particle -- is
1063 // skipped entirely rather than evaluated to a foregone conclusion.
1064 const bool patchRegular = a_isPatchRegular(lvl, din);
1065
1066 const auto positionValid = [&](const RealVect& a_pos) -> bool {
1067 return patchRegular || a_isPositionValid(a_pos);
1068 };
1069
1070 const BaseFab<bool>& exposureDin = (*exposure[lvl])[din];
1071
1072 std::vector<MergeParticle<Packed>> combined;
1073 combined.reserve(leaf.size());
1074
1075 for (std::size_t i = 0; i < leaf.size(); i++) {
1077
1078 p.position = leaf.position(i);
1079 p.weight = leaf.weight(i);
1080 p.globalID = leaf.particleID(i);
1081 p.ownerRank = leaf.rankID(i);
1082 p.isGhost = leaf.isGhost(i);
1083 p.payload = a_gather(leaf, i);
1084
1085 combined.push_back(p);
1086
1087 // Append unconditionally -- deduplicated by id (preferring the non-ghost copy, per
1088 // MergeParticle::isGhost's own docs) in the one pass below once STEP 1 finishes gathering,
1089 // rather than via a per-particle hash lookup here. The full payload lives in gatheredParticles;
1090 // particlesByID carries only (id, slot) so the dedup sort stays off the fat-struct path.
1091 gatheredParticles.push_back(p);
1092 particlesByID.emplace_back(p.globalID, gatheredParticles.size() - 1);
1093 }
1094
1095 patchWork.push_back(PatchWork{lvl, din, dx, dbl[din]});
1096 const int patchIdx = static_cast<int>(patchWork.size()) - 1;
1097
1098 if (combined.empty()) {
1099 continue;
1100 }
1101
1102 // Ground truth for how crowded each cell really is -- see kdFillCellHistogram(). Both this and
1103 // the live quota are cell data, so they live in the mesh holders allocated once above rather
1104 // than in per-patch scratch. Their ghost cell covers the gathered ghosts, which sit up to one
1105 // cell outside this patch, and any leaf centroid that lands just outside it.
1106 FArrayBox& cellCounts = (*histogram[lvl])[din];
1107 FArrayBox& quota = (*leafQuota[lvl])[din];
1108
1109 kdFillCellHistogram(cellCounts, combined, probLo, dx);
1110
1111 // Cap before the tree is built. The build itself may not divide a particle, so a heavy one would
1112 // otherwise set an unremovable floor on whatever leaf it lands in -- the reason this scope's
1113 // super-particle weights come out uneven where the per-cell build's do not.
1114 if (a_capWeights) {
1115 FArrayBox cellWeights(cellCounts.box(), 1);
1116 cellWeights.setVal(0.0);
1117
1118 for (const MergeParticle<Packed>& p : combined) {
1119 const IntVect cell = kdCellKeyOf(p.position, probLo, dx);
1120
1121 if (cellWeights.box().contains(cell)) {
1122 cellWeights(cell, 0) += p.weight;
1123 }
1124 }
1125
1126 kdCapCellWeights(combined, cellCounts, cellWeights, a_ppc, a_splitPlacement, probLo, dx, positionValid);
1127 }
1128
1129 buildKDQuotaLeaves(combined, quota, leaves, a_ppc, a_weightMedianCellWidths, dx, probLo, cellCounts);
1130
1131 for (const KDLeaf& bl : leaves) {
1132 members.clear();
1133 members.reserve(bl.hi - bl.lo);
1134
1135 bool anyLocal = false;
1136
1137 // Running minimum over ALL members; bl.hi > bl.lo always holds for a leaf. Folded into the
1138 // loop below so the boundary branch does not need a separate pass just to compute it.
1139 ParticleID anchor = combined[bl.lo].globalID;
1140
1141 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
1142 members.push_back(KDMember{combined[idx].globalID, combined[idx].ownerRank});
1143 anchor = std::min(anchor, combined[idx].globalID);
1144
1145 // "Local" here means physically resident in THIS patch, not merely owned by this rank --
1146 // a particle owned by this rank via a DIFFERENT one of its own patches, appearing here
1147 // only as a ghost, gives this leaf no standing (see MergeParticle::isGhost's own docs).
1148 if (!combined[idx].isGhost) {
1149 anyLocal = true;
1150 }
1151 }
1152
1153 if (!anyLocal) {
1154 // Nothing physically resident here, so this rank has no standing to act on it.
1155 continue;
1156 }
1157
1158 Real leafVolume = 1.0;
1159
1160 for (int dir = 0; dir < SpaceDim; dir++) {
1161 leafVolume *= (bl.boxHi[dir] - bl.boxLo[dir]);
1162 }
1163
1164 // Per-axis extent, not aggregate volume -- see kdMaxAxisSpan()'s own docs for why a
1165 // volume ratio alone cannot be trusted here (a long, thin leaf can have volume <=
1166 // one cell while still spanning several cells along one axis). leafVolume itself
1167 // is still needed below, unrelated to this check -- it's the box key's tie-break priority.
1168 const bool unmergeable = kdMaxAxisSpan(bl.boxLo, bl.boxHi, dx) > s_kdMaxLeafExtent;
1169
1170 // Per-resident-member exposure -- needed either way (leaf-wide OR for the interior/
1171 // boundary decision below; per-particle for the unmergeable-leaf listen-only case). Ghosts
1172 // are skipped entirely, not just because they have no standing (above) but because their
1173 // position lies outside this patch's own box -- the exposure mask carries no ghost cells, so
1174 // a ghost's cell is not addressable in it at all. Filtering on isGhost (not ownerRank) is what
1175 // makes this safe: a particle owned by this rank via a different patch is exactly as much
1176 // a ghost here, position-wise, as one owned by a different rank entirely.
1177 std::vector<bool> exposed(bl.hi - bl.lo, false);
1178 bool anyExposed = false;
1179 bool anyGhost = false;
1180
1181 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
1182 if (combined[idx].isGhost) {
1183 anyGhost = true;
1184
1185 continue;
1186 }
1187
1188 const IntVect cell = kdCellKeyOf(combined[idx].position, probLo, dx);
1189
1190 CH_assert(exposureDin.box().contains(cell));
1191
1192 const bool isExposed = exposureDin(cell, 0);
1193
1194 exposed[idx - bl.lo] = isExposed;
1195 anyExposed = anyExposed || isExposed;
1196 }
1197
1198 if (unmergeable) {
1199 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
1200 // exposed[] is already false for every ghost by construction (the exposure loop above
1201 // skips them), so this is equivalent to also checking !isGhost -- kept explicit anyway
1202 // as the actual invariant being relied on, not an implicit one.
1203 if (!combined[idx].isGhost && exposed[idx - bl.lo]) {
1204 // Listen-only sentinel; see SelfClaimEntry.
1205 selfClaims.push_back(SelfClaimEntry{combined[idx].globalID, KDBoxKey{}, -1});
1206 }
1207 }
1208
1209 continue;
1210 }
1211
1212 if (!anyExposed && !anyGhost) {
1213 // Interior: commit immediately, zero communication, direct insert -- safe ONLY because
1214 // anyGhost is false, i.e. this leaf genuinely contains no foreign data at all. Explicitly
1215 // requiring !anyGhost (not just !anyExposed) matters whenever a leaf spans cells: the old
1216 // argument ("a leaf whose members are all non-exposed can never contain a ghost, since
1217 // ghost-fill is symmetric at width 1") only holds when a leaf's own extent is <= 1 cell --
1218 // every member is then automatically within ghost-reach of every other member, so a ghost's
1219 // presence would force at least one resident's own cell to be exposed too. Once a leaf can
1220 // span SEVERAL cells, a ghost near one edge and a resident several
1221 // cells away at the other edge can coexist in the SAME leaf without that resident itself
1222 // ever being within ghost-width-1 of anything -- anyExposed alone would then be false even
1223 // though the leaf holds a foreign particle, and committing it here would merge that
1224 // particle's weight locally while its true home patch independently wins the SAME
1225 // particle's argmin via its own boundary box, double-counting it. Falling through to the
1226 // boundary-candidate branch below instead (rather than committing) is what lets a real
1227 // self-claim be raised for that foreign member, so the argmin actually arbitrates it.
1228 if (bl.hi - bl.lo >= 2) {
1229 RealVect centroid = RealVect::Zero;
1230 Real totalW = 0.0;
1231
1232 payloads.clear();
1233 weights.clear();
1234 positions.clear();
1235
1236 payloads.reserve(bl.hi - bl.lo);
1237 weights.reserve(bl.hi - bl.lo);
1238
1239 if (needPositions) {
1240 positions.reserve(bl.hi - bl.lo);
1241 }
1242
1243 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
1244 const MergeParticle<Packed>& p = combined[idx];
1245
1246 CH_assert(!p.isGhost);
1247
1248 payloads.push_back(p.payload);
1249 weights.push_back(p.weight);
1250
1251 if (needPositions) {
1252 positions.push_back(p.position);
1253 }
1254
1255 centroid += p.weight * p.position;
1256 totalW += p.weight;
1257 }
1258
1259 centroid /= totalW;
1260
1261 kdCheckCentroid(centroid, totalW, bl.boxLo, bl.boxHi);
1262
1263 // The placement can move the particle off the centroid, but never out of the leaf's own
1264 // box, so the EB test below still has something inside the leaf to judge. A placement
1265 // that lands in the solid falls back to the centroid rather than losing the leaf.
1266 //
1267 // The fallback is nested inside the first test rather than sequenced after it so that an
1268 // accepted position costs ONE predicate call. the predicate evaluates the geometry's
1269 // implicit function, which is the expensive term here, and it is paid once per committed
1270 // leaf -- i.e. once per super-particle produced.
1271 RealVect
1272 placed = kdPlaceMerged<Packed>(a_placement, centroid, totalW, bl.boxLo, bl.boxHi, weights, positions);
1273
1274 bool placedValid = positionValid(placed);
1275
1276 if (!placedValid && placed != centroid) {
1277 placed = centroid;
1278 placedValid = positionValid(placed);
1279 }
1280
1281 if (placedValid) {
1282 MergeParticle<Packed> merged;
1283
1284 merged.position = placed;
1285 merged.weight = totalW;
1286 merged.globalID = a_allocateID();
1287 merged.ownerRank = myRank;
1288 merged.payload = a_combine(payloads.data(), weights.data(), payloads.size());
1289
1290 mergedResults.push_back(MergedResult{merged, patchIdx});
1291
1292 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
1293 consumedIDs.push_back(combined[idx].globalID);
1294 }
1295 }
1296 }
1297
1298 continue;
1299 }
1300
1301 // Boundary candidate.
1302 if (members.size() >= 2) {
1303 const KDBoxKey key{leafVolume, anchor};
1304
1305 const int boxIdx = static_cast<int>(myBoxes.size());
1306 myBoxes.push_back(RuntimeBox{key, patchIdx, members});
1307
1308 for (const KDMember& m : members) {
1309 if (m.owner == myRank) {
1310 // Append, never collapse -- see selfClaims' own docs: this same particle can already
1311 // carry an entry from a DIFFERENT one of my own boxes (a different patch's independent
1312 // build), and both must survive to be argmin'd together in step 3.
1313 selfClaims.push_back(SelfClaimEntry{m.id, key, boxIdx});
1314 }
1315 }
1316 }
1317 else {
1318 // Exactly one member, and it must be the resident, exposed one (members.size()>=1 and
1319 // anyLocal both hold) -- can never reach the merge threshold alone, listen only.
1320 selfClaims.push_back(SelfClaimEntry{members[0].id, KDBoxKey{}, -1});
1321 }
1322 }
1323 }
1324 }
1325
1326 // particlesByID was appended to once per (id, patch-occurrence) above -- an id can appear more
1327 // than once (its home patch, plus any neighboring patch that gathered it as a ghost). Sort once
1328 // and collapse each run to a single entry, preferring the non-ghost copy when the run has one
1329 // (ghost-vs-ghost or non-ghost-vs-non-ghost duplicates carry identical data, so it doesn't matter
1330 // which of those survives) -- this is the dedup rule the old per-particle hash lookup used to
1331 // enforce inline.
1332 std::sort(particlesByID.begin(), particlesByID.end(), [](const auto& a_lhs, const auto& a_rhs) {
1333 return a_lhs.first < a_rhs.first;
1334 });
1335 {
1336 std::size_t writeIdx = 0;
1337
1338 for (std::size_t readIdx = 0; readIdx < particlesByID.size();) {
1339 std::size_t runEnd = readIdx + 1;
1340
1341 while (runEnd < particlesByID.size() && particlesByID[runEnd].first == particlesByID[readIdx].first) {
1342 runEnd++;
1343 }
1344
1345 std::size_t chosen = readIdx;
1346 for (std::size_t k = readIdx; k < runEnd; k++) {
1347 if (!gatheredParticles[particlesByID[k].second].isGhost) {
1348 chosen = k;
1349
1350 break;
1351 }
1352 }
1353
1354 if (writeIdx != chosen) {
1355 particlesByID[writeIdx] = particlesByID[chosen];
1356 }
1357 writeIdx++;
1358 readIdx = runEnd;
1359 }
1360
1361 particlesByID.resize(writeIdx);
1362 }
1363
1364 // Binary-search lookup into the now-sorted, deduplicated particlesByID -- replaces the old
1365 // unordered_map's .at().
1366 auto findParticle = [&particlesByID, &gatheredParticles](const ParticleID a_id) -> const MergeParticle<Packed>& {
1367 const auto it = std::lower_bound(particlesByID.begin(),
1368 particlesByID.end(),
1369 a_id,
1370 [](const std::pair<ParticleID, std::size_t>& a_entry, const ParticleID a_key) {
1371 return a_entry.first < a_key;
1372 });
1373
1374 CH_assert(it != particlesByID.end() && it->first == a_id);
1375
1376 return gatheredParticles[it->second];
1377 };
1378
1379 // selfClaims was appended to during STEP 1 -- sort by id once so STEP 3 can process it as grouped
1380 // runs of equal id (a particle's possibly-several self-claim entries, see SelfClaimEntry's own
1381 // docs) instead of hashing.
1382 std::sort(selfClaims.begin(), selfClaims.end(), [](const SelfClaimEntry& a_lhs, const SelfClaimEntry& a_rhs) {
1383 return a_lhs.id < a_rhs.id;
1384 });
1385
1386 CH_STOP(t_build);
1387
1388 // ==== STEP 2: Phase 1 -- claims to owners ====
1389 CH_START(t_carve);
1390 std::vector<std::vector<KDClaim>> claimSendByRank(numRanks);
1391
1392 for (std::size_t boxIdx = 0; boxIdx < myBoxes.size(); boxIdx++) {
1393 const RuntimeBox& box = myBoxes[boxIdx];
1394
1395 for (const KDMember& m : box.members) {
1396 if (m.owner != myRank) {
1397 claimSendByRank[m.owner].push_back(KDClaim{m.id, box.key, myRank, static_cast<int>(boxIdx)});
1398 }
1399 }
1400 }
1401
1402 // Sorted by memberID once received, so STEP 3's point lookups (claimsRange, below) and STEP 4's
1403 // full pass (grouped runs) don't need a hash map either.
1404 std::vector<KDClaim> incomingClaims = kdExchangeByRank(claimSendByRank);
1405 std::sort(incomingClaims.begin(), incomingClaims.end(), [](const KDClaim& a_lhs, const KDClaim& a_rhs) {
1406 return a_lhs.memberID < a_rhs.memberID;
1407 });
1408
1409 auto claimsRange = [&incomingClaims](const ParticleID a_id) {
1410 const auto lo = std::lower_bound(incomingClaims.begin(),
1411 incomingClaims.end(),
1412 a_id,
1413 [](const KDClaim& a_c, const ParticleID a_key) {
1414 return a_c.memberID < a_key;
1415 });
1416 const auto hi = std::upper_bound(incomingClaims.begin(),
1417 incomingClaims.end(),
1418 a_id,
1419 [](const ParticleID a_key, const KDClaim& a_c) {
1420 return a_key < a_c.memberID;
1421 });
1422
1423 return std::make_pair(lo, hi);
1424 };
1425
1426 // ==== STEP 3: local argmin -- nominal winner only, no deletion yet ====
1427 // The winner of a particle is either one of MY OWN boxes (rank == myRank, boxIdx identifies
1428 // WHICH one -- see selfClaims' own docs for why this can't just be "myRank", plural competing
1429 // boxes on this same rank are exactly the bug that distinction exists to prevent) or a foreign
1430 // rank (boxIdx meaningless).
1431 // rank: the rank owning the winning box.
1432 // boxIdx: that box's index within the winning rank's own box list.
1433 // key: the winning key itself, needed in STEP 4 to answer each incoming claim individually --
1434 // a claim won iff its own key is not strictly worse than this one.
1435 struct Winner
1436 {
1437 RankID rank;
1438 int boxIdx;
1439 KDBoxKey key;
1440 };
1441
1442 struct WinnerEntry
1443 {
1444 ParticleID id;
1445 Winner winner;
1446 };
1447
1448 // Built in ascending-id order for free -- selfClaims is sorted by id, and its grouped runs are
1449 // visited in that same order below -- so no separate sort is needed before findWinner's binary
1450 // search.
1451 std::vector<WinnerEntry> nominalWinner;
1452 nominalWinner.reserve(selfClaims.size());
1453
1454 for (std::size_t idx = 0; idx < selfClaims.size();) {
1455 const ParticleID id = selfClaims[idx].id;
1456
1457 std::size_t idxEnd = idx + 1;
1458
1459 while (idxEnd < selfClaims.size() && selfClaims[idxEnd].id == id) {
1460 idxEnd++;
1461 }
1462
1463 bool haveWinner = false;
1464 KDBoxKey bestKey{};
1465 RankID bestRank = myRank;
1466 int bestBoxIdx = -1;
1467
1468 for (std::size_t k = idx; k < idxEnd; k++) {
1469 if (selfClaims[k].boxIdx < 0) {
1470 // Listen-only sentinel, not an actual self-claim.
1471 continue;
1472 }
1473 if (!haveWinner || selfClaims[k].key < bestKey) {
1474 haveWinner = true;
1475 bestKey = selfClaims[k].key;
1476 bestRank = myRank;
1477 bestBoxIdx = selfClaims[k].boxIdx;
1478 }
1479 }
1480
1481 const auto range = claimsRange(id);
1482
1483 for (auto cit = range.first; cit != range.second; ++cit) {
1484 const KDClaim& c = *cit;
1485
1486 if (!haveWinner || c.key < bestKey) {
1487 haveWinner = true;
1488 bestKey = c.key;
1489 bestRank = c.proposerRank;
1490 // The winning claim's OWN box index (on ITS rank), not a sentinel -- step 4 compares
1491 // (rank, boxIdx) as the winning claim's IDENTITY, not just its key value, precisely so
1492 // that an exact key tie between two INDEPENDENT claims doesn't tell both of them "you
1493 // won" (see step 4's own comment). This must be the real index even for a foreign
1494 // winner, or that identity comparison could never match a foreign claim at all.
1495 bestBoxIdx = c.proposerBoxIdx;
1496 }
1497 }
1498
1499 if (haveWinner) {
1500 nominalWinner.push_back(WinnerEntry{id, Winner{bestRank, bestBoxIdx, bestKey}});
1501 }
1502
1503 idx = idxEnd;
1504 }
1505
1506 // Binary-search lookup into the now-sorted nominalWinner -- replaces the old unordered_map's
1507 // .find()/.at().
1508 auto findWinner = [&nominalWinner](const ParticleID a_id) -> const Winner* {
1509 const auto it = std::lower_bound(nominalWinner.begin(),
1510 nominalWinner.end(),
1511 a_id,
1512 [](const WinnerEntry& a_entry, const ParticleID a_key) {
1513 return a_entry.id < a_key;
1514 });
1515
1516 return (it != nominalWinner.end() && it->id == a_id) ? &it->winner : nullptr;
1517 };
1518
1519 // ==== STEP 4: Phase 2 -- verdicts to proposers, foreign members only ====
1520 // One verdict PER INCOMING CLAIM, not per rank: two of the SAME rank's own boxes can both claim
1521 // this particle (from two different patches that rank owns), and each needs its own, correct
1522 // answer -- a rank-level "your rank won" cannot tell them apart. See KDClaim::proposerBoxIdx.
1523
1524 std::vector<std::vector<KDVerdict>> verdictSendByRank(numRanks);
1525
1526 for (std::size_t idx = 0; idx < incomingClaims.size();) {
1527 const ParticleID id = incomingClaims[idx].memberID;
1528
1529 std::size_t idxEnd = idx + 1;
1530
1531 while (idxEnd < incomingClaims.size() && incomingClaims[idxEnd].memberID == id) {
1532 idxEnd++;
1533 }
1534
1535 const Winner* winner = findWinner(id);
1536
1537 // Every id that shows up in incomingClaims was claimed on one of MY OWN, locally-owned,
1538 // boundary-exposed particles -- STEP 1's classification is exhaustive over exposed members, so
1539 // it always left a (possibly listen-only) selfClaims entry for it, which always yields a
1540 // nominalWinner entry (haveWinner requires only ONE candidate, and the incoming claim itself
1541 // is one). This is a correctness invariant, not a defensive fallback.
1542 CH_assert(winner != nullptr);
1543
1544 for (std::size_t k = idx; k < idxEnd; k++) {
1545 const KDClaim& c = incomingClaims[k];
1546
1547 // Compare IDENTITY (which specific claim was chosen), not key VALUE: an exact key tie
1548 // between two INDEPENDENT claims is real and not vanishingly rare here (BrownianWalker's
1549 // regular initial distribution can make two independently-built boxes end up with the exact
1550 // same volume), and comparing key value alone would tell BOTH tied claimants "you won" --
1551 // exactly the kind of double-count this whole protocol exists to prevent. Step 3 already
1552 // deterministically picked exactly one winner among any tie; only that specific claim,
1553 // identified by (rank, box index), is told so.
1554 const bool won = (c.proposerRank == winner->rank) && (c.proposerBoxIdx == winner->boxIdx);
1555
1556 verdictSendByRank[c.proposerRank].push_back(KDVerdict{id, c.proposerBoxIdx, won});
1557 }
1558
1559 idx = idxEnd;
1560 }
1561
1562 const std::vector<KDVerdict> incomingVerdicts = kdExchangeByRank(verdictSendByRank);
1563
1564 // wonForeignByBox[boxIdx] = every foreign member THAT SPECIFIC one of my own boxes was told it won.
1565 // Indexed directly by boxIdx (a dense range over myBoxes) rather than hashed -- each box's own
1566 // list is bounded by its own membership (at most ppc), so sorting it once and using binary_search
1567 // at the read site below avoids both a per-box hash-table allocation (unordered_set would add one
1568 // per box, of which there can be thousands) and, for large ppc, the O(box size^2) cost a linear
1569 // scan would have across all of a box's foreign-member lookups.
1570
1571 std::vector<std::vector<ParticleID>> wonForeignByBox(myBoxes.size());
1572
1573 for (const KDVerdict& v : incomingVerdicts) {
1574 if (v.won) {
1575 wonForeignByBox[v.claimantBoxIdx].push_back(v.memberID);
1576 }
1577 }
1578
1579 for (std::vector<ParticleID>& won : wonForeignByBox) {
1580 std::sort(won.begin(), won.end());
1581 }
1582
1583 // ==== STEP 5: assemble each of my boxes' provisional membership, commit or not ====
1584 std::vector<std::vector<KDCommit>> commitSendByRank(numRanks);
1585
1586 for (std::size_t boxIdx = 0; boxIdx < myBoxes.size(); boxIdx++) {
1587 const RuntimeBox& box = myBoxes[boxIdx];
1588
1589 std::vector<ParticleID> survivingLocal;
1590 std::vector<ParticleID> survivingForeign;
1591
1592 for (const KDMember& m : box.members) {
1593 if (m.owner == myRank) {
1594 const Winner* winner = findWinner(m.id);
1595
1596 if (winner != nullptr && winner->rank == myRank && winner->boxIdx == static_cast<int>(boxIdx)) {
1597 survivingLocal.push_back(m.id);
1598 }
1599 }
1600 else {
1601 const std::vector<ParticleID>& won = wonForeignByBox[boxIdx];
1602
1603 if (std::binary_search(won.begin(), won.end(), m.id)) {
1604 survivingForeign.push_back(m.id);
1605 }
1606 }
1607 }
1608
1609 const std::size_t totalSurvivors = survivingLocal.size() + survivingForeign.size();
1610
1611 bool committed = false;
1612
1613 if (totalSurvivors >= 2) {
1614 RealVect centroid = RealVect::Zero;
1615 Real totalW = 0.0;
1616 RealVect surviveBoxLo, surviveBoxHi;
1617 bool haveSurviveBox = false;
1618
1619 payloads.clear();
1620 weights.clear();
1621 positions.clear();
1622
1623 payloads.reserve(totalSurvivors);
1624 weights.reserve(totalSurvivors);
1625
1626 if (needPositions) {
1627 positions.reserve(totalSurvivors);
1628 }
1629
1630 auto accumulate = [&](const ParticleID a_id) {
1631 const MergeParticle<Packed>& p = findParticle(a_id);
1632
1633 payloads.push_back(p.payload);
1634 weights.push_back(p.weight);
1635
1636 if (needPositions) {
1637 positions.push_back(p.position);
1638 }
1639
1640 centroid += p.weight * p.position;
1641 totalW += p.weight;
1642
1643 if (!haveSurviveBox) {
1644 surviveBoxLo = p.position;
1645 surviveBoxHi = p.position;
1646 haveSurviveBox = true;
1647 }
1648 else {
1649 for (int dir = 0; dir < SpaceDim; dir++) {
1650 surviveBoxLo[dir] = std::min(surviveBoxLo[dir], p.position[dir]);
1651 surviveBoxHi[dir] = std::max(surviveBoxHi[dir], p.position[dir]);
1652 }
1653 }
1654 };
1655
1656 for (const ParticleID id : survivingLocal) {
1657 accumulate(id);
1658 }
1659
1660 for (const ParticleID id : survivingForeign) {
1661 accumulate(id);
1662 }
1663
1664 centroid /= totalW;
1665
1666 kdCheckCentroid(centroid, totalW, surviveBoxLo, surviveBoxHi);
1667
1668 // Same placement rule as the interior tier, fallback nested for the same reason -- one predicate
1669 // call whenever the placed position is accepted. The box here is the surviving members' own,
1670 // since the carve may have taken some of the leaf's original members away.
1671 RealVect
1672 placed = kdPlaceMerged<Packed>(a_placement, centroid, totalW, surviveBoxLo, surviveBoxHi, weights, positions);
1673
1674 // Same per-patch escape hatch as the interior tier, asked of the patch this box belongs to. This
1675 // tier is the reason a_isPatchRegular is defined over the patch GROWN BY ONE CELL: surviveBox is
1676 // built from surviving members that may be foreign, and a foreign member sits up to one ghost
1677 // cell outside box.patchIdx's own box.
1678 const PatchWork& boxPatch = patchWork[box.patchIdx];
1679
1680 bool placedValid = a_isPatchRegular(boxPatch.level, boxPatch.din) || a_isPositionValid(placed);
1681
1682 if (!placedValid && placed != centroid) {
1683 placed = centroid;
1684 placedValid = a_isPositionValid(placed);
1685 }
1686
1687 if (placedValid) {
1688 committed = true;
1689
1690 MergeParticle<Packed> merged;
1691
1692 merged.position = placed;
1693 merged.weight = totalW;
1694 merged.globalID = a_allocateID();
1695 merged.ownerRank = myRank;
1696 merged.payload = a_combine(payloads.data(), weights.data(), payloads.size());
1697
1698 mergedResults.push_back(MergedResult{merged, box.patchIdx});
1699
1700 for (const ParticleID id : survivingLocal) {
1701 // This rank owns these, and the outcome is already known, so nothing need be awaited.
1702 consumedIDs.push_back(id);
1703 }
1704 }
1705 }
1706
1707 // Phase 3 outgoing: every foreign member this box nominally won needs to hear the outcome,
1708 // whether this box committed or not, since its owner is otherwise left not knowing whether to
1709 // delete it.
1710 //
1711 // This round cannot be collapsed into Phase 2. Winning a member's nominal argmin does not mean
1712 // the winning box will actually commit: it can independently lose OTHER members to OTHER,
1713 // unrelated lower-keyed boxes and end up under the merge threshold itself. Deleting on the
1714 // Phase 2 verdict alone then destroys that particle's weight, because no merged particle
1715 // anywhere ends up holding it. Concretely, with three ranks and box keys Kx < Ky < Kz:
1716 //
1717 // Bx{p1(X), p2(Y)}, By{p2(Y), p3(Z)}, Bz{p3(Z), p4(X)}
1718 //
1719 // Argmin gives p1,p2 -> Bx (Bx beats By on p2) and p3 -> By (By beats Bz). Assembling actual
1720 // memberships: Bx = {p1,p2} commits; By has lost p2 to Bx and is left with {p3} alone, so it
1721 // does NOT commit; Bz has lost p3 to By and is left with {p4} alone, so it does not either.
1722 // Z learns from Phase 2 only that By nominally won p3 -- deleting p3 there would lose it
1723 // outright. Phase 3 is what tells Z to release it instead. The same holds for chains of any
1724 // length.
1725 for (const ParticleID id : survivingForeign) {
1726 commitSendByRank[findParticle(id).ownerRank].push_back(KDCommit{id, committed});
1727 }
1728 }
1729
1730 // ==== STEP 6: Phase 3 -- commit/release, foreign members only ====
1731 const std::vector<KDCommit> incomingCommits = kdExchangeByRank(commitSendByRank);
1732
1733 for (const KDCommit& c : incomingCommits) {
1734 if (c.committed) {
1735 consumedIDs.push_back(c.memberID);
1736 }
1737 }
1738
1739 std::sort(consumedIDs.begin(), consumedIDs.end());
1740
1741 // STEP 7 below finds entries by binary search, which silently misses on an unsorted range.
1742 CH_assert(std::is_sorted(consumedIDs.begin(), consumedIDs.end()));
1743
1744 CH_STOP(t_carve);
1745
1746 // ==== STEP 7: remove consumed particles ====
1747 CH_START(t_remove);
1748 for (const PatchWork& pw : patchWork) {
1749 ParticleSoA<P, Traits>& leaf = a_particles[pw.level][pw.din];
1750
1751 std::size_t i = 0;
1752
1753 while (i < leaf.size()) {
1754 if (!leaf.isGhost(i) && std::binary_search(consumedIDs.begin(), consumedIDs.end(), leaf.particleID(i))) {
1755 leaf.remove(i);
1756 }
1757 else {
1758 i++;
1759 }
1760 }
1761 }
1762
1763 a_particles.clearGhostParticles();
1764
1765 CH_STOP(t_remove);
1766
1767 // ==== STEP 8: place merged results ====
1768 CH_START(t_place);
1769 std::vector<std::vector<MergeParticle<Packed>>> scatterByDestRank(numRanks);
1770
1771 auto insertHere = [&](const MergeParticle<Packed>& a_p, const int a_level, const DataIndex& a_din) {
1772 ParticleSoA<P, Traits>& leaf = a_particles[a_level][a_din];
1773
1774 a_scatter(leaf, a_p);
1775 };
1776
1777 for (const MergedResult& mr : mergedResults) {
1778 const PatchWork& pw = patchWork[mr.patchIdx];
1779
1780 RealVect boxRealLo, boxRealHi;
1781
1782 kdBoxRealBounds(boxRealLo, boxRealHi, pw.box, pw.dx, probLo);
1783
1784 bool inOwnBox = true;
1785
1786 for (int dir = 0; dir < SpaceDim && inOwnBox; dir++) {
1787 if (mr.particle.position[dir] < boxRealLo[dir] || mr.particle.position[dir] >= boxRealHi[dir]) {
1788 inOwnBox = false;
1789 }
1790 }
1791
1792 if (inOwnBox) {
1793 insertHere(mr.particle, pw.level, pw.din);
1794
1795 continue;
1796 }
1797
1798 const auto dst = a_particles.findDestination(mr.particle.position);
1799
1800 if (!dst.valid) {
1801 MayDay::Error("ParticleManagement::mergeKDCarve -- merged particle not found in any patch");
1802 }
1803
1804 if (dst.rank == myRank) {
1805 const DataIndex din = a_amr.getLevelTiles(realm)[dst.level]->getMyGrids().at(dst.gridIndex);
1806
1807 insertHere(mr.particle, dst.level, din);
1808 }
1809 else {
1810 MergeParticle<Packed> corrected = mr.particle;
1811 corrected.ownerRank = dst.rank;
1812
1813 scatterByDestRank[dst.rank].push_back(corrected);
1814 }
1815 }
1816
1817 const std::vector<MergeParticle<Packed>> incomingScattered = kdExchangeByRank(scatterByDestRank);
1818
1819 for (const MergeParticle<Packed>& p : incomingScattered) {
1820 const auto dst = a_particles.findDestination(p.position);
1821
1822 if (!dst.valid || dst.rank != myRank) {
1823 MayDay::Error("ParticleManagement::mergeKDCarve -- incoming scattered particle not "
1824 "found in any of this rank's own patches");
1825 }
1826
1827 const DataIndex din = a_amr.getLevelTiles(realm)[dst.level]->getMyGrids().at(dst.gridIndex);
1828
1829 MergeParticle<Packed> corrected = p;
1830 corrected.ownerRank = myRank;
1831
1832 insertHere(corrected, dst.level, din);
1833 }
1834
1835 CH_STOP(t_place);
1836}
1837
1838template <typename P,
1839 typename Packed,
1840 typename Traits,
1841 typename Gather,
1842 typename Combine,
1843 typename Scatter,
1844 typename Allocator,
1845 typename PosValid,
1846 typename PatchRegular>
1847inline void
1849 EBAMRFAB& a_cellHistogram,
1850 EBAMRFAB& a_leafQuota,
1851 const AmrMesh& a_amr,
1852 const int a_ppc,
1853 const Real a_weightMedianCellWidths,
1854 const KDPlacement a_placement,
1855 const bool a_capWeights,
1856 const KDSplitPlacement a_splitPlacement,
1857 const Gather& a_gather,
1858 const Combine& a_combine,
1859 const Scatter& a_scatter,
1860 const Allocator& a_allocateID,
1861 const PosValid& a_isPositionValid,
1862 const PatchRegular& a_isPatchRegular)
1863{
1864 // The patch-local merge is the ghost-free case of mergeKDInterior() writing back into its own
1865 // container, so it delegates rather than repeating the body.
1866 //
1867 // mergeKDInterior() commits a leaf only when the leaf holds no ghost. The caller here must not
1868 // have filled a ghost halo -- a ghost would be merged locally while its true owner merges it too --
1869 // so every gathered particle is resident, every leaf is ghost-free, and that test admits them all.
1870 // Passing a_particles as its own destination puts the super-particles back where the patch merge
1871 // has always put them.
1872 mergeKDInterior<P, Packed, Traits>(a_particles,
1873 a_particles,
1874 a_cellHistogram,
1875 a_leafQuota,
1876 a_amr,
1877 a_ppc,
1878 a_weightMedianCellWidths,
1879 a_placement,
1880 a_capWeights,
1881 a_splitPlacement,
1882 a_gather,
1883 a_combine,
1884 a_scatter,
1885 a_allocateID,
1886 a_isPositionValid,
1887 a_isPatchRegular);
1888}
1889
1890template <typename P,
1891 typename Packed,
1892 typename Traits,
1893 typename Gather,
1894 typename Combine,
1895 typename Scatter,
1896 typename Allocator,
1897 typename PosValid,
1898 typename PatchRegular>
1899inline void
1901 ParticleContainer<P, Traits>& a_interior,
1902 EBAMRFAB& a_cellHistogram,
1903 EBAMRFAB& a_leafQuota,
1904 const AmrMesh& a_amr,
1905 const int a_ppc,
1906 const Real a_weightMedianCellWidths,
1907 const KDPlacement a_placement,
1908 const bool a_capWeights,
1909 const KDSplitPlacement a_splitPlacement,
1910 const Gather& a_gather,
1911 const Combine& a_combine,
1912 const Scatter& a_scatter,
1913 const Allocator& a_allocateID,
1914 const PosValid& a_isPositionValid,
1915 const PatchRegular& a_isPatchRegular)
1916{
1917 using namespace detail;
1918
1919 CH_TIMERS("ParticleManagement::mergeKDInterior");
1920 CH_TIMER("ParticleManagement::mergeKDInterior::build", t_build);
1921 CH_TIMER("ParticleManagement::mergeKDInterior::commit", t_commit);
1922 CH_TIMER("ParticleManagement::mergeKDInterior::remove_place", t_place);
1923
1924 const std::string realm = a_particles.getRealm();
1925 const int finestLevel = a_amr.getFinestLevel();
1926 const RealVect probLo = a_amr.getProbLo();
1927
1928 // A leaf committed from patch (lvl,din) is written to the SAME patch of a_interior, which is only
1929 // meaningful when the two containers share a layout.
1930 CH_assert(a_interior.getRealm() == realm);
1931
1932 // Caller-owned per-cell scratch; see mergeKDCarve() for why the holders are not allocated here.
1933 EBAMRFAB& histogram = a_cellHistogram;
1934 EBAMRFAB& leafQuota = a_leafQuota;
1935
1936 CH_assert(histogram[0]->nComp() == 1);
1937 CH_assert(leafQuota[0]->nComp() == 1);
1938 CH_assert(histogram[0]->ghostVect() >= IntVect::Unit);
1939 CH_assert(leafQuota[0]->ghostVect() >= IntVect::Unit);
1940
1941 const int myRank = procID();
1942
1943 // Per-patch leaf scratch, declared once and reused across every patch below rather than reallocated
1944 // inside the loop -- the same reuse pattern as the histogram/leafQuota holders above.
1945 // buildKDQuotaLeaves() clear()s it on entry, so each reuse retains the heap capacity grown by
1946 // earlier patches instead of starting from nothing.
1947 std::vector<KDLeaf> leaves;
1948
1949 // Commit scratch, hoisted for the same reason and cleared per committed leaf. Declared per leaf
1950 // these cost a malloc/free apiece every time a leaf commits, and a leaf commits about as often as
1951 // a super-particle is produced -- the busiest count in the whole merge.
1952 std::vector<Packed> payloads;
1953 std::vector<Real> weights;
1954 std::vector<RealVect> positions;
1955
1956 // Only KDPlacement::Sample reads the members' positions; filling them for the other placements is a
1957 // store per member per leaf that nothing goes on to read.
1958 const bool needPositions = (a_placement == KDPlacement::Sample);
1959
1960 for (int lvl = 0; lvl <= finestLevel; lvl++) {
1961 const DisjointBoxLayout& dbl = a_amr.getGrids(realm)[lvl];
1962 const DataIterator& dit = dbl.dataIterator();
1963
1964 const RealVect dx = a_amr.getDx()[lvl] * RealVect::Unit;
1965
1966 const int nbox = dit.size();
1967
1968 // Serial (no omp): the leaves scratch is shared across patches by design, consumedIDs is appended
1969 // to from every patch, and a_allocateID() hands out ids from one counter.
1970 for (int mybox = 0; mybox < nbox; mybox++) {
1971 const DataIndex& din = dit[mybox];
1972
1973 ParticleSoA<P, Traits>& leaf = a_particles[lvl][din];
1974
1975 // Asked once per patch, then short-circuited into every position test below. Where the patch holds
1976 // no cut or covered cell there is no solid for a merged particle to land in, and the implicit
1977 // function evaluation -- the expensive term in the commit loop, paid once per super-particle -- is
1978 // skipped entirely rather than evaluated to a foregone conclusion.
1979 const bool patchRegular = a_isPatchRegular(lvl, din);
1980
1981 const auto positionValid = [&](const RealVect& a_pos) -> bool {
1982 return patchRegular || a_isPositionValid(a_pos);
1983 };
1984
1985 // Ghosts ARE gathered, unlike mergeKDPatch(). They are never merged here, but they must take
1986 // part in the partition: a ghost is what marks the leaf it lands in as contested, and the
1987 // per-cell quota has to see the cell's true occupancy to hand out the right number of slots.
1988 std::vector<MergeParticle<Packed>> combined;
1989 combined.reserve(leaf.size());
1990
1991 for (std::size_t i = 0; i < leaf.size(); i++) {
1993
1994 p.position = leaf.position(i);
1995 p.weight = leaf.weight(i);
1996 p.globalID = leaf.particleID(i);
1997 p.ownerRank = leaf.rankID(i);
1998 p.isGhost = leaf.isGhost(i);
1999 p.payload = a_gather(leaf, i);
2000
2001 combined.push_back(p);
2002 }
2003
2004 if (combined.empty()) {
2005 continue;
2006 }
2007
2008 CH_START(t_build);
2009
2010 // Ground truth for how crowded each cell really is -- see kdFillCellHistogram(). Both this and
2011 // the live quota are cell data, so they live in the mesh holders allocated once above rather
2012 // than in per-patch scratch. Their ghost cell covers the gathered ghosts, which sit up to one
2013 // cell outside this patch, and any leaf centroid that lands just outside it.
2014 FArrayBox& cellCounts = (*histogram[lvl])[din];
2015 FArrayBox& quota = (*leafQuota[lvl])[din];
2016
2017 kdFillCellHistogram(cellCounts, combined, probLo, dx);
2018
2019 // Cap before the tree is built. The build itself may not divide a particle, so a heavy one would
2020 // otherwise set an unremovable floor on whatever leaf it lands in -- the reason this scope's
2021 // super-particle weights come out uneven where the per-cell build's do not.
2022 if (a_capWeights) {
2023 FArrayBox cellWeights(cellCounts.box(), 1);
2024 cellWeights.setVal(0.0);
2025
2026 for (const MergeParticle<Packed>& p : combined) {
2027 const IntVect cell = kdCellKeyOf(p.position, probLo, dx);
2028
2029 if (cellWeights.box().contains(cell)) {
2030 cellWeights(cell, 0) += p.weight;
2031 }
2032 }
2033
2034 kdCapCellWeights(combined, cellCounts, cellWeights, a_ppc, a_splitPlacement, probLo, dx, positionValid);
2035 }
2036
2037 buildKDQuotaLeaves(combined, quota, leaves, a_ppc, a_weightMedianCellWidths, dx, probLo, cellCounts);
2038
2039 CH_STOP(t_build);
2040 CH_START(t_commit);
2041
2042 std::vector<ParticleID> consumedIDs;
2043 std::vector<MergeParticle<Packed>> mergedResults;
2044
2045 for (const KDLeaf& bl : leaves) {
2046 if (bl.hi - bl.lo < 2) {
2047 continue;
2048 }
2049
2050 // Cheap geometric test before the O(members) ghost scan below.
2051 if (kdMaxAxisSpan(bl.boxLo, bl.boxHi, dx) > s_kdMaxLeafExtent) {
2052 continue;
2053 }
2054
2055 // THE rule for this tier: commit iff the leaf holds no ghost. Every member is then a
2056 // particle physically resident in this patch, so no other patch can commit any of them --
2057 // a patch that draws one of them into a leaf of its own necessarily sees it as a ghost, and
2058 // that leaf is disqualified here by this very test. Committed leaves therefore never share
2059 // a member across patches, and no weight is counted twice. Boundary exposure is deliberately
2060 // NOT consulted: an exposed leaf that happens to hold no ghost is uncontested and merges
2061 // here, which is exactly what shrinks the skin relative to the carve.
2062 bool anyGhost = false;
2063
2064 for (std::size_t idx = bl.lo; idx < bl.hi && !anyGhost; idx++) {
2065 anyGhost = combined[idx].isGhost;
2066 }
2067
2068 if (anyGhost) {
2069 continue;
2070 }
2071
2072 RealVect centroid = RealVect::Zero;
2073 Real totalW = 0.0;
2074
2075 payloads.clear();
2076 weights.clear();
2077 positions.clear();
2078
2079 payloads.reserve(bl.hi - bl.lo);
2080 weights.reserve(bl.hi - bl.lo);
2081
2082 if (needPositions) {
2083 positions.reserve(bl.hi - bl.lo);
2084 }
2085
2086 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
2087 const MergeParticle<Packed>& p = combined[idx];
2088
2089 // Guaranteed by the anyGhost test above; merging a ghost here would double-count its
2090 // weight against its true owner.
2091 CH_assert(!p.isGhost);
2092
2093 payloads.push_back(p.payload);
2094 weights.push_back(p.weight);
2095
2096 if (needPositions) {
2097 positions.push_back(p.position);
2098 }
2099
2100 centroid += p.weight * p.position;
2101 totalW += p.weight;
2102 }
2103
2104 centroid /= totalW;
2105
2106 kdCheckCentroid(centroid, totalW, bl.boxLo, bl.boxHi);
2107
2108 // A placement that lands in the solid falls back to the centroid rather than losing the leaf.
2109 // The fallback is nested inside the first test rather than sequenced after it so that an
2110 // accepted position costs ONE predicate call. the predicate evaluates the geometry's
2111 // implicit function, which is the expensive term here, and it is paid once per committed leaf
2112 // -- i.e. once per super-particle produced.
2113 RealVect placed = kdPlaceMerged<Packed>(a_placement, centroid, totalW, bl.boxLo, bl.boxHi, weights, positions);
2114
2115 if (!positionValid(placed)) {
2116 if (placed == centroid) {
2117 continue;
2118 }
2119
2120 placed = centroid;
2121
2122 if (!positionValid(placed)) {
2123 continue;
2124 }
2125 }
2126
2127 MergeParticle<Packed> merged;
2128
2129 merged.position = placed;
2130 merged.weight = totalW;
2131 merged.globalID = a_allocateID();
2132 merged.ownerRank = myRank;
2133 merged.isGhost = false;
2134 merged.payload = a_combine(payloads.data(), weights.data(), payloads.size());
2135
2136 mergedResults.push_back(merged);
2137
2138 for (std::size_t idx = bl.lo; idx < bl.hi; idx++) {
2139 consumedIDs.push_back(combined[idx].globalID);
2140 }
2141 }
2142
2143 CH_STOP(t_commit);
2144
2145 if (mergedResults.empty()) {
2146 continue;
2147 }
2148
2149 CH_START(t_place);
2150
2151 // Consumed originals leave a_particles; the super-particles they became are written to
2152 // a_interior instead of back here. What is left behind in a_particles is precisely the skin.
2153 std::sort(consumedIDs.begin(), consumedIDs.end());
2154
2155 // Removal below finds entries by binary search, which silently misses on an unsorted range.
2156 CH_assert(std::is_sorted(consumedIDs.begin(), consumedIDs.end()));
2157
2158 {
2159 std::size_t i = 0;
2160
2161 while (i < leaf.size()) {
2162 if (!leaf.isGhost(i) && std::binary_search(consumedIDs.begin(), consumedIDs.end(), leaf.particleID(i))) {
2163 leaf.remove(i);
2164 }
2165 else {
2166 i++;
2167 }
2168 }
2169 }
2170
2171 RealVect boxRealLo, boxRealHi;
2172
2173 kdBoxRealBounds(boxRealLo, boxRealHi, dbl[din], dx, probLo);
2174
2175 for (const MergeParticle<Packed>& mr : mergedResults) {
2176 bool inOwnBox = true;
2177
2178 for (int dir = 0; dir < SpaceDim && inOwnBox; dir++) {
2179 if (mr.position[dir] < boxRealLo[dir] || mr.position[dir] >= boxRealHi[dir]) {
2180 inOwnBox = false;
2181 }
2182 }
2183
2184 if (inOwnBox) {
2185 a_scatter(a_interior[lvl][din], mr);
2186
2187 continue;
2188 }
2189
2190 // Every member was resident in this patch, so their weighted centroid is too -- this branch
2191 // is unreachable in exact arithmetic and exists only for round-off at a patch face. It must
2192 // still land on this rank: there is no exchange here to carry a particle anywhere else.
2193 const auto dst = a_particles.findDestination(mr.position);
2194
2195 if (!dst.valid || dst.rank != myRank) {
2196 MayDay::Error("ParticleManagement::mergeKDInterior -- merged particle left this rank's own patches");
2197 }
2198
2199 const DataIndex dstDin = a_amr.getLevelTiles(realm)[dst.level]->getMyGrids().at(dst.gridIndex);
2200
2201 a_scatter(a_interior[dst.level][dstDin], mr);
2202 }
2203
2204 CH_STOP(t_place);
2205 }
2206 }
2207}
2208
2209} // namespace ParticleManagement
2210
2211#include <CD_NamespaceFooter.H>
2212
2213#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
File containing some useful static methods related to random number generation.
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:46
const Vector< RefCountedPtr< LevelTiles > > & getLevelTiles(const std::string &a_realm) const
Get the tiled space representation.
Definition CD_AmrMesh.cpp:3568
const Vector< DisjointBoxLayout > & getGrids(const std::string &a_realm) const
Get the grids.
Definition CD_AmrMesh.cpp:3400
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:3663
RealVect getProbLo() const
Get lower-left corner of computational domain.
Definition CD_AmrMesh.cpp:3149
const Vector< Real > & getDx() const
Get spatial resolutions.
Definition CD_AmrMesh.cpp:3344
int getFinestLevel() const
Get finest grid level.
Definition CD_AmrMesh.cpp:3171
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:1286
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:1338
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:1195
double & weight(const std::size_t a_index) noexcept
Weight of particle i.
Definition CD_ParticleSoA.H:1229
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:1260
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:1040
static Real getUniformReal01()
Get a uniform real number on the interval [0,1].
Definition CD_RandomImplem.H:158
void kdCapCellWeights(std::vector< MergeParticle< Packed > > &a_particles, const FArrayBox &a_cellCounts, const FArrayBox &a_cellWeights, const int a_ppc, const KDSplitPlacement a_splitPlacement, const RealVect &a_probLo, const RealVect &a_dx, const PosValid &a_isPositionValid) noexcept
Cap every resident particle in a crowded cell at that cell's target leaf weight, splitting the ones a...
Definition CD_KDParticleMergeImplem.H:297
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:170
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:724
RealVect kdSplitDisplace(const KDSplitPlacement a_placement, const RealVect &a_parent, const RealVect &a_cellLo, const RealVect &a_dx, const Real a_spacing) noexcept
Place one piece of a split particle.
Definition CD_ParticleManagementImplem.H:242
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:124
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:385
bool kdIntVectLess(const IntVect &a_lhs, const IntVect &a_rhs) noexcept
Strict weak ordering over IntVect, lexicographic component-wise.
Definition CD_KDParticleMergeImplem.H:211
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:43
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:697
RealVect kdPlaceMerged(const KDPlacement a_placement, const RealVect &a_centroid, const Real a_totalWeight, const RealVect &a_boxLo, const RealVect &a_boxHi, const std::vector< Real > &a_weights, const std::vector< RealVect > &a_positions) noexcept
Place the super-particle a leaf reduces to, given the leaf's members and their weighted centroid.
Definition CD_KDParticleMergeImplem.H:774
void buildKDQuotaLeaves(std::vector< MergeParticle< Packed > > &a_particles, FArrayBox &a_used, std::vector< KDLeaf > &a_leaves, const int a_ppc, const Real a_weightMedianCellWidths, 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:474
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:191
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:244
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:430
Namespace for various particle management tools.
Definition CD_KDParticleMerge.H:33
KDSplitPlacement
Where the pieces of a split particle go.
Definition CD_ParticleManagement.H:65
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_weightMedianCellWidths, const KDPlacement a_placement, const bool a_capWeights, const KDSplitPlacement a_splitPlacement, const Gather &a_gather, const Combine &a_combine, const Scatter &a_scatter, const Allocator &a_allocateID, const PosValid &a_isPositionValid, const PatchRegular &a_isPatchRegular)
Run the uncontested tier of the kd merge, splitting the input into merged and leftover.
Definition CD_KDParticleMergeImplem.H:1900
void mergeKDCarve(ParticleContainer< P, Traits > &a_particles, EBAMRFAB &a_cellHistogram, EBAMRFAB &a_leafQuota, const AmrMesh &a_amr, const int a_ppc, const Real a_weightMedianCellWidths, const KDPlacement a_placement, const bool a_capWeights, const KDSplitPlacement a_splitPlacement, const Gather &a_gather, const Combine &a_combine, const Scatter &a_scatter, const Allocator &a_allocateID, const PosValid &a_isPositionValid, const PatchRegular &a_isPatchRegular)
Run one non-iterative pass of the kd-tree carve merge over every patch this rank owns.
Definition CD_KDParticleMergeImplem.H:832
KDPlacement
Where a kd merge puts the particle a leaf reduces to.
Definition CD_ParticleManagement.H:80
@ Random
A uniformly random point in the leaf's bounding box.
@ Centroid
The leaf's weighted centroid.
@ Sample
One of the leaf's own particles, drawn with probability proportional to weight.
void mergeKDPatch(ParticleContainer< P, Traits > &a_particles, EBAMRFAB &a_cellHistogram, EBAMRFAB &a_leafQuota, const AmrMesh &a_amr, const int a_ppc, const Real a_weightMedianCellWidths, const KDPlacement a_placement, const bool a_capWeights, const KDSplitPlacement a_splitPlacement, const Gather &a_gather, const Combine &a_combine, const Scatter &a_scatter, const Allocator &a_allocateID, const PosValid &a_isPositionValid, const PatchRegular &a_isPatchRegular)
Run one patch-local kd-tree merge over every patch this rank owns.
Definition CD_KDParticleMergeImplem.H:1848
Minimal, payload-agnostic description of one particle as input to a distributed merge.
Definition CD_ParticleManagement.H:519
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:551
RealVect position
Particle position.
Definition CD_ParticleManagement.H:523
Packed payload
Opaque, caller-defined payload. Never inspected by the merge logic – only carried through to the comb...
Definition CD_ParticleManagement.H:563
Real weight
Particle weight. Not required to be an integer.
Definition CD_ParticleManagement.H:528
int level
The AMR level this particle lives on. Used by the nearest-neighbor mergers, whose cell keys are level...
Definition CD_ParticleManagement.H:557
ParticleID globalID
Globally unique particle id, unique within one merge round across every rank.
Definition CD_ParticleManagement.H:537
RankID ownerRank
The rank owning this particle. Always read from the particle's own data, never inferred from MPI tran...
Definition CD_ParticleManagement.H:543
One leaf of a kd tree: a contiguous index range into the (in-place reordered) particle buffer,...
Definition CD_KDParticleMerge.H:68