flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
curvature_field.hpp
Go to the documentation of this file.
1
27#ifndef PECLET_FLOW_VOF_CURVATURE_FIELD_HPP
28#define PECLET_FLOW_VOF_CURVATURE_FIELD_HPP
29
30#include <algorithm>
31#include <chrono>
32#include <Kokkos_Core.hpp>
33#include <stdexcept>
34#include <vector>
35
36#include "mac_stencils.hpp" // peclet::flow::SExec, SField, I3, L3
37#include "policy.hpp"
38#include "vof/advect_wy.hpp" // wyIsMixed, wyReconstructCell
39#include "vof/curvature.hpp"
40#include "vof/surface_tension.hpp" // csfKappaDefined
41
42namespace peclet::flow::vof {
43
47KOKKOS_INLINE_FUNCTION bool vofIsInterface(double c, double eps) {
48 return c > eps && c < 1.0 - eps;
49}
50
58template <class SF>
59KOKKOS_INLINE_FUNCTION void curvHeightCell(long i, SF c, SF mx, SF my, SF mz, SF al, SF kap, SF br,
60 long s0, long s1, long s2, double mtol, double ptW,
61 double ieps, bool forceFb, bool oneDir, bool useFit,
62 VofMetric g, double peps) {
63 const long st[3] = {s0, s1, s2};
64 kap(i) = 0.0;
65 if (!vofIsInterface(c(i), ieps)) {
66 br(i) = static_cast<double>(kCurvNone);
67 return;
68 }
69 if (forceFb) {
70 br(i) = -1.0;
71 return;
72 }
73 // Order the three column directions by the cell's own |n_d|. The normal is used as a
74 // CATEGORICAL selector only — it is never differenced (see curvature.hpp).
75 const double am[3] = {Kokkos::fabs(mx(i)), Kokkos::fabs(my(i)), Kokkos::fabs(mz(i))};
76 int ord[3] = {0, 1, 2};
77 for (int p = 1; p < 3; ++p) // insertion sort, descending
78 for (int q = p; q > 0 && am[ord[q]] > am[ord[q - 1]]; --q) {
79 const int t = ord[q];
80 ord[q] = ord[q - 1];
81 ord[q - 1] = t;
82 }
83 const int ntry = oneDir ? 1 : 3;
84 for (int t = 0; t < ntry; ++t) {
85 const int d = ord[t];
86 const int d1 = (d + 1) % 3, d2 = (d + 2) % 3;
87 const long sd = st[d], s1 = st[d1], s2 = st[d2];
88 double hh[9];
89 int orient0 = 0;
90 bool ok = true;
91 for (int q = 0; q < 3 && ok; ++q)
92 for (int p = 0; p < 3 && ok; ++p) {
93 const long base = i + (p - 1) * s1 + (q - 1) * s2;
94 double col[kHfColumn];
95 for (int k = 0; k < kHfColumn; ++k)
96 col[k] = c(base + (k - kHfColumn / 2) * sd);
97 double h;
98 int orient;
99 if (!hfColumnHeight(col, kHfColumn, h, orient, mtol, peps)) {
100 ok = false;
101 break;
102 }
103 if (orient0 == 0)
104 orient0 = orient;
105 else if (orient != orient0) {
106 ok = false; // the nine columns do not describe one single-valued surface
107 break;
108 }
109 hh[p + 3 * q] = h;
110 }
111 if (!ok)
112 continue;
113 // Phase 3 (V2.3): the heights are in cells along `d` and the patch spans cells along
114 // `d1`/`d2`, so the metric turns them into the physical graph. Unit metric == today.
115 kap(i) = hfPatchKappa(hh, g, d);
116 br(i) = static_cast<double>(t == 0 ? kCurvHf : kCurvHfMixed);
117 return;
118 }
119
120 // Tier 2b — the MIXED height function. No direction gives nine consistent columns; pool
121 // the interface positions of whichever of the 27 columns DO close and fit a paraboloid
122 // through them in the target's own frame. Every cell it reads is inside the same
123 // 3x3x7-per-direction footprint tier 1 already used, so it costs no extra halo.
124 if (useFit) {
125 const double m0 = mx(i), m1 = my(i), m2 = mz(i);
126 const double mi[3] = {m0, m1, m2};
127 double nn[3] = {0.0, 0.0, 0.0};
128 // PHYSICAL frame (Phase 3, V2.4); identity at the unit metric.
129 if (vofPhysNormalInv(mi, g, nn) > 0.0) {
130 double t1[3], t2[3];
131 curvFrame(nn, t1, t2);
132 double vtx[8][3], ctr[3], area;
133 const int nvt = plicPolygon(m0, m1, m2, al(i), vtx);
135 const double org[3] = {ctr[0] - 0.5, ctr[1] - 0.5, ctr[2] - 0.5};
136 PtFit pf;
137 ptFitInit(pf);
138 for (int d = 0; d < 3; ++d) {
139 const int d1 = (d + 1) % 3, d2 = (d + 2) % 3;
140 const long sd = st[d], s1 = st[d1], s2 = st[d2];
141 for (int q = -1; q <= 1; ++q)
142 for (int p = -1; p <= 1; ++p) {
143 const long base = i + p * s1 + q * s2;
144 double col[kHfColumn];
145 for (int k = 0; k < kHfColumn; ++k)
146 col[k] = c(base + (k - kHfColumn / 2) * sd);
147 double hv;
148 int orient;
149 if (!hfColumnHeight(col, kHfColumn, hv, orient, mtol, peps))
150 continue;
151 double X[3];
152 X[d1] = static_cast<double>(p);
153 X[d2] = static_cast<double>(q);
154 X[d] = orient * hv; // the interface POSITION along d (h is the signed height)
155 ptFitAdd(pf, X, org, t1, t2, nn, ptW, g);
156 }
157 }
158 double a[6];
159 bool red = false;
160 if (pf.npt >= 6 && ptFitSolve(pf, a, red) && !red) {
161 kap(i) = paraboloidKappa(a);
162 br(i) = static_cast<double>(kCurvHfFit);
163 return;
164 }
165 }
166 }
167 br(i) = -1.0; // to the fallback
168}
169
171template <class SF>
172KOKKOS_INLINE_FUNCTION void curvFallbackStore(long i, const PvFit& fit, SF kap, SF br) {
173 double a[6];
174 bool red = false;
175 if (!pvFitSolve(fit, a, red)) {
176 kap(i) = 0.0;
177 br(i) = static_cast<double>(kCurvNoEstimate);
178 return;
179 }
180 kap(i) = paraboloidKappa(a);
181 br(i) = static_cast<double>(red ? kCurvPvReduced : kCurvPv);
182}
183
184// ---- tier 3 at cost (design G, `doc/vof_curvature_cost_design.md` §5.2-§5.4) -----------------
185
191#if defined(__CUDA_ARCH__) || defined(__HIP_DEVICE_COMPILE__)
192#define PECLET_VOF_DEVICE_NOINLINE __noinline__
193#else
194#define PECLET_VOF_DEVICE_NOINLINE
195#endif
196
200 double mz, double alpha,
201 const VofMetric& g,
202 PvMoments& P) {
203 pvMomentsBuild(mx, my, mz, alpha, g, P);
204}
205//
206// Each interfacial cell's PLIC polygon data is built ONCE per curvature pass, in the planes pass,
207// into a per-cell cache indexed by a slot map, and every target reads it (the polygon used to be
208// rebuilt by each of the ~16 targets whose 5^3 stencil holds the cell: 63 % of tier 3, and then
209// re-integrated in each target's frame: another 30 %). The cache holds the polygon's 3-D area
210// moments (core `PvMoments`), and each target TRANSFORMS them into its frame (`pvTermMoments`):
211// the same integrals as the Green's-theorem terms, a RECORDED change at round-off (WO-3). The
212// tier-3 body is one template over an ENTRY ACCESSOR, so the compacted path (`VofPvCached`: the
213// slot map + the cache) and the dense compaction oracle (`VofPvOnTheFly`: the entry built from the
214// planes on the spot) share one body; both hand out bit-identical entries because both run
215// `pvMomentsBuild` on the same plane and metric.
216
220 const int* slot;
222 KOKKOS_INLINE_FUNCTION bool present(long j) const { return slot[j] >= 0; }
223 KOKKOS_INLINE_FUNCTION const PvMoments& entry(long j) const { return cache[slot[j]]; }
224};
225
228template <class SF>
230 SF c, mx, my, mz, al;
231 double ieps;
232 VofMetric g;
233 KOKKOS_INLINE_FUNCTION bool present(long j) const { return vofIsInterface(c(j), ieps); }
235 PvMoments P;
236 curvMomentsBuild(mx(j), my(j), mz(j), al(j), g, P);
237 return P;
238 }
239};
240
245template <class SF, class Acc>
246KOKKOS_INLINE_FUNCTION void curvFallbackCell(long i, const Acc& acc, SF kap, SF br, long sy,
247 long sz, int gr, double dW, double cmin, VofMetric g) {
248 if (br(i) >= 0.0)
249 return;
250
251 double nn[3], t1[3], t2[3], org[3];
252 if (!pvFrameMoments(acc.entry(i), g, nn, t1, t2, org)) {
253 kap(i) = 0.0;
254 br(i) = static_cast<double>(kCurvNoEstimate);
255 return;
256 }
257
258 PvFit fit;
259 pvFitInit(fit);
260 for (int oz = -gr; oz <= gr; ++oz)
261 for (int oy = -gr; oy <= gr; ++oy)
262 for (int ox = -gr; ox <= gr; ++ox) {
263 const long j = i + (long)ox + (long)oy * sy + (long)oz * sz;
264 if (!acc.present(j))
265 continue;
266 const double off[3] = {static_cast<double>(ox), static_cast<double>(oy),
267 static_cast<double>(oz)};
268 PvTerm t;
269 if (pvTermMoments(t, acc.entry(j), off, org, t1, t2, nn, dW, cmin, g))
270 pvFitAccumLower(fit, t);
271 }
272 curvFallbackStore(i, fit, kap, br);
273}
274
277using VofPvTerms = Kokkos::View<PvTerm*, SExec::scratch_memory_space, Kokkos::MemoryUnmanaged>;
278using VofPvInts = Kokkos::View<int*, SExec::scratch_memory_space, Kokkos::MemoryUnmanaged>;
279using VofPvDbls = Kokkos::View<double*, SExec::scratch_memory_space, Kokkos::MemoryUnmanaged>;
285 static std::size_t shmem(int nk) {
286 return VofPvInts::shmem_size(nk) + VofPvTerms::shmem_size(nk) +
287 VofPvDbls::shmem_size(kPvEntries);
288 }
289 template <class Member>
291 : kidx(tm.team_scratch(0), nk)
292 , term(tm.team_scratch(0), nk)
293 , ent(tm.team_scratch(0), kPvEntries) {}
294};
295
310template <class Member, class SF, class Acc>
312 const Member& tm, const VofPvTeamScratch& sc, long i, const Acc& acc, SF kap, SF br, long sy,
313 long sz, int gr, double dW, double cmin, VofMetric g) {
314 if (br(i) >= 0.0) // uniform across the team: nothing writes br(i) before the barriers below
315 return;
316 double nn[3], t1[3], t2[3], org[3];
317 const bool framed = pvFrameMoments(acc.entry(i), g, nn, t1, t2, org);
318 const int side = 2 * gr + 1, nk = side * side * side;
319 int n = 0;
320 if (framed) {
321 Kokkos::parallel_scan(
322 Kokkos::TeamThreadRange(tm, nk),
323 [&](const int k, int& upd, const bool final) {
324 const int ox = k % side - gr, oy = (k / side) % side - gr, oz = k / (side * side) - gr;
325 const long j = i + (long)ox + (long)oy * sy + (long)oz * sz;
326 if (acc.present(j)) {
327 if (final)
328 sc.kidx(upd) = k;
329 ++upd;
330 }
331 },
332 n);
333 tm.team_barrier();
334 Kokkos::parallel_for(Kokkos::TeamThreadRange(tm, n), [&](const int p) {
335 const int k = sc.kidx(p);
336 const int ox = k % side - gr, oy = (k / side) % side - gr, oz = k / (side * side) - gr;
337 const long j = i + (long)ox + (long)oy * sy + (long)oz * sz;
338 const double off[3] = {static_cast<double>(ox), static_cast<double>(oy),
339 static_cast<double>(oz)};
340 PvTerm t;
341 pvTermMoments(t, acc.entry(j), off, org, t1, t2, nn, dW, cmin, g);
342 sc.term(p) = t;
343 });
344 tm.team_barrier();
345 Kokkos::parallel_for(Kokkos::TeamThreadRange(tm, kPvEntries), [&](const int e) {
346 double a = 0.0;
347 for (int p = 0; p < n; ++p)
348 if (sc.term(p).ok)
349 pvFitAccumEntry(a, sc.term(p), e);
350 sc.ent(e) = a;
351 });
352 }
353 tm.team_barrier(); // the entries are in scratch, and every lane has read br(i)
354 Kokkos::single(Kokkos::PerTeam(tm), [&]() {
355 if (!framed) {
356 kap(i) = 0.0;
357 br(i) = static_cast<double>(kCurvNoEstimate);
358 return;
359 }
360 int npoly = 0;
361 for (int p = 0; p < n; ++p)
362 npoly += sc.term(p).ok ? 1 : 0;
363 double ent[kPvEntries];
364 for (int e = 0; e < kPvEntries; ++e)
365 ent[e] = sc.ent(e);
366 PvFit fit;
367 pvFitFromEntries(fit, ent, npoly);
368 curvFallbackStore(i, fit, kap, br);
369 });
370}
371
373#if defined(KOKKOS_ENABLE_HIP)
374inline constexpr int kVofWarp = 64;
375#else
376inline constexpr int kVofWarp = 32;
377#endif
378
383 double* p;
384 KOKKOS_INLINE_FUNCTION double& operator()(long i) const { return p[i]; }
385};
386
391 double *c, *mx, *my, *mz, *al, *kap, *br;
392 const int* slot;
394 const long* list;
395 long n, sy, sz;
396 int gr;
397 double dW, cmin, ieps;
398 VofMetric gm;
399};
400
403template <int N>
409
412 public:
413 using IField = Kokkos::View<int*, SMem>;
414 using MomCache = Kokkos::View<PvMoments*, SMem>;
417 struct Stats {
418 long interfacial = 0;
420 long hf = 0;
421 long hfMixed = 0;
422 long hfFit = 0;
423 long pv = 0;
424 long pvReduced = 0;
425 long noEstimate = 0;
429 long clipped = 0;
430 };
431
433 void init(int nx, int ny, int nz, int ghost) {
434 if (ghost < kHfColumn / 2)
435 throw std::invalid_argument(
436 "peclet::flow::vof::VofCurvature: ghost width must be >= 3 (the height-function column "
437 "reaches 3 cells and the 5^3 PV stencil's MYC normals reach 3 cells)");
438 if (nx < 1 || ny < 1 || nz < 1)
439 throw std::invalid_argument("peclet::flow::vof::VofCurvature: empty block");
440 n_ = I3{nx, ny, nz};
441 g_ = ghost;
442 e_ = I3{nx + 2 * ghost, ny + 2 * ghost, nz + 2 * ghost};
443 len_ = static_cast<long>(e_.x) * e_.y * e_.z;
444 mx_ = SField("vof::curv::mx", len_);
445 my_ = SField("vof::curv::my", len_);
446 mz_ = SField("vof::curv::mz", len_);
447 alpha_ = SField("vof::curv::alpha", len_);
448 kappa_ = SField("vof::curv::kappa", len_);
449 branch_ = SField("vof::curv::branch", len_);
450 // WO-V9: the compaction lists. The grown one covers every cell `reconstructPlanes` writes; the
451 // inner one every cell the cascade runs on. Sized at the full region (worst case: an entirely
452 // interfacial block) so a scan can never overflow them.
453 const long grown = (long)(n_.x + 2 * kPvHalf) * (n_.y + 2 * kPvHalf) * (n_.z + 2 * kPvHalf);
454 listG_ = LField("vof::curv::listG", grown);
455 listI_ = LField("vof::curv::listI", (long)n_.x * n_.y * n_.z);
456 // Design G §5.2: the slot map over the extended block (read only inside the grown region,
457 // where the planes pass writes it) and the per-cell moment cache, sized to listG_'s capacity
458 // on the first worklist planes pass (`ensureCache_`; a block-container cascade whose passes are
459 // batched never runs one and never allocates it).
460 slot_ = IField("vof::curv::slot", len_);
461 cache_ = MomCache();
462 }
463
464 bool ready() const { return kappa_.extent(0) != 0; }
465
466 // ---- WO-V9: compaction, and the per-pass timers --------------------------------------------
467 //
468 // The cascade is the most divergent kernel in the VoF pipeline: on a resolved droplet the
469 // interfacial cells are ~0.5 % of the block, so a dense `parallel_for` puts at most one or two
470 // active lanes in a warp and the other thirty run the guard and idle for the whole height
471 // function. Compacting the interfacial cells into a contiguous list first makes those warps
472 // full. It is a pure re-ordering — every cell reads the same neighbours and writes the same
473 // value — so the two paths are BIT-IDENTICAL and the ctest gates that.
474 //
475 // The timers follow `WyAdvector`'s rule exactly: fence at the boundaries only when armed.
476 struct Timing {
477 double planes = 0.0;
478 double height = 0.0;
479 double fallback = 0.0;
480 double census = 0.0;
481 double compact = 0.0;
482 double clip = 0.0;
483 long calls = 0;
484 };
485 bool useWorklist = true;
486 bool timingOn = false;
488 void resetTiming() { tm = Timing(); }
489 const Timing& timing() const { return tm; }
490 double tick_() const {
491 if (!timingOn)
492 return 0.0;
493 Kokkos::fence();
494 return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch())
495 .count();
496 }
497 void addT_(double& acc, double t0) {
498 if (!timingOn)
499 return;
500 Kokkos::fence();
501 acc +=
502 std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch()).count() -
503 t0;
504 }
505 I3 inner() const { return n_; }
506 I3 extent() const { return e_; }
507 int ghost() const { return g_; }
508 SField kappa() const { return kappa_; }
509 SField branch() const { return branch_; }
512 SField planeM(int d) const { return d == 0 ? mx_ : (d == 1 ? my_ : mz_); }
513 SField planeAlpha() const { return alpha_; }
516 IField slotMap() const { return slot_; }
517
518 // ---- tunables (all measured knobs, defaults are the literature values) ---------------------
523 VofMetric metric;
540 double pureEps = 0.0;
543 double monoTol = 1e-6;
547 double cosMin = 0.2;
569 double interfaceEps = 0.0;
572 bool debugForceFallback = false;
600 bool useMixedHeightFit = false;
617 double kappaMax = 0.0;
637 double kappaClipValue() const { return kappaMax < 0.0 ? 1.0 / metric.minH() : kappaMax; }
638
642 computeBegin(c);
643 const double t3 = tick_();
644 fallbackPass(c);
646 return computeEnd();
647 }
648
655 if (!ready())
656 throw std::runtime_error("peclet::flow::vof::VofCurvature::compute: init() not called");
657 const double t0 = tick_();
658 if (useWorklist)
659 compact(c);
660 addT_(tm.compact, t0);
661 const double t1 = tick_();
663 addT_(tm.planes, t1);
664 const double t2 = tick_();
665 heightPass(c);
666 addT_(tm.height, t2);
667 }
669 const double t5 = tick_();
670 const long nclip = clipPass();
671 addT_(tm.clip, t5);
672 const double t4 = tick_();
673 Stats s = census();
674 addT_(tm.census, t4);
675 s.clipped = nclip;
676 ++tm.calls;
677 return s;
678 }
683 j.c = c.data();
684 j.mx = mx_.data();
685 j.my = my_.data();
686 j.mz = mz_.data();
687 j.al = alpha_.data();
688 j.kap = kappa_.data();
689 j.br = branch_.data();
690 j.slot = slot_.data();
691 j.cache = cache_.data();
692 j.list = listI_.data();
693 j.n = nI_;
694 j.sy = e_.x;
695 j.sz = static_cast<long>(e_.x) * e_.y;
696 j.gr = kPvHalf;
697 j.dW = weightWidth * metric.maxH();
698 j.cmin = cosMin;
699 j.ieps = interfaceEps;
700 j.gm = metric;
701 return j;
702 }
704 static constexpr int kFallbackBatch = 16;
710 static void fallbackBatch(const std::vector<VofCurvFallbackJob>& jobs) {
712 for (std::size_t j0 = 0; j0 < jobs.size(); j0 += kFallbackBatch) {
713 Table T;
714 T.nj = static_cast<int>(std::min<std::size_t>(kFallbackBatch, jobs.size() - j0));
715 T.off[0] = 0;
716 for (int k = 0; k < T.nj; ++k) {
717 T.job[k] = jobs[j0 + k];
718 T.off[k + 1] = T.off[k] + T.job[k].n;
719 }
720 for (int k = T.nj; k < kFallbackBatch; ++k)
721 T.off[k + 1] = T.off[T.nj];
722 if (T.off[T.nj] == 0)
723 continue;
724 if constexpr (!Kokkos::SpaceAccessibility<Kokkos::HostSpace,
725 SField::memory_space>::accessible) {
726 // C3 (§5.11): one team (a warp) per target on a device; the host keeps one thread per
727 // target below.
728 using Policy = Kokkos::TeamPolicy<SExec>;
729 int gr = 0;
730 for (int k = 0; k < T.nj; ++k)
731 gr = std::max(gr, T.job[k].gr);
732 const int nk = (2 * gr + 1) * (2 * gr + 1) * (2 * gr + 1);
733 Policy pol(SExec(), T.off[T.nj], kVofWarp);
734 pol.set_scratch_size(0, Kokkos::PerTeam(VofPvTeamScratch::shmem(nk)));
735 Kokkos::parallel_for(
736 "vof::curv::pv_batch_team", pol, KOKKOS_LAMBDA(const typename Policy::member_type& tm) {
737 const long t = tm.league_rank();
738 int b = 0;
739 while (t >= T.off[b + 1])
740 ++b;
741 const VofCurvFallbackJob& J = T.job[b];
742 const int side = 2 * J.gr + 1;
743 const VofPvTeamScratch sc(tm, side * side * side);
744 curvFallbackTeam(tm, sc, J.list[t - T.off[b]], VofPvCached{J.slot, J.cache},
745 VofRawField{J.kap}, VofRawField{J.br}, J.sy, J.sz, J.gr, J.dW,
746 J.cmin, J.gm);
747 });
748 continue;
749 }
750 Kokkos::parallel_for(
751 "vof::curv::pv_batch", Kokkos::RangePolicy<SExec>(SExec(), 0, T.off[T.nj]),
752 KOKKOS_LAMBDA(long t) {
753 int b = 0;
754 while (t >= T.off[b + 1])
755 ++b;
756 const VofCurvFallbackJob& J = T.job[b];
757 curvFallbackCell(J.list[t - T.off[b]], VofPvCached{J.slot, J.cache}, VofRawField{J.kap},
758 VofRawField{J.br}, J.sy, J.sz, J.gr, J.dW, J.cmin, J.gm);
759 });
760 }
761 }
762
767 void compact(SField c) {
768 const I3 e = e_, n = n_;
769 const int g = g_, gr = kPvHalf;
770 const double ieps = interfaceEps;
771 {
772 const int rx = n.x + 2 * gr, ry = n.y + 2 * gr, rz = n.z + 2 * gr;
773 const long region = (long)rx * ry * rz;
774 LField list = listG_;
775 long cnt = 0;
776 Kokkos::parallel_scan(
777 "vof::curv::compactG", Kokkos::RangePolicy<SExec>(SExec(), 0, region),
778 KOKKOS_LAMBDA(const long r, long& upd, const bool final) {
779 const int ix = (int)(r % rx);
780 const int iy = (int)((r / rx) % ry);
781 const int iz = (int)(r / ((long)rx * ry));
782 const long i = L3(g - gr + ix, g - gr + iy, g - gr + iz, e);
783 if (vofIsInterface(c(i), ieps)) {
784 if (final)
785 list(upd) = i;
786 ++upd;
787 }
788 },
789 cnt);
790 Kokkos::fence();
791 nG_ = cnt;
792 }
793 {
794 const long region = (long)n.x * n.y * n.z;
795 const int nx = n.x, ny = n.y;
796 LField list = listI_;
797 long cnt = 0;
798 Kokkos::parallel_scan(
799 "vof::curv::compactI", Kokkos::RangePolicy<SExec>(SExec(), 0, region),
800 KOKKOS_LAMBDA(const long r, long& upd, const bool final) {
801 const int ix = (int)(r % nx);
802 const int iy = (int)((r / nx) % ny);
803 const int iz = (int)(r / ((long)nx * ny));
804 const long i = L3(g + ix, g + iy, g + iz, e);
805 if (vofIsInterface(c(i), ieps)) {
806 if (final)
807 list(upd) = i;
808 ++upd;
809 }
810 },
811 cnt);
812 Kokkos::fence();
813 nI_ = cnt;
814 }
815 }
816
817 // The four passes below are PUBLIC only because nvcc forbids an extended __host__ __device__
818 // lambda inside a private or protected member function ("The enclosing parent function ... cannot
819 // have private or protected access within its class"). They are implementation detail; call
820 // `compute()`. `WyAdvector` is public for the same reason.
821 //
825 const I3 e = e_, n = n_;
826 const int g = g_, gr = kPvHalf;
827 const long sy = e_.x, sz = static_cast<long>(e_.x) * e_.y;
828 SField mx = mx_, my = my_, mz = mz_, al = alpha_;
829 const double ieps = interfaceEps;
830 const VofMetric gm = metric; // `g` is the ghost width in this scope
831 if (useWorklist) {
832 // The same two things the dense kernel does, as two coherent kernels: a branchless zeroing
833 // sweep (a pure store, bandwidth-bound) and the MYC reconstruction over the compacted list.
834 // Every cell ends with exactly the value the dense kernel would have written. Design G §5.2:
835 // the zero sweep also clears the slot map, and the list kernel builds each cell's polygon
836 // cache entry from the plane it just wrote and records its slot.
837 ensureCache_();
838 IField slot = slot_;
839 MomCache cache = cache_;
840 Kokkos::parallel_for(
841 "vof::curv::planes_zero",
842 MDRange3<SExec>(SExec(), {g - gr, g - gr, g - gr},
843 {g + n.x + gr, g + n.y + gr, g + n.z + gr}),
844 KOKKOS_LAMBDA(int x, int y, int z) {
845 const long i = L3(x, y, z, e);
846 mx(i) = 0.0;
847 my(i) = 0.0;
848 mz(i) = 0.0;
849 al(i) = 0.0;
850 slot(i) = -1;
851 });
852 LField list = listG_;
853 Kokkos::parallel_for(
854 "vof::curv::planes_list", Kokkos::RangePolicy<SExec>(SExec(), 0, nG_),
855 KOKKOS_LAMBDA(long t) {
856 const long i = list(t);
857 wyReconstructCell(c, i, sy, sz, mx, my, mz, al);
858 curvMomentsBuild(mx(i), my(i), mz(i), al(i), gm, cache(t));
859 slot(i) = static_cast<int>(t);
860 });
861 return;
862 }
863 Kokkos::parallel_for(
864 "vof::curv::planes",
865 MDRange3<SExec>(SExec(), {g - gr, g - gr, g - gr},
866 {g + n.x + gr, g + n.y + gr, g + n.z + gr}),
867 KOKKOS_LAMBDA(int x, int y, int z) {
868 const long i = L3(x, y, z, e);
869 if (!vofIsInterface(c(i), ieps)) {
870 mx(i) = 0.0;
871 my(i) = 0.0;
872 mz(i) = 0.0;
873 al(i) = 0.0;
874 return;
875 }
876 wyReconstructCell(c, i, sy, sz, mx, my, mz, al);
877 });
878 }
879
883 const I3 e = e_, n = n_;
884 const int g = g_;
885 const long st[3] = {1, e_.x, static_cast<long>(e_.x) * e_.y};
886 SField mx = mx_, my = my_, mz = mz_, al = alpha_, kap = kappa_, br = branch_;
887 const double mtol = monoTol, ptW = ptWeightWidth * metric.maxH(), ieps = interfaceEps;
888 const double peps = pureEps;
889 const VofMetric gm = metric; // `g` is the ghost width in this scope
890 const bool forceFb = debugForceFallback, oneDir = debugSingleDirection,
891 useFit = useMixedHeightFit;
892 const long s0 = st[0], s1 = st[1], s2 = st[2];
893 if (useWorklist) {
894 // Reset every inner cell (branchless: a pure pair of stores), then run the cascade over the
895 // compacted interfacial list. A non-interfacial cell ends at kappa = 0, branch = kCurvNone
896 // and an interfacial one at whatever the shared body writes -- the dense kernel's outcome,
897 // cell for cell.
898 Kokkos::parallel_for(
899 "vof::curv::hf_reset", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
900 KOKKOS_LAMBDA(int x, int y, int z) {
901 const long i = L3(x, y, z, e);
902 kap(i) = 0.0;
903 br(i) = static_cast<double>(kCurvNone);
904 });
905 LField list = listI_;
906 Kokkos::parallel_for(
907 "vof::curv::hf_list", Kokkos::RangePolicy<SExec>(SExec(), 0, nI_), KOKKOS_LAMBDA(long t) {
908 curvHeightCell(list(t), c, mx, my, mz, al, kap, br, s0, s1, s2, mtol, ptW, ieps,
909 forceFb, oneDir, useFit, gm, peps);
910 });
911 return;
912 }
913 Kokkos::parallel_for(
914 "vof::curv::hf", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
915 KOKKOS_LAMBDA(int x, int y, int z) {
916 curvHeightCell(L3(x, y, z, e), c, mx, my, mz, al, kap, br, s0, s1, s2, mtol, ptW, ieps,
917 forceFb, oneDir, useFit, gm, peps);
918 });
919 }
920
925 const I3 e = e_, n = n_;
926 const int g = g_, gr = kPvHalf;
927 SField mx = mx_, my = my_, mz = mz_, al = alpha_, kap = kappa_, br = branch_;
928 const double dW = weightWidth * metric.maxH(), cmin = cosMin, ieps = interfaceEps;
929 const VofMetric gm = metric; // `g` is the ghost width in this scope
930 const long sy = e_.x, sz = static_cast<long>(e_.x) * e_.y;
931 if (useWorklist) {
932 // Tier 3 is a SUBSET of the interfacial cells (those tier 1/2 could not serve), so the
933 // interfacial list already removes the whole empty domain; the branch guard inside the body
934 // removes the rest.
935 LField list = listI_;
936 const VofPvCached acc{slot_.data(), cache_.data()};
937 Kokkos::parallel_for(
938 "vof::curv::pv_list", Kokkos::RangePolicy<SExec>(SExec(), 0, nI_), KOKKOS_LAMBDA(long t) {
939 curvFallbackCell(list(t), acc, kap, br, sy, sz, gr, dW, cmin, gm);
940 });
941 return;
942 }
943 const VofPvOnTheFly<SField> acc{c, mx, my, mz, al, ieps, gm};
944 Kokkos::parallel_for(
945 "vof::curv::pv", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
946 KOKKOS_LAMBDA(int x, int y, int z) {
947 curvFallbackCell(L3(x, y, z, e), acc, kap, br, sy, sz, gr, dW, cmin, gm);
948 });
949 }
950
954 long clipPass() {
955 const double km = kappaClipValue();
956 if (!(km > 0.0))
957 return 0;
958 const I3 e = e_, n = n_;
959 const int g = g_;
960 SField kap = kappa_, br = branch_;
961 long cnt = 0;
962 if (useWorklist) {
963 LField list = listI_;
964 Kokkos::parallel_reduce(
965 "vof::curv::clip_list", Kokkos::RangePolicy<SExec>(SExec(), 0, nI_),
966 KOKKOS_LAMBDA(long t, long& acc) {
967 const long i = list(t);
968 // `!(|k| <= km)` rather than `|k| > km`: a NaN curvature is clipped (to +km, the sign
969 // of a NaN is meaningless) and counted, never silently kept.
970 if (csfKappaDefined(br(i)) && !(Kokkos::fabs(kap(i)) <= km)) {
971 kap(i) = (kap(i) == kap(i)) ? Kokkos::copysign(km, kap(i)) : km;
972 ++acc;
973 }
974 },
975 cnt);
976 } else {
977 Kokkos::parallel_reduce(
978 "vof::curv::clip", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
979 KOKKOS_LAMBDA(int x, int y, int z, long& acc) {
980 const long i = L3(x, y, z, e);
981 // `!(|k| <= km)` rather than `|k| > km`: a NaN curvature is clipped (to +km, the sign
982 // of a NaN is meaningless) and counted, never silently kept.
983 if (csfKappaDefined(br(i)) && !(Kokkos::fabs(kap(i)) <= km)) {
984 kap(i) = (kap(i) == kap(i)) ? Kokkos::copysign(km, kap(i)) : km;
985 ++acc;
986 }
987 },
988 cnt);
989 }
990 Kokkos::fence();
991 return cnt;
992 }
993
994 Stats census() const {
995 const I3 e = e_, n = n_;
996 const int g = g_;
997 SField br = branch_;
998 Stats s;
999 Kokkos::parallel_reduce(
1000 "vof::curv::census", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
1001 KOKKOS_LAMBDA(int x, int y, int z, long& ni, long& n1, long& n2, long& n2b, long& n3,
1002 long& n4, long& n5) {
1003 const int b = static_cast<int>(br(L3(x, y, z, e)));
1004 if (b == kCurvNone)
1005 return;
1006 ++ni;
1007 if (b == kCurvHf)
1008 ++n1;
1009 else if (b == kCurvHfMixed)
1010 ++n2;
1011 else if (b == kCurvHfFit)
1012 ++n2b;
1013 else if (b == kCurvPv)
1014 ++n3;
1015 else if (b == kCurvPvReduced)
1016 ++n4;
1017 else
1018 ++n5;
1019 },
1020 s.interfacial, s.hf, s.hfMixed, s.hfFit, s.pv, s.pvReduced, s.noEstimate);
1021 Kokkos::fence();
1022 return s;
1023 }
1024
1025 private:
1026 I3 n_{0, 0, 0}, e_{0, 0, 0};
1027 int g_ = 0;
1028 long len_ = 0;
1029 SField mx_, my_, mz_, alpha_, kappa_, branch_;
1030 LField listG_, listI_; // WO-V9: the compacted interfacial-cell lists
1031 long nG_ = 0, nI_ = 0; // ... and their lengths from the last compact()
1032 IField slot_; // design G §5.2: grown-list position of each interfacial cell, else -1
1033 MomCache cache_; // ... and each one's polygon, in grown-list order
1034
1037 void ensureCache_() {
1038 const long cap = static_cast<long>(listG_.extent(0));
1039 if (cap >= (1L << 31))
1040 throw std::runtime_error(
1041 "peclet::flow::vof::VofCurvature: grown list capacity >= 2^31 (the slot map is int)");
1042 if (static_cast<long>(cache_.extent(0)) < cap)
1043 cache_ = MomCache(
1044 Kokkos::view_alloc(std::string("vof::curv::cache"), Kokkos::WithoutInitializing), cap);
1045 }
1046};
1047
1048} // namespace peclet::flow::vof
1049
1050#endif // PECLET_FLOW_VOF_CURVATURE_FIELD_HPP
flow — VoF rung V1: Weymouth & Yue (2010) directionally-split, exactly volume-conserving geometric ad...
Curvature of the colour field on an extended (inner + ghost) block.
double interfaceEps
Wisp threshold on the interfacial predicate.
double monoTol
Tolerance of the column monotonicity test (hfColumnHeight).
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...
bool debugSingleDirection
DIAGNOSTIC ONLY — disable tier 2 (the non-preferred column directions), so the branch census separate...
void fallbackPass(SField c)
Tier 3, over the cells the height pass could not serve.
double pureEps
Tolerance of the PURITY test that ends a height-function column (hfIsFull/hfIsEmpty),...
void copyTunablesFrom(const VofCurvature &src)
Copy EVERY tunable above (the metric, the estimator knobs, the predicates, the clip,...
Kokkos::View< PvMoments *, SMem > MomCache
double kappaMax
Curvature admissibility clip (doc/vof_overlap_design.md §5.1, §11): after the cascade,...
long clipPass()
The admissibility clip (see kappaMax), over the same cells the cascade ran on: the compacted interfac...
static constexpr int kFallbackBatch
Jobs per launch of fallbackBatch (the table rides in the kernel's functor by value).
Kokkos::View< int *, SMem > IField
VofCurvFallbackJob fallbackJob(SField c) const
This cascade's tier-3 share for fallbackBatch (worklist mode only: the job walks the compacted interf...
bool useMixedHeightFit
Tier 2b, and it ships OFF — a measured decision, see the WO-O findings entry.
VofMetric metric
The anisotropic cell metric (Phase 3).
bool debugForceFallback
DIAGNOSTIC ONLY — disable tiers 1 and 2 so every interfacial cell goes to the PV fit.
SField planeM(int d) const
The PLIC planes the cascade reconstructed (planeM(d), planeAlpha()): for the block container's batche...
IField slotMap() const
The slot map of the moment cache (design G §5.2), for the batched cascade, which writes it with posit...
double weightWidth
Wendland support width d of the PV fit, in cell units (Han et al. §5: 2.5 with S = 5).
void computeBegin(SField c)
compute() in three parts, for a caller that runs MANY cascades (the block container): computeBegin (c...
void reconstructPlanes(SField c)
PLIC planes over the inner region grown by kPvHalf — every cell the 5^3 fit can read.
Stats compute(SField c)
Compute the curvature over the inner region from a colour field on the SAME extended block.
double kappaClipValue() const
The bound the clip applies this call (0 = off).
void heightPass(SField c)
Tiers 1 and 2.
void addT_(double &acc, double t0)
void init(int nx, int ny, int nz, int ghost)
double ptWeightWidth
Wendland support width of the tier-2b height-position fit, in cell units.
double cosMin
Minimum n_p . n for a stencil polygon to enter the PV fit (pvFitAdd).
void compact(SField c)
The two compaction scans: listG_ = every interfacial cell of the GROWN region (what reconstructPlanes...
flow — thin shim: the container-free VoF height-function / paraboloid curvature kernels (rung V3) now...
#define PECLET_VOF_DEVICE_NOINLINE
ONE compiled device body per tier-3 piece (C1 ruling, design G WO-3).
flow — portable (Kokkos) MAC stencil operators: Red-Black Gauss-Seidel smoothers + divergence.
Kokkos::View< double *, SExec::scratch_memory_space, Kokkos::MemoryUnmanaged > VofPvDbls
Kokkos::View< long *, SMem > LField
Definition advect_wy.hpp:90
bool csfKappaDefined(double branch)
Is the curvature of a cell carrying branch code branch usable?
constexpr int kVofWarp
The team size of a device tier-3 launch: one warp / wavefront.
bool vofIsInterface(double c, double eps)
wyIsMixed with a wisp threshold: the cell carries an interface only while eps < C < 1 - eps.
void wyReconstructCell(const SF &c, long i, long sy, long sz, SF mx, SF my, SF mz, SF alpha)
PLIC reconstruction of one cell: MYC normal from the 3^3 colour stencil + the analytic plane offset.
Kokkos::View< int *, SExec::scratch_memory_space, Kokkos::MemoryUnmanaged > VofPvInts
void curvFallbackCell(long i, const Acc &acc, SF kap, SF br, long sy, long sz, int gr, double dW, double cmin, VofMetric g)
Tier 3 for one target, one thread (host): the frame from the target's entry (pvFrameMoments: the phys...
Kokkos::View< PvTerm *, SExec::scratch_memory_space, Kokkos::MemoryUnmanaged > VofPvTerms
Team scratch of the device tier-3 body: the compacted stencil's canonical offset indices,...
void curvHeightCell(long i, SF c, SF mx, SF my, SF mz, SF al, SF kap, SF br, long s0, long s1, long s2, double mtol, double ptW, double ieps, bool forceFb, bool oneDir, bool useFit, VofMetric g, double peps)
WO-V9 — the per-cell bodies of the two cascade tiers, lifted VERBATIM out of the kernels they used to...
PECLET_VOF_DEVICE_NOINLINE void curvMomentsBuild(double mx, double my, double mz, double alpha, const VofMetric &g, PvMoments &P)
pvMomentsBuild, the one call every planes pass and the dense accessor make (out of line on a device,...
PECLET_VOF_DEVICE_NOINLINE void curvFallbackTeam(const Member &tm, const VofPvTeamScratch &sc, long i, const Acc &acc, SF kap, SF br, long sy, long sz, int gr, double dW, double cmin, VofMetric g)
curvFallbackCell for one target on a whole team (design G §5.4; the team shape of C3,...
void curvFallbackStore(long i, const PvFit &fit, SF kap, SF br)
Tier 3's last step: solve the accumulated PV system and write the target's kappa and branch.
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.
One block's share of a batched tier-3 launch (VofCurvature::fallbackBatch): its fields,...
const PvMoments * cache
... and its per-cell moment cache
const int * slot
the cascade's slot map (design G §5.2)
The job table of one batched launch, passed to the kernel BY VALUE (namespace scope: nvcc's extended ...
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
double clip
the curvature admissibility clip
double fallback
tier 3, the PLIC-volumetric paraboloid
double compact
the two parallel_scans (0 when the compaction is off)
double planes
the PLIC pass over the grown region
double height
tiers 1/2, the height-function cascade
double census
the branch census reduction
The cache-backed accessor: slot[j] >= 0 iff cell j of the grown region is interfacial,...
const PvMoments & entry(long j) const
The on-the-fly accessor of the dense mode (useWorklist = false, no list and no slot): the interfacial...
static std::size_t shmem(int nk)
Per-team level-0 scratch bytes for a stencil of nk offsets.
VofPvTeamScratch(const Member &tm, int nk)
A raw-pointer field accessor with the View<double*> call syntax, for kernels that walk SEVERAL blocks...
double & operator()(long i) const
flow — VoF rung V4 (WO-P): balanced-force continuum surface force + the capillary Δt.
static constexpr int N