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
+2 -1
View File
@@ -688,9 +688,10 @@ void CBORStream2Serializer::SerializeSequenceStart(const StartMessage& message)
CBOR_ENC(mapEncoder, "series_id", message.run_number);
CBOR_ENC(mapEncoder, "fluorescence", message.fluorescence_spectrum);
// Both, when both are present - the decoder has always read the two keys independently.
if (message.goniometer)
CBOR_ENC_GONIOMETER_MAP(mapEncoder, "goniometer", message);
else if (message.grid_scan)
if (message.grid_scan)
CBOR_ENC_GRID_SCAN(mapEncoder, "grid_scan", message.grid_scan.value());
CBOR_ENC(mapEncoder, "jungfrau_conversion_enabled", message.jungfrau_conversion_enabled);