Files
Jungfraujoch/docs/DETECTOR_GEOMETRY.md
T
leonarski_f 680c36c20d
Build Packages / Unit tests (push) Successful in 1h22m15s
Build Packages / build:windows:nocuda (push) Successful in 18m0s
Build Packages / build:windows:cuda (push) Successful in 20m30s
Build Packages / build:viewer-tgz:cpu (push) Successful in 10m32s
Build Packages / build:viewer-tgz:cuda (push) Successful in 11m39s
Build Packages / build:rugnux-tgz (x86_64) (push) Successful in 8m55s
Build Packages / build:rugnux:windows (push) Successful in 11m25s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 20m6s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 16m27s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 20m19s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 15m34s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 20m25s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 19m36s
Build Packages / build:rpm (rocky8) (push) Successful in 17m43s
Build Packages / build:rpm (rocky9) (push) Successful in 13m34s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 21m28s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 18m19s
Build Packages / DIALS test (push) Successful in 12m36s
Build Packages / XDS test (durin plugin) (push) Successful in 6m56s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 6m48s
Build Packages / XDS test (neggia plugin) (push) Successful in 6m7s
Build Packages / Generate python client (push) Successful in 11s
Build Packages / Build documentation (push) Successful in 36s
Build Packages / Create release (push) Skipped
Build Packages / build:rugnux:aarch64 (cross) (push) Successful in 5m11s
v1.0.0-rc.166 (#76)
* `rugnux --mode calibration` writes `<prefix>.json` beside the `.poni`, whose `dataset_settings` member is a `jfjoch_broker` `dataset_settings` body as it stands.
* `rugnux` and `jfjoch_viewer` read PILATUS miniCBF sweeps natively, without conversion.
* Masters written by other facilities open, including Eiger 1.x and third-party NXmx variants.
* `rugnux` measures the beam centre on every run, and indexes with it when the file's value indexes nothing.
* A detector swung out on a 2theta arm is placed where the file says it stands, and the calibration can hold the tilt fixed.
* `rugnux` writes the unmerged MTZ by default, and a P1 merge beside it, so a wrong space group can be re-merged without reprocessing.
* Significant improvements to symmetry handling in `rugnux`: the lattice, the point group, the setting and the systematic absences.
* The `rugnux` report gives the resolution the CC1/2 fit reached, beside the range the reflections were written to.
* The `rugnux` report gives the twinning statistics measured before the space group was decided, beside the ones measured after.
* The `rugnux` report gives the strong-direction diffraction limit, and warns when CC1/2 is not monotone with resolution.
* `rugnux` ranks screw axes on the evidence their absences carry, rather than on how many control reflections a candidate happens to have.
* Twinning is no longer reported when the L-test contradicts it.
* The `rugnux` report gives the detector tilt, the measured tilt and the direct beam beside the beam centre, and a post-refined beam centre is judged against the run's own measurement rather than the file's.
* `--no-refine-tilt` holds the detector tilt at the value in the file, instead of zeroing it, when the calibration starts from the spots.
* The `jfjoch_viewer` grid scan view draws the cells in the proportion of the scan steps, so the map has the shape of the scanned area.

Reviewed-on: #76
Co-authored-by: Filip Leonarski <filip.leonarski@psi.ch>
2026-09-02 21:17:31 +02:00

8.7 KiB
Raw Blame History

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 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.