From a6c21bd84aee0db2ed54546481d6e70015dff8d2 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 20 Sep 2026 18:58:09 +0200 Subject: [PATCH] Carry the twin-immune zone verdict as a value, not as report text The report decided whether to name a twin law by substring-matching ProcessResult::twin_zone_decision, a field every zone read overwrites. A run reads the zone up to four times, so the match saw the last comparison made rather than the refusal that stood, and TWIN_LAW and the _pdbx_reflns_twin mmCIF loop could appear on a crystal whose refusal was overturned - or be missing on one whose refusal stood. TwinZoneVerdict already returns the verdict; it now records it on the result, and the decision point reads that. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT --- rugnux/Rugnux.cpp | 7 ++++++- rugnux/Rugnux.h | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 10a46f362..bfa6def4f 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -1785,6 +1785,11 @@ namespace { z.tncs_normalised ? ", normalised per pseudo-translation class" : "", verdict < 0 ? "acentric, the added operators are a twin law or a pseudo-symmetry" : verdict > 0 ? "centric, the added operators are real" : "undecided"); + // The verdict as a VALUE, for the report to act on. Every caller acts on a decisive read - + // the refusal stands or it is overturned - so recording it here records the decision, where + // matching on the text records only the last comparison the run happened to make. + if (verdict != 0) + result.twin_zone_refusal_stood = verdict < 0; return verdict; } @@ -6924,7 +6929,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b // refused group adds, at the fraction its own disagreement implies (a lower bound). if (twin_sg && result.space_group_search && result.space_group_search->refused_point_group_representative - && result.twin_zone_decision.find("acentric, the added") != std::string::npos) { + && result.twin_zone_refusal_stood) { const auto &sgs = *result.space_group_search; const gemmi::GroupOps hi = sgs.refused_point_group_representative->operations(); const gemmi::GroupOps lo = twin_sg->operations(); diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 1aadb6cb2..e80f35dd2 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -364,6 +364,12 @@ struct ProcessResult { // The twin-immune zone read for a promotion a twin gate refused (the H ratio or the added-operator // R), and what was decided on it. Empty when no twin gate fired. std::string twin_zone_decision; + // What that zone DECIDED, carried as a value beside the text: true where the refusal STOOD (the + // added operators read acentric and are recorded as a twin law), false where the zone overturned + // the refusal or decided nothing. A run reads the zone up to four times and each read rewrites + // the text, so the decision cannot be recovered from the text - matching on it named the twin law + // of whichever comparison happened to be made last rather than of the refusal that stood. + bool twin_zone_refusal_stood = false; // Per lattice twin law outside the adopted group: the operator's own H, the fraction it implies // and its correlation, on the final merge. Report-only. std::vector twin_law_evidence;