Files
Jungfraujoch/image_analysis/bragg_integration/BraggIntegrationEngine.h
T
leonarski_fandClaude Opus 4.8 f6eca6c69e
Build Packages / build:viewer-tgz:cpu (push) Successful in 6m41s
Build Packages / build:viewer-tgz:cuda (push) Successful in 7m58s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 13m22s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 13m21s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 13m31s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 13m54s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 14m7s
Build Packages / build:windows:nocuda (push) Successful in 16m13s
Build Packages / build:windows:cuda (push) Successful in 17m40s
Build Packages / build:rpm (rocky8) (push) Successful in 10m58s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 12m37s
Build Packages / XDS test (durin plugin) (push) Successful in 7m17s
Build Packages / Generate python client (push) Successful in 28s
Build Packages / Build documentation (push) Successful in 1m9s
Build Packages / Create release (push) Skipped
Build Packages / build:rpm (ubuntu2404) (push) Successful in 11m52s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m44s
Build Packages / build:rpm (rocky9) (push) Successful in 13m44s
Build Packages / DIALS test (push) Successful in 13m38s
Build Packages / XDS test (neggia plugin) (push) Successful in 8m29s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 8m54s
Build Packages / Unit tests (push) Successful in 1h51m52s
Build Packages / Unit tests (pull_request) Successful in 1h38m57s
Build Packages / build:viewer-tgz:cpu (pull_request) Successful in 6m10s
Build Packages / build:viewer-tgz:cuda (pull_request) Successful in 6m44s
Build Packages / build:rpm (rocky8_nocuda) (pull_request) Successful in 10m9s
Build Packages / build:rpm (rocky9_nocuda) (pull_request) Successful in 11m10s
Build Packages / build:rpm (ubuntu2204_nocuda) (pull_request) Successful in 10m30s
Build Packages / build:rpm (ubuntu2404_nocuda) (pull_request) Successful in 9m52s
Build Packages / build:rpm (rocky8_sls9) (pull_request) Successful in 10m48s
Build Packages / build:rpm (rocky9_sls9) (pull_request) Successful in 11m9s
Build Packages / build:rpm (rocky8) (pull_request) Successful in 10m46s
Build Packages / build:rpm (rocky9) (pull_request) Successful in 12m2s
Build Packages / build:rpm (ubuntu2204) (pull_request) Successful in 11m16s
Build Packages / build:rpm (ubuntu2404) (pull_request) Successful in 10m47s
Build Packages / DIALS test (pull_request) Successful in 14m23s
Build Packages / XDS test (durin plugin) (pull_request) Successful in 7m59s
Build Packages / XDS test (JFJoch plugin) (pull_request) Successful in 7m43s
Build Packages / XDS test (neggia plugin) (pull_request) Successful in 7m47s
Build Packages / Generate python client (pull_request) Successful in 12s
Build Packages / Build documentation (pull_request) Successful in 1m11s
Build Packages / Create release (pull_request) Skipped
Build Packages / build:windows:nocuda (pull_request) Successful in 15m54s
Build Packages / build:windows:cuda (pull_request) Successful in 18m24s
bragg_integration: quantization-noise variance floor + summation fallback
The Kabsch profile-fit weights floored the per-pixel variance at 1.0, far
above the real detector noise floor (electronic noise is well below 1 for
both EIGER and JUNGFRAU). The true floor is the quantization noise from
rounding the charge-spread deposited energy to an integer: Var = 1/12. The
1.0 floor silently over-regularized — it inflated weak-reflection sigma ~6x
at high resolution and pinned the scaling error model's `a` term at its 0.25
floor, suppressing ISa.

Use PIXEL_VARIANCE_FLOOR = 1/12, and guard against the weak-reflection
runaways it exposes: fall back to the box-sum (summation) intensity when the
profile fit diverges from the summation seed by more than
PROFILE_SUMMATION_MAX_NSIGMA box-sum sigmas. Applied identically in the CPU
and GPU engines (GPU-vs-CPU equivalence test passes).

Battery of 24 rotation crystals: broad ISa gains (lysoC_14 7.8->10.4, plus
cytC/Ins/Myo), EcwtAL500 completeness 51->92% and CC1/2 35->79%, pding4_001
space group now correct; no space-group regressions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 14:30:15 +02:00

118 lines
6.2 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
// =============================================================================
// BraggIntegrationEngine — box-sum + profile-fitting 2D integrator, GPU-ready
// =============================================================================
//
// A reimplementation of BraggIntegrate2D (box sum) and ProfileIntegrate2D (Kabsch profile
// fit) under one roof, following the AzIntEngine / ROIIntegration pattern: a base class that
// extracts the fixed per-experiment configuration, a plain-C++ CPU engine (the fallback and the
// numeric oracle), and a CUDA engine (BraggIntegrationEngineGPU) that reaches the same result up
// to floating-point precision.
//
// Unlike BraggIntegrate2D/ProfileIntegrate2D, which read the raw CompressedImage per pixel type
// and reject the special/saturation +/-1 band, this engine reads the already-preprocessed int32
// image held in an ImagePreprocessorBuffer (the same buffer AzIntEngineGPU/ROIIntegrationGPU
// consume): masked/bad pixels are INT32_MIN and saturated pixels INT32_MAX, so bad-pixel identity
// is owned by the preprocessor and a pixel is valid iff v != INT32_MIN && v != INT32_MAX.
//
// The integrator is selected by BraggIntegrationSettings::Integrator:
// BoxSum -> BraggIntegrate2D equivalent (rough disk sum minus ring-mean background)
// ProfileGaussian -> per-reflection measured-width Gaussian profile fit (the default)
// ProfileEmpirical-> per-shell learned empirical profile fit
// The box sum is also the seed pass (Pass A) of the two profile modes, so it always runs.
//
// This class is intentionally standalone: it is NOT yet wired into IndexAndRefine. It takes a
// preprocessed image + the predicted reflections and returns the same vector<Reflection> shape
// (I, sigma, bkg, partiality, ...) that the downstream scaling/merge consumes unchanged.
// =============================================================================
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <vector>
#include "../../common/BraggIntegrationSettings.h"
#include "../../common/DiffractionExperiment.h"
#include "../../common/DiffractionGeometry.h"
#include "../../common/Reflection.h"
#include "../image_preprocessing/ImagePreprocessorBuffer.h"
namespace bragg_engine {
// Shared with both engines so the CPU and GPU paths stay numerically aligned.
constexpr int N_SHELL = 6; // resolution shells for per-shell profile learning
constexpr double STRONG_I_OVER_SIGMA = 5.0; // strong-spot threshold that seeds the profile
constexpr int MIN_STRONG_PER_SHELL = 30; // below this a shell falls back to the global profile
constexpr double C_CAPTURE = 2.5; // weak-spot radial capture term (monochromatic only)
// Per-pixel variance floor for the Kabsch fit weights (v = floor + signal). The detector noise floor is
// the quantization noise from rounding the charge-spread deposited energy to an integer: a uniform
// rounding error has variance 1/12. Electronic noise is far below this for both EIGER and JUNGFRAU. A
// larger floor (the previous 1.0) silently over-regularizes — it inflates weak-reflection sigma and
// pins the scaling error model's `a` term at its floor.
constexpr double PIXEL_VARIANCE_FLOOR = 1.0 / 12.0;
// Guard against profile-fit runaways: on a weak / near-zero reflection the reweighted Kabsch iteration
// has no real peak to lock onto and can manufacture intensity the box sum never sees. Fall back to the
// summation (box-sum) intensity when the profile result disagrees with the summation seed by more than
// this many box-sum sigmas (a real fit agrees within counting noise, so the margin is generous).
constexpr double PROFILE_SUMMATION_MAX_NSIGMA = 10.0;
} // namespace bragg_engine
// One reflection's extracted intensity, produced by the derived engine and turned into a
// Reflection by Finalize() (which owns the polarization correction and scale bookkeeping).
struct BraggFitResult {
float I = 0.0f;
float sigma = NAN;
float bkg = 0.0f;
float observed_x = 0.0f; // intensity-weighted centroid (BoxSum mode only)
float observed_y = 0.0f;
bool ok = false;
bool has_observed = false;
};
class BraggIntegrationEngine {
protected:
// --- fixed configuration extracted from the experiment (see ProfileIntegrate2D) ---
IntegratorMode mode;
bool empirical; // ProfileEmpirical (vs ProfileGaussian)
size_t xpixel, ypixel, npixel;
float r1_sq;
float r2, r2_sq;
float r3, r3_sq;
float min_sigma_ratio;
int R, G, GG; // profile-grid half-size, edge (2R+1) and area (G*G)
bool broadband; // a set bandwidth (stills) vs monochromatic (rotation)
double bw_sigma; // bandwidth sigma [dimensionless, * Rpx -> px]
bool apply_bkg_clip; // stills-only high-outlier background sigma-clip
bool use_ellipse; // radially elongate the per-reflection Gaussian
double c_radial; // radial variance coefficient of tan^2(2theta): parallax + capture
double F_px; // detector distance expressed in pixels
float beam_x, beam_y;
DiffractionGeometry geom; // kept for the per-reflection polarization correction
std::optional<float> polarization;
// Assemble output reflections from the per-reflection fit results (polarization + scale corr).
std::vector<Reflection> Finalize(const std::vector<Reflection> &predicted, size_t npredicted,
const std::vector<BraggFitResult> &results,
int64_t image_number) const;
public:
explicit BraggIntegrationEngine(const DiffractionExperiment &experiment);
virtual ~BraggIntegrationEngine() = default;
// predicted[0..npredicted) are the reflections to extract; image is the preprocessed int32
// frame (image.size() == npixel). Returns only the observed reflections.
virtual std::vector<Reflection> Run(const ImagePreprocessorBuffer &image,
const std::vector<Reflection> &predicted, size_t npredicted,
int64_t image_number) = 0;
};