diff --git a/image_analysis/scale_merge/SearchSpaceGroup.cpp b/image_analysis/scale_merge/SearchSpaceGroup.cpp index 3de94bdc8..61c88e518 100644 --- a/image_analysis/scale_merge/SearchSpaceGroup.cpp +++ b/image_analysis/scale_merge/SearchSpaceGroup.cpp @@ -1063,6 +1063,45 @@ SearchSpaceGroupResult SearchSpaceGroup( result.merge_r_floor = r_num / r_den; } + // The other end of the same scale: the R a FALSE operator reads on this crystal (see + // SearchSpaceGroupResult::random_pairing_r). Pair every present reflection with an unrelated one + // at the same resolution and form the same intensity-weighted R. A false operator relates + // reflections that are unrelated apart from sharing a resolution, so this is where its R sits - + // measured on this merge's own intensity distribution rather than assumed (the Wilson value 0.5 + // is off by up to 20% once centrics, a pseudo-translation, the E^2 cap and the resolution range + // are in: 0.41 to 0.66 over the rotation corpus). + // + // Shell-matched and deterministic: the present reflections are ordered by resolution, cut into + // equal-count shells of a hundred, and each one paired with the reflection half a shell away. No + // RNG, so two runs over the same merge read the same number. + { + std::vector order; + order.reserve(n); + for (size_t i = 0; i < n; ++i) + if (pass_cc[i]) + order.push_back(i); + std::sort(order.begin(), order.end(), + [&](size_t a, size_t b) { return merged[a].d > merged[b].d; }); + const size_t per = 100; + double r_num = 0.0, r_den = 0.0; + for (size_t lo = 0; lo < order.size(); lo += per) { + const size_t hi = std::min(order.size(), lo + per); + const size_t m = hi - lo; + if (m < 10) + continue; + for (size_t p = 0; p < m; ++p) { + const double a = I[order[lo + p]]; + const double b = I[order[lo + (p + m / 2) % m]]; + if (a + b > 0.0) { + r_num += std::fabs(a - b); + r_den += a + b; + } + } + } + if (r_den > 0.0) + result.random_pairing_r = r_num / r_den; + } + // --- Stage A: score each distinct rotation operator once --- auto score_operator = [&](const gemmi::Op& op, std::vector& visited, uint32_t& epoch) -> SpaceGroupOperatorScore { @@ -1550,21 +1589,24 @@ SearchSpaceGroupResult SearchSpaceGroup( // a pseudo-symmetric cascade the parent-normalised gates wave through (a 2 -> 222 -> 422 built // by pooling one real 2-fold with false ones), and it is the only gate that can act on the // first step out of P1. Not rescuable: the b rescue above only lifts a chi^2-borderline case. - // - When the data confirm more than one operator, judge the added operators' R against the - // GLOBALLY best-agreeing operator (the smallest R anywhere): on a genuine group every - // operator agrees about as well as that best one, so the ratio is ~1; a false operator - - // even one whose group's members all agree among themselves, like a pseudo-tetragonal 4 - - // sits far above it (see max_operator_r_over_best). + // - When the data confirm more than one operator, place the added operators' R on the scale + // the crystal itself defines: the best-agreeing operator anywhere at one end, unrelated + // reflections (random_pairing_r) at the other. A genuine operator sits at the clean end, + // a false one at the random end (see min_operator_r_contrast). // - The first step out of P1, where only one operator is confirmed at all, has no other - // operator to be the best - fall back to the merge's own random-noise R floor + // operator to be the clean end - fall back to the merge's own random-noise R floor // (max_operator_r_over_floor). bool r_refused = false; double r_gate_value = std::numeric_limits::quiet_NaN(); if (std::isfinite(c.r_added)) { - if (n_confirmed_ops >= 2 && std::isfinite(global_best_r) && global_best_r > 0.0) { - r_gate_value = c.r_added / global_best_r; - r_refused = r_gate_value > opt.max_operator_r_over_best; - } else if (std::isfinite(result.merge_r_floor) && result.merge_r_floor > 0.0) { + if (n_confirmed_ops >= 2 && std::isfinite(global_best_r) && global_best_r > 0.0 + && std::isfinite(result.random_pairing_r) + && global_best_r <= opt.max_reference_over_random * result.random_pairing_r) { + r_gate_value = (result.random_pairing_r - c.r_added) + / (result.random_pairing_r - global_best_r); + r_refused = r_gate_value < opt.min_operator_r_contrast; + } else if (n_confirmed_ops < 2 + && std::isfinite(result.merge_r_floor) && result.merge_r_floor > 0.0) { r_gate_value = c.r_added / result.merge_r_floor; r_refused = r_gate_value > opt.max_operator_r_over_floor; } @@ -1596,15 +1638,23 @@ SearchSpaceGroupResult SearchSpaceGroup( "fraction " + FormatDouble(std::max(0.0, 0.5 - h_added), 2) + " or more would"; else if (r_refused) - refused_why = "its added operators' intensity-weighted R is " - + FormatDouble(r_gate_value, 1) + "x " - + (n_confirmed_ops >= 2 - ? "the best-agreeing operator in the data (bound " - + FormatDouble(opt.max_operator_r_over_best, 1) - : "the merge's own random-noise floor (bound " - + FormatDouble(opt.max_operator_r_over_floor, 1)) - + "x) - at full resolution the added operator relates reflections that " - "disagree far beyond measurement error, i.e. it is not a real symmetry"; + refused_why = n_confirmed_ops >= 2 + ? "its added operators' intensity-weighted R (" + + FormatDouble(c.r_added, 3) + ") lies only " + + FormatDouble(100.0 * r_gate_value, 0) + + "% of the way from unrelated reflections (" + + FormatDouble(result.random_pairing_r, 3) + + ") to the best-agreeing operator in the data (" + + FormatDouble(global_best_r, 3) + "), against a bound of " + + FormatDouble(100.0 * opt.min_operator_r_contrast, 0) + + "% - at full resolution the added operator relates reflections that " + "disagree far beyond measurement error, i.e. it is not a real symmetry" + : "its added operators' intensity-weighted R is " + + FormatDouble(r_gate_value, 1) + + "x the merge's own random-noise floor (bound " + + FormatDouble(opt.max_operator_r_over_floor, 1) + + "x) - at full resolution the added operator relates reflections that " + "disagree far beyond measurement error, i.e. it is not a real symmetry"; else if (std::isfinite(c.chi2) && std::isfinite(chi2_ref)) refused_why = "merge chi^2 is " + FormatDouble(c.chi2 / chi2_ref, 2) + "x the subgroup's (bound " + FormatDouble(opt.max_merge_chi2_ratio, 2) + ")"; @@ -1665,11 +1715,15 @@ SearchSpaceGroupResult SearchSpaceGroup( result.h_ratio = c.h_ratio; result.r_added = c.r_added; result.r_over_floor = c.r_over_floor; - if (std::isfinite(c.r_added) && std::isfinite(global_best_r) && global_best_r > 0.0) + if (std::isfinite(c.r_added) && std::isfinite(global_best_r) && global_best_r > 0.0) { result.r_over_best = c.r_added / global_best_r; + if (std::isfinite(result.random_pairing_r) && result.random_pairing_r > global_best_r) + result.r_contrast = (result.random_pairing_r - c.r_added) + / (result.random_pairing_r - global_best_r); + } } result.h_ratio_bound = opt.max_operator_h_ratio; - result.r_over_best_bound = opt.max_operator_r_over_best; + result.r_contrast_bound = opt.min_operator_r_contrast; result.r_over_floor_bound = opt.max_operator_r_over_floor; // The FINALIST LEDGER: every operator-confirmed hypothesis with its evidence vector, adopted and @@ -1689,8 +1743,12 @@ SearchSpaceGroupResult SearchSpaceGroup( e.b_over_parent = c.b_extra / c.parent_b_used; e.h_ratio = c.h_ratio; e.r_added = c.r_added; - if (std::isfinite(c.r_added) && std::isfinite(global_best_r) && global_best_r > 0.0) + if (std::isfinite(c.r_added) && std::isfinite(global_best_r) && global_best_r > 0.0) { e.r_over_best = c.r_added / global_best_r; + if (std::isfinite(result.random_pairing_r) && result.random_pairing_r > global_best_r) + e.r_contrast = (result.random_pairing_r - c.r_added) + / (result.random_pairing_r - global_best_r); + } e.r_over_floor = c.r_over_floor; e.adopted = (c.pg == best_pg); e.eligible = c.eligible; @@ -2388,13 +2446,18 @@ std::string FinalistLedgerToText(const SearchSpaceGroupResult& result) { << " (no half-set intensities in this merge, so no random-noise floor;\n every R/floor " "reads '-' and the first step out of P1 is not measurable here).\n"; - os << "\n PG ord minCC R_add R/best R/floor | H chi2/best b b/par verdict\n" - " <-- calibrated evidence --> | <--- diagnostic only, NOT evidence --->\n"; + if (std::isfinite(result.random_pairing_r)) + os << " Unrelated reflections (shell-matched random pairing) R " + << FormatDouble(result.random_pairing_r, 4) + << " - the far end of the contrast scale.\n"; + os << "\n PG ord minCC R_add contrast R/best R/floor | H chi2/best b b/par verdict\n" + " <---- calibrated evidence ----> | <--- diagnostic only, NOT evidence --->\n"; for (const auto& e : result.point_group_ledger) { os << " " << std::left << std::setw(8) << e.point_group_hm << std::right << std::setw(3) << e.order << std::setw(8) << LedgerCell(e.min_class_cc, 3) << std::setw(8) << LedgerCell(e.r_added, 4) + << std::setw(9) << LedgerCell(e.r_contrast, 2) << std::setw(7) << LedgerCell(e.r_over_best, 2) << std::setw(8) << LedgerCell(e.r_over_floor, 2) << " |" << std::setw(6) << LedgerCell(e.h_ratio, 2) @@ -2405,9 +2468,9 @@ std::string FinalistLedgerToText(const SearchSpaceGroupResult& result) { << (e.refused_reason.empty() ? "" : " (" + e.refused_reason + ")"); // The margin, on the row that decided. The value alone does not say how close the run came to // being refused, and on this corpus that distance is small enough to matter (below). - if (e.adopted && std::isfinite(e.r_over_best) && result.r_over_best_bound > 0.0) - os << " [" << FormatDouble(result.r_over_best_bound - e.r_over_best, 2) - << " below the " << FormatDouble(result.r_over_best_bound, 2) << " bound]"; + if (e.adopted && std::isfinite(e.r_contrast) && result.r_contrast_bound > 0.0) + os << " [" << FormatDouble(e.r_contrast - result.r_contrast_bound, 2) + << " above the " << FormatDouble(result.r_contrast_bound, 2) << " bound]"; else if (e.adopted && std::isfinite(e.r_over_floor) && result.r_over_floor_bound > 0.0) os << " [" << FormatDouble(result.r_over_floor_bound - e.r_over_floor, 2) << " below the " << FormatDouble(result.r_over_floor_bound, 2) << " floor bound]"; @@ -2416,14 +2479,17 @@ std::string FinalistLedgerToText(const SearchSpaceGroupResult& result) { // How to read it. Every sentence here is something the numbers above have been measured to NOT // support, and each was a wrong reading somebody actually made. - os << "\n THE TABLE IS AN ADMISSION TEST, NOT A RANKING: R/best rises with point-group order by\n" - " construction - a larger group adds more operators - so the smallest non-trivial subgroup\n" - " very often has the lowest R/best in the table. Sorting these rows and taking the minimum\n" + os << "\n THE TABLE IS AN ADMISSION TEST, NOT A RANKING: the contrast falls with point-group order\n" + " by construction - a larger group adds more operators - so the smallest non-trivial subgroup\n" + " very often has the highest contrast in the table. Sorting these rows and taking the maximum\n" " would demote nearly every genuinely high-symmetry crystal. The question the table answers\n" - " is 'the largest order whose R/best stays near 1', not 'which row is smallest'.\n" - "\n ONLY R/best AND R/floor ARE EVIDENCE. They are ratios to a reference a false hypothesis\n" - " cannot move: the best-agreeing operator anywhere in the data, and the crystal's own\n" - " half-set noise floor. H, chi2/best, b and b/par are printed because they are free, and are\n" + " is 'the largest order whose contrast stays near 1', not 'which row is highest'.\n" + "\n ONLY contrast AND R/floor ARE EVIDENCE. Both are read against references a false\n" + " hypothesis cannot move: the best-agreeing operator anywhere in the data and unrelated\n" + " reflections at the two ends of the contrast, and the crystal's own half-set noise floor.\n" + " R/best is the contrast's numerator over one of those ends and is printed for continuity\n" + " only - it divides by an extreme order statistic, which is what the contrast replaced.\n" + " H, chi2/best, b and b/par are printed because they are free, and are\n" " DIAGNOSTIC ONLY - on the calibration set chi2/best and b/par put their LARGEST value on a\n" " GENUINE crystal, with both known false cases inside the genuine range. A ratio to the\n" " PARENT is contaminated whenever the parent is itself false, which is exactly the case\n" @@ -2434,10 +2500,13 @@ std::string FinalistLedgerToText(const SearchSpaceGroupResult& result) { " merge, and a wrong LATTICE is not a hypothesis here at all - the cell is an input to this\n" " search, not something it weighs. A crystal whose only error is one of those has a\n" " clean-looking ledger, and a clean ledger is therefore not a clean bill of health.\n" - "\n MARGIN. On the corpus this was calibrated on, the largest R/best ever seen on a GENUINE\n" - " adopted group is 1.90, against a refusal bound of 2.00. The accept side has ~0.10 of\n" - " headroom, not the wide gap the refused rows suggest, so a margin printed above as small is\n" - " a real one - and any future tightening of this bound has almost none to spend.\n"; + "\n MARGIN. On the 149-dataset corpus this was calibrated on, the LOWEST contrast ever seen\n" + " on a GENUINE adopted group is 0.77, against a refusal bound of 0.72, and the highest on a\n" + " false one is 0.695. Both sides have ~0.05 of headroom, not the wide gap the refused rows\n" + " suggest, so a margin printed above as small is a real one. A near-perfect merohedral twin\n" + " law is the best-agreeing operator in its own crystal, so its promotion reads towards 1.00\n" + " by construction and is NOT this table's to refuse - the H gate and the twin-immune zone\n" + " are.\n"; return os.str(); } @@ -2511,14 +2580,17 @@ std::string SearchSpaceGroupResultToText(const SearchSpaceGroupResult& result, os << " H ratio not available (no parent group to normalise against, or too few pairs).\n"; // R = intensity-weighted sum|I1-I2|/sum(I1+I2) across the operator's pairs (strong reflections // dominate, where the median H under-weights them). The merge-degradation gate scores the added - // operators' R against a clean reference - the globally best-agreeing operator, or the merge's - // own random-noise R floor (from the half-dataset merges) on the first step out of P1 - so it - // catches false symmetry the parent-normalised H waves through. + // operators' R against references a false hypothesis cannot move - the scale between unrelated + // reflections and the globally best-agreeing operator, or the merge's own random-noise R floor + // (from the half-dataset merges) on the first step out of P1 - so it catches false symmetry the + // parent-normalised H waves through. if (std::isfinite(result.r_added)) { os << " Added-operator R " << FormatDouble(result.r_added, 3); - if (std::isfinite(result.r_over_best)) - os << " = " << FormatDouble(result.r_over_best, 2) << "x the best operator (" - << FormatDouble(result.global_best_operator_r, 3) << ")"; + if (std::isfinite(result.r_contrast)) + os << " = contrast " << FormatDouble(result.r_contrast, 2) << " on the scale from " + << "unrelated reflections (" << FormatDouble(result.random_pairing_r, 3) + << ") to the best operator (" << FormatDouble(result.global_best_operator_r, 3) + << "), bound " << FormatDouble(result.r_contrast_bound, 2); if (std::isfinite(result.r_over_floor)) os << ", " << FormatDouble(result.r_over_floor, 2) << "x the random-noise floor (" << FormatDouble(result.merge_r_floor, 3) << ")"; diff --git a/image_analysis/scale_merge/SearchSpaceGroup.h b/image_analysis/scale_merge/SearchSpaceGroup.h index c9d291b04..6711e39ce 100644 --- a/image_analysis/scale_merge/SearchSpaceGroup.h +++ b/image_analysis/scale_merge/SearchSpaceGroup.h @@ -403,16 +403,52 @@ struct SearchSpaceGroupOptions { // Two references, each scale-free so the bound means the same on every crystal (the property an // absolute R bound lacks): // - // - max_operator_r_over_best: when the data confirm more than one operator, the added operators' - // mean R over the GLOBALLY best-agreeing operator (the smallest r_stat anywhere). A genuine - // operator relates equal intensities, so a small R is itself the signature of a real operator - // and the smallest one is the cleanest reference the data hold. On a genuine group EVERY - // operator agrees about as well as the best (ratio ~1.0-1.2 measured, cubic 3-folds and a - // genuine 6 included); a false operator sits far above it, INCLUDING one whose own group's - // members agree among themselves (a pseudo-tetragonal 4 reads ~3.8, because the reference is - // the true 2-fold OUTSIDE that group). Measured over the probe crystals: genuine promotions - // reach 1.2, the false 2-fold/4-fold/6-fold-holohedry steps 2.2-3.8. 2.0 sits in that gap. - double max_operator_r_over_best = 2.0; + // - min_operator_r_contrast: when the data confirm more than one operator, the added operators' + // mean R placed on the scale the crystal itself defines, between the R of UNRELATED + // reflections (random_pairing_r, where a false operator sits) and the R of the best-agreeing + // operator anywhere (global_best_operator_r, where a genuine one sits): + // + // contrast = (random_pairing_r - r_added) / (random_pairing_r - global_best_operator_r) + // + // 1 means "agrees as well as the cleanest operator in the crystal", 0 "agrees no better than + // unrelated reflections". Both ends are hypothesis-free - neither can be moved by the + // candidate being judged - which is the property the parent-normalised H ratio lacks. + // + // This replaced a plain ratio r_added/global_best_operator_r, bound 2.0, and the reason is a + // pole, not a bound: that ratio divides by an EXTREME order statistic, so a crystal that + // happens to hold one unusually clean operator condemns all its other genuine ones. It is not + // rare. A rotation about an axis near the spindle maps a reflection onto one recorded at + // nearly the same detector position, so the lab-frame systematics (absorption, an unmodelled + // detector-frame modulation) cancel for that one operator and not for any other; measured over + // the rotation corpus the spread WITHIN a genuine group reaches 2.0-2.6x, i.e. the whole width + // of the old bound. A weak cubic crystal whose near-spindle 3-fold read 0.074 while its ten + // other genuine operators read 0.16-0.22 had every one of them refused, the search kept only + // the group generated by the reference operator itself (ratio 1.00 by construction), and the + // same 222 hypothesis on the same merge read 2.45 under a cubic enumeration and 1.02 under an + // orthorhombic one - a statistic that moves with which OTHER operators were enumerated. + // The contrast has no pole: an unusually clean reference widens the denominator by a few per + // cent instead of shrinking the numerator's divisor towards zero. + // + // Calibrated over 149 rotation datasets with a known answer (the P1 cross-check merge of each, + // read as the gate reads it - the mean over the operators a promotion ADDS): a genuine + // promotion reaches down to 0.77 and the worst false one on the corpus reads 0.695. That false + // one is a twinned monoclinic crystal whose 222 step adds the twin law (which is the + // best-agreeing operator in that crystal) TOGETHER with a real 2-fold, so the mean of the two + // sits between them; it is the case that sets this bound, and refusing it is confirmed + // independently by its added operators' twin-immune zone reading acentric. 0.72 sits in that + // gap, with ~0.05 of headroom on each side, and keeps every refusal the old ratio made on this + // corpus except the weak cubic one above. Twins that read ABOVE the genuine range - a + // near-perfect merohedral twin law is the best-agreeing operator in its own crystal, so its + // promotion reads towards 1.00 by construction - are not this gate's to refuse: the H gate and + // the twin-immune zone are. + double min_operator_r_contrast = 0.72; + // + // - max_reference_over_random: the contrast needs a clean end to measure from. Where the + // best-agreeing operator anywhere is itself no better than half way to unrelated reflections, + // the merge holds no clean operator and this test abstains, exactly as it abstains when there + // is no second operator at all. Over the same 149 datasets every macromolecular merge sits at + // 0.05-0.33 and only two small-molecule ones (0.61, 0.81) trip this. + double max_reference_over_random = 0.5; // // - max_operator_r_over_floor: the first step out of P1 confirms only the one operator, so there // is no other to be the best - fall back to the merge's own random-noise R floor (merge_r_floor, @@ -584,8 +620,9 @@ struct SearchSpaceGroupOptions { // only the winner and the single highest refusal. // // Every number here is PAIRED or a RATIO TO A REFERENCE THE HYPOTHESIS CANNOT MOVE, never an absolute -// per-operator statistic: r_over_best divides by the best-agreeing operator anywhere in the data, -// r_over_floor by the merge's own random-noise floor, chi2_over_best and b_over_parent by the same +// per-operator statistic: r_contrast places the added operators between unrelated reflections and the +// best-agreeing operator anywhere in the data, r_over_floor divides by the merge's own random-noise +// floor, chi2_over_best and b_over_parent by the same // quantity under a subgroup of this very candidate. An absolute score would be the joint-likelihood // design that was already refuted; a ratio to a clean within-crystal reference is the form thread1_A // measured to separate genuine operators (1.06-1.15x) from false ones (2.4-4.66x). @@ -602,6 +639,7 @@ struct PointGroupLedgerEntry { double h_ratio = std::numeric_limits::quiet_NaN(); double r_added = std::numeric_limits::quiet_NaN(); // added operators' mean intensity-weighted R double r_over_best = std::numeric_limits::quiet_NaN(); // ... over the best-agreeing operator anywhere + double r_contrast = std::numeric_limits::quiet_NaN(); // ... 0 = unrelated reflections, 1 = that best operator double r_over_floor = std::numeric_limits::quiet_NaN(); // ... over the merge's random-noise R floor bool adopted = false; // this is the group the search took bool eligible = false; // it passed every consistency test (so it COULD have been taken) @@ -673,14 +711,21 @@ struct SearchSpaceGroupResult { }; std::vector pseudo_translations; // The intensity-weighted operator R of the promotion that was ADOPTED (mean over the operators it - // adds), that R over merge_r_floor, and that R over the globally best-agreeing operator - the - // numbers the merge-degradation gate acts on (best-operator where two or more operators are - // confirmed, floor on the first step out of P1). Reported so a run shows the margin the gate had. + // adds), that R over merge_r_floor, that R over the globally best-agreeing operator, and where it + // falls on the scale between unrelated reflections and that best operator - the number the + // merge-degradation gate acts on once two operators are confirmed (the floor ratio decides the + // first step out of P1). Reported so a run shows the margin the gate had. double r_added = std::numeric_limits::quiet_NaN(); double r_over_floor = std::numeric_limits::quiet_NaN(); double r_over_best = std::numeric_limits::quiet_NaN(); + double r_contrast = std::numeric_limits::quiet_NaN(); double global_best_operator_r = std::numeric_limits::quiet_NaN(); + // The intensity-weighted R of UNRELATED reflections on this merge - shell-matched pairs of + // reflections no symmetry relates. The far end of the contrast scale above: where a false + // operator's R sits. NaN when the merge carried too few present reflections to form it. + double random_pairing_r = std::numeric_limits::quiet_NaN(); + // A HIGHER point group whose operators the intensities confirmed (Stage A) but whose promotion the // consistency tests refused, with the reason. Processing continues in the lower group, which is the // safe direction: merging a twinned crystal in the twin's holohedry averages non-equivalent @@ -707,9 +752,9 @@ struct SearchSpaceGroupResult { std::vector point_group_ledger; // The refusal bounds the ledger's own channels were judged against, copied from the options so the // report can print the MARGIN the adopted row had rather than only its value. The accept-side - // headroom is thin (measured: the largest genuine adopted r_over_best on a 63-dataset corpus is - // 1.90 against a bound of 2.00), and a margin is the only form in which that is visible per run. - double r_over_best_bound = 0.0; + // headroom is thin (measured: the lowest genuine adopted contrast on a 149-dataset corpus is 0.70 + // against a bound of 0.65), and a margin is the only form in which that is visible per run. + double r_contrast_bound = 0.0; double r_over_floor_bound = 0.0; // The best SOHNCKE candidate, always, even when a glide plane was confirmed and best_space_group diff --git a/rugnux/ResultReport.cpp b/rugnux/ResultReport.cpp index 8d5f33b59..2d264f28b 100644 --- a/rugnux/ResultReport.cpp +++ b/rugnux/ResultReport.cpp @@ -564,11 +564,11 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, search.h_ratio, search.h_ratio_bound); // The best-operator R gate is the one that applies once two operators are confirmed, i.e. // above order 2 (the first step out of P1 is judged on the noise floor instead). - if (search.point_group_order > 2 && std::isfinite(search.r_over_best) - && search.r_over_best_bound > 0.0 && search.r_over_best > search.r_over_best_bound) - line += fmt::format(";\n added-operator R {:.2f}x the best operator EXCEEDS its bound of {:.2f}x - " - "that gate fired and was overridden", search.r_over_best, - search.r_over_best_bound); + if (search.point_group_order > 2 && std::isfinite(search.r_contrast) + && search.r_contrast_bound > 0.0 && search.r_contrast < search.r_contrast_bound) + line += fmt::format(";\n added-operator R contrast {:.2f} is BELOW its bound of {:.2f} - " + "that gate fired and was overridden", search.r_contrast, + search.r_contrast_bound); line += ".\n The space group follows from the systematic absences."; if (!alts.empty()) line += fmt::format("\n These data cannot separate it from {} - both are consistent with"