diff --git a/image_analysis/IndexAndRefine.cpp b/image_analysis/IndexAndRefine.cpp index b73a97ab..f2178a4b 100644 --- a/image_analysis/IndexAndRefine.cpp +++ b/image_analysis/IndexAndRefine.cpp @@ -495,15 +495,19 @@ void IndexAndRefine::ScaleImage(DataMessage &msg, IntegrationOutcome& outcome) { if (!scaling_engine) return; - auto scaling_start_time = std::chrono::steady_clock::now(); - scaling_engine->Scale(outcome); - auto scaling_end_time = std::chrono::steady_clock::now(); + // The external reference fixes the cell/space group, breaks the indexing ambiguity and reports CCref, + // but is NEVER a scale anchor: scaling an image against a foreign dataset injects cross-dataset + // systematics and is a worse reference than the data's own merge, so scaling self-references at the + // post-measurement merge for both workflows. Rotation resolves the ambiguity globally and self-scales + // in RotationScaleMerge (ChooseReindex / ReferenceIntensityCC), so there is nothing to do per image. + // Stills resolve the merohedral ambiguity per image here (each crystal indexes in a random hand; pick + // the hand best-correlated with the reference, once and for good). + if (experiment.IsRotationIndexing()) + return; - scale_cc[msg.number] = outcome.image_scale_cc.value_or(NAN); - msg.image_scale_b_factor = outcome.image_scale_b_factor_Ang2; - msg.image_scale_factor = outcome.image_scale_g; - msg.image_scale_mosaicity = outcome.mosaicity_deg; - msg.image_scale_cc = outcome.image_scale_cc; + auto scaling_start_time = std::chrono::steady_clock::now(); + scaling_engine->ResolveIndexingAmbiguity(outcome.reflections); + auto scaling_end_time = std::chrono::steady_clock::now(); msg.image_scale_time_s = std::chrono::duration(scaling_end_time - scaling_start_time).count(); } diff --git a/image_analysis/scale_merge/ScaleOnTheFly.h b/image_analysis/scale_merge/ScaleOnTheFly.h index 6166b4f1..63d9e587 100644 --- a/image_analysis/scale_merge/ScaleOnTheFly.h +++ b/image_analysis/scale_merge/ScaleOnTheFly.h @@ -39,8 +39,6 @@ class ScaleOnTheFly { bool Accept(const Reflection &r) const; [[nodiscard]] std::pair CalculateGlobalCC(const std::vector &reflections) const; - // Reindex this image's reflections into the ambiguity hand that best correlates with the reference. - void ResolveIndexingAmbiguity(std::vector &reflections) const; public: // resolve_ambiguity: when the reference is an external, correctly-handed dataset (not the data's own // running merge), pick per image the reindexing that agrees best with it - serial stills index each @@ -48,6 +46,10 @@ public: ScaleOnTheFly(const DiffractionExperiment &x, const std::vector &ref, bool resolve_ambiguity = false); + // Reindex this image's reflections into the merohedral-ambiguity hand that best correlates with the + // external reference (stills; done once per image at integration time, decoupled from scaling). + void ResolveIndexingAmbiguity(std::vector &reflections) const; + void Scale(IntegrationOutcome &integration_outcome) const; void Scale(std::vector &integration_outcome, size_t nthreads = 0) const; }; diff --git a/rugnux/Rugnux.cpp b/rugnux/Rugnux.cpp index 242b60ee..210ff371 100644 --- a/rugnux/Rugnux.cpp +++ b/rugnux/Rugnux.cpp @@ -591,12 +591,14 @@ ProcessResult Rugnux::Run(RugnuxObserver *observer) { return ScaleMergeResult{std::move(r.merged), std::move(r.statistics)}; } // Stills (rotation goes through RotationScaleMerge above): self-scale each image against the - // 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()) { - // 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). + // running merge with ScaleOnTheFly (fixed partiality), then merge directly. This runs even + // with an external reference: the reference is used only to break the per-image indexing + // ambiguity and to report CCref / inherit the R-free set, NOT as a scale anchor (scaling each + // image against a foreign dataset injects cross-dataset systematics and is a worse reference + // than the data's own merge). One pass: the per-image scale G is the exact one-pass solution, + // so iterating would only rebuild the reference from the freshly-scaled (noisy) data and + // degrade weak stills (measured CC1/2 collapse at the default 3 iters). + { phase("Scaling images (" + label + ")"); auto merge_result = MergeAll(experiment_, indexer->GetIntegrationOutcome(), false); indexer->ScaleAllImages(merge_result); diff --git a/rugnux/rugnux_cli.cpp b/rugnux/rugnux_cli.cpp index 78cde433..371cc39c 100644 --- a/rugnux/rugnux_cli.cpp +++ b/rugnux/rugnux_cli.cpp @@ -1035,17 +1035,12 @@ int main(int argc, char **argv) { merged_statistics = std::move(r.statistics); error_model_isa = r.isa; } else { - // 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 - ScaleOnTheFly(experiment, reference_data).Scale(reflections, nthreads); - } + // Scaling self-references: the reference MTZ (if any) fixes the cell/space group, reports + // CCref and provides the R-free test set, but is NOT a scale anchor - scaling each image + // against a foreign dataset injects cross-dataset systematics and is a worse reference than + // the data's own merge. The per-image scale G is the exact one-pass solution, so one pass + // (iterating a self-rebuilt reference only re-fits the freshly-scaled noise on weak stills). + ScaleOnTheFly(experiment, MergeAll(experiment, reflections)).Scale(reflections, nthreads); MergeOnTheFly merge_engine(experiment); merge_engine.ReferenceCell(experiment.GetUnitCell()); // Fit the (a, b) error model from symmetry-mate scatter before merging, exactly as the full