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) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 12:00:45 +02:00
co-authored by Claude Opus 5
parent 953302a9eb
commit a06c06931f
7 changed files with 40 additions and 1 deletions
+13
View File
@@ -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;
}
+8
View File
@@ -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<float> 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;
};
+1
View File
@@ -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 <num>` 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.
+1
View File
@@ -268,6 +268,7 @@ Integration:
| `--integrator <txt>` | Spot integrator: `gaussian` (profile-fit, default) \| `empirical` \| `boxsum` (classical fallback) |
| `--integration-radius <r>` | Signal-box radius `r1`, or `r1,r2,r3` (px). One value ⇒ `r2=r1+2`, `r3=r1+4` |
| `--background-trim <f>` | 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 <n>` | 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 <num>` | 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):
+1 -1
View File
@@ -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
+2
View File
@@ -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)
+14
View File
@@ -117,6 +117,7 @@ void print_usage() {
std::cout << " Integration" << std::endl;
std::cout << " --bandwidth <num> Relative X-ray bandwidth FWHM (e.g. 0.01 for 1% DMM); default from file or 0" << std::endl;
std::cout << " --integration-radius <r> Signal-box radius r1, or r1,r2,r3 (px). One value => r2=r1+2, r3=r1+4" << std::endl;
std::cout << " --max-hkl <n> 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 <f> 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 <txt> 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<int> report_shell_count; // --resolution-shells
std::optional<std::string> integration_radius_arg; // "r1" or "r1,r2,r3"
std::optional<double> background_trim_arg; // --background-trim: background-ring trimmed-mean fraction
std::optional<int64_t> max_hkl_arg; // --max-hkl: half-width of the predicted hkl cube
std::optional<IntegratorMode> integrator_mode; // --integrator boxsum|gaussian|empirical
bool simple_stills_flag = false; // --simple-stills: disable the default stills partiality post-refinement
std::optional<double> 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<int64_t>(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<int>(*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<float>(*background_trim_arg));