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
50 lines
2.5 KiB
C++
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);
|
|
|
|
|