flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_pressure_wallbounded.cpp
Go to the documentation of this file.
1// WO-H — 3-D WALL-BOUNDED PRESSURE CONVERGENCE, all three Krylov drivers.
2//
3// The coverage gap this closes. Every shipped domain-BC verification script (lid cavity vs Ghia,
4// developing channel, backward-facing step, de Vahl Davis) is quasi-2D — the third axis is 4 cells
5// — and 4 cells is inside the healthy regime. So from July 2026 until WO-B (2026-08-30) nothing
6// exercised the pressure solve on a genuinely 3-D wall-bounded grid, and MG-PCG stalled there:
7// 200/200 iterations with max|div(open*u)|/u ~ 1e-5 once the third axis reached 8 cells, while
8// Chebyshev took 13-14 on the identical operator. The cause (WO-C -> WO-H) was that the coarse
9// levels' PERIODIC ghost fill left a walled face's ghost holding the value from the opposite side
10// of the domain, and `prolongAdd` reads that ghost with weight 1/4 whatever the face openness — a
11// long-range coupling present in the prolongation and absent from the restriction, i.e. an
12// asymmetric V-cycle preconditioner, which is exactly what CG cannot tolerate.
13// `CutcellMG::applyNeumannGhost` is the repair.
14//
15// What this test gates, on nz >= 8 (never 4):
16// A. lid box, CONSTANT density — the WO-B headline configuration (PCG was 200/200 here);
17// B. hydrostatic column, constant density, uniform gravity;
18// C. RESIDUAL #1 of the WO-C findings: the gravity-driven hydrostatic column with a GLOBAL
19// density stratification (varRho, sharp slab). Both CG drivers used to run a *stationary*
20// iteration here — the residual rose to r/r0 = 6.98 and froze for 199 iterations — while
21// Chebyshev returned the machine-exact rest state;
22// D. RESIDUAL #2 of the WO-C findings: a small coefficient rho0/rho_f adjacent to a
23// PRESCRIBED-VELOCITY (inflow-type, BC type 2) face — heavy fluid against the lid, so the
24// operator coefficient there is 1/ratio — at ratio 1e2 and 1e3.
25// Each runs with driver in {MG-PCG, flexible MG-CG, Chebyshev} and is gated on BOTH the iteration
26// count (never the cap) and the physics (max|div(open*u)| relative to the case's velocity scale) —
27// a low iteration count alone is not a good solve, because the CG breakdown guard exits early and
28// keeps the last finite iterate.
29//
30// Demonstrating the failure: --no-coarse-ghost restores the pre-repair periodic-wrap coarse
31// ghost (measurement ablation only). Every PCG configuration below fails under it; the test prints
32// which mode it is running so an ablation run is self-labelling.
33#include <algorithm>
34#include <cmath>
35#include <cstdio>
36#include <cstdlib>
37#include <cstring>
38#include <Kokkos_Core.hpp>
39#include <string>
40#include <vector>
41
42#include "flow_ibm.hpp"
43
44namespace {
45int failures = 0;
46#define CHECK(cond) \
47 do { \
48 if (!(cond)) { \
49 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
50 ++failures; \
51 } \
52 } while (0)
53
54constexpr int NX = 24, NY = 24, NZ = 16; // the third axis is 16 cells: 4x the quasi-2D regime
55constexpr int LEVELS = 4;
56constexpr int MAXIT = 200;
57constexpr double RTOL = 1e-8;
58constexpr int STEPS = 8;
59constexpr double MU = 0.01, DT = 1.0, ULID = 1.0, GRAV = 0.1;
60// Health floor: the WO-B stall plateaus at div/u ~ 1e-5, four orders above the round-off floor a
61// converged solve reaches (1e-11 ... 1e-23). 1e-8 is the WO-B/WO-C battery's own `healthy` cut.
62constexpr double DIV_TOL = 1e-8;
63
64enum Driver { kPcg = 0, kFcg = 1, kCheb = 2 };
65const char* driverName(Driver d) {
66 return d == kPcg ? "MG-PCG" : (d == kFcg ? "FCG " : "Cheb ");
67}
68
69double maxAbs(const std::vector<double>& v) {
70 double m = 0;
71 for (double x : v)
72 m = std::fmax(m, std::fabs(x));
73 return m;
74}
75
76void selectDriver(peclet::flow::IbmSolver& s, Driver d) {
77 // Exercises the WO-H selector repair as a side effect: on the varRho cases below this call comes
78 // AFTER setDensityMode (which defaults the driver to Chebyshev), and `setPressurePcg(true, ...)`
79 // must genuinely switch back — before the repair its `on` flag was discarded and the solve
80 // silently stayed on Chebyshev.
81 if (d == kPcg)
82 s.setPressurePcg(true, MAXIT, RTOL);
83 else if (d == kFcg)
84 s.setPressureFcg(true, MAXIT, RTOL);
85 else
86 s.setPressureChebyshev(true, MAXIT, RTOL);
87}
88
89// One configuration x one driver. `varRho`: 0 = constant density, else the density ratio of a sharp
90// z-slab; `heavyAbove` puts the heavy fluid (hence the SMALL coefficient rho0/rho_f) against +z.
91// `lid`: +z is a prescribed-velocity face (BC type 2) instead of a wall; `gravity`: gravity closure
92// (varRho) or a uniform body force (constant density).
93struct Case {
94 const char* name;
95 bool lid;
96 bool gravity;
97 double ratio; // 0 => constant density
98 bool heavyAbove;
99 // Which drivers are GATED here (bit d). The ungated ones are still run and printed, tagged
100 // "open" — they are the residual defect WO-H measured and did NOT fix: at a high density
101 // CONTRAST the arithmetic coarsening of the face coefficient makes the V-cycle preconditioner
102 // INDEFINITE (measured directly: sym(M) assembled densely on an 8^3 walled box has a negative
103 // LDL pivot from ratio ~1e3, and one at ratio 1e4 even fully PERIODIC), and no choice of the CG
104 // beta survives an indefinite preconditioner. That is the coefficient-aware-coarsening item
105 // (VOF_PLAN S3), not the boundary-symmetry item this test's cases A/B pin down; Chebyshev, which
106 // needs only real spectrum bounds, is healthy on all of them. See doc/vof_workorders.md WO-H.
107 int gate;
108};
109
112bool gAblate = false;
113
114void run(const Case& cs, Driver d) {
116 s.setPressureCoarseGhost(!gAblate);
117 s.setRho(1.0);
118 s.setMu(MU);
119 s.setDt(DT);
120 s.setAdvection(false); // creeping Stokes: isolate the pressure solve
121 s.setDomainBc(4, 1, 0, 0, 0);
122 if (cs.lid)
123 s.setDomainBc(5, 2, ULID, 0.0, 0.0); // prescribed velocity (inflow-type) face
124 else
125 s.setDomainBc(5, 1, 0, 0, 0);
126 if (cs.lid) { // the lid box also walls +-x (periodic y), as in the WO-B battery
127 s.setDomainBc(0, 1, 0, 0, 0);
128 s.setDomainBc(1, 1, 0, 0, 0);
129 }
130 s.setPressureLevels(LEVELS);
131 s.setPressureGeometry(std::vector<double>((std::size_t)NX * NY * NZ, 10.0)); // all fluid
132
133 if (cs.ratio > 0.0) {
134 std::vector<double> rho((std::size_t)NX * NY * NZ);
135 for (int z = 0; z < NZ; ++z) {
136 const bool heavy = cs.heavyAbove ? (z >= NZ / 2) : (z < NZ / 2);
137 for (int y = 0; y < NY; ++y)
138 for (int x = 0; x < NX; ++x)
139 rho[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY] =
140 heavy ? cs.ratio : 1.0;
141 }
142 s.addField("rho");
143 s.setField("rho", rho);
144 s.setDensityMode(true); // defaults the driver to Chebyshev — selectDriver must override it
145 if (cs.gravity)
146 s.setPropertyModel("force_z", peclet::flow::ClosureKind::LinearMix, "rho", "", {0.0, -GRAV});
147 } else if (cs.gravity) {
148 s.setBodyForce(0.0, 0.0, -GRAV);
149 }
150 selectDriver(s, d);
151
152 long itMax = 0, itMed = 0;
153 std::vector<long> its;
154 double div = 0;
155 for (int k = 0; k < STEPS; ++k) {
156 s.step();
157 its.push_back(s.lastPressureIterations());
158 div = s.maxOpenDivergence();
159 }
160 const double umax = std::fmax(maxAbs(s.getVelocity(0)),
161 std::fmax(maxAbs(s.getVelocity(1)), maxAbs(s.getVelocity(2))));
162 double uscale = std::fmax(umax, cs.lid ? ULID : GRAV * DT);
163 const double divRel = div / uscale;
164 for (long v : its)
165 itMax = std::max(itMax, v);
166 {
167 std::vector<long> sorted = its;
168 std::sort(sorted.begin(), sorted.end());
169 itMed = sorted[sorted.size() / 2];
170 }
171 const bool gated = (cs.gate >> (int)d) & 1;
172 std::printf(" %-22s %s its median %3ld max %3ld div/u %.2e max|u| %.3e%s\n", cs.name,
173 driverName(d), itMed, itMax, divRel, umax,
174 gated ? "" : " [open: S3 coefficient coarsening, not gated]");
175 fflush(stdout);
176 if (!gated)
177 return;
178 CHECK(std::isfinite(umax));
179 CHECK(itMax < MAXIT); // never the cap: the stall ran 200/200 on every step
180 CHECK(divRel < DIV_TOL); // and the projection must actually be converged
181}
182} // namespace
183
184int main(int argc, char** argv) {
185 for (int i = 1; i < argc; ++i)
186 if (std::strcmp(argv[i], "--no-coarse-ghost") == 0)
187 gAblate = true;
188 Kokkos::initialize(argc, argv);
189 {
190 std::printf("3-D wall-bounded pressure convergence: %dx%dx%d, %d levels, %d steps, cap %d%s\n",
191 NX, NY, NZ, LEVELS, STEPS, MAXIT,
192 gAblate ? " [--no-coarse-ghost — pre-WO-H ablation, EXPECTED TO FAIL]" : "");
193 const int ALL = (1 << kPcg) | (1 << kFcg) | (1 << kCheb);
194 const int FCG_CHEB = (1 << kFcg) | (1 << kCheb);
195 const int CHEB_ONLY = (1 << kCheb);
196 const Case cases[] = {
197 // name lid grav ratio heavyAbove gated drivers
198 {"A lid box const-rho", true, false, 0.0, false, ALL},
199 {"B hydrostatic const", false, true, 0.0, false, ALL},
200 {"C hydro strat 1e2", false, true, 1e2, false, FCG_CHEB},
201 {"C hydro strat 1e3", false, true, 1e3, false, CHEB_ONLY},
202 {"D inflow small-c 1e2", true, false, 1e2, true, FCG_CHEB},
203 {"D inflow small-c 1e3", true, false, 1e3, true, FCG_CHEB},
204 };
205 for (const Case& c : cases)
206 for (Driver d : {kPcg, kFcg, kCheb})
207 run(c, d);
208 if (failures == 0)
209 std::printf("PASS: %zu wall-bounded configurations x 3 drivers (14 gated) on nz = %d\n",
210 sizeof(cases) / sizeof(cases[0]), NZ);
211 else
212 std::fprintf(stderr, "FAIL: %d check(s)\n", failures);
213 }
214 Kokkos::finalize();
215 return failures ? 1 : 0;
216}
int main()
Definition dbg_scene.cpp:13
flow — host-facing Kokkos IBM Navier-Stokes solver (drop-in flow-style API).
x
Definition cmp.py:24
y
Definition cmp.py:24
static constexpr int NY
static constexpr int NX
static constexpr int NZ
static constexpr int STEPS
static double maxAbs(const std::vector< double > &a)
static constexpr double GRAV
static constexpr double DT
static constexpr double MU
static int run(int bc)
static constexpr int LEVELS
#define CHECK(cond)
static constexpr double ULID