Bragg integration: propagate the background-estimate uncertainty, add an opt-in radial background correction
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>
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
#include "BraggIntegrationEngine.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <numeric>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
@@ -75,6 +77,37 @@ BraggIntegrationEngine::BraggIntegrationEngine(const DiffractionExperiment &expe
|
||||
bkg_clip_nsigma = broadband ? 3.0f : settings.GetBackgroundClipNSigma();
|
||||
bkg_trim = (broadband || bkg_clip_nsigma > 0.0f) ? 0.0f : settings.GetBackgroundTrimFraction();
|
||||
|
||||
// Radial-offset kernels for the background curvature correction. A stencil pixel at (dx, dy)
|
||||
// sits at radial offset dx*cos(phi) + dy*sin(phi) from the reflection, where phi is the
|
||||
// reflection's azimuth; averaging over phi makes the kernels position-independent, which is
|
||||
// exact to the extent the stencil is small against the reflection's radius (r3 = 10 px vs
|
||||
// hundreds). k_diff is the annulus histogram minus the disk histogram, each normalised, so
|
||||
// dot(k_diff, B) is directly mean_annulus(B) - mean_disk(B).
|
||||
bkg_radial = settings.IsBackgroundRadialCorrection();
|
||||
k_off = static_cast<int>(std::ceil(r3)) + 1;
|
||||
k_diff.assign(2 * k_off + 1, 0.0f);
|
||||
{
|
||||
std::vector<double> hist_disk(k_diff.size(), 0.0), hist_ann(k_diff.size(), 0.0);
|
||||
constexpr int n_phi = 512;
|
||||
const int span = static_cast<int>(std::ceil(r3)) + 1;
|
||||
for (int p = 0; p < n_phi; ++p) {
|
||||
const double phi = 2.0 * M_PI * p / n_phi, cp = std::cos(phi), sp = std::sin(phi);
|
||||
for (int dy = -span; dy <= span; ++dy)
|
||||
for (int dx = -span; dx <= span; ++dx) {
|
||||
const double d2 = static_cast<double>(dx) * dx + static_cast<double>(dy) * dy;
|
||||
const int k = k_off + static_cast<int>(std::lround(dx * cp + dy * sp));
|
||||
if (k < 0 || k >= static_cast<int>(k_diff.size()))
|
||||
continue;
|
||||
if (d2 < r1_sq) hist_disk[k] += 1.0;
|
||||
else if (d2 >= r2_sq && d2 < r3_sq) hist_ann[k] += 1.0;
|
||||
}
|
||||
}
|
||||
const double sd = std::accumulate(hist_disk.begin(), hist_disk.end(), 0.0);
|
||||
const double sa = std::accumulate(hist_ann.begin(), hist_ann.end(), 0.0);
|
||||
for (size_t k = 0; k < k_diff.size(); ++k)
|
||||
k_diff[k] = static_cast<float>(hist_ann[k] / sa - hist_disk[k] / sd);
|
||||
}
|
||||
|
||||
polarization = experiment.GetPolarizationFactor();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user