27#include <Kokkos_Core.hpp>
45 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
54 std::array<bool, 3> per{
true,
true,
true};
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);
60 const I3 e = adv.extent();
61 const int g = adv.ghost();
62 const std::array<bool, 3> p = per;
64 const I3 gs{nx, ny, nz};
65 adv.exchange = [e, g, p, bb, gs](
SField f) {
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(); }
76double order(
double coarse,
double fine) {
77 return (fine > 0.0 && coarse > 0.0) ? std::log2(coarse / fine) : 0.0;
82 std::printf(
"\n=== A slab translation (planar interface, uniform diagonal flow)\n");
84 const double h = 1.0 / nx;
86 cs.setup(nx, nx, nx, h);
90 const auto d0 =
cs.adv.diagnostics();
93 const double dt = 0.25 *
h;
95 for (
long s = 0;
s < 1024; ++
s)
97 const auto d1 =
cs.adv.diagnostics();
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,
104 std::printf(
" C range [%.17g, %.17g] wisps %ld\n", d1.minC, d1.maxC, d1.wisps);
105 CHECK(drift < 1e-13);
107 CHECK(d1.minC >= -1e-14 && d1.maxC <= 1.0 + 1e-14);
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;
119 cs.setup(nx, nx, nx, h);
123 const auto d0 =
cs.adv.diagnostics();
125 const double dt = 0.25 *
h;
126 const long steps = 4L * nx;
128 for (
long s = 0;
s <
steps; ++
s)
129 cs.adv.advect(dt, s);
130 const auto d1 =
cs.adv.diagnostics();
134 const double drift = std::fabs(d1.sumC -
d0.sumC) /
d0.sumC;
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);
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);
146 std::printf(
" -- long run, 32^3, 1024 steps (8 laps)\n");
148 const double h = 1.0 / nx;
150 cs.setup(nx, nx, nx, h);
154 const auto d0 =
cs.adv.diagnostics();
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);
165 const auto d1 =
cs.adv.diagnostics();
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);
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;
179 cs.setup(nx, nx, nz, h);
183 const auto d0 =
cs.adv.diagnostics();
197 cs.adv.cflLimit = 0.5;
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;
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();
210 const double rel = l1 /
d0.sumC;
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);
221 CHECK(rel > 0.002 && rel < 0.2);
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) {
242 cs.setup(nx, nx, nz, h);
245 const auto d0 =
cs.adv.diagnostics();
246 cs.adv.cflLimit = 0.5;
247 cs.adv.interfaceLocalCfl =
true;
248 cs.adv.wispEps = (
k == 0) ? 0.0 : 1
e-8;
250 for (
long t = 0;
t <
steps; ++
t) {
251 cs.adv.advect(dt, t);
252 const double c =
cs.adv.lastCfl();
256 worst[
k] = std::fmax(worst[k], c);
258 const auto d1 =
cs.adv.diagnostics();
259 drift[
k] = std::fabs(d1.sumC -
d0.sumC) /
d0.sumC;
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]);
269 const double bandBound = omega * (0.40 + 1.5 *
h) * dt / h;
270 const double diskBound = omega * 0.40 *
dt /
h;
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);
276 " the global max (what a whole-domain limiter would report) is %.4f (corner "
278 omega * 0.5 * M_SQRT2 * dt / h);
279 CHECK(worst[1] <= 1.002 * bandBound);
280 CHECK(worst[0] > 1.15 * bandBound);
287 CHECK(drift[0] < 1e-13);
288 CHECK(drift[1] < 1e-12);
292struct LeVequeResult {
293 double l1vol = 0,
rel = 0, drift = 0, divmax = 0, minC = 0, maxC = 0;
297LeVequeResult runLeVeque(
int nx,
long steps,
double T) {
298 const double h = 1.0 / nx;
300 cs.setup(nx, nx, nx, h);
304 const auto d0 =
cs.adv.diagnostics();
308 double worst = 0.0, divmax = 0.0;
309 for (
long s = 0;
s <
steps; ++
s) {
310 const double t = (
s + 0.5) * dt;
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);
320 const auto d1 =
cs.adv.diagnostics();
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);
333 std::printf(
"\n=== D LeVeque 3D deformation with time reversal (T = 3)\n");
334 const double T = 3.0;
340 if (
const char* v = std::getenv(
"PECLET_VOF_LEVEQUE_CFL");
v && std::atof(v) > 0.0)
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)};
345 for (
int k = 0;
k < 2; ++
k) {
346 const LeVequeResult
r = runLeVeque(res[k], nst[k], T);
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);
354 std::printf(
" L1 shape-error order 32->64: %.2f\n",
order(err[0], err[1]));
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);
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));
365 std::printf(
" (128^3 rung skipped; set PECLET_VOF_LEVEQUE_128=1 to run it)\n");
371 std::printf(
"\n=== E CFL guard\n");
373 cs.setup(16, 16, 16, 1.0 / 16);
381 CHECK(
cs.adv.cflLimit == 0.25);
386 cs.adv.advect(0.26 * (1.0 / 16), 0);
387 }
catch (
const std::runtime_error& ex) {
391 std::printf(
" CFL = 0.26 (> default limit): %s\n", threw ?
what.c_str() :
"NO THROW");
393 CHECK(
what.find(
"CFL") != std::string::npos);
399 cs.adv.advect(0.25 * (1.0 / 16), 0);
400 }
catch (
const std::runtime_error&) {
403 std::printf(
" CFL = 0.25 (at the bound): %s\n", threw ?
"THREW (wrong)" :
"ran");
407 cs.adv.cflLimit = 0.5;
410 cs.adv.advect(0.49 * (1.0 / 16), 0);
411 }
catch (
const std::runtime_error&) {
414 std::printf(
" CFL = 0.49 @ cflLimit 0.5: %s\n", threw ?
"THREW (wrong)" :
"ran");
420 std::printf(
"\n=== F worklist compaction is bitwise neutral\n");
422 const double h = 1.0 / nx,
T = 3.0,
dt = 3.75e-3;
423 std::vector<double>
out[2];
425 for (
int mode = 0; mode < 2; ++mode) {
427 cs.setup(nx, nx, nx, h);
428 cs.adv.useWorklist = (mode == 0);
431 for (
long s = 0;
s < 40; ++
s) {
433 cs.adv.advect(dt, s);
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));
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)
444 std::printf(
" 40 LeVeque steps, %ld mixed cells in the last sweep: %ld / %zu cells differ\n",
445 mixed, diff, out[0].size());
450void gateDilationTrap() {
451 std::printf(
"\n=== G the dilation-flag trap (diagnostic: what recomputing it costs)\n");
453 const double h = 1.0 / nx,
T = 3.0,
dt = 3.75e-3;
455 for (
int mode = 0; mode < 2; ++mode) {
457 cs.setup(nx, nx, nx, h);
458 cs.adv.debugRecomputeDilation = (mode == 1);
461 const double v0 =
cs.adv.diagnostics().sumC;
462 for (
long s = 0;
s < 200; ++
s) {
464 cs.adv.advect(dt, s);
466 drift[mode] = std::fabs(
cs.adv.diagnostics().sumC - v0) / v0;
469 " 200 LeVeque steps, 32^3: frozen flag %.3e recomputed per sweep %.3e"
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]);
478int main(
int argc,
char** argv) {
479 Kokkos::initialize(argc, argv);
481 std::printf(
"VoF rung V1 (WO-E) - Weymouth-Yue split advection, backend: %s\n",
482 peclet::flow::SExec::name());
491 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...
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
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,...