RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
rescore.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3rescore.py — re-apply the observation operator to runs already computed.
4
5 python rescore.py --dry-run # show what would change
6 python rescore.py # rewrite and publish the corrected records
7
8WHY. The dose ladder scored the wrong dose and said it had not (b5e9a51): the
9end point rounded past the final grid point, and a requested dose that was not
10itself a grid point silently took the last point BELOW it, however far below.
11On the exact arm `T4_D_4k` the "0.2 dpa" column carried 0.0665 dpa numbers -- 3x
12low -- while `reached_all_scoring_doses` reported True. Every run computed
13before that fix carries columns labelled with doses they were not taken at.
14
15The trajectories are sound; only the extraction was wrong. So this re-reads
16each run's saved solution and re-runs `run_ensemble.observe` under the current
17code, rather than recomputing 17 hours of solver time.
18
19SCOPE. A run can only be re-scored on the machine that produced it: the
20trajectory lives in `output/<stamp>_<run_id>_.../plots/plot_data.pkl`, and
21`output/` is gitignored (a 38 MB pickle per run, and one of live objects that
22will not survive a class refactor -- see .gitignore). What crossed the wire is
23the compact record, which is the thing being corrected. Run this on EVERY
24machine that computed rows; each fixes its own and pushes.
25"""
26from __future__ import annotations
27
28import argparse
29import json
30import sys
31import time
32import types
33from pathlib import Path
34
35HERE = Path(__file__).resolve().parent
36DT = HERE.parent
37MOD = DT.parent
38REPO = MOD.parent
39for p in (str(REPO), str(DT)):
40 if p not in sys.path:
41 sys.path.insert(0, p)
42
43import runs as manifest_mod # noqa: E402
44from gitsync import CampaignSync # noqa: E402
45
46
48 """Map run_id -> newest output directory produced on this machine."""
49 ids = {e["run_id"] for e in manifest_mod.manifest()}
50 found = {}
51 for d in sorted((MOD / "output").glob("*/"), reverse=True):
52 if not (d / "plots" / "plot_data.pkl").exists():
53 continue
54 name = d.name
55 # <stamp>_<run_id>_<solver_mode>_... -- match the longest id that fits,
56 # so T4_C4_P1_4k is not mistaken for T4_C4_4k.
57 for rid in sorted(ids, key=len, reverse=True):
58 if f"_{rid}_" in name and rid not in found:
59 found[rid] = d
60 break
61 return found
62
63
64def rescore(rid, run_dir, entry):
65 """Recompute the observables for one run from its saved solution."""
66 from radcluster_code.py_utils import visualization as viz
67 import run_ensemble as re_mod
68
69 d = viz.load_plot_data(str(run_dir / "plots" / "plot_data.pkl"))
70 res, idata = d["results"], d["input_data"]
71 sim = types.SimpleNamespace(input_data=idata)
72
73 cfg = {"I": entry["I"], "V": entry["V"], "dose": float(entry["dose"]),
74 "C_floor": float(idata.reactions.get("C_floor", 1e-15))}
75 out = re_mod.observe(res, sim, cfg, 1.0)
76
77 reads = entry["dose_read"]
78 reads = reads if isinstance(reads, (list, tuple)) else [reads]
79 lad = out.get("at_dose") or {}
80 scored = {}
81 for target in reads:
82 key = f"{float(target):g}"
83 got = lad.get(key)
84 scored[key] = got if got else {"missing": True,
85 "dose_reached": out.get("dose_reached")}
86 out["scored"] = scored
87 out["reached_all_scoring_doses"] = all(
88 not v.get("missing") for v in scored.values())
89 return out
90
91
92def main(argv=None):
93 ap = argparse.ArgumentParser(description=__doc__,
94 formatter_class=argparse.RawDescriptionHelpFormatter)
95 ap.add_argument("--dry-run", action="store_true",
96 help="report what would change; publish nothing")
97 ap.add_argument("--only", default=None, help="restrict to one run_id")
98 ap.add_argument("--branch", default="campaign-verification")
99 a = ap.parse_args(argv)
100
101 entries = {e["run_id"]: e for e in manifest_mod.manifest()}
102 found = find_runs()
103 if a.only:
104 found = {k: v for k, v in found.items() if k == a.only}
105 if not found:
106 print("no re-scoreable runs on this machine "
107 "(need output/<...>/plots/plot_data.pkl)")
108 return 0
109
110 sync = CampaignSync(REPO, branch=a.branch)
111 sync.ensure()
112 claims = sync.claims()
113
114 print(f"{len(found)} run(s) with a saved solution on this machine\n")
115 changed = []
116 for rid in sorted(found):
117 entry, run_dir = entries[rid], found[rid]
118 try:
119 new = rescore(rid, run_dir, entry)
120 except Exception as exc:
121 print(f" {rid:14s} RESCORE FAILED: {type(exc).__name__}: {exc}")
122 continue
123 old = claims.get(rid, {})
124 old_sc, new_sc = old.get("scored") or {}, new["scored"]
125 moved = []
126 for k in sorted(new_sc, key=float):
127 o, n = old_sc.get(k) or {}, new_sc[k]
128 od, nd = o.get("dose"), n.get("dose")
129 if od is None or nd is None:
130 moved.append(f"{k}: {'MISSING' if o.get('missing') else od} -> "
131 f"{'MISSING' if n.get('missing') else nd}")
132 elif abs(od - nd) > 1e-9 * max(abs(od), 1.0):
133 moved.append(f"{k} dpa: scored at {od:.4g} -> {nd:.4g}"
134 f" (d100 {o.get('d_100_nm', float('nan')):.3f}"
135 f" -> {n.get('d_100_nm', float('nan')):.3f} nm)")
136 flag = "" if new["reached_all_scoring_doses"] else " [did not reach all]"
137 print(f" {rid:14s} {'CHANGED' if moved else 'unchanged'}{flag}")
138 for m in moved:
139 print(f" {m}")
140 if moved:
141 changed.append((rid, new))
142
143 if a.dry_run:
144 print(f"\ndry run: {len(changed)} record(s) would be republished")
145 return 0
146
147 for rid, new in changed:
148 rec = dict(claims.get(rid, {}))
149 rec.update(new)
150 rec["rescored_at"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
151 rec["rescore_reason"] = ("dose ladder fix b5e9a51: end-point rounding "
152 "and off-grid checkpoints scored the wrong dose")
153 ok = sync.publish(rid, rec, {"observables.json":
154 json.dumps(rec, indent=2, sort_keys=True,
155 default=str) + "\n"})
156 print(f" republished {rid}: {'ok' if ok else 'PUSH FAILED'}")
157 print(f"\n{len(changed)} record(s) corrected")
158 return 0
159
160
161if __name__ == "__main__":
162 sys.exit(main())
main(argv=None)
Definition rescore.py:92
find_runs()
Definition rescore.py:47