flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_twophase.cpp
Go to the documentation of this file.
1// VoF rung V2a (WO-J) — the colour field wired into the Navier-Stokes solver: C -> LinearMix
2// closures -> rho(C)/mu(C) -> the variable-density projection, with C advected by the projected
3// face velocities.
4//
5// Gates, in the order they are run:
6//
7// A THE BRIDGE. The solver-driven advection must reproduce a STANDALONE `WyAdvector` driven by
8// the same PHYSICAL velocity field. This is the gate that pins the two blocks' index
9// conventions against each other: `flow` puts u(i) on the LOW (-x) face of cell i while
10// `WyAdvector` puts uf(i) on the HIGH (+x) face, so the bridge must shift by one cell along
11// each component's own axis. Omitting that shift is invisible in a uniform flow and invisible
12// in each axis' own discrete divergence, and it destroys conservation (measured: +35 % volume
13// on gate C's scene). The scene is the LeVeque deformation field, whose three components each
14// vary along all three axes — a uniform or a solid-body-rotation field would NOT detect the
15// axial half of the shift.
16//
17// B THE HYDROSTATIC ACID TEST, DRIVEN THROUGH C. A two-layer column at rest under gravity, the
18// density coming from a LinearMix closure on the colour field. Two halves:
19// B1 the pressure: dP/dz must equal -rho_f*g to machine precision, measured against the
20// ACTUAL colour field. This is the loud half — it fails on any inconsistency between the
21// closure's face density and the projection coefficient (the harmonic-rho_f ablation of
22// gate E turns it into a 30 % error).
23// B2 the velocity, with the interface HELD FIXED: max|u| must reproduce the hand-set-rho
24// reference (`test_vardensity_projection.cpp`) BITWISE. Holding the interface fixed is
25// not a weakening of the gate, it is what isolates the quantity the gate exists to
26// measure — see the long note at `hydrostaticC()`.
27//
28// C VOLUME CONSERVATION through the coupled step: a ratio-10 sphere in a sheared periodic box,
29// 1000 steps. The floor is the PROJECTION's own discrete divergence residual (WO-E finding 2),
30// so the test reports both.
31//
32// D C == const REDUCTIONS. Three separate statements, because "reduces to the single-phase
33// path" means different things depending on whether a rho closure is registered:
34// D1 a uniform C is EXACTLY stationary under the coupled advection (bitwise);
35// D2 VoF enabled with no closure is bitwise inert — u and P identical to VoF off;
36// D3 VoF + rho(C) with C == 1 is bitwise identical to the hand-set uniform-rho varRho path.
37// (A bitwise reduction to the CONSTANT-density solver is not attainable and never was: with
38// varRho on, the momentum RHS is a different kernel and the default pressure driver is
39// Chebyshev, not MG-PCG. That pre-existing reduction is measured at 2e-14 by
40// `test_vardensity_projection.cpp` and is not a VoF question.)
41//
42// E THE HARMONIC rho_f KNOB (WO-J item 5), measured: it must break the hydrostatic balance, and
43// loudly, because the momentum time term and the face body force keep the arithmetic mean.
44//
45// F THE INTERFACE-LOCAL CFL LIMITER: on a scene with a fast jet far from a quiescent interface,
46// the limiter must report the interface's Courant number, not the domain's.
47#include <cmath>
48#include <cstdio>
49#include <exception>
50#include <Kokkos_Core.hpp>
51#include <vector>
52
53#include "flow_ibm.hpp"
54#include "vof_advect_scenes.hpp"
55
56namespace {
61
62int failures = 0;
63#define CHECK(cond) \
64 do { \
65 if (!(cond)) { \
66 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
67 ++failures; \
68 } \
69 } while (0)
70
71std::size_t idx(int x, int y, int z, int nx, int ny) {
72 return (std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny;
73}
74double maxAbs(const std::vector<double>& v) {
75 double m = 0;
76 for (double x : v)
77 m = std::fmax(m, std::fabs(x));
78 return m;
79}
80double maxAbsDiff(const std::vector<double>& a, const std::vector<double>& b) {
81 // WO-R2: NaN-PROPAGATING. `std::fmax(m, NaN) == m`, so the obvious loop returns 0.000e+00 for a
82 // field that has gone entirely NaN and every bitwise gate built on it passes (WO-R found this on
83 // a drained open-boundary run). A non-finite difference must fail, so return it.
84 double m = 0;
85 for (std::size_t i = 0; i < a.size(); ++i) {
86 const double d = std::fabs(a[i] - b[i]);
87 if (!(d == d))
88 return d; // NaN
89 m = std::fmax(m, d);
90 }
91 return m;
92}
93double sum(const std::vector<double>& v) {
94 double s = 0;
95 for (double x : v)
96 s += x;
97 return s;
98}
99
100// ---------------------------------------------------------------- gate A: the bridge
101//
102// The LeVeque vector-potential sampler of `vof_advect_scenes.hpp` writes the ADVECTOR's convention
103// (u(i) at the high face, x = (i+1)h). The solver's u(i) sits at the LOW face, x = i*h. Both are
104// samples of the SAME physical field at the SAME physical points once the index shift is applied,
105// so writing the solver side by re-indexing the advector side is a statement about indices, not a
106// tautology: `u_solver(i+1) = u_adv(i)`, i.e. solver cell i's low face carries advector cell i-1's
107// value.
108void bridge() {
109 const int N = 24;
110 const double h = 1.0 / N, dt = 0.1 * h, T = 3.0;
111 // The solver's advector block runs in CELL units (h = 1), so the same Courant number
112 // a = u*dt/h is reproduced by handing the solver the SAME face values with dt/h as its dt.
113 const double dtSolver = dt / h;
114 const int steps = 20;
115
116 // --- reference: the standalone advector, prescribed field -------------------------------------
117 WyAdvector ref;
119 // WO-R2 item 4: the reference must be configured like the solver, wisp tolerance included —
120 // otherwise this gate measures the DIFFERENCE OF TWO PREDICATES (8.481e-09 with the solver at
121 // 1e-8 and the standalone at 0) instead of the bridge it exists to gate.
123 const I3 eR = ref.extent();
124 const int gR = ref.ghost();
125 const vofscene::Block bR{ref.inner(), eR, gR, I3{0, 0, 0}};
126 ref.exchange = [eR, gR](SField f) { vofscene::periodicFill(f, eR, gR, true, true, true); };
127 vofscene::initSphere(ref.colour(), bR, h, 0.35, 0.35, 0.35, 0.15);
128 ref.syncGhosts();
129 for (int s = 0; s < steps; ++s) {
130 vofscene::fillLeVeque(ref, bR, h, std::cos(M_PI * (s + 0.5) * dt / T));
131 ref.advect(dt, s);
132 }
133 auto refHost = Kokkos::create_mirror_view(ref.colour());
134 Kokkos::deep_copy(refHost, ref.colour());
135
136 // --- the solver path: the same physical field handed to it through set_field("u"/"v"/"w") -----
137 // mu = 0, no advection and no body force, so the momentum solve returns u unchanged and the
138 // projection is a no-op on an already-divergence-free field: step() transports C and nothing
139 // else. The velocity is re-prescribed every step (the field is time dependent).
141 s.setRho(1.0);
142 s.setMu(0.0);
143 s.setDt(dtSolver);
144 // bitwise / machine-precision gates: keep the legacy fixed-sweep momentum loop (the default
145 // residual stop, 1e-5 since 2026-09-02, leaves an O(1e-12) momentum residual)
146 s.setVelocityResidualTolerance(0.0);
147 s.setPressureGeometry(std::vector<double>((std::size_t)N * N * N, 10.0));
148 s.enableVof();
149 { // the same sphere, sampled by the same helper on a throwaway advector block
150 WyAdvector seed;
151 seed.init(N, N, N, h, peclet::flow::IbmSolver::kVofG);
152 const vofscene::Block bS{seed.inner(), seed.extent(), seed.ghost(), I3{0, 0, 0}};
153 vofscene::initSphere(seed.colour(), bS, h, 0.35, 0.35, 0.35, 0.15);
154 auto sh = Kokkos::create_mirror_view(seed.colour());
155 Kokkos::deep_copy(sh, seed.colour());
156 const I3 eS = seed.extent();
157 const int gS = seed.ghost();
158 std::vector<double> c0((std::size_t)N * N * N);
159 for (int z = 0; z < N; ++z)
160 for (int y = 0; y < N; ++y)
161 for (int x = 0; x < N; ++x)
162 c0[idx(x, y, z, N, N)] = sh(L3(x + gS, y + gS, z + gS, eS));
163 s.setVof(c0);
164 }
165 // Host samplers of the ADVECTOR's face values, so the solver side is the same numbers re-indexed.
166 WyAdvector probe;
168 const I3 eP = probe.extent();
169 const int gP = probe.ghost();
170 const vofscene::Block bP{probe.inner(), eP, gP, I3{0, 0, 0}};
171 std::vector<double> uu((std::size_t)N * N * N), vv(uu.size()), ww(uu.size());
172 for (int st = 0; st < steps; ++st) {
173 vofscene::fillLeVeque(probe, bP, h, std::cos(M_PI * (st + 0.5) * dt / T));
174 auto hu = Kokkos::create_mirror_view(probe.faceU());
175 auto hv = Kokkos::create_mirror_view(probe.faceV());
176 auto hw = Kokkos::create_mirror_view(probe.faceW());
177 Kokkos::deep_copy(hu, probe.faceU());
178 Kokkos::deep_copy(hv, probe.faceV());
179 Kokkos::deep_copy(hw, probe.faceW());
180 for (int z = 0; z < N; ++z)
181 for (int y = 0; y < N; ++y)
182 for (int x = 0; x < N; ++x) {
183 // solver low face of cell i == advector high face of cell i-1 (periodic wrap)
184 const int xm = (x + N - 1) % N, ym = (y + N - 1) % N, zm = (z + N - 1) % N;
185 uu[idx(x, y, z, N, N)] = hu(L3(xm + gP, y + gP, z + gP, eP));
186 vv[idx(x, y, z, N, N)] = hv(L3(x + gP, ym + gP, z + gP, eP));
187 ww[idx(x, y, z, N, N)] = hw(L3(x + gP, y + gP, zm + gP, eP));
188 }
189 s.setField("u", uu);
190 s.setField("v", vv);
191 s.setField("w", ww);
192 s.step();
193 }
194 const auto cs = s.getVof();
195 double dmax = 0.0, l1 = 0.0;
196 for (int z = 0; z < N; ++z)
197 for (int y = 0; y < N; ++y)
198 for (int x = 0; x < N; ++x) {
199 const double d = cs[idx(x, y, z, N, N)] - refHost(L3(x + gR, y + gR, z + gR, eR));
200 dmax = std::fmax(dmax, std::fabs(d));
201 l1 += std::fabs(d);
202 }
203 std::printf("A bridge (LeVeque, %d steps): max|C_solver - C_standalone| %.3e L1 %.3e\n", steps,
204 dmax, l1);
205 // Not bitwise: the solver re-projects the prescribed field each step, so the face values it
206 // advects with differ from the prescribed ones at the projection's residual (~1e-16 relative).
207 CHECK(dmax < 1e-11);
208}
209
210// ---------------------------------------------------------------- gate B: the acid test
211//
212// WHY THE VELOCITY HALF IS RUN WITH THE INTERFACE HELD FIXED.
213//
214// The acid test exists to catch an inconsistency between the momentum face density, the face body
215// force and the projection face coefficient — the three-way agreement that makes
216// w* = w^n + (dt/rho_f)(-dP/dz + f_f) = w^n
217// hold exactly (doc/variable_density_projection.md §3). The colour field is not part of that
218// statement; it only supplies rho. So the sharpest form of the gate through the C chain is: freeze
219// C and demand the hand-set-rho number, BITWISE. That is what `freeze = true` measures, and it
220// returns 2.176e-17 == the reference to the last bit.
221//
222// With the interface FREE the rest state is no longer a stable fixed point of the DISCRETE system,
223// and that is a property of the physics-plus-time-discretisation, not of the wiring:
224// * the colour field is an extra degree of freedom, and any residual velocity eps displaces it by
225// eps*dt;
226// * that displacement changes rho by (rho_l - rho_g)*eps*dt, so it changes the body force by
227// g*(rho_l-rho_g)*eps*dt and the resulting acceleration in the LIGHT layer by
228// g*(rho_l-rho_g)*eps*dt/rho_g — a loop gain of g*drho*dt/rho_g, which is 100 at ratio 1000,
229// g = 0.1, dt = 1;
230// * the projection removes only the part of that perturbation that is a discrete gradient. What
231// survives is an interfacial gravity-wave mode, and it is undamped at mu = 0.
232// Measured, starting from the exact fixed point 2.176e-17 (200 steps): the residual reaches
233// gain 100 (ratio 1e3, g = 0.1, dt = 1) -> 2.4e-12
234// gain 10 (ratio 1e3, g = 0.1, dt = 0.1) -> 7.9e-15
235// gain 1 (ratio 1e3, g = 1e-3, dt = 1) -> 1.2e-16
236// gain 0.2 (ratio 3, g = 0.1, dt = 1) -> 3.0e-14
237// i.e. the level it settles at is set by the loop gain, exactly as the mechanism predicts, and it
238// is NOT set by anything in the bridge (which gate A pins independently, and which gate B2 pins to
239// the last bit). The free-interface residual is therefore recorded and bounded, not gated at
240// machine zero — see the WO-J findings entry.
241double hydrostaticC(double ratio, bool freeze, bool harmonic, int steps, double g, double dt) {
242 const int N = 8, NZ = 24;
244 s.setRho(1.0);
245 s.setMu(0.0); // inviscid: the balance is exact (no viscous wall layer in the predictor)
246 // bitwise / machine-precision gates: keep the legacy fixed-sweep momentum loop (the default
247 // residual stop, 1e-5 since 2026-09-02, leaves an O(1e-12) momentum residual)
248 s.setVelocityResidualTolerance(0.0);
249 s.setDt(dt);
250 s.setDomainBc(4, 1, 0, 0, 0);
251 s.setDomainBc(5, 1, 0, 0, 0); // walls +-z, periodic x,y
252 s.setPressureGeometry(std::vector<double>((std::size_t)N * N * NZ, 10.0));
253 std::vector<double> c0((std::size_t)N * N * NZ);
254 for (int z = 0; z < NZ; ++z)
255 for (int y = 0; y < N; ++y)
256 for (int x = 0; x < N; ++x)
257 c0[idx(x, y, z, N, N)] = (z < NZ / 2) ? 1.0 : 0.0; // sharp: heavy (liquid) below
258 if (freeze) {
259 s.addField("C");
260 s.setField("C", c0);
261 s.exchangeField("C");
262 } else {
263 s.setVof(c0);
264 }
265 // rho = 1 + (ratio-1)*C (enables the variable-density path), then gravity from rho.
266 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "", {1.0, ratio - 1.0});
267 s.setPropertyModel("force_z", peclet::flow::ClosureKind::LinearMix, "rho", "", {0.0, -g});
268 if (harmonic)
269 s.setRhoFaceHarmonic(true);
270 double last = 0.0;
271 for (int it = 0; it < steps; ++it) {
272 s.step();
273 last = std::fmax(maxAbs(s.getVelocity(0)),
274 std::fmax(maxAbs(s.getVelocity(1)), maxAbs(s.getVelocity(2))));
275 CHECK(!std::isnan(last));
276 if (std::isnan(last))
277 return last;
278 }
279 // dP/dz against the ACTUAL colour column (with a free interface C is no longer the initial one).
280 const auto cNow = freeze ? c0 : s.getVof();
281 const auto p = s.getPressure();
282 double perr = 0;
283 const int xc = N / 2, yc = N / 2;
284 for (int z = 1; z < NZ; ++z) {
285 const double dp = p[idx(xc, yc, z, N, N)] - p[idx(xc, yc, z - 1, N, N)];
286 const double ra = 1.0 + (ratio - 1.0) * cNow[idx(xc, yc, z, N, N)];
287 const double rb = 1.0 + (ratio - 1.0) * cNow[idx(xc, yc, z - 1, N, N)];
288 perr = std::fmax(perr, std::fabs(dp + g * 0.5 * (ra + rb)) / (g * ratio));
289 }
290 std::printf("B ratio %-6g %-14s: steady max|u| %.3e dP/dz rel-err %.3e\n", ratio,
291 harmonic ? "HARMONIC rho_f" : (freeze ? "frozen C" : "free C"), last, perr);
292 if (!harmonic)
293 CHECK(perr < 1e-11);
294 return freeze ? last : perr;
295}
296
297// The hand-set-rho reference of test_vardensity_projection.cpp, on the identical grid, so the
298// frozen-interface C chain can be compared to it BITWISE.
299double hydrostaticRhoRef(double ratio, int steps, double g, double dt) {
300 const int N = 8, NZ = 24;
302 s.setRho(1.0);
303 s.setMu(0.0);
304 s.setDt(dt);
305 s.setDomainBc(4, 1, 0, 0, 0);
306 s.setDomainBc(5, 1, 0, 0, 0);
307 s.setPressureGeometry(std::vector<double>((std::size_t)N * N * NZ, 10.0));
308 std::vector<double> rho((std::size_t)N * N * NZ);
309 for (int z = 0; z < NZ; ++z)
310 for (int y = 0; y < N; ++y)
311 for (int x = 0; x < N; ++x)
312 rho[idx(x, y, z, N, N)] = (z < NZ / 2) ? ratio : 1.0;
313 s.addField("rho");
314 s.setField("rho", rho);
315 s.setDensityMode(true);
316 s.setPropertyModel("force_z", peclet::flow::ClosureKind::LinearMix, "rho", "", {0.0, -g});
317 double last = 0.0;
318 for (int it = 0; it < steps; ++it) {
319 s.step();
320 last = std::fmax(maxAbs(s.getVelocity(0)),
321 std::fmax(maxAbs(s.getVelocity(1)), maxAbs(s.getVelocity(2))));
322 }
323 return last;
324}
325
326// ---------------------------------------------------------------- gate C: conservation
327void conservation(int steps) {
328 const int N = 24;
329 const double dt = 1.0, amp = 5e-4, ratio = 10.0;
331 s.setRho(1.0);
332 s.setMu(0.05);
333 s.setDt(dt);
334 s.setAdvection(true);
335 s.setPressureGeometry(std::vector<double>((std::size_t)N * N * N, 10.0));
336 s.enableVof();
337 std::vector<double> c0((std::size_t)N * N * N), fx(c0.size());
338 for (int z = 0; z < N; ++z)
339 for (int y = 0; y < N; ++y)
340 for (int x = 0; x < N; ++x) {
341 const double dx = x + 0.5 - N / 2.0, dy = y + 0.5 - N / 2.0, dz = z + 0.5 - N / 2.0;
342 const double r = std::sqrt(dx * dx + dy * dy + dz * dz);
343 c0[idx(x, y, z, N, N)] = std::fmin(1.0, std::fmax(0.0, 0.5 - (r - N / 5.0)));
344 fx[idx(x, y, z, N, N)] = amp * std::sin(2 * M_PI * (z + 0.5) / N); // shear
345 }
346 s.setVof(c0);
347 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "", {1.0, ratio - 1.0});
348 s.setPropertyModel("mu", peclet::flow::ClosureKind::LinearMix, "C", "", {0.05, 0.45});
349 s.enableCellForce();
350 s.setField("force_x", fx);
351 const double v0 = sum(s.getVof());
352 double dmax = 0.0, cfl = 0.0, peak = 0.0, drift = 0.0;
353 for (int it = 0; it < steps; ++it) {
354 s.step();
355 dmax = std::fmax(dmax, std::fabs(s.maxOpenDivergence()));
356 cfl = std::fmax(cfl, s.vofLastCourant());
357 drift = (sum(s.getVof()) - v0) / v0; // the PEAK excursion, not just where it ends up
358 peak = std::fmax(peak, std::fabs(drift));
359 }
360 const auto d = s.vofDiagnostics();
361 std::printf(
362 "C conservation (%d coupled steps, ratio %g): dV/V final %+.3e PEAK %.3e "
363 "max|div(open u)| %.2e max cfl %.4f C in [%.2e, %.10f] mixed %ld\n",
364 steps, ratio, drift, peak, dmax, cfl, d.minC, d.maxC, d.mixed);
365 // The floor here is the PROJECTION's discrete divergence residual, not the advection: WY adds
366 // H(C-1/2)*div*dt/h to every full cell's budget, so the conservation floor is whatever the
367 // pressure solve leaves (WO-E finding 2 stated this in advance). Measured on this scene the
368 // residual wanders inside a band and does not accumulate. V1's own floor, with a prescribed
369 // field at max|div| ~ 1e-15, was 5.7e-14 over 3200 steps.
370 CHECK(peak < 1e-12);
371 CHECK(d.maxC <= 1.0 + 1e-12 && d.minC > -1e-12);
372}
373
374// ---------------------------------------------------------------- gate D: C == const reductions
375void constantColour() {
376 const int N = 12;
377 const std::size_t n = (std::size_t)N * N * N;
378 // All-fluid periodic box (VoF with an immersed solid is refused at this rung) with a SHEARED body
379 // force and advection on, so u and P are non-trivial fields and a bitwise comparison means
380 // something.
381 std::vector<double> fx(n), ones(n, 1.0);
382 for (int z = 0; z < N; ++z)
383 for (int y = 0; y < N; ++y)
384 for (int x = 0; x < N; ++x)
385 fx[idx(x, y, z, N, N)] = 1e-3 * std::sin(2 * M_PI * (z + 0.5) / N);
386 auto build = [&](peclet::flow::IbmSolver& s) {
387 s.setRho(2.0);
388 s.setMu(0.1);
389 s.setDt(2.0);
390 s.setVelocityResidualTolerance(0.0); // bitwise gates: legacy fixed-sweep momentum loop
391 s.setAdvection(true);
392 s.setPressureGeometry(std::vector<double>(n, 10.0));
393 s.enableCellForce();
394 s.setField("force_x", fx);
395 };
396
397 // D1 + D2: VoF on, NO closure. A uniform C must be bitwise stationary, and the flow must be
398 // bitwise identical to the same run with VoF off.
399 std::vector<std::vector<double>> u(2), p(2);
400 std::vector<double> cEnd;
401 for (int vofOn = 0; vofOn < 2; ++vofOn) {
403 build(s);
404 if (vofOn)
405 s.setVof(ones);
406 for (int it = 0; it < 30; ++it)
407 s.step();
408 u[vofOn] = s.getVelocity(0);
409 p[vofOn] = s.getPressure();
410 if (vofOn)
411 cEnd = s.getVof();
412 }
413 double dC = 0;
414 for (double c : cEnd)
415 dC = std::fmax(dC, std::fabs(c - 1.0));
416 const double du = maxAbsDiff(u[0], u[1]), dp = maxAbsDiff(p[0], p[1]);
417 std::printf(
418 "D1 uniform C stationary: max|C-1| %.3e\nD2 VoF inert (no closure): du %.3e dp %.3e "
419 "(|u| %.3e)\n",
420 dC, du, dp, maxAbs(u[0]));
421 CHECK(dC == 0.0);
422 CHECK(du == 0.0 && dp == 0.0);
423
424 // D3: VoF + rho(C) with C == 1 must be bitwise the hand-set uniform-rho varRho path.
425 std::vector<std::vector<double>> uv(2), pv(2);
426 for (int viaC = 0; viaC < 2; ++viaC) {
428 build(s);
429 if (viaC) {
430 s.setVof(ones);
431 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "", {0.0, 2.0});
432 } else {
433 s.addField("rho");
434 s.setField("rho", std::vector<double>(n, 2.0));
435 s.setDensityMode(true);
436 }
437 for (int it = 0; it < 30; ++it)
438 s.step();
439 uv[viaC] = s.getVelocity(0);
440 pv[viaC] = s.getPressure();
441 }
442 const double duv = maxAbsDiff(uv[0], uv[1]), dpv = maxAbsDiff(pv[0], pv[1]);
443 std::printf("D3 rho(C==1) == hand-set uniform rho: du %.3e dp %.3e\n", duv, dpv);
444 CHECK(duv == 0.0 && dpv == 0.0);
445}
446
447// ---------------------------------------------------------------- gate F: interface-local CFL
448//
449// A localized jet far from a quiescent interface: the global max Courant number is the jet's, the
450// interface-local one is the interface's. V1 measured the same effect on Zalesak (0.314 at a
451// quiescent corner versus 0.157 at the interface) and it is why a global max must not set the VoF
452// time step (VOF_PLAN.md §6).
453void interfaceLocalCfl() {
454 const int N = 24;
455 const double dt = 1.0;
457 s.setRho(1.0);
458 s.setMu(0.05);
459 s.setDt(dt);
460 s.setPressureGeometry(std::vector<double>((std::size_t)N * N * N, 10.0));
461 s.enableVof();
462 std::vector<double> c0((std::size_t)N * N * N), fx(c0.size());
463 for (int z = 0; z < N; ++z)
464 for (int y = 0; y < N; ++y)
465 for (int x = 0; x < N; ++x) {
466 const double dx = x + 0.5 - N / 2.0, dy = y + 0.5 - N / 2.0, dz = z + 0.5 - N / 2.0;
467 c0[idx(x, y, z, N, N)] =
468 std::fmin(1.0, std::fmax(0.0, 0.5 - (std::sqrt(dx * dx + dy * dy + dz * dz) - 3.0)));
469 // a jet localized around z = 3, i.e. ~9 cells from the nearest interface cell
470 const double a = (z + 0.5 - 3.0) / 1.5;
471 fx[idx(x, y, z, N, N)] = 2e-3 * std::exp(-a * a);
472 }
473 s.setVof(c0);
474 s.setPropertyModel("rho", peclet::flow::ClosureKind::LinearMix, "C", "", {1.0, 9.0});
475 s.enableCellForce();
476 s.setField("force_x", fx);
477 for (int it = 0; it < 40; ++it)
478 s.step();
479 const double local = s.vofLastCourant();
480 double global = 0.0;
481 for (int c = 0; c < 3; ++c)
482 global = std::fmax(global, maxAbs(s.getVelocity(c)) * dt);
483 std::printf("F CFL: interface-local %.4f global %.4f (over-throttle factor %.2fx)\n", local,
484 global, global / std::fmax(local, 1e-300));
485 CHECK(local < 0.5 * global); // the whole point: the far-field jet must not set the VoF dt
486 CHECK(local > 0.0); // ... and the interface's own faces must still be seen
487}
488} // namespace
489
490int main(int argc, char** argv) {
491 Kokkos::initialize(argc, argv);
492 {
493 bridge();
494 // B1/B2: the acid test through the C chain. `freeze` isolates the closure/projection
495 // consistency (bitwise against the hand-set-rho reference); the free-interface run records the
496 // residual the moving-interface degree of freedom leaves.
497 for (double ratio : {3.0, 1000.0}) {
498 const double frozen = hydrostaticC(ratio, /*freeze=*/true, false, 100, 0.1, 1.0);
499 const double ref = hydrostaticRhoRef(ratio, 100, 0.1, 1.0);
500 std::printf("B2 ratio %-6g frozen-C chain vs hand-set rho: %.17g vs %.17g (%s)\n", ratio,
501 frozen, ref, frozen == ref ? "BITWISE EQUAL" : "DIFFER");
502 CHECK(frozen == ref);
503 CHECK(frozen < 1e-12);
504 hydrostaticC(ratio, /*freeze=*/false, false, 100, 0.1, 1.0);
505 }
506 // E: the harmonic rho_f ablation must break the balance loudly.
507 //
508 // WO-R2 item 3: "loudly" got LOUDER under the exact level-0 operator, which is now the default
509 // once VoF is on. With the float bands the ablation settles at a dP/dz error of 0.3355; with
510 // the exact operator the same run leaves the Weymouth-Yue boundedness cap before step 100 and
511 // the advector throws. Both outcomes confirm the same statement (the harmonic face mean is not
512 // the consistent one for this discretization), so the gate accepts either — and says which it
513 // saw, because the two are different measurements. The ARITHMETIC runs above are unaffected
514 // and in fact tighter under the exact operator (ratio 1000 free C: dP/dz 1.02e-15 -> 4.26e-16).
515 {
516 double perr = 0.0;
517 bool blewUp = false;
518 try {
519 perr = hydrostaticC(1000.0, false, /*harmonic=*/true, 100, 0.1, 1.0);
520 std::printf("E harmonic rho_f dP/dz rel-err %.3e (arithmetic is the consistent mean)\n",
521 perr);
522 } catch (const std::exception& ex) {
523 blewUp = true;
524 std::printf("E harmonic rho_f: the ablation left the boundedness cap altogether — %s\n",
525 ex.what());
526 }
527 CHECK(blewUp || perr > 1e-3);
528 }
529 constantColour();
530 interfaceLocalCfl();
531 conservation(std::getenv("PECLET_VOF_TWOPHASE_LONG") ? 1000 : 200);
532 }
533 Kokkos::finalize();
534 if (failures == 0) {
535 std::printf("OK\n");
536 return 0;
537 }
538 std::fprintf(stderr, "%d failure(s)\n", failures);
539 return 1;
540}
static constexpr int kVofG
static constexpr double defaultVofWispEps()
The value enableVof starts from.
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
x
Definition cmp.py:24
y
Definition cmp.py:24
long L3(int x, int y, int z, I3 e)
Kokkos::View< double *, SMem > SField
void fillLeVeque(WyAdvector &a, Block b, double h, double phase)
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 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...
static constexpr int NZ
Where this block sits in the global grid: o is the global index of inner cell (0,0,...
static constexpr int N
static double maxAbs(const std::vector< double > &a)
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
#define CHECK(cond)