From b9f8c2b67580984d8205d36018e62465c727aa76 Mon Sep 17 00:00:00 2001 From: leonarski_f Date: Mon, 22 Jun 2026 14:59:23 +0200 Subject: [PATCH] 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 --- reader/HDF5MetadataSource.cpp | 51 +++++++++++++++++--- reader/HDF5MetadataSource.h | 9 ++++ reader/JFJochHDF5Reader.cpp | 7 ++- reader/JFJochReaderDataset.h | 6 +++ viewer/JFJochProcessController.cpp | 4 ++ viewer/JFJochViewerDatasetInfo.cpp | 11 +++-- viewer/charts/JFJochDatasetInfoChartView.cpp | 18 ++++--- viewer/charts/JFJochDatasetInfoChartView.h | 12 +++-- 8 files changed, 96 insertions(+), 22 deletions(-) diff --git a/reader/HDF5MetadataSource.cpp b/reader/HDF5MetadataSource.cpp index 8a784b05..bde7cd52 100644 --- a/reader/HDF5MetadataSource.cpp +++ b/reader/HDF5MetadataSource.cpp @@ -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("/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(numbers[i])] = static_cast(i); + } + } + dataset_ = dataset; return OpenResult{ @@ -726,6 +743,15 @@ HDF5ImageLocator::Location HDF5MetadataSource::ResolveMeta(int64_t global) const return {master_file, static_cast(global)}; } +std::optional 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 &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(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 HDF5MetadataSource::ReadReflections(size_t start return ret; } -std::vector HDF5MetadataSource::ReadSpots(int64_t image) const { - if (image < 0) +std::vector 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 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(image); + tmp_message.number = requested_image; ReadSpotsFromFiles(*master_file, *meta_file, image, meta_image_id, - static_cast(image), + requested_image, cached_geom, tmp_message); diff --git a/reader/HDF5MetadataSource.h b/reader/HDF5MetadataSource.h index 1196b721..7731e4e5 100644 --- a/reader/HDF5MetadataSource.h +++ b/reader/HDF5MetadataSource.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "JFJochReaderDataset.h" @@ -62,6 +63,14 @@ private: uint64_t number_of_images = 0; const HDF5ImageSource *image_source_ = nullptr; + // Inverse of dataset_->source_image_number (original image number -> local index in this + // master). Empty for a 1:1 source; populated when this metadata covers a subset of images. + std::unordered_map image_to_local_; + + // Translate a global/original image number to the local index in this metadata source, or + // nullopt if this source does not cover that image. Identity for a 1:1 source. + std::optional ToLocalIndex(int64_t image_number) const; + HDF5ImageLocator::Location ResolveMeta(int64_t global) const; std::optional ReadAxis(HDF5Object *file, const std::string &name); void ReadROIMetadata(HDF5ReadOnlyFile &file, JFJochReaderDataset &dataset) const; diff --git a/reader/JFJochHDF5Reader.cpp b/reader/JFJochHDF5Reader.cpp index 3dfcd9cd..511f9f87 100644 --- a/reader/JFJochHDF5Reader.cpp +++ b/reader/JFJochHDF5Reader.cpp @@ -142,9 +142,12 @@ void JFJochHDF5Reader::RegisterSnapshot(const std::string &name, const std::stri auto metadata = std::make_shared(); auto open_result = metadata->Open(master_path, default_experiment); - if (open_result.number_of_images != number_of_images) + // A snapshot may cover a subset of the images (a sub-range or filtered reprocessing); its + // /entry/detector/number map (read in Open) ties each snapshot image back to an original one. + // It just must not claim more images than the dataset has. + if (open_result.number_of_images > number_of_images) throw JFJochException(JFJochExceptionCategory::InputParameterInvalid, - "Snapshot image count does not match the open dataset"); + "Snapshot has more images than the open dataset"); // Snapshot pixels come from the existing image source; its own (integrated) master holds the // per-image metadata at the global index. diff --git a/reader/JFJochReaderDataset.h b/reader/JFJochReaderDataset.h index d4dc7d44..6fe956f0 100644 --- a/reader/JFJochReaderDataset.h +++ b/reader/JFJochReaderDataset.h @@ -47,6 +47,12 @@ struct JFJochReaderDataset { std::vector image_scale_b; std::vector max_value; + // Maps this dataset's image index -> the original/collected image number it came from. + // Empty means identity (image i == original image i). Lets a dataset be a subset (or strided + // selection) of the truly collected images: reprocessing snapshots over a sub-range, and (in + // future) a main dataset that was filtered on-the-fly during collection. + std::vector source_image_number; + std::vector roi; std::vector> roi_sum; std::vector> roi_sum_sq; diff --git a/viewer/JFJochProcessController.cpp b/viewer/JFJochProcessController.cpp index 7adc058c..ff705c2a 100644 --- a/viewer/JFJochProcessController.cpp +++ b/viewer/JFJochProcessController.cpp @@ -101,6 +101,10 @@ void JFJochProcessController::OnImageProcessed(const DataMessage &msg) { v[i] = val; }; auto &d = *live_dataset_; + // Map this ordinal back to its original image number (for the x-axis of subset/strided runs). + if (static_cast(d.source_image_number.size()) <= i) + d.source_image_number.resize(i + 1, 0); + d.source_image_number[i] = static_cast(msg.original_number.value_or(msg.number)); if (msg.spot_count) put(d.spot_count, *msg.spot_count); if (msg.spot_count_indexed) put(d.spot_count_indexed, *msg.spot_count_indexed); if (msg.spot_count_low_res) put(d.spot_count_low_res, *msg.spot_count_low_res); diff --git a/viewer/JFJochViewerDatasetInfo.cpp b/viewer/JFJochViewerDatasetInfo.cpp index 4adbda9a..54657890 100644 --- a/viewer/JFJochViewerDatasetInfo.cpp +++ b/viewer/JFJochViewerDatasetInfo.cpp @@ -19,6 +19,11 @@ namespace { constexpr int n = static_cast(sizeof(palette) / sizeof(palette[0])); return palette[((index % n) + n) % n]; } + + // A run's image->original-number map as floats for the chart x-axis (empty => identity). + std::vector XForRun(const JFJochReaderDataset &ds) { + return {ds.source_image_number.begin(), ds.source_image_number.end()}; + } } JFJochViewerDatasetInfo::JFJochViewerDatasetInfo(QWidget *parent) : QWidget(parent) { @@ -236,16 +241,16 @@ void JFJochViewerDatasetInfo::UpdatePlot() { if (run.id == active_id_) { primary_name = run.label; primary_color = color; continue; } if (!run.dataset || run.dataset == dataset) continue; // never draw the active run twice bool ignore = false; - overlays.push_back({run.label, ExtractMetric(*run.dataset, val, ignore), color}); + overlays.push_back({run.label, ExtractMetric(*run.dataset, val, ignore), color, XForRun(*run.dataset)}); } if (live_run_ && live_run_ != dataset) { bool ignore = false; overlays.push_back({QStringLiteral("Live"), ExtractMetric(*live_run_, val, ignore), - RunColor(static_cast(runs_.size()))}); + RunColor(static_cast(runs_.size())), XForRun(*live_run_)}); } chart_view->loadValues(data, image_number, one_over_d2, dataset.get(), primary_name, - std::move(overlays), primary_color); + std::move(overlays), primary_color, XForRun(*dataset)); if (dataset->experiment.GetGridScan()) { stack->setCurrentWidget(grid_scan_image); diff --git a/viewer/charts/JFJochDatasetInfoChartView.cpp b/viewer/charts/JFJochDatasetInfoChartView.cpp index c2f7cda0..25e65dde 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.cpp +++ b/viewer/charts/JFJochDatasetInfoChartView.cpp @@ -131,10 +131,12 @@ void JFJochDatasetInfoChartView::resetZoom() { void JFJochDatasetInfoChartView::loadValues(const std::vector &input, int64_t image, bool one_over_d2, const JFJochReaderDataset *dataset, const QString &primaryName, - std::vector overlays, const QColor &primaryColor) { + std::vector overlays, const QColor &primaryColor, + std::vector primaryX) { m_yOneOverD = one_over_d2; primaryName_ = primaryName; primary_color_ = primaryColor; + primary_x_ = std::move(primaryX); // d -> 1/d^2 for resolution plots; identity otherwise. Applied to every series alike. auto transform = [one_over_d2](const std::vector &in, std::vector &out) { @@ -166,12 +168,16 @@ void JFJochDatasetInfoChartView::loadValues(const std::vector &input, int } void JFJochDatasetInfoChartView::appendSeries(QLineSeries *s, const std::vector &vals, - double &mn, double &mx) const { + const std::vector &xs, double &mn, double &mx) const { + // x position for value index i: the mapped image number if available, else the index itself. + auto xpos = [&xs](int64_t i) -> double { + return i < static_cast(xs.size()) ? static_cast(xs[i]) : static_cast(i); + }; if (binning == 1) { for (int i = 0; i < static_cast(vals.size()); i++) { const double v = vals[static_cast(i)]; if (!std::isfinite(v)) continue; - s->append(i, v); + s->append(xpos(i), v); mn = std::min(mn, v); mx = std::max(mx, v); } @@ -187,7 +193,7 @@ void JFJochDatasetInfoChartView::appendSeries(QLineSeries *s, const std::vector< } if (count > 0) { const double mean = tmp / static_cast(count); - s->append((i + 0.5) * binning, mean); + s->append(xpos(static_cast(i) * binning + binning / 2), mean); mn = std::min(mn, mean); mx = std::max(mx, mean); } @@ -234,7 +240,7 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() { double dispMin = std::numeric_limits::infinity(); double dispMax = -std::numeric_limits::infinity(); - appendSeries(series, values, dispMin, dispMax); + appendSeries(series, values, primary_x_, dispMin, dispMax); // Overlay runs share the primary's axes and range; build them now so the Y range fits all. std::vector overlayLines; @@ -244,7 +250,7 @@ void JFJochDatasetInfoChartView::buildTimeDomainChart() { line->setName(ov.name); if (ov.color.isValid()) line->setColor(ov.color); - appendSeries(line, ov.values, dispMin, dispMax); + appendSeries(line, ov.values, ov.x, dispMin, dispMax); overlayLines.push_back(line); } diff --git a/viewer/charts/JFJochDatasetInfoChartView.h b/viewer/charts/JFJochDatasetInfoChartView.h index 7a1daf91..ce3f6bca 100644 --- a/viewer/charts/JFJochDatasetInfoChartView.h +++ b/viewer/charts/JFJochDatasetInfoChartView.h @@ -48,12 +48,15 @@ class JFJochDatasetInfoChartView : public QChartView { public: // One overlay line (a non-active run), drawn alongside the primary series with a legend entry. - struct NamedSeries { QString name; std::vector values; QColor color; }; + // x maps each value to an image number on the shared x-axis (empty => 0,1,2,... by index). + struct NamedSeries { QString name; std::vector values; QColor color; std::vector x; }; private: std::vector overlays_; QColor primary_color_; // colour of the active (primary) run, if assigned - // Append (binned) finite points of vals to s, extending the [mn, mx] data range. - void appendSeries(QLineSeries *s, const std::vector &vals, double &mn, double &mx) const; + std::vector primary_x_; // x (image numbers) for the primary series; empty => index + // Append (binned) finite points of vals to s at x positions (xs empty => index), extending [mn,mx]. + void appendSeries(QLineSeries *s, const std::vector &vals, const std::vector &xs, + double &mn, double &mx) const; // FFT view toggle bool m_showFFT = false; @@ -95,7 +98,8 @@ public: const JFJochReaderDataset *dataset = nullptr, const QString &primaryName = QString(), std::vector overlays = {}, - const QColor &primaryColor = QColor()); + const QColor &primaryColor = QColor(), + std::vector primaryX = {}); };