reader/viewer: snapshot image->original map; subset runs register and overlay

Foundation for a dataset (snapshot or, later, the main file) being a subset of the
truly collected images:
- JFJochReaderDataset gains source_image_number (image index -> original image
  number; empty = identity).
- HDF5MetadataSource reads /entry/detector/number into that map and an inverse
  (original -> local) map; FillPerImage / ReadSpots translate the requested global
  image to this source's local index via ToLocalIndex (return nothing if the image
  is not covered), so partial snapshots are correct and never read out of bounds.
- RegisterSnapshot now accepts a snapshot with fewer images than the dataset
  (only rejects more), so sub-range / strided reprocessing runs register.
- The dataset-info plot draws each run at its original image numbers (x map from
  source_image_number), so a subset run lands at the right place on the shared
  axis. The live run's map is filled from msg.original_number.

This makes the foundation ready for strided / filtered selections (e.g. reprocess
only images with >N spots) without restricting to a min-max sub-range.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 14:59:23 +02:00
co-authored by Claude Opus 4.8
parent 7ba097df1c
commit b9f8c2b675
8 changed files with 96 additions and 22 deletions
+44 -7
View File
@@ -693,6 +693,23 @@ HDF5MetadataSource::OpenResult HDF5MetadataSource::Open(const std::string &filen
dataset->experiment.ImagesPerTrigger(number_of_images);
cached_geom = dataset->experiment.GetDiffractionGeometry();
// Image-index -> original-image-number map (written as /entry/detector/number). When it is a
// genuine subset/strided selection, keep it so plots and per-image lookups use the original
// numbering; a plain 0..N-1 sequence is identity and left empty.
image_to_local_.clear();
auto numbers = master_file->ReadOptVector<uint64_t>("/entry/detector/number");
if (numbers.size() == number_of_images) {
bool identity = true;
for (size_t i = 0; i < numbers.size(); i++)
if (numbers[i] != i) { identity = false; break; }
if (!identity) {
dataset->source_image_number.assign(numbers.begin(), numbers.end());
for (size_t i = 0; i < numbers.size(); i++)
image_to_local_[static_cast<int64_t>(numbers[i])] = static_cast<int64_t>(i);
}
}
dataset_ = dataset;
return OpenResult{
@@ -726,6 +743,15 @@ HDF5ImageLocator::Location HDF5MetadataSource::ResolveMeta(int64_t global) const
return {master_file, static_cast<uint32_t>(global)};
}
std::optional<int64_t> HDF5MetadataSource::ToLocalIndex(int64_t image_number) const {
if (image_to_local_.empty())
return image_number; // 1:1 source (identity)
const auto it = image_to_local_.find(image_number);
if (it == image_to_local_.end())
return std::nullopt; // this source does not cover that image
return it->second;
}
// Reads spot data for a single image from the appropriate HDF5 source.
// master_image / source_image are the logical indices within master_file and
// source_file respectively (identical for NXmxVDS contiguous / integrated;
@@ -852,8 +878,13 @@ static void ReadSpotsFromFiles(HDF5Object &master_file,
GenerateSpotPlot(message, 1.5);
}
void HDF5MetadataSource::FillPerImage(DataMessage &message, int64_t image_number,
void HDF5MetadataSource::FillPerImage(DataMessage &message, int64_t requested_image,
const std::shared_ptr<const JFJochReaderDataset> &dataset) const {
const auto local_opt = ToLocalIndex(requested_image);
if (!local_opt)
return; // this metadata source does not cover the requested image
const int64_t image_number = *local_opt; // local index into this source (identity for 1:1)
auto loc = ResolveMeta(image_number);
auto &source_file = loc.file;
const uint32_t image_id = loc.local_index;
@@ -862,7 +893,7 @@ void HDF5MetadataSource::FillPerImage(DataMessage &message, int64_t image_number
const auto source_image = static_cast<hsize_t>(image_id);
ReadSpotsFromFiles(*master_file, *source_file, master_image, source_image,
image_number, dataset->experiment.GetDiffractionGeometry(), message);
requested_image, dataset->experiment.GetDiffractionGeometry(), message);
if (!dataset->az_int_bin_to_q.empty()) {
if (dataset->azimuthal_bins == 0) {
@@ -1094,10 +1125,16 @@ std::vector<IntegrationOutcome> HDF5MetadataSource::ReadReflections(size_t start
return ret;
}
std::vector<SpotToSave> HDF5MetadataSource::ReadSpots(int64_t image) const {
if (image < 0)
std::vector<SpotToSave> HDF5MetadataSource::ReadSpots(int64_t requested_image) const {
if (requested_image < 0)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"image number must be non-negative");
const auto local_opt = ToLocalIndex(requested_image);
if (!local_opt)
return {}; // this (subset) source does not cover the requested image
const int64_t image = *local_opt;
if (image >= number_of_images)
throw JFJochException(JFJochExceptionCategory::InputParameterInvalid,
"image must be less than number_of_images");
@@ -1107,17 +1144,17 @@ std::vector<SpotToSave> HDF5MetadataSource::ReadSpots(int64_t image) const {
"Cannot read spots if file not loaded");
// Per-image spot/MX data, resolved the same way as the pixels (or in our own master at the
// global index for an integrated _process.h5 snapshot).
// local index for an integrated _process.h5 snapshot).
const auto loc = ResolveMeta(image);
HDF5Object *meta_file = loc.file.get();
const size_t meta_image_id = loc.local_index;
DataMessage tmp_message;
tmp_message.number = static_cast<int64_t>(image);
tmp_message.number = requested_image;
ReadSpotsFromFiles(*master_file, *meta_file,
image, meta_image_id,
static_cast<int64_t>(image),
requested_image,
cached_geom,
tmp_message);