diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index cf08a2cee..4af5023fd 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -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); diff --git a/rugnux/RugnuxPasses.cpp b/rugnux/RugnuxPasses.cpp index a0776553b..7256ab640 100644 --- a/rugnux/RugnuxPasses.cpp +++ b/rugnux/RugnuxPasses.cpp @@ -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(strong_m - strong_f) + > STRONG_COUNT_SIGMAS * std::sqrt(static_cast(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(measured_arm.strong - file_arm.strong), - std::sqrt(static_cast(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(measured_arm.strong - file_arm.strong), + std::sqrt(static_cast(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) { diff --git a/tests/RugnuxTest.cpp b/tests/RugnuxTest.cpp index 8308608fa..c85c7b86e 100644 --- a/tests/RugnuxTest.cpp +++ b/tests/RugnuxTest.cpp @@ -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)); }