Files
Jungfraujoch/tools/battery/score.py
T
leonarski_fandClaude Opus 5.5 d495a01de4 battery: a handled GPU error is a crash only when it is a device fault
A planned fallback that rugnux logs with a CUDA error text - the rigid body's zone over its
engine's reserve, revalidated on the CPU, or an indexer worker out of device memory while other
jobs held the card - would have turned sets into crashes in every run. A handled error now counts
only when it names a device fault (unspecified launch failure, illegal memory access/address,
misaligned address, launch timeout, device-side assert, "CUDA device unusable"), which takes the
context down for every thread and is never a property of the data. "Processing failed" still
counts whatever its cause.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi
2026-10-10 09:24:48 +02:00

443 lines
25 KiB
Python

"""Scoring one rugnux run against its reference.
One dataset, one verdict, one cause, decided in this order: did it crash -> did it run -> is the
lattice right -> is the symmetry right. A crash is its own verdict and fails the set whatever its
report says, a no-crystal control included. Data quality is not scored: it is reported for a human to judge. 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.
A row may accept more than one reference (`ref_alternatives`, see `alternatives`): where two
descriptions of the same crystal are both defensible and nothing available to us decides between
them, either answer passes, and the report says which rows those are.
"""
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, plus the outer shells of its shell tables:
LOWRES_D / LOWRES_R_MEAS (the lowest-resolution shell) and HIGHRES_CC_HALF (the highest) from
the table over its own range, REFRES_LOWRES_D / REFRES_LOWRES_R_MEAS from the one over the
reference range (it follows REFRES_RANGE=)."""
out = {}
if not os.path.exists(path):
return out
header, first = None, False
for line in open(path, errors="replace"):
line = line.rstrip("\n")
m = REPORT_LINE.match(line)
if m:
out[m.group(1)] = m.group(2).strip()
continue
f = line.lstrip("#").split()
if f[:2] == ["d_min", "N_obs"]:
header, first = f, True
elif header and f and re.match(r"^\d+\.\d+$", f[0]):
# Three tables in report order: the run's own, the --report-resolution one, and the
# per-hand (FRIEDEL'S_LAW=FALSE) one, whose keys are written just above it.
prefix = ("ANOM_" if "ANOM_UNIQUE_REFLECTIONS" in out
else "REFRES_" if "REFRES_RANGE" in out else "")
if first:
out[prefix + "LOWRES_D"] = f[0]
out[prefix + "LOWRES_R_MEAS"] = percent(f[header.index("R_meas")])
first = False
if not prefix: # every row of the own table; the last is the highest shell
out["HIGHRES_CC_HALF"] = percent(f[header.index("CC1/2")])
elif header and not first:
header = None # past the last row
return out
# A line in rugnux's log or terminal output that makes a run a crash. A device fault (a kernel or copy
# that touched memory it must not, which takes the CUDA context down for every thread) counts even
# where rugnux handled it and went on: it is a defect, not a property of the data. A GPU failure that
# rugnux plans for - a buffer that does not fit, answered on the CPU - is not one.
DEVICE_FAULT = (r"unspecified launch failure|illegal memory access|illegal address|misaligned address"
r"|illegal instruction|launch timed out|hardware stack error|device-side assert"
r"|CUDA device unusable")
CRASH_LINE = re.compile(r"Processing failed|terminate called|Segmentation fault|AddressSanitizer"
r"|ERROR SUMMARY: [1-9]|" + DEVICE_FAULT)
def crash_note(exit_code, wd):
"""Why the run in wd counts as a crash, or "" when it does not: killed by a signal, or a
CRASH_LINE in its log (p_log.txt, whose lines are whole) or its terminal output (run.log, which
also has what the C++ runtime and compute-sanitizer print). A timeout (exit_code None) is not a
crash, and neither is rugnux's own refusal ("No crystal lattice found", exit 1)."""
if exit_code is None:
return ""
if exit_code < 0:
return f"killed by signal {-exit_code}"
for name in ("p_log.txt", "run.log"):
try:
for line in open(os.path.join(wd, name), errors="replace"):
if CRASH_LINE.search(line):
return f"exit {exit_code}: " + line.strip().split("] ")[-1][:300]
except OSError:
pass
return ""
def percent(s):
"""A shell table's "3.7%" as "0.037", and "-" (none) as ""."""
s = s.rstrip("%")
return "" if s == "-" else str(round(float(s) / 100, 4))
# REFRES_* keys of the report read as numbers, stored lower-case under the same name
REFRES_KEYS = ("refres_shells_past_limit", "refres_unique_reflections", "refres_completeness",
"refres_multiplicity", "refres_i_over_sigma", "refres_r_meas", "refres_cc_half",
"refres_isa", "refres_r_meas_weighted")
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]))
# XDS prints CC1/2 in % with one decimal; its value is taken at the bottom of that rounding, so a
# printed 100.0 does not demand a perfect CC1/2 of rugnux
XDS_CC_HALF_ROUNDING = 0.0005
def cc_half_noise_ratio(cc, cc_xds):
"""rugnux's half-set noise over XDS's, both read off CC1/2 over XDS's range; None without both.
CC1/2 = S / (S + E), with S the variance of the signal and E that of the half-set error, so
E / S = 1 / CC1/2 - 1 at any CC1/2. E goes as 1 / (observations): a ratio of 2 is a merge as
noisy as XDS's would be with half of its observations. Reported, not scored.
A CC1/2 at or below 0 is no signal at all, counted as 0.001."""
if cc is None or cc_xds is None:
return None
return round((1 / max(cc, 0.001) - 1) / (1 / (cc_xds - XDS_CC_HALF_ROUNDING) - 1), 3)
_AXIS_ROW = {"a": (1, 0, 0), "b": (0, 1, 0), "c": (0, 0, 1)}
def screw_axes_differing(sg, ref_sg):
"""The axes on which two space groups disagree about a screw: the principal row one of them
extinguishes and the other does not.
Only ever asked of two groups of the SAME point group on the same lattice, so their centrings
are the same and a row a centring already kills is killed in both - what is left is the screw."""
out = set()
for ax, hkl in _AXIS_ROW.items():
a, b = (s.operations().is_systematically_absent(hkl) for s in (sg, ref_sg))
if a != b:
out.add(ax)
return out
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 alternatives(entry):
"""The other answers a row accepts, from its `ref_alternatives`.
A knife-edge row - one where the deposition and our reduction disagree, the disagreement is
real, and no test available to us settles it - says so here instead of being quietly overridden
to our answer or dropped from scoring. Each alternative replaces the reference fields it names
(a space group, a cell, or both) and the row passes if our answer matches ANY of the
references, the deposited one included. `ref` keeps the deposition verbatim either way.
Every alternative must carry `why`: a reason a reader can check. Without one this would be a
way to launder a failure into a pass, so it is a schema error, not a silent pass."""
out = []
for i, alt in enumerate(entry.get("ref_alternatives") or []):
where = f"{entry.get('id', '?')}: ref_alternatives[{i}]"
if not (alt.get("why") or "").strip():
raise ValueError(f"{where} has no 'why'; an accepted alternative must state why both "
"answers are acceptable")
if not set(alt) & {"sg", "sgno", "cell"}:
raise ValueError(f"{where} replaces nothing; give the sg, the cell, or both")
out.append(alt)
return out
def alt_label(alt):
"""How an accepted alternative is named in one line: its space group, its cell, or both."""
bits = []
if alt.get("sg") or alt.get("sgno"):
bits.append(alt.get("sg") or sg_name(alt["sgno"]))
if alt.get("cell"):
bits.append("cell " + " ".join(f"{x:g}" for x in alt["cell"][:3]))
return ", ".join(bits)
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', 'crash: why' (crash_note), '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"])
alts = alternatives(entry) # raises on a row that accepts an answer for no reason
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"), "d_min_ref_rule": ref.get("dmin_rule"),
"d_min_xds": ref.get("dmin_xds"), "res_gain_pct": None,
"r_meas": fnum(rep.get("R_MEAS")), "cc_half": fnum(rep.get("CC_HALF")),
# R_MEAS is counted as XDS counts it; the weighted one weights each observation as the
# merge does (absent from reports before the weighting)
"r_meas_weighted": fnum(rep.get("R_MEAS_WEIGHTED")),
"isa": fnum(rep.get("ISA")), "completeness": fnum(rep.get("COMPLETENESS")),
"mosaicity_deg": fnum(rep.get("MOSAICITY_DEG")), "extra_lattice_intensity_pct": fnum(rep.get("EXTRA_LATTICE_INTENSITY_PCT")),
"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"), "multiplicity_ref": ref.get("multiplicity"),
"sg_relation": None,
# rugnux's own fit of the deposited model (--model; the open arm, and the in-house sets that
# name a model): a rigid-body placement scored on rugnux's own free set, so a trend number,
# not the depositor's R-free
"rfree": fnum(rep.get("R_FREE")), "rwork": fnum(rep.get("R_WORK")),
# the anomalous difference map's mean height (sigma) at the model's anomalous scatterers
# (P and heavier: S, metals, Cl, I, ...) - how much anomalous signal the merge carries
"anom_sigma": fnum(rep.get("ANOMALOUS_SCATTERER_MEAN_SIGMA")),
"model_fit": rep.get("MODEL_FIT"), "cc_model": fnum(rep.get("CC_MODEL_OVERALL")),
# R over ALL the reflections, with and without one free scale per resolution shell, and the
# size of that rescale. rfree above describes one run and is not comparable with another's:
# the model's scale can only bend as k*exp(-B s^2), so a change in the amplitudes' radial
# profile alone moves it. rmodel_shell_scaled has that taken out and IS comparable; it is
# the R to read in a delta table. radial_misfit says how much there was to take out.
"rmodel": fnum(rep.get("R_MODEL")), "rmodel_shell_scaled": fnum(rep.get("R_MODEL_SHELL_SCALED")),
"radial_misfit": fnum(rep.get("MODEL_RADIAL_MISFIT")),
"sg_label": None, "accepted_alt": None, "accepted_alt_why": None,
# the groups rugnux could not tell from the one it wrote (and, with an undetermined screw,
# the screw alternatives), as it lists them
"sg_alternatives": rep.get("SPACE_GROUP_ALTERNATIVES") or None,
}
# the same merge binned over the reference's range (--report-resolution, XDS arms)
r.update({k: fnum(rep.get(k.upper())) for k in REFRES_KEYS})
r["refres_range"] = rep.get("REFRES_RANGE")
# An XDS reference measured with FRIEDEL'S_LAW=FALSE counts its uniques, completeness,
# multiplicity, R_meas and CC1/2 per Bijvoet hand, so what is like for like is the per-hand
# table of the same rugnux merge - the Friedel-merged one has about twice the multiplicity.
# Both tables come out of one bare run: -A is presentation only and changes nothing that is
# compared here. ISa is not among them - there is one error model, fitted on the hands, and
# REFRES_ISA is it.
r["per_hand_reference"] = False
if ref.get("anomalous"):
for k in ("unique_reflections", "completeness", "multiplicity", "i_over_sigma",
"r_meas", "cc_half"):
v = fnum(rep.get("ANOM_REFRES_" + k.upper()))
if v is not None:
r["refres_" + k] = v
r["per_hand_reference"] = True
r["isa_ratio"] = round(r["refres_isa"] / r["isa_ref"], 4) if r["refres_isa"] and r["isa_ref"] else None
r["r_meas_ratio"] = (round(r["refres_r_meas"] / r["r_meas_ref"], 4)
if r["refres_r_meas"] and r["r_meas_ref"] else None)
# R_meas of the lowest-resolution shell: rugnux's own table, the reference-range table and XDS's
# (reported, not scored)
r.update(lowres_d=fnum(rep.get("LOWRES_D")), lowres_r_meas=fnum(rep.get("LOWRES_R_MEAS")),
refres_lowres_d=fnum(rep.get("REFRES_LOWRES_D")),
refres_lowres_r_meas=fnum(rep.get("REFRES_LOWRES_R_MEAS")),
lowres_d_ref=ref.get("dmin_low"), lowres_r_meas_ref=ref.get("r_meas_low"))
# CC1/2 of the highest-resolution shell: rugnux's own table and XDS's (reported, not scored)
r.update(cc_half_high=fnum(rep.get("HIGHRES_CC_HALF")), cc_half_high_ref=ref.get("cc_half_high"))
r["lowres_r_meas_ratio"] = (round(r["refres_lowres_r_meas"] / r["lowres_r_meas_ref"], 4)
if r["refres_lowres_r_meas"] and r["lowres_r_meas_ref"] else None)
r["cc_half_noise_ratio"] = cc_half_noise_ratio(r["refres_cc_half"], r["cc_half_ref"])
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"])
# With --model the reported group can be the model's enantiomorph, a label the data did not
# decide. The data's own answer is then the search's Sohncke group (same class up to hand).
if rep.get("SPACE_GROUP_ENANTIOMORPH") == "ASSUMED_FROM_MODEL" and rep.get("SOHNCKE_SPACE_GROUP"):
own = gemmi.find_spacegroup_by_name(rep["SOHNCKE_SPACE_GROUP"])
if own and own.number != r["sgno"] and sgequiv.indistinguishable(r["sgno"], own.number):
r["sg_label"] = r["sg"]
r["sgno"], r["sg"] = own.number, own.hm
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
if run_note.startswith("crash: "):
return verdict("crash", "crash", run_note[len("crash: "):])
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", "error", 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")
if entry.get("unscored"): # a reference known to be wrong: run, but do not score
return verdict("unscored", "reference_problem", entry["unscored"])
def against(cand):
"""Score the answer against one reference: (the numbers it yields, None) if that reference
accepts the answer, (the numbers, (verdict, cause, reason)) if it does not."""
n = {"volume_ratio": None, "sg_relation": None}
vr, edge = lattice_match(r["cell"], space_group(rep.get("SPACE_GROUP_NAME"), r["sgno"]),
cand["cell"], space_group(cand.get("sg"), cand["sgno"]))
n["volume_ratio"] = round(vr, 4)
if not (0.95 < vr < 1.05 and edge < 0.02):
if 0.45 <= vr <= 0.55:
return n, ("fail", "lattice_halved", f"primitive volume ratio {vr:.2f}")
if 1.9 <= vr <= 2.1:
return n, ("fail", "lattice_doubled", f"primitive volume ratio {vr:.2f}")
return n, ("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 cand.get("sg_measured")
pg_cand = point_group(cand["sgno"])
# Our group in our own setting: does some setting of the reference show the same Laue
# rotations and the same absences? Then intensities could never have told them apart.
same = sgequiv.same_intensities(space_group(r["sg"], r["sgno"]), cand["sgno"])
if by_sg:
rel = sgequiv.describe(cand["sgno"], r["sgno"])
n["sg_relation"] = rel
ok = (rel in ("match", "hand only (needs anomalous)")
or (rel == "UNDECIDABLE from intensities" and same))
else:
ok = r["pg"] == pg_cand
if ok:
return n, None
sg_cand = cand.get("sg") or sg_name(cand["sgno"])
if same:
# A different point group of the same Laue class with exactly the same absences
# (I41md vs I-42d, Pc vs P2/c, Pna21 vs Pnma, P2 vs P2/m): the merged intensities hold
# nothing that separates them, only a model or a strong anomalous signal does. rugnux
# must still write one group and lists the rest in SPACE_GROUP_ALTERNATIVES. Its answer
# is as right as the data allow, so the row passes, with its own cause. Being listed
# alone is NOT enough: the list also carries the screw alternatives of an undetermined
# axis, whose absences differ, and those keep the screw rule below.
offered = {g.number for g in (gemmi.find_spacegroup_by_name(x.strip()) for x in
(rep.get("SPACE_GROUP_ALTERNATIVES") or "").split("|")) if g}
listed = "listed" if cand["sgno"] in offered else "NOT listed"
return n, ("pass", "indistinguishable_alternative",
f"{r['sg']} vs reference {sg_cand}: same Laue class and absences, "
f"undecidable from intensities ({listed} in SPACE_GROUP_ALTERNATIVES)")
order = len(gemmi.find_spacegroup_by_number(r["sgno"]).operations().sym_ops)
order_ref = len(gemmi.find_spacegroup_by_number(cand["sgno"]).operations().sym_ops)
if r["pg"] == pg_cand:
# A screw the data never tested is not a wrong answer, it is an unanswerable question:
# the axial row it lives on was not recorded at all (the spindle's blind cone), so the
# run offers every member of the set as an alternative and writes the one claiming no
# screw, because a reflection file must carry one group. Scoring that as a failure
# measures the sweep, not the program - the same reason the hand of an enantiomorphic
# pair is not scored. Three conditions, all necessary: rugnux SAYS the axis is open,
# the reference is among the groups it offered, and the ONLY axis the two differ on is
# one it named. A screw called wrongly where the row WAS measured meets none of them
# and stays a failure.
# WHICH axis carries the disagreement is a property of the setting, and the two sides
# are named in different ones: the reference in the deposition's axis labels, ours - and
# SPACE_GROUP_SCREW_UNDETERMINED with it - in our own. Where the cells are a permutation
# of each other the two never meet: a deposited P 21 21 2 differs from our P 21 21 21 on
# c, while the same group written in our axes differs on b, the axis actually left open.
# So the axes are compared against the offered group, which is already in our setting;
# the reference being among the offered is a condition anyway, and a group offered under
# more than one setting passes if any of them fits.
undetermined = set((rep.get("SPACE_GROUP_SCREW_UNDETERMINED") or "NONE").split())
offered = [gemmi.find_spacegroup_by_name(x.strip())
for x in (rep.get("SPACE_GROUP_ALTERNATIVES") or "").split("|")]
ours = space_group(rep.get("SPACE_GROUP_NAME"), r["sgno"])
for o in (x for x in offered if x and x.number == cand["sgno"]):
differ = screw_axes_differing(ours, o)
if undetermined != {"NONE"} and differ and differ <= undetermined:
here = f" (here {o.hm})" if o.hm != sg_cand else ""
return n, ("unscored", "screw_undetermined",
f"{r['sg']} vs reference {sg_cand}{here}: the screw along "
f"{', '.join(sorted(differ))} is undeterminable from these data "
f"(offered {rep.get('SPACE_GROUP_ALTERNATIVES')})")
return n, ("fail", "sym_screw", f"{r['sg']} vs reference {sg_cand}")
cause = ("sym_under" if order < order_ref else
"sym_over" if order > order_ref else "sym_other")
return n, ("fail", cause, f"{r['sg']} vs reference {sg_cand}")
# The deposition (with any ref_override) decides, and its numbers are the ones reported: an
# accepted alternative changes the verdict, never what the row says about the deposition.
numbers, problem = against(ref)
r.update(numbers)
for alt in (alts if problem and problem[0] == "fail" else []):
if against(dict(ref, **{k: v for k, v in alt.items() if k != "why"}))[1] is None:
r["accepted_alt"], r["accepted_alt_why"] = alt_label(alt), alt["why"]
return verdict("pass", "accepted_alternative",
f"{r['sg']} vs reference {r['sg_ref']}: accepted alternative "
f"{r['accepted_alt']} - a knife-edge this battery does not decide")
if problem:
return verdict(*problem)
# Data quality (CC1/2, R_meas, resolution, ISa) is reported, never scored: that is for a human.
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)