Anisotropy verdict: the directional limits are a second hypothesis beside the tensor
The verdict gated only deltaB_linear, the Debye-Waller part of the tensor, against the data set's own systematic floor. A deficit that does not follow s^2 (a FLAT signature) can stop the signal far earlier in one direction and still read NOT_DETECTED: one open-arm set reported directional limits of 3.18/3.18/2.14 A beside NOT_DETECTED. The cone limits (<I/sigma> = 2 in 20 deg cones) were computed but never consulted. Now each statement is decided on its own evidence and either establishes anisotropy: - tensor: unchanged gate; - directional limits: at the best direction's limit the weakest cone's <I/sigma> is below the best cone's by more than 3.5 of their joint standard error (both read linearly in s^2 between shell centres, as the limits are), the weakest limit is measured (not censored at the data edge), and the limits are at least 0.25 A apart - the spread the report already calls "noticeable". Why the magnitude term: with the standard errors alone (the counting statistics of the cones) the weakest cone is "significantly" below the best on 172 of 216 battery sets, down to limits 0.5% apart (measured with a replica of ConeLimits on thea897b08battery's merged data). The cones differ systematically (multiplicity and coverage depend on direction), so significance alone made DETECTED near-universal. A refusal (CANNOT_DETERMINE: dose ramp, short sweep, triclinic, no unmerged data) is not overruled - its reasons bias the cones too - but now reports the measured directional limits. Report: ANISOTROPY_DETECTED_BY (TENSOR/DIRECTIONAL/BOTH/NONE); developer keys for the two cones' <I/sigma> and errors; the summary row names the test that carried the verdict, and CANNOT_DETERMINE rows give the directional limits; the mmCIF verdict comment says so. Report only: no intensity, flag or decision downstream reads the verdict (the French-Wilson prior uses the tensor regardless). Measured (battery replica, 225 sets with limits): directional statement established on 95; 23 NOT_DETECTED -> DETECTED (directional ratio 1.14-1.59); of the 17 sets with ratio > 1.2 and an uncensored weakest limit, 11 flip, 5 stay CANNOT_DETERMINE with limits reported, 1 (sparse, noisy cones) stays NOT_DETECTED. ANISOTROPY warnings (spread > 0.5 A) appear on 6 more sets. Confirmed with rugnux runs on the ci tier, those 17 and the private arm: p.hkl and MTZ data identical to the base (e34a5fb88) on every set compared. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -157,6 +157,29 @@ TEST_CASE("Anisotropy of an isotropic synthetic merge is small", "[anisotropy]")
|
||||
CHECK(std::fabs(result.delta_b) < 1.0);
|
||||
}
|
||||
|
||||
// The directional limits are a second hypothesis beside the tensor: the signal stops earlier along c*
|
||||
// than along a* and b*, and the cones see it with no model of the fall-off. Without unmerged
|
||||
// observations the tensor is refused, and a refusal stands - the cones overrule only NOT_DETECTED -
|
||||
// but the directional statement is still made and reported.
|
||||
TEST_CASE("Anisotropy directional limits establish a difference the cones measure", "[anisotropy]") {
|
||||
const gemmi::UnitCell cell = Cell(79, 79, 38, 90, 90, 90);
|
||||
const gemmi::SpaceGroup *sg = gemmi::find_spacegroup_by_name("P 43 21 2");
|
||||
// B 70 A^2 along a*/b* and 100 A^2 along c*: <I/sigma> = 2 near 2.4 A and 2.8 A respectively.
|
||||
const auto merged = SyntheticMerge(cell, sg, 2.0, 80.0, {-10.0, -10.0, 20.0, 0.0, 0.0, 0.0});
|
||||
const auto result = AnalyzeAnisotropy(merged, {}, cell, sg);
|
||||
REQUIRE(result.n_cells > 0);
|
||||
REQUIRE(std::isfinite(result.weak_cone_i_over_sigma));
|
||||
CHECK(result.d_min_spread > ANISOTROPY_SPREAD_NOTICEABLE_A);
|
||||
CHECK(result.weak_cone_i_over_sigma < result.best_cone_i_over_sigma);
|
||||
CHECK(result.directional_detected);
|
||||
CHECK_FALSE(result.tensor_detected);
|
||||
CHECK(result.verdict == AnisotropyVerdict::CannotDetermine);
|
||||
|
||||
// The same fall-off in every direction: the limits agree and nothing is established.
|
||||
const auto isotropic = AnalyzeAnisotropy(SyntheticMerge(cell, sg, 2.0, 80.0, {0, 0, 0, 0, 0, 0}), {}, cell, sg);
|
||||
CHECK_FALSE(isotropic.directional_detected);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// One partial of reflection (h,k,l) on image `frame`, with the fields ScaledObservations reads.
|
||||
Reflection Partial(int h, int k, int l, float frame, float I, float partiality) {
|
||||
|
||||
Reference in New Issue
Block a user