From 7d6c201e9592514af72dd016d70ce6e55ebd4fb8 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Sun, 4 Oct 2026 13:24:09 +0200 Subject: [PATCH] Rotation merge: partial corr carries the recomputed partiality, not the predictor's Every partial's corr is formed at integration as image_scale_corr = LP*QE*flight / partiality with the predictor's per-frame partiality. Ingest then recomputes every partiality from the frame-order-smoothed mosaicity (and the one exact-Bragg angle per rocking event), but corr_ingested kept the predictor's 1/p. Where partials are scaled every fitted frame's corr is rebuilt from the recomputed partiality, so that path only saw it in the first reference. Where they are not - fewer than 50 rocking events per frame: small molecules, and long-wavelength protein sweeps - corr is what the 3D combine divides each partial by, and the full became a weighted mean of I/p_pred while the event's recomputed fractions summed to 1.00: the combined fulls tracked 1/sum(p_pred) (r = -0.96) and scattered 13% rms about the plain sum of the same partials. corr_ingested is now rescaled by p_pred/p_recomputed once at ingest. Targeted battery against rc174-cand: every set on the partial-scaling path is bit-identical (5reo, 9qw8, lyso_x06da_ref, thau_x10sa_0p1deg, lyso_x06da_atten_wedge, myob_x06da_split, cytc_x06da_1, insu_I_x06da_5keV/6keV). Small molecules, SHELXL R1 against the COD model: aspirin 20 keV 0.0515 -> 0.0432 (ISa 10.3 -> 30.9), aspirin 25 keV 0.0451 -> 0.0396, HEPES 0.0452 -> 0.0420, citric acid 0.0747 -> 0.0726, dnba 0.0472 -> 0.0273, metformin 0.0405 -> 0.0326, nidppe 0.0498 -> 0.0426, lalanine 0.081 -> 0.057, cytidine 0.077 -> 0.071. Long-wavelength proteins on the fulls-alone path: lyso_x06da_5keV ISa 17.9 -> 27.5, thau_bl1a_3p8keV 21.7 -> 28.4, thau_bl1a_4p6keV 26.4 -> 33.5. One regression: lcystine_x10sa_25keV (a polycrystalline aggregate) now refuses 622 and reports P31 instead of P6122. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01K5K8jvPPbmCrbqnWkddTuB --- .../scale_merge/RotationScaleMerge.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/image_analysis/scale_merge/RotationScaleMerge.cpp b/image_analysis/scale_merge/RotationScaleMerge.cpp index b1371ee4e..177adc897 100644 --- a/image_analysis/scale_merge/RotationScaleMerge.cpp +++ b/image_analysis/scale_merge/RotationScaleMerge.cpp @@ -507,6 +507,9 @@ void RotationScaleMerge::Ingest() { logger.Info("RotationScaleMerge: ingested {} partial observations from {} frames ({} distinct hkl)", n_partials_obs, n_frames, rawrun_start.size()); + // The partiality the predictor gave each partial, which its corr was formed with (image_scale_corr + // divides by it); the recompute below replaces it. + const std::vector partiality_predicted(ingest_partiality.get(), ingest_partiality.get() + n_partials_obs); SmoothMosaicityAndPartiality(); if (!resident_ingest) { ParallelChunks(static_cast(partials.size()), ThreadsForWork(partials.size(), nthreads), @@ -529,6 +532,20 @@ void RotationScaleMerge::Ingest() { for (int i = lo; i < hi; ++i) corr_ingested[i] = partials[i].corr; }); } + // corr carries 1 / partiality, and it has to be the partiality everything downstream uses - the + // recomputed one. Where the partials are scaled, every fitted frame's corr is rebuilt from it + // anyway; where they are not (too few rocking events per frame, small molecules), corr is what + // the combine divides each partial by, and with the predictor's per-frame partiality left in it + // the parts of one rocking curve no longer add up to the reflection: measured on a 0.2 deg + // small-molecule sweep, the combined fulls scattered 13% rms about the plain sum of the same + // partials, and ISa was 10 where it is 31 with the recomputed partiality. + ParallelChunks(n_partials_obs, ThreadsForWork(n_partials_obs, nthreads), [&](int lo, int hi) { + for (int i = lo; i < hi; ++i) { + const float p_old = partiality_predicted[i], p_new = ingest_partiality[i]; + if (p_old > 0.0f && p_new > 0.0f) + corr_ingested[i] *= p_old / p_new; + } + }); #ifdef JFJOCH_USE_CUDA // Feed the device (probed at the top of Ingest). On the resident path the layout and every