69#include <Kokkos_Core.hpp>
80 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
85void checkClose(
double a,
double b,
double tol,
const char* what) {
86 if (!(std::fabs(a - b) <= tol)) {
87 std::fprintf(stderr,
"CHECK_CLOSE failed (%s): %.17g vs %.17g (|d| = %.3e > %.3e)\n", what, a,
88 b, std::fabs(a - b), tol);
93bool bitwiseEqual(
const std::vector<double>& a,
const std::vector<double>& b) {
94 if (
a.size() !=
b.size())
96 return std::memcmp(
a.data(),
b.data(),
a.size() *
sizeof(
double)) == 0;
99double maxAbs(
const std::vector<double>& a) {
102 if (!(std::fabs(
v) <=
m))
109double relDiffScaled(
const std::vector<double>& a,
const std::vector<double>& b,
double k) {
112 for (std::size_t i = 0;
i <
a.size(); ++
i) {
113 const double d = std::fabs(a[i] - k * b[i]);
126 std::vector<double>
u,
v,
w,
p;
127 std::array<double, 3>
spacing{};
133Fields runChannel(
int nx,
int ny,
int nz,
double lam,
bool arm) {
135 const double H = 1.0 /
lam;
136 const double ylo = (std::round(0.30 * ny) + 0.5) * H, yhi = (std::round(0.70 * ny) + 0.5) * H;
139 s.setPhysicalDomain({nx *
H,
ny *
H, nz *
H}, {0.0, 0.0, 0.0}, {nx,
ny, nz});
143 s.setBodyForce(
F, 0.0, 0.0);
144 s.setVelocityIterations(400);
145 s.setVelocityResidualTolerance(1e-14);
146 s.setPressurePcg(
true, 80, 1e-13);
148 const std::vector<double> cx =
s.cellCentres(0), cy =
s.cellCentres(1), cz =
s.cellCentres(2);
149 std::vector<double>
sdf((std::size_t)nx * ny * nz);
150 for (
int z = 0;
z < nz; ++
z)
151 for (
int y = 0;
y <
ny; ++
y)
152 for (
int x = 0;
x < nx; ++
x)
153 sdf[(std::size_t)
x + (std::size_t)y * nx + (std::size_t)
z * nx *
ny] =
154 std::min(cy[y] - ylo, yhi - cy[y]);
157 s.setSolid(sdf,
false);
158 for (
int it = 0;
it < 300; ++
it)
161 f.u =
s.getVelocity(0);
162 f.v =
s.getVelocity(1);
163 f.w =
s.getVelocity(2);
164 f.p =
s.getPressure();
165 f.spacing =
s.spacing();
172Fields runSphere(
int N,
double lam,
bool arm,
bool scene) {
174 const double H = 1.0 /
lam;
175 const double R = 0.30 *
N *
H;
178 s.setPhysicalDomain({
N *
H,
N *
H,
N *
H}, {0.0, 0.0, 0.0}, {
N,
N,
N});
182 s.setBodyForce(
F, 0.0, 0.0);
183 s.setAdvection(
false);
184 s.setVelocityIterations(200);
185 s.setVelocityResidualTolerance(1e-14);
186 s.setPressurePcg(
true, 120, 1e-13);
188 namespace g = peclet::core::geom;
189 g::SceneBuilder<double>
b;
190 b.addLeaf(g::kSphere, {
R});
191 g::Transform<double>
tr;
193 tr.translation = peclet::core::Vec3<double>{0.5 *
N *
H, 0.5 *
N *
H, 0.5 *
N *
H};
194 b.addInstance(0, tr);
195 std::vector<int> ni, ii;
196 std::vector<double> nr, ir;
197 b.encode(ni, nr, ii, ir);
198 s.setScene(ni, nr, ii, ir,
true);
199 s.setSolidFromScene(
true);
201 const std::vector<double> cx =
s.cellCentres(0), cy =
s.cellCentres(1), cz =
s.cellCentres(2);
202 const double c = 0.5 *
N *
H;
203 std::vector<double>
sdf((std::size_t)
N *
N *
N);
204 for (
int z = 0;
z <
N; ++
z)
205 for (
int y = 0;
y <
N; ++
y)
206 for (
int x = 0;
x <
N; ++
x)
207 sdf[(std::size_t)
x + (std::size_t)y *
N + (std::size_t)
z *
N *
N] =
208 std::sqrt((cx[x] - c) * (cx[x] - c) + (cy[y] - c) * (cy[y] - c) +
209 (cz[z] - c) * (cz[z] - c)) -
211 s.setSolid(sdf,
true);
213 for (
int it = 0;
it < 60; ++
it)
216 f.u =
s.getVelocity(0);
217 f.v =
s.getVelocity(1);
218 f.w =
s.getVelocity(2);
219 f.p =
s.getPressure();
220 f.spacing =
s.spacing();
221 f.div =
s.maxOpenDivergence();
229std::vector<double> sphereColour(
int n,
double R,
double cx,
double cy,
double cz,
int sub = 24) {
230 std::vector<double>
C((std::size_t)n * n * n, 0.0);
231 const double w = 1.0 / sub;
232 for (
int k = 0;
k <
n; ++
k)
233 for (
int j = 0;
j <
n; ++
j)
234 for (
int i = 0;
i <
n; ++
i) {
236 for (
int b = 0;
b < sub; ++
b)
237 for (
int a = 0;
a < sub; ++
a) {
238 const double px =
i + (
a + 0.5) * w, py = j + (b + 0.5) *
w;
239 const double r2 =
R *
R - (px - cx) * (px - cx) - (py - cy) * (py - cy);
242 const double hh = std::sqrt(r2);
243 const double lo = std::fmax(cz - hh, (
double)k), hi = std::fmin(cz + hh, (
double)k + 1);
247 C[(std::size_t)i + (std::size_t)
j *
n + (std::size_t)k * n * n] = acc / (sub * sub);
261Drop runDroplet(
int n,
double Rcells,
double lam,
bool constantKappa,
int steps) {
262 const double H = 1.0 /
lam;
263 const double SIGMA = 1.0;
266 s.setPhysicalDomain({
n *
H,
n *
H,
n *
H}, {0.0, 0.0, 0.0}, {
n,
n,
n});
270 s.setVelocityResidualTolerance(0.0);
271 s.setPressureGeometry(std::vector<double>((std::size_t)n * n * n, 10.0 * H));
272 s.setPressureChebyshev(
true, 500, 1e-14);
274 const std::vector<double>
C = sphereColour(n, Rcells, n / 2 + 0.13, n / 2 + 0.27, n / 2 + 0.11);
277 s.setSurfaceTension(
SIGMA);
279 d.kappa = 2.0 / (Rcells *
H);
281 s.setVofKappaConstant(
d.kappa);
283 s.computeVofCurvature();
284 d.dtCap =
s.capillaryDt();
285 s.setDt(0.5 *
d.dtCap);
286 for (
int k = 0;
k <
steps; ++
k)
288 for (
int c = 0;
c < 3; ++
c)
289 d.maxU = std::fmax(
d.maxU,
maxAbs(
s.getVelocity(c)));
291 const std::vector<double>
p =
s.getPressure();
292 const std::size_t inside =
293 (std::size_t)(n / 2) + (std::size_t)(n / 2) *
n + (std::size_t)(n / 2) *
n *
n;
294 const std::size_t outside = 1 + (std::size_t)1 * n + (std::size_t)1 *
n *
n;
295 d.dp =
p[inside] -
p[outside];
301 std::printf(
"=== units_vof_sigma ===\n");
303 const double Rc = 8.0;
304 const double lam = 1
e2;
307 const Drop
a = runDroplet(
N, Rc, 1.0,
true, 20);
308 const Drop
b = runDroplet(
N, Rc, lam,
true, 20);
309 std::printf(
" constant kappa: max|u| %.3e (lam=1) vs %.3e (lam=%g) -> %.3e rescaled\n",
a.maxU,
310 b.maxU, lam,
b.maxU * lam);
313 const double uScale = 1.0 *
a.kappa / 0.1;
314 CHECK(
a.maxU < 1e-14 * uScale);
315 CHECK(
b.maxU * lam < 1e-14 * uScale);
316 checkClose(
a.dp, 1.0 *
a.kappa, 1e-9 *
a.kappa,
"Young-Laplace at lam=1");
317 checkClose(
b.dp / lam,
a.dp, 1e-9 *
a.kappa,
"Young-Laplace at lam=1e2 (rescaled)");
318 std::printf(
" Young-Laplace: dp %.9e vs sigma*kappa %.9e (lam=%g: %.9e rescaled)\n",
a.dp,
319 a.kappa, lam,
b.dp / lam);
320 checkClose(
b.kappa / lam,
a.kappa, 1e-12 *
a.kappa,
"kappa scales as 1/length");
321 checkClose(
b.dtCap,
a.dtCap, 1e-12 *
a.dtCap,
322 "capillary_dt invariant under a length-unit change");
323 std::printf(
" capillary_dt %.9e vs %.9e (invariant: sigma and the time unit are)\n",
a.dtCap,
327 const Drop
ca = runDroplet(
N, Rc, 1.0,
false, 20);
328 const Drop cb = runDroplet(
N, Rc, lam,
false, 20);
329 const double ru = std::fabs(
ca.maxU - cb.maxU * lam) / std::fabs(
ca.maxU);
330 const double rp = std::fabs(
ca.dp - cb.dp / lam) / std::fabs(
ca.dp);
332 " computed kappa: max|u| %.6e vs %.6e rescaled (rel %.3e); dp %.6e vs %.6e "
334 ca.maxU, cb.maxU * lam, ru,
ca.dp, cb.dp / lam, rp);
348 std::vector<double> yc;
349 double ylo = 0.0, yhi = 0.0;
350 std::array<double, 3>
w{1.0, 1.0, 1.0};
354Channel runChannelBox(
int nx,
int ny,
int nz,
const std::array<double, 3>& extent,
int jlo,
int jhi,
356 const double RHO = 1.0,
MU = 0.1,
F = 0.01,
DT = 50.0;
359 s.setPhysicalDomain(extent, {0.0, 0.0, 0.0}, {nx,
ny, nz});
363 s.setBodyForce(
F, 0.0, 0.0);
364 s.setVelocityIterations(400);
365 s.setVelocityResidualTolerance(1e-14);
366 s.setPressurePcg(
true, 80, 1e-13);
368 ch.yc =
s.cellCentres(1);
371 std::vector<double>
sdf((std::size_t)nx * ny * nz);
372 for (
int z = 0;
z < nz; ++
z)
373 for (
int y = 0;
y <
ny; ++
y)
374 for (
int x = 0;
x < nx; ++
x)
375 sdf[(std::size_t)
x + (std::size_t)y * nx + (std::size_t)
z * nx *
ny] =
376 std::min(ch.yc[y] - ch.ylo, ch.yhi - ch.yc[y]);
377 s.setSolid(sdf,
false);
378 for (
int it = 0;
it < 300; ++
it)
380 ch.f.u =
s.getVelocity(0);
381 ch.f.v =
s.getVelocity(1);
382 ch.f.w =
s.getVelocity(2);
383 ch.f.p =
s.getPressure();
384 ch.f.spacing =
s.spacing();
385 const auto&
u =
s.unitScales();
386 ch.w = {
u.w[0],
u.w[1],
u.w[2]};
394void checkChannel(
const char* what,
const Channel& ch,
int nx,
int ny,
int nz,
int jlo,
int jhi,
396 const double MU = 0.1,
F = 0.01;
397 const double H = ch.yhi - ch.ylo;
398 const double umax =
F *
H *
H / (8.0 *
MU);
399 std::printf(
" %s: spacing (%.17g, %.17g, %.17g) aniso %d w (%.17g, %.17g, %.17g)\n", what,
400 ch.f.spacing[0], ch.f.spacing[1], ch.f.spacing[2], (
int)ch.aniso, ch.w[0], ch.w[1],
402 double relErr = 0.0, wallMax = 0.0;
403 for (
int z = 0;
z < nz; ++
z)
404 for (
int y = 0;
y <
ny; ++
y)
405 for (
int x = 0;
x < nx; ++
x) {
406 const std::size_t
i = (std::size_t)x + (std::size_t)
y * nx + (std::size_t)z * nx * ny;
407 const double uu = ch.f.u[
i];
408 if (y <= jlo || y >= jhi) {
409 if (!(std::fabs(uu) <= wallMax))
410 wallMax = std::fabs(uu);
413 const double ex =
F * (ch.yc[
y] - ch.ylo) * (ch.yhi - ch.yc[y]) / (2.0 *
MU);
414 const double d = std::fabs(uu - ex) /
umax;
420 " H %.17g u_max %.17g max rel |u - parabola| %.3e (bound %.0e) max|v| %.3e "
421 "max|w| %.3e max|u| on the wall/solid rows %.3e\n",
422 H, umax,
relErr, bound, mv, mw, wallMax);
425 " NOTE: a miss that is a SINGLE one-signed multiplicative factor across the "
426 "whole profile (rel err proportional to u, one constant ratio on every fluid row) "
427 "is the FLOAT momentum-operator storage (`IbmSolver::FV`, MReal; WO-M / "
428 "docs/SCALING_ISSUES.md #1), not the metric -- rebuild with "
429 "-DPECLET_FLOW_OPERATOR_DOUBLE to separate the two.\n");
431 CHECK(wallMax == 0.0);
432 CHECK(mv <= 1e-12 * umax);
433 CHECK(mw <= 1e-12 * umax);
436void gateAnisoPoiseuille() {
437 std::printf(
"=== units_anisotropic_poiseuille ===\n");
443 const Channel ch = runChannelBox(16, 40, 8, {16.0, 10.0, 16.0}, 12, 28,
true);
445 CHECK(ch.f.spacing[0] == 1.0 && ch.f.spacing[1] == 0.25 && ch.f.spacing[2] == 2.0);
446 CHECK(ch.w[0] == 0.0625 && ch.w[1] == 1.0 && ch.w[2] == 0.015625);
447 CHECK(ch.ylo == 3.125 && ch.yhi == 7.125);
448 checkChannel(
"stretched EXACTNESS (16, 40, 8) on (16, 10, 16)", ch, 16, 40, 8, 12, 28, 1e-9);
457 const Channel ch = runChannelBox(16, 40, 8, {16.0, 12.0, 16.0}, 12, 28,
true);
459 checkClose(ch.f.spacing[0], 1.0, 0.0,
"aniso spacing x");
460 checkClose(ch.f.spacing[1], 0.3, 1e-17,
"aniso spacing y");
461 checkClose(ch.f.spacing[2], 2.0, 0.0,
"aniso spacing z");
462 checkClose(ch.ylo, 3.75, 1e-14,
"aniso wall ylo");
463 checkClose(ch.yhi, 8.55, 1e-14,
"aniso wall yhi");
464 checkChannel(
"stretched FLOAT-FLOOR (16, 40, 8) on (16, 12, 16)", ch, 16, 40, 8, 12, 28, 1e-7);
474 const Channel
a = runChannelBox(16, 16, 16, {16.0, 16.0, 16.0}, 5, 11,
true);
476 CHECK(
a.w[0] == 1.0 &&
a.w[1] == 1.0 &&
a.w[2] == 1.0);
477 CHECK(
a.f.spacing[0] == 1.0 &&
a.f.spacing[1] == 1.0 &&
a.f.spacing[2] == 1.0);
478 checkChannel(
"isotropic control (16, 16, 16) at extent == cells", a, 16, 16, 16, 5, 11, 1e-9);
488 sc.setPhysicalDomain({16.0, 12.0, 16.0}, {0.0, 0.0, 0.0}, {16, 40, 8});
489 const auto sp =
sc.spacing();
490 std::printf(
" ADMITTED (collocated): spacing (%.17g, %.17g, %.17g), aniso %d\n", sp[0],
491 sp[1], sp[2], (
int)
sc.unitScales().aniso);
493 }
catch (
const std::exception& ex) {
495 std::printf(
" UNEXPECTED refusal (collocated): %s\n", ex.what());
511 ss.setPhysicalDomain({16.0, 12.0, 16.0}, {0.0, 0.0, 0.0}, {16, 40, 8});
513 std::printf(
" ADMITTED (enable_vof — Phase 3; see units_vof_aniso)\n");
514 }
catch (
const std::exception& ex) {
516 std::printf(
" UNEXPECTED refusal (enable_vof): %s\n", ex.what());
522 ss.setPhysicalDomain({16.0, 12.0, 16.0}, {0.0, 0.0, 0.0}, {16, 40, 8});
523 ss.hydroForceTorque();
524 std::printf(
" ADMITTED (hydro_force_torque)\n");
525 }
catch (
const std::exception& ex) {
527 std::printf(
" UNEXPECTED refusal (hydro_force_torque): %s\n", ex.what());
535 std::printf(
"=== units_identity ===\n");
540 CHECK(!
s.hasPhysicalDomain());
541 const auto h =
s.spacing();
542 CHECK(h[0] == 1.0 && h[1] == 1.0 && h[2] == 1.0);
543 const auto e =
s.domainExtent();
544 CHECK(e[0] == 6.0 && e[1] == 8.0 && e[2] == 10.0);
545 const auto o =
s.domainOrigin();
546 CHECK(o[0] == 0.0 && o[1] == 0.0 && o[2] == 0.0);
547 const auto c =
s.cellCentres(1);
549 CHECK(c[0] == 0.5 && c[7] == 7.5);
555 s.setPhysicalDomain({6.0, 8.0, 10.0}, {-2.0, 0.5, 100.0}, {6, 8, 10});
556 CHECK(
s.hasPhysicalDomain());
557 const auto h =
s.spacing();
558 CHECK(h[0] == 1.0 && h[1] == 1.0 && h[2] == 1.0);
559 const auto o =
s.domainOrigin();
560 CHECK(o[0] == -2.0 && o[1] == 0.5 && o[2] == 100.0);
561 const auto cx =
s.cellCentres(0);
562 CHECK(cx[0] == -1.5 && cx[5] == 3.5);
563 const auto cz =
s.cellCentres(2);
564 CHECK(cz[0] == 100.5 && cz[9] == 109.5);
565 const auto&
u =
s.unitScales();
569 CHECK(
u.rhoRef == 1.0 &&
u.tRef == 1.0);
570 CHECK(
u.muToInt() == 1.0 &&
u.pToInt() == 1.0 &&
u.forceToInt(0) == 1.0);
571 CHECK(
u.velToInt(2) == 1.0 &&
u.lenToInt() == 1.0 &&
u.sigmaToInt() == 1.0);
576 auto run = [](
bool arm) {
577 const int nx = 8,
ny = 24, nz = 8;
580 s.setPhysicalDomain({(double)nx, (
double)
ny, (double)nz}, {0.0, 0.0, 0.0}, {nx,
ny, nz});
584 s.setBodyForce(0.01, 0.0, 0.0);
585 s.setVelocityIterations(60);
586 s.setPressureIterations(4);
587 const double ylo = 7.5, yhi =
ny - 7.5;
588 std::vector<double>
sdf((std::size_t)nx * ny * nz);
589 for (
int z = 0;
z < nz; ++
z)
590 for (
int y = 0;
y <
ny; ++
y)
591 for (
int x = 0;
x < nx; ++
x)
592 sdf[(std::size_t)
x + (std::size_t)y * nx + (std::size_t)
z * nx *
ny] =
593 std::min((y + 0.5) - ylo, yhi - (y + 0.5));
594 s.setSolid(sdf,
true);
595 for (
int it = 0;
it < 30; ++
it)
598 f.u =
s.getVelocity(0);
599 f.v =
s.getVelocity(1);
600 f.w =
s.getVelocity(2);
601 f.p =
s.getPressure();
604 const Fields
a =
run(
false),
b =
run(
true);
610 std::printf(
" extent == cells, rho = dt = 1: bitwise identical to the cell-unit run\n");
616 std::printf(
"=== units_scale_invariance ===\n");
617 const double lam = 1e3;
618 const double TOL = 1
e-13;
621 const int nx = 8,
ny = 24, nz = 8;
622 const Fields
a = runChannel<peclet::flow::Staggered>(nx, ny, nz, 1.0,
true);
623 const Fields
b = runChannel<peclet::flow::Staggered>(nx, ny, nz, lam,
true);
624 checkClose(
a.spacing[0], 1.0, 0.0,
"channel spacing (system A)");
625 checkClose(
b.spacing[0], 1.0 / lam, 1e-18,
"channel spacing (system B)");
627 const double ru = relDiffScaled(
a.u,
b.u, lam);
628 const double rp = relDiffScaled(
a.p,
b.p, 1.0 / lam);
629 std::printf(
" Poiseuille (sampled SDF): u %.3e p %.3e\n", ru, rp);
637 const Fields
a = runSphere<peclet::flow::Staggered>(
N, 1.0,
true, scene != 0);
638 const Fields
b = runSphere<peclet::flow::Staggered>(
N, lam,
true, scene != 0);
639 const double ru = relDiffScaled(
a.u,
b.u, lam);
640 const double rv = relDiffScaled(
a.v,
b.v, lam);
641 const double rp = relDiffScaled(
a.p,
b.p, 1.0 / lam);
646 const double rd = std::fabs(
a.div -
b.div);
647 std::printf(
" sphere Stokes (%s): u %.3e v %.3e p %.3e div %.3e vs %.3e (d %.3e)\n",
648 scene ?
"analytic scene" :
"sampled SDF", ru, rv, rp,
a.
div,
b.
div, rd);
653 CHECK(
a.div < 1e-9 &&
b.div < 1e-9);
710 double K = 0.0,
umean = 0.0,
div = 0.0,
R = 0.0, pIterStep = 0.0;
712 std::array<double, 3>
spacing{};
718Zh runZhSphere(
int nx,
int ny,
int nz,
bool arm,
double L,
int levels) {
719 const double RHO = 1.0,
MU = 0.1,
DT = 60.0,
FX = 1
e-3,
PHI = 0.216;
720 const double R = std::pow(PHI * 3.0 / (4.0 * M_PI), 1.0 / 3.0) *
L;
723 s.setPhysicalDomain({
L,
L,
L}, {0.0, 0.0, 0.0}, {nx,
ny, nz});
727 s.setBodyForce(
FX, 0.0, 0.0);
728 s.setAdvection(
false);
729 s.setVelocityIterations(80);
730 s.setPressureLevels(levels);
731 s.setPressurePcg(
true, 300, 1e-8);
732 const std::vector<double> cx =
s.cellCentres(0), cy =
s.cellCentres(1), cz =
s.cellCentres(2);
733 const double c = 0.5 *
L;
734 std::vector<double>
sdf((std::size_t)nx * ny * nz);
735 for (
int z = 0;
z < nz; ++
z)
736 for (
int y = 0;
y <
ny; ++
y)
737 for (
int x = 0;
x < nx; ++
x)
738 sdf[(std::size_t)
x + (std::size_t)y * nx + (std::size_t)
z * nx *
ny] =
739 std::sqrt((cx[x] - c) * (cx[x] - c) + (cy[y] - c) * (cy[y] - c) +
740 (cz[z] - c) * (cz[z] - c)) -
742 s.setSolid(sdf,
true);
743 std::vector<double>
pit;
746 for (
int it = 0;
it < 400; ++
it) {
749 pit.push_back((
double)
s.lastPressureIterations());
751 const std::vector<double> uu =
s.getVelocity(0);
755 m /= (double)uu.size();
756 if (it >= 15 && std::fabs(m - prev) < 1
e-5 * (std::fabs(m) + 1
e-30))
761 const std::vector<double> uu =
s.getVelocity(0);
765 r.umean =
m / (double)uu.size();
767 r.K =
FX * (
L *
L *
L) / (6.0 * M_PI *
MU * R *
r.umean);
768 r.div =
s.maxOpenDivergence();
769 r.spacing =
s.spacing();
770 r.aniso =
s.unitScales().aniso;
771 std::vector<double>
half(
pit.begin() + (std::ptrdiff_t)(
pit.size() / 2),
pit.end());
772 std::sort(
half.begin(),
half.end());
773 const std::size_t nh =
half.size();
774 r.pIterStep = (nh % 2) ? half[nh / 2] : 0.5 * (
half[nh / 2 - 1] +
half[nh / 2]);
779double logLogOrder(
const std::vector<double>& Ns,
const std::vector<double>& err) {
780 double sx = 0, sy = 0, sxx = 0, sxy = 0;
781 const double n = (double)
Ns.size();
782 for (std::size_t i = 0;
i <
Ns.size(); ++
i) {
783 const double X = std::log(Ns[i]),
Y = std::log(err[i]);
789 return -(
n * sxy - sx * sy) / (n * sxx - sx * sx);
795 double Fx = 0.0, Fref = 0.0,
rel = 0.0;
797 std::array<double, 3>
spacing{};
800React runReactionSphere(
int nx,
int ny,
int nz,
bool arm,
double L,
int levels,
int nsteps) {
801 const double RHO = 1.0,
MU = 0.1,
DT = 60.0,
FX = 1
e-3,
PHI = 0.216;
802 const double R = std::pow(PHI * 3.0 / (4.0 * M_PI), 1.0 / 3.0) *
L;
805 s.setPhysicalDomain({
L,
L,
L}, {0.0, 0.0, 0.0}, {nx,
ny, nz});
809 s.setBodyForce(
FX, 0.0, 0.0);
810 s.setAdvection(
false);
811 s.setVelocityIterations(200);
812 s.setVelocityResidualTolerance(1e-12);
813 s.setPressureLevels(levels);
814 s.setPressurePcg(
true, 300, 1e-10);
816 namespace g = peclet::core::geom;
817 g::SceneBuilder<double>
b;
818 b.addLeaf(g::kSphere, {
R});
819 g::Transform<double>
tr;
820 tr.translation = peclet::core::Vec3<double>{0.5 *
L, 0.5 *
L, 0.5 *
L};
821 b.addInstance(0, tr);
822 std::vector<int> ni, ii;
823 std::vector<double> nr, ir;
824 b.encode(ni, nr, ii, ir);
825 s.setScene(ni, nr, ii, ir,
true);
826 s.setSolidFromScene(
true);
828 for (
int it = 0;
it < nsteps; ++
it)
831 out.spacing =
s.spacing();
835 const std::vector<double> sd =
s.getField(
"sdf");
837 for (
int z = 0;
z < nz; ++
z)
838 for (
int y = 0;
y <
ny; ++
y)
839 for (
int x = 0;
x < nx; ++
x) {
840 const int xm = (
x + nx - 1) % nx;
841 const double a = sd[(std::size_t)xm + (std::size_t)
y * nx + (std::size_t)z * nx * ny];
842 const double b2 = sd[(std::size_t)x + (std::size_t)
y * nx + (std::size_t)z * nx * ny];
843 if (0.5 * (a + b2) > 0.0)
847 const double vcell =
out.spacing[0] *
out.spacing[1] *
out.spacing[2];
848 out.Fref =
FX * vcell * (double)nf;
849 const std::vector<double> fr =
s.hydroForceTorqueReaction();
850 out.Fx = fr.empty() ? 0.0 : fr[0];
851 out.rel = std::fabs(
out.Fx -
out.Fref) / std::fabs(
out.Fref);
855void gateAnisoSphere() {
856 std::printf(
"=== units_anisotropic_sphere ===\n");
857 const double KREF = 7.442;
858 const double REC[3] = {7.299689474225098, 7.389063051924422, 7.416167928926183};
859 const int NS[3] = {16, 24, 32};
860 std::vector<double>
Ns, errS, errC;
861 for (
int i = 0;
i < 3; ++
i) {
863 const int levels = std::max(2, (
int)std::floor(std::log2((
double)
N)) - 1);
864 const Zh cub = runZhSphere(
N,
N,
N,
false, (
double)
N, levels);
865 const Zh str = runZhSphere(
N, 2 *
N,
N / 2,
true, (
double)
N, levels);
866 const double ec = std::fabs(cub.K - KREF) /
KREF,
es = std::fabs(str.K - KREF) /
KREF;
867 Ns.push_back((
double)
N);
870 std::printf(
" N=%2d cubic K %.17g err %.4f %% iters/step %.1f steps %3d div %.2e\n",
N,
871 cub.K, 100.0 * ec, cub.pIterStep, cub.steps, cub.div);
873 " stretched (%d,%d,%d) spacing (%g, %g, %g) aniso %d K %.17g err %.4f %%"
874 " iters/step %.1f steps %3d div %.2e\n",
875 N, 2 *
N,
N / 2, str.spacing[0], str.spacing[1], str.spacing[2], (
int)str.aniso, str.K,
876 100.0 * es, str.pIterStep, str.steps, str.div);
878 const double dRec = std::fabs(cub.K - REC[i]) / REC[
i];
879 const int RECSTEPS[3] = {60, 80, 75};
880 const double RECITERS[3] = {6.0, 7.0, 7.0};
882 " cubic vs perf_baseline.json %.17g rel %.3e (bound 1e-6) steps %d "
883 "(recorded %d) iters/step %.1f (recorded %.1f)\n",
884 REC[i], dRec, cub.steps, RECSTEPS[i], cub.pIterStep, RECITERS[i]);
886 CHECK(cub.steps == RECSTEPS[i]);
887 CHECK(cub.pIterStep == RECITERS[i]);
889 CHECK(es <= 4.0 * ec + 0.002);
891 std::printf(
" pressure iters/step cubic %.1f stretched %.1f (bound cubic + 2)\n",
892 cub.pIterStep, str.pIterStep);
893 CHECK(str.pIterStep <= cub.pIterStep + 2.0);
896 CHECK(errS[1] < errS[0]);
897 CHECK(errS[2] < errS[1]);
899 const double ps = logLogOrder(Ns, errS), pc = logLogOrder(Ns, errC);
900 std::printf(
" LS order of the ERROR sequence: stretched %.4f (bound >= 1.5), cubic %.4f\n", ps,
907 const int levels = std::max(2, (
int)std::floor(std::log2((
double)
N)) - 1);
908 const React rc = runReactionSphere(
N,
N,
N,
false, (
double)
N, levels, 300);
909 const React rs = runReactionSphere(
N, 2 *
N,
N / 2,
true, (
double)
N, levels, 300);
910 std::printf(
" hydro_force_torque_reaction, periodic Stokes identity F = F_body*V_fluid:\n");
912 " cubic (%d,%d,%d) spacing (%g,%g,%g) F_x %.17g identity %.17g rel %.3e"
913 " (%ld fluid u-DOFs)\n",
914 N,
N,
N, rc.spacing[0], rc.spacing[1], rc.spacing[2], rc.Fx, rc.Fref, rc.rel, rc.nFluid);
916 " stretched (%d,%d,%d) spacing (%g,%g,%g) F_x %.17g identity %.17g rel %.3e"
917 " (%ld fluid u-DOFs)\n",
918 N, 2 *
N,
N / 2, rs.spacing[0], rs.spacing[1], rs.spacing[2], rs.Fx, rs.Fref, rs.rel,
924 CHECK(rs.rel <= 1e-5);
925 CHECK(rc.rel <= 1e-5);
926 std::printf(
" stretched/cubic relative-gap ratio %.3f (both gated at 1e-5)\n",
933 double ratio = 0.0, rExact = 0.0, relRate = 0.0,
div = 0.0,
prof = 0.0, pmax = 0.0;
934 std::vector<double>
u,
v,
w,
p;
935 std::array<double, 3>
spacing{};
942Tgv runTgv(
int nx,
int ny,
int nz,
bool arm,
double Lx,
double Ly,
double Lz,
bool advect,
943 int nsteps,
double DT = 1.0) {
944 const double RHO = 1.0,
MU = 0.25;
945 const double hx = arm ? Lx / nx : 1.0, hy = arm ? Ly /
ny : 1.0, hz = arm ? Lz / nz : 1.0;
946 const double Lw = arm ? Lx : (double)nx;
947 const double k = 2.0 * M_PI / Lw;
948 const double ax = 1.0;
949 const double ay =
ax * (std::sin(0.5 * k * hx) / hx) * (hy / std::sin(0.5 * k * hy));
952 s.setPhysicalDomain({Lx, Ly, Lz}, {0.0, 0.0, 0.0}, {nx,
ny, nz});
957 s.setVelocityIterations(400);
958 s.setVelocityResidualTolerance(1e-14);
959 s.setPressurePcg(
true, 300, 1e-13);
960 s.setPressureGeometry(std::vector<double>((std::size_t)nx * ny * nz, 10.0));
961 const std::size_t
n = (std::size_t)nx * ny * nz;
962 std::vector<double>
u(n),
v(n),
w(n, 0.0);
963 for (
int z = 0;
z < nz; ++
z)
964 for (
int y = 0;
y <
ny; ++
y)
965 for (
int x = 0;
x < nx; ++
x) {
966 const std::size_t
i = (std::size_t)x + (std::size_t)
y * nx + (std::size_t)z * nx * ny;
967 u[
i] =
ax * std::cos(k * (x * hx)) * std::sin(k * ((y + 0.5) * hy));
968 v[
i] = -ay * std::sin(k * ((x + 0.5) * hx)) * std::cos(k * (y * hy));
970 s.uploadVelocity(u, v, w);
972 const double Lambda =
973 2.0 * (1.0 - std::cos(k * hx)) / (hx * hx) + 2.0 * (1.0 - std::cos(k * hy)) / (hy * hy);
974 r.rExact = std::pow(1.0 / (1.0 +
DT * (
MU /
RHO) * Lambda), (
double)nsteps);
975 for (
int it = 0;
it < nsteps; ++
it)
977 r.u =
s.getVelocity(0);
978 r.v =
s.getVelocity(1);
979 r.w =
s.getVelocity(2);
980 r.p =
s.getPressure();
981 r.spacing =
s.spacing();
982 r.aniso =
s.unitScales().aniso;
983 double num = 0.0,
den = 0.0;
984 for (std::size_t i = 0;
i <
n; ++
i) {
985 num +=
r.u[
i] *
u[
i];
989 double pmaxu = 0.0,
perr = 0.0;
990 for (std::size_t i = 0;
i <
n; ++
i) {
991 pmaxu = std::fmax(pmaxu, std::fabs(u[i]));
992 perr = std::fmax(perr, std::fabs(
r.u[i] -
r.ratio * u[i]));
994 r.prof =
perr / pmaxu;
995 r.relRate = std::fabs(
r.ratio -
r.rExact) /
r.rExact;
996 r.div =
s.maxOpenDivergence();
1002void gateAnisoTgv() {
1003 std::printf(
"=== units_anisotropic_tgv ===\n");
1004 const int N = 16,
NS = 10;
1008 runTgv(
N, 2 *
N, 4,
true, (
double)
N, (
double)
N, 4.0,
false, NS);
1009 std::printf(
" stretched Stokes (%d,%d,4) spacing (%g, %g, %g) aniso %d\n",
N, 2 *
N,
1010 t.spacing[0],
t.spacing[1],
t.spacing[2], (
int)
t.aniso);
1011 std::printf(
" amplitude ratio %.17g exact %.17g rel %.3e (bound 1e-10)\n",
t.ratio,
1012 t.rExact,
t.relRate);
1014 " max|div_o| %.3e (bound 1e-12) profile %.3e (bound 1e-10) max|P| %.3e "
1015 "(the phi proxy, bound 1e-12)\n",
1016 t.div,
t.prof,
t.pmax);
1018 CHECK(
t.relRate <= 1e-10);
1031 runTgv(
N, 2 *
N, 4,
true, (
double)
N, (
double)
N, 4.0,
true, NS, 0.05);
1033 " stretched NS (%d,%d,4) dt 0.05 amplitude ratio %.17g exact %.17g "
1034 "rel %.3e (bound 5e-3) max|div| %.3e (bound 1e-9)\n",
1035 N, 2 *
N,
t.ratio,
t.rExact,
t.relRate,
t.div);
1036 CHECK(
t.relRate <= 5e-3);
1042 const Tgv
a = runTgv(
N,
N, 4,
true, (
double)
N, (
double)
N, 4.0,
false, NS);
1043 const Tgv
b = runTgv(
N,
N, 4,
false, (
double)
N, (
double)
N, 4.0,
false, NS);
1044 std::printf(
" isotropic control (%d,%d,4) spacing (%g, %g, %g) aniso %d\n",
N,
N,
a.spacing[0],
1045 a.spacing[1],
a.spacing[2], (
int)
a.aniso);
1047 " amplitude ratio %.17g exact %.17g rel %.3e max|div_o| %.3e profile %.3e "
1049 a.ratio,
a.rExact,
a.relRate,
a.div,
a.prof,
a.pmax);
1051 CHECK(
a.relRate <= 1e-10);
1055 const bool bit = bitwiseEqual(
a.u,
b.u) && bitwiseEqual(
a.v,
b.v) && bitwiseEqual(
a.w,
b.w) &&
1056 bitwiseEqual(
a.p,
b.p);
1057 std::printf(
" armed at extent == cells vs the cell-unit run: bitwise %s\n",
1058 bit ?
"EQUAL" :
"DIFFERENT");
1089double sphereFracBox(
double cx,
double cy,
double cz,
double R,
double x0,
double y0,
double z0,
1090 const double h[3],
int sub = 24) {
1091 const double wx = h[0] / sub, wy = h[1] / sub;
1093 for (
int b = 0; b < sub; ++b)
1094 for (
int a = 0; a < sub; ++a) {
1095 const double px = x0 + (a + 0.5) * wx, py = y0 + (b + 0.5) * wy;
1096 const double r2 = R * R - (px - cx) * (px - cx) - (py - cy) * (py - cy);
1099 const double hh = std::sqrt(r2);
1100 const double lo = std::fmax(cz - hh, z0), hi = std::fmin(cz + hh, z0 + h[2]);
1104 return acc / (sub * sub * h[2]);
1110 const double SIGMA = 1.0,
MU = 0.1,
RHO = 1.0;
1112 s.setPhysicalDomain({n[0] * h[0], n[1] * h[1], n[2] * h[2]}, {0.0, 0.0, 0.0}, {n[0], n[1], n[2]});
1116 s.setVelocityResidualTolerance(0.0);
1117 const std::size_t nc = (std::size_t)n[0] * n[1] * n[2];
1118 const double hMin = std::fmin(h[0], std::fmin(h[1], h[2]));
1119 s.setPressureGeometry(std::vector<double>(nc, 10.0 * hMin));
1120 s.setPressureChebyshev(
true, 500, 1e-14);
1123 const double ctr[3] = {0.5 * n[0] * h[0] + 0.13 * h[0], 0.5 * n[1] * h[1] + 0.27 * h[1],
1124 0.5 * n[2] * h[2] + 0.11 * h[2]};
1125 std::vector<double> C(nc);
1126 for (
int k = 0; k < n[2]; ++k)
1127 for (
int j = 0; j < n[1]; ++j)
1128 for (
int i = 0; i < n[0]; ++i)
1129 C[(std::size_t)i + (std::size_t)j * n[0] + (std::size_t)k * n[0] * n[1]] =
1130 sphereFracBox(ctr[0], ctr[1], ctr[2], R, i * h[0], j * h[1], k * h[2], h);
1133 s.setSurfaceTension(
SIGMA);
1138 s.setVofKappaConstant(d.kappa);
1140 s.computeVofCurvature();
1141 d.dtCap = s.capillaryDt();
1142 s.setDt(0.5 * d.dtCap);
1143 for (
int q = 0; q < steps; ++q)
1145 for (
int c = 0; c < 3; ++c)
1146 d.maxU = std::fmax(d.maxU,
maxAbs(s.getVelocity(c)));
1147 const std::vector<double> p = s.getPressure();
1148 const std::size_t inside = (std::size_t)(n[0] / 2) + (std::size_t)(n[1] / 2) * n[0] +
1149 (std::size_t)(n[2] / 2) * n[0] * n[1];
1150 const std::size_t outside = 1 + (std::size_t)1 * n[0] + (std::size_t)1 * n[0] * n[1];
1151 d.dp = p[inside] - p[outside];
1156 std::printf(
"=== units_vof_aniso (Phase 3 S2: the balanced-force CSF on BOX cells) ===\n");
1159 const double base = 1.0 / 32.0;
1165 const Mesh meshes[3] = {
1166 {
"cubic ", {32, 32, 32}, {base, base, base}},
1167 {
"aspect 2 ", {32, 64, 32}, {base, base / 2, base}},
1168 {
"aspect 4 ", {32, 64, 128}, {base, base / 2, base / 4}},
1170 const double R = 8.0 * base;
1171 const double uScale = 1.0 * (2.0 / R) / 0.1;
1173 for (
int q = 0; q < 3; ++q) {
1174 const Mesh& m = meshes[q];
1178 std::printf(
" %s n = (%3d,%3d,%3d) h'/hMin = (%.2f, %.2f, %.2f)\n", m.name, m.n[0], m.n[1],
1179 m.n[2], m.h[0] / a.hMin, m.h[1] / a.hMin, m.h[2] / a.hMin);
1180 std::printf(
" exactness max|u| = %.3e (gate %.3e = 1e-14 * sigma*kappa/mu)\n", a.maxU,
1182 std::printf(
" Young-Laplace dp = %.9e vs sigma*kappa = %.9e (rel %.2e)\n", a.dp, a.kappa,
1183 std::fabs(a.dp - a.kappa) / a.kappa);
1184 std::printf(
" capillary_dt = %.9e (h_min = %.6e)\n", a.dtCap, a.hMin);
1185 CHECK(a.maxU < 1e-14 * uScale);
1186 checkClose(a.dp, a.kappa, 1e-9 * a.kappa,
"anisotropic Young-Laplace");
1190 const double want = meshes[0].h[0] / a.hMin;
1192 std::printf(
" dt_sigma(cubic)/dt_sigma(this) = %.4f (h^{3/2} predicts %.4f)\n", got,
1193 std::pow(want, 1.5));
1194 CHECK(std::fabs(got - std::pow(want, 1.5)) < 1e-9 * std::pow(want, 1.5));
1215 const double caC = 0.1 * cc.
maxU / 1.0, caA = 0.1 * ca.maxU / 1.0;
1216 std::printf(
" computed kappa: spurious Ca cubic %.3e vs aspect-2 %.3e (ratio %.2f, gate 4)\n",
1217 caC, caA, caA / caC);
1218 CHECK(caA < 4.0 * caC);
1219 checkClose(ca.dp, ca.kappa, 5e-2 * ca.kappa,
"anisotropic Young-Laplace (computed kappa)");
1236 double lam = 1.0, tau = 1.0, mas = 1.0;
1237 double len(
double x)
const {
return x /
lam; }
1238 double time(
double t)
const {
return t / tau; }
1239 double rho(
double r)
const {
return r *
lam *
lam *
lam / mas; }
1240 double mu(
double m)
const {
return m *
lam * tau / mas; }
1241 double diff(
double d)
const {
return d * tau / (
lam *
lam); }
1242 double vel(
double u)
const {
return u * tau /
lam; }
1243 double mdot(
double m)
const {
return m *
lam *
lam * tau / mas; }
1244 double latent(
double h)
const {
return h * tau * tau / (
lam *
lam); }
1245 double cond(
double k)
const {
return k * tau * tau * tau / (mas *
lam); }
1246 double rcp(
double c)
const {
return c *
lam * tau * tau / mas; }
1247 double pres(
double p)
const {
return p *
lam * tau * tau / mas; }
1248 double force(
double f)
const {
return f *
lam *
lam * tau * tau / mas; }
1249 double area(
double a)
const {
return a / (
lam *
lam); }
1250 double vol(
double v)
const {
return v / (
lam *
lam *
lam); }
1251 double energy(
double e)
const {
return e * tau * tau / (mas *
lam *
lam); }
1252 double power(
double p)
const {
return p * tau * tau * tau / (mas *
lam *
lam); }
1253 double rate(
double r)
const {
return r * tau; }
1256double relDiff(
const std::vector<double>& a,
const std::vector<double>& b) {
1257 return relDiffScaled(a, b, 1.0);
1259double relScalar(
double a,
double b) {
1260 const double den = std::fabs(a) > 0.0 ? std::fabs(a) : std::fabs(
b);
1261 return den > 0.0 ? std::fabs(a - b) /
den : 0.0;
1269 std::vector<double>
T,
x;
1272ScalarSine runScalarSine(
int N,
const std::array<double, 3>& ext,
double D,
double U,
double dt,
1273 int nsteps,
const Units& un,
bool dtFirst,
int scheme = 1) {
1274 const int ny = 4, nz = 4;
1276 s.setPhysicalDomain({un.len(ext[0]), un.len(ext[1]), un.len(ext[2])}, {0.0, 0.0, 0.0},
1278 s.setRho(un.rho(1.0));
1280 s.setDt(un.time(dt));
1281 s.addScalar(
"T", un.diff(D), scheme, 200);
1283 s.setDt(un.time(dt));
1284 s.setPressureGeometry(std::vector<double>((std::size_t)
N * ny * nz, un.len(1.0)));
1285 const double k = 2.0 * M_PI / un.len(ext[0]);
1286 const std::vector<double> cx =
s.cellCentres(0);
1287 std::vector<double> T0((std::size_t)
N * ny * nz);
1288 for (
int z = 0;
z < nz; ++
z)
1289 for (
int y = 0;
y <
ny; ++
y)
1290 for (
int x = 0;
x <
N; ++
x)
1291 T0[(std::size_t)
x + (std::size_t)y *
N + (std::size_t)
z *
N *
ny] = std::sin(k * cx[x]);
1292 s.setField(
"T", T0);
1294 s.setVelocity(0, std::vector<double>((std::size_t)
N * ny * nz, un.vel(
U)));
1295 for (
int n = 0;
n < nsteps; ++
n)
1298 r.T =
s.getField(
"T");
1300 double num = 0.0,
den = 0.0;
1301 for (
int z = 0;
z < nz; ++
z)
1302 for (
int y = 0;
y <
ny; ++
y)
1303 for (
int x = 0;
x <
N; ++
x) {
1304 const double sx = std::sin(k * cx[x]);
1305 num +=
r.T[(std::size_t)x + (std::size_t)
y *
N + (std::size_t)z *
N * ny] * sx;
1313double closureProbe(
const Units& un,
bool rhoFirst) {
1316 s.setPhysicalDomain({un.len(1.0), un.len(1.0), un.len(1.0)}, {0.0, 0.0, 0.0}, {
N,
N,
N});
1318 s.setRho(un.rho(998.0));
1319 s.setDt(un.time(1e-3));
1321 s.setPressureGeometry(std::vector<double>((std::size_t)
N *
N *
N, un.len(1.0)));
1322 s.addScalar(
"T", un.diff(1.4e-7), 1, 10);
1323 s.setField(
"T", std::vector<double>((std::size_t)
N *
N *
N, 310.0));
1326 {un.rho(998.0), un.force(9.81) / un.rho(1.0), 2.1e-4, 300.0});
1328 s.setRho(un.rho(998.0));
1329 s.setDt(un.time(1e-3));
1331 s.updateProperties();
1332 return s.getField(
"force_z")[0];
1336 std::printf(
"=== units_scalar_scale_invariance ===\n");
1337 const double TOL = 1
e-13;
1345 const std::array<double, 3> ext{2.0, 4.0 * 2.0 /
N, 4.0 * 2.0 /
N};
1346 const double h = 2.0 /
N,
D = 0.05,
dt = 0.5 *
h *
h /
D;
1347 for (
int adv = 0; adv < 2; ++adv) {
1348 const double U = adv ? 0.3 : 0.0;
1349 const ScalarSine
a = runScalarSine(
N, ext, D,
U, dt, 40, ref,
true);
1350 const ScalarSine
b = runScalarSine(
N, ext, D,
U, dt, 40, big,
false);
1351 const double rT = relDiff(
a.T,
b.T);
1352 std::printf(
" %s: max|T_A - T_B|/max|T_A| = %.3e (B registers the scalar before set_dt)\n",
1353 adv ?
"advection-diffusion (Koren)" :
"diffusion ", rT);
1359 const double fA = closureProbe(ref,
true), fB = closureProbe(big,
false);
1360 const double fC = closureProbe(big,
true);
1361 std::printf(
" Boussinesq force_z (internal): A %.17g B(rho/dt after) %.17g B(before) %.17g\n",
1363 CHECK(std::fabs(fA) > 0.0);
1364 CHECK(relScalar(fA, fB) <= TOL);
1365 CHECK(relScalar(fA, fC) <= TOL);
1368void gateScalarSine() {
1369 std::printf(
"=== units_scalar_sine_decay ===\n");
1383 const double Lx = 2.0,
D = 0.05,
k = 2.0 * M_PI / Lx, tEnd = 1.0 / (
D *
k *
k);
1384 for (
int box = 0;
box < 2; ++
box) {
1385 double err[2] = {0, 0};
1386 for (
int q = 0;
q < 2; ++
q) {
1387 const int N =
q ? 32 : 16;
1388 const double h = Lx /
N, dt0 = 0.25 *
h *
h /
D;
1389 const int ns = (
int)std::ceil(tEnd / dt0);
1390 const double dt = tEnd /
ns;
1391 const double hyz =
box ? 0.5 *
h :
h;
1392 const ScalarSine
r = runScalarSine(
N, {Lx, 4 * hyz, 4 * hyz},
D, 0.0,
dt,
ns, un,
true);
1393 const double s2 = std::sin(0.5 * k * h);
1394 const double rr = 1.0 / (1.0 +
dt *
D * 4.0 *
s2 *
s2 / (
h *
h));
1395 const double discrete = std::pow(rr, ns),
exact = std::exp(-D * k * k * tEnd);
1397 const double eDisc = std::fabs(
r.amp - discrete) / discrete;
1399 " %s N=%2d (%3d steps): amplitude %.15f discrete %.15f (rel %.2e) exact %.15f "
1401 box ?
"box " :
"cubic",
N,
ns,
r.amp, discrete, eDisc,
exact,
err[
q]);
1402 CHECK(eDisc <= 1e-12);
1404 const double order = std::log(err[0] / err[1]) / std::log(2.0);
1405 std::printf(
" %s observed order %.3f\n", box ?
"box " :
"cubic",
order);
1406 CHECK(order > 1.9 && order < 2.1);
1407 CHECK(err[1] < 1e-2);
1413 std::vector<double>
C,
T,
u,
p;
1417 double carryDep = 0.0, area = 0.0;
1420double stefanLambdaU(
double St) {
1421 const double rhs = St / std::sqrt(M_PI);
1422 double a = 1
e-8,
b = 5.0;
1423 for (
int i = 0;
i < 200; ++
i) {
1424 const double m = 0.5 * (
a +
b);
1425 ((
m * std::exp(m * m) * std::erf(m) - rhs) > 0 ? b :
a) =
m;
1427 return 0.5 * (
a +
b);
1434PcRun runStefanUnits(
int N,
const Units& un,
bool energy,
bool scalesFirst) {
1435 const double St = 1.0,
alpha = 1.0, x0p = 0.10, xep = 0.25, Fo = 0.5;
1436 const double lam = stefanLambdaU(St);
1437 const double t0 = (x0p / (2 *
lam)) * (x0p / (2 *
lam)) /
alpha;
1438 const double te = (xep / (2 *
lam)) * (xep / (2 *
lam)) /
alpha;
1439 const int ny = 4, nz = 4;
1441 const int ns = (
int)std::llround((te - t0) / (Fo /
D));
1442 const double dt = (te -
t0) / ns;
1444 s.setPhysicalDomain({un.len(
N), un.len(ny), un.len(nz)}, {0.0, 0.0, 0.0}, {
N,
ny, nz});
1446 s.setRho(un.rho(1.0));
1447 s.setMu(un.mu(1e-3));
1448 s.setDt(un.time(dt));
1452 s.setDomainBc(0, 1, 0, 0, 0);
1453 s.setDomainBc(1, 1, 0, 0, 0);
1454 s.setPressureGeometry(std::vector<double>((std::size_t)
N * ny * nz, un.len(1.0)));
1455 const double xg = x0p *
N;
1456 std::vector<double>
C((std::size_t)
N * ny * nz, 0.0),
T((std::size_t)
N * ny * nz, 0.0);
1457 for (
int z = 0;
z < nz; ++
z)
1458 for (
int y = 0;
y <
ny; ++
y)
1459 for (
int x = 0;
x <
N; ++
x) {
1460 const std::size_t
i = (std::size_t)x + (std::size_t)
y *
N + (std::size_t)z *
N * ny;
1461 C[
i] = std::fmin(1.0, std::fmax(0.0, (x + 1) - xg));
1462 const double xp = (
x + 0.5) /
N;
1463 T[
i] = xp < x0p ? 1.0 - std::erf(lam * xp / x0p) / std::erf(lam) : 0.0;
1467 s.addScalar(
"T", un.diff(D), 1, 60);
1468 s.setScalarBc(
"T", 0, 2, 1.0);
1469 s.setScalarBc(
"T", 1, 2, 0.0);
1471 s.enablePhaseChange(un.rho(1.0), un.rho(1.0), un.latent(1.0));
1473 s.setPhaseChangeThermal(
"T", 0.0, un.cond(D), un.cond(D), 0.0);
1475 s.setPhaseChangeEnergy(un.rcp(1.0), un.rcp(1.0));
1476 s.setPhaseChangeEnergyOrder(2);
1477 s.setPhaseChangeBudget(
true);
1481 for (
int n = 0;
n <
ns; ++
n) {
1482 s.applyPhaseChange(un.time(dt));
1487 r.T =
s.getField(
"T");
1489 for (
double q :
r.
C)
1491 r.layer = un.len(
N - sum / (ny * nz));
1492 r.d =
s.phaseChangeDiagnostics();
1494 r.b =
s.phaseChangeBudgetValues();
1495 r.carryDep =
s.phaseChangeCarryDeposited();
1497 r.area =
s.vofInterfaceArea();
1504PcRun runSuckingUnits(
int N,
int nsteps,
const Units& un) {
1505 const double ratio = 10.0, ja = 1.0, alpha_l = 1.0, x0p = 0.10, Fo = 0.5, cfl = 0.2;
1506 const double rr = 1.0 /
ratio;
1507 double b = ja / std::sqrt(M_PI);
1508 for (
int i = 0;
i < 300; ++
i) {
1509 const double g =
b * rr;
1510 b = ja * std::exp(-g * g) / (std::sqrt(M_PI) * std::erfc(g));
1512 const double t0 = (x0p / (2 *
b)) * (x0p / (2 *
b)) / alpha_l;
1513 const int ny = 4, nz = 4;
1514 const double al = alpha_l *
N *
N, rho_l = 1.0, rho_v = rr, cpl = 1.0;
1515 const double k_l = al * rho_l * cpl, k_v = k_l /
ratio, dT = 1.0;
1516 const double h_lv = rho_l * cpl * dT / (rho_v * ja);
1518 s.setPhysicalDomain({un.len(
N), un.len(ny), un.len(nz)}, {0.0, 0.0, 0.0}, {
N,
ny, nz});
1519 s.setRho(un.rho(rho_l));
1520 s.setMu(un.mu(1e-3));
1521 s.setDomainBc(0, 1, 0, 0, 0);
1522 s.setDomainBc(1, 3, 0, 0, 0);
1523 s.setPressureGeometry(std::vector<double>((std::size_t)
N * ny * nz, un.len(1.0)));
1524 const double X0 = x0p *
N;
1525 std::vector<double>
C((std::size_t)
N * ny * nz, 0.0),
T((std::size_t)
N * ny * nz, 0.0);
1526 for (
int z = 0;
z < nz; ++
z)
1527 for (
int y = 0;
y <
ny; ++
y)
1528 for (
int x = 0;
x <
N; ++
x) {
1529 const std::size_t
i = (std::size_t)x + (std::size_t)
y *
N + (std::size_t)z *
N * ny;
1530 C[
i] = std::fmin(1.0, std::fmax(0.0, (x + 1) -
X0));
1531 const double xp = (
x + 0.5) /
N;
1533 const double sv = xp / (2 * std::sqrt(alpha_l * t0)) - b * (1.0 - rr);
1534 T[
i] = dT - dT * std::erfc(sv) / std::erfc(b * rr);
1540 {un.rho(rho_v), un.rho(rho_l - rho_v)});
1541 s.setPressureFcg(
true, 4000, 1e-10);
1542 s.addScalar(
"T", un.diff(al), 1, 60);
1543 s.setScalarBc(
"T", 0, 2, 0.0);
1544 auto farT = [&](
double tt) {
1545 const double sv = 1.0 / (2 * std::sqrt(alpha_l * tt)) - b * (1.0 - rr);
1546 return dT - dT * std::erfc(sv) / std::erfc(b * rr);
1548 s.setScalarBc(
"T", 1, 2, farT(t0));
1550 s.enablePhaseChange(un.rho(rho_v), un.rho(rho_l), un.latent(h_lv));
1551 s.setPhaseChangeThermal(
"T", 0.0, un.cond(k_v), un.cond(k_l), 0.0);
1552 s.setPhaseChangeEnergy(un.rcp(rho_v * cpl), un.rcp(rho_l * cpl));
1554 for (
int n = 0;
n < nsteps; ++
n) {
1555 const double Xd =
b * std::sqrt(alpha_l / tcur) *
N;
1556 const double dt = std::fmin(cfl / std::fmax(Xd * (1.0 - rr), 1e-30), Fo / al);
1557 s.setDt(un.time(dt));
1558 s.setScalarBc(
"T", 1, 2, farT(tcur + dt));
1564 r.T =
s.getField(
"T");
1565 r.u =
s.getVelocity(0);
1566 r.p =
s.getPressure();
1567 r.d =
s.phaseChangeDiagnostics();
1571void gatePhaseChange() {
1572 std::printf(
"=== units_phase_change_scale_invariance ===\n");
1573 const double TOL = 1
e-12;
1581 for (
int energy = 0; energy < 2; ++energy) {
1582 const PcRun
a = runStefanUnits(32, ref, energy != 0,
true);
1583 const PcRun
b = runStefanUnits(32, big, energy != 0,
false);
1584 const double rC = relDiff(
a.C,
b.C), rT = relDiff(
a.T,
b.T);
1585 const double rL = relScalar(big.len(
a.layer),
b.layer);
1586 const double rM = relScalar(big.mdot(
a.d.mdotMean),
b.d.mdotMean);
1587 const double rA = relScalar(big.area(
a.d.area),
b.d.area);
1588 const double rAv = relScalar(big.area(
a.area),
b.area);
1589 const double rV = relScalar(big.vol(
a.d.removedVolume),
b.d.removedVolume);
1591 " Stefan (%s): C %.2e T %.2e layer %.2e mdot_mean %.2e interface_area %.2e "
1592 "vof_interface_area %.2e removed_volume %.2e\n",
1593 energy ?
"rho c_p T, order 2" :
"constant D ", rC, rT, rL, rM, rA, rAv, rV);
1595 CHECK(rC <= TOL && rT <= TOL && rL <= TOL);
1596 CHECK(rM <= TOL && rA <= TOL && rAv <= TOL && rV <= TOL);
1598 const double rq = relScalar(big.power(
a.d.qOperator),
b.d.qOperator);
1599 const double rh = relScalar(big.energy(
a.b.hOpen),
b.b.hOpen);
1600 const double rg = relScalar(big.power(
a.b.qGfm),
b.b.qGfm);
1601 std::printf(
" q_operator %.2e budget h_open %.2e q_gfm %.2e (carry %.3e vs %.3e)\n", rq,
1602 rh, rg, big.energy(
a.carryDep),
b.carryDep);
1603 CHECK(
a.d.qOperator != 0.0 &&
a.b.qGfm != 0.0);
1604 CHECK(rq <= TOL && rh <= TOL && rg <= TOL);
1605 CHECK(std::fabs(big.energy(
a.carryDep) -
b.carryDep) <=
1606 TOL * std::fabs(big.energy(
a.b.hOpen)) + 1e-300);
1612 const PcRun
a = runSuckingUnits(32, 12, ref);
1613 const PcRun
b = runSuckingUnits(32, 12, big);
1614 const double rC = relDiff(
a.C,
b.C), rT = relDiff(
a.T,
b.T);
1615 const double ru = relDiffScaled(
b.u,
a.u, big.vel(1.0)),
1616 rp = relDiffScaled(
b.p,
a.p, big.pres(1.0));
1617 const double rM = relScalar(big.mdot(
a.d.mdotMean),
b.d.mdotMean);
1618 std::printf(
" sucking interface, 12 full steps: C %.2e T %.2e u %.2e p %.2e mdot %.2e\n",
1619 rC, rT, ru, rp, rM);
1623 CHECK(rC <= 1e-11 && rT <= 1e-11 && ru <= 1e-11 && rp <= 1e-11 && rM <= 1e-11);
1629 const int nx = 16,
ny = 32, nz = 8;
1630 const double Lx = 1.6, Ly = 0.8, Lz = 0.4;
1632 s.setPhysicalDomain({Lx, Ly, Lz}, {0.0, 0.0, 0.0}, {nx,
ny, nz});
1635 s.setPressureGeometry(std::vector<double>((std::size_t)nx * ny * nz, 1.0));
1637 std::vector<double>
C((std::size_t)nx * ny * nz);
1638 for (std::size_t i = 0;
i <
C.size(); ++
i)
1639 C[i] = std::fmin(1.0, std::fmax(0.0, (
double)(i % nx) + 1.0 - 5.3));
1641 const double aDefault =
s.vofInterfaceArea();
1643 const double aPlic =
s.vofInterfaceArea();
1645 " box-cell plane: vof_interface_area %.15g (default sheet), %.15g (plic) vs %.15g\n",
1646 aDefault, aPlic, Ly * Lz);
1647 CHECK(relScalar(aPlic, Ly * Lz) <= 1e-12);
1648 CHECK(relScalar(aDefault, Ly * Lz) <= 1e-12);
1655 s.setPhysicalDomain({1.0, 0.25, 0.25}, {0.0, 0.0, 0.0}, {16, 4, 4});
1656 s.setPressureGeometry(std::vector<double>(16 * 4 * 4, 1.0));
1657 s.enablePhaseChange(1.0, 1000.0, 2.26e6);
1660 s.setMassFluxUniform(1e-3);
1661 }
catch (
const std::runtime_error&) {
1667 s.setMassFluxUniform(1e-3);
1668 const double kInt =
s.unitScales().mdotToInt();
1669 CHECK(relScalar(
s.getField(
"mdot")[0], 1e-3 * kInt) <= 1e-15);
1670 s.addScalar(
"T", 1e-7, 1, 10);
1671 s.setPhaseChangeThermal(
"T", 373.15, 0.025, 0.6, 0.0);
1672 s.setPhaseChangeMdotOperator(
true);
1673 std::vector<double>
C(16 * 4 * 4, 0.0);
1674 for (
int i = 0;
i < 16 * 4 * 4; ++
i)
1675 C[(std::size_t)
i] = std::fmin(1.0, std::fmax(0.0, (i % 16) + 1 - 6.5));
1679 s.applyPhaseChange(1e-4);
1680 }
catch (
const std::runtime_error&) {
1683 std::printf(
" refusals: set_mass_flux before the scales, operator mdot without rho c_p: %s\n",
1684 threw ?
"both raise" :
"MISSING");
1691 const std::string gate = argc > 1 ? argv[1] :
"identity";
1692 Kokkos::initialize(argc, argv);
1694 if (gate ==
"identity")
1696 else if (gate ==
"scale")
1698 else if (gate ==
"vof")
1700 else if (gate ==
"aniso")
1701 gateAnisoPoiseuille();
1702 else if (gate ==
"sphere")
1704 else if (gate ==
"tgv")
1706 else if (gate ==
"vofaniso")
1708 else if (gate ==
"scalar")
1710 else if (gate ==
"scalarsine")
1712 else if (gate ==
"phasechange")
1715 std::fprintf(stderr,
1716 "usage: test_units [identity|scale|vof|aniso|sphere|tgv|vofaniso|scalar|"
1717 "scalarsine|phasechange]\n");
1723 std::fprintf(stderr,
"%d failure(s)\n",
failures);
1726 std::printf(
"OK\n");
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
exact(Ds, K, hc, gs, nmax=1)
@ kAreaPlic
rung P0/P1: plicArea on the MYC normal. The old numbers.
WO-P3f — the ENERGY BUDGET of the phase-change energy solve.
static double maxAbs(const std::vector< double > &a)
static constexpr double DT
static constexpr double RHO
static constexpr double MU
static constexpr double F
static double relErr(const std::vector< double > &a, const std::vector< double > &b)
static constexpr double U
double sphereFracBox(double cx, double cy, double cz, double R, double x0, double y0, double z0, const double h[3], int sub=24)
Exact liquid fraction of the box cell [x0,x0+hx] x ... inside a sphere: midpoint-subsampled in x and ...
AnisoDrop runDropletAniso(const int n[3], const double h[3], double R, bool constantKappa, int steps)
n cells per axis on a box mesh of cell size h (physical). R is the PHYSICAL radius.
static constexpr double FX
static constexpr double SIGMA