From 8d18c635ff69eba9ddfd6603a7ffdd43232494ac Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 30 Jun 2026 20:00:42 +0200 Subject: [PATCH] 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) --- image_analysis/scale_merge/ScaleOnTheFly.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/image_analysis/scale_merge/ScaleOnTheFly.cpp b/image_analysis/scale_merge/ScaleOnTheFly.cpp index 3d431bd2..fda12d97 100644 --- a/image_analysis/scale_merge/ScaleOnTheFly.cpp +++ b/image_analysis/scale_merge/ScaleOnTheFly.cpp @@ -8,6 +8,12 @@ #include 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( 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( 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( 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;