docs: the spindle severity documented as the worst-case trigger it is
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m28s
Build Packages / build:windows:nocuda (push) Failing after 12m50s
Build Packages / build:windows:cuda (push) Failing after 13m18s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 19m20s
Build Packages / build:viewer-tgz:cpu (push) Successful in 21m51s
Build Packages / build:rugnux:windows (push) Failing after 9m1s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 21m59s
Build Packages / build:viewer-tgz:cuda (push) Successful in 23m20s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 27m8s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m2s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 20m4s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 24m28s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 27m15s
Build Packages / build:rpm (rocky9) (push) Successful in 24m13s
Build Packages / build:rpm (rocky8) (push) Successful in 29m0s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 23m21s
Build Packages / Generate python client (push) Successful in 53s
Build Packages / Create release (push) Skipped
Build Packages / Build documentation (push) Successful in 1m18s
Build Packages / DIALS test (push) Successful in 26m44s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 28m11s
Build Packages / XDS test (durin plugin) (push) Successful in 10m6s
Build Packages / XDS test (neggia plugin) (push) Successful in 9m8s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m15s
Build Packages / Unit tests (push) Failing after 1h26m58s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 8m28s
Build Packages / build:windows:nocuda (push) Failing after 12m50s
Build Packages / build:windows:cuda (push) Failing after 13m18s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 19m20s
Build Packages / build:viewer-tgz:cpu (push) Successful in 21m51s
Build Packages / build:rugnux:windows (push) Failing after 9m1s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 21m59s
Build Packages / build:viewer-tgz:cuda (push) Successful in 23m20s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 27m8s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 28m2s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 20m4s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 24m28s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 27m15s
Build Packages / build:rpm (rocky9) (push) Successful in 24m13s
Build Packages / build:rpm (rocky8) (push) Successful in 29m0s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 23m21s
Build Packages / Generate python client (push) Successful in 53s
Build Packages / Create release (push) Skipped
Build Packages / Build documentation (push) Successful in 1m18s
Build Packages / DIALS test (push) Successful in 26m44s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 28m11s
Build Packages / XDS test (durin plugin) (push) Successful in 10m6s
Build Packages / XDS test (neggia plugin) (push) Successful in 9m8s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 10m15s
Build Packages / Unit tests (push) Failing after 1h26m58s
Section 5.5 of the indexing documentation describes the per-image score as shipped: the folded formula, the geometric cone width, the pair-normal recovery of an axis too long to see, and the three trigger states with the rule that CANNOT SAY must engage. It states plainly that the score is a worst-case bound under an assumption of no symmetry rather than an estimate - an axis of order three or higher perpendicular to the spindle in fact repairs the cone, which a still cannot know - and gives the engagement rates actually measured instead of the decoy-null figure, which showed only that the estimator does not hallucinate rows and was never a false-alarm rate over harmless mountings. The run-level orbit-based fraction is pointed to as the exact measure available once the group is known. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EFEJG6WBQv8th4UJFNe53N
This commit is contained in:
@@ -16,6 +16,8 @@
|
||||
* `rugnux` writes reflection files in the conventions downstream programs read: `FreeR_flag` is 0 for the test set and 1 for the working set - it was the other way round - the merged and P1 MTZ carry the reserved `HKL_base` dataset so a CCP4 program reads the wavelength instead of falling back to 1.54187 A, and the merged mmCIF marks the free set as `_refln.status` = `f`.
|
||||
* The rugnux results report carries the space groups the data cannot separate and the enantiomorph state, the model's verdict and what it was allowed to decide, the detector geometry measured and what a single sweep cannot determine, the resolution the CC1/2 fit reached, which reciprocal axis each anisotropic diffraction limit belongs to, and twinning measured before and after the space group was decided; `REPORT_VERSION` is 7, and `SPACE_GROUP_ENANTIOMORPH= DETERMINED_FROM_MODEL` is now `ASSUMED_FROM_MODEL`.
|
||||
* The rugnux results report records how the crystal sat on the goniometer as `SPINDLE_SYMMETRY_AXIS_ANGLE_DEG=` and `SPINDLE_SYMMETRY_AXIS_ORDER=` - the angle from the spindle to the nearest symmetry axis and that axis's order - on every rotation run that determined a space group, not only when the angle was small enough to warn about.
|
||||
* The rugnux results report writes `SPINDLE_LOST_UNIQUE_FRACTION=` - the fraction (0-1) of unique reflections the mounting made unmeasurable under the measured point group - with the same number in the HDF5 master as `/entry/MX/spindleLostUniqueFraction`, and the spindle-mounting warning fires on that exact number instead of on the angle to the nearest symmetry axis.
|
||||
* Stills and grid scans carry a per-image `spindle_blind_fraction` - how much of a rotation sweep's blind cone the crystal's orientation would make unrecoverable, 0.5 and above calling for a second orientation - through the CBOR stream, the HDF5 files as `/entry/MX/spindleBlindFraction`, the REST plot and scan-result APIs, and the viewer and frontend plots; an absent value means the frame could not be assessed and is not a 0.
|
||||
* `rugnux --mode calibration` writes `<prefix>.json` beside the `.poni`, holding the geometry as a `jfjoch_broker` `dataset_settings` body, and refuses a fit that is not a measurement - no `.poni`, a non-zero exit, `converged` recorded in the `.json`; `--no-refine-tilt` holds the detector tilt at the file's value instead of zeroing it.
|
||||
* A snake grid scan with a negative slow step and an even number of rows no longer has its positions mirrored along the fast axis in the HDF5 master and the grid map, so the positions recorded for that configuration change; `jfjoch_viewer` draws grid scan cells in the proportion of the scan steps, labels the merge-statistics plot over the range the axis is drawn on, and builds its powder-calibration ring list from the loaded dataset's space group as well as its cell, so a centred sample cell no longer scales the whole fit.
|
||||
* The HDF5 master records `direct_beam_x`/`direct_beam_y` - where the undeflected beam lands, sent on the CBOR start message too - the beam size at the sample as `incident_beam_size` from the new `dataset_settings` `beam_size_x_um`/`beam_size_y_um`, and `/entry/MX/peakCountUnfiltered`; `dataset_settings` accepts any `smargon.chi_deg`, which was restricted to 0-90 degrees.
|
||||
|
||||
@@ -105,6 +105,86 @@ Selection is **not limited to a single lattice**: after the best cell is accepte
|
||||
|
||||
An optional reference unit cell (if supplied) restricts acceptance to cells within a relative distance tolerance in edge lengths (permutation-invariant).
|
||||
|
||||
### 5.5 Spindle alignment: the part of the blind cone symmetry cannot repair
|
||||
|
||||
A sweep about the spindle $\hat{\mathbf{e}}$ never brings a reciprocal point closer than
|
||||
$\theta_\mathrm{max}=\arcsin(\lambda/2d)$ to the axis onto the Ewald sphere, so a double cone of
|
||||
half-angle $\theta_\mathrm{max}$ is missing from every resolution shell — each shell losing its own
|
||||
$1-\cos\theta(d)$ — however long the sweep runs. Crystal symmetry normally repairs that loss by
|
||||
mapping the cone onto measured territory. It fails to when a symmetry axis lies inside the cone (the
|
||||
cone maps onto itself) — and, for a **2-fold**, equally when the axis is perpendicular to the
|
||||
spindle, because the diad carries the cone onto its opposite lobe, which the sweep leaves just as
|
||||
unmeasured. Friedel never helps: the cone is double-sided. The loss is a coherent cap rather than a
|
||||
scatter of absences, so it costs a map more than the same percentage lost at random.
|
||||
|
||||
The per-image score asks how much of that cone the frame's own orientation makes unrecoverable.
|
||||
The crystal's short lattice rows are read off the FFT row shortlist of §5.2 (a symmetry axis is
|
||||
always a lattice row, and usually among the short ones), and each plausible direction is scored as
|
||||
if it carried a lone 2-fold:
|
||||
|
||||
$$ \text{spindle blind fraction} = \frac{2}{\pi}\left(\arccos x - x\sqrt{1-x^{2}}\right),
|
||||
\qquad x = \min(\beta,\,90^\circ-\beta)\,/\,\theta_\mathrm{max}, $$
|
||||
|
||||
where $\beta$ is the direction's miss-angle from the spindle. The **fold** of $\beta$ about
|
||||
$45^\circ$ is the diad geometry above: both ends of the range are the bad case, and the closed form
|
||||
reproduces a Monte Carlo of the true double-cone self-overlap to 0.004 at
|
||||
$\theta_\mathrm{max}=15^\circ$ and 0.008 at $25^\circ$ (past $45^\circ$ it under-reports, by 0.05
|
||||
at $50^\circ$). $\theta_\mathrm{max}$ is taken from the **geometric** resolution of the setup — the
|
||||
detector corner at the recorded distance and wavelength — an upper bound on any sweep collected
|
||||
without moving the detector; the still's own spot resolution would understate the cone on exactly the
|
||||
weak frames that mislead. The worst direction wins, and the directions scored are the strong
|
||||
in-window rows **and the normals of their pairs** — the normal to two lattice rows is itself a
|
||||
reciprocal-lattice row and a symmetry axis is parallel in both bases, so a lone 2-fold on an axis far
|
||||
beyond the length window (a long monoclinic unique axis) is still seen by direction: measured on a
|
||||
synthetic lone-diad crystal with a 300 Å unique axis, the fraction of severe mounts reported severe
|
||||
rises from 0.60 to 1.00 with the pair normals, at no extra engagement on that class's harmless
|
||||
mounts. Nothing about the goniometer enters: the number describes the problem and leaves the remedy —
|
||||
a second sweep, a reorientation — to the beamline.
|
||||
|
||||
**This is a worst-case bound under an assumption of no symmetry, not an estimate.** A still cannot
|
||||
know the point group, so the nearest plausible row is scored as a lone 2-fold. An axis of order
|
||||
$\geq 3$ perpendicular to the spindle in fact **fully repairs** the cone (measured unrepaired
|
||||
fraction 0.000 for orders 3, 4 and 6, against 1.000 for a diad), which a still cannot see, so the
|
||||
bound is deliberately pessimistic on higher-symmetry crystals — that is the intended trade, because
|
||||
the number exists as a **trigger** for beamline automation, not as a physical quantity a user
|
||||
interprets.
|
||||
|
||||
**Trigger states.** The stored quantity is the continuous score; automation reads it through three
|
||||
fixed states with nothing to tune (`SpindleTrigger` in `SpindleBlindFraction.h`): **engage** at
|
||||
score ≥ 0.5, **don't engage** below, and **cannot say** when there is no value at all — too few
|
||||
spots, no shortlist, the consistency guard refused, the path never computed one. **Automation must
|
||||
treat CANNOT SAY as ENGAGE**: the error costs are asymmetric — a false negative is unrecoverable
|
||||
(one sweep is collected and the data stay short forever) while a false positive costs minutes of
|
||||
beamtime. Every transport keeps absence distinguishable from a measured zero (an absent CBOR key, a
|
||||
NaN in the HDF5 arrays, an absent optional after read-back). The 0.5 threshold is geometry, not
|
||||
tuning: the score is monotone in the folded miss-angle, so a threshold is a fold-angle gate, and 0.5
|
||||
gates at $\min(\beta,90^\circ-\beta) \le 0.404\,\theta_\mathrm{max}$; engaging on any overlap at
|
||||
all would gate at the cone edge, whose perpendicular band alone spans $\sin\theta_\mathrm{max}$ of
|
||||
orientation space per row (26 % at $15^\circ$) and unions over a frame's rows to well over half of
|
||||
all mountings — a trigger that always fires decides nothing.
|
||||
|
||||
**Reach and honest rates.** The score needs 60 spots (calibrated per crystal — 22 independent
|
||||
mounts — misses triple below it); below that, a frame that still indexed answers from the winning
|
||||
lattice's shortest rows, and otherwise the state is *cannot say*. Because the bound is pessimistic
|
||||
by design, it engages on a substantial share of harmless mountings: a single strong row's
|
||||
perpendicular band alone covers ~11 % of orientation space at the severe level
|
||||
($\theta_\mathrm{max}=15^\circ$), and the union over a frame's rows and pair normals reaches
|
||||
roughly a quarter to three quarters of random mountings depending on cone width and row count
|
||||
(measured 0.74 on a generic triclinic cell at $15^\circ$ via the lattice path). That is accepted:
|
||||
the cheap error is the extra wedge. An earlier figure of ~1 % false alarms (AUC 0.948) came from a
|
||||
null of five *decoy directions per frame* — it shows the estimator does not hallucinate rows near
|
||||
arbitrary directions, which is worth knowing, but it is **not** a false-alarm rate over harmless
|
||||
mountings, which geometry forbids to be that low.
|
||||
|
||||
**Offline, the guessing stops.** Once `rugnux` has merged a rotation run it holds the measured
|
||||
point group and the exact indexed orientation, and the run-level number is computed exactly instead:
|
||||
the group's proper rotations are applied to the blind double cone in the crystal's actual
|
||||
orientation, and the fraction of unique reflections no operator can recover is reported as
|
||||
`SPINDLE_LOST_UNIQUE_FRACTION` in the processing report and `/entry/MX/spindleLostUniqueFraction` in
|
||||
the master file (§ docs/RUGNUX_REPORT.md). That number clears or convicts a mounting the per-image
|
||||
bound can only be pessimistic about: a dihedral crystal with an in-plane diad on the spindle, or any
|
||||
cubic crystal in any orientation, loses nothing at all.
|
||||
|
||||
---
|
||||
|
||||
## 6. Bravais lattice / centering inference (“lattice search”)
|
||||
|
||||
Reference in New Issue
Block a user