diff --git a/reader/HDF5MetadataSource.cpp b/reader/HDF5MetadataSource.cpp index 1ea3a69e5..f05b2596d 100644 --- a/reader/HDF5MetadataSource.cpp +++ b/reader/HDF5MetadataSource.cpp @@ -10,6 +10,7 @@ #include "../image_analysis/spot_finding/SpotUtils.h" #include "../common/GridScanSettings.h" #include "../common/JFJochMath.h" +#include "../common/Logger.h" #include "../common/ROIDefinition.h" inline std::pair parse_bravais_lattice(const std::string &val) { @@ -100,6 +101,40 @@ std::string ResolveRelativeToMaster(const std::string &directory, return (std::filesystem::path(directory) / path).string(); } +// DECTRIS Eiger firmware 1.x writes the same values under different names, and a file from that era +// is still what a repository hands you. The modern spelling is tried first; the legacy one is a pure +// fallback, and it is safe because every current Eiger master carries BOTH (measured on thirteen +// masters from ten facilities, firmware release-2020.2.1 through release-2024.1.1 - all of them +// write detector_distance and countrate_correction_count_cutoff beside the NXmx names). So this can +// never change what a modern file reads. +float ReadWithLegacyFallback(HDF5Object &file, const std::string &nxmx, const std::string &legacy) { + if (file.Exists(nxmx)) + return file.GetFloat(nxmx); + if (file.Exists(legacy)) + return file.GetFloat(legacy); + throw JFJochException(JFJochExceptionCategory::HDF5, "Cannot find " + nxmx + " (nor " + legacy + ")"); +} + +int64_t ReadIntWithLegacyFallback(HDF5Object &file, const std::string &nxmx, const std::string &legacy) { + if (file.Exists(nxmx)) + return file.GetInt(nxmx); + if (file.Exists(legacy)) + return file.GetInt(legacy); + throw JFJochException(JFJochExceptionCategory::HDF5, "Cannot find " + nxmx + " (nor " + legacy + ")"); +} + +// Where the goniometer axes live. NXmx puts them in /entry/sample/transformations; firmware 1.x put +// them in /entry/sample/goniometer and wrote no transformation_type and no vector on them. Getting +// this one wrong is not a missing value but a WRONG ANSWER: a goniometer is only ever set from this +// group, so a file whose axes are somewhere else is read as stills, silently. +std::string GoniometerGroup(HDF5Object &file) { + if (file.Exists("/entry/sample/transformations")) + return "/entry/sample/transformations"; + if (file.Exists("/entry/sample/goniometer")) + return "/entry/sample/goniometer"; + return {}; +} + template void ReadVector(std::vector &v, HDF5Object &file, @@ -603,7 +638,9 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen dataset->experiment.BeamX_pxl(master_file->GetFloat("/entry/instrument/detector/beam_center_x")); dataset->experiment.BeamY_pxl(master_file->GetFloat("/entry/instrument/detector/beam_center_y")); - float det_distance = master_file->GetFloat("/entry/instrument/detector/distance"); + float det_distance = ReadWithLegacyFallback(*master_file, + "/entry/instrument/detector/distance", + "/entry/instrument/detector/detector_distance"); if (det_distance < 0.001) det_distance = 0.1; // Set to 100 mm, if det distance is less than 1 mm dataset->experiment.DetectorDistance_mm(det_distance * 1000.0); @@ -630,13 +667,14 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen // the CBOR stream and the writer, so looking only for "omega" - as this did - read a sweep // recorded as "phi" back as stills, silently. Prefer an axis that actually turns; fall back // to a stationary one, which still says where the head was. - if (master_file->Exists("/entry/sample/transformations")) { + const std::string gonio_group = GoniometerGroup(*master_file); + if (!gonio_group.empty()) { // A Smargon chi/phi is tagged with equipment_component - it is a head position, not the // spindle. Recognised by that tag and not by name: phi is an ordinary spindle name in MX, // so a file from anywhere else must not have its rotation axis read back as a head // position, nor its spindle mistaken for one here. - auto is_smargon_axis = [this](const std::string &name) { - const std::string dname = "/entry/sample/transformations/" + name; + auto is_smargon_axis = [this, &gonio_group](const std::string &name) { + const std::string dname = gonio_group + "/" + name; if (!master_file->Exists(dname)) return false; HDF5DataSet axis(*master_file, dname); @@ -645,10 +683,10 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen }; std::optional stationary; - for (const auto &name: master_file->FindLeafs("/entry/sample/transformations")) { + for (const auto &name: master_file->FindLeafs(gonio_group)) { if (is_smargon_axis(name)) continue; - auto axis = ReadAxis(master_file.get(), name); + auto axis = ReadAxis(master_file.get(), name, gonio_group); if (!axis.has_value()) continue; if (axis->IsScanning()) { @@ -667,9 +705,9 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen // the head position entirely - nothing in reader/ read it. std::optional chi, phi; if (is_smargon_axis("chi")) - chi = ReadAxis(master_file.get(), "chi"); + chi = ReadAxis(master_file.get(), "chi", gonio_group); if (is_smargon_axis("phi")) - phi = ReadAxis(master_file.get(), "phi"); + phi = ReadAxis(master_file.get(), "phi", gonio_group); if (chi.has_value() || phi.has_value()) { SmargonPosition smargon; if (chi.has_value()) { @@ -778,7 +816,9 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen if (master_file->Exists("/entry/instrument/detector/sensor_material")) detector.SensorMaterial(master_file->GetString("/entry/instrument/detector/sensor_material")); detector.SaturationLimit(SaturationLimitFromValue( - master_file->GetInt("/entry/instrument/detector/saturation_value"))); + ReadIntWithLegacyFallback(*master_file, + "/entry/instrument/detector/saturation_value", + "/entry/instrument/detector/detectorSpecific/countrate_correction_count_cutoff"))); // The reader hands every image out as signed int32 whatever the file stored (see PixelSigned // below), so that is the container depth the rest of the code has to see. DetectorSetup defaults // DECTRIS to 16 bits and GetByteDepthImage() prefers the detector's value over the image @@ -1141,8 +1181,9 @@ void HDF5MetadataSource::FillPerImage(DataMessage &message, int64_t requested_im } } -std::optional HDF5MetadataSource::ReadAxis(HDF5Object *file, const std::string &name) { - std::string dname = "/entry/sample/transformations/" + name; +std::optional HDF5MetadataSource::ReadAxis(HDF5Object *file, const std::string &name, + const std::string &group) { + std::string dname = group + "/" + name; if (!file->Exists(dname)) return {}; @@ -1160,9 +1201,20 @@ std::optional HDF5MetadataSource::ReadAxis(HDF5Object *file, con // Missing attribute means "not a transformation", so skip it rather than throwing: the search // for the goniometer walks every leaf and only stops early on an axis that turns, so a master // whose axis was stationary reached omega_end and could not be opened at all. - if (!dataset.AttrExists("transformation_type") - || (dataset.ReadAttrStr("transformation_type") != "rotation")) + // NXmx tags every axis; DECTRIS firmware 1.x tagged none of them, so absence has to mean two + // different things depending on the layout. In a transformations group it means "not an axis" + // (the writer's own AXISNAME_end and the rotation-width scalars live there and carry only units), + // and skipping is right. In the legacy goniometer group EVERY leaf is an axis and none is tagged, + // so skipping there would find no goniometer at all and the sweep would be read as stills. + const bool legacy_group = (group != "/entry/sample/transformations"); + if (dataset.AttrExists("transformation_type")) { + if (dataset.ReadAttrStr("transformation_type") != "rotation") + return {}; + } else if (!legacy_group) { return {}; + } else if (name.find("_end") != std::string::npos || name.find("_range") != std::string::npos) { + return {}; // the same companion datasets, by name, since there is no tag to go on + } std::vector end = file->ReadOptVector(dname + "_end"); @@ -1172,10 +1224,23 @@ std::optional HDF5MetadataSource::ReadAxis(HDF5Object *file, con double start = angle[0]; double incr = (angle.size() < 2) ? 0.0 : angle[1] - angle[0]; - std::vector axis_vec = dataset.ReadAttrVec("vector"); - if (axis_vec.size() != 3) + std::vector axis_vec; + if (dataset.AttrExists("vector")) { + axis_vec = dataset.ReadAttrVec("vector"); + if (axis_vec.size() != 3) + throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, + dname + " Vector must have 3 elements"); + } else if (legacy_group) { + // Firmware 1.x stored no direction at all. Assume the one every DECTRIS master since has + // written, and say so - a wrong guess here does not index, so it is visible rather than + // silent, and the rotation first pass will try the opposite sign anyway. + axis_vec = {-1.0, 0.0, 0.0}; + Logger("HDF5Reader").Warning("{} carries no axis direction (pre-NXmx layout); assuming " + "(-1,0,0), the direction current DECTRIS masters write", dname); + } else { throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, - dname + " Vector must have 3 elements"); + dname + " has no vector attribute"); + } Coord axis(axis_vec[0], axis_vec[1], axis_vec[2]); GoniometerAxis g_axis(name, start, incr, axis, {}); diff --git a/reader/HDF5MetadataSource.h b/reader/HDF5MetadataSource.h index 3db7ed285..62cce14c0 100644 --- a/reader/HDF5MetadataSource.h +++ b/reader/HDF5MetadataSource.h @@ -74,6 +74,9 @@ private: std::optional ToLocalIndex(int64_t image_number) const; HDF5ImageLocator::Location ResolveMeta(int64_t global) const; - std::optional ReadAxis(HDF5Object *file, const std::string &name); + // group is where the axes live: /entry/sample/transformations (NXmx) or + // /entry/sample/goniometer (DECTRIS firmware 1.x, which tags no axis and gives no vector). + std::optional ReadAxis(HDF5Object *file, const std::string &name, + const std::string &group); void ReadROIMetadata(HDF5ReadOnlyFile &file, JFJochReaderDataset &dataset) const; };