Bragg integration: integrate as far as the detector reaches, not to a fixed 1.0 A
Build Packages / build:viewer-tgz:cpu (push) Successful in 7m28s
Build Packages / build:viewer-tgz:cuda (push) Successful in 7m50s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 10m44s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 10m30s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 10m9s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 9m5s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 10m1s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 11m39s
Build Packages / build:rpm (rocky8) (push) Successful in 10m52s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 10m50s
Build Packages / build:rpm (rocky9) (push) Successful in 11m45s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m11s
Build Packages / Generate python client (push) Successful in 16s
Build Packages / Build documentation (push) Successful in 53s
Build Packages / Create release (push) Skipped
Build Packages / XDS test (durin plugin) (push) Successful in 7m16s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 7m22s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m56s
Build Packages / DIALS test (push) Successful in 10m58s
Build Packages / Unit tests (push) Successful in 1h2m58s
Build Packages / build:windows:nocuda (push) Canceled after 0s
Build Packages / build:windows:cuda (push) Canceled after 0s

BraggIntegrationSettings::DMinLimit_A had a setter that nothing anywhere called, so
it was always its 1.0 A default - in rugnux, the viewer and the broker alike, with
no option or API field to change it. It feeds the predictor as high_res_A, which
discards any reflection with |q| > 1/d_min, so integration simply stopped at 1.0 A
however far the detector reached.

Five of the 33 rotation test datasets have detectors reaching past it, down to
0.981 A. On one of them, run with no resolution limit, the shell table ended dead
at 1.00 A with that shell still at CC1/2 55.6% and <I/sig> 3.4 - cut mid-shell
rather than fading out. This branch had already made the sibling limits
detector-driven (spot finding, scaling), so the pipeline was finding spots the
detector could see and then refusing to integrate them.

Make it a std::optional: unset means as far as the detector reaches, a value limits.
The limit is only a bound on how far the lattice walk goes, never a second opinion
on what is measurable - both predictors independently drop reflections that miss the
detector (BraggPrediction.cpp, BraggPredictionRot.cpp) - which is what makes the
detector's own reach the right default. rugnux gains --integration-high-resolution
(0 = no limit, as for --spot-high-resolution); the derived per-axis prediction range
resolves against the same number, so the two cannot drift.

Full battery: 30/33 space groups, unchanged from before, 0 failures and the same
three known mismatches; 22 of 32 crystals bit-identical and nothing worse than 5
observations in ~500k. The datasets that gain do so because their detector reached
past 1.0 A - the effect is understated here because the harness caps each merge at
the XDS resolution anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-31 14:03:34 +02:00
co-authored by Claude Opus 5
parent 406c406988
commit 0ca159449f
7 changed files with 56 additions and 17 deletions
+18 -7
View File
@@ -32,22 +32,33 @@ namespace {
return static_cast<int>(std::ceil(axis_A / d_min_A)) + 1;
}
// The resolution the prediction walks out to: an explicit setting, else as far as the detector
// reaches. The predictor drops any reflection that misses the detector anyway, so this bound cannot
// add reflections the geometry does not offer - it only decides how much of the lattice is examined,
// which is why the detector's own reach is the right default and a fixed number was not. A geometry
// with no scattering angle at all (no distance, no wavelength) reports 0, which is not a resolution
// to divide by; nothing can be predicted from it either way.
float IntegrationDMin_A(const DiffractionExperiment &experiment) {
if (const auto fixed = experiment.GetBraggIntegrationSettings().GetDMinLimit_A())
return *fixed;
const float detector_A = experiment.GetDetectorMaxResolution_A();
return detector_A > 0.0f ? detector_A : 1.0f;
}
// An explicit setting is enforced as given, on every index - it is one number, deliberately, because
// it exists to bound the work rather than to describe the crystal. Otherwise the cell decides. The
// online path always carries a value (the broker bootstraps one and the API can change it), so a
// live acquisition never has its per-frame cost decided by whichever crystal was mounted.
void ApplyPredictionRange(BraggPredictionSettings &settings, const DiffractionExperiment &experiment,
const CrystalLattice &latt) {
const auto &bragg = experiment.GetBraggIntegrationSettings();
if (const auto fixed = bragg.GetMaxHKL()) {
if (const auto fixed = experiment.GetBraggIntegrationSettings().GetMaxHKL()) {
settings.max_h = settings.max_k = settings.max_l = *fixed;
return;
}
const UnitCell cell = latt.GetUnitCell();
const float d_min_A = bragg.GetDMinLimit_A();
settings.max_h = MaxIndexForAxis(cell.a, d_min_A);
settings.max_k = MaxIndexForAxis(cell.b, d_min_A);
settings.max_l = MaxIndexForAxis(cell.c, d_min_A);
settings.max_h = MaxIndexForAxis(cell.a, settings.high_res_A);
settings.max_k = MaxIndexForAxis(cell.b, settings.high_res_A);
settings.max_l = MaxIndexForAxis(cell.c, settings.high_res_A);
}
}
@@ -466,7 +477,7 @@ void IndexAndRefine::QuickPredictAndIntegrate(DataMessage &msg,
};
BraggPredictionSettings settings_prediction{
.high_res_A = experiment.GetBraggIntegrationSettings().GetDMinLimit_A(),
.high_res_A = IntegrationDMin_A(experiment),
.ewald_dist_cutoff = ewald_dist_cutoff,
// 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