Files
Jungfraujoch/reader/HDF5MetadataSource.h
T
leonarski_fandClaude Opus 5 df3696e6e2 reader: fall back to the pre-NXmx key names, so an Eiger 1.x master opens
Firmware 1.x writes the same three values under different names. Try the NXmx spelling first and
the old one only if it is absent:

    detector/distance         <- detector/detector_distance
    detector/saturation_value <- detectorSpecific/countrate_correction_count_cutoff
    sample/transformations    <- sample/goniometer

This cannot change what a current file reads: every modern Eiger master carries BOTH spellings.
Measured on thirteen masters from ten facilities, firmware release-2020.2.1 through
release-2024.1.1 - all of them write detector_distance and countrate_correction_count_cutoff beside
the NXmx names, and a goniometer group beside the transformations one.

The goniometer is the one that matters. A goniometer is only ever set from that one group, so a
file whose axes are somewhere else was not an error - it was read as STILLS, silently, and the run
completed with the wrong answer. The old layout also tags no axis with transformation_type and
gives no vector, both of which ReadAxis required, so absence now means two different things by
layout: in a transformations group it still means "not an axis" (that is how AXISNAME_end and the
width scalars are skipped), while in the legacy group every leaf IS an axis and the companions are
recognised by name instead. A missing direction defaults to the one every DECTRIS master since has
written and says so in a warning rather than assuming it silently; a wrong guess there does not
index, so it is visible, and the rotation first pass tries the opposite sign anyway.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 21:56:12 +02:00

83 lines
4.1 KiB
C++

// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
// SPDX-License-Identifier: GPL-3.0-only
#pragma once
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include "JFJochReaderDataset.h"
#include "HDF5ImageSource.h"
#include "../common/JFJochMessages.h"
#include "../common/SpotToSave.h"
#include "../common/GoniometerAxis.h"
#include "../common/DiffractionExperiment.h"
#include "../common/DiffractionGeometry.h"
#include "../image_analysis/IntegrationOutcome.h"
// Metadata side of the reader: everything read from one master file that is NOT raw pixels -
// dataset-level metadata (geometry, mask, ROI definitions, azimuthal mapping, the per-image plot
// arrays) and per-image metadata (spots, reflections, MX scalars, azimuthal profile, lattice).
//
// This is the swappable part: a dataset can have several metadata sources over the same images
// (the original _master.h5 plus reprocessing _process.h5 snapshots). Per-image metadata is
// located either through the shared image source (original file: metadata sits in the same files
// as the pixels) or in this master at the global index (integrated _process.h5 snapshot).
//
// HDF5 is not thread-safe; all calls must be made with the global hdf5_mutex held by the caller.
class HDF5MetadataSource {
public:
struct OpenResult {
HDF5ImageLocator::Layout image_layout; // layout implied by this master (for the image source)
uint64_t number_of_images = 0;
};
// Parse a master file's dataset-level metadata into a JFJochReaderDataset. default_experiment
// seeds the dataset's experiment. Returns the image layout it implies; the caller decides
// whether to use it to configure the shared image source.
OpenResult Open(const std::string &filename, const DiffractionExperiment &default_experiment);
// How per-image metadata is located: with image_source set, metadata is co-located with the
// pixels (original file); when null it lives in this master at the global index.
void UseImageSourceForMetadata(const HDF5ImageSource *image_source) { image_source_ = image_source; }
std::shared_ptr<JFJochReaderDataset> Dataset() const { return dataset_; }
uint64_t NumberOfImages() const { return number_of_images; }
// Per-image metadata (spots, azimuthal profile, MX scalars, lattice, reflections) into msg.
void FillPerImage(DataMessage &message, int64_t image_number,
const std::shared_ptr<const JFJochReaderDataset> &dataset) const;
std::vector<SpotToSave> ReadSpots(int64_t image) const;
// True if the file stores spot-finding results (/entry/MX); a plain DECTRIS file has none.
[[nodiscard]] bool HasSpots() const;
std::vector<IntegrationOutcome> ReadReflections(size_t start_image, std::optional<size_t> end_image) const;
CompressedImage ReadCalibration(std::vector<uint8_t> &tmp, const std::string &name) const;
private:
std::shared_ptr<HDF5ReadOnlyFile> master_file;
std::string master_filename;
std::shared_ptr<JFJochReaderDataset> dataset_;
DiffractionGeometry cached_geom;
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<int64_t, int64_t> 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<int64_t> ToLocalIndex(int64_t image_number) const;
HDF5ImageLocator::Location ResolveMeta(int64_t global) const;
// group is where the axes live: /entry/sample/transformations (NXmx) or
// /entry/sample/goniometer (DECTRIS firmware 1.x, which tags no axis and gives no vector).
std::optional<GoniometerAxis> ReadAxis(HDF5Object *file, const std::string &name,
const std::string &group);
void ReadROIMetadata(HDF5ReadOnlyFile &file, JFJochReaderDataset &dataset) const;
};