flow 0.4.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
collocated_longmarch.py
Go to the documentation of this file.
1#!/usr/bin/env python
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.
9
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.
13
14 SDFLOW_BUILD=build_ge BED=...npz N=192 STEPS=20000 EVERY=500 DT=60 python collocated_longmarch.py
15"""
16import os
17import sys
18
19import numpy as np
20
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 # noqa: E402
24
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") # gauge-exact | gauge-2a | plain | stag
31ROT = int(os.environ.get("ROT", "1")) # 0 = PM I ablation (set_rotational_pressure(False))
32ROTF = int(os.environ.get("ROTF", "0")) # 1 = filtered rotational (set_rotational_filter)
33ROTW = float(os.environ.get("ROTW", "1")) # rotational under-relaxation w (set_rotational_weight)
34WALLW = float(os.environ.get("WALLW", "0")) # wall-banded blend w0 (set_rotational_wall_weight)
35# DTSWITCH: comma list of "step:dt" pairs, e.g. "8000:600,14000:6" -- at the given step the
36# solver's dt is changed in place (Frank's discriminator: a TRUE fixed point is dt-free, so any
37# k motion after a switch proves the state was a stalled trajectory, not the fixed point).
38DTSWITCH = dict((int(a), float(b)) for a, b in
39 (kv.split(":") for kv in os.environ.get("DTSWITCH", "").split(",") if kv))
40MU, F0 = 0.1, 1e-3
41
42
43SHIFT = np.array([float(v) for v in os.environ.get("SHIFT", "0,0,0").split(",")]) # cells
44
45
46def bed_sdf(N, npz):
47 pk = np.load(npz)
48 box = np.asarray(pk["box"], float)
49 Rc = N / box[0]
50 c = np.asarray(pk["centers"]) * Rc + SHIFT # sub-cell translation (periodic; incidence probe)
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):
55 cs = c + sh * N
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:
62 continue
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
67
68
69sdf, R = bed_sdf(N, BED)
70sv = flow.Solver if KIND == "stag" else flow.SolverColocated
71s = sv(N, N, N)
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"])) # 2 = marching-squares apertures
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)
79if KIND != "stag":
80 if KIND == "default":
81 pass # AUTO: whatever the shipped default resolves to
82 elif KIND == "ghost":
83 s.set_ghost_projection(True) # fluid-only constraint + directional closures (route 2)
84 elif KIND == "fluidonly":
85 s.set_fluid_only_constraint(1) # Design A: fluid-only openness filter + gauge-exact G
86 elif KIND.startswith("fluidonly2"):
87 s.set_fluid_only_constraint(2) # Design B: SPD Kron star elimination + gauge-exact G
88 if "_m" in KIND: # e.g. fluidonly2_m13: pair with another cell gradient
89 s.set_face_interp(int(KIND.split("_m")[1]))
90 elif KIND.startswith("mode"):
91 s.set_face_interp(int(KIND[4:])) # numbered ablations (e.g. mode3 = adjoint (T,T^T) pair)
92 elif hasattr(s, "set_collocated_scheme"):
93 s.set_collocated_scheme(KIND)
94 else:
95 s.set_face_interp({"gauge-exact": 9, "plain": 0}[KIND])
96if not ROT:
97 s.set_rotational_pressure(False)
98if ROTF:
99 s.set_rotational_filter(True)
100if ROTW != 1.0:
101 s.set_rotational_weight(ROTW)
102if WALLW > 0:
103 s.set_rotational_wall_weight(WALLW)
104s.set_solid(sdf, cutcell_pressure=True, pressure_coarse="rediscretized")
105fluid = sdf >= 0.0
106
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)
110pk = pm = None
111Pprev = None
112for it in range(1, STEPS + 1):
113 if it in DTSWITCH:
114 s.set_dt(DTSWITCH[it])
115 print(f"# --- dt -> {DTSWITCH[it]} at step {it} ---", flush=True)
116 s.step()
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
121 if KIND == "stag":
122 m1 = 0.0
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])
126 for a in range(3):
127 flx = OX[a] * U[a]
128 div += np.roll(flx, -1, axis=a) - flx
129 m2 = float(np.sqrt((div[fluid] ** 2).mean())) / us
130 else:
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
134 m1sq = cnt = 0.0
135 div = np.zeros_like(U[0])
136 for a in range(3):
137 half = 0.5 * (U[a] + np.roll(U[a], 1, axis=a))
138 op = OX[a] > 0
139 m1sq += float(((UF[a] - half)[op] ** 2).sum()); cnt += int(op.sum())
140 flx = OX[a] * half
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}"
148 Pprev = Pf.copy()
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)
151 pk, pm = kc, m1