flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
anderson_rev1_probes.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Probes behind revision 1 of doc/steady_acceleration.md (an INSTRUMENT, not a gate).
3
4Three subcommands, each on the oracle's cases (tests/study/anderson_oracle.py builds the solvers):
5
6 slowmode plain march on a case, per-step residual split (velocity / c_P-weighted pressure) and
7 the full residual field at chosen steps; then the structure of the late residual:
8 decay rates, alignment, energy shares, the fluid components of the face-aperture graph
9 (sealed pockets) and the share of the pressure residual that sits off the main
10 component at aperture thresholds 0 ... 0.2. (note §1.2)
11 phase-a phase A run unconditionally (acc.step(True) every call, no handover) with the monitor
12 read after every map output: steps until the monitor stays within 1e-5 / 3e-5 of the
13 reference and until the velocity / W residual first reaches 3e-7. ("Revision 1" R1)
14 certify the full revision-1 driver with the monitor recorded after every step, then the
15 certification blocks replayed: d, R and the remainder bound per block. (§7, R4)
16
17Run from the flow tree with the OpenMP pool bounded, e.g.
18
19 OMP_NUM_THREADS=8 OMP_PROC_BIND=false PYTHONPATH=$PWD/build_omp \\
20 python tests/study/anderson_rev1_probes.py phase-a --case sphere --scheme collocated --N 16 \\
21 --window 5 --metric V --steps 300 --out e4.npz
22
23The dense bed needs `--case bed --arrangement <npz>` (centres, radii, L; see the log for how it is
24generated from the A1 scripts).
25"""
26import argparse
27import math
28import os
29import sys
30
31import numpy as np
32
33sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
34import anderson_oracle as AO # noqa: E402
35
36
37def build_case(flow, a, settings="production"):
38 """The oracle's sphere / bed cases (anderson_oracle.main's geometry), returning (solver,
39 oracle kwargs, settings, K-of-monitor)."""
40 st = AO.settings_of(settings)
41 if a.case == "sphere":
42 L, phi = 1.0, a.phi
43 R = (3.0 * phi / (4.0 * math.pi)) ** (1.0 / 3.0) * L
44
45 def sdf_fn(cx, cy, cz):
46 X, Y, Z = np.meshgrid(cx, cy, cz, indexing="ij")
47 return np.sqrt((X - 0.5) ** 2 + (Y - 0.5) ** 2 + (Z - 0.5) ** 2) - R
48
49 def K(u):
50 return L ** 3 / (6.0 * math.pi * a.mu * R * u)
51 else:
52 from scipy.spatial import cKDTree
53 z = np.load(a.arrangement)
54 cen, rad, L = np.asarray(z["centres"], float), np.asarray(z["radii"], float), float(z["L"])
55
56 def sdf_fn(cx, cy, cz):
57 X, Y, Z = np.meshgrid(cx, cy, cz, indexing="ij")
58 pts = np.stack([X.ravel(order="C"), Y.ravel(order="C"), Z.ravel(order="C")], axis=1)
59 d, idx = cKDTree(np.mod(cen, L), boxsize=L).query(np.mod(pts, L), k=min(4, len(cen)))
60 return (d - rad[idx]).min(axis=1).reshape(X.shape, order="C")
61
62 def K(u):
63 return L ** 3 / (3.0 * math.pi * a.mu * 2.0 * float(rad.mean()) * u * len(rad))
64 h = L / a.N
65 dt = a.dt if a.dt is not None else a.beta * h * h / a.mu
66 s, okw = AO.build_solver(flow, a.scheme, a.N, sdf_fn, L, rho=1.0, mu=a.mu, F=1.0, dt=dt,
67 advection=a.advection, settings=st)
68 return s, okw, st, K
69
70
71def inner(v, G=2):
72 return np.array(v[G:-G, G:-G, G:-G])
73
74
75def cmd_slowmode(flow, a):
76 s, okw, _, K = build_case(flow, a)
77 names = ["u", "v", "w", "p"]
78 views = [s.diagnostics.field_view(n) for n in names]
79 fluid = inner(s.diagnostics.field_view("sdf")) > 0.0
80 cP = okw["c_p"]
81 save_at = {int(k) for k in a.save.split(",")} if a.save else set()
82 x = [inner(v) for v in views]
83 rows, saved = [], {}
84 for k in range(1, a.steps + 1):
85 s.step()
86 y = [inner(v) for v in views]
87 r = [y[f] - x[f] for f in range(4)]
88 rp = r[3][fluid] - r[3][fluid].mean()
89 rows.append((k, *[float(np.sum(r[f] ** 2)) for f in range(3)], cP * cP * float(np.sum(rp ** 2)),
90 float(sum(np.sum(y[f] ** 2) for f in range(3))), float(s.get_u().mean())))
91 if k in save_at:
92 saved[f"r{k}"] = np.stack(r)
93 x = y
94 rows = np.array(rows)
95 W = np.sqrt(rows[:, 1:5].sum(1) / rows[:, 5])
96 V = np.sqrt(rows[:, 1:4].sum(1) / rows[:, 5])
97 P = np.sqrt(rows[:, 4] / rows[:, 5])
98 ref = a.ref if a.ref else rows[-1, 6]
99 print(f"reference monitor {ref:.12e} (K {K(ref):.8f})")
100 marks = [k for k in (1, 10, 50, 100, 150, 200, 250, 300, 325, 400, 500, 600, 800, 1000, 1200,
101 1500, 2000) if k <= a.steps]
102 for k in marks:
103 print(f"step {k:5d} W {W[k-1]:.3e} vel {V[k-1]:.3e} pres {P[k-1]:.3e} "
104 f"monitor err {abs(rows[k-1, 6] / ref - 1):.1e}")
105 for k0, k1 in zip(marks[:-1], marks[1:]):
106 if k0 >= 50:
107 rt = lambda q: (q[k1 - 1] / q[k0 - 1]) ** (1.0 / (k1 - k0)) # noqa: E731
108 print(f"rate {k0:4d}-{k1:4d}: W {rt(W):.6f} vel {rt(V):.6f} pres {rt(P):.6f}")
109 # structure: fluid components of the aperture graph. get_ox[i] is the -x face of cell i, i.e.
110 # the face between cells i-1 and i (the binding's docstring); rolled by -1 so that opn[ax][i]
111 # is the face between cells i and i+1. (Corrected 2026-10-03: until then this read get_ox[i]
112 # itself as the face (i, i+1), one cell off, which turned 205 sealed pockets / 217 cells into
113 # 1,085 / 1,168 on the A1 bed -- tests/study/pocket_pressure_probe.py checks the convention.)
114 from scipy.sparse import coo_matrix
115 from scipy.sparse.csgraph import connected_components
116 n = fluid.shape
117 idx = np.arange(fluid.size).reshape(n, order="F")
118 opn = [np.roll(np.asarray(o).reshape(n, order="F"), -1, axis=ax)
119 for ax, o in enumerate((s.get_ox(), s.get_oy(), s.get_oz()))]
120 mods = sorted(saved)
121 # the cut-cell operator's own graph at aperture 0: every open face couples its two cells,
122 # whatever their sdf sign, so a fluid cell joined to the main space only through a solid-
123 # CENTRED cut cell is not sealed. 'operator' below = fluid cells outside its main component.
124 # Its apertures are the PROJECTION's (get_ox_proj: binary under the collocated ghost scheme,
125 # identical to get_ox on the staggered grid).
126 opp = [np.roll(np.asarray(o).reshape(n, order="F"), -1, axis=ax)
127 for ax, o in enumerate((s.get_ox_proj(), s.get_oy_proj(), s.get_oz_proj()))]
128 node = fluid.copy()
129 for ax in range(3):
130 o = opp[ax] > 0.0
131 node |= o | np.roll(o, 1, axis=ax)
132 for th, nodes in ((0.0, node), (0.0, fluid), (1e-2, fluid), (5e-2, fluid), (0.1, fluid),
133 (0.2, fluid)):
134 Ii, Jj = [], []
135 for ax in range(3):
136 o = opp[ax] if nodes is node else opn[ax]
137 m = (o > th) & nodes & np.roll(nodes, -1, axis=ax)
138 Ii.append(idx[m])
139 Jj.append(np.roll(idx, -1, axis=ax)[m])
140 Ii, Jj = np.concatenate(Ii), np.concatenate(Jj)
141 A = coo_matrix((np.ones(Ii.size), (Ii, Jj)), shape=(fluid.size, fluid.size))
142 _, lab = connected_components(A, directed=False)
143 fl = lab.reshape(n, order="F")[fluid]
144 sizes = np.bincount(fl)
145 main = int(np.argmax(sizes))
146 line = []
147 for key in mods:
148 v = saved[key][3][fluid]
149 v = v - v.mean()
150 line.append(f"{key}: off-main {float(np.sum(v[fl != main] ** 2) / np.sum(v ** 2)):.3f}")
151 tag = "operator graph, " if nodes is node else ""
152 print(f"{tag}aperture > {th:g}: {len(np.unique(fl))} fluid components, main {sizes[main]} "
153 f"of {fl.size} fluid cells ({fl.size - sizes[main]} off main); " + "; ".join(line))
154 for key in mods:
155 r = saved[key]
156 rp = r[3][fluid] - r[3][fluid].mean()
157 ev = float(sum(np.sum(r[f] ** 2) for f in range(3)))
158 ep = cP * cP * float(np.sum(rp ** 2))
159 e = np.sort(rp ** 2)[::-1]
160 n90 = int(np.searchsorted(np.cumsum(e) / e.sum(), 0.9)) + 1
161 print(f"{key}: pressure share of W^2 {ep / (ev + ep):.6f}; 90% of the pressure energy in "
162 f"{n90} of {rp.size} fluid cells")
163
164
165def cmd_phase_a(flow, a):
166 s, okw, _, K = build_case(flow, a)
167 okw = dict(okw, metric=a.metric, ritz_scope="mixed")
168 acc = AO.AndersonOracle(s, window=a.window, **okw)
169 rows = []
170 for k in range(1, a.steps + 1):
171 acc.step(True)
172 if acc.status != "active":
173 print(f"status {acc.status} at step {k}: {acc.reason}")
174 break
175 R = acc.Rprev
176 vel = sum(float(np.dot(R[f, acc.inner], R[f, acc.inner])) for f in acc.vel)
177 rp = R[3, acc.fluid] - R[3, acc.fluid].mean()
178 pres = okw["c_p"] ** 2 * float(np.dot(rp, rp))
179 U2 = acc.usq(acc.Gprev)
180 rows.append((k, math.sqrt((vel + pres) / U2), math.sqrt(vel / U2), float(s.get_u().mean()),
181 acc.ritzRadius))
182 rows = np.array(rows)
183 if a.out:
184 np.savez(a.out, rows=rows)
185 ref = a.ref if a.ref else rows[-1, 3]
186 err = np.abs(rows[:, 3] / ref - 1)
187
188 def stays(tol):
189 bad = np.where(err > tol)[0]
190 return bad[-1] + 2 if bad.size else 1
191
192 def first(col, tol):
193 i = np.where(rows[:, col] <= tol)[0]
194 return int(i[0]) + 1 if i.size else None
195 print(f"{a.case} {a.scheme} N={a.N} m={a.window} metric={a.metric}: reference {ref:.12e}; "
196 f"monitor within 3e-5 from step {stays(3e-5)}, within 1e-5 from {stays(1e-5)}; velocity "
197 f"residual <= 3e-7 at {first(2, 3e-7)}; W residual <= 3e-7 at {first(1, 3e-7)}")
198
199
200def cmd_certify(flow, a):
201 s, okw, st, K = build_case(flow, a)
202 okw = dict(okw, metric=a.metric, ritz_scope="mixed")
203 mons, holder = [], {}
204
205 def cb(steps, phase):
206 mons.append((steps, phase, float(s.get_u().mean())))
207 res = AO.march_to_steady(s, lambda: float(s.get_u().mean()), rtol=st["rtol"],
208 max_steps=st["max_steps"], window=a.window, callback=cb, oracle_kw=okw,
209 holder=holder, budget=a.budget, rev=1)
210 print(f"{a.case} {a.scheme} N={a.N} m={a.window}: converged={res['converged']} "
211 f"{res['reason']} steps={res['steps']} accelerated={res['accelerated_steps']} "
212 f"K={K(res['monitor']):.10f}")
213 runs = []
214 for k, ph, m in mons:
215 if not runs or runs[-1][0] != ph:
216 runs.append((ph, []))
217 runs[-1][1].append((k, m))
218 for ph, rows in runs:
219 if ph == "accelerate":
220 print(f" accelerate {rows[0][0]}-{rows[-1][0]}")
221 continue
222 out, prev, dprev = [], None, None
223 for k, m in [rows[i] for i in range(4, len(rows), 5)]: # the instrument's samples
224 if prev is not None:
225 d = m - prev
226 if dprev:
227 R = d / dprev
228 bound = abs(d) / (1 - max(R, 0.997 ** 5)) if R < 1 else math.inf
229 out.append(f"[{k}: d/|m| {d / abs(m):+.1e} R {R:+.3f} bound/|m| "
230 f"{bound / abs(m):.1e}]")
231 else:
232 out.append(f"[{k}: d/|m| {d / abs(m):+.1e}]")
233 dprev = d
234 prev = m
235 print(f" {ph} {rows[0][0]}-{rows[-1][0]}: " + " ".join(out))
236
237
238def main():
239 sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "..",
240 "scripts"))
241 from _bootstrap import ensure_flow
242 flow = ensure_flow()
243 ap = argparse.ArgumentParser()
244 ap.add_argument("command", choices=("slowmode", "phase-a", "certify"))
245 ap.add_argument("--case", choices=("sphere", "bed"), default="sphere")
246 ap.add_argument("--scheme", choices=("staggered", "collocated"), required=True)
247 ap.add_argument("--N", type=int, default=16)
248 ap.add_argument("--phi", type=float, default=0.125)
249 ap.add_argument("--mu", type=float, default=1.0)
250 ap.add_argument("--beta", type=float, default=6.0)
251 ap.add_argument("--dt", type=float, default=None)
252 ap.add_argument("--advection", default=None)
253 ap.add_argument("--arrangement", default=None)
254 ap.add_argument("--window", type=int, default=5)
255 ap.add_argument("--metric", choices=("V", "W"), default="V")
256 ap.add_argument("--budget", type=int, default=None)
257 ap.add_argument("--steps", type=int, default=300)
258 ap.add_argument("--save", default="", help="slowmode: steps whose residual field is kept")
259 ap.add_argument("--ref", type=float, default=None, help="reference monitor value")
260 ap.add_argument("--out", default=None)
261 a = ap.parse_args()
262 {"slowmode": cmd_slowmode, "phase-a": cmd_phase_a, "certify": cmd_certify}[a.command](flow, a)
263
264
265if __name__ == "__main__":
266 main()
build_case(flow, a, settings="production")
static double mean(const std::vector< double > &a)