Files
Jungfraujoch/image_analysis/spot_finding/SpotUtils.h
T
leonarski_f 84228bf8be
Build Packages / Create release (push) Successful in 24s
Build Packages / build:viewer:macos-arm64:nocuda (push) Successful in 3m29s
Build Packages / build:rugnux:macos-arm64:nocuda (push) Successful in 2m43s
Build Packages / build:rugnux:linux-aarch64:cuda (push) Successful in 8m27s
Build Packages / build:rugnux:linux-x86_64:cuda (push) Successful in 9m53s
Build Packages / build:viewer:linux-x86_64:nocuda (push) Successful in 9m58s
Build Packages / build:viewer:linux-x86_64:cuda (push) Successful in 11m22s
Build Packages / build:jfjoch:rocky8:nocuda (push) Successful in 13m39s
Build Packages / build:viewer:windows-x86_64:nocuda (push) Successful in 18m37s
Build Packages / build:jfjoch:rocky9:nocuda (push) Successful in 16m32s
Build Packages / build:viewer:windows-x86_64:cuda (push) Successful in 24m11s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 25m30s
Build Packages / build:jfjoch:ubuntu2404:nocuda (push) Successful in 19m3s
Build Packages / build:jfjoch:ubuntu2204:nocuda (push) Successful in 20m23s
Build Packages / build:jfjoch:rocky8:cuda-sls9 (push) Successful in 19m41s
Build Packages / Generate python client (push) Successful in 50s
Build Packages / Build documentation (push) Successful in 1m16s
Build Packages / build:jfjoch:rocky9:cuda-sls9 (push) Successful in 21m0s
Build Packages / build:jfjoch:rocky8:cuda (push) Successful in 18m38s
Build Packages / build:rugnux:windows-x86_64:cuda (push) Successful in 14m33s
Build Packages / build:jfjoch:rocky9:cuda (push) Successful in 17m55s
Build Packages / build:jfjoch:ubuntu2204:cuda (push) Successful in 20m50s
Build Packages / build:jfjoch:ubuntu2404:cuda (push) Successful in 18m38s
Build Packages / Unit tests (push) Successful in 1h46m14s
v1.0.0-rc.173 (#83)
* jfjoch_broker: Optional per-dataset authentication - statistics, images and plots can require a bearer token, which jfjoch_viewer supports.
* jfjoch_viewer: Dark mode and a theme-matched colour scheme, a magnifier panel, and simpler contrast and background controls.
* Rugnux: Multiple performance improvements on GPU and CPU (CPU-only processing up to 40% faster, faster image decoding on ARM), with unchanged results.
* Rugnux: `--model` rigid-body refinement runs on the GPU, and the model-validation check is faster and more reliable.
* Rugnux: Improved scaling and merging - error model, outlier rejection, absorption correction and French-Wilson amplitudes now agree more closely with XDS and ctruncate.
* Rugnux: Improved integration - radial background on powder and ice rings, crowded rotation data keep their reflections, and CPU-only builds integrate large unit cells as GPU builds do.
* Rugnux: More robust detector geometry - measured beam centre, X-ray bandwidth and goniometer rate, and geometry refinement accepted only on significant evidence.
* Rugnux: Merged files are written in the standard setting, or in the setting of a reference MTZ, structure-factor mmCIF or model, with its free-R flags.
* Rugnux: Richer report - ice and powder rings, further lattices, superstructure candidates and mosaicity, with warnings worded as prompts to check.
* Rugnux: Clear error messages when a data set needs more GPU or host memory than is available.

Reviewed-on: #83
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-09-29 15:57:32 +02:00

95 lines
5.6 KiB
C++

// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include "../../common/DiffractionSpot.h"
void GenerateSpotPlot(DataMessage &msg, const std::vector<SpotToSave> &spots, float d_min_A);
void CountSpots(DataMessage &msg,
const DiffractionExperiment& experiment,
const std::vector<DiffractionSpot> &spots,
float d_min_A);
void CountSpots(DataMessage &msg,
const std::vector<SpotToSave> &spots,
float d_min_A);
float CountIceRingControlSpots(const std::vector<SpotToSave> &spots, float half_width_q_recipA);
void MarkIceRings(std::vector<SpotToSave> &spots, float tolerance_q_recipA);
// Flag every spot sitting within tolerance of one of the given ring positions (q = 2*pi/d). The same
// flag MarkIceRings sets, because it means the same thing downstream: a spot that belongs to a powder
// ring and not to this crystal.
void MarkRings(std::vector<SpotToSave> &spots, const std::vector<float> &rings_q_recipA,
float tolerance_q_recipA);
// What a POOLED spot list says about powder contamination: a crystalline phase other than the
// crystal, diffracting as rings the crystal's own reflections sit among. Hexagonal ice has a fixed
// list of rings and MarkIceRings above; nothing else that powders does - a shower of microcrystals
// around the crystal, salt out of the cryoprotectant, a phase nobody expected - and on such a pattern
// the contaminant's spots can outnumber the crystal's several times over in every frame.
//
// A ring is a q bin holding far more spots than the smooth spot-density fall-off around it. The
// baseline is a running median over a window wide compared with a ring, so it follows that fall-off
// without following the rings themselves. Pool over frames spread across the sweep: a powder ring is
// on every frame and the crystal's own rows are not, so pooling sharpens the rings and flattens
// everything else.
struct PowderRings {
// Ring centres, q = 2*pi/d. Empty on a pattern that shows no rings, which is most of them.
std::vector<float> rings_q_recipA;
// The share of the pooled spots that the rings hold OVER the baseline - what the contaminant
// contributes, not what happens to lie in a ring band. 0 where there is nothing.
float spot_fraction = 0.0f;
// The same, split between the rings on a hexagonal-ice position (ICE_RING_RES_A, within the
// measurement's half-width) and the rest - another phase, a salt or microcrystals. Report only.
float ice_spot_fraction = 0.0f;
float non_ice_spot_fraction = 0.0f;
size_t ice_ring_count = 0;
// How far the rings can be told apart, in A. A powder's rings crowd together as q grows, and past
// the point where ring bins are the majority of bins the baseline is itself made of rings and a
// ring no longer measures as one. Nothing finer than this can be separated from the crystal, so
// it is the resolution an indexing pass can be asked to trust on such a pattern. Absent where the
// rings stay separable over the whole range, which is the ordinary case.
std::optional<float> resolved_to_d_A;
};
PowderRings MeasurePowderRings(const std::vector<float> &spot_q_recipA, float half_width_q_recipA);
// The resolution (A) that this fraction of a pooled spot list lies COARSER than. A first pass that
// stops here is given that share of the spots, whatever the detector reaches and whatever the
// crystal's fall-off looks like - so it scales to any dataset without a resolution written into the
// program. Returns nothing on a pool too small to have quantiles.
std::optional<float> SpotResolutionQuantile(const std::vector<float> &spot_q_recipA, float fraction);
// Keep the strongest `count` spots. With deprioritise_ice, spots on the hexagonal ice bands are ordered
// last and so are the first to go; pass false where the run has no measurable ice, in which case the
// flag marks ordinary reflections and ordering on it would discard good data.
void FilterSpotsByCount(std::vector<SpotToSave> &input, int64_t count, bool deprioritise_ice);
void FilterSpuriousHighResolutionSpots(std::vector<SpotToSave> &spots, float threshold);
// How far the SCALED AND MERGED data from a run of images like this one are expected to reach, in A,
// from the spots of this one image alone. Nothing else - no lattice, no integration, no merge.
//
// The spots' intensity-weighted resolution distribution falls off at the crystal's own rate, and a
// quantile taken near the middle of that fall-off measures that rate. The far end of the distribution
// does not: the highest-resolution spot found says where DETECTION stops, which moves with the
// exposure and with how many reflections the unit cell puts on the frame. Merging then reaches a fixed
// factor further in 1/d than the quantile, because averaging many observations goes on measuring
// intensities that one image cannot detect.
//
// The answer is deliberately NOT limited to what this detector records. The quantile sits in the
// middle of the fall-off, well inside the recorded range, so it goes on measuring the crystal when
// the detector stops before the diffraction does - which is the case the number is most wanted for.
// Returns nothing when the image has too few spots to have a fall-off at all.
std::optional<float> GetResolution(const std::vector<SpotToSave> &spots);
void SpotAnalyze(const DiffractionExperiment &experiment,
const SpotFindingSettings &settings,
const std::vector<DiffractionSpot> &spots,
DataMessage &message);