diff --git a/image_analysis/indexing/AnalyzeIndexing.cpp b/image_analysis/indexing/AnalyzeIndexing.cpp index 17497199..6400b6c9 100644 --- a/image_analysis/indexing/AnalyzeIndexing.cpp +++ b/image_analysis/indexing/AnalyzeIndexing.cpp @@ -369,7 +369,9 @@ bool AnalyzeIndexing(DataMessage &message, message.spots[i].indexed = indexed_spots[i]; message.spots[i].lattice = indexed_spots[i] ? 0 : -1; } - message.profile_radius = FitProfileRadius(message.spots); + message.profile_radius = FitProfileRadius(message.spots, + experiment.GetBandwidthFWHM().value_or(0.0f) / 2.3548f, + experiment.GetWavelength_A()); message.spot_count_indexed = nspots_indexed; message.indexing_lattice = latt; message.indexing_unit_cell = latt.GetUnitCell(); diff --git a/image_analysis/indexing/FitProfileRadius.cpp b/image_analysis/indexing/FitProfileRadius.cpp index 2c3ca8c2..bd63a708 100644 --- a/image_analysis/indexing/FitProfileRadius.cpp +++ b/image_analysis/indexing/FitProfileRadius.cpp @@ -31,21 +31,31 @@ std::optional FitProfileRadius_MAD(const std::vector& xs) { return 1.4826f * med; } -std::optional FitProfileRadius(const std::vector& spots) { - float sum_squares = 0.0f; +std::optional FitProfileRadius(const std::vector& 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) { - sum_squares += s.dist_ewald_sphere * s.dist_ewald_sphere; - count++; + if (!s.indexed) + continue; + sum_squares += static_cast(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(s.d_A) * s.d_A); + sum_bw_var += sigma_bw * sigma_bw; } + count++; } if (count == 0) return std::nullopt; - auto std_dev = std::sqrt(sum_squares / count); - return std_dev; + const double variance = std::max(0.0, (sum_squares - sum_bw_var) / count); + return static_cast(std::sqrt(variance)); } diff --git a/image_analysis/indexing/FitProfileRadius.h b/image_analysis/indexing/FitProfileRadius.h index 7348ab49..c63f7288 100644 --- a/image_analysis/indexing/FitProfileRadius.h +++ b/image_analysis/indexing/FitProfileRadius.h @@ -9,5 +9,11 @@ #include "../../common/SpotToSave.h" std::optional FitProfileRadius_MAD(const std::vector& spots); -std::optional FitProfileRadius(const std::vector& spots); + +// Intrinsic excitation-error (mosaicity+divergence) width from the indexed-spot spread. When a finite +// energy bandwidth is given, its radial smear (bandwidth_sigma*lambda/(2 d^2)) is deconvolved out, so +// the result is the intrinsic width and bandwidth is not double-counted by prediction (which re-adds +// it). bandwidth_sigma = 0 reproduces the plain RMS (monochromatic / rotation). +std::optional FitProfileRadius(const std::vector& spots, + float bandwidth_sigma = 0.0f, float wavelength_A = 0.0f);