2 Commits
Author SHA1 Message Date
leonarski_fandClaude Opus 5 0e6ece3c38 Anisotropy: assemble only the reflections the floor actually reads
BuildObservationCells strides whole ASU clusters down to MAX_CLUSTERS =
20000 and keeps 1.4% of what it is handed - so the assembly before it
sorted and summed every integrated partial in the sweep to throw 98.6% of
the result away on the next line. The comment there says the striding
"costs nothing and bounds the work"; it bounded it after the expensive
part.

Sample first instead: keep a partial iff a splitmix64 hash of its ASU key
falls in the low one part in 2^n. Whole ASU clusters are mandatory rather
than convenient - the cluster-robust sandwich sums one score per cluster,
and the forbidden-direction signal IS the contrast between mates, so a
raw-hkl sample would collapse that onto the independent covariance and
inflate the floor. The hash rather than the key matters too: HKLKey::pack
puts h in the low bits, so masking the key directly would select lattice
planes instead of a sample.

The rate comes from the partial count, not from merged.size(): the merged
list is resolution-cut, and taking the rate from it made the first
version never fire at all on the crystal it was written for. One part in
2^n of the partials is one unique reflection in 2^n, so the count already
computed for the reserve is the right one.

DELTA_B is unchanged by construction, not by luck. AnalyzeAnisotropy
touches the unmerged observations in exactly two places - an empty test
and MeasureSystematicFloor - so the tensor, the shape verdict and the
directional limits come from the merged list alone. Over the battery:
0 verdict flips of 38; DELTA_B, DELTA_B_LINEAR, SHAPE, FREE_DIRECTIONS
and the D_MIN keys differ on none of the 38, byte-identical rather than
within noise; every reported key identical on 26; the mmCIF diff is
exactly _reflns.jfjoch_aniso_floor and _significance; no caution appears
or disappears anywhere.

The spread those two keys move within was measured rather than assumed,
and re-running cannot measure it because the code is deterministic. The
shipped code already samples - by striding cluster order - so moving the
phase of its own stride gives the sampling spread it has always carried.
Against that control the change moves the floor less: ratio range
0.803-1.481 against 0.497-1.343, geomean 1.031 against 0.941. The gate
therefore did not get easier to trip - the floor's geomean moves +3.1%
here against -5.9% for the phase control, the crystal nearest the gate is
not sampled at all and is bit-identical, and the two sampled crystals
nearest the gate both move away from it. N_OBSERVATIONS goes up, not
down, because a smaller pool lands the downstream integer stride slightly
above the cluster cap.

The ASU reduction is 14-45 ns a call in isolation, which nearly sank the
design; in place it is free, because it overlaps the memory stalls of the
Reflection stream it walks. The 24 crystals below the sampling cap run
the shipped algorithm plus the reduction and cost +1.79 s in total,
median +0.04 s.

The block that introduced this measures 8.07 s before and 1.97 s after,
which is 5.74 s off the largest crystal (-12.3%) and -11.9 s of
anisotropy time over the battery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHMmeM1d489zvNFT7ZMN2P
2026-08-26 14:11:56 +02:00
leonarski_fandClaude Opus 5 80e10f51b3 rugnux: measure and report diffraction anisotropy
rugnux now says whether a dataset's fall-off is direction-dependent, and by
how much. It corrects nothing and truncates nothing: no intensity is changed,
no reflection is dropped on a directional criterion, and the written files do
not depend on direction at all.

Two quantities, because they are not the same thing. The anisotropic deltaB is
the range of the principal components of the anisotropy tensor - a rate of
fall-off. The diffraction limit along each principal direction is where
<I/sigma(I)> in a 20 degree cone falls through 2 - where signal actually runs
out. One battery case has only 0.28 A between its directional limits and a 58x
ratio in cone <I/sigma>, so reporting either alone would miss it.

The tensor is a Laue-constrained deviatoric ADP tensor fitted on INTENSITIES
with no positivity cut, by weighted Gauss-Newton over 12 shells x 60
directions with a free constant per shell. Fitting amplitudes after a
positivity cut, which is what xtriage and ctruncate do, destroys about 40% of
the measured anisotropy - the cut keeps only the positive noise excursions in
whichever direction has died, and that is the direction carrying the signal.
Against the same 38 merged files rugnux reads 1.24x xtriage's eigenvalue
spread and 1.61x ctruncate's; on strong near-isotropic data all three agree to
a few percent, and they diverge exactly where a direction has died.

The verdict is gated three ways - not detected, detected, or cannot determine
- against the dataset's own systematic floor, measured in the tensor
directions its Laue symmetry forbids. The floor cannot be measured on merged
reflections, which have exact Laue symmetry by construction, so the floor is
taken from the unmerged observations and the verdict is "cannot determine"
without them. Triclinic has no forbidden subspace and always returns cannot
determine. A cubic crystal returns exactly zero, because that is its symmetry
and not a measurement.

A second axis reports the resolution signature: a genuine Debye-Waller
fall-off is linear through the origin in s^2, and a deficit that is flat is
something else. Magnitude alone had promoted a crystal that is 68% not a
Debye-Waller B into the top five of this battery; it now reads not detected
with the caution attached.

Following Sheriff & Hendrickson (1987) Acta Cryst. A43, 118-121 for the tensor
and Popov & Bourenkov (2003) Acta Cryst. D59, 1145-1153 for the estimator.

The directional limits are written as jfjoch_ local mmCIF items rather than
_reflns.pdbx_aniso_diffraction_limit_*, whose dictionary definition is
explicitly the ellipsoid fitted to a diffraction cut-off surface - a
construction rugnux does not perform. The generic anisotropic B tensor items
are written.

Changes no existing number; only REPORT_VERSION moves, 1 to 2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CHMmeM1d489zvNFT7ZMN2P
2026-08-25 19:13:16 +02:00