From 5e055151651ef2b9d4a4b1dfee4389c12d7f9bdb Mon Sep 17 00:00:00 2001 From: leonarski_f Date: Sat, 22 Aug 2026 22:41:51 +0200 Subject: [PATCH] Write the grid stage as a base stage, not head-mounted The grid translations went innermost, i.e. mounted on the head, so a grid position turned with the spindle. At SLS the grid is an Aerotech xyz that the spindle is mounted ON, so the mounting order is base -> grid -> omega -> chi -> phi -> sample and a grid position is independent of omega. Identical to the previous chain at omega = 0, which is every grid scan collected so far, and correct rather than incorrect when it is not. A head-mounted stage exists too - the Smargon translates, and that is what helical uses - and would sit on the other side of omega. Only the base stage is modelled for now, which is the one actually used; the comment says so. Measured: dials.import reads a master with the new chain. Co-Authored-By: Claude Opus 5 (1M context) --- docs/CHANGELOG.md | 2 +- writer/HDF5NXmx.cpp | 37 +++++++++++++++++++++---------------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 571bfc33..1145ccb6 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 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. * A grid scan and a goniometer axis are no longer alternatives - both can be set, and the grid scan is no longer silently dropped when an axis is present. -* HDF5: a grid-scan file now records the (stationary) spindle, so it can be opened by programs that require a rotation axis; DIALS refused the previous files outright. +* HDF5: a grid-scan file now records the (stationary) spindle, so it can be opened by programs that require a rotation axis; DIALS refused the previous files outright. The grid stage is written as a base stage, so a grid position does not turn with the spindle. * The rotation axis is read back from HDF5 under whatever name it carries; only `omega` was recognised before, so a sweep recorded as e.g. `phi` re-opened as stills with nothing to say so. * A goniometer axis that does not turn is now kept rather than discarded, and is distinguished from a rotation sweep - it records where the head was for a still or a grid scan. * `images_per_file` is now chosen from the acquisition when it is not given: a rotation sweep of at most 20000 images goes into a single data file, a grid scan splits on whole fast-axis rows, and stills and serial keep 1000. diff --git a/writer/HDF5NXmx.cpp b/writer/HDF5NXmx.cpp index 0762b05f..525e30c8 100644 --- a/writer/HDF5NXmx.cpp +++ b/writer/HDF5NXmx.cpp @@ -766,9 +766,13 @@ void NXmx::Sample(const StartMessage &start, const EndMessage &end) { }; // One chain, built from the base outwards, rather than the goniometer and the grid scan being - // alternatives. NXmx applies the deepest dependency first, so writing spindle -> chi -> phi -> - // grid translations makes the sample sit at the innermost end, which is what it physically is: - // the grid stage rides on the head, and the head rides on the spindle. + // alternatives. NXmx applies the deepest dependency first, so the order here is the mounting + // order: the grid stage is a BASE stage (an Aerotech xyz at SLS) that the spindle is mounted on, + // the spindle carries the head, and the head carries the sample. So + // base -> grid -> omega -> chi -> phi -> sample + // and a grid position therefore does NOT turn with omega. (A head-mounted grid stage exists too + // - the Smargon can translate - and would sit on the other side of omega; only the base stage is + // modelled for now, which is what is actually used.) const bool write_goniometer = (end.max_image_number > 0) && start.goniometer.has_value(); const bool write_grid_scan = start.grid_scan.has_value(); @@ -788,6 +792,20 @@ void NXmx::Sample(const StartMessage &start, const EndMessage &end) { transformations.NXClass("NXtransformations"); hdf5_file->HardLink("/entry/sample/transformations","/entry/sample/goniometer"); + // Base stage first: everything else is mounted on it. The position containers hold one entry + // per image and are empty if the scan stopped at the first image. + if (write_grid_scan && (end.max_image_number > 0)) { + SaveVector(transformations,"grid_scan_x", start.grid_scan->GetXContainer_m(end.max_image_number)) + ->Transformation("m", depends_on, "", "", + "translation", {1, 0, 0}, {0,0,0}, ""); + depends_on = "/entry/sample/transformations/grid_scan_x"; + + SaveVector(transformations,"grid_scan_y", start.grid_scan->GetYContainer_m(end.max_image_number)) + ->Transformation("m", depends_on, "", "", + "translation", {0, 1, 0}, {0,0,0}, ""); + depends_on = "/entry/sample/transformations/grid_scan_y"; + } + if (write_goniometer) { // Prefer the rotation axis refined by the offline analysis (rugnux); the broker leaves it empty // and the user-provided goniometer axis stands. @@ -852,19 +870,6 @@ void NXmx::Sample(const StartMessage &start, const EndMessage &end) { } } - // The grid stage is mounted on the head, so its translations are innermost. The position - // containers hold one entry per image and are empty if the scan stopped at the first image. - if (write_grid_scan && (end.max_image_number > 0)) { - SaveVector(transformations,"grid_scan_x", start.grid_scan->GetXContainer_m(end.max_image_number)) - ->Transformation("m", depends_on, "", "", - "translation", {1, 0, 0}, {0,0,0}, ""); - depends_on = "/entry/sample/transformations/grid_scan_x"; - - SaveVector(transformations,"grid_scan_y", start.grid_scan->GetYContainer_m(end.max_image_number)) - ->Transformation("m", depends_on, "", "", - "translation", {0, 1, 0}, {0,0,0}, ""); - depends_on = "/entry/sample/transformations/grid_scan_y"; - } } group.SaveScalar("depends_on", depends_on);