From 1bd39b0563ec3e35dda5219e6fc3fea92aa7f589 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 3 Oct 2026 21:00:25 +0200 Subject: [PATCH] Space-group search: cut the P1 search merge where a monotone fit of drops under 1 The search merge was cut at the first thin shell whose mean fell under 1. On a merge whose is flat near 1 - its ISa has collapsed, as on a strongly absorbing garnet - the first single shell to dip is decided by noise: the same crystal cut at 1.19 A without -march and 0.85 A with -march=x86-64-v3, and the coarser cut left each glide zone fewer than 20 absences, so Ia-3d became unjudgeable and I4(1)32 was adopted. The cut now comes from a non-increasing (pool-adjacent-violators) fit of the shell means, which moves only as much as its input does; that garnet's profile never falls under 1, so its search sees the full range and adopts Ia-3d under both builds with identical candidate tables. On monotone profiles nothing changes: myob/cytc/thau x10sa keep their cuts and p.mtz byte for byte. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01K5K8jvPPbmCrbqnWkddTuB --- rugnux/Rugnux.cpp | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 94a9325af..aaaa11d02 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -7133,11 +7133,39 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b [](const auto &a, const auto &b) { return a.first > b.first; }); // low -> high res const int bins = std::clamp(static_cast(rs.size() / 100), 1, 40); const size_t per = (rs.size() + bins - 1) / static_cast(bins); - for (size_t b = 0; b * per < rs.size(); ++b) { - const size_t lo = b * per, hi = std::min(rs.size(), lo + per); + // The shell means, then their best NON-INCREASING fit (pool adjacent violators, weighted + // by shell size). The cut is where the FIT drops under 1, not where the first shell + // does: on a merge whose is flat near 1 - its ISa has collapsed - the first + // single shell to dip under 1 is a coin toss that moved the cut from 0.85 to 1.19 A + // between two compiler flags on one crystal, and the coarser cut left its glide zones + // too few absences to be judged. The fit moves by as much as its input does. + std::vector mean, weight; + for (size_t lo = 0; lo < rs.size(); lo += per) { + const size_t hi = std::min(rs.size(), lo + per); double sum = 0.0; for (size_t j = lo; j < hi; ++j) sum += rs[j].second; - if (sum / static_cast(hi - lo) < 1.0) { + mean.push_back(sum / static_cast(hi - lo)); + weight.push_back(static_cast(hi - lo)); + } + std::vector block_mean, block_weight; + std::vector block_size; + for (size_t b = 0; b < mean.size(); ++b) { + block_mean.push_back(mean[b]); block_weight.push_back(weight[b]); block_size.push_back(1); + while (block_mean.size() > 1 && block_mean[block_mean.size() - 2] < block_mean.back()) { + const size_t k = block_mean.size() - 1; + const double w = block_weight[k - 1] + block_weight[k]; + block_mean[k - 1] = (block_mean[k - 1] * block_weight[k - 1] + block_mean[k] * block_weight[k]) / w; + block_weight[k - 1] = w; + block_size[k - 1] += block_size[k]; + block_mean.pop_back(); block_weight.pop_back(); block_size.pop_back(); + } + } + std::vector fit; + for (size_t k = 0; k < block_mean.size(); ++k) + fit.insert(fit.end(), block_size[k], block_mean[k]); + for (size_t b = 0; b < fit.size(); ++b) { + const size_t lo = b * per; + if (fit[b] < 1.0) { // 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 saturates at the merge's own ISa (times the