Write module_offset as a float, and declare offset_units
Build Packages / build:viewer-tgz:cpu (push) Successful in 13m34s
Build Packages / build:viewer-tgz:cuda (push) Successful in 15m43s
Build Packages / build:rpm (rocky9_nocuda) (push) Successful in 19m43s
Build Packages / build:rpm (rocky8_nocuda) (push) Successful in 23m14s
Build Packages / build:rpm (ubuntu2404_nocuda) (push) Successful in 18m44s
Build Packages / build:rpm (ubuntu2204_nocuda) (push) Successful in 24m21s
Build Packages / build:rpm (rocky9_sls9) (push) Successful in 18m50s
Build Packages / build:rpm (rocky8_sls9) (push) Successful in 22m37s
Build Packages / build:rpm (rocky9) (push) Successful in 19m37s
Build Packages / XDS test (durin plugin) (push) Successful in 11m11s
Build Packages / build:rpm (ubuntu2404) (push) Successful in 19m20s
Build Packages / build:rpm (rocky8) (push) Successful in 26m9s
Build Packages / Generate python client (push) Successful in 39s
Build Packages / Create release (push) Skipped
Build Packages / Build documentation (push) Successful in 1m13s
Build Packages / build:rpm (ubuntu2204) (push) Successful in 24m8s
Build Packages / XDS test (JFJoch plugin) (push) Successful in 9m22s
Build Packages / XDS test (neggia plugin) (push) Successful in 7m20s
Build Packages / DIALS test (push) Successful in 19m31s
Build Packages / Unit tests (push) Failing after 1h22m37s
Build Packages / build:windows:nocuda (push) Successful in 11m9s
Build Packages / build:windows:cuda (push) Successful in 13m51s

Two latent traps in the NXtransformations attributes, both inert today and both
wrong the moment they are not.

module_offset was an int32 with @vector = (0,0,0). NXmx types the field NX_FLOAT,
and a translation needs a unit vector to be well formed - the direction of a
zero-magnitude translation is arbitrary, not absent. Now a float with (0,0,1);
the transformation it describes is unchanged, since the magnitude is still zero.

Every transformation that carries an @offset wrote it without @offset_units, and
every caller passed an empty string. A reader then falls back to the axis's own
`units` - which on a rotation axis is degrees - and converts a length from
degrees to millimetres. nxmx only performs that conversion when the offset is
non-zero, so ours have never triggered it, but the first non-zero offset on a
goniometer axis would. The helper now always declares it, rather than leaving it
to a caller to remember.

Measured after the change: module_offset is H5T_IEEE_F32LE, a rotation axis
carries offset_units "m" beside units "deg", dials.import still reads the file,
and the tilted-geometry cross-check is unchanged at 1.6e-6 mm.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-22 23:14:40 +02:00
co-authored by Claude Opus 5
parent 842c43a86e
commit 7efcf631de
3 changed files with 12 additions and 3 deletions
+1
View File
@@ -15,6 +15,7 @@ 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.
* HDF5: `module_offset` is written as a float with a proper unit vector, and every transformation offset declares `offset_units`, so a reader does not fall back to the axis's own units - degrees on a rotation - when interpreting a length.
* The image stream carries the sample transformation chain (`transformations`) in mounting order, so a goniometer axis, the Smargon chi/phi and a grid stage can be described together and unambiguously.
* Smargon chi/phi are written for a still as well, and are read back from HDF5; before, they were dropped unless the run also had a rotation axis or a grid scan, and nothing read them.
+6 -2
View File
@@ -584,9 +584,13 @@ void NXmx::DetectorModule(const std::string &name, const std::vector<int32_t> &o
"", "", "translation", slow_axis,
{0,0,0}, "");
SaveScalar(module_group, "module_offset", 0)->
// The module origin coincides with the detector origin, so the offset is zero - but it is still
// a translation, and NXmx types module_offset NX_FLOAT. Write a float with a proper unit vector
// rather than an integer with a zero-length one, which is degenerate: the direction of a
// zero-magnitude translation is arbitrary, not absent.
SaveScalar(module_group, "module_offset", 0.0f)->
Transformation("m", "/entry/instrument/detector/transformations/" + nx_axis,
"", "", "translation", {0,0,0});
"", "", "translation", {0, 0, 1});
}
void NXmx::Facility(const StartMessage &start) {
+5 -1
View File
@@ -672,7 +672,11 @@ HDF5Object& HDF5Object::Transformation(const std::string& units, const std::stri
const std::vector<double> &offset, const std::string& offset_units) {
Transformation(units, depends_on, equipment, equipment_component, transformation_type, vector);
Attr("offset", offset);
if (!offset_units.empty()) Attr("offset_units", offset_units);
// An offset is a length, whatever the axis is. Without this attribute a reader falls back to
// `units`, which on a rotation axis is degrees - NXmx readers then try to convert degrees to
// millimetres and raise. Harmless while every offset we write is zero, and wrong the moment one
// is not, so state it always rather than only when a caller remembers to.
Attr("offset_units", offset_units.empty() ? std::string("m") : offset_units);
return *this;
}