reader: read a third-party NXmx master, and stop a broken one reading as empty
A valid NXmx master written outside the DECTRIS toolchain could not be opened. Measured on a Diamond-written master of a 360 deg EIGER 16M sweep, where the images and the meta file are pure DECTRIS and only the master is third-party - which is why the two sides disagree on units at all. Five independent things, of which two were silent: * The image size came from detectorSpecific/x_pixels_in_detector, a DECTRIS extension rather than NXmx, so a third-party writer has no reason to emit it. It now comes from the image array's own shape, as it already did for a VDS master. * Lengths were assumed to be metres and the units attribute was never read. A pixel size, sensor thickness or distance stated in millimetres - correct NXmx - was silently a factor of a thousand out. The unit is now read; an undeclared one still means metres, an unknown one is refused. * The detector distance can sit in NXinstrument rather than in NXdetector; that is now the last fallback after the NXmx and the firmware-1.x spellings. * A pixel mask that is an external link into a file not holding it passed the Exists() check and then threw on the open. Whether the array is there is now decided by opening it. * Each data file was re-opened and searched for /entry/data/data, ignoring the path the master's own link names. A master linking to a plain /data therefore found no images at all - and that was a warning and exit code 0 over a sweep sitting right there, not an error. The link is now taken at its word, and a master that links to data files but yields no images is an error. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3yNBXk4wKdMZy1ak2NY7f
This commit is contained in:
@@ -63,8 +63,8 @@ HDF5ImageLocator::Location HDF5ImageLocator::Resolve(int64_t global_image) const
|
||||
if (layout_.format == FileWriterFormat::NXmxLegacy) {
|
||||
const uint32_t file_id = global_image / layout_.images_per_file;
|
||||
const uint32_t local_index = global_image % layout_.images_per_file;
|
||||
const auto &path = layout_.legacy_files.at(file_id);
|
||||
return {OpenCached(path), local_index, path};
|
||||
const auto &data_file = layout_.legacy_files.at(file_id);
|
||||
return {OpenCached(data_file.path), local_index, data_file.path, data_file.dataset};
|
||||
}
|
||||
|
||||
if (layout_.format == FileWriterFormat::NXmxVDS
|
||||
@@ -161,7 +161,8 @@ std::vector<HDF5DataSourceMessage> HDF5ImageLocator::GetSourceMapping(uint64_t f
|
||||
throw JFJochException(JFJochExceptionCategory::HDF5,
|
||||
"Legacy image source file missing");
|
||||
|
||||
AppendOrExtendSourceMapping(ret, layout_.legacy_files.at(file_id), "/entry/data/data",
|
||||
const auto &data_file = layout_.legacy_files.at(file_id);
|
||||
AppendOrExtendSourceMapping(ret, data_file.path, data_file.dataset,
|
||||
source_image, local_image, 1);
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,13 @@ public:
|
||||
std::string dataset = "/entry/data/data";
|
||||
};
|
||||
|
||||
// One data file of a legacy multi-file dataset, with the dataset the master's link names
|
||||
// inside it - the same "take the link at its word" the VDS branch already does.
|
||||
struct LegacyFile {
|
||||
std::string path;
|
||||
std::string dataset;
|
||||
};
|
||||
|
||||
// Layout description, filled by the reader once the master file has been parsed. All paths
|
||||
// are absolute: legacy data files and VDS mapping filenames are resolved relative to the
|
||||
// master before being handed over, so the locator never deals with relative paths.
|
||||
@@ -41,7 +48,7 @@ public:
|
||||
HDF5DataSetLayout data_layout = HDF5DataSetLayout::CONTIGUOUS;
|
||||
std::shared_ptr<HDF5ReadOnlyFile> master_file;
|
||||
std::string master_filename;
|
||||
std::vector<std::string> legacy_files;
|
||||
std::vector<LegacyFile> legacy_files;
|
||||
size_t images_per_file = 1;
|
||||
std::vector<HDF5VirtualDatasetMapping> vds_mappings;
|
||||
};
|
||||
|
||||
@@ -104,7 +104,7 @@ inline std::pair<gemmi::CrystalSystem, char> parse_bravais_lattice(const std::st
|
||||
std::vector<hsize_t> GetDimension(HDF5Object &object, const std::string &path) {
|
||||
const auto dim = object.GetDimension(path);
|
||||
if (dim.size() != 3)
|
||||
throw JFJochException(JFJochExceptionCategory::HDF5, "Wrong dimension of /entry/data/data");
|
||||
throw JFJochException(JFJochExceptionCategory::HDF5, "Wrong dimension of " + path);
|
||||
return dim;
|
||||
}
|
||||
|
||||
@@ -139,18 +139,42 @@ 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 + ")");
|
||||
// A length in NXmx says which unit it is in, and a master written outside the DECTRIS toolchain
|
||||
// uses that freedom: a Diamond-written one states pixel size, sensor thickness and detector
|
||||
// distance in millimetres, which is correct NXmx. Reading those as metres is not a failure but a
|
||||
// silent factor of a thousand, so the unit is read rather than assumed. An undeclared unit means
|
||||
// metres - what every DECTRIS master and everything this system writes means by one. An unknown
|
||||
// unit is refused rather than guessed at, for the same reason.
|
||||
float ReadLength_m(HDF5Object &file, const std::string &name) {
|
||||
HDF5DataSet dataset(file, name);
|
||||
const float value = dataset.ReadScalar<float>();
|
||||
|
||||
if (!dataset.AttrExists("units"))
|
||||
return value;
|
||||
|
||||
const std::string units = dataset.ReadAttrStr("units");
|
||||
if (units == "m")
|
||||
return value;
|
||||
if (units == "mm")
|
||||
return value * 1e-3f;
|
||||
if (units == "um")
|
||||
return value * 1e-6f;
|
||||
throw JFJochException(JFJochExceptionCategory::HDF5, name + ": unknown length unit " + units);
|
||||
}
|
||||
|
||||
// The same value under different names. DECTRIS Eiger firmware 1.x writes detector_distance where
|
||||
// NXmx says distance, and a file from that era is still what a repository hands you; a Diamond
|
||||
// master puts the distance one level up, in NXinstrument rather than in NXdetector. The NXmx
|
||||
// spelling is tried first and the rest are pure fallbacks, which is safe because every current
|
||||
// Eiger master carries BOTH DECTRIS names (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. ReadIntWithLegacyFallback below is the same arrangement for saturation_value.
|
||||
float ReadLengthWithFallback_m(HDF5Object &file, const std::vector<std::string> &names) {
|
||||
for (const auto &name: names)
|
||||
if (file.Exists(name))
|
||||
return ReadLength_m(file, name);
|
||||
throw JFJochException(JFJochExceptionCategory::HDF5, "Cannot find " + names.front());
|
||||
}
|
||||
|
||||
int64_t ReadIntWithLegacyFallback(HDF5Object &file, const std::string &nxmx, const std::string &legacy) {
|
||||
@@ -416,7 +440,7 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
|
||||
// at the end. format stays NoFile if the master carries no image data.
|
||||
FileWriterFormat format = FileWriterFormat::NoFile;
|
||||
HDF5DataSetLayout data_layout = HDF5DataSetLayout::CONTIGUOUS;
|
||||
std::vector<std::string> legacy_format_files;
|
||||
std::vector<HDF5ImageLocator::LegacyFile> legacy_format_files;
|
||||
std::vector<HDF5VirtualDatasetMapping> vds_data_mappings;
|
||||
size_t images_per_file = 1;
|
||||
|
||||
@@ -511,11 +535,6 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
|
||||
|
||||
legacy_format_files.clear();
|
||||
|
||||
image_size_x = master_file->GetInt("/entry/instrument/detector/detectorSpecific/x_pixels_in_detector");
|
||||
image_size_y = master_file->GetInt("/entry/instrument/detector/detectorSpecific/y_pixels_in_detector");
|
||||
|
||||
//size_t expected_images = master_file->GetInt("/entry/instrument/detector/detectorSpecific/nimages");
|
||||
|
||||
images_per_file = 0;
|
||||
number_of_images = 0;
|
||||
uint32_t nfiles = 0;
|
||||
@@ -531,14 +550,26 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
|
||||
size_t fimages = 0;
|
||||
|
||||
try {
|
||||
auto fname = ResolveRelativeToMaster(directory.string(),
|
||||
master_file->GetLinkedFileName(dname));
|
||||
// The link names both the file and the dataset inside it. DECTRIS and this
|
||||
// system always call that dataset /entry/data/data, but a Diamond-written
|
||||
// master links to a plain /data - so looking up a fixed name found nothing,
|
||||
// and a full sweep read back as zero images.
|
||||
const auto [linked_file, data_path] = master_file->GetLinkedTarget(dname);
|
||||
const auto fname = ResolveRelativeToMaster(directory.string(), linked_file);
|
||||
|
||||
HDF5ReadOnlyFile data_file(fname);
|
||||
|
||||
fimages = GetDimension(data_file, "/entry/data/data")[0];
|
||||
// The image size comes from the array itself, as it does for a VDS master.
|
||||
// detectorSpecific/x_pixels_in_detector is a DECTRIS extension rather than
|
||||
// NXmx, so a third-party writer has no reason to emit it.
|
||||
const auto dim = GetDimension(data_file, data_path);
|
||||
fimages = dim[0];
|
||||
if (nfiles == 0) {
|
||||
image_size_y = dim[1];
|
||||
image_size_x = dim[2];
|
||||
}
|
||||
|
||||
legacy_format_files.push_back(fname);
|
||||
legacy_format_files.push_back({fname, data_path});
|
||||
|
||||
if (nfiles == 0 && data_file.Exists("/entry/roi"))
|
||||
dataset->roi = data_file.FindLeafs("/entry/roi");
|
||||
@@ -646,6 +677,10 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
|
||||
number_of_images, fimages);
|
||||
}
|
||||
} catch (JFJochException &e) {
|
||||
// Say why. Everything read here is optional per-image metadata except the
|
||||
// image array itself, and losing that silently leaves an empty dataset that
|
||||
// reads as "nothing to process" rather than as a broken file.
|
||||
Logger("HDF5Reader").Warning("{}: {}", dname, e.what());
|
||||
}
|
||||
|
||||
if (nfiles == 0)
|
||||
@@ -653,6 +688,13 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
|
||||
number_of_images += fimages;
|
||||
nfiles++;
|
||||
}
|
||||
|
||||
// The master says where its images are. If none of them could be read, that is a
|
||||
// broken dataset, not an empty one - processing it would end in a "no images"
|
||||
// warning and a successful exit over a sweep that is sitting right there.
|
||||
if (number_of_images == 0)
|
||||
throw JFJochException(JFJochExceptionCategory::HDF5,
|
||||
"Master file links to data files, but no images could be read from them");
|
||||
} else {
|
||||
image_size_x = master_file->GetInt("/entry/instrument/detector/detectorSpecific/x_pixels_in_detector");
|
||||
image_size_y = master_file->GetInt("/entry/instrument/detector/detectorSpecific/y_pixels_in_detector");
|
||||
@@ -684,9 +726,10 @@ 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 = ReadWithLegacyFallback(*master_file,
|
||||
"/entry/instrument/detector/distance",
|
||||
"/entry/instrument/detector/detector_distance");
|
||||
float det_distance = ReadLengthWithFallback_m(*master_file,
|
||||
{"/entry/instrument/detector/distance",
|
||||
"/entry/instrument/detector/detector_distance",
|
||||
"/entry/instrument/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);
|
||||
@@ -849,7 +892,7 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
|
||||
auto detector_name = master_file->GetString("/entry/instrument/detector/description");
|
||||
|
||||
DetectorSetup detector = DetDECTRIS(image_size_x, image_size_y, detector_name, {});
|
||||
detector.PixelSize_um(master_file->GetFloat("/entry/instrument/detector/x_pixel_size") * 1e6);
|
||||
detector.PixelSize_um(ReadLength_m(*master_file, "/entry/instrument/detector/x_pixel_size") * 1e6);
|
||||
// Whether the stored image is mirrored in Y. A file written before this was recorded is
|
||||
// mirrored - that is the only thing Jungfraujoch has ever produced - so absence means true.
|
||||
detector.MirrorY(master_file
|
||||
@@ -870,9 +913,10 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
|
||||
"/entry/instrument/detector/detectorSpecific/detector_orientation_quarter_turns")
|
||||
.value_or(0))));
|
||||
// Sensor thickness/material drive the parallax/absorption model, so take them from the file
|
||||
// rather than the DetectorSetup default (NXmx stores thickness in metres).
|
||||
// rather than the DetectorSetup default.
|
||||
if (master_file->Exists("/entry/instrument/detector/sensor_thickness"))
|
||||
detector.SensorThickness_um(master_file->GetFloat("/entry/instrument/detector/sensor_thickness") * 1e6);
|
||||
detector.SensorThickness_um(
|
||||
ReadLength_m(*master_file, "/entry/instrument/detector/sensor_thickness") * 1e6);
|
||||
if (master_file->Exists("/entry/instrument/detector/sensor_material"))
|
||||
detector.SensorMaterial(master_file->GetString("/entry/instrument/detector/sensor_material"));
|
||||
detector.SaturationLimit(SaturationLimitFromValue(
|
||||
@@ -916,17 +960,15 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
|
||||
}
|
||||
|
||||
if (image_size_x * image_size_y > 0) {
|
||||
auto mask_tmp = master_file->ReadOptVector<uint32_t>(
|
||||
"/entry/instrument/detector/pixel_mask",
|
||||
{0, 0},
|
||||
{image_size_y, image_size_x}
|
||||
);
|
||||
if (mask_tmp.empty())
|
||||
mask_tmp = master_file->ReadOptVector<uint32_t>(
|
||||
"/entry/instrument/detector/detectorSpecific/pixel_mask",
|
||||
{0, 0},
|
||||
{image_size_y, image_size_x}
|
||||
);
|
||||
// IsDataSet, not Exists: a mask can be an external link into a file that does not hold
|
||||
// it, which a deposition shipping no mask at all leaves behind. The link is there, so
|
||||
// the name exists; only opening it says whether the array does.
|
||||
std::vector<uint32_t> mask_tmp;
|
||||
for (const char *name: {"/entry/instrument/detector/pixel_mask",
|
||||
"/entry/instrument/detector/detectorSpecific/pixel_mask"}) {
|
||||
if (mask_tmp.empty() && master_file->IsDataSet(name))
|
||||
mask_tmp = master_file->ReadVector<uint32_t>(name, {0, 0}, {image_size_y, image_size_x});
|
||||
}
|
||||
if (mask_tmp.empty())
|
||||
mask_tmp = std::vector<uint32_t>(image_size_x * image_size_y);
|
||||
dataset->pixel_mask = std::make_shared<const PixelMask>(mask_tmp);
|
||||
|
||||
@@ -1117,7 +1117,7 @@ bool HDF5Object::IsExternalLink(const std::string& name) const {
|
||||
return (link_info.type == H5L_TYPE_EXTERNAL);
|
||||
}
|
||||
|
||||
std::string HDF5Object::GetLinkedFileName(const std::string& name) const {
|
||||
std::pair<std::string, std::string> HDF5Object::GetLinkedTarget(const std::string& name) const {
|
||||
H5L_info2_t link_info;
|
||||
|
||||
// Get information about the link
|
||||
@@ -1149,9 +1149,11 @@ std::string HDF5Object::GetLinkedFileName(const std::string& name) const {
|
||||
throw JFJochException(JFJochExceptionCategory::HDF5,
|
||||
"Failed to get link location");
|
||||
|
||||
std::string s(target_file_name);
|
||||
return {target_file_name, target_object_path};
|
||||
}
|
||||
|
||||
return s;
|
||||
std::string HDF5Object::GetLinkedFileName(const std::string& name) const {
|
||||
return GetLinkedTarget(name).first;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <vector>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "../common/JFJochException.h"
|
||||
#include "../compression/CompressionAlgorithmEnum.h"
|
||||
@@ -184,6 +185,10 @@ public:
|
||||
bool Exists(const std::string& name) const;
|
||||
bool IsDataSet(const std::string& name) const;
|
||||
bool IsExternalLink(const std::string& name) const;
|
||||
// {file, object path} an external link points at. The object path matters: the master of a
|
||||
// multi-file dataset names the dataset inside each data file, and a writer outside the DECTRIS
|
||||
// toolchain is free to call it something other than /entry/data/data.
|
||||
std::pair<std::string, std::string> GetLinkedTarget(const std::string& name) const;
|
||||
std::string GetLinkedFileName(const std::string& name) const;
|
||||
std::vector<std::string> FindLeafs(const std::string &name) const;
|
||||
std::vector<hsize_t> GetDimension(const std::string &name);
|
||||
|
||||
Reference in New Issue
Block a user