From ae19223549e64190e36dc6be9a107d3580dbb6f2 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 20 Sep 2026 18:56:42 +0200 Subject: [PATCH] battery: judge an undetermined screw in our own axes The screw_undetermined rule compared the axial rows of our answer with those of the reference as deposited. Which axis carries the disagreement is a property of the setting, so where the two cells are a permutation of each other the rule looked at one axis and SPACE_GROUP_SCREW_UNDETERMINED named another, and a row whose open screw the run had explicitly declined to decide was scored as a wrong space group. The comparison now runs against the offered alternative of the reference's number, which is already written in our axes; a group offered under more than one setting fires if any of them fits. Verdicts moved on 7mzt only (fail/sym_screw -> unscored/screw_undetermined) in both persisted runs re-scored; no other row changed. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT --- tools/battery/score.py | 25 ++++++++++++------- tools/battery/test_score.py | 48 +++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/tools/battery/score.py b/tools/battery/score.py index d033a7239..6d213c039 100644 --- a/tools/battery/score.py +++ b/tools/battery/score.py @@ -298,17 +298,26 @@ def judge(entry, rep, run_note): # 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("|")] - differ = screw_axes_differing(space_group(rep.get("SPACE_GROUP_NAME"), r["sgno"]), - space_group(cand.get("sg"), cand["sgno"])) - if (undetermined != {"NONE"} and differ and differ <= undetermined - and any(o and o.number == cand["sgno"] for o in offered)): - return n, ("unscored", "screw_undetermined", - f"{r['sg']} vs reference {sg_cand}: the screw along " - f"{', '.join(sorted(differ))} is undeterminable from these data " - f"(offered {rep.get('SPACE_GROUP_ALTERNATIVES')})") + 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") diff --git a/tools/battery/test_score.py b/tools/battery/test_score.py index 79d5d2e76..51898bc8a 100644 --- a/tools/battery/test_score.py +++ b/tools/battery/test_score.py @@ -93,5 +93,53 @@ class AcceptedAlternatives(unittest.TestCase): score.alternatives(e) +# A screw along an axis the sweep never recorded: an unanswerable question, not a wrong answer. +# The cells are synthetic, and the second is the first cyclically permuted. +ORTHO = [97.0, 108.0, 113.0, 90.0, 90.0, 90.0] +ORTHO_PERMUTED = [113.0, 97.0, 108.0, 90.0, 90.0, 90.0] + + +class ScrewUndetermined(unittest.TestCase): + def judge(self, ref_cell, undetermined, alternatives): + """Our P 21 21 21 on ORTHO against a deposited P 21 21 2 on ref_cell.""" + e = {"id": "test_screw", "arm": "open", + "ref": {"sg": "P 21 21 2", "sgno": 18, "cell": ref_cell}} + return score.judge(e, report("P 21 21 21", 19, ORTHO, + SPACE_GROUP_SCREW_UNDETERMINED=undetermined, + SPACE_GROUP_ALTERNATIVES=alternatives), "") + + def test_same_setting(self): + """The plain case: the reference is offered verbatim and the open axis is the one they + differ on.""" + r = self.judge(ORTHO, "c", "P 21 21 2") + self.assertEqual((r["verdict"], r["cause"]), ("unscored", "screw_undetermined")) + + def test_permuted_setting(self): + """The deposition's axes are a cyclic permutation of ours, so it names the open screw c + while we name it b. The comparison must be made in OUR setting - against the offered + group of the reference's number - or the rule misses a row it covers.""" + r = self.judge(ORTHO_PERMUTED, "b", "P 21 2 21") + self.assertEqual((r["verdict"], r["cause"]), ("unscored", "screw_undetermined")) + self.assertIn("the screw along b", r["reason"]) + + def test_a_wrong_screw_still_fails(self): + """The open axis is not the axis the two differ on: in our own setting the disagreement + sits on b while the run says it could not determine a, so the b row WAS measured. That is + a wrong answer and stays one - the new framing must not turn every permutation into an + excuse.""" + r = self.judge(ORTHO_PERMUTED, "a", "P 21 2 21") + self.assertEqual((r["verdict"], r["cause"]), ("fail", "sym_screw")) + + def test_nothing_undetermined_still_fails(self): + """Every axial row measured: no rule to apply.""" + r = self.judge(ORTHO, "NONE", "") + self.assertEqual((r["verdict"], r["cause"]), ("fail", "sym_screw")) + + def test_the_reference_must_be_among_the_offered(self): + """An open axis alone does not excuse a group the run never offered.""" + r = self.judge(ORTHO, "b", "P 21 21 21") + self.assertEqual((r["verdict"], r["cause"]), ("fail", "sym_screw")) + + if __name__ == "__main__": unittest.main()