Stop treating a goniometer axis and a grid scan as alternatives

They are not alternatives: a grid is usually collected at a particular head
position, so an axis and a grid describe different parts of the same setup. The
exclusion was enforced independently in four places - the API converter, the CBOR
serializer, the writer and the reader - and each silently dropped the grid scan
when an axis was present. Nothing warned.

The writer now builds one chain from the base outwards, spindle -> chi -> phi ->
helical -> grid translations, instead of two branches. NXmx applies the deepest
dependency first, so the sample ends up innermost, which is what it physically is:
the grid stage rides on the head and the head rides on the spindle. The grid
translations consequently move inside the rotation - identical to before at
omega = 0, and right rather than wrong when it is not.

A grid scan with no axis at all now writes a stationary omega. NXmx has no way to
say "there is no rotation", and a sample chain of translations alone is not
something readers accept: dxtbx raises outright on it, so every grid-scan master
we have written so far cannot be opened by DIALS. Measured on a file matching the
new chain: dials.import reads it. At 0 degrees the rotation is the identity
whatever the axis points along, so the conventional vector carries no geometric
claim - it only has to be well formed.

The API change is deliberately not breaking: no field changes type or
cardinality, only the prose saying the two were exclusive, and a request that set
both used to lose one silently and now does not.

JFJochReader_GridScan asserted the absence of a goniometer; it now asserts the
axis is present and stationary, which is the contract that matters - a grid scan
must not read back as a sweep.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 22:25:08 +02:00
co-authored by Claude Opus 5
parent b35672a0c3
commit 7039aa4e45
7 changed files with 92 additions and 63 deletions
+1 -1
View File
@@ -52,7 +52,7 @@ There are minor differences at the moment:
| - - axis | Array(float) | Vector for the rotation axis | |
| - - helical_step | Array(float) | Translation for helical scan for 1 image \[m\] | |
| - - screening_wedge | Array(float) | Wedge for screening \[deg\] (increment would correspond to difference between screening points) | |
| grid_scan | object | Grid scan definition (optional and exclusive with rotation axis) | |
| grid_scan | object | Grid scan definition (optional). May be sent together with `goniometer`: a grid is often collected at a given head position, recorded as a stationary axis (step 0) | |
| - n_fast | uint64 | Number of elements along fast axis | |
| - n_slow | uint64 | Number of elements along slow axis | |
| - step_x_axis | float | Step along X axis, can be negative \[m\] | |
+2
View File
@@ -15,6 +15,8 @@ This is an UNSTABLE release. It includes many experimental features, as well as
* rugnux: the detector geometry is also logged in XDS's convention (`ORGX`/`ORGY`, detector axis vectors, rotation axis), so it can be compared directly with an XDS refinement.
* HDF5: a data file missing next to a VDS master now reads as the error-pixel marker instead of zero counts, so those frames are masked rather than silently integrated as blank.
* The writer refuses a stream whose start message declares a different pixel format than its images carry, instead of writing a master that does not describe its own data.
* A grid scan and a goniometer axis are no longer alternatives - both can be set, and the grid scan is no longer silently dropped when an axis is present.
* HDF5: a grid-scan file now records the (stationary) spindle, so it can be opened by programs that require a rotation axis; DIALS refused the previous files outright.
* The rotation axis is read back from HDF5 under whatever name it carries; only `omega` was recognised before, so a sweep recorded as e.g. `phi` re-opened as stills with nothing to say so.
* A goniometer axis that does not turn is now kept rather than discarded, and is distinguished from a rotation sweep - it records where the head was for a still or a grid scan.
* `images_per_file` is now chosen from the acquisition when it is not given: a rotation sweep of at most 20000 images goes into a single data file, a grid scan splits on whole fast-axis rows, and stills and serial keep 1000.