flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_openbc_solid.cpp
Go to the documentation of this file.
1// Immersed solid TOGETHER with inflow/outflow domain boundary conditions -- the combination no
2// test anywhere covered, and the two defects that hid in the gap (SCALING_ISSUES #3,
3// doc/cutcell_openbc_convergence.md). One duct (-x inflow, +x outflow, y/z no-slip walls) and four
4// beds: no solid, a sphere clear of both open faces, a sphere cut by the outlet, a sphere cut by
5// the inlet. All four must converge and leave the PROJECTED open divergence at solver tolerance.
6//
7// Before the fix:
8// * the inlet-cut bed ran MG-PCG to its iteration cap with max|div| stuck at exactly the inflow
9// velocity -- the SDF ghost outside a non-periodic face was filled by PERIODIC WRAP, so a
10// solid cell against the inlet was handed the far side's fluid and came out fully OPEN, and
11// the prescribed inflow was pushed into a cell whose pressure row is entirely closed;
12// * the outlet-cut bed converged but was SILENTLY WRONG, because the Dirichlet outlet row
13// carried the literal openness 1.0 instead of the face's own aperture, so mass left through
14// solid. That half is pinned here by its own ablation (set_outflow_operator_coefficient).
15//
16// Note which divergence the gate reads: `maxOpenDivergence()` refills the outflow ghost with the
17// zero-gradient extrapolation BEFORE measuring, so at a partly blocked outlet it reports how far
18// zero-gradient is from the mass-conserving face -- a property of the boundary condition, not a
19// solver residual, and it does NOT decay. `maxOpenDivergenceProjected()` measures the corrected
20// face field and is the residual of the constraint the projection actually solved.
21#include <cmath>
22#include <cstdio>
23#include <Kokkos_Core.hpp>
24#include <stdexcept>
25#include <vector>
26
27#include "flow_ibm.hpp"
28
29namespace {
30int failures = 0;
31#define CHECK(cond) \
32 do { \
33 if (!(cond)) { \
34 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
35 ++failures; \
36 } \
37 } while (0)
38
39constexpr int NX = 32, NY = 16, NZ = 16, STEPS = 8, PMAXIT = 200;
40constexpr double U = 1.0, R = 4.3;
41
42// Cell centres sit at integer indices, so the domain spans [-0.5, N-0.5] on each axis: a sphere
43// centred at x = -0.5 is cut in half by the inlet plane, one at x = NX-0.5 by the outlet plane.
44std::vector<double> sphereSdf(double cx) {
45 std::vector<double> sdf((std::size_t)NX * NY * NZ);
46 const double cy = 0.5 * (NY - 1), cz = 0.5 * (NZ - 1);
47 for (int z = 0; z < NZ; ++z)
48 for (int y = 0; y < NY; ++y)
49 for (int x = 0; x < NX; ++x) {
50 const double dx = x - cx, dy = y - cy, dz = z - cz;
51 sdf[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY] =
52 std::sqrt(dx * dx + dy * dy + dz * dz) - R;
53 }
54 return sdf;
55}
56
57template <class S>
58void configure(S& s) {
59 s.setRho(1.0);
60 s.setMu(1.0);
61 s.setDt(0.5);
62 s.setAdvection(false);
63 s.setVelocityIterations(200);
64 s.setVelocityResidualTolerance(1e-10);
65 s.setPressureLevels(3);
66 s.setPressurePcg(true, PMAXIT, 1e-10);
67 s.setDomainBc(0, 2, U, 0.0, 0.0); // -x inflow
68 s.setDomainBc(1, 3, 0.0, 0.0, 0.0); // +x outflow
69 for (int f = 2; f < 6; ++f)
70 s.setDomainBc(f, 1, 0.0, 0.0, 0.0); // y/z no-slip walls
71}
72
73struct Result {
74 long iters;
75 double divp;
76};
77
78// cx < 0 with solid == false selects the all-fluid control.
79template <class S>
80Result run(double cx, bool solid, bool outflowOpCoeff = true) {
81 S s(NX, NY, NZ);
82 configure(s);
83 s.setOutflowOperatorCoefficient(outflowOpCoeff);
84 if (solid)
85 s.setSolid(sphereSdf(cx), /*cutcellPressure=*/true);
86 else
87 s.setPressureGeometry(std::vector<double>((std::size_t)NX * NY * NZ, 1e30));
88 Result r{0, 0.0};
89 for (int it = 0; it < STEPS; ++it) {
90 s.step();
91 r.iters = s.lastPressureIterations();
92 r.divp = s.maxOpenDivergenceProjected();
93 }
94 return r;
95}
96
97// A single fluid cell against the inlet plane, walled in by solid on its other five faces: the
98// pressure row is entirely closed while the inflow still feeds it. No pressure field satisfies
99// that, so set_solid must REJECT the geometry rather than hand the solve an inconsistent row.
100template <class S>
101bool sealedPocketRejected() {
102 S s(NX, NY, NZ);
103 configure(s);
104 std::vector<double> sdf((std::size_t)NX * NY * NZ, -1.0); // solid everywhere ...
105 const int py = NY / 2, pz = NZ / 2;
106 sdf[(std::size_t)0 + (std::size_t)py * NX + (std::size_t)pz * NX * NY] = 1.0; // ... but one cell
107 for (int x = 4; x < NX; ++x) // plus a fluid bulk downstream, so the rest of the run is sane
108 for (int z = 0; z < NZ; ++z)
109 for (int y = 0; y < NY; ++y)
110 sdf[(std::size_t)x + (std::size_t)y * NX + (std::size_t)z * NX * NY] = 1.0;
111 try {
112 s.setSolid(sdf, /*cutcellPressure=*/true);
113 } catch (const std::exception& e) {
114 std::printf("[openbc-solid] sealed inlet pocket rejected: %s\n", e.what());
115 return true;
116 }
117 return false;
118}
119} // namespace
120
121int main(int argc, char** argv) {
122 Kokkos::initialize(argc, argv);
123 {
124 // ---------------------------------------------------------------- STAGGERED (the reference)
125 const Result empty = run<peclet::flow::IbmSolver>(0.0, false);
126 const Result clear = run<peclet::flow::IbmSolver>(0.5 * NX, true);
127 const Result cutOut = run<peclet::flow::IbmSolver>(NX - 0.5, true);
128 const Result cutIn = run<peclet::flow::IbmSolver>(-0.5, true);
129 const char* fmt = "[openbc-solid] staggered %-26s iters %3ld projected max|div| %.3e\n";
130 std::printf(fmt, "all-fluid duct:", empty.iters, empty.divp);
131 std::printf(fmt, "sphere clear of the faces:", clear.iters, clear.divp);
132 std::printf(fmt, "sphere cutting the OUTLET:", cutOut.iters, cutOut.divp);
133 std::printf(fmt, "sphere cutting the INLET:", cutIn.iters, cutIn.divp);
134
135 const Result all[4] = {empty, clear, cutOut, cutIn};
136 for (const Result& r : all) {
137 CHECK(r.iters < PMAXIT); // the inlet-cut bed used to burn the cap on every step
138 CHECK(r.divp < 1e-7); // measured 3e-14 .. 6.2e-9
139 }
140
141 // The outlet half, pinned by its ablation: with the Dirichlet row back on the literal openness
142 // 1.0 the operator disagrees with the divergence constraint by (1 - aperture), and the
143 // projected divergence PLATEAUS four orders up instead of falling to solver tolerance.
144 const Result ablated = run<peclet::flow::IbmSolver>(NX - 0.5, true, /*outflowOpCoeff=*/false);
145 std::printf(
146 "[openbc-solid] staggered %-26s iters %3ld projected max|div| %.3e (the defect)\n",
147 "OUTLET, literal-1.0 row:", ablated.iters, ablated.divp);
148 CHECK(ablated.divp > 1e-4); // measured 6.7e-3
149
150 CHECK(sealedPocketRejected<peclet::flow::IbmSolver>());
151
152 // ------------------------------------------------------------------------------- COLLOCATED
153 // Every part of the fix is in the GEOMETRY, which is grid-independent: the SDF ghost
154 // extension, the boundary-face aperture the Dirichlet row carries, and the sealed-inlet
155 // rejection all run identically on `SolverColocated`. Nothing gated them there, which is the
156 // whole reason this block exists -- not because the collocated path needed its own fix.
157 //
158 // It reads a DIFFERENT divergence, and that is a property of the grid, not of this work:
159 // `maxOpenDivergenceProjected` delegates to `maxOpenDivergenceInternal` on the collocated
160 // path, which re-imposes the zero-gradient outflow face before measuring. So the number here
161 // is the same "how far is zero-gradient from the mass-conserving face" quantity the staggered
162 // `max_open_divergence()` reports, and it does not fall to solver tolerance at a partly
163 // blocked outlet. The gate is therefore the iteration count -- which is what the defect
164 // actually broke, capping every step -- plus the rejection. Measured against the pre-fix
165 // build, the inlet-cut bed reads 200 iters / max|div| 1.000e+00 here exactly as it does on the
166 // staggered grid, and the sealed pocket is not rejected: the defect was never grid-specific.
168 const Result cEmpty = run<Colo>(0.0, false);
169 const Result cClear = run<Colo>(0.5 * NX, true);
170 const Result cCutOut = run<Colo>(NX - 0.5, true);
171 const Result cCutIn = run<Colo>(-0.5, true);
172 const char* cfmt = "[openbc-solid] collocated %-26s iters %3ld max|div| %.3e\n";
173 std::printf(cfmt, "all-fluid duct:", cEmpty.iters, cEmpty.divp);
174 std::printf(cfmt, "sphere clear of the faces:", cClear.iters, cClear.divp);
175 std::printf(cfmt, "sphere cutting the OUTLET:", cCutOut.iters, cCutOut.divp);
176 std::printf(cfmt, "sphere cutting the INLET:", cCutIn.iters, cCutIn.divp);
177 const Result cAll[4] = {cEmpty, cClear, cCutOut, cCutIn};
178 for (const Result& r : cAll)
179 CHECK(r.iters < PMAXIT);
180 CHECK(sealedPocketRejected<Colo>());
181
182 if (failures == 0)
183 std::printf("[openbc-solid] OK\n");
184 }
185 Kokkos::finalize();
186 return failures == 0 ? 0 : 1;
187}
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 std::vector< double > sphereSdf()
static void configure(IbmSolver &s)
static int run(int bc)
static constexpr double U
#define CHECK(cond)
static constexpr int PMAXIT