2"""S1 probe (doc/collocated_accuracy_ceiling.md #5): does the ABC cell/face reconciliation carry a
3steady-state defect that could hold the ~0.3 % flow-excess plateau?
5Structural background (this is what the test checks, not assumes): at the JOINT fixed point of the
6mode-9 step -- velocity AND accumulated pressure both stationary -- the rotational update
7P += (rho/dt)*phi - mu*div(u*) together with A phi = -div(u*) forces (A + rho/(mu dt)) phi = 0,
8i.e. phi = 0 and div(u*) = 0. Consequences, each measured here at the march's stopping point:
10 m1 |uf - halfavg(u)| / <u> the projected face field vs the plain 1/2-1/2 average of the
11 cell field. projectCorrect writes uf = halfavg(u*) - grad_f(phi)
12 and the cell correction writes u = u* - gpCenterGrad(phi), so a
13 persistent gap is EXACTLY the un-converged gauge (phi != 0) plus
14 the gradient-pair mismatch: the S1 channel. If m1 ~ 0 the ABC
15 two-field loop closes and S1 is dead.
16 m2 alpha-div(halfavg(u)) h/<u> the constraint residual of the CELL field (the face field's is
17 pinned by the solve; the cell field's is the approximate
18 projection's O(h^2) remainder -- it should CONVERGE, not floor).
19 m3 |u_cell - cellavg(uf)| / <u> the doc's S1 quantity, split by wall distance (near = |sdf|<=2h).
20 This is a smoothing difference (h^2 D2u/4 in the bulk); the
21 interesting part is whether the NEAR-WALL part floors.
23Usage: SDFLOW_BUILD=build_ge BED=<packing.npz> python collocated_s1_reconciliation.py [N ...]
24 (defaults N = 96 128 192 -> R = 6, 8, 12 on the 16^3-box beds)
31sys.path.insert(0, os.path.abspath(os.path.join(
32 os.path.dirname(__file__),
"..",
"..", os.environ.get(
"SDFLOW_BUILD",
"build"))))
33from peclet
import flow
35BED = os.environ.get(
"BED",
"")
37 raise SystemExit(
"BED=<packing.npz> is required (use the phi=0.60 s3 bed)")
38MARCH_TOL = float(os.environ.get(
"MARCH_TOL",
"1e-8"))
39MAXS = int(os.environ.get(
"MARCH_MAX",
"4000"))
40DT = float(os.environ.get(
"DT",
"60.0"))
45 box = np.asarray(pk[
"box"], float)
46 assert np.allclose(box, box[0]), f
"{npz}: box {box} is not cubic"
48 c = np.asarray(pk[
"centers"]) * Rc
49 r = np.asarray(pk[
"scales"]) * Rc
50 ax = np.arange(N) + 0.5
51 S = np.full((N, N, N), 1e30)
52 for sh
in np.stack(np.meshgrid(*[[-1., 0., 1.]] * 3, indexing=
"ij"), -1).reshape(-1, 3):
54 keep = np.all((cs + (r + 3)[:,
None] > 0) & (cs - (r + 3)[:,
None] < N), axis=1)
55 for (cx, cy, cz), rr
in zip(cs[keep], r[keep]):
56 i0, i1 = np.searchsorted(ax, [cx - rr - 3, cx + rr + 3])
57 j0, j1 = np.searchsorted(ax, [cy - rr - 3, cy + rr + 3])
58 k0, k1 = np.searchsorted(ax, [cz - rr - 3, cz + rr + 3])
59 if i0 >= i1
or j0 >= j1
or k0 >= k1:
61 d = np.sqrt((ax[i0:i1,
None,
None] - cx) ** 2 + (ax[
None, j0:j1,
None] - cy) ** 2
62 + (ax[
None,
None, k0:k1] - cz) ** 2) - rr
63 np.minimum(S[i0:i1, j0:j1, k0:k1], d, out=S[i0:i1, j0:j1, k0:k1])
64 return np.asfortranarray(np.clip(S, -1e3, 1e3)), Rc
69 s = flow.SolverColocated(N, N, N)
70 s.set_rho(1.0); s.set_mu(0.1); s.set_dt(DT)
71 s.set_body_force(1e-3, 0, 0); s.set_advection(
False)
72 s.set_velocity_solver_params(150)
73 s.set_pressure_multigrid(
True, max(2, int(np.log2(N)) - 2))
74 s.set_pressure_pcg(
True, 300, 1e-8)
75 if hasattr(s,
"set_collocated_scheme"):
76 s.set_collocated_scheme(
"gauge-exact")
79 s.set_solid(sdf, cutcell_pressure=
True, pressure_coarse=
"rediscretized")
81 for it
in range(MAXS):
84 um = float(np.asarray(s.get_u()).mean())
85 if it > 10
and abs(um - prev) < MARCH_TOL * (abs(um) + 1e-30):
88 return s, sdf, R, it + 1
94 print(f
" {name:>34}: (empty)")
96 q = np.quantile(v, [0.5, 0.99])
97 print(f
" {name:>34}: max {v.max():.3e} p99 {q[1]:.3e} med {q[0]:.3e} "
98 f
"rms {np.sqrt((v**2).mean()):.3e}", flush=
True)
99 return float(np.sqrt((v ** 2).mean()))
103for N
in [int(x)
for x
in (sys.argv[1:]
or [96, 128, 192])]:
105 U = [np.asarray(s.get_u()), np.asarray(s.get_v()), np.asarray(s.get_w())]
106 UF = [np.asarray(s.get_uf()), np.asarray(s.get_vf()), np.asarray(s.get_wf())]
107 OX = [np.asarray(s.get_ox()), np.asarray(s.get_oy()), np.asarray(s.get_oz())]
109 uscale = float(np.abs(U[0][fluid]).mean())
110 print(f
"\nN={N} R={R:.1f} steps={steps} <|u|>_fluid={uscale:.4e} "
111 f
"k/R^2={float(U[0].mean())/1e-3*0.1/R**2:.6e}")
114 half = 0.5 * (U[a] + np.roll(U[a], 1, axis=a))
116 rms[f
"m1_ax{a}"] =
stats(f
"m1 |uf-halfavg(u)| ax{a} (open faces)",
117 (UF[a] - half)[openf], uscale)
120 div = np.zeros_like(U[0])
122 half = 0.5 * (U[a] + np.roll(U[a], 1, axis=a))
124 div += np.roll(flx, -1, axis=a) - flx
125 stats(
"m2 alpha-div(halfavg(u)) fluid", div[fluid], uscale)
127 divf = np.zeros_like(U[0])
130 divf += np.roll(flx, -1, axis=a) - flx
131 stats(
"m2f alpha-div(uf) fluid (solver)", divf[fluid], uscale)
134 cavg = 0.5 * (UF[a] + np.roll(UF[a], -1, axis=a))
136 near = fluid & (np.abs(sdf) <= 2.0)
137 bulk = fluid & (np.abs(sdf) > 2.0)
138 rms[f
"m3n_ax{a}"] =
stats(f
"m3 |u-cellavg(uf)| ax{a} NEAR wall", d[near], uscale)
139 rms[f
"m3b_ax{a}"] =
stats(f
"m3 |u-cellavg(uf)| ax{a} bulk", d[bulk], uscale)
141 lr = np.log(N / prev[
"N"])
142 orders = {k: np.log(prev[k] / v) / lr
for k, v
in rms.items()
143 if k
in prev
and v
and prev[k]}
144 print(
" orders vs previous N (positive = decays):",
145 " ".join(f
"{k}:{o:+.2f}" for k, o
in orders.items()), flush=
True)
146 prev = {
"N": N, **{k: v
for k, v
in rms.items()
if v}}