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 = {}); };