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
+5 -1
View File
@@ -490,7 +490,11 @@ TEST_CASE("JFJochReader_GridScan", "[HDF5][Full]") {
auto dataset = reader.GetDataset();
REQUIRE(!dataset->experiment.GetGoniometer().has_value());
// A grid scan carries a stationary spindle: NXmx cannot say "no rotation", and a chain of
// translations alone is not readable (dxtbx raises on it). It must not read back as a sweep.
REQUIRE(dataset->experiment.GetGoniometer().has_value());
CHECK(!dataset->experiment.GetGoniometer()->IsScanning());
CHECK(dataset->experiment.GetGoniometer()->GetIncrement_deg() == 0.0f);
REQUIRE(dataset->experiment.GetGridScan().has_value());
CHECK(dataset->experiment.GetGridScan()->IsSnakeScan());