From a291c01c09f7b0069743fcbbc35ada04a077a0db Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Thu, 8 Oct 2026 10:47:13 +0200 Subject: [PATCH] rugnux: the geometry pre-pass integrates at least 120 deg of the sweep The two-pass geometry pre-pass integrates the first --prepass-fraction (0.5) of the sweep, but never less than 120 deg of rotation (header oscillation), so a sweep up to 120 deg is pre-passed whole and one up to 240 deg gets 120 deg. Wider sweeps are unchanged. The range is computed in one place, Rugnux::PrepassEnd. Evidence (fraction sweep with the rc175-p1-baseline binary, 10 sweeps of 360 deg and 10 of 180 deg; pre-pass distance vs the full-sweep fit): median error 0.081 % at 90 deg, 0.042 % at 120, 0.042 % at 150 and 0.033 % at 180 on the 180-deg sweeps; 0.032 / 0.021 / 0.013 % at 120 / 150 / 180 deg on the 360-deg sweeps. 120 deg on a wide sweep matches what the whole-sweep pre-pass gave a 180-deg sweep. Below ~60 deg the pre-pass misses the distance outright (6rym: held at the header, 3.4 % off; 6vww and 6fwc: distance not freed). Merged quality moves by under 1 % at any angle >= 90 deg. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01SVmAWnzCmRKAXVUCdc4iNi --- docs/CPU_DATA_ANALYSIS_INDEXING.md | 2 +- docs/RUGNUX_ADVANCED.md | 2 +- docs/RUGNUX_OVERVIEW.md | 2 +- docs/RUGNUX_REPORT.md | 2 +- docs/RUGNUX_TUTORIAL.md | 2 +- rugnux/Rugnux.cpp | 17 ++++++++++++++--- rugnux/Rugnux.h | 4 ++++ rugnux/rugnux_cli.cpp | 2 +- 8 files changed, 24 insertions(+), 9 deletions(-) diff --git a/docs/CPU_DATA_ANALYSIS_INDEXING.md b/docs/CPU_DATA_ANALYSIS_INDEXING.md index 899ce9ba6..a4ccf9d8d 100644 --- a/docs/CPU_DATA_ANALYSIS_INDEXING.md +++ b/docs/CPU_DATA_ANALYSIS_INDEXING.md @@ -315,7 +315,7 @@ So every round is scored on the **widest** gate — the population the first pas The refinement above (§7.2) runs per image against that image's spots. For rotation data an additional **post-refinement** (on by default; `--rotation-no-postrefine` disables it) improves the detector distance, beam centre and crystal cell/axis using **all** frames at once, then re-integrates: -1. **Pass 1** integrates, scales and merges at the header geometry — the first half of the sweep by default (`--prepass-fraction`, `1` for all of it). The first-pass indexing has already sampled the whole sweep; what pass 1 adds is integrated reflections for the post-refinement below, and half a sweep holds plenty of them while costing half the time. Its merge, post-refinement and smoothed mosaicity describe a sweep of those frames alone. +1. **Pass 1** integrates, scales and merges at the header geometry — the first half of the sweep by default, but at least 120° of it, so a sweep up to 120° is pre-passed whole (`--prepass-fraction`, `1` for all of it). The first-pass indexing has already sampled the whole sweep; what pass 1 adds is integrated reflections for the post-refinement below, and half a sweep holds plenty of them while costing half the time. Its merge, post-refinement and smoothed mosaicity describe a sweep of those frames alone. 2. From pass-1's integrated reflections, the crystal and the detector are refined together over all its frames (Ceres, robust loss) in **one joint fit**, against both residuals at once: - the **positional** detector↔reciprocal residual at each partial's observed spot, and - a distance-independent **Ewald excitation** residual at each reflection's observed rocking centroid $\phi_\mathrm{obs}$. diff --git a/docs/RUGNUX_ADVANCED.md b/docs/RUGNUX_ADVANCED.md index 4b3956f5c..c67d5ce0e 100644 --- a/docs/RUGNUX_ADVANCED.md +++ b/docs/RUGNUX_ADVANCED.md @@ -342,7 +342,7 @@ For very long axes the direction grid's angular resolution binds well below 500 | `--single-pass-rotation[=num]` | Online-like single-pass rotation indexing (optional min angular range, deg) | | `--force-rotation-lattice ` | Force rotation lattice (9 floats, Å), skipping the first pass | | `--rotation-no-postrefine` | Rotation: disable the default-on two-pass geometry post-refine (see the rotation section) | -| `--prepass-fraction ` | Rotation: the two-pass geometry pre-pass integrates the first fraction `f` of the sweep (0.05-1, default **0.5**; `1` integrates the whole sweep twice). The second pass always integrates every frame | +| `--prepass-fraction ` | Rotation: the two-pass geometry pre-pass integrates the first fraction `f` of the sweep, but at least 120° of it (0.05-1, default **0.5**; `1` integrates the whole sweep twice). The second pass always integrates every frame | | `--refine-geometry[=N\|off]` | Stills: extra first pass that bundle-adjusts the shared beam/distance/cell from N strongly-indexed frames (default 200) then re-indexes; default ON for stills with a reference cell (`-C` / `-z`), `=off` disables | | `--index-ice-rings[=on\|off]` | Index on the spots flagged as sitting on an ice ring too, instead of setting them aside (default: **off**; no effect without `--detect-ice-rings`, which does the flagging) | diff --git a/docs/RUGNUX_OVERVIEW.md b/docs/RUGNUX_OVERVIEW.md index b2276095f..5739cf812 100644 --- a/docs/RUGNUX_OVERVIEW.md +++ b/docs/RUGNUX_OVERVIEW.md @@ -35,7 +35,7 @@ nothing, gives an axis harmonic of the measured centre's lattice, or loses to it are merged ([§1.4](CPU_DATA_ANALYSIS_IMAGE.md)). A failed pass triggers the discrete rescues — the rotation-axis sign, the first-pass depth ladder, the long-axis rescue — before anything is given up on. -**First integration pass.** At the geometry in the file, every frame of the first half of the sweep +**First integration pass.** At the geometry in the file, every frame of the first half of the sweep, at least 120° of it (`--prepass-fraction`) is predicted ([§8](CPU_DATA_ANALYSIS_INTEGRATION.md)) and profile-fit integrated ([§9](CPU_DATA_ANALYSIS_INTEGRATION.md)); partials are combined into fulls, scaled and merged diff --git a/docs/RUGNUX_REPORT.md b/docs/RUGNUX_REPORT.md index 5fdeb5899..ffaacd498 100644 --- a/docs/RUGNUX_REPORT.md +++ b/docs/RUGNUX_REPORT.md @@ -129,7 +129,7 @@ grep '^JFJOCH_DATASET_SETTINGS=' out_report.txt | cut -d' ' -f2- > geometry.json ``` **Which pass.** A rotation run integrates twice — once at the geometry in the input file (the first -half of the sweep, `--prepass-fraction`), then the whole sweep again at the post-refined geometry — and can integrate a third time if a guard rejects the second pass. +half of the sweep but at least 120°, `--prepass-fraction`), then the whole sweep again at the post-refined geometry — and can integrate a third time if a guard rejects the second pass. There is **one** report, for the pass that became the canonical output, and `PASS=` / `PASS_DECISION=` (`--developer`) in section 1 say which pass that is and on what evidence, so no number in the file is ambiguous about which geometry produced it. diff --git a/docs/RUGNUX_TUTORIAL.md b/docs/RUGNUX_TUTORIAL.md index 1dc615133..213fe8db5 100644 --- a/docs/RUGNUX_TUTORIAL.md +++ b/docs/RUGNUX_TUTORIAL.md @@ -109,7 +109,7 @@ resolution) sharpens both the space-group search and the error model. To tune th treated as independent stills use `--force-still`. By default a rotation run also **post-refines the geometry** in a second pass: the first pass -integrates and merges the first half of the sweep at the header geometry (`--prepass-fraction 1` for all of it), then the detector distance + beam centre and the crystal +integrates and merges the first half of the sweep (at least 120° of it) at the header geometry (`--prepass-fraction 1` for all of it), then the detector distance + beam centre and the crystal cell / rotation-axis are refined against the merged fulls (cross-validated; the distance moves 1 % a step and a larger move is ratified by re-indexing, and the gauge-weak beam centre stays within 15 px of the centre the first pass ran at or of the run's own measured centre, whichever is nearer), and the second pass diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 945ec8fed..135209be2 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -133,6 +133,9 @@ namespace { // Images the pre-scan reads when the beam-stop pre-pass is off and only the spot width is wanted; // with it on, that pre-pass's image count is used and one sample serves both. constexpr int BEAM_CENTER_PROJECTION_IMAGES = 60; + + // The least rotation the two-pass geometry pre-pass integrates, whatever --prepass-fraction asks. + constexpr double PREPASS_MIN_ROTATION_DEG = 120.0; } namespace rugnux_internal { @@ -536,6 +539,16 @@ std::vector Rugnux::FirstPassInputKey() const { return k; } +int Rugnux::PrepassEnd(int start_image, int end_image) const { + int end = start_image + static_cast(std::lround((end_image - start_image) * config_.prepass_fraction)); + // At least PREPASS_MIN_ROTATION_DEG of rotation, so a narrow sweep is pre-passed whole. The angle is + // the file's, so every pass of a run picks the same frames, before and after a rotation-scale fit. + if (const auto g = reader_.GetDataset()->experiment.GetGoniometer(); g && g->GetIncrement_deg() != 0.0f) + end = std::max(end, start_image + static_cast( + std::lround(PREPASS_MIN_ROTATION_DEG / std::abs(g->GetIncrement_deg())))); + return std::min(end, end_image); +} + ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, bool geometry_prepass) { Logger logger("Rugnux"); ProcessResult result; @@ -815,9 +828,7 @@ ProcessResult Rugnux::RunPipeline(RugnuxObserver *observer, bool write_output, b const auto start_time = std::chrono::steady_clock::now(); - // The geometry pre-pass integrates only the first prepass_fraction of the sweep (pass 2 reads it all). - const int prepass_end = start_image - + static_cast(std::lround((end_image - start_image) * config_.prepass_fraction)); + const int prepass_end = PrepassEnd(start_image, end_image); const int prepass_images = (prepass_end - start_image + config_.stride - 1) / config_.stride; const int loop_end = geometry_prepass ? prepass_end : end_image; diff --git a/rugnux/Rugnux.h b/rugnux/Rugnux.h index d9c261f95..7105b868e 100644 --- a/rugnux/Rugnux.h +++ b/rugnux/Rugnux.h @@ -845,6 +845,10 @@ class Rugnux { // geometry_prepass == true stops after post-refining the detector geometry from the first integration // and applies it (+ the refined lattice) to experiment_, so the next pass re-integrates with it. ProcessResult RunPipeline(RugnuxObserver *observer, bool write_output, bool geometry_prepass); + // The end of the first part of a rotation sweep, [start_image, PrepassEnd), that the geometry + // pre-pass integrates: the first config_.prepass_fraction of it, but at least + // PREPASS_MIN_ROTATION_DEG (Rugnux.cpp). + [[nodiscard]] int PrepassEnd(int start_image, int end_image) const; // The phases of RunPipeline that live in files of their own; each reads and writes RunPipeline's // locals through `p` (RugnuxPipeline.h). // The per-image loop over the sweep (azimuthal integration, or the full per-image analysis), what is diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 962c17e45..d54535561 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -148,7 +148,7 @@ void print_usage() { 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-scale Goniometer rotation scale: the stage turned k times the angle stored in the file (the commanded one). Applied to both passes; overrides the fitted correction" << std::endl; std::cout << " --rotation-no-postrefine Disable the (default-on) two-pass rotation post-refine (post-refine detector distance/beam + cell/axis, then re-integrate; only the refined pass is written, as the canonical _* output)" << std::endl; - std::cout << " --prepass-fraction Integrate the first fraction f of the sweep in the two-pass rotation geometry pre-pass (0.05-1, default 0.5; 1 = the whole sweep); pass 2 reads every frame" << std::endl; + std::cout << " --prepass-fraction Integrate the first fraction f of the sweep, but at least 120 deg of it, in the two-pass rotation geometry pre-pass (0.05-1, default 0.5; 1 = the whole sweep); pass 2 reads every frame" << std::endl; std::cout << " -X, --indexing-algorithm Indexing algorithm (FFBIDX|FFT|FFTW|Auto|None)" << std::endl; std::cout << " -S, --space-group Space group number (96) 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;