Files
leonarski_fandClaude Opus 5.5 6873d141a9 NXmx: a detector of N modules is written as module_0..N only
A master with known modules (every geometry-transformed JUNGFRAU since
10bdf1d11, and inputs with CrystFEL/CXI/XTC geometry) carried the whole-image
/entry/instrument/detector/module beside module_0..N. DIALS takes every
NXdetector_module as a panel: a JF4M file became 9 panels and dials.find_spots
counted every spot twice (5-image JF4M test file: 7124 spots, 3562 of them on
the whole-image panel). The whole-image module is now written only when there
are no modules; then 8 panels, 3562 spots, the same per panel as before.

Three things DIALS needs for the module-only layout to come out right:
- dxtbx lists the groups by name, so module_10 came before module_2 and DIALS
  panel n was not module n (nor the index /entry/MX/peakPanel gives). The index
  is zero-padded to the width of the last one: module_0..7, module_00..15.
- For several modules dxtbx builds panel groups from runs of transformations
  sharing an equipment_component and composes a group in reverse depends_on
  order: with translation and rot1-3 all "detector_arm" the translation stayed
  unrotated on a tilted detector (CI dials_tilted: 5.6 mm off). The detector
  transformations no longer name one; each is then its own level.
- data_origin/data_size stay [row, column], which dxtbx reads as slow, fast.

Reader: ReadNXmxModules accepts padded and unpadded names; the image
orientation is taken from the whole-image module, then detectorSpecific, and
only then from the first module, whose axes are its own placement. Old files
(whole-image only, whole-image + module_N) read as before.

DetectorModules::Moved() compares within 1e-3 px: a module read back went
through a float length and unit vector, ~1e-4 px off, so an as-imaged JUNGFRAU
read back as moved and a rotation run on such a file kept its modules instead
of processing one panel (rugnux: "placed off the image's own grid" on a
jfjoch_hdf5_test JF4M file).

Test: JFJochReader_VerticalModules, the CrMX layout (vertical modules, mirrored
in Y), identity and a mirrored quarter-turn orientation, round trip incl.
orientation. tests/nxmx_geometry_dials_test.py maps pixels through the panel
holding them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HVeDQqrcrEZy6XBUCMZvyT
2026-11-01 17:04:59 +01:00

77 lines
4.3 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "NXmxModules.h"
#include <cmath>
namespace {
// Internal frame (x = column, y = row downward, z = beam) -> McStas, a half turn about z.
std::vector<double> ToMcStas(const Coord &v) {
return {0.0 - v.x, 0.0 - v.y, 0.0 + v.z};
}
// A translation of `length` metres along v (pixels, internal frame), as NXmx writes one: the length as
// the value, the direction as a unit vector - and a zero translation with a unit vector all the same.
void SaveTranslation(HDF5Group &group, const std::string &name, const RotMatrix &delta, double dx_px, double dy_px,
double pixel_size_m, const std::string &depends_on) {
const double len_px = std::hypot(dx_px, dy_px);
const Coord dir = len_px > 0 ? Coord(static_cast<float>(dx_px / len_px), static_cast<float>(dy_px / len_px), 0)
: Coord(0, 0, 1);
SaveScalar(group, name, len_px * pixel_size_m)->
Transformation("m", depends_on, "", "", "translation", ToMcStas(delta * dir), {0, 0, 0}, "");
}
} // namespace
void WriteNXmxModules(HDF5Object &file, const DetectorModules &modules, double pixel_size_m,
const DetectorOrientation &orientation, const std::string &depends_on) {
const RotMatrix delta = orientation.Matrix();
// A NXmx reader lists the groups by name (dxtbx, through h5py), and its panel n is the n-th of them:
// the index is zero-padded to the width of the last one (module_00 ... module_15), so that panel n is
// module n - and the index /entry/MX/peakPanel gives.
const size_t digits = std::to_string(modules.size() - 1).size();
for (size_t n = 0; n < modules.size(); n++) {
const DetectorModule &m = modules[n];
std::string index = std::to_string(n);
index.insert(0, digits - index.size(), '0');
const std::string path = "/entry/instrument/detector/module_" + index;
HDF5Group group(file, path);
group.NXClass("NXdetector_module");
group.SaveVector("data_origin", std::vector<int32_t>{static_cast<int32_t>(m.y0), static_cast<int32_t>(m.x0)});
group.SaveVector("data_size", std::vector<int32_t>{static_cast<int32_t>(m.height),
static_cast<int32_t>(m.width)});
SaveTranslation(group, "module_offset", delta, m.ox, m.oy, pixel_size_m, depends_on);
SaveTranslation(group, "fast_pixel_direction", delta, m.fx, m.fy, pixel_size_m, path + "/module_offset");
SaveTranslation(group, "slow_pixel_direction", delta, m.sx, m.sy, pixel_size_m, path + "/module_offset");
}
}
void WriteModuleGeometryFile(const std::string &path, const DetectorModules &modules,
const DiffractionGeometry &geom) {
HDF5File file(path);
HDF5Group(file, "/entry").NXClass("NXentry");
HDF5Group(file, "/entry/instrument").NXClass("NXinstrument");
HDF5Group(file, "/entry/instrument/detector").NXClass("NXdetector");
HDF5Group transformations(file, "/entry/instrument/detector/transformations");
transformations.NXClass("NXtransformations");
// Sample -> image pixel (0, 0), square to the beam: what the modules' offsets are measured from. The
// tilt, if any, is the master file's to state; this file carries the modules only.
const double pixel_m = geom.GetPixelSize_mm() / 1000.0;
const RotMatrix delta = geom.GetOrientation().Matrix();
const auto v = ToMcStas(delta * Coord(-geom.GetBeamX_pxl() * static_cast<float>(pixel_m),
-geom.GetBeamY_pxl() * static_cast<float>(pixel_m),
geom.GetDetectorDistance_mm() / 1000.0f));
const double len = std::sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
SaveScalar(transformations, "translation", len)->
Transformation("m", ".", "detector", "detector_arm", "translation",
std::vector<double>{v[0] / len, v[1] / len, v[2] / len});
SaveScalar(file, "/entry/instrument/detector/depends_on",
std::string("/entry/instrument/detector/transformations/translation"));
WriteNXmxModules(file, modules, pixel_m, geom.GetOrientation(),
"/entry/instrument/detector/transformations/translation");
}