flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
mac_cutcell.hpp
Go to the documentation of this file.
1
9#ifndef PECLET_FLOW_MAC_CUTCELL_HPP
10#define PECLET_FLOW_MAC_CUTCELL_HPP
11
12#include <cstdlib>
13#include <Kokkos_Core.hpp>
14#include <Kokkos_MathematicalFunctions.hpp>
15#include <type_traits>
16#include <utility>
17
18#include "policy.hpp"
19
20namespace peclet::flow {
21
22using CCExec = Kokkos::DefaultExecutionSpace;
23using CCMem = CCExec::memory_space;
24using CCField = Kokkos::View<double*, CCMem>;
25using CCConst = Kokkos::View<const double*, CCMem>;
26
27// The EXACT level-0 operator apply — P1 of the defect-correction campaign
28// (docs/DEFECT_CORRECTION_PLAN.md). PER SOLVER (`set_pressure_exact_residual`): off at
29// construction, byte-identical when off, and turned ON by `enable_vof`.
30//
31// The rule: the residual and the Krylov matvec use the EXACT operator in double, in flux form;
32// everything below that line is a preconditioner and may stay float. With the gate on, the
33// level-0 matvec (matvecOverlap, the choke point for solvePCG and the flexible PCG) applies
34// applyCutcellOpExact -- matrix-free from the double face openness Level::ox/oy/oz that
35// buildCutcellOp assembles the float bands from -- so the Krylov fixed point becomes A_exact by
36// construction and the float hierarchy is demoted to a pure preconditioner, whose errors change
37// the convergence RATE and never the fixed point.
38//
39// Nothing inside vcycle() changes: residualCutcell, the smoother, restriction, the CA ring and
40// the AMG bottom all keep the float bands on purpose.
41//
42// `IbmSolver::enableVof` turns it ON (measured: on Hysing case 2 it removes 7.5 orders of the
43// projected flux divergence, 1.85e-03 -> 5.15e-11, and moves the iteration count, the step count,
44// the dt-limit census and both published functionals by nothing at all — the float `A*1 != 0`
45// defect is exactly what a moving interface's coefficient contrast amplifies).
46// `set_pressure_exact_residual(False)` after `enable_vof` is the ablation.
47
48// P2 shares this gate: the star overlay's additive delta (star_elimination.hpp) is part of the
49// same level-0 operator, so "the matvec is exact" has to mean the overlay too or the composed
50// operator is still the float one.
51
52struct C3 {
53 int x, y, z;
54};
55
56// Masked fluid fraction of a face from its SDF samples (centre + 6 axis neighbours). type 1/2/3 =
57// x/y/z face. (sd<=0 => closed.) Verbatim from cc_fraction_core.
58KOKKOS_INLINE_FUNCTION double ccFractionCore(double sd, double sxp, double sxm, double syp,
59 double sym, double szp, double szm, int type,
60 double dx, double dy, double dz) {
61 if (sd <= 0.0)
62 return 0.0;
63 const double gx = (sxp - sxm) / (2.0 * dx), gy = (syp - sym) / (2.0 * dy),
64 gz = (szp - szm) / (2.0 * dz);
65 double gmag = Kokkos::sqrt(gx * gx + gy * gy + gz * gz);
66 if (gmag < 1e-6)
67 gmag = 1e-6;
68 const double nx = gx / gmag, ny = gy / gmag, nz = gz / gmag;
69 double denom = (type == 1) ? (Kokkos::fabs(ny) * dy + Kokkos::fabs(nz) * dz)
70 : (type == 2) ? (Kokkos::fabs(nx) * dx + Kokkos::fabs(nz) * dz)
71 : (Kokkos::fabs(nx) * dx + Kokkos::fabs(ny) * dy);
72 if (denom < 1e-9)
73 denom = 1e-9;
74 double frac = 0.5 + sd / denom;
75 if (frac < 0.0)
76 frac = 0.0;
77 if (frac > 1.0)
78 frac = 1.0;
79 return frac;
80}
81
82KOKKOS_INLINE_FUNCTION double ccSampleExt(CCConst sdf, C3 ext, double x, double y, double z) {
83 const double fx = Kokkos::floor(x), fy = Kokkos::floor(y), fz = Kokkos::floor(z);
84 const double wx = x - fx, wy = y - fy, wz = z - fz;
85 int x0 = (int)fx, y0 = (int)fy, z0 = (int)fz;
86 auto cl = [](int v, int n) { return v < 0 ? 0 : (v >= n ? n - 1 : v); };
87 const int x1 = cl(x0 + 1, ext.x), y1 = cl(y0 + 1, ext.y), z1 = cl(z0 + 1, ext.z);
88 x0 = cl(x0, ext.x);
89 y0 = cl(y0, ext.y);
90 z0 = cl(z0, ext.z);
91 const long sy = ext.x, sz = static_cast<long>(ext.x) * ext.y;
92 auto F = [&](int xx, int yy, int zz) {
93 return sdf(static_cast<long>(xx) + static_cast<long>(yy) * sy + static_cast<long>(zz) * sz);
94 };
95 const double c00 = F(x0, y0, z0) * (1 - wx) + F(x1, y0, z0) * wx;
96 const double c10 = F(x0, y1, z0) * (1 - wx) + F(x1, y1, z0) * wx;
97 const double c01 = F(x0, y0, z1) * (1 - wx) + F(x1, y0, z1) * wx;
98 const double c11 = F(x0, y1, z1) * (1 - wx) + F(x1, y1, z1) * wx;
99 const double c0 = c00 * (1 - wy) + c10 * wy, c1 = c01 * (1 - wy) + c11 * wy;
100 return c0 * (1 - wz) + c1 * wz;
101}
102
103KOKKOS_INLINE_FUNCTION double ccFaceOpen(CCConst sdf, C3 ext, double fx, double fy, double fz,
104 int type, double dx, double dy, double dz) {
105 const double sd = ccSampleExt(sdf, ext, fx, fy, fz);
106 if (sd <= 0.0)
107 return 0.0;
108 const double e = 1.0;
109 return ccFractionCore(
110 sd, ccSampleExt(sdf, ext, fx + e, fy, fz), ccSampleExt(sdf, ext, fx - e, fy, fz),
111 ccSampleExt(sdf, ext, fx, fy + e, fz), ccSampleExt(sdf, ext, fx, fy - e, fz),
112 ccSampleExt(sdf, ext, fx, fy, fz + e), ccSampleExt(sdf, ext, fx, fy, fz - e), type, dx, dy,
113 dz);
114}
115
116// Fluid fraction (sd >= 0 = fluid) of a triangle with linear vertex values (a, b, c): the exact
117// linear-simplex level-set area fraction. Denominators (x-y)(x-z) are strictly positive whenever
118// the signs are mixed (x is the odd one out), guarded against exact-zero degeneracies.
119KOKKOS_INLINE_FUNCTION double ccTriFrac(double a, double b, double c) {
120 const bool pa = a >= 0.0, pb = b >= 0.0, pc = c >= 0.0;
121 const int np = (pa ? 1 : 0) + (pb ? 1 : 0) + (pc ? 1 : 0);
122 if (np == 3)
123 return 1.0;
124 if (np == 0)
125 return 0.0;
126 double x, y, z;
127 if (np == 1) { // rotate the positive vertex into x
128 if (pa) {
129 x = a;
130 y = b;
131 z = c;
132 } else if (pb) {
133 x = b;
134 y = c;
135 z = a;
136 } else {
137 x = c;
138 y = a;
139 z = b;
140 }
141 const double den = (x - y) * (x - z);
142 return den > 1e-300 ? (x * x) / den : 1.0;
143 }
144 // np == 2: rotate the negative vertex into x
145 if (!pa) {
146 x = a;
147 y = b;
148 z = c;
149 } else if (!pb) {
150 x = b;
151 y = c;
152 z = a;
153 } else {
154 x = c;
155 y = a;
156 z = b;
157 }
158 const double den = (x - y) * (x - z);
159 return 1.0 - (den > 1e-300 ? (x * x) / den : 1.0);
160}
161
162// MARCHING-SQUARES face openness (setApertureOrder(2), 2026-08-26): the O(h^2) upgrade of
163// ccFaceOpen motivated by the measured convexity bias of the one-sample linear model (the
164// tangent-plane estimate over-closes apertures on convex solids by +0.59%/+0.27% in bed
165// permeability at R=8/12, decaying ~h^2 -- flow doc/collocated_paper_plan.md row 51). Five
166// trilinear samples per face (4 corners + center), triangle-fan decomposition (4 triangles of
167// area 1/4 around the center sample -- no marching-squares saddle ambiguity), exact linear
168// fraction per triangle. Sub-resolution floor 1e-6 (measured lesson: alpha ~ 1e-12 rows from
169// exact geometry destroy the operator conditioning; the crude model's clip was an accidental
170// regularizer -- the floor makes the regularization explicit). Floor VALUE 1e-3 (2026-08-26):
171// 1e-6 measured insufficient -- alpha in [1e-6, 1e-2] rows drag plain RB-GS (levels=1) to a
172// ~1e-4 divergence floor within test budgets (redistribute_mpi_np4); a face open by <0.1% of
173// its area carries no resolved flux, so snapping it closed costs nothing measurable.
174KOKKOS_INLINE_FUNCTION double ccFaceOpenMS(CCConst sdf, C3 ext, double fx, double fy, double fz,
175 int type) {
176 const double e = 0.5;
177 double t1x = 0, t1y = 0, t1z = 0, t2x = 0, t2y = 0, t2z = 0; // tangent half-offsets
178 if (type == 1) {
179 t1y = e;
180 t2z = e;
181 } else if (type == 2) {
182 t1x = e;
183 t2z = e;
184 } else {
185 t1x = e;
186 t2y = e;
187 }
188 const double ccg = ccSampleExt(sdf, ext, fx, fy, fz);
189 if (ccg <= 0.0)
190 return 0.0; // CENTER GATE (DOF-support consistency, kept from order 1): a face whose
191 // staggered velocity point is solid has a MASKED u DOF -- an alpha > 0 aperture
192 // there is a constraint the projection cannot act on (measured: dropping this
193 // gate leaves an uncorrectable ~1.6e-4 divergence floor under plain RB-GS,
194 // redistribute_mpi_np4). The marching-squares fraction below only refines the
195 // AREA of center-fluid faces -- which is where the convexity bias lived.
196 const double c00 = ccSampleExt(sdf, ext, fx - t1x - t2x, fy - t1y - t2y, fz - t1z - t2z);
197 const double c10 = ccSampleExt(sdf, ext, fx + t1x - t2x, fy + t1y - t2y, fz + t1z - t2z);
198 const double c11 = ccSampleExt(sdf, ext, fx + t1x + t2x, fy + t1y + t2y, fz + t1z + t2z);
199 const double c01 = ccSampleExt(sdf, ext, fx - t1x + t2x, fy - t1y + t2y, fz - t1z + t2z);
200 const double cc = ccg;
201 const double frac = 0.25 * (ccTriFrac(c00, c10, cc) + ccTriFrac(c10, c11, cc) +
202 ccTriFrac(c11, c01, cc) + ccTriFrac(c01, c00, cc));
203 if (frac < 1e-3)
204 return 0.0;
205 if (frac > 1.0 - 1e-12)
206 return 1.0;
207 return frac;
208}
209
210// Fill staggered face openness over the whole extended block (ox[i] = -x face of cell i, etc.).
211// order 1 = the shipped one-sample linear model (byte-identical default); order 2 =
212// marching-squares (ccFaceOpenMS).
213inline void buildOpenness(CCField ox, CCField oy, CCField oz, CCConst sdf, C3 ext, double dx,
214 double dy, double dz, int order = 1) {
216 using MD = MDRange3<CCExec>;
217 if (order >= 2) {
218 Kokkos::parallel_for(
219 "peclet::flow::cc_open_ms", MD(space, {0, 0, 0}, {ext.x, ext.y, ext.z}),
220 KOKKOS_LAMBDA(int lx, int ly, int lz) {
221 const long i = static_cast<long>(lx) + static_cast<long>(ly) * ext.x +
222 static_cast<long>(lz) * static_cast<long>(ext.x) * ext.y;
223 ox(i) = ccFaceOpenMS(sdf, ext, lx - 0.5, ly, lz, 1);
224 oy(i) = ccFaceOpenMS(sdf, ext, lx, ly - 0.5, lz, 2);
225 oz(i) = ccFaceOpenMS(sdf, ext, lx, ly, lz - 0.5, 3);
226 });
227 return;
228 }
229 Kokkos::parallel_for(
230 "peclet::flow::cc_open", MD(space, {0, 0, 0}, {ext.x, ext.y, ext.z}),
231 KOKKOS_LAMBDA(int lx, int ly, int lz) {
232 const long i = static_cast<long>(lx) + static_cast<long>(ly) * ext.x +
233 static_cast<long>(lz) * static_cast<long>(ext.x) * ext.y;
234 ox(i) = ccFaceOpen(sdf, ext, lx - 0.5, ly, lz, 1, dx, dy, dz);
235 oy(i) = ccFaceOpen(sdf, ext, lx, ly - 0.5, lz, 2, dx, dy, dz);
236 oz(i) = ccFaceOpen(sdf, ext, lx, ly, lz - 0.5, 3, dx, dy, dz);
237 });
238}
239
240// Re-derive ONE staggered face-openness plane: component `a` at the HIGH domain-face index
241// (ext.a - g) along axis a. SCALING_ISSUES #3: that index is a GHOST index, so the distributed
242// openness halo exchange overwrites it with the periodic wrap of the opposite boundary -- which at
243// a non-periodic domain face is another boundary's aperture, not this one's. It is the face the
244// divergence weights the outgoing flux by AND (since the Dirichlet row carries the aperture) the
245// one the operator reads, so it has to be the geometry's own value. `buildOpenness` had it right
246// before the exchange; this re-derives it from the same (already exchanged, already
247// domain-extended) SDF rather than keeping a save buffer alive across a per-step set_solid.
248inline void buildOpennessHighFace(CCField oa, CCConst sdf, C3 ext, int g, int a, double dx,
249 double dy, double dz, int order = 1) {
251 const int dims[3] = {ext.x, ext.y, ext.z};
252 const long st[3] = {1, (long)ext.x, (long)ext.x * ext.y};
253 const int b = (a + 1) % 3, c = (a + 2) % 3;
254 const long sa = st[a], sb = st[b], sc = st[c];
255 const int bf = dims[a] - g;
256 const int type = a + 1;
257 using MD = MDRange2<CCExec>;
258 Kokkos::parallel_for(
259 "peclet::flow::cc_open_high_face", MD(space, {0, 0}, {dims[b], dims[c]}),
260 KOKKOS_LAMBDA(int p0, int p1) {
261 double q[3];
262 q[a] = (double)bf - 0.5;
263 q[b] = (double)p0;
264 q[c] = (double)p1;
265 const long i = (long)p0 * sb + (long)p1 * sc + (long)bf * sa;
266 oa(i) = (order >= 2) ? ccFaceOpenMS(sdf, ext, q[0], q[1], q[2], type)
267 : ccFaceOpen(sdf, ext, q[0], q[1], q[2], type, dx, dy, dz);
268 });
269}
270
271// HOST-only serial cutoff: below this many cells an OpenMP launch costs more than the work it does
272// (fork/join is ~20-30 us at 24 threads), so run the loop sequentially instead. That is
273// BIT-IDENTICAL for elementwise kernels and for colored sweeps (same-color cells are independent,
274// so the order within a sweep is irrelevant); reductions are deliberately NOT cut over — that would
275// change the FP summation order with the block size, and the multi-rank bit-exactness contract is
276// worth more than the microseconds. The device path never sees this (hostRunSerial is false there).
277//
278// MEASURED (5965WX, 24 threads, per-level V-cycle timer): the win lives almost entirely in the MG's
279// BOTTOM level, which fires ~24 trivial launches per V-cycle — 64^3/rank L4(4^3): 0.018 -> 0.007 s
280// per 50 V-cycles, ~8% of the whole V-cycle; 128^3/rank ~1.5%; 256^3/rank (the fat-rank target
281// size) ~0.2%, i.e. below run-to-run noise. A LARGER cutoff back-fires (131072 serializes levels
282// with real work: 64^3 projection 16.6 -> 20.6 ms/step), so keep it small.
283inline constexpr long kHostSerialCellCutoff = 8192L;
284// True when a host launch of `cells` cells should run sequentially instead (always false on
285// device).
286inline bool hostRunSerial(long cells) {
287 if constexpr (std::is_same_v<typename CCExec::memory_space, Kokkos::HostSpace>)
288 return cells > 0 && cells < kHostSerialCellCutoff;
289 else
290 return false;
291}
292
293// The host launch rule (doc/vof_step_performance_design.md §4.5, "rule H"): a 3-D elementwise
294// kernel is ONE cell body `f(x, y, z)`, launched
295// * on a device backend as MDRange3 with the default tiling (unchanged), and
296// * on a host backend as a RangePolicy over the (y, z) ROWS of the box, with the x loop inside
297// (a "pencil"), serially below kHostSerialCellCutoff cells.
298// The host MDRange it replaces (even with the {nx,2,2} row tiles this helper used to pass) cost
299// 2.4x a flat x loop in the MDRange tile machinery. The rows are statically partitioned over the
300// threads, identically for every kernel of the same box, so a thread keeps touching the same rows.
301// Elementwise kernels are order-independent, so every launch form is byte-identical.
302//
303// ccFor3 marks the x loop `omp simd`. CONTRACT (§4.5 item 4): an iteration writes only its own
304// cell(s) and reads no cell that another iteration of the same row writes (red-black colour passes
305// qualify: same-colour cells are independent). A body that breaks it -- a ghost copy whose source
306// and destination rows overlap -- uses ccForRows3, the same launch without the simd mark. With
307// -ffp-contract=off (the host flags, A1) vectorizing a non-reduction loop is bit-identical to the
308// scalar code.
309#if defined(_OPENMP)
310#define PECLET_FLOW_OMP_SIMD _Pragma("omp simd")
311#else
312#define PECLET_FLOW_OMP_SIMD
313#endif
314namespace ccdetail {
315template <bool Simd, class F>
316inline void ccRows3(const char* name, C3 lo, C3 hi, F f) {
318 using MD = MDRange3<CCExec>;
319 if constexpr (std::is_same_v<typename CCExec::memory_space, Kokkos::HostSpace>) {
320 const int ny = hi.y - lo.y, nz = hi.z - lo.z;
321 if (ny <= 0 || nz <= 0 || hi.x <= lo.x)
322 return;
323 const long rows = (long)ny * nz;
324 auto row = [=](long r) {
325 const int y = lo.y + (int)(r % ny), z = lo.z + (int)(r / ny);
326 if constexpr (Simd) {
328 for (int x = lo.x; x < hi.x; ++x)
329 f(x, y, z);
330 } else {
331 for (int x = lo.x; x < hi.x; ++x)
332 f(x, y, z);
333 }
334 };
335 if (hostRunSerial(rows * (hi.x - lo.x))) {
336 for (long r = 0; r < rows; ++r) // too small to be worth a fork/join (bit-identical)
337 row(r);
338 return;
339 }
340 Kokkos::parallel_for(name, Kokkos::RangePolicy<CCExec>(space, 0, rows), row);
341 } else {
342 Kokkos::parallel_for(name, MD(space, {lo.x, lo.y, lo.z}, {hi.x, hi.y, hi.z}), f);
343 }
344}
345} // namespace ccdetail
346
347template <class F>
348inline void ccFor3(const char* name, C3 lo, C3 hi, F f) {
349 ccdetail::ccRows3<true>(name, lo, hi, f);
350}
351// ccFor3 without the `omp simd` mark on the host x loop (see the contract above).
352template <class F>
353inline void ccForRows3(const char* name, C3 lo, C3 hi, F f) {
354 ccdetail::ccRows3<false>(name, lo, hi, f);
355}
356
357// Reduction sibling of ccFor3. On a host backend it is the PENCIL reduction of B2
358// (doc/vof_step_performance_design.md §5.8, a recorded order): a RangePolicy over the (y, z) rows
359// of the box -- the same static row partition as ccFor3 -- each thread reducing its rows in (y, z)
360// order with x ascending inside a row, scalar (no `omp simd` reduction), and the per-thread
361// partials combined by Kokkos in thread order. Sum results therefore depend on the thread count
362// only, as before; max/min reductions are order-free and unchanged. Unlike ccFor3 a reduction is
363// never cut over to a serial loop below kHostSerialCellCutoff (that would make the summation
364// order a function of the block size). The device branch is the MDRange reduction, unchanged.
365namespace ccdetail {
366template <class R, bool = Kokkos::is_reducer_v<std::decay_t<R>>>
368 using type = typename std::decay_t<R>::value_type;
369};
370template <class R>
371struct ReduceValue<R, false> { // a scalar result, or a View holding it
372 using D = std::decay_t<R>;
373 template <class T, bool = Kokkos::is_view_v<T>>
374 struct Pick {
375 using type = T;
376 };
377 template <class T>
378 struct Pick<T, true> {
379 using type = typename T::non_const_value_type;
380 };
381 using type = typename Pick<D>::type;
382};
383} // namespace ccdetail
384template <class F, class... R>
385inline void ccReduce3(const char* name, C3 lo, C3 hi, F f, R&&... reducers) {
387 using MD = MDRange3<CCExec>;
388 if constexpr (std::is_same_v<typename CCExec::memory_space, Kokkos::HostSpace>) {
389 const int ny = hi.y - lo.y > 0 ? hi.y - lo.y : 0, nz = hi.z - lo.z > 0 ? hi.z - lo.z : 0;
390 const long rows = hi.x > lo.x ? (long)ny * nz : 0;
391 Kokkos::parallel_reduce(
392 name, Kokkos::RangePolicy<CCExec>(space, 0, rows),
393 [=](long r, typename ccdetail::ReduceValue<R>::type&... acc) {
394 const int y = lo.y + (int)(r % ny), z = lo.z + (int)(r / ny);
395 for (int x = lo.x; x < hi.x; ++x)
396 f(x, y, z, acc...);
397 },
398 std::forward<R>(reducers)...);
399 } else {
400 Kokkos::parallel_reduce(name, MD(space, {lo.x, lo.y, lo.z}, {hi.x, hi.y, hi.z}), f,
401 std::forward<R>(reducers)...);
402 }
403}
404
405} // namespace peclet::flow
406
407#endif // PECLET_FLOW_MAC_CUTCELL_HPP
#define PECLET_FLOW_OMP_SIMD
void ccRows3(const char *name, C3 lo, C3 hi, F f)
void ccForRows3(const char *name, C3 lo, C3 hi, F f)
CCExec::memory_space CCMem
double ccFaceOpen(CCConst sdf, C3 ext, double fx, double fy, double fz, int type, double dx, double dy, double dz)
void ccReduce3(const char *name, C3 lo, C3 hi, F f, R &&... reducers)
constexpr long kHostSerialCellCutoff
double ccFaceOpenMS(CCConst sdf, C3 ext, double fx, double fy, double fz, int type)
double ccFractionCore(double sd, double sxp, double sxm, double syp, double sym, double szp, double szm, int type, double dx, double dy, double dz)
double ccTriFrac(double a, double b, double c)
void buildOpennessHighFace(CCField oa, CCConst sdf, C3 ext, int g, int a, double dx, double dy, double dz, int order=1)
void ccFor3(const char *name, C3 lo, C3 hi, F f)
Kokkos::View< double *, CCMem > CCField
Kokkos::DefaultExecutionSpace CCExec
double ccSampleExt(CCConst sdf, C3 ext, double x, double y, double z)
void buildOpenness(CCField ox, CCField oy, CCField oz, CCConst sdf, C3 ext, double dx, double dy, double dz, int order=1)
bool hostRunSerial(long cells)
Kokkos::View< const double *, CCMem > CCConst
flow — the multidimensional iteration policies: iteration order follows storage.
typename std::decay_t< R >::value_type type
static constexpr double F