Merge branch 'd-5ebi' into rc172-2
Build Packages / Create release (push) Successful in 14s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 7m19s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m35s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m19s
Build Packages / build:viewer-tgz:cuda (push) Successful in 12m8s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 16m4s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m54s
Build Packages / build:windows:nocuda (push) Successful in 17m27s
Build Packages / build:windows:cuda (push) Successful in 19m56s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 15m28s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 23m18s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 17m34s
Build Packages / Generate python client (push) Successful in 1m4s
Build Packages / build:rugnux:windows (push) Successful in 10m42s
Build Packages / Build documentation (push) Successful in 1m15s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 18m21s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m0s
Build Packages / build:rpm (rocky8) (push) Successful in 17m39s
Build Packages / build:rpm (rocky9) (push) Successful in 17m50s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 15m7s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 15m25s
Build Packages / Unit tests (push) Successful in 1h13m6s

This commit is contained in:
leonarski_f committed 2026-09-21 08:50:12 +02:00
commit 881d449d2e
2 files changed
+65 -4

No files matched your search

@@ -1377,6 +1377,8 @@ SearchSpaceGroupResult SearchSpaceGroup(
// reason as h_ratio: to report the adopted candidate's whether or not it decided.
double r_added = std::numeric_limits<double>::quiet_NaN();
double r_over_floor = std::numeric_limits<double>::quiet_NaN();
// The candidate's OWN operators, worst R over best R (see max_operator_r_spread).
double r_spread = std::numeric_limits<double>::quiet_NaN();
// Report-only, for the finalist ledger: the subgroup b the tests below compared
// this candidate against, whether it survived every consistency test, and - when
// it was the highest refusal - why it did not. The selection loop computes all
@@ -1629,6 +1631,23 @@ SearchSpaceGroupResult SearchSpaceGroup(
if (n_new > 0 && pairs_new >= opt.min_pairs_for_h)
r_added = r_new / n_new;
}
// Homogeneity of the candidate's OWN operators: the worst-agreeing over the best-agreeing,
// over all of them, parent and added alike (see max_operator_r_spread).
{
double r_lo = std::numeric_limits<double>::infinity(), r_hi = 0.0;
int n_op = 0;
for (const auto &rot : c.pg->rotations) {
if (rot.rot == gemmi::Op::identity().rot)
continue;
const auto &os = operator_score(rot);
if (os.n_pairs < opt.min_pairs_per_operator || !std::isfinite(os.r_stat)
|| os.r_stat <= 0.0)
continue;
r_lo = std::min(r_lo, os.r_stat); r_hi = std::max(r_hi, os.r_stat); ++n_op;
}
if (n_op >= 2)
c.r_spread = r_hi / r_lo;
}
c.parent_b_used = parent_b;
c.r_added = r_added;
c.r_over_floor = (std::isfinite(r_added) && std::isfinite(result.merge_r_floor)
@@ -1686,16 +1705,28 @@ SearchSpaceGroupResult SearchSpaceGroup(
if (r_refused)
consistent = false;
// Operator-homogeneity test. A necessary condition like H and the contrast, and the one that
// catches what a MEAN over the added operators cannot: a candidate whose added set mixes real
// operators with false ones. The gates above read summaries - mean H, mean R - so a promotion
// that adds two operators at the noise floor together with four several times worse reads a
// mean between them and passes, while the group it names is a mixture that cannot be a
// symmetry of one crystal. Asking instead that the candidate's operators agree with EACH OTHER
// sees the mixture directly (see max_operator_r_spread).
const bool spread_refused = std::isfinite(c.r_spread) && c.r_spread > opt.max_operator_r_spread;
if (spread_refused)
consistent = false;
if (!consistent) {
// Same three sentences the highest refusal gets below, but kept per candidate for the
// ledger: short, because the long form is written once for the group the user is told about.
c.why = h_refused ? "H ratio" : r_refused ? "added-operator R" : "merge chi^2";
c.why = h_refused ? "H ratio" : r_refused ? "added-operator R"
: spread_refused ? "operator spread" : "merge chi^2";
// Record the highest-order refusal so the caller can say WHY it is processing lower.
if (c.order > refused_order && c.pg->representative) {
refused_order = c.order;
refused_pg_hm = c.pg->representative->point_group_hm();
refused_pg_rep = c.pg->representative;
refused_twin_gate = h_refused || r_refused;
refused_twin_gate = h_refused || r_refused || spread_refused;
refused_fraction = h_refused ? std::max(0.0, 0.5 - h_added)
: std::numeric_limits<double>::quiet_NaN();
if (h_refused)
@@ -1727,6 +1758,13 @@ SearchSpaceGroupResult SearchSpaceGroup(
+ FormatDouble(opt.max_operator_r_over_floor, 1)
+ "x) - at full resolution the added operator relates reflections that "
"disagree far beyond measurement error, i.e. it is not a real symmetry";
else if (spread_refused)
refused_why = "its own operators disagree among themselves - the worst-agreeing "
"reads " + FormatDouble(c.r_spread, 1)
+ "x the intensity-weighted R of the best-agreeing one in the SAME "
"group (bound " + FormatDouble(opt.max_operator_r_spread, 1)
+ "x) - so the group pools operators the data support with operators "
"they do not, and cannot be a symmetry of one crystal";
else if (std::isfinite(c.chi2) && std::isfinite(chi2_ref))
refused_why = "merge chi^2 is " + FormatDouble(c.chi2 / chi2_ref, 2)
+ "x the subgroup's (bound " + FormatDouble(opt.max_merge_chi2_ratio, 2) + ")";
@@ -1822,6 +1860,7 @@ SearchSpaceGroupResult SearchSpaceGroup(
/ (result.random_pairing_r - global_best_r);
}
e.r_over_floor = c.r_over_floor;
e.r_spread = c.r_spread;
e.adopted = (c.pg == best_pg);
e.eligible = c.eligible;
e.refused_reason = c.why;
@@ -2589,14 +2628,15 @@ std::string FinalistLedgerToText(const SearchSpaceGroupResult& result) {
os << " Unrelated reflections (shell-matched random pairing) R "
<< FormatDouble(result.random_pairing_r, 4)
<< " - the far end of the contrast scale.\n";
os << "\n PG ord minCC R_add contrast R/best R/floor | H chi2/best b b/par verdict\n"
" <---- calibrated evidence ----> | <--- diagnostic only, NOT evidence --->\n";
os << "\n PG ord minCC R_add contrast spread R/best R/floor | H chi2/best b b/par verdict\n"
" <------- calibrated evidence -------> | <--- diagnostic only, NOT evidence --->\n";
for (const auto& e : result.point_group_ledger) {
os << " " << std::left << std::setw(8) << e.point_group_hm << std::right << std::setw(3)
<< e.order
<< std::setw(8) << LedgerCell(e.min_class_cc, 3)
<< std::setw(8) << LedgerCell(e.r_added, 4)
<< std::setw(9) << LedgerCell(e.r_contrast, 2)
<< std::setw(7) << LedgerCell(e.r_spread, 2)
<< std::setw(7) << LedgerCell(e.r_over_best, 2)
<< std::setw(8) << LedgerCell(e.r_over_floor, 2)
<< " |" << std::setw(6) << LedgerCell(e.h_ratio, 2)
@@ -471,6 +471,26 @@ struct SearchSpaceGroupOptions {
// the floor is a noisier reference than a confirmed operator (it can also be inflated by an
// indexing ambiguity that mixes hands), and the safe direction here is to stay in P1.
double max_operator_r_over_floor = 3.5;
//
// - max_operator_r_spread: the two tests above read a MEAN over the operators a promotion adds,
// so they are blind to an added set that MIXES real operators with false ones - the mean sits
// between the two populations and passes. That is what a pseudo-symmetric metric produces: a
// monoclinic crystal whose lattice is exactly hexagonal (a = c, beta = 120) carries two genuine
// horizontal 2-folds among the six a 622 promotion adds, and their R at the noise floor buys
// the other four a mean well inside the contrast bound. The mixture is invisible to any summary
// of the added set and obvious the moment the candidate's operators are compared with EACH
// OTHER: a point group is the claim that all of its operators are symmetries of one crystal, so
// they must agree with the merge about equally well.
//
// Measured over the 129 rotation datasets of one battery run that adopted a group of order 3 or
// more, as worst-agreeing over best-agreeing operator WITHIN the adopted group: every correct
// adoption sits at 1.0-2.64, the highest being the weak cubic crystal whose near-spindle 3-fold
// is unusually clean (the same crystal that motivated the contrast above), then 2.30 for a
// fine-sliced tetragonal one. The one over-called point group on that run reads 4.87. 3.2 sits
// in that gap with 0.56 of headroom below and 1.67 above - deliberately well clear of the
// 2.0-2.6 spread a genuine group reaches, which is exactly where the old ratio-to-best gate's
// bound of 2.0 sat and why that gate had to go.
double max_operator_r_spread = 3.2;
// Above this reduced chi^2 for the best subgroup (chi2_ref), the merged error model is treated as
// badly miscalibrated (weak, low-resolution data whose sigmas are far too small): the fixed-sigma
@@ -680,6 +700,7 @@ struct PointGroupLedgerEntry {
double r_over_best = std::numeric_limits<double>::quiet_NaN(); // ... over the best-agreeing operator anywhere
double r_contrast = std::numeric_limits<double>::quiet_NaN(); // ... 0 = unrelated reflections, 1 = that best operator
double r_over_floor = std::numeric_limits<double>::quiet_NaN(); // ... over the merge's random-noise R floor
double r_spread = std::numeric_limits<double>::quiet_NaN(); // this group's own worst operator R over its best
bool adopted = false; // this is the group the search took
bool eligible = false; // it passed every consistency test (so it COULD have been taken)
std::string refused_reason; // why it did not, when it was the highest refusal