// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute // SPDX-License-Identifier: GPL-3.0-only #pragma once #include "../../common/DiffractionSpot.h" void GenerateSpotPlot(DataMessage &msg, const std::vector &spots, float d_min_A); void CountSpots(DataMessage &msg, const DiffractionExperiment& experiment, const std::vector &spots, float d_min_A); void CountSpots(DataMessage &msg, const std::vector &spots, float d_min_A); float CountIceRingControlSpots(const std::vector &spots, float half_width_q_recipA); void MarkIceRings(std::vector &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 &spots, const std::vector &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 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 resolved_to_d_A; }; PowderRings MeasurePowderRings(const std::vector &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 SpotResolutionQuantile(const std::vector &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 &input, int64_t count, bool deprioritise_ice); void FilterSpuriousHighResolutionSpots(std::vector &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 GetResolution(const std::vector &spots); void SpotAnalyze(const DiffractionExperiment &experiment, const SpotFindingSettings &settings, const std::vector &spots, DataMessage &message);