From 486c0b06d18dc993d2a02a07be33a90c87f6e94b Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 3 Nov 2026 12:40:41 +0100 Subject: [PATCH] RESULT-CHANGING: the centred-lattice test asks only about a class the lattice metric hosts The centred-lattice test (run after a triclinic indexing whose search arms could test no operator) takes its candidate from the Niggli character table, which names a class on 3 deg relative tolerances. Le Page, which measures each two-fold's obliquity on the lattice itself at the 1 deg bound every other metric question of the run uses, can find fewer rotations than that class carries. The test then adopted a group the lattice does not host: on a triclinic crystal whose C-monoclinic two-fold is 1.7 deg oblique (XDS refines the same angle), C2 was adopted at operator CC 0.46 and written on a cell the group cannot describe, and the Le Page re-ask that follows cannot demote. The test now runs only where Le Page carries the candidate's holohedry; otherwise it logs why and leaves the question to the Le Page re-ask. Over the rc181 open, in-house and private battery rows this was the one final point group with more rotations than the metric hosts; every other centred-test adoption is hosted and unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01UqXYdL4qBGYRiSXSGXfCLu --- rugnux/RugnuxScaleMerge.cpp | 19 ++++++++++++++++++- tests/LePageLatticeTest.cpp | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/rugnux/RugnuxScaleMerge.cpp b/rugnux/RugnuxScaleMerge.cpp index e6396538f..76f190707 100644 --- a/rugnux/RugnuxScaleMerge.cpp +++ b/rugnux/RugnuxScaleMerge.cpp @@ -2642,6 +2642,23 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { // The metric only generates the candidate; the intensity CC gate below makes the decision // (a false pseudo-symmetry is rejected by the correlation). const auto cand = LatticeSearch(*end_msg.rotation_lattice); + // The candidate comes from the Niggli character table, which names a class on relative + // tolerances (3 deg). Le Page measures each two-fold's obliquity on the lattice itself, at + // the bound every other metric question of the run is put at (LATTICE_MAX_OBLIQUITY_DEG). + // Where it finds fewer rotations than the candidate's holohedry the lattice does not host + // that class, and what the intensities could confirm is a pseudo-symmetry: measured on a + // triclinic crystal whose C-monoclinic two-fold is 1.7 deg oblique (the angle XDS refines + // as well), C2 was adopted at operator CC 0.46 and written on a cell the group cannot + // describe. So it is not asked here; the metric re-ask below asks Le Page's own class. + const auto metric = LePageLattice(*end_msg.rotation_lattice); + const bool metric_hosts = metric + && static_cast(metric->n_operators) >= HolohedryRotations(cand.system); + if (!metric_hosts && cand.system != gemmi::CrystalSystem::Triclinic) + logger.Info("Centred-lattice test: the cell's character names a {} lattice, but some of its two-folds " + "are more than {:.1f} deg oblique (Le Page finds {} rotation(s)), so the lattice " + "does not host it and it is not offered to the intensities", + gemmi::crystal_system_str(cand.system), LATTICE_MAX_OBLIQUITY_DEG, + metric ? metric->n_operators : 0); // Exact integer reindex from the indexed setting straight to the candidate conventional // setting: P[i][j] = conv_real[i] . rot_reciprocal[j]. Derived from the two lattices, so it // holds even if the refined indexed cell is not exactly LatticeSearch's Niggli cell (whose @@ -2657,7 +2674,7 @@ bool Rugnux::ScaleMergeAndSymmetry(PipelineLocals &p) { reindex.a[i][j] = std::round(v); reindex_res = std::max(reindex_res, std::fabs(v - reindex.a[i][j])); } - if (cand.system != gemmi::CrystalSystem::Triclinic && reindex_res < 0.1) { + if (metric_hosts && cand.system != gemmi::CrystalSystem::Triclinic && reindex_res < 0.1) { std::vector merged_c = sm.merged; for (auto &m : merged_c) reindex_hkl(m, reindex); diff --git a/tests/LePageLatticeTest.cpp b/tests/LePageLatticeTest.cpp index 4027b86f1..f479f8046 100644 --- a/tests/LePageLatticeTest.cpp +++ b/tests/LePageLatticeTest.cpp @@ -6,6 +6,7 @@ #include "../common/Coord.h" #include "../common/UnitCell.h" #include "../image_analysis/lattice_search/LePageLattice.h" +#include "../image_analysis/lattice_search/LatticeSearch.h" #include "gemmi/symmetry.hpp" #include #include @@ -207,3 +208,24 @@ TEST_CASE("LePageLattice - a filtered group may not rest on an operator the filt }); CHECK(!r.has_value()); } + +TEST_CASE("LePageLattice - a two-fold the character table admits but the metric does not host") { + // A C-centred monoclinic description whose alpha is 1.7 deg off 90: the Niggli character table, + // on its relative tolerances, still names the class, while the two-fold is more oblique than + // Le Page's bound, so the lattice does not host it. The centred-lattice test in the rotation merge + // asks the intensities about a class only where both agree. A lattice 0.3 deg off is hosted. + const CrystalLattice oblique = CrystalLattice(120, 100, 70, 88.3, 115, 90).ToPrimitive('C'); + const auto named = LatticeSearch(oblique); + CHECK(named.system == gemmi::CrystalSystem::Monoclinic); + CHECK(named.centering == 'C'); + const auto metric = LePageLattice(oblique); + REQUIRE(metric.has_value()); + CHECK(metric->n_operators == 1); + CHECK(metric->system == gemmi::CrystalSystem::Triclinic); + + const auto hosted = LePageLattice(CrystalLattice(120, 100, 70, 89.7, 115, 90).ToPrimitive('C')); + REQUIRE(hosted.has_value()); + CHECK(hosted->n_operators == 2); + CHECK(hosted->system == gemmi::CrystalSystem::Monoclinic); + CHECK(hosted->centering == 'C'); +}