core 0.5.0
Shared MPI block decomposition + asynchronous ghost-layer exchange (header-only C++20)
Loading...
Searching...
No Matches
ghost_closure.hpp
Go to the documentation of this file.
1// core — directional ghost-cell closure primitives (shared flow <-> AMR).
2//
3// *** NO PRODUCTION CONSUMER (2026-08-19) *** — flow quarantined its ghost projection 2026-08-18
4// (the gauge-exact collocated scheme is its default) and AMR retired its NS AUTO-arm 2026-08-19
5// (the aperture MG-PCG covers advection; see core/docs/amr_aperture_advection_plan.md §RESOLVED).
6// Both consumers keep these closures compilable strictly for A/B studies and the regression
7// tests that pin the quarantined schemes' behaviour.
8//
9// The PURE per-face pieces of the directional ghost-cell IBM projection, lifted verbatim from
10// flow/src/ghost_projection.hpp (where they were validated 2nd-order on Zick & Homsy, staggered
11// AND collocated — see that header for the full scheme documentation): a solid staggered/averaged
12// face is closed by the momentum IBM's 1-D wall-anchored quadratic along the face's own axis,
13//
14// poly_D(th) * u_ghost = 2*u_bc + poly_Nc(th)*u_near + poly_N_nb(th)*u_far
15// th = sdf_near/(sdf_near - sdf_ghost), clamped [GP_THETA_MIN, 1]
16//
17// with the face-state cascade COUPLED / QUAD / LIN / BC_ONLY / EXPLICIT (sliver faces use the
18// extended th in (1,2)). Everything here is a pure function of 1-D sdf samples along one axis —
19// grid-agnostic, so the SAME classification + weights serve flow's uniform structured grid and
20// core's AMR octree (whose cut cells live in a locally uniform finest band). The grid-specific
21// parts (overlay SoA layout, neighbour wrap, delta kernels) stay with each consumer.
22//
23// float arithmetic throughout — identical to flow's cut_cell_ibm.hpp SCHEME-0 closure polys, so
24// the lifted functions are drop-in for flow (same bits on its ghost path).
25//
26// Host/device: functions are KOKKOS_INLINE_FUNCTION when Kokkos_Core.hpp has been included
27// BEFORE this header (device consumers must include Kokkos first), plain inline otherwise (the
28// host-only oracle builds).
29#ifndef PECLET_CORE_SCHEME_GHOST_CLOSURE_HPP
30#define PECLET_CORE_SCHEME_GHOST_CLOSURE_HPP
31
32#include <cmath>
33#include <cstdint>
34#include <limits>
35
36#ifdef KOKKOS_INLINE_FUNCTION
37#define PECLET_CORE_GP_HD KOKKOS_INLINE_FUNCTION
38#else
39#define PECLET_CORE_GP_HD inline
40#endif
41
43
44constexpr float GP_THETA_MIN = 1e-4f;
45
46enum GpState : int8_t {
49 GP_LIN = 2,
52};
53
54// The wall-anchored closure polynomials (flow cut_cell_ibm.hpp SCHEME 0, float — the same
55// arithmetic the momentum IBM uses, so the ghost closure agrees with the momentum solid masks).
56PECLET_CORE_GP_HD float gpPolyD(float xi) {
57 return xi * (1.0f + xi);
58}
59PECLET_CORE_GP_HD float gpPolyNc(float xi) {
60 return 2.0f * (xi * xi - 1.0f);
61}
63 return xi * (1.0f - xi);
64}
65
66// NaN sentinel + finiteness test for the optional exact-crossing thetas. The values are either
67// the NaN sentinel or a finite crossing fraction (never inf), so self-comparison is the portable
68// isfinite.
70#ifdef KOKKOS_INLINE_FUNCTION
71 return Kokkos::nan("");
72#else
73 return std::numeric_limits<float>::quiet_NaN();
74#endif
75}
77 return x == x;
78}
79
84PECLET_CORE_GP_HD void gpOrderWeights(int8_t st, float th, int order, float& wbc, float& w1,
85 float& w2, float& D) {
86 wbc = w1 = w2 = 0.0f;
87 D = 1.0f;
88 if (st == GP_BC_ONLY) {
89 wbc = 1.0f;
90 return;
91 }
92 if (st == GP_QUAD && order == 2) {
93 D = gpPolyD(th);
94 wbc = 2.0f / D;
95 w1 = gpPolyNc(th) / D;
96 w2 = gpPolyNnb(th) / D;
97 } else { // linear (GP_LIN, or a GP_QUAD face at order 1)
98 D = th;
99 wbc = 1.0f / th;
100 w1 = (th - 1.0f) / th;
101 }
102}
103
104struct GpFace {
105 int8_t state;
106 float th, wbc, w1, w2, D;
107};
108
118PECLET_CORE_GP_HD GpFace gpClassifyFace(float sg, float sn, float sf, float sb, float snb,
119 float sc1, float sc2, bool otherSolid,
120 float exStd = gpNan(), float exSliver = gpNan()) {
121 GpFace f{GP_COUPLED, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f};
122 if (sg >= 0.0f && snb >= 0.0f)
123 return f; // COUPLED
124 if (sg < 0.0f && otherSolid) {
125 f.state = GP_BC_ONLY; // sandwich: both own faces solid, wall BCs determine the axis
126 f.wbc = 1.0f;
127 return f;
128 }
129 float th;
130 if (sg < 0.0f) { // standard ghost (near face fluid guaranteed: not sandwich)
131 th = gpFinite(exStd) ? exStd : sn / (sn - sg);
132 th = th < GP_THETA_MIN ? GP_THETA_MIN : (th > 1.0f ? 1.0f : th);
133 } else { // sliver: face point fluid, neighbor center solid
134 if (sb >= 0.0f) {
135 f.state = GP_EXPLICIT; // no crossing on the u-line: explicit u* flux, no phi coupling
136 return f;
137 }
138 th = gpFinite(exSliver) ? exSliver : 1.0f + sg / (sg - sb);
139 const float lo = 1.0f + GP_THETA_MIN;
140 th = th < lo ? lo : (th > 2.0f ? 2.0f : th);
141 }
142 f.th = th;
143 const bool src1 = (sn >= 0.0f) && (sc1 >= 0.0f);
144 const bool src2 = (sf >= 0.0f) && (sc2 >= 0.0f);
145 if (!src1) {
146 f.state = GP_BC_ONLY;
147 f.wbc = 1.0f;
148 return f;
149 }
150 if (src2) {
151 f.state = GP_QUAD;
152 f.D = gpPolyD(th);
153 f.wbc = 2.0f / f.D;
154 f.w1 = gpPolyNc(th) / f.D;
155 f.w2 = gpPolyNnb(th) / f.D;
156 } else {
157 f.state = GP_LIN;
158 f.D = th;
159 f.wbc = 1.0f / th;
160 f.w1 = (th - 1.0f) / th;
161 }
162 return f;
163}
164
174template <class OV, class CellId>
175PECLET_CORE_GP_HD bool gpFillRow(const OV& ov, int slot, CellId cellId, const float F[3][4],
176 const float Cq[3][5], int matrixOrder, int rhsOrder,
177 const float* exStd = nullptr, const float* exSliver = nullptr) {
178 bool any = false;
179 bool anyPhi = false;
180 float rho = 1.0f;
181 GpFace faces[6];
182 const float nanv = gpNan();
183 for (int a = 0; a < 3; ++a) {
184 const bool solidM = F[a][1] < 0.0f; // own minus face (m=0)
185 const bool solidP = F[a][2] < 0.0f; // own plus face (m=1)
186 // minus side (k = 2a+1): ghost m=0, near m=1, far m=2, beyond m=-1; nb center q=-1;
187 // gradient cells q=+1, q=+2.
188 faces[2 * a + 1] =
189 gpClassifyFace(F[a][1], F[a][2], F[a][3], F[a][0], Cq[a][1], Cq[a][3], Cq[a][4], solidP,
190 exStd ? exStd[2 * a + 1] : nanv, exSliver ? exSliver[2 * a + 1] : nanv);
191 // plus side (k = 2a): ghost m=1, near m=0, far m=-1, beyond m=2; nb center q=+1;
192 // gradient cells q=-1, q=-2.
193 faces[2 * a] =
194 gpClassifyFace(F[a][2], F[a][1], F[a][0], F[a][3], Cq[a][3], Cq[a][1], Cq[a][0], solidM,
195 exStd ? exStd[2 * a] : nanv, exSliver ? exSliver[2 * a] : nanv);
196 }
197 float wbcM[6], w1M[6], w2M[6]; // matrix-order weights (wbc unused in the matrix)
198 for (int k = 0; k < 6; ++k) {
199 const GpFace& f = faces[k];
200 if (f.state != GP_COUPLED)
201 any = true;
202 if (f.state == GP_COUPLED || f.state == GP_QUAD || f.state == GP_LIN)
203 anyPhi = true;
204 wbcM[k] = w1M[k] = w2M[k] = 0.0f;
205 if (f.state == GP_QUAD || f.state == GP_LIN) {
206 float Dm;
207 gpOrderWeights(f.state, f.th, matrixOrder, wbcM[k], w1M[k], w2M[k], Dm);
208 if (Dm < rho)
209 rho = Dm;
210 }
211 }
212 if (!any)
213 return false;
214 ov.cell(slot) = cellId;
215 ov.rescale(slot) = rho;
216 ov.coupled(slot) = anyPhi ? 1 : 0;
217 for (int k = 0; k < 6; ++k) {
218 const GpFace& f = faces[k];
219 float wbc, w1, w2, D;
220 gpOrderWeights(f.state, f.th, rhsOrder, wbc, w1, w2, D);
221 if (f.state == GP_COUPLED || f.state == GP_EXPLICIT)
222 wbc = w1 = w2 = 0.0f;
223 ov.state(slot * 6 + k) = f.state;
224 ov.th(slot * 6 + k) = f.th;
225 ov.w_bc(slot * 6 + k) = wbc;
226 ov.w_n1(slot * 6 + k) = w1;
227 ov.w_n2(slot * 6 + k) = w2;
228 ov.wm_n1(slot * 6 + k) = w1M[k];
229 ov.wm_n2(slot * 6 + k) = w2M[k];
230 }
231 return true;
232}
233
234} // namespace peclet::core::scheme
235
236#endif // PECLET_CORE_SCHEME_GHOST_CLOSURE_HPP
#define PECLET_CORE_GP_HD
PECLET_CORE_GP_HD bool gpFillRow(const OV &ov, int slot, CellId cellId, const float F[3][4], const float Cq[3][5], int matrixOrder, int rhsOrder, const float *exStd=nullptr, const float *exSliver=nullptr)
Fill one overlay row from the per-axis sample sets.
PECLET_CORE_GP_HD float gpNan()
PECLET_CORE_GP_HD float gpPolyNc(float xi)
PECLET_CORE_GP_HD GpFace gpClassifyFace(float sg, float sn, float sf, float sb, float snb, float sc1, float sc2, bool otherSolid, float exStd=gpNan(), float exSliver=gpNan())
Classify + fill ONE face from its 1-D sdf samples along the face's axis.
PECLET_CORE_GP_HD float gpPolyD(float xi)
constexpr float GP_THETA_MIN
PECLET_CORE_GP_HD bool gpFinite(float x)
PECLET_CORE_GP_HD void gpOrderWeights(int8_t st, float th, int order, float &wbc, float &w1, float &w2, float &D)
Closure weights for one ghost face at the requested extrapolation order.
PECLET_CORE_GP_HD float gpPolyNnb(float xi)