From 583da3c6a02b2abfc86d10ce883aa047fe719d44 Mon Sep 17 00:00:00 2001 From: leonarski_f Date: Sun, 23 Aug 2026 11:20:34 +0200 Subject: [PATCH] Derive the rotation width for a chain that was sent A chain carried in the END message was written verbatim and stopped there, so AXISNAME_end and the rotation width - which the writer produces when it builds the chain itself - were simply absent. A one-image sweep sent that way imported as a still, since dxtbx prefers AXISNAME_end and only falls back to np.diff; omega_range_average is what DECTRIS-oriented tooling reads for the oscillation. They are derived here rather than added to the wire format: for a constant step they follow from the values, which is every case there is today, so carrying them would cost an array per axis and say nothing new. The step is taken over the endpoints, because the values arrive as floats and a single difference puts that noise straight into the reported width. The test now compares the two routes on the files. It could not have caught this before: it went through the reader, and the reader reads neither of these. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VfYvJT5Nb71suJCowRBn5z --- docs/CHANGELOG.md | 2 +- tests/JFJochReaderTest.cpp | 14 ++++++++++++++ writer/HDF5NXmx.cpp | 21 +++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 49aee755..067b1e34 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -16,7 +16,7 @@ This is an UNSTABLE release. It includes many experimental features, as well as * HDF5: a still or grid scan recorded with a Smargon head position is no longer read back as a single image. * 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 can carry the sample transformation chain (`transformations`, in the END message) in mounting order, so a goniometer axis, the Smargon chi/phi and a grid stage are described together and unambiguously. It is optional - a producer that does not send it gets the same chain built by the writer - so no metadata version change is needed. +* The image stream can carry the sample transformation chain (`transformations`, in the END message) in mounting order, so a goniometer axis, the Smargon chi/phi and a grid stage are described together and unambiguously. It is optional - a producer that does not send it gets the same chain built by the writer - so no metadata version change is needed. The rotation width and end angles are derived by the writer rather than sent. * 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. * The image stream and HDF5 now record `mirror_y`, whether the assembled image is mirrored in Y relative to the detector's raw readout, and it is read back. * rugnux: an image integrated in pyFAI through the `.poni` file written by `--mode calibration` now comes out with the correct azimuth; it was 180 degrees out. Radial integration is unchanged. diff --git a/tests/JFJochReaderTest.cpp b/tests/JFJochReaderTest.cpp index 783cfad6..1c8393da 100644 --- a/tests/JFJochReaderTest.cpp +++ b/tests/JFJochReaderTest.cpp @@ -2991,6 +2991,20 @@ TEST_CASE("JFJochReader_TransformationChain_SentAndBuilt", "[HDF5][Full]") { CHECK(sent.GetDatasetSettings().GetSmargonPosition()->phi_deg == Catch::Approx(-7.25f).margin(1e-3)); + // Compared on the files, not through the reader: the reader reads neither AXISNAME_end nor the + // rotation width, so it cannot see the two routes diverge - and it did, until the writer started + // deriving them for a chain it was handed. + { + HDF5ReadOnlyFile built_file("test_chain_built_master.h5"); + HDF5ReadOnlyFile sent_file("test_chain_sent_master.h5"); + CHECK(built_file.FindLeafs("/entry/sample/transformations") + == sent_file.FindLeafs("/entry/sample/transformations")); + CHECK(sent_file.ReadVector("/entry/sample/transformations/omega_end").size() + == static_cast(x.GetImageNum())); + CHECK(sent_file.ReadVector("/entry/sample/transformations/omega_range_average").at(0) + == Catch::Approx(0.1).margin(1e-4)); + } + remove("test_chain_built_master.h5"); remove("test_chain_sent_master.h5"); REQUIRE(H5Fget_obj_count(H5F_OBJ_ALL, H5F_OBJ_ALL) == 0); diff --git a/writer/HDF5NXmx.cpp b/writer/HDF5NXmx.cpp index 36418408..e0f1f2ae 100644 --- a/writer/HDF5NXmx.cpp +++ b/writer/HDF5NXmx.cpp @@ -825,6 +825,27 @@ void NXmx::Sample(const StartMessage &start, const EndMessage &end) { axis.GetEquipment(), axis.GetEquipmentComponent(), type, vector, offset, ""); depends_on = base + axis.GetName(); + + // AXISNAME_end and the rotation width are derived here rather than carried on the wire. + // They follow from the values themselves for a constant step, which is every case there + // is today, so sending them would cost another array per axis and say nothing new. + const auto &values = axis.GetValues(); + // Over the endpoints rather than the first pair: the values arrive as floats, and a + // single difference carries that noise straight into the reported rotation width. + const double step = (values.size() > 1) + ? (values.back() - values.front()) / static_cast(values.size() - 1) + : 0.0; + if (axis.IsRotation() && (step != 0.0)) { + std::vector end(values.size()); + for (size_t i = 0; i < values.size(); i++) + end[i] = values[i] + step; + SaveVector(transformations, axis.GetName() + "_end", end)->Units(axis.GetUnits()); + SaveScalar(transformations, axis.GetName() + "_range_average", + static_cast(step))->Units(axis.GetUnits()); + SaveScalar(transformations, axis.GetName() + "_range_total", + static_cast(step * static_cast(values.size()))) + ->Units(axis.GetUnits()); + } } group.SaveScalar("depends_on", depends_on); return;