From 1089b7590630f5d7fdbfe421e304aa2ef0fe260a Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sat, 18 Jul 2026 17:45:39 +0200 Subject: [PATCH] rugnux: two-pass writes both passes (_01 header geometry, _02 refined) The two-pass geometry post-refinement previously wrote only the second (refined-geometry) pass. Write both instead, so the header-geometry and post-refined results can be compared directly and the better one kept: _01_* first pass, at the header geometry (the plain single-pass baseline) _02_* second pass, at the post-refined geometry Each is a complete standalone result - its own merged reflections (.mtz/.cif), per-image _process.h5 (with --write-process-h5), ice-ring flags and mmCIF detector geometry, all consistent with the geometry that pass actually used. Mechanics: the pre-pass now stores the post-refined detector geometry instead of applying it mid-pass (so the first pass finishes and writes at the header geometry), and no longer returns early - it continues through the full write. Run() then applies the stored geometry, lets the second pass re-search a reindex-derived space group, and routes the two passes to the _01 / _02 prefixes. The second pass's result is unchanged from before. Co-Authored-By: Claude Opus 4.8 (1M context) --- rugnux/Rugnux.cpp | 75 ++++++++++++++++++++++++++++--------------- rugnux/Rugnux.h | 16 +++++++-- rugnux/rugnux_cli.cpp | 2 +- 3 files changed, 65 insertions(+), 28 deletions(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 7e58eb87..c133f1e8 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -217,18 +217,42 @@ void Rugnux::RefineStillsGeometry(int start_image, int end_image, int images_to_ } ProcessResult Rugnux::Run(RugnuxObserver *observer) { - // Rotation two-pass geometry post-refinement: a first pass integrates and post-refines the detector - // geometry (distance + beam from the observed spot positions, cell scale + axis from phi_obs); the second - // pass re-indexes and re-integrates with the refined geometry and is the only one written out - so the - // _process.h5 / mtz (and the ice-ring flags, which move with the geometry) reflect the refined geometry. + // Rotation two-pass geometry post-refinement: the first pass integrates at the header geometry and + // post-refines the detector geometry (distance + beam from the observed spot positions, cell scale + axis + // from phi_obs); the second pass re-indexes and re-integrates with the refined geometry. BOTH passes are + // written out - "_01_*" is the first (header-geometry) pass and "_02_*" the second (refined + // geometry) - so the two can be compared and the better one kept, and the ice-ring flags / geometry stored + // in each master reflect the geometry that pass actually used. if (config_.mode == ProcessMode::FullAnalysis && config_.rotation_postrefine_geometry && experiment_.IsRotationIndexing()) { + Logger logger("Rugnux"); + const std::string base_prefix = config_.output_prefix; const auto gonio_snapshot = experiment_.GetGoniometer(); - Logger("Rugnux").Info("Rotation two-pass geometry post-refinement: running the geometry pre-pass"); - RunPipeline(observer, /*write_output=*/false, /*geometry_prepass=*/true); - if (cancelled_) return ProcessResult{}; + prepass_detector_geometry_.reset(); + prepass_sg_reindexed_ = false; + + logger.Info("Rotation two-pass geometry post-refinement: first pass (header geometry) -> {}_01_*", + base_prefix); + config_.output_prefix = base_prefix + "_01"; + auto pass1 = RunPipeline(observer, /*write_output=*/true, /*geometry_prepass=*/true); + if (cancelled_) { config_.output_prefix = base_prefix; return pass1; } if (gonio_snapshot) experiment_.Goniometer(*gonio_snapshot); // undo the pre-pass goniometer shift - return RunPipeline(observer, /*write_output=*/true, /*geometry_prepass=*/false); + + // Apply the post-refined detector geometry for the second pass; let the second pass re-search a + // reindex-derived space group (see the pre-pass note above). + if (prepass_detector_geometry_) { + const auto &g = *prepass_detector_geometry_; + experiment_.BeamX_pxl(g[0]).BeamY_pxl(g[1]).DetectorDistance_mm(g[2]); + } + if (prepass_sg_reindexed_) + experiment_.SpaceGroupNumber(std::nullopt); + + logger.Info("Rotation two-pass geometry post-refinement: second pass (refined geometry) -> {}_02_*", + base_prefix); + config_.output_prefix = base_prefix + "_02"; + auto pass2 = RunPipeline(observer, /*write_output=*/true, /*geometry_prepass=*/false); + config_.output_prefix = base_prefix; + return pass2; } return RunPipeline(observer, /*write_output=*/true, /*geometry_prepass=*/false); } @@ -929,11 +953,14 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b const auto pr = PostRefineRotationGeometry(outcomes, *rot->axis, experiment_.GetDiffractionGeometry(), rot->lattice, prs, logger); if (pr.ok && pr.detector_refined) { - experiment_.BeamX_pxl(static_cast(pr.beam_x_after_px)) - .BeamY_pxl(static_cast(pr.beam_y_after_px)) - .DetectorDistance_mm(static_cast(pr.distance_after_mm)); - logger.Info("Two-pass: applied refined detector geometry (distance {:.3f} mm, " - "beam {:.2f},{:.2f} px) for the second integration pass", + // Store, don't apply: this pass continues to write its own (header-geometry) output; + // Run() applies the refined geometry before the second pass. + prepass_detector_geometry_ = std::array{ + static_cast(pr.beam_x_after_px), + static_cast(pr.beam_y_after_px), + static_cast(pr.distance_after_mm)}; + logger.Info("Two-pass: post-refined detector geometry (distance {:.3f} mm, " + "beam {:.2f},{:.2f} px) will drive the second integration pass", pr.distance_after_mm, pr.beam_x_after_px, pr.beam_y_after_px); } else { logger.Info("Two-pass: geometry post-refine committed no detector change " @@ -1122,19 +1149,17 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b result.space_group_number = experiment_.GetSpaceGroupNumber(); } - // Rotation two-pass geometry pre-pass done: the determined space group is now fixed on experiment_, so - // the second pass REUSES it instead of re-searching at the slightly changed geometry (which could flip - // a borderline determination and mix the two passes). The lattice is deliberately NOT forced - the - // second pass re-indexes at the refined geometry so the CELL comes out self-consistent with it (forcing - // the pre-pass lattice would pin the nominal cell and undo the geometry refinement). EXCEPTION: a + // Rotation two-pass geometry pre-pass: the determined space group is now fixed on experiment_, so the + // second pass REUSES it instead of re-searching at the slightly changed geometry (which could flip a + // borderline determination and mix the two passes). The lattice is deliberately NOT forced - the second + // pass re-indexes at the refined geometry so the CELL comes out self-consistent with it (forcing the + // pre-pass lattice would pin the nominal cell and undo the geometry refinement). EXCEPTION: a // reindex-derived space group (the centred-lattice test) is in a conventional setting the indexer's - // primitive frame does not share, so reusing it would break the second pass's indexing - clear it and - // let the second pass re-search (it re-runs the same test). Stop here; the caller re-runs the pass. - if (geometry_prepass) { - if (sg_reindexed) - experiment_.SpaceGroupNumber(std::nullopt); - return result; - } + // primitive frame does not share, so reusing it would break the second pass's indexing - remember it so + // the caller clears it and lets the second pass re-search. Unlike a throwaway pre-pass, this one now + // CONTINUES to the write below so its own (header-geometry) result is saved as the first pass. + if (geometry_prepass) + prepass_sg_reindexed_ = sg_reindexed; // 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), diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index 75f66628..436efa9a 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -3,6 +3,7 @@ #pragma once +#include #include #include #include @@ -70,8 +71,9 @@ struct ProcessConfig { // Rotation two-pass geometry post-refinement (FullAnalysis, rotation only). When set, a first pass // integrates and post-refines the detector distance + beam (from the observed spot positions) and the // cell scale + rotation axis (from the observed rocking centroids phi_obs), each committed only if it - // improves a held-out residual; the second pass re-integrates with the refined geometry and lattice and - // is the one written out (--rotation-refine-geometry). + // improves a held-out residual; the second pass re-integrates with the refined geometry and lattice. + // BOTH passes are written out - "_01_*" (first pass, header geometry) and "_02_*" (second + // pass, refined geometry) - so the two can be compared and the better one kept (--rotation-post-refine). bool rotation_postrefine_geometry = false; // Scaling / merging (FullAnalysis). reference_data (from a reference MTZ) also drives the @@ -146,6 +148,16 @@ class Rugnux { // fed to the second pass's Bragg prediction. Empty outside the rotation two-pass. std::vector prepass_mosaicity_; + // Two-pass geometry pre-pass: the post-refined detector geometry (beam_x, beam_y in px; distance in mm) + // fitted by the first pass and applied by Run() before the second pass. Empty when the pre-pass + // committed no change (the second pass then reproduces the first). + std::optional> prepass_detector_geometry_; + + // Two-pass geometry pre-pass: whether the first pass's space group came from the centred-lattice reindex + // test (a conventional setting the indexer's primitive frame does not share), so Run() clears it and lets + // the second pass re-search rather than reuse it. + bool prepass_sg_reindexed_ = false; + // Stills global geometry-refinement first pass (config_.refine_geometry): index a spread sample of // frames, bundle-adjust the shared beam/distance/cell from the strongest ones, and apply the result // to experiment_ so the main pass re-indexes with it. No-op (leaves experiment_ unchanged) if it diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 0c9f6260..9ae4e4c2 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -74,7 +74,7 @@ void print_usage() { std::cout << " --single-pass-rotation[=num] Use online-like single-pass rotation indexing (optional: min angular range deg)" << std::endl; std::cout << " --redo-rotation-spots Redo spot finding for two-pass rotation indexing" << std::endl; std::cout << " --force-rotation-lattice Force rotation indexer with external lattice (in Angstrom) : \"a0x,a0y,a0z,a1x,a1y,a1z,a2x,a2y,a2z\" (9 floats, skips first pass)" << std::endl; - std::cout << " --rotation-post-refine Two-pass rotation: post-refine detector distance/beam + cell/axis, then re-integrate with them (writes the 2nd pass)" << std::endl; + std::cout << " --rotation-post-refine Two-pass rotation: post-refine detector distance/beam + cell/axis, then re-integrate with them; writes both passes (_01 header geometry, _02 refined)" << std::endl; std::cout << " -X, --indexing-algorithm Indexing algorithm (FFBIDX|FFT|FFTW|Auto|None)" << std::endl; std::cout << " -S, --space-group Space group number (92) or symbol (P43212) - for indexing and scaling" << std::endl; std::cout << " -C, --unit-cell Fix reference unit cell: \"a,b,c,alpha,beta,gamma\"" << std::endl;