19#include <Kokkos_Core.hpp>
33 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
51 pl.m[0] = std::sin(th) * std::cos(
ps);
52 pl.m[1] = std::sin(th) * std::sin(
ps);
53 pl.m[2] = std::cos(th);
54 pl.A = pl.m[0] *
p[0] + pl.m[1] *
p[1] + pl.m[2] *
p[2];
57double frac(
const Plane& pl,
int i,
int j,
int k) {
64 const int s[3],
double*
cosApp =
nullptr,
int* branch =
nullptr) {
65 const double nw[3] = {0.0, 0.0, 1.0};
66 const double sdfF = (
f[2] + 0.5) - zw;
74 const int ds[3] = {
s[0] -
f[0],
s[1] -
f[1],
s[2] -
f[2]};
79const char*
kPivName[4] = {
"volume (DEFAULT)",
"interface p_f (Afkhami-Bussmann)",
80 "wall-normal (WO-S as written)",
"contact-line"};
83 const double thetas[5] = {30.0, 60.0, 90.0, 120.0, 150.0};
84 const double psis[3] = {0.0, 37.0, 90.0};
85 std::printf(
"G0a/G0b flat-wall idempotence (EXACT m_f): max |C_fill - C_exact| over the band\n");
87 " theta | pivot 0 volume | pivot 1 interface | pivot 2 wall-normal |"
88 " pivot 3 contact-line\n");
89 double worst[4] = {0.0, 0.0, 0.0, 0.0};
90 for (
int ti = 0;
ti < 5; ++
ti) {
91 double e[4] = {0.0, 0.0, 0.0, 0.0};
92 for (
int pi = 0;
pi < 3; ++
pi)
94 const double p[3] = {3.35, 3.6, zw};
96 for (
int dx = -1;
dx <= 1; ++
dx)
97 for (
int dy = -1;
dy <= 1; ++
dy) {
98 const int f[3] = {3 +
dx, 3 +
dy, 3};
99 const double cf =
frac(pl, f[0], f[1], f[2]);
102 for (
int ks = 0;
ks <= 2; ++
ks) {
103 const int s[3] = {
f[0],
f[1],
ks};
105 const double ref =
frac(pl, s[0], s[1], s[2]);
106 e[
piv] = std::fmax(e[
piv], std::fabs(
got - ref));
110 std::printf(
" %5.0f | %17.3e | %18.3e | %19.3e | %20.3e\n", thetas[
ti], e[0], e[1], e[2],
113 worst[
piv] = std::fmax(worst[
piv], e[
piv]);
117 CHECK(worst[0] <= 1e-12);
118 CHECK(worst[1] <= 1e-12);
119 CHECK(worst[3] <= 1e-12);
125 for (
double thA = 20.0;
thA <= 160.0;
thA += 20.0)
126 for (
double thT = 15.0;
thT <= 165.0;
thT += 15.0) {
127 const double p[3] = {3.35, 3.6, zw};
129 const int f[3] = {3, 3, 3};
130 const double cf =
frac(pl, f[0], f[1], f[2]);
133 const double nw[3] = {0, 0, 1};
139 double mf[3] = {pl.m[0], pl.m[1], pl.m[2]};
149 "G0c rotation: max |m_theta . n_w - cos(theta)| %.3e ; max |plane(p_f) - alpha| %.3e\n",
157 const double p[3] = {3.35, 3.6, zw};
158 const Plane pl =
makePlane(85.0, 0.0, p);
159 const int f[3] = {3, 3, 3};
160 const double cf =
frac(pl, f[0], f[1], f[2]);
161 const int s[3] = {3, 3, 2};
167 "G0d wetting limits (C_f = %.4f at theta_a = 85 deg): C_band = %.6f (theta 0), "
168 "%.6f (30), %.6f (150), %.6f (180)\n",
185 "G0e fluid-only Youngs next to a flat wall, from EXACT plane fractions:\n"
186 " theta | angle-to-wall err | full-stencil err | azimuth err | fill err (band)\n");
188 for (
double th = 30.0; th <= 150.0; th += 30.0) {
190 for (
double psi = 0.0; psi <= 90.0; psi += 45.0)
191 for (
double sh = 0.0;
sh < 1.0;
sh += 0.25) {
192 const double p[3] = {3.0 +
sh, 3.5, zw};
194 const int f[3] = {3, 3, 3};
196 unsigned char fl[27],
flAll[27];
197 for (
int k = 0;
k < 3; ++
k)
198 for (
int j = 0;
j < 3; ++
j)
199 for (
int i = 0;
i < 3; ++
i) {
201 const int cx =
f[0] +
i - 1, cy =
f[1] +
j - 1, cz =
f[2] +
k - 1;
203 fl[
q] = (cz + 1 <= zw) ? 0u : 1u;
206 double m[3],
mAll[3];
210 const double mn = std::sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
213 eAng, std::fabs(std::acos(std::fmax(-1.0, std::fmin(1.0, m[2] /
mn))) /
kDeg - th));
215 eFull, std::fabs(std::acos(std::fmax(-1.0, std::fmin(1.0,
mAll[2] /
an))) /
kDeg - th));
217 const double tx =
m[0],
ty =
m[1];
218 if (std::sqrt(
tx *
tx +
ty *
ty) > 1e-12) {
219 double d = std::atan2(
ty,
tx) /
kDeg - psi;
224 eAz = std::fmax(
eAz, std::fabs(d));
227 const double cf =
frac(pl, f[0], f[1], f[2]);
228 if (cf > 1e-9 && cf < 1.0 - 1e-9)
229 for (
int ks = 0;
ks <= 2; ++
ks) {
230 const int sc[3] = {
f[0],
f[1],
ks};
232 frac(pl, sc[0], sc[1], sc[2])));
235 std::printf(
" %5.0f | %17.3f | %16.3f | %12.3f | %16.3e\n", th,
eAng,
eFull,
eAz,
eFill);
239 std::printf(
" worst azimuth error %.3f deg ; worst end-to-end band error %.3e\n",
worstAz,
261 int nx = 40, nz = 32;
265 double zw = 4.25,
R = 10.0, sigma = 1.0, mu = 0.5;
266 std::vector<double>
sdf, colour;
270 sdf.assign((std::size_t)nx * nx * nz, 0.0);
271 colour.assign((std::size_t)nx * nx * nz, 0.0);
274 const double V = 2.0 / 3.0 *
M_PI *
R *
R *
R;
275 const double Rc = std::cbrt(3.0 * V / (
M_PI * (1.0 -
ct) * (1.0 -
ct) * (2.0 +
ct)));
276 const double zc = zw -
Rc *
ct;
277 const double cx = nx * 0.5, cy = nx * 0.5;
278 for (
int z = 0;
z < nz; ++
z)
279 for (
int y = 0;
y < nx; ++
y)
280 for (
int x = 0;
x < nx; ++
x) {
281 const std::size_t
i =
282 (std::size_t)x + (std::size_t)
y * nx + (std::size_t)z * (std::size_t)nx * nx;
283 sdf[
i] = std::fmin((z + 0.5) - zw, (nz - zw) - (z + 0.5));
285 for (
int a = 0;
a < 4; ++
a)
286 for (
int b = 0;
b < 4; ++
b)
287 for (
int c = 0;
c < 4; ++
c) {
288 const double px =
x + (
a + 0.5) / 4.0, py = y + (b + 0.5) / 4.0,
289 pz =
z + (
c + 0.5) / 4.0;
293 const double dx = px - cx,
dy = py - cy,
dz = pz -
zc;
294 if (dx * dx + dy * dy + dz * dz <
Rc *
Rc)
306 double theta = 0,
h = 0,
a = 0, drift = 0,
ca = 0, umaxAll = 0, apparent = 0;
307 long iters = 0, contact = 0, neighbour = 0, pure = 0, neutral = 0;
308 std::vector<double> filled, kind,
eps;
317 s.setSolid(
sc.sdf,
true);
320 s.setSurfaceTension(
sc.sigma);
323 const double dtc = 0.5 *
s.capillaryDt();
326 const double v0 =
s.vofDiagnostics().volume;
328 r.filled =
s.getVofFilledColour();
329 for (
int i = 0;
i <
steps; ++
i) {
330 const double c =
s.vofMaxCourant();
332 dt = std::fmin(
dtc, dt * 0.15 / c);
334 }
else if (dt <
dtc && c < 0.075) {
335 dt = std::fmin(
dtc, 1.2 * dt);
339 r.iters = std::max(
r.iters,
s.lastPressureIterations());
341 const auto d =
s.vofDiagnostics();
342 const auto cd =
s.contactAngleDiagnostics();
343 const auto cc =
s.getVof();
344 const auto eps =
s.getVofGeometry(0);
345 const int ix =
sc.nx / 2;
346 for (
int z = 0;
z <
sc.nz; ++
z) {
347 const std::size_t
i =
348 (std::size_t)
ix + (std::size_t)
ix *
sc.nx + (std::size_t)z * (std::size_t)
sc.nx *
sc.nx;
351 r.a = std::sqrt(std::fmax((6.0 *
d.volume / (
M_PI *
r.h) -
r.h *
r.h) / 3.0, 1e-12));
352 r.theta = 2.0 * std::atan2(
r.h,
r.a) /
kDeg;
353 r.drift = std::fabs(
d.volume -
v0) /
v0;
354 const int z0 = (
int)std::ceil(
sc.zw);
355 for (
int c = 0;
c < 3; ++
c) {
356 const auto v =
s.getVelocity(c);
357 for (
int z = 0;
z <
sc.nz; ++
z)
358 for (
int y = 0;
y <
sc.nx; ++
y)
359 for (
int x = 0;
x <
sc.nx; ++
x) {
360 const double av = std::fabs(v[(std::size_t)x + (std::size_t)y *
sc.nx +
361 (std::size_t)z * (std::size_t)
sc.nx *
sc.nx]);
362 r.umaxAll = std::fmax(
r.umaxAll,
av);
364 r.ca = std::fmax(
r.ca,
sc.mu *
av /
sc.sigma);
367 r.kind =
s.getVofGeometry(4);
371 r.apparent =
cd.meanApparentAngle;
372 r.contact =
cd.contactCells;
373 r.neighbour =
cd.neighbourCells;
374 r.pure =
cd.pureCells;
375 r.neutral =
cd.neutralCells;
376 r.filled =
s.getVofFilledColour();
381 const double thetas[3] = {60.0, 90.0, 120.0};
382 const int steps = std::getenv(
"PECLET_VOF_WETTING_LONG") ? 600 : 150;
384 "G1 the prescribed angle is a fixed point (cap of the SET angle on a flat SDF wall "
385 "at z = 4.25 — CUT wall cells, D/dx = 20, sigma 1, mu 0.5, %d steps):\n",
388 for (
double th :
thetas) {
391 " theta_set %5.1f -> %7.3f (err %+6.3f) h %6.3f a %6.3f apparent %6.2f "
392 "dV/V %.2e Ca(open) %.3e max|u| all %.3e band th/nbr/pure/neu "
393 "%ld/%ld/%ld/%ld iters %ld\n",
394 th,
r.theta,
r.theta - th,
r.h,
r.a,
r.apparent,
r.drift,
r.ca,
r.umaxAll,
r.contact,
395 r.neighbour,
r.pure,
r.neutral,
r.iters);
397 worst = std::fmax(worst, std::fabs(
r.theta - th));
399 std::printf(
" G1 worst |theta - theta_set| = %.3f deg (gate 3.0)\n", worst);
405 const int NX =
a.nx,
NZ =
a.nz;
406 const auto at = [&](
const std::vector<double>&
v,
int x,
int y,
int z) {
407 return v[(std::size_t)x + (std::size_t)
y *
NX + (std::size_t)z * (std::size_t)
NX *
NX];
409 for (
int z = 0;
z <
NZ; ++
z)
410 for (
int y = 0;
y <
NX; ++
y)
411 for (
int x = 0;
x <
NX; ++
x) {
412 if (
at(
a.kind, x, y, z) < 0.5)
414 const double d = std::fabs(
at(
a.filled, x, y, z) -
at(
b.filled, x, y, z));
415 dm = std::fmax(dm, d);
419 for (
int dz = -3;
dz <= 3 && !
reach; ++
dz)
420 for (
int dy = -3;
dy <= 3 && !
reach; ++
dy)
421 for (
int dx = -3;
dx <= 3 && !
reach; ++
dx) {
422 const int px =
x +
dx, py =
y +
dy, pz =
z +
dz;
425 if (
at(
a.kind, px, py, pz) < 0.5)
433 "G6 theta = 90 vs WO-Q's neutral fill on the same scene, at t = 0: "
434 "max |C_theta - C_neutral| over ALL solid cells %.3e, and over the solid cells the V3 "
435 "cascade can reach (a fluid cell within +/-3) %.3e. NOT bitwise, and it cannot be — "
436 "the neutral rule is the MEAN of the fluid face neighbours, the theta rule is a plane "
437 "FRACTION, and the two coincide only where the interface is already perpendicular to "
438 "the wall; the all-cells number is dominated by band cells deeper than the neutral "
439 "rule's 3-pass reach, which it leaves untouched and the theta walk fills.\n"
440 " after %d steps: theta %.3f (theta-fill) vs %.3f (neutral), max|u| %.3e vs %.3e, "
441 "Ca(open) %.3e vs %.3e\n",
443 CHECK(std::fabs(
a1.theta -
b1.theta) <= 1.0);
457 std::printf(
"\nK3 (Phase 3) anisotropic idempotence: max |C_fill - C_exact| over the band\n");
458 const double thetas[5] = {30.0, 60.0, 90.0, 120.0, 150.0};
459 const double psis[3] = {0.0, 37.0, 90.0};
461 const double hs[3][3] = {{1.0, 1.0, 1.0}, {1.0, 1.0, 2.0}, {2.0, 1.5, 1.0}};
463 for (
int q = 0;
q < 3; ++
q) {
464 const VofMetric
g{{
hs[
q][0],
hs[
q][1],
hs[
q][2]}};
466 for (
int ti = 0;
ti < 5; ++
ti)
467 for (
int pi = 0;
pi < 3; ++
pi) {
470 const double nP[3] = {std::sin(th) * std::cos(
ps), std::sin(th) * std::sin(
ps),
474 vofIndexNormal(
nP, g,
mIdx);
475 const double p[3] = {3.35 *
g.h[0], 3.6 *
g.h[1], zw *
g.h[2]};
476 const double A =
nP[0] *
p[0] +
nP[1] *
p[1] +
nP[2] *
p[2];
477 for (
int dx = -1;
dx <= 1; ++
dx)
478 for (
int dy = -1;
dy <= 1; ++
dy) {
479 const int f[3] = {3 +
dx, 3 +
dy, 3};
482 f[1] *
g.h[1], f[2] *
g.h[2], g);
488 const double nwIdx[3] = {0.0, 0.0,
g.h[2]};
489 const double sdfF = (
f[2] + 0.5) *
g.h[2] - zw *
g.h[2];
498 std::fabs(std::acos(ca < -1.0 ? -1.0 : (
ca > 1.0 ? 1.0 :
ca)) - th) /
kDeg);
499 for (
int ks = 0;
ks <= 2; ++
ks) {
500 const int sc[3] = {
f[0],
f[1],
ks};
501 const int ds[3] = {
sc[0] -
f[0],
sc[1] -
f[1],
sc[2] -
f[2]};
504 sc[1] *
g.h[1], sc[2] *
g.h[2], g);
507 worst = std::fmax(worst, std::fabs(
got - ref));
513 " h = (%.3g, %.3g, %.3g): worst |C_fill - C_exact| %.3e apparent-angle error"
516 CHECK(worst <= 1e-12);
521 std::printf(
" worst over all metrics: %.3e (gate 1e-12), angle %.3e deg (gate 1 deg)\n",
528 const VofMetric
one{{1.0, 1.0, 1.0}};
529 for (
int ti = 0;
ti < 5; ++
ti)
530 for (
int pi = 0;
pi < 3; ++
pi)
532 const double p[3] = {3.35, 3.6, zw};
534 for (
int dx = -1;
dx <= 1; ++
dx)
535 for (
int dy = -1;
dy <= 1; ++
dy) {
536 const int f[3] = {3 +
dx, 3 +
dy, 3};
537 const double cf =
frac(pl, f[0], f[1], f[2]);
540 const double nw[3] = {0.0, 0.0, 1.0};
541 const double sdfF = (
f[2] + 0.5) - zw;
546 std::sin(thetas[
ti] *
kDeg),
sdfF,
piv, 1e-6, m2, a2, c2, one);
548 if (!(m1[0] == m2[0] && m1[1] == m2[1] && m1[2] == m2[2] && a1 == a2 && c1 == c2))
552 std::printf(
" RULE B: unit-metric overload vs explicit {1,1,1}: %ld/%ld differing\n",
diff,
562 Kokkos::initialize(
argc, argv);
569 if (!std::getenv(
"PECLET_VOF_WETTING_KERNEL_ONLY"))
576 std::printf(
"all vof wetting (G0) checks passed\n");
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
flow — thin shim: the container-free VoF PLIC geometric toolbox (rung V0) now live in core.
flow — thin shim: the container-free VoF theta-consistent solid-band fill (rung V5b) now live in core...