flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
advect_wy.hpp
Go to the documentation of this file.
1
72#ifndef PECLET_FLOW_VOF_ADVECT_WY_HPP
73#define PECLET_FLOW_VOF_ADVECT_WY_HPP
74
75#include <chrono>
76#include <cstdio>
77#include <functional>
78#include <Kokkos_Core.hpp>
79#include <stdexcept>
80
81#include "mac_stencils.hpp" // peclet::flow::SExec, SField, SMem, I3, L3
82#include "policy.hpp"
83#include "vof/cutcell.hpp"
84#include "vof/plic.hpp"
85#include "vof/wetting.hpp"
86
88
89using UCField = Kokkos::View<unsigned char*, SMem>;
90using LField = Kokkos::View<long*, SMem>;
91
96 return c > 0.0 && c < 1.0;
97}
98
111KOKKOS_INLINE_FUNCTION bool wyIsMixed(double c, double eps) {
112 return c > eps && c < 1.0 - eps;
113}
114
123KOKKOS_INLINE_FUNCTION bool wyColourJump(double a, double b, double eps) {
124 const double d = a - b;
125 return (d > eps) || (d < -eps);
126}
127
130inline constexpr int kWySweepPerm[6][3] = {{0, 1, 2}, {1, 2, 0}, {2, 0, 1},
131 {0, 2, 1}, {2, 1, 0}, {1, 0, 2}};
132
138template <class SF>
139KOKKOS_INLINE_FUNCTION void wyReconstructCell(const SF& c, long i, long sy, long sz, SF mx, SF my,
140 SF mz, SF alpha) {
141 double st[27];
142 for (int kk = -1; kk <= 1; ++kk)
143 for (int jj = -1; jj <= 1; ++jj)
144 for (int ii = -1; ii <= 1; ++ii)
145 st[plicSt(ii + 1, jj + 1, kk + 1)] = c(i + ii + jj * sy + kk * sz);
146 double m[3];
147 mycNormal(st, m);
148 mx(i) = m[0];
149 my(i) = m[1];
150 mz(i) = m[2];
151 alpha(i) = plicAlpha(m[0], m[1], m[2], c(i));
152}
153
156template <class SF>
157KOKKOS_INLINE_FUNCTION double wyFaceFlux(double a, long p, long sd, int dir, const SF& c,
158 const SF& mx, const SF& my, const SF& mz, const SF& alpha,
159 double eps = 0.0) {
160 if (a > 0.0) { // donor is p; the outflow slab is the |a|-thick layer at its + face
161 const double cd = c(p);
162 return wyIsMixed(cd, eps) ? plicSlabVolume(mx(p), my(p), mz(p), alpha(p), dir, 1.0 - a, 1.0)
163 : cd * a;
164 }
165 if (a < 0.0) { // donor is p + sd; the outflow slab is the |a|-thick layer at its - face
166 const long q = p + sd;
167 const double cd = c(q), aa = -a;
168 return -(wyIsMixed(cd, eps) ? faceFluxVolume(mx(q), my(q), mz(q), alpha(q), dir, aa) : cd * aa);
169 }
170 return 0.0;
171}
172
187template <class SF, class MF>
188KOKKOS_INLINE_FUNCTION double wyFaceFluxBc(double a, long p, long sd, int dir, const SF& c,
189 const SF& mx, const SF& my, const SF& mz,
190 const SF& alpha, const MF& outside, double eps = 0.0) {
191 if (a > 0.0) { // donor is p
192 const double cd = c(p);
193 return (wyIsMixed(cd, eps) && !outside(p))
194 ? plicSlabVolume(mx(p), my(p), mz(p), alpha(p), dir, 1.0 - a, 1.0)
195 : cd * a;
196 }
197 if (a < 0.0) { // donor is p + sd
198 const long q = p + sd;
199 const double cd = c(q), aa = -a;
200 return -((wyIsMixed(cd, eps) && !outside(q))
201 ? faceFluxVolume(mx(q), my(q), mz(q), alpha(q), dir, aa)
202 : cd * aa);
203 }
204 return 0.0;
205}
206
209 public:
212 struct Diagnostics {
213 double sumC = 0.0;
214 double minC = 0.0;
215 double maxC = 0.0;
216 long mixed = 0;
217 long wisps = 0;
218 // --- rung V5a (WO-Q), all zero unless cut-cell geometry is attached -----------------------
219 double volume = 0.0;
221 double rawVolume = 0.0;
226 double solidFillSum = 0.0;
227 double solidSumC = 0.0;
228 double minCFluid = 0.0;
229 double maxCFluid = 0.0;
230 double clippedVolume = 0.0;
231 double clippedSigned = 0.0;
232 long cutCells = 0;
233 long solidCells = 0;
234 long clampedFaces = 0;
235 };
236
242 void init(int nx, int ny, int nz, double h, int ghost = 3) {
243 if (ghost < 2)
244 throw std::invalid_argument("peclet::flow::vof::WyAdvector: ghost width must be >= 2");
245 if (nx < 1 || ny < 1 || nz < 1)
246 throw std::invalid_argument("peclet::flow::vof::WyAdvector: empty block");
247 n_ = I3{nx, ny, nz};
248 g_ = ghost;
249 h_ = h;
250 e_ = I3{nx + 2 * ghost, ny + 2 * ghost, nz + 2 * ghost};
251 len_ = static_cast<long>(e_.x) * e_.y * e_.z;
252 c_ = SField("vof::C", len_);
253 mx_ = SField("vof::mx", len_);
254 my_ = SField("vof::my", len_);
255 mz_ = SField("vof::mz", len_);
256 alpha_ = SField("vof::alpha", len_);
257 flux_ = SField("vof::flux", len_);
258 uf_ = SField("vof::uf", len_);
259 vf_ = SField("vof::vf", len_);
260 wf_ = SField("vof::wf", len_);
261 cc_ = UCField("vof::cc", len_);
262 listCap_ = static_cast<long>(n_.x + 2) * (n_.y + 2) * (n_.z + 2);
263 list_ = LField("vof::worklist", listCap_);
264 }
265
266 // ---- geometry / storage ------------------------------------------------------------------
267 I3 inner() const { return n_; }
268 I3 extent() const { return e_; }
269 int ghost() const { return g_; }
270 double h() const { return h_; }
271 long size() const { return len_; }
273 long index(int x, int y, int z) const { return L3(x + g_, y + g_, z + g_, e_); }
274
276 SField colour() const { return c_; }
280 SField faceU() const { return uf_; }
281 SField faceV() const { return vf_; }
282 SField faceW() const { return wf_; }
283 SField faceVel(int d) const { return d == 0 ? uf_ : (d == 1 ? vf_ : wf_); }
284
285 // ---- rung V5a (WO-Q): optional cut-cell geometry ------------------------------------------
286 //
287 // OFF by default and EVERY kernel branches on `hasGeometry()` OUTSIDE its lambda, so with no
288 // geometry attached the arithmetic executed is literally the V1 expression (not a multiplication
289 // by 1.0) and the whole V1/V2a/V3/V4 battery stays byte-identical. See `vof/cutcell.hpp` for the
290 // three rules the geometry branch adds and why the conserved functional is `sum eps_eff C`.
291 //
292 // USAGE (the solver does exactly this in `buildVofBlock`):
293 // a.enableGeometry();
294 // <fill faceOpenness(d) with the +d face openness and epsFraction() with buildCellFraction,
295 // on the INNER region, then run the block's own ghost policy on each of them>
296 // a.classifyGeometry(); // writes kindDouble() over every index except the low plane
297 // <run the block's ghost policy on kindDouble()> // makes the low plane / ghosts the owner's
298 // a.finalizeGeometry(); // kindDouble() -> the unsigned-char classification
299 bool hasGeometry() const { return hasGeom_; }
302 if (len_ <= 0)
303 throw std::runtime_error("peclet::flow::vof::WyAdvector: enableGeometry before init()");
304 if (!hasGeom_) {
305 for (int d = 0; d < 3; ++d)
306 of_[d] = SField("vof::openness", len_);
307 eps_ = SField("vof::eps", len_);
308 kindD_ = SField("vof::kindD", len_);
309 kind_ = UCField("vof::kind", len_);
310 fill_ = UCField("vof::fillState", len_);
311 mark_ = UCField("vof::fillMark", len_);
312 hasGeom_ = true;
313 }
314 }
316 void disableGeometry() { hasGeom_ = false; }
319 SField faceOpenness(int d) const { return of_[d]; }
321 SField epsFraction() const { return eps_; }
323 SField kindDouble() const { return kindD_; }
325 UCField cellKind() const { return kind_; }
329 bool cutFluxClamp = true;
331 long clampedFaces() const { return clampedFaces_; }
333 double clippedVolume() const { return clippedVolume_; }
334 double clippedSigned() const { return clippedSigned_; }
335
341 if (!hasGeom_)
342 return;
343 const I3 e = e_;
344 const long sx = 1, sy = e_.x, sz = static_cast<long>(e_.x) * e_.y;
345 SField ox = of_[0], oy = of_[1], oz = of_[2], ep = eps_, kd = kindD_;
346 Kokkos::parallel_for(
347 "vof::wy::classify", MDRange3<SExec>(SExec(), {0, 0, 0}, {e.x, e.y, e.z}),
348 KOKKOS_LAMBDA(int x, int y, int z) {
349 const long i = L3(x, y, z, e);
350 if (x == 0 || y == 0 || z == 0) {
351 kd(i) = 0.0; // the exchange owns this plane
352 return;
353 }
354 const double oLo[3] = {ox(i - sx), oy(i - sy), oz(i - sz)};
355 const double oHi[3] = {ox(i), oy(i), oz(i)};
356 kd(i) = vofIsSolidCell(ep(i), oLo, oHi) ? 1.0 : 0.0;
357 });
358 Kokkos::fence();
359 }
362 if (!hasGeom_)
363 return;
364 SField kd = kindD_;
365 UCField kk = kind_;
366 Kokkos::parallel_for(
367 "vof::wy::finalize_geom", Kokkos::RangePolicy<SExec>(SExec(), 0, len_),
368 KOKKOS_LAMBDA(long i) { kk(i) = kd(i) > 0.5 ? kVofSolid : kVofFluid; });
369 Kokkos::fence();
370 }
371
372 // ---- rung V5b (WO-S): the theta-consistent band fill ---------------------------------------
373 //
374 // OFF by default: `hasWetting()` false runs WO-Q's neutral pass 1 verbatim, so the whole V5a
375 // battery stays byte-identical. When on, PASS 1 ONLY is replaced (passes 2-3 and the shrinking
376 // depth budget are untouched) by the theta-plane of `vof/wetting.hpp`.
377 //
378 // USAGE (the solver does exactly this):
379 // a.enableWetting();
380 // <fill wallSdf() and contactAngle() on the INNER region, then run the block's ghost policy>
381 // ... per fill: a.buildWettingNormals(); <exchange wettingNormal(0..2)>; a.solidBandFill();
382 //
383 // Every read the theta pass makes is owner-correct out to ghost depth 3 (the exchanged sdf, the
384 // exchanged colour, the exchanged fluid-only normals), which is what keeps the INNER result
385 // decomposition-independent — the same argument WO-Q finding 5 makes for the classification.
386 bool hasWetting() const { return hasWet_; }
388 if (!hasGeom_)
389 throw std::runtime_error("peclet::flow::vof::WyAdvector: enableWetting needs geometry");
390 if (!hasWet_) {
391 sdfB_ = SField("vof::wallSdf", len_);
392 thetaB_ = SField("vof::contactAngle", len_);
393 for (int d = 0; d < 3; ++d)
394 mfl_[d] = SField("vof::fluidNormal", len_);
395 appB_ = SField("vof::apparentAngle", len_);
396 wetB_ = UCField("vof::wetBranch", len_);
397 hasWet_ = true;
398 }
399 }
400 void disableWetting() { hasWet_ = false; }
403 SField wallSdf() const { return sdfB_; }
405 SField contactAngle() const { return thetaB_; }
407 SField wettingNormal(int d) const { return mfl_[d]; }
409
416 VofMetric metric;
420 double wettingTangentEps = 1e-6;
423 double wettingPureEps = 1e-8;
424
428 if (!hasWet_)
429 return;
430 const I3 e = e_, n = n_;
431 const int g = g_;
432 SField c = c_, mx = mfl_[0], my = mfl_[1], mz = mfl_[2];
433 UCField kk = kind_;
434 Kokkos::parallel_for(
435 "vof::wy::wetting_normals",
436 MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
437 KOKKOS_LAMBDA(int x, int y, int z) {
438 const long i = L3(x, y, z, e);
439 mx(i) = 0.0;
440 my(i) = 0.0;
441 mz(i) = 0.0;
442 if (kk(i) != kVofFluid)
443 return;
444 double c27[27];
445 unsigned char fl[27];
446 for (int kz = -1; kz <= 1; ++kz)
447 for (int ky = -1; ky <= 1; ++ky)
448 for (int kx = -1; kx <= 1; ++kx) {
449 const int q = plicSt(kx + 1, ky + 1, kz + 1);
450 const long j = L3(x + kx, y + ky, z + kz, e);
451 c27[q] = c(j);
452 fl[q] = kk(j) == kVofFluid ? 1u : 0u;
453 }
454 double m[3];
455 if (!youngsNormalFluidOnly(c27, fl, m))
456 return;
457 mx(i) = m[0];
458 my(i) = m[1];
459 mz(i) = m[2];
460 });
461 Kokkos::fence();
462 }
463
470 void wettingCensus(long counts[kVofWetCount], double& meanAppDeg, long& nApp) const {
472 }
474 int ghost) const {
475 for (int b = 0; b < kVofWetCount; ++b)
476 counts[b] = 0;
477 meanAppDeg = 0.0;
478 nApp = 0;
479 if (!hasWet_)
480 return;
481 const I3 e = e_, n = n_;
482 const int g = g_;
483 UCField wb = wetB_, kk = kind_;
484 SField ap = appB_;
485 using MD = MDRange3<SExec>;
486 const int gw = ghost < 0 ? 0 : (ghost > g ? g : ghost);
487 MD pol(SExec(), {g - gw, g - gw, g - gw}, {g + n.x + gw, g + n.y + gw, g + n.z + gw});
488 for (int b = 0; b < kVofWetCount; ++b) {
489 long acc = 0;
490 const unsigned char bb = static_cast<unsigned char>(b);
491 Kokkos::parallel_reduce(
492 "vof::wy::wet_census", pol,
493 KOKKOS_LAMBDA(int x, int y, int z, long& a) {
494 const long i = L3(x, y, z, e);
495 if (kk(i) == kVofSolid && wb(i) == bb) // SOLID cells only: the band is the write set
496 ++a;
497 },
498 acc);
499 counts[b] = acc;
500 }
501 double sum = 0.0;
502 long cnt = 0;
503 Kokkos::parallel_reduce(
504 "vof::wy::wet_apparent", pol,
505 KOKKOS_LAMBDA(int x, int y, int z, double& a, long& c2) {
506 const long i = L3(x, y, z, e);
507 if (wb(i) == static_cast<unsigned char>(kVofWetTheta)) {
508 a += ap(i);
509 ++c2;
510 }
511 },
512 sum, cnt);
513 Kokkos::fence();
514 nApp = cnt;
515 meanAppDeg = cnt ? sum / static_cast<double>(cnt) * (180.0 / 3.14159265358979323846) : 0.0;
516 }
517
522 if (!hasGeom_)
523 return;
524 {
525 UCField fs = fill_, mk = mark_, kk = kind_;
526 Kokkos::parallel_for(
527 "vof::wy::fill_reset", Kokkos::RangePolicy<SExec>(SExec(), 0, len_),
528 KOKKOS_LAMBDA(long i) {
529 fs(i) = kk(i) == kVofSolid ? kVofFillNone : kVofFillFluid;
530 mk(i) = 0u;
531 });
532 }
533 if (hasWet_) {
534 UCField wb = wetB_;
535 Kokkos::parallel_for(
536 "vof::wy::wet_reset", Kokkos::RangePolicy<SExec>(SExec(), 0, len_),
537 KOKKOS_LAMBDA(long i) { wb(i) = static_cast<unsigned char>(kVofWetNone); });
538 }
539 for (int k = 1; k <= 3; ++k) {
540 if (hasWet_ && k == 1)
542 else
544 }
545 Kokkos::fence();
546 }
547
548 // ---- hooks -------------------------------------------------------------------------------
550 std::function<void(SField)> exchange;
552 std::function<double(double)> globalMax;
553
554 // ---- rung V-BC (WO-R) hooks ----------------------------------------------------------------
564 void setOutsideMask(UCField m) { outside_ = m; }
565 UCField outsideMask() const { return outside_; }
566 bool hasOutsideMask() const { return outside_.extent(0) == static_cast<std::size_t>(len_); }
567
577 double bcFaceVolume(int f) const { return bcVol_[f]; }
579 for (int f = 0; f < 6; ++f)
580 bcVol_[f] = 0.0;
581 }
587 void setBcFaceOwned(int f, bool on) { bcOwn_[f] = on ? 1u : 0u; }
588
589 // ---- WO-V9: per-kernel timing (OFF by default, and inert when off) ------------------------
590 //
591 // A phase boundary on a device backend is meaningless without a fence — queued kernels would be
592 // billed to whichever phase happens to call the clock next — so every boundary here fences,
593 // exactly as `IbmSolver::phaseTick` already does for the step's three coarse phases. That fence
594 // is a synchronisation, not a computation: it changes WHEN work happens, never WHAT it computes,
595 // and the gate on this instrument is that a run with `timingOn` true and one with it false agree
596 // bit for bit. When it is false, `tick_()` returns 0.0 without fencing and `addT_` returns
597 // without reading the clock, so the instrumented build is the uninstrumented one plus a
598 // predictable branch.
599 struct Timing {
600 double freeze = 0.0;
601 double reconstruct = 0.0;
602 double fluxes = 0.0;
603 double sweep = 0.0;
604 double clip = 0.0;
605 double exchange = 0.0;
607 long sweeps = 0;
608 };
609 bool timingOn = false;
611 void resetTiming() { tm = Timing(); }
612 const Timing& timing() const { return tm; }
613 static double wallSeconds() {
614 return std::chrono::duration<double>(std::chrono::steady_clock::now().time_since_epoch())
615 .count();
616 }
617 double tick_() const {
618 if (!timingOn)
619 return 0.0;
620 Kokkos::fence();
621 return wallSeconds();
622 }
623 void addT_(double& acc, double t0) {
624 if (!timingOn)
625 return;
626 Kokkos::fence();
627 acc += wallSeconds() - t0;
628 }
632 template <class F>
633 void timedExchange(F&& fn) {
634 const double t0 = tick_();
635 fn();
636 addT_(tm.exchange, t0);
637 }
639 timedExchange([&] { exchange(f); });
640 }
641
644 bool useWorklist = true;
648 double cflLimit = 0.25;
652 bool interfaceLocalCfl = false;
663 double wispEps = 0.0;
664
667 void syncGhosts() {
669 exchange(c_);
670 }
671
674 void advect(double dt, long step) {
676 const double dth = dt / h_;
677 const double cflLocal =
679 : maxCourant(dth);
681
682 // (1) THE dilation flag: frozen ONCE from C^n, used unchanged by all three sweeps.
684 if (hasGeom_) { // rung V5a: the clip / clamp census is per STEP, not per sweep
685 clippedVolume_ = 0.0;
686 clippedSigned_ = 0.0;
687 clampedFaces_ = 0;
688 }
689
690 // (2) three directional sweeps
691 const int* perm = kWySweepPerm[static_cast<int>(step % 6)];
692 for (int s = 0; s < 3; ++s) {
693 const int d = perm[s];
694 if (debugRecomputeDilation && s > 0)
695 freezeDilationFlag(); // the trap, on purpose — see `debugRecomputeDilation`
696 reconstruct();
697 computeFluxes(d, dth);
698 applySweep(d, dth);
699 if (hasGeom_)
700 clipCutCells(); // rung V5a rule 3 (diagnostic clip, cut cells only)
701 exchangeTimed(c_);
702 }
703 ++steps_;
704 }
705
706 double lastCfl() const { return lastCfl_; }
707 long lastMixedCount() const { return mixedCount_; }
708
709 // ---- rung V2b (WO-K) hooks -----------------------------------------------------------------
710 // The momentum-consistent transport of `rho^c u_c` must be driven by the SAME PLIC planes, the
711 // SAME face Courant numbers and the SAME sweep order as this colour advection. The planes are
712 // re-reconstructed before EVERY sweep and overwritten in place, so a sibling advector that ran
713 // after `advect()` returned would see only the last sweep's planes — the momentum sweeps have to
714 // be INTERLEAVED with the colour sweeps. `MomentumConsistentAdvector` therefore drives the step
715 // itself out of the public pieces below (`freezeDilationFlag` / `reconstruct` / `computeFluxes` /
716 // `applySweep` / `exchange`) instead of calling `advect()`, and reads the planes through these
717 // accessors between `computeFluxes` and `applySweep`, i.e. while they still describe the field
718 // the sweep is about to move. Nothing here changes `advect()`.
719
722 SField planeM(int d) const { return d == 0 ? mx_ : (d == 1 ? my_ : mz_); }
724 SField planeAlpha() const { return alpha_; }
726 UCField dilationFlag() const { return cc_; }
732 SField faceFlux() const { return flux_; }
733
740 double checkCourant(double dt) {
741 const double dth = dt / h_;
742 const double cflLocal =
744 : maxCourant(dth);
745 const double cfl = globalMax ? globalMax(cflLocal) : cflLocal;
746 enforceCfl(cfl, dt);
747 return cfl;
748 }
749
755 void enforceCfl(double cfl, double dt) {
756 lastCfl_ = cfl;
757 if (!(cfl <= cflLimit)) {
758 char msg[256];
759 std::snprintf(msg, sizeof(msg),
760 "peclet::flow::vof::WyAdvector: CFL = max|uf| dt/h = %.6g exceeds the "
761 "Weymouth-Yue boundedness cap %.6g (dt = %.6g, h = %.6g) - reduce dt",
762 cfl, cflLimit, dt, h_);
763 throw std::runtime_error(msg);
764 }
765 }
766
771 void noteBatchedStep() { ++steps_; }
774 bool bcFaceOwned(int f) const { return bcOwn_[f] != 0; }
775 void addBcFaceVolume(int f, double v) { bcVol_[f] += v; }
776
779 const I3 e = e_, n = n_;
780 const int g = g_;
781 SField c = c_;
782 using MD = MDRange3<SExec>;
783 double sum = 0.0, mn = 0.0, mx = 0.0;
784 long mixed = 0, wisps = 0;
785 MD pol(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z});
786 Kokkos::parallel_reduce(
787 "vof::wy::diag_sum", pol,
788 KOKKOS_LAMBDA(int x, int y, int z, double& acc) { acc += c(L3(x, y, z, e)); }, sum);
789 Kokkos::parallel_reduce(
790 "vof::wy::diag_min", pol,
791 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
792 acc = Kokkos::fmin(acc, c(L3(x, y, z, e)));
793 },
794 Kokkos::Min<double>(mn));
795 Kokkos::parallel_reduce(
796 "vof::wy::diag_max", pol,
797 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
798 acc = Kokkos::fmax(acc, c(L3(x, y, z, e)));
799 },
800 Kokkos::Max<double>(mx));
801 Kokkos::parallel_reduce(
802 "vof::wy::diag_counts", pol,
803 KOKKOS_LAMBDA(int x, int y, int z, long& nm, long& nw) {
804 const double v = c(L3(x, y, z, e));
805 if (wyIsMixed(v)) {
806 ++nm;
807 if (v < 1e-8 || v > 1.0 - 1e-8)
808 ++nw;
809 }
810 },
811 mixed, wisps);
812 Kokkos::fence();
814 dg.sumC = sum;
815 dg.minC = mn;
816 dg.maxC = mx;
817 dg.mixed = mixed;
818 dg.wisps = wisps;
819 if (hasGeom_)
821 return dg;
822 }
823
826 double maxCourant(double dth) const {
827 const I3 e = e_, n = n_;
828 const int g = g_;
829 SField u = uf_, v = vf_, w = wf_;
830 double m = 0.0;
831 Kokkos::parallel_reduce(
832 "vof::wy::cfl", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
833 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
834 const long i = L3(x, y, z, e);
835 acc = Kokkos::fmax(acc, Kokkos::fabs(u(i)));
836 acc = Kokkos::fmax(acc, Kokkos::fabs(v(i)));
837 acc = Kokkos::fmax(acc, Kokkos::fabs(w(i)));
838 },
839 Kokkos::Max<double>(m));
840 Kokkos::fence();
841 return m * dth;
842 }
843
866 double maxCourantInterface(double dth) const {
867 const I3 e = e_, n = n_;
868 const int g = g_;
869 const long sx = 1, sy = e_.x, sz = static_cast<long>(e_.x) * e_.y;
870 SField c = c_, u = uf_, v = vf_, w = wf_;
871 const double weps = wispEps;
872 double m = 0.0;
873 Kokkos::parallel_reduce(
874 "vof::wy::cfl_interface", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
875 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
876 const long i = L3(x, y, z, e);
877 const double ci = c(i);
878 const bool band = wyIsMixed(ci, weps) || wyColourJump(c(i - sx), ci, weps) ||
879 wyColourJump(c(i + sx), ci, weps) ||
880 wyColourJump(c(i - sy), ci, weps) ||
881 wyColourJump(c(i + sy), ci, weps) ||
882 wyColourJump(c(i - sz), ci, weps) || wyColourJump(c(i + sz), ci, weps);
883 if (!band)
884 return;
885 acc = Kokkos::fmax(acc, Kokkos::fabs(u(i)));
886 acc = Kokkos::fmax(acc, Kokkos::fabs(u(i - sx)));
887 acc = Kokkos::fmax(acc, Kokkos::fabs(v(i)));
888 acc = Kokkos::fmax(acc, Kokkos::fabs(v(i - sy)));
889 acc = Kokkos::fmax(acc, Kokkos::fabs(w(i)));
890 acc = Kokkos::fmax(acc, Kokkos::fabs(w(i - sz)));
891 },
892 Kokkos::Max<double>(m));
893 Kokkos::fence();
894 // Max's identity is -inf; an empty band (a uniform colour field) means no constraint, not an
895 // unconstrained step. Clamp so the reported CFL is a number and the cap test is meaningful.
896 return Kokkos::fmax(m, 0.0) * dth;
897 }
898
902 double maxCourantInterfaceAuto(double dth) const {
903 return hasGeom_ ? maxCourantInterfaceCut(dth) : maxCourantInterface(dth);
904 }
905
908 double maxDiscreteDivergence(double dth) const {
909 const I3 e = e_, n = n_;
910 const int g = g_;
911 const long sy = e_.x, sz = static_cast<long>(e_.x) * e_.y;
912 SField u = uf_, v = vf_, w = wf_;
913 double m = 0.0;
914 Kokkos::parallel_reduce(
915 "vof::wy::divmax", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
916 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
917 const long i = L3(x, y, z, e);
918 const double d = (u(i) - u(i - 1)) + (v(i) - v(i - sy)) + (w(i) - w(i - sz));
919 acc = Kokkos::fmax(acc, Kokkos::fabs(d) * dth);
920 },
921 Kokkos::Max<double>(m));
922 Kokkos::fence();
923 return m;
924 }
925
926 public:
927 // ---- implementation detail -----------------------------------------------------------------
928 // These stay PUBLIC only because nvcc rejects an extended __host__ __device__ lambda whose
929 // enclosing member function has private or protected access. Treat them as private.
930
931 void requireExchange() const {
932 if (!exchange)
933 throw std::runtime_error("peclet::flow::vof::WyAdvector: the `exchange` hook is not set");
934 }
935
937 const double _t0 = tick_();
938 SField c = c_;
939 UCField cc = cc_;
940 Kokkos::parallel_for(
941 "vof::wy::freeze", Kokkos::RangePolicy<SExec>(SExec(), 0, len_),
942 // thesis eq. A.28: g = 1 if C^n > 1/2, else 0. Strict `>` (the tie is measure zero and the
943 // proof only needs the flag to be a constant of the step).
944 KOKKOS_LAMBDA(long i) { cc(i) = c(i) > 0.5 ? 1u : 0u; });
945 addT_(tm.freeze, _t0);
946 }
947
951 void reconstruct() {
952 const double _t0 = tick_();
955 }
957 const I3 e = e_;
958 const int g = g_;
959 const int rx = n_.x + 2, ry = n_.y + 2, rz = n_.z + 2;
960 const long region = static_cast<long>(rx) * ry * rz;
961 const long sy = e_.x, sz = static_cast<long>(e_.x) * e_.y;
962 SField c = c_, mx = mx_, my = my_, mz = mz_, al = alpha_;
963 const double weps = wispEps; // WO-R2 item 4: the reconstruction and the flux share it
964
965 if (useWorklist) {
966 LField list = list_;
967 long cnt = 0;
968 Kokkos::parallel_scan(
969 "vof::wy::worklist", Kokkos::RangePolicy<SExec>(SExec(), 0, region),
970 KOKKOS_LAMBDA(const long r, long& upd, const bool final) {
971 const int ix = static_cast<int>(r % rx);
972 const int iy = static_cast<int>((r / rx) % ry);
973 const int iz = static_cast<int>(r / (static_cast<long>(rx) * ry));
974 const long i = L3(g - 1 + ix, g - 1 + iy, g - 1 + iz, e);
975 if (wyIsMixed(c(i), weps)) {
976 if (final)
977 list(upd) = i;
978 ++upd;
979 }
980 },
981 cnt);
982 Kokkos::fence();
983 mixedCount_ = cnt;
984 Kokkos::parallel_for(
985 "vof::wy::plic", Kokkos::RangePolicy<SExec>(SExec(), 0, cnt),
986 KOKKOS_LAMBDA(long t) { wyReconstructCell(c, list(t), sy, sz, mx, my, mz, al); });
987 } else {
988 Kokkos::parallel_for(
989 "vof::wy::plic_dense", Kokkos::RangePolicy<SExec>(SExec(), 0, region),
990 KOKKOS_LAMBDA(const long r) {
991 const int ix = static_cast<int>(r % rx);
992 const int iy = static_cast<int>((r / rx) % ry);
993 const int iz = static_cast<int>(r / (static_cast<long>(rx) * ry));
994 const long i = L3(g - 1 + ix, g - 1 + iy, g - 1 + iz, e);
995 if (wyIsMixed(c(i), weps))
996 wyReconstructCell(c, i, sy, sz, mx, my, mz, al);
997 });
998 }
999 }
1000
1004 void computeFluxes(int d, double dth) {
1005 const double _t0 = tick_();
1006 computeFluxesImpl(d, dth);
1007 addT_(tm.fluxes, _t0);
1008 }
1009 void computeFluxesImpl(int d, double dth) {
1010 if (hasGeom_) { // rung V5a: openness-weighted flux (`cutcell.hpp` eq. 1)
1011 // COMPOSED with the V-BC boundary rule since WO-R2 item 2: `computeFluxesCut` carries the
1012 // out-of-domain mask and the per-face ledger itself, so `F = o_f * C_datum * a` at a domain
1013 // face of a geometry-carrying block, including a domain face a solid cuts (o_f < 1).
1014 computeFluxesCut(d, dth);
1015 return;
1016 }
1017 const I3 e = e_;
1018 const int g = g_;
1019 const long sd =
1020 d == 0 ? 1 : (d == 1 ? static_cast<long>(e_.x) : static_cast<long>(e_.x) * e_.y);
1021 // d-index [g-1, g+n_d): the `-` face of the first inner cell through the `+` face of the last.
1022 // Transverse indices stay on the inner region.
1023 int lo[3] = {g, g, g};
1024 const int hi[3] = {g + n_.x, g + n_.y, g + n_.z};
1025 lo[d] -= 1;
1026 SField c = c_, mx = mx_, my = my_, mz = mz_, al = alpha_, fl = flux_, u = faceVel(d);
1027 const double weps = wispEps;
1028 if (hasOutsideMask()) { // WO-R: boundary donors are DATA, not a reconstructable interface
1029 UCField ob = outside_;
1030 Kokkos::parallel_for(
1031 "vof::wy::flux_bc",
1032 MDRange3<SExec>(SExec(), {lo[0], lo[1], lo[2]}, {hi[0], hi[1], hi[2]}),
1033 KOKKOS_LAMBDA(int x, int y, int z) {
1034 const long p = L3(x, y, z, e);
1035 fl(p) = wyFaceFluxBc(u(p) * dth, p, sd, d, c, mx, my, mz, al, ob, weps);
1036 });
1038 return;
1039 }
1040 Kokkos::parallel_for(
1041 "vof::wy::flux", MDRange3<SExec>(SExec(), {lo[0], lo[1], lo[2]}, {hi[0], hi[1], hi[2]}),
1042 KOKKOS_LAMBDA(int x, int y, int z) {
1043 const long p = L3(x, y, z, e);
1044 fl(p) = wyFaceFlux(u(p) * dth, p, sd, d, c, mx, my, mz, al, weps);
1045 });
1046 }
1047
1054 const I3 e = e_, n = n_;
1055 const int g = g_;
1056 const int b = (d + 1) % 3, c2 = (d + 2) % 3;
1057 const long st[3] = {1, e_.x, static_cast<long>(e_.x) * e_.y};
1058 const long sd = st[d], sb = st[b], sc = st[c2];
1059 const int nb = (b == 0) ? n.x : (b == 1) ? n.y : n.z;
1060 const int nc = (c2 == 0) ? n.x : (c2 == 1) ? n.y : n.z;
1061 const int nd = (d == 0) ? n.x : (d == 1) ? n.y : n.z;
1062 SField fl = flux_;
1063 for (int s = 0; s < 2; ++s) {
1064 if (!bcOwn_[2 * d + s])
1065 continue; // not a global domain face this block owns: nothing to account for
1066 const int fa = (s == 0) ? (g - 1) : (g + nd - 1);
1067 double acc = 0.0;
1068 Kokkos::parallel_reduce(
1069 "vof::wy::bcvol", MDRange2<SExec>(SExec(), {g, g}, {g + nb, g + nc}),
1070 KOKKOS_LAMBDA(int p0, int p1, double& r) {
1071 r += fl(static_cast<long>(p0) * sb + static_cast<long>(p1) * sc +
1072 static_cast<long>(fa) * sd);
1073 },
1074 acc);
1075 Kokkos::fence();
1076 if (bcOwn_[2 * d + s])
1077 bcVol_[2 * d + s] += (s == 0) ? acc : -acc;
1078 }
1079 }
1080
1082 void applySweep(int d, double dth) {
1083 const double _t0 = tick_();
1084 applySweepImpl(d, dth);
1085 addT_(tm.sweep, _t0);
1086 ++tm.sweeps;
1087 }
1088 void applySweepImpl(int d, double dth) {
1089 if (hasGeom_) { // rung V5a: the update in FLUID-VOLUME units
1090 applySweepCut(d, dth);
1091 return;
1092 }
1093 const I3 e = e_, n = n_;
1094 const int g = g_;
1095 const long sd =
1096 d == 0 ? 1 : (d == 1 ? static_cast<long>(e_.x) : static_cast<long>(e_.x) * e_.y);
1097 SField c = c_, fl = flux_, u = faceVel(d);
1098 UCField cc = cc_;
1099 Kokkos::parallel_for(
1100 "vof::wy::update", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
1101 KOKKOS_LAMBDA(int x, int y, int z) {
1102 const long i = L3(x, y, z, e);
1103 // The dilation term must scale the SAME uf by the SAME dt/h as the flux, or the exact
1104 // cancellation in full cells (file header) is lost to rounding.
1105 const double aP = u(i) * dth, aM = u(i - sd) * dth;
1106 const double dil = cc(i) ? (aP - aM) : 0.0;
1107 c(i) = c(i) + (fl(i - sd) - fl(i)) + dil;
1108 });
1109 }
1110
1111 // ---- rung V5a (WO-Q) kernel bodies ---------------------------------------------------------
1112 // Siblings of the V1 kernels above, reached only through the `hasGeom_` branch at the top of
1113 // each. The V1 bodies are untouched, so "no geometry" is byte-identical by construction rather
1114 // than by a tolerance.
1115
1118 void computeFluxesCut(int d, double dth) {
1119 const I3 e = e_;
1120 const int g = g_;
1121 const long sd =
1122 d == 0 ? 1 : (d == 1 ? static_cast<long>(e_.x) : static_cast<long>(e_.x) * e_.y);
1123 int lo[3] = {g, g, g};
1124 const int hi[3] = {g + n_.x, g + n_.y, g + n_.z};
1125 lo[d] -= 1;
1126 SField c = c_, mx = mx_, my = my_, mz = mz_, al = alpha_, fl = flux_, u = faceVel(d),
1127 o = of_[d], ep = eps_;
1128 UCField kd = kind_;
1129 const bool clamp = cutFluxClamp;
1130 const double weps = wispEps;
1131 // WO-R2 item 2 — V5a x V-BC. With BOTH rungs armed a domain-face flux in a geometry-carrying
1132 // block is `o_f * C_datum * a`: the out-of-domain donor is fluxed ALGEBRAICALLY (it is a
1133 // prescribed datum with no reconstructable interface — `wyFaceFluxBc`), and the openness
1134 // weight `o_f` still applies, because a solid may cut the domain face itself (o_f < 1 there,
1135 // 1 on the clear part). The per-face boundary ledger then accumulates the SAME `o_f`-weighted
1136 // number the update telescopes on, so the WO-R budget identity holds unchanged in cut cells.
1137 // The clamp keeps its contract: it applies only to a MIXED donor, and an out-of-domain donor
1138 // has taken the algebraic branch, whose flux is already exactly bounded.
1139 const bool bc = hasOutsideMask();
1140 UCField ob = bc ? outside_ : UCField();
1141 long nclamp = 0;
1142 Kokkos::parallel_reduce(
1143 "vof::wy::flux_cut", MDRange3<SExec>(SExec(), {lo[0], lo[1], lo[2]}, {hi[0], hi[1], hi[2]}),
1144 KOKKOS_LAMBDA(int x, int y, int z, long& acc) {
1145 const long p = L3(x, y, z, e);
1146 const double op = o(p);
1147 if (op == 0.0) {
1148 fl(p) = 0.0;
1149 return;
1150 }
1151 const double a = u(p) * dth;
1152 const double raw =
1153 vofCutFlux(op, bc ? wyFaceFluxBc(a, p, sd, d, c, mx, my, mz, al, ob, weps)
1154 : wyFaceFlux(a, p, sd, d, c, mx, my, mz, al, weps));
1155 if (!clamp || a == 0.0) {
1156 fl(p) = raw;
1157 return;
1158 }
1159 const long q = a > 0.0 ? p : p + sd;
1160 const double cq = c(q);
1161 // algebraic branch: already exactly bounded, and clamping it would break the exact
1162 // full-cell cancellation. An out-of-domain donor is on that branch by construction.
1163 if (!wyIsMixed(cq, weps) || (bc && ob(q))) {
1164 fl(p) = raw;
1165 return;
1166 }
1167 const double aa = a > 0.0 ? a : -a;
1168 const double mag = raw > 0.0 ? raw : -raw;
1169 const double cl = vofCutFluxClamp(mag, op * aa, vofEpsEff(ep(q), kd(q) == kVofSolid), cq);
1170 if (cl != mag)
1171 ++acc;
1172 fl(p) = a > 0.0 ? cl : -cl;
1173 },
1174 nclamp);
1175 Kokkos::fence();
1176 clampedFaces_ += nclamp;
1177 if (bc)
1179 }
1180
1183 void applySweepCut(int d, double dth) {
1184 const I3 e = e_, n = n_;
1185 const int g = g_;
1186 const long sd =
1187 d == 0 ? 1 : (d == 1 ? static_cast<long>(e_.x) : static_cast<long>(e_.x) * e_.y);
1188 SField c = c_, fl = flux_, u = faceVel(d), o = of_[d], ep = eps_;
1189 UCField cc = cc_, kd = kind_;
1190 Kokkos::parallel_for(
1191 "vof::wy::update_cut", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
1192 KOKKOS_LAMBDA(int x, int y, int z) {
1193 const long i = L3(x, y, z, e);
1194 if (kd(i) == kVofSolid)
1195 return;
1196 const double epsEff = vofEpsEff(ep(i), false);
1197 // Same `o_f a_f` in the flux and in the dilation term — that shared factor is what makes
1198 // the two contributions telescope against the projection's openness-weighted divergence.
1199 // `o * (u * dth)` and NOT `(o * u) * dth`: the flux forms the Courant number FIRST
1200 // (`wyFaceFlux(u(p) * dth, ...)`), so only this association makes the two cancel to the
1201 // last bit in a full cell — the property that keeps the conservation floor on the
1202 // interface area rather than on the domain volume (see the file header).
1203 const double aP = o(i) * (u(i) * dth), aM = o(i - sd) * (u(i - sd) * dth);
1204 const double dil = cc(i) ? (aP - aM) : 0.0;
1205 c(i) = c(i) + ((fl(i - sd) - fl(i)) + dil) / epsEff;
1206 });
1207 }
1208
1213 const double _t0 = tick_();
1215 addT_(tm.clip, _t0);
1216 }
1218 const I3 e = e_, n = n_;
1219 const int g = g_;
1220 SField c = c_, ep = eps_;
1221 UCField kd = kind_;
1222 double acc = 0.0, sgn = 0.0;
1223 Kokkos::parallel_reduce(
1224 "vof::wy::clip_cut", MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
1225 KOKKOS_LAMBDA(int x, int y, int z, double& a, double& b) {
1226 const long i = L3(x, y, z, e);
1227 if (kd(i) == kVofSolid)
1228 return;
1229 const double epsEff = vofEpsEff(ep(i), false);
1230 if (!(epsEff < 1.0))
1231 return; // whole fluid cell: V1 boundedness applies verbatim, do not touch it
1232 const double cv = c(i);
1233 if (cv >= 0.0 && cv <= 1.0)
1234 return;
1235 double moved = 0.0;
1236 c(i) = vofClipCut(cv, epsEff, moved);
1237 a += Kokkos::fabs(moved);
1238 b += moved;
1239 },
1240 acc, sgn);
1241 Kokkos::fence();
1242 clippedVolume_ += acc;
1243 clippedSigned_ += sgn;
1244 }
1245
1249 double maxCourantInterfaceCut(double dth) const {
1250 const I3 e = e_, n = n_;
1251 const int g = g_;
1252 const long sx = 1, sy = e_.x, sz = static_cast<long>(e_.x) * e_.y;
1253 SField c = c_, u = uf_, v = vf_, w = wf_, ox = of_[0], oy = of_[1], oz = of_[2], ep = eps_;
1254 UCField kd = kind_;
1255 const double weps = wispEps;
1256 double m = 0.0;
1257 Kokkos::parallel_reduce(
1258 "vof::wy::cfl_interface_cut",
1259 MDRange3<SExec>(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z}),
1260 KOKKOS_LAMBDA(int x, int y, int z, double& acc) {
1261 const long i = L3(x, y, z, e);
1262 if (kd(i) == kVofSolid)
1263 return;
1264 const double ci = c(i);
1265 const bool band = wyIsMixed(ci, weps) || wyColourJump(c(i - sx), ci, weps) ||
1266 wyColourJump(c(i + sx), ci, weps) ||
1267 wyColourJump(c(i - sy), ci, weps) ||
1268 wyColourJump(c(i + sy), ci, weps) ||
1269 wyColourJump(c(i - sz), ci, weps) || wyColourJump(c(i + sz), ci, weps);
1270 if (!band)
1271 return;
1272 const double ei = ep(i);
1273 acc = Kokkos::fmax(acc, vofCutCourant(ox(i), u(i) * dth, ei));
1274 acc = Kokkos::fmax(acc, vofCutCourant(ox(i - sx), u(i - sx) * dth, ei));
1275 acc = Kokkos::fmax(acc, vofCutCourant(oy(i), v(i) * dth, ei));
1276 acc = Kokkos::fmax(acc, vofCutCourant(oy(i - sy), v(i - sy) * dth, ei));
1277 acc = Kokkos::fmax(acc, vofCutCourant(oz(i), w(i) * dth, ei));
1278 acc = Kokkos::fmax(acc, vofCutCourant(oz(i - sz), w(i - sz) * dth, ei));
1279 },
1280 Kokkos::Max<double>(m));
1281 Kokkos::fence();
1282 return Kokkos::fmax(m, 0.0);
1283 }
1284
1288 void solidBandFillPass(int k) {
1289 const I3 e = e_, n = n_;
1290 const int g = g_, maxDepth = 3 - k;
1291 const long sx = 1, sy = e_.x, sz = static_cast<long>(e_.x) * e_.y;
1292 SField c = c_;
1293 UCField fs = fill_, mk = mark_;
1294 Kokkos::parallel_for(
1295 "vof::wy::band_fill", MDRange3<SExec>(SExec(), {0, 0, 0}, {e.x, e.y, e.z}),
1296 KOKKOS_LAMBDA(int x, int y, int z) {
1297 if (vofGhostDepth(x, y, z, g, n.x, n.y, n.z) > maxDepth)
1298 return;
1299 const long i = L3(x, y, z, e);
1300 if (fs(i) != kVofFillNone)
1301 return;
1302 const long nb[6] = {i - sx, i + sx, i - sy, i + sy, i - sz, i + sz};
1303 double acc = 0.0;
1304 int cnt = 0;
1305 for (int q = 0; q < 6; ++q)
1306 if (vofFillReadable(fs(nb[q]), k)) {
1307 acc += c(nb[q]);
1308 ++cnt;
1309 }
1310 if (cnt == 0)
1311 return; // no donor yet — a later pass may reach it, or it stays as it was
1312 c(i) = acc / cnt;
1313 mk(i) = 1u;
1314 });
1315 Kokkos::fence();
1316 Kokkos::parallel_for(
1317 "vof::wy::band_fill_commit", Kokkos::RangePolicy<SExec>(SExec(), 0, len_),
1318 KOKKOS_LAMBDA(long i) {
1319 if (mk(i)) {
1320 fs(i) = static_cast<unsigned char>(kVofFillPass0 + k);
1321 mk(i) = 0u;
1322 }
1323 });
1324 Kokkos::fence();
1325 }
1326
1332 const I3 e = e_, n = n_;
1333 const int g = g_, maxDepth = 2; // pass k = 1
1334 const long sx = 1, sy = e_.x, sz = static_cast<long>(e_.x) * e_.y;
1335 SField c = c_, sdf = sdfB_, th = thetaB_, ap = appB_;
1336 SField mx = mfl_[0], my = mfl_[1], mz = mfl_[2];
1337 UCField fs = fill_, mk = mark_, kk = kind_, wb = wetB_;
1338 const int pivot = wettingPivot;
1339 const double tEps = wettingTangentEps, pureEps = wettingPureEps;
1340 const VofMetric gme = metric; // `g` is the ghost width in this scope
1341 Kokkos::parallel_for(
1342 "vof::wy::band_fill_theta", MDRange3<SExec>(SExec(), {0, 0, 0}, {e.x, e.y, e.z}),
1343 KOKKOS_LAMBDA(int x, int y, int z) {
1344 if (vofGhostDepth(x, y, z, g, n.x, n.y, n.z) > maxDepth)
1345 return;
1346 const long i = L3(x, y, z, e);
1347 if (fs(i) != kVofFillNone)
1348 return; // fluid, or already carrying data
1349 // The wall normal, solid -> fluid, from the exchanged SDF. Depth <= 2 guarantees the
1350 // central difference stays inside the g = 3 block. The difference is taken on the INDEX
1351 // lattice, so `gw` is an index-space gradient; `vofWettingPlane` pulls it back to the
1352 // physical direction itself (V4.1).
1353 double nw[3] = {0.5 * (sdf(i + sx) - sdf(i - sx)), 0.5 * (sdf(i + sy) - sdf(i - sy)),
1354 0.5 * (sdf(i + sz) - sdf(i - sz))};
1355 // Phase 3 (V4.2): the WALK is an index walk along the PHYSICAL wall normal, i.e. along
1356 // `H^-1 n_w ~ H^-2 grad_xi(sdf)`. Written as one division per axis followed by ONE L2
1357 // normalisation, so at `metric.h = {1,1,1}` it is `gw/|gw|` — today's vector, bit for
1358 // bit (a second normalisation of an already-unit vector would NOT be).
1359 double wk[3] = {nw[0] / (gme.h[0] * gme.h[0]), nw[1] / (gme.h[1] * gme.h[1]),
1360 nw[2] / (gme.h[2] * gme.h[2])};
1361 const double nn = Kokkos::sqrt(wk[0] * wk[0] + wk[1] * wk[1] + wk[2] * wk[2]);
1362 if (nn > 1e-12) {
1363 wk[0] /= nn;
1364 wk[1] /= nn;
1365 wk[2] /= nn;
1366 for (int step = 1; step <= 4; ++step) {
1367 const int fx = x + static_cast<int>(Kokkos::round(step * wk[0]));
1368 const int fy = y + static_cast<int>(Kokkos::round(step * wk[1]));
1369 const int fz = z + static_cast<int>(Kokkos::round(step * wk[2]));
1370 if (fx < 0 || fy < 0 || fz < 0 || fx >= e.x || fy >= e.y || fz >= e.z)
1371 break;
1372 const long fi = L3(fx, fy, fz, e);
1373 if (kk(fi) != kVofFluid)
1374 continue; // still inside the solid: keep walking outward
1375 const double cf = c(fi);
1376 if (cf <= pureEps || cf >= 1.0 - pureEps) {
1377 // The anchor column carries no interface, but the CONTINUED interface may still
1378 // reach this band cell from the column next door (the wetting case: the contact
1379 // line crosses the first fluid row one column in). Average the theta-planes of the
1380 // anchor's MIXED fluid neighbours; with none, the pure continuation stands.
1381 double acc2 = 0.0;
1382 int cnt2 = 0;
1383 const bool inner =
1384 fx >= 1 && fy >= 1 && fz >= 1 && fx + 1 < e.x && fy + 1 < e.y && fz + 1 < e.z;
1385 if (inner)
1386 for (int kz = -1; kz <= 1; ++kz)
1387 for (int ky = -1; ky <= 1; ++ky)
1388 for (int kx = -1; kx <= 1; ++kx) {
1389 const long gi = L3(fx + kx, fy + ky, fz + kz, e);
1390 if (kk(gi) != kVofFluid)
1391 continue;
1392 const double cg = c(gi);
1393 if (cg <= pureEps || cg >= 1.0 - pureEps)
1394 continue;
1395 const double mg[3] = {mx(gi), my(gi), mz(gi)};
1396 if (Kokkos::fabs(mg[0]) + Kokkos::fabs(mg[1]) + Kokkos::fabs(mg[2]) <= 0.0)
1397 continue;
1398 double mt2[3], al2, ca2;
1399 const double t2 = th(i);
1400 vofWettingPlane(mg, cg, nw, Kokkos::cos(t2), Kokkos::sin(t2), sdf(gi),
1401 pivot, tEps, mt2, al2, ca2, gme);
1402 const int ds2[3] = {x - (fx + kx), y - (fy + ky), z - (fz + kz)};
1404 ++cnt2;
1405 }
1406 c(i) = cnt2 ? acc2 / cnt2 : cf;
1407 mk(i) = 1u;
1408 wb(i) = static_cast<unsigned char>(cnt2 ? kVofWetNeighbour : kVofWetPure);
1409 return;
1410 }
1411 const double mf[3] = {mx(fi), my(fi), mz(fi)};
1412 if (Kokkos::fabs(mf[0]) + Kokkos::fabs(mf[1]) + Kokkos::fabs(mf[2]) <= 0.0)
1413 break; // no usable fluid-only normal -> the neutral fallback below
1414 double mth[3], alphaTh, cosApp;
1415 const double t0 = th(i);
1416 const int br = vofWettingPlane(mf, cf, nw, Kokkos::cos(t0), Kokkos::sin(t0), sdf(fi),
1418 const int ds[3] = {x - fx, y - fy, z - fz};
1419 c(i) = vofWettingFraction(mth, alphaTh, ds);
1420 ap(i) = Kokkos::acos(cosApp < -1.0 ? -1.0 : (cosApp > 1.0 ? 1.0 : cosApp));
1421 mk(i) = 1u;
1422 wb(i) = static_cast<unsigned char>(br);
1423 return;
1424 }
1425 }
1426 // Fallback: WO-Q's neutral pass-1 rule, verbatim.
1427 const long nb[6] = {i - sx, i + sx, i - sy, i + sy, i - sz, i + sz};
1428 double acc = 0.0;
1429 int cnt = 0;
1430 for (int q = 0; q < 6; ++q)
1431 if (vofFillReadable(fs(nb[q]), 1)) {
1432 acc += c(nb[q]);
1433 ++cnt;
1434 }
1435 if (cnt == 0)
1436 return;
1437 c(i) = acc / cnt;
1438 mk(i) = 1u;
1439 wb(i) = static_cast<unsigned char>(kVofWetNeutral);
1440 });
1441 Kokkos::fence();
1442 Kokkos::parallel_for(
1443 "vof::wy::band_fill_theta_commit", Kokkos::RangePolicy<SExec>(SExec(), 0, len_),
1444 KOKKOS_LAMBDA(long i) {
1445 if (mk(i)) {
1446 fs(i) = static_cast<unsigned char>(kVofFillPass0 + 1);
1447 mk(i) = 0u;
1448 }
1449 });
1450 Kokkos::fence();
1451 }
1452
1455 const I3 e = e_, n = n_;
1456 const int g = g_;
1457 SField c = c_, ep = eps_;
1458 UCField kd = kind_;
1459 using MD = MDRange3<SExec>;
1460 MD pol(SExec(), {g, g, g}, {g + n.x, g + n.y, g + n.z});
1461 double vol = 0.0, raw = 0.0, solidC = 0.0;
1462 long ncut = 0, nsolid = 0;
1463 Kokkos::parallel_reduce(
1464 "vof::wy::cut_sums", pol,
1465 KOKKOS_LAMBDA(int x, int y, int z, double& v, double& r, double& sc) {
1466 const long i = L3(x, y, z, e);
1467 if (kd(i) == kVofSolid) {
1468 sc += c(i);
1469 return;
1470 }
1471 v += vofEpsEff(ep(i), false) * c(i);
1472 r += ep(i) * c(i);
1473 },
1474 vol, raw, solidC);
1475 Kokkos::parallel_reduce(
1476 "vof::wy::cut_counts", pol,
1477 KOKKOS_LAMBDA(int x, int y, int z, long& nc, long& ns) {
1478 const long i = L3(x, y, z, e);
1479 if (kd(i) == kVofSolid) {
1480 ++ns;
1481 return;
1482 }
1483 const double ee = vofEpsEff(ep(i), false);
1484 if (ee < 1.0)
1485 ++nc;
1486 },
1487 ncut, nsolid);
1488 double mn = 1e300, mx = -1e300;
1489 Kokkos::parallel_reduce(
1490 "vof::wy::cut_minfluid", pol,
1491 KOKKOS_LAMBDA(int x, int y, int z, double& a) {
1492 const long i = L3(x, y, z, e);
1493 if (kd(i) == kVofSolid || ep(i) < 1.0)
1494 return;
1495 a = Kokkos::fmin(a, c(i));
1496 },
1497 Kokkos::Min<double>(mn));
1498 Kokkos::parallel_reduce(
1499 "vof::wy::cut_maxfluid", pol,
1500 KOKKOS_LAMBDA(int x, int y, int z, double& a) {
1501 const long i = L3(x, y, z, e);
1502 if (kd(i) == kVofSolid || ep(i) < 1.0)
1503 return;
1504 a = Kokkos::fmax(a, c(i));
1505 },
1506 Kokkos::Max<double>(mx));
1507 Kokkos::fence();
1508 dg.volume = vol;
1509 dg.rawVolume = raw;
1510 dg.solidFillSum = solidC;
1511 dg.cutCells = ncut;
1512 dg.solidCells = nsolid;
1513 dg.minCFluid = mn > 1e299 ? 0.0 : mn;
1514 dg.maxCFluid = mx < -1e299 ? 0.0 : mx;
1515 dg.clippedVolume = clippedVolume_;
1516 dg.clippedSigned = clippedSigned_;
1517 dg.clampedFaces = clampedFaces_;
1518 }
1519
1520 private:
1521 I3 n_{0, 0, 0}, e_{0, 0, 0};
1522 int g_ = 3;
1523 double h_ = 1.0;
1524 long len_ = 0, listCap_ = 0;
1525 SField c_, mx_, my_, mz_, alpha_, flux_, uf_, vf_, wf_;
1526 UCField cc_, outside_; // outside_: the WO-R out-of-domain mask (empty = the V1 path)
1527 double bcVol_[6] = {0, 0, 0, 0, 0, 0};
1528 unsigned char bcOwn_[6] = {0, 0, 0, 0, 0, 0};
1529 LField list_;
1530 long mixedCount_ = 0, steps_ = 0;
1531 double lastCfl_ = 0.0;
1532 // rung V5a (WO-Q) cut-cell geometry; `hasGeom_` false => every V1 kernel runs unchanged.
1533 bool hasGeom_ = false;
1534 SField of_[3], eps_, kindD_;
1535 UCField kind_, fill_, mark_;
1536 // rung V5b (WO-S) wetting; `hasWet_` false => pass 1 is WO-Q's neutral rule, byte-identically.
1537 bool hasWet_ = false;
1538 SField sdfB_, thetaB_, mfl_[3], appB_;
1539 UCField wetB_;
1540 double clippedVolume_ = 0.0, clippedSigned_ = 0.0;
1541 long clampedFaces_ = 0;
1542};
1543
1544} // namespace peclet::flow::vof
1545
1546#endif // PECLET_FLOW_VOF_ADVECT_WY_HPP
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
double clippedVolume() const
|liquid volume| the cut-cell clip moved during the last advect() (0 without geometry).
SField contactAngle() const
The prescribed contact angle per cell, in RADIANS, measured through the liquid.
double bcFaceVolume(int f) const
Signed liquid volume that crossed domain face f (0..5 = −x,+x,−y,+y,−z,+z) since the last resetBcFace...
void accumulateBcFaceVolume(int d)
Sum the just-computed d-fluxes on the two block-boundary d-faces into bcVol_, signed so that POSITIVE...
double maxCourantInterfaceAuto(double dth) const
The interface-local Courant number for this dt/h, using the CUT-CELL rule when geometry is attached (...
UCField cellKind() const
The cell classification (VofCellKind).
bool debugRecomputeDilation
DIAGNOSTIC ONLY — recompute the dilation flag before every sweep instead of freezing it once.
void applySweepImpl(int d, double dth)
void cutDiagnostics(Diagnostics &dg) const
The cut-cell half of diagnostics() (zero-cost when no geometry is attached).
SField wettingNormal(int d) const
The fluid-only Youngs normal of each fluid cell (buildWettingNormals, then exchanged).
double maxCourant(double dth) const
Max |uf| dt / h over this block's owned faces (the + face of every inner cell; the - face of the firs...
double maxCourantInterface(double dth) const
Max |uf| dt/h over the faces of INTERFACE-ADJACENT cells only.
bool cutFluxClamp
Weymouth's admissible-interval clamp on the openness-weighted flux (cutcell.hpp vofCutFluxClamp).
void classifyGeometry()
Classify every cell from eps and the six face openness values (cutcell.hpp rule 1) into kindDouble().
double maxDiscreteDivergence(double dth) const
Max |discrete face divergence| * dt/h over inner cells — the exact quantity the conservation floor is...
void applySweep(int d, double dth)
C_i += (F_{i-} - F_{i+}) + c_i (a_{i+} - a_{i-}), over inner cells.
UCField dilationFlag() const
The frozen dilation indicator H(C^n - 1/2) of the current step (pressure cells).
SField colour() const
Colour field on the extended block (x-fastest).
void buildWettingNormals()
The fluid-only Youngs normal at every FLUID cell of the INNER region (the block's own ghost policy th...
void applySweepCut(int d, double dth)
eps_i C_i += (F_- - F_+) + c_i (o_+ a_+ - o_- a_-), then divide by eps_eff.
SField faceFlux() const
The geometric LIQUID volume flux of the sweep computeFluxes last ran, stored at the face's - side (so...
void wettingCensusGhost(long counts[kVofWetCount], double &meanAppDeg, long &nApp, int ghost) const
long index(int x, int y, int z) const
Linear index of the inner-block cell (x,y,z), 0-based within the inner region.
void enforceCfl(double cfl, double dt)
Record cfl as this step's Courant number and enforce the Weymouth-Yue cap – the one guard advect(),...
double wettingPureEps
A donor with C <= wettingPureEps or >= 1 - wettingPureEps is pure phase: the band gets the plain cont...
SField planeM(int d) const
PLIC normal component d of the current reconstruction (valid on the inner region grown by 1,...
bool interfaceLocalCfl
Take the CFL over INTERFACE-ADJACENT cells only (mixed cells and their face neighbours) instead of ov...
double cflLimit
Abort threshold on max |uf| dt / h.
void solidBandFillPassWetting()
Pass 1 of the band fill with the theta-consistent rule (WO-S).
void init(int nx, int ny, int nz, double h, int ghost=3)
void wettingCensus(long counts[kVofWetCount], double &meanAppDeg, long &nApp) const
Band-fill branch census over the INNER region (VofWettingBranch counts) and the mean APPARENT contact...
double checkCourant(double dt)
The interface-local (or global) Courant number for this dt, all-reduced, with the Weymouth-Yue bounde...
bool useWorklist
Compaction of the reconstruction pass onto the mixed cells.
SField wallSdf() const
The SDF on this block, in cells, > 0 in fluid (the solver embeds + exchanges it).
SField faceOpenness(int d) const
Face openness of the +d face of each cell (the advector's own high-face convention — the SAME convent...
bool bcFaceOwned(int f) const
The WO-R boundary ledger, for the batched step: whether face f is a global domain face this block own...
SField planeAlpha() const
PLIC plane offset of the current reconstruction.
void computeFluxesImpl(int d, double dth)
double wispEps
WO-R2 item 4 — the wisp tolerance on the mixed-cell predicate (wyIsMixed(c, wispEps)),...
void clipCutCells()
Rule 3: clip C into [0,1] in CUT cells only (eps_eff < 1), accumulating the liquid volume the clip mo...
std::function< double(double)> globalMax
All-reduce max across ranks; identity when unset (single block).
void solidBandFill()
The three-pass neutral (90 deg) solid-band fill of cutcell.hpp.
double wettingTangentEps
Below |sin(theta_apparent)| = wettingTangentEps the interface is parallel to the wall and no rotation...
void syncGhosts()
Bring the colour ghosts up to date.
long clampedFaces() const
Faces the clamp had to bind, last advect().
void addT_(double &acc, double t0)
std::function< void(SField)> exchange
Refresh EVERY ghost layer of the given cell field (MPI exchange + domain BC fill)....
SField kindDouble() const
The cell classification as a DOUBLE (1 = solid), so it can ride the block's own halo exchange.
void setOutsideMask(UCField m)
Install the OUT-OF-DOMAIN mask (vof/colour_bc.hppbuildOutsideMask): 1 on every ghost cell outside the...
void finalizeGeometry()
kindDouble() (ghost-exchanged) -> the unsigned-char classification every kernel reads.
void disableGeometry()
Drop the geometry (back to the uncut kernels, byte-identically).
void advect(double dt, long step)
One Weymouth-Yue step: freeze the dilation flag, then three sweeps in the permutation selected by ste...
void reconstruct()
PLIC over the inner region grown by one cell in every direction: the donor of a face of an inner cell...
void computeFluxes(int d, double dth)
One flux per d-face touched by an inner cell, stored at the cell on the face's - side.
void addBcFaceVolume(int f, double v)
void setBcFaceOwned(int f, bool on)
Which of the six BLOCK boundary faces are GLOBAL domain faces this block owns.
void solidBandFillPass(int k)
One pass of the neutral solid-band fill.
void timedExchange(F &&fn)
Run fn (a ghost exchange) under the exchange accumulator.
VofMetric metric
Which VofWettingPivot anchors the theta-plane (a measured ablation; see wetting.hpp).
SField epsFraction() const
Fluid volume fraction of each cell (buildCellFraction, a multiple of 1/64).
void noteBatchedStep()
Bookkeeping of a step the block container ran on this advector's views in its batched launches (VofBl...
void computeFluxesCut(int d, double dth)
F_f = o_f * wyFaceFlux(a_f, ...).
Diagnostics diagnostics() const
Local census over the inner region.
const Timing & timing() const
SField faceVel(int d) const
SField faceU() const
Face velocity in direction d at the +d face of each cell: uf(i,j,k) sits at (i+1/2,...
void enableGeometry()
Allocate the geometry views. Idempotent; does not classify.
double maxCourantInterfaceCut(double dth) const
Rule 2: the interface-local Courant number in cut cells is o_f |a_f| / max(eps_i, 0....
flow — thin shim: the container-free VoF cut-cell colour-transport rules (rung V5a) now live in core.
flow — portable (Kokkos) MAC stencil operators: Red-Black Gauss-Seidel smoothers + divergence.
Kokkos::View< long *, SMem > LField
Definition advect_wy.hpp:90
double wyFaceFluxBc(double a, long p, long sd, int dir, const SF &c, const SF &mx, const SF &my, const SF &mz, const SF &alpha, const MF &outside, double eps=0.0)
WO-R sibling of wyFaceFlux for a face whose DONOR lies outside the global domain.
constexpr int kWySweepPerm[6][3]
The six permutations of (x, y, z), cycled by step index so no direction is systematically favoured (p...
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.
bool wyColourJump(double a, double b, double eps)
The interface-BAND jump test of maxCourantInterface: two neighbouring cells carry a DIFFERENT colour.
bool wyIsMixed(double c)
The mixed-cell predicate.
Definition advect_wy.hpp:95
double wyFaceFlux(double a, long p, long sd, int dir, const SF &c, const SF &mx, const SF &my, const SF &mz, const SF &alpha, double eps=0.0)
Signed Eulerian donor-cell flux through the dir-face between cell p and cell p + sd,...
Kokkos::View< unsigned char *, SMem > UCField
Definition advect_wy.hpp:89
long L3(int x, int y, int z, I3 e)
Kokkos::DefaultExecutionSpace SExec
Kokkos::View< double *, SMem > SField
flow — thin shim: the container-free VoF PLIC geometric toolbox (rung V0) now live in core.
flow — the multidimensional iteration policies: iteration order follows storage.
long solidCells
inner cells classified SOLID
double maxC
max C over inner cells (may be > 1)
double solidFillSum
Sum of C over inner SOLID cells ON THE WORKING BLOCK, i.e.
long clampedFaces
faces Weymouth's admissible interval had to clamp, last step
double rawVolume
sum of eps*C (raw buildCellFraction eps) over the same cells; differs from volume only on eps == 0 ce...
double minCFluid
min C over inner fluid cells with eps == 1 (uncut fluid)
double clippedSigned
the same, signed (created positive, destroyed negative)
double clippedVolume
|liquid volume| the cut-cell clip moved during the last step
long cutCells
inner cells with 0 < eps_eff < 1
double sumC
sum of C over inner cells (cell-volume units, not scaled by h^3)
double maxCFluid
max C over the same set
long wisps
cells with 0 < C < 1e-8 or 1-1e-8 < C < 1
double minC
min C over inner cells (may be < 0: no clipping at this rung)
double solidSumC
filled by the SOLVER from the canonical G=2 colour field
double volume
sum of eps_eff*C over inner FLUID cells: the EXACTLY conserved functional of the cut-cell scheme (cut...
double reconstruct
the MYC normal + plicAlpha pass (once per sweep)
double exchange
the g = 3 ghost exchange (once per sweep, plus the sibling fields of the momentum/energy advectors)
long sweeps
sweeps counted (3 per advect() call)
double sweep
the colour/momentum update pass (once per sweep)
double freeze
the frozen dilation flag (once per step)
double clip
the cut-cell [0,1] clip + census (once per sweep, V5a only)
double fluxes
the per-face geometric flux pass (once per sweep)
static void cg(CCField x, CCConst b, OpV AC, OpV AW, OpV AE, OpV AS, OpV AN, OpV AB, OpV AT, C3 e, Exch &&exchange, const Reduce &red, long gCells, int iters)
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)
flow — thin shim: the container-free VoF theta-consistent solid-band fill (rung V5b) now live in core...