From a06c06931f4fd99bc87c32cc5656f24d2f259068 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Fri, 31 Jul 2026 12:00:45 +0200 Subject: [PATCH] Bragg prediction: how far to walk the lattice is a setting, not a literal max_hkl was hardcoded to 100 at the one place production builds the prediction settings, so the only way to change it was to edit and rebuild - and it is not a constant of the method, it is a property of the cell. An axis is truncated once a/d_min exceeds it: 100 covers a 150 A axis at 1.5 A, but the same axis at 1.0 A, or a 250 A axis anywhere, loses its outermost reflections with nothing said. Move it into BraggIntegrationSettings next to the other prediction/integration parameters and add rugnux --max-hkl (1..511, default 100 - no behaviour change). Like the integration radii and the background trim it stays out of the OpenAPI, so the broker keeps the default it has today and live analysis cannot be handed a range that would not finish; the offline front end, which knows its cell, can ask for more. RugnuxCommandLine emits it when it is not the default. Measured on five rotation crystals at --max-hkl 200: two are bit-identical at no cost, and three were being truncated - one gains 419k observations (+17%) and takes its high-shell CC1/2 from 15.1% to 25.8% for +14% wall clock, the other two gain 12k and 5.8k observations with CC1/2 76.6->82.4% and 52.1->55.3% for +9% and +1%. ISa is unchanged throughout, and no frame overflowed the prediction buffer. Co-Authored-By: Claude Opus 5 (1M context) --- common/BraggIntegrationSettings.cpp | 13 +++++++++++++ common/BraggIntegrationSettings.h | 8 ++++++++ docs/CHANGELOG.md | 1 + docs/RUGNUX.md | 1 + image_analysis/IndexAndRefine.cpp | 2 +- rugnux/RugnuxCommandLine.cpp | 2 ++ rugnux/rugnux_cli.cpp | 14 ++++++++++++++ 7 files changed, 40 insertions(+), 1 deletion(-) diff --git a/common/BraggIntegrationSettings.cpp b/common/BraggIntegrationSettings.cpp index aa8ae524..20f62598 100644 --- a/common/BraggIntegrationSettings.cpp +++ b/common/BraggIntegrationSettings.cpp @@ -108,3 +108,16 @@ BraggIntegrationSettings &BraggIntegrationSettings::BackgroundTrimFraction(float float BraggIntegrationSettings::GetBackgroundTrimFraction() const { return bkg_trim_fraction; } + +BraggIntegrationSettings &BraggIntegrationSettings::MaxHKL(int input) { + check_min("Maximum hkl index", input, 1); + // The GPU predictor launches one thread per candidate, so the cost is (2n+1)^3: 511 is 1.1e9 + // candidates per frame, already far past the point where prediction dominates a run. + check_max("Maximum hkl index", input, 511); + max_hkl = input; + return *this; +} + +int BraggIntegrationSettings::GetMaxHKL() const { + return max_hkl; +} diff --git a/common/BraggIntegrationSettings.h b/common/BraggIntegrationSettings.h index 974dc47c..0ff58a9a 100644 --- a/common/BraggIntegrationSettings.h +++ b/common/BraggIntegrationSettings.h @@ -27,6 +27,12 @@ class BraggIntegrationSettings { // keeps the tuned high-side sigma-clip for broadband (non-zero bandwidth) data instead. 0 = plain ring // mean (rugnux --background-trim). float bkg_trim_fraction = 0.10f; + // Half-width of the hkl cube the predictor walks: every reflection with |h|,|k|,|l| <= this is + // tested against the Ewald sphere, and nothing outside it can ever be predicted. An axis is + // truncated once a/d_min exceeds this, so 100 covers a 150 A axis at 1.5 A but not at 1.0 A. The + // cost is the cube (2n+1)^3 of candidates on the GPU, which is why it is not simply set high: + // raising it lengthens every frame's prediction whether or not the cell needs the room. + int max_hkl = 100; public: BraggIntegrationSettings& R1(float input); @@ -36,6 +42,7 @@ public: BraggIntegrationSettings& FixedProfileRadius_recipA(std::optional input); BraggIntegrationSettings& Integrator(IntegratorMode input); BraggIntegrationSettings& BackgroundTrimFraction(float input); + BraggIntegrationSettings& MaxHKL(int input); [[nodiscard]] IntegratorMode GetIntegrator() const; @@ -47,4 +54,5 @@ public: [[nodiscard]] float GetMinimumSigmaInRegardsToI() const; [[nodiscard]] float GetBackgroundTrimFraction() const; + [[nodiscard]] int GetMaxHKL() const; }; diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 2d795fec..d1e5e728 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -13,6 +13,7 @@ This is an UNSTABLE release. It includes many experimental features, as well as * rugnux: Per-image **geometry refinement** weights spots by confidence. * rugnux: `--min-image-cc` now works for rotation data (opt-in). * rugnux: New `--search-min-zeta` (rotation default 0.85). +* rugnux: New `--max-hkl` sets how far the Bragg predictor walks the lattice (default 100, unchanged). An axis is truncated once `a/d_min` exceeds it, so a long-axis or high-resolution dataset was quietly losing its outermost reflections; raising it recovers them at a cost that grows as the cube. * rugnux: Reports how close a symmetry axis lies to the spindle. * rugnux: Azimuthal-integration and spot-finding resolution limits default to the detector — including **rotation** data, which no longer keeps a 1.5 Å spot-finding limit (`--spot-high-resolution ` still sets one). * **API (breaking for older clients)**: the `image_scale_b` value is gone from the `plot_type` enum, so a client that requests that plot now gets an error rather than a curve. Also, `azim_int_settings.high_q_recipA` and `spot_finding_settings.high_resolution_limit` are no longer `required`. Both now mean "as far as the detector reaches" when unset, and are **omitted** from the response instead of carrying a placeholder value. A client generated from an rc.160-or-earlier spec that assumes the field is always present (the generated C++ `from_json` does `j.at(...)`) raises when it is missing. Because the azimuthal-integration limit now defaults to unset, a stock rc.161 broker omits `high_q_recipA` from `GET /config/azim_int` with no operator action. Regenerate the client (`jfjoch-client` 1.0.0-rc.161, `frontend/src/client`) or read both fields as optional. diff --git a/docs/RUGNUX.md b/docs/RUGNUX.md index 2c524ed9..135a2d39 100644 --- a/docs/RUGNUX.md +++ b/docs/RUGNUX.md @@ -268,6 +268,7 @@ Integration: | `--integrator ` | Spot integrator: `gaussian` (profile-fit, default) \| `empirical` \| `boxsum` (classical fallback) | | `--integration-radius ` | Signal-box radius `r1`, or `r1,r2,r3` (px). One value ⇒ `r2=r1+2`, `r3=r1+4` | | `--background-trim ` | Monochromatic (rotation + still): symmetric trimmed-mean fraction for the background ring, 0≤f<0.5 (default 0.10; 0 = plain mean) — removes the high-side bias that over-subtracts weak high-angle spots | +| `--max-hkl ` | Predict reflections with \|h\|,\|k\|,\|l\| ≤ `n` (default 100, max 511). An axis is truncated once `a/d_min` exceeds `n`, so a long axis — or a short one taken to high resolution — silently loses its outermost reflections. Cost grows as `(2n+1)³` candidates per frame, so raise it only where the cell needs the room | | `--bandwidth ` | Relative X-ray bandwidth FWHM (e.g. `0.01` for a 1% DMM); default from file or 0 (monochromatic) | Geometry overrides (defaults are taken from the input file; override them to reprocess with a corrected geometry): diff --git a/image_analysis/IndexAndRefine.cpp b/image_analysis/IndexAndRefine.cpp index b89f7ade..a429e5e4 100644 --- a/image_analysis/IndexAndRefine.cpp +++ b/image_analysis/IndexAndRefine.cpp @@ -438,7 +438,7 @@ void IndexAndRefine::QuickPredictAndIntegrate(DataMessage &msg, const BraggPredictionSettings settings_prediction{ .high_res_A = experiment.GetBraggIntegrationSettings().GetDMinLimit_A(), .ewald_dist_cutoff = ewald_dist_cutoff, - .max_hkl = 100, + .max_hkl = experiment.GetBraggIntegrationSettings().GetMaxHKL(), // Centering is a hypothesis to confirm, not assume: with no user-fixed space group, predict // in P so the centering-absent reflections are integrated and the space-group search can // confirm or disprove centering (and catch a missed superstructure). A user-fixed space diff --git a/rugnux/RugnuxCommandLine.cpp b/rugnux/RugnuxCommandLine.cpp index 83118757..a6b7683f 100644 --- a/rugnux/RugnuxCommandLine.cpp +++ b/rugnux/RugnuxCommandLine.cpp @@ -124,6 +124,8 @@ std::string RugnuxCommandLine(const ProcessConfig &config, // custom fraction, or 0 when the box is unchecked) to reproduce the GUI's choice faithfully. if (bragg.GetBackgroundTrimFraction() != 0.10f) add("--background-trim", num(bragg.GetBackgroundTrimFraction())); + if (bragg.GetMaxHKL() != 100) + add("--max-hkl", std::to_string(bragg.GetMaxHKL())); if (config.rotation_indexing) { if (config.two_pass_rotation) diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 15d6b4c9..32c6f891 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -117,6 +117,7 @@ void print_usage() { std::cout << " Integration" << std::endl; std::cout << " --bandwidth Relative X-ray bandwidth FWHM (e.g. 0.01 for 1% DMM); default from file or 0" << std::endl; std::cout << " --integration-radius Signal-box radius r1, or r1,r2,r3 (px). One value => r2=r1+2, r3=r1+4" << std::endl; + std::cout << " --max-hkl Predict reflections with |h|,|k|,|l| <= n (default 100). Raise for a long axis: an axis is truncated once a/d_min exceeds n. Cost grows as (2n+1)^3" << std::endl; std::cout << " --background-trim Monochromatic (rotation + still): symmetric trimmed-mean fraction for the background ring (0<=f<0.5, default 0.10; 0 = plain mean). Removes the high-side bias that over-subtracts weak high-angle spots (broadband data keep the sigma-clip instead)" << std::endl; std::cout << " --integrator Spot integrator boxsum|gaussian|empirical (default: gaussian profile-fit; boxsum is the classical fallback)" << std::endl; std::cout << " --simple-stills stills: treat every reflection as a full (p=1, single-pass scale/merge); disables the default physical partiality post-refinement" << std::endl; @@ -164,6 +165,7 @@ enum { OPT_BANDWIDTH, OPT_INTEGRATION_RADIUS, OPT_BACKGROUND_TRIM, + OPT_MAX_HKL, OPT_REJECT_OUTLIERS, OPT_REFERENCE_COLUMN, OPT_MODEL, @@ -271,6 +273,7 @@ static option long_options[] = { {"bandwidth", required_argument, nullptr, OPT_BANDWIDTH}, {"integration-radius", required_argument, nullptr, OPT_INTEGRATION_RADIUS}, {"background-trim", required_argument, nullptr, OPT_BACKGROUND_TRIM}, + {"max-hkl", required_argument, nullptr, OPT_MAX_HKL}, {"integrator", required_argument, nullptr, OPT_INTEGRATOR}, {"simple-stills", no_argument, nullptr, OPT_SIMPLE_STILLS}, {"detect-ice-rings", optional_argument, nullptr, OPT_DETECT_ICE_RINGS}, @@ -550,6 +553,7 @@ static int RunRugnux(int argc, char **argv) { std::optional report_shell_count; // --resolution-shells std::optional integration_radius_arg; // "r1" or "r1,r2,r3" std::optional background_trim_arg; // --background-trim: background-ring trimmed-mean fraction + std::optional max_hkl_arg; // --max-hkl: half-width of the predicted hkl cube std::optional integrator_mode; // --integrator boxsum|gaussian|empirical bool simple_stills_flag = false; // --simple-stills: disable the default stills partiality post-refinement std::optional outlier_reject_nsigma; // merge per-observation outlier rejection @@ -841,6 +845,9 @@ static int RunRugnux(int argc, char **argv) { case OPT_BACKGROUND_TRIM: background_trim_arg = parse_double_arg(optarg, "--background-trim", logger); break; + case OPT_MAX_HKL: + max_hkl_arg = parse_number_arg(optarg, "--max-hkl", logger, 1, 511); + break; case OPT_INTEGRATOR: if (strcmp(optarg, "boxsum") == 0) integrator_mode = IntegratorMode::BoxSum; else if (strcmp(optarg, "gaussian") == 0) integrator_mode = IntegratorMode::ProfileGaussian; @@ -1490,6 +1497,13 @@ static int RunRugnux(int argc, char **argv) { : "profile (empirical)"); } + if (max_hkl_arg) { + BraggIntegrationSettings bis = experiment.GetBraggIntegrationSettings(); + bis.MaxHKL(static_cast(*max_hkl_arg)); + experiment.ImportBraggIntegrationSettings(bis); + logger.Info("Predicting reflections with |h|,|k|,|l| <= {}", *max_hkl_arg); + } + if (background_trim_arg) { BraggIntegrationSettings bis = experiment.GetBraggIntegrationSettings(); bis.BackgroundTrimFraction(static_cast(*background_trim_arg));