StillsPartialityRefine: measure the run width only on shells with signal

Without a resolution limit the outer d*^2 shells of the width measurement lie beyond the diffraction
limit, where the summed intensity is noise or even negative; one such shell made the whole measurement
fail, and 5 of 11 still-battery sets (sacla237, pal86, pal90, lcls242_s, lcls242_t) fell back to the
per-crystal window fit. A shell now enters the fit only where its summed intensity is measured to 10 %
(sum I / sigma >= 10), and at least three shells are needed.

Weighting the fit by the shells' signal instead was tried and rejected: it lets the strong low-resolution
shells set the width alone, and on the MicroMAX pink set the dimple R-free went 0.2442 -> 0.2748.

The matrix subsets, which are cut at their resolution, are unchanged within 0.0010 dimple R-free
(SwissFEL 0.2453, -C/-S 0.2513, mono 0.3188, pink 0.2442, SACLA 243 0.2455, SACLA 237 0.2327,
LCLS 222 0.2335, PAL 86 0.3628).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
This commit is contained in:
leonarski_fandClaude Opus 5.5 committed 2026-10-21 21:23:28 +02:00
1 parent 419db6ba0e
commit b440e3792b
1 file changed
+23 -14
@@ -434,7 +434,7 @@ std::optional<StillsPartialityRefine::RunWidth> StillsPartialityRefine::MeasureR
// intensities of thousands of crystals average out, and fitted as s^2 = gamma0^2 + gamma_e^2 d*^2 +
// (bandwidth_sigma lambda d*^2 / 2)^2.
const auto &indexing = experiment_.GetIndexingSettings();
struct Point { double x, de2, w; };
struct Point { double x, de2, w, var_w; };
std::vector<Point> pts;
for (const auto &o: outcomes) {
if (!o.image_scale_g || !(*o.image_scale_g > 0.0))
@@ -445,11 +445,12 @@ std::optional<StillsPartialityRefine::RunWidth> StillsPartialityRefine::MeasureR
for (const auto &r: o.reflections) {
if (r.on_ice_ring || !AcceptReflection(r, d_min_limit_, d_max_limit_))
continue;
const double w = r.I * r.prescaling_corr * r.qe_corr * r.flight_corr / *o.image_scale_g;
if (!std::isfinite(w) || !std::isfinite(r.dist_ewald) || !(r.d > 0.0f))
const double corr = r.prescaling_corr * r.qe_corr * r.flight_corr / *o.image_scale_g;
const double w = r.I * corr, sigma_w = r.sigma * corr;
if (!std::isfinite(w) || !std::isfinite(sigma_w) || !std::isfinite(r.dist_ewald) || !(r.d > 0.0f))
continue;
pts.push_back({1.0 / (static_cast<double>(r.d) * r.d),
static_cast<double>(r.dist_ewald) * r.dist_ewald, w});
static_cast<double>(r.dist_ewald) * r.dist_ewald, w, sigma_w * sigma_w});
}
}
constexpr size_t N_SHELLS = 8;
@@ -458,24 +459,32 @@ std::optional<StillsPartialityRefine::RunWidth> StillsPartialityRefine::MeasureR
std::sort(pts.begin(), pts.end(), [](const Point &a, const Point &b) { return a.x < b.x; });
Logger logger("StillsPartialityRefine");
std::array<double, N_SHELLS> sx{}, sy{};
// A shell's moment is no better than the summed intensity it is weighted by, so a shell is used only
// where that sum is measured to 10 % (sum I / sigma(sum I) >= 10): the shells beyond the diffraction
// limit, where the sum is noise and can even be negative, say nothing about the width.
constexpr double MIN_SHELL_SIGNAL = 10.0;
std::vector<double> sx, sy;
for (size_t k = 0; k < N_SHELLS; ++k) {
double sw = 0.0, swde2 = 0.0, sde2 = 0.0, sumx = 0.0;
double sw = 0.0, svar = 0.0, swde2 = 0.0, sde2 = 0.0, sumx = 0.0;
const size_t lo = k * pts.size() / N_SHELLS, hi = (k + 1) * pts.size() / N_SHELLS;
for (size_t i = lo; i < hi; ++i) {
sw += pts[i].w;
svar += pts[i].var_w;
swde2 += pts[i].w * pts[i].de2;
sde2 += pts[i].de2;
sumx += pts[i].x;
}
if (!(sw > 0.0))
return std::nullopt;
const double n = static_cast<double>(hi - lo);
sx[k] = sumx / n;
sy[k] = UntruncatedVariance(swde2 / sw, sde2 / n);
logger.Debug("Stills rocking width at d = {:.2f} A: {:.2e} 1/A ({:.3f} deg)", 1.0 / std::sqrt(sx[k]),
std::sqrt(sy[k]), std::sqrt(sy[k] / sx[k]) * kRadToDeg);
if (!(sw >= MIN_SHELL_SIGNAL * std::sqrt(svar)) || !(swde2 > 0.0))
continue;
const double x = sumx / n, y = UntruncatedVariance(swde2 / sw, sde2 / n);
sx.push_back(x);
sy.push_back(y);
logger.Debug("Stills rocking width at d = {:.2f} A: {:.2e} 1/A ({:.3f} deg), I/sigma {:.0f}", 1.0 / std::sqrt(x),
std::sqrt(y), std::sqrt(y / x) * kRadToDeg, sw / std::sqrt(svar));
}
if (sx.size() < 3)
return std::nullopt;
// Least squares for s^2 = A + B d*^2 + C d*^4 over the shells. Every term is a variance, so a negative
// one is unphysical: of the fits on each subset of the three terms, the best with none negative is kept.
@@ -489,7 +498,7 @@ std::optional<StillsPartialityRefine::RunWidth> StillsPartialityRefine::MeasureR
const int m = static_cast<int>(terms.size());
Eigen::MatrixXd M = Eigen::MatrixXd::Zero(m, m);
Eigen::VectorXd v = Eigen::VectorXd::Zero(m);
for (size_t k = 0; k < N_SHELLS; ++k)
for (size_t k = 0; k < sx.size(); ++k)
for (int i = 0; i < m; ++i) {
const double fi = std::pow(sx[k], terms[i]);
v(i) += fi * sy[k];
@@ -503,7 +512,7 @@ std::optional<StillsPartialityRefine::RunWidth> StillsPartialityRefine::MeasureR
for (int i = 0; i < m; ++i)
coef[terms[i]] = c(i);
double residual = 0.0;
for (size_t k = 0; k < N_SHELLS; ++k) {
for (size_t k = 0; k < sx.size(); ++k) {
const double r = sy[k] - coef[0] - coef[1] * sx[k] - coef[2] * sx[k] * sx[k];
residual += r * r;
}