From f9d174ba38c3a9b29537b7d42ae880d92d3c0f77 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 20 Sep 2026 19:35:07 +0200 Subject: [PATCH] Report-only twin-immune zones: measure the tNCS on the merge they read The zone table is read on the P1 cross-check merge but was normalised by result.tncs, a pseudo-translation refined on the adopted group's merge. Where the pseudo-translation is not compatible with the adopted symmetry that merge has already averaged across its own phase classes, so the vector fitted there is poorly determined: measured on one crystal, the correlation between E^2 and cos(2 pi h.u) reads 0.04 on the group merge against 0.25 on the P1 one, with the optimum elsewhere entirely. A vector that names the wrong classes normalises the zone by noise. That merge is also the one a committed change of basis re-indexes, so a vector measured before the search is not even in its frame. Measure it on the P1 cross-check merge itself instead. The nearest existing vector, pre_tncs, is out of scope there and predates any change of basis, so this is one extra AnalyzeTranslationalNCS (+2 to +10 s on the crystals measured, the largest on a 800k-reflection P1 merge). Report-only, and verified so: over five rotation crystals with a detected pseudo-translation the merged reflections (p.hkl, p.mtz, p.cif, p_P1.mtz) are byte-identical, and the adopted space group, the twinning verdict and TWIN_ZONE_DECISION - the deciding path, which takes its vector from the merge the search ran on - are unchanged. What moves is the reported TWIN_ZONE_n/TWIN_ZONE_CONTROL rows: <|E^2-1|> by up to 0.02 on a zone and 0.06 on a control, the zone evidence by up to 35 nats out of 1000-1600. Also say in the pseudo-symmetry text which vector the modulation belongs to: the ratio is read on the search's own vector over d >= 4 A, which is what its control is drawn against, while the vector printed beside it is that one re-refined on the whole data. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT --- image_analysis/scale_merge/TranslationalNCS.cpp | 7 +++++-- rugnux/Rugnux.cpp | 14 +++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/image_analysis/scale_merge/TranslationalNCS.cpp b/image_analysis/scale_merge/TranslationalNCS.cpp index 7c0634f3b..fc7f6cd32 100644 --- a/image_analysis/scale_merge/TranslationalNCS.cpp +++ b/image_analysis/scale_merge/TranslationalNCS.cpp @@ -767,9 +767,12 @@ std::string TranslationalNCSToText(const TranslationalNCSResult &result) { os << "\n [z = " << std::setprecision(1) << result.peak_z << " against a permutation null of " << result.null_mean << " +- " << std::setprecision(2) << result.null_sd << "%]\n"; if (result.modulation_measured) - os << " Intensity modulation along that vector: " << std::setprecision(1) << result.modulation + os << " Intensity modulation: " << std::setprecision(1) << result.modulation << "x between the strongest and weakest phase class [random-vector control " - << result.modulation_null << "x]\n"; + << result.modulation_null << "x]\n" + << " [read on the search's own vector over d >= 4 A, which is what the control is drawn\n" + << " against; the vector reported above is that one re-refined on the whole data, so it\n" + << " points more accurately and this number is not re-read off it]\n"; if (!result.modulation_measured) { os << " The modulation test could not be run - too few acentric reflections beyond 4 A - so\n" << " the peak above stands alone.\n" diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 10a46f362..a1de9c4a3 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -7467,9 +7467,21 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b // because it is folded under no symmetry, at full resolution and production-scaled. // Report-only: nothing reads it back. if (determined != nullptr && determined->number > 1) { + // The pseudo-translation the zone is normalised by is measured on THIS merge, not + // taken from `result.tncs`. That one is fitted on the adopted group's merge, and + // where the pseudo-translation is not compatible with the adopted symmetry that + // merge has already averaged across its own phase classes, so the vector fitted + // there is poorly determined - measured on one crystal, the correlation between E^2 + // and cos(2 pi h.u) reads 0.04 on the group merge against 0.25 here, with the + // optimum elsewhere entirely, and a vector that names the wrong classes normalises + // the zone by noise. It is also the basis this merge is indexed on: a change of + // basis committed by the search leaves a vector measured before it pointing + // somewhere else. + const auto p1_tncs = AnalyzeTranslationalNCS( + p1.merged, gemmi::UnitCell(*result.consensus_cell)); result.twin_immune_zones = AnalyzeTwinImmuneZones( p1.merged, gemmi::UnitCell(*result.consensus_cell), *determined, - TranslationalNCSVectorForLTest(result.tncs)); + TranslationalNCSVectorForLTest(p1_tncs)); if (!result.twin_immune_zones->zones.empty()) logger.Info("{}", TwinImmuneZonesToText(*result.twin_immune_zones)); }