Files
Jungfraujoch/image_analysis/bragg_integration/BackgroundAperture.h
T
leonarski_f b00c668e3a
Build Packages / Create release (push) Successful in 8s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 5m11s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 10m32s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 18m26s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 22m11s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 23m32s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 25m36s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 27m28s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 28m10s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 28m10s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 29m24s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 21m45s
Build Packages / Generate python client (push) Successful in 31s
Build Packages / Build documentation (push) Successful in 1m21s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 24m2s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 29m25s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 30m27s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 27m12s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 32m20s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 33m36s
Build Packages / Unit tests (push) Successful in 2h7m18s
Build Packages / Validation Battery (push) Successful in 33m19s
v1.0.0-rc.179 (#90)
* Rugnux: Stills processing needs only the unit cell and space group beyond what the file records - detector distance, beam centre, tilt, panel placement, saturation level, gain and polarisation plane are found from the images - and is up to twice as fast.
* Rugnux: Stills can be indexed without a GPU, with a CPU implementation of the fast-feedback indexer (macOS, computers without a GPU).
* Rugnux: Rotation and stills data are integrated in the detector's own units with one gain model, improving data from CCD detectors.
* Web frontend: The Rugnux live page shows the placed model with its electron density and anomalous difference map in a Mol* viewer.

Reviewed-on: #90
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-10-24 14:52:29 +02:00

100 lines
5.9 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <cstddef>
#include <cstdint>
#include <vector>
#include "../../common/DetectorModules.h"
#include "../../common/DiffractionGeometry.h"
#include "../../common/DiffractionSpot.h"
#include "../image_preprocessing/ImagePreprocessorBuffer.h"
#include "RingBackground.h"
// How far out the background ring should reach, measured on apertures with nothing in them.
//
// The ring mean stands in for the background under the signal disk. A wider ring averages more pixels,
// so its own noise falls; but it also reaches further from the reflection, so whatever the background
// does between the disk and the ring - a curved solvent ring, the tile structure of a fibre-optic CCD,
// the tails of reflections nobody predicted - enters the intensity as a bias. Which of the two wins is
// a property of the detector and the sample, not a constant: on a pixel detector counting a fraction of
// a photon per pixel the noise wins and the widest ring is best, on a binned CCD reading hundreds of
// ADU a pixel the structure wins and the narrowest is.
//
// So it is measured. Apertures are laid on a grid over pre-scan frames, away from every spot found; on
// each, the mean of the r1 disk and the mean of the r2..r3 ring are taken for every candidate r3, and
// the run keeps the r3 whose ring predicts the disk best - the smallest mean square of the difference.
// With nothing in the aperture the difference is exactly the error the ring makes in the background of
// a reflection: its noise plus its bias. The disk's own noise is the same for every candidate, so it
// does not move the choice. The ring is clipped as the integrator clips it (high side, at
// mean + nsigma * sqrt(gain * mean)).
//
// The same apertures also say whether the radial background-curvature correction helps: each difference
// is taken a second time with the correction applied as the integrator applies it - the ring's clip
// following the frame's own ring background (RingBackground), and that background averaged over the
// ring pixels the clip kept minus over the disk's, which is what the integrator takes off a reflection
// (BraggCurveCorrection).
// Where the background really is a function of the radius the corrected ring predicts the disk better;
// where it is not - a strongly polarised beam, panels of different response - it predicts it worse, and
// the run measures which. On stills the ring is chosen on the plain differences first, so its reach does
// not rest on the radial model; the correction is then kept where it improves that ring. Rotation measures
// every stencil the spot width can give it, and reads the one it gets.
namespace background_aperture {
// The squared disk-minus-ring differences, summed per candidate r3 over every aperture measured: plain,
// and with the radial correction (counted separately: a frame without a ring background adds none).
struct Tally {
std::vector<double> sum_d2, sum_d2_radial;
// Sum of the squared per-aperture change the correction makes to d^2, for the standard error of
// the paired comparison (RadialZ).
std::vector<double> sum_dd2;
size_t apertures = 0, apertures_radial = 0;
void Add(const Tally &other);
// The mean square difference of each candidate, plain or corrected; empty where nothing was measured.
[[nodiscard]] std::vector<double> MeanSquare(bool radial) const;
// How many standard errors the correction lowers candidate k's mean square by, over the apertures
// measured both ways (positive: the correction helps).
[[nodiscard]] double RadialZ(size_t k) const;
};
// A signal disk r1, the inner edge of its ring r2, and the candidate outer edges r3.
struct Stencil {
float r1 = 0.0f, r2 = 0.0f;
std::vector<float> r3;
};
// The candidate outer radii: from r2 + 2 up to max_r3, one pixel apart.
[[nodiscard]] std::vector<float> CandidateR3(float r2, float max_r3);
// The apertures of one frame, appended to `tally`. Centres lie on a grid over the frame, and an aperture
// is used only where every pixel out to the largest candidate is readable and no found spot is within
// that radius plus SPOT_CLEARANCE_PX of its centre. `rings` is this frame's ring background, or empty;
// `geometry` and `modules` place the pixels on the plane to read it at their resolution.
void MeasureFrame(ImagePreprocessorBuffer &image, int width, int height,
const std::vector<DiffractionSpot> &spots, float r1, float r2,
const std::vector<float> &r3, float clip_nsigma, float gain,
const RingBackground &rings, const DiffractionGeometry &geometry,
const DetectorModules &modules, Tally &tally);
// Whether the radial correction makes candidate k's ring predict the disk better - its mean square
// difference lower by at least the fraction min_gain - on apertures that were all measured both ways.
[[nodiscard]] bool RadialHelps(const Tally &tally, size_t k, double min_gain = 0.0);
// The part of the background error the correction must take away on a rotation run. A still has no other
// correction, so any gain is taken there; a rotation run already corrects the frames whose ice score shows
// a smooth ring, and the frame's curve on every frame replaces that only for a clear gain. Measured: on
// photon-counting sweeps the apertures gain 0.1-5% (z 3-4) and every image corrected costs ISa and
// anomalous signal; where it pays - home-source CCD rings, a smooth ice ring - they gain 13-31%.
constexpr double ROTATION_RADIAL_MIN_GAIN = 0.10;
// The candidate with the smallest mean square difference, or -1 where nothing was measured.
[[nodiscard]] int Best(const std::vector<double> &mean_square);
constexpr float SPOT_CLEARANCE_PX = 4.0f;
// At most this many apertures a frame: a 16 Mpx detector would otherwise lay twenty thousand.
constexpr size_t MAX_APERTURES_PER_FRAME = 2000;
} // namespace background_aperture