scaling: robust (Cauchy) loss on the per-image scale fit

The per-image G/B/mosaicity fit used a plain L2 loss, so a few outlier
reflections occasionally dragged a frame into a bad optimum - a stochastic
(~15% of runs) per-frame mis-scaling that elevated R-meas and collapsed CC1/2
at low symmetry (the image-level CC1/2 half-split makes the damage look patchy
across shells, while the data is genuinely noisier). A Cauchy loss (3 sigma)
soft-downweights those outliers without a hard cut: MyoB 0/10 catastrophic
(was ~2/10), R-meas stable, and ISa improves on every test crystal
(EP0210 9.2->12.4, MyoB 12.5->14.6, lysoC 10.4->11.2, cytC 11.5->12.6),
most on low symmetry.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-30 20:00:42 +02:00
co-authored by Claude Opus 4.8
parent e45a1577d6
commit 8d18c635ff
+9 -3
View File
@@ -8,6 +8,12 @@
#include <ceres/rotation.h>
namespace {
// Robust loss scale (in sigma units) for the per-image scale fit: a few outlier reflections
// (zingers, overlaps, a mis-predicted spot) must not drag a frame's G/B into a bad optimum -
// that is the stochastic per-frame mis-scaling that elevates R-meas and collapses CC1/2 at low
// symmetry. Cauchy down-weights residuals beyond ~this many sigma without a hard cut.
constexpr double SCALE_ROBUST_K = 3.0;
double SafeInv(double x, double fallback) {
if (!std::isfinite(x) || x == 0.0)
return fallback;
@@ -248,19 +254,19 @@ void ScaleOnTheFly::Scale(IntegrationOutcome &integration_outcome) const {
case PartialityModel::Fixed: {
auto *cost = new ceres::AutoDiffCostFunction<IntensityFixedResidual, 1, 1, 1>(
new IntensityFixedResidual(r, Itrue, sigma, r.partiality));
problem.AddResidualBlock(cost, nullptr, &result.G, &result.B);
problem.AddResidualBlock(cost, new ceres::CauchyLoss(SCALE_ROBUST_K), &result.G, &result.B);
}
break;
case PartialityModel::Unity: {
auto *cost = new ceres::AutoDiffCostFunction<IntensityFixedResidual, 1, 1, 1>(
new IntensityFixedResidual(r, Itrue, sigma, 1.0));
problem.AddResidualBlock(cost, nullptr, &result.G, &result.B);
problem.AddResidualBlock(cost, new ceres::CauchyLoss(SCALE_ROBUST_K), &result.G, &result.B);
}
break;
case PartialityModel::Rotation: {
auto *cost = new ceres::AutoDiffCostFunction<ScalingRotationResidual, 1, 1, 1, 1, 1>(
new ScalingRotationResidual(r, Itrue, sigma));
problem.AddResidualBlock(cost, nullptr, &result.G, &result.B, &result.mos,
problem.AddResidualBlock(cost, new ceres::CauchyLoss(SCALE_ROBUST_K), &result.G, &result.B, &result.mos,
&result.wedge);
}
break;