46 C0, COUPLED, assemble, build_geo, divergence, overlay_cells, phi_man, periodic_u,
90 """Per-axis discrete cell-center gradient (grid units) of a cell field P that is only
91 defined on fluid cells (solid cells read as 0 — what the solver's decoupled rows hold).
92 central : plain central difference (mode-0 predictor; reads the solid 0)
93 pcc : projectCorrectCenter — 1/2 (g- + g+), closed-face gradient zeroed (binary)
94 open : centerGradOpen with binary openness — full-weight open-face gradient
95 open_real: centerGradOpen with the REAL sampled openness (reads the solid 0 through
96 partially-open faces — the shipping mode-6 kernel)
97 ghost : central where both neighbours fluid; 2nd-order one-sided else; 2-point
98 one-sided fallback; 0 when sandwiched. Never reads solid cells."""
100 Pm = np.where(act, P, 0.0)
103 Pp1 = np.roll(Pm, -1, axis=a)
104 Pn1 = np.roll(Pm, +1, axis=a)
105 ap1 = np.roll(act, -1, axis=a)
106 an1 = np.roll(act, +1, axis=a)
107 if mode ==
"central":
108 g = 0.5 * (Pp1 - Pn1)
110 g = 0.5 * (np.where(an1, Pm - Pn1, 0.0) + np.where(ap1, Pp1 - Pm, 0.0))
112 om, op = an1.astype(float), ap1.astype(float)
113 g = (om * (Pm - Pn1) + op * (Pp1 - Pm)) / np.maximum(om + op, 1e-12)
114 elif mode ==
"open_real":
116 op = np.roll(o_real[a], -1, axis=a)
117 g = (om * (Pm - Pn1) + op * (Pp1 - Pm)) / (om + op + 1e-12)
118 elif mode ==
"ghost":
119 Pp2 = np.roll(Pm, -2, axis=a)
120 Pn2 = np.roll(Pm, +2, axis=a)
121 ap2 = np.roll(act, -2, axis=a)
122 an2 = np.roll(act, +2, axis=a)
123 g = 0.5 * (Pp1 - Pn1)
124 osm = act & ~an1 & ap1
125 g = np.where(osm & ap2, 0.5 * (-3.0 * Pm + 4.0 * Pp1 - Pp2),
126 np.where(osm, Pp1 - Pm, g))
127 osp = act & ~ap1 & an1
128 g = np.where(osp & an2, 0.5 * (3.0 * Pm - 4.0 * Pn1 + Pn2),
129 np.where(osp, Pm - Pn1, g))
130 g = np.where(act & ~an1 & ~ap1, 0.0, g)
132 raise ValueError(mode)
133 out.append(np.where(act, g, 0.0))
143 print(
"\n[C1] ghost-closed divergence of the FACE-AVERAGED exact Stokes field (/h)")
144 print(
" (near-IB: localized boundary truncation ~O(h); bulk: O(h^2) — gated on the FIXED")
145 print(
" shell r in [0.5,0.7]: the all-bulk max hugs the surface, where |d3 u| is ~100x")
146 print(
" larger, so its max-norm order approaches 2 only asymptotically (measured);")
147 print(
" mode-0 contrast: openness divergence o_f*faceavg on the same field, O(1) at cuts)")
148 print(f
"{'N':>5} {'near-IB':>12} {'ord':>6} {'bulk(all)':>12} {'ord':>6} "
149 f
"{'bulk(shell)':>12} {'ord':>6} {'mode0 IB':>12} {'ord':>6}")
156 d = divergence(geo, uf, stokes_u) / geo[
"h"]
159 d0 = np.zeros_like(d)
161 om, op = o[a], np.roll(o[a], -1, axis=a)
162 ufp = np.roll(uf[a], -1, axis=a)
163 d0 += (op * ufp - om * uf[a]).ravel()
165 ov = overlay_cells(geo).ravel()
166 interior = ((np.abs(geo[
"Xc"][0]) < 0.5 - 2 * geo[
"h"])
167 & (np.abs(geo[
"Xc"][1]) < 0.5 - 2 * geo[
"h"])
168 & (np.abs(geo[
"Xc"][2]) < 0.5 - 2 * geo[
"h"])).ravel()
170 bulk = geo[
"active"].ravel() & ~ov & interior
171 rr = np.sqrt(sum((geo[
"Xc"][q] - C0[q]) ** 2
for q
in range(3))).ravel()
172 shell = bulk & (rr >= 0.5) & (rr < 0.7)
173 e_ib = float(np.abs(d[ov]).max())
174 e_bk = float(np.abs(d[bulk]).max())
175 e_sh = float(np.abs(d[shell]).max())
176 e_m0 = float(np.abs(d0[ov & (np.abs(d0) < 1e30)]).max())
177 o_ib =
order(prev
and prev[0], e_ib, prev
and prev[4], N)
178 o_bk =
order(prev
and prev[1], e_bk, prev
and prev[4], N)
179 o_sh =
order(prev
and prev[2], e_sh, prev
and prev[4], N)
180 o_m0 =
order(prev
and prev[3], e_m0, prev
and prev[4], N)
181 print(f
"{N:>5} {e_ib:>12.3e} {o_ib:>6.2f} {e_bk:>12.3e} {o_bk:>6.2f} "
182 f
"{e_sh:>12.3e} {o_sh:>6.2f} {e_m0:>12.3e} {o_m0:>6.2f}")
183 prev = (e_ib, e_bk, e_sh, e_m0, N)
184 slopes = {
"ib": o_ib,
"bulk": o_sh}
190 print(
"\n[C2] cell-gradient operators on a smooth P at CUT cells (fluid center, solid")
191 print(
" axis-neighbour); error vs the analytic gradient, physical units. gauge = +5")
192 print(
" added to P (a constant MUST not change a gradient).")
194 modes = [
"central",
"pcc",
"open",
"open_real",
"ghost"]
196 hdr += f
" {m:>11} {'ord':>5}"
197 hdr += f
" {'ghost gauge':>12}"
205 P = phi_man(*geo[
"Xc"])
214 cut = act & (~np.roll(act, -1, axis=a) | ~np.roll(act, +1, axis=a))
216 e = max(e, float(np.abs(G3[a][cut] / geo[
"h"] - gex[a][cut]).max()))
217 o =
order(prev.get(m), e, prev.get(
"N"), N)
218 row += f
" {e:>11.2e} {o:>5.2f}"
223 dg = max(float(np.abs(G3g[a] - G3[a])[act].max())
for a
in range(3))
224 row += f
" {dg:>12.1e}"
225 gauge_ok &= dg < 1e-12
228 return slopes, gauge_ok
233 """Direct solve of the singular Neumann-like system with the incompatibility dumped
234 UNIFORMLY (residual = lambda*e), the sparse-friendly equivalent of mean removal.
235 See ghost_projection_apriori.test_solve for the derivation + measured failure modes."""
236 r0 = int(np.nonzero(activef)[0][0])
239 Apin.rows[r0], Apin.data[r0] = [r0], [1.0]
240 lu = spla.splu(Apin.tocsc())
243 ev = activef.astype(float)
247 phi_e = lu.solve(evp)
248 lam = (float((arow @ phi_b)[0]) - b[r0]) / (float((arow @ phi_e)[0]) - 1.0)
249 return phi_b - lam * phi_e, lam
253 """One manufactured projection chain; returns error metrics."""
256 rho = row_rescale(geo)
258 pm = phi_man(*geo[
"Xc"])
260 ustar = [np.where(act, U3[a] + Gpm[a], 0.0)
for a
in range(3)]
263 A = assemble(geo, rho)
264 b = -divergence(geo, uf, periodic_u, rho=rho)
266 phig = phi.reshape(act.shape)
269 dphi = phi - pm.ravel()
270 dphi -= dphi[actf].mean()
271 e_phi = float(np.abs(dphi[actf]).max())
274 ufc = [uf[a] - (phig - np.roll(phig, +1, axis=a))
for a
in range(3)]
278 st_m, _ = geo[
"states"][(a, -1)]
279 fluid_face = (st_m == COUPLED) & act
280 if np.any(fluid_face):
281 e_f = max(e_f, float(np.abs(ufc[a] - uf_ex[a])[fluid_face].max()))
285 ucorr = [np.where(act, ustar[a] - Gphi[a], 0.0)
for a
in range(3)]
286 ovm = overlay_cells(geo)
287 e_c, e_c_ib, s2, nf = 0.0, 0.0, 0.0, 0
289 err = np.abs(ucorr[a] - U3[a])
290 e_c = max(e_c, float(err[act].max()))
291 s2 += float((err[act] ** 2).sum())
293 near = act & (ovm | np.roll(ovm, +1, axis=a) | np.roll(ovm, -1, axis=a))
295 e_c_ib = max(e_c_ib, float(err[near].max()))
296 e_c_l2 = float(np.sqrt(s2 / max(nf, 1)))
299 diag = divergence(geo, ufc, periodic_u, rho=rho, u_explicit=uf)
301 ident = float(np.abs(diag[actf] + res[actf]).max())
302 scale = max(1.0, float(np.abs(b[actf]).max()))
303 return dict(e_phi=e_phi, e_f=e_f, e_c=e_c, e_c_ib=e_c_ib, e_c_l2=e_c_l2,
304 ident=ident < 1e-10 * scale)