flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_vof_advect.cpp
Go to the documentation of this file.
1// VoF rung V1 (WO-E) — gate battery for Weymouth-Yue split geometric advection
2// (src/vof/advect_wy.hpp) on prescribed velocity fields. Standalone: no Solver, no MPI.
3//
4// A slab translation : an axis-normal planar interface under uniform diagonal flow is
5// transported to round-off; volume drift over 1024 steps
6// B sphere translation : L1 shape error 2nd order under refinement (16/32/64);
7// volume drift < 1e-13 over 1024 steps
8// C Zalesak slotted disk : one revolution, L1 error recorded and compared in order of
9// magnitude with published geometric-VoF values
10// D LeVeque deformation : 3D deformation field with time reversal (T = 3) at 32^3 and 64^3
11// (128^3 opt-in, PECLET_VOF_LEVEQUE_128=1); volume drift vs the
12// measured discrete face divergence
13// E CFL guard : a step at CFL >= 0.5 aborts with a clear error
14// F worklist neutrality : compaction on/off is bitwise identical
15// G the dilation trap : recomputing H(C-1/2) between sweeps, measured (diagnostic only)
16//
17// The velocity fields are built so that the DISCRETE face divergence, not merely the analytic one,
18// vanishes: uniform and solid-body rotation are exactly zero bitwise, and the LeVeque field is
19// sampled as the discrete curl of an edge vector potential (see vof_advect_scenes.hpp).
20// Weymouth-Yue conserves volume to exactly the accuracy of that divergence, so this is a
21// precondition for the conservation gates, not a nicety.
22#include <array>
23#include <cmath>
24#include <cstdio>
25#include <cstdlib>
26#include <cstring>
27#include <Kokkos_Core.hpp>
28#include <stdexcept>
29#include <string>
30#include <vector>
31
32#include "vof/advect_wy.hpp"
33#include "vof_advect_scenes.hpp"
34
35namespace {
39using vofscene::Block;
40
41int failures = 0;
42#define CHECK(cond) \
43 do { \
44 if (!(cond)) { \
45 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
46 ++failures; \
47 } \
48 } while (0)
49
51struct Case {
52 WyAdvector adv;
53 Block blk;
54 std::array<bool, 3> per{true, true, true};
55
56 void setup(int nx, int ny, int nz, double h, bool px = true, bool py = true, bool pz = true) {
57 adv.init(nx, ny, nz, h, 3);
58 blk = vofscene::blockOf(adv, I3{0, 0, 0});
59 per = {px, py, pz};
60 const I3 e = adv.extent();
61 const int g = adv.ghost();
62 const std::array<bool, 3> p = per;
63 const Block bb = blk;
64 const I3 gs{nx, ny, nz};
65 adv.exchange = [e, g, p, bb, gs](SField f) {
66 vofscene::periodicFill(f, e, g, p[0], p[1], p[2]);
67 vofscene::clampFill(f, bb, gs, p[0], p[1], p[2]);
68 };
69 }
70 SField c() const { return adv.colour(); }
71 I3 e() const { return adv.extent(); }
72 I3 n() const { return adv.inner(); }
73 int g() const { return adv.ghost(); }
74};
75
76double order(double coarse, double fine) {
77 return (fine > 0.0 && coarse > 0.0) ? std::log2(coarse / fine) : 0.0;
78}
79
80// =============================================================================== gate A: slab
81void gateSlab() {
82 std::printf("\n=== A slab translation (planar interface, uniform diagonal flow)\n");
83 const int nx = 32;
84 const double h = 1.0 / nx;
85 Case cs;
86 cs.setup(nx, nx, nx, h);
87 vofscene::initSlab(cs.c(), cs.blk, h, 0, 0.2, 0.7);
88 cs.adv.syncGhosts();
89 SField c0 = vofscene::copyOf(cs.c(), "slab0");
90 const auto d0 = cs.adv.diagnostics();
91
92 // CFL 0.25 with |u| = 1 in each direction; 128 steps is one full lap, run 8 laps (1024 > 1000).
93 const double dt = 0.25 * h;
94 vofscene::fillUniform(cs.adv, cs.blk, 1.0, 1.0, 1.0);
95 for (long s = 0; s < 1024; ++s)
96 cs.adv.advect(dt, s);
97 const auto d1 = cs.adv.diagnostics();
98
99 const double linf = vofscene::lInfDiff(cs.c(), c0, cs.e(), cs.n(), cs.g());
100 const double l1 = vofscene::l1Diff(cs.c(), c0, cs.e(), cs.n(), cs.g());
101 const double drift = std::fabs(d1.sumC - d0.sumC) / d0.sumC;
102 std::printf(" 1024 steps (8 laps), CFL 0.25: Linf %.3e L1 %.3e volume drift %.3e\n", linf,
103 l1, drift);
104 std::printf(" C range [%.17g, %.17g] wisps %ld\n", d1.minC, d1.maxC, d1.wisps);
105 CHECK(drift < 1e-13);
106 CHECK(linf < 1e-11); // a plane under uniform flow is transported exactly up to accumulated eps
107 CHECK(d1.minC >= -1e-14 && d1.maxC <= 1.0 + 1e-14);
108}
109
110// ============================================================================= gate B: sphere
111void gateSphere() {
112 std::printf("\n=== B sphere translation (uniform diagonal flow, periodic box)\n");
113 const int res[3] = {16, 32, 64};
114 double err[3] = {0, 0, 0};
115 for (int k = 0; k < 3; ++k) {
116 const int nx = res[k];
117 const double h = 1.0 / nx;
118 Case cs;
119 cs.setup(nx, nx, nx, h);
120 vofscene::initSphere(cs.c(), cs.blk, h, 0.5, 0.5, 0.5, 0.25);
121 cs.adv.syncGhosts();
122 SField c0 = vofscene::copyOf(cs.c(), "sph0");
123 const auto d0 = cs.adv.diagnostics();
124
125 const double dt = 0.25 * h; // CFL 0.25 on each component
126 const long steps = 4L * nx; // one full diagonal lap: steps*dt*1 = 1.0 exactly
127 vofscene::fillUniform(cs.adv, cs.blk, 1.0, 1.0, 1.0);
128 for (long s = 0; s < steps; ++s)
129 cs.adv.advect(dt, s);
130 const auto d1 = cs.adv.diagnostics();
131
132 const double l1 = vofscene::l1Diff(cs.c(), c0, cs.e(), cs.n(), cs.g());
133 err[k] = l1 * h * h * h; // volume-weighted L1 shape error
134 const double drift = std::fabs(d1.sumC - d0.sumC) / d0.sumC;
135 std::printf(
136 " %3d^3 %4ld steps L1(vol) %.4e L1/V %.4e drift %.2e C[%.3e,%.5f] wisps %ld\n", nx,
137 steps, err[k], l1 / d0.sumC, drift, d1.minC, d1.maxC, d1.wisps);
138 CHECK(drift < 1e-13);
139 }
140 const double o1 = order(err[0], err[1]), o2 = order(err[1], err[2]);
141 std::printf(" L1 shape-error order: 16->32 %.2f 32->64 %.2f\n", o1, o2);
142 CHECK(o2 > 1.7);
143 CHECK(o1 > 1.5);
144
145 // long run: the conservation gate proper (>= 1000 steps)
146 std::printf(" -- long run, 32^3, 1024 steps (8 laps)\n");
147 const int nx = 32;
148 const double h = 1.0 / nx;
149 Case cs;
150 cs.setup(nx, nx, nx, h);
151 vofscene::initSphere(cs.c(), cs.blk, h, 0.5, 0.5, 0.5, 0.25);
152 cs.adv.syncGhosts();
153 SField c0 = vofscene::copyOf(cs.c(), "sphL0");
154 const auto d0 = cs.adv.diagnostics();
155 vofscene::fillUniform(cs.adv, cs.blk, 1.0, 1.0, 1.0);
156 const double dt = 0.25 * h;
157 double worstDrift = 0.0;
158 for (long s = 0; s < 1024; ++s) {
159 cs.adv.advect(dt, s);
160 if ((s + 1) % 128 == 0) {
161 const auto d = cs.adv.diagnostics();
162 worstDrift = std::fmax(worstDrift, std::fabs(d.sumC - d0.sumC) / d0.sumC);
163 }
164 }
165 const auto d1 = cs.adv.diagnostics();
166 const double l1 = vofscene::l1Diff(cs.c(), c0, cs.e(), cs.n(), cs.g());
167 std::printf(" drift(worst over 8 checkpoints) %.3e final %.3e L1(vol) %.4e wisps %ld\n",
168 worstDrift, std::fabs(d1.sumC - d0.sumC) / d0.sumC, l1 * h * h * h, d1.wisps);
169 CHECK(worstDrift < 1e-13);
170 CHECK(d1.minC > -1e-14 && d1.maxC < 1.0 + 1e-14);
171}
172
173// ============================================================================ gate C: Zalesak
174void gateZalesak() {
175 std::printf("\n=== C Zalesak slotted disk, one revolution (100^2 in-plane)\n");
176 const int nx = 100, nz = 4;
177 const double h = 1.0 / nx;
178 Case cs;
179 cs.setup(nx, nx, nz, h);
180 vofscene::initZalesak(cs.c(), cs.blk, h, 0.5, 0.75, 0.15, 0.025, 0.85);
181 cs.adv.syncGhosts();
182 SField c0 = vofscene::copyOf(cs.c(), "zal0");
183 const auto d0 = cs.adv.diagnostics();
184
185 // The CFL guard is global, while the disk sits at r = 0.25 and the fastest faces are at the
186 // domain corner (r ~ 0.5): the step count is set by the corner, not by the interface, so this
187 // run takes ~3x the steps a solver limiting dt at the interface would. PECLET_VOF_ZALESAK_STEPS
188 // exposes that (the minimum admissible count here is 629).
189 // At the published 1000-step setup the GLOBAL max CFL is 0.314 (corner), above the default 3D
190 // cap of 0.25, while every cell the interface ever visits stays at CFL <= 0.157. Weymouth's
191 // bound is a per-flux statement, so the run is admissible and the global guard is merely a
192 // conservative proxy; raise the cap here deliberately rather than shrinking dt, which would
193 // break comparability with the published L1 values.
194 // (Follow-up noted in VOF_PLAN.md: at V2 the solver's dt limiter should measure the CFL over
195 // interface-adjacent cells, not the whole domain, or quiescent far-field corners will throttle
196 // the step for nothing.)
197 cs.adv.cflLimit = 0.5;
198 long steps = 1000;
199 if (const char* v = std::getenv("PECLET_VOF_ZALESAK_STEPS"); v && std::atol(v) > 0)
200 steps = std::atol(v);
201 const double T = 1.0, omega = 2.0 * M_PI / T, dt = T / steps;
202 vofscene::fillRotation(cs.adv, cs.blk, h, 0.5, 0.5, omega);
203 std::printf(" discrete |div| dt/h max = %.3e (exactly zero expected)\n",
204 cs.adv.maxDiscreteDivergence(dt / h));
205 for (long s = 0; s < steps; ++s)
206 cs.adv.advect(dt, s);
207 const auto d1 = cs.adv.diagnostics();
208
209 const double l1 = vofscene::l1Diff(cs.c(), c0, cs.e(), cs.n(), cs.g());
210 const double rel = l1 / d0.sumC;
211 // `rel` is the metric of THINC-scaling (Xie & Xiao 2021, eq. 27) and of most VoF papers:
212 // sum|C - C_ex| / sum|C_ex|. Their published N=100 values: THINC-scaling 1.55e-2,
213 // MTHINC 1.61e-2, UMTHINC 2.61e-2, THINC/QQ 3.22e-2. `perLayer` is the per-2D-cell mean
214 // absolute error E1 of Cassinelli et al. (arXiv:1903.11949, eq. 15).
215 std::printf(" %ld steps CFL %.3f L1(abs) %.4e L1/V %.4e E1(per 2D cell) %.4e\n", steps,
216 cs.adv.lastCfl(), l1, rel, l1 * h * h / nz);
217 std::printf(" drift %.3e C[%.3e,%.6f] mixed %ld wisps %ld\n",
218 std::fabs(d1.sumC - d0.sumC) / d0.sumC, d1.minC, d1.maxC, d1.mixed, d1.wisps);
219 CHECK(std::fabs(d1.sumC - d0.sumC) / d0.sumC < 1e-13);
220 // published geometric-VoF relative shape errors for this case are ~0.01-0.02; a 10x miss is a bug
221 CHECK(rel > 0.002 && rel < 0.2);
222}
223
224// ====================================================== gate C2: the interface Courant BAND
225// (WO-R2)
226//
227// The band `maxCourantInterface` limits on is "this cell's colour differs from a neighbour's".
228// Written with an exact `!=` it has no wisp tolerance, so the Weymouth-Yue round-off residue the
229// interface leaves behind (min C ~ -3.8e-17 on this scene) keeps the whole WAKE inside the band and
230// the interface-local limiter creeps back towards the global max — which is the thing it exists to
231// avoid. Measured here on the published Zalesak setup, one revolution, `interfaceLocalCfl = true`
232// (the setting `IbmSolver` uses; `lastCfl()` is what `vof_last_courant()` reports).
233void gateZalesakBand() {
234 std::printf("\n=== C2 interface Courant band on Zalesak: the wisp tolerance (WO-R2 item 4b)\n");
235 const int nx = 100, nz = 4;
236 const double h = 1.0 / nx;
237 const long steps = 1000;
238 const double T = 1.0, omega = 2.0 * M_PI / T, dt = T / steps;
239 double first[2] = {0, 0}, last[2] = {0, 0}, worst[2] = {0, 0}, drift[2] = {0, 0};
240 for (int k = 0; k < 2; ++k) {
241 Case cs;
242 cs.setup(nx, nx, nz, h);
243 vofscene::initZalesak(cs.c(), cs.blk, h, 0.5, 0.75, 0.15, 0.025, 0.85);
244 cs.adv.syncGhosts();
245 const auto d0 = cs.adv.diagnostics();
246 cs.adv.cflLimit = 0.5; // as in gate C: the corner faces run above the 3D bound
247 cs.adv.interfaceLocalCfl = true; // the solver's setting
248 cs.adv.wispEps = (k == 0) ? 0.0 : 1e-8;
249 vofscene::fillRotation(cs.adv, cs.blk, h, 0.5, 0.5, omega);
250 for (long t = 0; t < steps; ++t) {
251 cs.adv.advect(dt, t);
252 const double c = cs.adv.lastCfl();
253 if (t == 0)
254 first[k] = c;
255 last[k] = c;
256 worst[k] = std::fmax(worst[k], c);
257 }
258 const auto d1 = cs.adv.diagnostics();
259 drift[k] = std::fabs(d1.sumC - d0.sumC) / d0.sumC;
260 std::printf(
261 " wispEps %-6g : band Courant after step 1 %.4f, after %ld %.4f, worst %.4f, "
262 "volume drift %.3e\n",
263 cs.adv.wispEps, first[k], steps, last[k], worst[k], drift[k]);
264 }
265 // The a-priori bound on a band that is "mixed cells and their face neighbours": the disk's
266 // farthest point sits at r = 0.25 + 0.15 = 0.40 from the rotation centre, the band reaches one
267 // cell beyond it and the reduction takes the faces of those cells, i.e. r_max + 1.5 h. That is
268 // the number the guarded run must not exceed — and it is what it measures, to the printed digit.
269 const double bandBound = omega * (0.40 + 1.5 * h) * dt / h;
270 const double diskBound = omega * 0.40 * dt / h;
271 std::printf(
272 " a-priori: the disk's own faces reach %.4f (r = 0.40) and the band's outermost "
273 "faces %.4f (r = 0.40 + 1.5h)\n",
274 diskBound, bandBound);
275 std::printf(
276 " the global max (what a whole-domain limiter would report) is %.4f (corner "
277 "r = 0.7071)\n",
278 omega * 0.5 * M_SQRT2 * dt / h);
279 CHECK(worst[1] <= 1.002 * bandBound); // with the guard the band stays ON the interface
280 CHECK(worst[0] > 1.15 * bandBound); // without it the round-off wake widens the band
281 // Neither touches conservation beyond round-off: with the guard off the split advection is exact
282 // to the summation floor (3.9e-16 here at every OpenMP thread count); with wispEps = 1e-8 the
283 // clipped sub-wisp cells are the only leak, and WHICH cells fall under 1e-8 is decided by
284 // round-off, i.e. by the reduction order: measured 2.6e-14 (3 threads) ... 2.0e-13 (1 thread),
285 // 1.1e-13 on the 2-thread CI runner. 1e-12 is ~ 300 wisps of 1e-8 over the disk's ~2800 cells
286 // and 1000 steps -- still four orders under the wisp size itself.
287 CHECK(drift[0] < 1e-13);
288 CHECK(drift[1] < 1e-12);
289}
290
291// ============================================================================ gate D: LeVeque
292struct LeVequeResult {
293 double l1vol = 0, rel = 0, drift = 0, divmax = 0, minC = 0, maxC = 0;
294 long wisps = 0;
295};
296
297LeVequeResult runLeVeque(int nx, long steps, double T) {
298 const double h = 1.0 / nx;
299 Case cs;
300 cs.setup(nx, nx, nx, h);
301 vofscene::initSphere(cs.c(), cs.blk, h, 0.35, 0.35, 0.35, 0.15);
302 cs.adv.syncGhosts();
303 SField c0 = vofscene::copyOf(cs.c(), "lv0");
304 const auto d0 = cs.adv.diagnostics();
305
306 const double dt = T / steps;
307 LeVequeResult r;
308 double worst = 0.0, divmax = 0.0;
309 for (long s = 0; s < steps; ++s) {
310 const double t = (s + 0.5) * dt; // midpoint sampling => the reversal is time-symmetric
311 vofscene::fillLeVeque(cs.adv, cs.blk, h, std::cos(M_PI * t / T));
312 if (s % 25 == 0)
313 divmax = std::fmax(divmax, cs.adv.maxDiscreteDivergence(dt / h));
314 cs.adv.advect(dt, s);
315 if ((s + 1) % (steps / 8) == 0) {
316 const auto d = cs.adv.diagnostics();
317 worst = std::fmax(worst, std::fabs(d.sumC - d0.sumC) / d0.sumC);
318 }
319 }
320 const auto d1 = cs.adv.diagnostics();
321 const double l1 = vofscene::l1Diff(cs.c(), c0, cs.e(), cs.n(), cs.g());
322 r.l1vol = l1 * h * h * h;
323 r.rel = l1 / d0.sumC;
324 r.drift = std::fmax(worst, std::fabs(d1.sumC - d0.sumC) / d0.sumC);
325 r.divmax = divmax;
326 r.minC = d1.minC;
327 r.maxC = d1.maxC;
328 r.wisps = d1.wisps;
329 return r;
330}
331
332void gateLeVeque() {
333 std::printf("\n=== D LeVeque 3D deformation with time reversal (T = 3)\n");
334 const double T = 3.0;
335 // Step count from a target CFL (max|u| = 2 for this field), so the resolutions are compared at
336 // the SAME Courant number. Default 0.24 respects Weymouth's own 3D boundedness bound
337 // 1/(2(N-1)) = 1/4 (thesis eq. A.33); PECLET_VOF_LEVEQUE_CFL raises it to probe the gap between
338 // that bound and the community-standard 0.5 cap.
339 double cfl = 0.24;
340 if (const char* v = std::getenv("PECLET_VOF_LEVEQUE_CFL"); v && std::atof(v) > 0.0)
341 cfl = std::atof(v);
342 const int res[2] = {32, 64};
343 const long nst[2] = {std::lround(T * 2 * res[0] / cfl), std::lround(T * 2 * res[1] / cfl)};
344 double err[2];
345 for (int k = 0; k < 2; ++k) {
346 const LeVequeResult r = runLeVeque(res[k], nst[k], T);
347 err[k] = r.l1vol;
348 std::printf(
349 " %3d^3 %4ld steps L1(vol) %.4e L1/V %.4e drift %.3e |div|dt/h %.2e"
350 " C[%.2e,%.6f] wisps %ld\n",
351 res[k], nst[k], r.l1vol, r.rel, r.drift, r.divmax, r.minC, r.maxC, r.wisps);
352 CHECK(r.drift < 1e-13);
353 }
354 std::printf(" L1 shape-error order 32->64: %.2f\n", order(err[0], err[1]));
355
356 if (const char* v = std::getenv("PECLET_VOF_LEVEQUE_128"); v && std::atoi(v) != 0) {
357 const LeVequeResult r = runLeVeque(128, std::lround(T * 2 * 128 / cfl), T);
358 std::printf(
359 " 128^3 3200 steps L1(vol) %.4e L1/V %.4e drift %.3e |div|dt/h %.2e"
360 " C[%.2e,%.6f] wisps %ld\n",
361 r.l1vol, r.rel, r.drift, r.divmax, r.minC, r.maxC, r.wisps);
362 std::printf(" L1 shape-error order 64->128: %.2f\n", order(err[1], r.l1vol));
363 CHECK(r.drift < 1e-13);
364 } else {
365 std::printf(" (128^3 rung skipped; set PECLET_VOF_LEVEQUE_128=1 to run it)\n");
366 }
367}
368
369// =========================================================================== gate E: CFL guard
370void gateCfl() {
371 std::printf("\n=== E CFL guard\n");
372 Case cs;
373 cs.setup(16, 16, 16, 1.0 / 16);
374 vofscene::initSphere(cs.c(), cs.blk, 1.0 / 16, 0.5, 0.5, 0.5, 0.25);
375 cs.adv.syncGhosts();
376 vofscene::fillUniform(cs.adv, cs.blk, 1.0, 0.0, 0.0);
377
378 // The DEFAULT must be Weymouth's proven 3D bound 1/(2(N-1)) = 0.25 (thesis eq. A.33), not the
379 // 2D value 0.5 that the literature quotes without qualification. Pinned here so a future edit
380 // cannot silently widen the boundedness margin.
381 CHECK(cs.adv.cflLimit == 0.25);
382
383 bool threw = false;
384 std::string what;
385 try {
386 cs.adv.advect(0.26 * (1.0 / 16), 0); // past the 3D bound -> must abort
387 } catch (const std::runtime_error& ex) {
388 threw = true;
389 what = ex.what();
390 }
391 std::printf(" CFL = 0.26 (> default limit): %s\n", threw ? what.c_str() : "NO THROW");
392 CHECK(threw);
393 CHECK(what.find("CFL") != std::string::npos);
394
395 // Weymouth's bound is inclusive, so exactly 0.25 must RUN — this is the CFL every physics
396 // gate above is deliberately run at.
397 threw = false;
398 try {
399 cs.adv.advect(0.25 * (1.0 / 16), 0);
400 } catch (const std::runtime_error&) {
401 threw = true;
402 }
403 std::printf(" CFL = 0.25 (at the bound): %s\n", threw ? "THREW (wrong)" : "ran");
404 CHECK(!threw);
405
406 // The cap is deliberately raisable (2D work / probing the gap): the guard must track it.
407 cs.adv.cflLimit = 0.5;
408 threw = false;
409 try {
410 cs.adv.advect(0.49 * (1.0 / 16), 0); // under the raised cap -> must run
411 } catch (const std::runtime_error&) {
412 threw = true;
413 }
414 std::printf(" CFL = 0.49 @ cflLimit 0.5: %s\n", threw ? "THREW (wrong)" : "ran");
415 CHECK(!threw);
416}
417
418// ========================================================================== gate F: worklist
419void gateWorklist() {
420 std::printf("\n=== F worklist compaction is bitwise neutral\n");
421 const int nx = 32;
422 const double h = 1.0 / nx, T = 3.0, dt = 3.75e-3;
423 std::vector<double> out[2];
424 long mixed = 0;
425 for (int mode = 0; mode < 2; ++mode) {
426 Case cs;
427 cs.setup(nx, nx, nx, h);
428 cs.adv.useWorklist = (mode == 0);
429 vofscene::initSphere(cs.c(), cs.blk, h, 0.35, 0.35, 0.35, 0.15);
430 cs.adv.syncGhosts();
431 for (long s = 0; s < 40; ++s) {
432 vofscene::fillLeVeque(cs.adv, cs.blk, h, std::cos(M_PI * (s + 0.5) * dt / T));
433 cs.adv.advect(dt, s);
434 }
435 if (mode == 0)
436 mixed = cs.adv.lastMixedCount();
437 auto hc = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), cs.c());
438 out[mode].assign(hc.data(), hc.data() + hc.extent(0));
439 }
440 long diff = 0;
441 for (std::size_t i = 0; i < out[0].size(); ++i)
442 if (std::memcmp(&out[0][i], &out[1][i], sizeof(double)) != 0)
443 ++diff;
444 std::printf(" 40 LeVeque steps, %ld mixed cells in the last sweep: %ld / %zu cells differ\n",
445 mixed, diff, out[0].size());
446 CHECK(diff == 0);
447}
448
449// ==================================================================== gate G: the dilation trap
450void gateDilationTrap() {
451 std::printf("\n=== G the dilation-flag trap (diagnostic: what recomputing it costs)\n");
452 const int nx = 32;
453 const double h = 1.0 / nx, T = 3.0, dt = 3.75e-3;
454 double drift[2];
455 for (int mode = 0; mode < 2; ++mode) {
456 Case cs;
457 cs.setup(nx, nx, nx, h);
458 cs.adv.debugRecomputeDilation = (mode == 1);
459 vofscene::initSphere(cs.c(), cs.blk, h, 0.35, 0.35, 0.35, 0.15);
460 cs.adv.syncGhosts();
461 const double v0 = cs.adv.diagnostics().sumC;
462 for (long s = 0; s < 200; ++s) {
463 vofscene::fillLeVeque(cs.adv, cs.blk, h, std::cos(M_PI * (s + 0.5) * dt / T));
464 cs.adv.advect(dt, s);
465 }
466 drift[mode] = std::fabs(cs.adv.diagnostics().sumC - v0) / v0;
467 }
468 std::printf(
469 " 200 LeVeque steps, 32^3: frozen flag %.3e recomputed per sweep %.3e"
470 " (%.0fx worse)\n",
471 drift[0], drift[1], drift[1] / std::fmax(drift[0], 1e-300));
472 CHECK(drift[0] < 1e-13);
473 CHECK(drift[1] > 1e3 * drift[0]); // the trap must be loud here, so it stays documented
474}
475
476} // namespace
477
478int main(int argc, char** argv) {
479 Kokkos::initialize(argc, argv);
480 {
481 std::printf("VoF rung V1 (WO-E) - Weymouth-Yue split advection, backend: %s\n",
482 peclet::flow::SExec::name());
483 gateSlab();
484 gateSphere();
485 gateZalesak();
486 gateZalesakBand();
487 gateLeVeque();
488 gateCfl();
489 gateWorklist();
490 gateDilationTrap();
491 std::printf("\n%s (%d failure%s)\n", failures ? "FAILED" : "PASSED", failures,
492 failures == 1 ? "" : "s");
493 }
494 Kokkos::finalize();
495 return failures ? 1 : 0;
496}
flow — VoF rung V1: Weymouth & Yue (2010) directionally-split, exactly volume-conserving geometric ad...
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
int main()
Definition dbg_scene.cpp:13
float worst
Definition cmp.py:18
dict res
Definition disc_bc.py:230
Kokkos::View< double *, SMem > SField
double lInfDiff(SField a, SField b, I3 e, I3 n, int g)
max |a - b| over the inner region.
void fillLeVeque(WyAdvector &a, Block b, double h, double phase)
double l1Diff(SField a, SField b, I3 e, I3 n, int g)
sum |a - b| over the inner region (cell-fraction units).
SField copyOf(SField src, const char *name)
void initZalesak(SField c, Block b, double h, double cx, double cy, double r, double sw, double stop, int nsub=64)
Zalesak's slotted disk in the (x,y) plane, uniform in z.
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...
void clampFill(SField f, Block b, I3 gs, bool px, bool py, bool pz)
Zero-gradient (Neumann) fill of the ghost cells that lie OUTSIDE the global domain on a non-periodic ...
Block blockOf(const WyAdvector &a, I3 origin)
void fillRotation(WyAdvector &a, Block b, double h, double cx, double cy, double omega)
Solid-body rotation about (cx,cy) in the (x,y) plane, w = 0.
void initSlab(SField c, Block b, double h, int axis, double lo, double hi)
Slab zlo < x < zhi along axis axis, exact fractions (axis-aligned planes).
Where this block sits in the global grid: o is the global index of inner cell (0,0,...
#define CHECK(cond)