Two independent pieces in the same code path. The background-estimate variance was never propagated. A reflection's background comes from a finite ring of n_b pixels, so subtracting it adds var(B)/n_b per signal pixel - sqrt(1 + n_d/n_b) = 1.109 with the shipped stencil. Both engines omitted it, which is exactly the 1.11-1.19 gap measured between the off-ring scatter and the reported sigma. Three lines each; it affects every dataset, not only iced ones. The radial correction is new and OFF by default (--background-radial). The signal disk and the background ring are concentric, so for any background LINEAR in position <B>_ann == <B>_disk identically and a plane fit buys nothing; the leading error is the CURVATURE of the radial background, which on a sharp ice ring reaches +26 counts on a single reflection. Since every reflection uses the same stencil, that error is a fixed kernel over radial offset - one short dot product per reflection and no extra pixel reads. Validated on empty apertures before any C++: mean |bias| over 9 bands / 3 crystals 4.33 -> 0.79 counts with the scatter unchanged. Three things it cost a battery each to learn, all now in the code: - the radial curve must be accumulated from CLIPPED annulus pixels, inside the clip pass, or it carries neighbour tails and zingers (so it is inert under --integrator boxsum, which has no clip pass); - the GPU version was a 1.8x slowdown from atomicAdd contention on a small radial array - staged in shared memory per block it now costs nothing measurable; - it is battery-NEUTRAL as a default, because the reflections whose bias it fixes are the ones the ice handling already excludes. Hence off by default. CPU/GPU parity extended with two radial sections: 9002 assertions. Also fixes a latent French-Wilson quadrature collapse: j_max = I + 8 sigma on a fixed 400-point grid degenerates to a single cell once sigma >> 50 <I>, giving F = 0.1 sqrt(sigma) with sigmaF -> 0. Harmless today, but any sigma-inflation scheme detonates it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
151 lines
5.0 KiB
C++
151 lines
5.0 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <cmath>
|
|
|
|
#include "BraggIntegrationSettings.h"
|
|
#include "JFJochException.h"
|
|
|
|
#define check_max(param, val, max) if ((val) > (max)) throw JFJochException(JFJochExceptionCategory::InputParameterAboveMax, param)
|
|
#define check_min(param, val, min) if ((val) < (min)) throw JFJochException(JFJochExceptionCategory::InputParameterBelowMin, param)
|
|
#define check_finite(param, val) if (!std::isfinite(val)) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, param)
|
|
|
|
BraggIntegrationSettings &BraggIntegrationSettings::R1(float input) {
|
|
check_finite("Integration radius R1", input);
|
|
check_min("Integration radius R1", input, 0.1);
|
|
check_max("Integration radius R1", input, 20.0);
|
|
r_1 = input;
|
|
return *this;
|
|
}
|
|
|
|
BraggIntegrationSettings &BraggIntegrationSettings::R2(float input) {
|
|
check_finite("Background inner radius R2", input);
|
|
check_min("Background inner radius R2", input, 0.1);
|
|
check_max("Background inner radius R2", input, 30.0);
|
|
|
|
if (input <= r_1)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Background inner radius (R2) must be larger than integration radius (R1)");
|
|
|
|
r_2 = input;
|
|
return *this;
|
|
}
|
|
|
|
BraggIntegrationSettings &BraggIntegrationSettings::R3(float input) {
|
|
check_finite("Background outer radius R3", input);
|
|
check_min("Background outer radius R3", input, 0.1);
|
|
check_max("Background outer radius R3", input, 40.0);
|
|
|
|
if (input <= r_2)
|
|
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
|
|
"Background outer radius (R3) must be larger than background inner radius (R2)");
|
|
|
|
r_3 = input;
|
|
return *this;
|
|
}
|
|
|
|
BraggIntegrationSettings &BraggIntegrationSettings::DMinLimit_A(std::optional<float> input) {
|
|
if (input) {
|
|
check_finite("Minimum d-spacing", *input);
|
|
check_min("Minimum d-spacing", *input, 0.5);
|
|
check_max("Minimum d-spacing", *input, 100.0);
|
|
}
|
|
d_min_limit_A = input;
|
|
return *this;
|
|
}
|
|
|
|
BraggIntegrationSettings & BraggIntegrationSettings::FixedProfileRadius_recipA(std::optional<float> input) {
|
|
if (input) {
|
|
check_finite("Profile radius", input.value());
|
|
check_min("Profile radius [A^-1]", input.value(), 0.000001);
|
|
check_max("Profile radius [A^-1]", input.value(), 0.01);
|
|
}
|
|
fixed_profile_radius = input;
|
|
return *this;
|
|
|
|
}
|
|
|
|
std::optional<float> BraggIntegrationSettings::GetFixedProfileRadius_recipA() const {
|
|
return fixed_profile_radius;
|
|
}
|
|
|
|
BraggIntegrationSettings &BraggIntegrationSettings::Integrator(IntegratorMode input) {
|
|
integrator_mode = input;
|
|
return *this;
|
|
}
|
|
|
|
IntegratorMode BraggIntegrationSettings::GetIntegrator() const {
|
|
return integrator_mode;
|
|
}
|
|
|
|
float BraggIntegrationSettings::GetR1() const {
|
|
return r_1;
|
|
}
|
|
|
|
float BraggIntegrationSettings::GetR2() const {
|
|
return r_2;
|
|
}
|
|
|
|
float BraggIntegrationSettings::GetR3() const {
|
|
return r_3;
|
|
}
|
|
|
|
std::optional<float> BraggIntegrationSettings::GetDMinLimit_A() const {
|
|
return d_min_limit_A;
|
|
}
|
|
|
|
float BraggIntegrationSettings::GetMinimumSigmaInRegardsToI() const {
|
|
return minimum_sigma_in_regards_to_i;
|
|
}
|
|
|
|
BraggIntegrationSettings &BraggIntegrationSettings::BackgroundTrimFraction(float input) {
|
|
check_finite("Background trim fraction", input);
|
|
check_min("Background trim fraction", input, 0.0);
|
|
check_max("Background trim fraction", input, 0.49); // must leave a central majority after trimming
|
|
bkg_trim_fraction = input;
|
|
if (input > 0.0f)
|
|
bkg_clip_nsigma = 0.0f; // the two ring estimators are alternatives, not a stack
|
|
return *this;
|
|
}
|
|
|
|
float BraggIntegrationSettings::GetBackgroundTrimFraction() const {
|
|
return bkg_trim_fraction;
|
|
}
|
|
|
|
BraggIntegrationSettings &BraggIntegrationSettings::MaxHKL(std::optional<int> input) {
|
|
if (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;
|
|
}
|
|
|
|
std::optional<int> BraggIntegrationSettings::GetMaxHKL() const {
|
|
return max_hkl;
|
|
}
|
|
|
|
BraggIntegrationSettings &BraggIntegrationSettings::BackgroundClipNSigma(float input) {
|
|
check_finite("Background clip nsigma", input);
|
|
check_min("Background clip nsigma", input, 0.0);
|
|
bkg_clip_nsigma = input;
|
|
if (input > 0.0f)
|
|
bkg_trim_fraction = 0.0f; // the two ring estimators are alternatives, not a stack
|
|
return *this;
|
|
}
|
|
|
|
float BraggIntegrationSettings::GetBackgroundClipNSigma() const {
|
|
return bkg_clip_nsigma;
|
|
}
|
|
|
|
BraggIntegrationSettings &BraggIntegrationSettings::BackgroundRadialCorrection(bool input) {
|
|
bkg_radial_correction = input;
|
|
return *this;
|
|
}
|
|
|
|
bool BraggIntegrationSettings::IsBackgroundRadialCorrection() const {
|
|
return bkg_radial_correction;
|
|
}
|