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
* 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>
100 lines
5.9 KiB
C++
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
|