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
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
95 lines
5.6 KiB
C++
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);
|
|
|
|
|