From 5e3a7d113c1298a67bb81e2f18c202dcd2b03bc9 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 30 Aug 2026 21:26:01 +0200 Subject: [PATCH] rugnux: keep a rotation-axis sign the first pass corrected The two-pass post-refinement snapshots the goniometer before pass 1 and restores it after, to undo the start/stride shift a sub-range run applies inside the pass. But the rotation-axis sign rescue also runs inside the pass, and adopts the opposite axis when the file's own indexes almost nothing - so the restore threw that away, handed pass 2 the sign that had already failed, and applied pass 1's fitted rotation scale to the axis it was not fitted on. It self-heals: pass 2's rescue fires again, and a genuinely wrong sign indexes almost no frames, so the run reaches the same answer having paid for one redundant first pass. But the correctness of that rests entirely on "a wrong sign always scores below half the validation frames", which nothing states and nothing tests. The restore now keeps the axis direction and reverts only the angles, which is what it was for. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N --- rugnux/Rugnux.cpp | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 01587643d..d990985d7 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -1326,7 +1326,20 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) { pass1.pass_number = 1; pass1.pass_count = 2; if (cancelled_) { config_.output_prefix = base_prefix; return pass1; } - if (gonio_snapshot) experiment_.Goniometer(*gonio_snapshot); // undo the pre-pass goniometer shift + // Undo the pre-pass goniometer shift (a sub-range or strided run moves start/increment inside + // the pass), but KEEP an axis direction the pass changed. The rotation-axis sign rescue runs + // inside the pass and adopts the opposite axis when the file's own indexes almost nothing; + // restoring the snapshot wholesale threw that away, handing pass 2 the sign that failed and + // then applying pass 1's fitted rotation scale to the axis it was not fitted on. It + // self-healed - pass 2's rescue fires again and a wrong sign indexes almost no frames - but + // only by repeating a whole first pass, and only while "a wrong sign always scores below + // half" holds, which is an invariant nothing states or tests. + if (gonio_snapshot) { + GoniometerAxis restored = *gonio_snapshot; + if (const auto after_pass1 = experiment_.GetGoniometer()) + restored.Axis(after_pass1->GetAxis()); + experiment_.Goniometer(restored); + } // A measured spot budget is the pass's own, not the run's: give the second pass the same list to // measure from, so the two passes cannot ratchet each other down. The canonical pass then reads // the budget off the refined geometry, where the spots that do lie on the lattice actually do. @@ -1475,8 +1488,15 @@ ProcessResult Rugnux::RunAllPasses(RugnuxObserver *observer) { // only costs a pass on a crystal that was going to be wrong otherwise. experiment_.BeamX_pxl(header_geometry[0]).BeamY_pxl(header_geometry[1]) .DetectorDistance_mm(header_geometry[2]); - if (prepass_rotation_scale_ && gonio_snapshot) - experiment_.Goniometer(*gonio_snapshot); // and the header rotation angles with it + if (prepass_rotation_scale_ && gonio_snapshot) { + // The header rotation angles with it - but not the axis DIRECTION, which is not + // part of the geometry being reverted and may have been corrected by the sign + // rescue (see the restore before pass 2). + GoniometerAxis restored = *gonio_snapshot; + if (const auto current = experiment_.GetGoniometer()) + restored.Axis(current->GetAxis()); + experiment_.Goniometer(restored); + } config_.output_prefix = base_prefix; auto redo = RunPipeline(observer, /*write_output=*/true, /*geometry_prepass=*/false); redo.post_refine = pass1.post_refine;