Space-group search: cut the P1 search merge where a monotone fit of <I/sigma> drops under 1
The search merge was cut at the first thin shell whose mean <I/sigma> fell under 1. On a merge whose <I/sigma> 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) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K5K8jvPPbmCrbqnWkddTuB
This commit is contained in:
1 parent
581e1c8ca2
commit
1bd39b0563
1 file changed
+31
-3
+31
-3
@@ -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<int>(rs.size() / 100), 1, 40);
|
||||
const size_t per = (rs.size() + bins - 1) / static_cast<size_t>(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 <I/sigma> 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<double> 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<double>(hi - lo) < 1.0) {
|
||||
mean.push_back(sum / static_cast<double>(hi - lo));
|
||||
weight.push_back(static_cast<double>(hi - lo));
|
||||
}
|
||||
std::vector<double> block_mean, block_weight;
|
||||
std::vector<size_t> 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<double> 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
|
||||
|
||||
Reference in new issue
Block a user