reader: split raw-image reading into HDF5ImageLocator + HDF5ImageSource

Decouple the raw-pixel side of JFJochHDF5Reader from the rest as the first
step toward swappable per-dataset metadata snapshots.

- HDF5ImageLocator: single owner of the legacy/VDS/contiguous layout resolution
  plus a persistent open-file cache, replacing the four duplicated resolvers
  (GetImageLocation, ReadSpots, ReadReflections) and their per-call file caches.
  Also hosts the source-mapping logic (former GetHDF5DataSource body).
- HDF5ImageSource: raw-pixel reading (locator + LoadImageDataset); the part whose
  links to files stay fixed while the metadata master may change.
- JFJochHDF5Reader keeps a thin GetImageLocation/GetRawImage/GetHDF5DataSource that
  delegate to image_source_; the six layout members are gone, parsed into a local
  Layout handed to the source at the end of ReadFile. Cache cleared on Close().

Verified: tests/jfjoch_test [HDF5] (79 cases / 1775 assertions), and
jfjoch_process/azint/extract_hkl/scale relink unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 10:15:09 +02:00
co-authored by Claude Opus 4.8
parent ec8a45dc09
commit 23d27f30c4
7 changed files with 379 additions and 273 deletions
+4
View File
@@ -2,6 +2,10 @@ ADD_LIBRARY(JFJochReader STATIC
JFJochReader.cpp JFJochReader.h
JFJochHDF5Reader.cpp
JFJochHDF5Reader.h
HDF5ImageLocator.cpp
HDF5ImageLocator.h
HDF5ImageSource.cpp
HDF5ImageSource.h
JFJochReaderImage.cpp
JFJochReaderImage.h
JFJochReaderDataset.cpp
+160
View File
@@ -0,0 +1,160 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "HDF5ImageLocator.h"
#include "../common/JFJochException.h"
namespace {
// Coalesce consecutive single-image mappings into one contiguous range when the source and
// virtual images stay contiguous in the same file/dataset.
void AppendOrExtendSourceMapping(std::vector<HDF5DataSourceMessage> &ret,
const std::string &filename,
const std::string &dataset,
uint64_t source_first_image,
uint64_t virtual_first_image,
uint64_t image_count) {
if (image_count == 0)
return;
if (!ret.empty()) {
auto &last = ret.back();
if (last.filename == filename
&& last.dataset == dataset
&& last.source_first_image + last.image_count == source_first_image
&& last.virtual_first_image + last.image_count == virtual_first_image) {
last.image_count += image_count;
return;
}
}
ret.push_back(HDF5DataSourceMessage{
.filename = filename,
.dataset = dataset,
.source_first_image = source_first_image,
.virtual_first_image = virtual_first_image,
.image_count = image_count
});
}
}
void HDF5ImageLocator::Configure(Layout layout) {
file_cache_.clear();
layout_ = std::move(layout);
}
void HDF5ImageLocator::Clear() {
file_cache_.clear();
layout_ = Layout{};
}
std::shared_ptr<HDF5ReadOnlyFile> HDF5ImageLocator::OpenCached(const std::string &path) const {
auto it = file_cache_.find(path);
if (it != file_cache_.end())
return it->second;
auto file = std::make_shared<HDF5ReadOnlyFile>(path);
file_cache_[path] = file;
return file;
}
HDF5ImageLocator::Location HDF5ImageLocator::Resolve(int64_t global_image) const {
if (global_image < 0)
throw JFJochException(JFJochExceptionCategory::HDF5, "Image out of bounds");
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;
return {OpenCached(layout_.legacy_files.at(file_id)), local_index};
}
if (layout_.format == FileWriterFormat::NXmxVDS
&& layout_.data_layout == HDF5DataSetLayout::VIRTUAL) {
const auto image = static_cast<hsize_t>(global_image);
for (const auto &mapping: layout_.vds_mappings) {
if (!mapping.ContainsVirtualImage(image))
continue;
return {OpenCached(mapping.filename), static_cast<uint32_t>(mapping.SourceImage(image))};
}
throw JFJochException(JFJochExceptionCategory::HDF5,
"Image not covered by /entry/data/data VDS mappings");
}
// Contiguous / integrated: pixels live in the master file at the global index.
if (!layout_.master_file)
throw JFJochException(JFJochExceptionCategory::HDF5, "Master file not loaded");
return {layout_.master_file, static_cast<uint32_t>(global_image)};
}
std::vector<HDF5DataSourceMessage> HDF5ImageLocator::GetSourceMapping(uint64_t first_image,
std::optional<uint64_t> image_count,
uint64_t total_images) const {
if (first_image > total_images)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"First image outside dataset range");
const uint64_t requested_count = image_count.value_or(total_images - first_image);
if (first_image + requested_count > total_images)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Requested image range outside dataset range");
std::vector<HDF5DataSourceMessage> ret;
if (requested_count == 0)
return ret;
// Integrated / contiguous source: link directly to the original master file.
if (layout_.format == FileWriterFormat::NXmxVDS && layout_.data_layout != HDF5DataSetLayout::VIRTUAL) {
AppendOrExtendSourceMapping(ret, layout_.master_filename, "/entry/data/data",
first_image, 0, requested_count);
return ret;
}
// VDS source: expand VDS mappings to original source files, not to the VDS master.
if (layout_.format == FileWriterFormat::NXmxVDS && layout_.data_layout == HDF5DataSetLayout::VIRTUAL) {
for (uint64_t local_image = 0; local_image < requested_count; ++local_image) {
const hsize_t virtual_image = first_image + local_image;
bool found = false;
for (const auto &mapping: layout_.vds_mappings) {
if (!mapping.ContainsVirtualImage(virtual_image))
continue;
const uint64_t source_image = mapping.SourceImage(virtual_image);
const std::string dataset = mapping.dataset.empty() ? "/entry/data/data" : mapping.dataset;
AppendOrExtendSourceMapping(ret, mapping.filename, dataset, source_image, local_image, 1);
found = true;
break;
}
if (!found)
throw JFJochException(JFJochExceptionCategory::HDF5,
"Image not covered by /entry/data/data VDS mappings");
}
return ret;
}
// Legacy source: link directly to the linked data files.
if (layout_.format == FileWriterFormat::NXmxLegacy) {
if (layout_.images_per_file == 0)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot generate HDF5 source mapping: images_per_file is zero");
for (uint64_t local_image = 0; local_image < requested_count; ++local_image) {
const uint64_t source_global_image = first_image + local_image;
const uint64_t file_id = source_global_image / layout_.images_per_file;
const uint64_t source_image = source_global_image % layout_.images_per_file;
if (file_id >= layout_.legacy_files.size())
throw JFJochException(JFJochExceptionCategory::HDF5,
"Legacy image source file missing");
AppendOrExtendSourceMapping(ret, layout_.legacy_files.at(file_id), "/entry/data/data",
source_image, local_image, 1);
}
return ret;
}
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Unsupported HDF5 file layout for source mapping");
}
+60
View File
@@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "../writer/HDF5Objects.h" // HDF5ReadOnlyFile, HDF5VirtualDatasetMapping, HDF5DataSetLayout
#include "../common/JFJochMessages.h" // FileWriterFormat, HDF5DataSourceMessage
// Turns a global image number into the HDF5 file + local index that physically holds its pixels,
// for all three on-disk layouts (legacy linked data files, VDS, contiguous/integrated). This is
// the part of the reader whose "links to files stay" constant: it knows where the raw images
// live, independent of which master file the per-image metadata is read from.
//
// Open data-file handles are cached, so scanning many images (e.g. reprocessing) does not reopen
// the same file on every read. HDF5 is not thread-safe, so every call must be made with the
// global hdf5_mutex held by the caller; the locator does no locking of its own.
class HDF5ImageLocator {
public:
struct Location {
std::shared_ptr<HDF5ReadOnlyFile> file;
uint32_t local_index = 0;
};
// 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.
struct Layout {
FileWriterFormat format = FileWriterFormat::NoFile;
HDF5DataSetLayout data_layout = HDF5DataSetLayout::CONTIGUOUS;
std::shared_ptr<HDF5ReadOnlyFile> master_file;
std::string master_filename;
std::vector<std::string> legacy_files;
size_t images_per_file = 1;
std::vector<HDF5VirtualDatasetMapping> vds_mappings;
};
void Configure(Layout layout);
void Clear();
// Resolve a global image number to {file, local index}. Throws if the image is not covered
// by the layout. Does not bounds-check against the total image count - the caller does that.
Location Resolve(int64_t global_image) const;
// Source mapping for re-writing a derived file (e.g. _process.h5) so it links back to the
// original pixel sources rather than to a master. total_images is supplied by the caller.
std::vector<HDF5DataSourceMessage> GetSourceMapping(uint64_t first_image,
std::optional<uint64_t> image_count,
uint64_t total_images) const;
private:
Layout layout_;
mutable std::map<std::string, std::shared_ptr<HDF5ReadOnlyFile> > file_cache_;
std::shared_ptr<HDF5ReadOnlyFile> OpenCached(const std::string &path) const;
};
+66
View File
@@ -0,0 +1,66 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#include "HDF5ImageSource.h"
#include "../common/JFJochException.h"
void HDF5ImageSource::Configure(HDF5ImageLocator::Layout layout) {
locator_.Configure(std::move(layout));
}
void HDF5ImageSource::Clear() {
locator_.Clear();
}
HDF5ImageLocator::Location HDF5ImageSource::Resolve(int64_t global) const {
return locator_.Resolve(global);
}
std::vector<HDF5DataSourceMessage> HDF5ImageSource::GetSourceMapping(uint64_t first_image,
std::optional<uint64_t> image_count,
uint64_t total_images) const {
return locator_.GetSourceMapping(first_image, image_count, total_images);
}
CompressedImage HDF5ImageSource::ReadImageAt(std::vector<uint8_t> &buffer,
const HDF5ImageLocator::Location &loc) const {
return LoadImageDataset(buffer, *loc.file, loc.local_index);
}
CompressedImage HDF5ImageSource::LoadImageDataset(std::vector<uint8_t> &tmp, HDF5Object &file, hsize_t number) {
std::vector<hsize_t> start = {static_cast<hsize_t>(number), 0, 0};
const std::string dataset_name = "/entry/data/data";
HDF5DataSet dataset(file, dataset_name);
HDF5DataSpace dataspace(dataset);
HDF5DataType datatype(dataset);
HDF5Dcpl dcpl(dataset);
if (dataspace.GetNumOfDimensions() != 3)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"/entry/data/data dataset must be 3D");
auto dim = dataspace.GetDimensions();
CompressionAlgorithm algorithm = CompressionAlgorithm::NO_COMPRESSION;
auto chunk_size = dcpl.GetChunking();
if ((chunk_size.size() == 3) && (chunk_size[0] == 1) && (chunk_size[1] == dim[1]) && (chunk_size[2] == dim[2])) {
dataset.ReadDirectChunk(tmp, start);
algorithm = dcpl.GetCompression();
} else {
dataset.ReadVectorToU8(tmp, start, {1, dim[1], dim[2]});
algorithm = CompressionAlgorithm::NO_COMPRESSION;
}
if (datatype.IsFloat())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Float datasets not supported at this time");
return {
tmp, dim[2], dim[1],
CalcImageMode(datatype.GetElemSize(), datatype.IsFloat(), datatype.IsSigned()),
algorithm
};
}
+36
View File
@@ -0,0 +1,36 @@
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <cstdint>
#include <optional>
#include <vector>
#include "HDF5ImageLocator.h"
#include "../common/CompressedImage.h"
// Raw-pixel side of the reader. Turns a global image number into a CompressedImage, using
// HDF5ImageLocator to find the file (with its open-file cache). This is the part whose "links
// to files stay" constant: switching which master the per-image metadata is read from never
// touches it. Caller must hold the global hdf5_mutex (HDF5 is not thread-safe).
class HDF5ImageSource {
public:
void Configure(HDF5ImageLocator::Layout layout);
void Clear();
// Where image `global` physically lives. Also used by the metadata source to find the data
// file that holds a legacy/VDS image's per-image metadata.
HDF5ImageLocator::Location Resolve(int64_t global) const;
// Read the pixels at a resolved location into a CompressedImage backed by `buffer`.
CompressedImage ReadImageAt(std::vector<uint8_t> &buffer, const HDF5ImageLocator::Location &loc) const;
std::vector<HDF5DataSourceMessage> GetSourceMapping(uint64_t first_image,
std::optional<uint64_t> image_count,
uint64_t total_images) const;
private:
HDF5ImageLocator locator_;
static CompressedImage LoadImageDataset(std::vector<uint8_t> &tmp, HDF5Object &file, hsize_t number);
};
+46 -261
View File
@@ -288,15 +288,23 @@ void JFJochHDF5Reader::ReadROIMetadata(HDF5ReadOnlyFile &file, JFJochReaderDatas
void JFJochHDF5Reader::ReadFile(const std::string &filename) {
std::unique_lock ul(hdf5_mutex);
image_source_.Clear();
try {
auto dataset = std::make_shared<JFJochReaderDataset>();
master_file = std::make_shared<HDF5ReadOnlyFile>(filename);
master_filename = filename;
dataset->experiment = default_experiment;
// Image-layout state is accumulated locally while parsing, then handed to image_locator_
// 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<HDF5VirtualDatasetMapping> vds_data_mappings;
size_t images_per_file = 1;
std::filesystem::path master_path(filename);
master_file_directory = master_path.parent_path().string();
vds_data_mappings.clear();
std::string master_file_directory = master_path.parent_path().string();
dataset->arm_date = master_file->GetString("/entry/start_time");
@@ -679,12 +687,27 @@ void JFJochHDF5Reader::ReadFile(const std::string &filename) {
ReadROIMetadata(*master_file, *dataset);
// Hand the parsed image layout to the locator. VDS mapping filenames are resolved to
// absolute paths here so the locator only ever deals with real paths.
for (auto &m : vds_data_mappings)
m.filename = ResolveRelativeToMaster(master_file_directory, m.filename);
image_source_.Configure(HDF5ImageLocator::Layout{
.format = format,
.data_layout = data_layout,
.master_file = master_file,
.master_filename = master_filename,
.legacy_files = std::move(legacy_format_files),
.images_per_file = images_per_file,
.vds_mappings = std::move(vds_data_mappings)
});
dataset->experiment.ImagesPerTrigger(number_of_images);
cached_geom = dataset->experiment.GetDiffractionGeometry();
SetStartMessage(dataset);
} catch (const std::exception &e) {
master_file = {};
number_of_images = 0;
image_source_.Clear();
SetStartMessage({});
throw;
}
@@ -695,77 +718,10 @@ uint64_t JFJochHDF5Reader::GetNumberOfImages() const {
return number_of_images;
}
CompressedImage JFJochHDF5Reader::LoadImageDataset(std::vector<uint8_t> &tmp, HDF5Object &file, hsize_t number) {
std::vector<hsize_t> start = {static_cast<hsize_t>(number), 0, 0};
const std::string dataset_name = "/entry/data/data";
HDF5DataSet dataset(file, dataset_name);
HDF5DataSpace dataspace(dataset);
HDF5DataType datatype(dataset);
HDF5Dcpl dcpl(dataset);
if (dataspace.GetNumOfDimensions() != 3)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"/entry/data/data dataset must be 3D");
auto dim = dataspace.GetDimensions();
CompressionAlgorithm algorithm = CompressionAlgorithm::NO_COMPRESSION;
auto chunk_size = dcpl.GetChunking();
if ((chunk_size.size() == 3) && (chunk_size[0] == 1) && (chunk_size[1] == dim[1]) && (chunk_size[2] == dim[2])) {
dataset.ReadDirectChunk(tmp, start);
algorithm = dcpl.GetCompression();
} else {
dataset.ReadVectorToU8(tmp, start, {1, dim[1], dim[2]});
algorithm = CompressionAlgorithm::NO_COMPRESSION;
}
if (datatype.IsFloat())
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Float datasets not supported at this time");
return {
tmp, dim[2], dim[1],
CalcImageMode(datatype.GetElemSize(), datatype.IsFloat(), datatype.IsSigned()),
algorithm
};
}
std::pair<std::shared_ptr<HDF5ReadOnlyFile>, uint32_t> JFJochHDF5Reader::GetImageLocation(int64_t image_number) {
if (image_number >= number_of_images || image_number < 0)
HDF5ImageLocator::Location JFJochHDF5Reader::GetImageLocation(int64_t image_number) const {
if (image_number >= static_cast<int64_t>(number_of_images) || image_number < 0)
throw JFJochException(JFJochExceptionCategory::HDF5, "Image out of bounds");
uint32_t image_id;
std::shared_ptr<HDF5ReadOnlyFile> data_file;
if (format == FileWriterFormat::NXmxLegacy) {
uint32_t file_id = image_number / images_per_file;
image_id = image_number % images_per_file;
data_file = std::make_shared<HDF5ReadOnlyFile>(legacy_format_files.at(file_id));
} else if (format == FileWriterFormat::NXmxVDS && data_layout == HDF5DataSetLayout::VIRTUAL) {
const auto image = static_cast<hsize_t>(image_number);
for (const auto &mapping: vds_data_mappings) {
if (!mapping.ContainsVirtualImage(image))
continue;
image_id = static_cast<uint32_t>(mapping.SourceImage(image));
data_file = std::make_shared<HDF5ReadOnlyFile>(
ResolveRelativeToMaster(master_file_directory, mapping.filename)
);
return {std::move(data_file), image_id};
}
throw JFJochException(JFJochExceptionCategory::HDF5,
"Image not covered by /entry/data/data VDS mappings");
} else {
image_id = image_number;
data_file = master_file;
}
return {std::move(data_file), image_id};
return image_source_.Resolve(image_number);
}
std::shared_ptr<JFJochReaderRawImage> JFJochHDF5Reader::GetRawImage(int64_t image_number) {
@@ -775,9 +731,9 @@ std::shared_ptr<JFJochReaderRawImage> JFJochHDF5Reader::GetRawImage(int64_t imag
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot load image if file not loaded");
auto [source_file, image_id] = GetImageLocation(image_number);
auto loc = GetImageLocation(image_number);
auto ret = std::make_shared<JFJochReaderRawImage>();
ret->image = LoadImageDataset(ret->image_buffer, *source_file, image_id);
ret->image = image_source_.ReadImageAt(ret->image_buffer, loc);
return ret;
}
@@ -921,9 +877,11 @@ bool JFJochHDF5Reader::LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot load image if file not loaded");
auto [source_file, image_id] = GetImageLocation(image_number);
auto loc = GetImageLocation(image_number);
auto &source_file = loc.file;
const uint32_t image_id = loc.local_index;
message.image = LoadImageDataset(buffer, *source_file, image_id);
message.image = image_source_.ReadImageAt(buffer, loc);
message.number = image_number;
const auto master_image = static_cast<hsize_t>(image_number);
@@ -1037,11 +995,9 @@ void JFJochHDF5Reader::Close() {
std::unique_lock ul(hdf5_mutex);
master_file = {};
number_of_images = 0;
legacy_format_files.clear();
vds_data_mappings.clear();
master_file_directory.clear();
master_filename.clear();
cached_geom = DiffractionGeometry{};
image_source_.Clear();
SetStartMessage({});
}
@@ -1109,35 +1065,6 @@ CompressedImage JFJochHDF5Reader::ReadCalibration(std::vector<uint8_t> &tmp, con
};
}
void AppendOrExtendSourceMapping(std::vector<HDF5DataSourceMessage> &ret,
const std::string &filename,
const std::string &dataset,
uint64_t source_first_image,
uint64_t virtual_first_image,
uint64_t image_count) {
if (image_count == 0)
return;
if (!ret.empty()) {
auto &last = ret.back();
if (last.filename == filename
&& last.dataset == dataset
&& last.source_first_image + last.image_count == source_first_image
&& last.virtual_first_image + last.image_count == virtual_first_image) {
last.image_count += image_count;
return;
}
}
ret.push_back(HDF5DataSourceMessage{
.filename = filename,
.dataset = dataset,
.source_first_image = source_first_image,
.virtual_first_image = virtual_first_image,
.image_count = image_count
});
};
std::vector<HDF5DataSourceMessage> JFJochHDF5Reader::GetHDF5DataSource(uint64_t first_image,
std::optional<uint64_t> image_count) const {
std::unique_lock ul(hdf5_mutex);
@@ -1146,90 +1073,7 @@ std::vector<HDF5DataSourceMessage> JFJochHDF5Reader::GetHDF5DataSource(uint64_t
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot generate HDF5 source mapping if file not loaded");
if (first_image > number_of_images)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"First image outside dataset range");
const uint64_t requested_count = image_count.value_or(number_of_images - first_image);
if (first_image + requested_count > number_of_images)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Requested image range outside dataset range");
std::vector<HDF5DataSourceMessage> ret;
if (requested_count == 0)
return ret;
// Integrated / contiguous source: link directly to original master file.
if (format == FileWriterFormat::NXmxVDS && data_layout != HDF5DataSetLayout::VIRTUAL) {
AppendOrExtendSourceMapping(ret,
master_filename,
"/entry/data/data",
first_image,
0,
requested_count);
return ret;
}
// VDS source: expand VDS mappings to original source files, not to the VDS master.
if (format == FileWriterFormat::NXmxVDS && data_layout == HDF5DataSetLayout::VIRTUAL) {
for (uint64_t local_image = 0; local_image < requested_count; ++local_image) {
const hsize_t virtual_image = first_image + local_image;
bool found = false;
for (const auto &mapping: vds_data_mappings) {
if (!mapping.ContainsVirtualImage(virtual_image))
continue;
const uint64_t source_image = mapping.SourceImage(virtual_image);
const std::string filename = ResolveRelativeToMaster(master_file_directory, mapping.filename);
const std::string dataset = mapping.dataset.empty() ? "/entry/data/data" : mapping.dataset;
AppendOrExtendSourceMapping(ret,
filename,
dataset,
source_image,
local_image,
1);
found = true;
break;
}
if (!found)
throw JFJochException(JFJochExceptionCategory::HDF5,
"Image not covered by /entry/data/data VDS mappings");
}
return ret;
}
// Legacy source: link directly to linked data files.
if (format == FileWriterFormat::NXmxLegacy) {
if (images_per_file == 0)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot generate HDF5 source mapping: images_per_file is zero");
for (uint64_t local_image = 0; local_image < requested_count; ++local_image) {
const uint64_t source_global_image = first_image + local_image;
const uint64_t file_id = source_global_image / images_per_file;
const uint64_t source_image = source_global_image % images_per_file;
if (file_id >= legacy_format_files.size())
throw JFJochException(JFJochExceptionCategory::HDF5,
"Legacy image source file missing");
AppendOrExtendSourceMapping(ret,
legacy_format_files.at(file_id),
"/entry/data/data",
source_image,
local_image,
1);
}
return ret;
}
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Unsupported HDF5 file layout for source mapping");
return image_source_.GetSourceMapping(first_image, image_count, number_of_images);
}
@@ -1255,19 +1099,6 @@ std::vector<IntegrationOutcome> JFJochHDF5Reader::ReadReflections(size_t start_i
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot read reflections if file not loaded");
// Cache of data files opened during this call (keyed by path) to avoid
// re-opening the same file for every image in a multi-image batch.
std::map<std::string, std::shared_ptr<HDF5ReadOnlyFile> > open_files;
auto get_data_file = [&](const std::string &path) -> std::shared_ptr<HDF5ReadOnlyFile> {
auto it = open_files.find(path);
if (it != open_files.end())
return it->second;
auto f = std::make_shared<HDF5ReadOnlyFile>(path);
open_files[path] = f;
return f;
};
std::vector<IntegrationOutcome> ret;
ret.reserve(end_image_val - start_image + 1);
@@ -1277,23 +1108,11 @@ std::vector<IntegrationOutcome> JFJochHDF5Reader::ReadReflections(size_t start_i
// Generic (non-image-specific) detector geometry from experiment setup.
outcome.geom = cached_geom;
// Determine which HDF5 file holds the reflections and MX metadata for
// this image, and what local image index to use within that file.
HDF5Object *meta_file = master_file.get();
size_t meta_image_id = img;
std::shared_ptr<HDF5ReadOnlyFile> owned_source; // keeps the file alive
if (format == FileWriterFormat::NXmxLegacy) {
// Reflections and per-image MX data are in the individual data files,
// addressed by the source-local image index.
const size_t file_id = img / images_per_file;
meta_image_id = img % images_per_file;
owned_source = get_data_file(legacy_format_files.at(file_id));
meta_file = owned_source.get();
}
// NXmxVDS with contiguous layout (integrated): everything is in master_file
// with global indices — the defaults set above are correct.
// Per-image reflections and MX metadata live in the same file as the image pixels,
// at the source-local index (the locator keeps the data-file handle cached).
const auto loc = GetImageLocation(static_cast<int64_t>(img));
HDF5Object *meta_file = loc.file.get();
const size_t meta_image_id = loc.local_index;
// ── reflections ──────────────────────────────────────────────────────
const std::string refl_group = fmt::format("/entry/reflections/image_{:06d}", meta_image_id);
@@ -1339,46 +1158,12 @@ std::vector<SpotToSave> JFJochHDF5Reader::ReadSpots(int64_t image) const {
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"Cannot read spots if file not loaded");
// Cache of data files opened during this call (keyed by path) to avoid
// re-opening the same file for every image in a multi-image batch.
std::map<std::string, std::shared_ptr<HDF5ReadOnlyFile> > open_files;
// Spot/MX data lives in the same file as the image pixels, at the source-local index
// (the locator keeps the data-file handle cached across calls).
const auto loc = GetImageLocation(image);
HDF5Object *meta_file = loc.file.get();
const size_t meta_image_id = loc.local_index;
auto get_data_file = [&](const std::string &path) -> std::shared_ptr<HDF5ReadOnlyFile> {
auto it = open_files.find(path);
if (it != open_files.end())
return it->second;
auto f = std::make_shared<HDF5ReadOnlyFile>(path);
open_files[path] = f;
return f;
};
std::vector<SpotToSave> ret;
// Resolve which HDF5 file and local image index hold the MX spot data.
// Uses the same logic as LoadImage_i / ReadReflections so all three
// code paths stay in sync.
HDF5Object *meta_file = master_file.get();
size_t meta_image_id = image;
std::shared_ptr<HDF5ReadOnlyFile> owned_source;
if (format == FileWriterFormat::NXmxLegacy) {
const size_t file_id = image / images_per_file;
meta_image_id = image % images_per_file;
owned_source = get_data_file(legacy_format_files.at(file_id));
meta_file = owned_source.get();
} else if (format == FileWriterFormat::NXmxVDS && data_layout == HDF5DataSetLayout::VIRTUAL) {
for (const auto &mapping: vds_data_mappings) {
if (!mapping.ContainsVirtualImage(image))
continue;
meta_image_id = mapping.SourceImage(image);
owned_source = get_data_file(ResolveRelativeToMaster(master_file_directory, mapping.filename));
meta_file = owned_source.get();
break;
}
}
// NXmxVDS contiguous / NXmxIntegrated: master_file with global index —
// the defaults set above are already correct.
DataMessage tmp_message;
tmp_message.number = static_cast<int64_t>(image);
+7 -12
View File
@@ -5,23 +5,20 @@
#include "JFJochReader.h"
#include "JFJochReaderSpots.h"
#include "HDF5ImageSource.h"
#include "../writer/HDF5Objects.h"
#include "../image_analysis/IntegrationOutcome.h"
class JFJochHDF5Reader : public JFJochReader {
FileWriterFormat format = FileWriterFormat::NoFile;
HDF5DataSetLayout data_layout = HDF5DataSetLayout::CONTIGUOUS;
// Raw-pixel side: where each image physically lives + the open-file cache. Stays fixed while
// the metadata read from master_file may change.
HDF5ImageSource image_source_;
// Metadata side: the master file the per-image and dataset-level metadata is read from.
std::shared_ptr<HDF5ReadOnlyFile> master_file;
DiffractionGeometry cached_geom;
std::vector<std::string> legacy_format_files;
std::vector<HDF5VirtualDatasetMapping> vds_data_mappings;
std::string master_file_directory;
std::string master_filename;
size_t images_per_file = 1;
DiffractionGeometry cached_geom;
size_t number_of_images = 0;
bool LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset,
@@ -30,8 +27,6 @@ class JFJochHDF5Reader : public JFJochReader {
int64_t image_number,
bool update_dataset) override;
CompressedImage LoadImageDataset(std::vector<uint8_t> &tmp, HDF5Object &file, hsize_t number);
template <class T>
void ReadVector(std::vector<T> &v,
HDF5Object &file,
@@ -39,7 +34,7 @@ class JFJochHDF5Reader : public JFJochReader {
size_t image0,
size_t nimages);
std::pair<std::shared_ptr<HDF5ReadOnlyFile>, uint32_t> GetImageLocation(int64_t image_number);
HDF5ImageLocator::Location GetImageLocation(int64_t image_number) const;
std::optional<GoniometerAxis> ReadAxis(HDF5Object *file, const std::string &name);
// Read the master-file ROI definitions: logical definitions into experiment.ROI()