// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute // 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 is the Bragg integrator used by the pipeline (bound in MXAnalysisWithoutFPGA: the GPU // engine when a device is present, otherwise the CPU engine). It takes a preprocessed image + // the predicted reflections and returns the vector (I, sigma, bkg, partiality, ...) // that the downstream scaling/merge consumes unchanged. // ============================================================================= #include #include #include #include #include #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 polarization; // Assemble output reflections from the per-reflection fit results (polarization + scale corr). std::vector Finalize(const std::vector &predicted, size_t npredicted, const std::vector &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 Run(const ImagePreprocessorBuffer &image, const std::vector &predicted, size_t npredicted, int64_t image_number) = 0; };