RadCluster
Graph-based cluster dynamics for irradiated materials
Loading...
Searching...
No Matches
verify.py
Go to the documentation of this file.
1#!/usr/bin/env python
2"""Close the self-learning loop: turn UNVERIFIED candidates into measurements.
3
4`learn.py` will not score an observable that has not survived a change of grid
5EXTENT (see learn.EXTENT_TOL). That is the right rule -- refinement at fixed
6extent proved nothing, and occupancy proved less -- but on its own it only
7*labels* the problem. This script is the other half: it finds the rows the
8ledger most wants verified and emits the runs that would verify them.
9
10 python verify.py # show what needs verifying, and the commands
11 python verify.py --run --workers 6
12
13THE ONE INVARIANT THAT MATTERS. `run_ensemble` builds `theta_hash` from every
14design column EXCEPT row_id / cond_row_id / matrix / base_idx / param_j /
15condition -- and `theta_id` IS included. A verification row must therefore be a
16byte-for-byte copy of the original design row with ONLY row_id and cond_row_id
17changed. Renumber `theta_id` and the hash changes, the pair never forms, and
18the run is silently wasted. This is not hypothetical: the V12/V13/V15 designs
19renumber theta_id and so cannot pair with their own parents.
20"""
21from __future__ import annotations
22
23import argparse
24import csv
25import json
26import subprocess
27import sys
28from pathlib import Path
29
30HERE = Path(__file__).resolve().parent
31LEDGER = HERE / "calibration_ledger.json"
32RESULTS = HERE / "results"
33
34# Columns run_ensemble excludes from theta_hash; only these may be rewritten.
35REWRITABLE = ("row_id", "cond_row_id")
36
37
38def recover_V(row: dict):
39 occ, mn = row.get("occ_v"), row.get("mean_n_v")
40 if not occ or mn is None or occ <= 0:
41 return None
42 return int(round(mn / occ))
43
44
45def load_result_rows() -> dict:
46 """row_id -> raw result row (last one wins; they are replicates)."""
47 out = {}
48 for f in sorted(RESULTS.glob("*.jsonl")):
49 for line in f.read_text(encoding="utf-8", errors="replace").splitlines():
50 line = line.strip()
51 if not line:
52 continue
53 try:
54 r = json.loads(line)
55 except json.JSONDecodeError:
56 continue
57 out.setdefault(str(r.get("row_id")), r)
58 return out
59
60
61def candidates(led: dict, top: int) -> list:
62 """Unverified rows, best first.
63
64 Ranked by how much a verification would TELL US: rows already sitting in
65 several bands are the ones whose status the campaign is actually resting on,
66 so they are the ones worth the CPU.
67 """
68 out = []
69 for stem, st in led.get("stages", {}).items():
70 if not st.get("in_scope"):
71 continue
72 for r in st.get("rows", []):
73 if not r.get("valid") or r.get("n_verified", 0) > 0:
74 continue
75 out.append({"stage": stem, "design": st.get("design"),
76 "row_id": str(r.get("row_id")), "label": r.get("label"),
77 "n_in_range": r.get("n_in_range", 0),
78 "log_distance": r.get("log_distance", 1e9)})
79 out.sort(key=lambda c: (-c["n_in_range"], c["log_distance"]))
80 return out[:top]
81
82
83def main(argv=None) -> int:
84 ap = argparse.ArgumentParser(description=__doc__,
85 formatter_class=argparse.RawDescriptionHelpFormatter)
86 ap.add_argument("--top", type=int, default=8,
87 help="how many unverified rows to verify (default 8)")
88 ap.add_argument("--factor", type=int, default=4,
89 help="grid-extent multiplier (default 4)")
90 ap.add_argument("--run", action="store_true", help="launch, don't just print")
91 ap.add_argument("--workers", type=int, default=4)
92 ap.add_argument("--tag", default="AUTOVERIFY")
93 a = ap.parse_args(argv)
94
95 if not LEDGER.exists():
96 print("no ledger - run learn.py first", file=sys.stderr)
97 return 2
98 led = json.loads(LEDGER.read_text(encoding="utf-8"))
99 res = load_result_rows()
100 cands = candidates(led, a.top)
101 if not cands:
102 print("nothing to verify: every valid in-scope row already has an "
103 "extent pair.")
104 return 0
105
106 # Group by (design file, original grid config). Rows run on different grids
107 # cannot share one verification command.
108 groups: dict = {}
109 skipped = []
110 for c in cands:
111 raw = res.get(c["row_id"])
112 if raw is None or not c["design"]:
113 skipped.append((c["row_id"], "no raw result or no design file"))
114 continue
115 V = recover_V(raw)
116 if not V:
117 skipped.append((c["row_id"], "cannot recover V (no occ_v)"))
118 continue
119 key = (c["design"], V, raw.get("bin_V_bin"), raw.get("bin_I_bin"),
120 raw.get("bin_i_discrete"), raw.get("bin_v_discrete"),
121 raw.get("equations"))
122 groups.setdefault(key, []).append((c, raw))
123
124 print(f"{len(cands)} unverified candidates -> {len(groups)} verification run(s)\n")
125 cmds = []
126 for gi, ((design, V, vbin, ibin, idisc, vdisc, eqs), items) in \
127 enumerate(sorted(groups.items(), key=lambda kv: str(kv[0]))):
128 src = HERE / design
129 if not src.exists():
130 skipped.append((design, "design file missing"))
131 continue
132 by_id = {r["row_id"]: r for r in csv.DictReader(src.open(encoding="utf-8"))}
133 hdr = list(next(iter(by_id.values())).keys())
134 newV = V * a.factor
135 # Hold the bin RATIO fixed while the extent grows, so the test isolates
136 # EXTENT from RESOLUTION. r = (V/v_discrete)**(1/v_bin).
137 import math
138 vd = max(int(vdisc or 5), 1)
139 if vbin and V > vd:
140 r_bin = (V / vd) ** (1.0 / int(vbin))
141 newvbin = max(2, int(round(math.log(newV / vd) / math.log(r_bin))))
142 else:
143 newvbin = vbin
144 out_rows, picked = [], []
145 for off, (c, _raw) in enumerate(items):
146 drow = by_id.get(c["row_id"])
147 if drow is None:
148 skipped.append((c["row_id"], f"row not in {design}"))
149 continue
150 nr = dict(drow) # VERBATIM copy ...
151 nid = str(900000 + gi * 100 + off) # ... except the two safe fields
152 nr["row_id"] = nid
153 if "cond_row_id" in nr:
154 nr["cond_row_id"] = nid
155 out_rows.append(nr)
156 picked.append(f"{c['row_id']}({c['label'] or '-'}, {c['n_in_range']}/6)")
157 if not out_rows:
158 continue
159 name = f"{a.tag}_g{gi}"
160 dpath = HERE / "design" / f"{name}.csv"
161 with dpath.open("w", newline="", encoding="utf-8") as fh:
162 w = csv.DictWriter(fh, fieldnames=hdr)
163 w.writeheader()
164 w.writerows(out_rows)
165 cmd = ["python", "run_ensemble.py",
166 "--design", f"design/{name}.csv",
167 "--conditions", "conditions_S8.json", "--spec", "parameters_S4.json",
168 "--out", f"results/{name}.jsonl",
169 "--machine", "0", "--of", "1",
170 "--equations", str(eqs or "bin_moment"),
171 "--i-discrete", str(idisc or 100), "--i-bin", str(ibin or 36),
172 "--v-discrete", str(vd), "--v-bin", str(newvbin),
173 "--allow-mixed", "--I", "80000", "--V", str(newV),
174 "--dose", "15.0", "--lnl", "1", "--rtol", "1e-5",
175 "--solver-mode", "full_system", "--timeout-s", "43200",
176 "--workers", str(a.workers), "--omp-threads", "1"]
177 cmds.append(cmd)
178 print(f"[{name}] V {V} -> {newV} (v_bin {vbin} -> {newvbin}, ratio held)")
179 for p in picked:
180 print(f" {p}")
181 print(" " + " ".join(cmd) + "\n")
182
183 if skipped:
184 print("skipped:")
185 for rid, why in skipped:
186 print(f" {rid}: {why}")
187
188 if a.run:
189 for cmd in cmds:
190 print("launching:", " ".join(cmd[-6:]))
191 subprocess.Popen(cmd, cwd=HERE,
192 stdout=open(HERE / "results" / "verify.log", "a"),
193 stderr=subprocess.STDOUT)
194 print(f"\n{len(cmds)} run(s) launched. Re-run learn.py when they finish; "
195 "pairs form automatically.")
196 else:
197 print("dry run - pass --run to launch.")
198 return 0
199
200
201if __name__ == "__main__":
202 raise SystemExit(main())
recover_V(dict row)
Definition verify.py:38
dict load_result_rows()
Definition verify.py:45
list candidates(dict led, int top)
Definition verify.py:61
int main(argv=None)
Definition verify.py:83