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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
This commit is contained in:
2026-08-31 07:17:41 +02:00
co-authored by Claude Opus 5
parent 7c3ab72fed
commit 5e3a7d113c
+23 -3
View File
@@ -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;