Files
Jungfraujoch/image_analysis/spot_finding/SpotUtils.h
T
leonarski_fandClaude Opus 5.5 e9da892790
Build Packages / Create release (push) Successful in 15s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m56s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m39s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m23s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m48s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m22s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m11s
Build Packages / build:windows:nocuda (push) Successful in 18m24s
Build Packages / build:windows:cuda (push) Successful in 20m42s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 24m38s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 19m11s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 19m24s
Build Packages / Generate python client (push) Successful in 45s
Build Packages / build:rugnux:windows (push) Successful in 11m0s
Build Packages / Build documentation (push) Successful in 1m22s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m36s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m52s
Build Packages / build:rpm (rocky8) (push) Successful in 18m15s
Build Packages / build:rpm (rocky9) (push) Successful in 18m10s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 14m2s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 12m29s
Build Packages / Unit tests (push) Successful in 1h14m50s
Rugnux report: low warning thresholds, worded as prompts to check; ice apart from powder
Owner decision: a warning is a prompt to check and must catch the real cases (9min) at the
cost of some spurious ones. The physically motivated corrections stay (<|L|> outside its
physical range is not twinning; a single sweep's indexing choice; NO_LATTICE on rotation;
the no-crystal report); thresholds raised only to cut noise come back down:

- SUPERCELL_POSSIBLE warns wherever the class measures and rocks (as before rc173's audit
  fix), worded as "check the cell", naming weak ordered intensity of a correct cell and spots
  of further lattice domains as the other readings. 9min (rock 4.2%) warns again.
- LATTICE_TRANSLATION warns on every admitted vector (>=75% of the origin); below 90% the
  wording names a very strong pseudo-translation as the other reading.
- PSEUDO_TRANSLATION warns on every detection; below a 20% peak it is worded as weak, check.
- SWEEP_GAPS warns where the degraded ranges cover at least 1% of the sweep (the 4 sets of 77
  below that had 1-2 frames, 0.4-0.6% of the sweep).
- Powder rings are split between hexagonal-ice positions and the rest (MeasurePowderRings,
  report-only fields); ICE_RINGS and POWDER_RINGS warn separately from 5% of the spots, and
  ICE_RINGS also where the merge's ice gate found ice.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D1G8gJVAy6gp1K5Dz3NE5C
2026-09-26 14:02:05 +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);