flow
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
mac_approx_projection.hpp
Go to the documentation of this file.
1
12#ifndef PECLET_FLOW_MAC_APPROX_PROJECTION_HPP
13#define PECLET_FLOW_MAC_APPROX_PROJECTION_HPP
14
15#include <Kokkos_Core.hpp>
16
17#include "mac_cutcell.hpp" // peclet::flow::C3, CCField, CCConst, CCExec, ccSampleExt
18
19namespace peclet::flow {
20
21// const view of the (float) IBM stencil coefficients (same alias mac_ibm.hpp defines; this header
22// is included first, so declare it here for the mode-4 stencilMatvec).
23using MConst = Kokkos::View<const float*, CCMem>;
24
25// Average cell-centered velocities onto the staggered face layout: uf(i) is the velocity at the low
26// (-x) face of cell i (located at i-1/2) = ½(U(i-1)+U(i)); likewise vf/wf along y/z. This
27// reproduces the exact layout the staggered solver stores directly, so divergOpen / projectCorrect
28// (mac_pressure.hpp) act on uf/vf/wf unchanged. Computed over the block where the axis neighbour
29// exists ([1,e) per axis); the cell velocity ghosts must be filled first.
30inline void centerToFace(CCField uf, CCField vf, CCField wf, CCConst U, CCConst V, CCConst W, C3 e,
31 int g) {
32 (void)g;
33 CCExec space;
34 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
35 Kokkos::parallel_for(
36 "peclet::flow::center_to_face", MD(space, {1, 1, 1}, {e.x, e.y, e.z}),
37 KOKKOS_LAMBDA(int x, int y, int z) {
38 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
39 const long i = (long)x + (long)y * sy + (long)z * sz;
40 uf(i) = 0.5 * (U(i) + U(i - sx));
41 vf(i) = 0.5 * (V(i) + V(i - sy));
42 wf(i) = 0.5 * (W(i) + W(i - sz));
43 });
44}
45
46// Wall-aware variant of centerToFace (opt-in: Solver::setFaceInterp(1); collocated only). At a face
47// whose adjacent cell centers straddle the immersed boundary (one SDF negative), the plain average
48// ½(U_fluid + U_solid=0) implicitly places the wall AT THE SOLID CELL CENTER (theta = 1) regardless
49// of the true axis intercept theta = sdf_c/(sdf_c - sdf_n) — an O(1) relative flux error at every
50// cut face that makes the collocated drag first-order (doc/collocated_first_order_analysis.md).
51// Here the face value is reconstructed from the fluid side by a WALL-ANCHORED quadratic in
52// (theta - s) — exactly zero at the wall — least-squares-fitted to the fluid-side samples
53// {U_c, U_far, U_far2} with the near-wall cell SHED smoothly as theta -> 0 (weight
54// theta²/(theta²+0.15²)): a sliver cell's value is slaved to the wall by the momentum IBM and
55// carries no independent information, so downweighting it bounds the noise amplification without
56// dropping the order — the theta->0 limit is the (still 2nd-order) quadratic through
57// {wall, far, far2}. A small Tikhonov term on the curvature keeps the 2x2 solve well-posed when
58// far2 is unavailable; a fluid cell sandwiched along the axis falls back to the wall-anchored
59// linear through U_c (bounded: xe/theta <= 1/2). Faces whose center lies behind the wall
60// (theta <= 1/2) get 0, continuously (the parabola vanishes at the wall); their open area is a
61// corner sliver. Interior fluid faces reproduce the plain average bit-exactly. Validated a priori
62// against analytic Stokes flow past a sphere: face-value error O(h²) (the plain average is O(h)).
63// Per-face stencil of the wall-aware map: up to 3 (cell index, weight) pairs such that
64// uf(face) = sum_k w[k]*U(idx[k]); returns the entry count (0 = closed face or corner sliver:
65// uf = 0). Shared by the forward map (centerToFaceWallAware) and its TRANSPOSE
66// (transposeGradWallAware) so the two stay exact adjoints — the accuracy of the constrained steady
67// state hinges on that pairing, not on the forward truncation alone.
68KOKKOS_INLINE_FUNCTION int wallAwareFaceStencil(CCConst sdf, long i, long sa, int cFace, int eAxis,
69 double xcen, long idx[3], double w[3]) {
70 const double sm = sdf(i - sa), sp = sdf(i); // low cell (coord cFace-1) / high cell (cFace)
71 const bool fm = sm >= 0.0, fp = sp >= 0.0;
72 if (fm && fp) { // both fluid: the plain 1/2-1/2 average, unchanged
73 idx[0] = i;
74 w[0] = 0.5;
75 idx[1] = i - sa;
76 w[1] = 0.5;
77 return 2;
78 }
79 if (!fm && !fp)
80 return 0; // fully solid face (openness 0; value irrelevant)
81 const long ic = fp ? i : i - sa; // the fluid cell
82 const double sc = fp ? sp : sm, ss = fp ? sm : sp;
83 double th = sc / (sc - ss); // axis intercept from the fluid center (ibmFillEntry convention)
84 th = th < 1e-4 ? 1e-4 : (th > 1.0 ? 1.0 : th);
85 // Evaluation abscissa in wall coordinates (theta - s): the face-CENTER distance theta - 1/2 by
86 // default, or (xcen >= 0, mode 3) the OPEN-FACE-CENTROID wall distance from the static geometry
87 // build. The projection's flux model is o_f * uf, so the correct uf is the open-area MEAN of the
88 // velocity — the midpoint-at-centroid quadrature — not the face-center point value: the open area
89 // lies on the fluid side, farther from the wall, so the (accurate) face-center value UNDERCOUNTS
90 // the flux at O(h). Measured a priori (Stokes-past-sphere): centroid evaluation drops the
91 // systematic flux bias by ~2 orders of magnitude at N=128.
92 const double xe = (xcen >= 0.0) ? xcen : th - 0.5;
93 if (xe <= 0.0)
94 return 0; // face (or its open centroid) behind the wall
95 const int cc = fp ? cFace : cFace - 1; // fluid cell's axis coordinate
96 const int up = fp ? 1 : -1; // coordinate step AWAY from the solid
97 const long dir = fp ? -sa : sa; // index step TOWARD the solid
98 const bool ok1 = (cc + up >= 0) && (cc + up < eAxis) && sdf(ic - dir) >= 0.0;
99 if (!ok1) { // sandwiched along this axis: wall-anchored linear through U_c
100 idx[0] = ic;
101 w[0] = Kokkos::fmin(xe / th, 2.0); // capped (xe can exceed theta at centroid evaluation)
102 return 1;
103 }
104 const bool ok2 = (cc + 2 * up >= 0) && (cc + 2 * up < eAxis) && sdf(ic - 2 * dir) >= 0.0;
105 const double wc = th * th / (th * th + 0.0225); // shed the sliver cell (theta* = 0.15)
106 const double w2 = ok2 ? 0.25 : 0.0;
107 const double xc = th, xf = th + 1.0, xg = th + 2.0;
108 const double S11 = wc * xc * xc + xf * xf + w2 * xg * xg;
109 const double S12 = wc * xc * xc * xc + xf * xf * xf + w2 * xg * xg * xg;
110 const double S22 = wc * xc * xc * xc * xc + xf * xf * xf * xf + w2 * xg * xg * xg * xg + 0.01;
111 const double det = S11 * S22 - S12 * S12;
112 // coefficient of datum k (abscissa x_k, weight w_k) in T = a*xe + b*xe^2:
113 // c_k = w_k * [ x_k*(S22*xe - S12*xe^2) + x_k^2*(S11*xe^2 - S12*xe) ] / det
114 const double A = (S22 * xe - S12 * xe * xe) / det, B = (S11 * xe * xe - S12 * xe) / det;
115 idx[0] = ic;
116 w[0] = wc * (xc * A + xc * xc * B);
117 idx[1] = ic - dir;
118 w[1] = xf * A + xf * xf * B;
119 if (!ok2)
120 return 2;
121 idx[2] = ic - 2 * dir;
122 w[2] = w2 * (xg * A + xg * xg * B);
123 return 3;
124}
125
126KOKKOS_INLINE_FUNCTION double wallAwareFaceValue(CCConst U, CCConst sdf, long i, long sa, int cFace,
127 int eAxis, double xcen) {
128 long idx[3];
129 double w[3];
130 const int n = wallAwareFaceStencil(sdf, i, sa, cFace, eAxis, xcen, idx, w);
131 double v = 0.0;
132 for (int k = 0; k < n; ++k)
133 v += w[k] * U(idx[k]);
134 return v;
135}
136
137// Static per-face geometry for the mode-3 centroid quadrature: for each face whose adjacent cell
138// centers straddle the boundary, subsample the face plane (4x4 trilinear SDF), locate the OPEN-area
139// centroid, and store its axis wall-distance (in cells, measured toward the solid side; two-point
140// line intercept from the trilinear SDF, clamped to [0,2]). 0 => no open samples (no flux). Faces
141// with both centers on one side store -1 (sentinel: stencil falls back to theta-1/2, which the
142// both-fluid branch never reads anyway). One-time cost at setSolid; the geometry is static.
143inline void buildFaceCentroidDist(CCField xcx, CCField xcy, CCField xcz, CCConst sdf, C3 e) {
144 CCExec space;
145 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
146 Kokkos::parallel_for(
147 "peclet::flow::face_centroid_dist", MD(space, {1, 1, 1}, {e.x, e.y, e.z}),
148 KOKKOS_LAMBDA(int x, int y, int z) {
149 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
150 const long i = (long)x + (long)y * sy + (long)z * sz;
151 // face center in the sampling frame where CELL CENTERS sit at integer coordinates
152 // (buildOpenness convention): -x face of cell (x,y,z) is at (x-1/2, y, z), etc.
153 const double fc[3][3] = {{x - 0.5, (double)y, (double)z},
154 {(double)x, y - 0.5, (double)z},
155 {(double)x, (double)y, z - 0.5}};
156 const long st[3] = {sx, sy, sz};
157 CCField out[3] = {xcx, xcy, xcz};
158 for (int a = 0; a < 3; ++a) {
159 const double sm = sdf(i - st[a]), sp = sdf(i);
160 if ((sm >= 0.0) == (sp >= 0.0)) {
161 out[a](i) = -1.0; // not a solid-bordering face: stencil never reads it
162 continue;
163 }
164 const double dir = (sm < 0.0) ? -1.0 : 1.0; // axis step TOWARD the solid side
165 const int t1 = (a + 1) % 3, t2 = (a + 2) % 3;
166 double c1 = 0.0, c2 = 0.0;
167 int nOpen = 0;
168 for (int j1 = 0; j1 < 4; ++j1)
169 for (int j2 = 0; j2 < 4; ++j2) {
170 const double o1 = -0.375 + 0.25 * j1, o2 = -0.375 + 0.25 * j2;
171 double p[3] = {fc[a][0], fc[a][1], fc[a][2]};
172 p[t1] += o1;
173 p[t2] += o2;
174 if (ccSampleExt(sdf, e, p[0], p[1], p[2]) > 0.0) {
175 c1 += o1;
176 c2 += o2;
177 ++nOpen;
178 }
179 }
180 if (nOpen == 0) {
181 out[a](i) = 0.0; // fully covered face: zero flux
182 continue;
183 }
184 double p0[3] = {fc[a][0], fc[a][1], fc[a][2]};
185 p0[t1] += c1 / nOpen;
186 p0[t2] += c2 / nOpen; // open-area centroid on the face
187 double p1[3] = {p0[0], p0[1], p0[2]};
188 p1[a] += dir; // one cell toward the solid
189 const double s0 = ccSampleExt(sdf, e, p0[0], p0[1], p0[2]);
190 const double s1 = ccSampleExt(sdf, e, p1[0], p1[1], p1[2]);
191 double d = (s0 > 0.0 && s0 - s1 > 1e-12) ? s0 / (s0 - s1) : 0.0;
192 out[a](i) = d < 0.0 ? 0.0 : (d > 2.0 ? 2.0 : d); // cap at 2 cells: never leaves the
193 // fitted data range [theta, theta+2], so the stencil weights stay interpolation-bounded
194 }
195 });
196}
197
198// centerToFace with the wall-aware reconstruction at solid-bordering faces (see above). The SDF is
199// the cell-centered field (ghosts filled); U/V/W ghosts must be filled first, as for centerToFace.
200// useCen (mode 3): evaluate at the stored open-centroid wall distance xcx/xcy/xcz instead of the
201// face center (the flux-consistent quadrature).
203 CCConst W, CCConst sdf, CCConst xcx, CCConst xcy, CCConst xcz,
204 bool useCen, C3 e, int g) {
205 (void)g;
206 CCExec space;
207 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
208 Kokkos::parallel_for(
209 "peclet::flow::center_to_face_wall", MD(space, {1, 1, 1}, {e.x, e.y, e.z}),
210 KOKKOS_LAMBDA(int x, int y, int z) {
211 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
212 const long i = (long)x + (long)y * sy + (long)z * sz;
213 uf(i) = wallAwareFaceValue(U, sdf, i, sx, x, e.x, useCen ? xcx(i) : -1.0);
214 vf(i) = wallAwareFaceValue(V, sdf, i, sy, y, e.y, useCen ? xcy(i) : -1.0);
215 wf(i) = wallAwareFaceValue(W, sdf, i, sz, z, e.z, useCen ? xcz(i) : -1.0);
216 });
217}
218
219// The O-WEIGHTED EXACT ADJOINT of the wall-aware constraint, applied to a cell field p:
220// out(i) = sum over the (up to 6) axis faces f whose stencil references cell i of
221// c_i(f)·o(f)·(p(f) - p(f-sa)) — i.e. Cᵀp for the constraint C = D·diag(o)·T. For interior fluid
222// cells (o = 1, ½/½ weights) this is exactly the central difference ½(p(i+sa) - p(i-sa)).
223//
224// WHY the o-weighting is essential (learned the hard way): the wall-anchored T deliberately has
225// row sums != 1 at cut faces (the no-slip anchor pulls them down), so the UNWEIGHTED transpose
226// force Tᵀ·G·P no longer telescopes to zero over a periodic box — the fluid feels a net spurious
227// pressure force proportional to the near-wall gradients, which feeds back through the flow and
228// DIVERGES (the mode-3 runaway; capping the abscissa does not help because the weights were never
229// the problem). With the exact adjoint the pressure does NO work on the constraint manifold
230// ((u, Cᵀp) = (Cu, p) = 0), cutting the feedback unconditionally, and o·T·Tᵀ stays
231// interpolation-bounded so the per-step projection remains contractive. Used (setFaceInterp(2/3))
232// for BOTH the incremental predictor's -grad(P) and the projection's cell correction: at steady
233// state the predictor gradient IS the pressure force, so it must be the adjoint of the constraint —
234// upgrading T alone (mode 1) measurably WORSENS the drag.
236 bool useCen, int axis, C3 e, int g) {
237 CCExec space;
238 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
239 Kokkos::parallel_for(
240 "peclet::flow::transpose_grad_wall", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
241 KOKKOS_LAMBDA(int x, int y, int z) {
242 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
243 const long i = (long)x + (long)y * sy + (long)z * sz;
244 const long sa = (axis == 0) ? sx : (axis == 1) ? sy : sz;
245 const int eA = (axis == 0) ? e.x : (axis == 1) ? e.y : e.z;
246 const int ca = (axis == 0) ? x : (axis == 1) ? y : z;
247 double acc = 0.0;
248 long idx[3];
249 double w[3];
250 for (int m = -2; m <= 3; ++m) { // all faces whose stencil can reference this cell
251 const int cf = ca + m;
252 if (cf < 1 || cf >= eA)
253 continue;
254 const long f = i + (long)m * sa;
255 const int n = wallAwareFaceStencil(sdf, f, sa, cf, eA, useCen ? xc(f) : -1.0, idx, w);
256 for (int k = 0; k < n; ++k)
257 if (idx[k] == i)
258 acc += w[k] * o(f) * (p(f) - p(f - sa));
259 }
260 out(i) = acc;
261 });
262}
263
264// Cell fluid volume fraction cs (mode 4): 1 in clear fluid, 0 in solid, subsampled (4x4x4 trilinear
265// SDF) at cut cells. Static, one-time at setSolid. Used to weight the FV momentum time term and the
266// body/pressure forcing so a cut cell is driven over its FLUID volume only (omitting cs is an O(h)
267// surface error — part of what keeps modes 0-3 first-order).
268inline void buildCellFraction(CCField cs, CCConst sdf, C3 e, int g) {
269 CCExec space;
270 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
271 Kokkos::parallel_for(
272 "peclet::flow::cell_fraction", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
273 KOKKOS_LAMBDA(int x, int y, int z) {
274 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
275 const double sc = sdf(i);
276 // fully clear one cell away on all axes -> not a cut cell; cheap exact classification
277 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
278 const bool f0 = sc >= 0.0;
279 const bool cut = (sdf(i + sx) >= 0.0) != f0 || (sdf(i - sx) >= 0.0) != f0 ||
280 (sdf(i + sy) >= 0.0) != f0 || (sdf(i - sy) >= 0.0) != f0 ||
281 (sdf(i + sz) >= 0.0) != f0 || (sdf(i - sz) >= 0.0) != f0;
282 if (!cut) {
283 cs(i) = sc >= 0.0 ? 1.0 : 0.0;
284 return;
285 }
286 int nf = 0;
287 for (int j0 = 0; j0 < 4; ++j0)
288 for (int j1 = 0; j1 < 4; ++j1)
289 for (int j2 = 0; j2 < 4; ++j2) {
290 const double o0 = -0.375 + 0.25 * j0, o1 = -0.375 + 0.25 * j1,
291 o2 = -0.375 + 0.25 * j2;
292 if (ccSampleExt(sdf, e, x + o0, y + o1, z + o2) > 0.0)
293 ++nf;
294 }
295 cs(i) = nf / 64.0;
296 });
297}
298
299// FINITE-VOLUME MOMENTUM OPERATOR (mode 4): applies L_FV(U) over the fluid control volume of each
300// cell (the centroid wall-gradient is validated a priori in tests/study/fv_wallflux_apriori.py):
301// L_FV(U)_i = idt·cs_i·U_i + mu·[ Σ_f o_f·(U_i − U_nbr) + Σ_a W_a·g_a^centroid(U) ]
302// The o_f-weighted two-point face fluxes + the fragment-centroid wall drag = the same finite-volume
303// boundary geometry the projection uses. In the interior (cs=1, o_f=1, W=0) this is exactly the
304// backward-Euler diffusion operator idt·U − mu·Lap(U) — identical to the IBM matrix M there, so the
305// defect correction (M·u − L_FV·u) vanishes and interior cells stay byte-identical to mode 0.
306inline void fvViscousApply(CCField Lu, CCConst U, CCConst sdf, CCConst cs, CCConst ox, CCConst oy,
307 CCConst oz, double mu, double idt, C3 e, int g) {
308 CCExec space;
309 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
310 Kokkos::parallel_for(
311 "peclet::flow::fv_viscous_apply", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
312 KOKKOS_LAMBDA(int x, int y, int z) {
313 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
314 const long st[3] = {sx, sy, sz};
315 const long i = (long)x + (long)y * sy + (long)z * sz;
316 CCConst oa[3] = {ox, oy, oz};
317 double W[3], of[3]; // fragment normal per axis; low/high face openness
318 double diag = 0.0, offs = 0.0, aw = 0.0;
319 for (int a = 0; a < 3; ++a) {
320 const double om = oa[a](i), op = oa[a](i + st[a]);
321 W[a] = om - op;
322 of[a] = om + op; // low + high face openness
323 diag += om + op;
324 offs += om * U(i - st[a]) + op * U(i + st[a]);
325 aw += (W[a] < 0.0 ? -W[a] : W[a]);
326 }
327 double wall = 0.0;
328 if (aw > 1e-12) { // centroid-anchored wall drag mu·Σ_a W_a g_a (a-priori-validated:
329 // fv_wallflux_apriori.py)
330 double nx = 0.5 * (sdf(i + sx) - sdf(i - sx));
331 double ny = 0.5 * (sdf(i + sy) - sdf(i - sy));
332 double nz = 0.5 * (sdf(i + sz) - sdf(i - sz));
333 double nn = Kokkos::sqrt(nx * nx + ny * ny + nz * nz);
334 if (nn > 1e-12) {
335 nx /= nn;
336 ny /= nn;
337 nz /= nn;
338 const double nv[3] = {nx, ny, nz};
339 const double sdi = sdf(i);
340 // foot point p* = x − sdi·n̂, clamped into the sampleable block [1, e−2] so the two
341 // off-cell trilinear taps (p*+2σ) never index outside the padded field (guards a NaN/
342 // large sdi too — ccSampleExt only clamps integer indices, not a NaN coordinate).
343 auto cl = [](double v, double hi) { return v < 1.0 ? 1.0 : (v > hi ? hi : v); };
344 const double px = cl(x - sdi * nx, e.x - 2.0);
345 const double py = cl(y - sdi * ny, e.y - 2.0);
346 const double pz = cl(z - sdi * nz, e.z - 2.0);
347 for (int a = 0; a < 3; ++a) {
348 const double sg = nv[a] >= 0.0 ? 1.0 : -1.0;
349 const double u1 = ccSampleExt(U, e, px + (a == 0 ? sg : 0.0),
350 py + (a == 1 ? sg : 0.0), pz + (a == 2 ? sg : 0.0));
351 const double u2 =
352 ccSampleExt(U, e, px + (a == 0 ? 2.0 * sg : 0.0), py + (a == 1 ? 2.0 * sg : 0.0),
353 pz + (a == 2 ? 2.0 * sg : 0.0));
354 wall += W[a] * sg * (2.0 * u1 - 0.5 * u2);
355 }
356 }
357 }
358 // FV viscous operator = idt·cs·U + μ·[Σo_f(U_i−U_nbr) − Σ_a W_a g_a]. The wall term sign
359 // is −μ Σ W_a g_a: from ∫_CV −μ∇²u = −μ[flux_out − flux_in], the wall flux g_a enters with
360 // a minus, so a resistive wall (∂u/∂n<0) ADDS to the operator (dissipative), matching the
361 // interior −μLap. (+ would be anti-dissipative and blows the solve up.)
362 Lu(i) = idt * cs(i) * U(i) + mu * (diag * U(i) - offs - wall);
363 (void)of;
364 });
365}
366
367// Lagrange quadratic through (-1,a1),(0,a2),(+1,a3) evaluated at xx (Basilisk embed.h `quadratic`).
368KOKKOS_INLINE_FUNCTION double eQuad(double xx, double a1, double a2, double a3) {
369 return (a1 * (xx - 1.0) + a3 * (xx + 1.0)) * xx * 0.5 - a2 * (xx - 1.0) * (xx + 1.0);
370}
371
372// TRUE-NORMAL wall gradient d(U)/dn at the embedded boundary of a cut cell — the Basilisk embed.h
373// `dirichlet_gradient` (no-slip U_wall = 0), in cell units (h = 1). n̂ = unit inward-to-FLUID normal
374// (∇sdf), p = boundary-point offset from the cell centre (cell units). Along the dominant-|n̂| axis
375// it places two image points 1 and 2 cells into the fluid, interpolates U there by TRANSVERSE
376// bi-quadratic interpolation of the cell-centred values (`eQuad`×`eQuad`), and fits the quadratic
377// {0 at wall, v0 at d0, v1 at d1} for a 2nd-order derivative. Fallbacks (Basilisk): if the near
378// image point's 3×3 transverse stencil straddles the wall, a biased-linear `embed_interpolate`
379// anchored at the fluid home cell (uses only fluid cells); if even the home cell is solid, the
380// degenerate 1-point estimate through the cell centre. A-priori-validated O(h²) with 0% degenerate
381// on the Stokes sphere (tests/study/fv_wallflux_apriori.py, variant C). Interior/transverse reads
382// of U and sdf must be halo-filled (the velocity block carries G=2 ghosts).
383KOKKOS_INLINE_FUNCTION double embedDirichletGradient(CCConst U, CCConst sdf, C3 e, int x, int y,
384 int z, double nx, double ny, double nz,
385 double px, double py, double pz) {
386 const double nv[3] = {nx, ny, nz}, pv[3] = {px, py, pz};
387 const int ci[3] = {x, y, z}, ext[3] = {e.x, e.y, e.z};
388 const long st[3] = {1, (long)e.x, (long)e.x * e.y};
389 int da = 0;
390 double best = Kokkos::fabs(nv[0]);
391 if (Kokkos::fabs(nv[1]) > best) {
392 best = Kokkos::fabs(nv[1]);
393 da = 1;
394 }
395 if (Kokkos::fabs(nv[2]) > best)
396 da = 2;
397 const int t1 = (da + 1) % 3, t2 = (da + 2) % 3;
398 const double sgn = nv[da] >= 0.0 ? 1.0 : -1.0;
399 auto clampi = [](int v, int n) { return v < 0 ? 0 : (v >= n ? n - 1 : v); };
400 auto rd = [](double v) { // round to nearest integer, clamped to {-1,0,1} (Basilisk j/k)
401 double r = v >= 0.0 ? Kokkos::floor(v + 0.5) : Kokkos::ceil(v - 0.5);
402 return r > 1.0 ? 1.0 : (r < -1.0 ? -1.0 : r);
403 };
404 double vL[2], dL[2];
405 bool defd[2];
406 for (int l = 0; l < 2; ++l) {
407 const int io = (l + 1) * (int)sgn; // ±1, ±2 cells into the fluid along the dominant axis
408 const double dl = ((double)io - pv[da]) / nv[da]; // distance (cells) from wall to image plane
409 const double y1 = pv[t1] + dl * nv[t1], z1 = pv[t2] + dl * nv[t2];
410 const int jj = (int)rd(y1), kk = (int)rd(z1);
411 const double ly = y1 - jj, lz = z1 - kk;
412 double vv[3][3];
413 bool sf[3][3];
414 for (int dk = -1; dk <= 1; ++dk)
415 for (int dj = -1; dj <= 1; ++dj) {
416 int cc[3];
417 cc[da] = ci[da] + io;
418 cc[t1] = ci[t1] + jj + dj;
419 cc[t2] = ci[t2] + kk + dk;
420 const long idx = (long)clampi(cc[0], ext[0]) * st[0] + (long)clampi(cc[1], ext[1]) * st[1] +
421 (long)clampi(cc[2], ext[2]) * st[2];
422 vv[dj + 1][dk + 1] = U(idx);
423 sf[dj + 1][dk + 1] = sdf(idx) >= 0.0;
424 }
425 bool full = true;
426 for (int a2 = 0; a2 < 3; ++a2)
427 for (int b2 = 0; b2 < 3; ++b2)
428 full = full && sf[a2][b2];
429 const bool home = sf[1][1];
430 const double vbq =
431 eQuad(lz, eQuad(ly, vv[0][0], vv[1][0], vv[2][0]), eQuad(ly, vv[0][1], vv[1][1], vv[2][1]),
432 eQuad(ly, vv[0][2], vv[1][2], vv[2][2]));
433 double vbl = vv[1][1]; // biased-linear embed_interpolate (fluid-side only), anchored at home
434 {
435 const int fp = ly >= 0.0 ? 2 : 0, fm = ly >= 0.0 ? 0 : 2;
436 if (sf[fp][1])
437 vbl += Kokkos::fabs(ly) * (vv[fp][1] - vv[1][1]);
438 else if (sf[fm][1])
439 vbl += Kokkos::fabs(ly) * (vv[1][1] - vv[fm][1]);
440 }
441 {
442 const int fp = lz >= 0.0 ? 2 : 0, fm = lz >= 0.0 ? 0 : 2;
443 if (sf[1][fp])
444 vbl += Kokkos::fabs(lz) * (vv[1][fp] - vv[1][1]);
445 else if (sf[1][fm])
446 vbl += Kokkos::fabs(lz) * (vv[1][1] - vv[1][fm]);
447 }
448 vL[l] = full ? vbq : vbl;
449 dL[l] = dl;
450 defd[l] = full || home;
451 }
452 if (defd[0] && defd[1]) { // 2-point quadratic fit (3rd-order value -> 2nd-order gradient)
453 const double d0 = dL[0], d1 = dL[1], v0 = vL[0], v1 = vL[1];
454 return (v0 * d1 / d0 - v1 * d0 / d1) / (d1 - d0);
455 }
456 if (defd[0]) // near image point only -> 1-point linear
457 return vL[0] / dL[0];
458 double d0 = Kokkos::fabs(pv[da] / nv[da]); // degenerate sliver: 1-point through the cell centre
459 if (d0 < 1e-3)
460 d0 = 1e-3;
461 const long ii = (long)ci[0] * st[0] + (long)ci[1] * st[1] + (long)ci[2] * st[2];
462 return U(ii) / d0;
463}
464
465// EMBED viscous operator (setFaceInterp(5)): identical to fvViscousApply except the wall drag is
466// the TRUE-NORMAL Basilisk gradient (embedDirichletGradient) rather than the axis-by-axis
467// fragment-normal estimate. Per cut cell the fragment area is |W| = |o_{a−}−o_{a+}|
468// (divergence-theorem normal) and the wall flux is +μ·area·d(U)/dn — algebraically the −μ·(W·∇U) of
469// fvViscousApply but with the O(h²) true-normal derivative in place of the O(h) axis
470// reconstruction. Interior cells (area→0) reduce to idt·cs·U + μ(diag·U − offs) exactly, so the
471// mode-0 defect vanishes there.
473 CCConst oy, CCConst oz, double mu, double idt, C3 e, int g) {
474 CCExec space;
475 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
476 Kokkos::parallel_for(
477 "peclet::flow::embed_viscous_apply", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
478 KOKKOS_LAMBDA(int x, int y, int z) {
479 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
480 const long st[3] = {sx, sy, sz};
481 const long i = (long)x + (long)y * sy + (long)z * sz;
482 CCConst oa[3] = {ox, oy, oz};
483 double Wv[3];
484 double diag = 0.0, offs = 0.0;
485 for (int a = 0; a < 3; ++a) {
486 const double om = oa[a](i), op = oa[a](i + st[a]);
487 Wv[a] = om - op;
488 diag += om + op;
489 offs += om * U(i - st[a]) + op * U(i + st[a]);
490 }
491 const double area = Kokkos::sqrt(Wv[0] * Wv[0] + Wv[1] * Wv[1] + Wv[2] * Wv[2]);
492 double wall = 0.0;
493 if (area > 1e-12) {
494 double nx = 0.5 * (sdf(i + sx) - sdf(i - sx));
495 double ny = 0.5 * (sdf(i + sy) - sdf(i - sy));
496 double nz = 0.5 * (sdf(i + sz) - sdf(i - sz));
497 const double nn = Kokkos::sqrt(nx * nx + ny * ny + nz * nz);
498 if (nn > 1e-12) {
499 nx /= nn;
500 ny /= nn;
501 nz /= nn;
502 const double sdi = sdf(i); // foot-point (boundary centroid proxy) offset, cell units
503 const double dudn = embedDirichletGradient(U, sdf, e, x, y, z, nx, ny, nz, -sdi * nx,
504 -sdi * ny, -sdi * nz);
505 wall = area * dudn; // +μ·area·dudn == −μ·(W·∇U) with the true-normal derivative
506 }
507 }
508 Lu(i) = idt * cs(i) * U(i) + mu * (diag * U(i) - offs + wall);
509 });
510}
511
512// 7-point stencil matvec y = M·u using the stored (rs-scaled) IBM operator coefficients. Used to
513// form the mode-4 defect-correction RHS b = M·u^k − rs·L_FV(u^k) + rs·b_FV, whose fixed point
514// satisfies L_FV·u* = b_FV exactly, with M only the (stable, small-cell-safe) preconditioner.
515inline void stencilMatvec(CCField y, CCConst u, MConst AC, MConst AW, MConst AE, MConst AS,
516 MConst AN, MConst AB, MConst AT, C3 e, int g) {
517 CCExec space;
518 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
519 Kokkos::parallel_for(
520 "peclet::flow::stencil_matvec", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
521 KOKKOS_LAMBDA(int x, int y2, int z) {
522 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
523 const long i = (long)x + (long)y2 * sy + (long)z * sz;
524 y(i) = (double)AC(i) * u(i) + (double)AW(i) * u(i - sx) + (double)AE(i) * u(i + sx) +
525 (double)AS(i) * u(i - sy) + (double)AN(i) * u(i + sy) + (double)AB(i) * u(i - sz) +
526 (double)AT(i) * u(i + sz);
527 });
528}
529
530// u -= d over the inner cells (the mode-2 correction applies the transposeGradWallAware field).
531inline void subtractField(CCField u, CCConst d, C3 e, int g) {
532 CCExec space;
533 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
534 Kokkos::parallel_for(
535 "peclet::flow::subtract_field", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
536 KOKKOS_LAMBDA(int x, int y, int z) {
537 const long i = (long)x + (long)y * e.x + (long)z * (long)e.x * e.y;
538 u(i) -= d(i);
539 });
540}
541
542// OPENNESS-WEIGHTED cell pressure gradient along one axis (Basilisk centered_gradient / embed cell
543// correction): out(i) = (o(i)·(p(i)−p(i−sa)) + o(i+sa)·(p(i+sa)−p(i))) / (o(i)+o(i+sa)). In the
544// interior (o=1) this is the central difference ½(p(i+sa)−p(i−sa)); at a CUT cell with one closed
545// face (o=0) it returns the OPEN face gradient at FULL weight (not the plain projectCorrectCenter's
546// ½), which is the flux-consistent pressure force the approximate projection needs at the embedded
547// boundary — the O(h) under-correction the plain map makes there (analysis defect (b)). Used for
548// both the incremental −grad(P^n) predictor and the projection correction of the EMBED path (mode
549// 6), so the two stay the openness-adjoint of the fs-weighted divergence constraint.
550inline void centerGradOpen(CCField out, CCConst p, CCConst o, int axis, C3 e, int g) {
551 CCExec space;
552 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
553 Kokkos::parallel_for(
554 "peclet::flow::center_grad_open", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
555 KOKKOS_LAMBDA(int x, int y, int z) {
556 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
557 const long i = (long)x + (long)y * sy + (long)z * sz;
558 const long sa = (axis == 0) ? sx : (axis == 1) ? sy : sz;
559 const double om = o(i), op = o(i + sa);
560 out(i) = (om * (p(i) - p(i - sa)) + op * (p(i + sa) - p(i))) / (om + op + 1e-12);
561 });
562}
563
564// u,v,w -= openness-weighted cell pressure gradient of phi (centerGradOpen per axis). The embed
565// analogue of projectCorrectCenter: the cut-cell cell velocity gets the full open-face pressure
566// force.
568 CCConst oy, CCConst oz, C3 e, int g) {
569 CCExec space;
570 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
571 Kokkos::parallel_for(
572 "peclet::flow::correct_center_open", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
573 KOKKOS_LAMBDA(int x, int y, int z) {
574 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
575 const long i = (long)x + (long)y * sy + (long)z * sz;
576 const double omx = ox(i), opx = ox(i + sx);
577 const double omy = oy(i), opy = oy(i + sy);
578 const double omz = oz(i), opz = oz(i + sz);
579 u(i) -= (omx * (phi(i) - phi(i - sx)) + opx * (phi(i + sx) - phi(i))) / (omx + opx + 1e-12);
580 v(i) -= (omy * (phi(i) - phi(i - sy)) + opy * (phi(i + sy) - phi(i))) / (omy + opy + 1e-12);
581 w(i) -= (omz * (phi(i) - phi(i - sz)) + opz * (phi(i + sz) - phi(i))) / (omz + opz + 1e-12);
582 });
583}
584
585// Cell-centered velocity correction u_c -= grad_c(phi), grad_c per axis = ½·(g⁻ + g⁺) of the two
586// adjacent FACE pressure-gradients, but a face that is fully CLOSED (openness 0 — a solid
587// neighbour) contributes a ZERO gradient instead of reading that neighbour's φ. Rationale:
588// * interior fluid cell (both faces open): ½(φᵢ₊₁-φᵢ₋₁) — the central difference, bulk unchanged;
589// * immersed cut cell (solid neighbour): the closed face's φ is DECOUPLED (≈0, AC≈0 in the
590// operator), so
591// reading it corrupts the gradient — zeroing that face uses only the fluid-side (open)
592// gradient;
593// * domain-BC wall (Neumann, φ-ghost = interior): the closed wall face truly has ∂φ/∂n≈0, and
594// zeroing it
595// gives ½·g_open — identical to the previous central difference with the Neumann ghost (no
596// change there).
597// Cut faces (0<o<1) keep their real gradient (the neighbour is fluid). Only the cell field is
598// touched; the projection's face divergence-free guarantee is unaffected. phi ghosts + face
599// openness must be filled first.
601 CCConst oy, CCConst oz, C3 e, int g) {
602 CCExec space;
603 using MD = Kokkos::MDRangePolicy<CCExec, Kokkos::Rank<3>>;
604 Kokkos::parallel_for(
605 "peclet::flow::correct_center", MD(space, {g, g, g}, {e.x - g, e.y - g, e.z - g}),
606 KOKKOS_LAMBDA(int x, int y, int z) {
607 const long sx = 1, sy = e.x, sz = (long)e.x * e.y;
608 const long i = (long)x + (long)y * sy + (long)z * sz;
609 const double gm_x = (ox(i) > 1e-12) ? (phi(i) - phi(i - sx)) : 0.0;
610 const double gp_x = (ox(i + sx) > 1e-12) ? (phi(i + sx) - phi(i)) : 0.0;
611 const double gm_y = (oy(i) > 1e-12) ? (phi(i) - phi(i - sy)) : 0.0;
612 const double gp_y = (oy(i + sy) > 1e-12) ? (phi(i + sy) - phi(i)) : 0.0;
613 const double gm_z = (oz(i) > 1e-12) ? (phi(i) - phi(i - sz)) : 0.0;
614 const double gp_z = (oz(i + sz) > 1e-12) ? (phi(i + sz) - phi(i)) : 0.0;
615 u(i) -= 0.5 * (gm_x + gp_x);
616 v(i) -= 0.5 * (gm_y + gp_y);
617 w(i) -= 0.5 * (gm_z + gp_z);
618 });
619}
620
621} // namespace peclet::flow
622
623#endif // PECLET_FLOW_MAC_APPROX_PROJECTION_HPP
flow — portable (Kokkos) cut-cell pressure-operator face openness from an SDF.
void transposeGradWallAware(CCField out, CCConst p, CCConst sdf, CCConst o, CCConst xc, bool useCen, int axis, C3 e, int g)
void projectCorrectCenterOpen(CCField u, CCField v, CCField w, CCConst phi, CCConst ox, CCConst oy, CCConst oz, C3 e, int g)
double eQuad(double xx, double a1, double a2, double a3)
void buildFaceCentroidDist(CCField xcx, CCField xcy, CCField xcz, CCConst sdf, C3 e)
void subtractField(CCField u, CCConst d, C3 e, int g)
void stencilMatvec(CCField y, CCConst u, MConst AC, MConst AW, MConst AE, MConst AS, MConst AN, MConst AB, MConst AT, C3 e, int g)
void buildCellFraction(CCField cs, CCConst sdf, C3 e, int g)
double embedDirichletGradient(CCConst U, CCConst sdf, C3 e, int x, int y, int z, double nx, double ny, double nz, double px, double py, double pz)
void centerToFaceWallAware(CCField uf, CCField vf, CCField wf, CCConst U, CCConst V, CCConst W, CCConst sdf, CCConst xcx, CCConst xcy, CCConst xcz, bool useCen, C3 e, int g)
void fvViscousApply(CCField Lu, CCConst U, CCConst sdf, CCConst cs, CCConst ox, CCConst oy, CCConst oz, double mu, double idt, C3 e, int g)
void centerGradOpen(CCField out, CCConst p, CCConst o, int axis, C3 e, int g)
void embedViscousApply(CCField Lu, CCConst U, CCConst sdf, CCConst cs, CCConst ox, CCConst oy, CCConst oz, double mu, double idt, C3 e, int g)
void centerToFace(CCField uf, CCField vf, CCField wf, CCConst U, CCConst V, CCConst W, C3 e, int g)
int wallAwareFaceStencil(CCConst sdf, long i, long sa, int cFace, int eAxis, double xcen, long idx[3], double w[3])
Kokkos::View< double *, CCMem > CCField
Kokkos::DefaultExecutionSpace CCExec
void projectCorrectCenter(CCField u, CCField v, CCField w, CCConst phi, CCConst ox, CCConst oy, CCConst oz, C3 e, int g)
Kokkos::View< const float *, CCMem > MConst
double ccSampleExt(CCConst sdf, C3 ext, double x, double y, double z)
double wallAwareFaceValue(CCConst U, CCConst sdf, long i, long sa, int cFace, int eAxis, double xcen)
Kokkos::View< const double *, CCMem > CCConst
static constexpr double AC