44#include <Kokkos_Core.hpp>
68using peclet::flow::vof::plicAlpha;
71using peclet::flow::vof::plicNormalizeL1;
72using peclet::flow::vof::plicVolume;
74using peclet::flow::vof::vofIndexNormal;
75using peclet::flow::vof::VofMetric;
76using peclet::flow::vof::vofPhysNormal;
82 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
87std::size_t idx(
int x,
int y,
int z,
int nx,
int ny) {
88 return (std::size_t)x + (std::size_t)y * nx + (std::size_t)z * (std::size_t)nx * ny;
96 if (
const char* e = std::getenv(
"PECLET_P3C_AREA"))
97 s.setPhaseChangeArea(std::atoi(e));
109 if (
const char* e = std::getenv(
"PECLET_P3F_CARRY"))
110 s.setPhaseChangeCarryConserve(e[0] !=
'0');
111 if (
const char* e = std::getenv(
"PECLET_P3F_KAPPA"))
112 s.setPhaseChangeFitCurvature(std::atof(e));
117 if (
const char* e = std::getenv(
"PECLET_P3G_ORDER"))
118 s.setPhaseChangeEnergyOrder(std::atoi(e));
119 if (
const char* e = std::getenv(
"PECLET_P3G_OPMDOT"))
120 s.setPhaseChangeMdotOperator(e[0] !=
'0');
121 if (
const char* e = std::getenv(
"PECLET_P3G_GFMORDER"))
122 s.setPhaseChangeGfmOrder(std::atoi(e));
123 if (
const char* e = std::getenv(
"PECLET_P3G_CURVDIST"))
124 s.setPhaseChangeCurvatureDistance(e[0] !=
'0');
129 const double s2 = std::sqrt(2.0), hex = 3.0 * std::sqrt(3.0) / 4.0;
130 CHECK(
plicArea(1.0, 0.0, 0.0, plicAlpha(1.0, 0.0, 0.0, 0.5)) == 1.0);
131 CHECK(
plicArea(0.0, 0.0, -1.0, plicAlpha(0.0, 0.0, -1.0, 0.3)) == 1.0);
132 CHECK(std::fabs(
plicArea(0.5, 0.5, 0.0, plicAlpha(0.5, 0.5, 0.0, 0.5)) - s2) < 1e-15);
133 const double t = 1.0 / 3.0;
134 CHECK(std::fabs(
plicArea(t, t, t, plicAlpha(t, t, t, 0.5)) - hex) < 1e-15);
139 const double ms[][3] = {{1, 0, 0}, {0.5, 0.5, 0}, {0.02, 0.98, 0},
140 {
t,
t,
t}, {0.2, 0.3, 0.5}, {-0.2, 0.3, -0.5},
141 {1
e-9, 0.4, 0.6}, {0.1, 0.1, 0.8}, {0.45, 0.45, 0.1}};
143 for (
const auto& m : ms)
144 for (double
v = 0.02;
v < 0.999;
v += 0.02) {
145 const double al = plicAlpha(m[0], m[1], m[2], v);
146 const double q = std::sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
147 const double d = 1
e-6;
149 q * (plicVolume(m[0], m[1], m[2], al + d) - plicVolume(m[0], m[1], m[2], al - d)) /
151 worst = std::fmax(worst, std::fabs(
plicArea(m[0], m[1], m[2], al) - ref));
153 std::printf(
"K1 plicArea: max |analytic - |m|2 dV/dalpha (FD)| = %.3e\n", worst);
177 const double ns[][3] = {{1, 0, 0}, {0, 0, -1}, {1, 1, 0}, {1, -1, 0},
178 {1, 1, 1}, {1, 2, 3}, {0.2, 0.3, 0.5}, {3, 1, -2},
179 {0.05, 1.0, 0.02}, {1, 0.5, 0.25}};
180 double worstPlic = 0.0, worstSplit = 0.0, worstPart = 0.0, worstColourAxis = 0.0;
181 int rows = 0, axisRows = 0;
182 for (
const auto& nn :
ns) {
183 const double q = std::sqrt(nn[0] * nn[0] + nn[1] * nn[1] + nn[2] * nn[2]);
184 const double u[3] = {nn[0] /
q, nn[1] /
q, nn[2] /
q};
185 const bool axis = (std::fabs(u[0]) == 1.0 || std::fabs(u[1]) == 1.0 || std::fabs(u[2]) == 1.0);
186 for (
double off = -0.86;
off < 0.87;
off += 0.07) {
190 bool crossed =
false;
191 for (
int k = 0;
k < 8; ++
k) {
194 const double phi =
u[0] * (
p[0] - 0.5) + u[1] * (p[1] - 0.5) +
u[2] * (
p[2] - 0.5) - off;
201 if (k && ((v[k].psi < 0.0) != (
v[0].psi < 0.0)))
207 double m[3] = {
u[0],
u[1],
u[2]};
208 const double l1 = plicNormalizeL1(m);
209 const double alpha = (0.5 * (
m[0] +
m[1] +
m[2])) +
off / l1;
214 double sumC = 0.0, sumS = 0.0;
215 for (
int lc = 0; lc < 8; ++lc) {
219 worstPlic = std::fmax(worstPlic, std::fabs(sumC - exact) / exact);
220 worstSplit = std::fmax(worstSplit, std::fabs(sumS - sumC) / exact);
222 for (
int drop = 1; drop < 8; drop += 3) {
224 for (
int k = 0;
k < 8; ++
k) {
226 if ((k & drop) == drop && k != 0)
229 double sc = 0.0,
ss = 0.0;
230 for (
int lc = 0; lc < 8; ++lc) {
234 worstPart = std::fmax(worstPart, std::fabs(sc - sumC) / exact);
235 worstPart = std::fmax(worstPart, std::fabs(ss - sumC) / exact);
239 if (axis && std::fabs(off) < 0.4) {
245 for (
int k = 0;
k < 8; ++
k) {
248 const double phi =
u[0] * (
p[0] - 0.5) + u[1] * (p[1] - 0.5) +
u[2] * (
p[2] - 0.5) - off;
249 const double cc = std::fmin(1.0, std::fmax(0.0, 0.5 - phi));
253 for (
int lc = 0; lc < 8; ++lc)
255 worstColourAxis = std::fmax(worstColourAxis, std::fabs(sa - exact) / exact);
260 "K6 joined sheet on a PLANE (%d rows, %d axis-aligned): max rel |PLIC-source sum - "
261 "exact| %.3e, |split - centroid| %.3e, |retargeted - plain| %.3e, "
262 "|colour-source axis - exact| %.3e\n",
263 rows, axisRows, worstPlic, worstSplit, worstPart, worstColourAxis);
268 CHECK(worstPlic < 1e-13);
269 CHECK(worstSplit < 1e-13);
270 CHECK(worstPart < 1e-13);
271 CHECK(worstColourAxis == 0.0);
288 const double ms[][3] = {{1, 0, 0}, {0, -1, 0}, {0.5, 0.5, 0}, {0.02, 0.98, 0},
289 {0.2, 0.3, 0.5}, {-0.2, 0.3, -0.5}, {0.1, 0.1, 0.8}, {0.45, 0.45, 0.1}};
290 double worst1 = 0.0, worst2 = 0.0, worstMetric = 0.0, worstNrm = 0.0;
292 for (
const auto& m : ms) {
293 const double q = std::sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
296 for (
int k = 1;
k < 3; ++
k)
297 if (std::fabs(m[k]) > std::fabs(m[d]))
299 const int d1 = (
d + 1) % 3, d2 = (d + 2) % 3;
300 for (
double v = 0.05;
v < 0.999;
v += 0.05) {
301 const double al = plicAlpha(m[0], m[1], m[2], v);
304 for (
int qq = 0; qq < 3; ++qq)
305 for (
int pp = 0; pp < 3; ++pp)
306 hh[pp + 3 * qq] = -(m[d1] * (pp - 1) + m[d2] * (qq - 1)) /
m[
d];
308 worstMetric = std::fmax(worstMetric, std::fabs(metric - q / std::fabs(m[d])));
311 const double dot = (
ns[0] *
m[0] +
ns[1] *
m[1] +
ns[2] *
m[2]) / q;
312 worstNrm = std::fmax(worstNrm, std::fabs(std::fabs(dot) - 1.0));
317 worst1 = std::fmax(worst1, std::fabs(a1 - a0) / a0);
318 worst2 = std::fmax(worst2, std::fabs(a2 - a0) / a0);
319 if (std::fabs(m[d]) == q) {
320 if (a1 == a0 && a2 == a0)
328 "K5 area modes on a PLANE: max rel |metric - 1| %.3e, |normal| %.3e, "
329 "mode1 vs mode0 %.3e, mode2 vs mode0 %.3e (%d axis-aligned rows BITWISE)\n",
330 worstMetric, worstNrm, worst1, worst2, exactAxis);
331 CHECK(worstMetric < 1e-15);
332 CHECK(worstNrm < 1e-15);
333 CHECK(worst1 < 1e-14);
334 CHECK(worst2 < 1e-14);
337 const double t1[3] = {0.0, 1.0, 0.0}, t2[3] = {0.0, 0.0, 1.0}, nn[3] = {1.0, 0.0, 0.0};
338 double a[6] = {0.1, 0.0, 0.0, 0.05, 0.0, 0.05},
ns[3];
340 CHECK(ns[0] == 1.0 && ns[1] == 0.0 && ns[2] == 0.0);
343 CHECK(std::fabs(ns[1] + 0.3 / std::sqrt(1.09)) < 1e-15);
349 double m[3] = {0.37, -0.51, 0.28};
350 double n[3] = {1, 0, 0};
352 const double alpha = plicAlpha(m[0], m[1], m[2], 0.4);
354 const double g[3] = {0.7, -1.3, 2.1};
355 const double Tsat = 3.5;
356 const double exact =
g[0] *
n[0] +
g[1] *
n[1] +
g[2] *
n[2];
359 for (
int dz = -2;
dz <= 2; ++
dz)
360 for (
int dy = -2;
dy <= 2; ++
dy)
361 for (
int dx = -2;
dx <= 2; ++
dx) {
362 if (!dx && !dy && !dz)
377 std::printf(
"K2 one-sided gradient fit: %.17g vs exact %.17g (n = %.4f %.4f %.4f, %d samples)\n",
378 fit, exact, n[0], n[1], n[2], fg.
n);
380 CHECK(std::fabs(fit - exact) < 1e-14);
388void quadraticGradientGate() {
389 double m[3] = {0.37, -0.51, 0.28};
390 double n[3] = {1, 0, 0};
392 const double alpha = plicAlpha(m[0], m[1], m[2], 0.4);
394 const double Tsat = 3.5,
g = 2.19,
q = -0.83;
396 for (
int dz = -2;
dz <= 2; ++
dz)
397 for (
int dy = -2;
dy <= 2; ++
dy)
398 for (
int dx = -2;
dx <= 2; ++
dx) {
399 if (!dx && !dy && !dz)
412 "K3 quadratic fit on a curved profile: quad %.17g vs exact %.17g (lin %.17g, "
415 CHECK(std::fabs(quad - g) < 1e-13);
416 CHECK(std::fabs(lin - g) > 0.1);
424 for (
double C : {0.15, 0.5, 0.87}) {
425 const double phic = 0.5 -
C, nd = -1.0;
428 std::printf(
"K4 GFM theta at C = %.2f: gas %.17g (exact %.17g), liquid %.17g (exact %.17g)\n",
429 C, thGas, 1.5 - C, thLiq, 0.5 + C);
430 CHECK(std::fabs(thGas - (1.5 - C)) < 1e-15);
431 CHECK(std::fabs(thLiq - (0.5 + C)) < 1e-15);
438 for (
double nd = 1e-3; nd > 1
e-14; nd *= 0.5) {
440 CHECK(std::fabs(th - prev) < 1e-3);
443 std::printf(
"K4 GFM theta continuity to n_d -> 0: limit %.17g (thetaMax 1.9)\n", prev);
469void gfmSecondOrderGate() {
470 const double a = 0.83,
b = -1.47,
c = 0.62;
471 double worstLap = 0.0, worstFlux = 0.0, worstLin = 0.0, worst1 = 0.0;
472 std::printf(
"K5 second-order GFM row (T = %.2f %+.2f x %+.2f x^2, exact T'' = %.4f):\n", a, b, c,
474 for (
int i = 1;
i <= 19; ++
i) {
475 const double th = 0.05 *
i;
476 const double Tg =
a +
b * th +
c * th * th, T0 =
a, Tm =
a -
b +
c;
480 const double lap = r2.aGamma * (
Tg - T0) + r2.aBehind * (Tm - T0);
482 const double q2 = r2.aGamma * (T0 -
Tg) + (r2.aBehind - 1.0) * (T0 - Tm);
483 const double q1 =
r1.aGamma * (T0 -
Tg) + (
r1.aBehind - 1.0) * (T0 - Tm);
485 const double qlin = r2.aGamma * (0.0 -
b * th) + (r2.aBehind - 1.0) * (0.0 - (-
b));
486 worstLap = std::fmax(worstLap, std::fabs(lap - 2 * c));
487 worstFlux = std::fmax(worstFlux, std::fabs(q2 + (b + c)));
488 worstLin = std::fmax(worstLin, std::fabs(qlin + b));
489 worst1 = std::fmax(worst1, std::fabs(q1 + (b + c)));
492 " theta %.2f a_G %8.4f a_B %6.4f | T'' %.17g (err %.2e) Q %.17g "
493 "(err %.2e) | order-1 Q err %+.3e\n",
494 th, r2.aGamma, r2.aBehind, lap, std::fabs(lap - 2 * c), q2, std::fabs(q2 + (b + c)),
498 "K5 worst over theta in [0.05, 0.95]: |T''_row - T''| %.3e, |Q - Q_exact| %.3e "
499 "(linear profile %.3e); the SHIPPED two-point row: %.3e\n",
500 worstLap, worstFlux, worstLin, worst1);
501 CHECK(worstLap < 1e-14);
502 CHECK(worstFlux < 1e-14);
503 CHECK(worstLin < 1e-14);
507 CHECK(
r.aGamma == 1.0 &&
r.aBehind == 1.0);
510 for (
double th : {0.1, 0.5, 1.3, 1.9}) {
512 CHECK(
rf.aGamma == 1.0 / th &&
rf.aBehind == 1.0);
516 for (
double nd : {-1.0, -0.8, -0.5, 0.6, 0.9}) {
519 const double kap = -2.0 / 12.0;
520 const double want = std::fabs(0.3 - 1.0 * nd + 0.25 * kap * (1.0 - nd * nd)) / std::fabs(nd);
522 CHECK(std::fabs(got - std::fmin(std::fmax(want, 0.1), 1.9)) < 1e-15);
524 std::printf(
"K5 curvature-consistent theta: inert at kappa = 0, closed form otherwise\n");
529 const int nx = 64,
ny = 4, nz = 4;
530 const double x0 = 32.25, mdot = 0.02,
dt = 1.0;
535 std::vector<double>
C((std::size_t)nx * ny * nz, 0.0);
536 for (
int z = 0;
z < nz; ++
z)
537 for (
int y = 0;
y <
ny; ++
y)
538 for (
int x = 0;
x < nx; ++
x)
539 C[idx(x, y, z, nx, ny)] = std::fmin(1.0, std::fmax(0.0, x0 - x));
542 s.enablePhaseChange(1.0, 1.0, 1.0);
545 s.setMassFluxUniform(mdot);
547 const auto c =
s.getVof();
551 return sum / (
ny * nz);
553 double worst = 0.0,
lo = 1e30, hi = -1e30;
556 for (
int k = 1;
k <= 1000; ++
k) {
557 s.applyPhaseChange(dt);
558 worst = std::fmax(worst, std::fabs(
pos() - (x0 - mdot * dt * k)));
559 const auto d =
s.phaseChangeDiagnostics();
560 lo = std::fmin(lo,
d.minC);
561 hi = std::fmax(hi,
d.maxC);
562 clips +=
d.deficitCells;
563 redist +=
d.redistributed;
566 "P0a planar regression, 1000 steps: max |x - (x0 - mdot t/rho_l)| = %.3e ; C in [%.17g, "
567 "%.17g] ; %ld cell-crossings clipped, |redistributed| = %.6g\n",
568 worst, lo, hi, clips, redist);
569 CHECK(worst < 1e-12);
577 const int nx = 64,
ny = 4, nz = 4;
578 const double rg = 1.0, rl = 100.0, mdot = 0.01,
x0 = 32.25;
583 s.setDomainBc(0, 1, 0, 0, 0);
584 s.setDomainBc(1, 1, 0, 0, 0);
585 s.setPressureGeometry(std::vector<double>((std::size_t)nx * ny * nz, 1.0));
586 std::vector<double>
C((std::size_t)nx * ny * nz, 0.0);
587 for (
int z = 0;
z < nz; ++
z)
588 for (
int y = 0;
y <
ny; ++
y)
589 for (
int x = 0;
x < nx; ++
x)
590 C[idx(x, y, z, nx, ny)] = std::fmin(1.0, std::fmax(0.0, x0 - x));
594 s.enablePhaseChange(rg, rl, 1.0);
597 s.setMassFluxUniform(mdot);
598 const double Sc = mdot * (1.0 / rg - 1.0 / rl);
599 std::vector<double> sink((std::size_t)nx * ny * nz, 0.0);
600 for (
int z = 0;
z < nz; ++
z)
601 for (
int y = 0;
y <
ny; ++
y)
602 sink[idx(nx - 6, y, z, nx, ny)] = -Sc;
603 s.setDivergenceSource(sink);
604 s.setPressureFcg(
true, 400, 1e-12);
605 for (
int k = 0;
k < 20; ++
k)
607 const auto u =
s.getVelocity(0);
608 const auto src =
s.getField(
"pc_source");
609 const auto usr =
s.getField(
"div_source");
610 const auto v =
s.getVelocity(1),
w =
s.getVelocity(2);
611 const double ug = mdot * (1.0 / rg - 1.0 / rl);
612 double liq = 0.0, plat = 0.0, spread = 0.0, dmax = 0.0;
613 double p0 =
u[idx(40, 0, 0, nx, ny)];
614 for (
int x = 2;
x < 30; ++
x)
615 liq = std::fmax(liq, std::fabs(u[idx(x, 0, 0, nx, ny)]));
616 for (
int x = 35;
x < 56; ++
x) {
617 plat +=
u[idx(x, 0, 0, nx, ny)] / 21.0;
618 spread = std::fmax(spread, std::fabs(u[idx(x, 0, 0, nx, ny)] - p0));
620 for (
int z = 0;
z + 1 < nz; ++
z)
621 for (
int y = 0;
y + 1 <
ny; ++
y)
622 for (
int x = 0;
x + 1 < nx; ++
x) {
623 const double d = (
u[idx(x + 1, y, z, nx, ny)] -
u[idx(x, y, z, nx, ny)]) +
624 (v[idx(x, y + 1, z, nx, ny)] -
v[idx(x, y, z, nx, ny)]) +
625 (w[idx(x, y, z + 1, nx, ny)] -
w[idx(x, y, z, nx, ny)]);
627 std::fmax(dmax, std::fabs(d - src[idx(x, y, z, nx, ny)] - usr[idx(x, y, z, nx, ny)]));
629 const auto d =
s.phaseChangeDiagnostics();
631 "P0b ratio 100, closed column + balanced sink: u_gas = %.17g (exact %.17g, rel %.3e), "
632 "plateau spread %.3e, max|u_liquid| %.3e, max|div - S| %.3e, source sum %.6g into %ld "
633 "cells, pressure iters %d\n",
634 plat, ug, (plat - ug) / ug, spread, liq, dmax,
d.sourceSum,
d.sourceCells,
635 s.lastPressureIterations());
636 CHECK(std::fabs((plat - ug) / ug) < 1e-10);
637 CHECK(liq < 1e-12 * ug + 1e-15);
639 CHECK(
s.lastPressureIterations() < 400);
641 CHECK(std::fabs(u[idx(32, 0, 0, nx, ny)]) < 1e-15);
642 CHECK(std::fabs(u[idx(33, 0, 0, nx, ny)]) < 1e-15);
647 const double rhs = St / std::sqrt(M_PI);
648 double a = 1
e-8,
b = 5.0;
649 for (
int i = 0;
i < 200; ++
i) {
650 const double m = 0.5 * (
a +
b);
651 ((
m * std::exp(m * m) * std::erf(m) - rhs) > 0 ? b :
a) =
m;
653 return 0.5 * (
a +
b);
656double stefanRun(
int N,
double& exact) {
657 const double St = 1.0,
alpha = 1.0, x0p = 0.10, xep = 0.25, Fo = 0.5;
659 const double t0 = (x0p / (2 *
lam)) * (x0p / (2 *
lam)) /
alpha;
660 const double te = (xep / (2 *
lam)) * (xep / (2 *
lam)) /
alpha;
661 const int ny = 4, nz = 4;
663 int ns = (
int)std::llround((te - t0) / (Fo /
D));
664 const double dt = (te -
t0) / ns;
669 s.setDomainBc(0, 1, 0, 0, 0);
670 s.setDomainBc(1, 1, 0, 0, 0);
671 s.setPressureGeometry(std::vector<double>((std::size_t)
N * ny * nz, 1.0));
672 const double xg = x0p *
N;
673 std::vector<double>
C((std::size_t)
N * ny * nz, 0.0),
T((std::size_t)
N * ny * nz, 0.0);
674 for (
int z = 0;
z < nz; ++
z)
675 for (
int y = 0;
y <
ny; ++
y)
676 for (
int x = 0;
x <
N; ++
x) {
677 C[idx(x, y, z,
N, ny)] = std::fmin(1.0, std::fmax(0.0, (x + 1) - xg));
678 const double xp = (
x + 0.5) /
N;
679 T[idx(x, y, z,
N, ny)] = xp < x0p ? 1.0 - std::erf(lam * xp / x0p) / std::erf(lam) : 0.0;
683 s.addScalar(
"T", D, 1, 60);
684 s.setScalarBc(
"T", 0, 2, 1.0);
685 s.setScalarBc(
"T", 1, 2, 0.0);
687 s.enablePhaseChange(1.0, 1.0, 1.0);
690 s.setPhaseChangeThermal(
"T", 0.0, D, D, 0.0);
694 s.setPhaseChangePlaneDirichlet(
false);
695 s.setPhaseChangeQuadraticFit(
false);
696 for (
int k = 0;
k <
ns; ++
k) {
697 s.applyPhaseChange(dt);
700 const auto c =
s.getVof();
704 exact = 2 *
lam * std::sqrt(alpha * te) *
N;
705 const auto d =
s.phaseChangeDiagnostics();
707 "P1 Stefan N=%d (%d steps, St=1, lambda=%.6f): layer = %.5f cells, exact %.5f, rel %+.4f %% "
708 "; C in [%.3e, %.17g], unresolved %.3e, fallback %ld\n",
709 N, ns, lam,
N - sum / (ny * nz), exact, 100.0 * ((
N - sum / (ny * nz)) - exact) / exact,
710 d.minC,
d.maxC,
d.unresolved,
d.fallbackCells);
713 return N - sum / (
ny * nz);
718 const double x = stefanRun(64, exact);
719 CHECK(std::fabs(x - exact) / exact < 0.02);
727double stefanRunP23(
int N,
double& exact) {
728 const double St = 1.0,
alpha = 1.0, x0p = 0.10, xep = 0.25, Fo = 0.5;
730 const double t0 = (x0p / (2 *
lam)) * (x0p / (2 *
lam)) /
alpha;
731 const double te = (xep / (2 *
lam)) * (xep / (2 *
lam)) /
alpha;
732 const int ny = 4, nz = 4;
734 int ns = (
int)std::llround((te - t0) / (Fo /
D));
735 const double dt = (te -
t0) / ns;
740 s.setDomainBc(0, 1, 0, 0, 0);
741 s.setDomainBc(1, 1, 0, 0, 0);
742 s.setPressureGeometry(std::vector<double>((std::size_t)
N * ny * nz, 1.0));
743 const double xg = x0p *
N;
744 std::vector<double>
C((std::size_t)
N * ny * nz, 0.0),
T((std::size_t)
N * ny * nz, 0.0);
745 for (
int z = 0;
z < nz; ++
z)
746 for (
int y = 0;
y <
ny; ++
y)
747 for (
int x = 0;
x <
N; ++
x) {
748 C[idx(x, y, z,
N, ny)] = std::fmin(1.0, std::fmax(0.0, (x + 1) - xg));
749 const double xp = (
x + 0.5) /
N;
750 T[idx(x, y, z,
N, ny)] = xp < x0p ? 1.0 - std::erf(lam * xp / x0p) / std::erf(lam) : 0.0;
754 s.addScalar(
"T", D, 1, 60);
755 s.setScalarBc(
"T", 0, 2, 1.0);
756 s.setScalarBc(
"T", 1, 2, 0.0);
758 s.enablePhaseChange(1.0, 1.0, 1.0);
761 s.setPhaseChangeThermal(
"T", 0.0, D, D, 0.0);
762 s.setPhaseChangePlaneDirichlet(
true);
763 s.setPhaseChangeQuadraticFit(
true);
764 for (
int k = 0;
k <
ns; ++
k) {
765 s.applyPhaseChange(dt);
768 const auto c =
s.getVof();
772 exact = 2 *
lam * std::sqrt(alpha * te) *
N;
773 const double layer =
N - sum / (
ny * nz);
774 const auto d =
s.phaseChangeDiagnostics();
776 "P1' Stefan N=%d, PLANE-ANCHORED Dirichlet + QUADRATIC fit: layer = %.5f cells, "
777 "exact %.5f, rel %+.4f %% ; C in [%.3e, %.17g]\n",
778 N, layer, exact, 100.0 * (layer - exact) / exact,
d.minC,
d.maxC);
786 const double x = stefanRunP23(64, exact);
787 CHECK(std::fabs(x - exact) / exact < 0.001);
796void energyIdentity(
double rcpRatio) {
797 const int nx = 32,
ny = 8, nz = 8;
798 const double T0 = 3.25,
U = 0.17;
799 std::vector<double>
C((std::size_t)nx * ny * nz, 0.0),
T((std::size_t)nx * ny * nz, T0);
800 for (
int z = 0;
z < nz; ++
z)
801 for (
int y = 0;
y <
ny; ++
y)
802 for (
int x = 0;
x < nx; ++
x)
803 C[idx(x, y, z, nx, ny)] =
804 std::fmin(1.0, std::fmax(0.0, 0.37 * x + 0.21 * y - 0.13 * z - 3.4));
809 s.setPressureGeometry(std::vector<double>((std::size_t)nx * ny * nz, 1.0));
812 s.addScalar(
"T", 0.0, 1, 1);
814 s.setVelocity(0, std::vector<double>((std::size_t)nx * ny * nz,
U));
815 s.enablePhaseChange(1.0, 1.0, 1.0);
818 s.setPhaseChangeThermal(
"T", T0, 1.0, 1.0, 0.0);
819 s.setPhaseChangeEnergy(1.0, rcpRatio);
821 for (
int k = 0;
k < 20; ++
k) {
822 s.advectVofKinematic(1.0);
823 const auto t =
s.getField(
"T");
828 "ENERGY uniform-T identity at rho c_p ratio %g: max |T - T0| = %.3e over 20 "
836double suckingB(
double ja,
double rr) {
837 double b = ja / std::sqrt(M_PI);
838 for (
int i = 0;
i < 300; ++
i) {
839 const double g =
b * rr;
840 b = ja * std::exp(-g * g) / (std::sqrt(M_PI) * std::erfc(g));
851 const double ratio = 10.0, ja = 1.0, alpha_l = 1.0, x0p = 0.10, xep = 0.25, Fo = 0.5, cfl = 0.2;
852 const double rr = 1.0 /
ratio,
b = suckingB(ja, rr);
853 const double t0 = (x0p / (2 *
b)) * (x0p / (2 *
b)) / alpha_l;
854 const double te = (xep / (2 *
b)) * (xep / (2 *
b)) / alpha_l;
855 const int ny = 4, nz = 4;
856 const double al = alpha_l *
N *
N, rho_l = 1.0, rho_v = rr, cpl = 1.0;
857 const double k_l = al * rho_l * cpl, k_v = k_l /
ratio, dT = 1.0;
858 const double h_lv = rho_l * cpl * dT / (rho_v * ja);
862 s.setDomainBc(0, 1, 0, 0, 0);
863 s.setDomainBc(1, 3, 0, 0, 0);
864 s.setPressureGeometry(std::vector<double>((std::size_t)
N * ny * nz, 1.0));
865 const double X0 = x0p *
N;
866 std::vector<double>
C((std::size_t)
N * ny * nz, 0.0),
T((std::size_t)
N * ny * nz, 0.0);
867 for (
int z = 0;
z < nz; ++
z)
868 for (
int y = 0;
y <
ny; ++
y)
869 for (
int x = 0;
x <
N; ++
x) {
870 C[idx(x, y, z,
N, ny)] = std::fmin(1.0, std::fmax(0.0, (x + 1) -
X0));
871 const double xp = (
x + 0.5) /
N;
873 const double sv = xp / (2 * std::sqrt(alpha_l * t0)) - b * (1.0 - rr);
874 T[idx(x, y, z,
N, ny)] = dT - dT * std::erfc(sv) / std::erfc(b * rr);
880 s.setPressureFcg(
true, 4000, 1e-10);
881 s.addScalar(
"T", al, 1, 60);
882 s.setScalarBc(
"T", 0, 2, 0.0);
883 auto farT = [&](
double tt) {
884 const double sv = 1.0 / (2 * std::sqrt(alpha_l * tt)) - b * (1.0 - rr);
885 return dT - dT * std::erfc(sv) / std::erfc(b * rr);
887 s.setScalarBc(
"T", 1, 2, farT(t0));
889 s.enablePhaseChange(rho_v, rho_l, h_lv);
892 s.setPhaseChangeThermal(
"T", 0.0, k_v, k_l, 0.0);
893 s.setPhaseChangeEnergy(rho_v * cpl, rho_l * cpl);
895 int ns = 0, capped = 0;
898 const double Xd =
b * std::sqrt(alpha_l / tcur) *
N;
899 double dt = std::fmin(cfl / std::fmax(Xd * (1.0 - rr), 1e-30), Fo / al);
900 dt = std::fmin(dt, te - tcur);
902 s.setScalarBc(
"T", 1, 2, farT(tcur + dt));
906 itmax = std::max<long>(itmax,
s.lastPressureIterations());
907 capped +=
s.lastPressureIterations() >= 4000 ? 1 : 0;
909 const auto c =
s.getVof();
913 const double layer =
N - sum / (
ny * nz);
914 const double exact = 2 *
b * std::sqrt(alpha_l * te) *
N;
915 const auto d =
s.phaseChangeDiagnostics();
917 "P2 sucking interface N=%d (%d steps, ratio %g, Ja %g, b = %.6f): layer = %.5f "
918 "cells, exact %.5f, rel %+.4f %% ; pressure iters max %d/4000 (capped %d), "
919 "band_div %.3e, C in [%.3e, %.17g]\n",
920 N, ns, ratio, ja, b, layer, exact, 100.0 * (layer - exact) / exact, (
int)itmax, capped,
921 d.bandDiv,
d.minC,
d.maxC);
923 CHECK(std::fabs(layer - exact) / exact < 0.01);
928 const int nx = 32,
ny = 4, nz = 4;
929 std::vector<double>
C((std::size_t)nx * ny * nz, 0.0);
930 for (
int z = 0;
z < nz; ++
z)
931 for (
int y = 0;
y <
ny; ++
y)
932 for (
int x = 0;
x < nx; ++
x)
933 C[idx(x, y, z, nx, ny)] = std::fmin(1.0, std::fmax(0.0, 16.4 - x));
941 b.enablePhaseChange(1.0, 1.0, 1.0);
943 b.setMassFluxUniform(0.0);
944 for (
int k = 0;
k < 10; ++
k)
945 b.applyPhaseChange(1.0);
946 const auto ca =
a.getVof(), cb =
b.getVof();
948 for (std::size_t i = 0;
i <
ca.size(); ++
i)
949 diff = std::fmax(diff, std::fabs(ca[i] - cb[i]));
950 std::printf(
"INERT mdot == 0: max |C_pc - C_ref| = %.3e (bitwise expected)\n", diff);
965void anisoAreaAndGradient() {
966 std::printf(
"\nK4/K5 (Phase 3): anisotropic area and one-sided gradient\n");
967 const double hs[3][3] = {{1.0, 1.0, 1.0}, {1.0, 1.0, 2.0}, {2.0, 1.5, 4.0}};
968 const double ns[4][3] = {
969 {1.0, 0.0, 0.0}, {0.6, -0.8, 0.0}, {0.4243, -0.7071, 0.5657}, {0.5774, 0.5774, 0.5774}};
973 long bitA = 0, totA = 0;
974 for (
int q = 0;
q < 3; ++
q) {
975 const VofMetric
g{{hs[
q][0], hs[
q][1], hs[
q][2]}};
976 for (
int p = 0;
p < 4; ++
p) {
977 const double*
n =
ns[
p];
980 vofIndexNormal(n, g, m);
981 const double alpha = 0.5 * (
m[0] +
m[1] +
m[2]);
987 for (
int k = 1;
k < 3; ++
k)
988 if (std::fabs(n[k]) > std::fabs(n[d]))
990 const int t1 = (
d + 1) % 3, t2 = (d + 2) % 3;
991 const int sub = 2000;
992 const double w1 =
g.h[t1] / sub, w2 =
g.h[t2] / sub;
994 for (
int b = 0;
b < sub; ++
b)
995 for (
int a = 0;
a < sub; ++
a) {
997 x[t1] = (
a + 0.5) * w1;
998 x[t2] = (
b + 0.5) * w2;
1000 const double dPlane = 0.5 * (
n[0] *
g.h[0] +
n[1] *
g.h[1] +
n[2] *
g.h[2]);
1001 x[
d] = (dPlane -
n[t1] *
x[t1] -
n[t2] *
x[t2]) / n[d];
1002 if (x[d] >= 0.0 && x[d] <=
g.h[d])
1005 const double proj = (double)inside * w1 * w2;
1006 const double ref = proj / std::fabs(n[d]);
1007 const double rel = std::fabs(got - ref) / std::fmax(ref, 1e-12);
1008 worstA = std::fmax(worstA, rel);
1012 if (got ==
plicArea(m[0], m[1], m[2], alpha))
1018 " K4a plicAreaMetric vs a 2000^2 footprint quadrature: max rel %.3e (gate 2e-3,\n"
1019 " the quadrature's own 1/sub floor); isotropic == plicArea bitwise %ld/%ld\n",
1020 worstA, bitA, totA);
1021 CHECK(worstA < 2e-3);
1022 CHECK(bitA == totA);
1025 double worstMc = 0.0;
1026 long bitMc = 0, totMc = 0;
1027 for (
int q = 0;
q < 3; ++
q) {
1028 const VofMetric
g{{hs[
q][0], hs[
q][1], hs[
q][2]}};
1029 for (
int p = 0;
p < 4; ++
p) {
1030 const double*
n =
ns[
p];
1032 vofIndexNormal(n, g, m);
1033 const double qn = std::sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
1034 const double mu[3] = {
m[0] / qn,
m[1] / qn,
m[2] / qn};
1035 for (
double off = -0.3;
off <= 0.31;
off += 0.15) {
1037 bool crossed =
false;
1038 for (
int k = 0;
k < 8; ++
k) {
1045 mu[0] * (pc[0] - 0.5) + mu[1] * (pc[1] - 0.5) + mu[2] * (pc[2] - 0.5) - off;
1052 if (k && ((v[k].psi < 0.0) != (
v[0].psi < 0.0)))
1058 const double alpha = 0.5 * (mu[0] + mu[1] + mu[2]) + off;
1060 if (!(exact > 1e-6))
1062 double sum = 0.0, sumIso = 0.0;
1063 for (
int lc = 0; lc < 8; ++lc) {
1067 worstMc = std::fmax(worstMc, std::fabs(sum - exact) / exact);
1077 " K4b joined sheet on a stretched plane: max rel |sum - exact| %.3e (gate 1e-13);"
1078 "\n isotropic == the unit-metric overload bitwise %ld/%ld\n",
1079 worstMc, bitMc, totMc);
1080 CHECK(worstMc < 1e-13);
1081 CHECK(bitMc == totMc);
1089 double worstPhys = 0.0, worstIdx = 0.0;
1090 for (
int q = 1;
q < 3; ++
q) {
1091 const VofMetric
g{{hs[
q][0], hs[
q][1], hs[
q][2]}};
1092 for (
int p = 0;
p < 4; ++
p) {
1093 const double*
n =
ns[
p];
1095 vofIndexNormal(n, g, m);
1097 const double sMet = vofPhysNormal(m, g, np);
1099 const double alpha = 0.5 * (
m[0] +
m[1] +
m[2]);
1101 const double Gtrue = 3.7,
Tg = 1.5;
1103 for (
int dz = -2;
dz <= 2; ++
dz)
1104 for (
int dy = -2;
dy <= 2; ++
dy)
1105 for (
int dx = -2;
dx <= 2; ++
dx) {
1106 if (dx == 0 && dy == 0 && dz == 0)
1108 const double di[3] = {(double)dx, (
double)
dy, (double)dz};
1114 const double T =
Tg + Gtrue * phiP;
1115 const double wP =
pcGradWeight(dp[0], dp[1], dp[2], np);
1120 (
double)dx, (
double)dy, (
double)dz);
1126 worstPhys = std::fmax(worstPhys, std::fabs(
pcGradSolve(fp) - Gtrue) / Gtrue);
1127 worstIdx = std::fmax(worstIdx, std::fabs(
pcGradSolve(fi) - Gtrue) / Gtrue);
1132 " K5 one-sided dT/dn on a linear profile: PHYSICAL offsets rel err %.3e"
1133 " (gate 1e-12)\n the same fit on INDEX offsets: %.3e <- what the metric"
1135 worstPhys, worstIdx);
1136 CHECK(worstPhys < 1e-12);
1137 CHECK(worstIdx > 1e-2);
1143 Kokkos::initialize(argc, argv);
1149 quadraticGradientGate();
1151 gfmSecondOrderGate();
1152 anisoAreaAndGradient();
1157 energyIdentity(1e4);
1163 std::fprintf(stderr,
"%d CHECK(s) failed\n",
failures);
1166 std::printf(
"all phase-change gates passed\n");
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
exact(Ds, K, hc, gs, nmax=1)
double pcGradSolve2(const PcGradFit &f)
The QUADRATIC one-sided fit (WO-P23): T - T_G = G phi + Q phi^2, returning G.
void pcGradAdd(PcGradFit &f, double w, double phi, double T, double Tg)
Add one pure-phase sample at normal distance phi with temperature T.
double pcCentreDistance(double mx, double my, double mz, double alpha)
Signed distance (in cells) from the CELL CENTRE to the PLIC plane, measured along n and POSITIVE ON T...
double pcGfmTheta(double phiC, double nd, double s, double thetaMin, double thetaMax)
thetaMin keeps the coefficient finite when the plane grazes the cell centre (the standard GFM clamp);...
@ kMcDepositSplit
clipped to each cell's cube, each cell gets its own piece
@ kMcDepositCentroid
the whole triangle to the cell holding its centroid
PcGfmRow pcGfmRow(double theta, bool behind, int order)
double hfAreaElement(const double h[9])
The height function's area element sqrt(1 + h_x^2 + h_y^2), with the same central differences hfPatch...
double pcOffsetDistance(double phic, const double n[3], double dx, double dy, double dz)
Signed distance from the plane of the neighbour at INTEGER cell offset d, given the centre distance p...
double plicArea(double mx, double my, double mz, double alpha)
Area of the PLIC polygon {x in [0,1]^3 : m.x = alpha}, as a multiple of h^2.
double pcGfmThetaK(double phiC, double nd, double s, double kappa, double thetaMin, double thetaMax)
WO-P3g item 3 — the curvature-consistent GFM distance.
@ kAreaNormal
variant B: plicArea rebuilt on the cascade's normal.
@ kAreaMetric
variant A: the PLIC footprint x the cascade's slope. DEFAULT.
@ kAreaPlic
rung P0/P1: plicArea on the MYC normal. The old numbers.
@ kMcSrcColour
the C = 1/2 level set (the WO's design)
@ kMcSrcPlic
the zero of the PLIC-reconstructed signed distance (the smooth variant)
double plicAreaMetric(double mx, double my, double mz, double alpha, const VofMetric &g)
The PHYSICAL area of the PLIC polygon of an ANISOTROPIC cell, in hRef^2 (flow/doc/anisotropic_vof....
void pvSurfaceNormal(const double a[6], const double t1[3], const double t2[3], const double nn[3], double m[3])
The unit normal of the fitted paraboloid z = a0 + a1 x + a2 y + ... AT THE ORIGIN of its own frame (t...
double interfaceAreaFromNormal(int mode, double mx, double my, double mz, double alpha, double c, const double ns[3], const VofMetric &g)
The projection axis of kAreaMetric is the one ns is most aligned with — the column direction on tiers...
void mcCornerPos(int k, double p[3])
Local coordinates of dual-cube corner k (bit 0 = x, bit 1 = y, bit 2 = z).
void hfSurfaceNormal(const double h[9], int d, double m[3])
The unit surface normal implied by the SAME 3x3 patch of heights, in world components.
double pcGradSolve(const PcGradFit &f)
The fitted normal derivative dT/dn (0 if the side carried no usable sample).
double pcUnitNormal(double mx, double my, double mz, double n[3])
Unit (L2) interface normal from a PLIC normal of any scale.
double mcCubeCornerArea(const McVertex v[8], int lc, int src, int deposit, const VofMetric &g)
The interfacial area inside one dual cube that belongs to its local corner lc.
double pcGradWeight(double dx, double dy, double dz, const double n[3])
Malan's collinearity weight for a sample at cell offset d: w = xi / |d| with xi = (d....
fit(a, t, t0=40.0, t1=160.0)
One dual-cube corner = one cell centre.
Accumulator for dT/dn fitted through the interface value: T - T_G = G * phi, so G = sum_j w_j phi_j (...
static constexpr double U
static double stefanLambda(double St)
void applyAreaModeEnv(S &s)