diff --git a/image_analysis/pixel_refinement/PixelRefine.cpp b/image_analysis/pixel_refinement/PixelRefine.cpp index c398b9d3..bef4392b 100644 --- a/image_analysis/pixel_refinement/PixelRefine.cpp +++ b/image_analysis/pixel_refinement/PixelRefine.cpp @@ -920,8 +920,11 @@ void PixelRefine::Run(const T *image, BuildParameterBlocks(data, beam, dist_mm, detector_rot, latt_vec0, latt_vec1, latt_vec2); - // Anchor for orientation regularization = the spot-centroid orientation we - // started from (captured before any pixel-level refinement moved it). + // Anchor for orientation regularization = the orientation the LSQ starts from + // (captured before the predict<->refine iterations move it). When the global + // sweep ran first this is the swept orientation, not the original spot-centroid + // one - which is intended: the regularizer keeps the LSQ near its own starting + // point, it is not meant to pull a deliberate sweep back. if (iter == 0) for (int i = 0; i < 3; ++i) orient_prior[i] = latt_vec0[i]; diff --git a/image_analysis/scale_merge/Merge.cpp b/image_analysis/scale_merge/Merge.cpp index 3acf2ba4..cfb829e9 100644 --- a/image_analysis/scale_merge/Merge.cpp +++ b/image_analysis/scale_merge/Merge.cpp @@ -103,6 +103,14 @@ float MergeOnTheFly::CorrectedSigma(float I_corr, float sigma_corr, uint64_t hkl } void MergeOnTheFly::RefineErrorModel(const std::vector &outcomes) { + // Reset to identity up front: every early return below then leaves the model + // inactive (CorrectedSigma returns sigma unchanged) rather than keeping a stale + // a/b from a previous call alongside a freshly-cleared mean map. + error_model_active = false; + error_model_a = 1.0; + error_model_b = 0.0; + error_model_mean_I.clear(); + // --- 1. Collect accepted, scaled observations grouped by symmetry-equivalent hkl, // applying exactly the filters AddImage uses. --- struct Obs { float I, sigma; }; @@ -134,7 +142,6 @@ void MergeOnTheFly::RefineErrorModel(const std::vector &outc // (b*I)^2 term uses the reflection mean, so the mean (not I_i) is the abscissa. --- struct Sample { double s2, I2, dev2; }; std::vector samples; - error_model_mean_I.clear(); for (const auto &[key, obs]: groups) { if (obs.size() < 2) @@ -176,7 +183,9 @@ void MergeOnTheFly::RefineErrorModel(const std::vector &outc return v[v.size() / 2]; }; - double Ass = 0, AsI = 0, AII = 0, Bs = 0, BI = 0; + // Per-intensity-bin medians of (sigma^2, ^2, dev2). + std::vector bs2, bI2, bd2; + bs2.reserve(n_bins); bI2.reserve(n_bins); bd2.reserve(n_bins); const size_t per = samples.size() / n_bins; for (int bin = 0; bin < n_bins; ++bin) { const size_t lo = bin * per; @@ -188,8 +197,22 @@ void MergeOnTheFly::RefineErrorModel(const std::vector &outc vI2.push_back(samples[i].I2); vd2.push_back(samples[i].dev2); } - const double s2 = median(vs2), I2 = median(vI2), d2 = median(vd2); - const double wgt = 1.0 / std::max(d2 * d2, 1e-30); + bs2.push_back(median(vs2)); + bI2.push_back(median(vI2)); + bd2.push_back(median(vd2)); + } + + // Relative-weighted (1/dev2^2) least squares for (a, b^2). Floor the weight's dev2 at a + // small fraction of the typical bin dev2: an absolute floor (1e-30) does not stop a + // near-zero-scatter bin from acquiring a runaway weight and hijacking the fit, so the + // floor must scale with the data. The regression target keeps the unfloored dev2. + std::vector bd2_sorted = bd2; + const double dev2_floor = std::max(1e-30, 1e-3 * median(bd2_sorted)); + double Ass = 0, AsI = 0, AII = 0, Bs = 0, BI = 0; + for (int bin = 0; bin < n_bins; ++bin) { + const double s2 = bs2[bin], I2 = bI2[bin], d2 = bd2[bin]; + const double d2w = std::max(d2, dev2_floor); + const double wgt = 1.0 / (d2w * d2w); Ass += wgt * s2 * s2; AsI += wgt * s2 * I2; AII += wgt * I2 * I2; @@ -197,8 +220,10 @@ void MergeOnTheFly::RefineErrorModel(const std::vector &outc BI += wgt * I2 * d2; } + // Reject a near-collinear (ill-conditioned) system *relatively*: det lies in + // [0, Ass*AII] by Cauchy-Schwarz, so compare against that scale rather than 1e-30. const double det = Ass * AII - AsI * AsI; - if (std::fabs(det) < 1e-30) + if (!(det > 1e-10 * Ass * AII)) return; const double a = std::clamp((Bs * AII - BI * AsI) / det, 0.25, 100.0); const double b2 = std::max((Ass * BI - AsI * Bs) / det, 0.0);