diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b96378ee..01b8c789 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## 1.0.0 ### Unreleased +* rugnux: the detector-frame modulation correction is fitted on a grid spanning the detector rather than the reflections that happen to be present, so whether it is applied no longer depends on how far integration reached. On a crystal where it was being refused, merged R_meas improves by 4 percentage points. * rugnux: the beam-stop pre-scan reads and accumulates its frames on several threads instead of one. On a 16M-pixel rotation dataset it drops from 7.9 s to 4.8 s; the shadow it finds is unchanged. ### 1.0.0-rc.161 diff --git a/image_analysis/scale_merge/RotationScaleMerge.cpp b/image_analysis/scale_merge/RotationScaleMerge.cpp index 2ce92ebc..2687ec2f 100644 --- a/image_analysis/scale_merge/RotationScaleMerge.cpp +++ b/image_analysis/scale_merge/RotationScaleMerge.cpp @@ -1820,26 +1820,30 @@ void RotationScaleMerge::RefineModulation(int n_iter, int n_groups) { // the detector + beam (not the rotation), the same correction concept transfers to stills. if (fulls.empty()) return; - float pxmin = std::numeric_limits::infinity(), pxmax = -pxmin, pymin = pxmin, pymax = -pxmin; - for (const Obs &o : fulls) { - if (!std::isfinite(o.px) || !std::isfinite(o.py)) continue; - pxmin = std::min(pxmin, o.px); pxmax = std::max(pxmax, o.px); - pymin = std::min(pymin, o.py); pymax = std::max(pymax, o.py); - } - if (!(pxmax > pxmin) || !(pymax > pymin)) + // The grid spans the DETECTOR, not the reflections that happen to be present. Taking its extent from + // the fulls made the cell size depend on how far INTEGRATION reached - reflections this surface never + // fits, since it is fitted on the merged (in-resolution-range) ones alone. On a crystal integrated to + // the detector corner but merged well short of it that is the difference between a grid the in-range + // reflections over-determine and one they over-fit, and the cross-validation gate then flips between + // engaging the surface and refusing it: 28.4% merged R_meas with it against 32.8% without, moving + // non-monotonically as the integration limit was varied. Cells outside the data cost nothing - with no + // observations in them their factor stays at 1. + const float pxmax = static_cast(x.GetXPixelsNumConv()); + const float pymax = static_cast(x.GetYPixelsNumConv()); + if (!(pxmax > 0.0f) || !(pymax > 0.0f)) return; // 24 rather than 16: held out on unseen frames, 16 -> 24 is worth 0.1-0.2 pp of merged R_meas, // while 24 -> 48 buys 0.02 and 64 is worse. The cross-validation gate below refuses the surface // outright where 24 is too fine for the data, so the finer grid needs no separate guard. constexpr int NB = 24; - const float sx = NB / (pxmax - pxmin), sy = NB / (pymax - pymin); + const float sx = NB / pxmax, sy = NB / pymax; std::vector cell(fulls.size(), -1); ParallelChunks(static_cast(fulls.size()), ThreadsForWork(fulls.size(), nthreads), [&](int lo, int hi) { for (int i = lo; i < hi; ++i) { const Obs &o = fulls[i]; if (!std::isfinite(o.px) || !std::isfinite(o.py)) continue; - const int ix = std::clamp(static_cast((o.px - pxmin) * sx), 0, NB - 1); - const int iy = std::clamp(static_cast((o.py - pymin) * sy), 0, NB - 1); + const int ix = std::clamp(static_cast(o.px * sx), 0, NB - 1); + const int iy = std::clamp(static_cast(o.py * sy), 0, NB - 1); cell[i] = ix * NB + iy; } });