battery: recheck subcommand reruns the model checks of an existing run

`battery.py recheck RUN [--only a,b] [--jobs N]` reruns model_check.py
(REFMAC) and depdata_check.py on every open-arm row of a finished run from
its own p.mtz, and updates the refmac_* / dep_* fields of results.json in
place (verdicts untouched). Previous results.json, report and each set's
model_check directory are kept with a .pre-recheck-<stamp> suffix; the
manifest gains a "rechecks" entry {date, end, runner_git, runner_dirty, sets,
fields, previous_results, command} so the tools version behind each published
number is auditable. The report is re-rendered and the run made read-only
again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-09-25 20:50:22 +02:00
1 parent ae444437f4
commit 77f2ce1092
2 files changed
+92 -1

No files matched your search

+13
View File
@@ -258,6 +258,19 @@ set (`refmac_reason` says why). The runner records `refmac_rfree`, `refmac_rwork
`model_check.py <p.mtz> <pdb id> --workdir DIR` runs it on one merge and prints every field
(setting, reindexing operators of both data sets, what the depositor's amplitudes were made from).
### Rechecking an existing run
`battery.py recheck RUN [--only a,b] [--jobs N]` reruns the model check and the depositor-data
comparison on every open-arm row of a finished run, from the run's own `p.mtz` (CPU only; rugnux is
not run again), so numbers made by an older `model_check.py` / `depdata_check.py` can be brought up
to date without reprocessing. It updates the `refmac_*` and `dep_*` fields of `results.json` in
place and nothing else (verdicts stay as scored). The previous files stay beside the new ones:
`results.json.pre-recheck-<stamp>`, `report.{md,html}.pre-recheck-<stamp>` and each set's
`model_check.pre-recheck-<stamp>/`. The report is re-rendered, and `manifest.json` gains an entry
in its `rechecks` list - `date`, `end`, `runner_git`, `runner_dirty` (the tools version that made
the new numbers), `sets`, `fields`, `previous_results` and `command` - so which version of the
checks produced which figures stays auditable. The run directory is made read-only again.
### The depositor-data comparison (open arm, always on)
`tools/battery/depdata_check.py` compares our merge with the depositor's own data without CCP4:
+79 -1
View File
@@ -18,6 +18,8 @@ Usage (README.md in this directory is the full guide):
battery.py compare RUN_A RUN_B [--all] [--rerun-changed]
battery.py report RUN --out DIR [--baseline RUN] re-render a run's report into DIR/<run>.{md,html}
battery.py abort RUN [--reason TEXT] mark an unfinished run as aborted
battery.py recheck RUN [--only a,b] [--jobs N] rerun the model checks of a finished run's open
arm on its p.mtz files; recorded in the manifest
battery.py discover [--refresh] open arm: propose inputs for new directories
battery.py refs --arm inhouse|private [--write] re-read the XDS references from CORRECT.LP
battery.py remap --arm A MAP.json [--write] follow a rename of the data directories
@@ -37,6 +39,7 @@ A full-arm run (no --only / --tier) takes the site lock and refuses to start if
A run that did not finish is never compared or reported on unless asked (--allow-incomplete).
"""
import argparse
import concurrent.futures
import datetime
import fcntl
import hashlib
@@ -519,6 +522,76 @@ def cmd_report(a, site):
print(write_report(a.run, a.baseline, a.out, a.allow_incomplete))
def recheck_one(e, wd, pdb_cache):
"""The post-processing checks of one open-arm row, on the run's own p.mtz (a worker process)."""
out = {}
if e["model"] and os.path.exists(os.path.join(wd, "p.mtz")):
out.update(depdata_check.check_set(wd, e["id"], pdb_cache))
out.update(check_model(e, wd))
return out
def set_writable(paths):
for p in paths:
os.chmod(p, os.stat(p).st_mode | stat.S_IWUSR)
def cmd_recheck(a, site):
"""Rerun model_check / depdata_check on a finished run's open-arm merges and update its
results.json in place. The previous results.json is kept beside it and the recheck is recorded
in manifest.json ("rechecks": which runner version produced the refmac_* / dep_* numbers)."""
run = os.path.abspath(a.run)
report.load_run(run, False) # refuses a run that did not finish
# the raw files: load_run rescores rows, and results.json keeps the verdicts as they were scored
man = json.load(open(os.path.join(run, "manifest.json")))
results = json.load(open(os.path.join(run, "results.json")))
only = set(a.only.split(",")) if a.only else None
rows = [r for r in results if r["arm"] == "open" and (only is None or r["set"] in only)]
if only and len(rows) != len(only):
sys.exit("not open-arm rows of this run: " + ", ".join(sorted(only - {r["set"] for r in rows})))
if not rows:
sys.exit("nothing to recheck")
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
entry = {"date": datetime.datetime.now().isoformat(timespec="seconds"),
"runner_git": git(HERE, "rev-parse", "--short", "HEAD") or "unknown",
"runner_dirty": bool(git(HERE, "status", "--porcelain", "--", ".")),
"sets": [r["set"] for r in rows],
"fields": list(REFMAC_KEYS + depdata_check.KEYS),
"previous_results": "results.json.pre-recheck-" + stamp,
"command": " ".join(sys.argv)}
pdb_cache = site.get("pdb_cache") or model_check.DEFAULT_CACHE
# a finished run directory is read-only: open up only what the recheck writes
wds = [os.path.join(run, "work", "open", r["set"]) for r in rows]
set_writable([run, os.path.join(run, "results.json"), os.path.join(run, "manifest.json")] + wds)
for wd in wds: # the previous check's files stay, renamed
if os.path.isdir(os.path.join(wd, "model_check")):
set_writable([os.path.join(wd, "model_check")])
os.rename(os.path.join(wd, "model_check"), os.path.join(wd, "model_check.pre-recheck-" + stamp))
shutil.copy2(os.path.join(run, "results.json"), os.path.join(run, "results.json.pre-recheck-" + stamp))
with concurrent.futures.ProcessPoolExecutor(a.jobs) as pool:
futures = {pool.submit(recheck_one, {"id": r["set"], "model": r.get("model")}, wd, pdb_cache): r
for r, wd in zip(rows, wds)}
for n, fut in enumerate(concurrent.futures.as_completed(futures), 1):
r = futures[fut]
r.update(fut.result())
print(f"[{n}/{len(rows)}] {r['set']:24s} {r['refmac_status'] or '-':8s} "
f"R_free depflags {report.f(r.get('refmac_rfree_depflags'), '{:.4f}')} "
f"depdata {report.f(r.get('refmac_rfree_depdata'), '{:.4f}')} "
f"{r.get('refmac_reason') or ''}",
flush=True)
write_json(os.path.join(run, "results.json"), results)
man.setdefault("rechecks", []).append(dict(entry, end=datetime.datetime.now().isoformat(timespec="seconds")))
write_json(os.path.join(run, "manifest.json"), man)
for name in ("report", "report_PRIVATE"):
for ext in (".md", ".html"):
if os.path.exists(os.path.join(run, name + ext)):
set_writable([os.path.join(run, name + ext)])
os.rename(os.path.join(run, name + ext),
os.path.join(run, f"{name}{ext}.pre-recheck-{stamp}"))
print(f"report {write_report(run, man.get('baseline'))}", flush=True)
make_read_only(run)
def cmd_discover(a, site):
"""Propose an input for every open-arm directory the manifest does not have yet; with
--refresh, also show where today's discovery disagrees with the manifest's (unpinned) input."""
@@ -655,6 +728,11 @@ def main():
ab.add_argument("--reason", default="aborted by hand")
ab.add_argument("--force", action="store_true", help="even if its runner still seems alive")
rc = sub.add_parser("recheck")
rc.add_argument("run")
rc.add_argument("--only", help="comma-separated set ids (default: every open-arm row)")
rc.add_argument("--jobs", type=int, default=8, help="sets checked in parallel (CPU; REFMAC)")
d = sub.add_parser("discover")
d.add_argument("--refresh", action="store_true")
@@ -674,7 +752,7 @@ def main():
report.MANIFESTS = {arm: {e["id"]: e for e in load_sets(site, arm)}
for arm, cfg in site["arms"].items() if os.path.exists(cfg["manifest"])}
{"run": cmd_run, "list": cmd_list, "compare": cmd_compare, "report": cmd_report,
"abort": cmd_abort, "discover": cmd_discover, "refs": cmd_refs, "remap": cmd_remap}[a.cmd](a, site)
"abort": cmd_abort, "recheck": cmd_recheck, "discover": cmd_discover, "refs": cmd_refs, "remap": cmd_remap}[a.cmd](a, site)
if __name__ == "__main__":