tools/battery: committed, reproducible validation battery

One runner for the open (vs PDB deposition) and in-house (vs XDS) arms,
replacing the scattered scripts outside the repository. Each run writes a
new, read-only directory named <date>_<rugnux git>_<label> holding a copy
of the binary with its sha256 and build flags, manifest.json (inputs,
references, options, host, GPU sharing) and results.json in one schema
for all arms, plus per-set work directories and a Markdown and a
self-contained HTML report (per-arm and per-population pass rates,
resolution gain, ISa/R_meas distributions, timing, failures, delta vs a
baseline run).

- Dataset lists and references are data files: open.json (151 public
  sets, deposited group/cell/resolution, pinned inputs) and inhouse.json
  (standard test crystals and no-crystal controls, XDS references). The
  private arm's manifest stays outside the repository; a private run gets
  its own run directory and report.
- Scoring is setting-invariant: lattice by Niggli-reduced primitive cell
  (beta vs 180-beta, C2 vs I2), symmetry via sgequiv on the open arm and
  by point group against XDS, then merge usability. Re-scoring the rc169
  and rc170 battery outputs reproduces their verdicts.
- Input discovery (`discover`) ranks sweeps by frame count, never by
  file size or sort order; a run never searches, it runs what the
  manifest names.
- `compare A B` flags per-set changes beyond noise; `--rerun-changed`
  reruns them with A's saved binary to separate real changes from
  run-to-run noise.
- `--tier smoke` selects a 22-set subset spanning the populations; a
  full-arm run takes a lock; `--gpulock` queues on the shared GPU and
  marks the timing as not a reference.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
leonarski_fandClaude Opus 5 committed 2026-09-20 18:45:02 +02:00
1 parent 30b6800289
commit 5e7b53dd7b
10 files changed
+1374

No files matched your search

+3
View File
@@ -39,3 +39,6 @@ common/GitInfo.cpp
# Review reports, work plans and investigation notes. They are working material for one task, they
# go stale the moment the code moves, and Sphinx would publish anything left under docs/.
docs/review/
# Python bytecode (tools/battery and the root scripts)
__pycache__/
+445
View File
@@ -0,0 +1,445 @@
#!/usr/bin/env python3
"""Rugnux validation battery: run, score, report, compare.
Arms and where their truth comes from:
open public PDB depositions (and a few published small-molecule sets); scored against the
deposited space group, cell and resolution. Manifest: open.json (committed).
inhouse standard test crystals measured at PSI (lysozyme, thaumatin, insulin, cytochrome C,
myoglobin) plus no-crystal controls; scored against XDS (point group only - XDS never
tests a screw axis). Manifest: inhouse.json (committed).
private user data, scored like inhouse. Its manifest lives OUTSIDE the repository (path in the
site config) and a private run gets its own run directory and report.
Usage:
battery.py run --rugnux BIN --label L [--arm open,inhouse|private] [--only a,b | --tier smoke]
[--build-dir DIR] [--gpulock] [--threads N] [--unforced] [--baseline RUN]
battery.py compare RUN_A RUN_B [--all] [--rerun-changed]
battery.py report RUN --out DIR [--baseline RUN] re-render a run's report
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
A run writes <runs_root>/<YYYYMMDD-HHMM>_<rugnux git>_<label>/ holding a copy of the binary,
manifest.json (what was run, on what, with which references), results.json (one row per set),
report.md + report.html and work/<arm>/<set>/ (log, report, cif). It is made read-only when done.
A full-arm run (no --only / --tier) takes the site lock and refuses to start if another holds it.
"""
import argparse
import datetime
import fcntl
import hashlib
import json
import os
import re
import shutil
import socket
import stat
import subprocess
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import inputs # noqa: E402
import report # noqa: E402
import score # noqa: E402
DEFAULT_SITE = os.path.join(HERE, "site.json")
# ------------------------------------------------------------------ configuration and manifests
def load_site(path):
site = json.load(open(path))
for arm in site["arms"].values():
arm["manifest"] = os.path.join(HERE, arm["manifest"]) # an absolute path stays as it is
return site
def load_sets(site, arm):
cfg = site["arms"][arm]
man = json.load(open(cfg["manifest"]))
for e in man["sets"]:
e["arm"] = arm
return man["sets"]
def select(site, arms, only, tier):
sets = [e for arm in arms for e in load_sets(site, arm)]
if only:
want = [s.strip() for s in only.split(",") if s.strip()]
unknown = [w for w in want if w not in {e["id"] for e in sets}]
if unknown:
sys.exit(f"not in the {'/'.join(arms)} manifest(s): {', '.join(unknown)}")
sets = [e for e in sets if e["id"] in want]
if tier:
sets = [e for e in sets if tier in e.get("tiers", {})]
return sets
# ------------------------------------------------------------------ the binary
def sha256(path):
h = hashlib.sha256()
with open(path, "rb") as fh:
for block in iter(lambda: fh.read(1 << 20), b""):
h.update(block)
return h.hexdigest()
def binary_info(path, build_dir):
"""Version and git hash as the binary itself prints them, plus the build flags from the
CMakeCache of its build tree (given, or found above the binary)."""
out = subprocess.run([path], capture_output=True, text=True, timeout=120).stdout
m = re.search(r"Version (\S+) \(git (\w+)", out)
info = {"version": m.group(1) if m else None, "git": m.group(2) if m else "unknown"}
cache = None
d = build_dir or os.path.dirname(os.path.realpath(path))
for _ in range(4):
if os.path.exists(os.path.join(d, "CMakeCache.txt")):
cache = os.path.join(d, "CMakeCache.txt")
break
d = os.path.dirname(d)
if cache:
vals = {}
for line in open(cache, errors="replace"):
m = re.match(r"^(CMAKE_BUILD_TYPE|CMAKE_CXX_FLAGS|CMAKE_HOME_DIRECTORY|JFJOCH_USE_CUDA):\w+=(.*)$",
line.strip())
if m:
vals[m.group(1)] = m.group(2)
info["cmake_cache"] = cache
info["flags"] = (f"{vals.get('CMAKE_BUILD_TYPE', '?')} CXX_FLAGS='{vals.get('CMAKE_CXX_FLAGS', '')}' "
f"CUDA={vals.get('JFJOCH_USE_CUDA', '?')}")
src = vals.get("CMAKE_HOME_DIRECTORY")
if src:
info["source"] = src
info["source_head"] = git(src, "rev-parse", "HEAD")
info["source_dirty"] = bool(git(src, "status", "--porcelain", "--untracked-files=no"))
return info
def git(d, *args):
r = subprocess.run(["git", "-C", d] + list(args), capture_output=True, text=True)
return r.stdout.strip() if r.returncode == 0 else None
def gpu_others():
"""Number of processes other than ours on the GPU right now (None if nvidia-smi is absent)."""
try:
out = subprocess.run(["nvidia-smi", "--query-compute-apps=pid", "--format=csv,noheader"],
capture_output=True, text=True, timeout=30).stdout
except (OSError, subprocess.TimeoutExpired):
return None
return len([x for x in out.split() if x.strip().isdigit()])
# ------------------------------------------------------------------ running
def command(binary, e, opts):
cmd = [binary, "-o", "p", "--no-export-unmerged"]
if opts["threads"]:
cmd += ["-N", str(opts["threads"])]
ref = e.get("ref") or {}
# On an XDS-referenced arm the only thing fixed is XDS's resolution range, so the two programs
# merge the same reflections. --unforced leaves rugnux its own cut.
if opts["xds_range"] and e.get("reference") == "xds":
if ref.get("dmin"):
cmd += ["--scaling-high-resolution", f"{ref['dmin']:.3f}"]
if ref.get("dmax"):
cmd += ["--scaling-low-resolution", f"{ref['dmax']:.3f}"]
cmd += opts["extra_args"]
cmd.append(e["input_path"])
if opts["gpulock"]:
cmd = [opts["gpulock"]] + cmd
return cmd
def failure_note(log):
"""rugnux's own last error message, without the spdlog timestamp and severity prefix."""
try:
lines = [l.strip() for l in open(log, errors="replace") if l.strip()]
except OSError:
return ""
for l in reversed(lines):
if "[error]" in l or "failed" in l.lower():
return l.split("] ")[-1][:300]
return lines[-1].split("] ")[-1][:300] if lines else ""
def run_one(run_dir, binary, e, opts):
wd = os.path.join(run_dir, "work", e["arm"], e["id"])
os.makedirs(wd)
row = {"set": e["id"], "arm": e["arm"], "tags": e.get("tags", []), "input": e["input_path"],
"gpu_others": gpu_others()}
if not os.path.exists(e["input_path"]):
row.update(score.judge(e, {}, "no input"), exit_code=None, elapsed_s=None, wall_s=None, cmd=None)
return row
cmd = command(binary, e, opts)
row["cmd"] = " ".join(cmd)
t0 = time.time()
note = ""
with open(os.path.join(wd, "run.log"), "w") as log:
try:
rc = subprocess.run(cmd, cwd=wd, stdout=log, stderr=subprocess.STDOUT,
timeout=opts["timeout"]).returncode
except subprocess.TimeoutExpired:
rc, note = None, "timeout"
row["elapsed_s"] = round(time.time() - t0, 1)
row["exit_code"] = rc
if rc:
note = f"exit {rc}: {failure_note(os.path.join(wd, 'run.log'))}"
row.update(score.judge(e, score.read_report(os.path.join(wd, "p_report.txt")), note))
# rugnux's own WALL_TIME where it wrote one: the runner's clock also counts the wait for a
# GPU slot under --gpulock
row["wall_s"] = row["rugnux_wall_s"] if row["rugnux_wall_s"] is not None else row["elapsed_s"]
return row
def make_read_only(path):
for dp, dirs, files in os.walk(path):
for fn in files:
p = os.path.join(dp, fn)
if not os.path.islink(p):
os.chmod(p, os.stat(p).st_mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH))
for dp, dirs, files in os.walk(path, topdown=False):
os.chmod(dp, os.stat(dp).st_mode & ~(stat.S_IWUSR | stat.S_IWGRP | stat.S_IWOTH))
def write_json(path, obj):
with open(path + ".tmp", "w") as fh:
json.dump(obj, fh, indent=1)
os.replace(path + ".tmp", path)
def write_manifest(path, man):
"""A manifest with one set per line, so a diff of it reads set by set."""
lines = [json.dumps(e) for e in man["sets"]]
head = json.dumps({k: v for k, v in man.items() if k != "sets"})[:-1]
with open(path, "w") as fh:
fh.write(head + ', "sets": [\n ' + ",\n ".join(lines) + "\n]}\n")
def write_report(run_dir, baseline=None, out_dir=None):
"""report.md and report.html (report_PRIVATE.* for a private run) into out_dir, by default
the run directory itself."""
man = json.load(open(os.path.join(run_dir, "manifest.json")))
base = os.path.join(out_dir or run_dir, "report_PRIVATE" if man.get("private") else "report")
doc = report.build(run_dir, baseline)
open(base + ".md", "w").write(doc.markdown())
open(base + ".html", "w").write(doc.html())
return base + ".html"
def run_sets(site, sets, arms, binary_src, build_dir, label, opts, subset, baseline=None, lock=None):
"""Run the given manifest rows into a new run directory. Returns the directory."""
private = any(site["arms"][a].get("private") for a in arms)
if private and len(arms) > 1:
sys.exit("the private arm runs on its own: its results must not share a run or a report")
lock_fh = None
if lock:
os.makedirs(os.path.dirname(lock), exist_ok=True)
lock_fh = open(lock, "w")
try:
fcntl.flock(lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
sys.exit(f"another full battery run holds {lock}; refusing to start a second one")
info = binary_info(binary_src, build_dir)
stamp = datetime.datetime.now().strftime("%Y%m%d-%H%M")
run_dir = os.path.join(opts["runs_root"],
f"{stamp}_{info['git']}_{label}" + ("_private" if private else ""))
os.makedirs(run_dir) # never reuse or overwrite a run directory
binary = os.path.join(run_dir, "bin", "rugnux")
os.makedirs(os.path.dirname(binary))
shutil.copy2(binary_src, binary)
info.update(copied_from=os.path.realpath(binary_src), sha256=sha256(binary))
for e in sets:
e["reference"] = site["arms"][e["arm"]]["reference"]
e.setdefault("input_path", os.path.join(site["arms"][e["arm"]]["data_root"], e["input"]))
man = {"label": label, "arms": arms, "private": private, "subset": subset, "baseline": baseline,
"binary": info,
"runner_git": git(HERE, "rev-parse", "--short", "HEAD") or "unknown",
"runner_dirty": bool(git(HERE, "status", "--porcelain", "--", ".")),
"host": socket.gethostname(), "start": datetime.datetime.now().isoformat(timespec="seconds"),
"end": None, "complete": False, "options": {k: v for k, v in opts.items() if k != "runs_root"},
"gpu_shared": bool(opts["gpulock"]), "sets": sets}
write_json(os.path.join(run_dir, "manifest.json"), man)
print(f"run directory {run_dir}", flush=True)
results = []
for n, e in enumerate(sets, 1):
row = run_one(run_dir, binary, e, opts)
results.append(row)
if row.get("gpu_others"):
man["gpu_shared"] = True
write_json(os.path.join(run_dir, "results.json"), results)
print(f"[{n}/{len(sets)}] {e['arm']:8s} {e['id']:24s} {row['verdict']:8s} "
f"{row.get('sg') or '-':12s} d={report.f(row.get('d_min'))} "
f"t={report.f(row.get('wall_s'), '{:.0f}')}s {row.get('reason') or ''}", flush=True)
man.update(end=datetime.datetime.now().isoformat(timespec="seconds"), complete=True)
write_json(os.path.join(run_dir, "manifest.json"), man)
print(f"report {write_report(run_dir, baseline)}", flush=True)
make_read_only(run_dir)
if lock_fh:
lock_fh.close()
return run_dir
# ------------------------------------------------------------------ subcommands
def cmd_run(a, site):
arms = [x.strip() for x in a.arm.split(",")]
for arm in arms:
if arm not in site["arms"]:
sys.exit(f"unknown arm {arm}; the site config has {', '.join(site['arms'])}")
sets = select(site, arms, a.only, a.tier)
if not sets:
sys.exit("nothing selected")
subset = a.only or (f"tier {a.tier}" if a.tier else None)
opts = {"threads": a.threads, "xds_range": not a.unforced, "timeout": a.timeout,
"gpulock": site["gpulock"] if a.gpulock else None, "extra_args": a.extra.split(),
"runs_root": a.runs_root or site["runs_root"]}
run_sets(site, sets, arms, a.rugnux, a.build_dir, a.label, opts, subset, a.baseline,
lock=None if subset else site["lock_file"])
def cmd_compare(a, site):
man_a, res_a = report.load_run(a.run_a)
man_b, res_b = report.load_run(a.run_b)
rows = report.compare_rows(res_a, res_b)
doc = report.Doc(f"{os.path.basename(os.path.normpath(a.run_a))} vs "
f"{os.path.basename(os.path.normpath(a.run_b))}")
doc.p(f"A: rugnux {man_a['binary'].get('version')} ({man_a['binary']['sha256'][:12]}); "
f"B: rugnux {man_b['binary'].get('version')} ({man_b['binary']['sha256'][:12]}). "
f"{len(rows)} sets, {sum(1 for r in rows if r[3])} changed beyond noise.")
report.compare_table(doc, rows, only_changed=not a.all)
print(doc.markdown())
if not a.rerun_changed:
return
# Rerun the changed sets with A's own binary and options: a set that moves again under the
# same binary was noise; one that reproduces A was changed by B for real.
changed = {k for k, ra, rb, ch in rows if ra and rb and [c for c in ch if c != "time"]}
if not changed:
print("nothing changed beyond noise; no rerun")
return
sets = [e for e in man_a["sets"] if (e["arm"], e["id"]) in changed]
opts = dict(man_a["options"], runs_root=a.runs_root or site["runs_root"])
label = "rerunA-" + man_a["label"]
rerun = run_sets(site, sets, man_a["arms"], os.path.join(a.run_a, "bin", "rugnux"), None, label,
opts, ",".join(e["id"] for e in sets))
_, res_a2 = report.load_run(rerun)
again = {k: ch for k, ra, ra2, ch in report.compare_rows(res_a, res_a2) if ra and ra2}
doc = report.Doc("Changed sets, rerun with A's binary")
rows_out = []
for k, ra, rb, ch in rows:
if k not in changed:
continue
noise = [c for c in again.get(k, []) if c != "time"]
rows_out.append([k[1], k[0], ", ".join(c for c in ch if c != "time"),
", ".join(noise) or "-",
"NOISE - A itself moved" if noise else "REAL - A reproduces"])
doc.table(["set", "arm", "A -> B", "A -> A again", "reading"], rows_out)
doc.p(f"rerun directory: {rerun}")
print(doc.markdown())
def cmd_report(a, site):
# a finished run directory is read-only, so a re-render goes elsewhere
os.makedirs(a.out, exist_ok=True)
print(write_report(a.run, a.baseline, a.out))
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."""
cfg = site["arms"]["open"]
root = cfg["data_root"]
sets = {e["id"]: e for e in load_sets(site, "open")}
for d in sorted(os.listdir(root)):
if d.startswith(".") or not os.path.isdir(os.path.join(root, d)):
continue
e = sets.get(d)
if e and (not a.refresh or e.get("pinned")):
continue
path, fmt, note = inputs.find_input(os.path.join(root, d))
rel = os.path.relpath(path, root) if path else None
if not e:
print(f"NEW {d:10s} {rel} [{note}]")
elif rel != e["input"]:
print(f"DIFF {d:10s} manifest {e['input']}\n {'':10s} discover {rel} [{note}]")
for d in sorted(set(sets) - set(os.listdir(root))):
print(f"GONE {d:10s} in the manifest, not on disk")
def cmd_refs(a, site):
"""Re-read every set's XDS reference from the CORRECT.LP beside its input."""
cfg = site["arms"][a.arm]
if cfg["reference"] != "xds":
sys.exit(f"the {a.arm} arm is not referenced to XDS")
man = json.load(open(cfg["manifest"]))
for e in man["sets"]:
lp = os.path.join(cfg["data_root"], os.path.dirname(e["input"]), "CORRECT.LP")
if not os.path.exists(lp):
print(f"{e['id']:24s} no CORRECT.LP")
continue
new = inputs.parse_correct_lp(lp)
if new != e.get("ref"):
print(f"{e['id']:24s} {e.get('ref')}\n{'':24s} -> {new}")
e["ref"] = new
if a.write:
write_manifest(cfg["manifest"], man)
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--site", default=os.environ.get("BATTERY_SITE", DEFAULT_SITE),
help="site config (data roots, runs root, private manifest, lock file)")
sub = ap.add_subparsers(dest="cmd", required=True)
r = sub.add_parser("run")
r.add_argument("--rugnux", required=True)
r.add_argument("--label", required=True, help="short name, part of the run directory's name")
r.add_argument("--arm", default="open,inhouse", help="comma-separated (default: open,inhouse)")
r.add_argument("--only", help="comma-separated set ids")
r.add_argument("--tier", help="named subset from the manifests, e.g. smoke")
r.add_argument("--build-dir", help="build tree whose CMakeCache.txt records the flags")
r.add_argument("--gpulock", action="store_true",
help="queue each rugnux behind the site's GPU lock (timing is then not a reference)")
r.add_argument("--threads", type=int, default=0, help="-N for rugnux (default: its own choice)")
r.add_argument("--unforced", action="store_true",
help="do not force XDS's resolution range on the XDS-referenced arms")
r.add_argument("--timeout", type=int, default=10800)
r.add_argument("--extra", default="", help="extra rugnux arguments, applied to every set")
r.add_argument("--baseline", help="earlier run directory to report the delta against")
r.add_argument("--runs-root")
c = sub.add_parser("compare")
c.add_argument("run_a")
c.add_argument("run_b")
c.add_argument("--all", action="store_true", help="list unchanged sets too")
c.add_argument("--rerun-changed", action="store_true",
help="rerun the changed sets with A's saved binary to separate real change from noise")
c.add_argument("--runs-root")
p = sub.add_parser("report")
p.add_argument("run")
p.add_argument("--baseline")
p.add_argument("--out", required=True, help="directory for the re-rendered report")
d = sub.add_parser("discover")
d.add_argument("--refresh", action="store_true")
f = sub.add_parser("refs")
f.add_argument("--arm", required=True)
f.add_argument("--write", action="store_true")
a = ap.parse_args()
site = load_site(a.site)
{"run": cmd_run, "compare": cmd_compare, "report": cmd_report,
"discover": cmd_discover, "refs": cmd_refs}[a.cmd](a, site)
if __name__ == "__main__":
main()
+38
View File
@@ -0,0 +1,38 @@
{"arm": "inhouse", "sets": [
{"id": "E08_lyso_2_006", "input": "E08_lyso_2_006/lyso_2_006_master.h5", "ref": {"sgno": 92, "cell": [76.879, 76.879, 36.961, 90.0, 90.0, 90.0], "anomalous": true, "isa": 20.75, "completeness": 99.3, "r_meas": 0.182, "cc_half": 0.995, "multiplicity": 3.51, "dmin": 1.966, "dmax": 50.0}, "tags": ["h5", "lysozyme"]},
{"id": "E08_lyso_2_007", "input": "E08_lyso_2_007/lyso_2_007_master.h5", "ref": {"sgno": 92, "cell": [76.904, 76.904, 36.976, 90.0, 90.0, 90.0], "anomalous": true, "isa": 18.14, "completeness": 99.3, "r_meas": 0.18, "cc_half": 0.994, "multiplicity": 3.51, "dmin": 1.973, "dmax": 50.0}, "tags": ["h5", "lysozyme"]},
{"id": "lyso_2", "input": "lyso_2/lyso_2_002_master.h5", "ref": {"sgno": 92, "cell": [78.235, 78.235, 37.798, 90.0, 90.0, 90.0], "anomalous": true, "isa": 16.56, "completeness": 99.9, "r_meas": 0.239, "cc_half": 0.998, "multiplicity": 10.97, "dmin": 1.264, "dmax": 50.0}, "tags": ["h5", "lysozyme"]},
{"id": "lyso_6_5keV", "input": "lyso_6_5keV/lyso_6_001_master.h5", "ref": {"sgno": 92, "cell": [78.225, 78.225, 37.897, 90.0, 90.0, 90.0], "anomalous": true, "isa": 19.36, "completeness": 86.4, "r_meas": 0.064, "cc_half": 0.998, "multiplicity": 8.85, "dmin": 2.45, "dmax": 50.0}, "tags": ["h5", "lysozyme", "long-wavelength"]},
{"id": "lysoC_14_f46f0a", "input": "lysoC_14_f46f0a/lysoC_14_56b5b9_master.h5", "ref": {"sgno": 89, "cell": [78.387, 78.387, 37.427, 90.0, 90.0, 90.0], "anomalous": true, "isa": 23.33, "completeness": 100.0, "r_meas": 0.219, "cc_half": 0.998, "multiplicity": 11.86, "dmin": 1.431, "dmax": 50.0}, "tags": ["h5", "lysozyme"]},
{"id": "lyso_ref", "input": "lyso_ref/hewl_35_001_master.h5", "ref": {"sgno": 89, "cell": [78.091, 78.091, 37.704, 90.0, 90.0, 90.0], "anomalous": true, "isa": 28.31, "completeness": 100.0, "r_meas": 0.045, "cc_half": 1.0, "multiplicity": 13.76, "dmin": 1.2, "dmax": 50.0}, "tags": ["h5", "lysozyme"], "tiers": {"smoke": "lysozyme reference set"}},
{"id": "lyso_strong", "input": "lyso_strong/lyso_5_f6ac7e_master.h5", "ref": {"sgno": 92, "cell": [77.769, 77.769, 37.442, 90.0, 90.0, 90.0], "anomalous": false, "isa": 8.63, "completeness": 65.8, "r_meas": 0.112, "cc_half": 0.998, "multiplicity": 11.46, "dmin": 1.18, "dmax": 999.0}, "tags": ["h5", "lysozyme"]},
{"id": "lyso_half_image", "input": "lyso_half_image/lyso3_7_b81d83_master.h5", "ref": {"sgno": 1, "cell": [37.267, 79.125, 81.145, 87.995, 90.636, 90.239], "anomalous": true, "isa": 1.87, "completeness": 43.2, "r_meas": 3.17, "cc_half": 0.179, "multiplicity": 1.44, "dmin": 0.8, "dmax": 50.0}, "tags": ["h5", "lysozyme"]},
{"id": "thau_16keV", "input": "thau_16keV/lyso_4_d2bb9b_master.h5", "ref": {"sgno": 92, "cell": [57.958, 57.958, 150.547, 90.0, 90.0, 90.0], "anomalous": true, "isa": 44.79, "completeness": 94.3, "r_meas": 0.073, "cc_half": 1.0, "multiplicity": 9.76, "dmin": 1.3, "dmax": 50.0}, "tags": ["h5", "thaumatin"]},
{"id": "Thau_9", "input": "Thau_9/Thau_9_002_master.h5", "ref": {"sgno": 92, "cell": [58.488, 58.488, 150.788, 90.0, 90.0, 90.0], "anomalous": true, "isa": 9.15, "completeness": 99.0, "r_meas": 0.23, "cc_half": 0.997, "multiplicity": 11.37, "dmin": 2.197, "dmax": 50.0}, "tags": ["h5", "thaumatin"]},
{"id": "thau_BL1A_1p9A", "input": "thau_BL1A_1p9A/JTH14_1p9_50ds_90T_k0_JF4M_01_50001_master.h5", "ref": {"sgno": 89, "cell": [58.031, 58.031, 150.577, 90.0, 90.0, 90.0], "anomalous": true, "isa": 34.46, "completeness": 91.8, "r_meas": 0.072, "cc_half": 0.999, "multiplicity": 7.95, "dmin": 1.78, "dmax": 100.0}, "tags": ["h5", "thaumatin", "long-wavelength"]},
{"id": "thau_BL1A_2p7A", "input": "thau_BL1A_2p7A/JTH14_2p7_50ds_19T_k0_JF4M_01_50001_master.h5", "ref": {"sgno": 89, "cell": [58.007, 58.007, 150.511, 90.0, 90.0, 90.0], "anomalous": true, "isa": 35.64, "completeness": 92.0, "r_meas": 0.062, "cc_half": 0.998, "multiplicity": 7.94, "dmin": 2.53, "dmax": 100.0}, "tags": ["h5", "thaumatin", "long-wavelength"]},
{"id": "thau_BL1A_3p3A", "input": "thau_BL1A_3p3A/JTH14_3p3_50ds_12T_k0_JF4M_01_50001_master.h5", "ref": {"sgno": 89, "cell": [57.895, 57.895, 150.213, 90.0, 90.0, 90.0], "anomalous": true, "isa": 30.8, "completeness": 92.2, "r_meas": 0.057, "cc_half": 0.997, "multiplicity": 7.92, "dmin": 3.1, "dmax": 100.0}, "tags": ["h5", "thaumatin", "long-wavelength"], "tiers": {"smoke": "long wavelength (3.3 A), KEK BL-1A thaumatin"}},
{"id": "thau_injection", "input": "thau_injection/lyso_13_0c7e7a_master.h5", "ref": {"sgno": 92, "cell": [57.922, 57.922, 150.678, 90.0, 90.0, 90.0], "anomalous": true, "isa": 36.23, "completeness": 86.2, "r_meas": 0.037, "cc_half": 1.0, "multiplicity": 9.96, "dmin": 1.28, "dmax": 50.0}, "tags": ["h5", "thaumatin"]},
{"id": "thau_pink", "input": "thau_pink/Thau-Thaumatin_2_master.h5", "ref": {"sgno": 92, "cell": [58.784, 58.784, 152.226, 90.0, 90.0, 90.0], "anomalous": true, "isa": 21.18, "completeness": 98.5, "r_meas": 0.08, "cc_half": 0.999, "multiplicity": 9.38, "dmin": 1.4, "dmax": 50.0}, "tags": ["h5", "thaumatin", "pink-beam"], "tiers": {"smoke": "pink beam"}},
{"id": "Ins_H_2_fa9841", "input": "Ins_H_2_fa9841/Ins_H_2_fa9841_master.h5", "ref": {"sgno": 146, "cell": [81.393, 81.393, 33.272, 90.0, 90.0, 120.0], "anomalous": true, "isa": 6.82, "completeness": 95.0, "r_meas": 0.118, "cc_half": 0.988, "multiplicity": 3.61, "dmin": 1.455, "dmax": 50.0}, "tags": ["h5", "insulin"]},
{"id": "Ins_H_3_d05684", "input": "Ins_H_3_d05684/Ins_H_3_d05684_master.h5", "ref": {"sgno": 146, "cell": [81.378, 81.378, 33.269, 90.0, 90.0, 120.0], "anomalous": true, "isa": 17.69, "completeness": 97.4, "r_meas": 0.068, "cc_half": 0.998, "multiplicity": 3.52, "dmin": 1.544, "dmax": 50.0}, "tags": ["h5", "insulin"], "tiers": {"smoke": "rhombohedral insulin (H3)"}},
{"id": "Ins_I_16", "input": "Ins_I_16/Ins_16_776a92_master.h5", "ref": {"sgno": 197, "cell": [77.085, 77.085, 77.085, 90.0, 90.0, 90.0], "anomalous": false, "isa": 18.91, "completeness": 92.4, "r_meas": 0.401, "cc_half": 0.999, "multiplicity": 29.04, "dmin": 1.08, "dmax": 999.0}, "tags": ["h5", "insulin"]},
{"id": "Ins_I_2_1a530d", "input": "Ins_I_2_1a530d/Ins_I_2_1a530d_master.h5", "ref": {"sgno": 197, "cell": [77.582, 77.582, 77.582, 90.0, 90.0, 90.0], "anomalous": true, "isa": 4.15, "completeness": 90.8, "r_meas": 0.22, "cc_half": 0.998, "multiplicity": 13.33, "dmin": 1.3, "dmax": 50.0}, "tags": ["h5", "insulin"]},
{"id": "Ins_I_3_5e90e3", "input": "Ins_I_3_5e90e3/Ins_I_3_5e90e3_master.h5", "ref": {"sgno": 197, "cell": [77.52, 77.52, 77.52, 90.0, 90.0, 90.0], "anomalous": true, "isa": 25.08, "completeness": 100.0, "r_meas": 0.349, "cc_half": 0.998, "multiplicity": 13.82, "dmin": 1.621, "dmax": 50.0}, "tags": ["h5", "insulin"]},
{"id": "Ins_I_6_13keV", "input": "Ins_I_6_13keV/Ins_I_6_001_master.h5", "ref": {"sgno": 197, "cell": [77.468, 77.468, 77.468, 90.0, 90.0, 90.0], "anomalous": true, "isa": 18.83, "completeness": 100.0, "r_meas": 0.279, "cc_half": 0.998, "multiplicity": 17.21, "dmin": 1.635, "dmax": 50.0}, "tags": ["h5", "insulin"]},
{"id": "Ins_I_6_5keV", "input": "Ins_I_6_5keV/Ins_I_6_006_master.h5", "ref": {"sgno": 197, "cell": [77.785, 77.785, 77.785, 90.0, 90.0, 90.0], "anomalous": true, "isa": 17.54, "completeness": 91.7, "r_meas": 0.073, "cc_half": 0.999, "multiplicity": 11.17, "dmin": 2.45, "dmax": 50.0}, "tags": ["h5", "insulin", "long-wavelength"]},
{"id": "Ins_I_6_6keV", "input": "Ins_I_6_6keV/Ins_I_6_004_master.h5", "ref": {"sgno": 197, "cell": [77.71, 77.71, 77.71, 90.0, 90.0, 90.0], "anomalous": true, "isa": 17.95, "completeness": 92.4, "r_meas": 0.065, "cc_half": 0.999, "multiplicity": 12.42, "dmin": 2.04, "dmax": 50.0}, "tags": ["h5", "insulin", "long-wavelength"]},
{"id": "Ins_I_7_5keV", "input": "Ins_I_7_5keV/Ins_I_7_001_master.h5", "ref": {"sgno": 197, "cell": [77.694, 77.694, 77.694, 90.0, 90.0, 90.0], "anomalous": true, "isa": 20.01, "completeness": 91.8, "r_meas": 0.076, "cc_half": 0.999, "multiplicity": 12.73, "dmin": 2.45, "dmax": 50.0}, "tags": ["h5", "insulin", "long-wavelength"], "tiers": {"smoke": "cubic insulin at 5 keV (long wavelength), fast"}},
{"id": "cytC_10", "input": "cytC_10/cytC_10_002_master.h5", "ref": {"sgno": 152, "cell": [83.659, 83.659, 86.011, 90.0, 90.0, 120.0], "anomalous": true, "isa": 31.75, "completeness": 99.8, "r_meas": 0.232, "cc_half": 0.999, "multiplicity": 10.65, "dmin": 2.039, "dmax": 50.0}, "tags": ["h5", "cytochrome-c"]},
{"id": "cytC_2_0770b5", "input": "cytC_2_0770b5/cytC_2_0770b5_master.h5", "ref": {"sgno": 152, "cell": [83.836, 83.836, 86.733, 90.0, 90.0, 120.0], "anomalous": true, "isa": 24.49, "completeness": 99.9, "r_meas": 0.084, "cc_half": 0.999, "multiplicity": 7.67, "dmin": 1.875, "dmax": 50.0}, "tags": ["h5", "cytochrome-c"]},
{"id": "cytC_3_874285", "input": "cytC_3_874285/cytC_3_874285_master.h5", "ref": {"sgno": 152, "cell": [83.719, 83.719, 88.617, 90.0, 90.0, 120.0], "anomalous": true, "isa": 26.98, "completeness": 99.9, "r_meas": 0.097, "cc_half": 0.999, "multiplicity": 7.82, "dmin": 1.69, "dmax": 50.0}, "tags": ["h5", "cytochrome-c"]},
{"id": "MyoB_13", "input": "MyoB_13/MyoB_13_110ee0_master.h5", "ref": {"sgno": 4, "cell": [35.229, 28.461, 63.076, 90.0, 106.504, 90.0], "anomalous": true, "isa": 5.21, "completeness": 97.7, "r_meas": 0.247, "cc_half": 0.965, "multiplicity": 3.29, "dmin": 1.742, "dmax": 50.0}, "tags": ["h5", "myoglobin"]},
{"id": "MyoB_9_446b70", "input": "MyoB_9_446b70/MyoB_9_446b70_master.h5", "ref": {"sgno": 3, "cell": [35.182, 28.544, 62.83, 90.0, 106.138, 90.0], "anomalous": true, "isa": 7.59, "completeness": 99.4, "r_meas": 0.232, "cc_half": 0.987, "multiplicity": 3.47, "dmin": 1.422, "dmax": 50.0}, "tags": ["h5", "myoglobin"], "tiers": {"smoke": "monoclinic myoglobin, screw not arbitrable by XDS"}},
{"id": "MyoB2-1", "input": "MyoB2-1/MyoB2-1_75979a_master.h5", "ref": {"sgno": 1, "cell": [28.705, 34.796, 64.373, 74.314, 89.879, 90.057], "anomalous": false, "isa": 5.54, "completeness": 74.2, "r_meas": 0.493, "cc_half": 0.966, "multiplicity": 2.87, "dmin": 1.495, "dmax": 50.0}, "tags": ["h5", "myoglobin", "twin"], "tiers": {"smoke": "known hard: triclinic twin"}},
{"id": "MyoB2-2", "input": "MyoB2-2/MyoB2-2_5ddd7d_master.h5", "ref": {"sgno": 3, "cell": [35.576, 28.811, 63.529, 90.0, 105.593, 90.0], "anomalous": false, "isa": 2.19, "completeness": 58.0, "r_meas": 1.106, "cc_half": 0.655, "multiplicity": 4.71, "dmin": 0.99, "dmax": 999.0}, "tags": ["h5", "myoglobin", "twin"]},
{"id": "MyoB2-4", "input": "MyoB2-4/MyoB2-4_079740_master.h5", "ref": {"sgno": 3, "cell": [35.39, 28.783, 63.626, 90.0, 105.544, 90.0], "anomalous": false, "isa": 12.41, "completeness": 77.7, "r_meas": 0.577, "cc_half": 0.984, "multiplicity": 4.38, "dmin": 1.506, "dmax": 50.0}, "tags": ["h5", "myoglobin", "twin"]},
{"id": "MyoB2-5", "input": "MyoB2-5/MyoB2-5_7cef9c_master.h5", "ref": {"sgno": 1, "cell": [43.226, 67.346, 121.692, 100.15, 89.855, 96.62], "anomalous": false, "isa": 1.24, "completeness": 11.8, "r_meas": 1.527, "cc_half": 0.125, "multiplicity": 1.41, "dmin": 1.11, "dmax": 999.0}, "tags": ["h5", "myoglobin", "twin"]},
{"id": "onlyice", "input": "onlyice/onlyice_master.h5", "expect": "no_lattice", "tags": ["h5", "control"], "tiers": {"smoke": "no-crystal control (ice rings only): must refuse"}},
{"id": "nothing_3400ac", "input": "nothing/test-28_3400ac_master.h5", "expect": "no_lattice", "tags": ["h5", "control"]},
{"id": "nothing_9bf604", "input": "nothing/test-28_9bf604_master.h5", "expect": "no_lattice", "tags": ["h5", "control"]}
]}
+154
View File
@@ -0,0 +1,154 @@
"""Finding a dataset's input file, and reading an XDS CORRECT.LP as a reference.
Both run only when a manifest is (re)built - `battery.py discover` and `battery.py refs`. A battery
run itself never searches: it runs the input the manifest names, so two runs of one manifest always
process the same files.
"""
import glob
import os
import re
# XDS writes calibration images beside the frames; none of them is a sweep.
XDS_AUX = ("-CORRECTIONS", "BKGINIT", "BKGPIX", "BLANK", "GAIN", "DECAY", "ABS.", "MODPIX", "FRAME.")
def master_frames(path):
"""Number of frames a NXmx master describes.
A sweep is chosen by frame count, never by file size: a master with a few embedded screening
frames can outweigh the master of a real sweep whose frames sit in external data files."""
import h5py
try:
with h5py.File(path, "r") as h:
det = "entry/instrument/detector/detectorSpecific"
if f"{det}/nimages" in h:
n = int(h[f"{det}/nimages"][()])
if f"{det}/ntrigger" in h:
n *= max(1, int(h[f"{det}/ntrigger"][()]))
return n
omega = "entry/sample/goniometer/omega"
if omega in h and h[omega].shape:
return int(h[omega].shape[0])
n = 0
for k in h.get("entry/data", {}):
try:
n += int(h["entry/data"][k].shape[0])
except (KeyError, OSError): # an external link whose file is missing
pass
return n
except OSError:
return 0
def detector_frame(path):
"""'marCCD' or 'SMV' if the file's header says so, else None. These formats have no reliable
extension (.mccd, .img, .001, ...; .img is SMV at one site and marCCD at the next)."""
try:
with open(path, "rb") as fh:
head = fh.read(64)
if head[:2] in (b"II", b"MM"):
fh.seek(1028)
return "marCCD" if fh.read(3) == b"MMX" else None
if head[:1] == b"{" and b"HEADER_BYTES" in head:
return "SMV"
except OSError:
pass
return None
def find_input(d):
"""(input file, format, note) for the largest sweep under directory d, or (None, None, why).
Frames are grouped per directory by template (the last run of digits in the name), and the
biggest group by frame count wins; a tie goes to the alphabetically first. Groups are never
merged across directories - two crystals can have identically named frames side by side."""
masters = [m for m in glob.glob(f"{d}/**/*master*.h5", recursive=True)
if not os.path.basename(m).startswith("._")]
if not masters:
# Diamond writes a NeXus .nxs master beside _NNNNNN.h5 data files
masters = [m for m in glob.glob(f"{d}/**/*.nxs", recursive=True)
if not os.path.basename(m).startswith("._") and "meta" not in os.path.basename(m)]
if masters:
best = min(masters, key=lambda m: (-master_frames(m), m))
return best, "nxs" if best.endswith(".nxs") else "h5", \
f"master, {master_frames(best)} frames, {len(masters)} master(s)"
groups = {}
for p in glob.glob(f"{d}/**/*", recursive=True):
name = os.path.basename(p)
if not os.path.isfile(p) or name.startswith("._") or any(a in name.upper() for a in XDS_AUX):
continue
key = (os.path.dirname(p), re.sub(r"\d+(?=\D*$)", "#", name))
groups.setdefault(key, []).append(p)
for key, files in sorted(groups.items(), key=lambda kv: (-len(kv[1]), kv[0])):
if len(files) < 10:
break
files.sort()
if files[0].endswith((".cbf", ".cbf.gz")):
fmt = "cbf"
else:
fmt = detector_frame(files[0])
if fmt:
return files[0], fmt, f'{fmt} "{key[1]}" x{len(files)} of {len(groups)} group(s)'
return None, None, "no readable sweep found"
def input_format(path):
"""Format tag of an input file named in a manifest."""
if path.endswith(".h5"):
return "h5"
if path.endswith(".nxs"):
return "nxs"
if path.endswith((".cbf", ".cbf.gz")):
return "cbf"
return detector_frame(path) or "other"
def parse_correct_lp(path):
"""Reference values from an XDS CORRECT.LP: the space group and cell CORRECT refined, the
resolution range it merged, and its overall merging statistics.
CORRECT.LP states the group and cell twice - first as the INPUT to INTEGRATE (routinely P1),
then as CORRECT's own answer. The last SPACE_GROUP_NUMBER and the refined UNIT CELL PARAMETERS
are the ones the statistics were computed in."""
txt = open(path, errors="replace").read()
ref = {}
sgs = re.findall(r"SPACE_GROUP_NUMBER=\s*(\d+)", txt)
ref["sgno"] = int(sgs[-1]) if sgs else None
cells = re.findall(r"UNIT CELL PARAMETERS\s+([\d.\s]+)", txt)
if cells:
ref["cell"] = [float(x) for x in cells[-1].split()[:6]]
else:
m = re.findall(r"UNIT_CELL_CONSTANTS=\s*([\d.\s]+)", txt)
ref["cell"] = [float(x) for x in m[-1].split()[:6]] if m else None
m = re.search(r"FRIEDEL'S_LAW=\s*(TRUE|FALSE)", txt)
ref["anomalous"] = bool(m and m.group(1) == "FALSE")
m = re.search(r"^\s*a\s+b\s+ISa\s*\n\s*[\d.Ee+-]+\s+[\d.Ee+-]+\s+([\d.]+)", txt, re.M)
ref["isa"] = float(m.group(1)) if m else None
# the last "... AS FUNCTION OF RESOLUTION" table; shells from low to high resolution, then total
seg = txt[txt.rfind("AS FUNCTION OF RESOLUTION"):]
num = lambda x: float(x.rstrip("*%"))
shells, total = [], None
for line in seg.splitlines():
f = line.split()
if f and f[0] == "total" and len(f) >= 11:
total = f
break
if f and re.match(r"^\d+\.\d+$", f[0]) and len(f) >= 11:
shells.append(f)
if total:
ref["completeness"] = num(total[4])
ref["r_meas"] = round(num(total[9]) / 100, 4)
ref["cc_half"] = round(num(total[10]) / 100, 4)
ref["multiplicity"] = round(int(total[1]) / int(total[2]), 2) if int(total[2]) else None
# The resolution range to hand to rugnux so both programs merge the same reflections: the
# explicit INCLUDE_RESOLUTION_RANGE, or - when XDS.INP left the high limit at 0.0, "the whole
# detector" - the finest shell actually tabulated.
m = re.search(r"INCLUDE_RESOLUTION_RANGE=\s*([\d.]+)\s+([\d.]+)", txt)
include_low, include_high = (float(m.group(1)), float(m.group(2))) if m else (0.0, 0.0)
table_high = float(shells[-1][0]) if shells else 0.0
ref["dmin"] = (include_high if include_high > 0 else table_high) or None
ref["dmax"] = include_low or None
return ref
+153
View File
@@ -0,0 +1,153 @@
{"arm": "open", "sets": [
{"id": "11if", "input": "11if/BopeA_18500_a_B2-PEF_11if/data/PSL-1503_13949_master.h5", "ref": {"sg": "P 43", "sgno": 78, "cell": [51.14, 51.14, 71.92, 90.0, 90.0, 90.0], "dmin": 1.51}, "tags": ["h5", "tetragonal"], "tiers": {"smoke": "Eiger NXmx from another facility, 4-fold screw"}},
{"id": "36gk", "input": "36gk/CLS-0074_5-3_36GK/data/CLS-0074_5-3_master.h5", "ref": {"sg": "I 2 2 2", "sgno": 23, "cell": [120.58, 189.49, 199.69, 90.0, 90.0, 90.0], "dmin": 2.28}, "tags": ["h5", "orthorhombic"]},
{"id": "3inp", "input": "3inp/IDP02542_3inp/data/idp02542b.001", "ref": {"sg": "F 41 3 2", "sgno": 210, "cell": [224.08, 224.08, 224.08, 90.0, 90.0, 90.0], "dmin": 2.05}, "tags": ["marCCD", "cubic"]},
{"id": "3ky7", "input": "3ky7/IDP90258_3ky7/data/idp90258f.001", "ref": {"sg": "P 43 3 2", "sgno": 212, "cell": [125.176, 125.176, 125.176, 90.0, 90.0, 90.0], "dmin": 2.35}, "tags": ["marCCD", "cubic"]},
{"id": "5ebi", "input": "5ebi/dna-rna-chimera_Ba_high_2_001.img", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [35.72, 44.1, 35.72, 90.0, 119.98, 90.0], "dmin": 1.09}, "pinned": true, "tags": ["marCCD", "monoclinic", "twin"]},
{"id": "5epe", "input": "5epe/030805_5epe/data/E1_7_set.001", "ref": {"sg": "F 2 3", "sgno": 196, "cell": [157.536, 157.536, 157.536, 90.0, 90.0, 90.0], "dmin": 1.9}, "tags": ["marCCD", "cubic"]},
{"id": "5f6m", "input": "5f6m/crystal3_00001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [54.81, 58.51, 67.42, 90.0, 90.0, 90.0], "dmin": 1.1}, "tags": ["cbf", "orthorhombic", "diffuse"]},
{"id": "5j23", "input": "5j23/012132_5j23/data/XZ2_set.001", "ref": {"sg": "H 3", "sgno": 146, "cell": [175.822, 175.822, 136.839, 90.0, 90.0, 120.0], "dmin": 2.3}, "tags": ["marCCD", "trigonal", "twin"]},
{"id": "5jvn", "input": "5jvn/5jvn/data/PPDK-AV2820_w1_3_0001.cbf", "ref": {"sg": "P 6 2 2", "sgno": 177, "cell": [249.43, 249.43, 84.06, 90.0, 90.0, 120.0], "dmin": 2.9}, "tags": ["cbf", "hexagonal"]},
{"id": "5lzl", "input": "5lzl/pcalad/alad-levoil6_M3S15_1_0001.cbf", "ref": {"sg": "P 31 2 1", "sgno": 152, "cell": [205.561, 205.561, 199.171, 90.0, 90.0, 120.0], "dmin": 3.47}, "tags": ["cbf", "trigonal"]},
{"id": "5m17", "input": "5m17/BxGH99_WTDD_HA00AG2801_2_0001.cbf", "ref": {"sg": "I 4", "sgno": 79, "cell": [108.576, 108.576, 67.746, 90.0, 90.0, 90.0], "dmin": 1.03}, "tags": ["cbf", "tetragonal"]},
{"id": "5nw5", "input": "5nw5/5NW5_1_00001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [92.14, 169.8, 390.16, 90.0, 90.0, 90.0], "dmin": 6.502}, "tags": ["cbf", "orthorhombic", "low-resolution"]},
{"id": "5reo", "input": "5reo/cbf/Mpro-x0752_1_0001.cbf", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [112.39, 52.59, 44.38, 90.0, 103.04, 90.0], "dmin": 1.88}, "tags": ["cbf", "monoclinic"], "tiers": {"smoke": "fastest set (10 s): miniCBF, monoclinic"}},
{"id": "5src", "input": "5src/5src/data/FRS004_15_1_00001.cbf", "ref": {"sg": "P 43", "sgno": 78, "cell": [88.68, 88.68, 39.23, 90.0, 90.0, 90.0], "dmin": 1.05}, "tags": ["cbf", "tetragonal"]},
{"id": "6fid", "input": "6fid/Trypsin_x1_align_2_0001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [59.947, 64.107, 69.694, 90.0, 90.0, 90.0], "dmin": 2.2}, "tags": ["cbf", "orthorhombic"]},
{"id": "6fvz", "input": "6fvz/maox215_6fvz/data/maox215_w1_1_0001.cbf", "ref": {"sg": "C 2 2 2", "sgno": 21, "cell": [131.182, 222.753, 86.482, 90.0, 90.0, 90.0], "dmin": 1.8}, "tags": ["cbf", "orthorhombic"]},
{"id": "6fwc", "input": "6fwc/maox225_6fwc/data/maox225_1_00001.cbf", "ref": {"sg": "C 2 2 2", "sgno": 21, "cell": [131.728, 222.051, 86.293, 90.0, 90.0, 90.0], "dmin": 1.7}, "tags": ["cbf", "orthorhombic"]},
{"id": "6h2p", "input": "6h2p/ProlMut1MnCAC_6h2p/data/Prol_Mut1_Mn_X_pk1_2_0001.cbf", "ref": {"sg": "C 2 2 21", "sgno": 20, "cell": [103.453, 107.075, 216.543, 90.0, 90.0, 90.0], "dmin": 1.479}, "tags": ["cbf", "orthorhombic", "long-wavelength"]},
{"id": "6h5t", "input": "6h5t/6h5t/data/SH3-1_x2_1_001.img", "ref": {"sg": "I 4 2 2", "sgno": 97, "cell": [86.792, 86.792, 141.768, 90.0, 90.0, 90.0], "dmin": 1.689}, "tags": ["marCCD", "tetragonal"]},
{"id": "6hv2", "input": "6hv2/6hv2/data/coll_1_master.h5", "ref": {"sg": "P 61 2 2", "sgno": 178, "cell": [68.928, 68.928, 133.563, 90.0, 90.0, 120.0], "dmin": 1.709}, "tags": ["h5", "hexagonal"]},
{"id": "6hwj", "input": "6hwj/bg3006_2_0001.cbf", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [59.812, 96.07, 80.252, 90.0, 106.69, 90.0], "dmin": 1.979}, "tags": ["cbf", "monoclinic"]},
{"id": "6i3j", "input": "6i3j/6i3j/data/BF19_go_1_0001.img", "ref": {"sg": "F 2 2 2", "sgno": 22, "cell": [134.382, 203.846, 226.744, 90.0, 90.0, 90.0], "dmin": 2.59}, "tags": ["marCCD", "orthorhombic"]},
{"id": "6iu5", "input": "6iu5/6iu5_VIT1_MBD_Zn/peak/peak_000001.cbf", "ref": {"sg": "P 31", "sgno": 144, "cell": [84.942, 84.942, 98.19, 90.0, 90.0, 120.0], "dmin": 2.25}, "pinned": true, "tags": ["cbf", "trigonal", "twin"]},
{"id": "6iu6", "input": "6iu6/6iu6_VIT1_MBD_Ni/peak/peak_000001.cbf", "ref": {"sg": "P 31", "sgno": 144, "cell": [84.744, 84.744, 97.398, 90.0, 90.0, 120.0], "dmin": 2.9}, "pinned": true, "tags": ["cbf", "trigonal", "twin"]},
{"id": "6iu8", "input": "6iu8/6iu8_VIT1_MBD_Co/low/low_000001.cbf", "ref": {"sg": "P 31", "sgno": 144, "cell": [85.503, 85.503, 98.355, 90.0, 90.0, 120.0], "dmin": 2.7}, "tags": ["cbf", "trigonal"]},
{"id": "6iu9", "input": "6iu9/6iu9_VIT1_MBD_Fe/peak/data01_000001.cbf", "ref": {"sg": "P 31", "sgno": 144, "cell": [85.321, 85.321, 97.573, 90.0, 90.0, 120.0], "dmin": 3.0}, "pinned": true, "tags": ["cbf", "trigonal", "twin"]},
{"id": "6jgi", "input": "6jgi/6jgi/data/00001.img", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [50.857, 62.403, 69.172, 90.0, 90.0, 90.0], "dmin": 0.85}, "tags": ["marCCD", "orthorhombic"]},
{"id": "6jgj", "input": "6jgj/6jgj/data/000001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [50.87, 62.34, 68.85, 90.0, 90.0, 90.0], "dmin": 0.77}, "tags": ["cbf", "orthorhombic", "cdte"]},
{"id": "6moj", "input": "6moj/A9_1_00001.cbf", "ref": {"sg": "I 41 2 2", "sgno": 98, "cell": [130.418, 130.418, 293.453, 90.0, 90.0, 90.0], "dmin": 2.431}, "tags": ["cbf", "tetragonal"]},
{"id": "6o2h", "input": "6o2h/lys_nitr_10_1_0001.cbf", "ref": {"sg": "P 1", "sgno": 1, "cell": [27.424, 32.134, 34.513, 88.657, 108.46, 111.877], "dmin": 1.212}, "tags": ["cbf", "triclinic", "diffuse"]},
{"id": "6oel", "input": "6oel/1449-8_3_001.img", "ref": {"sg": "F 41 3 2", "sgno": 210, "cell": [328.1, 328.1, 328.1, 90.0, 90.0, 90.0], "dmin": 3.1}, "tags": ["SMV", "cubic"], "tiers": {"smoke": "SMV reader (ADSC), cubic F4132"}},
{"id": "6p8p", "input": "6p8p/14_12_1_0001.cbf", "ref": {"sg": "P 4", "sgno": 75, "cell": [97.525, 97.525, 60.134, 90.0, 90.0, 90.0], "dmin": 1.635}, "tags": ["cbf", "tetragonal"]},
{"id": "6pb3", "input": "6pb3/14_16_1_000001.cbf", "ref": {"sg": "P 6", "sgno": 168, "cell": [100.436, 100.436, 48.86, 90.0, 90.0, 120.0], "dmin": 2.048}, "tags": ["cbf", "hexagonal"]},
{"id": "6pxb", "input": "6pxb/TJB1_3_rachel_1_0001.cbf", "ref": {"sg": "P 32", "sgno": 145, "cell": [64.021, 64.021, 119.447, 90.0, 90.0, 120.0], "dmin": 1.747}, "tags": ["cbf", "trigonal"]},
{"id": "6pxc", "input": "6pxc/TJB6_1_Rachel_1_0001.cbf", "ref": {"sg": "I 2 2 2", "sgno": 23, "cell": [44.19, 64.827, 87.239, 90.0, 90.0, 90.0], "dmin": 1.6}, "tags": ["cbf", "orthorhombic"]},
{"id": "6r72", "input": "6r72/V-CK63-8-ld_1_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [117.805, 110.754, 155.615, 90.0, 93.23, 90.0], "dmin": 3.95}, "pinned": true, "tags": ["h5", "monoclinic"]},
{"id": "6rlr", "input": "6rlr/_b4_1_master.h5", "ref": {"sg": "P 1", "sgno": 1, "cell": [39.986, 39.998, 63.643, 80.39, 76.29, 68.15], "dmin": 2.0}, "tags": ["h5", "triclinic", "twin"]},
{"id": "6toc", "input": "6toc/zenodo/dts_1_00001.cbf", "ref": {"sg": "P 42", "sgno": 77, "cell": [31.523, 31.523, 81.599, 90.0, 90.0, 90.0], "dmin": 1.853}, "tags": ["cbf", "tetragonal", "twin"]},
{"id": "6ttn", "input": "6ttn/6ttn/data/H6H_33_01_hyo_full_1_0001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [39.894, 79.841, 104.701, 90.0, 90.0, 90.0], "dmin": 1.12}, "tags": ["cbf", "orthorhombic"]},
{"id": "6u7g", "input": "6u7g/6u7g/data/SNB02_11_8_1_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [99.555, 98.682, 147.525, 90.0, 104.6, 90.0], "dmin": 2.35}, "pinned": true, "tags": ["h5", "monoclinic"]},
{"id": "6ukf", "input": "6ukf/6ukf/data/XDC-7_Pn6_000001.cbf", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [61.018, 37.317, 69.027, 90.0, 109.768, 90.0], "dmin": 1.0}, "tags": ["cbf", "monoclinic"]},
{"id": "6vww", "input": "6vww/IDP51000_6vww/data/m4H11g_00001.cbf", "ref": {"sg": "P 63", "sgno": 173, "cell": [150.539, 150.539, 111.31, 90.0, 90.0, 120.0], "dmin": 2.2}, "tags": ["cbf", "hexagonal", "twin"], "tiers": {"smoke": "merohedral twin, P63"}},
{"id": "6w4h", "input": "6w4h/IDP51000_6W4H/data/idp51000-201-a_1_2_3.001", "ref": {"sg": "P 31 2 1", "sgno": 152, "cell": [167.74, 167.74, 51.942, 90.0, 90.0, 120.0], "dmin": 1.8}, "tags": ["marCCD", "trigonal"]},
{"id": "6wzo", "input": "6wzo/9_1_1_000001.cbf", "ref": {"sg": "P 1", "sgno": 1, "cell": [43.718, 50.061, 69.337, 106.499, 90.094, 97.145], "dmin": 1.42}, "tags": ["cbf", "triclinic"]},
{"id": "6yqf", "input": "6yqf/6yqf/data/SYCE2TEX12-8_1_0001.cbf", "ref": {"sg": "P 21 21 2", "sgno": 18, "cell": [42.67, 59.68, 156.49, 90.0, 90.0, 90.0], "dmin": 3.327}, "tags": ["cbf", "orthorhombic"]},
{"id": "6z8o", "input": "6z8o/HASE01-gaKr1_w1_1_1_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [63.716, 97.022, 121.318, 90.0, 104.656, 90.0], "dmin": 2.2}, "tags": ["h5", "monoclinic"]},
{"id": "6ze4", "input": "6ze4/806/o8_1_0001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [93.56, 109.88, 116.11, 90.0, 90.0, 90.0], "dmin": 1.6}, "tags": ["cbf", "orthorhombic"], "tiers": {"smoke": "known hard: P212121 under-called as P21 since rc168"}},
{"id": "7arr", "input": "7arr/PF4N_04D_w1_1_00001.cbf", "ref": {"sg": "P 1", "sgno": 1, "cell": [30.937, 32.068, 43.087, 114.16, 91.88, 109.86], "dmin": 1.1}, "tags": ["cbf", "triclinic"]},
{"id": "7atg", "input": "7atg/7atg/data/put-2-2.0s_5_00001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [17.97, 31.02, 43.86, 90.0, 90.0, 90.0], "dmin": 0.6}, "tags": ["cbf", "orthorhombic"]},
{"id": "7bgt", "input": "7bgt/MPMV2243_3_001.img", "ref": {"sg": "P 1", "sgno": 1, "cell": [29.296, 67.618, 69.716, 76.845, 83.875, 83.645], "dmin": 1.93}, "tags": ["marCCD", "triclinic"]},
{"id": "7brr", "input": "7brr/7brr/data/3C_2_1_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [55.45, 99.02, 59.583, 90.0, 108.54, 90.0], "dmin": 1.35}, "tags": ["h5", "monoclinic"]},
{"id": "7dkp", "input": "7dkp/7dkp/data/LOX1-sree-p1a9-1p2_w1_1_1_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [49.84, 169.451, 49.842, 90.0, 93.51, 90.0], "dmin": 1.45}, "tags": ["h5", "monoclinic"]},
{"id": "7k1l", "input": "7k1l/IDP52015_7k1l/data/cs1C2eg_00001.cbf", "ref": {"sg": "P 63", "sgno": 173, "cell": [150.83, 150.83, 110.73, 90.0, 90.0, 120.0], "dmin": 2.25}, "tags": ["cbf", "hexagonal"], "tiers": {"smoke": "known hard: screw axis (P6 vs P63)"}},
{"id": "7kcn", "input": "7kcn/7kcn/data/ttr_hiuase_1_D1_33_00001.cbf", "ref": {"sg": "P 41 2 2", "sgno": 91, "cell": [67.03, 67.03, 116.89, 90.0, 90.0, 90.0], "dmin": 1.46}, "tags": ["cbf", "tetragonal"]},
{"id": "7l6j", "input": "7l6j/7l6j/data/idp97824-102sm-a_1_1_7.001", "ref": {"sg": "I 41 3 2", "sgno": 214, "cell": [171.693, 171.693, 171.693, 90.0, 90.0, 90.0], "dmin": 1.78}, "tags": ["marCCD", "cubic"]},
{"id": "7l84", "input": "7l84/301_helical_1_0001.cbf", "ref": {"sg": "P 43 21 2", "sgno": 96, "cell": [79.344, 79.344, 37.81, 90.0, 90.0, 90.0], "dmin": 1.7}, "tags": ["cbf", "tetragonal"]},
{"id": "7mzt", "input": "7mzt/7mzt/data/BVG2_Pn3_000001.cbf", "ref": {"sg": "P 21 21 2", "sgno": 18, "cell": [113.619, 96.989, 108.33, 90.0, 90.0, 90.0], "dmin": 4.07}, "tags": ["cbf", "orthorhombic"]},
{"id": "7n0i", "input": "7n0i/8_2_1_00001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [75.846, 131.557, 140.048, 90.0, 90.0, 90.0], "dmin": 2.2}, "tags": ["cbf", "orthorhombic", "tncs"]},
{"id": "7n2s", "input": "7n2s/1449-4_1_00001.cbf", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [83.211, 52.789, 106.291, 90.0, 98.278, 90.0], "dmin": 2.37}, "tags": ["cbf", "monoclinic"]},
{"id": "7orr", "input": "7orr/7orr/data/Nsp10-VT00022_1_master.h5", "ref": {"sg": "I 21 3", "sgno": 199, "cell": [105.88, 105.88, 105.88, 90.0, 90.0, 90.0], "dmin": 1.79}, "tags": ["h5", "cubic"]},
{"id": "7os3", "input": "7os3/pos2_1/pos2_1_00001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [78.15, 91.05, 105.84, 90.0, 90.0, 90.0], "dmin": 2.177}, "tags": ["cbf", "orthorhombic"]},
{"id": "7ph1", "input": "7ph1/7ph1/data/9_1_0001.cbf", "ref": {"sg": "I 2 2 2", "sgno": 23, "cell": [74.975, 81.289, 124.248, 90.0, 90.0, 90.0], "dmin": 1.18}, "tags": ["cbf", "orthorhombic"]},
{"id": "7pq7", "input": "7pq7/7pq7/data/1436_p52_A3_1_1_00001.cbf", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [120.88, 51.73, 75.54, 90.0, 125.14, 90.0], "dmin": 1.55}, "tags": ["cbf", "monoclinic"]},
{"id": "7qij", "input": "7qij/907/dg046_2_data_1_00001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [143.46, 324.92, 369.38, 90.0, 90.0, 90.0], "dmin": 4.1}, "tags": ["cbf", "orthorhombic"]},
{"id": "7qis", "input": "7qis/7qis/data/Chymo_Dfe_x1_2_0001.cbf", "ref": {"sg": "P 61", "sgno": 169, "cell": [100.269, 100.269, 206.215, 90.0, 90.0, 120.0], "dmin": 1.83}, "tags": ["cbf", "hexagonal"]},
{"id": "7ris", "input": "7ris/7ris/data/GLVase_Ca_we21108b7b_1_2_2.001_master.h5", "ref": {"sg": "P 32 2 1", "sgno": 154, "cell": [44.54, 44.54, 189.95, 90.0, 90.0, 120.0], "dmin": 1.72}, "pinned": true, "tags": ["h5", "trigonal"]},
{"id": "7rji", "input": "7rji/7rji/data/DD3_00000.cbf", "ref": {"sg": "H 3 2", "sgno": 155, "cell": [83.036, 83.036, 124.83, 90.0, 90.0, 120.0], "dmin": 1.71}, "tags": ["cbf", "trigonal"]},
{"id": "7t5t", "input": "7t5t/A1_2_00001.cbf", "ref": {"sg": "P 42 21 2", "sgno": 94, "cell": [95.309, 95.309, 104.915, 90.0, 90.0, 90.0], "dmin": 1.35}, "pinned": true, "tags": ["cbf", "tetragonal"]},
{"id": "7tcd", "input": "7tcd/7tcd/data/col_1_master.h5", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [138.504, 47.902, 78.068, 90.0, 107.56, 90.0], "dmin": 1.7}, "tags": ["h5", "monoclinic"]},
{"id": "7yzx", "input": "7yzx/7yzx/data/783-1_1_0001.cbf", "ref": {"sg": "P 63 2 2", "sgno": 182, "cell": [169.42, 169.42, 141.83, 90.0, 90.0, 120.0], "dmin": 1.9}, "tags": ["cbf", "hexagonal"]},
{"id": "8a1a", "input": "8a1a/L1F11v1_8a1a/data/L1-F11-v1_9ly1_D06_01_2_master.h5", "ref": {"sg": "P 65", "sgno": 170, "cell": [191.866, 191.866, 122.409, 90.0, 90.0, 120.0], "dmin": 2.05}, "tags": ["h5", "hexagonal"]},
{"id": "8agq", "input": "8agq/8agq_zip_8AGQ/data/coll_1_00001.cbf", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [89.948, 55.447, 54.787, 90.0, 113.55, 90.0], "dmin": 1.093}, "tags": ["cbf", "monoclinic"]},
{"id": "8dqb", "input": "8dqb/KloxA_17380_a_B1-Apo_I23_Form_8dqb/data/PSL-1405_653_master.h5", "ref": {"sg": "I 2 3", "sgno": 197, "cell": [164.124, 164.124, 164.124, 90.0, 90.0, 90.0], "dmin": 2.5}, "tags": ["h5", "cubic"]},
{"id": "8dyz", "input": "8dyz/lys_1_2_0001.cbf", "ref": {"sg": "P 43 21 2", "sgno": 96, "cell": [79.632, 79.632, 38.296, 90.0, 90.0, 90.0], "dmin": 1.272}, "tags": ["cbf", "tetragonal", "diffuse"]},
{"id": "8dz7", "input": "8dz7/lys_rt_2_38_0001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [30.486, 56.401, 73.852, 90.0, 90.0, 90.0], "dmin": 1.34}, "tags": ["cbf", "orthorhombic", "diffuse"]},
{"id": "8egn", "input": "8egn/PsaeA_00137_b_B5-az13643701_8egn/data/FKC8_4_0001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [71.69, 75.16, 109.79, 90.0, 90.0, 90.0], "dmin": 1.95}, "tags": ["cbf", "orthorhombic"]},
{"id": "8iya", "input": "8iya/8iya/data/11_1_master.h5", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [102.747, 50.077, 109.248, 90.0, 91.754, 90.0], "dmin": 2.43}, "tags": ["h5", "monoclinic"]},
{"id": "8k1g", "input": "8k1g/8k1g/data/KP02_0001.cbf", "ref": {"sg": "I 4 2 2", "sgno": 97, "cell": [182.01, 182.01, 80.71, 90.0, 90.0, 90.0], "dmin": 2.09}, "tags": ["cbf", "tetragonal"]},
{"id": "8oic", "input": "8oic/8oic/data/_b20_1_master.h5", "ref": {"sg": "P 1", "sgno": 1, "cell": [73.055, 94.665, 120.589, 105.081, 89.959, 93.826], "dmin": 2.8}, "tags": ["h5", "triclinic"]},
{"id": "8owm", "input": "8owm/p13x4_1_00001.cbf", "ref": {"sg": "P 1", "sgno": 1, "cell": [95.544, 95.629, 95.841, 90.416, 93.59, 117.775], "dmin": 1.7}, "tags": ["cbf", "triclinic"]},
{"id": "8pqd", "input": "8pqd/8pqd/data/11AT05_w1_1_1_master.h5", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [59.39, 59.37, 192.89, 90.0, 90.0, 90.0], "dmin": 1.5}, "tags": ["h5", "orthorhombic"]},
{"id": "8qaw", "input": "8qaw/p18x3/p18x3_1_00001.cbf.gz", "ref": {"sg": "H 3", "sgno": 146, "cell": [137.68, 137.68, 265.907, 90.0, 90.0, 120.0], "dmin": 1.55}, "tags": ["cbf", "trigonal", "long-axis"]},
{"id": "8qj5", "input": "8qj5/8qj5/data/Lcsbc3_1_2_00001.cbf", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [57.61, 100.63, 77.93, 90.0, 96.06, 90.0], "dmin": 1.63}, "tags": ["cbf", "monoclinic"]},
{"id": "8qq7", "input": "8qq7/cbf/SpNox-Sp49-B3-B_088_1_0001.cbf", "ref": {"sg": "P 64 2 2", "sgno": 181, "cell": [145.967, 145.967, 153.619, 90.0, 90.0, 120.0], "dmin": 3.62}, "tags": ["cbf", "hexagonal"]},
{"id": "8r5r", "input": "8r5r/8r5r/data/D5-33805_1_1_master.h5", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [91.74, 132.91, 137.5, 90.0, 90.0, 90.0], "dmin": 3.08}, "tags": ["h5", "orthorhombic"]},
{"id": "8rud", "input": "8rud/p08x03_1_00001.cbf", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [78.066, 91.386, 114.547, 90.0, 96.9, 90.0], "dmin": 2.1}, "tags": ["cbf", "monoclinic"]},
{"id": "8s38", "input": "8s38/MX733_2_00001.cbf", "ref": {"sg": "I 21 21 21", "sgno": 24, "cell": [95.368, 163.1, 219.023, 90.0, 90.0, 90.0], "dmin": 1.89}, "tags": ["cbf", "orthorhombic"]},
{"id": "8sa8", "input": "8sa8/KlaeA_00906_a_B1-PLP_8sa8/data/PSL-0412_255_master.h5", "ref": {"sg": "I 1 2 1", "sgno": 5, "cell": [87.887, 131.544, 165.363, 90.0, 104.48, 90.0], "dmin": 1.3}, "tags": ["h5", "monoclinic"]},
{"id": "8sqo", "input": "8sqo/BrabA_00028_a_A1-Mg_8sqo/data/PSL-1801_942_master.h5", "ref": {"sg": "P 4 3 2", "sgno": 207, "cell": [112.91, 112.91, 112.91, 90.0, 90.0, 90.0], "dmin": 1.55}, "tags": ["h5", "cubic"]},
{"id": "8sqq", "input": "8sqq/BrabA_00028_a_A1-Apo_8sqq/data/PSL-1810_965_master.h5", "ref": {"sg": "F 4 3 2", "sgno": 209, "cell": [171.52, 171.52, 171.52, 90.0, 90.0, 90.0], "dmin": 2.25}, "tags": ["h5", "cubic"]},
{"id": "8sqt", "input": "8sqt/BrabA_00028_a_A1-Fe_8sqt/data/PSL-1812_971_master.h5", "ref": {"sg": "F 4 3 2", "sgno": 209, "cell": [170.72, 170.72, 170.72, 90.0, 90.0, 90.0], "dmin": 2.2}, "tags": ["h5", "cubic"]},
{"id": "8t7r", "input": "8t7r/8t7r/data/GREEN-04_Pn5_000001.cbf", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [357.076, 259.582, 255.361, 90.0, 133.13, 90.0], "dmin": 3.84}, "tags": ["cbf", "monoclinic"]},
{"id": "8tha", "input": "8tha/1TEL_double_helix_8tha/data/A3_1_00001.cbf", "ref": {"sg": "P 64", "sgno": 172, "cell": [69.16, 69.16, 29.13, 90.0, 90.0, 120.0], "dmin": 1.68}, "tags": ["cbf", "hexagonal"]},
{"id": "8tyy", "input": "8tyy/CPS_2_1_000001.cbf", "ref": {"sg": "F 4 3 2", "sgno": 209, "cell": [214.891, 214.891, 214.891, 90.0, 90.0, 90.0], "dmin": 1.68}, "tags": ["cbf", "cubic"]},
{"id": "8u0i", "input": "8u0i/8u0i/data/dI-2-05a_4_00001.cbf", "ref": {"sg": "P 43 21 2", "sgno": 96, "cell": [50.317, 50.317, 90.596, 90.0, 90.0, 90.0], "dmin": 1.541}, "tags": ["cbf", "tetragonal"]},
{"id": "8v4o", "input": "8v4o/CaalA_00629_a_FS11-AMP_8v4o/data/PSL-1602_2180_master.h5", "ref": {"sg": "P 61 2 2", "sgno": 178, "cell": [139.46, 139.46, 544.99, 90.0, 90.0, 120.0], "dmin": 2.7}, "tags": ["h5", "hexagonal"]},
{"id": "8xbp", "input": "8xbp/8xbp/data/puck2872_3_1_master.h5", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [148.29, 50.78, 60.21, 90.0, 92.33, 90.0], "dmin": 1.99}, "tags": ["h5", "monoclinic"]},
{"id": "8xte", "input": "8xte/1101/G2_1_00001.cbf", "ref": {"sg": "P 32 2 1", "sgno": 154, "cell": [208.77, 208.77, 67.22, 90.0, 90.0, 120.0], "dmin": 1.99}, "tags": ["cbf", "trigonal"]},
{"id": "8xtf", "input": "8xtf/1102/puck03_12_1_master.h5", "ref": {"sg": "H 3 2", "sgno": 155, "cell": [211.75, 211.75, 67.42, 90.0, 90.0, 120.0], "dmin": 2.13}, "tags": ["h5", "trigonal"]},
{"id": "8xtg", "input": "8xtg/1100/mpag15_1_00001.cbf", "ref": {"sg": "P 32 2 1", "sgno": 154, "cell": [199.54, 199.54, 67.15, 90.0, 90.0, 120.0], "dmin": 2.0}, "tags": ["cbf", "trigonal"]},
{"id": "8y74", "input": "8y74/1_1/1_1_master.h5", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [125.782, 76.553, 87.143, 90.0, 92.449, 90.0], "dmin": 1.9}, "tags": ["h5", "monoclinic"]},
{"id": "8ys9", "input": "8ys9/8YS9_xray-data_8ys9/data/SDS_09_18112_master.h5", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [71.04, 77.68, 83.16, 90.0, 90.0, 90.0], "dmin": 1.46}, "tags": ["h5", "orthorhombic"]},
{"id": "9b22", "input": "9b22/KlpnC_20447_a_B1-AR6-AMP_9b22/data/PSL-1011_1580_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [39.826, 92.659, 57.716, 90.0, 91.68, 90.0], "dmin": 1.3}, "tags": ["h5", "monoclinic"]},
{"id": "9bn8", "input": "9bn8/EscoA_17938_a_AE1-UMA-A1AQS_9bn8/data/PSL-0401_8296_master.h5", "ref": {"sg": "P 41", "sgno": 76, "cell": [65.488, 65.488, 134.764, 90.0, 90.0, 90.0], "dmin": 1.35}, "tags": ["h5", "tetragonal"]},
{"id": "9c18", "input": "9c18/BLVRB-2_5217_master.h5", "ref": {"sg": "P 1", "sgno": 1, "cell": [41.94, 41.977, 60.212, 84.11, 87.24, 63.66], "dmin": 1.9}, "tags": ["h5", "triclinic"]},
{"id": "9chw", "input": "9chw/D1.001", "ref": {"sg": "P 61", "sgno": 169, "cell": [98.686, 98.686, 82.062, 90.0, 90.0, 120.0], "dmin": 2.16}, "tags": ["marCCD", "hexagonal"]},
{"id": "9crw", "input": "9crw/data_9crw/data/Kip3-475-22-2_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [83.96, 104.57, 118.78, 90.0, 93.37, 90.0], "dmin": 2.49}, "tags": ["h5", "monoclinic"]},
{"id": "9e2t", "input": "9e2t/ga026f4b-1_5_00001.cbf", "ref": {"sg": "P 1", "sgno": 1, "cell": [75.499, 78.082, 101.219, 94.63, 103.39, 114.55], "dmin": 2.28}, "tags": ["cbf", "triclinic"]},
{"id": "9ea5", "input": "9ea5/K4_1_00001.cbf", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [65.868, 73.064, 98.399, 90.0, 108.725, 90.0], "dmin": 2.0}, "tags": ["cbf", "monoclinic"]},
{"id": "9fcg", "input": "9fcg/IBCH-15-p02x06_2_00001.cbf.gz", "ref": {"sg": "P 4", "sgno": 75, "cell": [87.79, 87.79, 35.633, 90.0, 90.0, 90.0], "dmin": 1.54}, "tags": ["cbf", "tetragonal"], "tiers": {"smoke": "cbf.gz reader, pure 4-fold"}},
{"id": "9fhc", "input": "9fhc/te/te312_2_001.img", "ref": {"sg": "I 2 3", "sgno": 197, "cell": [227.46, 227.46, 227.46, 90.0, 90.0, 90.0], "dmin": 2.2}, "tags": ["marCCD", "cubic"]},
{"id": "9gdj", "input": "9gdj/slh34_w1_1_1_master.h5", "ref": {"sg": "P 41 21 2", "sgno": 92, "cell": [123.92, 123.92, 126.44, 90.0, 90.0, 90.0], "dmin": 1.47}, "tags": ["h5", "tetragonal", "cdte"]},
{"id": "9gjx", "input": "9gjx/BlNTR_9gjx/data/SC6_1_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [76.776, 115.807, 103.807, 90.0, 110.29, 90.0], "dmin": 2.4}, "tags": ["h5", "monoclinic"]},
{"id": "9gqg", "input": "9gqg/MX-2555/FKBP51-MSP500-A-5522-11_1_3094759/FKBP51-MSP500-A-5522-11_1_1_master.h5", "ref": {"sg": "P 32 2 1", "sgno": 154, "cell": [48.157, 48.157, 188.036, 90.0, 90.0, 120.0], "dmin": 2.0}, "tags": ["h5", "trigonal"]},
{"id": "9hnc", "input": "9hnc/asp_7_00001.cbf", "ref": {"sg": "P 1 2 1", "sgno": 3, "cell": [123.764, 123.645, 187.679, 90.0, 90.063, 90.0], "dmin": 1.879}, "tags": ["cbf", "monoclinic"]},
{"id": "9hs7", "input": "9hs7/9hs7/data/B28X1_1_0001.cbf", "ref": {"sg": "P 65", "sgno": 170, "cell": [65.44, 65.44, 88.77, 90.0, 90.0, 120.0], "dmin": 1.698}, "tags": ["cbf", "hexagonal"]},
{"id": "9i0a", "input": "9i0a/carm1END468_9i0a_tar_9I0A/data/X151B1_1_master.h5", "ref": {"sg": "P 21 21 2", "sgno": 18, "cell": [75.237, 98.676, 208.551, 90.0, 90.0, 90.0], "dmin": 2.22}, "tags": ["h5", "orthorhombic"]},
{"id": "9i80", "input": "9i80/20230422-PX1-LecA_RO5-5315-1/LecA-RO5_1_master.h5", "ref": {"sg": "P 41", "sgno": 76, "cell": [81.214, 81.214, 165.029, 90.0, 90.0, 90.0], "dmin": 1.95}, "tags": ["h5", "tetragonal", "twin"]},
{"id": "9ig7", "input": "9ig7/9IG7/data/KOD-P596-G2_3_00001.cbf", "ref": {"sg": "P 21 21 2", "sgno": 18, "cell": [111.472, 153.471, 69.03, 90.0, 90.0, 90.0], "dmin": 2.6}, "tags": ["cbf", "orthorhombic"]},
{"id": "9ih9", "input": "9ih9/run_01_05_datacollection_9IH9/data/design-Xinjian-P1Thro-4_1_5_1_master.h5", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [78.758, 133.933, 82.32, 90.0, 101.356, 90.0], "dmin": 1.7}, "tags": ["h5", "monoclinic"]},
{"id": "9jzo", "input": "9jzo/JS_35P2_C3-002_diffraction_files_9JZO/data/JS_35P2_C3-002_0001.cbf", "ref": {"sg": "P 1", "sgno": 1, "cell": [41.63, 43.1, 54.2, 112.97, 90.11, 118.18], "dmin": 1.4}, "tags": ["cbf", "triclinic"], "tiers": {"smoke": "triclinic P1"}},
{"id": "9khr", "input": "9khr/PfPlrx-DTT-9KHR/data-PfPlrx-DTT-9KHR/ods1467-lp13D1-01001.mccd", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [48.704, 50.31, 78.018, 90.0, 90.0, 90.0], "dmin": 2.0}, "tags": ["marCCD", "orthorhombic"], "tiers": {"smoke": "marCCD reader (.mccd), fast"}},
{"id": "9mh4", "input": "9mh4/KlaeA_00150_a_B1_9mh4/data/PSL-0501_2719_master.h5", "ref": {"sg": "P 21 3", "sgno": 198, "cell": [138.65, 138.65, 138.65, 90.0, 90.0, 90.0], "dmin": 3.05}, "tags": ["h5", "cubic"]},
{"id": "9min", "input": "9min/1151/A8_31_00001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [95.45, 98.54, 155.74, 90.0, 90.0, 90.0], "dmin": 2.05}, "tags": ["cbf", "orthorhombic"], "tiers": {"smoke": "known hard: halved axis"}},
{"id": "9o0h", "input": "9o0h/3TEL-GG-TNK1-UBA_9o0h/data/E1_1_00001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [55.15, 65.54, 112.9, 90.0, 90.0, 90.0], "dmin": 2.24}, "tags": ["cbf", "orthorhombic"]},
{"id": "9p7q", "input": "9p7q/9p7q_9P7Q/data/SAMDC_MTA_273_40_00001.cbf", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [96.978, 45.018, 72.061, 90.0, 105.109, 90.0], "dmin": 2.21}, "tags": ["cbf", "monoclinic"]},
{"id": "9pbb", "input": "9pbb/9pbb_9PBB/data/SAMDC_MTA_293_30_00001.cbf", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [97.418, 45.876, 72.248, 90.0, 104.971, 90.0], "dmin": 2.17}, "pinned": true, "tags": ["cbf", "monoclinic"]},
{"id": "9q41", "input": "9q41/apoSPDS_1_master.h5", "ref": {"sg": "C 2 2 21", "sgno": 20, "cell": [118.568, 133.704, 82.369, 90.0, 90.0, 90.0], "dmin": 1.95}, "tags": ["h5", "orthorhombic"]},
{"id": "9q66", "input": "9q66/PREP_JP417_D8a_12694_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [105.853, 67.294, 158.019, 90.0, 99.109, 90.0], "dmin": 2.01}, "tags": ["h5", "monoclinic"]},
{"id": "9qw8", "input": "9qw8/MX-2555/run_03_datacollection [2023-09-08 22:19:51]/FKBP12BRD4-5519-6-BD1-PPU688_w1_1_1_master.h5", "ref": {"sg": "P 1", "sgno": 1, "cell": [35.648, 35.65, 100.919, 86.458, 84.218, 72.475], "dmin": 1.8}, "tags": ["h5", "triclinic"]},
{"id": "9rci", "input": "9rci/FEN1-CD029134_A09-3_AD049A-10_1_master.h5", "ref": {"sg": "P 1", "sgno": 1, "cell": [35.869, 39.297, 100.916, 98.3, 90.32, 90.09], "dmin": 1.662}, "tags": ["h5", "triclinic", "tncs"]},
{"id": "9rcs", "input": "9rcs/760_B7_x1_1_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [70.01, 78.8, 82.34, 90.0, 88.64, 90.0], "dmin": 3.012}, "tags": ["h5", "monoclinic", "cdte"]},
{"id": "9rp9", "input": "9rp9/data_9RP9/data/TAR148-TAR148_X2_1_master.h5", "ref": {"sg": "C 1 2 1", "sgno": 5, "cell": [73.48, 59.78, 91.67, 90.0, 100.85, 90.0], "dmin": 2.1}, "tags": ["h5", "monoclinic"]},
{"id": "9sl0", "input": "9sl0/wt-1_9sl0/data/design-Yan-HLA-WT-1_1_5_1_master.h5", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [60.181, 80.201, 111.589, 90.0, 90.0, 90.0], "dmin": 1.6}, "tags": ["h5", "orthorhombic"]},
{"id": "9t6s", "input": "9t6s/CadC-AF6586_1_5_1_master.h5", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [63.004, 64.574, 102.705, 90.0, 90.0, 90.0], "dmin": 2.0}, "tags": ["h5", "orthorhombic", "tncs"]},
{"id": "9upt", "input": "9upt/Raw data 1A/C12_7_00001.img", "ref": {"sg": "P 6", "sgno": 168, "cell": [158.27, 158.27, 53.962, 90.0, 90.0, 120.0], "dmin": 2.369}, "tags": ["SMV", "hexagonal"]},
{"id": "9vyb", "input": "9vyb/Antitoxin Phd_9vyb/data/G7-5_4187_master.h5", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [44.37, 47.76, 48.35, 90.0, 90.0, 90.0], "dmin": 2.12}, "tags": ["h5", "orthorhombic"]},
{"id": "9w3y", "input": "9w3y/9w3y/data/collect_01_00001_master.h5", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [60.75, 69.98, 94.24, 90.0, 90.0, 90.0], "dmin": 1.5}, "tags": ["h5", "orthorhombic"]},
{"id": "9yl4", "input": "9yl4/data_MJ4720/UWO0004_06_0001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [95.837, 111.339, 402.967, 90.0, 90.0, 90.0], "dmin": 3.7}, "pinned": true, "tags": ["cbf", "orthorhombic"]},
{"id": "9yzk", "input": "9yzk/CC1_21_TGGGAAGATACTGGATGAGGT_empty_9yzk/data/run_29_00001.cbf", "ref": {"sg": "I 1 2 1", "sgno": 5, "cell": [75.836, 163.022, 192.278, 90.0, 98.614, 90.0], "dmin": 5.1}, "tags": ["cbf", "monoclinic"], "tiers": {"smoke": "I2/C2 setting choice at low resolution"}},
{"id": "9z44", "input": "9z44/CC1_10_CCCGGCCGG_Cclamp_9z44/data/run_41_00001.cbf", "ref": {"sg": "I 1 2 1", "sgno": 5, "cell": [73.491, 127.653, 141.183, 90.0, 91.984, 90.0], "dmin": 7.2}, "tags": ["cbf", "monoclinic"]},
{"id": "9z72", "input": "9z72/A7_1_00001.cbf", "ref": {"sg": "P 31 2 1", "sgno": 152, "cell": [59.173, 59.173, 426.203, 90.0, 90.0, 120.0], "dmin": 2.38}, "tags": ["cbf", "trigonal", "long-axis"], "tiers": {"smoke": "long axis c=426 A, at the FFT ceiling"}},
{"id": "9zlo", "input": "9zlo/ASP0887_09_0114_master.h5", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [38.39, 89.98, 106.97, 90.0, 90.0, 90.0], "dmin": 2.0}, "tags": ["h5", "orthorhombic"]},
{"id": "9zm0", "input": "9zm0/Atg23ANNS_9zm0_9ZM0/data/SeAtg23_B_10_2115_master.h5", "ref": {"sg": "P 1 21 1", "sgno": 4, "cell": [50.409, 30.101, 91.244, 90.0, 97.146, 90.0], "dmin": 2.1}, "tags": ["h5", "monoclinic"]},
{"id": "9zmu", "input": "9zmu/BrmeA_18154_a_B2-Apo_9zmu/data/PSL-0613_7518_master.h5", "ref": {"sg": "P 65 2 2", "sgno": 179, "cell": [47.81, 47.81, 492.58, 90.0, 90.0, 120.0], "dmin": 1.98}, "tags": ["h5", "hexagonal"]},
{"id": "cuhf2", "input": "cuhf2/03_CuHF2pyz2PF6b_P_O/CuHF2pyz2PF6b_P_O_01.nxs", "pinned": true, "tags": ["nxs", "small-molecule"]},
{"id": "cytidine", "input": "cytidine/20151020-Cytidine-2th-30/fixed-omega--180-phi-scan01_00001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [13.98, 14.788, 5.119, 90.0, 90.0, 90.0], "dmin": null}, "tags": ["cbf", "orthorhombic", "small-molecule"]},
{"id": "dnba", "input": "dnba/35dnba_30K_2_04_00001.cbf", "ref": {"sg": "C 1 2/c 1", "sgno": 15, "cell": [20.2635, 8.7575, 9.6697, 90.0, 109.941, 90.0], "dmin": 0.48}, "tags": ["cbf", "monoclinic", "small-molecule"]},
{"id": "lalanine", "input": "lalanine/pgw240050_01_00001.cbf", "ref": {"sg": "P 21 21 21", "sgno": 19, "cell": [5.7952, 5.933, 12.362, 90.0, 90.0, 90.0], "dmin": null}, "tags": ["cbf", "orthorhombic", "small-molecule"], "tiers": {"smoke": "small molecule, miniCBF"}},
{"id": "metformin", "input": "metformin/013_Mmetformin_01_master.h5", "ref": {"sg": "P 1 21/c 1", "sgno": 14, "cell": [7.9104, 13.8794, 7.931, 90.0, 114.606, 90.0], "dmin": 0.45}, "tags": ["h5", "monoclinic", "small-molecule"], "tiers": {"smoke": "small molecule, Diamond I19 NXmx master"}},
{"id": "nidppe", "input": "nidppe/001_NiDppeCl2_01_master.h5", "ref": {"sg": "P 1 21/c 1", "sgno": 14, "cell": [11.2779, 13.3386, 15.8739, 90.0, 98.7953, 90.0], "dmin": 0.77}, "tags": ["h5", "monoclinic", "small-molecule"]}
]}
+107
View File
@@ -0,0 +1,107 @@
"""One document, written twice: Markdown and a self-contained HTML page.
The report is built as a list of blocks (heading, paragraph, bullet list, table) and each writer
turns the same list into its format, so the two can never say different things.
"""
import html
CSS = """
:root { --bg:#fff; --fg:#1d1d1f; --mute:#6e6e73; --line:#d9d9de; --head:#f3f3f5;
--pass:#1a7f37; --fail:#c62828; --warn:#9a6700; --passbg:#e8f5ec; --failbg:#fdecea; }
@media (prefers-color-scheme: dark) {
:root { --bg:#161618; --fg:#e8e8ea; --mute:#9a9aa0; --line:#3a3a3e; --head:#232326;
--pass:#5cc97a; --fail:#ff7b72; --warn:#e3b341; --passbg:#16301f; --failbg:#3a1a1a; }
}
body { background:var(--bg); color:var(--fg); margin:0 auto; max-width:1400px; padding:16px;
font:14px/1.45 system-ui, -apple-system, "Segoe UI", sans-serif; }
h1 { font-size:22px; margin:8px 0 4px; } h2 { font-size:18px; margin:28px 0 8px; }
h3 { font-size:15px; margin:20px 0 6px; }
p, li { max-width:900px; } .mute { color:var(--mute); }
.wrap { overflow-x:auto; margin:8px 0 16px; }
table { border-collapse:collapse; font-variant-numeric:tabular-nums; }
th, td { border-bottom:1px solid var(--line); padding:4px 8px; text-align:left; white-space:nowrap; }
th { background:var(--head); position:sticky; top:0; }
td.num, th.num { text-align:right; }
td.pass { color:var(--pass); font-weight:600; } td.fail { color:var(--fail); font-weight:600; }
td.warn { color:var(--warn); font-weight:600; }
tr.fail td { background:var(--failbg); }
code { font:12px ui-monospace, Menlo, monospace; }
"""
class Doc:
def __init__(self, title):
self.title = title
self.blocks = []
def h(self, level, text):
self.blocks.append(("h", level, text))
def p(self, text):
self.blocks.append(("p", text))
def ul(self, items):
self.blocks.append(("ul", list(items)))
def table(self, head, rows, num=(), row_class=None, cell_class=None):
"""head: column titles; rows: lists of str; num: indices of right-aligned columns;
row_class(row) and cell_class(i, value) give optional CSS classes (HTML only)."""
self.blocks.append(("table", head, rows, set(num), row_class, cell_class))
def markdown(self):
out = [f"# {self.title}", ""]
for b in self.blocks:
if b[0] == "h":
out += ["#" * b[1] + " " + b[2], ""]
elif b[0] == "p":
out += [b[1], ""]
elif b[0] == "ul":
out += ["- " + x for x in b[1]] + [""]
else:
_, head, rows, num, _, _ = b
if not rows:
out += ["(none)", ""]
continue
out.append("| " + " | ".join(head) + " |")
out.append("|" + "|".join("--:" if i in num else ":--" for i in range(len(head))) + "|")
for r in rows:
out.append("| " + " | ".join(str(x).replace("|", "/") for x in r) + " |")
out.append("")
return "\n".join(out)
def html(self):
e = html.escape
out = ["<!DOCTYPE html>", '<html lang="en"><head><meta charset="utf-8">',
'<meta name="viewport" content="width=device-width, initial-scale=1">',
f"<title>{e(self.title)}</title><style>{CSS}</style></head><body>",
f"<h1>{e(self.title)}</h1>"]
for b in self.blocks:
if b[0] == "h":
out.append(f"<h{b[1]}>{e(b[2])}</h{b[1]}>")
elif b[0] == "p":
out.append(f"<p>{e(b[1])}</p>")
elif b[0] == "ul":
out.append("<ul>" + "".join(f"<li>{e(x)}</li>" for x in b[1]) + "</ul>")
else:
_, head, rows, num, row_class, cell_class = b
if not rows:
out.append('<p class="mute">(none)</p>')
continue
t = ['<div class="wrap"><table><thead><tr>']
t += [f'<th class="num">{e(h)}</th>' if i in num else f"<th>{e(h)}</th>"
for i, h in enumerate(head)]
t.append("</tr></thead><tbody>")
for r in rows:
rc = row_class(r) if row_class else ""
t.append(f'<tr class="{rc}">' if rc else "<tr>")
for i, x in enumerate(r):
cls = ["num"] if i in num else []
if cell_class and cell_class(i, x):
cls.append(cell_class(i, x))
c = f' class="{" ".join(cls)}"' if cls else ""
t.append(f"<td{c}>{e(str(x))}</td>")
t.append("</tr>")
t.append("</tbody></table></div>")
out.append("".join(t))
out.append("</body></html>")
return "\n".join(out)
+243
View File
@@ -0,0 +1,243 @@
"""The per-run report and the run-to-run comparison."""
import json
import os
import statistics
from render import Doc
# A change smaller than these is treated as run-to-run noise. They are a starting point, to be
# re-calibrated from `compare --rerun-changed`, which measures the noise directly.
NOISE = {
"d_min": ("rel", 0.02), # 2% of the resolution limit
"isa": ("rel", 0.05),
"r_meas": ("rel", 0.05),
"cc_half": ("abs", 0.005),
"completeness": ("abs", 1.0), # percentage points
"cell_dev_pct": ("abs", 0.2), # percentage points
"wall_s": ("rel", 0.25), # and at least 10 s - see beyond_noise()
}
def load_run(run_dir):
man = json.load(open(os.path.join(run_dir, "manifest.json")))
res = json.load(open(os.path.join(run_dir, "results.json")))
return man, res
def key(r):
return (r["arm"], r["set"])
def f(x, fmt="{:.2f}", dash="-"):
return dash if x is None else fmt.format(x)
def pct(x, nd=1):
return "-" if x is None else f"{100 * x:.{nd}f}%"
def median(xs):
xs = [x for x in xs if x is not None]
return statistics.median(xs) if xs else None
def quartiles(xs):
xs = sorted(x for x in xs if x is not None)
if not xs:
return None
if len(xs) < 4:
return xs[0], xs[len(xs) // 2], xs[len(xs) // 2], xs[len(xs) // 2], xs[-1]
q = statistics.quantiles(xs, n=4)
return xs[0], q[0], q[1], q[2], xs[-1]
def beyond_noise(name, a, b):
if a is None or b is None:
return (a is None) != (b is None)
kind, tol = NOISE[name]
d = abs(b - a)
if name == "wall_s" and d < 10:
return False
return d > (tol * abs(a) if kind == "rel" else tol)
def changes(ra, rb):
"""What changed between two results of the same set, beyond noise."""
out = []
if ra["verdict"] != rb["verdict"]:
out.append(f"verdict {ra['verdict']}->{rb['verdict']}")
if ra.get("sgno") != rb.get("sgno"):
out.append("space group")
if ra.get("volume_ratio") and rb.get("volume_ratio") and \
abs(ra["volume_ratio"] - rb["volume_ratio"]) > 0.05:
out.append("lattice")
for name in ("d_min", "isa", "r_meas", "cc_half", "completeness", "cell_dev_pct"):
if beyond_noise(name, ra.get(name), rb.get(name)):
out.append(name)
if beyond_noise("wall_s", ra.get("wall_s"), rb.get("wall_s")):
out.append("time")
return out
def compare_rows(res_a, res_b):
"""[(key, ra, rb, changes)] over the sets both runs have, then those only one has."""
a = {key(r): r for r in res_a}
b = {key(r): r for r in res_b}
rows = [(k, a[k], b[k], changes(a[k], b[k])) for k in sorted(a) if k in b]
rows += [(k, a[k], None, ["only in A"]) for k in sorted(a) if k not in b]
rows += [(k, None, b[k], ["only in B"]) for k in sorted(b) if k not in a]
return rows
def arrow(x, y, fmt=lambda v: str(v)):
x = "-" if x is None else fmt(x)
y = "-" if y is None else fmt(y)
return x if x == y else f"{x} -> {y}"
def compare_table(doc, rows, only_changed=True):
head = ["set", "arm", "verdict", "space group", "d_min", "ISa", "R_meas", "CC1/2",
"cell dev %", "time s", "beyond noise"]
out = []
for (arm, s), ra, rb, ch in rows:
if only_changed and not ch:
continue
ra, rb = ra or {}, rb or {}
out.append([s, arm, arrow(ra.get("verdict"), rb.get("verdict")),
arrow(ra.get("sg"), rb.get("sg")),
arrow(ra.get("d_min"), rb.get("d_min"), lambda v: f"{v:.2f}"),
arrow(ra.get("isa"), rb.get("isa"), lambda v: f"{v:.1f}"),
arrow(ra.get("r_meas"), rb.get("r_meas"), lambda v: f"{100 * v:.1f}%"),
arrow(ra.get("cc_half"), rb.get("cc_half"), lambda v: f"{v:.3f}"),
arrow(ra.get("cell_dev_pct"), rb.get("cell_dev_pct"), lambda v: f"{v:.2f}"),
arrow(ra.get("wall_s"), rb.get("wall_s"), lambda v: f"{v:.0f}"),
", ".join(ch)])
doc.table(head, out, row_class=lambda r: "fail" if "verdict" in r[-1] else "")
def crystals(rs):
"""Rows that describe a crystal - the no-crystal controls would only pollute a distribution."""
return [r for r in rs if "control" not in r.get("tags", [])]
def scored(rs):
return [r for r in rs if r["verdict"] in ("pass", "fail")]
def rate(rs):
s = scored(rs)
n = sum(r["verdict"] == "pass" for r in s)
return f"{n}/{len(s)} ({100 * n / len(s):.0f}%)" if s else "-"
def build(run_dir, baseline=None):
"""Markdown and HTML report of one run, with the delta against a baseline run if given."""
man, res = load_run(run_dir)
b = man["binary"]
doc = Doc(f"Rugnux battery - {os.path.basename(os.path.normpath(run_dir))}")
if man.get("private"):
doc.p("PRIVATE ARM - confidential user data. This report stays in the run directory; it "
"must not be copied into the repository, an issue or any public report.")
doc.ul([
f"rugnux {b.get('version', '?')}, sha256 {b['sha256'][:16]}, build flags: {b.get('flags') or 'unknown'}",
f"runner {man['runner_git']}, host {man['host']}, {man['start']} -> {man.get('end') or 'NOT FINISHED'}",
f"arms: {', '.join(man['arms'])}; {len(man['sets'])} sets"
+ (f" (subset: {man['subset']})" if man.get("subset") else " (full arm)"),
"resolution: XDS's range forced on the XDS-referenced arms" if man["options"]["xds_range"]
else "resolution: rugnux's own choice everywhere (unforced)",
"timing: NOT a reference - the GPU was shared" if man.get("gpu_shared")
else "timing: GPU was not shared during the run",
f"command options: {' '.join(man['options']['extra_args']) or '(none beyond output selection)'}",
])
forced = {e["arm"] for e in man["sets"]
if man["options"]["xds_range"] and e.get("reference") == "xds"}
order = {arm: i for i, arm in enumerate(man["arms"])}
def gain(rs, arm):
return "forced" if arm in forced else f(median(r.get("res_gain_pct") for r in rs), "{:+.1f}%")
doc.h(2, "Summary")
head = ["arm", "sets", "pass", "fail", "unscored", "not run", "pass rate",
"median res. gain", "median ISa", "median R_meas", "median time s", "total time min"]
rows = []
for arm in man["arms"]:
rs = [r for r in res if r["arm"] == arm]
cnt = {v: sum(r["verdict"] == v for r in rs) for v in ("pass", "fail", "unscored", "not_run")}
xs = crystals(rs)
rows.append([arm, len(rs), cnt["pass"], cnt["fail"], cnt["unscored"], cnt["not_run"], rate(rs),
gain(xs, arm), f(median(r.get("isa") for r in xs), "{:.1f}"),
pct(median(r.get("r_meas") for r in xs)),
f(median(r.get("wall_s") for r in rs), "{:.0f}"),
f(sum(r.get("wall_s") or 0 for r in rs) / 60, "{:.1f}")])
doc.table(head, rows, num=range(1, 12))
doc.p("Pass rate is over the scored sets (pass + fail). Resolution gain is (reference d_min - "
"our d_min) / reference d_min: positive = finer than the deposition, or than XDS; 'forced' "
"where XDS's resolution range was imposed, which makes it zero by construction. Medians "
"leave out the no-crystal controls. R_meas and CC1/2 are pooled over each run's own "
"resolution range, so they describe a run and do not rank two.")
doc.h(3, "By population")
rows = []
for arm in man["arms"]:
rs = [r for r in res if r["arm"] == arm]
for tag in sorted({t for r in rs for t in r.get("tags", [])}):
sub = [r for r in rs if tag in r.get("tags", [])]
rows.append([arm, tag, len(sub), rate(sub), gain(sub, arm)])
doc.table(["arm", "population", "sets", "pass rate", "median res. gain"], rows, num=(2, 4))
doc.h(3, "Distributions (sets that merged, controls left out)")
rows = []
for arm in man["arms"]:
rs = crystals(r for r in res if r["arm"] == arm)
for name, lab, fm in (("isa", "ISa", "{:.1f}"), ("r_meas", "R_meas", None),
("cc_half", "CC1/2", "{:.3f}"), ("res_gain_pct", "res. gain %", "{:+.1f}"),
("wall_s", "time s", "{:.0f}")):
if name == "res_gain_pct" and arm in forced:
continue
q = quartiles(r.get(name) for r in rs)
if q:
cells = [pct(x) if fm is None else f(x, fm) for x in q]
rows.append([arm, lab, sum(r.get(name) is not None for r in rs)] + cells)
doc.table(["arm", "metric", "n", "min", "q1", "median", "q3", "max"], rows, num=range(2, 8))
doc.h(2, "Failures")
fails = [r for r in res if r["verdict"] == "fail"]
doc.table(["set", "arm", "cause", "reason"],
[[r["set"], r["arm"], r["cause"], r["reason"]] for r in fails])
other = [r for r in res if r["verdict"] in ("unscored", "not_run")]
if other:
doc.p("Not scored: " + "; ".join(f"{r['set']} ({r['reason']})" for r in other))
if baseline:
bman, bres = load_run(baseline)
doc.h(2, f"Delta vs baseline {os.path.basename(os.path.normpath(baseline))}")
doc.p(f"Baseline rugnux {bman['binary'].get('version', '?')}. Pass rates on the sets both "
"runs have:")
both = {key(r) for r in res} & {key(r) for r in bres}
rows = []
for arm in man["arms"]:
ra = [r for r in bres if key(r) in both and r["arm"] == arm]
rb = [r for r in res if key(r) in both and r["arm"] == arm]
rows.append([arm, len(rb), rate(ra), rate(rb)])
doc.table(["arm", "common sets", "baseline", "this run"], rows, num=(1,))
doc.p("Sets whose result moved beyond noise (thresholds in report.py NOISE; confirm with "
"`compare --rerun-changed` before believing a single-set change):")
compare_table(doc, compare_rows(bres, res))
doc.h(2, "Per set")
head = ["set", "arm", "verdict", "space group", "ref", "cell dev %", "V ratio", "d_min", "ref d_min",
"gain", "compl %", "mult", "R_meas", "CC1/2", "ISa", "ref ISa", "idx", "time s", "note"]
rows = []
for r in sorted(res, key=lambda r: (order.get(r["arm"], 9), r["set"])):
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}"),
f(r.get("d_min")), f(r.get("d_min_ref")), f(r.get("res_gain_pct"), "{:+.1f}%"),
f(r.get("completeness"), "{:.1f}"), f(r.get("multiplicity"), "{:.1f}"),
pct(r.get("r_meas")), f(r.get("cc_half"), "{:.3f}"), f(r.get("isa"), "{:.1f}"),
f(r.get("isa_ref"), "{:.1f}"), pct(r.get("indexing_rate"), 0),
f(r.get("wall_s"), "{:.0f}"), r.get("reason") or ""])
doc.table(head, rows, num=range(5, 18),
row_class=lambda row: "fail" if row[2] == "fail" else "",
cell_class=lambda i, v: {"pass": "pass", "fail": "fail"}.get(v) if i == 2 else None)
return doc
+173
View File
@@ -0,0 +1,173 @@
"""Scoring one rugnux run against its reference.
One dataset, one verdict, one cause, decided in this order: did it run -> is the lattice right ->
is the symmetry right -> is the merge usable. The order matters: a halved axis also makes the
screw along it unobservable, and counting that row twice would hide the defect that cost the
reflections.
"""
import itertools
import os
import re
import gemmi
import sgequiv
REPORT_LINE = re.compile(r"^([A-Z_0-9]+)=\s*(.*)$")
def read_report(path):
"""KEY= value lines of a rugnux _report.txt."""
out = {}
if os.path.exists(path):
for line in open(path, errors="replace"):
m = REPORT_LINE.match(line.rstrip("\n"))
if m:
out[m.group(1)] = m.group(2).strip()
return out
def fnum(s):
try:
return float(s.split()[0])
except (AttributeError, ValueError, IndexError):
return None
def space_group(name, sgno):
"""The space group in the SETTING the cell was given in: by name when there is one (I 1 2 1 and
C 1 2 1 are both number 5, with different centring vectors), else the standard setting."""
sg = gemmi.find_spacegroup_by_name(name) if name else None
return sg or gemmi.find_spacegroup_by_number(int(sgno or 1))
def primitive_reduced(cell, sg):
"""Niggli-reduced PRIMITIVE cell of a cell given in space group sg (a gemmi.SpaceGroup), and
its volume.
Setting-invariant: two settings of one lattice (C2 vs I2, a/c swapped, beta vs 180-beta)
reduce to the same cell, so they compare equal."""
gv = gemmi.GruberVector(gemmi.UnitCell(*cell), sg)
gv.niggli_reduce()
red = gv.cell_parameters()
return red, gemmi.UnitCell(*red).volume
def lattice_match(cell, sg, ref_cell, ref_sg):
"""(primitive volume ratio, largest relative deviation of the reduced edges)."""
red, vol = primitive_reduced(cell, sg)
ref_red, ref_vol = primitive_reduced(ref_cell, ref_sg)
edge_dev = max(abs(a - b) / b for a, b in zip(sorted(red[:3]), sorted(ref_red[:3])))
return vol / ref_vol, edge_dev
def cell_dev_pct(cell, ref):
"""Largest relative deviation of a, b, c in percent, minimised over the six axis orders, so
P212121 with a and c exchanged is not scored as a 6% error."""
return min(max(abs(p[i] - ref[i]) / ref[i] * 100 for i in range(3))
for p in itertools.permutations(cell[:3]))
def point_group(sgno):
return gemmi.find_spacegroup_by_number(int(sgno)).point_group_hm()
def sg_name(sgno):
return gemmi.find_spacegroup_by_number(int(sgno)).hm if sgno else None
def judge(entry, rep, run_note):
"""Score one set. entry is its manifest row (with 'ref'), rep its parsed report, run_note
what the runner saw ('', 'timeout', 'exit N: message', 'no input')."""
ref = dict(entry.get("ref") or {})
if entry.get("ref_override"): # a reference measured by hand, replacing the file's
ref.update(entry["ref_override"])
r = {
"sgno": None, "sg": None, "pg": None,
"sgno_ref": ref.get("sgno"), "sg_ref": ref.get("sg") or sg_name(ref.get("sgno")),
"pg_ref": point_group(ref["sgno"]) if ref.get("sgno") else None,
"cell": None, "cell_ref": ref.get("cell"), "cell_dev_pct": None, "volume_ratio": None,
"d_min": None, "d_min_ref": ref.get("dmin"), "res_gain_pct": None,
"r_meas": fnum(rep.get("R_MEAS")), "cc_half": fnum(rep.get("CC_HALF")),
"isa": fnum(rep.get("ISA")), "completeness": fnum(rep.get("COMPLETENESS")),
"multiplicity": fnum(rep.get("MULTIPLICITY")), "i_over_sigma": fnum(rep.get("I_OVER_SIGMA")),
"indexing_rate": fnum(rep.get("INDEXING_RATE")), "images": fnum(rep.get("IMAGES_PROCESSED")),
"rugnux_wall_s": fnum(rep.get("WALL_TIME")), "rugnux_verdict": rep.get("VERDICT_TEXT"),
"r_meas_ref": ref.get("r_meas"), "cc_half_ref": ref.get("cc_half"), "isa_ref": ref.get("isa"),
"completeness_ref": ref.get("completeness"),
}
if rep.get("SPACE_GROUP_NUMBER"):
r["sgno"] = int(fnum(rep["SPACE_GROUP_NUMBER"]))
r["sg"] = rep.get("SPACE_GROUP_NAME") or sg_name(r["sgno"])
r["pg"] = point_group(r["sgno"])
if rep.get("UNIT_CELL_CONSTANTS"):
r["cell"] = [float(x) for x in rep["UNIT_CELL_CONSTANTS"].split()[:6]]
rng = (rep.get("INCLUDE_RESOLUTION_RANGE") or "").split()
if len(rng) == 2:
r["d_min"] = float(rng[1])
if r["d_min"] and r["d_min_ref"]:
r["res_gain_pct"] = round((r["d_min_ref"] - r["d_min"]) / r["d_min_ref"] * 100, 1) + 0.0 # no -0.0
if r["cell"] and r["cell_ref"]:
r["cell_dev_pct"] = round(cell_dev_pct(r["cell"], r["cell_ref"]), 3)
def verdict(v, cause, reason):
r.update(verdict=v, cause=cause, reason=reason)
return r
have_lattice = r["sgno"] is not None and r["cell"] is not None
if entry.get("expect") == "no_lattice":
# a no-crystal control: the right answer is to refuse
if have_lattice:
return verdict("fail", "false_lattice",
f"reported a lattice ({r['sg']}) on a set with no crystal")
return verdict("pass", None, "no lattice reported, as expected")
if not have_lattice:
if run_note == "no input":
return verdict("not_run", "no_input", "input file not found")
if run_note == "timeout":
return verdict("fail", "timeout", "timed out")
if re.search(r"Error reading input|Cannot open", run_note):
return verdict("fail", "reader", run_note)
if re.search(r"index|lattice", run_note, re.I):
return verdict("fail", "indexing", run_note)
return verdict("fail", "crash", run_note or "no lattice in the report")
if not ref.get("sgno") or not ref.get("cell"):
return verdict("unscored", "no_reference", "no reference to score against")
vr, edge = lattice_match(r["cell"], space_group(rep.get("SPACE_GROUP_NAME"), r["sgno"]),
ref["cell"], space_group(ref.get("sg"), ref["sgno"]))
r["volume_ratio"] = round(vr, 4)
if not (0.95 < vr < 1.05 and edge < 0.02):
if 0.45 <= vr <= 0.55:
return verdict("fail", "lattice_halved", f"primitive volume ratio {vr:.2f}")
if 1.9 <= vr <= 2.1:
return verdict("fail", "lattice_doubled", f"primitive volume ratio {vr:.2f}")
return verdict("fail", "lattice_other",
f"primitive volume ratio {vr:.2f}, reduced edges off by {edge:.1%}")
# XDS never tests a screw axis, so against an XDS reference only the point group can be
# judged - unless the group was measured by hand (sg_measured).
by_sg = entry["arm"] == "open" or ref.get("sg_measured")
if by_sg:
rel = sgequiv.describe(ref["sgno"], r["sgno"])
r["sg_relation"] = rel
ok = rel in ("match", "hand only (needs anomalous)", "UNDECIDABLE from intensities")
else:
ok = r["pg"] == r["pg_ref"]
if not ok:
order = len(gemmi.find_spacegroup_by_number(r["sgno"]).operations().sym_ops)
order_ref = len(gemmi.find_spacegroup_by_number(ref["sgno"]).operations().sym_ops)
if r["pg"] == r["pg_ref"]:
return verdict("fail", "sym_screw", f"{r['sg']} vs reference {r['sg_ref']}")
cause = ("sym_under" if order < order_ref else
"sym_over" if order > order_ref else "sym_other")
return verdict("fail", cause, f"{r['sg']} vs reference {r['sg_ref']}")
if r["r_meas"] is not None and r["r_meas"] > 0.6:
return verdict("fail", "merge", f"R_meas {r['r_meas']:.0%} overall")
if r["cc_half"] is not None and r["cc_half"] < 0.5:
return verdict("fail", "merge", f"CC1/2 {r['cc_half']:.2f} overall")
note = ""
if r["sgno"] != ref["sgno"]:
note = f"{r['sg']} vs reference {r['sg_ref']} ({r.get('sg_relation') or 'screws not judged against XDS'})"
return verdict("pass", None, note)
+47
View File
@@ -0,0 +1,47 @@
"""Which space-group differences a diffraction experiment could actually have decided.
A merged dataset sees only (a) which reflections are equivalent - the rotation parts of the
operations in their real orientation - and (b) which are systematically absent. Two space
groups agreeing on both cannot be told apart without solving the structure. Body centring is
the usual culprit: h+k+l=2n already forces h00: h=2n, so a 2_1 along <100> adds nothing.
"""
import gemmi, collections
_N = 7
_HKL = [(h, k, l) for h in range(-_N, _N+1) for k in range(-_N, _N+1) for l in range(-_N, _N+1)
if (h, k, l) != (0, 0, 0)]
def _signature(sg):
ops = sg.operations()
return (frozenset(tuple(map(tuple, op.rot)) for op in ops),
frozenset(tuple(t) for t in ops.cen_ops),
tuple(ops.is_systematically_absent(h) for h in _HKL))
_by_sig = collections.defaultdict(set)
for _n in range(1, 231):
_by_sig[_signature(gemmi.find_spacegroup_by_number(_n))].add(_n)
CLASS = {n: frozenset(s) for s in _by_sig.values() for n in s if len(s) > 1}
def indistinguishable(a, b):
"""True if intensities alone could never have separated these two space groups."""
return a == b or b in CLASS.get(a, ())
def enantiomorphic(a, b):
"""True if the pair differs only by hand - anomalous signal can break it, absences cannot."""
return (indistinguishable(a, b) and a != b
and gemmi.find_spacegroup_by_number(a).is_enantiomorphic())
def describe(a, b):
if a == b: return "match"
if enantiomorphic(a, b): return "hand only (needs anomalous)"
if indistinguishable(a, b): return "UNDECIDABLE from intensities"
return "REAL difference (absences differ)"
if __name__ == "__main__":
seen = set()
for n, s in sorted(CLASS.items()):
if s in seen: continue
seen.add(s)
ns = sorted(s)
print(" ", " = ".join("%d %s" % (i, gemmi.find_spacegroup_by_number(i).hm) for i in ns),
" ->", describe(ns[0], ns[1]))
+11
View File
@@ -0,0 +1,11 @@
{
"runs_root": "/data/battery/runs",
"lock_file": "/data/battery/full_run.lock",
"gpulock": "/data/tmp/gpulock",
"arms": {
"open": {"manifest": "open.json", "reference": "deposition", "data_root": "/home/data/nonsls/raw"},
"inhouse": {"manifest": "inhouse.json", "reference": "xds", "data_root": "/data/rotation_test"},
"private": {"manifest": "/data/battery/private.json", "reference": "xds", "data_root": "/data/rotation_test",
"private": true}
}
}