From 9527fa6e2c9698ec48e13769bc6ff3f6ca05bb94 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Tue, 14 Jul 2026 12:05:08 +0200 Subject: [PATCH] rugnux: scale self-referenced stills in a single pass (fix weak-data collapse) The stills self-scaling loop rebuilt the merge reference FROM the just-scaled reflections every iteration, so on weak data each pass re-fit its own noise and the merged CC1/2 collapsed (PfluDING combine: 0.3% at the default 3 iters). It is provably pointless too: ScaleOnTheFly::Scale re-solves the per-image G from raw I and never reads the prior correction, so against a fixed reference passes 2..N are bit-identical - the single G is the exact one-pass solution and iteration only ever does the harmful reference rebuild (N=1 is best on every dataset, including strong lyso 82% vs 76% at N=3; leave-one-out and external anchors were tested and do not beat it). Do one self-reference pass; rotation (RotationScaleMerge) and the external-reference branch are untouched. Validated: PfluDING-combined CC1/2 0.3% -> 42.0% by default (pinned 2.5 A); strong data unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) --- rugnux/Rugnux.cpp | 12 ++++++------ rugnux/rugnux_cli.cpp | 7 ++++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 5ceb6a80..242b60ee 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -594,12 +594,12 @@ ProcessResult Rugnux::Run(RugnuxObserver *observer) { // running merge with ScaleOnTheFly (fixed partiality), then merge directly. With an external // reference each image is already scaled against it during the per-image pass, so skip. if (config_.reference_data.empty()) { - for (int i = 0; i < config_.scaling_iter; i++) { - phase("Scaling images (" + label + ", iteration " + std::to_string(i + 1) + "/" - + std::to_string(config_.scaling_iter) + ")"); - auto merge_result = MergeAll(experiment_, indexer->GetIntegrationOutcome(), false); - indexer->ScaleAllImages(merge_result); - } + // One pass: the per-image scale G is the exact one-pass solution, so iterating only + // rebuilds the reference from the freshly-scaled (noisy) data - degrading weak stills + // instead of converging (measured CC1/2 collapse at the default 3 iters on weak data). + phase("Scaling images (" + label + ")"); + auto merge_result = MergeAll(experiment_, indexer->GetIntegrationOutcome(), false); + indexer->ScaleAllImages(merge_result); } const std::vector &merge_input = indexer->GetIntegrationOutcome(); diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index fa417766..78cde433 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -1035,7 +1035,12 @@ int main(int argc, char **argv) { merged_statistics = std::move(r.statistics); error_model_isa = r.isa; } else { - for (int i = 0; i < scaling_iter; i++) { + // The per-image scale G is the exact one-pass solution (Scale re-solves from raw I and never + // reads the prior correction), so with a self-rebuilt reference iterating only re-fits the + // freshly-scaled noise and degrades weak stills (measured CC1/2 collapse at the default 3 + // iters). Scale the self-reference merge once; a fixed external reference keeps scaling_iter. + const int n_self = reference_data.empty() ? 1 : scaling_iter; + for (int i = 0; i < n_self; i++) { if (reference_data.empty()) ScaleOnTheFly(experiment, MergeAll(experiment, reflections)).Scale(reflections, nthreads); else