flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
collocated_checkerboard_tail.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""The slow (pi,0,0) pressure-checkerboard tail of the collocated steady march (2026-10-01).
3
4Instrument behind doc/collocated_invisible_subspace.md ยง11. It marches the case of
5study_avg_velocity_spheres.py (one sphere per periodic unit cell, phi = 0.125, centred on a grid
6vertex, rho = mu = f = 1, nu dt / h^2 = beta, MG-PCG rtol 1e-8, velocity cap 200) and records per
7step <u_x>, rms(du) and the rms of the fluid pressure increment, plus the last four iterates, so
8that the asymptotic contraction rate and the spatial structure of the slow component can be read.
9
10 geom N1,N2,.. fluid-centre / crossing-fraction census of the sphere per N
11 run TAG N beta stag|col auto|ghost|gauge-exact STEPS [shift_h] [opts]
12 march STEPS steps; shift_h moves the centre by (s,s,s) h;
13 opts (comma list): norot (PM I ablation), prtol12 (rtol 1e-12)
14 -> $PROBE_OUT/TAG.npz (default ./probe_out)
15 analyze TAG.. tail rate per step, its amplitude, the extrapolated limit
16 structure TAG.. where the slow component lives: wall band, Fourier content
17 stops TAG.. replay the history through the study's old (5bcf353) and
18 current stop tests: step and K bias against the limit
19
20Run (from flow/, pool bounded): OMP_NUM_THREADS=4 OMP_PROC_BIND=false PYTHONPATH=<flow build>
21python tests/study/collocated_checkerboard_tail.py run col16 16 6 col auto 800
22"""
23import math
24import os
25import sys
26
27import numpy as np
28
29L, RHO, MU, F, PHI = 1.0, 1.0, 1.0, 1.0, 0.125
30OUT = os.environ.get("PROBE_OUT", "probe_out")
31HERE = os.path.dirname(os.path.abspath(__file__))
32
33
34def radius():
35 return (3.0 * PHI / (4.0 * math.pi)) ** (1.0 / 3.0) * L
36
37
38def sdf_on(cx, cy, cz, shift_h, h):
39 X, Y, Z = np.meshgrid(cx, cy, cz, indexing="ij")
40 c = 0.5 * L + shift_h * h
41 return np.sqrt((X - c) ** 2 + (Y - c) ** 2 + (Z - c) ** 2) - radius()
42
43
44def old_stop(m, tol=1e-6, every=5):
45 """The stop of study_avg_velocity_spheres.py at 5bcf353: |m_n - m_{n-5}| < tol |m_n|."""
46 prev = 0.0
47 for it in range(len(m)):
48 if it % every == every - 1:
49 if it > 10 and abs(m[it] - prev) < tol * (abs(m[it]) + 1e-30):
50 return it + 1
51 prev = m[it]
52 return -1
53
54
55def new_stop(m):
56 """The study's current march() replayed on a recorded history (-1 if it never stops)."""
57 sys.path.insert(0, HERE)
58 import study_avg_velocity_spheres as st
59 k = {"i": -1}
60
61 def step():
62 k["i"] += 1
63 if k["i"] >= len(m):
64 raise IndexError
65
66 try:
67 n, ok = st.march(step, lambda: float(m[k["i"]]))
68 except IndexError:
69 return -1
70 return n if ok else -1
71
72
73def geom(Ns):
74 R = radius()
75 for N in Ns:
76 h = L / N
77 c = (np.arange(N) + 0.5) * h
78 s = sdf_on(c, c, c, 0.0, h) / h
79 th = []
80 for ax in range(3):
81 a, b = s, np.roll(s, -1, axis=ax)
82 m1, m2 = (a > 0) & (b < 0), (a < 0) & (b > 0)
83 th += [a[m1] / (a[m1] - b[m1]), b[m2] / (b[m2] - a[m2])]
84 th = np.concatenate(th)
85 print(f"N={N:3d} R/h={R / h:7.4f} min fluid sdf/h={s[s > 0].min():.4f} "
86 f"#fluid<0.05h={np.sum((s > 0) & (s < 0.05)):3d} min theta={th.min():.4f} "
87 f"#theta<0.05={np.sum(th < 0.05)}")
88
89
90def run(tag, N, beta, solver, scheme, steps, shift=0.0, opts=""):
91 sys.path.insert(0, os.path.join(HERE, "..", "..", "scripts"))
92 from _bootstrap import ensure_flow
93 flow = ensure_flow()
94 opts = set(o for o in opts.split(",") if o)
95 s = (flow.Solver if solver == "stag" else flow.SolverColocated)((N, N, N), extent=(L, L, L))
96 h = L / N
97 s.set_rho(RHO); s.set_mu(MU); s.set_dt(beta * RHO * h * h / MU)
98 s.set_body_force((F, 0.0, 0.0)); s.set_advection(False)
99 s.diagnostics.set_velocity_solver_params(200)
100 s.set_pressure_multigrid(True, levels=max(2, int(np.log2(N)) - 1))
101 s.set_pressure_pcg(True, max_iter=200, rtol=1e-12 if "prtol12" in opts else 1e-8)
102 if solver == "col" and scheme != "auto":
103 s.set_collocated_scheme(scheme)
104 if "norot" in opts:
105 s.diagnostics.set_rotational_pressure(False)
106 cx, cy, cz = s.cell_centers()
107 sdf = sdf_on(cx, cy, cz, shift, h)
108 s.set_solid(np.asfortranarray(sdf), cutcell_pressure=True)
109
110 m, du, dp = np.zeros(steps), np.zeros(steps), np.zeros(steps)
111 pit, mres = np.zeros(steps, int), np.zeros(steps)
112 keep, prevU, prevP, fluid = {}, None, None, sdf > 0
113 for it in range(steps):
114 s.step()
115 U = np.stack([s.get_u(), s.get_v(), s.get_w()])
116 P = s.get_p().copy()
117 m[it] = float(U[0].mean())
118 pit[it] = s.diagnostics.last_pressure_iterations()
119 mres[it] = s.diagnostics.last_momentum_residual()
120 if prevU is not None:
121 du[it] = np.sqrt(np.mean((U - prevU) ** 2))
122 d = (P - prevP)[fluid]
123 dp[it] = np.sqrt(np.mean((d - d.mean()) ** 2))
124 prevU, prevP = U, P
125 keep[it] = (U, P)
126 keep.pop(it - 4, None)
127 its = sorted(keep)
128 os.makedirs(OUT, exist_ok=True)
129 np.savez_compressed(os.path.join(OUT, tag + ".npz"), m=m, du=du, dp=dp, pit=pit, mres=mres,
130 sdf=sdf / h, U=np.stack([keep[i][0] for i in its]),
131 P=np.stack([keep[i][1] for i in its]), N=N, beta=beta)
132 print(f"[{tag}] N={N} beta={beta} {solver}/{scheme} shift={shift} opts={sorted(opts)} "
133 f"old_stop={old_stop(m)} vmg={bool(s.diagnostics.velocity_multigrid_active())} "
134 f"p-iters(last)={pit[-1]} mom-res(last)={mres[-1]:.2e} m_final={m[-1]:.10e}", flush=True)
135
136
137def tail(m, floor=1e-11):
138 """(per-step rate, window, back-projected amplitude, geometric-extrapolated limit)."""
139 rel = np.abs(np.diff(m)) / abs(m[-1])
140 ok = np.where(rel > floor)[0]
141 hi = ok[-1] + 1 if len(ok) else len(m) - 1
142 lo = max(5, hi // 2)
143 r = float(np.exp((np.log(rel[hi - 1]) - np.log(rel[lo])) / (hi - 1 - lo)))
144 return r, (lo, hi), rel[lo] / r ** lo, m[hi] + (m[hi] - m[hi - 1]) * r / (1.0 - r)
145
146
147def analyze(tag):
148 z = np.load(os.path.join(OUT, tag + ".npz"))
149 m, du, dp, beta = z["m"], z["du"], z["dp"], float(z["beta"])
150 r, (lo, hi), A0, m_inf = tail(m)
151 print(f"[{tag}] N={int(z['N'])} beta={beta} tail rate {r:.5f}/step over [{lo},{hi}) "
152 f"(as a backward-Euler mode lambda L^2 = {(1 / r - 1) / beta * int(z['N']) ** 2:.2f}); "
153 f"|dm|/m amplitude at step 0 {A0:.2e}; m_inf = {m_inf:.10e}")
154 rel = np.abs(np.diff(m)) / abs(m[-1])
155 print(" step |dm|/|m| rms du rms dP(fluid)")
156 for k in [0, 4, 9, 19, 29, 49, 69, 99, 149, 199, 299, 399, 599, 799, 1199]:
157 if k < len(m) - 1:
158 print(f" {k + 1:4d} {rel[k]:.3e} {du[k + 1]:.3e} {dp[k + 1]:.3e}")
159
160
161def structure(tag):
162 """Where the last step's increment lives: wall band and Fourier content."""
163 z = np.load(os.path.join(OUT, tag + ".npz"))
164 U, P, sdf = z["U"], z["P"], z["sdf"]
165 N, fluid = sdf.shape[0], sdf > 0
166 d = U[-1] - U[-2]
167 dP = P[-1] - P[-2]
168 dP = np.where(fluid, dP - dP[fluid].mean(), 0.0)
169 e = (d ** 2).sum(axis=0)
170 print(f" [{tag}] last-step increment:")
171 for lo, hi, name in [(-1e9, 0, "solid"), (0, 1, "0<sdf<h"), (1, 2, "h<sdf<2h"),
172 (2, 1e9, "sdf>2h")]:
173 msk = (sdf > lo) & (sdf <= hi)
174 print(f" {name:9s} {100 * e[msk].sum() / e.sum():5.1f}% of |du|^2, "
175 f"{100 * (dP[msk] ** 2).sum() / (dP ** 2).sum():5.1f}% of |dP|^2 "
176 f"({100 * msk.mean():4.1f}% of cells)")
177 h2 = N // 2
178 for name, f in (("du_x", d[0]), ("dP", dP), ("P", np.where(fluid, P[-1] - P[-1][fluid].mean(), 0))):
179 E = np.abs(np.fft.fftn(f)) ** 2
180 E /= E.sum()
181 cb = {"(pi,0,0)": E[h2, 0, 0], "(0,pi,0)": E[0, h2, 0], "(0,0,pi)": E[0, 0, h2],
182 "(pi,pi,pi)": E[h2, h2, h2]}
183 print(f" {name:5s} energy fraction " + " ".join(f"{k} {100 * v:5.1f}%" for k, v in cb.items()))
184
185
186def stops(tag):
187 z = np.load(os.path.join(OUT, tag + ".npz"))
188 m = z["m"]
189 m_inf = tail(m)[3]
190 f = [old_stop(m), new_stop(m)]
191 g = ["%5d (K bias %+.1e)" % (n, m_inf / m[n - 1] - 1) if n > 0 else " never" for n in f]
192 print(f"[{tag}] old stop {g[0]} current stop {g[1]}")
193
194
195if __name__ == "__main__":
196 cmd, a = sys.argv[1], sys.argv[2:]
197 if cmd == "geom":
198 geom([int(x) for x in a[0].split(",")])
199 elif cmd == "run":
200 run(a[0], int(a[1]), float(a[2]), a[3], a[4], int(a[5]),
201 float(a[6]) if len(a) > 6 else 0.0, a[7] if len(a) > 7 else "")
202 else:
203 for t in a:
204 {"analyze": analyze, "structure": structure, "stops": stops}[cmd](t)
run(tag, N, beta, solver, scheme, steps, shift=0.0, opts="")
static double mean(const std::vector< double > &a)