51#include <Kokkos_Core.hpp>
66 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
71std::size_t idx(
int x,
int y,
int z,
int n) {
72 return (std::size_t)x + (std::size_t)y * n + (std::size_t)z * (std::size_t)n * n;
79std::vector<double> sphereC(
int n,
double R,
double cx,
double cy,
double cz,
int sub = 24) {
80 std::vector<double>
C((std::size_t)n * n * n, 0.0);
81 const double w = 1.0 / sub;
82 for (
int k = 0;
k <
n; ++
k)
83 for (
int j = 0;
j <
n; ++
j)
84 for (
int i = 0;
i <
n; ++
i) {
86 for (
int b = 0;
b < sub; ++
b)
87 for (
int a = 0;
a < sub; ++
a) {
88 const double px =
i + (
a + 0.5) * w, py = j + (b + 0.5) *
w;
89 const double r2 =
R *
R - (px - cx) * (px - cx) - (py - cy) * (py - cy);
92 const double h = std::sqrt(r2);
93 const double lo = std::fmax(cz - h, (
double)k), hi = std::fmin(cz + h, (
double)k + 1);
97 C[idx(i, j, k, n)] =
acc / (sub * sub);
102double maxAbs(
const std::vector<double>& v) {
105 m = std::fmax(
m, std::fabs(
x));
109 return std::fmax(std::fmax(
maxAbs(
s.getVelocity(0)),
maxAbs(
s.getVelocity(1))),
112double maxAbsDiff(
const std::vector<double>& a,
const std::vector<double>& b) {
117 for (std::size_t i = 0;
i <
a.size(); ++
i) {
118 const double d = std::fabs(a[i] - b[i]);
141std::unique_ptr<IbmSolver> makeDroplet(
const Cfg& c) {
142 auto s = std::make_unique<IbmSolver>(
c.n,
c.n,
c.n);
148 s->setVelocityResidualTolerance(0.0);
149 s->setPressureGeometry(std::vector<double>((std::size_t)
c.n *
c.n *
c.n, 10.0));
150 s->setPressureChebyshev(
true, 500, 1e-14);
152 s->setVof(sphereC(
c.n,
c.R,
c.n / 2 + 0.13,
c.n / 2 + 0.27,
c.n / 2 + 0.11));
153 s->setPropertyModel(
"rho", ClosureKind::LinearMix,
"C",
"", {
c.rhoG,
c.rhoL -
c.rhoG});
154 s->setSurfaceTension(
c.sigma);
156 s->setVofInterfaceEps(
c.eps);
157 s->setCsfMode(
c.mode);
159 s->setVofKappaConstant(
c.kappa);
160 s->setDt(
c.dtFac *
s->capillaryDt());
164double runDroplet(
const Cfg& c,
int steps,
IbmSolver** keep =
nullptr) {
165 auto s = makeDroplet(c);
166 for (
int k = 0;
k <
steps; ++
k)
168 const double m = maxVel(*s);
175void gateExactBalance() {
176 std::printf(
"\n=== P1 THE EXACTNESS GATE: constant kappa -> max|u| at machine zero\n");
177 const int ns[3] = {16, 32, 48};
178 const double Rs[3] = {4.0, 8.0, 12.0};
179 for (
int i = 0;
i < 3; ++
i) {
183 c.kappa = 2.0 / Rs[
i];
184 const double m = runDroplet(c, 30);
185 std::printf(
" n = %2d R = %4.1f mu = 0.1 max|u| = %.4e (Ca = %.2e)\n", ns[i], Rs[i],
186 m,
c.mu * m /
c.sigma);
189 const double mus[4] = {1
e-3, 1
e-2, 1
e-1, 1.0};
190 for (
double mu : mus) {
194 const double m = runDroplet(c, 30);
195 std::printf(
" n = 32 R = 8.0 mu = %-8.4g max|u| = %.4e\n", mu, m);
202 "\n=== P2 the ablation: the SAME constant kappa, forced by a cell-centred sigma*kappa*grad "
204 " face-interpolated like a body force (set_csf_mode(1))\n");
209 const double mb = runDroplet(bal, 30), ma = runDroplet(abl, 30);
210 std::printf(
" balanced-force (face difference) max|u| = %.4e Ca = %.2e\n", mb,
211 bal.mu * mb / bal.sigma);
212 std::printf(
" cell-centred force, interpolated max|u| = %.4e Ca = %.2e -> %.1e x worse\n",
213 ma, abl.mu * ma / abl.sigma, ma / std::fmax(mb, 1e-300));
215 CHECK(ma > 1e6 * std::fmax(mb, 1e-300));
220void gateYoungLaplace() {
221 std::printf(
"\n=== P3 Young-Laplace: the equilibrium pressure field is sigma*kappa*C + const\n");
227 runDroplet(c, 40, &s);
228 std::unique_ptr<IbmSolver> own(s);
229 const auto P =
s->getPressure();
230 const auto C =
s->getVof();
234 for (std::size_t i = 0;
i <
C.size(); ++
i)
240 double res = 0.0, jump = 0.0;
243 for (std::size_t i = 0;
i <
C.size(); ++
i) {
244 res = std::fmax(res, std::fabs(P[i] - p0 -
c.sigma *
c.kappa * C[i]));
250 jump = pl / (double)nl - p0;
251 const double exact =
c.sigma *
c.kappa;
252 std::printf(
" dP(liquid - gas) = %.10f sigma*kappa = %.10f rel err %.3e\n", jump, exact,
253 std::fabs(jump - exact) / exact);
254 std::printf(
" max |P - p0 - sigma*kappa*C| over the WHOLE field = %.3e\n", res);
255 CHECK(std::fabs(jump - exact) / exact < 1e-12);
260void gateCapillaryDt() {
261 std::printf(
"\n=== P4 the capillary time step\n");
262 const double sigma = 2.0, rg = 1.0, rl = 700.0;
264 auto s = std::make_unique<IbmSolver>(n, n, n);
268 s->setPressureGeometry(std::vector<double>((std::size_t)n * n * n, 10.0));
269 s->setPressureChebyshev(
true, 300, 1e-13);
271 s->setVof(sphereC(n, 6.0, n / 2 + 0.13, n / 2 + 0.27, n / 2 + 0.11));
272 s->setPropertyModel(
"rho", ClosureKind::LinearMix,
"C",
"", {rg, rl - rg});
273 s->setSurfaceTension(sigma);
274 const double want = std::sqrt((rg + rl) * 1.0 / (4.0 * M_PI * sigma));
275 const double got =
s->capillaryDt();
276 std::printf(
" closure-driven rho field: dt_sigma = %.12g closed form = %.12g rel %.2e\n",
277 got, want, std::fabs(got - want) / want);
278 CHECK(std::fabs(got - want) / want < 1e-14);
280 s->enableVofMomentum(rg, rl);
281 const double got2 =
s->capillaryDt();
282 std::printf(
" declared phase pair: dt_sigma = %.12g\n", got2);
283 CHECK(std::fabs(got2 - want) / want < 1e-14);
285 s->setDt(1.01 * want);
289 }
catch (
const std::exception& e) {
291 std::printf(
" step() at 1.01 dt_sigma REFUSED, as it must\n");
294 s->setCapillaryCfl(1e30);
295 s->setDt(0.5 * want);
297 std::printf(
" set_capillary_cfl(1e30) disables the check\n");
302 std::printf(
"\n=== P5 inert when off: bitwise identical to the rung-V2a path\n");
305 auto s = std::make_unique<IbmSolver>(n, n, n);
309 s->setPressureGeometry(std::vector<double>((std::size_t)n * n * n, 10.0));
310 s->setPressureChebyshev(
true, 300, 1e-13);
312 s->setVof(sphereC(n, 6.0, n / 2 + 0.13, n / 2 + 0.27, n / 2 + 0.11));
313 s->setPropertyModel(
"rho", ClosureKind::LinearMix,
"C",
"", {1.0, 9.0});
315 s->setSurfaceTension(0.0);
316 s->setVofInterfaceEps(1e-8);
319 const std::size_t
nc = (std::size_t)n * n * n;
320 std::vector<double>
u(nc),
v(nc),
w(nc);
321 for (
int k = 0;
k <
n; ++
k)
322 for (
int j = 0;
j <
n; ++
j)
323 for (
int i = 0;
i <
n; ++
i) {
324 u[idx(i, j, k, n)] = 0.02 * std::sin(2 * M_PI * (j + 0.5) / n);
325 v[idx(i, j, k, n)] = 0.0;
326 w[idx(i, j, k, n)] = 0.0;
328 s->uploadVelocity(u, v, w);
332 for (
int k = 0;
k < 12; ++
k) {
336 const double du = std::fmax(
maxAbsDiff(
a->getVelocity(0),
b->getVelocity(0)),
338 const double dp =
maxAbsDiff(
a->getPressure(),
b->getPressure());
340 std::printf(
" du = %.3e dp = %.3e dC = %.3e\n", du, dp, dc);
341 CHECK(du == 0.0 && dp == 0.0 && dc == 0.0);
345void gateWispGuard() {
347 "\n=== P6 the wisp guard: kappa must not be computed where there is no interface\n"
348 " The GATE is on the mechanism (max|kappa| over the cells the cascade served), which\n"
349 " is deterministic and separated by five orders. The velocity CONSEQUENCE is "
351 " rather than gated: whether a given run is merely degraded or destroyed depends on\n"
352 " whether a garbage-kappa cell happens to sit next to a real one, so it is loud but\n"
353 " configuration-dependent (measured at 64^3, 60 steps: 1.2e-2 unguarded and STILL\n"
354 " GROWING against 1.4e-4 decaying — see the study script).\n");
355 const int ns[2] = {32, 48};
356 const double Rs[2] = {8.0, 12.0};
357 for (
int i = 0;
i < 2; ++
i)
358 for (
double eps : {1
e-8, 0.0}) {
363 auto s = makeDroplet(c);
364 double u0 = 0, uMax = 0, kmax = 0;
365 for (
int k = 0;
k < 40; ++
k) {
373 }
catch (
const std::runtime_error& e) {
374 if (!std::strstr(
e.what(),
"exceeds the Weymouth-Yue boundedness cap"))
376 std::printf(
" n = %2d eps = %-6g DESTROYED at step %d (peak max|u| %.3e): %s\n",
377 ns[i], eps, k + 1, uMax,
e.what());
382 const double u = maxVel(*s);
385 uMax = std::fmax(uMax, u);
389 const double uEnd = maxVel(*s);
390 const auto ka =
s->getVofCurvature();
391 const auto br =
s->getVofCurvatureBranch();
393 for (std::size_t j = 0;
j < ka.size(); ++
j)
394 if (br[j] > 0.5 && br[j] < 5.5) {
396 kmax = std::fmax(kmax, std::fabs(ka[j]));
399 " n = %2d eps = %-6g max|kappa| = %.4e (2/R = %.4f) cells served %4ld "
400 "max|u| %.3e -> %.3e (peak %.3e)\n",
401 ns[i], eps, kmax, 2.0 /
c.R, interf, u0, uEnd, uMax);
403 CHECK(kmax < 1.5 * 2.0 /
c.R);
406 CHECK(kmax > 100.0 * 2.0 /
c.R);
412void gateSpuriousCurrents() {
414 "\n=== P7 spurious currents with the REAL curvature (a measurement, not a pass/fail)\n"
415 " Ca = mu max|u| / sigma after 60 steps; La = sigma rho D / mu^2\n");
416 const int ns[3] = {16, 32, 48};
417 const double Rs[3] = {4.0, 8.0, 12.0};
419 for (
int i = 0;
i < 3; ++
i) {
424 const double m = runDroplet(c, 60, &s);
425 std::unique_ptr<IbmSolver> own(s);
426 const auto d =
s->csfDiagnostics();
427 const double ca =
c.mu *
m /
c.sigma;
428 std::printf(
" n = %2d D/dx = %4.1f La = %6.0f Ca = %.3e%s orphan faces %ld/%ld/%ld\n",
429 ns[i], 2 * Rs[i],
c.sigma *
c.rhoL * 2 * Rs[i] / (
c.mu *
c.mu), ca,
430 i ?
"" :
" ",
d.orphanFaces[0],
d.orphanFaces[1],
d.orphanFaces[2]);
432 std::printf(
" ratio to the coarser rung: %.2f x\n", prev / ca);
435 CHECK(
d.orphanFaces[0] +
d.orphanFaces[1] +
d.orphanFaces[2] < 20);
441int main(
int argc,
char** argv) {
442 Kokkos::initialize(argc, argv);
450 gateSpuriousCurrents();
454 std::fprintf(stderr,
"\nFAILED (%d)\n",
failures);
457 std::printf(
"\nPASSED (0 failures)\n");
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
exact(Ds, K, hc, gs, nmax=1)
static double maxAbs(const std::vector< double > &a)
static double maxAbsDiff(const std::vector< double > &a, const std::vector< double > &b)