|
chombo-discharge
|
Internal-only implementation details of the ParticleManagement API. More...
Classes | |
| struct | KDLeaf |
| One leaf of a kd tree: a contiguous index range into the (in-place reordered) particle buffer, plus its axis-aligned bounding box. More... | |
| struct | NNCellKeyHasher |
| Hash functor for NNCellKey – REQUIRED whenever NNCellKey is used as a hash-map key; NNCellKey must never be used as an ordered (std::map or std::set) key. More... | |
| struct | NNFallbackCandidate |
| One additional ranked candidate beyond a query's primary nearest neighbor. More... | |
| struct | NNMergeEdge |
| One candidate merge edge: a single particle's own nearest-neighbor query result. More... | |
| struct | NNMergeProposal |
| A cross-patch merge proposal: one particle's own nearest-neighbor query result, bundled as a self-contained, atomic pair for shipment to whichever rank owns the candidate. More... | |
| struct | NNMergeResult |
| One resolved merge: the two consumed particles' ids and the newly created merged particle (not yet placed into any patch – see placeMergedParticles()). More... | |
| struct | NNMergeVerdict |
| The judge's reply to one proposal. More... | |
| struct | NNPooledParticle |
| Pooled per-round record for one particle: the particle plus its cached boundary-exposure flag. More... | |
Typedefs | |
| using | NNCellKey = std::pair< int, IntVect > |
| A cell key together with the AMR level it was computed on – see cellKeyOf()'s warning. | |
Functions | |
| template<typename Packed > | |
| 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 exactly one super-particle, subject to a live per-cell leaf quota of a_ppc. | |
| template<typename T > | |
| 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 bucket, return the flat concatenation of everything received. | |
| template<typename Packed > | |
| 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). | |
| 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. | |
| 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. | |
| bool | kdIntVectLess (const IntVect &a_lhs, const IntVect &a_rhs) noexcept |
| Strict weak ordering over IntVect, lexicographic component-wise. | |
| template<typename Packed > | |
| 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. | |
| template<typename Packed , typename PosValid > | |
| 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 above it into pieces. | |
| template<typename Packed > | |
| 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 used by buildKDQuotaLeaves(). | |
| template<typename Packed > | |
| 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 this node's total particle weight on either side. | |
| 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. | |
| 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 lie within [a_boxLo,a_boxHi], using strictly positive weights, is mathematically guaranteed to itself lie within that same box. | |
| template<typename Packed > | |
| 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. | |
| IntVect | cellKeyOf (const RealVect &a_position, const RealVect &a_probLo, const RealVect &a_dx) noexcept |
| The unclamped, position-derived cell index a given physical position falls in. | |
| Real | nnMergeDistSquared (const RealVect &a_a, const RealVect &a_b) noexcept |
| Squared Euclidean distance between two physical positions. | |
| int | nnMergeChebyshevCellDistance (const IntVect &a_a, const IntVect &a_b) noexcept |
| Chebyshev (max-per-axis) distance between two cell-index vectors – see CD_NearestNeighborParticleMerge.H's a_maxCellDistance docs. | |
| bool | nnMergeCrossLevelTooFar (const RealVect &a_posA, const int a_levelA, const RealVect &a_posB, const int a_levelB, const RealVect &a_probLo, const std::vector< RealVect > &a_dxByLevel, const int a_maxCellDistance) noexcept |
| a_maxCellDistance check for two positions that may be on DIFFERENT AMR levels. | |
| void | nnMergeWeightedCentroid (RealVect &a_mergedPos, Real &a_mergedWeight, const RealVect &a_posA, const Real a_weightA, const RealVect &a_posB, const Real a_weightB) noexcept |
| Weighted-centroid merge of two positions/weights – shared by resolveTrivialTier() and judgeProposals() so the formula is written exactly once. | |
| Real | nnMergePointToCellDistSquared (const RealVect &a_point, const IntVect &a_cell, const RealVect &a_probLo, const RealVect &a_dx) noexcept |
| Squared distance from a physical point to a cell's axis-aligned bounding box (0 if the point is inside). | |
| template<typename T > | |
| std::vector< T > | nnMergeExchangeByRank (const std::vector< std::vector< T > > &a_sendByRank) |
| Generic alltoallv-style exchange: send a per-destination-rank bucket of trivially- copyable records, return the flat concatenation of everything received. | |
| template<typename Packed > | |
| bool | nnMergeAnyCellCrowded (const std::vector< MergeParticle< Packed > > &a_localValid, const std::vector< MergeParticle< Packed > > &a_ghosts, const RealVect &a_probLo, const RealVect &a_dx, const NNCellBudget &a_cellBudget) noexcept |
| Exact per-cell crowding pre-filter shared by buildNNSpatialIndex()/buildNNCellSpatialIndex(): is any cell in this patch's local+ghost particle set over its a_cellBudget? | |
| template<class P , Real P::* weight, RealVect P::* position> | |
| void | buildKDCellLeaves (const std::vector< P > &a_particles, const int a_maxLeaves, const Real a_weightMedianLength, const bool a_capWeights, const std::function< RealVect(const RealVect &)> &a_splitPosition, const BinaryParticleReconcile< P > &a_particleReconcile, std::vector< std::pair< const P *, const P * > > &a_leaves) noexcept |
| Build a KD partition of a list of particles and return the leaf particle ranges. | |
| 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. | |
| template<typename T , typename > | |
| T | partitionParticles (const T a_numParticles) |
| Partition particles so that all MPI rank draw a cumulative number of particles equal to a_numParticles. | |
Internal-only implementation details of the ParticleManagement API.
Internal-only implementation details of the ParticleManagement API (continued).
These helpers have no external callers; they are the algorithms sitting behind the public factory/wrapper functions (make*Merger, partitionParticleWeights) and are not part of the public interface. Public callers qualify them with detail::.
Reopens the detail namespace introduced above to house partitionParticles alongside the other internal helpers; see the first detail block for the full rationale. Not part of the public interface – public callers qualify with detail::.
| using ParticleManagement::detail::NNCellKey = typedef std::pair<int, IntVect> |
A cell key together with the AMR level it was computed on – see cellKeyOf()'s warning.
Two NNCellKeys are equal only if BOTH the level and the cell index match; a coarse- level cell and an unrelated fine-level cell that happen to share the same bare IntVect are correctly kept distinct because their level differs. This is the key type for every live per-cell count pooled across multiple levels (see a_liveCellCount in resolveTrivialTier()/ judgeProposals()/findNearestNeighborCandidates()).
|
inlinenoexcept |
Build a KD partition of a list of particles and return the leaf particle ranges.
Partitions the input particles into at most a_maxLeaves spatial groups ("leaves"). Each split divides the node along the longest bounding-box axis, at the weight median while the node is no wider than a_weightMedianLength and at the count median above that – the same size-dependent rule the whole-patch build uses, and the same one a fixed 'weight' or 'count' setting expresses as a crossover above or below every node's size. Under the weight median the halves carry as nearly equal a weight as possible (differing by at most one physical particle), which may require dividing the median particle itself, in which case a_particleReconcile fixes up the daughters' non-weight members; the count median never divides a particle. This is the partitioning primitive behind super-particle merging: the caller reduces each returned leaf range to a single merged particle.
Implementation note: the tree is built in a flat, reusable per-thread scratch arena (two ping-ponged contiguous particle buffers + a flat range list), so there is no per-node heap allocation, no pointer chasing, and the scratch is reused across calls on the same thread.
Template parameters: P -> Particle type exposing the weight/position data members below P::*weight -> Pointer to the Real data member holding the particle weight P::*position -> Pointer to the RealVect data member holding the particle position.
P must be an AoS-style particle type carrying weight and position as data members – MergeParticle is the one this library provides, and the one every in-tree caller uses. ParticleSoA payload types (ItoParticle, ...) store those two in the container rather than the payload, so pack the columns into such an intermediary AoS structure first.
| [in] | a_particles | Input particles (copied into internal scratch; not modified). |
| [in] | a_maxLeaves | Maximum number of leaf nodes (the per-cell target particle count). |
| [in] | a_weightMedianLength | Node size, as a length, at or below which the split plane is the weight median rather than the count median. Negative disables the weight median entirely; a huge value selects it everywhere. |
| [in] | a_capWeights | If true, divide every input particle heavier than the target leaf weight (total weight / a_maxLeaves) into co-located integer pieces before partitioning, so no single particle can dominate a leaf. |
| [in] | a_splitPosition | Where a split particle's pieces go, given the parent's position. The identity reproduces the parent's position exactly; see KDSplitPlacement for why that is not always what you want. |
| [in] | a_particleReconcile | Reconciliation callback applied to the two daughters when the median is split. |
| [out] | a_leaves | Filled with one {begin,end} pointer pair per leaf. |
|
inlinenoexcept |
Build one whole-patch kd tree: partition a_particles by position into leaves, each of which becomes exactly one super-particle, subject to a live per-cell leaf quota of a_ppc.
a_particles is reordered IN PLACE, so each returned leaf's [lo,hi) range is contiguous in the reordered buffer; no particle is ever copied or split synthetically.
A leaf collapses to exactly ONE super-particle at its own weighted centroid, so the number of leaves whose centroid falls in a cell is that cell's post-merge population. That count is tracked live in a per-cell map while splitting, and a split that would push a cell past a_ppc is refused. Splitting takes the heaviest candidate leaf first and stops when nothing may legally be split.
Splits are along the longest real-space axis and are deliberately UNSNAPPED to the grid, so a leaf may straddle a cell face and draw members from either side. The split plane is the count median while the node is wider than a_weightMedianCellWidths cell widths, and the weight median once it is narrower: above that scale a split apportions leaves between cells, which is a question of counts; below it the weight median drives the resulting super-particles toward equal weight.
The quota constrains only leaves that can actually merge. mergeKDCarve() and mergeKDPatch() veto any leaf wider than s_kdMaxLeafExtent and leaves its members behind individually, so such a leaf costs its cell its full member count rather than one particle; the quota therefore does not block splitting it further. As a result a cell can finish slightly above a_ppc when a leaf is both too wide to merge and in a cell already at quota, since both available choices overshoot.
Particles in cells at or below a_ppc are excluded from the partition and emitted as singleton leaves. A singleton never merges (the merge threshold is two members), so an uncrowded cell cannot be drained by a neighbour's merge.
| Packed | See MergeParticle. |
| [in,out] | a_particles | Particles to partition; reordered in place. |
| [in,out] | a_used | Live per-cell leaf quota for this patch, zeroed on entry. Its box must cover every cell a leaf centroid can fall in, i.e. it needs at least one ghost cell. |
| [out] | a_leaves | Every resulting leaf, in no particular order. |
| [in] | a_ppc | Target particles per cell, enforced here as a per-cell ceiling on leaf count. |
| [in] | a_weightMedianCellWidths | Leaf size, in cell widths, at or below which the split plane switches from the count median to the weight median. 0 disables the weight median. |
| [in] | a_dx | This patch's own cell spacing. |
| [in] | a_probLo | Physical position of the domain's low corner, for computing cell indices. |
| [in] | a_cellCounts | Per-cell TRUE particle-count histogram (see kdFillCellHistogram()). A cell is crowded, and its particles eligible to merge, iff its count exceeds a_ppc. A cell outside this box reads as 0 (empty): unlike a_used, whose box coverage is a hard precondition, this lookup guards rather than asserts, since a particle's own cell is always in-box but the guard costs nothing and documents the intent. |
|
inlinenoexcept |
The unclamped, position-derived cell index a given physical position falls in.
The cell-key convention used throughout for the crowding trigger and a_maxCellDistance. Not clamped to any patch's box, so a ghost buckets into its true origin cell and particles in different patches' local frames stay directly comparable.
| [in] | a_position | Physical position. |
| [in] | a_probLo | Physical position of the domain's low corner. |
| [in] | a_dx | Cell spacing (per direction) of the position's OWN level. |
|
inlinenoexcept |
Axis-aligned bounding box of particles[a_lo, a_hi).
| [out] | a_boxLo | Low corner. |
| [out] | a_boxHi | High corner. |
| [in] | a_particles | Particle buffer. |
| [in] | a_lo | Start of the range (inclusive). |
| [in] | a_hi | End of the range (exclusive). |
|
inlinenoexcept |
Real-space bounds of a box: the half-open region [lo, hi) that its cells cover.
Half-open on purpose – the high corner is the OUTER face of the last cell, so testing pos >= lo && pos < hi per direction partitions space between abutting boxes with no gap and no double-cover, which is exactly the "is this particle still in its own patch" test both callers make.
| [out] | a_boxLo | Low corner (inclusive). |
| [out] | a_boxHi | High corner (exclusive). |
| [in] | a_box | Box whose bounds are wanted. |
| [in] | a_dx | Grid spacing at the box's level. |
| [in] | a_probLo | Lower-left corner of the physical domain. |
|
inlinenoexcept |
Cap every resident particle in a crowded cell at that cell's target leaf weight, splitting the ones above it into pieces.
A particle heavier than a leaf's target weight cannot be equalized away by any partition: it alone sets a floor on whichever leaf it lands in. The whole-patch build is forbidden from dividing a particle mid-tree (see kdSplitWeightMedian()), which is exactly why its super-particle weights come out uneven. Dividing beforehand sidesteps that: it is local, it needs no id arbitration, and it happens before any leaf exists.
Only CROWDED cells are capped (count > a_ppc). An uncrowded cell's particles are emitted as singleton leaves and never merged, so splitting there would raise its population above the target with nothing to bring it back down. Capping a crowded cell only ever raises its count, so it stays crowded and a_cellCounts remains correct for the one thing buildKDQuotaLeaves() asks of it.
Ghosts are never capped. A ghost belongs to another patch, which is capping it too; splitting it here as well would double-count its weight.
A displaced piece is validated with the caller's own position predicate and falls back to the parent's position if it fails. Unlike the per-cell build this has no CellInfo to test the volume fraction with, and a piece that is never merged – its leaf vetoed for width, or lost at a contested boundary – keeps the position it was given, so a raw displacement in a cut cell could otherwise leave a particle inside the solid.
| Packed | See MergeParticle. |
| [in,out] | a_particles | Gathered particles for this patch; grown in place by the split pieces. |
| [in] | a_cellCounts | Per-cell true particle counts (see kdFillCellHistogram()). |
| [in] | a_cellWeights | Per-cell total weight, over the same box as a_cellCounts. |
| [in] | a_ppc | Target particles per cell. |
| [in] | a_splitPlacement | Where each piece goes – see ParticleManagement::KDSplitPlacement. |
| [in] | a_probLo | Physical position of the domain's low corner. |
| [in] | a_dx | This patch's own cell spacing. |
| [in] | a_isPositionValid | Predicate rejecting positions inside the embedded boundary. |
|
inlinenoexcept |
The unclamped, position-derived cell index a physical position falls in.
Same convention as the nearest-neighbor merge's cellKeyOf() – not clamped to any patch's box, so positions in different patches' local frames stay directly comparable.
| [in] | a_position | Physical position. |
| [in] | a_probLo | Physical position of the domain's low corner. |
| [in] | a_dx | Cell spacing. |
|
inlinenoexcept |
Debug-only sanity check on a merged particle's position: a weighted centroid of positions that all lie within [a_boxLo,a_boxHi], using strictly positive weights, is mathematically guaranteed to itself lie within that same box.
CH_assert this explicitly (rather than trust the arithmetic silently) so that a stray sign error, a mis-set/negative weight, or a wrong id feeding the wrong position into the accumulation surfaces immediately as an assertion in a DEBUG build, instead of silently producing a merged superparticle placed somewhere non-physical. A relative tolerance accounts for floating- point round-off in the summation, not for any real violation.
| [in] | a_centroid | Computed weighted centroid. |
| [in] | a_totalWeight | Sum of weights used to compute a_centroid. |
| [in] | a_boxLo | Low corner of the bounding box every contributing position lies within. |
| [in] | a_boxHi | High corner of the bounding box every contributing position lies within. |
|
inline |
Generic Alltoallv-style exchange for a trivially-copyable record type: send a per-destination-rank bucket, return the flat concatenation of everything received.
A deliberate, self-contained duplicate of the nearest-neighbor merge's nnMergeExchangeByRank() (same MPI_Alltoall/Alltoallv shape) rather than a shared dependency – that function lives in a header that transitively pulls in EBGeometry, which this file must not depend on (see this file's own
). In a serial (non-MPI) build, or with a single rank, returns this rank's own bucket unchanged.
| T | Trivially-copyable record type (KDClaim, KDVerdict, KDCommit, or MergeParticle<Packed> with Packed itself trivially copyable). |
| [in] | a_sendByRank | One bucket per destination rank (size == number of ranks). |
|
inlinenoexcept |
Tally a per-cell particle-count histogram over one patch's gathered particles.
Built from every particle's own position, so it is unaffected by how buildKDQuotaLeaves() later partitions the space – a node-local count is NOT a safe substitute, see that function's own docs for why. Shared by mergeKDCarve()/mergeKDPatch()/mergeKDInterior(): all three need the same ground truth, and the invariant that it comes from true positions rather than from the partition is easy to break if each keeps its own copy.
a_counts must include at least one ghost cell, since a gathered ghost sits up to one cell outside the patch's own box.
The count is stored in a_counts' Real elements but is always a non-negative integer. A double represents every integer exactly up to 2^53, so consumers – e.g. the crowding test in buildKDQuotaLeaves() – may static_cast<int> it losslessly for any realistic per-cell count.
| Packed | See MergeParticle. |
| [out] | a_counts | Per-cell counts, zeroed here before tallying. |
| [in] | a_particles | Gathered particles for this patch. |
| [in] | a_probLo | Physical position of the domain's low corner. |
| [in] | a_dx | This patch's own cell spacing. |
|
inlinenoexcept |
Strict weak ordering over IntVect, lexicographic component-wise.
Used to sort/binary-search the per-patch, per-cell TRUE particle-count histogram (see buildKDQuotaLeaves()'s own docs) – IntVect has no built-in total order.
| [in] | a_lhs | Left-hand cell index. |
| [in] | a_rhs | Right-hand cell index. |
|
inlinenoexcept |
Largest per-axis extent of [a_boxLo,a_boxHi], expressed as a fraction of that axis's own cell width.
The correctness backstop buildKDQuotaLeaves(), mergeKDCarve() and mergeKDPatch() rely on to decide whether a leaf is compact enough to merge: unlike an aggregate VOLUME ratio (a product across axes), this cannot be fooled by an anisotropic (long-and-thin) bounding box – e.g. a box 5 cells long in one axis and 0.2 cells wide in another has volume ratio 1.0 (deceptively "one cell's worth") but a maxAxisSpan of 5.0 (correctly flagged as still spanning several cells). Particles hugging a curved EB surface, or funneled along a narrow L-shaped coarse-fine strip, produce exactly this kind of elongated cluster.
| [in] | a_boxLo | Low corner. |
| [in] | a_boxHi | High corner. |
| [in] | a_dx | Cell spacing. |
|
inlinenoexcept |
Place the super-particle a leaf reduces to, given the leaf's members and their weighted centroid.
The three placements differ in what they keep of the leaf. The centroid keeps its mean position and discards its spread, so repeated merges contract the sub-cell distribution onto a lattice of leaf centres. Sampling keeps a position one of the leaf's own particles occupied, drawn with probability proportional to weight, which is distribution-preserving. Random keeps only the leaf's extent.
Unlike the per-cell build, these leaves are deliberately not snapped to the grid, so a leaf can straddle a cell face and the placed particle can land in a different cell from the centroid – which is what the per-cell leaf quota counted. That is the algorithm working as intended (a leaf merges across a face precisely because it is allowed to span one), not an error: the quota bounds how many leaves are centred in a cell, not where each leaf's particle ends up.
| Packed | See MergeParticle. |
| [in] | a_placement | Which placement to apply. |
| [in] | a_centroid | The leaf's weighted centroid, already computed by the caller. |
| [in] | a_totalWeight | The leaf's total weight, i.e. the sum of a_weights. |
| [in] | a_boxLo | Low corner of the leaf's bounding box. |
| [in] | a_boxHi | High corner of the leaf's bounding box. |
| [in] | a_weights | The members' weights, parallel to a_positions. |
| [in] | a_positions | The members' positions, parallel to a_weights. Read only by KDPlacement::Sample, so callers may leave it empty for the other two placements rather than filling a vector no branch will look at. |
|
inlinenoexcept |
Split particles[a_lo,a_hi) in place by the longest axis at the count-median – the sole split rule used by buildKDQuotaLeaves().
The split point is the position-sorted median particle (via nth_element), balancing COUNT as evenly as possible – mirrors the per-cell build's own logic, but by count, never by weight, and with NO synthetic particle splitting (a particle is never divided across the two children). nth_element divides the range in half by count however the positions are distributed, so a tightly clustered node still splits evenly.
| [in,out] | a_particles | Particle buffer; reordered in [a_lo,a_hi). |
| [in] | a_lo | Start of the range (inclusive). |
| [in] | a_hi | End of the range (exclusive); a_hi - a_lo must be >= 2. |
| [in] | a_axis | Split axis – the node's longest box axis, precomputed once by the caller (which already needs the box for the span test) rather than re-derived here from a second bounding box. |
|
inlinenoexcept |
Place one piece of a split particle.
Shared by the per-cell and whole-patch builds so both spread split pieces the same way.
The jitter kernel is TRUNCATED to the cell, not clamped and not reflected. Clamping stacks every out-of-range draw onto the wall. Reflecting folds the overhanging part of the kernel back on top of the part that stayed, doubling the density within half a bandwidth of each wall – and since the fold is per-direction, a parent near a corner is folded once per direction and the corner gets 2^D times the density it should. Truncation is the correct conditional distribution: draw uniformly from the part of the kernel that lies inside the cell. Its only cost is a mild inward bias for parents within half a bandwidth of a wall, which is second order in the bandwidth and has no directional structure.
| [in] | a_placement | Which rule to apply. Center returns the parent's position unchanged. |
| [in] | a_parent | The parent particle's position. |
| [in] | a_cellLo | Low corner of the cell the parent lives in. |
| [in] | a_dx | Cell spacing, per direction. |
| [in] | a_spacing | Jitter bandwidth – the local mean interparticle spacing. |
|
inlinenoexcept |
Split particles[a_lo,a_hi) in place by the longest axis at the WEIGHT median – the plane with half this node's total particle weight on either side.
Used instead of the count median so that the resulting leaves carry roughly equal total weight, and therefore so do the super-particles they collapse into. Uniform super-particle weight is what keeps statistical noise down, and is what kd_partition = weight asks for.
Unlike the per-cell build this NEVER divides a particle synthetically, so exact equality is not attainable: once a single particle's weight exceeds a leaf's target weight, that particle alone sets a floor no partition can get under. The bound the per-cell build guarantees (max/min <= 2) is bought precisely by that synthetic division and is therefore out of reach here by construction.
A weight median can land very unevenly BY COUNT – one dominant particle carrying half the node's weight forces a 1-vs-(n-1) split. That is harmless here only because buildKDQuotaLeaves() enforces a hard per-cell leaf quota independently of how the split plane behaves: an uneven split can cost merge quality but cannot leave a cell above target. Under any of the earlier, quota-less formulations the same degeneracy was fatal.
| [in,out] | a_particles | Particle buffer; reordered in [a_lo,a_hi). |
| [in] | a_lo | Start of the range (inclusive). |
| [in] | a_hi | End of the range (exclusive); a_hi - a_lo must be >= 2. |
| [in] | a_axis | Split axis – the node's longest box axis, precomputed once by the caller (which already needs the box for the span test) rather than re-derived here from a second bounding box. |
|
inlinenoexcept |
Exact per-cell crowding pre-filter shared by buildNNSpatialIndex()/buildNNCellSpatialIndex(): is any cell in this patch's local+ghost particle set over its a_cellBudget?
A count-only tally keyed as the crowding trigger (cellKeyOf(pos, probLo, dx), local and ghost alike); checks the true per-cell maximum, not an average density (a low-average patch can still hold one over-threshold cell).
| Packed | See MergeParticle. |
| [in] | a_localValid | This patch's own full valid-particle set. |
| [in] | a_ghosts | Every ghost shipped to this patch this round. |
| [in] | a_probLo | Physical position of the domain's low corner. |
| [in] | a_dx | Cell spacing of THIS PATCH's own level. |
| [in] | a_cellBudget | The count each cell is drained down to – see NNCellBudget. |
|
inlinenoexcept |
Chebyshev (max-per-axis) distance between two cell-index vectors – see CD_NearestNeighborParticleMerge.H's a_maxCellDistance docs.
| [in] | a_a | First cell index. |
| [in] | a_b | Second cell index. |
|
inlinenoexcept |
a_maxCellDistance check for two positions that may be on DIFFERENT AMR levels.
Raw cell indices computed with two different dx values are not directly comparable, so both positions are re-expressed in the finer of the two levels' cell-size units (higher AMR level = finer) before taking the Chebyshev distance. Reduces to the same-level case when a_levelA == a_levelB.
| [in] | a_posA | First participant's position. |
| [in] | a_levelA | First participant's AMR level. |
| [in] | a_posB | Second participant's position. |
| [in] | a_levelB | Second participant's AMR level. |
| [in] | a_probLo | Physical position of the domain's low corner. |
| [in] | a_dxByLevel | Cell spacing per AMR level. |
| [in] | a_maxCellDistance | The cap (see CD_NearestNeighborParticleMerge.H's a_maxCellDistance docs). |
|
inlinenoexcept |
Squared Euclidean distance between two physical positions.
| [in] | a_a | First position. |
| [in] | a_b | Second position. |
|
inline |
Generic alltoallv-style exchange: send a per-destination-rank bucket of trivially- copyable records, return the flat concatenation of everything received.
Packs one bucket per rank and exchanges via MPI_Alltoall (byte counts) + MPI_Alltoallv (payload). In a serial (non-MPI) build, or with a single rank, this returns this rank's own bucket unchanged.
| T | Trivially-copyable record type (NNMergeProposal<Packed>, NNMergeVerdict, or MergeParticle<Packed> – all satisfy this given Packed itself is required to). |
| [in] | a_sendByRank | One bucket per destination rank (size == number of ranks). |
|
inlinenoexcept |
Squared distance from a physical point to a cell's axis-aligned bounding box (0 if the point is inside).
| [in] | a_point | Physical position. |
| [in] | a_cell | Cell index. |
| [in] | a_probLo | Physical position of the domain's low corner. |
| [in] | a_dx | Cell spacing. |
|
inlinenoexcept |
Weighted-centroid merge of two positions/weights – shared by resolveTrivialTier() and judgeProposals() so the formula is written exactly once.
| [out] | a_mergedPos | Weighted-centroid position. |
| [out] | a_mergedWeight | Summed weight. |
| [in] | a_posA | First particle's position. |
| [in] | a_weightA | First particle's weight. |
| [in] | a_posB | Second particle's position. |
| [in] | a_weightB | Second particle's weight. |
|
inline |
Partition particles so that all MPI rank draw a cumulative number of particles equal to a_numParticles.
| [in] | a_numParticles | Total number of particles to be drawn. |