flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
block_container.hpp
Go to the documentation of this file.
1
57#ifndef PECLET_FLOW_VOF_BLOCK_CONTAINER_HPP
58#define PECLET_FLOW_VOF_BLOCK_CONTAINER_HPP
59
60#include <algorithm>
61#include <array>
62#include <cmath>
63#include <cstdio>
64#include <Kokkos_Core.hpp>
65#include <map>
66#include <memory>
67#include <stdexcept>
68#include <string>
69#include <vector>
70
71#include "mac_stencils.hpp" // peclet::flow::SExec, SField, I3, L3
72#include "peclet/core/decomp/block_decomposer.hpp"
73#include "vof/advect_wy.hpp"
74#include "vof/block_batch.hpp"
75#include "vof/colour_field.hpp"
78#include "policy.hpp"
79
80namespace peclet::flow::vof {
81
82// ---------------------------------------------------------------------------------------------
83// index boxes
84// ---------------------------------------------------------------------------------------------
85
88struct VofBox {
89 int lo[3]{0, 0, 0};
90 int hi[3]{0, 0, 0};
91
92 int n(int d) const { return hi[d] - lo[d]; }
93 long cells() const { return static_cast<long>(n(0)) * n(1) * n(2); }
94 bool empty() const { return n(0) <= 0 || n(1) <= 0 || n(2) <= 0; }
95 I3 size() const { return I3{n(0), n(1), n(2)}; }
96 I3 origin() const { return I3{lo[0], lo[1], lo[2]}; }
97
98 bool contains(const VofBox& o) const {
99 for (int d = 0; d < 3; ++d)
100 if (o.lo[d] < lo[d] || o.hi[d] > hi[d])
101 return false;
102 return true;
103 }
104 bool operator==(const VofBox& o) const {
105 for (int d = 0; d < 3; ++d)
106 if (lo[d] != o.lo[d] || hi[d] != o.hi[d])
107 return false;
108 return true;
109 }
110 bool operator!=(const VofBox& o) const { return !(*this == o); }
111
112 static VofBox grown(const VofBox& b, int m) {
113 VofBox r = b;
114 for (int d = 0; d < 3; ++d) {
115 r.lo[d] -= m;
116 r.hi[d] += m;
117 }
118 return r;
119 }
121 static VofBox intersect(const VofBox& a, const VofBox& b) {
122 VofBox r;
123 for (int d = 0; d < 3; ++d) {
124 r.lo[d] = a.lo[d] > b.lo[d] ? a.lo[d] : b.lo[d];
125 r.hi[d] = a.hi[d] < b.hi[d] ? a.hi[d] : b.hi[d];
126 }
127 return r;
128 }
129};
130
138struct VofRun {
139 int g0 = 0;
140 int len = 0;
141 int loc0 = 0;
142};
143
150inline int vofAxisRuns(int a, int b, int gs, bool periodic, VofRun out[5], int outside = 0) {
151 int n = 0;
152 if (b <= a)
153 return 0;
154 if (!periodic) {
155 const int s = a < -outside ? -outside : a;
156 const int e = b > gs + outside ? gs + outside : b;
157 if (e > s)
158 out[n++] = VofRun{s, e - s, s - a};
159 return n;
160 }
161 if (b - a > 3 * gs) // guarded by vofClampBox(); a block box is at most the whole axis
162 throw std::runtime_error("peclet::flow::vof: block range exceeds three periodic images");
163 int k = a;
164 while (k < b && n < 5) {
165 const int w = ((k % gs) + gs) % gs;
166 int len = gs - w;
167 if (len > b - k)
168 len = b - k;
169 out[n++] = VofRun{w, len, k - a};
170 k += len;
171 }
172 return n;
173}
174
177inline VofBox vofClampBox(VofBox b, I3 gs, const std::array<bool, 3>& per) {
178 const int g[3] = {gs.x, gs.y, gs.z};
179 for (int d = 0; d < 3; ++d) {
180 if (per[d]) {
181 if (b.n(d) > g[d]) {
182 b.lo[d] = 0;
183 b.hi[d] = g[d];
184 }
185 } else {
186 if (b.lo[d] < 0)
187 b.lo[d] = 0;
188 if (b.hi[d] > g[d])
189 b.hi[d] = g[d];
190 if (b.hi[d] <= b.lo[d]) {
191 b.lo[d] = 0;
192 b.hi[d] = g[d] < 1 ? 1 : g[d];
193 }
194 }
195 }
196 return b;
197}
198
199// ---------------------------------------------------------------------------------------------
200// master assignment (rung W1 item a)
201// ---------------------------------------------------------------------------------------------
202
207enum class VofMasterAssign {
208 RoundRobin = 0,
209 Lpt = 1,
210 WeightedOrb = 2
211};
212
217inline void vofAssignLpt(const std::vector<long>& w, int size, std::vector<int>& master) {
218 const std::size_t n = w.size();
219 master.assign(n, 0);
220 if (n == 0 || size <= 1)
221 return;
222 std::vector<std::size_t> ord(n);
223 for (std::size_t i = 0; i < n; ++i)
224 ord[i] = i;
225 // stable, and the comparator is a strict weak ordering on the weight alone, so equal weights
226 // keep block-id order -> with equal blocks LPT reproduces the round robin exactly.
227 std::stable_sort(ord.begin(), ord.end(),
228 [&](std::size_t a, std::size_t b) { return w[a] > w[b]; });
229 std::vector<long> load(static_cast<std::size_t>(size), 0);
230 for (std::size_t k = 0; k < n; ++k) {
231 int best = 0;
232 for (int r = 1; r < size; ++r)
233 if (load[static_cast<std::size_t>(r)] < load[static_cast<std::size_t>(best)])
234 best = r;
235 master[ord[k]] = best;
236 load[static_cast<std::size_t>(best)] += w[ord[k]];
237 }
238}
239
246inline void vofAssignOrb(const std::vector<long>& w, int size, std::vector<int>& master) {
247 const std::size_t n = w.size();
248 master.assign(n, 0);
249 if (n == 0 || size <= 1)
250 return;
251 std::vector<peclet::core::Real> wr(n);
252 for (std::size_t i = 0; i < n; ++i)
253 wr[i] = static_cast<peclet::core::Real>(w[i]);
254 peclet::core::decomp::BlockDecomposer<1> dec(
255 static_cast<std::size_t>(size),
256 peclet::core::IVec<1>{static_cast<peclet::core::Index>(n)}, wr);
257 for (int r = 0; r < size; ++r) {
258 const auto b = dec.block(static_cast<std::size_t>(r));
259 for (auto i = b.origin[0]; i < b.origin[0] + b.size[0]; ++i)
260 master[static_cast<std::size_t>(i)] = r;
261 }
262}
263
264// ---------------------------------------------------------------------------------------------
265// one block
266// ---------------------------------------------------------------------------------------------
267
270 long id = 0;
271 int master = 0;
272 int lo[3]{0, 0, 0};
273 int hi[3]{0, 0, 0};
274 double volume = 0.0;
275 double centroid[3]{0, 0, 0};
276 double velocity[3]{0, 0, 0};
278 double moment[6]{0, 0, 0, 0, 0, 0};
279 bool recentred = false;
280 long cells = 0;
284 double discarded = 0.0;
290 double area = 0.0;
298 long debrisCells = 0;
299 double debrisVolume = 0.0;
300 double debrisReturned = 0.0;
301 double debrisLost = 0.0;
305 long fullAxis = 0;
308 double residueReturned = 0.0;
309};
310
313class VofBlock {
314 public:
315 long id = 0;
316 int master = 0;
321
322 bool mine() const { return mine_; }
323 WyAdvector& advector() { return adv_; }
324 const WyAdvector& advector() const { return adv_; }
325 VofBox extended(int ghost) const { return VofBox::grown(box, ghost); }
326 const VofBlockStats& stats() const { return st_; }
331 VofCurvature& curvature() { return curv_; }
332 const VofCurvature& curvature() const { return curv_; }
333 SField csfForce(int c) const { return f_[c]; }
338 static constexpr int kAuxLen = 10;
339 void serializeAux(double out[kAuxLen]) const {
340 for (int d = 0; d < 3; ++d)
341 out[d] = prevCentroid_[d];
342 out[3] = hasPrev_ ? 1.0 : 0.0;
343 out[4] = st_.debrisReturned;
344 out[5] = st_.debrisLost;
345 out[6] = static_cast<double>(st_.debrisUnresolved); // a cell count: exact in a double
346 out[7] = st_.discarded; // §12.2: the re-centring ledger migrates too
347 out[8] = static_cast<double>(st_.fullAxis);
348 out[9] = st_.residueReturned;
349 }
350 void deserializeAux(const double in[kAuxLen]) {
351 for (int d = 0; d < 3; ++d)
352 prevCentroid_[d] = in[d];
353 hasPrev_ = (in[3] != 0.0);
354 st_.debrisReturned = in[4];
355 st_.debrisLost = in[5];
356 st_.debrisUnresolved = static_cast<long>(in[6]);
357 st_.discarded = in[7];
358 st_.fullAxis = static_cast<long>(in[8]);
359 st_.residueReturned = in[9];
360 }
361
362 friend class VofBlockSet;
363
364 private:
365 bool mine_ = false;
366 bool allocated_ = false;
367 WyAdvector adv_;
368 VofCurvature curv_;
369 SField f_[3];
370 VofBlockStats st_;
371 double prevCentroid_[3]{0, 0, 0};
372 bool hasPrev_ = false;
373};
374
375// ---------------------------------------------------------------------------------------------
376// the exchange interface (implemented in vof/block_exchange.hpp)
377// ---------------------------------------------------------------------------------------------
378
379class VofBlockSet;
380
384 virtual ~VofBlockExchangeBase() = default;
393 virtual void gatherFaceVel(std::vector<VofBlock>& blocks, int ghost) = 0;
396 virtual void scatterColourMax(std::vector<VofBlock>& blocks, SField cLocal) = 0;
398 virtual void syncTable(std::vector<VofBlock>& blocks) = 0;
402 virtual void gatherColour(std::vector<VofBlock>& blocks, SField cLocal) = 0;
406 virtual void scatterForceSum(std::vector<VofBlock>& blocks, SField fx, SField fy, SField fz) = 0;
410 virtual void scatterColourSum(std::vector<VofBlock>&, SField) {
411 throw std::runtime_error("peclet::flow::vof: this block exchange has no colour SUM scatter");
412 }
416 virtual void migrateColour(std::vector<VofBlock>& blocks, const std::vector<int>& oldMaster) = 0;
418 virtual long gatherBytes() const { return 0; }
419 virtual long scatterBytes() const { return 0; }
420};
421
422// ---------------------------------------------------------------------------------------------
423// the block set
424// ---------------------------------------------------------------------------------------------
425
429 public:
435 void init(I3 gs, std::array<bool, 3> per, int rank, int size, double h = 1.0) {
436 h_ = h;
437 gs_ = gs;
438 per_ = per;
439 rank_ = rank;
440 size_ = size < 1 ? 1 : size;
441 blocks_.clear();
442 statsPend_.clear();
443 step_ = 0;
444 }
445
449 void setMetric(const VofMetric& g) {
450 metric_ = g;
451 curvProto.metric = g;
452 for (auto& b : blocks_) {
453 b.adv_.metric = g;
454 b.curv_.metric = g;
455 }
456 }
457 const VofMetric& metric() const { return metric_; }
458
459 void setExchange(std::shared_ptr<VofBlockExchangeBase> x) { exch_ = std::move(x); }
460 VofBlockExchangeBase* exchange() const { return exch_.get(); }
461
462 int margin() const { return margin_; }
463 int ghost() const { return ghost_; }
467 int recentrePad = 2;
474 double cflLimit = 0.25;
480 bool batched = true;
493 bool usePool = true;
494 int poolCapacity = 8;
497 bool csfEnabled = false;
498 double sigma = 0.0;
512 VofCurvature c;
513 c.kappaMax = -1.0;
514 return c;
515 }
522 double bubbleEps = 1e-12;
528 double wispEps = 0.0;
530 void setWispEps(double eps) {
531 wispEps = eps;
532 curvProto.pureEps = eps; // the cascade's purity test follows the advector (review finding 1)
533 for (auto& b : blocks_)
534 if (b.allocated_) {
535 b.adv_.wispEps = eps;
536 b.curv_.pureEps = eps;
537 }
538 }
546 double areaEps = 1e-8;
549 bool allowShrink = true;
555 static constexpr double kDebrisMaxC = 0.5;
558 bool debrisCensus = true;
561 bool debrisRemove = false;
564 bool timingOn = false;
565 double debrisSeconds = 0.0;
566 long debrisCalls = 0;
567 static double tick_() {
568 Kokkos::fence();
570 }
573 static constexpr double kDebrisMinAttached = 1.0;
574 double cellSize() const { return h_; }
575
576 std::size_t count() const { return blocks_.size(); }
579 std::vector<VofBlock>& blocks() {
580 flushStats();
581 return blocks_;
582 }
583 const std::vector<VofBlock>& blocks() const { return blocks_; }
584 long step() const { return step_; }
585 void setStep(long s) { step_ = s; }
586
590 int masterOf(long id) const { return static_cast<int>(id % size_); }
591
598 void seedSphere(double cx, double cy, double cz, double r, int subLevels = 4, int extraPad = 0) {
599 flushStats(); // C2: deferred statistics are indexed by block; apply them before the table grows
600 VofBox bb;
601 const double c[3] = {cx, cy, cz};
602 for (int d = 0; d < 3; ++d) {
603 bb.lo[d] = static_cast<int>(std::floor((c[d] - r) / h_)) - 1;
604 bb.hi[d] = static_cast<int>(std::ceil((c[d] + r) / h_)) + 1;
605 }
606 VofBlock b;
607 b.id = static_cast<long>(blocks_.size());
608 b.master = masterOf(b.id);
609 b.box = vofClampBox(VofBox::grown(bb, margin_ + extraPad), gs_, per_);
610 b.mine_ = (b.master == rank_);
611 blocks_.push_back(std::move(b));
612 const std::size_t idx = blocks_.size() - 1;
613 // Re-install every earlier block's hook: `push_back` may have reallocated the table, and the
614 // hooks capture `this` + an index (so they survive it), but the advectors themselves moved.
615 for (std::size_t k = 0; k + 1 < blocks_.size(); ++k)
616 if (blocks_[k].mine_ && blocks_[k].allocated_)
617 installHook(k);
618 VofBlock& nb = blocks_[idx];
619 if (nb.mine_) {
620 allocate(idx);
621 const I3 e = nb.adv_.extent();
622 const int g = ghost_;
623 const I3 o = nb.box.origin();
624 const double hh = h_;
625 SField cf = nb.adv_.colour();
626 Kokkos::parallel_for(
627 "vof::block::seed_sphere",
628 MDRange3<SExec>(SExec(), {0, 0, 0}, {e.x, e.y, e.z}),
629 KOKKOS_LAMBDA(int x, int y, int z) {
630 const double gx = (x - g + o.x) * hh, gy = (y - g + o.y) * hh, gz = (z - g + o.z) * hh;
631 cf(L3(x, y, z, e)) = sphereCellFraction(cx, cy, cz, r, gx, gy, gz, hh, subLevels);
632 });
633 Kokkos::fence();
634 fillBlockGhosts(blocks_[idx]);
635 measure(blocks_[idx], 0.0);
636 }
637 }
638
644 void advect(double dt, SField cLocal) {
645 if (!exch_)
646 throw std::runtime_error("peclet::flow::vof::VofBlockSet: no exchange installed");
647 exch_->gatherFaceVel(blocks_, ghost_);
648 std::vector<VofBlock*> deb;
649 const bool batch = batchEligible();
650 if (batch)
651 advectBatched(dt); // read #1 also applies the previous step's deferred statistics (C2)
652 else
653 flushStats();
654 for (auto& b : blocks_) {
655 if (!b.mine_)
656 continue;
657 if (!batch)
658 b.adv_.advect(dt, step_);
659 b.st_.recentred = false;
660 // Before the re-centring, so the bubble box a re-centre measures is the one the census saw.
661 // §13: also whenever the advectors run a wisp guard -- the sub-wispEps residue must be
662 // returned on the kinematic path too (the leak is not a CSF matter).
663 if ((csfEnabled && debrisCensus) || wispEps > 0.0)
664 deb.push_back(&b);
665 }
666 // After every block has advected: each block's pass touches only its own colour, so running
667 // them together (their sums in one launch) is the per-block sequence reordered, bit for bit.
668 if (!deb.empty())
669 debrisPassBatch(deb, /*deferLedger=*/batch);
670 if (batch) {
671 // §5.9 stage 5: every bubble box in one launch, then read #2 -- the boxes, the debris sums
672 // and the WO-R ledger in ONE device->host copy; the recentre decisions exactly as before.
673 recentreBatched(!deb.empty());
674 } else {
675 for (std::size_t k = 0; k < blocks_.size(); ++k)
676 if (blocks_[k].mine_)
677 recentre(k);
678 }
679 exch_->syncTable(blocks_);
680 for (auto& b : blocks_)
681 b.mine_ = (b.master == rank_);
682 // Rung W1: re-balance the master assignment on the CURRENT boxes, before the scatter, so the
683 // step's union is produced by the new owners and there is no half-migrated state anywhere.
684 // The boxes have just been replicated, so `plannedMasters()` is the same on every rank.
685 lastReassigned_ = 0;
686 if (reassignEvery > 0 && ((step_ + 1) % reassignEvery) == 0)
687 lastReassigned_ = assignMasters();
688 exch_->scatterColourMax(blocks_, cLocal);
689 if (batch) {
690 measureBatched(dt); // C2 (§5.10): one launch, no host read; applied at the next read #1
691 } else {
692 for (auto& b : blocks_)
693 if (b.mine_)
694 measure(b, dt);
695 }
696 ++step_;
697 }
698
700 long lastReassigned() const { return lastReassigned_; }
701
702 // ---- rung W2: per-block curvature + the CSF face force ---------------------------------------
703
707 void enableCsf(double sigmaValue) {
708 csfEnabled = true;
710 for (std::size_t i = 0; i < blocks_.size(); ++i)
711 if (blocks_[i].mine_ && blocks_[i].allocated_)
712 allocateCsf(i);
713 }
714
718 if (!exch_)
719 throw std::runtime_error("peclet::flow::vof::VofBlockSet: no exchange installed");
720 curvStats_ = VofCurvature::Stats{};
721 if (csfBatchEligible()) { // C1 §5.9 stage 6: every pass for all blocks in one launch
723 exch_->scatterForceSum(blocks_, fx, fy, fz);
724 return;
725 }
726 // Tier 3 of every local block in ONE batched launch (VofCurvature::computeBegin): bit-identical
727 // to the per-block cascade, which ran the 5^3 fit's full latency once per block, serially.
728 bool batch = true;
729 for (auto& b : blocks_)
730 if (b.mine_ && !b.curv_.useWorklist)
731 batch = false;
732 if (batch) {
733 std::vector<VofCurvFallbackJob> jobs;
734 for (auto& b : blocks_)
735 if (b.mine_) {
736 b.curv_.computeBegin(b.adv_.colour());
737 jobs.push_back(b.curv_.fallbackJob(b.adv_.colour()));
738 }
740 }
741 for (auto& b : blocks_) {
742 if (!b.mine_)
743 continue;
744 const VofCurvature::Stats st =
745 batch ? b.curv_.computeEnd() : b.curv_.compute(b.adv_.colour());
746 curvStats_.interfacial += st.interfacial;
747 curvStats_.hf += st.hf;
748 curvStats_.hfMixed += st.hfMixed;
749 curvStats_.hfFit += st.hfFit;
750 curvStats_.pv += st.pv;
751 curvStats_.pvReduced += st.pvReduced;
752 curvStats_.noEstimate += st.noEstimate;
753 curvStats_.clipped += st.clipped;
754 buildCsfForce(b);
755 }
756 exch_->scatterForceSum(blocks_, fx, fy, fz);
757 }
758 VofCurvature::Stats csfCurvatureStats() const { return curvStats_; }
759
762 void setKappaMax(double km) {
763 curvProto.kappaMax = km;
764 for (auto& b : blocks_)
765 b.curv_.kappaMax = km;
766 }
767
768 void allocateCsf(std::size_t idx) {
769 VofBlock& b = blocks_[idx];
770 const I3 n = b.adv_.inner();
771 b.curv_.init(n.x, n.y, n.z, ghost_);
772 // EVERY tunable of the prototype, the metric included (review finding 1: this list once
773 // dropped `pureEps`, so a block WY at wispEps 1e-8 was judged at the 1e-10 purity floor and the
774 // HF tier decayed into the PV fallback silently, 63.8 % -> 52.3 %; and it never carried the
775 // metric, so a cascade re-allocated by a re-centring ran an anisotropic block at unit metric).
776 b.curv_.copyTunablesFrom(curvProto);
777 const long len = static_cast<long>(b.adv_.extent().x) * b.adv_.extent().y * b.adv_.extent().z;
778 for (int c = 0; c < 3; ++c)
779 b.f_[c] = SField("vof::block::csf", len);
780 }
781
789 const I3 e = b.adv_.extent(), n = b.adv_.inner();
790 const int g = ghost_;
791 // Phase 3 (V3.1): the same per-axis pressure-gradient weight `1/h_a'^2` the global
792 // `Solver::addCsfRhs` uses. `h_` stays the (index) block spacing and is not that weight.
793 const double sig = sigma;
794 const double wgt[3] = {metric_.h[0] * metric_.h[0], metric_.h[1] * metric_.h[1],
795 metric_.h[2] * metric_.h[2]};
796 SField cv = b.adv_.colour(), kp = b.curv_.kappa(), kb = b.curv_.branch();
797 const long sy = e.x, sz = static_cast<long>(e.x) * e.y;
798 for (int c = 0; c < 3; ++c) {
799 SField ff = b.f_[c];
800 const double wc = wgt[c];
801 const long strd = (c == 0) ? 1 : (c == 1 ? sy : sz);
802 Kokkos::parallel_for(
803 "vof::block::csf_force",
804 MDRange3<SExec>(SExec(), {g, g, g},
805 {g + n.x, g + n.y, g + n.z}),
806 KOKKOS_LAMBDA(int x, int y, int z) {
807 const long i = L3(x, y, z, e);
808 const double dC = cv(i) - cv(i - strd);
809 double f = 0.0;
810 if (dC != 0.0) {
811 double kf = 0.0;
812 vof::csfFaceCurvature(kp(i - strd), kb(i - strd), kp(i), kb(i), kf);
813 f = vof::csfFaceForce(sig, kf, dC, wc);
814 }
815 ff(i) = f;
816 });
817 }
818 }
819
822 if (!exch_)
823 throw std::runtime_error("peclet::flow::vof::VofBlockSet: no exchange installed");
824 exch_->scatterColourSum(blocks_, sLocal);
825 }
826
829 if (!exch_)
830 throw std::runtime_error("peclet::flow::vof::VofBlockSet: no exchange installed");
831 exch_->syncTable(blocks_);
832 exch_->scatterColourMax(blocks_, cLocal);
833 }
834
835 // ---- rung W1 item (a): master assignment and re-assignment ---------------------------------
836
840 std::vector<int> plannedMasters() const {
841 std::vector<long> w(blocks_.size());
842 for (std::size_t i = 0; i < blocks_.size(); ++i)
843 w[i] = blocks_[i].box.cells();
844 std::vector<int> m;
845 switch (assignMode) {
847 vofAssignLpt(w, size_, m);
848 break;
850 vofAssignOrb(w, size_, m);
851 break;
853 default:
854 m.assign(blocks_.size(), 0);
855 for (std::size_t i = 0; i < blocks_.size(); ++i)
856 m[i] = static_cast<int>(blocks_[i].id % size_);
857 break;
858 }
859 return m;
860 }
861
866 flushStats(); // C2: a migrating block carries its previous centroid (serializeAux)
867 if (blocks_.empty() || size_ <= 1)
868 return 0;
869 const std::vector<int> want = plannedMasters();
870 std::vector<int> old(blocks_.size());
871 long moved = 0;
872 for (std::size_t i = 0; i < blocks_.size(); ++i) {
873 old[i] = blocks_[i].master;
874 if (old[i] != want[i])
875 ++moved;
876 }
877 if (moved == 0)
878 return 0;
879 for (std::size_t i = 0; i < blocks_.size(); ++i) {
880 blocks_[i].master = want[i];
881 blocks_[i].st_.master = want[i];
882 }
883 // gaining ranks allocate FIRST (zeroed), so the migration has somewhere to land
884 for (std::size_t i = 0; i < blocks_.size(); ++i)
885 if (want[i] == rank_ && old[i] != rank_) {
886 blocks_[i].mine_ = true;
887 allocate(i);
888 }
889 if (exch_)
890 exch_->migrateColour(blocks_, old);
891 for (std::size_t i = 0; i < blocks_.size(); ++i) {
892 if (old[i] == rank_ && want[i] != rank_) {
893 release(i);
894 blocks_[i].mine_ = false;
895 } else if (want[i] == rank_ && old[i] != rank_) {
896 // A GAINED block only. A block this rank keeps is left completely alone -- `measure` would
897 // reset its previous centroid and blank one step of its reported velocity.
898 blocks_[i].mine_ = true;
899 fillBlockGhosts(blocks_[i]);
900 double aux[VofBlock::kAuxLen];
901 blocks_[i].serializeAux(aux); // the migrated previous centroid survives the re-measure
902 measure(blocks_[i], 0.0);
903 blocks_[i].deserializeAux(aux);
904 }
905 }
906 return moved;
907 }
908
909 // ---- general seeding: a block from an arbitrary global colour field -------------------------
910
916 void seedBox(const VofBox& bb, int extraPad = 0) {
917 flushStats(); // C2, as in seedSphere
918 VofBlock b;
919 b.id = static_cast<long>(blocks_.size());
920 b.master = static_cast<int>(b.id % size_);
921 b.box = vofClampBox(VofBox::grown(bb, margin_ + extraPad), gs_, per_);
922 b.seed_ = bb;
923 b.mine_ = (b.master == rank_);
924 blocks_.push_back(std::move(b));
925 const std::size_t idx = blocks_.size() - 1;
926 for (std::size_t k = 0; k + 1 < blocks_.size(); ++k)
927 if (blocks_[k].mine_ && blocks_[k].allocated_)
928 installHook(k);
929 if (blocks_[idx].mine_)
930 allocate(idx);
931 }
932
935 if (!exch_)
936 throw std::runtime_error("peclet::flow::vof::VofBlockSet: no exchange installed");
937 flushStats(); // C2: its per-block measure() must not be overwritten by an older packet
938 exch_->gatherColour(blocks_, cLocal);
939 for (auto& b : blocks_)
940 if (b.mine_) {
941 clipToSeed(b);
943 measure(b, 0.0);
944 }
945 }
946
947 // ---- rung W3: checkpoint / restart of the block state ---------------------------------------
948 //
949 // A run longer than one job's wall clock has to write its markers out and read them back. The
950 // union colour field is NOT enough to do that: `seedBox`/`finishSeeding` gather each marker out
951 // of the UNION and clip to the seed extent, so two markers that touch each adopt a slice of the
952 // other (WO-W12 open item 5, measured -2.7 % / +7.1 %). A block's ONLY state is its own inner
953 // colour (everything else is either replicated -- the table -- or recomputed every step), so a
954 // checkpoint is exactly {box, colour} per block and the restart below is EXACT whatever the
955 // markers are doing.
956
959 std::vector<double> blockColourHost(std::size_t idx) {
960 VofBlock& b = blocks_.at(idx);
961 if (!b.mine_ || !b.allocated_)
962 return {};
963 const I3 n = b.adv_.inner();
964 SField c = b.adv_.colour();
965 auto hc = Kokkos::create_mirror_view(c);
966 Kokkos::deep_copy(hc, c);
967 std::vector<double> out(static_cast<std::size_t>(n.x) * static_cast<std::size_t>(n.y) *
968 static_cast<std::size_t>(n.z));
969 std::size_t q = 0;
970 for (int z = 0; z < n.z; ++z)
971 for (int y = 0; y < n.y; ++y)
972 for (int x = 0; x < n.x; ++x)
973 out[q++] = hc(b.adv_.index(x, y, z));
974 return out;
975 }
976
979 std::vector<double> blockKappaHost(std::size_t idx) {
980 VofBlock& b = blocks_.at(idx);
981 if (!b.mine_ || !b.allocated_ || !csfEnabled)
982 return {};
983 const I3 n = b.adv_.inner();
984 SField k = b.curv_.kappa();
985 auto hk = Kokkos::create_mirror_view(k);
986 Kokkos::deep_copy(hk, k);
987 std::vector<double> out(static_cast<std::size_t>(n.x) * static_cast<std::size_t>(n.y) *
988 static_cast<std::size_t>(n.z));
989 std::size_t q = 0;
990 for (int z = 0; z < n.z; ++z)
991 for (int y = 0; y < n.y; ++y)
992 for (int x = 0; x < n.x; ++x)
993 out[q++] = hk(b.adv_.index(x, y, z));
994 return out;
995 }
996
1000 void seedBoxWithColour(const VofBox& bb, const std::vector<double>& colour) {
1001 flushStats(); // C2, as in seedSphere
1002 VofBlock b;
1003 b.id = static_cast<long>(blocks_.size());
1004 b.master = masterOf(b.id);
1005 b.box = vofClampBox(bb, gs_, per_);
1006 b.mine_ = (b.master == rank_);
1007 blocks_.push_back(std::move(b));
1008 const std::size_t idx = blocks_.size() - 1;
1009 for (std::size_t k = 0; k + 1 < blocks_.size(); ++k)
1010 if (blocks_[k].mine_ && blocks_[k].allocated_)
1011 installHook(k);
1012 if (!blocks_[idx].mine_)
1013 return;
1014 allocate(idx);
1015 VofBlock& nb = blocks_[idx];
1016 const I3 n = nb.adv_.inner();
1017 const std::size_t want = static_cast<std::size_t>(n.x) * static_cast<std::size_t>(n.y) *
1018 static_cast<std::size_t>(n.z);
1019 if (colour.size() != want)
1020 throw std::runtime_error(
1021 "peclet::flow::vof::VofBlockSet::seedBoxWithColour: colour size does not match the box");
1022 SField c = nb.adv_.colour();
1023 auto hc = Kokkos::create_mirror_view(c);
1024 Kokkos::deep_copy(hc, c); // ghosts as allocated (zero); the fill below re-does them anyway
1025 std::size_t q = 0;
1026 for (int z = 0; z < n.z; ++z)
1027 for (int y = 0; y < n.y; ++y)
1028 for (int x = 0; x < n.x; ++x)
1029 hc(nb.adv_.index(x, y, z)) = colour[q++];
1030 Kokkos::deep_copy(c, hc);
1031 Kokkos::fence();
1032 fillBlockGhosts(nb);
1033 measure(nb, 0.0);
1034 }
1035
1043 const I3 e = b.adv_.extent(), n = b.adv_.inner(), o = b.box.origin();
1044 const int g = ghost_;
1045 const int lx = b.seed_.lo[0], hx = b.seed_.hi[0], ly = b.seed_.lo[1], hy = b.seed_.hi[1],
1046 lz = b.seed_.lo[2], hz = b.seed_.hi[2];
1047 if (hx <= lx || hy <= ly || hz <= lz)
1048 return; // no seed box recorded (a sphere seed): nothing to clip
1049 SField c = b.adv_.colour();
1050 Kokkos::parallel_for(
1051 "vof::block::clip_seed",
1052 MDRange3<SExec>(SExec(), {0, 0, 0}, {n.x, n.y, n.z}),
1053 KOKKOS_LAMBDA(int x, int y, int z) {
1054 const int gx = x + o.x, gy = y + o.y, gz = z + o.z;
1056 c(L3(x + g, y + g, z + g, e)) = 0.0;
1057 });
1058 Kokkos::fence();
1059 }
1060
1064 const I3 e = b.adv_.extent(), n = b.adv_.inner(), o = b.box.origin();
1065 const int g = ghost_;
1066 SField f = b.adv_.colour();
1067 // 1. every ghost cell -> 0 (the block's far field: C = 0, the continuous phase, by the margin).
1068 Kokkos::parallel_for(
1069 "vof::block::ghost_zero",
1070 MDRange3<SExec>(SExec(), {0, 0, 0}, {e.x, e.y, e.z}),
1071 KOKKOS_LAMBDA(int x, int y, int z) {
1072 if (x >= g && x < g + n.x && y >= g && y < g + n.y && z >= g && z < g + n.z)
1073 return;
1074 f(L3(x, y, z, e)) = 0.0;
1075 });
1076 // 2. an axis the block spans entirely, and which is globally periodic, wraps within the block.
1077 const bool sx = per_[0] && b.box.n(0) == gs_.x;
1078 const bool sy = per_[1] && b.box.n(1) == gs_.y;
1079 const bool sz = per_[2] && b.box.n(2) == gs_.z;
1080 if (sx || sy || sz)
1081 vof::periodicFill(f, e, g, sx, sy, sz);
1082 // 3. the part of the extended box outside a NON-periodic domain takes the globally clamped
1083 // (zero-gradient) value — the same rule the structured colour field uses.
1084 if (!per_[0] || !per_[1] || !per_[2])
1085 vof::clampFill(f, e, g, o, gs_, per_[0], per_[1], per_[2]);
1086 }
1087
1089 std::vector<VofBlockStats> statsAll() {
1090 flushStats(); // C2: the last batched measure's values reach the host here or at read #1
1091 // `id`, `master` and the box come from the REPLICATED table, so they are reported on every
1092 // rank; only the measured entries (volume, centroid, velocity, moments, area) are the
1093 // master's. Filling them here rather than in `measure()` is what makes that promise true on
1094 // a rank that masters nothing.
1095 std::vector<VofBlockStats> v;
1096 v.reserve(blocks_.size());
1097 for (const auto& b : blocks_) {
1098 VofBlockStats q = b.st_;
1099 q.id = b.id;
1100 q.master = b.master;
1101 for (int d = 0; d < 3; ++d) {
1102 q.lo[d] = b.box.lo[d];
1103 q.hi[d] = b.box.hi[d];
1104 }
1105 q.cells = b.box.cells();
1106 v.push_back(q);
1107 }
1108 return v;
1109 }
1110
1114 void masterCensus(std::vector<long>& masters, std::vector<long>& cells) const {
1115 masters.assign(size_, 0);
1116 cells.assign(size_, 0);
1117 for (const auto& b : blocks_) {
1118 masters[b.master] += 1;
1119 cells[b.master] += VofBox::grown(b.box, 0).cells();
1120 }
1121 }
1123 double cellImbalance() const {
1124 std::vector<long> m, c;
1125 masterCensus(m, c);
1126 long tot = 0, mx = 0;
1127 for (long v : c) {
1128 tot += v;
1129 mx = v > mx ? v : mx;
1130 }
1131 if (tot == 0)
1132 return 1.0;
1133 return static_cast<double>(mx) * size_ / static_cast<double>(tot);
1134 }
1135
1136 I3 globalSize() const { return gs_; }
1137 std::array<bool, 3> periodic() const { return per_; }
1138 int rank() const { return rank_; }
1139 int size() const { return size_; }
1140
1141 private:
1145 void installHook(std::size_t idx) {
1146 VofBlockSet* self = this;
1147 blocks_[idx].adv_.exchange = [self, idx](SField) { self->fillBlockGhosts(self->blocks_[idx]); };
1148 }
1149
1150 void allocate(std::size_t idx) {
1151 VofBlock& b = blocks_[idx];
1152 b.adv_ = acquireAdvector(b.box.n(0), b.box.n(1), b.box.n(2));
1153 installHook(idx);
1154 b.allocated_ = true;
1155 b.st_.id = b.id;
1156 b.st_.master = b.master;
1157 if (csfEnabled)
1158 allocateCsf(idx);
1159 }
1160
1163 void release(std::size_t idx) {
1164 VofBlock& b = blocks_[idx];
1165 retireAdvector(std::move(b.adv_));
1166 b.adv_ = WyAdvector();
1167 b.curv_ = VofCurvature();
1168 for (int c = 0; c < 3; ++c)
1169 b.f_[c] = SField();
1170 b.allocated_ = false;
1171 b.hasPrev_ = false;
1172 }
1173
1180 WyAdvector acquireAdvector(int nx, int ny, int nz) {
1181 const std::array<int, 3> key{nx, ny, nz};
1182 if (usePool) {
1183 auto it = pool_.find(key);
1184 if (it != pool_.end() && !it->second.empty()) {
1185 WyAdvector a = std::move(it->second.back());
1186 it->second.pop_back();
1187 ++poolHits_;
1188 Kokkos::deep_copy(a.colour(), 0.0);
1189 for (int d = 0; d < 3; ++d)
1190 Kokkos::deep_copy(a.faceVel(d), 0.0);
1191 Kokkos::fence();
1192 a.cflLimit = cflLimit;
1193 a.wispEps = wispEps;
1194 a.metric = metric_; // a pooled advector carries whatever metric it last had
1195 a.globalMax = nullptr;
1196 a.exchange = nullptr; // re-installed by installHook
1197 return a;
1198 }
1199 }
1200 ++poolMisses_;
1201 WyAdvector a;
1202 a.init(nx, ny, nz, h_, ghost_);
1203 a.cflLimit = cflLimit;
1204 a.wispEps = wispEps;
1205 a.metric = metric_; // a re-centred block's fresh advector keeps the anisotropic metric
1206 a.globalMax = nullptr; // the block IS the whole domain of its own advector
1207 return a;
1208 }
1209
1210 void retireAdvector(WyAdvector&& a) {
1211 if (!usePool || a.colour().extent(0) == 0)
1212 return;
1213 const I3 n = a.inner();
1214 const std::array<int, 3> key{n.x, n.y, n.z};
1215 auto& v = pool_[key];
1216 if (static_cast<int>(v.size()) >= poolCapacity)
1217 return;
1218 a.exchange = nullptr; // the hook captures a block index; never carry it into the pool
1219 v.push_back(std::move(a));
1220 }
1221
1222 public:
1223 long poolHits() const { return poolHits_; }
1224 long poolMisses() const { return poolMisses_; }
1225 void clearPool() { pool_.clear(); }
1226
1227 // NOTE: everything below is public ONLY because nvcc refuses an extended `__host__ __device__`
1228 // lambda inside a private or protected member function ("The enclosing parent function ... cannot
1229 // have private or protected access within its class"). These are implementation details of the
1230 // step; treat them as such.
1231 public:
1234 bool bubbleBox(const VofBlock& b, VofBox& out) const {
1235 const I3 e = b.adv_.extent(), n = b.adv_.inner();
1236 const int g = ghost_;
1237 const double eps = bubbleEps > wispEps ? bubbleEps : wispEps; // §12.2
1238 SField c = b.adv_.colour();
1239 int lo[3] = {n.x, n.y, n.z}, hi[3] = {-1, -1, -1};
1240 using MD = MDRange3<SExec>;
1241 MD pol(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z});
1242 Kokkos::parallel_reduce(
1243 "vof::block::bbox_lo", pol,
1244 KOKKOS_LAMBDA(int x, int y, int z, int& lx, int& ly, int& lz) {
1245 if (!(Kokkos::fabs(c(L3(x, y, z, e))) > eps))
1246 return;
1247 const int px = x - g, py = y - g, pz = z - g;
1248 lx = px < lx ? px : lx;
1249 ly = py < ly ? py : ly;
1250 lz = pz < lz ? pz : lz;
1251 },
1252 Kokkos::Min<int>(lo[0]), Kokkos::Min<int>(lo[1]), Kokkos::Min<int>(lo[2]));
1253 Kokkos::parallel_reduce(
1254 "vof::block::bbox_hi", pol,
1255 KOKKOS_LAMBDA(int x, int y, int z, int& hx, int& hy, int& hz) {
1256 if (!(Kokkos::fabs(c(L3(x, y, z, e))) > eps))
1257 return;
1258 const int px = x - g, py = y - g, pz = z - g;
1259 hx = px > hx ? px : hx;
1260 hy = py > hy ? py : hy;
1261 hz = pz > hz ? pz : hz;
1262 },
1263 Kokkos::Max<int>(hi[0]), Kokkos::Max<int>(hi[1]), Kokkos::Max<int>(hi[2]));
1264 Kokkos::fence();
1265 if (hi[0] < lo[0])
1266 return false; // the block is empty
1267 for (int d = 0; d < 3; ++d) {
1268 out.lo[d] = lo[d] + b.box.lo[d];
1269 out.hi[d] = hi[d] + 1 + b.box.lo[d];
1270 }
1271 return true;
1272 }
1273
1276 void recentre(std::size_t idx) {
1277 VofBox bb;
1278 if (!bubbleBox(blocks_[idx], bb))
1279 return;
1280 recentreTo(idx, bb);
1281 }
1283 void recentreTo(std::size_t idx, const VofBox& bb) {
1284 VofBlock& b = blocks_[idx];
1285 const VofBox need = vofClampBox(VofBox::grown(bb, margin_), gs_, per_);
1286 bool wasteful = false;
1287 if (allowShrink)
1288 for (int d = 0; d < 3; ++d)
1289 if (b.box.n(d) > need.n(d) + 2 * (margin_ + recentrePad))
1290 wasteful = true;
1291 if (b.box.contains(need) && !wasteful)
1292 return;
1293 const VofBox want = VofBox::grown(bb, margin_ + recentrePad);
1294 const VofBox nb = vofClampBox(want, gs_, per_);
1295 if (nb == b.box)
1296 return;
1297 // §12.3: an axis on which the box spans the whole periodic domain is copied WITH the wrap
1298 // (source index = destination global index mod L) -- lossless, a marker as long as the domain
1299 // is legal. Every other axis keeps the plain copy by unwrapped global index.
1300 const int L[3] = {gs_.x, gs_.y, gs_.z};
1301 bool wrap[3];
1302 bool anyWrap = false;
1303 for (int d = 0; d < 3; ++d) {
1304 wrap[d] = per_[d] && nb.n(d) == L[d];
1305 anyWrap = anyWrap || (wrap[d] && want.n(d) >= L[d]);
1306 }
1307 // What the move DROPS: the colour of old-box cells that fall outside the new box. Measured
1308 // directly rather than as `sum(old) - sum(new)`, which at |sum| ~ 1e2 is 1e-13 of summation
1309 // rounding and says nothing about the wisps (measured -5.7e-14 that way on the G1 scene).
1310 const double dropped = outOfBoxSum(b, nb, wrap);
1311 // exact copy by global index (both boxes are in the same unwrapped global frame)
1312 WyAdvector fresh = acquireAdvector(nb.n(0), nb.n(1), nb.n(2));
1313 const I3 se = b.adv_.extent(), de = fresh.extent();
1314 const I3 dn = fresh.inner();
1315 const int g = ghost_;
1316 const int sx = b.box.lo[0], sy = b.box.lo[1], sz = b.box.lo[2];
1317 const int snx = b.box.n(0), sny = b.box.n(1), snz = b.box.n(2);
1318 const int dx = nb.lo[0], dy = nb.lo[1], dz = nb.lo[2];
1319 const bool wx = wrap[0], wy = wrap[1], wz = wrap[2];
1320 const int Lx = L[0], Ly = L[1], Lz = L[2];
1321 SField src = b.adv_.colour(), dst = fresh.colour();
1322 Kokkos::parallel_for(
1323 "vof::block::recentre_copy",
1324 MDRange3<SExec>(SExec(), {0, 0, 0}, {dn.x, dn.y, dn.z}),
1325 KOKKOS_LAMBDA(int x, int y, int z) {
1326 const int gx = x + dx, gy = y + dy, gz = z + dz;
1327 int lx = gx - sx, ly = gy - sy, lz = gz - sz;
1328 if (wx)
1329 lx = ((lx % Lx) + Lx) % Lx;
1330 if (wy)
1331 ly = ((ly % Ly) + Ly) % Ly;
1332 if (wz)
1333 lz = ((lz % Lz) + Lz) % Lz;
1334 double v = 0.0;
1335 if (lx >= 0 && lx < snx && ly >= 0 && ly < sny && lz >= 0 && lz < snz)
1336 v = src(L3(lx + g, ly + g, lz + g, se));
1337 dst(L3(x + g, y + g, z + g, de)) = v;
1338 });
1339 Kokkos::fence();
1340 b.box = nb;
1341 retireAdvector(std::move(b.adv_));
1342 b.adv_ = std::move(fresh);
1343 b.adv_.cflLimit = cflLimit;
1344 b.adv_.globalMax = nullptr;
1345 installHook(idx);
1346 if (csfEnabled)
1348 fillBlockGhosts(blocks_[idx]);
1349 blocks_[idx].st_.recentred = true;
1350 blocks_[idx].st_.discarded += dropped;
1351 if (anyWrap)
1352 ++blocks_[idx].st_.fullAxis;
1353 }
1354
1373 std::vector<VofBlock*> one{&b};
1374 debrisPassBatch(one);
1375 }
1384 void debrisPassBatch(std::vector<VofBlock*>& bs, bool deferLedger = false) {
1385 const double t0 = timingOn ? tick_() : 0.0;
1386 if (batchEligible()) {
1388 if (!deferLedger) {
1389 Kokkos::deep_copy(pkHost_, pk_);
1391 }
1392 } else {
1394 }
1395 if (timingOn) {
1396 Kokkos::fence();
1397 debrisSeconds += tick_() - t0;
1398 }
1399 debrisCalls += static_cast<long>(bs.size());
1400 }
1402 struct DebrisJob {
1403 double* c;
1404 const long *listD, *listR, *listA;
1405 long nDc, nDr, nR, nA;
1406 };
1407 static constexpr int kDebrisBatch = 16;
1412 void debrisPassBatchImpl(std::vector<VofBlock*>& bs) {
1413 const bool doDebris = csfEnabled && debrisCensus, doResidue = wispEps > 0.0;
1414 const int g = ghost_;
1415 // The interfacial predicate of D and A is `max(interfaceEps, wispEps)` (review finding 7): R
1416 // is `0 < |C| <= wispEps`, so the three lists are DISJOINT whatever the two thresholds are --
1417 // on a kinematic block run curvProto.interfaceEps is 0 while wispEps is 1e-8, and an A/R or
1418 // D/R overlap would drop a W share or count one cell in both ledgers.
1419 const double weps = wispEps, cfull = kDebrisMaxC;
1420 const double ieps = curvProto.interfaceEps > weps ? curvProto.interfaceEps : weps;
1421 // per-block list offsets into the shared scratch
1422 std::vector<long> off(bs.size() + 1, 0);
1423 for (std::size_t k = 0; k < bs.size(); ++k) {
1424 const I3 n = bs[k]->adv_.inner();
1425 off[k + 1] = off[k] + static_cast<long>(n.x) * n.y * n.z;
1426 }
1427 auto ensure = [&](LField& f, const char* name) {
1428 if (static_cast<long>(f.extent(0)) < off.back())
1429 f = LField(Kokkos::view_alloc(std::string(name), Kokkos::WithoutInitializing), off.back());
1430 };
1431 if (doDebris)
1432 ensure(listD_, "vof::block::debris_list");
1433 if (doResidue)
1434 ensure(listR_, "vof::block::residue_list");
1435 struct Pending {
1436 VofBlock* b;
1437 long k, nD, nR, nA, nDr;
1438 bool removeD;
1439 };
1440 std::vector<Pending> pend;
1441 std::vector<DebrisJob> jobs;
1442 for (std::size_t k = 0; k < bs.size(); ++k) {
1443 VofBlock& b = *bs[k];
1444 b.st_.debrisCells = 0;
1445 b.st_.debrisVolume = 0.0;
1446 const I3 e = b.adv_.extent(), n = b.adv_.inner();
1447 const long region = static_cast<long>(n.x) * n.y * n.z, o = off[k];
1448 const long sy = e.x, sz = static_cast<long>(e.x) * e.y;
1449 const int nx = n.x, ny = n.y;
1450 SField c = b.adv_.colour();
1451 long nD = 0, nR = 0;
1452 if (doDebris) {
1453 LField listD = listD_;
1454 Kokkos::parallel_scan(
1455 "vof::block::debris_mark", Kokkos::RangePolicy<SExec>(SExec(), 0, region),
1456 KOKKOS_LAMBDA(const long r, long& upd, const bool final) {
1457 const int ix = static_cast<int>(r % nx);
1458 const int iy = static_cast<int>((r / nx) % ny);
1459 const int iz = static_cast<int>(r / (static_cast<long>(nx) * ny));
1460 const long i = L3(g + ix, g + iy, g + iz, e);
1461 if (!vofDebrisCell(c, i, sy, sz, ieps, cfull))
1462 return;
1463 if (final)
1464 listD(o + upd) = i;
1465 ++upd;
1466 },
1467 nD);
1468 }
1469 if (doResidue) {
1470 LField listR = listR_;
1471 Kokkos::parallel_scan(
1472 "vof::block::residue_mark", Kokkos::RangePolicy<SExec>(SExec(), 0, region),
1473 KOKKOS_LAMBDA(const long r, long& upd, const bool final) {
1474 const int ix = static_cast<int>(r % nx);
1475 const int iy = static_cast<int>((r / nx) % ny);
1476 const int iz = static_cast<int>(r / (static_cast<long>(nx) * ny));
1477 const double ci = c(L3(g + ix, g + iy, g + iz, e));
1478 if (!vofResidueCell(ci, weps))
1479 return;
1480 if (final)
1481 listR(o + upd) = L3(g + ix, g + iy, g + iz, e);
1482 ++upd;
1483 },
1484 nR);
1485 }
1486 const bool removeD = doDebris && debrisRemove && nD > 0;
1487 if (!removeD && nR == 0) {
1488 if (doDebris && nD > 0) { // census only: report what WOULD be removed
1489 LField listD = listD_;
1490 double dVd = 0.0;
1491 Kokkos::parallel_reduce(
1492 "vof::block::debris_census", Kokkos::RangePolicy<SExec>(SExec(), 0, 1),
1493 KOKKOS_LAMBDA(const long, double& acc) {
1494 double a = 0.0;
1495 for (long t = 0; t < nD; ++t)
1496 a += c(listD(o + t));
1497 acc += a;
1498 },
1499 dVd);
1500 b.st_.debrisCells = nD;
1501 b.st_.debrisVolume = dVd;
1502 }
1503 continue; // nothing to remove: nothing else runs
1504 }
1505 ensure(listA_, "vof::block::attached_list");
1506 LField listA = listA_;
1507 long nA = 0;
1508 Kokkos::parallel_scan(
1509 "vof::block::debris_attached", Kokkos::RangePolicy<SExec>(SExec(), 0, region),
1510 KOKKOS_LAMBDA(const long r, long& upd, const bool final) {
1511 const int ix = static_cast<int>(r % nx);
1512 const int iy = static_cast<int>((r / nx) % ny);
1513 const int iz = static_cast<int>(r / (static_cast<long>(nx) * ny));
1514 const long i = L3(g + ix, g + iy, g + iz, e);
1515 if (!vofAttachedCell(c, i, sy, sz, ieps, cfull))
1516 return;
1517 if (final)
1518 listA(o + upd) = i;
1519 ++upd;
1520 },
1521 nA);
1522 Pending pb{&b, static_cast<long>(k), nD, nR, nA, removeD ? nD : 0, removeD};
1523 pend.push_back(pb);
1524 DebrisJob j;
1525 j.c = c.data();
1526 j.listD = doDebris ? listD_.data() + o : nullptr;
1527 j.listR = doResidue ? listR_.data() + o : nullptr;
1528 j.listA = listA_.data() + o;
1529 j.nDc = doDebris ? nD : 0;
1530 j.nDr = pb.nDr;
1531 j.nR = nR;
1532 j.nA = nA;
1533 jobs.push_back(j);
1534 }
1535 if (pend.empty())
1536 return;
1537 // [0] sum_D, [1] sum_R, [2] W, [3] VA, [4] removed volume (then `lost`), [5] acted?
1538 // The SUMS run on one thread per block in list order (a fixed summation order: the state stays
1539 // decomposition-independent bitwise); the per-cell writes are independent once dV and W are
1540 // known -- D, R and A are disjoint -- so they run in parallel and every cell receives exactly
1541 // the double the serial loop gave it (review finding 4). `lost` (a cap at 1, expected never to
1542 // fire) is summed in list order from a per-cell scratch only when some cell was capped.
1543 const long np = static_cast<long>(pend.size());
1544 if (static_cast<long>(debrisOut_.extent(0)) < 6 * np) {
1545 debrisOut_ = SField("vof::block::debris_out", 6 * np);
1546 debrisOutHost_ = Kokkos::create_mirror_view(debrisOut_);
1547 }
1548 SField out = debrisOut_;
1549 const double vaMin = kDebrisMinAttached;
1550 for (long j0 = 0; j0 < np; j0 += kDebrisBatch) {
1551 DebrisTable T;
1552 T.nj = static_cast<int>(std::min<long>(kDebrisBatch, np - j0));
1553 for (int q = 0; q < T.nj; ++q)
1554 T.job[q] = jobs[j0 + q];
1555 const long base = 6 * j0;
1556 Kokkos::parallel_for(
1557 "vof::block::debris_sums", Kokkos::RangePolicy<SExec>(SExec(), 0, T.nj),
1558 KOKKOS_LAMBDA(const long q) {
1559 const DebrisJob& J = T.job[q];
1560 const VofRawField c{J.c};
1561 const long nDc = J.nDc, nDr = J.nDr, nR = J.nR, nA = J.nA;
1562 double sD = 0.0, sR = 0.0, W = 0.0, VA = 0.0;
1563 for (long t = 0; t < nDc; ++t)
1564 sD += c(J.listD[t]);
1565 for (long t = 0; t < nR; ++t)
1566 sR += c(J.listR[t]);
1567 for (long t = 0; t < nA; ++t) {
1568 const double ca = c(J.listA[t]);
1569 W += ca * (1.0 - ca);
1570 VA += ca;
1571 }
1572 const long ob = base + 6 * q;
1573 out(ob + 0) = sD;
1574 out(ob + 1) = sR;
1575 out(ob + 2) = W;
1576 out(ob + 3) = VA;
1577 out(ob + 4) = 0.0;
1578 out(ob + 5) = (VA < vaMin || !(W > 0.0)) ? 0.0 : 1.0;
1579 // the removed volume, in the fixed order D (if removing) then R -- the serial order
1580 double dV = 0.0;
1581 for (long t = 0; t < nDr; ++t)
1582 dV += c(J.listD[t]);
1583 for (long t = 0; t < nR; ++t)
1584 dV += c(J.listR[t]);
1585 out(ob + 4) = dV; // carried to the write kernel; `lost` replaces it below
1586 });
1587 }
1588 Kokkos::deep_copy(debrisOutHost_, out); // ONE device->host copy for every block
1589 auto hoAll = debrisOutHost_;
1590 for (long p = 0; p < np; ++p) {
1591 const Pending& pb = pend[p];
1592 VofBlock& b = *pb.b;
1593 const long o = off[pb.k];
1594 const I3 n = b.adv_.inner();
1595 const long region = static_cast<long>(n.x) * n.y * n.z;
1596 double ho[6];
1597 for (int q = 0; q < 6; ++q)
1598 ho[q] = hoAll(6 * p + q);
1599 const long nD = pb.nD, nR = pb.nR, nA = pb.nA, nDr = pb.nDr;
1600 const bool removeD = pb.removeD;
1601 if (ho[5] != 0.0) {
1602 const double dV = ho[4], W = ho[2];
1603 if (static_cast<long>(debrisEx_.extent(0)) < region)
1604 debrisEx_ = SField(Kokkos::view_alloc("vof::block::debris_excess",
1605 Kokkos::WithoutInitializing),
1606 region);
1607 SField ex = debrisEx_;
1608 SField c = b.adv_.colour();
1609 LField listD = listD_, listR = listR_, listA = listA_;
1610 const long nZ = nDr + nR, nTot = nZ + nA;
1611 long capped = 0;
1612 Kokkos::parallel_reduce(
1613 "vof::block::debris_write", Kokkos::RangePolicy<SExec>(SExec(), 0, nTot),
1614 KOKKOS_LAMBDA(const long t, long& cap) {
1615 if (t < nDr) {
1616 c(listD(o + t)) = 0.0;
1617 return;
1618 }
1619 if (t < nZ) {
1620 c(listR(o + t - nDr)) = 0.0;
1621 return;
1622 }
1623 const long ta = t - nZ;
1624 const long i = listA(o + ta);
1625 const double ca = c(i);
1626 const double d = dV * (ca * (1.0 - ca)) / W;
1627 double cn = ca + d;
1628 double e = 0.0;
1629 if (cn > 1.0) {
1630 e = cn - 1.0;
1631 cn = 1.0;
1632 ++cap;
1633 }
1634 ex(ta) = e;
1635 c(i) = cn;
1636 },
1637 capped);
1638 double lost = 0.0;
1639 if (capped > 0) {
1640 Kokkos::parallel_reduce(
1641 "vof::block::debris_lost", Kokkos::RangePolicy<SExec>(SExec(), 0, 1),
1642 KOKKOS_LAMBDA(const long, double& acc) {
1643 double a = 0.0; // list order, as the serial loop accumulated it
1644 for (long t = 0; t < nA; ++t)
1645 a += ex(t);
1646 acc += a;
1647 },
1648 lost);
1649 }
1650 ho[4] = lost;
1651 }
1652 if (doDebris && !debrisRemove && nD > 0) { // census of what WOULD be removed
1653 b.st_.debrisCells = nD;
1654 b.st_.debrisVolume = ho[0];
1655 }
1656 if (ho[5] == 0.0) { // step 3: no attached interface worth a cell -- the clip bounds it
1657 if (removeD)
1658 b.st_.debrisUnresolved += nD;
1659 continue;
1660 }
1661 fillBlockGhosts(b); // the inner colour moved; the next reader expects valid ghosts
1662 if (removeD) {
1663 b.st_.debrisCells = nD;
1664 b.st_.debrisVolume = ho[0];
1665 b.st_.debrisReturned += ho[0];
1666 }
1667 b.st_.residueReturned += ho[1];
1668 b.st_.debrisLost += ho[4];
1669 }
1670 }
1671
1673 double outOfBoxSum(const VofBlock& b, const VofBox& nb) const {
1674 const bool nowrap[3] = {false, false, false};
1675 return outOfBoxSum(b, nb, nowrap);
1676 }
1679 double outOfBoxSum(const VofBlock& b, const VofBox& nb, const bool wrap[3]) const {
1680 const I3 e = b.adv_.extent(), n = b.adv_.inner();
1681 const int g = ghost_;
1682 const int sx = b.box.lo[0], sy = b.box.lo[1], sz = b.box.lo[2];
1683 const int lx = nb.lo[0], ly = nb.lo[1], lz = nb.lo[2];
1684 const int hx = nb.hi[0], hy = nb.hi[1], hz = nb.hi[2];
1685 const bool wx = wrap[0], wy = wrap[1], wz = wrap[2];
1686 SField c = b.adv_.colour();
1687 double v = 0.0;
1688 Kokkos::parallel_reduce(
1689 "vof::block::out_of_box",
1690 MDRange3<SExec>(SExec(), {g, g, g},
1691 {g + n.x, g + n.y, g + n.z}),
1692 KOKKOS_LAMBDA(int x, int y, int z, double& a) {
1693 const int gx = x - g + sx, gy = y - g + sy, gz = z - g + sz;
1694 const bool inX = wx || (gx >= lx && gx < hx), inY = wy || (gy >= ly && gy < hy),
1695 inZ = wz || (gz >= lz && gz < hz);
1696 if (inX && inY && inZ)
1697 return;
1698 a += c(L3(x, y, z, e));
1699 },
1700 v);
1701 Kokkos::fence();
1702 return v;
1703 }
1704
1705 double innerSum(const VofBlock& b) const {
1706 const I3 e = b.adv_.extent(), n = b.adv_.inner();
1707 const int g = ghost_;
1708 SField c = b.adv_.colour();
1709 double v = 0.0;
1710 Kokkos::parallel_reduce(
1711 "vof::block::inner_sum",
1712 MDRange3<SExec>(SExec(), {g, g, g},
1713 {g + n.x, g + n.y, g + n.z}),
1714 KOKKOS_LAMBDA(int x, int y, int z, double& a) { a += c(L3(x, y, z, e)); }, v);
1715 Kokkos::fence();
1716 return v;
1717 }
1718
1725 double interfaceArea(const VofBlock& b) const {
1726 const I3 e = b.adv_.extent(), n = b.adv_.inner();
1727 const int g = ghost_;
1728 const double hh = h_, aeps = areaEps;
1729 SField c = b.adv_.colour();
1730 const long sy = e.x, sz = static_cast<long>(e.x) * e.y;
1731 double a = 0.0;
1732 Kokkos::parallel_reduce(
1733 "vof::block::area",
1734 MDRange3<SExec>(SExec(), {g, g, g},
1735 {g + n.x, g + n.y, g + n.z}),
1736 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
1737 const long i = L3(x, y, z, e);
1738 const double ci = c(i);
1739 if (!(ci > aeps) || !(ci < 1.0 - aeps))
1740 return;
1741 double st[27];
1742 for (int dz = -1; dz <= 1; ++dz)
1743 for (int dy = -1; dy <= 1; ++dy)
1744 for (int dx = -1; dx <= 1; ++dx)
1745 st[vof::plicSt(dx + 1, dy + 1, dz + 1)] = c(i + dx + dy * sy + dz * sz);
1746 double m[3];
1747 vof::mycNormal(st, m);
1748 const double al = vof::plicAlpha(m[0], m[1], m[2], ci);
1749 double v[8][3], ctr[3], ar = 0.0;
1750 const int nv = vof::plicPolygon(m[0], m[1], m[2], al, v);
1751 vof::polygonAreaCentroid(v, nv, ctr, ar);
1752 acc += ar;
1753 },
1754 a);
1755 Kokkos::fence();
1756 (void)hh;
1757 return a; // CELL units squared, matching `volume`'s cell volumes
1758 }
1759
1761 void measure(VofBlock& b, double dt) {
1762 const I3 e = b.adv_.extent(), n = b.adv_.inner(), o = b.box.origin();
1763 const int g = ghost_;
1764 const double hh = h_;
1765 SField c = b.adv_.colour();
1766 using MD = MDRange3<SExec>;
1767 MD pol(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z});
1768 double v = 0.0, mx = 0.0, my = 0.0, mz = 0.0;
1769 Kokkos::parallel_reduce(
1770 "vof::block::moments1", pol,
1771 KOKKOS_LAMBDA(int x, int y, int z, double& av, double& ax, double& ay, double& az) {
1772 const double q = c(L3(x, y, z, e));
1773 av += q;
1774 ax += q * (x - g + o.x + 0.5) * hh;
1775 ay += q * (y - g + o.y + 0.5) * hh;
1776 az += q * (z - g + o.z + 0.5) * hh;
1777 },
1778 v, mx, my, mz);
1779 Kokkos::fence();
1780 b.st_.id = b.id;
1781 b.st_.master = b.master;
1782 for (int d = 0; d < 3; ++d) {
1783 b.st_.lo[d] = b.box.lo[d];
1784 b.st_.hi[d] = b.box.hi[d];
1785 }
1786 b.st_.cells = b.box.cells();
1787 b.st_.volume = v;
1788 if (v <= 0.0) {
1789 b.st_.area = 0.0;
1790 for (int d = 0; d < 3; ++d)
1791 b.st_.centroid[d] = b.st_.velocity[d] = 0.0;
1792 for (int d = 0; d < 6; ++d)
1793 b.st_.moment[d] = 0.0;
1794 return;
1795 }
1796 const double cx = mx / v, cy = my / v, cz = mz / v;
1797 double sxx = 0, syy = 0, szz = 0, sxy = 0, sxz = 0, syz = 0;
1798 Kokkos::parallel_reduce(
1799 "vof::block::moments2", pol,
1800 KOKKOS_LAMBDA(int x, int y, int z, double& axx, double& ayy, double& azz, double& axy,
1801 double& axz, double& ayz) {
1802 const double q = c(L3(x, y, z, e));
1803 const double px = (x - g + o.x + 0.5) * hh - cx, py = (y - g + o.y + 0.5) * hh - cy,
1804 pz = (z - g + o.z + 0.5) * hh - cz;
1805 axx += q * px * px;
1806 ayy += q * py * py;
1807 azz += q * pz * pz;
1808 axy += q * px * py;
1809 axz += q * px * pz;
1810 ayz += q * py * pz;
1811 },
1812 sxx, syy, szz, sxy, sxz, syz);
1813 Kokkos::fence();
1814 const double nc[3] = {cx, cy, cz};
1815 for (int d = 0; d < 3; ++d) {
1816 b.st_.velocity[d] = (b.hasPrev_ && dt > 0.0) ? (nc[d] - b.prevCentroid_[d]) / dt : 0.0;
1817 b.prevCentroid_[d] = nc[d];
1818 b.st_.centroid[d] = nc[d];
1819 }
1820 b.hasPrev_ = true;
1821 b.st_.area = interfaceArea(b);
1822 const double iv = 1.0 / v;
1823 b.st_.moment[0] = sxx * iv;
1824 b.st_.moment[1] = syy * iv;
1825 b.st_.moment[2] = szz * iv;
1826 b.st_.moment[3] = sxy * iv;
1827 b.st_.moment[4] = sxz * iv;
1828 b.st_.moment[5] = syz * iv;
1829 }
1830
1831 // ---- C1: the batched container stages (`vof/block_batch.hpp`) -------------------------------
1832
1836 bool csfBatchEligible() const {
1837 if (!batchEligible())
1838 return false;
1839 for (const auto& b : blocks_) {
1840 if (!b.mine_)
1841 continue;
1842 if (!b.curv_.ready() || !b.curv_.useWorklist || b.curv_.timingOn || !b.f_[0].extent(0))
1843 return false;
1844 }
1845 return true;
1846 }
1847
1856 std::vector<VofBlock*> mb;
1857 for (auto& b : blocks_)
1858 if (b.mine_)
1859 mb.push_back(&b);
1860 const std::size_t nb = mb.size();
1861 if (nb == 0)
1862 return;
1863 std::vector<VofCurvJob> jobs(nb);
1864 bool anyClip = false;
1865 for (std::size_t k = 0; k < nb; ++k) {
1866 VofBlock& b = *mb[k];
1867 const VofCurvature& cv = b.curv_;
1868 const VofCurvFallbackJob fj = cv.fallbackJob(b.adv_.colour());
1869 VofCurvJob J;
1870 J.c = fj.c;
1871 J.mx = fj.mx;
1872 J.my = fj.my;
1873 J.mz = fj.mz;
1874 J.al = fj.al;
1875 J.kap = fj.kap;
1876 J.br = fj.br;
1877 J.slot = cv.slotMap().data();
1878 for (int c = 0; c < 3; ++c)
1879 J.f[c] = b.f_[c].data();
1880 J.e = cv.extent();
1881 J.n = cv.inner();
1882 J.g = cv.ghost();
1883 J.ieps = cv.interfaceEps;
1884 J.mtol = cv.monoTol;
1885 J.ptW = cv.ptWeightWidth * cv.metric.maxH(); // heightPass's `ptW`
1886 J.peps = cv.pureEps;
1887 J.dW = fj.dW; // fallbackPass's `weightWidth * metric.maxH()`
1888 J.cmin = fj.cmin;
1889 J.km = cv.kappaClipValue();
1890 J.forceFb = cv.debugForceFallback;
1891 J.oneDir = cv.debugSingleDirection;
1892 J.useFit = cv.useMixedHeightFit;
1893 J.gm = cv.metric;
1894 anyClip = anyClip || (J.km > 0.0);
1895 jobs[k] = J;
1896 }
1897 if (static_cast<std::size_t>(cStartG_.extent(0)) < nb) {
1898 cStartG_ = LField("vof::block::batch_curv_sG", nb);
1899 cEndG_ = LField("vof::block::batch_curv_eG", nb);
1900 cStartI_ = LField("vof::block::batch_curv_sI", nb);
1901 cEndI_ = LField("vof::block::batch_curv_eI", nb);
1902 cCnt_ = LField("vof::block::batch_curv_counts", 8 * nb);
1903 cCntHost_ = Kokkos::create_mirror_view(cCnt_);
1904 }
1905 Kokkos::deep_copy(SExec(), cCnt_, 0L);
1906 const double wgt[3] = {metric_.h[0] * metric_.h[0], metric_.h[1] * metric_.h[1],
1907 metric_.h[2] * metric_.h[2]}; // buildCsfForce's `1/h_a'^2` weights
1908 for (std::size_t j0 = 0; j0 < nb; j0 += kVofBlockBatch) {
1909 VofCurvTable T;
1910 T.nj = static_cast<int>(std::min<std::size_t>(kVofBlockBatch, nb - j0));
1911 T.base = static_cast<int>(j0);
1912 for (int k = 0; k < T.nj; ++k)
1913 T.job[k] = jobs[j0 + k];
1914 const long rg = vofSetOffsets(T, vofCurvGrown);
1915 if (static_cast<long>(cListG_.extent(0)) < rg)
1916 cListG_ = LField(
1917 Kokkos::view_alloc(std::string("vof::block::batch_curv_listG"), Kokkos::WithoutInitializing),
1918 rg);
1919 // design G §5.2: the chunk's moment cache, sized and reallocated exactly like cListG_ (the
1920 // counts stay on the device); its positions are the slots, hence the int guard
1921 if (rg >= (1L << 31))
1922 throw std::runtime_error(
1923 "peclet::flow::vof::VofBlockSet: grown list capacity >= 2^31 (the slot map is int)");
1924 if (static_cast<long>(cCacheG_.extent(0)) < rg)
1925 cCacheG_ = Kokkos::View<PvMoments*, SMem>(
1926 Kokkos::view_alloc(std::string("vof::block::batch_curv_cacheG"), Kokkos::WithoutInitializing),
1927 rg);
1928 T.cache = cCacheG_.data();
1929 vofCurvCompact(T, true, cListG_, cStartG_, cEndG_);
1931 vofCurvPlanesList(T, cListG_, cStartG_, cEndG_);
1932 const long ri = vofSetOffsets(T, vofCurvInner);
1933 if (static_cast<long>(cListI_.extent(0)) < ri)
1934 cListI_ = LField(
1935 Kokkos::view_alloc(std::string("vof::block::batch_curv_listI"), Kokkos::WithoutInitializing),
1936 ri);
1937 vofCurvCompact(T, false, cListI_, cStartI_, cEndI_);
1938 vofCurvHfReset(T);
1939 vofCurvListPasses(T, anyClip, cListI_, cStartI_, cEndI_, cCnt_); // tiers 1-3, clip, census
1940 vofCsfForceBatch(T, sigma, wgt[0], wgt[1], wgt[2]);
1941 }
1942 Kokkos::deep_copy(cCntHost_, cCnt_); // the one read of the curvature stage
1943 for (std::size_t k = 0; k < nb; ++k) {
1944 const long* ct = &cCntHost_(8 * k);
1945 curvStats_.interfacial += ct[0];
1946 curvStats_.hf += ct[1];
1947 curvStats_.hfMixed += ct[2];
1948 curvStats_.hfFit += ct[3];
1949 curvStats_.pv += ct[4];
1950 curvStats_.pvReduced += ct[5];
1951 curvStats_.noEstimate += ct[6];
1952 curvStats_.clipped += ct[7];
1953 }
1954 }
1955
1960 bool batchEligible() const {
1961 if (!batched)
1962 return false;
1963 for (const auto& b : blocks_) {
1964 if (!b.mine_)
1965 continue;
1966 const WyAdvector& a = b.adv_;
1967 if (!b.allocated_ || a.hasGeometry() || a.debugRecomputeDilation || !a.useWorklist ||
1968 a.timingOn || a.globalMax || !a.exchange || a.ghost() != ghost_)
1969 return false;
1970 }
1971 return true;
1972 }
1973
1975 VofBlockJob makeJob(VofBlock& b, double dt) const {
1976 WyAdvector& a = b.adv_;
1977 VofBlockJob J;
1978 J.c = a.colour().data();
1979 J.mx = a.planeM(0).data();
1980 J.my = a.planeM(1).data();
1981 J.mz = a.planeM(2).data();
1982 J.al = a.planeAlpha().data();
1983 J.fl = a.faceFlux().data();
1984 for (int d = 0; d < 3; ++d)
1985 J.u[d] = a.faceVel(d).data();
1986 J.cc = a.dilationFlag().data();
1987 J.outside = a.hasOutsideMask() ? a.outsideMask().data() : nullptr;
1988 J.e = a.extent();
1989 J.n = a.inner();
1990 J.o = b.box.origin();
1991 J.g = a.ghost();
1992 J.dth = dt / a.h(); // WyAdvector::advect's `dth`
1993 J.weps = a.wispEps;
1994 J.interfaceCfl = a.interfaceLocalCfl;
1995 const int L[3] = {gs_.x, gs_.y, gs_.z};
1996 for (int d = 0; d < 3; ++d)
1997 J.span[d] = per_[d] && b.box.n(d) == L[d];
1998 for (int f = 0; f < 6; ++f)
1999 J.bcOwn[f] = a.bcFaceOwned(f) ? 1u : 0u;
2000 return J;
2001 }
2002
2003 static VofBlockTable batchTable(const std::vector<VofBlockJob>& jobs, std::size_t j0) {
2004 VofBlockTable T;
2005 T.nj = static_cast<int>(std::min<std::size_t>(kVofBlockBatch, jobs.size() - j0));
2006 T.base = static_cast<int>(j0);
2007 for (int k = 0; k < T.nj; ++k)
2008 T.job[k] = jobs[j0 + k];
2009 return T;
2010 }
2011
2017 for (int a = 0; a < 3; ++a) {
2018 VofBlockTable P;
2019 P.nj = 0;
2020 P.base = T.base;
2021 for (int k = 0; k < T.nj; ++k)
2022 if (T.job[k].span[a])
2023 P.job[P.nj++] = T.job[k];
2024 if (P.nj == 0)
2025 continue;
2026 const int b = (a + 1) % 3, c = (a + 2) % 3;
2027 vofSetOffsets(P, [b, c](const VofBlockJob& J) {
2028 const int dims[3] = {J.e.x, J.e.y, J.e.z};
2029 return static_cast<long>(dims[b]) * dims[c];
2030 });
2031 vofBatchPeriodic(P, a);
2032 }
2033 if (!per_[0] || !per_[1] || !per_[2])
2034 vofBatchClamp(T, gs_, per_[0], per_[1], per_[2]);
2035 }
2036
2039 void ensureRead1(std::size_t len) {
2040 if (static_cast<std::size_t>(bRead1_.extent(0)) >= len)
2041 return;
2042 if (bRead1_.extent(0) == 0)
2043 bRead1_ = SField("vof::block::batch_read1", len);
2044 else
2045 Kokkos::resize(bRead1_, len);
2046 bRead1Host_ = Kokkos::create_mirror_view(bRead1_);
2047 }
2048
2056 void measureBatched(double dt) {
2057 flushStats();
2058 std::vector<std::size_t> idx;
2059 for (std::size_t k = 0; k < blocks_.size(); ++k)
2060 if (blocks_[k].mine_)
2061 idx.push_back(k);
2062 const std::size_t nb = idx.size();
2063 if (nb == 0)
2064 return;
2065 std::vector<VofBlockJob> jobs(nb);
2066 for (std::size_t k = 0; k < nb; ++k) {
2067 VofBlock& b = blocks_[idx[k]];
2068 jobs[k] = makeJob(b, 0.0);
2069 b.st_.id = b.id;
2070 b.st_.master = b.master;
2071 for (int d = 0; d < 3; ++d) {
2072 b.st_.lo[d] = b.box.lo[d];
2073 b.st_.hi[d] = b.box.hi[d];
2074 }
2075 b.st_.cells = b.box.cells();
2076 }
2077 ensureRead1(kVofStats * nb);
2078 for (std::size_t j0 = 0; j0 < nb; j0 += kVofBlockBatch) {
2079 const VofBlockTable T = batchTable(jobs, j0);
2080 VofStatsPrev P;
2081 for (int k = 0; k < T.nj; ++k) {
2082 const VofBlock& b = blocks_[idx[j0 + k]];
2083 for (int d = 0; d < 3; ++d)
2084 P.c[k][d] = b.prevCentroid_[d];
2085 P.valid[k] = b.hasPrev_ ? 1u : 0u;
2086 }
2087 vofBatchStats(T, P, h_, areaEps, dt, bRead1_, 0);
2088 }
2089 statsPend_ = std::move(idx);
2090 }
2091
2094 void applyStats(const double* hs) {
2095 for (std::size_t k = 0; k < statsPend_.size(); ++k) {
2096 VofBlock& b = blocks_[statsPend_[k]];
2097 const double* q = hs + kVofStats * k;
2098 b.st_.volume = q[0];
2099 if (q[0] <= 0.0) {
2100 b.st_.area = 0.0;
2101 for (int d = 0; d < 3; ++d)
2102 b.st_.centroid[d] = b.st_.velocity[d] = 0.0;
2103 for (int d = 0; d < 6; ++d)
2104 b.st_.moment[d] = 0.0;
2105 continue;
2106 }
2107 for (int d = 0; d < 3; ++d) {
2108 b.st_.velocity[d] = q[4 + d];
2109 b.prevCentroid_[d] = q[1 + d];
2110 b.st_.centroid[d] = q[1 + d];
2111 }
2112 b.hasPrev_ = true;
2113 b.st_.area = q[7];
2114 for (int d = 0; d < 6; ++d)
2115 b.st_.moment[d] = q[8 + d];
2116 }
2117 statsPend_.clear();
2118 }
2119
2122 void flushStats() {
2123 if (statsPend_.empty())
2124 return;
2125 const std::size_t len = kVofStats * statsPend_.size();
2126 Kokkos::deep_copy(Kokkos::subview(bRead1Host_, std::make_pair(std::size_t(0), len)),
2127 Kokkos::subview(bRead1_, std::make_pair(std::size_t(0), len)));
2128 applyStats(bRead1Host_.data());
2129 }
2130
2136 void advectBatched(double dt) {
2137 std::vector<VofBlock*> mb;
2138 for (auto& b : blocks_)
2139 if (b.mine_)
2140 mb.push_back(&b);
2141 if (mb.empty()) {
2142 flushStats();
2143 return;
2144 }
2145 const std::size_t nb = mb.size();
2146 std::vector<VofBlockJob> jobs(nb);
2147 bool anyMask = false;
2148 for (std::size_t k = 0; k < nb; ++k) {
2149 jobs[k] = makeJob(*mb[k], dt);
2150 anyMask = anyMask || (jobs[k].outside != nullptr);
2151 }
2152 if (static_cast<std::size_t>(bStart_.extent(0)) < nb) {
2153 bStart_ = LField("vof::block::batch_start", nb);
2154 bEnd_ = LField("vof::block::batch_end", nb);
2155 }
2156 // read #1's packet: the deferred statistics of the last batched measure (C2) first, then this
2157 // step's Courant numbers -- one device->host copy for both
2158 const std::size_t off = kVofStats * statsPend_.size();
2159 ensureRead1(off + nb);
2160 auto cfl = Kokkos::subview(bRead1_, std::make_pair(off, off + nb));
2161 ensurePacket(nb);
2162 // (1) Courant numbers: one launch, one read, the throw before any block moves (R4)
2163 for (std::size_t j0 = 0; j0 < nb; j0 += kVofBlockBatch)
2165 Kokkos::deep_copy(Kokkos::subview(bRead1Host_, std::make_pair(std::size_t(0), off + nb)),
2166 Kokkos::subview(bRead1_, std::make_pair(std::size_t(0), off + nb)));
2167 applyStats(bRead1Host_.data());
2168 for (std::size_t k = 0; k < nb; ++k)
2169 mb[k]->adv_.enforceCfl(bRead1Host_(off + k), dt);
2170 Kokkos::deep_copy(SExec(), pk_, 0.0); // the WO-R ledger slots accumulate over the sweeps
2171 const int* perm = kWySweepPerm[static_cast<int>(step_ % 6)];
2172 for (std::size_t j0 = 0; j0 < nb; j0 += kVofBlockBatch) {
2173 VofBlockTable T = batchTable(jobs, j0);
2174 // (2) THE dilation flag, frozen once from C^n
2176 vofBatchFreeze(T);
2177 // (3) three directional sweeps
2178 for (int s = 0; s < 3; ++s) {
2179 const int d = perm[s];
2180 const long region = vofSetOffsets(T, vofJobGrown1);
2181 if (static_cast<long>(bList_.extent(0)) < region)
2182 bList_ = LField(Kokkos::view_alloc(std::string("vof::block::batch_list"),
2183 Kokkos::WithoutInitializing),
2184 region);
2185 vofBatchWorklist(T, bList_, bStart_, bEnd_);
2186 vofBatchPlic(T, bList_, bStart_, bEnd_);
2187 vofSetOffsets(T, [d](const VofBlockJob& J) { return vofJobFaces(J, d); });
2188 vofBatchFlux(T, d);
2189 if (anyMask)
2190 vofBatchBcLedger(T, d, pk_, kPk, kPkBcv);
2192 vofBatchUpdate(T, d);
2193 batchGhostFill(T);
2194 }
2195 }
2196 for (std::size_t k = 0; k < nb; ++k)
2197 mb[k]->adv_.noteBatchedStep();
2198 }
2199
2208 void debrisBatched(std::vector<VofBlock*>& bs) {
2209 const std::size_t nb = bs.size();
2210 if (nb == 0)
2211 return;
2214 F.doResidue = wispEps > 0.0;
2215 F.remove = debrisRemove;
2216 F.vaMin = kDebrisMinAttached;
2217 // the predicates of debrisPassBatchImpl (review finding 7 there: the three lists are disjoint)
2218 const double weps = wispEps, cfull = kDebrisMaxC;
2219 const double ieps = curvProto.interfaceEps > weps ? curvProto.interfaceEps : weps;
2220 std::vector<VofBlockJob> jobs(nb);
2221 for (std::size_t k = 0; k < nb; ++k) {
2222 bs[k]->st_.debrisCells = 0;
2223 bs[k]->st_.debrisVolume = 0.0;
2224 jobs[k] = makeJob(*bs[k], 0.0);
2225 }
2226 if (static_cast<std::size_t>(dCap_.extent(0)) < nb) {
2227 for (int q = 0; q < 6; ++q)
2228 dRange_[q] = LField("vof::block::batch_debris_range", nb);
2229 dCap_ = Kokkos::View<int*, SMem>("vof::block::batch_debris_cap", nb);
2230 }
2231 ensurePacket(nb);
2232 Kokkos::deep_copy(SExec(), dCap_, 0);
2233 const LField sD = dRange_[0], eD = dRange_[1], sR = dRange_[2], eR = dRange_[3],
2234 sA = dRange_[4], eA = dRange_[5];
2235 for (std::size_t j0 = 0; j0 < nb; j0 += kVofBlockBatch) {
2236 VofBlockTable T = batchTable(jobs, j0);
2237 const long region = vofSetOffsets(T, vofJobInner);
2238 auto ensure = [&](LField& f, const char* name) {
2239 if (static_cast<long>(f.extent(0)) < region)
2240 f = LField(Kokkos::view_alloc(std::string(name), Kokkos::WithoutInitializing), region);
2241 };
2242 if (F.doDebris) {
2243 ensure(listD_, "vof::block::debris_list");
2244 vofBatchMark(T, 0, ieps, cfull, weps, listD_, sD, eD);
2245 }
2246 if (F.doResidue) {
2247 ensure(listR_, "vof::block::residue_list");
2248 vofBatchMark(T, 1, ieps, cfull, weps, listR_, sR, eR);
2249 }
2250 ensure(listA_, "vof::block::attached_list");
2251 vofBatchMark(T, 2, ieps, cfull, weps, listA_, sA, eA);
2252 vofBatchDebrisSums(T, F, listD_, listR_, listA_, sD, eD, sR, eR, sA, eA, pk_, kPk, 0);
2253 if (static_cast<long>(debrisEx_.extent(0)) < region)
2254 debrisEx_ = SField(
2255 Kokkos::view_alloc("vof::block::debris_excess", Kokkos::WithoutInitializing), region);
2256 vofBatchDebrisWrite(T, F, listD_, listR_, listA_, sD, sR, sA, eA, pk_, kPk, 0, debrisEx_,
2257 dCap_);
2258 vofBatchDebrisLost(T, sA, eA, pk_, kPk, 0, debrisEx_, dCap_);
2259 batchGhostFill(T); // the inner colour moved; the next reader expects valid ghosts
2260 }
2261 }
2262
2264 void applyDebrisLedger(std::vector<VofBlock*>& bs, bool doDebris) {
2265 for (std::size_t k = 0; k < bs.size(); ++k) {
2266 VofBlockStats& st = bs[k]->st_;
2267 const double* ho = &pkHost_(kPk * k);
2268 const long nD = static_cast<long>(ho[kDbND]);
2269 const bool removeD = doDebris && debrisRemove && nD > 0;
2270 const bool acted = ho[kDbActed] != 0.0;
2271 if (ho[kDbPending] == 0.0) {
2272 if (doDebris && nD > 0) { // census only: report what WOULD be removed
2273 st.debrisCells = nD;
2274 st.debrisVolume = ho[kDbSumD];
2275 }
2276 continue; // nothing to remove: nothing else ran
2277 }
2278 if (doDebris && !debrisRemove && nD > 0) { // census of what WOULD be removed
2279 st.debrisCells = nD;
2280 st.debrisVolume = ho[kDbSumD];
2281 }
2282 if (!acted) { // step 3: no attached interface worth a cell -- the clip bounds it
2283 if (removeD)
2284 st.debrisUnresolved += nD;
2285 continue;
2286 }
2287 if (removeD) {
2288 st.debrisCells = nD;
2289 st.debrisVolume = ho[kDbSumD];
2290 st.debrisReturned += ho[kDbSumD];
2291 }
2292 st.residueReturned += ho[kDbSumR];
2293 st.debrisLost += ho[kDbDV]; // `lost`, written over the dV slot by the act
2294 }
2295 }
2296
2299 static constexpr int kPkBox = kDbSlots, kPkBcv = kDbSlots + 6, kPk = kDbSlots + 12;
2300 void ensurePacket(std::size_t nb) {
2301 if (static_cast<std::size_t>(pk_.extent(0)) < kPk * nb) {
2302 pk_ = SField("vof::block::batch_packet", kPk * nb);
2303 pkHost_ = Kokkos::create_mirror_view(pk_);
2304 }
2305 }
2306
2312 std::vector<std::size_t> idx;
2313 std::vector<VofBlock*> mb;
2314 for (std::size_t k = 0; k < blocks_.size(); ++k)
2315 if (blocks_[k].mine_) {
2316 idx.push_back(k);
2317 mb.push_back(&blocks_[k]);
2318 }
2319 const std::size_t nb = idx.size();
2320 if (nb == 0)
2321 return;
2322 std::vector<VofBlockJob> jobs(nb);
2323 for (std::size_t k = 0; k < nb; ++k)
2324 jobs[k] = makeJob(*mb[k], 0.0);
2325 ensurePacket(nb);
2326 const double eps = bubbleEps > wispEps ? bubbleEps : wispEps; // bubbleBox's threshold (§12.2)
2327 for (std::size_t j0 = 0; j0 < nb; j0 += kVofBlockBatch)
2328 vofBatchBox(batchTable(jobs, j0), eps, pk_, kPk, kPkBox);
2329 Kokkos::deep_copy(pkHost_, pk_); // read #2
2330 if (ranDebris)
2332 for (std::size_t k = 0; k < nb; ++k) { // the WO-R ledger (no block installs a mask today)
2333 if (jobs[k].outside == nullptr)
2334 continue;
2335 for (int f = 0; f < 6; ++f)
2336 if (jobs[k].bcOwn[f])
2337 mb[k]->adv_.addBcFaceVolume(f, pkHost_(kPk * k + kPkBcv + f));
2338 }
2339 for (std::size_t k = 0; k < nb; ++k) {
2340 const double* hb = &pkHost_(kPk * k + kPkBox);
2341 const int lo[3] = {static_cast<int>(hb[0]), static_cast<int>(hb[1]), static_cast<int>(hb[2])};
2342 const int hi[3] = {static_cast<int>(hb[3]), static_cast<int>(hb[4]), static_cast<int>(hb[5])};
2343 if (hi[0] < lo[0])
2344 continue; // the block is empty (bubbleBox returned false)
2345 VofBlock& b = *mb[k];
2346 VofBox bb;
2347 for (int d = 0; d < 3; ++d) {
2348 bb.lo[d] = lo[d] + b.box.lo[d];
2349 bb.hi[d] = hi[d] + 1 + b.box.lo[d];
2350 }
2351 recentreTo(idx[k], bb);
2352 }
2353 }
2354
2355 private:
2356 VofMetric metric_;
2357 double h_ = 1.0;
2358 I3 gs_{0, 0, 0};
2359 std::array<bool, 3> per_{true, true, true};
2360 int rank_ = 0, size_ = 1;
2361 int margin_ =
2362 3;
2363 int ghost_ = 3;
2364 long step_ = 0;
2365 std::vector<VofBlock> blocks_;
2366 std::shared_ptr<VofBlockExchangeBase> exch_;
2367 std::map<std::array<int, 3>, std::vector<WyAdvector>> pool_;
2368 long poolHits_ = 0, poolMisses_ = 0;
2369 long lastReassigned_ = 0;
2370 VofCurvature::Stats curvStats_{};
2371 LField listD_, listA_, listR_;
2372 SField debrisEx_;
2373 SField debrisOut_;
2374 SField::host_mirror_type debrisOutHost_;
2375 // C1 batched stages: per-job device scalars (indexed by the job's place among the master blocks)
2376 SField bRead1_;
2377 SField::host_mirror_type bRead1Host_;
2378 std::vector<std::size_t> statsPend_;
2379 LField bList_, bStart_, bEnd_;
2380 LField dRange_[6];
2381 Kokkos::View<int*, SMem> dCap_;
2382 LField cListG_, cListI_, cStartG_, cEndG_, cStartI_, cEndI_;
2383 Kokkos::View<PvMoments*, SMem> cCacheG_;
2384 LField cCnt_;
2385 LField::host_mirror_type cCntHost_;
2386 SField pk_;
2387 SField::host_mirror_type pkHost_;
2388};
2389
2390} // namespace peclet::flow::vof
2391
2392#endif // PECLET_FLOW_VOF_BLOCK_CONTAINER_HPP
flow — VoF rung V1: Weymouth & Yue (2010) directionally-split, exactly volume-conserving geometric ad...
flow — batched execution of the VoF block container's stages (C1, doc/vof_step_performance_design....
The container: a replicated table of blocks, the per-step orchestration (gather → advect → re-centre ...
void scatterSum(SField sLocal)
S = sum_k C_k of the CURRENT (replicated) table into sLocal's inner region (§5.6).
VofBlockJob makeJob(VofBlock &b, double dt) const
The raw-pointer job of one master block for the batched launches.
VofCurvature::Stats csfCurvatureStats() const
long assignMasters()
Apply plannedMasters(): allocate on the gaining ranks, migrate the colour, free on the losing ranks.
bool bubbleBox(const VofBlock &b, VofBox &out) const
Tight bounding box of C > 0 over the inner region, in GLOBAL indices.
void recentreTo(std::size_t idx, const VofBox &bb)
recentre for a bubble box already measured (the batched path's read #2).
void enableCsf(double sigmaValue)
Turn on the block CSF.
void recentre(std::size_t idx)
Move / resize the block when the bubble has spent its margin (or when the box has grown wastefully la...
double bubbleEps
The colour threshold that defines the BUBBLE EXTENT (and hence the box).
void applyDebrisLedger(std::vector< VofBlock * > &bs, bool doDebris)
The host half of the debris pass: the ledger of debrisPassBatchImpl, from the packet.
void setExchange(std::shared_ptr< VofBlockExchangeBase > x)
VofBlockExchangeBase * exchange() const
long lastReassigned() const
Blocks that changed master at the last advect() (rung W1 item a).
static constexpr double kDebrisMaxC
Debris predicate threshold (doc/vof_overlap_design.md §11): an interfacial cell is debris when NO cel...
void debrisBatched(std::vector< VofBlock * > &bs)
Stage 4 of §5.9, the debris/residue pass of debrisPassBatchImpl on every block of bs in batched launc...
int recentrePad
Cells of slack added beyond the margin when a block is re-centred, so a translating bubble re-allocat...
void flushStats()
Bring deferred statistics to the host now (a separate read; the step itself folds it into read #1).
void debrisPassBatchImpl(std::vector< VofBlock * > &bs)
bool csfEnabled
Rung W2: form the CSF face force on each block (its own curvature cascade, the V4 balanced -force rul...
bool batched
C1 (doc/vof_step_performance_design.md §4.6, §5.9): run each stage of the container step for ALL mast...
double interfaceArea(const VofBlock &b) const
Rung W1 item (d): the marker's interface area, in CELL units squared (the same convention volume uses...
std::vector< VofBlock > & blocks()
The block table.
void debrisPassBatch(std::vector< VofBlock * > &bs, bool deferLedger=false)
The pass on several master blocks at once.
bool timingOn
Instrument: fence-bounded wall time of the debris/residue pass (all blocks, all calls) while timingOn...
void debrisPass(VofBlock &b)
Debris + residue pass on one master block (doc/vof_overlap_design.md §5.2-5.3, predicate of §11,...
double areaEps
Wisp guard on the INTERFACE-AREA predicate (VofBlockStats::area), the same threshold and the same rea...
void computeCsf(SField fx, SField fy, SField fz)
Per-block curvature cascade + the CSF face force, summed into the caller's three face-force patches.
void clipToSeed(VofBlock &b)
A seeded block's colour is its OWN bubble's: the gather copies the whole INNER box (bubble extent + m...
std::array< bool, 3 > periodic() const
VofCurvature curvProto
The curvature cascade's TUNABLES for every block, copied into each block's own VofCurvature when it i...
double sigma
surface-tension coefficient of the block CSF (cell units)
std::vector< double > blockColourHost(std::size_t idx)
One block's INNER colour, copied to the host in x-fastest order over blocks_[idx]....
void setKappaMax(double km)
Set the curvature admissibility clip (VofCurvature::kappaMax, index units) on the prototype AND on ev...
void seedSphere(double cx, double cy, double cz, double r, int subLevels=4, int extraPad=0)
Seed a spherical bubble (centre and radius in CELL units, global).
void finishSeeding(SField cLocal)
Gather the colour of every seedBox-seeded block from cLocal, fill its ghosts and measure.
bool debrisCensus
Run the debris pass in advect() (only while the block CSF is on – a kinematic block run never pays fo...
void advectBatched(double dt)
Stages 1-3 of §5.9 for every master block: the Courant numbers in one launch and ONE host read (the t...
bool debrisRemove
Remove the debris and return its volume (§5.3).
void seedBoxWithColour(const VofBox &bb, const std::vector< double > &colour)
Seed a block whose INNER box is EXACTLY bb (not grown by the margin: bb is what blockColourHost was p...
void buildCsfForce(VofBlock &b)
The V4 balanced-force CSF on the BLOCK's faces — the same csfFaceCurvature + csfFaceForce pair Solver...
void seedBox(const VofBox &bb, int extraPad=0)
Seed one block on the global index box bb (the BUBBLE extent; the inner box is bb grown by margin),...
void recentreBatched(bool ranDebris)
Stage 5 of §5.9 on the batched path: every master block's bubble box in one launch,...
const std::vector< VofBlock > & blocks() const
void init(I3 gs, std::array< bool, 3 > per, int rank, int size, double h=1.0)
void masterCensus(std::vector< long > &masters, std::vector< long > &cells) const
Load-balance census of the CURRENT master assignment: masters[r] = blocks mastered by rank r,...
void ensureRead1(std::size_t len)
Grow read #1's packet to len doubles, KEEPING its contents (deferred statistics may wait in the front...
const VofMetric & metric() const
void measureBatched(double dt)
C2 (§5.10): measure() for every master block in one launch (vofBatchStats), with NO host read – the v...
double cellImbalance() const
max/mean of the per-rank block-cell load (1.0 = perfect). 0 blocks -> 1.0.
static VofCurvature blockCurvProto()
The block prototype: the class defaults, plus the admissibility clip ON at 1/Delta_min (doc/vof_overl...
void measure(VofBlock &b, double dt)
Volume, centroid, centroid velocity and the central second moments over the inner box.
void applyStats(const double *hs)
Apply the deferred statistics in hs (host, kVofStats doubles per pending block) exactly as measure() ...
void scatter(SField cLocal)
Union the current colour into cLocal without advecting (used right after seeding).
void advect(double dt, SField cLocal)
One kinematic block step: gather the face velocity, advect every local block, re-centre,...
void setMetric(const VofMetric &g)
The anisotropic cell metric (Phase 3).
std::vector< VofBlockStats > statsAll()
Per-bubble census on the master (empty entries on a non-master).
int poolCapacity
advectors kept per extent; beyond it the retired one is freed
static constexpr double kDebrisMinAttached
Guard of the volume return (§5.3 step 3): a marker whose attached interface holds less than this much...
void fillBlockGhosts(VofBlock &b)
The block's own colour ghost policy — see the file header.
bool csfBatchEligible() const
The batched cascade applies where the container step is batched and every master block's cascade runs...
double outOfBoxSum(const VofBlock &b, const VofBox &nb) const
Sum of the block's colour over inner cells whose GLOBAL index falls outside nb.
std::vector< int > plannedMasters() const
The master each block WOULD get under assignMode, from the current block cell counts.
void computeCsfBatched()
Stage 6 of §5.9: the curvature cascade (compaction, planes, tiers 1-2, tier 3, the clip,...
double cflLimit
The Weymouth-Yue cap of every block advector.
double innerSum(const VofBlock &b) const
std::vector< double > blockKappaHost(std::size_t idx)
One marker's OWN curvature over its inner box, x-fastest (empty unless this rank masters it and the b...
VofMasterAssign assignMode
Rung W1 item (a): how masters are chosen, and how often the choice is revisited.
void setWispEps(double eps)
Set wispEps on the set AND on every live advector (pooled ones take it on reuse).
void batchGhostFill(VofBlockTable T)
fillBlockGhosts of every job of T, pass by pass in the per-block pass order: zero,...
bool usePool
Rung W1 item (c): recycle the advectors a re-centring retires, keyed by the EXACT extent.
static constexpr int kPkBox
The per-job packet of read #2 (§5.9): the debris slots (VofDebrisSlot), the bubble box,...
bool batchEligible() const
The batched path applies to the plain advect() configuration: every master block an uncut all-fluid a...
double wispEps
The Weymouth-Yue wisp tolerance of every block advector (WyAdvector::wispEps); the solver sets it to ...
double outOfBoxSum(const VofBlock &b, const VofBox &nb, const bool wrap[3]) const
... where an axis flagged in wrap spans the whole periodic domain in nb and is copied with the wrap: ...
bool allowShrink
Let a block SHRINK when it has grown much larger than the bubble needs.
static VofBlockTable batchTable(const std::vector< VofBlockJob > &jobs, std::size_t j0)
int masterOf(long id) const
Master assignment: round robin by block id (VOF_PLAN §10 W0; the weighted-ORB assignment is W1).
void allocateCsf(std::size_t idx)
VofBox extended(int ghost) const
const VofCurvature & curvature() const
void deserializeAux(const double in[kAuxLen])
void serializeAux(double out[kAuxLen]) const
static constexpr int kAuxLen
The non-colour state a master carries between steps: the previous centroid (so a migrated block's rep...
VofBox seed_
The BUBBLE extent this block was seeded from (empty for a sphere seed, which paints its own colour).
VofCurvature & curvature()
Rung W2: the block's own curvature cascade and the CSF face force it forms.
const WyAdvector & advector() const
VofBox box
the INNER box, global
const VofBlockStats & stats() const
Curvature of the colour field on an extended (inner + ghost) block.
double interfaceEps
Wisp threshold on the interfacial predicate.
static void fallbackBatch(const std::vector< VofCurvFallbackJob > &jobs)
Tier 3 of many cascades in as few launches as possible: one kernel per kFallbackBatch jobs over their...
double pureEps
Tolerance of the PURITY test that ends a height-function column (hfIsFull/hfIsEmpty),...
double kappaMax
Curvature admissibility clip (doc/vof_overlap_design.md §5.1, §11): after the cascade,...
VofMetric metric
The anisotropic cell metric (Phase 3).
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
flow — VoF rung V2a (WO-J): the bridge between the solver's G = 2 block and the colour field's own g ...
flow — VoF rung V3 (WO-O): the view-level driver for the curvature cascade.
flow — portable (Kokkos) MAC stencil operators: Red-Black Gauss-Seidel smoothers + divergence.
long vofJobGrown1(const VofBlockJob &J)
The inner region grown by one cell per side: WyAdvector::reconstructImpl's range.
void vofCurvCompact(const VofCurvTable &T, bool grown, LField list, LField start, LField end)
VofCurvature::compact: the interfacial cells of the grown (grown) or inner region,...
bool vofDebrisCell(const SF &c, long i, long sy, long sz, double ieps, double cfull)
The debris predicate of doc/vof_overlap_design.md §11 on cell i: interfacial, and NO cell of the mark...
void vofBatchFlux(const VofBlockTable &T, int d)
WyAdvector::computeFluxesImpl (uncut): one flux per d-face, wyFaceFlux (or, for a job carrying the WO...
Kokkos::View< long *, SMem > LField
Definition advect_wy.hpp:90
void vofCurvListPasses(const VofCurvTable &T, bool anyClip, LField list, LField start, LField end, LField cnt)
The whole cascade list stage of a chunk: tiers 1-2, tier 3, the clip (only if some job clips),...
void vofBatchCfl(const VofBlockTable &T, SField out)
One team per job: WyAdvector::maxCourant or maxCourantInterface (per the job's interfaceCfl),...
long vofSetOffsets(VofBlockTable &T, F size)
Fill T.off from a per-job range size.
void vofBatchClamp(const VofBlockTable &T, I3 gs, bool px, bool py, bool pz)
Pass 3 (colour_field.hpp::clampFill, skip = 0): the part of each extended block outside a NON-periodi...
void vofBatchPeriodic(const VofBlockTable &T, int a)
Pass 2 on axis a (colour_field.hpp::periodicFill, one axis): only the jobs whose block spans the peri...
constexpr int kWySweepPerm[6][3]
The six permutations of (x, y, z), cycled by step index so no direction is systematically favoured (p...
void vofBatchGhostZero(const VofBlockTable &T)
Pass 1: every ghost cell of every job -> 0.
void vofAssignOrb(const std::vector< long > &w, int size, std::vector< int > &master)
Weighted ORB over a 1-D block space: the blocks are laid out as a 1-D "grid" of N cells in block-id o...
constexpr int kVofStats
Doubles per job in the statistics packet: volume, centroid[3], velocity[3], area, moment[6].
constexpr int kVofBlockBatch
Jobs per launch; a container with more master blocks runs each stage in chunks.
@ kDbND
debris count
@ kDbActed
1 when the return ran (pending and VA >= kDebrisMinAttached and W > 0)
@ kDbSumR
sum_R C in list order (signed residue)
@ kDbPending
1 when there was something to remove (removeD or nR > 0)
@ kDbSumD
sum_D C in list order (the removed / census debris volume)
@ kDbDV
the volume to return (D if removing, then R); replaced by lost once acted
void vofBatchDebrisSums(const VofBlockTable &T, VofDebrisFlags F, LField listD, LField listR, LField listA, LField sD0, LField eD0, LField sR0, LField eR0, LField sA0, LField eA0, SField out, int stride, int slot0)
Step 2 of the pass: one thread per job, every sum in list order (the per-block kernel's single thread...
long vofJobFaces(const VofBlockJob &J, int d)
A sweep's face range of one job: the inner region with one extra layer on the low side of d.
void vofCurvHfReset(const VofCurvTable &T)
heightPass (worklist mode), part 1: every inner cell to kappa = 0, branch = kCurvNone.
long vofJobInner(const VofBlockJob &J)
void vofCurvPlanesList(const VofCurvTable &T, LField list, LField start, LField end)
Part 2: wyReconstructCell over the grown interfacial list (upper bound: the grown region),...
void vofBatchMark(const VofBlockTable &T, int kind, double ieps, double cfull, double weps, LField list, LField start, LField end)
One mark scan over the concatenated inner regions (T.off = vofJobInner), in each block's inner-box in...
void vofCsfForceBatch(const VofCurvTable &T, double sig, double w0, double w1, double w2)
VofBlockSet::buildCsfForce for every job, the three components per cell: the V4 balanced-force CSF at...
void vofBatchBox(const VofBlockTable &T, double eps, SField out, int stride, int slot0)
One team per job: the tight box of |C| > eps over the inner region, in the block's LOCAL inner frame ...
VofBox vofClampBox(VofBox b, I3 gs, const std::array< bool, 3 > &per)
Clamp a block box so it is representable: at most the whole axis on a periodic axis,...
void vofBatchPlic(const VofBlockTable &T, LField list, LField start, LField end)
wyReconstructCell over each job's compacted list: launched over the upper bound (the job's region),...
void vofAssignLpt(const std::vector< long > &w, int size, std::vector< int > &master)
Longest-processing-time greedy (Graham 1969): heaviest block first onto the currently least-loaded ra...
void vofCurvPlanesZero(const VofCurvTable &T)
reconstructPlanes (worklist mode), part 1: zero the four plane fields and clear the slot map (design ...
void vofBatchDebrisLost(const VofBlockTable &T, LField sA0, LField eA0, SField out, int stride, int slot0, SField ex, Kokkos::View< int *, SMem > capf)
lost: the capped excess of an acted job summed in list order on one thread (only where a cap fired,...
int vofAxisRuns(int a, int b, int gs, bool periodic, VofRun out[5], int outside=0)
Decompose [a, b) on one axis into global runs.
void vofBatchBcLedger(const VofBlockTable &T, int d, SField out, int stride, int slot0)
The WO-R boundary ledger (WyAdvector::accumulateBcFaceVolume) of the jobs carrying the mask: one thre...
long vofJobLen(const VofBlockJob &J)
VofMasterAssign
How a block's MASTER rank is chosen.
@ Lpt
longest-processing-time greedy on the block cell counts (rung W1 default)
@ RoundRobin
id % size — rung W0's assignment; blind to block SIZE
@ WeightedOrb
weighted ORB over a 1-D "block space" (core::decomp::BlockDecomposer<1>)
bool vofAttachedCell(const SF &c, long i, long sy, long sz, double ieps, double cfull)
The ATTACHED predicate (§5.3 step 2): interfacial with a cell at or above cfull within two.
void clampFill(SField f, I3 e, int g, I3 o, I3 gs, bool px, bool py, bool pz, int skip=0)
Zero-gradient (Neumann) fill of the ghost cells that lie OUTSIDE the global domain on a non-periodic ...
void vofBatchWorklist(const VofBlockTable &T, LField list, LField start, LField end)
WyAdvector::reconstructImpl's worklist scan, over the concatenated grown-by-one regions (T....
void periodicFill(SField f, I3 e, int g, bool px, bool py, bool pz)
Periodic wrap of a single-block extended field at ghost width g (the single-rank ghost path; under MP...
long vofCurvInner(const VofCurvJob &J)
void vofBatchStats(const VofBlockTable &T, const VofStatsPrev &P, double hh, double aeps, double dt, SField out, long off)
VofBlockSet::measure for every job in one launch, one team per job: the first moments,...
double csfFaceForce(double sigma, double kf, double dC, double hGrad)
The balanced-force CSF contribution to the momentum RHS at one staggered velocity unknown,...
void vofBatchFreeze(const VofBlockTable &T)
WyAdvector::freezeDilationFlag over every job's extended block.
void vofBatchUpdate(const VofBlockTable &T, int d)
WyAdvector::applySweepImpl (uncut) over the inner regions (T.off = vofJobInner); the expression verba...
bool vofResidueCell(double ci, double weps)
The residue predicate (§13): 0 < |C| <= wispEps.
bool csfFaceCurvature(double k0, double b0, double k1, double b1, double &kf)
Face curvature for the balanced-force CSF, from the two cells sharing the face.
long vofCurvGrown(const VofCurvJob &J)
The inner region grown by kPvHalf: every cell VofCurvature::reconstructPlanes writes.
void vofBatchDebrisWrite(const VofBlockTable &T, VofDebrisFlags F, LField listD, LField listR, LField listA, LField sD0, LField sR0, LField sA0, LField eA0, SField out, int stride, int slot0, SField ex, Kokkos::View< int *, SMem > capf)
Step 4, the act: removed cells -> 0, attached C += dV C(1-C)/W capped at 1 (the per-block write kerne...
long L3(int x, int y, int z, I3 e)
Kokkos::DefaultExecutionSpace SExec
Kokkos::View< double *, SMem > SField
flow — the multidimensional iteration policies: iteration order follows storage.
Gather / scatter / table replication.
virtual void scatterForceSum(std::vector< VofBlock > &blocks, SField fx, SField fy, SField fz)=0
SUM every master block's CSF face force into the caller's three face-force patches (TBFsolver's UNPAC...
virtual void migrateColour(std::vector< VofBlock > &blocks, const std::vector< int > &oldMaster)=0
Move a block's state from oldMaster[i] to blocks[i].master after a re-assignment.
virtual void scatterColourSum(std::vector< VofBlock > &, SField)
SUM every master block's inner colour into the caller's colour patch: S = sum_k C_k,...
virtual void scatterColourMax(std::vector< VofBlock > &blocks, SField cLocal)=0
UNION the masters' inner colour into the caller's local colour patch: zero the inner region,...
virtual void syncTable(std::vector< VofBlock > &blocks)=0
Replicate the (possibly re-centred) boxes so every rank's table agrees again.
virtual void gatherColour(std::vector< VofBlock > &blocks, SField cLocal)=0
Fill every MASTER block's INNER colour from the caller's colour patch (the mirror of scatterColourMax...
virtual long gatherBytes() const
Bytes moved by the last gather / scatter on THIS rank (sent + received).
virtual void gatherFaceVel(std::vector< VofBlock > &blocks, int ghost)=0
Fill every MASTER block's faceU/V/W over its EXTENDED box, from the face velocity the ranks own.
One master block's share of a batched container launch.
One block's share of the batched sums (raw pointers: the table rides in the functor).
Per-block Lagrangian census, computed on the master (VOF_PLAN §10 W1 outputs, available at W0).
long debrisUnresolved
cumulative debris cells left for want of an attached interface
double debrisReturned
cumulative sum of debrisVolume over the removals
double area
Interface area of the marker, in cell-size units squared: the sum of the PLIC polygon areas over the ...
double moment[6]
Central second moments Σ C (x−x̄)(y−ȳ)/Σ C, order xx, yy, zz, xy, xz, yz (the deformation).
long fullAxis
Cumulative re-centrings whose box had to span a whole periodic axis (vof_overlap_design §12....
long cells
inner-box cell count (the master's work)
double debrisLost
cumulative volume the return could not place (C capped at 1)
double debrisVolume
their colour, summed in list order (cell volumes), this step
double volume
Σ C over the inner box, in cell volumes.
double residueReturned
Cumulative SIGNED volume of the sub-wispEps residue cleared after each advection and returned to the ...
double discarded
Cumulative colour dropped by re-centrings, i.e.
bool recentred
the box moved at the end of the last step
double velocity[3]
d(centroid)/dt of the last advect
long debrisCells
Marker DEBRIS (doc/vof_overlap_design.md §5.2 as amended by §11): an interfacial cell (vofIsInterface...
The job table of one launch, by value.
VofBlockJob job[kVofBlockBatch]
Half-open global cell box [lo, hi).
bool operator!=(const VofBox &o) const
bool contains(const VofBox &o) const
static VofBox intersect(const VofBox &a, const VofBox &b)
Intersection in global coordinates (may be empty).
bool operator==(const VofBox &o) const
static VofBox grown(const VofBox &b, int m)
One block's share of a batched tier-3 launch (VofCurvature::fallbackBatch): its fields,...
One master block's cascade (VofCurvature, worklist mode) for the batched launches: its planes,...
Per-call census of the cascade over this block's inner region (LOCAL; a distributed caller sums them ...
long pvReduced
tier 3 with the rank-deficient 3-parameter model
long hfMixed
tier 2a: HF in one of the other two directions
long hfFit
tier 2b: mixed HF – paraboloid through the consistent columns
long interfacial
cells carrying an interface (vofIsInterface, i.e.
long pv
tier 3: PV paraboloid fit, full 6-parameter model
long noEstimate
NO estimate produced (must be 0 on every gated case)
long clipped
Cells whose curvature the admissibility clip (kappaMax) bounded this call.
long hf
tier 1: HF in the preferred direction
Flags of the pass, uniform over the blocks.
A raw-pointer field accessor with the View<double*> call syntax, for kernels that walk SEVERAL blocks...
One contiguous run of GLOBAL indices covered by a block's index range on one axis,...
int loc0
block-local index (0-based within [a, b)) the run starts at
int g0
first GLOBAL index of the run
What measure() reads besides the colour: each job's previous centroid and whether it is valid (the ho...
double c[kVofBlockBatch][3]
unsigned char valid[kVofBlockBatch]
flow — VoF rung V4 (WO-P): balanced-force continuum surface force + the capillary Δt.
static constexpr double F
static double perm(const std::vector< double > &gsdf, const BlockDecomposer< 3 > &dec, int rank, bool amg, int levels, MPI_Comm comm)
static std::vector< double > colour(int m)