RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
make_tables.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3make_tables.py — emit the verification tables from the campaign board.
4
5 python make_tables.py # markdown to stdout
6 python make_tables.py --latex # LaTeX bodies
7 python make_tables.py --out FILE # write markdown to FILE
8
9This is plan S3.5's reporting helper. It reads the published records and
10REFUSES to print a value from a run that missed its comparison dose (S3.4.1):
11such a row appears in a "did not reach" line with the dose it achieved, never as
12a metric. The rule is mechanical here rather than a matter of discipline.
13
14Table 4 ships as the NO-COARSENING variant. The production-configuration exact
15arm was attempted three times and abandoned each time (see S3 note below); the
16NC variant is the one where both arms provably solve identical equations, which
17is what a verification table requires.
18"""
19from __future__ import annotations
20
21import argparse
22import glob
23import json
24import sys
25import types
26from pathlib import Path
27
28HERE = Path(__file__).resolve().parent
29DT = HERE.parent
30MOD = DT.parent
31REPO = MOD.parent
32for p in (str(REPO), str(DT), str(HERE)):
33 if p not in sys.path:
34 sys.path.insert(0, p)
35
36SYNC = HERE / ".sync" / "claims"
37# The salvaged discrete arm: 2026-09-07, 12006 equations, 17.6 h, LOOP_COAL
38# silently absent -- which is exactly what makes it the right reference for the
39# no-coarsening rungs.
40NC_REF_GLOB = str(MOD / "output" / "20260907_093333_T4_D_4k_*" / "plots" / "plot_data.pkl")
41
42
43def board() -> dict:
44 out = {}
45 for f in sorted(SYNC.glob("*.json")):
46 try:
47 j = json.loads(f.read_text(encoding="utf-8"))
48 out[j.get("run_id", f.stem)] = j
49 except Exception:
50 pass
51 return out
52
53
55 """Re-score the 2026-09-07 discrete arm from its own saved trajectory."""
56 from radcluster_code.py_utils import visualization as viz
57 import run_ensemble as re_mod
58 f = glob.glob(NC_REF_GLOB)
59 if not f:
60 return None
61 d = viz.load_plot_data(f[0])
62 sim = types.SimpleNamespace(input_data=d["input_data"])
63 cfg = {"I": 4000, "V": 4000, "dose": 2.0,
64 "C_floor": float(d["input_data"].reactions.get("C_floor", 1e-15))}
65 return re_mod.observe(d["results"], sim, cfg, 1.0)["at_dose"]["2"]
66
67
68# B4 is not a campaign row -- it IS the tracked reference run. Score it from
69# its own saved trajectory so Table 1 shows the rung the ladder converges toward
70# instead of a "did not reach ?" hole.
71REF_RUN_GLOB = str(MOD / "output" /
72 "20260906_063055_full_system_bin_moment_CD_fission_I80000V20000_im5vm5"
73 / "plots" / "plot_data.pkl")
74
75
77 from radcluster_code.py_utils import visualization as viz
78 import run_ensemble as re_mod
79 f = glob.glob(REF_RUN_GLOB)
80 if not f:
81 return None
82 d = viz.load_plot_data(f[0])
83 sim = types.SimpleNamespace(input_data=d["input_data"])
84 cfg = {"I": 80000, "V": 20000, "dose": 40.0,
85 "C_floor": float(d["input_data"].reactions.get("C_floor", 1e-15))}
86 o = re_mod.observe(d["results"], sim, cfg, 1.0)
87 v = (o.get("at_dose") or {}).get("15.72")
88 if not v:
89 return None
90 return {"status": "done", "N_eq": 189, "scored": {"15.72": v},
91 "delta_FP": v.get("delta_FP", float("nan")), "wall_s": 1317.37}
92
93
94def cell(rec, dose_key):
95 """The scored block for a run, or None when it may not be reported."""
96 if not rec or rec.get("status") not in ("done",):
97 return None
98 v = (rec.get("scored") or {}).get(dose_key)
99 if not v or v.get("missing"):
100 return None
101 return v
102
103
104def table4(B, md):
105 ref = nc_reference()
106 md.append("## Table 4 — closure verification against the exact solution\n")
107 md.append("**I = V = 4000, `LOOP_COAL = 0` in both arms, scored at 2.0000 dpa.**")
108 md.append("Both arms solve identical equations, so the deviations below are "
109 "closure error.\n")
110 if ref is None:
111 md.append("_reference trajectory not present on this machine_\n")
112 return
113 md.append("| rung | resolution | N_eq | d_100 (nm) | dev | d_111 (nm) | dev | d_cav (nm) | dev |")
114 md.append("|---|---|---|---|---|---|---|---|---|")
115 md.append(f"| **D (exact)** | — | 8006 (12006 solved) | {ref['d_100_nm']:.3f} | — "
116 f"| {ref['d_111_nm']:.3f} | — | {ref['d_cavity_nm']:.3f} | — |")
117 rows = (("C1", "1/10", "T4_C1_nc"), ("C2", "1/40", "T4_C2_nc"),
118 ("C3", "1/160", "T4_C3_nc"), ("C4", "1/800", "T4_C4_nc"),
119 ("C4, P=1 constant", "1/800", "T4_C4_P1_nc"))
120 for lbl, res, rid in rows:
121 v = cell(B.get(rid), "2")
122 if v is None:
123 r = B.get(rid) or {}
124 md.append(f"| {lbl} | {res} | — | _did not reach_ "
125 f"({r.get('dose_reached', '?')} dpa) | | | | | |")
126 continue
127 d = lambda k: (v[k] - ref[k]) / ref[k] * 100
128 md.append(f"| {lbl} | {res} | {B[rid].get('N_eq', '—')} "
129 f"| {v['d_100_nm']:.3f} | {d('d_100_nm'):+.1f}% "
130 f"| {v['d_111_nm']:.3f} | {d('d_111_nm'):+.1f}% "
131 f"| {v['d_cavity_nm']:.3f} | {d('d_cavity_nm'):+.1f}% |")
132 md.append("")
133
134
135def simple_table(B, md, title, subtitle, rows, dose_key):
136 md.append(f"## {title}\n")
137 md.append(subtitle + "\n")
138 md.append("| column | N_eq | d_100 (nm) | d_111 (nm) | d_cav (nm) | delta_FP | wall |")
139 md.append("|---|---|---|---|---|---|---|")
140 missed = []
141 for lbl, rid in rows:
142 rec = B.get(rid)
143 v = cell(rec, dose_key)
144 if v is None:
145 missed.append((lbl, rec or {}))
146 continue
147 md.append(f"| {lbl} | {rec.get('N_eq', '—')} | {v['d_100_nm']:.3f} "
148 f"| {v['d_111_nm']:.3f} | {v['d_cavity_nm']:.3f} "
149 f"| {rec.get('delta_FP', float('nan')):.3f} "
150 f"| {rec.get('wall_s', 0)/60:.1f} m |")
151 md.append("")
152 if missed:
153 md.append("**Did not reach the comparison dose** (plan §3.4.1 — reported "
154 "with the dose achieved, never as a metric):\n")
155 for lbl, r in missed:
156 md.append(f"- **{lbl}** — reached {r.get('dose_reached', '?')} of "
157 f"{r.get('dose_target', '?')} dpa. {r.get('note', '')}")
158 md.append("")
159
160
161def main(argv=None):
162 ap = argparse.ArgumentParser()
163 ap.add_argument("--out")
164 a = ap.parse_args(argv)
165 B = board()
167 if r:
168 B["_B4"] = r
169 md = ["# Verification study — results", ""]
170 md.append("Generated by `make_tables.py` from the campaign board "
171 "(`campaign-verification`). Every value is scored through "
172 "`run_ensemble.observe`, the same operator the calibration ledger "
173 "uses, so a table entry and a ledger row for one configuration "
174 "agree by construction.\n")
175
176 table4(B, md)
177
179 B, md, "Table 1 — closure convergence at the production domain",
180 "I = 80000, V = 20000, scored at 15.72 dpa. Deviations are quoted "
181 "relative to **B2**, the most-resolved rung that completes.",
182 (("B1 i_d 6400", "T1_B1"), ("B2 i_d 1600", "T1_B2"),
183 ("B3 i_d 400", "T1_B3"), ("B4 = production (reference run)", "_B4")),
184 "15.72")
185
187 B, md, "Table 2 — intra-bin closure and tolerance",
188 "One knob changed from B4 at a time.",
189 (("P=1 constant", "T2_P1"), ("P=3 lognormal", "T2_P3"),
190 ("I_bin 10", "T2_IBIN10"), ("I_bin 40", "T2_IBIN40"),
191 ("rtol 1e-4", "T2_RTOL4"), ("rtol 1e-6", "T2_RTOL6")),
192 "15.72")
193
195 B, md, "Table 3 — helium",
196 "Model-to-model; no experimental column.",
197 (("fission QSS", "T3_FISS_QSS"), ("fission dynamic", "T3_FISS_DYN"),
198 ("fusion QSS", "T3_FUS_QSS"), ("fusion dynamic", "T3_FUS_DYN")),
199 "15.72")
200
201 md.append("## Limitations that must be stated\n")
202 md.append("1. **Table 4 verifies the no-coarsening variant.** The production "
203 "configuration runs with `LOOP_COAL = 1`. Its exact arm was "
204 "attempted three times and abandoned: the channel is O(NC²) in the "
205 "number of populated sessile sizes, and even gated at 2×C_floor it "
206 "spent 2.8 h on a single output step at 0.014 of 2 dpa. So the "
207 "closure error measured here is **not demonstrably the production "
208 "run's closure error** — plan §1.3's transfer argument does not "
209 "carry over unmodified.")
210 md.append("2. **The no-coarsening variant is unphysical.** Without the "
211 "channel the ½⟨111⟩ mean size pins at the mobility cutoff and the "
212 "loop density runs 20–60× over the EUROFER97 data "
213 "(`rate_kernels.cpp:1930`). That does not invalidate the "
214 "verification — verification asks whether the closure reproduces "
215 "the exact solution of the *same* equations, which these arms do — "
216 "but nothing in Table 4 may be compared to experiment.")
217 md.append("3. **Table 1 has no B1.** The most-resolved rung reached 0.079 of "
218 "40 dpa in 18.9 h and was stopped. Deviations are quoted relative "
219 "to B2 (N_eq = 2048); B2 → B3 → B4 still spans 10× in N_eq.")
220 md.append("4. **`delta_FP` ≈ 0.05–0.06 throughout**, against the model's own "
221 "1e-2 gate. A separate, disclosed defect: a verification study "
222 "measures closure error against the same equations, and "
223 "conservation error is not that. Do not conflate them.\n")
224 out = "\n".join(md)
225 if a.out:
226 Path(a.out).write_text(out, encoding="utf-8")
227 print(f"wrote {a.out}")
228 else:
229 print(out)
230 return 0
231
232
233if __name__ == "__main__":
234 sys.exit(main())
dict board()
table4(B, md)
cell(rec, dose_key)
main(argv=None)
simple_table(B, md, title, subtitle, rows, dose_key)