flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
steady_acceleration_gates.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Gate instrument for the steady-march Anderson acceleration (doc/steady_acceleration.md, rev 2,
3work orders WO-4, WO-5, WO-7 and WO-8). An INSTRUMENT, not a ctest: it prints one summary line per run and,
4with --json, appends one JSON record per run. The numbers go to doc/steady_acceleration_log.md.
5
6Subcommands (all on the module on PYTHONPATH; bound the OpenMP pool):
7
8 oracle WO-4: the C++ accelerator against the NumPy oracle (tests/study/anderson_oracle.py,
9 rev 2) — the per-step relative velocity residual of unconditional step(True) calls on
10 two identical solvers, max relative difference over the first --steps (30) steps.
11 run G1 / G2 / G6 / G7: march_to_steady per window (0 = accelerate=False), recording steps,
12 accelerated steps, converged / reason, K, wall time, pressure iterations per step by
13 phase (mixed vs plain), accelerator seconds, memory; with window 0 among the windows,
14 the G7c line per case: plain converged vs accelerated converged per window. The tight
15 staggered bed also prints the G1 bed line: |K_acc/K_inf - 1| <= |K_plain/K_inf - 1| +
16 1e-8, K_plain from window 0 of the same run or --k-plain (the logged certificate).
17 g3 G3: --steps (400) unconditional acc.step(True) per dt (--betas), tight; status,
18 restarts, running-min residual, final residual and K. With --extend-to N (WO-8): past
19 --steps, keep stepping until the running-min residual reaches the depth bar (sphere
20 1e-9) or N steps in total; every criterion is evaluated at the end of the run. The bed
21 has no depth bar: run it with --steps 3000 (a fixed 3000 calls per dt). K is sampled
22 after the last call that did not restart (review R3).
23 g5 G5: interrupt an accelerated march at step --at (25), checkpoint get_field u,v,w,p (or
24 set_state, velocity only), restore into a fresh solver, march again; against the
25 uninterrupted accelerated march.
26 g8 G8: accelerator overhead (acc.seconds over --steps accelerated steps) against the mean
27 plain step (last_step_timers()['step'], after --warmup plain steps, so both are timed
28 late in the march), and memory_bytes against the §6.3 formula.
29 mpi G4: tight march_to_steady under mpirun (flow.mpi_block + init_mpi), global <u_x>
30 monitor; and --hash: the state hash after --steps accelerated steps (np = 1 against a
31 serial build).
32
33Cases (as the oracle builds them):
34 sphere §11 of doc/collocated_invisible_subspace.md: one sphere at the centre of the periodic unit
35 cube, phi (0.125), rho = F = 1, mu (1), nu dt / h^2 = beta (6) unless --dt; Stokes unless
36 --advection. K = F L^3 / (6 pi mu R <u_x>) (Zick & Homsy normalisation). The Z&H simple
37 cubic array is the same case at --phi 0.343 / 0.45.
38 bed a random arrangement (.npz: centres, radii, L), A1's K = F L^3 / (3 pi mu D <u_x> N_p).
39 --force sets the body force (finite Re), --implicit-advection A1's implicit advection.
40
41Settings: production = PCG(200, 1e-8), 200 velocity sweeps, rtol 1e-4, max_steps 5000; tight =
42PCG(400, 1e-12), velocity residual tolerance 1e-12, rtol 1e-10, max_steps 20000.
43"""
44import argparse
45import hashlib
46import json
47import math
48import os
49import sys
50import time
51
52import numpy as np
53
54HERE = os.path.dirname(os.path.abspath(__file__))
55
56
57def settings_of(name):
58 if name == "production":
59 return dict(pcg=(200, 1e-8), vel_rtol=None, rtol=1e-4, max_steps=5000)
60 if name == "tight":
61 return dict(pcg=(400, 1e-12), vel_rtol=1e-12, rtol=1e-10, max_steps=20000)
62 raise ValueError(name)
63
64
65# ---------------------------------------------------------------------------------------------- cases
66class Case:
67 """Geometry + physics of one case; build() returns a configured solver (global, single rank)."""
68
69 def __init__(self, a):
70 self.a = a
71 self.rho = 1.0
72 self.mu = a.mu
73 if a.case == "sphere":
74 self.L = 1.0
75 self.R = (3.0 * a.phi / (4.0 * math.pi)) ** (1.0 / 3.0) * self.L
76 self.F = 1.0 if a.force is None else a.force
77 else:
78 z = np.load(a.arrangement)
79 self.cen = np.asarray(z["centres"], float)
80 self.rad = np.asarray(z["radii"], float)
81 self.L = float(z["L"])
82 self.Dm = 2.0 * float(self.rad.mean())
83 self.npart = len(self.rad)
84 self.F = 1.0 if a.force is None else a.force
85 self.N = a.N
86 self.h = self.L / self.N
87
88 def dt(self, beta=None):
89 if self.a.dt is not None and beta is None:
90 return self.a.dt
91 b = self.a.beta if beta is None else beta
92 return b * self.rho * self.h * self.h / self.mu
93
94 def sdf_global(self, cx, cy, cz):
95 X, Y, Z = np.meshgrid(cx, cy, cz, indexing="ij")
96 if self.a.case == "sphere":
97 c = 0.5 * self.L
98 return np.sqrt((X - c) ** 2 + (Y - c) ** 2 + (Z - c) ** 2) - self.R
99 from scipy.spatial import cKDTree
100 pts = np.stack([X.ravel(order="C"), Y.ravel(order="C"), Z.ravel(order="C")], axis=1)
101 tree = cKDTree(np.mod(self.cen, self.L), boxsize=self.L)
102 k = min(4, len(self.cen))
103 d, idx = tree.query(np.mod(pts, self.L), k=k)
104 return (d - self.rad[idx]).min(axis=1).reshape(X.shape, order="C")
105
106 def K(self, u):
107 if not (math.isfinite(u) and u != 0.0):
108 return math.nan
109 if self.a.case == "sphere":
110 return self.F * self.L ** 3 / (6.0 * math.pi * self.mu * self.R * u)
111 return self.F * self.L ** 3 / (3.0 * math.pi * self.mu * self.Dm * u * self.npart)
112
113 def configure(self, s, settings, beta=None):
114 a = self.a
115 s.set_rho(self.rho)
116 s.set_mu(self.mu)
117 s.set_dt(self.dt(beta))
118 s.set_body_force((self.F, 0.0, 0.0))
119 if a.advection:
120 s.set_advection(True)
121 if a.implicit_advection:
122 s.set_implicit_advection(True)
123 s.set_advection_scheme(a.advection)
124 else:
125 s.set_advection(False)
126 s.diagnostics.set_velocity_solver_params(200)
127 s.set_pressure_multigrid(True, levels=max(2, int(np.log2(self.N)) - 1))
128 s.set_pressure_pcg(True, max_iter=settings["pcg"][0], rtol=settings["pcg"][1])
129 if settings["vel_rtol"] is not None:
130 s.set_velocity_residual_tolerance(settings["vel_rtol"])
131
132 def build(self, flow, scheme, settings, beta=None):
133 Cls = flow.Solver if scheme == "staggered" else flow.SolverColocated
134 N, L = self.N, self.L
135 s = Cls((N, N, N), extent=(L, L, L))
136 self.configure(s, settings, beta)
137 cx, cy, cz = s.cell_centers()
138 s.set_solid(np.asfortranarray(self.sdf_global(cx, cy, cz)), cutcell_pressure=True)
139 return s
140
141
142class Proxy:
143 """The solver as march_to_steady sees it, recording the accelerator the driver creates."""
144
145 def __init__(self, s, holder):
146 self.step = s.step
147 self.diagnostics = _DiagProxy(s.diagnostics, holder)
148
149
151 def __init__(self, d, holder):
152 self._d = d
153 self._h = holder
154
155 def anderson_accelerator(self, **kw):
156 acc = self._d.anderson_accelerator(**kw)
157 self._h["acc"] = acc
158 return acc
159
160
161def umean(s):
162 return float(s.get_u().mean())
163
164
165def emit(a, rec):
166 if a.json:
167 with open(a.json, "a") as f:
168 f.write(json.dumps(rec) + "\n")
169
170
171def common_rec(a, case):
172 return dict(label=a.label, case=a.case, scheme=a.scheme, N=a.N, phi=a.phi, mu=a.mu,
173 dt=case.dt(), F=case.F, advection=a.advection, settings=a.settings,
174 threads=os.environ.get("OMP_NUM_THREADS"))
175
176
177# ---------------------------------------------------------------------------------------------- run
178def cmd_run(flow, a):
179 st = settings_of(a.settings)
180 if a.max_steps:
181 st["max_steps"] = a.max_steps
182 case = Case(a)
183 conv, Ks = {}, {}
184 for w in a.window:
185 s = case.build(flow, a.scheme, st)
186 holder = {}
187 trace = []
188
189 def cb(steps, phase, s=s, holder=holder, trace=trace):
190 acc = holder.get("acc")
191 trace.append((phase, int(s.diagnostics.last_pressure_iterations()),
192 acc.residual if acc is not None else None,
193 acc.status if acc is not None else None))
194
195 t0 = time.perf_counter()
196 res = flow.march_to_steady(Proxy(s, holder), lambda s=s: umean(s), rtol=st["rtol"],
197 max_steps=st["max_steps"], accelerate=(w > 0),
198 window=max(w, 1), callback=cb)
199 wall = time.perf_counter() - t0
200 acc = holder.get("acc")
201 piters = {}
202 for ph in ("accelerate", "certify", "plain"):
203 its = [t[1] for t in trace if t[0] == ph]
204 piters[ph] = (sum(its), len(its))
205 phases = []
206 for t in trace:
207 if phases and phases[-1][0] == t[0]:
208 phases[-1][1] += 1
209 else:
210 phases.append([t[0], 1])
211 rec = common_rec(a, case)
212 rec.update(window=w, converged=res.converged, reason=res.reason, steps=res.steps,
213 accelerated_steps=res.accelerated_steps, num_restarts=res.num_restarts,
214 monitor=res.monitor, K=case.K(res.monitor), wall_s=wall,
215 pressure_iterations=sum(v[0] for v in piters.values()),
216 piters_by_phase=piters,
217 status=acc.status if acc else None, acc_reason=acc.reason if acc else None,
218 num_resets=acc.num_resets if acc else None,
219 acc_seconds=acc.seconds if acc else None,
220 memory_bytes=acc.memory_bytes if acc else None,
221 phases="".join(f"{p[0][0]}{p[1]} " for p in phases).strip())
222 if a.trace:
223 rec["trace"] = trace
224 pi = {k: (f"{v[0] / v[1]:.2f}" if v[1] else "-") for k, v in piters.items()}
225 print(f"{a.label:>8s} {a.case} {a.scheme:10s} N={a.N} {a.settings:10s} m={w} "
226 f"conv={res.converged} {res.reason} steps={res.steps} acc={res.accelerated_steps} "
227 f"K={rec['K']:.10f} wall={wall:.2f}s piters={rec['pressure_iterations']} "
228 f"pit/step a/c/p={pi['accelerate']}/{pi['certify']}/{pi['plain']} "
229 f"rst={res.num_restarts} "
230 f"status={rec['status']} accsec={rec['acc_seconds']} [{rec['phases']}]",
231 flush=True)
232 emit(a, rec)
233 conv[w] = res.converged
234 Ks[w] = rec["K"]
235 del s, acc
236 holder.clear()
237 g1_bed(a, case, conv, Ks)
238 if 0 in conv and len(conv) > 1: # G7c (rev 2): plain converged vs accelerated converged
239 accw = {w: c for w, c in conv.items() if w > 0}
240 ok = (not conv[0]) or all(accw.values())
241 print(f"G7c {a.case} {a.scheme} N={a.N} {a.settings}: plain converged={conv[0]}; "
242 + " ".join(f"m={w} converged={c}" for w, c in accw.items())
243 + f" -> {'PASS' if ok else 'FAIL'}", flush=True)
244 rec = common_rec(a, case)
245 rec.update(gate="g7c", plain_converged=conv[0],
246 accelerated_converged={str(w): c for w, c in accw.items()}, ok=ok)
247 emit(a, rec)
248
249
250# G1 on the dense bed (orchestrator decision, 2026-10-03): at tight rtol the stop instrument
251# certifies early on this bed for BOTH paths (its tail is ~0.9999, the instrument assumes 0.997), so
252# the accelerated K is compared with K_inf, the extrapolation of the 60 000-step plain march (log
253# WO-8), allowing the plain march's own certificate error at the SAME dt:
254# pass iff |K_acc / K_inf - 1| <= |K_plain / K_inf - 1| + 1e-8 at both ends of K_inf.
255# K_plain is the plain tight certificate (window 0 in the same run, or --k-plain from the log).
256BED_K_INF = (98.9156994, 98.9156995)
257
258
259def g1_bed(a, case, conv, Ks):
260 if a.case != "bed" or a.scheme != "staggered" or a.settings != "tight":
261 return
262 kp = Ks.get(0, a.k_plain)
263 if kp is None or not (0 in conv or a.k_plain is not None):
264 return
265 # Extension (orchestrator 2026-10-03): when the plain tight march does not certify within
266 # max_steps, the accelerated K must be closer to K_inf than the plain K at max_steps.
267 uncert = (0 in conv and not conv[0]) or a.plain_uncertified
268 for w, K in Ks.items():
269 if w == 0:
270 continue
271 ends = [(abs(K / ki - 1.0), abs(kp / ki - 1.0)) for ki in BED_K_INF]
272 if uncert:
273 ok = conv[w] and all(ea < ep for ea, ep in ends)
274 rule = "acc closer than the uncertified plain K at max_steps"
275 else:
276 ok = conv[w] and all(ea <= ep + 1e-8 for ea, ep in ends)
277 rule = "acc <= plain + 1e-8"
278 print(f"G1 bed staggered dt={case.dt():.6g} m={w}: |K_acc/K_inf-1| "
279 f"{ends[0][0]:.2e}..{ends[1][0]:.2e}, |K_plain/K_inf-1| {ends[0][1]:.2e}.."
280 f"{ends[1][1]:.2e} (K_plain {kp!r}) -> {'PASS' if ok else 'FAIL'} ({rule})",
281 flush=True)
282 rec = common_rec(a, case)
283 rec.update(gate="g1_bed", window=w, K_acc=K, K_plain=kp, K_inf=list(BED_K_INF),
284 err_acc=[e[0] for e in ends], err_plain=[e[1] for e in ends], ok=ok)
285 emit(a, rec)
286
287
288# ---------------------------------------------------------------------------------------------- oracle
289def cmd_oracle(flow, a):
290 sys.path.insert(0, HERE)
291 import anderson_oracle as O
292 st = settings_of(a.settings)
293 case = Case(a)
294 m = a.window[0]
295 # C++
296 s1 = case.build(flow, a.scheme, st)
297 acc = s1.diagnostics.anderson_accelerator(window=m)
298 rc = []
299 for _ in range(a.steps):
300 acc.step(True)
301 rc.append(acc.residual)
302 # oracle (rev 2: velocity metric, no instability guard — the oracle's radius has no consequence)
303 s2 = case.build(flow, a.scheme, st)
304 us = s2.unit_scales
305 tau = st["vel_rtol"] if st["vel_rtol"] else st["pcg"][1]
306 okw = dict(signature=(case.dt() * us["time_to_internal"], case.rho * us["density_to_internal"],
307 case.mu * us["viscosity_to_internal"],
308 case.F * us["force_density_to_internal"][0], 0.0, 0.0),
309 c_p=1.0, gauged=True, collocated_advection=False, cells=(a.N,) * 3, metric="V",
310 ritz_scope="mixed", inner_tolerance=tau, ritz_action="none")
311 orc = O.AndersonOracle(s2, window=m, **okw)
312 ro = []
313 for _ in range(a.steps):
314 orc.step(True)
315 ro.append(orc.residual)
316 rel = [abs(x - y) / abs(y) if y != 0 else abs(x - y) for x, y in zip(rc, ro)]
317 worst = max(rel)
318 print(f"oracle {a.case} {a.scheme} N={a.N} m={m} {a.settings}: max rel diff of the residual "
319 f"over {a.steps} steps = {worst:.3e} (step {rel.index(worst) + 1}); C++ "
320 f"{rc[0]:.6e} .. {rc[-1]:.6e}, oracle {ro[0]:.6e} .. {ro[-1]:.6e}", flush=True)
321 for k, (x, y, r) in enumerate(zip(rc, ro, rel)):
322 if a.trace:
323 print(f" {k + 1:3d} {x:.15e} {y:.15e} {r:.2e}")
324 rec = common_rec(a, case)
325 rec.update(gate="oracle", window=m, steps=a.steps, max_rel=worst, cpp=rc, oracle=ro)
326 emit(a, rec)
327
328
329# ---------------------------------------------------------------------------------------------- g3
330# §8 G3: the running-min residual must reach this (sphere). The dense bed has no depth bar
331# (orchestrator decision 2026-10-03): it runs a fixed 3000 accelerated calls per dt (--steps 3000),
332# because no residual depth pins its K to 1e-8 at every dt (measured K-error / residual ratio
333# 37 - 830, log "Review fixes").
334G3_DEPTH = {"sphere": 1e-9, "bed": None}
335
336
337def cmd_g3(flow, a):
338 st = settings_of("tight")
339 case = Case(a)
340 depth = G3_DEPTH[a.case]
341 Ks = {}
342 for beta in a.betas:
343 s = case.build(flow, a.scheme, st, beta=beta)
344 acc = s.diagnostics.anderson_accelerator(window=a.window[0])
345 res, rmin, rst_steps = [], math.inf, []
346 u, u_call = math.nan, 0 # <u_x> sampled after the last NON-restart call (review R3)
347 t0 = time.perf_counter()
348 for k in range(max(a.steps, a.extend_to)):
349 if k >= a.steps and depth is not None and rmin <= depth:
350 break # --extend-to: the depth bar is reached
351 r0 = acc.num_restarts
352 acc.step(True)
353 if acc.num_restarts > r0:
354 rst_steps.append(k + 1)
355 else:
356 u, u_call = umean(s), k + 1
357 res.append(acc.residual)
358 rmin = min(rmin, acc.residual)
359 if acc.status != "active":
360 break
361 wall = time.perf_counter() - t0
362 K = case.K(u)
363 Ks[beta] = K
364 per100 = max([sum(1 for q in rst_steps if lo < q <= lo + 100)
365 for lo in range(0, len(res), 100)] or [0])
366 deep = depth is None or rmin <= depth
367 ok = (acc.status == "active" and per100 <= 1 and deep and res[-1] <= 10 * rmin)
368 print(f"g3 {a.case} {a.scheme} N={a.N} beta={beta:g} m={a.window[0]}: status={acc.status} "
369 f"({acc.reason}) steps={len(res)} restarts={acc.num_restarts} (max/100 {per100}) "
370 f"min_res={rmin:.3e} (bar {depth if depth is not None else 'none'}) "
371 f"final_res={res[-1]:.3e} K={K:.12f} "
372 f"(at call {u_call}) wall={wall:.1f}s -> {'PASS' if ok else 'FAIL'}",
373 flush=True)
374 rec = common_rec(a, case)
375 rec.update(gate="g3", beta=beta, window=a.window[0], status=acc.status,
376 reason=acc.reason, steps=len(res), restarts=acc.num_restarts,
377 restart_calls=rst_steps, restarts_max_per_100=per100, min_res=rmin,
378 depth=depth, final_res=res[-1], K=K, u=u, K_call=u_call, ok=ok, wall_s=wall)
379 emit(a, rec)
380 del acc, s
381 if len(Ks) > 1:
382 vals = [v for v in Ks.values() if math.isfinite(v)]
383 spread = (max(vals) - min(vals)) / abs(min(vals)) if vals else math.nan
384 print(f"g3 {a.case} {a.scheme} N={a.N}: K over betas {sorted(Ks)} spread {spread:.3e} "
385 f"-> {'PASS' if spread <= 1e-8 else 'FAIL'} (<= 1e-8)", flush=True)
386 rec = common_rec(a, case)
387 rec.update(gate="g3_spread", betas=list(Ks), Ks=list(Ks.values()), spread=spread)
388 emit(a, rec)
389
390
391# ---------------------------------------------------------------------------------------------- g5
392def cmd_g5(flow, a):
393 st = settings_of(a.settings)
394 case = Case(a)
395 m = a.window[0]
396 s0 = case.build(flow, a.scheme, st)
397 full = flow.march_to_steady(s0, lambda: umean(s0), rtol=st["rtol"],
398 max_steps=st["max_steps"], accelerate=True, window=m)
399 K0 = case.K(full.monitor)
400 for mode in ("field", "state"):
401 s1 = case.build(flow, a.scheme, st)
402 acc = s1.diagnostics.anderson_accelerator(window=m)
403 for _ in range(a.at): # the first a.at steps of phase A (residual far above the target)
404 acc.step(True)
405 ck = {f: np.array(s1.get_field(f)) for f in ("u", "v", "w", "p")}
406 vel = [np.array(s1.get_u()), np.array(s1.get_v()), np.array(s1.get_w())]
407 del acc, s1
408 s2 = case.build(flow, a.scheme, st)
409 if mode == "field":
410 for f, v in ck.items():
411 s2.set_field(f, np.asfortranarray(v))
412 else:
413 s2.set_state(*[np.asfortranarray(v) for v in vel])
414 res = flow.march_to_steady(s2, lambda s2=s2: umean(s2), rtol=st["rtol"],
415 max_steps=st["max_steps"], accelerate=True, window=m)
416 K = case.K(res.monitor)
417 rel = abs(K / K0 - 1.0)
418 total = a.at + res.steps
419 print(f"g5 {a.case} {a.scheme} N={a.N} {a.settings} m={m} restore={mode}: uninterrupted "
420 f"{full.steps} steps K={K0:.12f}; interrupted at {a.at} + {res.steps} = {total} "
421 f"steps K={K:.12f} |dK|/K={rel:.2e} extra={total - full.steps} "
422 f"-> {'PASS' if rel <= 1e-8 and res.converged else 'FAIL'}", flush=True)
423 rec = common_rec(a, case)
424 rec.update(gate="g5", restore=mode, window=m, at=a.at, full_steps=full.steps,
425 K_full=K0, steps_after=res.steps, total=total, K=K, rel=rel,
426 converged=res.converged)
427 emit(a, rec)
428
429
430# ---------------------------------------------------------------------------------------------- g8
431def cmd_g8(flow, a):
432 st = settings_of(a.settings)
433 case = Case(a)
434 m = a.window[0]
435 s = case.build(flow, a.scheme, st)
436 for _ in range(a.warmup): # warm-up: time plain steps late in the march, not in the transient
437 s.step()
438 plain = []
439 for _ in range(a.steps):
440 s.step()
441 plain.append(s.diagnostics.last_step_timers()["step"])
442 acc = s.diagnostics.anderson_accelerator(window=m)
443 for _ in range(10): # history fill + engagement
444 acc.step(True)
445 t_acc0 = acc.seconds
446 steps = []
447 for _ in range(a.steps):
448 acc.step(True)
449 steps.append(s.diagnostics.last_step_timers()["step"])
450 over = (acc.seconds - t_acc0) / a.steps
451 mp = float(np.mean(plain))
452 ms = float(np.mean(steps))
453 ns = 7 if (a.scheme == "collocated" and a.advection) else 4
454 npad = (a.N + 4) ** 3
455 formula = (2 * m + 3) * ns * 8 * npad
456 print(f"g8 {a.case} {a.scheme} N={a.N} m={m}: mean plain step {mp * 1e3:.3f} ms, mean step "
457 f"under acceleration {ms * 1e3:.3f} ms, accelerator {over * 1e3:.3f} ms/step = "
458 f"{100 * over / mp:.2f} % of the plain step ({100 * over / ms:.2f} % of the step under "
459 f"acceleration; {a.warmup} plain warm-up steps); memory_bytes {acc.memory_bytes} vs formula "
460 f"{formula} ({'equal' if acc.memory_bytes == formula else 'DIFFERENT'}); "
461 f"status {acc.status} engaged columns {acc.num_columns}", flush=True)
462 rec = common_rec(a, case)
463 rec.update(gate="g8", window=m, steps=a.steps, plain_ms=mp * 1e3, acc_step_ms=ms * 1e3,
464 overhead_ms=over * 1e3, overhead_pct=100 * over / mp, warmup=a.warmup,
465 overhead_pct_of_acc_step=100 * over / ms,
466 memory_bytes=acc.memory_bytes, formula=formula)
467 emit(a, rec)
468
469
470# ---------------------------------------------------------------------------------------------- mpi
471def cmd_mpi(flow, a):
472 from mpi4py import MPI
473 comm = MPI.COMM_WORLD
474 rank, size = comm.Get_rank(), comm.Get_size()
475 st = settings_of(a.settings)
476 case = Case(a)
477 N, L = a.N, case.L
478 Cls = flow.Solver if a.scheme == "staggered" else flow.SolverColocated
479 if a.serial: # the serial build / the single-rank path: the global solver, no init_mpi
480 s = case.build(flow, a.scheme, st)
481 else:
482 origin, bsize = flow.mpi_block(N, N, N)
483 s = Cls(tuple(bsize), extent=(L, L, L), global_cells=(N, N, N),
484 origin=(0.0, 0.0, 0.0))
485 s.init_mpi(N, N, N)
486 case.configure(s, st)
487 g = (np.arange(N) + 0.5) * (L / N)
488 gsdf = case.sdf_global(g, g, g)
489 ox, oy, oz = origin
490 lx, ly, lz = bsize
491 s.set_solid(np.asfortranarray(gsdf[ox:ox + lx, oy:oy + ly, oz:oz + lz]),
492 cutcell_pressure=True)
493
494 def gmean():
495 u = np.asarray(s.get_u())
496 tot = comm.allreduce(float(u.sum()), op=MPI.SUM)
497 return tot / float(N * N * N)
498
499 if a.hash:
500 acc = s.diagnostics.anderson_accelerator(window=a.window[0])
501 for _ in range(a.steps):
502 acc.step(True)
503 loc = {f: np.ascontiguousarray(getattr(s, "get_" + f)(), dtype=np.float64)
504 for f in ("u", "v", "w", "p")}
505 allf = comm.gather(loc, root=0)
506 if rank == 0:
507 hs = {f: hashlib.sha256(np.concatenate([np.asarray(r[f]).ravel() for r in allf])
508 .tobytes()).hexdigest() for f in loc}
509 tot = hashlib.sha256("".join(hs[f] for f in sorted(hs)).encode()).hexdigest()
510 print(f"mpi-hash {a.scheme} N={N} np={size} serial={a.serial} m={a.window[0]} "
511 f"{a.steps} accelerated steps: {tot} status={acc.status} "
512 f"res={acc.residual:.6e}", flush=True)
513 rec = common_rec(a, case)
514 rec.update(gate="g4_hash", np=size, serial=a.serial, steps=a.steps, hash=tot)
515 emit(a, rec)
516 return
517 for w in a.window:
518 if w != a.window[0]:
519 raise SystemExit("mpi: one window per process")
520 t0 = time.perf_counter()
521 res = flow.march_to_steady(s, gmean, rtol=st["rtol"], max_steps=st["max_steps"],
522 accelerate=(w > 0), window=max(w, 1))
523 wall = time.perf_counter() - t0
524 if rank == 0:
525 K = case.K(res.monitor)
526 print(f"mpi {a.case} {a.scheme} N={N} np={size} {a.settings} m={w}: "
527 f"conv={res.converged} {res.reason} steps={res.steps} "
528 f"acc={res.accelerated_steps} K={K:.14f} wall={wall:.1f}s", flush=True)
529 rec = common_rec(a, case)
530 rec.update(gate="g4", np=size, window=w, converged=res.converged, reason=res.reason,
531 steps=res.steps, accelerated_steps=res.accelerated_steps, K=K,
532 monitor=res.monitor, wall_s=wall)
533 emit(a, rec)
534
535
536def main():
537 ap = argparse.ArgumentParser()
538 ap.add_argument("cmd", choices=("run", "oracle", "g3", "g5", "g8", "mpi"))
539 ap.add_argument("case", choices=("sphere", "bed"))
540 ap.add_argument("--scheme", choices=("staggered", "collocated"), required=True)
541 ap.add_argument("--N", type=int, default=16)
542 ap.add_argument("--phi", type=float, default=0.125)
543 ap.add_argument("--mu", type=float, default=1.0)
544 ap.add_argument("--beta", type=float, default=6.0, help="nu dt / h^2 (ignored with --dt)")
545 ap.add_argument("--betas", type=float, nargs="+", default=[6.0, 60.0, 600.0, 1e4])
546 ap.add_argument("--dt", type=float, default=None)
547 ap.add_argument("--force", type=float, default=None)
548 ap.add_argument("--advection", default=None)
549 ap.add_argument("--implicit-advection", action="store_true")
550 ap.add_argument("--arrangement", default=None)
551 ap.add_argument("--settings", choices=("production", "tight"), default="production")
552 ap.add_argument("--window", type=int, nargs="+", default=[0, 5])
553 ap.add_argument("--max-steps", type=int, default=None)
554 ap.add_argument("--k-plain", type=float, default=None,
555 help="run, tight staggered bed: the plain tight certificate's K at this dt "
556 "(from the log) for the G1 bed criterion, when window 0 is not run")
557 ap.add_argument("--plain-uncertified", action="store_true",
558 help="run, G1 bed: --k-plain is the plain march's K at max_steps (it did not "
559 "certify); the accelerated K must then be closer to K_inf")
560 ap.add_argument("--steps", type=int, default=30)
561 ap.add_argument("--at", type=int, default=25)
562 ap.add_argument("--warmup", type=int, default=100, help="g8: plain steps before timing")
563 ap.add_argument("--extend-to", type=int, default=0,
564 help="g3: past --steps, continue until the running-min residual reaches the "
565 "depth bar (sphere 1e-9; the bed has none) "
566 "or this many steps in total (0 = no extension)")
567 ap.add_argument("--serial", action="store_true")
568 ap.add_argument("--hash", action="store_true")
569 ap.add_argument("--trace", action="store_true")
570 ap.add_argument("--json", default=None)
571 ap.add_argument("--label", default="")
572 a = ap.parse_args()
573 sys.path.insert(0, os.path.join(HERE, "..", "..", "scripts"))
574 from _bootstrap import ensure_flow
575 flow = ensure_flow()
576 {"run": cmd_run, "oracle": cmd_oracle, "g3": cmd_g3, "g5": cmd_g5, "g8": cmd_g8,
577 "mpi": cmd_mpi}[a.cmd](flow, a)
578
579
580if __name__ == "__main__":
581 main()
configure(self, s, settings, beta=None)
static double mean(const std::vector< double > &a)