From 7ae1da277ad5ecedfb3c8c9944fa64a8e86f6338 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 19 Sep 2026 09:48:22 +0200 Subject: [PATCH] Space-group search: log the refusal the two-merge rule overrides When the all-observation and Lorentz-filtered search merges disagree, the higher point group is adopted. The losing arm's answer was dropped, so a promotion one arm refused on its twin gates (H ratio, added-operator R) went through with nothing in the output saying a gate had fired - and the report printed the adopted group's failing H ratio ("2.02 against a bound of 1.85") as if it were a margin. Seen on an H3 partial twin (5j23), refused on both twin gates by the all-observation merge and promoted to R32 by the filtered one. Now the losing arm's refusal (group and reason), or the fact that it never confirmed the operators, is logged as a warning and written to the report (SPACE_GROUP_ARM_OVERRIDE). A bound the adopted group exceeds is printed as "EXCEEDS its bound - that gate fired and was overridden", for the H ratio and for the best-operator R. The comment that claimed both arms keep their vetoes now describes the rule as implemented. The rule itself is unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- rugnux/ResultReport.cpp | 21 +++++++++++++++++++-- rugnux/Rugnux.cpp | 31 +++++++++++++++++++++++++++---- rugnux/Rugnux.h | 4 ++++ 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/rugnux/ResultReport.cpp b/rugnux/ResultReport.cpp index bb48a625c..4106b22e4 100644 --- a/rugnux/ResultReport.cpp +++ b/rugnux/ResultReport.cpp @@ -528,9 +528,21 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, // operator and candidate tables that justify it. std::string line = fmt::format(" Point group {} from the intensity correlations", search.point_group_hm); + // A bound the adopted group EXCEEDS is a gate that fired and was overridden - the point group + // was taken from the other search arm - and must not read as the margin of a pass. if (std::isfinite(search.h_ratio)) - line += fmt::format(";\n twin-law H ratio {:.2f} against a bound of {:.2f}", - search.h_ratio, search.h_ratio_bound); + line += search.h_ratio > search.h_ratio_bound + ? fmt::format(";\n twin-law H ratio {:.2f} EXCEEDS its bound of {:.2f} - that gate fired " + "and was overridden", search.h_ratio, search.h_ratio_bound) + : fmt::format(";\n twin-law H ratio {:.2f} against a bound of {:.2f}", + 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); 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" @@ -541,6 +553,11 @@ ReportDocument BuildReportDocument(const std::string &output_prefix, " cannot name. Molecular replacement settles it: one of the two refines and the\n" " other does not - the normal outcome for a chiral group, not a failure of the\n" " run.")); + if (!result.space_group_arm_override.empty()) { + Add(s, KeyText("SPACE_GROUP_ARM_OVERRIDE", result.space_group_arm_override)); + Add(s, Prose(fmt::format(" The two search merges disagreed and the higher point group was taken:\n {}.", + result.space_group_arm_override))); + } if (refused) Add(s, Prose(fmt::format( " A promotion to {} was refused: {}.\n" diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 60942321b..20d4d0063 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -5280,9 +5280,12 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b // rotations are real, because a promotion is refused by the most damning statistic it can // be shown and only one arm has to be starved for that to happen, while the ABSENCES are // always judged on the all-observation merge, which is the only one holding the weak - // reflections a systematic absence is made of. Both searches keep their vetoes, so a merohedral twin - // stays refused in each (verified on a real twin at the three frame ranges where it - // over-promotes: refused in both arms every time). + // reflections a systematic absence is made of. The vetoes are NOT kept per arm: a promotion + // one arm refused on its twin gates (H ratio, added-operator R) is adopted when the other arm + // confirms it, because the higher order wins. That held on one real twin at three frame + // ranges, where both arms refused, but an H3 partial twin has been seen refused by the + // all-observation arm on both twin gates and promoted to R32 by the filtered one. So the + // losing arm's refusal is logged and carried to the report below; the rule is unchanged. if (rsm && rsm->GetSearchMinZeta() > 0.0 && !sg_search.point_group_hm.empty()) { const double zeta = rsm->GetSearchMinZeta(); rsm->SetSearchMinZeta(0.0); @@ -5341,10 +5344,29 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b stats_text << " !! " << msg << "\n\n"; result.warnings.push_back({PathologyCode::SYMMETRY_AMBIGUITY, msg}); } - if (order_of(alt) > order_of(sg_search)) + // The arm whose lower answer is about to be overridden, and why it stopped there. A + // refusal by a consistency gate is the case that matters: the higher group is adopted + // anyway, and without this nothing in the output says a gate ever fired. + const auto overridden = [&](const SearchSpaceGroupResult &lower, const char *lower_name, + const char *higher_name) { + std::string msg; + if (!lower.refused_point_group_hm.empty()) + msg = fmt::format("the {} merge REFUSED {} ({}); overridden, because the {} merge " + "confirmed the higher point group", lower_name, + lower.refused_point_group_hm, lower.refused_reason, higher_name); + else + msg = fmt::format("the {} merge did not confirm the operators of the higher point " + "group (no gate refused it); the {} merge did", lower_name, + higher_name); + logger.Warning("Space-group search: {}", msg); + result.space_group_arm_override = msg; + }; + if (order_of(alt) > order_of(sg_search)) { logger.Info("Space-group search: all-observation merge supports {} where the " "Lorentz-filtered one supports {} - taking the higher symmetry", alt.point_group_hm, sg_search.point_group_hm); + overridden(sg_search, "Lorentz-filtered", "all-observation"); + } if (order_of(alt) < order_of(sg_search)) { // The filtered arm found symmetry the all-observation arm did not. Discarding it // was costing real point groups: over the corpus the two arms disagree on eight @@ -5359,6 +5381,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b // that found it and the absences are re-judged here, on every observation. That // is what fixed_point_group is for: Stage A still runs and still reports what it // would have refused, only the choice is overridden. + overridden(alt, "all-observation", "Lorentz-filtered"); sg_opts.fixed_point_group = sg_search.point_group_representative; auto split = SearchSpaceGroup(sm_all.merged, sg_opts); sg_opts.fixed_point_group.reset(); diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 54ab34074..b6e8d67bc 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -334,6 +334,10 @@ struct ProcessResult { // populated only when a search ran (no space group was fixed). The library no longer renders it to // text - the CLI formats it for stdout, the viewer draws it as a table. std::optional space_group_search; + // When the two search arms (all observations / Lorentz-filtered) disagreed and the higher point + // group was taken from one of them: what the other arm said - which gate refused the promotion, + // or that it never confirmed the operators. Empty when the arms agreed. + std::string space_group_arm_override; // ---- Material for the results report (_report.txt, see rugnux/ResultReport.h) ----