From 54f2de31bb1fb25e431ea8dfde564b130098b1d6 Mon Sep 17 00:00:00 2001
From: Filip Leonarski
Date: Wed, 2 Sep 2026 11:03:18 +0200
Subject: [PATCH] grid scan: say how the stationary spindle angle is stated,
and pin it
A grid scan is a set of stills at a stationary spindle, and the angle it stood
at is what relates one grid to another taken elsewhere on the circle. Users were
finding an all-zero omega in the file and concluding the angle could not be
recorded at all.
It already can, and has since the goniometer and the grid scan stopped being
alternatives: send the axis with step 0 and its start angle, and that angle is
written per image into the NXmx sample chain, read back by reader/, and taken by
dials.import as a set of stills. Measured on a generated 12-image grid: the
placeholder file carries omega = 0 x 12, the same file with the axis sent at
step 0 carries omega = 90 x 12, and dials.import reports "still: 1, sweep: 0"
for both. Nothing in the code needed changing, so nothing was; what was missing
was that nobody could tell, and that no test held the behaviour down.
So: the API and the HDF5 documentation now say it in as many words, and three
tests pin the three legs the value crosses - the OpenAPI request (which used to
drop the grid scan whenever an axis was present, unpinned until now), the CBOR
start message, and the file round trip.
Also corrects a claim two comments and the HDF5 page were making. NXmx can
express "no rotation" perfectly well - a sample may depend_on "." - so the
placeholder is not there for the standard's sake. It is there because dxtbx
cannot read a sample chain of translations alone: strip the rotation axis from a
grid scan master and dials.import dies in get_dxtbx_goniometer with a matmul
dimension mismatch. Recorded so nobody removes the placeholder on the strength
of the standard.
One thing the change does not fix, because it cannot: a stationary angle is
invisible to DIALS when a grid scan is present. dxtbx picks the first varying
axis as the scan axis, which is a grid translation, so the oscillation reads
(0, 0); and with exactly one rotation axis in the chain it builds a single-axis
goniometer whose fixed rotation is the identity, never consulting the angle. The
same angle IS visible when it is the only candidate (oscillation reads (90, 0))
or when a Smargon head puts a second rotation axis in the chain (the setting
rotation then carries it). The value is in the file and correct either way.
---
broker/gen/model/Grid_scan.h | 4 ++--
broker/gen/model/Rotation_axis.h | 2 +-
broker/jfjoch_api.yaml | 12 +++++++++---
broker/redoc-static.html | 9 ++++++---
common/DiffractionExperiment.cpp | 6 ++++--
docs/CBOR.md | 2 +-
docs/CHANGELOG.md | 1 +
docs/HDF5.md | 8 ++++++++
docs/python_client/docs/GridScan.md | 2 +-
docs/python_client/docs/RotationAxis.md | 2 +-
frontend/src/client/types.gen.ts | 12 +++++++++---
frontend/src/client/zod.gen.ts | 7 +++++--
tests/CBORTest.cpp | 26 +++++++++++++++++++++++++
tests/DiffractionExperimentTest.cpp | 24 +++++++++++++++++++++++
tests/JFJochReaderTest.cpp | 18 +++++++++++++++++
writer/HDF5NXmx.cpp | 11 +++++++----
16 files changed, 123 insertions(+), 23 deletions(-)
diff --git a/broker/gen/model/Grid_scan.h b/broker/gen/model/Grid_scan.h
index 79a3e98bb..b21d28bd3 100644
--- a/broker/gen/model/Grid_scan.h
+++ b/broker/gen/model/Grid_scan.h
@@ -12,7 +12,7 @@
/*
* Grid_scan.h
*
- * Definition of a grid scan. May be combined with a goniometer axis: a grid is often collected at a particular head position, and a stationary axis records where that was.
+ * Definition of a grid scan. Combine it with a goniometer axis to state the angle the spindle stood at: send `goniometer` with `step` 0 and the `start` angle of the grid scan, and that angle is written per image into the NXmx sample transformation chain. Without one the spindle is recorded at 0, which says that nobody stated an angle rather than that the spindle stood at 0.
*/
#ifndef Grid_scan_H_
@@ -25,7 +25,7 @@ namespace org::openapitools::server::model
{
///
-/// Definition of a grid scan. May be combined with a goniometer axis: a grid is often collected at a particular head position, and a stationary axis records where that was.
+/// Definition of a grid scan. Combine it with a goniometer axis to state the angle the spindle stood at: send `goniometer` with `step` 0 and the `start` angle of the grid scan, and that angle is written per image into the NXmx sample transformation chain. Without one the spindle is recorded at 0, which says that nobody stated an angle rather than that the spindle stood at 0.
///
class Grid_scan
{
diff --git a/broker/gen/model/Rotation_axis.h b/broker/gen/model/Rotation_axis.h
index 9352cacc8..237066870 100644
--- a/broker/gen/model/Rotation_axis.h
+++ b/broker/gen/model/Rotation_axis.h
@@ -67,7 +67,7 @@ public:
bool nameIsSet() const;
void unsetName();
///
- /// Angle step (per image) in degrees
+ /// Angle step (per image) in degrees. 0 for an axis that does not turn: the axis then records the angle the spindle stood at, which is how a grid scan or a set of stills states its head position.
///
float getStep() const;
void setStep(float const value);
diff --git a/broker/jfjoch_api.yaml b/broker/jfjoch_api.yaml
index 6da578e5a..7b3fe7587 100644
--- a/broker/jfjoch_api.yaml
+++ b/broker/jfjoch_api.yaml
@@ -241,8 +241,11 @@ components:
schemas:
grid_scan:
description: |
- Definition of a grid scan. May be combined with a goniometer axis: a grid is often collected
- at a particular head position, and a stationary axis records where that was.
+ Definition of a grid scan. Combine it with a goniometer axis to state the angle the spindle
+ stood at: send `goniometer` with `step` 0 and the `start` angle of the grid scan, and that
+ angle is written per image into the NXmx sample transformation chain. Without one the
+ spindle is recorded at 0, which says that nobody stated an angle rather than that the
+ spindle stood at 0.
type: object
required:
- n_fast
@@ -298,7 +301,10 @@ components:
type: number
format: float
example: 0.1
- description: Angle step (per image) in degrees
+ description: |
+ Angle step (per image) in degrees. 0 for an axis that does not turn: the axis then
+ records the angle the spindle stood at, which is how a grid scan or a set of stills
+ states its head position.
start:
type: number
format: float
diff --git a/broker/redoc-static.html b/broker/redoc-static.html
index 0ca215c18..5c88781ce 100644
--- a/broker/redoc-static.html
+++ b/broker/redoc-static.html
@@ -472,8 +472,11 @@ the full width of a slit-defined one. [um]
beam_size_y_um
number <float> >= 0
Second element of /entry/instrument/beam/incident_beam_size in NXmx
Vertical size of the X-ray beam where it meets the sample. [um]
object (rotation_axis)
Definition of a crystal rotation axis
-
object (grid_scan)
Definition of a grid scan. May be combined with a goniometer axis: a grid is often collected
-at a particular head position, and a stationary axis records where that was.
+
object (grid_scan)
Definition of a grid scan. Combine it with a goniometer axis to state the angle the spindle
+stood at: send goniometer with step 0 and the start angle of the grid scan, and that
+angle is written per image into the NXmx sample transformation chain. Without one the
+spindle is recorded at 0, which says that nobody stated an angle rather than that the
+spindle stood at 0.
header_appendix
any
Header appendix, added as user_data/user to start ZeroMQ message (can be any valid JSON)
In general, it is not saved in HDF5 file.
However, if values are placed in "hdf5" object, jfjoch_writer will write them in /entry/user of the HDF5 file.
@@ -993,7 +996,7 @@ then image might be replaced in the buffer between calling /images and /image.cb