diff --git a/image_analysis/scale_merge/SearchSpaceGroup.cpp b/image_analysis/scale_merge/SearchSpaceGroup.cpp index 8f2caf11..bcc779a3 100644 --- a/image_analysis/scale_merge/SearchSpaceGroup.cpp +++ b/image_analysis/scale_merge/SearchSpaceGroup.cpp @@ -465,14 +465,14 @@ SearchSpaceGroupResult SearchSpaceGroup( bool consistent = c.pg->rotations.empty() || !std::isfinite(c.chi2) || !std::isfinite(chi2_ref) || c.chi2 <= chi2_ref * opt.max_merge_chi2_ratio; - // Rescue a genuine high-symmetry merge whose chi^2 lands just past the ratio bound because its - // data are imperfectly scaled (see max_merge_chi2_rescue). The systematic-error test tells it - // apart from a twin: promote only if the extra intensity-proportional error b, relative to the - // largest confirmed subgroup (by rotation-set inclusion), stayed within max_systematic_b_ratio - - // a genuine step barely moves it, a twin's balloons. - if (!consistent && std::isfinite(c.chi2) && std::isfinite(chi2_ref) - && c.chi2 <= chi2_ref * opt.max_merge_chi2_rescue) { - double parent_b = -1.0; + // Systematic-error test vs the largest confirmed subgroup (by rotation-set inclusion): merging + // under a genuine operator gains multiplicity without intensity-proportional disagreement, so the + // merge error model's b barely moves; a merohedral twin forces non-equivalent reflections together + // and b balloons. It both RESCUES a genuine step whose chi^2 drifts just past the ratio bound + // (imperfectly scaled data) and VETOES a twin whose chi^2 now looks self-consistent but whose b + // balloons - the chi^2 ratio alone no longer separates them. + double parent_b = -1.0; + if (!c.pg->rotations.empty()) { int parent_order = 0; for (const auto& s : pg_cands) if (s.order < c.order && s.order > parent_order @@ -481,9 +481,16 @@ SearchSpaceGroupResult SearchSpaceGroup( parent_order = s.order; parent_b = s.b_extra; } - if (parent_b > 1e-4 && c.b_extra <= parent_b * opt.max_systematic_b_ratio) - consistent = true; } + if (!consistent && std::isfinite(c.chi2) && std::isfinite(chi2_ref) + && c.chi2 <= chi2_ref * opt.max_merge_chi2_rescue + && parent_b > 1e-4 && c.b_extra <= parent_b * opt.max_systematic_b_ratio) + consistent = true; + // Veto a chi^2-passing promotion whose b clearly ballooned (above the largest genuine step, below a + // twin); a genuine but imperfectly-scaled high-symmetry merge stays under the bound and is untouched. + if (consistent && parent_b > 1e-4 && c.b_extra > parent_b * opt.max_systematic_b_veto) + consistent = false; + if (!consistent) continue; if (c.order > best_pg_order || (c.order == best_pg_order && c.min_class_cc > best_pg_min_cc)) { diff --git a/image_analysis/scale_merge/SearchSpaceGroup.h b/image_analysis/scale_merge/SearchSpaceGroup.h index 076f6e87..bb32dff6 100644 --- a/image_analysis/scale_merge/SearchSpaceGroup.h +++ b/image_analysis/scale_merge/SearchSpaceGroup.h @@ -106,6 +106,17 @@ struct SearchSpaceGroupOptions { double max_merge_chi2_rescue = 2.30; double max_systematic_b_ratio = 1.90; + // Veto bound. The systematic-b test above is otherwise only a rescue - it can promote a chi^2-borderline + // genuine step but never demote a chi^2-passing one. A merohedral twin whose within-orbit scatter looks + // self-consistent (chi^2 ratio below max_merge_chi2_ratio) therefore slips through on chi^2 alone even + // though merging its non-equivalent reflections balloons b. So a chi^2-passing high-symmetry promotion + // whose b, relative to the largest confirmed subgroup, exceeds this bound is vetoed and kept in its true + // lower symmetry. Calibrated on the rotation-test battery, where the largest genuine step (a P422 + // tetragonal) sits at b-ratio ~1.8 and a merohedral R3->R32 twin at ~2.6 - an empty gap - so 2.0 keeps + // every genuine high-symmetry merge (they are never demoted) while catching the twin. The veto only ever + // keeps a clearly-ballooned promotion down, never promotes. + double max_systematic_b_veto = 2.0; + // --- Stage B: space group (screw axes / centering) --- bool determine_space_group = true; // false: stop at the symmorphic representative