Build Packages / Create release (push) Successful in 7s
Build Packages / build:portable:macos-arm64:nocuda (push) Successful in 4m56s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 9m10s
Build Packages / build:portable:linux-x86_64:nocuda (push) Successful in 13m51s
Build Packages / build:portable:linux-x86_64:cuda (push) Successful in 16m56s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 20m59s
Build Packages / build:portable:windows-x86_64:nocuda (push) Successful in 21m25s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 21m17s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 14m14s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m59s
Build Packages / build:portable:windows-x86_64:cuda (push) Successful in 25m48s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 15m52s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m33s
Build Packages / Generate python client (push) Successful in 29s
Build Packages / Build documentation (push) Successful in 1m19s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m49s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 25m31s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 25m24s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 26m5s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 22m19s
Build Packages / Unit tests (push) Failing after 2h28m24s
Build Packages / Validation Battery (push) Successful in 40m33s
* Rugnux: Processes a collection live while jfjoch_broker writes it (`rugnux <master.h5> <broker URL>`). * Rugnux: Processing starts on the first images and confirms its choices once all are in, so analysis can begin during collection. * Rugnux: Faster processing, especially on servers with several GPUs and CPU sockets and on computers without a GPU (`--no-gpu`, macOS). * Rugnux: Uses much less GPU memory, so large data sets stay on the GPU of smaller cards, and a data set too large for the card is processed on the CPU instead. * Rugnux: On macOS, Bragg integration runs on the Apple GPU. * Rugnux: Better handling of twinning and pseudo-symmetry, with the twin hypothesis written as `<prefix>_twin_<SG>.mtz`, and a wrong `--model` no longer confirms the data or decides the hand. * Rugnux: `--model` accepts predicted (AlphaFold) models and falls back to molecular replacement when the model is not placed as a rigid body (experimental `--mr` forces it), settling space group, hand and twinning and writing a different answer as `<prefix>_mr.mtz`. * Rugnux: Molecular replacement is faster and settles the space group in more cases with several copies in the asymmetric unit. * Rugnux: Deposition-ready mmCIF, a richer report, and numbered steps on the terminal with the full log in `<prefix>_log.txt`; `_P1.mtz`, `--estimate-beam-center` and `--beam-center-search` are removed. * Rugnux: The beam-stop shadow is found correctly on tilted detectors. * Rugnux: Stills files that record a stationary goniometer axis get the default stills geometry refinement. * jfjoch_viewer: "Analyze dataset" uses the same defaults as a bare `rugnux <file>`, and shows the results of processed CBF, marCCD and SMV sweeps. * jfjoch_viewer: The Files panel refreshes on F5 and every few seconds, and the plot type and other selections stay put during live collection. * jfjoch_writer: Memory stays bounded and old data files are closed efficiently on long runs, any failure ends the run in an error state the broker is told about, and the master file appears only after its data files; the ZeroMQ pusher logs when a stalled writer makes it drop images. * Fixes: The TCP image pusher no longer blocks the broker's receiver, GPU out-of-memory failures stop with a clear message, and the XDS plugin masks module and chip edges. --------- Co-authored-by: Filip Leonarski <jungfrau@filips-mac-mini.home> Reviewed-on: #85 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
214 lines
15 KiB
C++
214 lines
15 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <optional>
|
|
#include <vector>
|
|
|
|
// Spot-intensity extraction method used by the Bragg integration engine. ProfileGaussian (default)
|
|
// profile-fits with a measured-width Gaussian (Kabsch-style) - more accurate intensities than the
|
|
// classical uniform BoxSum; validated on anomalous data (stronger S/Cl peaks vs box-sum). BoxSum is
|
|
// the simpler, faster fallback. ProfileEmpirical learns the profile per resolution shell from strong
|
|
// spots - see docs/CPU_DATA_ANALYSIS_INTEGRATION.md (Bragg integration).
|
|
enum class IntegratorMode { BoxSum, ProfileGaussian, ProfileEmpirical };
|
|
|
|
// What the integrator does about a signal region shared with a neighbouring reflection. Off is the
|
|
// historical behaviour: a neighbour's signal is kept out of this reflection's BACKGROUND ring, but
|
|
// nothing keeps it out of the reflection's own SIGNAL region, so on a dense pattern a crowded
|
|
// reflection reads high. Reject is XDS's MINPK (Kabsch, Acta Cryst D66, 133-144 (2010)) - drop the
|
|
// reflection when too little of its expected profile is cleanly its own. Exclude - the default -
|
|
// drops only the shared PIXELS from the fit; a profile fit is the amplitude of a normalised profile,
|
|
// so leaving pixels out renormalises it by construction and the reflection is kept unbiased rather
|
|
// than discarded. A box sum has no profile to renormalise with, so Exclude does nothing there; only
|
|
// Reject acts on it.
|
|
enum class OverlapMode { Off, Reject, Exclude };
|
|
|
|
// What the diffracted beam flies through between the sample and the detector. Air is what a beamline
|
|
// has unless it was built not to have one; helium and vacuum are what long-wavelength stations use,
|
|
// because air absorbs unusably at low energy. It is an explicit choice, not something detected: no
|
|
// field of the NXmx application definition, and no field of any master file rugnux reads, describes
|
|
// the medium in the flight path - see sensor_absorption::FlightPathAttenuation.
|
|
enum class FlightPathMedium { Air, Helium, Vacuum };
|
|
|
|
// The hkl half-width the broker bootstraps when a config carries no bragg_integration block. Matches
|
|
// the max_hkl default in broker/jfjoch_api.yaml, so an omitting client and an omitting config agree.
|
|
constexpr int BRAGG_ONLINE_DEFAULT_MAX_HKL = 100;
|
|
|
|
// The spot footprint measured on the data (image_analysis/bragg_integration/SpotFootprint.h): the
|
|
// radial and tangential standard deviations of a spot [px], tabulated against the distance from the
|
|
// beam centre in bins of bin_px. Empty until measured; the integrator then uses the fixed r1/r2/r3
|
|
// stencil alone.
|
|
struct SpotFootprint {
|
|
static constexpr int MAX_BINS = 16;
|
|
float bin_px = 0.0f;
|
|
std::vector<float> sigma_rad;
|
|
std::vector<float> sigma_tan;
|
|
[[nodiscard]] bool empty() const { return sigma_rad.empty(); }
|
|
};
|
|
|
|
class BraggIntegrationSettings {
|
|
IntegratorMode integrator_mode = IntegratorMode::ProfileGaussian;
|
|
float r_1 = 4;
|
|
float r_2 = 6;
|
|
// The background ring's precision is set by how many pixels it averages, not by how big the
|
|
// reflection is: the ring mean's error enters the intensity n_inner times over, so var(b)/n_B is
|
|
// a first-order term in sigma. At r3 = 10 the ring holds ~200 px against the r1 disk's ~50, and
|
|
// widening it to 13 roughly doubles that for no cost in signal - the disk is untouched, and the
|
|
// extra pixels sit further from the reflection, not closer.
|
|
float r_3 = 13;
|
|
// How many times the beam's radial streak to push the r2..r3 background ring out by, per
|
|
// reflection. A bandwidth streaks a spot radially by bw_sigma*Rpx, and against a fixed pixel ring
|
|
// that puts the background annulus on the reflection's own tails at high resolution, where it
|
|
// measures signal as background. The ring's radial semi-axes become r2 + this*bw_sigma*Rpx and
|
|
// r3 + this*bw_sigma*Rpx, the tangential ones stay r2 and r3, and the r1 signal disk stays a
|
|
// circle (growing it trips the all-or-nothing n_inner_valid gate). 0 reproduces the fixed
|
|
// circular stencil exactly, and so does any monochromatic beam, where the streak is zero.
|
|
float stencil_k_sigma = 0.0f;
|
|
// Integration/prediction resolution limit. Unset means "as far as the detector reaches", resolved
|
|
// from the geometry where it is used. The predictor independently rejects any reflection that misses
|
|
// the detector, so this is a bound on how far the lattice walk goes rather than a second opinion on
|
|
// what is measurable - a fixed default simply truncated every experiment whose detector reached
|
|
// past it.
|
|
std::optional<float> d_min_limit_A;
|
|
std::optional<float> fixed_profile_radius;
|
|
// Diagnostic: the rocking width the PREDICTION window is opened to, in place of the per-image
|
|
// sigma_M. Prediction and partiality use one number today, so a sigma_M that moves takes the
|
|
// integrated reflection population with it; pinning this holds the population still while the
|
|
// partiality keeps using the measured sigma_M, which separates the two effects.
|
|
std::optional<float> forced_prediction_mosaicity_deg;
|
|
float minimum_sigma_in_regards_to_i = 0.02;
|
|
// The r2..r3 background ring is estimated with ONE of two robust means, never both: a high-side
|
|
// sigma-clip (bkg_clip_nsigma, the default) or a symmetric trimmed mean (bkg_trim_fraction). Setting
|
|
// either through its setter clears the other, so whichever was asked for last is the one in force;
|
|
// with both at 0 the ring is a plain mean.
|
|
//
|
|
// Symmetric trimmed-mean fraction: drop the lowest and highest this fraction of ring pixels before
|
|
// averaging. Robust to the high-side contamination (neighbour-spot wings, tails, zingers) that
|
|
// biases a plain ring mean up, but a symmetric trim is NOT a consistent estimator of the mean of a
|
|
// right-skewed (Poisson) sample - it sits ~0.1 ct/px low at every level, which with ~50 ring pixels
|
|
// adds ~5 counts to every partial. Kept reachable (rugnux --background-trim) for back compatibility;
|
|
// 0.10 was the shipped value.
|
|
float bkg_trim_fraction = 0.0f;
|
|
// High-side-only sigma clip: reject ring pixels above mean + this many sqrt(mean). Rejects the same
|
|
// contamination as the trim - measurably better, in fact - without cutting the low side, so it does
|
|
// not carry the trim's skew bias. Measured empty-aperture pedestal, counts: plain mean -0.03..-0.20,
|
|
// 10% symmetric trim +5.05..+6.34, 4 sigma clip +0.02..+0.54. Whatever is set here is what the
|
|
// engine applies (rugnux --background-clip); the front end picks the default, and rugnux lowers it
|
|
// to 3 sigma for broadband (non-zero bandwidth) data, where longer spots leak further into the ring.
|
|
float bkg_clip_nsigma = 4.0f;
|
|
// Variance of a background pixel per unit of its mean, the "n*sqrt(mean)" of the clip above being
|
|
// n*sqrt(this * mean). 1 for a detector that counts photons, which is what every HDF5 and CBF input
|
|
// is taken to be. A CCD reports ADU instead, and on a Rigaku Saturn a pixel scatters 4-5x its mean:
|
|
// there the photon clip cuts the background's upper tail at about 2 sigma and adds a positive offset
|
|
// to every reflection. Set by rugnux from the pre-scan for SMV and marCCD input (BackgroundGain.h).
|
|
float bkg_pixel_gain = 1.0f;
|
|
// Radial background curvature correction. The signal disk and the background annulus are
|
|
// concentric, so for ANY background linear in position their means are equal - a plane fit buys
|
|
// nothing and the leading error is the CURVATURE of the radial background, which the flat annulus
|
|
// mean is structurally blind to. Sitting on an ice ring that reaches +26 counts on a single
|
|
// reflection. When on, a radial background curve is accumulated per image from the annulus pixels
|
|
// that are already read, and each reflection's background is corrected by
|
|
// mean_annulus(B) - mean_disk(B), evaluated as a fixed kernel over radial offset (O(1), no extra
|
|
// pixel reads). Measured empty-aperture bias over 9 bands on 3 crystals: 4.33 -> 0.79 counts mean
|
|
// |bias|, scatter unchanged.
|
|
//
|
|
// Unset means AUTO: apply it per image where that image's peak-excluded ice score says a SMOOTH
|
|
// powder ring is present, and not otherwise. NOT the default - see below. The correction models the
|
|
// background as a function of radius alone, so it helps exactly where that is true and not
|
|
// elsewhere. Measured against a fixed external model, band-versus-decoy-band: on a crystal with
|
|
// pure smooth ice it removes 43% of the ice bands' excess amplitude, with the effect 7x stronger
|
|
// inside the bands than outside; on a crystal whose ice is discrete crystallite SPOTS - no smooth
|
|
// radial ring to model - the excess amplitude instead GREW by half; on clean data it is inert to
|
|
// four decimal places. The ice score's two channels separate those two morphologies, so the
|
|
// correction is gated on the smooth one. Auto only engages where a peak-excluded score exists
|
|
// (adaptive spot finding); the plain profile carries the Bragg peaks and cannot support a
|
|
// threshold, so without it auto stays off.
|
|
//
|
|
// OFF by default. Auto targets correctly - over the rotation battery it fires on ten crystals and
|
|
// every one of them is ice-positive - but it costs 1.35x the wall clock, and on the merge
|
|
// statistics it is the familiar sign-mixed trade rather than a win: high-shell CC1/2 worse on
|
|
// three of the four crystals that move materially. The case for it rests on agreement with an
|
|
// external model, which is the better arbiter but a narrower one, so it stays opt-in until that
|
|
// is settled on its own evidence.
|
|
std::optional<bool> bkg_radial_correction = false;
|
|
// Half-width of the hkl cube the predictor walks: every reflection with |h|,|k|,|l| <= this is
|
|
// tested against the Ewald sphere, and nothing outside it can ever be predicted. An axis is
|
|
// truncated once a/d_min exceeds this, and the GPU cost is the cube (2n+1)^3 of candidates, so
|
|
// neither a small nor a large fixed value is right for every crystal.
|
|
//
|
|
// Unset (the default) means "take it from the refined cell", which is exact: the predictor keeps
|
|
// only |q| <= 1/d_min and h = a.q, so no reflection can have |h| > a/d_min. See MaxHKLForCell.
|
|
// Offline that is what is wanted. ONLINE it is not: the broker bootstraps a concrete value
|
|
// (BRAGG_ONLINE_DEFAULT_MAX_HKL) so per-image cost stays predictable across samples.
|
|
std::optional<int> max_hkl;
|
|
// Overlap treatment and the MINPK threshold: the least fraction of a reflection's expected profile
|
|
// that must be usable for the reflection to be kept. Excluding the shared pixels is the default:
|
|
// over the rotation battery it costs 1.1% of the wall clock (23% on a genuinely crowded crystal,
|
|
// nothing where no two predictions touch) and buys ISa on 15 crystals against 5, cutting the summed
|
|
// shortfall against XDS by a third.
|
|
//
|
|
// As in XDS, one threshold governs both ways a reflection can lose part of its profile. Under
|
|
// OverlapMode::Reject it is the fraction that must be cleanly the reflection's own rather than a
|
|
// neighbour's. In every profile mode it is also the fraction that must be READABLE - not masked,
|
|
// untrusted, in a detector gap or overloaded - because the profile fit renormalises to the pixels
|
|
// it can read (dials calls that valid_foreground_threshold, and defaults it to the same 0.75).
|
|
OverlapMode overlap_mode = OverlapMode::Exclude;
|
|
float overlap_min_peak = 0.75f;
|
|
// What the diffracted beam crosses between the sample and its pixel, which sets the flight-path
|
|
// attenuation correction (sensor_absorption::FlightPathAttenuation). Air by default; the
|
|
// correction is computed from the NIST attenuation coefficient of the medium, the stated
|
|
// sample-to-detector distance and the stated wavelength, and nothing about it is fitted.
|
|
//
|
|
// It is a declared choice rather than a detected one, and that is not a gap waiting to be
|
|
// closed. Nothing in the files says what the medium was, and the implied transmission does not
|
|
// separate the cases either: in this corpus a CONFIRMED helium station sits at 51% implied air
|
|
// transmission and a CONFIRMED air station at 63%, so any rule dividing them would be a
|
|
// threshold fitted between two points rather than a physical criterion. The honest design is an
|
|
// explicit option, a stated default, and the assumption printed in the report.
|
|
FlightPathMedium flight_path = FlightPathMedium::Air;
|
|
SpotFootprint footprint;
|
|
|
|
public:
|
|
BraggIntegrationSettings& R1(float input);
|
|
BraggIntegrationSettings& R2(float input);
|
|
BraggIntegrationSettings& R3(float input);
|
|
BraggIntegrationSettings& StencilKSigma(float input);
|
|
BraggIntegrationSettings& DMinLimit_A(std::optional<float> input);
|
|
BraggIntegrationSettings& FixedProfileRadius_recipA(std::optional<float> input);
|
|
BraggIntegrationSettings& ForcedPredictionMosaicity_deg(std::optional<float> input);
|
|
BraggIntegrationSettings& Integrator(IntegratorMode input);
|
|
BraggIntegrationSettings& BackgroundTrimFraction(float input);
|
|
BraggIntegrationSettings& BackgroundClipNSigma(float input);
|
|
BraggIntegrationSettings& BackgroundPixelGain(float input);
|
|
BraggIntegrationSettings& BackgroundRadialCorrection(std::optional<bool> input);
|
|
BraggIntegrationSettings& MaxHKL(std::optional<int> input);
|
|
BraggIntegrationSettings& Overlap(OverlapMode input);
|
|
BraggIntegrationSettings& OverlapMinPeak(float input);
|
|
BraggIntegrationSettings& FlightPath(FlightPathMedium input);
|
|
BraggIntegrationSettings& Footprint(const SpotFootprint &input);
|
|
|
|
|
|
[[nodiscard]] IntegratorMode GetIntegrator() const;
|
|
[[nodiscard]] float GetR1() const;
|
|
[[nodiscard]] float GetR2() const;
|
|
[[nodiscard]] float GetR3() const;
|
|
[[nodiscard]] float GetStencilKSigma() const;
|
|
[[nodiscard]] std::optional<float> GetFixedProfileRadius_recipA() const;
|
|
[[nodiscard]] std::optional<float> GetForcedPredictionMosaicity_deg() const;
|
|
[[nodiscard]] std::optional<float> GetDMinLimit_A() const;
|
|
|
|
[[nodiscard]] float GetMinimumSigmaInRegardsToI() const;
|
|
[[nodiscard]] float GetBackgroundTrimFraction() const;
|
|
[[nodiscard]] float GetBackgroundClipNSigma() const;
|
|
[[nodiscard]] float GetBackgroundPixelGain() const;
|
|
// Unset = auto (gate per image on the smooth-ice score); see bkg_radial_correction.
|
|
[[nodiscard]] std::optional<bool> GetBackgroundRadialCorrection() const;
|
|
[[nodiscard]] std::optional<int> GetMaxHKL() const;
|
|
[[nodiscard]] OverlapMode GetOverlap() const;
|
|
[[nodiscard]] float GetOverlapMinPeak() const;
|
|
// See flight_path: air unless the user said otherwise, which no file can say for them.
|
|
[[nodiscard]] FlightPathMedium GetFlightPath() const;
|
|
[[nodiscard]] const SpotFootprint &GetFootprint() const;
|
|
};
|