SearchSpaceGroup: ask a promotion's twin-immune zone on a twinned merge, read at the other twin laws' fraction
6iu9 (deposited P3_1, merohedrally twinned at 0.4 or more by its 321 law: H 0.097, CC 0.79) went from P3_1 to P3_121 when the penalised per-frame scale smoother (495f6a97b) landed; the ingest partiality fix alone (7d6c201e9) keeps P3_1. Bisected on the two binaries: smoother-only 6iu9 P3_121, partiality-only P3_1. The smoother only removed the luck: the P3_1 answer rested on the Lorentz-filtered arm's added-operator R contrast reading 0.71 against a bound of 0.72 (the all-observation arm already passed 32 at 0.82); better scaling moved it to 0.87, no gate refused, and the twin-immune zone - which reads -401 nats, acentric - is only consulted where a gate fired. A near-perfect twin passes every agreement gate by construction (H ratio 0.97 here); only the zone can refuse it. It was confined to refusals because a genuine trigonal crystal read -46 nats with no gate firing; on the battery 5uth (genuine P3_121, twinned by a 622 law, other-law CC 0.14) reads -206. The reason: the centric density was untwinned, but a twin by a law OTHER than the zone's operators reaches the zone as it reaches every reflection, so a genuine zone reads 0.81 at a = 0.2. The operators' own twin law cannot reach their zone, so only the centric side needs it. - TwinningAnalysis: the zone's centric density is the twinned one at a caller-given fraction (weighted sum of two chi^2_1, via exp(-y) I0(y)); the control's calibration expectation is taken at the same fraction (-KL(acentric || centric_a), -0.130 at a = 0 as before). At a = 0 nothing changes. - SearchSpaceGroup: twin_fraction_outside = the fraction implied by the strongest CC of a lattice rotation outside the group, relative to the group's own mean CC, through rho = 2a(1-a)/((1-a)^2+a^2); exported for the adopted group and used by Rugnux's TwinZoneVerdict and the zone report. - New Stage A test: a candidate that does not hold every lattice rotation, on a P1 merge whose <|L|> is in the partial-twin band [0.375, 0.44), is refused when the zone over one of its index-2 subgroups reads acentric by 20 nats (the TwinZoneVerdict bound), unless zones_ambiguous. The band's lower end is the L-test gate's: below it something else compresses the zones too (a pseudo-cubic small-molecule set, cuhf2, read its 422 zone at 0.64 beside a control at 0.58 and was turned to P222 without it). Zone evidence (calibrated, at the other-law fraction): 6iu9 32 -230 / -143 (refused, P3_1); 5uth +209 / +33 (P3_121 kept); 8xtg +324 / +65; 6vww P6 +20 / +8.5; 7k1l P6 +8 / +7.5. Battery (open+inhouse, targeted, against 20261004-2350 all2-full; only beyond-noise change listed): - 6iu9: fail -> pass, P3_121 -> P3_1, R_meas 17.4% -> 14.4%, ISa 4.6 -> 5.4, R-free 0.318 -> 0.327. - unchanged: 6iu5, 6iu6, 6iu8 (P3_1), 5uth, 5j23, insu_H_x06da_twin/notwin, 6vww, 7k1l, 8xte, 8xtg, 9i80, 4bwl, 2wnq, 8c3e, 2wnn, 2xfw, 5ebi, 6p8j, 6z9g, 6rlr, 6toc, 3mc4; 321/312 controls 5lzl, 6w4h, 9gqg, 6pxb, 9z72; all 14 small-molecule sets (cuhf2 checked again after the band). - private subset (8 sets): no change beyond noise. Tests: [twinning] (new: zones read at the other twin laws' fraction), SearchSpaceGroup*. 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:
@@ -227,14 +227,17 @@ namespace {
|
||||
// Debye-Waller B along c* alone (A^2) - the same in both twin domains, a twin law being a lattice
|
||||
// operation - and `scale_jitter` a log-normal factor of that width on every observed intensity,
|
||||
// a nuisance with no direction that no normalisation removes.
|
||||
std::vector<MergedReflection> WilsonTetragonal(const char *true_group, double twin_fraction,
|
||||
double anisotropy_b = 0.0, double scale_jitter = 0.0) {
|
||||
// `twin_super` names the group whose extra operator is the twin law.
|
||||
std::vector<MergedReflection> WilsonMerge(const char *true_group, const char *twin_super,
|
||||
const gemmi::UnitCell &cell, double twin_fraction,
|
||||
double anisotropy_b = 0.0, double scale_jitter = 0.0) {
|
||||
const gemmi::SpaceGroup &sub = gemmi::get_spacegroup_by_name(true_group);
|
||||
const gemmi::SpaceGroup &super = gemmi::get_spacegroup_by_name("P 4 2 2");
|
||||
const gemmi::SpaceGroup &super = gemmi::get_spacegroup_by_name(twin_super);
|
||||
const gemmi::Op twin = jfjoch_test::TwinLaw(sub, super);
|
||||
const gemmi::GroupOps gops = sub.operations();
|
||||
const gemmi::ReciprocalAsu rasu(&sub);
|
||||
const gemmi::UnitCell cell(47, 47, 63, 90, 90, 90);
|
||||
const int hmax = static_cast<int>(cell.a / 2.5) + 1, kmax = static_cast<int>(cell.b / 2.5) + 1,
|
||||
lmax = static_cast<int>(cell.c / 2.5) + 1;
|
||||
auto true_intensity = [&](const gemmi::Op::Miller &hkl) {
|
||||
const auto asu = rasu.to_asu(hkl, gops).first;
|
||||
const double u1 = jfjoch_test::detail::UniformFromHkl(asu);
|
||||
@@ -246,9 +249,9 @@ namespace {
|
||||
* std::exp(-anisotropy_b * s_c * s_c / 2.0);
|
||||
};
|
||||
std::vector<MergedReflection> out;
|
||||
for (int h = -19; h <= 19; ++h)
|
||||
for (int k = -19; k <= 19; ++k)
|
||||
for (int l = -26; l <= 26; ++l) {
|
||||
for (int h = -hmax; h <= hmax; ++h)
|
||||
for (int k = -kmax; k <= kmax; ++k)
|
||||
for (int l = -lmax; l <= lmax; ++l) {
|
||||
if (std::make_tuple(h, k, l) <= std::make_tuple(-h, -k, -l))
|
||||
continue;
|
||||
const gemmi::Op::Miller hkl{{h, k, l}};
|
||||
@@ -271,6 +274,12 @@ namespace {
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<MergedReflection> WilsonTetragonal(const char *true_group, double twin_fraction,
|
||||
double anisotropy_b = 0.0, double scale_jitter = 0.0) {
|
||||
return WilsonMerge(true_group, "P 4 2 2", gemmi::UnitCell(47, 47, 63, 90, 90, 90), twin_fraction,
|
||||
anisotropy_b, scale_jitter);
|
||||
}
|
||||
}
|
||||
|
||||
// Reflections centric in the adopted group but acentric in a subgroup are their own twin mates under
|
||||
@@ -345,6 +354,28 @@ TEST_CASE("Twin-immune zones are normalised for anisotropy and calibrated by the
|
||||
}
|
||||
}
|
||||
|
||||
// A twin by a law OTHER than the operators a zone is read for averages the zone's reflections with
|
||||
// unrelated ones like every other reflection, so a genuine 321 crystal twinned by a 622 law reads its
|
||||
// 2-folds' zone acentric against the untwinned centric density. Read at the twin fraction, it reads
|
||||
// centric again - while a crystal twinned by the zone's own operators, which cannot reach the zone,
|
||||
// stays acentric.
|
||||
TEST_CASE("Twin-immune zones are read at the twin fraction of the other twin laws", "[twinning]") {
|
||||
const gemmi::SpaceGroup &p321 = gemmi::get_spacegroup_by_name("P 3 2 1");
|
||||
const gemmi::SpaceGroup &p3 = gemmi::get_spacegroup_by_name("P 3");
|
||||
const gemmi::UnitCell hexagonal(51, 51, 71, 90, 90, 120);
|
||||
|
||||
const auto genuine = WilsonMerge("P 3 2 1", "P 6 2 2", hexagonal, 0.1);
|
||||
const auto untwinned_read = AnalyzeTwinImmuneZone(genuine, hexagonal, p321, p3);
|
||||
REQUIRE(untwinned_read.zones.size() == 1);
|
||||
CHECK(untwinned_read.zones[0].calibrated_evidence_nats < -20.0);
|
||||
const auto twinned_read = AnalyzeTwinImmuneZone(genuine, hexagonal, p321, p3, nullptr, 1, 0.1);
|
||||
CHECK(twinned_read.zones[0].calibrated_evidence_nats > 20.0);
|
||||
|
||||
// Twinned by the 2-folds themselves, with no other law to read the zone at.
|
||||
const auto twin = WilsonMerge("P 3", "P 3 2 1", hexagonal, 0.1);
|
||||
CHECK(AnalyzeTwinImmuneZone(twin, hexagonal, p321, p3).zones[0].calibrated_evidence_nats < -20.0);
|
||||
}
|
||||
|
||||
// Averaging each intensity over its orbit under a group leaves the L-test where it was when the group
|
||||
// is the crystal's, and narrows it towards the perfect-twin 0.375 when an operator of the group is
|
||||
// not - read on the same pairs. A perfect twin of the subgroup already reads 0.375 unmerged, so there
|
||||
|
||||
Reference in New Issue
Block a user