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