From 01626a54e0ce5a79c1fa391a2f549ee4de7adc57 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Mon, 10 Aug 2026 05:52:23 +0200 Subject: [PATCH] Space-group search: keep the lowest shell when no shell passes the cut The P1 merge that feeds the space-group search is cut at the first 1/40 shell whose mean I/sigma falls below 1. If even the lowest-resolution shell fails, the cut was abandoned altogether and the search was handed the whole merge out to the detector corner. That inverts under its own feedback. The bound is absolute, while the merged I/sigma it tests is capped by the merge's own asymptote: once noise-dominated high-resolution reflections have inflated the error model's b, no shell can reach 1 - so the case where the cut is abandoned is exactly the case where the merge is worst. Measured on one crystal: b 0.239 -> 0.782, ISa 4.2 -> 1.3, no shell above the bound, 46853 reflections into the search become 3989103, the added operator's agreement falls 0.978 -> 0.429, and a C-centred monoclinic crystal merges as triclinic. Fall back to the lowest shell's own high-resolution edge instead, when that shell is populated enough to define one. It is what the healthy case does anyway - shell 0 is the lowest-resolution fortieth of reciprocal volume, 46852 reflections on the crystal above. Only reachable when rugnux chooses its own resolution limit: an explicit --scaling-high-resolution also constrains the merge this cut is derived from and lands it above the crossing, which is why the battery at matched limits cannot see any of this. Both battery arms are therefore unchanged - space group identical on all 38 crystals, every quality column identical, whether rugnux picks the limit or takes it from the reference. On a deliberately degraded integration the crystal above returns to its true symmetry (294655 unique reflections back to 102514, R_meas 41.4 -> 29.3%) and its error model recovers. Four crystals in the battery sit within a factor of two of the bound, and one sits 0.19% from it. Co-Authored-By: Claude Opus 5 (1M context) --- rugnux/Rugnux.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 52d1d535..dc9cb468 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -1529,13 +1529,17 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b double sum = 0.0; for (size_t j = lo; j < hi; ++j) sum += rs[j].second; if (sum / static_cast(hi - lo) < 1.0) { - // Cut the noise-dominated high-res shells, but only when a good low-res region - // exists above the cutoff. If even the lowest-res shell (b == 0) is below the - // threshold - as happens for very weak data whose I/sigma is floored by the - // error model although CC1/2 is still high - there is no clean/noise boundary, - // and cutting here would discard essentially all reflections and leave the - // re-merge with no resolution range. Leave d_min_search at 0 (no cut) instead. - if (b > 0) d_min_search = rs[lo].first; + // Cut the noise-dominated high-res shells. When even the lowest-res shell fails, + // keep that shell alone rather than abandoning the cut. The bound is absolute + // while the merged I/sigma it tests is capped by the merge's own ISa, so a merge + // whose ISa has fallen below 1 cannot satisfy it in ANY shell - and that is + // exactly the merge the search must not be handed whole, since the noise which + // drove ISa down is what the cut exists to remove. Abandoning it feeds back: + // more noise into the search, a weaker operator statistic, and a lost symmetry. + if (b > 0) + d_min_search = rs[lo].first; + else if (per >= 400) + d_min_search = rs[per - 1].first; break; } }