Beam-centre gates: decide on the strong-reflection count, not on how a scaling loop stopped

MeasuredCentreClearlyLoses, different-lattice branch: the measured centre loses on the pre-pass
subset when it measures clearly fewer strong reflections (Poisson, 2 sigma) than the file's centre.
The subset CC1/2 is no longer read there (still logged). A third of the frames in five blocks merges
to a multiplicity of ~1.09 in P1, so its CC1/2 reads the relative scale of the blocks: on 9gdj the
file centre's subset CC1/2 moved 0.899 / 0.880 / 0.832 / 0.846 with the scaling path (plain 1e-3 /
1e-5, Anderson 1e-3 / 1e-5), ~50x the Fisher noise the gate allowed, and flipped the early verdict.
The count is comparable across lattice classes (taken on the primitive lattice) and stable across
paths: 36.6-38.6k against 103.4-103.6k on 9gdj.

MeasuredCentreWins, same-lattice branch: the measured centre has to be ahead on the strong count by
more than 2 Poisson sigmas; within that the file's centre stays (it adopted at 1874 vs 1873 on one
in-house small-molecule set, and at 0.4 sigma on 9zmu).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PXc1zD1yxHqaR1DGdCTHL4
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-19 18:49:24 +02:00
1 parent 0894230671
commit e724f3d26a
3 files changed
+36 -19

No files matched your search

+5 -4
View File
@@ -839,10 +839,11 @@ bool ArmsHoldSameLattice(const ProcessResult &a, const ProcessResult &b);
std::string MeasuredCentreWins(const ProcessResult &file_arm, const ProcessResult &measured_arm);
// The search-merge numbers of a pass, as MeasuredCentreWins reads them.
SearchMergeSignal SearchSignal(const ProcessResult &r);
// Whether the measured centre loses on every line MeasuredCentreWins draws by more than `sigmas`
// times that line's noise - the Poisson noise of a count, the standard error of a CC1/2 - with both
// arms merged from the same frames. The run asks it at the near-tie distance of its first passes
// (FirstPassRun::NEAR_TIE_SIGMA); anything else, a near tie or a win, is decided on all the frames.
// Whether the measured centre is behind on the strong-reflection count by more than `sigmas` times
// its Poisson noise - and, on the same lattice, also behind on the inner-shell CC1/2 line
// MeasuredCentreWins draws - with both arms merged from the same frames. The run asks it at the
// near-tie distance of its first passes (FirstPassRun::NEAR_TIE_SIGMA); anything else, a near tie or
// a win, is decided on all the frames.
bool MeasuredCentreClearlyLoses(const SearchMergeSignal &file_arm, const SearchMergeSignal &measured_arm,
bool same_lattice, double sigmas);
+21 -10
View File
@@ -89,6 +89,9 @@ namespace {
// How much better the alternative beam centre's search merge has to be for the run to move off
// the centre the file gave it (the beam-centre arbitration in RunAllPasses).
constexpr double MAX_CC_HALF_LOSS = 0.05;
// How far, in Poisson sigmas, the alternative's strong-reflection count has to be ahead on the same
// lattice - the near-tie distance of the first passes (FirstPassRun::NEAR_TIE_SIGMA).
constexpr double STRONG_COUNT_SIGMAS = 2.0;
// Say so rather than printing a zero that reads as a measurement. ("not compared", not "not
// measured": completeness IS measured and reported; what did not happen is its use in arbitrating
@@ -292,7 +295,11 @@ std::string MeasuredCentreWins(const ProcessResult &file_arm, const ProcessResul
// two arms here integrate with the same settings, so the geometry is the only difference, and the
// file's centre has no standing the measured one lacks - neither indexed the whole spot list.
if (ArmsHoldSameLattice(file_arm, measured_arm)) {
if (measured_arm.search_merge_strong_reflections > file_arm.search_merge_strong_reflections)
// Ahead by more than the Poisson noise of the two counts; within it the file's centre stays.
const int64_t strong_m = measured_arm.search_merge_strong_reflections;
const int64_t strong_f = file_arm.search_merge_strong_reflections;
if (static_cast<double>(strong_m - strong_f)
> STRONG_COUNT_SIGMAS * std::sqrt(static_cast<double>(strong_m + strong_f)))
return fmt::format("the measured centre measures more of the crystal - {} reflections at "
"I/sigma >= 2 before corrections against {}",
measured_arm.search_merge_strong_reflections,
@@ -331,15 +338,19 @@ bool MeasuredCentreClearlyLoses(const SearchMergeSignal &file_arm, const SearchM
const auto loses = [sigmas](double margin, double noise) {
return std::isfinite(margin) && std::isfinite(noise) && margin < -sigmas * noise;
};
if (same_lattice)
return loses(static_cast<double>(measured_arm.strong - file_arm.strong),
std::sqrt(static_cast<double>(measured_arm.strong + file_arm.strong)))
&& loses(measured_arm.inner_cc_half - file_arm.inner_cc_half - MAX_CC_HALF_LOSS,
std::hypot(cc_se(measured_arm.inner_cc_half, measured_arm.inner_reflections),
cc_se(file_arm.inner_cc_half, file_arm.inner_reflections)));
return loses(measured_arm.cc_half - file_arm.cc_half - MAX_CC_HALF_LOSS,
std::hypot(cc_se(measured_arm.cc_half, measured_arm.reflections),
cc_se(file_arm.cc_half, file_arm.reflections)));
// Both arms integrate the same frames with the same settings, and the strong reflections are
// counted on the primitive lattice, so the counts compare across lattice classes as well. The
// subset's CC1/2 does not decide the different-lattice case: a third of the frames in blocks merges
// to a multiplicity near one in P1, its CC1/2 then reads the relative scale of the blocks, and
// measured it moved by 0.07 with how far the scaling loop ran - fifty times its Fisher noise.
const bool behind_on_signal = loses(static_cast<double>(measured_arm.strong - file_arm.strong),
std::sqrt(static_cast<double>(measured_arm.strong + file_arm.strong)));
if (!same_lattice)
return behind_on_signal;
return behind_on_signal
&& loses(measured_arm.inner_cc_half - file_arm.inner_cc_half - MAX_CC_HALF_LOSS,
std::hypot(cc_se(measured_arm.inner_cc_half, measured_arm.inner_reflections),
cc_se(file_arm.inner_cc_half, file_arm.inner_reflections)));
}
ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) {
+10 -5
View File
@@ -645,6 +645,8 @@ TEST_CASE("MeasuredCentreWins", "[process]") {
// Same lattice, same CC1/2: the arm that measured more signal wins, whichever centre it is...
CHECK(!MeasuredCentreWins(file, arm(swapped, tri, 0.87, 9500)).empty());
CHECK(MeasuredCentreWins(file, arm(swapped, tri, 0.87, 8500)).empty());
// ...but not by less than twice the Poisson noise of the two counts (sqrt(18100) = 135)...
CHECK(MeasuredCentreWins(file, arm(swapped, tri, 0.87, 9100)).empty());
// ...a tie keeps the file's centre, and a higher CC1/2 alone does not move it.
CHECK(MeasuredCentreWins(file, arm(swapped, tri, 0.99, 9000)).empty());
@@ -676,12 +678,15 @@ TEST_CASE("MeasuredCentreClearlyLoses", "[process]") {
m.inner_reflections = 20;
CHECK_FALSE(MeasuredCentreClearlyLoses(inconsistent, m, true, 2.0));
// Different lattices: the CC1/2 line, 0.05 above the file's centre.
// Different lattices: the count alone - the subset's CC1/2 is not read.
SearchMergeSignal d = file;
d.cc_half = 0.80;
d.strong = 3000;
d.cc_half = 0.99;
CHECK(MeasuredCentreClearlyLoses(file, d, false, 2.0));
d.cc_half = 0.95; // exactly on the line
d.strong = 8900; // 100 behind: within two sigmas
d.cc_half = 0.50;
CHECK_FALSE(MeasuredCentreClearlyLoses(file, d, false, 2.0));
// Nothing measured is no loss: it is decided on all the frames.
CHECK_FALSE(MeasuredCentreClearlyLoses(file, SearchMergeSignal{}, false, 2.0));
// A merge with no strong reflection in it measured none of the crystal. (An arm without a merge
// is never asked: RunAllPasses judges it on all the frames.)
CHECK(MeasuredCentreClearlyLoses(file, SearchMergeSignal{}, false, 2.0));
}