From da5ef0c5032031a329943bab69714c1575146d64 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 3 Oct 2026 21:40:42 +0200 Subject: [PATCH] SearchSpaceGroup: offer every setting of the chosen rotation set, not only on a near-exact cell The non-reference settings of the adopted point group were offered only when the cell hosted their rotations to within ~0.1 deg, while the reference settings - which hold the very same rotations - were offered without asking. A cell refined free after integration a few tenths of a degree off 90 therefore lost every setting but the reference one. On an orthorhombic set whose measured screws lie on a and c and whose b row was never recorded, that left P2(1)2(1)2(1) as the only candidate covering both screws, and it was reported as determined. With P 21 2 21 offered, the two tie, the b-axis screw is reported as undetermined, and the model check uses the setting the data describe. The point-group stage still asks the cell whether a rotation set it adds is hosted; only the setting enumeration within an already chosen set stops asking. Validation: myob/cytc/thau x10sa p.mtz byte-identical (GPU); 7mzt fail -> unscored (b screw undetermined, P 21 21 21 or P 21 2 21); 5cc8 unchanged; [SearchSpaceGroup] 23 cases pass incl. a new section with a cell 0.2-0.3 deg off 90. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01K5K8jvPPbmCrbqnWkddTuB --- image_analysis/scale_merge/SearchSpaceGroup.cpp | 14 +++++++++----- tests/SearchSpaceGroupTest.cpp | 11 +++++++++++ 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/image_analysis/scale_merge/SearchSpaceGroup.cpp b/image_analysis/scale_merge/SearchSpaceGroup.cpp index 3b99f6e3e..59eb289eb 100644 --- a/image_analysis/scale_merge/SearchSpaceGroup.cpp +++ b/image_analysis/scale_merge/SearchSpaceGroup.cpp @@ -1900,9 +1900,14 @@ SearchSpaceGroupResult SearchSpaceGroup( // the point group is one only a non-reference setting carries (Stage A's second pass), since // otherwise there is no candidate at all and the group would be lost after being found. These are // alternative namings at the same order, so nothing here can promote the point group; what they - // add is a screw or a centering on the axis the data show it on. Two refusals bound them: a - // setting whose axes the cell does not have is not offered, and one predicting exactly the - // absences a candidate already offered predicts is the same hypothesis under another name. + // add is a screw or a centering on the axis the data show it on. One refusal bounds them: a setting + // predicting exactly the absences a candidate already offered predicts is the same hypothesis + // under another name. The cell is NOT asked again here: a setting of the chosen rotation set holds + // the very rotations the reference candidates hold, so it fits the cell exactly as well as they + // do - and they are offered without asking. Asking only the non-reference ones turned a free- + // refined cell a few tenths of a degree off 90 into a refusal of every setting but the reference + // one, leaving the group with the measured screws on a and c (P 2_1 2 2_1) unoffered and P2_12_12_1 + // the only candidate that covered both. if (opt.cell.has_value() && (opt.enumerate_all_settings || (opt.enumerate_all_rotation_sets && sg_cands.empty()))) { std::vector> signatures; @@ -1910,8 +1915,7 @@ SearchSpaceGroupResult SearchSpaceGroup( signatures.push_back(AbsenceSignature(*c)); for (const auto& sg : gemmi::spacegroup_tables::main) { if (!sg.is_sohncke() || sg.is_reference_setting() || - RotationSetOf(sg) != best_pg->rotation_set || - !CellHostsRotations(*opt.cell, sg.operations())) + RotationSetOf(sg) != best_pg->rotation_set) continue; auto sig = AbsenceSignature(sg); if (std::find(signatures.begin(), signatures.end(), sig) != signatures.end()) diff --git a/tests/SearchSpaceGroupTest.cpp b/tests/SearchSpaceGroupTest.cpp index 88123bdd3..59b912021 100644 --- a/tests/SearchSpaceGroupTest.cpp +++ b/tests/SearchSpaceGroupTest.cpp @@ -444,6 +444,17 @@ TEST_CASE("SearchSpaceGroup names an orthorhombic screw pair on the axes it lies REQUIRE(result.best_space_group.has_value()); CHECK(result.best_space_group->xhm() == "P 2 21 21"); } + + // A cell refined free after integration is a few tenths of a degree off 90. The reference + // candidates hold the same rotations and are offered on it, so the other settings must be too. + SECTION("a free cell a few tenths off 90 still offers it") { + opt.cell = gemmi::UnitCell(40.0, 50.0, 60.0, 90.32, 90.19, 90.19); + opt.enumerate_all_settings = true; + const auto result = SearchSpaceGroup(merged, opt); + INFO(SearchSpaceGroupResultToText(result)); + REQUIRE(result.best_space_group.has_value()); + CHECK(result.best_space_group->xhm() == "P 2 21 21"); + } } // A screw on a row the sweep never recorded is not a group the data refused, it is a question