Files
Jungfraujoch/tools/battery/timing.py
T
leonarski_fandClaude Opus 5.5 234774ff09
Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m30s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 11m43s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 7m21s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 14m56s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 20m57s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m54s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 15m14s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 11m35s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 26m0s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 17m21s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m35s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 20m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 20m41s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 22m5s
Build Packages / Build documentation (push) Successful in 1m0s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 14m33s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 21m10s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 15m21s
Build Packages / Unit tests (push) Successful in 1h26m10s
Build Packages / Validation Battery (push) Failing after 1h6m19s
battery: a "standard" timing benchmark on open data, with every model committed
battery.py timing --benchmark standard (now the default) runs 9zlo, 9s02 and
9gjx from their AlphaFold DB models (AF-P17090-F1, AF-P32322-F1, AF-Q65MG6-F1
v6, with their PAE, as the 2026-10-16 AlphaFold MR scan used them; all three
GREEN there) and 4nwv from its deposited model moved by the existing fixed
transform. --benchmark inhouse keeps the earlier myob_x10sa / cytc_x10sa /
4nwv list. Same checks; expected groups are the deposited ones, R-free <= 0.50
for the predictions (0.35 for 4nwv), placement CC >= 0.30.

So that a machine needs only the raw data:
- the deposited coordinates of 9zlo, 9s02, 9gjx, 4nwv and 4dc8 (the inhouse
  myoglobin reference) are committed in tools/battery/models/pdb/ as the
  compressed mmCIF RCSB serves (CC0; 50-660 kB each, 1.6 MB together), and
  model_check.deposition reads an entry there before the pdb_cache, taking
  its refinement record from the file itself - no cache JSON, no network;
- open/inhouse arms default their manifest and reference, so a site config
  is just runs_root plus the data roots (site.timing.example.json);
- baseline_timing is now per benchmark ({"standard": RUN, "inhouse": RUN}).

Verified on the workstation through the GPU lock with a site config whose
pdb_cache is an empty directory (still empty afterwards): standard 4/4 pass
(9zlo 19 s, 9s02 30 s, 9gjx 21 s, 4nwv 89 s), inhouse 3/3 pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
2026-10-16 16:49:04 +02:00

214 lines
10 KiB
Python

"""The timing benchmarks: a few sets, each started from a model rugnux has to place by molecular
replacement, timed per step and checked for a right answer.
timing.json lists the sets of each benchmark (README, 'The timing benchmark'): 'standard', three
open-arm EIGER 16M sets and 4nwv, needs nothing but the open data; 'inhouse' is two in-house standard
proteins and 4nwv. A set is started from its AlphaFold DB prediction (models/, committed with its
predicted aligned error) or from its deposited model moved by a fixed rigid-body transform, made at
run time from the entry committed in models/pdb/ (`start_model`). Each runs the battery's own command
for its arm with --model <start model>. A set passes (`check`) when every one of these holds:
space_group the battery's own verdict (score.judge: lattice and space group against the
reference, the hand excepted) is a pass
placed MODEL_PLACEMENT is MR: molecular replacement placed the model, and the R-factors,
maps and model written are of that placement
mr_group MR_SPACE_GROUP is the expected group, hand included - the model decides it
r_free R_FREE <= rfree_max (a random structure: 0.586 acentric, Wilson 1950)
confirmed CC_MODEL_CONFIRMED_TO_D_MIN is not NONE: some shell's CC(model, data) clears
rugnux's empirical null
placement_cc the placed model (p_model.pdb) has the reference model's intensities: their
correlation, shell by shell normalised, 10-4 A, the best over the reindexing
operators the two lattices allow, is >= cc_min. Independent of rugnux's own scores:
|F| is the same for every origin the space group allows and every symmetry copy, so
a right placement correlates highly and a wrong one (or the wrong hand) near zero.
"""
import gzip
import json
import math
import os
import re
import gemmi
import numpy as np
import model_check
import score
HERE = os.path.dirname(os.path.abspath(__file__))
MANIFEST = os.path.join(HERE, "timing.json")
CC_D_MAX, CC_D_MIN = 10.0, 4.0 # the placement check's resolution range (A)
CC_SHELLS = 8 # equal-count shells for the normalisation
STEP_LINE = re.compile(r"TIMING step (.+?)(?:, \d+ images)? ([\d.]+)$")
def names():
return list(json.load(open(MANIFEST))["benchmarks"])
def load(benchmark):
return json.load(open(MANIFEST))["benchmarks"][benchmark]
def read_model(path):
"""A model with strict NCS expanded (as rugnux's validation does) and no hydrogens or waters."""
st = gemmi.read_structure(path)
st.setup_entities()
if any(not op.given for op in st.ncs):
st.expand_ncs(gemmi.HowToNameCopiedChain.Short)
st.remove_hydrogens()
st.remove_waters()
return st
def rotation(axis, deg):
x, y, z = np.asarray(axis, float) / np.linalg.norm(axis)
c, s = math.cos(math.radians(deg)), math.sin(math.radians(deg))
t = 1 - c
return [[c + x * x * t, x * y * t - z * s, x * z * t + y * s],
[y * x * t + z * s, c + y * y * t, y * z * t - x * s],
[z * x * t - y * s, z * y * t + x * s, c + z * z * t]]
def start_model(start, wd, pdb_cache):
"""The file given to --model: the committed prediction, or the deposited model (models/pdb/,
else the pdb_cache) moved by the manifest's transform, written into the set's work directory."""
if start.get("alphafold"):
return os.path.join(HERE, start["alphafold"])
mv = start["moved"]
meta = model_check.deposition(mv["pdb"], pdb_cache)
if meta is None:
return None
st = read_model(meta["xyz"])
pos = np.array([a.pos.tolist() for ch in st[0] for r in ch for a in r])
centre = pos.mean(axis=0)
rot = np.array(rotation(mv["axis"], mv["deg"]))
tr = gemmi.Transform(gemmi.Mat33(rot.tolist()),
gemmi.Vec3(*(centre - rot @ centre + np.asarray(mv["shift"], float))))
st[0].transform_pos_and_adp(tr)
out = os.path.join(wd, f"{mv['pdb']}_moved.cif.gz")
with gzip.open(out, "wt") as fh:
fh.write(st.make_mmcif_document().as_string())
return out
def intensities(st):
hkl, f = model_check.fcalc(st, CC_D_MIN)
return hkl, f.astype(float) ** 2
def normalised(i, d):
"""Each intensity over its shell's mean (equal-count shells in d)."""
out = np.empty_like(i)
for sel in np.array_split(np.argsort(d), CC_SHELLS):
out[sel] = i[sel] / max(i[sel].mean(), 1e-30)
return out
def placement_cc(placed_path, reference_path):
"""Correlation of the placed model's intensities with the reference model's, each in its own cell
and space group, the placed model's indices carried into the reference's frame by every change of
basis the two lattices allow; the best one."""
placed, ref = read_model(placed_path), read_model(reference_path)
sg_p, sg_r = placed.find_spacegroup(), ref.find_spacegroup()
hp, ip = intensities(placed)
hr, ir = intensities(ref)
d_r = ref.cell.calculate_d_array(hr)
keep = d_r <= CC_D_MAX
hr, ir = hr[keep], ir[keep]
best = None
for t in model_check.basis_changes(placed.cell, sg_p, ref.cell, sg_r):
hm = hp @ t
whole = np.all(np.abs(hm - np.round(hm)) < 1e-3, axis=1)
n = int(whole.sum())
h, i, _, _ = model_check.to_group(np.round(hm[whole]).astype(int), ip[whole], np.ones(n),
np.zeros(n, int), ref.cell, sg_r, np.arange(n))
_, a, b = np.intersect1d(model_check.hkl_key(h), model_check.hkl_key(hr), return_indices=True)
if len(a) < 100:
continue
d = ref.cell.calculate_d_array(hr[b])
cc = float(np.corrcoef(normalised(i[a], d), normalised(ir[b], d))[0, 1])
if best is None or cc > best[0]:
best = (cc, len(a), model_check.op_string(t))
return best
def steps(wd):
"""{step: seconds} from the TIMING step lines of rugnux's log, in run order."""
out = {}
log = os.path.join(wd, "p_log.txt")
if os.path.exists(log):
for line in open(log, errors="replace"):
m = STEP_LINE.search(line.rstrip())
if m:
out[m.group(1)] = out.get(m.group(1), 0.0) + float(m.group(2))
return out
def check(t, row, wd, reference_path):
"""The benchmark's verdict on one finished set: bench_checks {name: [ok, value, limit]},
bench_verdict pass/fail, bench_reason (the checks that failed) and steps."""
rep = score.read_report(os.path.join(wd, "p_report.txt"))
exp = t["expect"]
mr_sg = gemmi.find_spacegroup_by_name(rep.get("MR_SPACE_GROUP") or "")
checks = {
"space_group": [row["verdict"] == "pass", row.get("sg"), row.get("sg_ref")],
"placed": [rep.get("MODEL_PLACEMENT") == "MR", rep.get("MODEL_PLACEMENT"), "MR"],
"mr_group": [bool(mr_sg) and mr_sg.number == exp["mr_sgno"], rep.get("MR_SPACE_GROUP"),
gemmi.find_spacegroup_by_number(exp["mr_sgno"]).hm],
"r_free": [row.get("rfree") is not None and row["rfree"] <= exp["rfree_max"], row.get("rfree"),
exp["rfree_max"]],
"confirmed": [rep.get("CC_MODEL_CONFIRMED_TO_D_MIN") not in (None, "NONE"),
rep.get("CC_MODEL_CONFIRMED_TO_D_MIN"), "not NONE"],
}
placed = os.path.join(wd, "p_model.pdb")
cc = None
if os.path.exists(placed) and reference_path:
cc = placement_cc(placed, reference_path)
checks["placement_cc"] = [cc is not None and cc[0] >= exp["cc_min"], cc and round(cc[0], 3), exp["cc_min"]]
bad = [k for k, (ok, _, _) in checks.items() if not ok]
return {"bench_checks": checks, "bench_verdict": "fail" if bad else "pass",
"bench_reason": ", ".join(f"{k} {checks[k][1]} (want {checks[k][2]})" for k in bad) or None,
"bench_cc_op": cc and cc[2], "bench_cc_n": cc and cc[1],
"mr_seconds": score.fnum(rep.get("MR_SECONDS")), "mr_decision": rep.get("MR_DECISION"),
"steps": steps(wd)}
def console(rows):
"""One block per set: verdict, wall time, the checks, the step times."""
out = []
for r in rows:
out.append(f"{r['set']:12s} {r.get('bench_verdict') or '-':5s} wall {r.get('wall_s') or 0:7.1f} s"
f" MR {r.get('mr_seconds') or 0:.1f} s {r.get('bench_reason') or ''}")
for k, (ok, v, lim) in (r.get("bench_checks") or {}).items():
out.append(f" {'ok ' if ok else 'BAD'} {k:13s} {v} (want {lim})")
out.append(" " + ", ".join(f"{k} {v:.1f}" for k, v in (r.get("steps") or {}).items()))
return "\n".join(out)
def section(doc, rows):
"""The report's section for a timing run (rows of results.json carrying bench_* keys)."""
rows = [r for r in rows if r.get("bench_checks")]
if not rows:
return
bad = [r for r in rows if r["bench_verdict"] != "pass"]
doc.h(2, f"Timing benchmark: {len(rows) - len(bad)} of {len(rows)} pass")
doc.p("Each set started from a model it has to place by molecular replacement (timing.json). A set "
"passes when the battery's verdict passes, MODEL_PLACEMENT is MR, MR_SPACE_GROUP is the expected "
"group (hand included), R_FREE is at most its limit, CC_MODEL_CONFIRMED_TO_D_MIN is not NONE, "
f"and the placed model's intensities correlate with the reference model's ({CC_D_MAX:.0f}-{CC_D_MIN:.0f} A, "
"shell-normalised) at least as much as its limit.")
names = list(rows[0]["bench_checks"])
doc.table(["set", "verdict", "wall s", "MR s"] + names + ["why"],
[[r["set"], r["bench_verdict"], f"{r['wall_s']:.1f}" if r.get("wall_s") else "-",
f"{r['mr_seconds']:.1f}" if r.get("mr_seconds") else "-"]
+ [("" if r["bench_checks"][k][0] else "BAD ") + str(r["bench_checks"][k][1]) for k in names]
+ [r.get("bench_reason") or ""] for r in rows],
row_class=lambda row: "fail" if row[1] != "pass" else "")
step_names = []
for r in rows:
step_names += [k for k in r.get("steps", {}) if k not in step_names]
doc.table(["step"] + [r["set"] for r in rows],
[[k] + [f"{r['steps'][k]:.1f}" if k in r.get("steps", {}) else "-" for r in rows]
for k in step_names])