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);