2"""Long-march probe of the STALL hypothesis (2026-08-20): the collocated march quasi-freezes on a
3slow u-P manifold far from the true fixed point (measured: |uf - halfavg(u)| rms 4.5e-2 <u> at the
4stopping point, while the ONLY true fixed point of the map has phi = 0, uf == halfavg(u), and a
5dt-free steady system). If that is the plateau mechanism, marching far past the <u> criterion
6must show k creeping toward the staggered answer as the reconciliation gap decays (however
7slowly); if k and the gap are EXACTLY stationary, the implemented map has a non-closing loop that
8differs from the model and the discrepancy must be found in code.
10Runs the phi=0.60 bed, collocated gauge-exact, for STEPS steps regardless of any criterion,
11printing every EVERY steps: k (cell + flux estimators), m1 = rms|uf - halfavg(u)|/<|u|>,
12m2 = rms alpha-div(halfavg(u))/<|u|>, and the drift of each since the last print.
14 SDFLOW_BUILD=build_ge BED=...npz N=192 STEPS=20000 EVERY=500 DT=60 python collocated_longmarch.py
21sys.path.insert(0, os.path.abspath(os.path.join(
22 os.path.dirname(__file__),
"..",
"..", os.environ.get(
"SDFLOW_BUILD",
"build"))))
23from peclet
import flow
25BED = os.environ[
"BED"]
26N = int(os.environ.get(
"N",
"192"))
27STEPS = int(os.environ.get(
"STEPS",
"20000"))
28EVERY = int(os.environ.get(
"EVERY",
"500"))
29DT = float(os.environ.get(
"DT",
"60.0"))
30KIND = os.environ.get(
"KIND",
"gauge-exact")
31ROT = int(os.environ.get(
"ROT",
"1"))
32ROTF = int(os.environ.get(
"ROTF",
"0"))
33ROTW = float(os.environ.get(
"ROTW",
"1"))
34WALLW = float(os.environ.get(
"WALLW",
"0"))
38DTSWITCH = dict((int(a), float(b))
for a, b
in
39 (kv.split(
":")
for kv
in os.environ.get(
"DTSWITCH",
"").split(
",")
if kv))
43SHIFT = np.array([float(v)
for v
in os.environ.get(
"SHIFT",
"0,0,0").split(
",")])
48 box = np.asarray(pk[
"box"], float)
50 c = np.asarray(pk[
"centers"]) * Rc + SHIFT
51 r = np.asarray(pk[
"scales"]) * Rc
52 ax = np.arange(N) + 0.5
53 S = np.full((N, N, N), 1e30)
54 for sh
in np.stack(np.meshgrid(*[[-1., 0., 1.]] * 3, indexing=
"ij"), -1).reshape(-1, 3):
56 keep = np.all((cs + (r + 3)[:,
None] > 0) & (cs - (r + 3)[:,
None] < N), axis=1)
57 for (cx, cy, cz), rr
in zip(cs[keep], r[keep]):
58 i0, i1 = np.searchsorted(ax, [cx - rr - 3, cx + rr + 3])
59 j0, j1 = np.searchsorted(ax, [cy - rr - 3, cy + rr + 3])
60 k0, k1 = np.searchsorted(ax, [cz - rr - 3, cz + rr + 3])
61 if i0 >= i1
or j0 >= j1
or k0 >= k1:
63 d = np.sqrt((ax[i0:i1,
None,
None] - cx) ** 2 + (ax[
None, j0:j1,
None] - cy) ** 2
64 + (ax[
None,
None, k0:k1] - cz) ** 2) - rr
65 np.minimum(S[i0:i1, j0:j1, k0:k1], d, out=S[i0:i1, j0:j1, k0:k1])
66 return np.asfortranarray(np.clip(S, -1e3, 1e3)), Rc
70sv = flow.Solver
if KIND ==
"stag" else flow.SolverColocated
72s.set_rho(1.0); s.set_mu(MU); s.set_dt(DT)
73s.set_body_force(F0, 0, 0); s.set_advection(
False)
74s.set_velocity_solver_params(int(os.environ.get(
"VIT",
"150")))
75if int(os.environ.get(
"APORDER",
"1")) != 1:
76 s.set_aperture_order(int(os.environ[
"APORDER"]))
77s.set_pressure_multigrid(
True, int(os.environ.get(
"MGL",
"0"))
or max(2, int(np.log2(N)) - 2))
78s.set_pressure_pcg(
True, 300, 1e-8)
83 s.set_ghost_projection(
True)
84 elif KIND ==
"fluidonly":
85 s.set_fluid_only_constraint(1)
86 elif KIND.startswith(
"fluidonly2"):
87 s.set_fluid_only_constraint(2)
89 s.set_face_interp(int(KIND.split(
"_m")[1]))
90 elif KIND.startswith(
"mode"):
91 s.set_face_interp(int(KIND[4:]))
92 elif hasattr(s,
"set_collocated_scheme"):
93 s.set_collocated_scheme(KIND)
95 s.set_face_interp({
"gauge-exact": 9,
"plain": 0}[KIND])
97 s.set_rotational_pressure(
False)
99 s.set_rotational_filter(
True)
101 s.set_rotational_weight(ROTW)
103 s.set_rotational_wall_weight(WALLW)
104s.set_solid(sdf, cutcell_pressure=
True, pressure_coarse=
"rediscretized")
107print(f
"# bed {os.path.basename(BED)} N={N} R={R:.1f} kind={KIND} rot={ROT} rotf={ROTF} rotw={ROTW} wallw={WALLW} dt={DT} steps={STEPS}", flush=
True)
108print(f
"{'step':>7} {'k_cell/R2':>13} {'k_face/R2':>13} {'m1_rms':>10} {'m2_rms':>10} "
109 f
"{'dk_cell':>10} {'dm1':>10}", flush=
True)
112for it
in range(1, STEPS + 1):
114 s.set_dt(DTSWITCH[it])
115 print(f
"# --- dt -> {DTSWITCH[it]} at step {it} ---", flush=
True)
117 if it % EVERY == 0
or it == 50:
118 U = [np.asarray(s.get_u()), np.asarray(s.get_v()), np.asarray(s.get_w())]
119 us = float(np.abs(U[0][fluid]).mean()) + 1e-300
120 kc = float(U[0].mean()) * MU / F0 / R ** 2
123 OX = [np.asarray(s.get_ox()), np.asarray(s.get_oy()), np.asarray(s.get_oz())]
124 kf = float((OX[0] * U[0]).mean()) * MU / F0 / R ** 2
125 div = np.zeros_like(U[0])
128 div += np.roll(flx, -1, axis=a) - flx
129 m2 = float(np.sqrt((div[fluid] ** 2).mean())) / us
131 UF = [np.asarray(s.get_uf()), np.asarray(s.get_vf()), np.asarray(s.get_wf())]
132 OX = [np.asarray(s.get_ox()), np.asarray(s.get_oy()), np.asarray(s.get_oz())]
133 kf = float((OX[0] * UF[0]).mean()) * MU / F0 / R ** 2
135 div = np.zeros_like(U[0])
137 half = 0.5 * (U[a] + np.roll(U[a], 1, axis=a))
139 m1sq += float(((UF[a] - half)[op] ** 2).sum()); cnt += int(op.sum())
141 div += np.roll(flx, -1, axis=a) - flx
142 m1 = np.sqrt(m1sq / cnt) / us
143 m2 = float(np.sqrt((div[fluid] ** 2).mean())) / us
144 dk =
"" if pk
is None else f
"{kc - pk:+.2e}"
145 dm =
"" if pm
is None else f
"{m1 - pm:+.2e}"
146 Pf = np.asarray(s.get_p())
147 dP =
"" if Pprev
is None else f
"{np.abs(Pf - Pprev).max():.3e}"
149 print(f
"{it:>7} {kc:>13.7e} {kf:>13.7e} {m1:>10.3e} {m2:>10.3e} {dk:>10} {dm:>10} "
150 f
"dP={dP:>10} |P|={np.abs(Pf).max():.3e}", flush=
True)