rugnux: the external reference MTZ is never a scale anchor (stills + rotation)
An external reference MTZ is a poor per-image scale reference: it is a different crystal/dataset, so scaling against it injects cross-dataset systematics rather than removing per-image scale error (measured: reference-scaled R-free 0.410 vs self-scaled 0.388 on OCP; 0.378 vs 0.350 on LOV). Its real value is fixing the cell/space group, breaking the merohedral indexing ambiguity, reporting CCref and providing the R-free test set - none of which need it to be a scale anchor. Make that consistent across both workflows: - Stills: the per-image pass (ScaleImage) now uses the reference ONLY to break the indexing ambiguity (once, for good), not to fit G. Scaling self-references at the post-measurement merge for every run, whether or not a reference is given (Rugnux + rugnux_cli --scale). - Rotation: ScaleImage no longer scales against the reference either (it was dead code - RotationScaleMerge recomputes the per-frame scale, so the reference never actually moved the result). Rotation resolves the ambiguity globally (ChooseReindex / ReferenceIntensityCC) and self-scales in RotationScaleMerge. - A reference-as-scale mode, if ever wanted, would be a dedicated later step, not this per-image pass. Validated: OCP CCref 56.5%->64.6%, R-free 0.400->0.393 (honest 2.36 A cutoff instead of a reference- propped 1.5 A); LOV CCref 88.3%->90.1%, R-free 0.336->0.331 with more reflections; rotation lyso_ref with the reference gives ISa 17.8 == de-novo 15.6 (same 15975 reflections, CC1/2 99.8%) - the reference demonstrably does not touch rotation scaling, only cell/SG + ambiguity + CCref. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<float>(scaling_end_time - scaling_start_time).count();
|
||||
}
|
||||
|
||||
|
||||
@@ -39,8 +39,6 @@ class ScaleOnTheFly {
|
||||
|
||||
bool Accept(const Reflection &r) const;
|
||||
[[nodiscard]] std::pair<double, size_t> CalculateGlobalCC(const std::vector<Reflection> &reflections) const;
|
||||
// Reindex this image's reflections into the ambiguity hand that best correlates with the reference.
|
||||
void ResolveIndexingAmbiguity(std::vector<Reflection> &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<MergedReflection> &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<Reflection> &reflections) const;
|
||||
|
||||
void Scale(IntegrationOutcome &integration_outcome) const;
|
||||
void Scale(std::vector<IntegrationOutcome> &integration_outcome, size_t nthreads = 0) const;
|
||||
};
|
||||
|
||||
+8
-6
@@ -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);
|
||||
|
||||
+6
-11
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user