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
+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;
}