From 465cf1aa5ead86211196a0ce0a6492c413ca671a Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Mon, 13 Jul 2026 08:20:19 +0200 Subject: [PATCH] rugnux: resolve the indexing ambiguity against a reference MTZ (rotation) A reference MTZ is now allowed for rotation data: it fixes the space group and cell (on the CLI) and resolves the indexing (merohedral) ambiguity, but is NOT used to scale - the rotation merge stays self-consistent. Previously rotation + reference was a hard error. After the space group is determined, ChooseReindex/ReferenceIntensityCC pick the reindexing whose merged intensities best correlate with the reference and, if it is not the identity, the twin-law reindex is baked into the integration outcome (hkl labels only; the cell is unchanged, the reindex is metric- preserving), the rotation scale-merge is re-ingested and the data re-merged in that indexing. No-op for a holohedral crystal (no twin laws), e.g. lysozyme. Stills resolve the per-image ambiguity in ScaleOnTheFly, not here. Co-Authored-By: Claude Opus 4.8 (1M context) --- rugnux/Rugnux.cpp | 39 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 89a54a97..ab46fdac 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -30,6 +30,7 @@ #include "../image_analysis/scale_merge/Merge.h" #include "../image_analysis/scale_merge/RotationScaleMerge.h" #include "../image_analysis/scale_merge/ResolutionCutoff.h" +#include "../image_analysis/scale_merge/ReindexAmbiguity.h" #include "../image_analysis/scale_merge/ScalingResult.h" #include "../image_analysis/scale_merge/SearchSpaceGroup.h" #include "../image_analysis/lattice_search/LatticeSearch.h" @@ -565,12 +566,15 @@ ProcessResult Rugnux::Run(RugnuxObserver *observer) { const bool is_rotation = experiment_.IsRotationIndexing(); // rotation indexing -> rotation scaling/merge std::optional rsm; if (is_rotation) { - if (!config_.reference_data.empty() || rot_ss.GetRefineB() + if (rot_ss.GetRefineB() || experiment_.GetRefineRotationWedgeInScaling() || rot_ss.GetRotationWedgeForScaling().has_value()) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, "Rotation scaling/merging (RotationScaleMerge) does not support " - "reference scaling, B-factor refinement or wedge refinement"); + "B-factor refinement or wedge refinement"); + // A reference MTZ is allowed for rotation: it fixes the space group / cell (on the CLI) and + // resolves the indexing ambiguity (below), but is NOT used to scale - the rotation merge stays + // self-consistent. rsm.emplace(experiment_, indexer->GetIntegrationOutcome(), result.consensus_cell, static_cast(config_.scaling_iter), config_.spot_finding.ice_ring_width_Q_recipA, @@ -814,6 +818,37 @@ ProcessResult Rugnux::Run(RugnuxObserver *observer) { result.space_group_number = experiment_.GetSpaceGroupNumber(); } + // Reference-based indexing-ambiguity resolution (rotation). When a reference MTZ is supplied and + // the crystal has an indexing ambiguity (merohedral: lattice symmetry higher than the Laue group), + // pick the reindexing that best correlates with the reference intensities and re-merge in it. The + // twin-law reindex is metric-preserving, so only the hkl labels change (the cell is unchanged). + // Stills resolve the ambiguity per image in ScaleOnTheFly, not here. + if (rsm && !config_.reference_data.empty() && result.consensus_cell + && experiment_.GetSpaceGroupNumber().has_value()) { + const int sg_num = static_cast(*experiment_.GetSpaceGroupNumber()); + const auto choice = ChooseReindex( + sm.merged, *result.consensus_cell, sg_num, + [&](const std::vector &m) { + return ReferenceIntensityCC(m, config_.reference_data, sg_num); + }); + if (!choice.is_identity) { + logger.Info("Reference: resolved indexing ambiguity by reindexing to match the reference " + "(CC {:.3f}, vs {:.3f} unchanged)", choice.score, choice.identity_score); + for (auto &io : indexer->GetIntegrationOutcome()) + for (auto &r : io.reflections) { + const gemmi::Op::Miller h = choice.op.apply_to_hkl({{r.h, r.k, r.l}}); + r.h = h[0]; r.k = h[1]; r.l = h[2]; + } + rsm.emplace(experiment_, indexer->GetIntegrationOutcome(), result.consensus_cell, + static_cast(config_.scaling_iter), + config_.spot_finding.ice_ring_width_Q_recipA, + config_.nthreads, logger, config_.observation_dump_path); + rsm->Ingest(); + phase("Re-merging in the reference indexing"); + sm = scale_and_merge(experiment_.GetGemmiSpaceGroup()->short_name(), false); + } + } + // Ice-ring CC1/2 mask: a hexagonal-ice ring whose merged half-set CC1/2 collapses well below its // resolution shoulders has been decorrelated by ice (its Bragg is unrecoverable) - drop it and // re-merge. Weak/absent rings track their neighbours and stay, so completeness on clean crystals