flow 1.3.0
Kokkos cut-cell IBM incompressible Navier-Stokes solver + pnm pore extraction
Loading...
Searching...
No Matches
anderson_synthetic_maps.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Synthetic linear maps through the Anderson oracle (doc/steady_acceleration.md, Revision 2).
3
4An INSTRUMENT, not a gate. It drives `anderson_oracle.AndersonOracle` and the oracle's
5`march_to_steady` with a fake solver whose `step()` is a linear map on the padded `u` buffer, so the
6design's algorithm runs unchanged on maps whose spectrum is known exactly. No flow build is needed.
7
8The maps are core U4's (core-anderson tests/test_anderson.cpp `runU4`): Box(25, 20, 20, g = 2);
9inner entries k < nBulk = 9997 get lam = 0.9 k / (nBulk - 1); entry nBulk gets the outlier; entries
10nBulk + 1, nBulk + 2 carry a 2x2 block B (their diagonal lam is 0); c = 1 everywhere (ghosts: lam 0,
11so they stay 1). Optional evaluation noise: a fresh seeded random vector of norm sigma ||g|| per
12evaluation (core U8's noise model). The oracle's other fields v, w, p stay 0.
13
14 B = rotation(rot, 0.5) core U4 (outlier 1.02, rot 1.01: unstable) and its stable twin
15 B = [[lam0, kappa], [0, lam0]] a STABLE non-normal block: rho(J) = lam0 < 1 for every kappa,
16 numerical range radius lam0 + |kappa| / 2
17
18Sections (all printed; `--json` writes one record per run):
19 raw unconditional `acc.step(True)` like core U4, under rev 1 (does the guard fire?) and rev 2
20 (calls to residual 1e-8 / 1e-10 / 1e-12)
21 driver the rev-1 and rev-2 `march_to_steady` (rtol 1e-4) on U4's two maps, noise 0 / 1e-12 / 1e-8
22
23 python tests/study/anderson_synthetic_maps.py [--json out.jsonl]
24"""
25import argparse
26import json
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 O # noqa: E402
35
36N = (25, 20, 20)
37G = 2
38E = tuple(n + 2 * G for n in N)
39
40
41class _Diag:
42 def __init__(self, s):
43 self.s = s
44
45 def field_view(self, name):
46 return self.s.f[name]
47
49 return 0
50
52 return False
53
54
56 """x <- lam x + c on u, plus the 2x2 block B on two inner entries; optional noise."""
57
58 def __init__(self, outlier, block, sigma=0.0, seed=20261002):
59 self.f = {k: np.zeros(E, order="F") for k in ("u", "v", "w", "p")}
60 self.f["sdf"] = np.ones(E, order="F")
61 self.diagnostics = _Diag(self)
62 X, Y, Z = np.meshgrid(*[np.arange(e) for e in E], indexing="ij")
63 inner = (X >= G) & (X < E[0] - G) & (Y >= G) & (Y < E[1] - G) & (Z >= G) & (Z < E[2] - G)
64 self.inner = np.flatnonzero(inner.reshape(-1, order="F"))
65 nb = self.inner.size - 3
66 self.lam = np.zeros(int(np.prod(E)))
67 self.lam[self.inner[:nb]] = 0.9 * np.arange(nb) / (nb - 1)
68 self.lam[self.inner[nb]] = outlier
69 self.c = np.ones_like(self.lam)
70 self.i0, self.i1 = self.inner[nb + 1], self.inner[nb + 2]
71 self.B = np.asarray(block, float)
72 self.sigma = sigma
73 self.rng = np.random.default_rng(seed)
74 self.u = self.f["u"].reshape(-1, order="F")
75 assert np.shares_memory(self.u, self.f["u"])
76 xs = self.c / (1.0 - self.lam)
77 xs[[self.i0, self.i1]] = np.linalg.solve(np.eye(2) - self.B, [1.0, 1.0])
78 self.xstar = xs
79
80 def step(self):
81 t = self.u.copy()
82 g = self.lam * t + self.c
83 ab = np.array([t[self.i0], t[self.i1]])
84 g[[self.i0, self.i1]] += self.B @ ab
85 if self.sigma > 0.0:
86 n = np.zeros_like(g)
87 n[self.inner] = self.rng.standard_normal(self.inner.size)
88 g += n * (self.sigma * np.linalg.norm(g[self.inner]) / np.linalg.norm(n))
89 self.u[:] = g
90
91 def monitor(self):
92 return float(self.u[self.inner].mean())
93
94 def monitor_exact(self):
95 return float(self.xstar[self.inner].mean())
96
97
98def rotation(rho, theta=0.5):
99 return [[rho * math.cos(theta), -rho * math.sin(theta)],
100 [rho * math.sin(theta), rho * math.cos(theta)]]
101
102
103def jordan(lam0, kappa):
104 return [[lam0, kappa], [0.0, lam0]]
105
106
107def oracle_kw(rev):
108 return dict(signature=(1.0,), c_p=1.0, gauged=False, collocated_advection=False, cells=N,
109 metric="V", ritz_scope="mixed", inner_tolerance=0.0,
110 ritz_action="none" if rev == 2 else "stop")
111
112
113MAPS = {
114 "U4 unstable (1.02, rotation 1.01)": (1.02, rotation(1.01)),
115 "U4 stable twin (0.996, rotation 0.95)": (0.996, rotation(0.95)),
116 **{f"stable non-normal (lam0 {l0}, kappa {k})": (0.5, jordan(l0, k))
117 for l0 in (0.99, 0.996) for k in (0.1, 1.0, 10.0)},
118}
119
120
121def raw(name, rev, calls=400):
122 outlier, block = MAPS[name]
123 s = LinearMapSolver(outlier, block)
124 acc = O.AndersonOracle(s, window=5, **oracle_kw(rev))
125 readings, hit, engaged = [], {}, None
126 for k in range(1, calls + 1):
127 acc.step(True)
128 if engaged is None and acc.engaged:
129 engaged = k
130 if math.isfinite(acc.ritzRadius):
131 readings.append((k, acc.residual, acc.ritzRadius))
132 for t in (1e-8, 1e-10, 1e-12):
133 if t not in hit and acc.residual <= t:
134 hit[t] = k
135 if acc.status != "active" or acc.residual <= 1e-13:
136 break
137 err = float(np.max(np.abs(s.u[s.inner] - s.xstar[s.inner])) / np.max(np.abs(s.xstar[s.inner])))
138 return dict(section="raw", map=name, rev=rev, engaged=engaged, status=acc.status, last_call=k,
139 residual=acc.residual, restarts=acc.numRestarts,
140 calls_to=[hit.get(1e-8), hit.get(1e-10), hit.get(1e-12)], max_rel_err=err,
141 max_reading=max((r[2] for r in readings), default=None),
142 readings_tail=readings[-3:], would_fire=list(acc.ritz_would_fire))
143
144
145def driver(name, rev, sigma):
146 outlier, block = MAPS[name]
147 s = LinearMapSolver(outlier, block, sigma)
148 hold, phases = {}, []
149
150 def cb(steps, phase):
151 if phases and phases[-1][0] == phase:
152 phases[-1][1] += 1
153 else:
154 phases.append([phase, 1])
155 res = O.march_to_steady(s, s.monitor, rtol=1e-4, max_steps=5000, accelerate=True, window=5,
156 callback=cb, oracle_kw=oracle_kw(rev), holder=hold, rev=rev)
157 m = res["monitor"]
158 return dict(section="driver", map=name, rev=rev, sigma=sigma, converged=res["converged"],
159 reason=res["reason"], steps=res["steps"],
160 phases=" ".join(f"{p[0][0]}{p[1]}" for p in phases),
161 monitor_rel_err=abs(m / s.monitor_exact() - 1.0) if math.isfinite(m) else None)
162
163
164def main():
165 ap = argparse.ArgumentParser()
166 ap.add_argument("--json", default=None)
167 a = ap.parse_args()
168 recs = []
169 for name in MAPS:
170 for rev in (1, 2):
171 r = raw(name, rev)
172 recs.append(r)
173 fired = f"'{r['status']}' at call {r['last_call']}" if r["status"] != "active" else "active"
174 print(f"raw rev{rev} {name}: engaged {r['engaged']}, {fired}, calls to residual "
175 f"1e-8/1e-10/1e-12 {r['calls_to']}, restarts {r['restarts']}, max Ritz "
176 f"{r['max_reading']:.5f}, last readings "
177 f"{[(k, f'{x:.2e}', round(z, 5)) for k, x, z in r['readings_tail']]}", flush=True)
178 for name in ("U4 unstable (1.02, rotation 1.01)", "U4 stable twin (0.996, rotation 0.95)"):
179 for rev in (1, 2):
180 for sigma in (0.0, 1e-12, 1e-8):
181 r = driver(name, rev, sigma)
182 recs.append(r)
183 print(f"drv rev{rev} {name} sigma {sigma:g}: converged={r['converged']} "
184 f"{r['reason']} steps={r['steps']} ({r['phases']}), monitor vs exact fixed "
185 f"point {r['monitor_rel_err']}", flush=True)
186 if a.json:
187 with open(a.json, "w") as f:
188 for r in recs:
189 f.write(json.dumps(r) + "\n")
190
191
192if __name__ == "__main__":
193 main()
__init__(self, outlier, block, sigma=0.0, seed=20261002)
static double mean(const std::vector< double > &a)