--min-image-cc is consumed only by the stills merge (MergeOnTheFly); RotationScaleMerge never reads it. On rotation data it was accepted and then silently did nothing, so a run that looked filtered was not. It now says so. FitProfileRadius_MAD had zero callers - a robust twin sitting uncalled next to the non-robust estimator that is actually used is a trap, so it goes. Neither changes any result: verified on a rotation dataset (indexing rate, cell, space group and merge statistics identical, warning emitted). Context for anyone tempted to wire that estimator in: I tested exactly that today and it is NOT justified. The population it would clip is truncated by construction - a spot is only marked `indexed` when its fractional-Miller norm is inside the indexing tolerance - and is measurably shorter-tailed than Gaussian (kurtosis 2.85). Across four serial-stills datasets a MAD-clipped variant only narrowed the prediction window (-17% integrated reflections everywhere), which was neutral on strong data and destroyed real signal on weak data (one set lost completeness 96.0 -> 93.9%), with R-free 0.3753 -> 0.3767. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
36 lines
1.6 KiB
C++
36 lines
1.6 KiB
C++
// SPDX-FileCopyrightText: 2025 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include "FitProfileRadius.h"
|
|
#include <algorithm> // std::nth_element
|
|
#include <cmath> // std::fabs
|
|
|
|
std::optional<float> FitProfileRadius(const std::vector<SpotToSave>& spots,
|
|
float bandwidth_sigma, float wavelength_A) {
|
|
double sum_squares = 0.0; // measured excitation-error variance (sum dist_ewald^2)
|
|
double sum_bw_var = 0.0; // energy-bandwidth contribution to subtract out
|
|
int count = 0;
|
|
|
|
for (const auto &s: spots) {
|
|
if (!s.indexed)
|
|
continue;
|
|
sum_squares += static_cast<double>(s.dist_ewald_sphere) * s.dist_ewald_sphere;
|
|
// The energy bandwidth smears each reflection radially by sigma_bw = bandwidth_sigma*|recip_z|
|
|
// = bandwidth_sigma*lambda/(2 d^2) (the same term prediction re-adds per reflection, ~1/d^2 so
|
|
// largest at high resolution). Deconvolve it from the measured spread so the profile radius is
|
|
// the *intrinsic* mosaicity+divergence width and bandwidth is not double-counted at prediction.
|
|
if (bandwidth_sigma > 0.0f && s.d_A > 0.0f) {
|
|
const double sigma_bw = bandwidth_sigma * wavelength_A / (2.0 * static_cast<double>(s.d_A) * s.d_A);
|
|
sum_bw_var += sigma_bw * sigma_bw;
|
|
}
|
|
count++;
|
|
}
|
|
|
|
if (count == 0)
|
|
return std::nullopt;
|
|
|
|
const double variance = std::max(0.0, (sum_squares - sum_bw_var) / count);
|
|
return static_cast<float>(std::sqrt(variance));
|
|
}
|
|
|