chombo-discharge
Loading...
Searching...
No Matches
CD_ParticleManagementImplem.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_PARTICLEMANAGEMENTIMPLEM_H
14#define CD_PARTICLEMANAGEMENTIMPLEM_H
15
16// Std includes
17#include <algorithm>
18#include <array>
19#include <cstdint>
20#include <functional>
21#include <limits>
22#include <utility>
23#include <type_traits>
24
25// Chombo includes
26#include <CH_Timer.H>
27
28// Our includes
29#include <CD_Random.H>
30#include <CD_ParticleLoops.H>
32#include <CD_NamespaceHeader.H>
33
34namespace ParticleManagement {
35
37mergeMethodFromString(const std::string& a_str) noexcept
38{
39 // Legacy selectors. Each one is now a method plus a specifier, or is gone; abort with the replacement
40 // spelled out rather than silently picking one of the axes for the user.
41 const auto replaced = [&a_str](const std::string& a_replacement) -> void {
42 const std::string msg = "ParticleManagement::mergeMethodFromString - '" + a_str + "' has been replaced by " +
43 a_replacement;
44
45 MayDay::Abort(msg.c_str());
46 };
47
48 if (a_str == "none") {
50 }
51 else if (a_str == "kd_cell") {
53 }
54 else if (a_str == "kd_patch") {
56 }
57 else if (a_str == "kd_amr") {
59 }
60 else if (a_str == "nn_amr") {
62 }
63 else if (a_str == "reinitialize") {
65 }
66 else if (a_str == "external") {
68 }
69 else if (a_str == "kd_carve") {
70 replaced("'kd_amr' with ItoSolver.kd_amr_boundary = carve");
71
73 }
74 else if (a_str == "kd_skin_nn") {
75 replaced("'kd_amr' with ItoSolver.kd_amr_boundary = nn");
76
78 }
79 else if (a_str == "nn_pair_tree") {
80 replaced("'nn_amr' with ItoSolver.nn_search = tree");
81
83 }
84 else if (a_str == "nn_pair_hash") {
85 replaced("'nn_amr' with ItoSolver.nn_search = hash");
86
88 }
89 else if (a_str == "nn_pair_onecell") {
90 replaced("'nn_amr' with ItoSolver.nn_search = onecell");
91
93 }
94 else if (a_str == "nn_sfc") {
95 // Removed: a Hilbert sort followed by repeated nearest-adjacent-pair merging inside one cell. It
96 // was strictly worse than the per-cell kd merge on every axis that was measured, so it is gone
97 // rather than deprecated.
98 MayDay::Abort("ParticleManagement::mergeMethodFromString - 'nn_sfc' has been removed; use 'kd_cell'");
99
101 }
102 else if (a_str == "equal_weight_kd" || a_str == "equal_weight_kd_sampled" || a_str == "reinitialize_bvh") {
103 replaced("'kd_cell' with ItoSolver.kd_partition = weight and ItoSolver.kd_placement = " +
104 std::string(a_str == "equal_weight_kd" ? "centroid"
105 : (a_str == "equal_weight_kd_sampled" ? "sample" : "random")));
106
108 }
109 else {
110 MayDay::Abort(("ParticleManagement::mergeMethodFromString - unknown merge method '" + a_str + "'").c_str());
111
113 }
114}
115
116inline KDPartition
117kdPartitionFromString(const std::string& a_str) noexcept
118{
119 if (a_str == "weight") {
120 return KDPartition::Weight;
121 }
122 else if (a_str == "count") {
123 return KDPartition::Count;
124 }
125 else if (a_str == "hybrid") {
126 return KDPartition::Hybrid;
127 }
128 else if (a_str == "weight_capped") {
130 }
131 else {
132 const std::string msg = "ParticleManagement::kdPartitionFromString - unknown partition '" + a_str +
133 "' (expected 'weight', 'count', 'hybrid' or 'weight_capped')";
134 MayDay::Abort(msg.c_str());
135 }
136
137 return KDPartition::Weight;
138}
139
140inline KDPlacement
141kdPlacementFromString(const std::string& a_str) noexcept
142{
143 if (a_str == "centroid") {
145 }
146 else if (a_str == "sample") {
147 return KDPlacement::Sample;
148 }
149 else if (a_str == "random") {
150 return KDPlacement::Random;
151 }
152 else {
153 const std::string msg = "ParticleManagement::kdPlacementFromString - unknown placement '" + a_str +
154 "' (expected 'centroid', 'sample' or 'random')";
155 MayDay::Abort(msg.c_str());
156 }
157
159}
160
161inline KDSplitPlacement
162kdSplitPlacementFromString(const std::string& a_str) noexcept
163{
164 if (a_str == "center") {
166 }
167 else if (a_str == "jitter") {
169 }
170 else if (a_str == "cell") {
172 }
173 else {
174 const std::string msg = "ParticleManagement::kdSplitPlacementFromString - unknown split placement '" + a_str +
175 "' (expected 'center', 'jitter' or 'cell')";
176 MayDay::Abort(msg.c_str());
177 }
178
180}
181
182inline KDAmrBoundary
183kdAmrBoundaryFromString(const std::string& a_str) noexcept
184{
185 if (a_str == "carve") {
187 }
188 else if (a_str == "nn") {
189 return KDAmrBoundary::Nn;
190 }
191 else {
192 const std::string msg = "ParticleManagement::kdAmrBoundaryFromString - unknown boundary rule '" + a_str +
193 "' (expected 'carve' or 'nn')";
194 MayDay::Abort(msg.c_str());
195 }
196
198}
199
200inline NNSearch
201nnSearchFromString(const std::string& a_str) noexcept
202{
203 if (a_str == "tree") {
204 return NNSearch::Tree;
205 }
206 else if (a_str == "hash") {
207 return NNSearch::Hash;
208 }
209 else if (a_str == "onecell") {
210 return NNSearch::OneCell;
211 }
212 else {
213 const std::string msg = "ParticleManagement::nnSearchFromString - unknown search backend '" + a_str +
214 "' (expected 'tree', 'hash' or 'onecell')";
215 MayDay::Abort(msg.c_str());
216 }
217
218 return NNSearch::Tree;
219}
220
221namespace detail {
222
241inline RealVect
243 const RealVect& a_parent,
244 const RealVect& a_cellLo,
245 const RealVect& a_dx,
246 const Real a_spacing) noexcept
247{
248 if (a_placement == KDSplitPlacement::Center) {
249 return a_parent;
250 }
251
252 RealVect x;
253
254 for (int dir = 0; dir < SpaceDim; dir++) {
255 const Real cellLo = a_cellLo[dir];
256 const Real cellHi = cellLo + a_dx[dir] * (1.0 - 1.0e-12);
257
258 Real lo = cellLo;
259 Real hi = cellHi;
260
261 if (a_placement == KDSplitPlacement::Jitter) {
262 // Truncate the kernel to the cell rather than folding it back in -- see the details above.
263 lo = std::max(cellLo, a_parent[dir] - 0.5 * a_spacing);
264 hi = std::min(cellHi, a_parent[dir] + 0.5 * a_spacing);
265 }
266
267 x[dir] = (hi > lo) ? (lo + (hi - lo) * Random::getUniformReal01()) : lo;
268 }
269
270 return x;
271}
272
273template <class P, Real P::*weight, RealVect P::*position>
274inline void
275buildKDCellLeaves(const std::vector<P>& a_particles,
276 const int a_maxLeaves,
277 const Real a_weightMedianLength,
278 const bool a_capWeights,
279 const std::function<RealVect(const RealVect&)>& a_splitPosition,
280 const BinaryParticleReconcile<P>& a_particleReconcile,
281 std::vector<std::pair<const P*, const P*>>& a_leaves) noexcept
282{
283 CH_TIME("ParticleManagement::buildKDCellLeaves");
284
285 // Half-open particle range [lo, hi) into the flat working buffer, plus the node weight. This replaces
286 // the old heap-allocated, shared_ptr-linked KDNode<P> (one alloc + atomic refcount per node).
287 struct NodeRange
288 {
289 std::size_t lo;
290 std::size_t hi;
291 Real w;
292 };
293
294 // Reusable per-thread scratch: capacity is retained across calls (cleared, never freed), so after the
295 // first cell the merge does no heap allocation. The two particle buffers are ping-ponged level by level,
296 // so each node's particles are always a contiguous span -- no per-node particle list.
297 thread_local std::vector<P> s_cur;
298 thread_local std::vector<P> s_nxt;
299 thread_local std::vector<NodeRange> s_curNodes;
300 thread_local std::vector<NodeRange> s_nxtNodes;
301
302 a_leaves.clear();
303
304 const std::size_t numInput = a_particles.size();
305 if (numInput == 0 || a_maxLeaves <= 0) {
306 return;
307 }
308
309 constexpr Real splitThresh = 2.0 - std::numeric_limits<Real>::min();
310 const std::size_t maxLeaves = static_cast<std::size_t>(a_maxLeaves);
311
312 // Seed the root level. Reserve room for split products: the median split adds at most one particle per
313 // internal node, i.e. fewer than maxLeaves extra particles over the whole build. The optional weight
314 // cap below adds at most maxLeaves more, since every piece it makes carries at least the cap.
315 s_cur.clear();
316 s_cur.reserve(numInput + 2 * maxLeaves);
317 s_cur.insert(s_cur.end(), a_particles.begin(), a_particles.end());
318
319 // Total weight, read from the working copy.
320 Real W = 0.0;
321 for (const P& p : s_cur) {
322 W += p.*weight;
323 }
324
325 // Optional weight cap. A particle heavier than a leaf's target weight cannot be equalized away by any
326 // partition -- it alone sets a floor on the leaf it lands in -- so divide it into co-located integer
327 // pieces first. The division is exactly conservative and moves no mass, so unlike a median split it
328 // cannot perturb sub-cell position; it only removes the floor. Bounded growth: every piece carries at
329 // least one cap's worth of weight, so at most maxLeaves pieces are ever created.
330 if (a_capWeights && a_maxLeaves > 1) {
331 const long long cap = std::max(1LL, static_cast<long long>(std::ceil(W / static_cast<Real>(a_maxLeaves))));
332
333 const std::size_t numOriginal = s_cur.size();
334
335 for (std::size_t i = 0; i < numOriginal; i++) {
336 const long long w = static_cast<long long>(std::llround(s_cur[i].*weight));
337
338 if (w > cap) {
339 // Integer pieces, each no heavier than the cap, summing exactly to w.
340 const long long numPieces = (w + cap - 1) / cap;
341 const std::vector<long long> pieces = partitionParticleWeights<long long>(w, numPieces);
342
343 if (!pieces.empty()) {
344 const P parent = s_cur[i];
345
346 s_cur[i].*weight = static_cast<Real>(pieces[0]);
347
348 for (std::size_t k = 1; k < pieces.size(); k++) {
349 P daughter = parent;
350 daughter.*weight = static_cast<Real>(pieces[k]);
351 daughter.*position = a_splitPosition(parent.*position);
352
353 // Same contract as the median split: the caller fixes up the non-weight members.
354 a_particleReconcile(s_cur[i], daughter, parent);
355
356 s_cur.push_back(std::move(daughter));
357 }
358 }
359 }
360 }
361 }
362
363 s_curNodes.clear();
364 s_curNodes.push_back({static_cast<std::size_t>(0), s_cur.size(), W});
365
366 // Breadth-first: split every splittable leaf of the current level, streaming the children contiguously
367 // into the next buffer, until we reach maxLeaves leaves or nothing can be split further.
368 bool keepGoing = true;
369 while (keepGoing && s_curNodes.size() < maxLeaves) {
370 keepGoing = false;
371
372 s_nxt.clear();
373 s_nxtNodes.clear();
374
375 // Each split turns one leaf into two (+1 leaf), so this level may split at most this many nodes
376 // before reaching maxLeaves. Nodes beyond the budget are carried forward unchanged -- they must NOT
377 // be dropped (doing so would discard their particles and break weight conservation).
378 std::size_t splitBudget = maxLeaves - s_curNodes.size();
379
380 for (std::size_t ni = 0; ni < s_curNodes.size(); ni++) {
381 const NodeRange node = s_curNodes[ni];
382
383 // ---- split this node into s_nxt ----
384 P* const beg = s_cur.data() + node.lo;
385 P* const end = s_cur.data() + node.hi;
386 const std::size_t n = node.hi - node.lo;
387 const Real Wn = node.w;
388
389 // A. Split along the longest bounding-box extent. The box is also what selects the split rule, so
390 // it is computed before the splittability test rather than after it -- the two questions cannot be
391 // separated once the rule depends on the node's size.
392 RealVect loCorner = +std::numeric_limits<Real>::max() * RealVect::Unit;
393 RealVect hiCorner = -std::numeric_limits<Real>::max() * RealVect::Unit;
394
395 for (P* p = beg; p != end; ++p) {
396 const RealVect& pos = (*p).*position;
397
398 for (int dir = 0; dir < SpaceDim; dir++) {
399 loCorner[dir] = std::min(pos[dir], loCorner[dir]);
400 hiCorner[dir] = std::max(pos[dir], hiCorner[dir]);
401 }
402 }
403
404 const int splitDir = (hiCorner - loCorner).maxDir(true);
405
406 // A node narrower than the crossover splits at the weight median, a wider one at the count median.
407 // The caller passes a length, not a rule: 'weight' is a crossover at infinity and 'count' one below
408 // zero, so a fixed rule and the size-dependent one are the same knob (see KDPartition).
409 const bool weightMedian = (hiCorner[splitDir] - loCorner[splitDir]) <= a_weightMedianLength;
410
411 // What makes a node splittable depends on the rule that was just chosen for it. The weight median
412 // needs at least two units of weight to divide; the count median needs at least two particles, and
413 // never divides a particle, so it can drive a node down to a single member however heavy that
414 // member is.
415 const bool splittable = weightMedian ? (node.w > splitThresh) : (n >= 2);
416
417 if (splittable && splitBudget > 0) {
418 splitBudget--;
419
420 std::sort(beg, end, [splitDir](const P& p1, const P& p2) -> bool {
421 return (p1.*position)[splitDir] < (p2.*position)[splitDir];
422 });
423
424 // Count median. The two halves get as nearly equal a particle COUNT as possible, and no particle
425 // is ever divided, so this path creates no new particles and needs no reconciliation -- the leaf
426 // count can therefore never overshoot the target during the build, unlike the weight median.
427 // The weights of the halves are whatever the partition happens to give.
428 if (!weightMedian) {
429 const std::size_t mid = n / 2;
430
431 Real wl = 0.0;
432 for (std::size_t i = 0; i < mid; i++) {
433 wl += beg[i].*weight;
434 }
435
436 const std::size_t leftLo = s_nxt.size();
437 s_nxt.insert(s_nxt.end(), beg, beg + mid);
438 s_nxtNodes.push_back({leftLo, s_nxt.size(), wl});
439
440 const std::size_t rightLo = s_nxt.size();
441 s_nxt.insert(s_nxt.end(), beg + mid, end);
442 s_nxtNodes.push_back({rightLo, s_nxt.size(), Wn - wl});
443
444 keepGoing = true;
445
446 continue;
447 }
448
449 // B. Weight-balanced median particle.
450 std::size_t id = 0;
451 Real wl = 0.0;
452 Real wr = Wn - beg[0].*weight;
453
454 for (std::size_t i = 1; i < n; i++) {
455 const Real& w = beg[id].*weight;
456 if (wl + w < wr) {
457 id = i;
458 wl += w;
459 wr = Wn - wl - beg[id].*weight;
460 }
461 else {
462 break;
463 }
464 }
465
466 const P med = beg[id];
467 const Real pw = med.*weight;
468 const Real dw = wr - wl;
469
470 CH_assert(wl + wr + pw == Wn);
471
472 // C. Decide how the median is distributed; produce at most one extra particle per side.
473 bool hasExtraL = false;
474 bool hasExtraR = false;
475 P extraL;
476 P extraR;
477
478 if (pw >= splitThresh && pw >= std::abs(dw)) {
479 Real dwl = dw;
480 Real dwr = 0.0;
481
482 const Real ddw = pw - dw;
483 const long long Nsplit = (long long)ddw;
484
485 if (Nsplit > 0LL) {
486 const long long Nr = Nsplit / 2;
487 const long long Nl = Nsplit - Nr;
488
489 dwl += (ddw / Nsplit) * Nl;
490 dwr += (ddw / Nsplit) * Nr;
491 }
492
493 if (dwl > 0.0 && dwr > 0.0) {
494 // Split the median particle across both children.
495 extraL = med;
496 extraR = med;
497
498 extraL.*position = a_splitPosition(med.*position);
499 extraR.*position = a_splitPosition(med.*position);
500
501 CH_assert(dwl >= 1.0);
502 CH_assert(dwr >= 1.0);
503
504 wl += dwl;
505 wr += dwr;
506
507 extraL.*weight = dwl;
508 extraR.*weight = dwr;
509
510 a_particleReconcile(extraL, extraR, med);
511
512 hasExtraL = true;
513 hasExtraR = true;
514 }
515 else if (dwl > 0.0 && dwr == 0.0) {
516 extraL = med;
517 extraL.*position = a_splitPosition(med.*position);
518 CH_assert(dwl >= 1.0);
519 wl += dwl;
520 extraL.*weight = dwl;
521 hasExtraL = true;
522 }
523 else if (dwl == 0.0 && dwr > 0.0) {
524 extraR = med;
525 extraR.*position = a_splitPosition(med.*position);
526 CH_assert(dwr >= 1.0);
527 wr += dwr;
528 extraR.*weight = dwr;
529 hasExtraR = true;
530 }
531 else {
532 MayDay::Abort("ParticleManagement::buildKDCellLeaves - logic bust");
533 }
534 }
535 else {
536 // Median assigned whole to the lighter side (weight unchanged).
537 if (wl <= wr) {
538 wl = wl + pw;
539 extraL = med;
540 hasExtraL = true;
541 }
542 else {
543 wr = wr + pw;
544 extraR = med;
545 hasExtraR = true;
546 }
547 }
548
549 CH_assert(std::abs(wl + wr - Wn) <= Wn * std::numeric_limits<Real>::epsilon() * 16);
550 CH_assert(std::abs(wl - wr) <= 1.0);
551
552 // D. Stream the two children contiguously into s_nxt. Particle order within each child matches the
553 // old implementation (base half first, then the median's piece), so the leaf reductions are
554 // bit-for-bit identical. NOTE: reading from s_cur (stable) and writing to s_nxt (separate buffer).
555 const std::size_t leftLo = s_nxt.size();
556 s_nxt.insert(s_nxt.end(), beg, beg + id);
557 if (hasExtraL) {
558 s_nxt.push_back(std::move(extraL));
559 }
560 s_nxtNodes.push_back({leftLo, s_nxt.size(), wl});
561
562 const std::size_t rightLo = s_nxt.size();
563 s_nxt.insert(s_nxt.end(), beg + id + 1, end);
564 if (hasExtraR) {
565 s_nxt.push_back(std::move(extraR));
566 }
567 s_nxtNodes.push_back({rightLo, s_nxt.size(), wr});
568
569 keepGoing = true;
570 }
571 else {
572 // Cannot split further -- carry this leaf forward unchanged.
573 const std::size_t lo = s_nxt.size();
574 s_nxt.insert(s_nxt.end(), s_cur.begin() + node.lo, s_cur.begin() + node.hi);
575 s_nxtNodes.push_back({lo, s_nxt.size(), node.w});
576 }
577 }
578
579 s_cur.swap(s_nxt);
580 s_curNodes.swap(s_nxtNodes);
581 }
582
583 // Emit the leaves as contiguous ranges into the (now stable) working buffer.
584 a_leaves.reserve(s_curNodes.size());
585 for (const NodeRange& r : s_curNodes) {
586 const P* const base = s_cur.data() + r.lo;
587 a_leaves.emplace_back(base, base + (r.hi - r.lo));
588 }
589}
590
591} // namespace detail
592
593template <class Packed, Real Packed::*packWeight, RealVect Packed::*packPosition, class P, class Traits>
594inline ParticleMerger<P, Traits>
596 const Real a_weightMedianCellWidths,
597 const bool a_capWeights,
598 const KDSplitPlacement a_splitPlacement,
599 std::function<RealVect()> a_probLo,
600 std::function<Packed(const ParticleSoA<P, Traits>&, std::size_t)> a_gather,
602 std::function<void(ParticleSoA<P, Traits>&, const Packed*, const Packed*, const CellInfo&)> a_scatterLeaf) noexcept
603{
604 return [=](ParticleSoA<P, Traits>& a_particles, const CellInfo& a_cellInfo, int a_ppc) noexcept {
605 CH_TIMERS("ParticleManagement::makeKDCellMerger");
606 CH_TIMER("ParticleManagement::makeKDCellMerger::populate", t1);
607 CH_TIMER("ParticleManagement::makeKDCellMerger::build_kd", t2);
608 CH_TIMER("ParticleManagement::makeKDCellMerger::scatter", t3);
609
610 CH_START(t1);
611 thread_local std::vector<Packed> particles;
612
613 particles.clear();
614 particles.reserve(a_particles.size());
615
616 Real W = 0.0;
617
618 for (std::size_t i = 0; i < a_particles.size(); i++) {
619 Packed p = a_gather(a_particles, i);
620 W += p.*packWeight;
621 particles.emplace_back(std::move(p));
622 }
623 CH_STOP(t1);
624
625 if (W < 2.0 || a_ppc <= 0) {
626 return;
627 }
628
629 // Where a split particle's pieces go. Center is the identity, so the default costs nothing. In a cut
630 // cell every rule falls back to Center: a displaced piece could otherwise land inside the boundary.
631 const Real dxCell = a_cellInfo.getDx();
632 const RealVect cellLo = a_probLo() + dxCell * RealVect(a_cellInfo.getGridIndex());
633 const bool regular = a_cellInfo.getVolFrac() >= 1.0;
634
635 // Local mean interparticle spacing -- the finest scale this cell's population can resolve, and so the
636 // natural jitter bandwidth. No free parameter.
637 const Real spacing = dxCell / std::pow(static_cast<Real>(std::max(1, a_ppc)), 1.0 / SpaceDim);
638
639 const std::function<RealVect(const RealVect&)> splitPosition = [=](const RealVect& a_parent) -> RealVect {
640 if (!regular || a_splitPlacement == KDSplitPlacement::Center) {
641 return a_parent;
642 }
643
644 return detail::kdSplitDisplace(a_splitPlacement, a_parent, cellLo, dxCell * RealVect::Unit, spacing);
645 };
646
647 CH_START(t2);
648 thread_local std::vector<std::pair<const Packed*, const Packed*>> leaves;
649
650 // The crossover is given in cell widths, so it becomes a length only once the cell is known.
651 detail::buildKDCellLeaves<Packed, packWeight, packPosition>(particles,
652 a_ppc,
653 a_weightMedianCellWidths * a_cellInfo.getDx(),
654 a_capWeights,
655 splitPosition,
656 a_reconcile,
657 leaves);
658 CH_STOP(t2);
659
660 CH_START(t3);
661 a_particles.clear();
662 for (const auto& leaf : leaves) {
663 a_scatterLeaf(a_particles, leaf.first, leaf.second, a_cellInfo);
664 }
665 CH_STOP(t3);
666 };
667}
668
669template <class Context, class P, class Traits>
670inline ParticleMerger<P, Traits>
671makeReinitializeMerger(std::function<std::pair<long long, Context>(const ParticleSoA<P, Traits>&)> a_aggregate,
672 std::function<void(ParticleSoA<P, Traits>&, const RealVect&, long long, const Context&)> a_emit,
673 std::function<RealVect()> a_probLo) noexcept
674{
675 return [=](ParticleSoA<P, Traits>& a_particles, const CellInfo& a_cellInfo, int a_ppc) noexcept {
676 CH_TIME("ParticleManagement::makeReinitializeMerger");
677
678 if (a_ppc <= 0) {
679 return;
680 }
681
682 const auto [numPhysical, context] = a_aggregate(a_particles);
683
684 if (numPhysical <= 0LL) {
685 return;
686 }
687
688 const std::vector<long long> weights = partitionParticleWeights(numPhysical, (long long)a_ppc);
689
690 const Real dx = a_cellInfo.getDx();
691 const Real kappa = a_cellInfo.getVolFrac();
692 const RealVect cellPos = a_probLo() + dx * (a_cellInfo.getGridIndex() + 0.5 * RealVect::Unit);
693 const RealVect& validLo = a_cellInfo.getValidLo();
694 const RealVect& validHi = a_cellInfo.getValidHi();
695 const RealVect& bndryCentroid = a_cellInfo.getBndryCentroid();
696 const RealVect& bndryNormal = a_cellInfo.getBndryNormal();
697
698 a_particles.clear();
699
700 for (const long long wt : weights) {
701 const RealVect x = Random::randomPosition(cellPos, validLo, validHi, bndryCentroid, bndryNormal, dx, kappa);
702 a_emit(a_particles, x, wt, context);
703 }
704 };
705}
706
707template <typename P, typename Traits, typename T, typename>
708inline void
709removePhysicalParticles(ParticleSoA<P, Traits>& a_particles, const T a_numPhysPartToRemove) noexcept
710{
711 CH_TIME("ParticleManagement::removePhysicalParticles(SoA)");
712
713 constexpr T zero = (T)0;
714
715 if (a_numPhysPartToRemove < zero) {
716 MayDay::Error("ParticleManagement::removePhysicalParticles(SoA) - 'a_numPhysPartoToRemove < 0'");
717 }
718
719 const std::size_t numComp = a_particles.size();
720
721 if (numComp > 0) {
722 T numRemoved = zero;
723
724 // 1. Compute the minimum particle weight.
725 T minWeight = std::numeric_limits<T>::max();
726 for (std::size_t i = 0; i < numComp; i++) {
727 minWeight = std::min(minWeight, (T)a_particles.weight(i));
728 }
729
730 // 2. Trim particle weights down to minWeight.
731 for (std::size_t i = 0; i < numComp; i++) {
732 const T diff1 = (T)a_particles.weight(i) - minWeight;
733 const T diff2 = a_numPhysPartToRemove - numRemoved;
734
735 CH_assert(diff1 >= zero);
736 CH_assert(diff2 >= zero);
737
738 const T r = std::max(0LL, std::min(diff1, diff2));
739
740 a_particles.weight(i) -= 1.0 * r;
741 numRemoved += r;
742 }
743
744 // 3. "Uniformly" subtract the particle weights.
745 if (a_numPhysPartToRemove - numRemoved > zero) {
746 const T numCompParticles = (T)numComp;
747 const T uniformWeight = (a_numPhysPartToRemove - numRemoved) / numCompParticles;
748 const T uniformRemainder = (a_numPhysPartToRemove - numRemoved) % numCompParticles;
749
750 if (uniformWeight > zero) {
751 // uniformWeight is constant over the loop, so the running accumulation equals
752 // uniformWeight * numComp -- hoist it out so the body is a pure elementwise update.
753 double* const w = a_particles.weightColumn();
754
755 ParticleLoops::loop(a_particles, [&](const std::size_t i) {
756 w[i] -= 1.0 * uniformWeight;
757 });
758
759 numRemoved += uniformWeight * static_cast<T>(numComp);
760 }
761
762 if (uniformRemainder > zero) {
763 T W = 0;
764
765 for (std::size_t i = 0; i < numComp; i++) {
766
767 // Never remove so that weight is negative.
768 const T w = std::min((T)a_particles.weight(i), uniformRemainder - W);
769
770 a_particles.weight(i) -= 1.0 * w;
771
772 W += w;
773 numRemoved += w;
774
775 if (W == uniformRemainder) {
776 break;
777 }
778 }
779 }
780 }
781
782 CH_assert(numRemoved == a_numPhysPartToRemove);
783 }
784}
785
786template <typename P, typename Traits>
787inline void
788deleteParticles(ParticleSoA<P, Traits>& a_particles, const Real a_weightThresh) noexcept
789{
790 CH_TIME("ParticleManagement::deleteParticles(SoA)");
791
792 // Swap-and-pop: do not advance the index after a removal (a new particle now occupies slot i).
793 std::size_t i = 0;
794 while (i < a_particles.size()) {
795 if (a_particles.weight(i) < a_weightThresh) {
796 a_particles.remove(i);
797 }
798 else {
799 i++;
800 }
801 }
802}
803
804template <typename T, typename>
805inline void
806partitionParticleWeights(std::vector<T>& a_weights, const T a_numPhysicalParticles, const T a_maxCompParticles) noexcept
807{
808 // assign() rather than resize() throughout: it leaves the buffer's capacity alone, which is the whole
809 // point of the overload. The clear() covers the one path that writes nothing.
810 a_weights.clear();
811
812 constexpr T zero = (T)0;
813 constexpr T one = (T)1;
814
815 if (a_maxCompParticles > zero) {
816 if (a_numPhysicalParticles <= a_maxCompParticles) {
817 a_weights.assign(a_numPhysicalParticles, one);
818 }
819 else {
820 const T W = a_numPhysicalParticles / a_maxCompParticles;
821 T r = a_numPhysicalParticles % a_maxCompParticles;
822
823 if (W > zero) {
824 a_weights.assign(a_maxCompParticles, W);
825
826 for (std::size_t i = 0; i < a_weights.size() && r > zero; i++) {
827 a_weights[i] += one;
828 r--;
829 }
830 }
831 else {
832 a_weights.assign(1, r);
833 }
834 }
835 }
836}
837
838template <typename T, typename>
839inline std::vector<T>
840partitionParticleWeights(const T a_numPhysicalParticles, const T a_maxCompParticles) noexcept
841{
842 std::vector<T> ret;
843
844 partitionParticleWeights(ret, a_numPhysicalParticles, a_maxCompParticles);
845
846 return ret;
847}
848
849namespace detail {
850
851template <typename T, typename>
852inline T
853partitionParticles(const T a_numParticles)
854{
855#ifdef CH_MPI
856 const T quotient = a_numParticles / numProc();
857 const T remainder = a_numParticles % numProc();
858
859 Vector<T> particlesPerRank(numProc(), quotient);
860
861 for (int i = 0; i < remainder; i++) {
862 particlesPerRank[i]++;
863 }
864
865 return particlesPerRank[procID()];
866#else
867 return a_numParticles;
868#endif
869}
870
871} // namespace detail
872
873template <typename P, typename Traits, typename T, typename>
874inline void
875drawRandomParticles(ParticleSoA<P, Traits>& a_particles,
876 const T a_numParticles,
877 const std::function<RealVect()>& a_distribution)
878{
879 a_particles.clear();
880
881 // Each rank draws its own share (unit weight) into the buffer. Routing to owners happens later when
882 // the buffer is added to a ParticleContainer.
883 const T numParticles = detail::partitionParticles(a_numParticles);
884
885 for (T t = 0; t < numParticles; t++) {
886 a_particles.append(a_distribution(), 1.0);
887 }
888}
889
890template <typename P, typename Traits, typename T, typename>
891inline void
892drawGaussianParticles(ParticleSoA<P, Traits>& a_particles,
893 const T a_numParticles,
894 const RealVect& a_center,
895 const Real a_radius) noexcept
896{
897 CH_TIME("ParticleManagement::drawGaussianParticles(SoA)");
898
899 std::normal_distribution<Real> gauss(0.0, a_radius);
900
901 auto ranGauss = [&]() -> RealVect {
902 return a_center + Random::get(gauss) * Random::getDirection();
903 };
904
905 drawRandomParticles(a_particles, a_numParticles, ranGauss);
906}
907
908template <typename P, typename Traits, typename T, typename>
909inline void
910drawBoxParticles(ParticleSoA<P, Traits>& a_particles,
911 const T a_numParticles,
912 const RealVect& a_loCorner,
913 const RealVect& a_hiCorner) noexcept
914{
915 CH_TIME("ParticleManagement::drawBoxParticles(SoA)");
916
917 CH_assert(a_hiCorner >= a_loCorner);
918
919 auto ranBox = [&]() -> RealVect {
920 return RealVect(D_DECL(a_loCorner[0] + (a_hiCorner[0] - a_loCorner[0]) * Random::getUniformReal01(),
921 a_loCorner[1] + (a_hiCorner[1] - a_loCorner[1]) * Random::getUniformReal01(),
922 a_loCorner[2] + (a_hiCorner[2] - a_loCorner[2]) * Random::getUniformReal01()));
923 };
924
925 drawRandomParticles(a_particles, a_numParticles, ranBox);
926}
927
928template <typename P, typename Traits, typename T, typename>
929inline void
930drawSphereParticles(ParticleSoA<P, Traits>& a_particles,
931 const T a_numParticles,
932 const RealVect& a_center,
933 const Real a_radius) noexcept
934{
935 CH_TIME("ParticleManagement::drawSphereParticles(SoA)");
936
937 auto ranSphere = [&]() -> RealVect {
938 RealVect x = std::numeric_limits<Real>::max() * RealVect::Unit;
939
940 while (x.vectorLength() > a_radius) {
941 for (int d = 0; d < SpaceDim; d++) {
942 x[d] = a_radius * Random::getUniformReal11();
943 }
944 }
945
946 return x + a_center;
947 };
948
949 drawRandomParticles(a_particles, a_numParticles, ranSphere);
950}
951} // namespace ParticleManagement
952
953#include <CD_NamespaceFooter.H>
954
955#endif
Declaration of a namespace for SIMD-decorated loops over SoA particles.
Namespace containing various particle management utilities.
File containing some useful static methods related to random number generation.
Class for the cell-information that is often queried when merging particles inside a cell.
Definition CD_CellInfo.H:26
Arena-backed Struct-of-Arrays particle container for a single grid patch.
Definition CD_ParticleSoA.H:655
void append(const RealVect &a_position, const double a_weight)
Append one particle with a default-constructed payload.
Definition CD_ParticleSoA.H:962
std::size_t size() const noexcept
Number of particles currently stored.
Definition CD_ParticleSoA.H:882
void clear() noexcept
Drop all particles (keeps the arena; invalidates the cell sort unless there was nothing to drop).
Definition CD_ParticleSoA.H:915
static Real get(T &a_distribution)
For getting a random number from a user-supplied distribution. T must be a distribution for which we ...
Definition CD_RandomImplem.H:219
static RealVect getDirection()
Get a random direction in space.
Definition CD_RandomImplem.H:182
static Real getUniformReal11()
Get a uniform real number on the interval [-1,1].
Definition CD_RandomImplem.H:166
static Real getUniformReal01()
Get a uniform real number on the interval [0,1].
Definition CD_RandomImplem.H:158
static RealVect randomPosition(const RealVect &a_lo, const RealVect &a_hi) noexcept
Return a random position in the cube (a_lo, a_hi);.
Definition CD_RandomImplem.H:286
ALWAYS_INLINE void loop(const ParticleSoA< P, Traits > &a_soa, Functor &&a_kernel)
Launch a kernel over every particle in a ParticleSoA, decorating the loop with CD_PRAGMA_SIMD.
Definition CD_ParticleLoops.H:87
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 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.
Definition CD_ParticleManagementImplem.H:275
Namespace for various particle management tools.
Definition CD_KDParticleMerge.H:33
KDAmrBoundary kdAmrBoundaryFromString(const std::string &a_str) noexcept
Turn an input string into a kd AMR boundary rule.
Definition CD_ParticleManagementImplem.H:183
KDSplitPlacement kdSplitPlacementFromString(const std::string &a_str) noexcept
Turn an input string into a split placement rule.
Definition CD_ParticleManagementImplem.H:162
KDSplitPlacement
Where the pieces of a split particle go.
Definition CD_ParticleManagement.H:65
@ Cell
Each piece drawn uniformly in the owning cell.
@ Jitter
Each piece drawn from the local mean interparticle spacing around the parent, truncated to the cell.
@ Center
Every piece at the parent's position. No spatial perturbation, duplicate positions.
std::function< void(P &p1, P &p2, const P &p0)> BinaryParticleReconcile
Declaration of a reconciliation function when splitting particles.
Definition CD_ParticleManagement.H:207
ParticleMerger< P, Traits > makeKDCellMerger(const Real a_weightMedianCellWidths, const bool a_capWeights, const KDSplitPlacement a_splitPlacement, std::function< RealVect()> a_probLo, std::function< Packed(const ParticleSoA< P, Traits > &, std::size_t)> a_gather, BinaryParticleReconcile< Packed > a_reconcile, std::function< void(ParticleSoA< P, Traits > &, const Packed *, const Packed *, const CellInfo &)> a_scatterLeaf) noexcept
Create a per-cell KD-tree super-particle merger as a reusable ParticleMerger.
Definition CD_ParticleManagementImplem.H:595
KDPartition
How a kd merge divides a node into two children.
Definition CD_ParticleManagement.H:48
@ WeightCapped
PROTOTYPE. Cap every particle at the target leaf weight, then split at the weight median.
@ Weight
Split at the weight median at every node; halves carry near-equal weight.
@ Count
Split at the count median at every node; halves hold near-equal particle counts.
@ Hybrid
Count median while the node is wider than the crossover length, weight median below it.
KDAmrBoundary
How the AMR-scope kd merge resolves the leaves that touch a patch or rank boundary.
Definition CD_ParticleManagement.H:94
@ Nn
Merge the interior with the kd tree and the boundary skin with nearest-neighbour pairs.
@ Carve
Arbitrate contested particles between patches (z-buffer carve).
KDPartition kdPartitionFromString(const std::string &a_str) noexcept
Turn an input string into a kd partition rule.
Definition CD_ParticleManagementImplem.H:117
ParticleMerger< P, Traits > makeReinitializeMerger(std::function< std::pair< long long, Context >(const ParticleSoA< P, Traits > &)> a_aggregate, std::function< void(ParticleSoA< P, Traits > &, const RealVect &, long long, const Context &)> a_emit, std::function< RealVect()> a_probLo) noexcept
Create a reinitialize super-particle merger as a reusable ParticleMerger.
Definition CD_ParticleManagementImplem.H:671
NNSearch nnSearchFromString(const std::string &a_str) noexcept
Turn an input string into a nearest-neighbour search backend.
Definition CD_ParticleManagementImplem.H:201
NNSearch
How the AMR-scope nearest-neighbour merge finds a particle's merge candidates.
Definition CD_ParticleManagement.H:105
@ Tree
One whole-patch PointCloudBVH per patch.
@ OneCell
One PointCloudBVH per occupied cell; merge distance structurally fixed at 1 cell.
@ Hash
One whole-patch PointCloudHashGrid per patch.
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.
ParticleMergeMethod
The super-particle merge methods, named by the scope they group particles over.
Definition CD_ParticleManagement.H:118
@ KdPatch
Patch: mergeKDPatch, one kd tree per patch, patch-local (no ghosts).
@ NnAmr
AMR: nearest-neighbour pair merge over the hierarchy – see NNSearch.
@ KdCell
Cell: makeKDCellMerger, one kd tree per cell.
@ KdAmr
AMR: one kd tree per patch, resolved across patches – see KDAmrBoundary.
@ External
Cell: caller-supplied per-cell merger.
@ Reinitialize
Cell: makeReinitializeMerger; positions discarded and redrawn.
ParticleMergeMethod mergeMethodFromString(const std::string &a_str) noexcept
Map a merge-method selector string to a ParticleMergeMethod.
Definition CD_ParticleManagementImplem.H:37
KDPlacement kdPlacementFromString(const std::string &a_str) noexcept
Turn an input string into a kd placement rule.
Definition CD_ParticleManagementImplem.H:141