battery: reference_mismatch is an annotation, not a skip

Owner decision: an outlier that has an explanation stays in the battery, because it tells
something; removing it makes the results opaque. So 3mc4 and 3r6o run the REFMAC and
depositor-data checks like every other set again, and their numbers count in the plots and
statistics. The manifest's reference_mismatch note (the frames are a ~30% twinned crystal,
the deposition another, untwinned one) is printed in the per-set table's note column and
under the REFMAC R_free ratio plot. battery.py is back to its state before 52025483d.

Checked: battery recheck of 20261017-1139_e25876_twin-isa-mismatch - REFMAC ran on both
(R_free ratio 1.362 / 1.363, dep_cc_delta_all -0.163 / -0.319) and the report shows the note
beside both rows; test_score.py OK.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
This commit is contained in:
2026-10-17 13:45:50 +02:00
co-authored by Claude Opus 5.5
parent 7c1befd484
commit c346597a48
3 changed files with 22 additions and 32 deletions
+3 -4
View File
@@ -860,10 +860,9 @@ whole range; the manifest keeps the rule used as `dmin_rule`.
reference (`sg`, `sgno`, `cell`, `dmin`) and tags. Set `"pinned": true` when the choice between
sweeps was deliberate. Record the source and DOI in `docs/EXTERNAL_TEST_DATA.md`.
A set whose frames are not the crystal the deposited data came from (checked independently,
e.g. with XDS on the same frames) gets `"reference_mismatch": "<why>"`: the REFMAC check and the
depositor-data comparison are skipped (`refmac_status` / `dep_status` `skipped`, the reason in
`refmac_reason` / `dep_reason` and in the report), while space group, cell and d_min are still
scored against the deposition.
e.g. with XDS on the same frames) gets `"reference_mismatch": "<why>"`. It is an annotation only:
every check still runs and every number still counts; the report prints the note beside the set's
row and under the REFMAC plot, so an outlier that has an explanation stays visible with it.
- **Small molecules** carry `"cod": "<COD id>"`, the published structure the SHELXL check refines
(pick an entry whose Niggli-reduced cell and space group match the row's reference; prefer a
modern, ordered, low-R one).
+3 -21
View File
@@ -477,7 +477,7 @@ def run_one(run_dir, binary, e, opts):
if e.get("timing"):
row.update(timing.check(e["timing"], row, wd, reference))
elif e["reference"] == "deposition" and row["model"] and os.path.exists(os.path.join(wd, "p.mtz")):
row.update(check_depdata(e, wd, opts["pdb_cache"]))
row.update(depdata_check.check_set(wd, e["id"], opts["pdb_cache"]))
if opts["model_check"] and e["arm"] == "open":
row.update(check_model(e, wd))
if e.get("cod"):
@@ -496,20 +496,6 @@ REFMAC_KEYS = ("refmac_rfree", "refmac_rwork", "refmac_rfree_depflags", "refmac_
"refmac_reason", "refmac_refined_ratio") + tuple("refmac_" + k for k in REFMAC_REFINED)
def mismatch_reason(e):
"""Why the deposited data are not a reference for this set's frames (open.json's
`reference_mismatch`), or None: the REFMAC and depositor-data checks are then skipped."""
return e.get("reference_mismatch") and "reference mismatch: " + e["reference_mismatch"]
def check_depdata(e, wd, pdb_cache):
"""Our merge against the depositor's data (depdata_check.py), unless the manifest says the
deposition is not a reference for these frames."""
if mismatch_reason(e):
return dict({k: None for k in depdata_check.KEYS}, dep_status="skipped", dep_reason=mismatch_reason(e))
return depdata_check.check_set(wd, e["id"], pdb_cache)
def check_model(e, wd):
"""R-free of the deposited model against this run's merge, by REFMAC (model_check.py)."""
out = {k: None for k in REFMAC_KEYS}
@@ -517,8 +503,6 @@ def check_model(e, wd):
mtz = os.path.join(wd, "p.mtz")
if not PDB_ID.match(pdb):
return dict(out, refmac_status="skipped", refmac_reason="not a PDB entry")
if mismatch_reason(e):
return dict(out, refmac_status="skipped", refmac_reason=mismatch_reason(e))
if not os.path.exists(mtz):
return dict(out, refmac_status="skipped", refmac_reason="no merged MTZ")
try:
@@ -1005,7 +989,7 @@ 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(check_depdata(e, wd, pdb_cache))
out.update(depdata_check.check_set(wd, e["id"], pdb_cache))
out.update(check_model(e, wd))
return out
@@ -1047,10 +1031,8 @@ def cmd_recheck(a, site):
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))
mismatch = {e["id"]: e.get("reference_mismatch") for e in load_sets(site, "open")}
with concurrent.futures.ProcessPoolExecutor(a.jobs) as pool:
futures = {pool.submit(recheck_one, {"id": r["set"], "model": r.get("model"),
"reference_mismatch": mismatch.get(r["set"])}, wd, pdb_cache): r
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]
+16 -7
View File
@@ -201,6 +201,14 @@ ALIASES = {}
# {arm: {set id: manifest row}} of today's manifests (battery.py sets it); rescore() scores with them
MANIFESTS = {}
def reference_note(r):
"""The manifest's reference_mismatch note for a row (why its deposition is a poor reference for
these frames), or None. An annotation only: the row is scored and plotted as any other."""
note = (MANIFESTS.get(r["arm"], {}).get(r["set"]) or {}).get("reference_mismatch")
return note and "reference mismatch: " + note
# the site's cache of deposited models and structure factors (battery.py sets it)
PDB_CACHE = None
@@ -523,9 +531,9 @@ def build(run_dir, baseline=None, allow_incomplete=False):
"REFMAC check (--model-check): R_free of the deposited model against our merge / against "
"the deposited structure factors, both on the depositor's free set with the same protocol, "
"one point per set, sorted; below 1 = our data fit the model better")
skipped = [r for r in res if r.get("refmac_status") == "skipped" and r["arm"] == "open"]
if skipped:
doc.p("REFMAC check skipped: " + "; ".join(f"{r['set']} ({r['refmac_reason']})" for r in skipped))
notes = [r for r in res if reference_note(r)]
if notes:
doc.p("Read with the manifest's note: " + "; ".join(f"{r['set']} - {reference_note(r)}" for r in notes))
dep = [r for r in res if r.get("dep_status") == "ok"]
if dep:
@@ -547,9 +555,9 @@ def build(run_dir, baseline=None, allow_incomplete=False):
doc.table(["set", "dep. data", "our d_min", "dep. d_min", "common refl.", "dCC all",
"dCC outer 2", "CC past limit", "to d A", "REFMAC ratio", "ISa", "tags"],
rows, num=range(2, 11))
other = [r for r in res if r.get("dep_status") not in (None, "ok")]
if other:
doc.p("No depositor-data comparison: " + "; ".join(f"{r['set']} ({r['dep_reason']})" for r in other))
other = [r for r in res if r.get("dep_status") not in (None, "ok")]
if other:
doc.p("No comparison: " + "; ".join(f"{r['set']} ({r['dep_reason']})" for r in other))
xds = [r for r in res if r.get("refres_range")]
if xds:
@@ -670,7 +678,8 @@ def build(run_dir, baseline=None, allow_incomplete=False):
[h for h, _ in model] + (["model fit"] if model else []) + ["time s", "note"]
rows = []
for r in sorted(res, key=lambda r: (order.get(r["arm"], 9), r["set"])):
note = "; ".join(x for x in (r.get("reason"), r.get("sg_label") and f"labelled {r['sg_label']} from the model")
note = "; ".join(x for x in (r.get("reason"), r.get("sg_label") and f"labelled {r['sg_label']} from the model",
reference_note(r))
if x)
rows.append([r["set"], r["arm"], r["verdict"], r.get("sg") or "-", r.get("sg_ref") or "-",
f(r.get("cell_dev_pct")), f(r.get("volume_ratio"), "{:.3f}"),