56#include <Kokkos_Core.hpp>
76 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
81std::size_t idx(
int x,
int y,
int z,
int nx,
int ny) {
82 return (std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny;
84double maxAbs(
const std::vector<double>& v) {
87 m = std::fmax(
m, std::fabs(
x));
90double maxAbsDiff(
const std::vector<double>& a,
const std::vector<double>& b) {
95 for (std::size_t i = 0;
i <
a.size(); ++
i) {
96 const double d = std::fabs(a[i] - b[i]);
105 return std::fmax(std::fmax(
maxAbs(
s.getVelocity(0)),
maxAbs(
s.getVelocity(1))),
109double maxFaceVel(S& s) {
110 return std::fmax(std::fmax(
maxAbs(
s.getFaceVelocity(0)),
maxAbs(
s.getFaceVelocity(1))),
115double checkerboardZ(
const std::vector<double>& f,
int nx,
int ny,
int nz) {
117 const int xc = nx / 2, yc =
ny / 2;
118 for (
int z = 1;
z + 1 < nz; ++
z)
119 m = std::fmax(m, std::fabs(f[idx(xc, yc, z, nx, ny)] - 0.5 * (f[idx(xc, yc, z - 1, nx, ny)] +
120 f[idx(xc, yc, z + 1, nx, ny)])));
131 double cellU = 0, faceU = 0, pErr = 0, cb = 0;
138HydroResult
hydrostatic(
double ratio,
double mu,
int steps,
bool periodic,
bool throughColour,
139 int bfp = -1,
double chebRtol = -1.0) {
140 const int N = 8,
NZ = 24;
141 const double g = 0.1;
146 s.setVelocityResidualTolerance(0.0);
148 s.setDomainBc(4, 1, 0, 0, 0);
149 s.setDomainBc(5, 1, 0, 0, 0);
151 s.setPressureGeometry(std::vector<double>((std::size_t)
N *
N *
NZ, 10.0));
156 auto heavy = [&](
int z) {
return periodic ? (
z >=
NZ / 4 &&
z < 3 *
NZ / 4) : (
z <
NZ / 2); };
157 std::vector<double> fld((std::size_t)
N *
N *
NZ);
158 for (
int z = 0;
z <
NZ; ++
z)
159 for (
int y = 0;
y <
N; ++
y)
160 for (
int x = 0;
x <
N; ++
x)
161 fld[idx(x, y, z,
N,
N)] =
162 heavy(z) ? (throughColour ? 1.0 :
ratio) : (throughColour ? 0.0 : 1.0);
164 for (
int z = 0;
z <
NZ; ++
z)
165 rbar += (heavy(z) ? ratio : 1.0) /
NZ;
173 s.setField(
"C", fld);
174 s.exchangeField(
"C");
175 s.setPropertyModel(
"rho", ClosureKind::LinearMix,
"C",
"", {1.0,
ratio - 1.0});
178 s.setField(
"rho", fld);
179 s.setDensityMode(
true);
185 s.setPropertyModel(
"force_z", ClosureKind::LinearMix,
"rho",
"", std::vector<double>{0.0, -
g});
187 s.setBodyForce(0.0, 0.0, rbar * g);
189 s.setBalancedForceProjection(bfp == 1);
191 s.setPressureChebyshev(
true, 4000, chebRtol);
192 for (
int k = 0;
k <
steps; ++
k)
196 r.faceU = maxFaceVel(s);
197 r.iters =
s.lastPressureIterations();
198 r.cb = checkerboardZ(
s.getVelocity(2),
N,
N,
NZ);
199 const auto p =
s.getPressure();
201 for (
int z = 1;
z <
NZ; ++
z) {
202 const double dp =
p[idx(
N / 2,
N / 2, z,
N,
N)] -
p[idx(
N / 2,
N / 2, z - 1,
N,
N)];
203 const double rf = 0.5 * ((heavy(z) ?
ratio : 1.0) + (heavy(z - 1) ?
ratio : 1.0));
204 r.pErr = std::fmax(
r.pErr, std::fabs(dp + g * rf - off) / (g * ratio));
209void gateHydrostatic() {
211 "\n=== T1 hydrostatic at ratio 1000, mu = 0: dP/dz == -rho_f g. Staggered: exact. "
213 " the FACE field at round-off; the balance is an exact fixed point, so the cell "
215 " and dP/dz are the transient from P = 0 -- reported, and required to DECAY.\n");
220 const Case
cases[3] = {{
"periodic, hand-set rho, frozen",
true,
false},
221 {
"walled, hand-set rho ",
false,
false},
222 {
"walled, through C (frozen) ",
false,
true}};
223 for (
const Case& c :
cases) {
224 const auto st = hydrostatic<Stag>(1000.0, 0.0, 100,
c.periodic,
c.colour);
225 const auto co = hydrostatic<Colo>(1000.0, 0.0, 100,
c.periodic,
c.colour, 0);
226 const auto co4 = hydrostatic<Colo>(1000.0, 0.0, 400,
c.periodic,
c.colour, 0);
227 std::printf(
" %s staggered cell %.3e face %.3e dP/dz %.3e cb %.3e it %ld\n",
c.name,
228 st.cellU,
st.faceU,
st.pErr,
st.cb,
st.iters);
230 " %s COLLOCATED cell %.3e face %.3e dP/dz %.3e cb %.3e it %ld (400 steps: "
231 "cell %.3e dP/dz %.3e)\n",
232 c.name, co.cellU, co.faceU, co.pErr, co.cb, co.iters, co4.cellU, co4.pErr);
240 CHECK(co.faceU < (
c.periodic ? 1.8e-9 : 1
e-12));
241 CHECK(co4.cellU < co.cellU);
242 CHECK(co4.pErr < co.pErr);
243 CHECK(co.iters < 200);
246 "\n The mu sweep (walled, hand-set rho, ratio 1000, 100 steps). The staggered balance is\n"
247 " only approached at mu > 0 (A = rho_f/dt - mu*Lap does not commute with the discrete\n"
248 " gradient at variable rho: WO-P's mu*dt^2 residue).\n");
249 for (
double mu : {0.0, 1
e-3, 1
e-2, 1
e-1}) {
250 const auto st = hydrostatic<Stag>(1000.0, mu, 100,
false,
false);
251 const auto co = hydrostatic<Colo>(1000.0, mu, 100,
false,
false, 0);
253 " mu = %-6g staggered face %.3e dP/dz %.3e | COLLOCATED face %.3e "
255 mu,
st.faceU,
st.pErr, co.faceU, co.pErr);
256 CHECK(co.faceU < 1e-12);
260void gateInvisibleSubspace() {
262 "\n=== T1b the invisible subspace: a CELL checkerboard is annihilated by centerToFace\n"
263 " (1/2(U(i)+U(i-1)) kills the odd-even mode), so the approximate projection cannot\n"
264 " see it and cannot remove it. CONTROL = the VALIDATED constant-density collocated\n"
265 " path with a plain body force, where rung V8 is completely inert. The V8 column's\n"
266 " transient lives largely in that mode, and must decay.\n");
267 const int N = 8,
NZ = 24;
268 const double g = 0.1;
269 for (
double mu : {0.0, 0.01}) {
274 s.setVelocityResidualTolerance(0.0);
275 s.setDomainBc(4, 1, 0, 0, 0);
276 s.setDomainBc(5, 1, 0, 0, 0);
277 s.setPressureGeometry(std::vector<double>((std::size_t)
N *
N *
NZ, 10.0));
278 s.setBodyForce(0.0, 0.0, -g);
279 for (
int k = 0;
k < 100; ++
k)
282 " CONTROL constant-rho collocated, body force, mu = %-5g cell |u| %.3e face |uf| %.3e"
284 mu, maxVel(s), maxFaceVel(s), checkerboardZ(
s.getVelocity(2),
N,
N,
NZ));
286 const auto a = hydrostatic<Colo>(1000.0, 0.0, 100,
false,
false, 0);
287 const auto b = hydrostatic<Colo>(1000.0, 0.0, 400,
false,
false, 0);
289 " V8 walled ratio 1000 mu = 0: 100 steps cell %.3e (cb %.3e), 400 steps cell %.3e "
290 "(cb %.3e) -> decayed %.2fx\n",
291 a.cellU,
a.cb,
b.cellU,
b.cb,
a.cellU / std::fmax(
b.cellU, 1e-300));
299std::vector<double> sphereC(
int n,
double R,
double cx,
double cy,
double cz,
int sub = 24) {
300 std::vector<double>
C((std::size_t)n * n * n, 0.0);
301 const double w = 1.0 / sub;
302 for (
int k = 0;
k <
n; ++
k)
303 for (
int j = 0;
j <
n; ++
j)
304 for (
int i = 0;
i <
n; ++
i) {
306 for (
int b = 0;
b < sub; ++
b)
307 for (
int a = 0;
a < sub; ++
a) {
308 const double px =
i + (
a + 0.5) * w, py = j + (b + 0.5) *
w;
309 const double r2 =
R *
R - (px - cx) * (px - cx) - (py - cy) * (py - cy);
312 const double h = std::sqrt(r2);
313 const double lo = std::fmax(cz - h, (
double)k), hi = std::fmin(cz + h, (
double)k + 1);
317 C[idx(i, j, k, n, n)] =
acc / (sub * sub);
323std::unique_ptr<S> makeDroplet(
int n,
double R,
double sigma,
double mu,
double rhoG,
double rhoL,
324 double kappa,
double dtFac,
int bfp = -1) {
325 auto s = std::make_unique<S>(n, n, n);
329 s->setVelocityResidualTolerance(0.0);
330 s->setPressureGeometry(std::vector<double>((std::size_t)n * n * n, 10.0));
331 s->setPressureChebyshev(
true, 500, 1e-14);
333 s->setVof(sphereC(n, R, n / 2 + 0.13, n / 2 + 0.27, n / 2 + 0.11));
334 s->setPropertyModel(
"rho", ClosureKind::LinearMix,
"C",
"", {rhoG, rhoL - rhoG});
335 s->setSurfaceTension(sigma);
337 s->setVofKappaConstant(kappa);
338 s->setDt(dtFac *
s->capillaryDt());
340 s->setBalancedForceProjection(bfp == 1);
345 double cellU = 0, faceU = 0, face30 = 0;
352DropResult runDroplet(
int n,
double R,
double sigma,
double mu,
double ratio,
double kappa,
353 double dtFac,
int steps,
int bfp = -1) {
356 auto s = makeDroplet<S>(n, R, sigma, mu, 1.0, ratio, kappa, dtFac, bfp);
357 for (
int k = 0;
k <
steps; ++
k) {
360 r.face30 = maxFaceVel(*s);
362 r.cellU = maxVel(*s);
363 r.faceU = maxFaceVel(*s);
364 r.iters =
s->lastPressureIterations();
365 }
catch (
const std::exception& e) {
372void gateStaticDroplet() {
374 "\n=== T2 static droplet, CONSTANT kappa: the face force is exactly grad(sigma*kappa*C), "
376 " the balance is an exact fixed point. On the STAGGERED grid the force goes through\n"
377 " A = rho_f/dt - mu*Lap, which does not commute with the discrete gradient at "
379 " rho or near walls, so the balance is only approached (WO-P's mu*dt^2 residue). The\n"
380 " collocated step (balanced-force projection OFF) no longer annihilates "
382 " CSF per step: a decaying transient residue remains (doc §4.6.5 model: 2.2e-4 at\n"
383 " ratio 1 after 30 steps, 1.6e-5 at ratio 1000, decaying to 2.4e-17 / 2.1e-7 in 300\n"
384 " steps; 1-2 decades above staggered at ratio 1000). Exact from step 1 needs the\n"
385 " balanced-force projection. Gated here: no throw, decay 30 -> 90 steps, and the\n"
386 " 30-step face frozen at 10x its WO-V1 value.\n");
390 const double ratios[4] = {1.0, 10.0, 100.0, 1000.0};
391 const double frozen30[4] = {9.99e-4, 2.40e-3, 1.46e-3, 6.22e-4};
392 for (
int q = 0;
q < 4; ++
q) {
393 const double ratio = ratios[
q];
394 const auto st = runDroplet<Stag>(32, 8.0, 1.0, 0.1, ratio, 0.25, 0.5, 30);
395 const auto co = runDroplet<Colo>(32, 8.0, 1.0, 0.1, ratio, 0.25, 0.5, 90, 0);
397 std::printf(
" ratio %6g staggered THREW: %.90s\n", ratio,
st.what.c_str());
399 std::printf(
" ratio %6g staggered cell %.4e face %.4e it %ld\n", ratio,
st.cellU,
402 std::printf(
" ratio %6g COLLOCATED THREW: %.90s\n", ratio, co.what.c_str());
405 " ratio %6g COLLOCATED face %.4e (30 steps) -> %.4e (90 steps), cell %.4e "
407 ratio, co.face30, co.faceU, co.cellU, co.iters);
411 CHECK(co.faceU < co.face30);
412 CHECK(co.face30 <= frozen30[q]);
413 CHECK(co.iters < 500);
416 "\n The ratio-1000 mu sweep (the WO-T face-acceleration form went unstable at mu = 0.1,\n"
417 " ~4x per step; the predictor form is stable at every mu dt). Decay 30 -> 90 steps.\n");
420 const double mus[3] = {0.0, 0.01, 0.1};
421 const double frozenMu[3] = {6.74e-4, 6.50e-4, 6.22e-4};
422 for (
int q = 0;
q < 3; ++
q) {
423 const double mu = mus[
q];
424 const auto st = runDroplet<Stag>(32, 8.0, 1.0, mu, 1000.0, 0.25, 0.5, 30);
425 const auto co = runDroplet<Colo>(32, 8.0, 1.0, mu, 1000.0, 0.25, 0.5, 90, 0);
427 " ratio 1000, mu = %-5g staggered face %.4e%s | COLLOCATED face %.4e -> "
429 mu,
st.faceU,
st.threw ?
" (THREW)" :
"", co.face30, co.faceU, co.threw ?
" (THREW)" :
"");
431 CHECK(co.faceU < co.face30);
432 CHECK(co.face30 <= frozenMu[q]);
442void gateBalancedOn() {
443 std::printf(
"\n=== T1/T1b/T2 ON (the V8 default): static balances exact from step 1\n");
448 const Case
cases[3] = {{
"periodic, hand-set rho, frozen",
true,
false},
449 {
"walled, hand-set rho ",
false,
false},
450 {
"walled, through C (frozen) ",
false,
true}};
459 for (
const Case& c :
cases)
461 const auto co = hydrostatic<Colo>(1000.0, 0.0, steps,
c.periodic,
c.colour, -1,
462 c.periodic ? 1e-14 : -1.0);
463 std::printf(
" T1 %s %3d step(s) cell %.3e face %.3e dP/dz %.3e it %ld\n",
c.name, steps,
464 co.cellU, co.faceU, co.pErr, co.iters);
465 CHECK(co.faceU < 1e-12);
466 CHECK(co.cellU < 1e-10);
467 CHECK(co.pErr < 1e-10);
478 const auto d = hydrostatic<Colo>(1000.0, 0.0, 100,
true,
false, -1, -1.0);
479 std::printf(
" T1 periodic at the default rtol, 100 steps: face %.3e cell %.3e dP/dz %.3e\n",
480 d.faceU,
d.cellU,
d.pErr);
484 const auto a = hydrostatic<Colo>(1000.0, 0.0, 100,
false,
false);
485 std::printf(
" T1b walled ratio 1000, 100 steps: cell %.3e (cb %.3e)\n",
a.cellU,
a.cb);
488 const double ratios[4] = {1.0, 10.0, 100.0, 1000.0};
489 for (
double ratio : ratios) {
490 const auto co = runDroplet<Colo>(32, 8.0, 1.0, 0.1, ratio, 0.25, 0.5, 30);
491 std::printf(
" T2 ratio %6g cell %.4e face %.4e it %ld%s\n", ratio, co.cellU, co.faceU,
492 co.iters, co.threw ?
" THREW" :
"");
493 const double tol = (
ratio == 1.0) ? 1e-14 : 1
e-13;
494 CHECK(!co.threw && co.faceU < tol && co.cellU < tol);
496 for (
double mu : {0.0, 0.01, 0.1}) {
497 const auto st = runDroplet<Stag>(32, 8.0, 1.0, mu, 1000.0, 0.25, 0.5, 30);
498 const auto co = runDroplet<Colo>(32, 8.0, 1.0, mu, 1000.0, 0.25, 0.5, 30);
500 " T2 ratio 1000 mu %-5g staggered (OFF) face %.4e | COLLOCATED face %.4e "
502 mu,
st.faceU, co.faceU, co.cellU);
503 CHECK(!co.threw && co.faceU < 1e-13);
509void gateUniformReduction() {
511 "\n=== T3 uniform rho, mu = 0: the V8 face-force predictor IS the validated cell-force one\n"
512 " (avg_f(P(i)-P(i-s)) is the central difference) -> agreement at round-off\n");
513 const int N = 16,
NZ = 8;
514 std::vector<std::vector<double>> uu, pp;
515 for (
int var = 0; var < 2; ++var) {
520 s.setPressureGeometry(std::vector<double>((std::size_t)
N *
N *
NZ, 10.0));
521 s.setBodyForce(1e-2, 3e-3, 0.0);
524 s.setField(
"rho", std::vector<double>((std::size_t)
N *
N *
NZ, 2.0));
525 s.setDensityMode(
true);
526 s.setPressurePcg(
true, 400, 1e-14);
528 s.setPressurePcg(
true, 400, 1e-14);
530 for (
int k = 0;
k < 30; ++
k)
532 uu.push_back(
s.getVelocity(0));
533 pp.push_back(
s.getPressure());
536 std::printf(
" max|du| %.3e (|u| %.3e) max|dP| %.3e (|P| %.3e)\n", du,
maxAbs(uu[0]), dp,
538 CHECK(du < 1e-13 * std::fmax(
maxAbs(uu[0]), 1e-30));
539 CHECK(dp < 1e-11 * std::fmax(
maxAbs(pp[0]), 1e-30));
545 "\n=== T4 the bridge (G6): the collocated colour transport is the SAME kernel on the SAME\n"
546 " faces. uf_(i) sits at i-1/2 = flow's staggered u(i), so a staggered solver handed\n"
547 " the collocated projected face field must reproduce the colour BITWISE.\n");
549 const int steps = 20;
556 co.setPressureGeometry(std::vector<double>((std::size_t)
N *
N *
N, 10.0));
557 co.setPressurePcg(
true, 400, 1e-14);
560 std::vector<double> c0((std::size_t)
N *
N *
N, 0.0);
561 for (
int z = 0;
z <
N; ++
z)
562 for (
int y = 0;
y <
N; ++
y)
563 for (
int x = 0;
x <
N; ++
x) {
564 const double dx =
x + 0.5 - (
N / 2 + 0.13), dy = y + 0.5 - (
N / 2 + 0.27),
565 dz =
z + 0.5 - (
N / 2 + 0.11);
566 c0[idx(x, y, z,
N,
N)] = (
dx *
dx +
dy *
dy +
dz *
dz < 36.0) ? 1.0 : 0.0;
572 std::vector<double>
u((std::size_t)
N *
N *
N),
v(
u.size()),
w(
u.size());
573 const double k = 2.0 * M_PI /
N;
574 for (
int z = 0;
z <
N; ++
z)
575 for (
int y = 0;
y <
N; ++
y)
576 for (
int x = 0;
x <
N; ++
x) {
577 const double X =
x + 0.5,
Y =
y + 0.5,
Z =
z + 0.5;
578 u[idx(x, y, z,
N,
N)] = std::sin(k * Z) + 0.7 * std::cos(k * Y);
579 v[idx(x, y, z,
N,
N)] = std::sin(k * X) + 0.7 * std::cos(k * Z);
580 w[idx(x, y, z,
N,
N)] = std::sin(k * Y) + 0.7 * std::cos(k * X);
587 const std::vector<double> uf = co.getFaceVelocity(0),
vf = co.getFaceVelocity(1),
588 wf = co.getFaceVelocity(2);
589 const std::vector<double>
c1 = co.getVof();
590 std::printf(
" collocated max|div(open uf)| after the seeding step = %.3e\n",
591 co.maxOpenDivergence());
592 CHECK(co.maxOpenDivergence() < 1e-10);
599 st.setPressureGeometry(std::vector<double>((std::size_t)
N *
N *
N, 10.0));
602 st.setField(
"u", uf);
603 st.setField(
"v",
vf);
604 st.setField(
"w", wf);
605 st.setVofStepParity(co.vofStepParity());
607 for (
int k = 0;
k <
steps; ++
k) {
608 co.advectVofKinematic(0.1);
609 st.advectVofKinematic(0.1);
613 " collocated vs staggered on the SAME face field, %d kinematic steps: max|dC| = "
618 std::printf(
" max|C - C0| over the run = %.3e (sum C: collocated %.15e, staggered %.15e)\n",
619 maxAbsDiff(co.getVof(), c1), co.vofDiagnostics().sumC,
st.vofDiagnostics().sumC);
625 std::printf(
"\n=== T5 scope: what the rung refuses, loudly\n");
627 std::vector<double>
solid((std::size_t)
N *
N *
N);
628 for (
int z = 0;
z <
N; ++
z)
629 for (
int y = 0;
y <
N; ++
y)
630 for (
int x = 0;
x <
N; ++
x) {
631 const double dx =
x + 0.5 -
N / 2.0,
dy =
y + 0.5 -
N / 2.0,
dz =
z + 0.5 -
N / 2.0;
632 solid[idx(x, y, z,
N,
N)] = std::sqrt(dx * dx + dy * dy + dz * dz) - 4.0;
639 s.setSolid(solid,
true);
643 }
catch (
const std::exception&) {
646 std::printf(
" enable_vof + immersed solid (collocated) -> throws: %s\n",
647 threw ?
"yes" :
"NO");
655 s.setSolid(solid,
true);
657 s.setField(
"rho", std::vector<double>((std::size_t)
N *
N *
N, 1.0));
658 s.setDensityMode(
true);
662 }
catch (
const std::exception&) {
665 std::printf(
" set_density_mode + immersed solid -> throws: %s\n",
666 threw ?
"yes" :
"NO");
674 s.setPressureGeometry(std::vector<double>((std::size_t)
N *
N *
N, 10.0));
677 s.enableVofMomentum(1.0, 10.0);
678 }
catch (
const std::exception&) {
681 std::printf(
" enable_vof_momentum (collocated) -> throws: %s\n",
682 threw ?
"yes" :
"NO");
690 s.setPressureGeometry(std::vector<double>((std::size_t)
N *
N *
N, 10.0));
692 s.setField(
"rho", std::vector<double>((std::size_t)
N *
N *
N, 1.0));
693 s.setDensityMode(
true);
694 s.setRhoFaceHarmonic(
true);
698 }
catch (
const std::exception&) {
701 std::printf(
" set_rho_face_harmonic (collocated) -> throws: %s\n",
702 threw ?
"yes" :
"NO");
710 s.setPressureGeometry(std::vector<double>((std::size_t)
N *
N *
N, 10.0));
712 s.setField(
"rho", std::vector<double>((std::size_t)
N *
N *
N, 1.0));
713 s.setDensityMode(
true);
714 s.setIncrementalPressure(
false);
718 }
catch (
const std::exception&) {
721 std::printf(
" non-incremental pressure (collocated V8) -> throws: %s\n",
722 threw ?
"yes" :
"NO");
729int main(
int argc,
char** argv) {
730 Kokkos::initialize(argc, argv);
733 gateInvisibleSubspace();
736 gateUniformReduction();
742 std::fprintf(stderr,
"\n%d CHECK(s) failed\n",
failures);
745 std::printf(
"\nAll rung V8 collocated gates passed.\n");
Weymouth-Yue split advection of a colour field on an extended (inner + ghost) block.
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
long L3(int x, int y, int z, I3 e)
hydrostatic(ratio, mu=0.0, steps=100, g=0.1, dt=1.0, N=8, NZ=24)
static double maxAbs(const std::vector< double > &a)
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)
static std::vector< double > colour(int m)