A miniCBF header states three things about how the instrument is put together that the reader was assuming instead: which laboratory direction the image's columns run along, which its rows run along, and which the spindle turns about. Some beamlines append a CBF template block holding the full imgCIF axis table, which says all three outright. Two instruments in the corpus are not what was assumed, in two different ways. One mounts its detector a quarter turn round, so the image's columns run vertically. Another turns its spindle about the VERTICAL, with the image mounted the usual way; its table says so, and its "# Oscillation_axis" line says so a second way, by naming the image direction the spindle runs along rather than a vector. Either error leaves the spindle 90 degrees from the image. That is not a sign, so the run's axis-sign rescue cannot reach it, and no refinement recovers it: all three affected sweeps indexed nothing usable. So the table is read. The element axes give the image orientation, matched against the eight discrete mountings exactly as the NXmx module directions already are - the match itself moves to DetectorOrientation, so both readers share one definition rather than two copies. The goniometer axis with no parent gives the spindle DIRECTION; its sign stays the rescue's business, which is the part a convention can legitimately differ on. The detector axis with no parent gives the 2theta arm, replacing the assumption that the arm shares the spindle's axis - the one header stating both states them with the same vector, so this changes no answer, only what it rests on. imgCIF's frame differs from the internal one by a half turn about x, a rotation and not a mirror, as writer/HDF5NXmx.cpp already records from the other side. Where a header carries no table, a "+SLOW" on the Oscillation_axis line still says the spindle runs along the image's slow direction. That is the only thing one of the three affected sets says about it. The axis NAME on that line stays unusable - the header that carries both says "X.CW" where its own table says Y - but the direction token is not: where both are present they agree, which is what makes reading it evidence rather than a guess. Also: naming a frame with no directory at all now finds its sweep. parent_path() of a bare filename is empty and iterating an empty path finds nothing, so running from inside the data directory reported that no images were found. Measured, with nothing on the command line. The vertical-spindle protein set goes from no usable lattice to 100% indexed, P 6(3) 2 2 with a cell 0.43% from deposited, 87846 reflections at 86.3% completeness and CC(1/2) 0.995. Its companion from the same detector, which has no table and only the +SLOW token, goes from a spurious monoclinic cell at 2.3% completeness and I/sigma 0.21 to the right orthorhombic lattice, 97.7% indexed, 59.7% complete, CC(1/2) 0.996. The quarter-turned set's three sweeps, at three arm positions, now all index without the hand-passed quarter turn they needed and agree on one cell to 0.03 A. Six miniCBF sets that state no table and no +SLOW - including one whose Oscillation_axis line names an axis in a third dialect - are byte-identical in .hkl, .mtz, .cif and the image statistics, as are two NXmx sets, which is the shared orientation matcher moving nothing on that path either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f
132 lines
8.7 KiB
Markdown
132 lines
8.7 KiB
Markdown
# Detector geometry
|
||
|
||
At the moment Jungfraujoch supports solely flat detectors. The default option is to place modules in their actual location
|
||
vs. detector frame. It is not recommended to place detector modules stacked.
|
||
|
||
The simplest case is detector perpendicular to the beam. In this case it is enough to provide beam center, detector distance
|
||
and wavelength.
|
||
|
||
For more complex case, one can provide tilt of the detector rotation in PyFAI convention.
|
||
This convention uses Point Of Nominal Interaction (PONI) definition. Beam X and Y would correspond to the location on the detector,
|
||
where beam from the sample is perpendicular to the detector surface and not to the actual direct beam location. Then tilt of the detector
|
||
is defined with three rotation angles: `rot1` (rotating detector right), `rot2` (rotating detector downwards), `rot3` (rotating detector clockwise).
|
||
See [PyFAI documentation](https://pyfai.readthedocs.io/en/stable/) for more details.
|
||
|
||
## What a pixel coordinate means: (0, 0) is the centre of the first pixel
|
||
|
||
Pixel coordinates in Jungfraujoch and rugnux are **0-based and pixel-centred**: an integer coordinate
|
||
is the *centre* of that pixel, so pixel *i* covers [*i* − 0.5, *i* + 0.5) and the sensor spans
|
||
−0.5 … width − 0.5. A beam centre of 948.0 × 546.0 sits in the middle of pixel [546][948], not on any
|
||
of its corners; 948.5 is the boundary between pixel 948 and 949.
|
||
|
||
This holds throughout the code: spot and reflection centroids are intensity-weighted sums of the
|
||
integer pixel indices, the resolution and azimuthal-bin maps evaluate pixel (col, row) at exactly
|
||
(col, row), and a fractional coordinate is turned back into a pixel index by rounding, not by
|
||
truncation. The same convention applies to every coordinate the system exposes — the beam centre
|
||
(`beam_x_pxl`/`beam_y_pxl` in the API and broker configuration, `--beam-x`/`--beam-y` in rugnux,
|
||
`beam_center_x`/`beam_center_y` in NXmx and in the CBOR stream), the spot and predicted-reflection
|
||
positions written to HDF5, and the PONI reported by `--mode calibration`.
|
||
|
||
Other programs place the origin differently, and the difference is worth half a pixel — enough to
|
||
matter when a geometry is copied between programs and then refined:
|
||
|
||
| Convention | Beam centre equivalent to our *x* = 948.0 |
|
||
|---|---|
|
||
| Jungfraujoch, rugnux | 948.0 |
|
||
| XDS (`ORGX`/`ORGY`) | 949.0 — also pixel-centred, but pixels are numbered from 1 |
|
||
| Measured from the edge of the sensor, in length units — pyFAI (`Poni2`, fast axis), DIALS/dxtbx | (948.0 + 0.5) × pixel size, because the centre of pixel *i* is at (*i* + 0.5) × pixel size from the edge |
|
||
| pyFAI `Poni1` (slow axis) | (height − 1 − *y* + 0.5) × pixel size — pyFAI measures the slow axis from the opposite edge, and the `.poni` declares `orientation: 2` to say so |
|
||
|
||
The `.poni` file written by `rugnux --mode calibration` is in pyFAI's frame and so already carries
|
||
that half pixel; the pixel values the same run reports are ours. `Rot3` in that file is our rot3
|
||
negated and turned by 180°: the half turn sets the azimuthal reference, because pyFAI's in-plane axes
|
||
are the negatives of ours. It leaves 2θ untouched, so it moves only the azimuth.
|
||
|
||
## Inside: two axis vectors; outside: rot1/rot2/rot3
|
||
|
||
Internally the detector plane is one orthogonal matrix whose columns are the **fast axis** (the
|
||
laboratory direction of a +1 column step), the **slow axis** (+1 row step) and the **normal** (the
|
||
sample→PONI direction). Every geometry calculation — resolution, azimuth, polarization, prediction,
|
||
refinement — is that matrix applied to the offset of a pixel from the PONI.
|
||
|
||
`rot1`/`rot2`/`rot3` remain the way the tilt is stated from outside, and the two views convert both
|
||
ways: `R = Rz(-rot3)·Rx(-rot2)·Ry(+rot1)` in the internal frame, and back from the columns as
|
||
|
||
```
|
||
rot2 = asin(-slow.z) rot1 = atan2(-fast.z, normal.z) rot3 = atan2(slow.x, slow.y)
|
||
```
|
||
|
||
with `rot2` in [-90°, 90°]. The angles are what is stored and what is written out, so a geometry
|
||
given as angles comes back exactly as it was given.
|
||
|
||
## What a miniCBF header states about the mounting
|
||
|
||
A PILATUS miniCBF gives the geometry twice. The `# ` lines every writer produces carry the distance, the
|
||
beam centre and the angles; some beamlines then append a CBF template block holding a full **imgCIF axis
|
||
table**, which states the laboratory direction of the image's fast and slow pixel directions, of the base
|
||
goniometer axis, and of a 2theta arm where there is one. Where that table is present it is read, in
|
||
preference to any assumption - it is the same information NXmx puts in `fast_pixel_direction` /
|
||
`slow_pixel_direction` and the goniometer `vector`, in the form this format states it.
|
||
|
||
imgCIF's laboratory frame has Z from the sample towards the source and Y opposite gravity, so it differs
|
||
from the internal frame by a half turn about x - a rotation, not a mirror, so an axis carried through it
|
||
turns the same way by the same angle.
|
||
|
||
Two things a header can state that an assumption gets wrong by 90 degrees, which no refinement recovers
|
||
and which the run's axis-sign rescue cannot reach either, a quarter turn not being a sign:
|
||
|
||
* the image mounted a quarter turn round, so its columns run vertically;
|
||
* a spindle that turns about the **vertical** rather than the horizontal.
|
||
|
||
Where a header carries no axis table, a `+SLOW` on its `# Oscillation_axis` line still says the spindle
|
||
runs along the image's slow direction rather than its fast one. The axis *name* on that line is not
|
||
usable - one header says `X.CW +SLOW` where its own table says the axis is Y - but the direction token is,
|
||
and on the header that states both they agree.
|
||
|
||
## A detector swung out on a 2theta arm
|
||
|
||
Chemical crystallography reaches high angle by swinging the detector out on a 2theta arm rather than by
|
||
moving it closer. The arm turns the detector about the sample, so it changes nothing else: the distance
|
||
is still measured along the detector normal, and the beam centre is still the point of normal incidence,
|
||
which is where the arm's own axis meets the detector and does not move. The swing is therefore exactly a
|
||
PONI rotation, and the direct beam is what moves - by `distance * tan(2theta)`, off the beam centre and
|
||
often off the detector altogether.
|
||
|
||
Nothing has to be given for this: rugnux takes it from the file. An NXmx master states the detector's
|
||
position as a `depends_on` chain of transformations, and the arm is one rotation in that chain - so the
|
||
chain is followed, rather than a field of one particular name being looked for. A PILATUS miniCBF states
|
||
it as `# Detector_2theta`, which turns about the same axis as the base spindle, the two being one axis on
|
||
the four-circle geometry those headers describe.
|
||
|
||
## Mirrored and quarter-turned detectors
|
||
|
||
On top of the continuous tilt the detector setup carries a **discrete image orientation**: whether the
|
||
stored image is mirrored in Y, and how many multiples of 90° about the beam it is turned by. It is
|
||
applied to the offset from the PONI before the tilt.
|
||
|
||
The distinction matters because these two operations are exact pixel remappings — an image can be
|
||
shown the right way up without resampling anything — while an arbitrary in-plane rotation cannot.
|
||
`rot3` is therefore reserved for the genuinely arbitrary part: an in-plane angle is **never**
|
||
decomposed into a quarter turn plus a residual, and the discrete part is set only where something
|
||
states it (the detector configuration, `--detector-mirror-y` / `--detector-quarter-turns`, or the
|
||
value a Jungfraujoch-written file records).
|
||
|
||
Both operations leave the distance from the PONI unchanged, so resolution, the solid-angle correction
|
||
and anything else that needs only a radius are unaffected by them. Polarization *is* affected, and
|
||
correctly so: it is computed from the azimuth in the **laboratory**, and what these operations change
|
||
is which pixel index lands at which laboratory azimuth.
|
||
|
||
This is a different setting from the `mirror_y` in the JSON configuration below, which flips the
|
||
**module layout** while the image is being assembled and so decides what the stored pixels are. The
|
||
discrete image orientation changes no pixel at all.
|
||
|
||
## Macromolecular crystallography convention for the vertical direction
|
||
One place of confusion is the convention to have point (0,0) of the detector in the top left corner of the detector,
|
||
with Y values increasing downwards. This is also consistent with computer image formats.
|
||
|
||
However, other techniques (as well as internal operation of PSI X-ray detectors) might follow convention, for point (0,0)
|
||
being in the bottom left corner and Y values increasing upwards. Such a convention is used, for example, by PyFAI.
|
||
|
||
In general, convention is controlled in Jungfraujoch with a setting in the JSON configuration file, which allows mirroring detector in Y.
|
||
|
||
Extra care has to be taken by the user to ensure that no errors are made. |