SearchSpaceGroup: operator-homogeneity gate reads the R gap between a subgroup and the rest
The gate that refuses a point group whose own operators "disagree among
themselves" read worst-agreeing over best-agreeing operator R. That divides
by an extreme order statistic - the pole the contrast gate was introduced to
remove - so a genuine group whose operators merely spread in a continuum is
refused as soon as one operator happens to agree unusually well.
That is what refused 432 on a private cubic F432 crystal after the overload
drop (37a8c8e24; bisected: that commit alone gives F23, b126b9fc1 does not
change the search). The crystal's intensity-weighted R is dominated by a few
dozen strong low-order reflections. Dropping the overloaded ones (whose
flank-extrapolated values disagreed ~2x with their mates) raised every
operator CC and made the cleanest 432 operator cleaner still, while
anisotropic systematic error keeps the genuine 23 operators spread ~2x
among themselves: worst-over-best went 2.68 -> 4.0 against the 3.2 bound.
H ratio (1.25) and contrast (0.78) passed throughout.
A mixture of real and false operators is specific: the real ones form a
subgroup (the true group's intersection with the candidate) and everything
outside it is false. The statistic is now the largest factor by which every
operator outside some subgroup reads worse than every operator inside it -
a gap, found by walking the operators sorted by R and reading each prefix
that closes into a group. It is never above worst-over-best, so nothing
admitted before can be refused; the bound stays 3.2. Read off the operator
tables of the all2 full battery and run 3: genuine adopted groups reach at
most 2.5 (the cubic crystal 1.8-2.0), the 5ebi 622 mixture reads 3.6
(was 6.0), 9hnc's false 422 3.4, and 2wnn's false 222 3.8 (unchanged, still
refused by this gate).
Targeted runs vs all2-full (f64f76): the private cubic set F23 -> F432
(pass); every other set identical to the last digit - five other private sets; open 5epe 8sqt 3inp 9fhc 6oel 7orr
8tyy 3ky7 6h5t 6moj 2wnn 5ebi 9hnc; lyso_x06da_ref, lyso_x06da_5keV,
myob_x06da_split; all small-molecule sets (citric R1 .0374, aspirin
.0363/.0358, hepes .0310, yag .1021, kdp, lcystine, cytidine, dnba,
lalanine, metformin, nidppe, cuhf2); and the high-spread trigonal/
tetragonal/hexagonal panel (insu_H twin/notwin, insu_I weak/low_isa, 6v2r,
8xtg, 5j23, 6iu6, 6iu8, 3mc4, 6vww, 7k1l, 9i80, 6pxb, 9hs7).
SearchSpaceGroup* and [SearchSpaceGroup] test cases pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K5K8jvPPbmCrbqnWkddTuB
This commit is contained in:
@@ -13,6 +13,7 @@
|
||||
* Rugnux keeps the screw axes it found when a higher point group is adopted after the twin-law check, instead of writing the group without screws (e.g. P 4 2 2 for P4_2 2_1 2).
|
||||
* Rugnux drops a rotation reflection whose spot held a saturated (overloaded) pixel, as XDS does, and reports the count as `OBSERVATIONS_REJECTED_OVERLOAD=`.
|
||||
* Rugnux fits a rotation sweep's per-frame scale on the partial reflections, even with few reflections per frame, when scaling from the fulls alone leaves no measurable error model.
|
||||
* Rugnux no longer refuses a genuine high-symmetry point group because one of its operators agrees with the data unusually well; it refuses a group whose operators split into an agreeing subgroup and a disagreeing rest.
|
||||
* Rugnux names glide planes in groups without a centre of symmetry (e.g. I-42d, Fdd2), reports a group with the same absences (C2/c and Cc) as an alternative, and states how the centre was decided (`SPACE_GROUP_CENTRE`, `CENTRE_STATISTICS_*`).
|
||||
|
||||
### 1.0.0-rc.173
|
||||
|
||||
@@ -1394,7 +1394,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).
|
||||
// The widest R gap between a subgroup of the candidate's own and the rest (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
|
||||
@@ -1653,11 +1654,12 @@ 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).
|
||||
// Homogeneity of the candidate's OWN operators, parent and added alike (see
|
||||
// max_operator_r_spread): the largest factor by which every operator outside a subgroup agrees
|
||||
// worse than every operator inside it. A subgroup that agrees better than all the rest is a
|
||||
// prefix of the operators sorted by R, so each prefix that closes into a group is one split.
|
||||
{
|
||||
double r_lo = std::numeric_limits<double>::infinity(), r_hi = 0.0;
|
||||
int n_op = 0;
|
||||
std::vector<std::pair<double, gemmi::Op>> by_r;
|
||||
for (const auto &rot : c.pg->rotations) {
|
||||
if (rot.rot == gemmi::Op::identity().rot)
|
||||
continue;
|
||||
@@ -1665,10 +1667,19 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
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;
|
||||
by_r.emplace_back(os.r_stat, rot);
|
||||
}
|
||||
std::sort(by_r.begin(), by_r.end(),
|
||||
[](const auto &a, const auto &b) { return a.first < b.first; });
|
||||
if (by_r.size() >= 2) {
|
||||
c.r_spread = 1.0;
|
||||
std::vector<gemmi::Op> subgroup;
|
||||
for (size_t k = 1; k < by_r.size(); ++k) {
|
||||
subgroup.push_back(by_r[k - 1].second);
|
||||
if (GeneratedRotationSet(subgroup).size() == subgroup.size() + 1)
|
||||
c.r_spread = std::max(c.r_spread, by_r[k].first / by_r[k - 1].first);
|
||||
}
|
||||
}
|
||||
if (n_op >= 2)
|
||||
c.r_spread = r_hi / r_lo;
|
||||
}
|
||||
c.parent_b_used = parent_b;
|
||||
c.r_added = r_added;
|
||||
@@ -1732,8 +1743,9 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
// 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).
|
||||
// symmetry of one crystal. Asking instead whether the candidate's operators split into a
|
||||
// subgroup that agrees and a remainder that does not 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;
|
||||
@@ -1825,10 +1837,10 @@ SearchSpaceGroupResult SearchSpaceGroup(
|
||||
+ "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)
|
||||
refused_why = "its own operators split in two - every operator outside one of its "
|
||||
"subgroups reads at least " + FormatDouble(c.r_spread, 1)
|
||||
+ "x the intensity-weighted R of every operator inside it (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 (l_refused)
|
||||
|
||||
@@ -488,17 +488,27 @@ struct SearchSpaceGroupOptions {
|
||||
// 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.
|
||||
// OTHER: a point group is the claim that all of its operators are symmetries of one crystal.
|
||||
//
|
||||
// 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.
|
||||
// What a mixture looks like is specific: the operators that are real form a SUBGROUP (the true
|
||||
// group's intersection with the candidate), and every operator outside it is false. So the
|
||||
// statistic is the largest factor by which every operator outside some subgroup reads a worse R
|
||||
// than every operator inside it - a GAP between a subgroup and the rest.
|
||||
//
|
||||
// It was worst-over-best until it was measured to divide by an extreme order statistic, the pole
|
||||
// the contrast above had already been introduced to remove. Genuine operators do not all agree
|
||||
// equally well: anisotropic systematic error spreads them in a continuum, widest where a few
|
||||
// dozen strong reflections dominate R. On a weak cubic crystal that spread alone took
|
||||
// worst-over-best past the bound once dropping its overloaded reflections made its cleanest
|
||||
// operator cleaner still, and the genuine group was refused; its widest gap stays near 2. A
|
||||
// real mixture is a gap and reads about the same either way: a c-axis 2-fold and two horizontal
|
||||
// ones at 0.04-0.06 under a 622 whose other eight operators read 0.23-0.26 give 3.6
|
||||
// (worst-over-best 6.0), and a 222 that adds two false 2-folds to one real one 3.8 either way.
|
||||
// The gap is never above worst-over-best, so no group admitted before can be refused now.
|
||||
//
|
||||
// The bound was calibrated as worst-over-best over the 129 rotation datasets of one battery run
|
||||
// that adopted a group of order 3 or more: every correct adoption sat at 1.0-2.64 and the one
|
||||
// over-called point group on that run at 4.87, and 3.2 sits in that gap.
|
||||
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
|
||||
@@ -724,7 +734,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
|
||||
double r_spread = std::numeric_limits<double>::quiet_NaN(); // widest R gap between a subgroup of its own and the rest
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user