flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
test_scalar_cutcell_geometry.cpp
Go to the documentation of this file.
1// Cut-cell scalar geometry record (doc/scalar_ibm_design.md §2, §3; WO-2), single rank.
2//
3// ghosts — §2.5 prerequisite: sdf_ carries BOTH ghost layers on every single-rank path — the
4// periodic wrap, and extendSdfDomainGhosts' constant extension (wall) and mirror
5// ('slip'). Fails if layer 2 is stale on any of them.
6// G-geom (e) spheres R/h in {8, 16, 32}, 3 random offsets: |V_sphere / V_exact - 1| <= 2 (h/R)^2
7// and |sum |A_phi| / 4 pi R^2 - 1| <= 3 (h/R)^2, each with order >= 1.8 (RMS over the
8// offsets, between successive resolutions); every cut cell `single` with rho >= 0.9;
9// sealed volume <= 1e-6 of the fluid volume (ruling 2026-10-03; the count is
10// reported); R0 = 100 %.
11// G-geom (f) |A^snap - areaPL| <= 2e-3 max A per cell.
12// G-geom (g) anisotropic h' = (1, 1, 2): (e) with h = max h', R0 >= 99.9 % and no R2.
13// pipe — G7's pipe (fluid inside, z periodic, nz = 4), R/h in {16, 32, 64}: R0 = 100 %, 0
14// sealed.
15//
16// The sphere is SOLID (the G1 configuration), so the measured volume is sum (1 - kappa) V over
17// the inner cells. Per-cell PL records (rho, kind, areaPL) are recomputed on the host from a
18// mirror of sdf_ through the SAME sample assembly (scg::cellSamples) and core kernel, and the
19// device record's kappa must equal them bitwise on a host backend (both sides -ffp-contract=off).
20// On a device backend (WO-9) nvcc contracts FMAs inside the kernel, so the clause there is G11's
21// geometry tolerance: |kappa_device - kappa_host| <= 1e-12 (kappa in [0, 1]); the facet count must
22// still match exactly.
23#include <cmath>
24#include <cstdio>
25#include <cstring>
26#include <Kokkos_Core.hpp>
27#include <random>
28#include <type_traits>
29#include <vector>
30
31#include "flow_ibm.hpp"
32
33namespace {
34int failures = 0;
35#define CHECK(cond) \
36 do { \
37 if (!(cond)) { \
38 std::fprintf(stderr, "CHECK failed: %s\n at %s:%d\n", #cond, __FILE__, __LINE__); \
39 ++failures; \
40 } \
41 } while (0)
42
43namespace cs = peclet::core::scheme;
44namespace scg = peclet::flow::scg;
46constexpr int G = IbmSolver::G;
47
48// ---------------------------------------------------------------------------------------------
49// ghosts: expected global index of a ghost sample beyond the domain on one axis
50// ---------------------------------------------------------------------------------------------
51int expectIndex(int i, int n, int bcLo, int bcHi) {
52 if (i >= 0 && i < n)
53 return i;
54 const int t = i < 0 ? bcLo : bcHi;
55 if (t == 0)
56 return ((i % n) + n) % n; // periodic
57 if (t == 4)
58 return i < 0 ? -1 - i : 2 * n - 1 - i; // mirror about the face
59 return i < 0 ? 0 : n - 1; // constant normal extension
60}
61
62// sdf_ ghost layers 1 and 2 on every face slab (transverse inner range) against the expected
63// global sample. `bc` per face (0 periodic, 1 wall, 4 slip).
64void checkGhosts(const char* label, const int bc[6]) {
65 const int nx = 12, ny = 10, nz = 8;
66 auto f = [&](int x, int y, int z) { return 1000.0 * x + 37.0 * y + 1.25 * z - 300.0; };
67 std::vector<double> sdf((std::size_t)nx * ny * nz);
68 for (int z = 0; z < nz; ++z)
69 for (int y = 0; y < ny; ++y)
70 for (int x = 0; x < nx; ++x)
71 sdf[(std::size_t)x + (std::size_t)y * nx + (std::size_t)z * nx * ny] = f(x, y, z);
72 IbmSolver s(nx, ny, nz);
73 for (int face = 0; face < 6; ++face)
74 if (bc[face] != 0)
75 s.setDomainBc(face, bc[face], 0.0, 0.0, 0.0);
76 s.setSolid(sdf, false);
77 auto h = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), s.fieldView("sdf"));
78 const int ex = nx + 2 * G, ey = ny + 2 * G;
79 const int n[3] = {nx, ny, nz};
80 long bad = 0, checked = 0;
81 for (int a = 0; a < 3; ++a)
82 for (int side = 0; side < 2; ++side)
83 for (int layer = 1; layer <= G; ++layer) {
84 const int ia = side ? n[a] - 1 + layer : -layer;
85 const int b = (a + 1) % 3, c = (a + 2) % 3;
86 for (int jb = 0; jb < n[b]; ++jb)
87 for (int jc = 0; jc < n[c]; ++jc) {
88 int g3[3];
89 g3[a] = ia;
90 g3[b] = jb;
91 g3[c] = jc;
92 const double got =
93 h((long)(g3[0] + G) + (long)(g3[1] + G) * ex + (long)(g3[2] + G) * (long)ex * ey);
94 const int ex3[3] = {expectIndex(g3[0], nx, bc[0], bc[1]),
95 expectIndex(g3[1], ny, bc[2], bc[3]),
96 expectIndex(g3[2], nz, bc[4], bc[5])};
97 ++checked;
98 if (got != f(ex3[0], ex3[1], ex3[2]))
99 ++bad;
100 }
101 }
102 std::printf(" ghosts %-28s: %ld / %ld ghost samples (layers 1-2) stale\n", label, bad, checked);
103 CHECK(bad == 0);
104}
105
106// ---------------------------------------------------------------------------------------------
107// one geometry: the record + host recomputation
108// ---------------------------------------------------------------------------------------------
109struct Measure {
110 double solidVol = 0.0, fluidVol = 0.0, area = 0.0; // physical (hRef = 1)
111 long cutCells = 0, notSingle = 0, lowRho = 0, sealed = 0, facets = 0, r0 = 0, r2 = 0;
112 double minRho = 1.0, maxSnapRes = 0.0; // (f): max |A^snap - areaPL| / max A
113 double sealedMaxKappa = 0.0, sealedVolFrac = 0.0; // evidence for the '0 sealed' clause
114 long kappaMismatch = 0, countMismatch = 0;
115 double maxKappaDiff = 0.0; // max |kappa_device - kappa_host| (0 on a host backend)
116};
117
118// WO-9 / G11: a device execution space computes the record with FMA contraction.
119constexpr bool kDeviceBackend =
120 !std::is_same_v<Kokkos::DefaultExecutionSpace::memory_space, Kokkos::HostSpace>;
121
122Measure measure(IbmSolver& s, const int n[3], const double hp[3]) {
123 Measure m;
124 const auto& rec = s.scalarCutGeometry();
125 const auto c = s.scalarCutCensus();
126 const int ex = n[0] + 2 * G, ey = n[1] + 2 * G;
127 const long sy = ex, sz = (long)ex * ey;
128 auto sdf = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), s.fieldView("sdf"));
129 auto kap = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), rec.kappa);
130 auto sax = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), rec.sax);
131 auto say = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), rec.say);
132 auto saz = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), rec.saz);
133 auto alpha = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), rec.fac.alpha);
134 auto rung = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), rec.fac.rungF);
135 const double vol = hp[0] * hp[1] * hp[2];
136 const double maxA = std::fmax(hp[1] * hp[2], std::fmax(hp[0] * hp[2], hp[0] * hp[1]));
137 long nfacHost = 0;
138 for (int z = G; z < n[2] + G; ++z)
139 for (int y = G; y < n[1] + G; ++y)
140 for (int x = G; x < n[0] + G; ++x) {
141 const long i = x + y * sy + z * sz;
142 double corner[8], face[6], centre;
143 scg::cellSamples(sdf, i, sy, sz, corner, face, centre);
144 const cs::CutCellGeometry g = cs::cutCellGeometryFanTet(corner, face, centre, hp);
145 m.maxKappaDiff = std::fmax(m.maxKappaDiff, std::fabs(g.kappa - kap(i)));
146 if (kDeviceBackend ? !(std::fabs(g.kappa - kap(i)) <= 1e-12)
147 : std::memcmp(&g.kappa, &kap(i), sizeof(double)) != 0)
148 ++m.kappaMismatch;
149 m.fluidVol += g.kappa * vol;
150 m.solidVol += (1.0 - g.kappa) * vol;
151 double ap[6];
152 scg::cellSnapped(sax, say, saz, i, sy, sz, ap);
153 double area[2][3], cen[2][3];
154 nfacHost += scg::cellFacets(g, ap, hp, area, cen);
155 if (g.nFacet >= 1) {
156 ++m.cutCells;
157 const double rho = std::sqrt(g.areaPL[0] * g.areaPL[0] + g.areaPL[1] * g.areaPL[1] +
158 g.areaPL[2] * g.areaPL[2]) /
159 g.areaSum;
160 m.minRho = std::fmin(m.minRho, rho);
161 if (g.kind != cs::CutCellKind::single)
162 ++m.notSingle;
163 if (rho < 0.9)
164 ++m.lowRho;
165 double aSnap[3];
166 cs::facetAreaVector(ap, hp, aSnap);
167 for (int d = 0; d < 3; ++d)
168 m.maxSnapRes = std::fmax(m.maxSnapRes, std::fabs(aSnap[d] - g.areaPL[d]) / maxA);
169 }
170 }
171 for (long f = 0; f < rec.fac.n; ++f) {
172 m.area += alpha(f) * vol;
173 if (rung(f) == cs::kProbeR0)
174 ++m.r0;
175 if (rung(f) == cs::kProbeR2)
176 ++m.r2;
177 }
178 m.facets = rec.fac.n;
179 m.sealed = c.numSealed;
180 m.sealedVolFrac = c.sealedVolume / m.fluidVol; // hRef = 1: physical = internal
181 {
182 auto unk = Kokkos::create_mirror_view_and_copy(Kokkos::HostSpace(), rec.unknown);
183 for (int z = G; z < n[2] + G; ++z)
184 for (int y = G; y < n[1] + G; ++y)
185 for (int x = G; x < n[0] + G; ++x) {
186 const long i = x + y * sy + z * sz;
187 if (kap(i) > 0.0 && unk(i) < 0.5)
188 m.sealedMaxKappa = std::fmax(m.sealedMaxKappa, kap(i));
189 }
190 }
191 if (nfacHost != rec.fac.n)
192 ++m.countMismatch;
193 CHECK(c.numFacets == rec.fac.n);
194 CHECK(c.rungs[0] + c.rungs[1] + c.rungs[2] + c.rungs[3] == rec.fac.n);
195 return m;
196}
197
198// Sphere of radius R (physical = internal, hRef = 1) on cells n over extent n*hp, centre at the
199// box centre + off. SOLID inside.
200Measure sphereCase(const int n[3], const double hp[3], double R, const double off[3]) {
201 const std::array<double, 3> extent{n[0] * hp[0], n[1] * hp[1], n[2] * hp[2]};
202 IbmSolver s(n[0], n[1], n[2], extent, {0.0, 0.0, 0.0}, {(long)n[0], (long)n[1], (long)n[2]});
203 std::vector<double> sdf((std::size_t)n[0] * n[1] * n[2]);
204 const double c[3] = {0.5 * extent[0] + off[0], 0.5 * extent[1] + off[1],
205 0.5 * extent[2] + off[2]};
206 for (int z = 0; z < n[2]; ++z)
207 for (int y = 0; y < n[1]; ++y)
208 for (int x = 0; x < n[0]; ++x) {
209 const double dx = (x + 0.5) * hp[0] - c[0], dy = (y + 0.5) * hp[1] - c[1],
210 dz = (z + 0.5) * hp[2] - c[2];
211 sdf[(std::size_t)x + (std::size_t)y * n[0] + (std::size_t)z * n[0] * n[1]] =
212 std::sqrt(dx * dx + dy * dy + dz * dz) - R;
213 }
214 s.setSolid(sdf, false);
215 return measure(s, n, hp);
216}
217
218// G-geom (e) / (g): returns 0 on pass.
219// `strictR0`: R0 = 100 % (the isotropic set). Otherwise (h' = (1,1,2)) R0 >= 99.9 % and no R2 —
220// orchestrator ruling 2026-10-03 (doc/scalar_ibm_log.md, WO-2): a tiny corner facet's snapped area
221// vector can point exactly along the coarse axis and its probe take R1b.
222void sphereSet(const char* label, const double hp[3], bool strictR0) {
223 const double h = std::fmax(hp[0], std::fmax(hp[1], hp[2]));
224 const double ratios[3] = {8.0, 16.0, 32.0};
225 std::mt19937_64 rng(4242);
226 std::uniform_real_distribution<double> u(-0.5, 0.5);
227 double rmsV[3], rmsA[3];
228 for (int r = 0; r < 3; ++r) {
229 const double R = ratios[r] * h;
230 int n[3];
231 for (int a = 0; a < 3; ++a)
232 n[a] = (int)std::lround((2.0 * R + 8.0 * h) / hp[a]);
233 const double Vex = 4.0 / 3.0 * M_PI * R * R * R, Aex = 4.0 * M_PI * R * R;
234 const double bound2 = (h / R) * (h / R);
235 double sV = 0.0, sA = 0.0;
236 for (int o = 0; o < 3; ++o) {
237 const double off[3] = {u(rng) * h, u(rng) * h, u(rng) * h};
238 const Measure m = sphereCase(n, hp, R, off);
239 const double eV = m.solidVol / Vex - 1.0, eA = m.area / Aex - 1.0;
240 sV += eV * eV;
241 sA += eA * eA;
242 std::printf(
243 " %s R/h=%2.0f off %d: eV %+.3e (bound %.2e) eA %+.3e (bound %.2e) cut %ld "
244 "facets %ld R0 %ld R2 %ld min rho %.4f not-single %ld sealed %ld (max kappa %.1e, "
245 "vol/fluid %.1e) (f) %.2e\n",
246 label, ratios[r], o, eV, 2.0 * bound2, eA, 3.0 * bound2, m.cutCells, m.facets, m.r0, m.r2,
247 m.minRho, m.notSingle, m.sealed, m.sealedMaxKappa, m.sealedVolFrac, m.maxSnapRes);
248 CHECK(std::fabs(eV) <= 2.0 * bound2);
249 CHECK(std::fabs(eA) <= 3.0 * bound2);
250 CHECK(m.notSingle == 0 && m.lowRho == 0);
251 // '0 sealed' restated (orchestrator ruling 2026-10-03): the sealed volume sum kappa V over
252 // sealed cells <= 1e-6 of the fluid volume, the §9 threshold. The count stays reported.
253 CHECK(m.sealedVolFrac <= 1e-6);
254 CHECK(m.facets > 0);
255 if (strictR0)
256 CHECK(m.r0 == m.facets);
257 else
258 CHECK((double)m.r0 >= 0.999 * (double)m.facets && m.r2 == 0);
259 CHECK(m.maxSnapRes <= 2e-3);
260 if (m.kappaMismatch != 0 || m.countMismatch != 0)
261 std::printf(" record vs host: %ld kappa mismatches (max diff %.2e), facet count %s\n",
262 m.kappaMismatch, m.maxKappaDiff, m.countMismatch ? "DIFFERS" : "equal");
263 CHECK(m.kappaMismatch == 0 && m.countMismatch == 0);
264 }
265 rmsV[r] = std::sqrt(sV / 3.0);
266 rmsA[r] = std::sqrt(sA / 3.0);
267 }
268 for (int r = 0; r + 1 < 3; ++r) {
269 const double oV = std::log2(rmsV[r] / rmsV[r + 1]), oA = std::log2(rmsA[r] / rmsA[r + 1]);
270 std::printf(" %s order %2.0f->%2.0f: volume %.3f area %.3f\n", label, ratios[r],
271 ratios[r + 1], oV, oA);
272 CHECK(oV >= 1.8);
273 CHECK(oA >= 1.8);
274 }
275}
276
277// G7's pipe: fluid inside r < R, z periodic with nz = 4, R/h in {16, 32, 64}.
278void pipeSet() {
279 std::mt19937_64 rng(77);
280 std::uniform_real_distribution<double> u(-0.5, 0.5);
281 for (double R : {16.0, 32.0, 64.0}) {
282 const int nxy = (int)std::lround(2.0 * R + 8.0);
283 const int n[3] = {nxy, nxy, 4};
284 const double hp[3] = {1.0, 1.0, 1.0};
285 for (int o = 0; o < 3; ++o) {
286 const double cx = 0.5 * nxy + u(rng), cy = 0.5 * nxy + u(rng);
287 IbmSolver s(n[0], n[1], n[2], {(double)n[0], (double)n[1], (double)n[2]}, {0.0, 0.0, 0.0},
288 {(long)n[0], (long)n[1], (long)n[2]});
289 std::vector<double> sdf((std::size_t)n[0] * n[1] * n[2]);
290 for (int z = 0; z < n[2]; ++z)
291 for (int y = 0; y < n[1]; ++y)
292 for (int x = 0; x < n[0]; ++x) {
293 const double dx = x + 0.5 - cx, dy = y + 0.5 - cy;
294 sdf[(std::size_t)x + (std::size_t)y * n[0] + (std::size_t)z * n[0] * n[1]] =
295 R - std::sqrt(dx * dx + dy * dy);
296 }
297 s.setSolid(sdf, false);
298 const Measure m = measure(s, n, hp);
299 const double eV = m.fluidVol / (M_PI * R * R * n[2]) - 1.0;
300 std::printf(
301 " pipe R/h=%2.0f off %d: eV %+.3e facets %ld R0 %ld min rho %.4f sealed %ld "
302 "(f) %.2e\n",
303 R, o, eV, m.facets, m.r0, m.minRho, m.sealed, m.maxSnapRes);
304 CHECK(m.r0 == m.facets && m.facets > 0);
305 CHECK(m.sealed == 0 && m.notSingle == 0);
306 CHECK(m.maxSnapRes <= 2e-3);
307 if (m.kappaMismatch != 0 || m.countMismatch != 0)
308 std::printf(" record vs host: %ld kappa mismatches (max diff %.2e), facet count %s\n",
309 m.kappaMismatch, m.maxKappaDiff, m.countMismatch ? "DIFFERS" : "equal");
310 CHECK(m.kappaMismatch == 0 && m.countMismatch == 0);
311 }
312 }
313}
314
315} // namespace
316
317int main(int argc, char** argv) {
318 Kokkos::ScopeGuard guard(argc, argv);
319 {
320 const int periodic[6] = {0, 0, 0, 0, 0, 0};
321 const int walls[6] = {1, 1, 4, 4, 0, 0}; // x walls (constant extension), y slip (mirror)
322 const int mixed[6] = {0, 0, 1, 4, 4, 1};
323 checkGhosts("periodic wrap", periodic);
324 checkGhosts("x wall + y slip", walls);
325 checkGhosts("y wall/slip + z slip/wall", mixed);
326
327 const double iso[3] = {1.0, 1.0, 1.0}, aniso[3] = {1.0, 1.0, 2.0};
328 sphereSet("iso ", iso, true);
329 sphereSet("aniso", aniso, false);
330 pipeSet();
331 }
332 if (failures) {
333 std::fprintf(stderr, "%d failure(s)\n", failures);
334 return 1;
335 }
336 std::printf("OK\n");
337 return 0;
338}
static constexpr int G
Definition flow_ibm.hpp:80
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
Definition jc.py:1
void cellSamples(const V &s, long i, long sy, long sz, double corner[8], double face[6], double &centre)
The 15 PL samples of the cell at extended linear index i (corner index bx + 2 by + 4 bz,...
void cellSnapped(const V &sax, const V &say, const V &saz, long i, long sy, long sz, double ap[6])
The snapped apertures of cell i in face order -x,+x,-y,+y,-z,+z.
int cellFacets(const cs::CutCellGeometry &g, const double ap[6], const double h[3], double area[2][3], double cen[2][3])
The operator facets of one cell (§2.3) from its PL record and snapped apertures: returns the count (0...
Solver< Staggered > IbmSolver
static constexpr int G
#define CHECK(cond)
static int expectIndex(int i, int n, int bcLo, int bcHi)