From daac3c2da15b62da5279cfecd5e5118629ca1eb0 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Wed, 26 Aug 2026 20:24:34 +0200 Subject: [PATCH] rugnux: report nine resolution shells, as XDS does The binning rule was already XDS's: equal steps in 1/d^2 between the lowest- and the highest-resolution reflection the merge kept, anchored on the data rather than on the nominal low-resolution cut. Only the count differed - ten shells against XDS's nine - so at the same resolution limits neither the boundaries nor the reflection populations matched, and a per-shell number could not be read across. Forced to nine on a tetragonal rotation dataset, rugnux now gives 4.27 3.03 2.47 2.14 1.92 1.75 1.62 1.52 1.43 against CORRECT.LP's 4.26 3.02 2.47 2.14 1.92 1.75 1.62 1.52 1.43; the hundredth of an angstrom in the first two shells is each program anchoring on its own lowest-resolution reflection, the two sets of survivors not being identical. This matters most to the comparisons that are made shell by shell. rugnux_vs_xds.py hands XDS's resolution range to rugnux but not its shell count, so its low-resolution R_meas column compared a shell running to 4.49 A against one running to 4.26 A - 814 unique reflections against 943. The outer shell was nearly immune, its inner edge being d_min*sqrt(n/(n-1)), which is 1.51 at ten shells and 1.52 at nine. --resolution-shells still sets any count. rugnux_stills_ab.py pins the count for both its arms and now pins nine, so its tables read the same way as everything else. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Vi1gV6Z45aZL5wLwe85Ksn --- common/ScalingSettings.h | 3 ++- docs/CHANGELOG.md | 1 + docs/CPU_DATA_ANALYSIS.md | 5 +++++ docs/RUGNUX.md | 2 +- image_analysis/scale_merge/RotationScaleMerge.h | 2 +- rugnux/rugnux_cli.cpp | 2 +- rugnux_stills_ab.py | 2 +- 7 files changed, 12 insertions(+), 5 deletions(-) diff --git a/common/ScalingSettings.h b/common/ScalingSettings.h index 56f41159..a53ab9ed 100644 --- a/common/ScalingSettings.h +++ b/common/ScalingSettings.h @@ -104,7 +104,8 @@ class ScalingSettings { // high_resolution_limit_A is set - that manual limit always wins and disables the auto-cut. ResolutionCutoffMethod resolution_cutoff = ResolutionCutoffMethod::CCHalfLogistic; double resolution_cc_target = 0.30; // CC1/2 value defining the fall-off limit before the +1 shell - int report_shell_count = 10; // number of resolution shells in the reported statistics table + int report_shell_count = 9; // resolution shells in the reported statistics table (XDS's count, + // over the same equal-1/d^2 bins, so the two tables read row for row) public: ScalingSettings& RefineRotationWedge(bool input); diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3aeb2b8b..3771378d 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -21,6 +21,7 @@ This is an UNSTABLE release. It includes many experimental features, as well as * A failed `/initialize` is reported to `/wait_until_running` and `/wait_till_done` as soon as it happens, instead of when their timeout expires. * `space_group_number` accepts space groups up to 230 in the API schema, so cubic space groups can be recorded. The broker always accepted them; the generated clients rejected them before the request was sent. * The results report's `REPORT_VERSION` is 3, two sections having been added. Existing key names and table columns are unchanged. +* The merged statistics table has **9** resolution shells instead of 10, which is what XDS reports. The bins were already XDS's - equal steps in 1/d^2 between the lowest- and the highest-resolution reflection the merge kept - so at the same resolution limits the two tables now have the same shell boundaries and can be read row for row. `--resolution-shells` sets a different count. * `rugnux --model` now settles the frame the merged reflections are written in, not only the frame the R-factors and the maps are computed in: the `.mtz`/`.cif`/`.hkl` come out in the model's indexing, and where the data were merged in the model's enantiomorph they take the model's hand and space group - which on anomalous data puts I(+) and I(-) the right way round. The indexing choice is logged with the winning R-free and the runner-up, so a decision made within noise is visible. * `rugnux --model` can resolve the indexing ambiguity of a **serial stills** run, which a model could not do before: structure factors computed from the model become the per-image reference, the same role a reference MTZ plays. It needs the cell and space group up front (`-C` / `-S`). Without one or the other, a merohedral serial run still merges both hands together and says so. * The rugnux documentation opens with a quick start - the default run, and runs with a reference MTZ, with a model, or with the space group and cell pinned - and explains the indexing ambiguity: what it costs on rotation and on serial data, and which of `-z` / `--model` resolves it in each case. The long reference pages now carry a table of contents. diff --git a/docs/CPU_DATA_ANALYSIS.md b/docs/CPU_DATA_ANALYSIS.md index 7136d073..f205f61f 100644 --- a/docs/CPU_DATA_ANALYSIS.md +++ b/docs/CPU_DATA_ANALYSIS.md @@ -834,6 +834,11 @@ An internal-consistency term can inflate uncertainties when multiple observation ### 10.5 Merging statistics +The shells are **nine bins of equal width in $1/d^2$**, laid between the lowest- and the +highest-resolution reflection the merge actually kept — XDS's rule and XDS's count, so at the same +resolution limits the two programs' tables have the same shell boundaries and can be read row for +row. `--resolution-shells` changes the count; the binning rule does not change with it. + Per-shell and overall merging statistics are computed on corrected intensities, including: - number of observations and of unique reflections, and multiplicity, - mean $I/\sigma(I)$, diff --git a/docs/RUGNUX.md b/docs/RUGNUX.md index b80d0929..44873d0c 100644 --- a/docs/RUGNUX.md +++ b/docs/RUGNUX.md @@ -951,7 +951,7 @@ Scaling and merging: | `--scaling-low-resolution ` | Low-resolution limit for scaling and merging, Å (default: 50, the value XDS configurations use; 0 removes the limit). Reflections coarser than this sit behind or beside the beam stop and are measured on a background it has eaten into | | `--resolution-cutoff ` | Automatic high-resolution cutoff for the written reflections and reported shells: `cc-logistic` \| `off` (default: `cc-logistic`; ignored when `--scaling-high-resolution` is set) | | `--resolution-cc-target ` | CC1/2 target defining the `cc-logistic` fall-off (default: 0.30) | -| `--resolution-shells ` | Number of resolution shells in the reported statistics table (default: 10) | +| `--resolution-shells ` | Number of resolution shells in the reported statistics table (default: 9). The bins are equal steps in 1/d² between the lowest- and highest-resolution reflection merged, which is XDS's rule, and 9 is XDS's count — so at the same resolution limits the two tables have the same shells and can be read row for row | | `--min-partiality ` | Minimum partiality to accept a reflection (default: 0.02) | | `--ice-min-score ` | Ice-presence gate: the measured per-run ice score (1 = no ice) a dataset must reach before **any** ice handling is applied — the flagging and the exclusion from scaling (default: 1.5; 0 = no gate). The eleven fixed hexagonal bands cover 16–26 % of the unique reflections whether or not the crystal has ice, so handling ice on a clean crystal only costs completeness | | `--ice-min-spot-ratio ` | The second ice-presence channel: found **spots** on the hexagonal rings over the same q width of ice-free flanks beside them (1 = spots spread evenly). Ice in large crystallites diffracts as discrete spots and leaves the radial profile flat, so `--ice-min-score` alone is blind to it (default: 2.0; 0 disables this channel) | diff --git a/image_analysis/scale_merge/RotationScaleMerge.h b/image_analysis/scale_merge/RotationScaleMerge.h index 90e46cf8..0e895f54 100644 --- a/image_analysis/scale_merge/RotationScaleMerge.h +++ b/image_analysis/scale_merge/RotationScaleMerge.h @@ -181,7 +181,7 @@ private: // scaling, combine and error model always run over the full range). Manual d_min_limit wins. ResolutionCutoffMethod resolution_cutoff_method = ResolutionCutoffMethod::Off; double resolution_cc_target = 0.30; - int report_shell_count = 10; + int report_shell_count = 9; // Flat buffers, allocated once by Ingest() and reused across Run() calls. std::vector partials; // all per-frame partials, grouped by frame diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 0ed8fd15..8a5d70ef 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -149,7 +149,7 @@ void print_usage() { std::cout << " --scaling-low-resolution Low resolution limit for scaling/merging, in A (default: 50, the value XDS configurations use; 0 = no limit). Reflections coarser than this sit behind or beside the beam stop and are measured on a background it has eaten into" << std::endl; std::cout << " --resolution-cutoff Automatic high-resolution cutoff for the written reflections + reported shells: cc-logistic|off (default: cc-logistic; ignored when --scaling-high-resolution is set)" << std::endl; std::cout << " --resolution-cc-target CC1/2 target defining the cc-logistic fall-off (default: 0.30)" << std::endl; - std::cout << " --resolution-shells Number of resolution shells in the reported statistics table (default: 10)" << std::endl; + std::cout << " --resolution-shells Number of resolution shells in the reported statistics table (default: 9, as XDS)" << std::endl; std::cout << " --ice-min-score Ice-presence gate: measured ice score (1 = no ice) a run must reach before ANY ice handling is applied - the flagging and the exclusion from scaling (default: 1.5). The eleven hexagonal bands cover 16-26% of the unique reflections whether or not the crystal has ice, so handling ice on a clean crystal is a pure loss. 0 = no gate (always handle ice)" << std::endl; std::cout << " --ice-min-spot-ratio Second ice-presence channel: found spots on the hexagonal rings over the same q width of ice-free flanks beside them (1 = spots spread evenly). Ice in large crystallites diffracts as discrete spots and leaves the radial profile flat, so --ice-min-score alone is blind to it. Default 2.0; 0 disables this channel" << std::endl; std::cout << " --min-partiality Minimum partiality to accept reflection (default: 0.02)" << std::endl; diff --git a/rugnux_stills_ab.py b/rugnux_stills_ab.py index aaafb277..cb8427e5 100755 --- a/rugnux_stills_ab.py +++ b/rugnux_stills_ab.py @@ -648,7 +648,7 @@ def main(): help="unit-cell file for compare_hkl -p, per dataset. Required by " "--crystfel-merge; the indexamajig line at the top of the stream names " "the file that produced it") - ap.add_argument("--shells", type=int, default=10, help="resolution shells (default 10)") + ap.add_argument("--shells", type=int, default=9, help="resolution shells (default 9)") ap.add_argument("--progress", action="store_true") args = ap.parse_args()