Files
Jungfraujoch/image_analysis/spot_finding/SpotUtils.h
T
leonarski_fandClaude Opus 5 54adcaafcc Estimate the resolution the merged data will reach, not the furthest spot found
The per-image resolution estimate was the 5th percentile of the spot d spacings -
an extreme order statistic, so it measured where detection stops rather than how
well the crystal diffracts. A large cell puts more reflections past the same
threshold and scored better than a small cell that diffracts further; intensity
was not used at all, so a weak crystal padded with spurious high-resolution
detections ran away; and nothing clamped the answer to what the detector can
deliver. Against the resolution the merged data actually reach it was 42% out in
log-RMS, with 1 of 38 rotation datasets inside 0.2 A.

Take instead the 1/d^2 beyond which 30% of the sum of sqrt(I) over the image's
non-ice spots lies, report 1/(2.25 sqrt of it), clamp at the detector corner, and
take the median over images. A quantile from the middle of the distribution
measures the shape of the falloff - the crystal's own exp(-B/2d^2) - where an
extreme one measures the threshold. sqrt(I) is the Poisson significance of a
summed photon count, so a marginal high-resolution detection cannot carry the
answer and neither can a handful of very strong low-resolution reflections. The
2.25 is the multiplicity gain: merging keeps measuring intensities a fixed factor
in 1/d past the point where a single frame detects them.

Spearman 0.881 -> 0.954, log-RMS 42% -> 8.9%, median error 0.79 -> 0.07 A, and 32
of 38 within 0.2 A. Both constants sit on a broad plateau, the scale is stable
across dataset halves and across resolution ranges, and no second predictor
survives leave-one-out. The residual is around 9%, set by multiplicity, symmetry
and radiation damage - none of which a spot list can see.

The estimate feeds only reporting: the image stream, HDF5, the plots, the scan
result and the preview ring. It sets no cutoff and no search limit, and the
scaling and merging output is byte-identical.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NNnL26LAvruQ9eLUUWvrJ
2026-08-24 18:50:50 +02:00

50 lines
2.5 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);
// 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.
//
// detector_d_min_A is the corner of the detector, which the answer is never allowed to beat; pass 0 to
// leave it unclamped. 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, float detector_d_min_A = 0.0f);
void SpotAnalyze(const DiffractionExperiment &experiment,
const SpotFindingSettings &settings,
const std::vector<DiffractionSpot> &spots,
DataMessage &message);