reader: open a master whose beamline writes standards-correct NXmx differently

Four defects, hit in sequence, that between them stopped eight masters from one beamline before any
geometry question was reached. The files are correct NeXus; the reader was assuming one writer's
conventions.

ReadScalar demanded rank 0. NXmx puts no rank on distance, saturation_value, two_theta or det_z, and
these files write them as shape (1,). Any dataspace holding exactly one element is now accepted; a
genuine vector is still refused.

frame_time was read unconditionally and NXmx does not require it.

A virtual-dataset source filename of "." was resolved as a relative path, giving <dir>/. - but "."
is HDF5's spelling for THIS file, and these masters compose /entry/data/data as a virtual dataset
over datasets in themselves that are external links to the data files.

The virtual source's DATASET PATH was parsed and then ignored in favour of a hardcoded
/entry/data/data, while these data files keep their images at the root. Fixing that exposed a fifth:
the positional-read fast path used the master's own path, but a dataset reached through an external
link lives in another file and a chunk address is an offset into THAT file.

An audit of all 38 masters in the non-SLS corpus finds exactly these eight need the change and the
other 30 need nothing. Corpus A/B over 73 dataset pairs, base and patched back to back: 64
byte-identical results, 9 identical failures, none differing. The blast radius is bounded by
construction - JFJochReader is linked only by rugnux and jfjoch_viewer, and the one shared header
this touches only ever accepts more, so nothing that opened before can read differently.

With it the eight files read completely: 12850 images, no decode errors, seven of them spanning two
source datasets through the master's own external links.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Lc5JG6kJqZoCWaoZ43JGTW
This commit is contained in:
2026-08-29 19:58:33 +02:00
co-authored by Claude Opus 5
parent 4ef6bd9952
commit 60c15d6931
7 changed files with 87 additions and 24 deletions
+16 -5
View File
@@ -844,13 +844,19 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
detector.ReadOutTime(std::chrono::nanoseconds(0));
dataset->experiment.Detector(detector);
// frame_time is the period between frames, count_time the exposure within one. NXmx requires
// neither, and a master written outside the DECTRIS toolchain often carries only count_time;
// falling back to it says "no dead time", which is the honest reading of a file that does not
// state one. What is read here is metadata - the one place frame time is divided by is the
// JUNGFRAU summation, which a dataset read from a DECTRIS-style file never reaches.
const float count_time_s = master_file->GetFloat("/entry/instrument/detector/count_time");
dataset->experiment.FrameTime(
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::duration<float>(
master_file->GetFloat("/entry/instrument/detector/frame_time"))),
master_file->GetOptFloat("/entry/instrument/detector/frame_time")
.value_or(count_time_s))),
std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::duration<float>(
master_file->GetFloat("/entry/instrument/detector/count_time")))
std::chrono::duration<float>(count_time_s))
);
if (master_file->Exists("/entry/instrument/detector/calibration")) {
@@ -878,9 +884,14 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
ReadROIMetadata(*master_file, *dataset);
// Resolve VDS mapping filenames to absolute paths so the image source's locator only ever
// deals with real paths, then report the layout to the caller.
// deals with real paths, then report the layout to the caller. "." is HDF5's spelling for
// "the file this dataset is in", not a relative path - a master is allowed to compose its
// VDS over datasets in ITSELF, which are then external links to the data files. Resolved as
// a path it became <dir>/. and no image could be opened at all.
for (auto &m : vds_data_mappings)
m.filename = ResolveRelativeToMaster(master_file_directory, m.filename);
m.filename = (m.filename == ".")
? master_filename
: ResolveRelativeToMaster(master_file_directory, m.filename);
dataset->experiment.ImagesPerTrigger(number_of_images);
cached_geom = dataset->experiment.GetDiffractionGeometry();