34#include <Kokkos_Core.hpp>
60 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
65double order(
double coarse,
double fine) {
66 return (fine > 0.0 && coarse > 0.0) ? std::log2(coarse / fine) : 0.0;
75 void setup(
int nx,
int ny,
int nz,
double h) {
76 adv.init(nx, ny, nz, h, 3);
77 curv.init(nx, ny, nz, 3);
79 const I3
e = adv.extent();
80 const int g = adv.ghost();
83 if (
const char* v = std::getenv(
"PECLET_VOF_CURV_PTW"))
84 curv.ptWeightWidth = std::atof(v);
85 if (
const char* v = std::getenv(
"PECLET_VOF_CURV_FIT"))
86 curv.useMixedHeightFit = (std::atoi(v) != 0);
88 SField c()
const {
return adv.colour(); }
89 I3
e()
const {
return adv.extent(); }
90 I3
n()
const {
return adv.inner(); }
91 int g()
const {
return adv.ghost(); }
105Err curvError(
const Case& cs,
double kExact,
int margin = 0) {
106 const I3
e =
cs.e(),
n =
cs.n();
107 const int g =
cs.g() + margin;
108 SField kap =
cs.curv.kappa(), br =
cs.curv.branch();
110 const double den = (kExact != 0.0) ? std::fabs(kExact) : 1.0;
111 double sum = 0.0,
mx = 0.0;
112 long cnt = 0, nan = 0;
113 Kokkos::parallel_reduce(
117 {
g +
n.x - 2 * margin,
g +
n.y - 2 * margin,
g +
n.z - 2 * margin}),
118 KOKKOS_LAMBDA(
int x,
int y,
int z,
double& acc,
double& m,
long& c,
long& bad) {
119 const long i =
L3(x, y, z, e);
120 if (
static_cast<int>(br(i)) == vf::kCurvNone)
122 const double k = kap(i);
127 const double r = Kokkos::fabs(k - kExact) /
den;
129 m = Kokkos::fmax(m, r);
132 sum, Kokkos::Max<double>(mx), cnt, nan);
137 r.l1 = cnt ? sum /
static_cast<double>(cnt) : 0.0;
142void printStats(
const char* tag,
const VofCurvature::Stats& s) {
143 const double t =
s.interfacial ?
static_cast<double>(
s.interfacial) : 1.0;
145 " %s interfacial %5ld | HF %5.2f%% HFdir %5.2f%% HFfit %5.2f%% PV %5.2f%%"
146 " PVred %5.3f%% none %ld clipped %ld\n",
147 tag,
s.interfacial, 100.0 *
s.hf / t, 100.0 *
s.hfMixed / t, 100.0 *
s.hfFit / t,
148 100.0 *
s.pv / t, 100.0 *
s.pvReduced / t,
s.noEstimate,
s.clipped);
153 std::printf(
"\n=== A geometry primitives (polygon, area/centroid, Green monomials)\n");
160 s = 1664525u *
s + 1013904223u;
161 return static_cast<double>(
s >> 8) / 16777216.0;
163 double maxA = 0.0, maxC = 0.0;
165 for (
int t = 0;
t < 4000; ++
t) {
166 double m[3] = {2.0 * rnd() - 1.0, 2.0 * rnd() - 1.0, 2.0 * rnd() - 1.0};
167 const double l1 = std::fabs(m[0]) + std::fabs(m[1]) + std::fabs(m[2]);
170 for (
int k = 0;
k < 3; ++
k)
172 const double V = 0.05 + 0.9 * rnd();
173 const double al = vf::plicAlpha(m[0], m[1], m[2], V);
174 double v[8][3], ctr[3], area;
175 const int nv = vf::plicPolygon(m[0], m[1], m[2], al, v);
178 vf::polygonAreaCentroid(v, nv, ctr, area);
179 const double d = 1
e-6;
181 (vf::plicVolume(m[0], m[1], m[2], al + d) - vf::plicVolume(m[0], m[1], m[2], al - d)) /
183 const double l2 = std::sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
184 maxA = std::max(maxA, std::fabs(area - l2 * dVda) / std::max(area, 1e-3));
185 for (
int k = 0;
k < 3; ++
k) {
186 double mp[3] = {
m[0],
m[1],
m[2]}, mm[3] = {
m[0],
m[1],
m[2]};
190 (vf::plicVolume(mp[0], mp[1], mp[2], al) - vf::plicVolume(mm[0], mm[1], mm[2], al)) /
192 maxC = std::max(maxC, std::fabs(ctr[k] - (-dVdm / dVda)));
196 std::printf(
" %d random planes: max rel |Gamma| error %.3e max centroid error %.3e\n", nsamp,
204 double xy[8][2] = {{0, 0}, {1, 0}, {1, 1}, {0, 1}};
206 vf::polygonMoments2d(xy, 4, q);
207 const double ex[6] = {1.0, 0.5, 0.5, 1.0 / 3.0, 0.25, 1.0 / 3.0};
209 for (
int i = 0;
i < 6; ++
i)
210 m = std::max(m, std::fabs(q[i] - ex[i]));
211 std::printf(
" unit square monomials (1,x,y,x2,xy,y2) = (%g,%g,%g,%g,%g,%g) max err %.3e\n",
212 q[0], q[1], q[2], q[3], q[4], q[5], m);
219 for (
int k = 0;
k < nv; ++
k) {
220 const double a = 2.0 * M_PI *
k / nv + 0.37;
221 xy[
k][0] = 1.3 + 0.9 * std::cos(a);
222 xy[
k][1] = -0.6 + 1.4 * std::sin(a);
225 vf::polygonMoments2d(xy, nv, q);
227 double lo[2] = {1e30, 1e30}, hi[2] = {-1e30, -1e30};
228 for (
int k = 0;
k < nv; ++
k)
229 for (
int d = 0;
d < 2; ++
d) {
230 lo[
d] = std::min(lo[d], xy[k][d]);
231 hi[
d] = std::max(hi[d], xy[k][d]);
234 const double dx = (hi[0] -
lo[0]) / NQ, dy = (hi[1] - lo[1]) / NQ, dA =
dx *
dy;
235 double o[6] = {0, 0, 0, 0, 0, 0};
236 for (
int j = 0;
j < NQ; ++
j)
237 for (
int i = 0;
i < NQ; ++
i) {
238 const double x =
lo[0] + (
i + 0.5) * dx, y = lo[1] + (j + 0.5) *
dy;
240 for (
int k = 0;
k < nv && in; ++
k) {
241 const int k1 = (
k + 1) % nv;
243 (xy[
k1][0] - xy[
k][0]) * (y - xy[k][1]) - (xy[
k1][1] - xy[
k][1]) * (x - xy[k][0]);
257 for (
int i = 0;
i < 6; ++
i)
258 m = std::max(m, std::fabs(q[i] - o[i]) / std::max(std::fabs(o[i]), 1e-3));
259 std::printf(
" random pentagon vs %dx%d quadrature: max rel err %.3e\n", NQ, NQ, m);
266 std::printf(
"\n=== B2 shape sanity (plane -> 0, cylinder -> 1/R, i.e. kappa = 2H)\n");
268 const double h = 1.0 /
N;
273 cs.setup(
N,
N,
N, h);
274 double m[3] = {0.47, -0.31, 0.86};
275 const double l1 = std::fabs(m[0]) + std::fabs(m[1]) + std::fabs(m[2]);
276 for (
int k = 0;
k < 3; ++
k)
278 const double m0 =
m[0],
m1 =
m[1],
m2 =
m[2];
281 cs.blk, KOKKOS_LAMBDA(
long i,
int gx,
int gy,
int gz) {
283 vf::planeCellFraction(m0, m1, m2, 0.5 * (m0 + m1 + m2), gx * h, gy * h, gz * h, h);
286 const auto st =
cs.curv.compute(
cs.c());
290 const Err er = curvError(cs, 0.0, 8);
291 printStats(
"plane ", st);
292 std::printf(
" kappa (interior only): L1 %.3e max %.3e (analytic 0) cells %ld NaN %ld\n",
293 er.l1, er.max, er.n, er.nan);
296 CHECK(er.max < 1e-9);
302 cs.setup(
N,
N,
N, h);
303 const double R = 0.3, cx = 0.5, cy = 0.5;
307 cs.blk, KOKKOS_LAMBDA(
long i,
int gx,
int gy,
int) {
308 const double x0 = gx *
h, y0 = gy *
h,
w =
h /
NS;
310 for (
int q = 0;
q <
NS; ++
q) {
311 const double y = y0 + (
q + 0.5) * w - cy;
312 for (
int p = 0;
p <
NS; ++
p) {
313 const double x =
x0 + (
p + 0.5) * w - cx;
314 if (x * x + y * y < R * R)
318 cc(i) =
static_cast<double>(in) / (
static_cast<double>(NS) *
NS);
321 const auto st =
cs.curv.compute(
cs.c());
322 const double kEx =
h /
R;
323 const Err er = curvError(cs, kEx);
324 printStats(
"cylinder", st);
325 std::printf(
" kappa: L1 %.3e max %.3e (analytic %.6f = 1/R) NaN %ld\n", er.l1, er.max,
336void gateStaticSphere() {
337 std::printf(
"\n=== B exact-fraction sphere: static convergence of kappa vs 2/R\n");
338 const int Ns[3] = {16, 32, 64};
341 for (
int q = 0;
q < 3; ++
q) {
343 const double h = 1.0 /
N;
344 const double R = 0.3;
346 cs.setup(
N,
N,
N, h);
349 const auto st =
cs.curv.compute(
cs.c());
350 const double kEx = 2.0 *
h /
R;
351 const Err er = curvError(cs, kEx);
354 std::printf(
" N = %3d D/dx = %5.1f C*dx = %.4f\n",
N, 2.0 * R / h, h / R);
356 std::printf(
" kappa rel error: L1 %.4e max %.4e NaN %ld\n", er.l1, er.max, er.nan);
360 std::printf(
" convergence order L1: %.2f, %.2f max: %.2f, %.2f\n",
order(l1[0], l1[1]),
362 const double oL1 =
order(l1[0], l1[2]) / 2.0;
363 std::printf(
" fitted order over 16->64: L1 %.2f max %.2f\n", oL1,
order(mx[0], mx[2]) / 2.0);
369 const int levs[3] = {4, 6, 8};
370 for (
int q = 0;
q < 3; ++
q) {
372 const double h = 1.0 /
N,
R = 0.3;
374 cs.setup(
N,
N,
N, h);
377 cs.curv.compute(
cs.c());
378 abl[
q] = curvError(cs, 2.0 * h / R).l1;
380 std::printf(
" init ablation at 32^3, octree levels 4/6/8: L1 %.4e / %.4e / %.4e\n", abl[0],
382 CHECK(std::fabs(abl[1] - abl[2]) < 0.2 * abl[2]);
387 std::printf(
"\n=== C sphere sweep incl. D/dx < 5 (the fallback MUST engage)\n");
388 const double Rc[6] = {1.4, 2.2, 3.5, 6.0, 12.0, 24.0};
389 for (
int q = 0;
q < 6; ++
q) {
390 const double Rcell = Rc[
q];
391 const int N =
static_cast<int>(std::ceil(2.6 * Rcell)) + 8;
392 const double h = 1.0 /
N,
R = Rcell *
h;
394 cs.setup(
N,
N,
N, h);
399 const auto st =
cs.curv.compute(
cs.c());
400 const Err er = curvError(cs, 2.0 * h / R);
401 std::printf(
" D/dx = %5.1f C*dx = %.4f N = %3d\n", 2.0 * Rcell, 1.0 / Rcell,
N);
403 std::printf(
" kappa rel error: L1 %.4e max %.4e NaN %ld\n", er.l1, er.max, er.nan);
408 if (2.0 * Rcell < 5.0)
415void gateTranslating() {
416 std::printf(
"\n=== D translating droplet (advection-realistic fractions) -- a MEASUREMENT\n");
418 " the plateau is the method's physics (Han/Evrard/Desjardins 2024 3; "
420 const int Ns[3] = {16, 32, 64};
421 double l1[3],
mx[3], l1e[3];
422 for (
int q = 0;
q < 3; ++
q) {
424 const double h = 1.0 /
N,
R = 0.2;
426 cs.setup(
N,
N,
N, h);
430 cs.curv.compute(
cs.c());
431 l1e[
q] = curvError(cs, 2.0 * h / R).l1;
433 const double dt = 0.25 *
h;
435 const long steps =
static_cast<long>(std::lround(2.0 * R / (0.25 * h)));
436 for (
long s = 0;
s <
steps; ++
s)
437 cs.adv.advect(dt, s);
438 const auto st =
cs.curv.compute(
cs.c());
439 const Err er = curvError(cs, 2.0 * h / R);
442 std::printf(
" N = %3d D/dx = %5.1f C*dx = %.4f after %ld steps (1 diameter)\n",
N,
443 2.0 * R / h, h / R, steps);
445 std::printf(
" kappa rel error: L1 %.4e (exact fractions %.4e) max %.4e NaN %ld\n",
446 er.l1, l1e[q], er.max, er.nan);
450 std::printf(
" ADVECTED order L1: %.2f, %.2f max: %.2f, %.2f\n",
order(l1[0], l1[1]),
452 std::printf(
" EXACT order L1: %.2f, %.2f\n",
order(l1e[0], l1e[1]),
order(l1e[1], l1e[2]));
453 std::printf(
" ratio advected/exact L1: %.1fx, %.1fx, %.1fx at C*dx %.3f, %.3f, %.3f\n",
454 l1[0] / l1e[0], l1[1] / l1e[1], l1[2] / l1e[2], 1.0 / (0.2 * Ns[0]),
455 1.0 / (0.2 * Ns[1]), 1.0 / (0.2 * Ns[2]));
466void gateDeviceHost() {
467 std::printf(
"\n=== E device kernel vs serial host loop (same backend, bitwise)\n");
471 const double h = 1.0 /
N,
R = 0.3;
473 cs.setup(
N,
N,
N, h);
476 cs.curv.useMixedHeightFit =
false;
479 cs.curv.compute(
cs.c());
482 const int g =
cs.g();
483 auto ch = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(),
cs.c());
484 auto kh = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(),
cs.curv.kappa());
485 auto bh = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(),
cs.curv.branch());
486 const long st[3] = {1,
e.x,
static_cast<long>(
e.x) *
e.y};
487 auto C = [&](
long i) {
return ch(i); };
489 long bitDiff = 0,
cmp = 0;
493 long oldDiff = 0, oldBranch = 0;
495 const int gr = vf::kPvHalf;
496 const vf::VofMetric gm =
cs.curv.metric;
497 const double dW =
cs.curv.weightWidth * gm.maxH();
499 std::vector<double> hm(3 * ch.extent(0), 0.0),
ha(ch.extent(0), 0.0);
500 std::vector<vf::PvMoments> hp(ch.extent(0));
501 for (
int z = g - gr;
z <
g +
N + gr; ++
z)
502 for (
int y = g - gr;
y <
g +
N + gr; ++
y)
503 for (
int x = g - gr;
x <
g +
N + gr; ++
x) {
504 const long i =
L3(x, y, z, e);
507 double stc[27],
m[3];
508 for (
int kk = -1; kk <= 1; ++kk)
509 for (
int jj = -1; jj <= 1; ++jj)
510 for (
int ii = -1; ii <= 1; ++ii)
511 stc[vf::plicSt(ii + 1, jj + 1, kk + 1)] =
C(i + ii * st[0] + jj * st[1] + kk * st[2]);
512 vf::mycNormal(stc, m);
514 hm[3 *
i + 1] =
m[1];
515 hm[3 *
i + 2] =
m[2];
516 ha[
i] = vf::plicAlpha(m[0], m[1], m[2],
C(i));
517 vf::pvMomentsBuild(m[0], m[1], m[2], ha[i], gm, hp[i]);
520 for (
int z = g;
z <
g +
N; ++
z)
521 for (
int y = g;
y <
g +
N; ++
y)
522 for (
int x = g;
x <
g +
N; ++
x) {
523 const long i =
L3(x, y, z, e);
525 int branch = vf::kCurvNone;
527 const double am[3] = {std::fabs(hm[3 * i]), std::fabs(hm[3 * i + 1]),
528 std::fabs(hm[3 * i + 2])};
529 int ord[3] = {0, 1, 2};
530 for (
int p = 1;
p < 3; ++
p)
531 for (
int qq = p; qq > 0 &&
am[ord[qq]] >
am[ord[qq - 1]]; --qq)
532 std::swap(ord[qq], ord[qq - 1]);
534 for (
int t = 0;
t < 3 &&
branch < 0; ++
t) {
535 const int d = ord[
t], d1 = (
d + 1) % 3, d2 = (d + 2) % 3;
539 for (
int qq = 0; qq < 3 &&
ok; ++qq)
540 for (
int p = 0;
p < 3 &&
ok; ++
p) {
541 const long base =
i + (
p - 1) * st[d1] + (qq - 1) *
st[d2];
542 double col[vf::kHfColumn];
543 for (
int k = 0;
k < vf::kHfColumn; ++
k)
544 col[k] =
C(base + (k - vf::kHfColumn / 2) *
st[
d]);
547 if (!vf::hfColumnHeight(col, vf::kHfColumn, hv, orient,
cs.curv.monoTol)) {
553 else if (orient != orient0) {
561 kappa = vf::hfPatchKappa(hh);
562 branch = (
t == 0) ? vf::kCurvHf :
vf::kCurvHfMixed;
567 double nn[3], t1[3], t2[3],
org[3];
568 if (!vf::pvFrameMoments(hp[i], gm, nn, t1, t2, org)) {
569 branch = vf::kCurvNoEstimate;
573 for (
int oz = -gr; oz <= gr; ++oz)
574 for (
int oy = -gr; oy <= gr; ++oy)
575 for (
int ox = -gr;
ox <= gr; ++
ox) {
576 const long j =
L3(x + ox, y + oy, z + oz, e);
579 const double off[3] = {
static_cast<double>(
ox),
static_cast<double>(oy),
580 static_cast<double>(oz)};
582 if (vf::pvTermMoments(t, hp[j], off, org, t1, t2, nn, dW,
cs.curv.cosMin, gm))
583 vf::pvFitAccum(fit, t);
587 if (!vf::pvFitSolve(fit, a, red)) {
588 branch = vf::kCurvNoEstimate;
590 kappa = vf::paraboloidKappa(a);
591 branch = red ? vf::kCurvPvReduced : vf::kCurvPv;
596 int bOld = vf::kCurvNoEstimate;
598 const double m0 = hm[3 *
i],
m1 = hm[3 *
i + 1],
m2 = hm[3 *
i + 2];
599 const double n2 = m0 * m0 +
m1 *
m1 +
m2 *
m2;
601 bOld = vf::kCurvNoEstimate;
603 const double invn = 1.0 / std::sqrt(n2);
604 const double nn[3] = {m0 * invn,
m1 * invn,
m2 * invn};
606 vf::curvFrame(nn, t1, t2);
607 double v[8][3], ctr[3], area;
608 const int nv = vf::plicPolygon(m0, m1, m2, ha[i], v);
609 vf::polygonAreaCentroid(v, nv, ctr, area);
610 const double org[3] = {ctr[0] - 0.5, ctr[1] - 0.5, ctr[2] - 0.5};
613 for (
int oz = -gr; oz <= gr; ++oz)
614 for (
int oy = -gr; oy <= gr; ++oy)
615 for (
int ox = -gr;
ox <= gr; ++
ox) {
616 const long j =
L3(x + ox, y + oy, z + oz, e);
619 const double off[3] = {
static_cast<double>(
ox),
static_cast<double>(oy),
620 static_cast<double>(oz)};
621 vf::pvFitAdd(fit, hm[3 * j], hm[3 * j + 1], hm[3 * j + 2], ha[j], off, org,
622 t1, t2, nn,
cs.curv.weightWidth,
cs.curv.cosMin);
626 if (!vf::pvFitSolve(fit, a, red)) {
627 bOld = vf::kCurvNoEstimate;
629 kOld = vf::paraboloidKappa(a);
630 bOld = red ? vf::kCurvPvReduced : vf::kCurvPv;
636 else if (kOld != kappa) {
638 oldMax = std::max(oldMax, std::fabs(kOld - kappa));
643 if (kh(i) != kappa) {
647 CHECK(
static_cast<int>(bh(i)) == branch);
649 std::printf(
" %ld cells compared: %ld bitwise identical, %ld differing (max |d| %.3e)\n",
cmp,
652 " INFO vs the pre-WO-3 Green's-theorem oracle: %ld kappa differing (max |d| %.3e), "
653 "%ld branch flips\n",
654 oldDiff, oldMax, oldBranch);
655 if (std::is_same<peclet::flow::SMem, Kokkos::HostSpace>::value) {
665void gateFallbackAlone() {
666 std::printf(
"\n=== F the PV fallback forced on EVERY interfacial cell (its own accuracy)\n");
667 const int Ns[3] = {16, 32, 64};
669 for (
int q = 0;
q < 3; ++
q) {
671 const double h = 1.0 /
N,
R = 0.3;
673 cs.setup(
N,
N,
N, h);
674 cs.curv.debugForceFallback =
true;
677 const auto st =
cs.curv.compute(
cs.c());
678 const Err er = curvError(cs, 2.0 * h / R);
680 std::printf(
" N = %3d C*dx = %.4f",
N, h / R);
681 std::printf(
" PV %ld PV-reduced %ld none %ld L1 %.4e max %.4e\n",
st.pv,
st.pvReduced,
682 st.noEstimate, er.l1, er.max);
685 CHECK(
st.hf == 0 &&
st.hfMixed == 0 &&
st.hfFit == 0);
687 std::printf(
" PV-only convergence order L1: %.2f, %.2f\n",
order(l1[0], l1[1]),
688 order(l1[1], l1[2]));
689 CHECK(l1[2] < l1[0]);
693void gateMixedHeightFitAblation() {
694 std::printf(
"\n=== G tier 2b (mixed height-position fit) ON vs OFF -- why it ships OFF\n");
695 const int Ns[3] = {16, 32, 64};
696 double l1[2][3],
mx[2][3];
697 for (
int mode = 0; mode < 2; ++mode)
698 for (
int q = 0;
q < 3; ++
q) {
700 const double h = 1.0 /
N,
R = 0.3;
702 cs.setup(
N,
N,
N, h);
703 cs.curv.useMixedHeightFit = (mode == 1);
706 cs.curv.compute(
cs.c());
707 const Err er = curvError(cs, 2.0 * h / R);
709 mx[mode][
q] = er.max;
711 for (
int mode = 0; mode < 2; ++mode)
713 " tier 2b %-3s L1 %.3e / %.3e / %.3e (order %.2f) max %.3e / %.3e / %.3e"
715 mode ?
"ON" :
"OFF", l1[mode][0], l1[mode][1], l1[mode][2],
716 order(l1[mode][0], l1[mode][2]) / 2.0,
mx[mode][0],
mx[mode][1],
mx[mode][2],
719 " the fit serves the cells tier 1 cannot; its data set is the columns that CLOSED,"
720 " a slope-selected\n and therefore asymmetric subset, so its bias is scale"
721 " invariant. The PV fallback's 5^3 PLIC\n polygons exist at every slope and are"
726 CHECK(mx[0][2] < 0.25 * mx[1][2]);
742double sphereFracAniso(
double cx,
double cy,
double cz,
double R,
double x0,
double y0,
double z0,
743 const double h[3],
int sub = 24) {
744 const double wx =
h[0] / sub, wy =
h[1] / sub;
746 for (
int b = 0;
b < sub; ++
b)
747 for (
int a = 0;
a < sub; ++
a) {
748 const double px =
x0 + (
a + 0.5) * wx, py = y0 + (b + 0.5) * wy;
749 const double r2 =
R *
R - (px - cx) * (px - cx) - (py - cy) * (py - cy);
752 const double hh = std::sqrt(r2);
753 const double lo = std::fmax(cz - hh, z0), hi = std::fmin(cz + hh, z0 + h[2]);
757 return acc / (sub * sub *
h[2]);
763void fillSphereAniso(Case& cs,
const double h[3],
double R,
const double ctr[3],
int sub = 24) {
765 const int g =
cs.g();
766 auto host = Kokkos::create_mirror_view(
cs.c());
767 for (
int z = 0;
z <
e.z; ++
z)
768 for (
int y = 0;
y <
e.y; ++
y)
769 for (
int x = 0;
x <
e.x; ++
x)
770 host(
L3(x, y, z, e)) = sphereFracAniso(ctr[0], ctr[1], ctr[2], R, (x - g) * h[0],
771 (y - g) * h[1], (z - g) * h[2], h, sub);
772 Kokkos::deep_copy(
cs.c(), host);
775void gateAnisotropic() {
776 std::printf(
"\n=== K2 (Phase 3) ANISOTROPIC cells: kappa in 1/hRef on a stretched grid\n");
786 double pmax[4] = {0, 0, 0, 0};
787 const double hs[4][3] = {{1.0, 2.0, 4.0}, {1.0, 1.0, 1.0}, {2.5, 1.0, 1.7}, {1.0, 1.0, 1.0}};
788 for (
int q = 0;
q < 4; ++
q) {
789 const double*
h = hs[
q];
792 cs.setup(
N,
N,
N, 1.0);
793 const vf::VofMetric
g{{
h[0],
h[1],
h[2]}};
799 double n[3] = {0.4243, -0.7071, 0.5657};
801 for (
int d = 0;
d < 3; ++
d)
802 n[d] *= hs[q - 1][d];
803 const double ctr[3] = {0.5 *
N *
h[0], 0.5 *
N *
h[1], 0.5 *
N *
h[2]};
804 const double d =
n[0] * ctr[0] +
n[1] * ctr[1] +
n[2] * ctr[2];
806 const int gg =
cs.g();
807 auto host = Kokkos::create_mirror_view(
cs.c());
808 for (
int z = 0;
z <
e.z; ++
z)
809 for (
int y = 0;
y <
e.y; ++
y)
810 for (
int x = 0;
x <
e.x; ++
x) {
811 const double x0 = (
x - gg) * h[0], y0 = (y - gg) *
h[1], z0 = (
z - gg) * h[2];
812 host(
L3(x, y, z, e)) = vf::planeCellFractionAniso(n[0], n[1], n[2], d, x0, y0, z0, g);
814 Kokkos::deep_copy(
cs.c(), host);
816 const auto st =
cs.curv.compute(
cs.c());
817 const Err er = curvError(cs, 0.0, 4);
818 std::printf(
" plane on h = (%.3g, %.3g, %.3g): max |kappa| %.3e L1 %.3e\n", h[0], h[1],
819 h[2], er.max, er.l1);
826 " plane: stretched vs its CUBIC index-equivalent control -> %.3e vs %.3e, "
828 pmax[0], pmax[1], pmax[2], pmax[3]);
831 CHECK(pmax[0] <= 4.0 * std::fmax(pmax[1], 1e-15));
832 CHECK(pmax[2] <= 4.0 * std::fmax(pmax[3], 1e-15));
837 const double h[3] = {1.0, 1.0, 2.0};
838 const int base[3] = {16, 32, 64};
840 for (
int q = 0;
q < 3; ++
q) {
842 const double R = 0.25 * nx *
h[0];
843 const double ctr[3] = {0.5 * nx *
h[0] + 0.13, 0.5 *
ny *
h[1] + 0.27,
844 0.5 * nz *
h[2] + 0.11};
846 cs.setup(nx, ny, nz, 1.0);
847 const vf::VofMetric
g{{
h[0],
h[1],
h[2]}};
850 fillSphereAniso(cs, h, R, ctr);
852 const auto st =
cs.curv.compute(
cs.c());
853 const Err er = curvError(cs, 2.0 / R);
856 std::printf(
" sphere h = (1,1,2) n = %dx%dx%d D/h_min = %.0f\n", nx, ny, nz, 2.0 * R);
858 std::printf(
" kappa rel error: L1 %.4e max %.4e NaN %ld\n", er.l1, er.max, er.nan);
862 const double oL1 =
order(l1[0], l1[2]) / 2.0, oMx =
order(mx[0], mx[2]) / 2.0;
863 std::printf(
" fitted order over 16->64: L1 %.2f max %.2f (cubic: 2.26 / 1.86)\n", oL1,
866 CHECK(mx[2] < mx[0]);
871 const double h[3] = {1.0, 1.0, 2.0};
872 for (
int axis = 0;
axis < 3;
axis += 2) {
874 const int nx = (
axis == 0) ? 16 :
N,
ny =
N, nz = (
axis == 2) ? 16 :
N / 2;
875 const double R = 8.0;
877 cs.setup(nx, ny, nz, 1.0);
878 const vf::VofMetric
g{{
h[0],
h[1],
h[2]}};
882 const int gg =
cs.g();
883 const int t1 = (
axis + 1) % 3, t2 = (axis + 2) % 3;
884 const int nn[3] = {nx,
ny, nz};
885 const double c1 = 0.5 * nn[t1] *
h[t1] + 0.13,
c2 = 0.5 * nn[t2] *
h[t2] + 0.27;
886 auto host = Kokkos::create_mirror_view(
cs.c());
888 for (
int z = 0;
z <
e.z; ++
z)
889 for (
int y = 0;
y <
e.y; ++
y)
890 for (
int x = 0;
x <
e.x; ++
x) {
891 const int gi[3] = {
x - gg,
y - gg,
z - gg};
892 const double a0 = gi[t1] *
h[t1], b0 = gi[t2] *
h[t2];
894 for (
int q = 0;
q < sub; ++
q)
895 for (
int pp = 0; pp < sub; ++pp) {
896 const double pa = a0 + (pp + 0.5) * h[t1] / sub;
897 const double pb = b0 + (
q + 0.5) * h[t2] / sub;
898 if ((pa - c1) * (pa -
c1) + (pb - c2) * (pb -
c2) < R * R)
901 host(
L3(x, y, z, e)) = (double)in / (sub * sub);
903 Kokkos::deep_copy(
cs.c(), host);
905 const auto st =
cs.curv.compute(
cs.c());
906 const Err er = curvError(cs, 1.0 / R);
907 std::printf(
" cylinder along axis %d (h_axis = %.3g): L1 %.3e max %.3e noEstimate %ld\n",
908 axis, h[axis], er.l1, er.max,
st.noEstimate);
919 const double h = 1.0 /
N,
R = 0.3;
920 double kap[2][2] = {{0, 0}, {0, 0}};
921 std::vector<double>
f[2];
922 for (
int q = 0;
q < 2; ++
q) {
924 cs.setup(
N,
N,
N, h);
926 cs.curv.metric = vf::VofMetric{{1.0, 1.0, 1.0}};
929 cs.curv.compute(
cs.c());
930 auto host = Kokkos::create_mirror_view(
cs.curv.kappa());
931 Kokkos::deep_copy(host,
cs.curv.kappa());
932 f[
q].assign(host.data(), host.data() + host.extent(0));
936 for (std::size_t i = 0;
i <
f[0].size(); ++
i)
937 if (!(f[0][i] == f[1][i]))
939 std::printf(
" RULE B: default vs explicit unit metric, differing kappa cells: %ld / %zu\n",
959void gatePurityAgreement() {
960 std::printf(
"\n=== H advector/cascade purity agreement (the silent-HF-collapse gate)\n");
961 const double WISP = 1
e-8;
962 const double DEFICIT = 5
e-9;
966 double col[vf::kHfColumn] = {1.0, 1.0, 1.0, 0.63, 0.0, 0.0, 0.0};
967 double hClean = 0.0, hDrift = 0.0;
968 int oClean = 0, oDrift = 0;
969 const bool okClean = vf::hfColumnHeight(col, vf::kHfColumn, hClean, oClean, 1e-6);
971 for (
int k = 0;
k < vf::kHfColumn; ++
k)
973 col[
k] = 1.0 - DEFICIT;
977 CHECK(!vf::hfColumnHeight(col, vf::kHfColumn, hj, oj, 1e-6));
979 const bool okDrift = vf::hfColumnHeight(col, vf::kHfColumn, hDrift, oDrift, 1e-6, WISP);
981 CHECK(oDrift == oClean);
982 std::printf(
" H1 column: clean h = %.15f, drifted h = %.15f, |dh| = %.3e (deficit %.1e)\n",
983 hClean, hDrift, std::fabs(hDrift - hClean), DEFICIT);
984 CHECK(std::fabs(hDrift - hClean) < 4.0 * DEFICIT);
990 const double h = 1.0 /
N,
R = 0.25;
991 vf::VofCurvature::Stats
st[2];
992 double kerr[2] = {0.0, 0.0};
993 for (
int q = 0;
q < 2; ++
q) {
995 cs.setup(
N,
N,
N, h);
998 auto hc = Kokkos::create_mirror_view(
cs.c());
999 Kokkos::deep_copy(hc,
cs.c());
1000 for (std::size_t i = 0;
i < hc.extent(0); ++
i)
1001 if (hc(i) >= 1.0 - 1e-12)
1002 hc(i) = 1.0 - DEFICIT;
1003 Kokkos::deep_copy(
cs.c(), hc);
1004 cs.adv.syncGhosts();
1005 cs.curv.interfaceEps = WISP;
1006 cs.curv.pureEps = (
q == 0) ? 0.0 : WISP;
1007 st[
q] =
cs.curv.compute(
cs.c());
1008 auto kh = Kokkos::create_mirror_view(
cs.curv.kappa());
1009 auto bh = Kokkos::create_mirror_view(
cs.curv.branch());
1010 Kokkos::deep_copy(kh,
cs.curv.kappa());
1011 Kokkos::deep_copy(bh,
cs.curv.branch());
1012 const double kExact = 2.0 / (
R /
h);
1015 for (std::size_t i = 0;
i < kh.extent(0); ++
i)
1017 acc += std::fabs(kh(i) - kExact) / kExact;
1020 kerr[
q] =
n ?
acc /
n : 0.0;
1022 const long hf0 =
st[0].hf +
st[0].hfMixed +
st[0].hfFit;
1023 const long hf1 =
st[1].hf +
st[1].hfMixed +
st[1].hfFit;
1025 " H2 sphere with bulk at 1 - %.0e: floor-only HF = %ld / %ld (%.1f %% fallback), "
1026 "L1 kappa err %.4f\n",
1027 DEFICIT, hf0, st[0].interfacial,
1028 100.0 *
double(st[0].pv + st[0].pvReduced) /
double(st[0].interfacial), kerr[0]);
1030 " shared HF = %ld / %ld (%.1f %% fallback), "
1031 "L1 kappa err %.4f\n",
1032 hf1, st[1].interfacial,
1033 100.0 *
double(st[1].pv + st[1].pvReduced) /
double(st[1].interfacial), kerr[1]);
1035 CHECK(hf1 > 4 * hf0);
1036 CHECK(st[1].noEstimate == 0);
1037 CHECK(kerr[1] < kerr[0]);
1049void gateClipLive() {
1050 std::printf(
"\n=== I curvature admissibility clip is live (a detached cell at C = 1e-6)\n");
1052 const double h = 1.0 /
N,
R = 6.0 *
h;
1053 const int px = 8, py = 17, pz = 16;
1054 std::vector<double> kv[2];
1055 long target[2] = {0, 0};
1056 vf::VofCurvature::Stats
st[2];
1057 for (
int q = 0;
q < 2; ++
q) {
1059 cs.setup(
N,
N,
N, h);
1061 auto hc = Kokkos::create_mirror_view(
cs.c());
1062 Kokkos::deep_copy(hc,
cs.c());
1063 const long it =
L3(px +
cs.g(), py +
cs.g(), pz +
cs.g(),
cs.e());
1064 CHECK(hc(it) == 0.0);
1066 Kokkos::deep_copy(
cs.c(), hc);
1067 cs.adv.syncGhosts();
1070 cs.curv.kappaMax = (
q == 0) ? 0.0 :
vf::VofBlockSet::blockCurvProto().kappaMax;
1071 st[
q] =
cs.curv.compute(
cs.c());
1072 printStats(q == 0 ?
"clip off" :
"clip on ",
st[
q]);
1073 auto kh = Kokkos::create_mirror_view(
cs.curv.kappa());
1074 Kokkos::deep_copy(kh,
cs.curv.kappa());
1075 kv[
q].assign(kh.data(), kh.data() + kh.extent(0));
1078 const double k0 = kv[0][target[0]],
k1 = kv[1][target[1]];
1079 long above = 0, differ = 0, differAbove = 0;
1080 for (std::size_t i = 0;
i < kv[0].size(); ++
i) {
1081 const bool big = std::fabs(kv[0][i]) > 1.0;
1082 above += big ? 1 : 0;
1083 if (!(kv[0][i] == kv[1][i])) {
1085 differAbove += big ? 1 : 0;
1089 " detached cell kappa: clip off %.4f, clip on %.4f; cells above 1/Delta without the "
1090 "clip %ld, cells changed by the clip %ld (all of them above: %s)\n",
1091 k0, k1, above, differ, differ == differAbove ?
"yes" :
"NO");
1092 CHECK(std::fabs(k0) > 1.0);
1093 CHECK(std::fabs(k1) <= 1.0);
1094 CHECK(st[0].clipped == 0);
1095 CHECK(st[1].clipped == above);
1096 CHECK(differ == differAbove);
1097 CHECK(st[1].clipped >= 1);
1102 Kokkos::initialize(argc, argv);
1104 std::printf(
"VoF rung V3 (WO-O) - HF curvature cascade + PV fallback, backend: %s\n",
1105 peclet::flow::SExec::name());
1112 gateFallbackAlone();
1113 gateMixedHeightFitAblation();
1115 gatePurityAgreement();
1117 std::printf(
"\n%s (%d failure%s)\n",
failures ?
"FAILED" :
"PASSED",
failures,
flow — VoF rung V1: Weymouth & Yue (2010) directionally-split, exactly volume-conserving geometric ad...
flow — VoF Part III rung W0: the per-bubble VoF BLOCK, a third container over the same L1 kernels (su...
Curvature of the colour field on an extended (inner + ghost) block.
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
flow — thin shim: the container-free VoF height-function / paraboloid curvature kernels (rung V3) now...
flow — VoF rung V3 (WO-O): the view-level driver for the curvature cascade.
bool wyIsMixed(double c)
The mixed-cell predicate.
long L3(int x, int y, int z, I3 e)
Kokkos::DefaultExecutionSpace SExec
Kokkos::View< double *, SMem > SField
fit(a, t, t0=40.0, t1=160.0)
void forEachExtended(Block b, F fn)
Visit every cell of the extended block with (local linear index, global cell index).
void periodicFill(SField f, I3 e, int g, bool px, bool py, bool pz)
Periodic wrap of a single-block extended field (serial reference path; the MPI path uses core's GridH...
void fillUniform(WyAdvector &a, Block b, double ux, double uy, double uz)
Uniform translation. Discrete divergence is EXACTLY zero (identical values differenced).
void initSphere(SField c, Block b, double h, double cx, double cy, double cz, double r, int levels=4)
Sphere of fluid (C = 1 inside), exact to ~5e-7 relative volume at R/h = 16 via the octree helper in p...
Block blockOf(const WyAdvector &a, I3 origin)
Where this block sits in the global grid: o is the global index of inner cell (0,0,...
static double maxDiff(const std::vector< double > &a, const std::vector< double > &b)