Beam centre: measure the direct beam from the symmetry of the spots, before indexing

The centre in the file is often a placeholder, and nothing measures it until post-refinement
has already indexed the sweep - by which time a wrong centre has chosen the lattice. Two exact
facts about a rotation sweep give it from spot positions alone, with no cell, no orientation
matrix and nothing indexed.

Rotating 180 degrees about the spindle and taking -h negates a reflection's component along the
spindle and leaves the rest, so with the spindle perpendicular to the beam the Laue condition is
preserved and the spots recorded half a turn apart are mirror images along the spindle. Those are
Friedel mates, not the same reflection. The same reflection appears twice for a different reason:
it meets the Ewald sphere on two crossings, generally not half a turn apart, differing only in the
sign of the component perpendicular to both the spindle and the beam. The first observable gives
the coordinate along the spindle, the second the coordinate across it. Each candidate pairing
votes and the true value accumulates while wrong pairings scatter.

Both observables need guarding, because a vote is a comb and the tallest tooth is not always the
right one. Along the spindle a false pairing cannot fake the equality of Friedel amplitudes.
Across it, the two crossings of one reflection are separated by a sweep angle its own position
fixes, which no accidental pair reproduces.

The mirror is exact in the laboratory frame, so it is only as good as the rotation axis. Every
file here states an ideal axis and none of them has one; a skew about the beam spreads the vote
instead of shifting it, and past a milliradian it moves an otherwise correct answer by pixels
while every internal statistic still looks healthy. It is therefore fitted, not assumed. A tilt of
the axis towards the beam is measured and reported but not applied, being confounded with the
detector rotation until that is fitted too.

Nothing inside the fit can see a wrong tooth - when the vote flips, every frame pair flips with
it - so the answer is checked from outside, by asking whether it depends on where the search
began. That, and a floor on the angular span the pairs cover, are what refuse the cases this
cannot measure: a sweep barely past half a turn is the dangerous one, not the short one, because
at exactly half a turn there is nothing to fit and just past it there is almost nothing.

Where the sweep is too short for any of this the radial background profile gives a coarser centre
from a handful of images, and where neither can measure it the file's value is kept.

The beam-stop projection now takes its own frames rather than sharing the sample, so turning this
on cannot change the mask; and both samples keep away from the ends of the sweep, where shutter
synchronisation spoils an image. Reading twice as many frames as before costs a few seconds once,
and is what makes the answer independent of which frames were drawn.

Off by default. Over the 38-crystal rotation battery it serves every dataset, agrees with XDS's
refined direct beam to 0.116 px in the median against 0.135 for the value in the file, and changes
no space group.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 23:54:09 +02:00
co-authored by Claude Opus 5
parent b34aabe39f
commit abfdb89b79
16 changed files with 2066 additions and 20 deletions
+1
View File
@@ -3,6 +3,7 @@
### 1.0.0-rc.161
This is an UNSTABLE release. It includes many experimental features, as well as many AI generated fixes. We recommend using rc.152 for production use.
* rugnux: New **beam-centre measurement before indexing** (`--estimate-beam-center`), which takes the direct beam from the symmetry of the spots where the sweep reaches at least half a turn, from the radial background profile where it does not, and keeps the value in the file where neither can measure it; the rotation axis' skew about the beam is fitted at the same time (`--no-fit-spindle` keeps the axis from the file).
* Lattice search: a reduced cell whose β sits on the Niggli type boundary now keeps its centring instead of falling through to triclinic, so a centred lattice is no longer read as primitive according to which way its refined cell happened to reduce.
* rugnux: The second rotation pass now reuses a space group that came from the intensity-based centred-lattice test, as it already did for every other group, instead of re-deciding the symmetry at the refined geometry.
* Space-group search: where the search on the well-measured observations (`--search-min-zeta`) and the search on all of them disagree, the answer is now always the one the merge of all the observations supports, instead of whichever of the two found more symmetry.
+31
View File
@@ -151,6 +151,37 @@ Jungfraujoch uses $|\Delta_\mathrm{Ewald}|$ as an operational proxy for excitati
- profile radius estimation (see §11.1),
- still partiality option in scaling/merging (§10.2).
### 1.4 Measuring the direct beam before indexing
Two exact facts about a rotation sweep let the direct beam be measured from spot positions alone,
before anything is indexed (`--estimate-beam-center`).
**Friedel mates half a turn apart.** Rotating 180° about the spindle $\mathbf{m}$ and taking $-h$
negates a reflection's component along $\mathbf{m}$ and leaves the rest. With the spindle
perpendicular to the beam this preserves $\mathbf{q}\cdot\mathbf{S}_0$, so $-h$ satisfies the Laue
condition at $\varphi+180°$ exactly where $h$ satisfies it at $\varphi$, and the spots recorded half a
turn apart are mirror images along the spindle direction. This gives the beam coordinate **along**
the spindle. Note these are Friedel mates, not the same reflection.
**The second crossing.** The same reflection meets the Ewald sphere twice, at two angles that are
generally *not* 180° apart, differing only in the sign of the lab component perpendicular to both
$\mathbf{m}$ and the beam. This gives the remaining coordinate. The two crossings are separated by a
sweep angle fixed by the reflection's own position, so genuine pairs are identified without a cell or
an orientation matrix.
Neither observable requires the reflections to be indexed: each candidate pairing votes for a beam
coordinate, and the true value accumulates while wrong pairings scatter.
The mirror is exact in the **laboratory** frame, so it is sensitive to the spindle's orientation. A
skew of the spindle about the beam *spreads* the vote rather than shifting it, and is fitted
alongside the centre (`--no-fit-spindle` keeps the axis from the file); a tilt of the spindle towards
the beam is measured and reported but not applied. The frames read are sampled away from both ends of
the sweep, where shutter synchronisation can spoil an image.
Where the sweep is shorter than half a turn the spot symmetry cannot be formed, and the centre is
taken instead from the centroid of the radial background profile, which needs only a few images.
Where neither method can measure the centre, the value from the file is kept.
---
## 2. Azimuthal integration (radial profiles)
+7
View File
@@ -393,6 +393,13 @@ Detector mask:
| --- | --- |
| `--detect-beam-stop[=N\|off]` | Find the beam stop and its holder in a projection of N images and add them to the pixel mask as bit 9, so nothing shadowed by them is integrated. **On by default** (60 images); `=off` disables. Reflections behind the stop are attenuated but not flagged, so they integrate low with a plausible sigma and no existing rejection catches them |
Geometry:
| Option | Description |
| --- | --- |
| `--estimate-beam-center` | Measure the direct beam before indexing, from the symmetry of the spots where the sweep reaches at least half a turn and from the radial background profile where it does not; the value in the file is kept where neither can measure it. Off by default |
| `--no-fit-spindle` | With the above, keep the rotation axis given in the file instead of fitting its skew about the beam |
Spot finding:
| Option | Description |