flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_wetting.cpp
Go to the documentation of this file.
1// VoF rung V5b (WO-S) — the theta-consistent solid-band fill, as a PURE KERNEL test.
2//
3// G0 THE FLAT-WALL LIMIT. A plane interface meeting a flat wall at angle theta, exact fractions
4// everywhere: the fill must reproduce the exact fractions of the CONTINUED plane in the solid
5// cells. That is the Afkhami & Bussmann (IJNMF 57:453, 2008) height-function contact-angle
6// boundary condition written as volume fractions, and it is an IDEMPOTENCE statement — the fill
7// is the identity on a configuration that already satisfies it, so theta is a fixed point of the
8// scheme. It is the gate that discriminates the pivot rules (see `vof/wetting.hpp`).
9//
10// G0a idempotence at theta in {30,60,90,120,150}, several interface orientations, DEFAULT pivot.
11// G0b the same with the two ablation pivots -> the measured error of each (no gate; a number).
12// G0c the rotation itself: m_theta . n_w == cos(theta) and the plane passes through p_f.
13// G0d the wetting limits: theta = 0 fills the band with liquid, theta = 180 empties it.
14// G0e the fluid-only Youngs normal recovered from exact fractions next to the wall -> the angle
15// error it injects (a number; it is the accuracy ceiling of the whole rung).
16#include <cmath>
17#include <cstdio>
18#include <cstdlib>
19#include <Kokkos_Core.hpp>
20#include <vector>
21
22#include "flow_ibm.hpp"
23#include "vof/plic.hpp"
24#include "vof/wetting.hpp"
25
26namespace {
27using namespace peclet::flow::vof;
28
29int failures = 0;
30#define CHECK(cond) \
31 do { \
32 if (!(cond)) { \
33 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
34 ++failures; \
35 } \
36 } while (0)
37
38const double kDeg = M_PI / 180.0;
39
40// The wall: n_w = +z, solid for z < zw. Cell (i,j,k) is the unit cube with corner (i,j,k).
41const double zw = 3.0;
42
43// A plane through the point `p` making angle `theta` with n_w, with its tangent direction
44// `t_hat` = (cos(psi), sin(psi), 0) in the wall plane.
45struct Plane {
46 double m[3], A;
47};
48Plane makePlane(double thetaDeg, double psiDeg, const double p[3]) {
49 Plane pl;
50 const double th = thetaDeg * kDeg, ps = psiDeg * kDeg;
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];
55 return pl;
56}
57double frac(const Plane& pl, int i, int j, int k) {
58 return planeCellFraction(pl.m[0], pl.m[1], pl.m[2], pl.A, i, j, k, 1.0);
59}
60
61// One (fluid cell f, solid cell s) pair of the fill, evaluated on the host with the SAME
62// container-free kernels the device fill calls.
63double fillOnce(const double mf[3], double cf, double thetaDeg, int pivot, const int f[3],
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;
67 double mth[3], alphaTh, ca;
68 const int br = vofWettingPlane(mf, cf, nw, std::cos(thetaDeg * kDeg), std::sin(thetaDeg * kDeg),
69 sdfF, pivot, 1e-6, mth, alphaTh, ca);
70 if (cosApp)
71 *cosApp = ca;
72 if (branch)
73 *branch = br;
74 const int ds[3] = {s[0] - f[0], s[1] - f[1], s[2] - f[2]};
76}
77
78// ------------------------------------------------------------------ G0a / G0b
79const char* kPivName[4] = {"volume (DEFAULT)", "interface p_f (Afkhami-Bussmann)",
80 "wall-normal (WO-S as written)", "contact-line"};
81
82void idempotence() {
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");
86 std::printf(
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)
93 for (int piv = 0; piv < 4; ++piv) {
94 const double p[3] = {3.35, 3.6, zw};
95 const Plane pl = makePlane(thetas[ti], psis[pi], p);
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]);
100 if (cf <= 1e-9 || cf >= 1.0 - 1e-9)
101 continue; // pure-phase donor: the fill takes the continuation branch
102 for (int ks = 0; ks <= 2; ++ks) {
103 const int s[3] = {f[0], f[1], ks};
104 const double got = fillOnce(pl.m, cf, thetas[ti], piv, f, s);
105 const double ref = frac(pl, s[0], s[1], s[2]);
106 e[piv] = std::fmax(e[piv], std::fabs(got - ref));
107 }
108 }
109 }
110 std::printf(" %5.0f | %17.3e | %18.3e | %19.3e | %20.3e\n", thetas[ti], e[0], e[1], e[2],
111 e[3]);
112 for (int piv = 0; piv < 4; ++piv)
113 worst[piv] = std::fmax(worst[piv], e[piv]);
114 }
115 for (int piv = 0; piv < 4; ++piv)
116 std::printf(" pivot %d (%s): worst %.3e\n", piv, kPivName[piv], worst[piv]);
117 CHECK(worst[0] <= 1e-12); // G0, the shipped default
118 CHECK(worst[1] <= 1e-12);
119 CHECK(worst[3] <= 1e-12);
120}
121
122// ------------------------------------------------------------------ G0c
123void rotation() {
124 double worstAng = 0.0, worstPivot = 0.0;
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};
128 const Plane pl = makePlane(thA, 37.0, p);
129 const int f[3] = {3, 3, 3};
130 const double cf = frac(pl, f[0], f[1], f[2]);
131 if (cf <= 1e-6 || cf >= 1.0 - 1e-6)
132 continue;
133 const double nw[3] = {0, 0, 1};
134 double mth[3], alphaTh, ca;
135 vofWettingPlane(pl.m, cf, nw, std::cos(thT * kDeg), std::sin(thT * kDeg), (f[2] + 0.5) - zw,
136 kVofPivotInterface, 1e-6, mth, alphaTh, ca);
137 worstAng = std::fmax(worstAng, std::fabs(mth[2] - std::cos(thT * kDeg)));
138 // the plane must pass through p_f
139 double mf[3] = {pl.m[0], pl.m[1], pl.m[2]};
140 plicNormalizeL1(mf);
141 double pf[3];
142 vofPlicCentroid(mf, plicAlpha(mf[0], mf[1], mf[2], cf), pf);
143 worstPivot = std::fmax(worstPivot,
144 std::fabs(mth[0] * pf[0] + mth[1] * pf[1] + mth[2] * pf[2] - alphaTh));
145 // the measured apparent angle is the input angle
146 worstAng = std::fmax(worstAng, std::fabs(std::acos(ca) / kDeg - thA) / 180.0);
147 }
148 std::printf(
149 "G0c rotation: max |m_theta . n_w - cos(theta)| %.3e ; max |plane(p_f) - alpha| %.3e\n",
151 CHECK(worstAng <= 1e-12);
152 CHECK(worstPivot <= 1e-12);
153}
154
155// ------------------------------------------------------------------ G0d
156void limits() {
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};
162 const double c0 = fillOnce(pl.m, cf, 0.0, kVofPivotInterface, f, s);
163 const double c180 = fillOnce(pl.m, cf, 180.0, kVofPivotInterface, f, s);
164 const double c30 = fillOnce(pl.m, cf, 30.0, kVofPivotInterface, f, s);
165 const double c150 = fillOnce(pl.m, cf, 150.0, kVofPivotInterface, f, s);
166 std::printf(
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",
169 cf, c0, c30, c150, c180);
170 CHECK(c0 == 1.0);
171 CHECK(c180 == 0.0);
172 CHECK(c30 > cf && c150 < cf); // wetting adds liquid to the band, non-wetting removes it
173}
174
175// ------------------------------------------------------------------ G0e
176// What the fluid-only Youngs estimator can and cannot measure next to a wall, and what that costs
177// the FILL. Two errors are reported per theta:
178// * the angle of the recovered normal to the wall -> unusable (the one-sided wall-normal
179// difference reads a saturating profile over half the distance);
180// * the AZIMUTH of its in-wall component -> what the construction actually uses;
181// followed by the end-to-end band error of the fill driven by the fluid-only normal instead of the
182// exact one, which is the accuracy of the rung as shipped.
183void fluidOnlyYoungs() {
184 std::printf(
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");
187 double worstAz = 0.0, worstFill = 0.0;
188 for (double th = 30.0; th <= 150.0; th += 30.0) {
189 double eAng = 0.0, eFull = 0.0, eAz = 0.0, eFill = 0.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};
193 const Plane pl = makePlane(th, psi, p);
194 const int f[3] = {3, 3, 3};
195 double c27[27];
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) {
200 const int q = plicSt(i, j, k);
201 const int cx = f[0] + i - 1, cy = f[1] + j - 1, cz = f[2] + k - 1;
202 c27[q] = frac(pl, cx, cy, cz);
203 fl[q] = (cz + 1 <= zw) ? 0u : 1u; // a cell entirely below the wall is solid
204 flAll[q] = 1u;
205 }
206 double m[3], mAll[3];
207 if (!youngsNormalFluidOnly(c27, fl, m))
208 continue;
210 const double mn = std::sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
211 const double an = std::sqrt(mAll[0] * mAll[0] + mAll[1] * mAll[1] + mAll[2] * mAll[2]);
212 eAng = std::fmax(
213 eAng, std::fabs(std::acos(std::fmax(-1.0, std::fmin(1.0, m[2] / mn))) / kDeg - th));
214 eFull = std::fmax(
215 eFull, std::fabs(std::acos(std::fmax(-1.0, std::fmin(1.0, mAll[2] / an))) / kDeg - th));
216 // azimuth of the in-wall component against the true one
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;
220 while (d > 180.0)
221 d -= 360.0;
222 while (d < -180.0)
223 d += 360.0;
224 eAz = std::fmax(eAz, std::fabs(d));
225 }
226 // end-to-end: the fill with the fluid-only normal vs the exact continued plane
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};
231 eFill = std::fmax(eFill, std::fabs(fillOnce(m, cf, th, kVofPivotVolume, f, sc) -
232 frac(pl, sc[0], sc[1], sc[2])));
233 }
234 }
235 std::printf(" %5.0f | %17.3f | %16.3f | %12.3f | %16.3e\n", th, eAng, eFull, eAz, eFill);
236 worstAz = std::fmax(worstAz, eAz);
237 worstFill = std::fmax(worstFill, eFill);
238 }
239 std::printf(" worst azimuth error %.3f deg ; worst end-to-end band error %.3e\n", worstAz,
240 worstFill);
241 CHECK(worstAz <= 3.0);
242 CHECK(worstFill <= 0.05);
243}
244
245// ============================================================ G1/G6: the solver-level gates
246//
247// G1 THE PRESCRIBED ANGLE IS A FIXED POINT of the whole scheme. A liquid spherical cap of the
248// right volume for the SET angle is placed on a flat SDF wall, surface tension is turned on
249// and the drop is relaxed: the equilibrium must still be that cap. This is the statement the
250// rung actually makes (the fill is exactly idempotent at theta, gate G0a, so the continuum
251// equilibrium is a fixed point of the discrete scheme too), and it is what the study script
252// `tests/study/vof_wetting.py` then extends with the from-90-degrees relaxation.
253//
254// G6 INERTNESS AND THE 90-DEGREE LIMIT. `set_contact_angle(90)` is NOT bit-identical to WO-Q's
255// neutral fill and cannot be: the neutral rule is the MEAN of the fluid face neighbours, the
256// theta rule is the fraction of a plane, and the two agree only where the interface is already
257// perpendicular to the wall. The measured difference is the number reported here; the real
258// inertness statement — no `set_contact_angle` call leaves every V5a number byte-identical —
259// is gated by diffing the `vof_cutcell` ctest against the same binary built at V5a.
260struct CapScene {
261 int nx = 40, nz = 32;
262 // A QUARTER-integer wall: genuinely CUT wall cells (eps = 3/4) whose tangential MAC faces
263 // are still OPEN. At exactly k + 1/2 those faces sit on the SDF zero level, buildOpenness
264 // closes them and the contact line cannot move at all — see the WO-S findings.
265 double zw = 4.25, R = 10.0, sigma = 1.0, mu = 0.5;
266 std::vector<double> sdf, colour;
267 // A SLIT (a second wall at nz - zw): the box is periodic, so a lone ramp SDF puts a spurious
268 // fluid/solid seam at the wrap plane three cells from the band.
269 void build(double thetaDeg) {
270 sdf.assign((std::size_t)nx * nx * nz, 0.0);
271 colour.assign((std::size_t)nx * nx * nz, 0.0);
272 // the cap of the same volume as a hemisphere of radius R, at contact angle theta
273 const double ct = std::cos(thetaDeg * kDeg);
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; // the cap sphere's centre
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));
284 int in = 0, tot = 0;
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;
290 if (pz < zw || pz > nz - zw)
291 continue;
292 ++tot;
293 const double dx = px - cx, dy = py - cy, dz = pz - zc;
294 if (dx * dx + dy * dy + dz * dz < Rc * Rc)
295 ++in;
296 }
297 colour[i] = tot ? (double)in / tot : 0.0;
298 }
299 }
300};
301
302// Relax the cap and return the apparent angle from the cap relations (h on the axis and the
303// conserved volume; reading the contact radius off the first fluid plane instead biases it, see
304// the study script).
305struct CapResult {
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;
309 int nx = 0, nz = 0;
310};
311CapResult relaxCap(double thetaSet, int steps, bool setAngle = true) {
312 CapScene sc;
313 sc.build(setAngle ? thetaSet : 90.0);
314 peclet::flow::IbmSolver s(sc.nx, sc.nx, sc.nz);
315 s.setRho(1.0);
316 s.setMu(sc.mu);
317 s.setSolid(sc.sdf, true);
318 s.enableVof();
319 s.setVof(sc.colour);
320 s.setSurfaceTension(sc.sigma);
321 if (setAngle)
322 s.setContactAngle(thetaSet);
323 const double dtc = 0.5 * s.capillaryDt();
324 double dt = dtc;
325 s.setDt(dt);
326 const double v0 = s.vofDiagnostics().volume;
327 CapResult r;
328 r.filled = s.getVofFilledColour();
329 for (int i = 0; i < steps; ++i) {
330 const double c = s.vofMaxCourant();
331 if (c > 0.15) {
332 dt = std::fmin(dtc, dt * 0.15 / c);
333 s.setDt(dt);
334 } else if (dt < dtc && c < 0.075) {
335 dt = std::fmin(dtc, 1.2 * dt);
336 s.setDt(dt);
337 }
338 s.step();
339 r.iters = std::max(r.iters, s.lastPressureIterations());
340 }
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;
349 r.h += cc[i] * eps[i];
350 }
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);
363 if (z >= z0)
364 r.ca = std::fmax(r.ca, sc.mu * av / sc.sigma);
365 }
366 }
367 r.kind = s.getVofGeometry(4);
368 r.eps = eps;
369 r.nx = sc.nx;
370 r.nz = sc.nz;
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();
377 return r;
378}
379
380void solverGates() {
381 const double thetas[3] = {60.0, 90.0, 120.0};
382 const int steps = std::getenv("PECLET_VOF_WETTING_LONG") ? 600 : 150;
383 std::printf(
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",
386 steps);
387 double worst = 0.0;
388 for (double th : thetas) {
389 const CapResult r = relaxCap(th, steps);
390 std::printf(
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);
396 CHECK(r.drift <= 1e-9);
397 worst = std::fmax(worst, std::fabs(r.theta - th));
398 }
399 std::printf(" G1 worst |theta - theta_set| = %.3f deg (gate 3.0)\n", worst);
400 CHECK(worst <= 3.0);
401
402 // G6: the 90-degree limit against WO-Q's neutral fill, on the SAME scene.
403 const CapResult a = relaxCap(90.0, 0, true), b = relaxCap(90.0, 0, false);
404 double dm = 0.0, dmReach = 0.0;
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];
408 };
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)
413 continue; // fluid cell: not part of the band
414 const double d = std::fabs(at(a.filled, x, y, z) - at(b.filled, x, y, z));
415 dm = std::fmax(dm, d);
416 // ... and restricted to the band cells the V3 cascade can actually reach (a fluid cell
417 // within the +/-3 the height-function columns and the MYC stencil span)
418 bool reach = false;
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;
423 if (px < 0 || py < 0 || pz < 0 || px >= NX || py >= NX || pz >= NZ)
424 continue;
425 if (at(a.kind, px, py, pz) < 0.5)
426 reach = true;
427 }
428 if (reach)
429 dmReach = std::fmax(dmReach, d);
430 }
431 const CapResult a1 = relaxCap(90.0, steps, true), b1 = relaxCap(90.0, steps, false);
432 std::printf(
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",
442 dm, dmReach, steps, a1.theta, b1.theta, a1.umaxAll, b1.umaxAll, a1.ca, b1.ca);
443 CHECK(std::fabs(a1.theta - b1.theta) <= 1.0);
444}
445
446// ============================================================ PHASE 3 GATE K3: anisotropic cells
447//
448// `flow/doc/anisotropic_vof.md` §6 (decision V4.1) and §11 K3. The claim: theta is a PHYSICAL
449// angle, so the rotation happens in the physical frame (both inputs are index-space gradients and
450// are pulled back through `H^-1`), and the plane goes back to the unit cube as `m_idx = H m_theta`.
451// The gate is the same IDEMPOTENCE statement as G0a — a plane already meeting the wall at theta
452// must be reproduced exactly — on stretched cells, with the wall normal along the FINE and along
453// the COARSE axis.
454//
455// Everything is in units of hRef: `g.h[a] = h_a/hRef >= 1`.
456void anisoIdempotence() {
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};
460 // wall normal along z: h = (1,1,2) puts the COARSE axis across the wall, (2,1.5,1) a fine one.
461 const double hs[3][3] = {{1.0, 1.0, 1.0}, {1.0, 1.0, 2.0}, {2.0, 1.5, 1.0}};
462 double worstAll = 0.0, worstAng = 0.0;
463 for (int q = 0; q < 3; ++q) {
464 const VofMetric g{{hs[q][0], hs[q][1], hs[q][2]}};
465 double worst = 0.0, wang = 0.0;
466 for (int ti = 0; ti < 5; ++ti)
467 for (int pi = 0; pi < 3; ++pi) {
468 const double th = thetas[ti] * kDeg, ps = psis[pi] * kDeg;
469 // The PHYSICAL plane at angle theta to the wall normal (+z), azimuth psi.
470 const double nP[3] = {std::sin(th) * std::cos(ps), std::sin(th) * std::sin(ps),
471 std::cos(th)};
472 // its INDEX normal, and a global offset through a point of the band
473 double mIdx[3];
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};
480 // exact fraction of the anisotropic cell f
481 const double cf = planeCellFractionAniso(nP[0], nP[1], nP[2], A, f[0] * g.h[0],
482 f[1] * g.h[1], f[2] * g.h[2], g);
483 if (cf <= 1e-9 || cf >= 1.0 - 1e-9)
484 continue;
485 // the wall normal as the fill sees it: an INDEX-space gradient of the SDF, i.e.
486 // H * n_w for the physical +z wall (the central difference of a linear sdf on the
487 // index lattice picks up h_a per axis).
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];
490 for (int piv = 0; piv < 4; ++piv) {
491 double mth[3], alphaTh, ca;
492 const int br = vofWettingPlane(mIdx, cf, nwIdx, std::cos(th), std::sin(th), sdfF, piv,
493 1e-6, mth, alphaTh, ca, g);
494 (void)br;
495 if (piv == 0)
496 wang = std::fmax(
497 wang,
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]};
502 const double got = vofWettingFraction(mth, alphaTh, ds);
503 const double ref = planeCellFractionAniso(nP[0], nP[1], nP[2], A, sc[0] * g.h[0],
504 sc[1] * g.h[1], sc[2] * g.h[2], g);
505 // pivots 0/1/3 are the idempotent family (G0a); pivot 2 is the measured ablation.
506 if (piv != 2)
507 worst = std::fmax(worst, std::fabs(got - ref));
508 }
509 }
510 }
511 }
512 std::printf(
513 " h = (%.3g, %.3g, %.3g): worst |C_fill - C_exact| %.3e apparent-angle error"
514 " %.3e deg\n",
515 hs[q][0], hs[q][1], hs[q][2], worst, wang);
516 CHECK(worst <= 1e-12);
517 CHECK(wang <= 1.0);
518 worstAll = std::fmax(worstAll, worst);
519 worstAng = std::fmax(worstAng, wang);
520 }
521 std::printf(" worst over all metrics: %.3e (gate 1e-12), angle %.3e deg (gate 1 deg)\n",
523
524 // RULE B: the unit-metric overload is the pre-Phase-3 signature and must be BITWISE equal to the
525 // explicit {1,1,1} call — the claim the isotropic battery rests on.
526 {
527 long diff = 0, tot = 0;
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)
531 for (int piv = 0; piv < 4; ++piv) {
532 const double p[3] = {3.35, 3.6, zw};
533 const Plane pl = makePlane(thetas[ti], psis[pi], p);
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]);
538 if (cf <= 1e-9 || cf >= 1.0 - 1e-9)
539 continue;
540 const double nw[3] = {0.0, 0.0, 1.0};
541 const double sdfF = (f[2] + 0.5) - zw;
542 double m1[3], a1, c1, m2[3], a2, c2;
543 vofWettingPlane(pl.m, cf, nw, std::cos(thetas[ti] * kDeg),
544 std::sin(thetas[ti] * kDeg), sdfF, piv, 1e-6, m1, a1, c1);
545 vofWettingPlane(pl.m, cf, nw, std::cos(thetas[ti] * kDeg),
546 std::sin(thetas[ti] * kDeg), sdfF, piv, 1e-6, m2, a2, c2, one);
547 ++tot;
548 if (!(m1[0] == m2[0] && m1[1] == m2[1] && m1[2] == m2[2] && a1 == a2 && c1 == c2))
549 ++diff;
550 }
551 }
552 std::printf(" RULE B: unit-metric overload vs explicit {1,1,1}: %ld/%ld differing\n", diff,
553 tot);
554 CHECK(diff == 0);
555 }
556}
557
558} // namespace
559
560int main(int argc, char** argv) {
561 std::setvbuf(stdout, nullptr, _IONBF, 0);
562 Kokkos::initialize(argc, argv);
563 {
564 idempotence();
565 rotation();
566 limits();
569 if (!std::getenv("PECLET_VOF_WETTING_KERNEL_ONLY"))
570 solverGates();
571 }
572 Kokkos::finalize();
573 if (failures)
574 std::fprintf(stderr, "%d check(s) FAILED\n", failures);
575 else
576 std::printf("all vof wetting (G0) checks passed\n");
577 return failures ? 1 : 0;
578}
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
float worst
Definition cmp.py:18
x
Definition cmp.py:24
y
Definition cmp.py:24
branch(c, f, a)
list iters
Definition prof.py:105
one(timed)
Definition prof.py:109
flow — thin shim: the container-free VoF PLIC geometric toolbox (rung V0) now live in core.
static constexpr int NX
static constexpr int NZ
#define CHECK(cond)
flow — thin shim: the container-free VoF theta-consistent solid-band fill (rung V5b) now live in core...