rc164
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
278f8e3d73 |
Anisotropy: carry the sort key with the part, as the merge's ingest does
ScaledObservations assembles every integrated partial into rocking events before the tensor is fitted, and sorted them through a pointer. On the largest sweep that is 29.1 million parts spread over about 2.5 GB of 88-byte records against a 32 MB L3, and the comparator dereferenced one of those pointers on every one of roughly 660 million comparisons. Single-threaded, once per run, at the end of a run whose image loop uses sixteen threads and a GPU - and for a diagnostic that corrects no intensity and removes no reflection. It was 8% of one run, 12%, 15% and 16% of others, and 19% of the largest. The fix is the idiom this repository already uses for this exact sort: RotationScaleMerge's ingest sorts the same partials on the same key through a narrow record and its header says why. ScaledObservations was the one place that read the key back through the pointer instead. The key now travels with the part. Twelve lines, no bit-packing, and a reserve of the upper bound so the growth chain stops copying a few hundred megabytes on a large sweep. Ties are real and had to be handled rather than argued away: the predictor emits both Ewald intersections and near the blind region both land on the same frame, so one crystal has 7424 tied adjacent pairs, 3778 of them in reverse arrival order. The comparator keeps the same four values in the same order, so introsort - whose every branch is decided by comparator outcomes at positions and never by element size - makes the same comparisons and the same swaps and leaves the same permutation. Checked rather than asserted: a hash of the sorted permutation of all 29.1 million parts, a hash of the returned vector's raw bytes, and the md5 of the whole .cif all match on eight crystals, each first run twice on the baseline as a determinism control. The sort is 1.7-2.1x faster and the function 1.5-2.0x, which is 6.7 s of single-threaded critical path over those eight crystals and 10.8% of total wall clock over six timed warm and interleaved. A wider element costs 7% in the grouping pass that follows. A 24-byte variant that indexes back instead was built and is marginally faster to sort and slower overall, so the plainest version is also the fastest. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CHMmeM1d489zvNFT7ZMN2P |
||
|
|
0da593b32a |
Anisotropy: say what was measured, and stop crying wolf
Four reporting changes and a restored changelog line. The gate boundaries are unchanged - see below - and no merged number moves: .mtz and .hkl are byte-identical on two crystals, and REPORT_VERSION stays 3 because everything here is an added key or prose. A directional diffraction limit that is really the edge of the measured data was printed as if it were the crystal's. One crystal reported 1.09 / 0.99 / 0.99 A against a 0.994 A cutoff, so two of its three limits were the cutoff. A censored limit now prints with a "<" and sets ANISOTROPY_D_MIN_CENSORED. It fires on 4 of 28 crystals, and on two of those all three limits are censored, which means their reported directional spread was binning noise. The caution about a too-high symmetry assignment fired on every crystal with one deviatoric degree of freedom - every tetragonal, trigonal and hexagonal case, 17 of 38. A caution that fires on 45% of runs is noise. It is now conditional on the signature that actually indicates it, a large symmetry-forbidden signal with a gate that established nothing, and fires on 2 of 38: the crystal already on record as space-group unstable, and one other. The forbidden-direction z is now reported so the reader can see it; its battery median is 6.5. The verdict line quoted deltaB_linear while the headline reported deltaB, and the two differ - deltaB_linear is the larger on 12 of 31 crystals. It now writes out the arithmetic, says which number the gate tests, and says which to act on. The observation count the floor was measured on is reported, and the mmCIF verdict item carries its vocabulary like its neighbours. On the boundaries: they were calibrated against XDS-derived unmerged data, while production measures the floor from rugnux's own observations, and those disagree by 0.12x to 16.2x with 7 of 29 verdicts flipping. Re-measured on production input - by merging each cubic crystal in proper subgroups of its own Laue class, where the true anisotropy is exactly zero and every deltaB that comes back is manufactured - the false-positive rate is 24% at 2.0, 10% at 3.5 and 5% at 5.0. Each sits inside the published band read as its worst-case figure rather than its average. 3.5 maximises power minus false-positive rate across the grid; moving to 4.0 costs a real detection and reduces the false-positive rate by nothing. So the boundaries stay, and the "strong" band at 5.0 is a 5% statement on real data rather than the 0.5% its derivation suggested. The changelog entry for the diagnostic itself was committed with only its first line, having been extracted by a script that took one line of a four-line bullet. Restored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CHMmeM1d489zvNFT7ZMN2P |
||
|
|
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 |