Build Packages / build:windows:nocuda (push) Successful in 17m20s
Build Packages / build:windows:cuda (push) Successful in 19m52s
Build Packages / build:viewer-tgz:cpu (push) Successful in 9m38s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m18s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 9m34s
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m42s
Build Packages / HDF5 consumer tests (DIALS, XDS) (push) Successful in 20m33s
Build Packages / Create release (push) Successful in 33s
Build Packages / build:rugnux:windows (push) Successful in 12m0s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 15m42s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 14m59s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m8s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 14m35s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 16m55s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 16m58s
Build Packages / Generate python client (push) Successful in 16s
Build Packages / build:rpm (rocky8) (push) Successful in 15m21s
Build Packages / Build documentation (push) Successful in 54s
Build Packages / build:rpm (rocky9) (push) Successful in 16m23s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 12m2s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 10m6s
Build Packages / Unit tests (push) Successful in 1h10m26s
* 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>
133 lines
8.8 KiB
Markdown
133 lines
8.8 KiB
Markdown
# Detector geometry
|
||
|
||
At the moment Jungfraujoch supports solely flat detectors. The default option is to place modules in their actual location
|
||
relative to the detector frame. It is not recommended to place detector modules stacked.
|
||
|
||
The simplest case is a detector perpendicular to the beam. In this case it is enough to provide beam center, detector distance
|
||
and wavelength.
|
||
|
||
For a more complex case, one can provide the detector tilt in the 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.
|
||
|
||
There are two things a header can state that an assumption gets wrong by 90 degrees — an error no
|
||
refinement recovers, and one 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 `mirror_y` in the JSON configuration file (described 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 a convention where point (0,0)
|
||
is in the bottom left corner and Y values increase upwards. Such a convention is used, for example, by PyFAI.
|
||
|
||
In general, the convention is controlled in Jungfraujoch with a setting in the JSON configuration file, which allows the detector to be mirrored in Y.
|
||
|
||
The convention in use is worth checking whenever a geometry is carried between programs. |