RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
make_M_figures.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""make_M_figures.py — campaign M: binning convergence, drawn.
3
4Two figures:
5
6 M_convergence.pdf the <100> and cavity observables against N_eq, one panel
7 each (the two <111> panels are omitted), with
8 the exact arm as a horizontal line. This is the
9 convergence statement: a rung is converged when its
10 panel has gone flat AND flat AT the reference line.
11 Flat alone is self-consistency, which is why the
12 reference is drawn even when it is the only thing on the
13 panel that is not a bin-moment solve.
14
15 M_2d_sweep.pdf deviation against i_discrete, one curve per I_bin. Kept
16 as a diagnostic; it is no longer carried in the report,
17 where the M2D tables make the same point.
18
19Reads the campaign board, so it draws whatever has actually completed and says
20so, rather than failing when a rung is missing.
21"""
22from __future__ import annotations
23
24import glob
25import json
26import sys
27import types
28from pathlib import Path
29
30import numpy as np
31import matplotlib
32matplotlib.use("Agg")
33import matplotlib.pyplot as plt
34import matplotlib.ticker as mticker
35
36HERE = Path(__file__).resolve().parent
37DT = HERE.parent
38MOD = DT.parent
39REPO = MOD.parent
40for p in (str(REPO), str(DT), str(HERE)):
41 if p not in sys.path:
42 sys.path.insert(0, p)
43
44import runs as manifest_mod # noqa: E402
45
46SYNC = HERE / ".sync" / "claims"
47OUT = REPO / "docs" / "Formulation" / "verification_campaign" / "figs"
48SCORE_DOSE = "20"
49
50LABEL_PT, TICK_PT, LEGEND_PT = 22, 20, 15
51plt.rcParams.update({
52 "axes.labelsize": LABEL_PT, "axes.titlesize": LABEL_PT,
53 "xtick.labelsize": TICK_PT, "ytick.labelsize": TICK_PT,
54 "legend.fontsize": LEGEND_PT, "lines.linewidth": 3,
55})
56
57# (key, axis name, unit) -- the unit is kept separate from the name so that a
58# common power of ten can be factored out of the tick labels and carried in the
59# axis label instead. Densities vary by a few parts in a thousand across these
60# rungs, and a log axis over that range labels its MINOR ticks, which produced
61# ordinates reading "3.74 x 10^21", "3.72 x 10^21" and so on down the panel.
62OBS = [("N_loops_111", r"$\frac{1}{2}\langle111\rangle$ density", "m$^{-3}$"),
63 ("d_111_nm", r"$\frac{1}{2}\langle111\rangle$ diameter", "nm"),
64 ("N_loops_100", r"$\langle100\rangle$ density", "m$^{-3}$"),
65 ("d_100_nm", r"$\langle100\rangle$ diameter", "nm"),
66 ("N_voids", r"Cavity density", "m$^{-3}$"),
67 ("d_cavity_nm", r"Cavity diameter", "nm")]
68
69# Panels of the convergence figure. The two <111> panels are omitted: the
70# mean-size behaviour they showed is set out quantitatively in the text, and
71# the density panel adds nothing the <100> and cavity panels do not.
72CONV_KEYS = ("N_loops_100", "d_100_nm", "N_voids", "d_cavity_nm")
73
74
75def _factor(values):
76 """Common power of ten for a set of values, and its label fragment.
77
78 Returns (divisor, prefix) so that the panel plots y/divisor and the axis
79 label reads e.g. "density ($10^{21}$ m$^{-3}$)". Values within two decades
80 of unity are left alone.
81 """
82 v = np.abs(np.asarray(list(values), float))
83 v = v[np.isfinite(v) & (v > 0)]
84 if v.size == 0:
85 return 1.0, ""
86 e = int(np.floor(np.log10(np.median(v))))
87 if -2 <= e <= 2:
88 return 1.0, ""
89 return 10.0 ** e, rf"$10^{{{e}}}$ "
90
91
92def board() -> dict:
93 out = {}
94 for f in sorted(SYNC.glob("*.json")):
95 try:
96 j = json.loads(f.read_text(encoding="utf-8"))
97 out[j.get("run_id", f.stem)] = j
98 except Exception:
99 pass
100 return out
101
102
103def scored(rec):
104 if not rec or rec.get("status") != "done":
105 return None
106 v = (rec.get("scored") or {}).get(SCORE_DOSE)
107 return None if (not v or v.get("missing") or v.get("off_grid")) else v
108
109
111 """The exact arm, whichever M1 rung is serving as it."""
112 rid = getattr(manifest_mod, "M_REF_RUN", None)
113 if rid and scored(B.get(rid)):
114 return rid, scored(B[rid])
115 return None, None
116
117
119 rows = []
120 # The REFERENCED ladder (M2R, I = V = 1000). M2 at 10000 has no exact arm
121 # -- the discrete solve there diverges -- so plotting it here would put a
122 # convergence figure in front of a reader with nothing to converge TO.
123 for nb in manifest_mod.M2R_BINS:
124 rec = B.get(f"M2R_B{nb}")
125 v = scored(rec)
126 if v:
127 rows.append((rec.get("N_eq", np.nan), nb, v))
128 if not rows:
129 print(" no completed M2 rungs; nothing to draw")
130 return None
131 rid, ref = ref_record(B)
132
133 panels = [o for o in OBS if o[0] in CONV_KEYS]
134 fig, axes = plt.subplots(2, 2, figsize=(15, 11))
135 x = [r[0] for r in rows]
136 for ax, (key, name, unit) in zip(axes.ravel(), panels):
137 y = [r[2][key] for r in rows]
138 series = list(y) + ([ref[key]] if ref is not None else [])
139 div, pref = _factor(series)
140 ax.plot(x, [v / div for v in y], "o-", color="steelblue", ms=10,
141 label="bin-moment rungs")
142 if ref is not None:
143 ax.axhline(ref[key] / div, color="tomato", ls="--", lw=3,
144 label=f"exact ({rid})")
145 else:
146 # Say it on the figure, not only in the caption: a panel with no
147 # reference line shows self-consistency, not accuracy, and a reader
148 # scanning figures will not have the caption in view.
149 ax.text(0.5, 0.06, "no exact arm — self-convergence only",
150 transform=ax.transAxes, ha="center", color="tomato",
151 fontsize=LEGEND_PT)
152 # Both axes linear. N_eq spans 120 to 250 and the observables a few
153 # parts in a thousand; a logarithmic axis over less than one decade
154 # labels its minor ticks, and those labels ran into one another.
155 ax.xaxis.set_major_locator(mticker.MaxNLocator(nbins=5, integer=True))
156 ax.yaxis.set_major_locator(mticker.MaxNLocator(nbins=6))
157 ax.set_xlabel(r"$N_{\rm eq}$")
158 ax.set_ylabel(f"{name} ({pref}{unit})")
159 ax.grid(True, which="major", alpha=0.25)
160 axes.ravel()[0].legend(loc="best", frameon=True, framealpha=0.92)
161 fig.tight_layout()
162 OUT.mkdir(parents=True, exist_ok=True)
163 p = OUT / "M_convergence.pdf"
164 fig.savefig(p, bbox_inches="tight")
165 plt.close(fig)
166 return p
167
168
169def two_d(B):
170 """The 2-D sweep: deviation against i_discrete, one curve per I_bin.
171
172 This is the figure the M2R ladder could not draw. M2R swept I_bin at fixed
173 i_discrete and found d_111 flat at -5 to -7%; the convergence axis for that
174 observable is i_discrete, so the ladder was moving along a contour of the
175 error surface rather than down it. Plotted this way the curves collapse:
176 the spread BETWEEN I_bin values at one i_discrete is small, while the fall
177 ALONG i_discrete is the whole effect.
178 """
179 rid, ref = ref_record(B)
180 if ref is None:
181 print(" no exact arm; skipping the 2-D figure")
182 return None
183 fig, axes = plt.subplots(3, 2, figsize=(15, 15))
184 cmap = plt.get_cmap("viridis")
185 drawn = False
186 for ax, (key, name, _unit) in zip(axes.ravel(), OBS):
187 for c, nb in enumerate(manifest_mod.M2D_BINS):
188 xs, ys = [], []
189 for i_d in manifest_mod.M2D_IDISC:
190 v = scored(B.get(f"M2D_I{i_d}_B{nb}"))
191 if v is None or not ref[key]:
192 continue
193 xs.append(i_d)
194 ys.append((v[key] - ref[key]) / ref[key] * 100.0)
195 if xs:
196 drawn = True
197 ax.plot(xs, ys, "o-", ms=9,
198 color=cmap(c / max(1, len(manifest_mod.M2D_BINS) - 1)),
199 label=f"$I_{{\\rm bin}}$ = {nb}")
200 ax.axhline(0.0, color="tomato", ls="--", lw=3)
201 ax.set_xscale("log")
202 ax.set_xlabel(r"$i_{\rm discrete}$")
203 ax.set_ylabel(f"{name} dev (%)")
204 ax.grid(True, which="both", alpha=0.25)
205 if not drawn:
206 plt.close(fig)
207 print(" no completed 2-D cells yet")
208 return None
209 axes.ravel()[0].legend(loc="best", frameon=True, framealpha=0.92)
210 fig.tight_layout()
211 OUT.mkdir(parents=True, exist_ok=True)
212 p = OUT / "M_2d_sweep.pdf"
213 fig.savefig(p, bbox_inches="tight")
214 plt.close(fig)
215 return p
216
217
218def main():
219 B = board()
220 for f in (convergence, two_d):
221 p = f(B)
222 if p:
223 print(f" wrote {p.relative_to(REPO)}")
224 return 0
225
226
227if __name__ == "__main__":
228 sys.exit(main())