* Rugnux: basic support for CCD images (marCCD, SMV) and for gzipped miniCBF. * `jfjoch_viewer`: opens the CCD formats, and fixes to the dataset plots. * Documentation updates. Reviewed-on: #81 Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
44 KiB
Data analysis: indexing and geometry (§4–§7)
Part of the CPU/GPU data-analysis reference; the section numbers are continuous across its four parts.
:local:
:depth: 2
4. Indexing overview
Indexing maps observed reciprocal-space vectors \mathbf{s}_i to a lattice such that:
$
\mathbf{s}_i \approx h_i\mathbf{a}^* + k_i\mathbf{b}^* + l_i\mathbf{c}^*,
$
with integer (h_i,k_i,l_i).
Jungfraujoch supports two complementary indexing strategies:
- FFT-based indexing (Rossmann-type): does not require an a priori unit cell; suitable for unknown samples.
- Fast-feedback indexing (TORO-like): requires an approximate unit cell; optimized for speed and feedback.
Both feed into a common robust refinement/selection stage which maximizes the number of inliers under an indexing tolerance, and which can return more than one lattice per image (multi-lattice indexing; see §5.4).
4.1 Indexed-spot decision (inlier test)
Given a trial lattice with direct basis vectors \mathbf{a},\mathbf{b},\mathbf{c} (used here as reciprocal-space dot-test vectors), fractional indices are estimated by:
$
h_f = \mathbf{s}\cdot\mathbf{a},\quad
k_f = \mathbf{s}\cdot\mathbf{b},\quad
l_f = \mathbf{s}\cdot\mathbf{c}.
$
Let (h,k,l)=(\mathrm{round}(h_f),\mathrm{round}(k_f),\mathrm{round}(l_f)) and define the fractional residual:
$
\delta^2 = (h_f-h)^2 + (k_f-k)^2 + (l_f-l)^2.
$
A spot is indexed if \delta^2 < \tau^2, where \tau is the configured tolerance.
For indexed spots, the reciprocal lattice point \mathbf{p} = h\mathbf{a}^*+k\mathbf{b}^*+l\mathbf{c}^* is used to compute \Delta_\mathrm{Ewald}(\mathbf{p}) (stored as a diagnostic and later used in profile-radius estimation).
A frame is taken to be this crystal's when at least a fraction g = 0.20 of its in-resolution, non-ice spots index. On rotation data that decision is what admits the frame to integration, so its denominator matters: every spot handed to it that is not a reflection of this crystal argues against the frame.
That test decides a frame. Whether a rotation run has a lattice at all is decided on the spots instead. A frame count comes from serial crystallography, where each image is its own experiment; a rotation sweep is one crystal and one orientation matrix, its frames are not independent of each other, and what such a count mostly measures is how many spots happen to land on a frame — a sweep carrying four spots an image cannot reach a six-spot bar on three frames in four however right the lattice is. The refusal therefore compares the fraction of all spots in the sampled frames that the lattice explains against what the same lattice explains when each frame's spots are put at another frame's angle: same lattice, same spots, same detector, same refinement, with only the claim that these spots were seen at these angles removed. That difference is the evidence, and it carries no spots-per-frame number anywhere, so nothing has to be chosen for a crystal that diffracts weakly. Measured over a hundred datasets the permuted level never exceeds 2.6 % and the smallest true margin is seventeen points. --min-indexed-spots (default 6, floor 4 — four is where a lattice stops being fitted by any three spots) still sets the reported indexing rate and the count the first pass ranks candidate lattices by; every rescue and every arbiter still counts frames.
4.2 The spot budget
Only the strongest --max-spots spots of an image are kept (FilterSpotsByCount), and that budget therefore sets the denominator above. Detections are not all reflections — background structure, unlisted ice and detector artefacts are found too — so a budget deeper than an image's reflections makes the test above a measurement of the background rather than of the crystal, and a larger budget can integrate fewer images.
rugnux measures the budget instead of fixing it. With the sweep's lattice in hand, the first pass tallies the spots of a sample of frames by their rank in the intensity-ordered list: how many images carried a spot at that rank, and on how many of them it indexed. Weighting each indexed spot by 1-g and each unindexed one by -g — the same weighting the frame test applies to the list as a whole — the running sum over ranks
E(N) = n_\mathrm{indexed}(N) - g\, n_\mathrm{counted}(N)
rises exactly while the spots at that depth lie on the lattice more often than g, and falls after. The budget is \arg\max_N E(N). Its meaning is "as deep into the list as the image is still showing reflections of this crystal": deeper spots cannot help the frame test and can only push a frame towards rejection. On crystals whose spot lists are reflections all the way down the maximum is at the end of the list and the budget is unchanged.
The peak has to be one. Under the null — the spots lie on the lattice at the same rate at every depth — E is a driftless random walk in the counted spots, with per-spot variance g(1-g), and the maximum of such a walk is positive whatever the data; an \arg\max taken on its own would shorten every dataset, including one with nothing to shorten. What the budget acts on is the fall from the peak to the end of the list, E(N^*) - E(L), which is the maximum of the same walk read backwards; by the reflection principle its null law is P(\mathrm{fall} > z\sqrt{g(1-g)T}) = 2(1-\Phi(z)) over T counted spots in all, so the search over ranks is already accounted for and no further multiple-comparison correction applies. The budget is taken only where the fall clears that bar at z = 3.29, one false shortening in a thousand measurements; otherwise the whole list is kept.
5. FFT indexing (unknown unit cell)
FFT indexing follows a classical approach: detect dominant periodicities by projecting reciprocal-space points onto many directions and Fourier transforming the resulting 1D histograms.
5.1 Directional projections and histograms
Choose a set of unit vectors \{\mathbf{u}_d\} on a half-sphere (a near-uniform distribution generated via a golden-angle construction). For each direction d, form a histogram in the scalar projection:
$
t_{id} = \left|\mathbf{u}_d\cdot \mathbf{s}_i\right|.
$
Bin width is chosen approximately as:
$
\Delta t \approx \frac{1}{2 L_\mathrm{max}},
$
where L_\mathrm{max} is the maximum expected real-space unit-cell edge (Å). The histogram extent is tied to the maximum q used (set by a high-resolution cutoff for indexing).
5.2 FFT peak picking and candidate vectors
For each direction, the FFT magnitude spectrum is computed; peaks correspond to periodicities along \mathbf{u}_d. Each direction yields a candidate real-space length L chosen not by raw magnitude but by maximum prominence above a running-mean local background (subtracting the broad low-frequency envelope that otherwise dominates on weak or pink-beam frames), subject to L\ge L_\mathrm{min}.
The running-mean background window keeps a constant width and is slid inward at the ends of the spectrum rather than truncated there, so a peak within half a window of either end — which is where the longest cells sit — is judged against as much background as any other. Both window bounds stay monotonically non-decreasing in the bin index, so the GPU kernel's running sum is still valid.
The longest basis vector the transform can return is fft_max_unit_cell_A, since the histogram is sized from it and its last usable bin is that length; the shortest is fft_min_unit_cell_A (rugnux --fft-min-unit-cell, default 10 Å), below which a candidate is discarded. The defaults are unchanged (500 Å and 10 Å), but the accepted range for the maximum now reaches 1200 Å, and a reference cell given with -C moves both bounds on its own — up to reach a long axis, down to admit a small-molecule cell — since a cell the search cannot represent cannot be found by it.
Candidate vectors are \mathbf{v}_d = L_d\,\mathbf{u}_d.
A collinearity filter removes nearly parallel vectors (e.g. within 5°) and attempts to resolve harmonic ambiguity: shorter “fundamental” vectors may be preferred over longer harmonics if their peak magnitude is sufficiently strong relative to the dominant peak.
5.3 Lattice reduction and cell candidates
Triples of candidate vectors are combined to form candidate bases (\mathbf{A},\mathbf{B},\mathbf{C}), each reduced to its Niggli-reduced cell (Gruber-vector reduction) before comparison, and filtered by allowed length and angle ranges. Two passes are run: a standard pass forms shortest-vector triples from the ~30 strongest filtered directions; if the best cell then indexes fewer than half the spots, a widened fallback anchors the two shortest axes and lets the third range over up to ~60 candidate vectors (deduplicated by Niggli cell), catching large, elongated or superstructure cells the first pass misses.
A triple whose three vectors are coplanar is rejected before refinement. The length and angle filters cannot see it — any flat combination satisfies them — and a cell that flat has a metric determinant small enough for float to get its sign wrong, after which the guard against a negative argument to the square root places \mathbf{c} in the $\mathbf{a}$-\mathbf{b} plane, the reciprocal volume diverges and the solver reports a not-a-number Jacobian. The test is the volume fraction |V|/(|\mathbf{a}||\mathbf{b}||\mathbf{c}|), which must reach 0.02 — about 1.1° off flat, well below the flattest genuine candidate observed and far above where float loses the sign — and it is applied both where triples are produced and at the optimizer's entry points.
A shortlist confined to one plane cannot close a cell at all, and the row it is missing is the plane normal. That is detected from the eigenvalue ratio of the shortlisted directions' scatter matrix, and one further transform is then spent with the same direction count inside a narrow cap about the normal. More directions do not substitute for it: at the exact true direction a very long axis can still rank far below the shortlist cut, so for this rescue the obstacle is the ranking rather than the sampling, and a denser grid costs several times the device memory for the same answer.
Sampling has a limit of its own, and it binds well before the 1200 Å the accepted range for fft_max_unit_cell_A admits (§5.2). A direction off a real-space axis of length a by an angle \theta smears each projected lattice plane by about \theta/d_\mathrm{min} in the projection, so the planes (spacing 1/a) stay resolved only while \theta \lesssim d_\mathrm{min}/(2a). The shipped grid of 16384 directions puts the nearest one within about 0.6° of any axis, which satisfies that bound only up to roughly 120–150 Å at typical indexing resolutions; a longer axis is not refused but returned as a plausible sub-cell or harmonic. Raising the maximum cell alone therefore does not extend the reach — the direction grid has to resolve the axis before the histogram can represent it.
5.4 Robust refinement and best-cell selection
Candidate bases are refined against observed spots using an iterative inlier‑focused least‑squares procedure (trimmed/contracting threshold). Candidates are then ranked:
- more indexed spots wins — unless two candidates index within ~10 % of each other, in which case
- the smaller-volume cell is preferred (when the volumes differ by more than ~5 %), avoiding a doubled supercell, then
- the smaller refinement score, then the spot count again.
Selection is not limited to a single lattice: after the best cell is accepted, further lattices are added as separate crystals provided fewer than ~40 % of their indexed spots overlap an already-accepted lattice (up to two extra by default), so split or multi-lattice crystals are indexed rather than discarded.
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 (the results report). 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”)
If the space group is supplied by the user, its lattice constraints are assumed for refinement and subsequent processing.
If not, Jungfraujoch attempts to infer the most plausible Bravais lattice type from the metric tensor after Niggli reduction:
- Niggli reduction is performed to obtain a reduced cell in
G^6representation (Gruber vector). - The reduced cell is compared against a list of Niggli classes corresponding to Bravais lattices and centerings.
- The highest-symmetry class that matches within tolerances is selected (relative metric tolerance and angular tolerance). The list is walked in order of decreasing symmetry and the first class that fits is taken, so a class that only just fits can pre-empt a lower-symmetry one that fits exactly.
The output includes:
- a conventional cell,
- crystal system (triclinic, monoclinic, …),
- centering symbol (one of
P, C, I, F, R; theA/Bvariants are not emitted here — they are handled only later as prediction absences, §8.4).
This stage provides centering information used for systematic absences in prediction (§8.4) and for reporting.
A metric symmetry has to earn itself. The class is chosen from the unrefined candidate against a fixed angular tolerance (3°), so a lattice that is pseudo-symmetric to a few tenths of a degree is promoted a class too far — and the constraint then snaps a real angle to the ideal one, which throws nearly every reflection of every frame outside tolerance. Measured on a monoclinic crystal pseudo-C-orthorhombic to 0.42°, the promoted cell indexed 2 of 60 frames where its own primitive cell indexed 39: the same lattice, \mathbf{b}_{oC}=-(\mathbf{a}+2\mathbf{c}), at exactly twice the volume. A more accurate candidate is more likely to be promoted, not less: a run escapes the promotion only when the raw candidate misses the tolerance window.
The lookup can also land short: near the Niggli type-I/type-II boundary the character is decided by the last digits of the refined cell, and the class it names caps which point groups the space-group search enumerates. The metric-symmetry re-ask of §13.1 covers this — rotations the named class has no room for are put to the intensities directly, and where the metric group exceeds the class's holohedry the merge is reindexed into the metric cell and the search re-run there.
The rotation first pass refines the twelve best candidate lattices rather than four. The pre-refinement indexed fraction is an unreliable ranking, so a correct cell can sit below several degenerate ones and never be refined at all.
For rotation data the first pass therefore refines the constrained cell and an unconstrained (triclinic) one from the same spots — which it finds itself, over a sample spread across the sweep, rather than reading what the acquisition wrote — and settles the two on how many of a fixed set of validation frames each actually indexes. The bar is a clear majority, not a margin. An unconstrained refinement holds no cell parameter fixed, so it can only index at least as many frames as the constrained one, and on genuine symmetry it indexes a few more — a percentage margin therefore demotes real lattices (measured: a genuine $I$-centred orthorhombic to P1). Only a constrained cell that fails outright while its unconstrained cell works distinguishes a false promotion. The intensities settle the space group later regardless (§13).
Two further hypotheses are weighed at the same point, both by default. Where two first-pass answers
have primitive volumes in a small integer ratio (2–4×) and tie on the validation frames, the frame
count has saturated — a spurious axis multiple indexes every frame its true sub-cell does — so the
tie is settled at the granularity that does not saturate: which cell accounts for more of the
validation frames' spots. That comparison leans toward the smaller cell by construction (indexing
is a fractional-Miller test, so multiplying an axis by n multiplies that axis's residual by n),
and only a reflection class the larger cell genuinely adds — a real superstructure's satellite rows
— can pay for the loss; the occupancy of that added class is computed and logged beside the decision,
deliberately without a threshold, but the spot count is what decides. Separately, the 10 Å floor of
§5.2 is applied to the FFT's per-direction peak search, so a lattice row whose true repeat is below
it is reported at its first harmonic and the cell assembled from those harmonics is an exact integer
supercell of the true one; a second first-pass hypothesis with the floor lowered to 5 Å therefore
also runs (rotation only, and not when a cell was given — -C already lowers the floor to cover it),
and its answer is adopted only when it is an integer sub-cell of the standing one and ties or beats
it on the validation frames. Everywhere else the standard pass's answer stands.
A first pass that ends with no usable lattice tries a leaner, shallower one. A crystal sitting in a crystalline powder floods the pass with spots that are not its own — measured, 2112 spots a frame against the 80 a clean crystal on the same beamline gives — and the FFT then takes its cell out of the powder shells. So the pass is retried over how much of each frame it reads: the strongest 30, 80, 200 or all spots an image, each at the file's resolution and at the resolutions a quarter and a half of this sample's own spots lie coarser than, with the rings measured in §3.3 set aside where there are any. The rungs are decided late, on the validation-frame count — the way the rotation-axis sign already is — and one is adopted only on a win of a sixth of the frames. The same ladder is asked inside the beam-centre check of §1.4, where a centre that is wrong and a spot list that is too deep otherwise hide each other.
The frame count cannot arbitrate an axis harmonic, though: a cell twice as long must place every spot twice as accurately to score the same, and across rungs the bias compounds, since the leanest and shallowest rung is both the one a halved axis scores best on and the one a smaller cell is easiest on. The winning rung is therefore put through the same harmonic arbiter as the hypotheses above — which of two cells accounts for more of the validation frames' spots — against every rung that cleared the bar and whose primitive volume is a near-integer multiple of the winner's. A winner that loses that question is a sub-multiple, and the ladder then adopts nothing rather than promoting the rival, which on the crystal this was measured on is a multiple of the true axis in its own right.
Note. In ambiguous or special cases, forcing space group to P1 (no symmetry assumptions) is recommended.
7. Geometry and lattice refinement
Refinement adjusts experimental geometry and crystal parameters to minimize discrepancies between observed spot reciprocal vectors and those predicted by a lattice model with integer indices.
7.1 Parameterization
The refinement jointly optimizes, depending on mode and constraints:
- beam center
(x_\mathrm{beam}, y_\mathrm{beam}), - detector distance
D, - detector tilt angles (two-angle model; third rotation often held at 0),
- rotation axis direction (for rotation datasets),
- crystal orientation (a global rotation),
- unit-cell parameters, with constraints determined by inferred crystal system.
The detector distance is not refined against one crystal's spots at all: the positional residual leaves it degenerate with the cell scale, so it is fitted elsewhere - by the rotation post-refinement, which frees it alongside the whole crystal and adds the distance-independent rocking-angle residual that breaks the degeneracy, and by the stills --refine-geometry bundle. Per image, the beam centre and the crystal orientation are refined, and the unit cell as well for stills. The first-pass rotation indexing refines the detector tilt and the rotation-axis direction too, against the spots accumulated across the sweep; everywhere else both are held fixed, because on a single crystal a tilt is absorbed almost exactly by the crystal orientation. A lighter orientation-only mode refines just the crystal orientation, for stills whose geometry is already trusted. It carries a weak small-rotation prior penalising the whole angle-axis vector (all three components, at a low weight); what it is there for is the poorly-determined out-of-plane component, which is the one the data barely constrain.
For higher symmetries, constraints are enforced, e.g.
- cubic:
a=b=c,\ \alpha=\beta=\gamma=90^\circ, - tetragonal:
a=b, - hexagonal:
a=b,\ \gamma=120^\circ, - monoclinic (unique axis
b):\alpha=\gamma=90^\circ,\betarefined.
7.2 Residuals and objective
For each indexed spot assigned integer (h,k,l), compute:
- observed reciprocal vector
\mathbf{s}_\mathrm{obs}from its detector position and current geometry, - predicted reciprocal vector
\mathbf{s}_\mathrm{pred}(h,k,l;\ \text{lattice params}).
Residual is: $ \mathbf{r} = \mathbf{s}\mathrm{obs} - \mathbf{s}\mathrm{pred}. $
A non-linear least squares solver minimizes \sum \|\mathbf{r}\|^2 over all selected inlier spots.
7.3 Rotation datasets: bringing observations to a common reference frame
For oscillation/rotation data, each image corresponds to a rotation angle \phi about an axis \mathbf{m}_2. Observed reciprocal vectors are rotated “back to start” so that all images are refined in a single reference crystal frame:
$
\mathbf{s}\mathrm{obs,ref} = R(\phi),\mathbf{s}\mathrm{obs},
$
where R(\phi) is the rotation by +\phi about the goniometer axis as stored in the file. The sign is a convention and it is load-bearing: rotating the observations forward by +\phi means the crystal itself turns by -\phi about that stored axis, i.e. R(\phi) is the inverse of the crystal's own rotation from the reference orientation to frame \phi. The same convention is why the unmerged-MTZ batch headers and the XDS geometry echo carry the axis negated relative to the input file (Rugnux ▸ the unmerged export) — a reimplementation that takes R(\phi) as the crystal rotation must use R(-\phi) here instead. The angle \phi is taken at the centre of each frame's oscillation (the frame angle plus half the oscillation width).
7.4 Multi-stage tightening of inlier tolerance
Refinement is performed in stages with decreasing acceptance tolerance for including reflections (three stages, indexing tolerance 0.3\to0.2\to0.1), which stabilizes convergence when starting from imperfect indexing and approximate geometry.
The loose first stage necessarily admits some spots that are not reflections of this lattice — the fraction of randomly placed spots inside a fractional-Miller tolerance t is \tfrac{4}{3}\pi t^3, i.e. 11 % at t=0.3 — and an unweighted fit lets them pull the orientation. Each residual is therefore weighted by how strong its spot is for its resolution: the frame's spots are cut into equal-count resolution shells and each intensity is divided by its shell median, mapped to w^2=r/(1+r). The shell normalisation is what makes this safe — genuine high-resolution spots are legitimately weaker and carry the cell and distance information, so an un-normalised intensity weight would suppress exactly the spots the fit needs. The weight is a property of the spot and never of the current residual, so it does not depend on how far the geometry is from convergence.
The rotation chain commits its best round, not the round it stops on. After the winning candidate is selected, the refinement is run again — solve, re-accumulate the reciprocal-space cloud under the refined geometry, solve again — up to twenty times, and the loop stops on a test of the detector-tilt step. The chain is a trajectory and its last point is not always its best one: every solve ends by fitting only the spots inside its tightest gate, so a cell with a direction the data barely constrain (which is what a free cell whose metric is near a Bravais class has) can slide along it, pulling a core of spots tighter while the periphery falls out of the fit altogether. Measured on such a chain, the spots inside the tight gate rise over seventeen rounds while the spots inside the widest gate peak at round three and fall away — and round three is the round that merges at ISa 11.0 against 6.3 and R_\mathrm{meas} 0.148 against 0.213. Nothing the run consulted could see it: indexed fraction, validation frames, validation spots and the tight-gate count all prefer the overfitted end.
So every round is scored on the widest gate — the population the first pass selects on and the last pass does not fit, which makes it the one a converged solve is not optimising — and the best-scoring round is committed. Two conditions keep that from acting on noise. The score is a count of spots, so a lead of fewer than \sqrt{\text{count}} of them leaves the last round standing. And the round taken has to be the less distorted lattice as well as the better-fitting one: the lattice search (§6) is re-asked every round to measure how far the cell sits from the ideal metric of the class it matches (imposing that class is measured fatal — the snap puts almost everything outside the refinement's own gate), and an earlier round is taken only when it matched the same class and sits closer to it. Same class is a precondition and not a precaution: the deviation is a fraction of whichever class's tolerance admitted it, so two classes' deviations are not the same quantity, and a round that matched no class reports zero, which means "nothing was asserted" rather than "undistorted". A chain that has settled scores its rounds within a spot or two of each other and a symmetry-constrained solve holds its distortion at zero throughout, so the rule fires on neither: measured over 914 chains, an earlier round scores higher on 44 % of them and the committed round changes on 1 dataset in 54.
7.5 Rotation geometry post-refinement (two-pass)
The refinement above (§7.2) runs per image against that image's spots. For rotation data an additional post-refinement (on by default; --rotation-no-postrefine disables it) improves the detector distance, beam centre and crystal cell/axis using all frames at once, then re-integrates:
-
Pass 1 integrates, scales and merges at the header geometry.
-
From pass-1's integrated reflections, the crystal and the detector are refined together over all frames (Ceres, robust loss) in one joint fit, against both residuals at once:
- the positional detector↔reciprocal residual at each partial's observed spot, and
- a distance-independent Ewald excitation residual at each reflection's observed rocking centroid
\phi_\mathrm{obs}.
Free: the crystal orientation, the unit cell (every parameter the crystal system leaves free, not one overall scale), the goniometer-axis direction, the detector distance and the beam centre. The positional residual on its own is degenerate with the cell scale — that is why this used to be split into a cell-scale step and a distance step — but the excitation residual does not involve the detector at all, so it fixes the absolute size of the reciprocal lattice and breaks the degeneracy inside the same problem. Splitting it instead cost accuracy twice over: pass 1 frees the whole lattice against a frozen distance, so the distortion it absorbs is anisotropic and no single scale can undo it; and whatever bias is left in that scale goes straight into the distance, which is only ever determined relative to the cell.
The fit is cross-validated on a deterministic split of the reflections (an avalanche-mixed
hklhash, not a frame split and not anh+k+lparity, which would collide with a centering condition and leave the held-out half empty): fitted on one half, committed only if it lowers the held-out residual — both families of it, since the excitation residual is the only evidence of the cell scale and the positional values outnumber it about three to one — and the move stays inside its bounds: every free cell angle within 1° and the beam centre within 15 px of the nearest centre anything already believes.The distance and the cell lengths are bounded one step at a time, not as a whole. One per cent was once a cap on the entire move, and as a cap it was the opposite of its job — a header is most worth correcting when it is most wrong, and a geometry genuinely several per cent out could never be reached (measured: a refused fit of 310.000 → 305.692 mm whose cell landed within 0.06 % of the deposited one). It is a trust region instead. The first solve is asked in the wide box around nominal exactly as before, so a fit that settles within one step commits unchanged; a fit that wants more is re-fitted as a walk of one-per-cent steps, each seeded where the last arrived and each required to lower the held-out residual, stopping where a step stops paying. A walk that uses every step it is allowed has not settled — it stopped because it ran out of steps, not because it arrived — and is refused, which is the runaway the cap stood in for, tested where it can be seen. A move of more than one step is additionally ratified by re-indexing at where it arrived: that is what separates the failure the cap was really aimed at (a second lattice, whose spots bias every cross-validation fold identically) from a wrong header, since a second lattice does not index better at the new geometry and a real distance error does.
The geometry the run commits is re-fitted on all the reflections once the held-out half has approved it — a walk from where it arrived, one step wide; everything else from nominal in the wide box — and the bounds are asked again of that fit rather than only of the half that earned it. A move outside them leaves the geometry at nominal, as every other refusal does, and the refusal says so in the report (
POSTREFINE_REFUSED, with what the fit wanted) rather than passing silently. Detector tilt is held fixed, being gauge-coupled to the crystal orientation on a single crystal. -
Pass 2 re-indexes de novo and re-integrates at the committed geometry. Only the detector distance and beam centre carry over: the refined cell, orientation and axis are what make the distance identifiable, but pass 2 re-indexes from scratch, so they are not propagated. Where that re-index indexes too few frames the run falls back to pass 1's lattice and integrates it at the refined geometry — and the cell is then scaled to the distance it will be used at, since a real-space cell is measured against the distance its spots were seen at, and carrying it across a distance change otherwise scales the whole cell by the ratio of the two. The orientation is untouched.
The space group is determined after pass 2, on the geometry the run refined, and pass 1 does not search at all: a decision taken on the worse of the two passes and then carried forward is a constraint on the better one, and would have to be reconciled with what pass 2 later found. The guard that chooses which pass is written compares each pass's first merge — P1 on both sides, full resolution range, before the correction surfaces — which both passes produce anyway, so it never compares statistics computed in two different space groups. What it compares there is the signal each pass measured: the count of unique reflections merged at I/\sigma \ge 2. Self-consistency cannot do this job — against an external arbiter the signal count named the more accurate geometry on 23 of 27 arm-dataset pairs where R_\mathrm{meas}, CC_{1/2} and ISa managed 13, a coin flip — and that merge's own CC_{1/2} least of all, being pooled over the whole range, uncut and uncorrected, so the shells with no signal in them dominate it and they are exactly the shells a geometry move disturbs (it reads 0.13 on a crystal whose data merge at 0.995). The refined pass is sent back only where it merges more unique reflections than its cell can hold, where it measured decisively less signal (10 %, and only where it holds no more reflections either — a wider integration disk pulls weak reflections in and dilutes the strong fraction without measuring less), or where it lost the axial rows the systematic absences are read off. One index-time veto remains and is keyed to pass 1's lattice rather than its group: a centred pass-1 lattice against a primitive pass-2 one.
Only pass 2 is written, as the canonical <prefix>_* output. Pass 1's merge exists to give the guard something to judge pass 2 against, so it stops short of the parts of the merge that only fill in a file — the correction surfaces, the twinning and radiation-damage analyses, the R-free flags and the amplitudes — and writes no merged files of its own.
Goniometer rotation scale (report only). A stage that turns further than it was commanded to leaves no trace in the file, because the stored \omega values are the commanded ones; the excess then presents as the crystal drifting, in this program and in others. The excitation residual already measures it without a new degree of freedom: it rotates by -\phi\,\mathbf{u} with \mathbf{u} an unnormalised 3-vector, so |\mathbf{u}| is the factor by which the stage actually turned, and normalising the axis throws it away. It is reported, and warned about beyond 0.5 %, under its own leave-a-fifth-of-the-sweep-out check — a fold that merely soaked up noise cannot raise the flag. It is a detector, not a calibration: nothing corrects the data, and it under-reads the true magnitude, because the fit only sees reflections that indexed at the nominal angle and per-frame orientation refinement has already absorbed part of the error.
7.6 Detector geometry from powder rings
Everything above fits the geometry to Bragg data, where the beam centre is the weakest parameter: it is gauge-coupled to the crystal orientation, which is why §7.5 bounds it to within 15 px of a centre something already believes rather than letting the spots place it freely. A powder ring has no orientation to be coupled to. Where it falls on the detector depends on the geometry and on nothing else, which makes a calibrant — LaB₆, silver behenate, CeO₂, silicon — or even ice an independent constraint on exactly the quantity Bragg data cannot pin.
The ring positions are matched to the observed rings and the geometry is refined (Ceres, five parameters: beam centre, distance, and the two detector tilts) so that the |s| predicted at each observed ring point matches the ring it belongs to. The two tilts can be held fixed (rugnux --no-refine-tilt, the viewer's Refine detector tilt tick box), leaving a three-parameter fit: a tilt a downstream program cannot express is better left out of the fit than refined and then dropped, since the centre and the distance of a tilted fit have already absorbed it.
Calibrants. LaB₆, silver behenate, CeO₂ and silicon are held as unit cells and their rings enumerated from them. Ice is held as the hexagonal-ice ring positions of §3.3 instead — measured to 1.522 Å, calculated below it — because hexagonal ice is P6_3/mmc with oxygen on 4f and enumerating hkl from its cell would emit rings the oxygen sublattice extinguishes. A calibrant is therefore a list of ring q values throughout, not a cell.
What a ring can and cannot determine. A ring is a conic centred on the beam, so a wrong centre makes its apparent radius oscillate once per turn, r(\phi)=R+\delta_x\cos\phi+\delta_y\sin\phi, with the same amplitude on every ring. A detector tilt \beta produces a \cos\phi term too — not the \cos2\phi one might expect — but one that grows as the ring's radius squared, r(\phi)=R+(R^2/F)(\beta_x\cos\phi+\beta_y\sin\phi); the true \cos2\phi term is O(R^3\beta^2/F^2), hundredths of a pixel. The two are therefore separated by how the amplitude scales with radius, which needs at least two rings — on a single ring they are exactly degenerate. None of this uses the calibrant's $d$-spacings, so the centre is determined without assuming anything about the standard.
The distance is different: it follows from r=F\tan2\theta with \sin\theta=\lambda/2d, so a fractional error in the lattice constant passes straight into it, and the $\lambda$–F pair is separated only by the curvature of \tan(2\arcsin(\lambda/2d)) across the rings — \partial\ln r/\partial\ln F=1 at every ring against \partial\ln r/\partial\ln\lambda=4\tan\theta/\sin4\theta, which runs from about 1.05 at low angle to 1.43 at high. That lever collapses as the detector moves back and the rings crowd into small 2\theta, so distance is a short-distance measurement and the wavelength is better calibrated by other means.
Reading the rings. The ring points come from one of two measurements, both accumulated over every processed image rather than one. The default reads the azimuthally-binned profile (§2) summed over the run: for each ring and each azimuthal sector, the radial peak is fitted against a locally interpolated background and the measured (q,\phi) mapped back through the current geometry to the pixel it came from. The alternative pools the spot lists, which samples each arc wherever the spot finder's threshold happens to bite. The accumulated profile is the same size however many images went into it; the pooled spot list is capped, each image contributing an equal share.
A plain radial profile — one azimuthal sector — has averaged the ring over every direction and carries no centre at all, so the profile route requires at least four sectors and uses 32 by default. Sixteen to thirty-two are enough; beyond that the limit is the ring's own texture, not counting statistics.
The extraction window around a ring is capped at half the gap to its neighbour, because the background under a peak is taken from the ends of that window: hexagonal ice has a triplet of rings (1.947, 1.916 and 1.882 Å) whose neighbours sit only 0.05–0.06 Å⁻¹ apart in q = 2\pi/d, which a fixed window merges into a single peak. Where only one ring is in reach the two tilts are held at their input values rather than fitted, since on a single ring they are degenerate with the centre (above) and the fit would otherwise trade the centre away for them.