The rc178 branch continued after #88 was squash-merged; this is its content fromb9ada7914(the tree #88 squashed) to094eee31a, as one change on main. The individual commits stay on the rc178 branch: - still battery: --cold evicts each set's input from the page cache; Alvra pixel masks; manifest fixes - AGIPD: converter to one rigid panel per module, int16 photons, mask folded in; still battery reads the converted sets - DetectorModules: panel look-up by id, plane -> panel, plane bounds and a per-pixel walk; mask size checked - tests: [PanelLayout] equivalence harness, stacked vs assembled panel layout - Dense per-pixel maps through the panel table (panel layout stage 1) - Panel id on spots and the panel table in the start message - Viewer and preview show a panel detector as it lies on the detector plane - Panel layout stage 3: per-panel spot finding (CPU+GPU, CCL) and raster algorithms on the plane raster - Panel layout stage 2: prediction and Bragg integration through the panel table - XTC reader: frames handed up as stored, streams indexed in parallel - XFEL reader: integer frames stay integer, one gain, placement by panel - EXTERNAL_TEST_DATA_STILLS.md: images used vs the entry for every set; drop "Not in the battery" - still battery: EuXFEL sets read the depositors' Cheetah CXI files directly; AGIPD converter removed - float32_to_int16.py: a non-finite pixel becomes -32768, the int16 error marker, not a zero count - CrystFEL geometry: read the pixel masks and bad regions instead of ignoring them - Panel layout stage 5: the XFEL reader hands up the stored frame and its panel table - Panel table persisted in NXmx; modulation grid and pre-scan memo key on the plane - still battery: Alvra sets take their pixel mask from the CrystFEL geometry, not --mask-good - still battery: take the deposited model as mmCIF where the RCSB has no PDB-format file - still battery: dimple gets PDB-format coordinates converted from an mmCIF-only deposition - stills fixed cost: geometry refinement and pre-scan do the same work in less wall time - stills geometry sample and trial spot finding run as many workers as the image pass - Stills integration: background ring reach measured on empty apertures, radial curvature from the whole frame - StillsPartialityRefine: the partiality width is the run's, measured on the crystals, not the prediction window's - MergeOnTheFly: capture term and multiplicative outlier band for the stills merge (both opt-in) - Stills given a cell: the written cell is the mean over the frames that are merged - Stills: the radial curvature correction decided per run on the same empty apertures - StillsPartialityRefine: measure the run width only on shells with signal - Stills pre-scan: report the paired z of the radial-correction comparison - module placement: the five concurrent fits each keep every thread - still battery: score R-free against the deposited data through the same dimple protocol - still battery: the R-free target is the lower of the deposition's dimple and published values - Stills: background clip follows the radial curve; signal disk from the lattice spacing - Stills keep an overloaded observation as a lower bound instead of dropping it - Stills: drop the lattice-spacing signal disk; the correction decision back on the plain clip - battery: R-free against the deposition without refinement only, one protocol for rotation and stills - still battery: EuXFEL sets run on a 2,000-image subset for now - stills geometry refinement: fit the detector distance and the cell together - stills geometry refinement: a distance given with --detector-distance is held in the joint fit - Metal integration engine: back to its panel-s5 kernels; Mac stills keep the old clip, no saturation count - Metal integration test: the CPU comparison runs on a rotation experiment - Stills: kept overloads go back to the drop where they are not concentrated at low resolution - stills distance search: an absolute 30-3000 mm scan, a sample found without d-limits, the fit iterated - battery results: XFEL still battery 20261022-1358 on rc1780cf62452c- 12 sets, cold, R_free unrefined - battery results: still summary gains the resolution of our merge and of the deposition - Pre-scan masks a pixel at the saturation limit on most frames, as one holding the error value - Stills keep an overload as a lower bound only where it has pixels at the limit; run-level gate removed - XFEL reader: a per-frame mask is read the way the frames are, its chunk inflated outside the HDF5 lock - stills distance search: every centre scanned with a wider acceptance, a larger sample where nothing indexes - rugnux usage: --beam-center-check describes the absolute 30-3000 mm stills distance scan, not the old 60-160 % grid - stills: detector tilt refined in the geometry pre-pass, kept where held-out frames agree - CLAUDE.md: the changelog is written once, just before the merge into main - condensed one-liners of what users get - broker: serve the placed model and its maps of the last rugnux live run (/rugnux/model.cif, /rugnux/map.ccp4) - frontend: Mol* viewer of the placed model and its density on the Rugnux live page - writer: _model.cif from rugnux live keeps the input model's metadata, as the served model.cif does - rugnux live maps: one unit cell centred on the model; the viewer opens on all of it - Anomalous difference map: ANOM/PHANOM in _maps.mtz, /rugnux/map.ccp4?type=anom, a magenta mesh in the viewer - Mol* viewer: open with the maps drawn within 10 A of the centre of view - Mol* viewer: aaredb's structure-viewer setup, on a black stage - rugnux: the gain of a stills file that states none is read off its single-photon peak where the background resolves photons - Mol* viewer: in line with AareDB's StructureView at its current head (8ece2c2) - ACKNOWLEDGEMENT: the web frontend's Mol* viewer follows AareDB's setup (G. Gotthard, PSI) - stills tilt decision: the modules stay where the run placed them - still battery: no distance pins and no per-set tuning flags; gain from the data except where a file's baseline hides it - battery results: rc178 6f6f69 - full still battery and full rotation battery with REFMAC - Bragg integration: a CCD rotation sweep's sigmas at the measured gain, intensities in photons - BackgroundGain: the gain of a frame that resolves photons is the spacing of its photon peaks, so a baseline left in the frame no longer hides it - ShadowFinder: measure the background's azimuthal modulation instead of predicting it from the polarization - BackgroundGain: one gain estimate for rotation and stills; the still battery no longer gives EuXFEL 152 its gain - ShadowFinder: measure the modulation robustly under a wide one-sided shadow - ShadowFinderTest: a one-sided shadow is not fitted as part of the background's modulation - ShadowFinder: back to the predicted polarization factor, in the plane the background shows - MXAnalysisAfterFPGA: the Bragg engine's edge mask in the image's layout, so raw / untransformed FPGA runs start again - battery results: still battery on rc178 e54f55 replaces the earlier rc178 still tables - XFELReader: per-frame mask and photon conversion without a library call or a branch per pixel - stills geometry trial: the hypotheses a search asks are indexed side by side - stills tilt decision: the fits start beside the pre-scan and the geometry trial - XFELReader: a deflated frame is inflated and converted a piece at a time, while it is in the cache - XFELReader: a byte-shuffled frame is unshuffled a piece at a time, a whole pixel per step - GeometryRefiner: residuals carry only the blocks a fit moves; beam and distance one block - GeometryRefiner: a spot's residual differentiates its observed and predicted halves apart - docs: EXTERNAL_TEST_DATA lists every open-arm battery set, with corrected values and counts - battery: doc_check.py cross-checks the manifests against the dataset pages - stills module placement: the placement fit gets half the threads, each held-out fit an eighth - still battery: the EuXFEL sets run on all their frames, as the published still runs did - battery results: still battery on rc178 a0ce6c, cold on a quiet machine, replaces the earlier rc178 table - macOS build: explicit size_t in the XFEL reader's row bounds; parentheses in the CPU preprocessor's select Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
189 lines
11 KiB
C++
189 lines
11 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <cstddef>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <string>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "JFJochReader.h"
|
|
#include "DetectorLayout.h"
|
|
#include "HDF5ImageSource.h"
|
|
|
|
class HDF5ReadOnlyFile;
|
|
|
|
// Reads serial (stills) frames from HDF5 files written by the XFEL pipelines, and places their panels on
|
|
// the detector plane:
|
|
//
|
|
// - a SACLA Cheetah file (one group per shot, /tag-N/data, the sensors stacked in the frame): the
|
|
// sensors are placed where the file's /metadata puts them, with its camera length and pixel mask,
|
|
// and the photon energy of every shot;
|
|
// - a Cheetah CXI file of a single-panel detector (the frames stacked at /entry_1/data_1/data): one
|
|
// flat panel with the pixel size, photon energy and camera length the file states, centred on the
|
|
// beam - the run measures the centre;
|
|
// - any of these, or files holding one frame each, laid out by a CrystFEL geometry file instead.
|
|
//
|
|
// A detector of several panels is handed out as stored: the image is the stored frame, and each panel a
|
|
// module (DetectorModules) placing its box of the frame on the detector plane exactly where the layout puts
|
|
// it, which the run maps its pixels, spots and predictions through. Pixels on no panel, and every masked or
|
|
// bad region, are masked. A single panel is put on an image of its own whole, as a block, its axes turned to
|
|
// the nearest image axis and its first pixel at a whole pixel; the rest of its placement is its module's. Pixel values are
|
|
// converted to photons with the gain where one is given or measured, one value for every panel, and are
|
|
// otherwise left in detector units: integers then stay integers, a 16-bit signed frame in its own type.
|
|
//
|
|
// One wavelength and one camera length hold for the whole run: where the file states one per shot, it
|
|
// is the mean over every frame of the run.
|
|
//
|
|
// A path is one file, or a directory, every .h5/.cxi file of which is read in name order. Only a few
|
|
// files are held open at a time, so a run of thousands of files is read as well as one.
|
|
class JFJochXFELReader : public JFJochReader {
|
|
std::vector<xfel::Mask> extra_masks_;
|
|
struct Frame {
|
|
size_t file;
|
|
std::string dataset;
|
|
int64_t index; // along the first dimension of a 3D dataset; -1 for a 2D one
|
|
std::string group; // the shot group the geometry's % stands for, if any
|
|
};
|
|
|
|
xfel::Layout geom_;
|
|
std::string source_; // where geom_ came from, for messages
|
|
std::vector<std::string> files_;
|
|
std::vector<Frame> frames_;
|
|
std::shared_ptr<JFJochReaderDataset> dataset_;
|
|
|
|
// The most recently used files, held open; the least recent is closed past MAX_OPEN_FILES.
|
|
static constexpr size_t MAX_OPEN_FILES = 8;
|
|
mutable std::vector<std::pair<size_t, std::unique_ptr<HDF5ReadOnlyFile>>> open_files_;
|
|
HDF5ReadOnlyFile &FileAt(size_t file) const; // under hdf5_mutex
|
|
std::shared_ptr<const HDF5ImageSource::RawFile> RawFileAt(size_t file) const; // under hdf5_mutex
|
|
|
|
size_t frame_width_ = 0, frame_height_ = 0; // the stored frame
|
|
size_t width_ = 0, height_ = 0; // the image handed out
|
|
// A single panel: where its pixels land on the image - its first stored pixel, its size, and the image
|
|
// index of its first pixel and the steps of +1 fs and +1 ss there. Empty for a stored frame.
|
|
struct Block {
|
|
size_t src = 0;
|
|
int64_t width = 0, height = 0;
|
|
int64_t dst = 0, step_fs = 1, step_ss = 0;
|
|
};
|
|
std::vector<Block> blocks_;
|
|
// What one photon reads, the same on every panel; 1 where none is known.
|
|
float adu_per_photon_ = 1.0f;
|
|
bool counts_in_photons_ = false;
|
|
double clen_m_ = 0, distance_mm_ = 0, photon_energy_eV_ = 0;
|
|
// Masks stored with one plane per frame (a Cheetah CXI's per-shot mask), applied as each frame is read.
|
|
std::vector<xfel::Mask> frame_masks_;
|
|
// Marks in bad every stored pixel the mask m, read as v, leaves out (on its panel, where it has one); v holds
|
|
// the n pixels of the frame from first on.
|
|
template <class T>
|
|
void MarkBad(const xfel::Mask &m, const T *v, size_t first, size_t n, std::vector<uint8_t> &bad) const;
|
|
|
|
// Where every frame is stored as one chunk of a plain little-endian number type - deflated (and
|
|
// optionally byte-shuffled), as Cheetah writes it, or bitshuffle/LZ4 (or zstd), as Jungfraujoch does -
|
|
// only the chunk's place in the file is asked of HDF5 under its lock; the bytes are read and decoded
|
|
// outside it, on every reading thread at once, the way the NXmx reader reads its images
|
|
// (HDF5ImageSource: the chunk lookup, its RawFile, and CompressedImage's decoders).
|
|
struct Layout {
|
|
uint64_t height = 0, width = 0;
|
|
bool direct_chunks = false, shuffled = false, deflated = false;
|
|
CompressionAlgorithm algorithm = CompressionAlgorithm::NO_COMPRESSION; // where not deflated
|
|
bool stored_float = false, stored_signed = false;
|
|
size_t stored_size = 0;
|
|
// A 2- or 4-byte little-endian integer or a 4-byte float, read as stored; any other type HDF5 reads
|
|
// as float.
|
|
bool plain_type = false;
|
|
bool operator==(const Layout &) const = default;
|
|
};
|
|
Layout layout_;
|
|
// For each of frame_masks_, how it is stored where it is read as the frames are (ReadStored): plain
|
|
// unsigned integers of the frame's size, stored alike in every file; plain_type false where HDF5 reads it.
|
|
std::vector<Layout> frame_mask_layouts_;
|
|
// Positional-read handles on the files, opened as first needed (under hdf5_mutex), with the size of
|
|
// each file's user block, which HDF5 chunk addresses count from. Like open_files_, only the most
|
|
// recently used MAX_OPEN_FILES are held (raw_order_); a reader still using a released one keeps it.
|
|
mutable std::vector<std::shared_ptr<const HDF5ImageSource::RawFile>> raw_files_;
|
|
mutable std::vector<uint64_t> user_block_;
|
|
mutable std::vector<size_t> raw_order_;
|
|
static Layout ReadLayout(HDF5ReadOnlyFile &file, const Frame &frame);
|
|
Layout FrameMaskLayout(const xfel::Mask &m) const; // under hdf5_mutex
|
|
|
|
// The files, in the order given, every frame in them, and the run's mean of every value the geometry
|
|
// takes from the files.
|
|
void Open(const std::vector<std::string> &files);
|
|
// Places the panels, given the gains, on the detector plane; builds the dataset.
|
|
void Place();
|
|
|
|
bool LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset,
|
|
DataMessage &message,
|
|
std::vector<uint8_t> &buffer,
|
|
int64_t image_number,
|
|
bool update_dataset) override;
|
|
|
|
// The frame's pixels, stored as layout says, as stored (plain_type), or as float, into stored; with
|
|
// keep_compressed, a chunk decoded by CompressedImage (not deflated) is left as stored in the file, and
|
|
// true returned. Given piece, the pixels are handed to piece(first pixel, their bytes, pixels) instead: a
|
|
// deflated chunk a piece at a time, as it is inflated (InflateInPieces) or unshuffled, any other at once.
|
|
template <class Buffer, class Piece = std::nullptr_t>
|
|
bool ReadStored(const Frame &frame, const Layout &layout, Buffer &stored, bool keep_compressed,
|
|
Piece piece = nullptr) const;
|
|
// The deflated stream compressed, bytes long once inflated, to piece a piece of whole pixels of size bytes
|
|
// at a time.
|
|
template <class Piece>
|
|
void InflateInPieces(const std::vector<uint8_t> &compressed, size_t bytes, size_t size, const Frame &frame,
|
|
Piece piece) const;
|
|
// The frame's stored pixels, as In, as the image handed out (as stored, or placed), as Out, each through
|
|
// convert; a pixel masked marks is err.
|
|
template <class Out, class In, class Buffer, class Convert>
|
|
void PlaceInto(const Frame &frame, const std::vector<uint8_t> &masked, Out err, Buffer &buffer, Convert convert) const;
|
|
template <class Buffer>
|
|
CompressedImage DecodeInto(int64_t image_number, Buffer &buffer) const;
|
|
|
|
public:
|
|
JFJochXFELReader();
|
|
~JFJochXFELReader() override;
|
|
|
|
// gain, where given, is the detector units per photon of every panel, whatever the geometry says.
|
|
// files are read in the order given, their frames one after another.
|
|
void ReadFiles(const std::vector<std::string> &files, const std::string &geometry_file,
|
|
std::optional<float> gain = std::nullopt);
|
|
|
|
// Cheetah CXI files of a single-panel detector: one flat panel, placed by
|
|
// what the file states, centred on the beam.
|
|
static bool CanReadCXI(const std::string &path);
|
|
// Why a Cheetah CXI file is not read without a geometry file (a tiled detector's slab), or nothing
|
|
// if the path is not one.
|
|
static std::optional<std::string> CXIRefusal(const std::string &path);
|
|
void ReadCXI(const std::vector<std::string> &files, std::optional<float> gain = std::nullopt);
|
|
|
|
// Cheetah HDF5 files with one group per shot and the panels' placement in /metadata (posx/posy in um, the
|
|
// in-plane angle, pixel size, camera length) - as SACLA writes them - placed by that table.
|
|
static bool CanReadPanelTable(const std::string &path);
|
|
void ReadPanelTable(const std::vector<std::string> &files, std::optional<float> gain = std::nullopt);
|
|
|
|
// Mask arrays in other files, in the frames' own layout (--mask-good / --mask-bad), applied with those
|
|
// the data files state; set before reading.
|
|
void ExtraMasks(const std::vector<xfel::Mask> &masks) { extra_masks_ = masks; }
|
|
|
|
// Converts to photons with this gain from here on (detector units per photon, every panel).
|
|
void ApplyGain(float adu_per_photon);
|
|
// Back to detector units: undoes ApplyGain where nothing read states a gain.
|
|
void ClearGain();
|
|
|
|
// False where no gain is known, so the images are in detector units, not photons.
|
|
[[nodiscard]] bool CountsInPhotons() const;
|
|
// True where the frames are stored as floats, which are rounded to whole detector units as read.
|
|
[[nodiscard]] bool StoredFloat() const;
|
|
// False where nothing read states the photon energy (a geometry file over frames of no known layout):
|
|
// the caller gives it.
|
|
[[nodiscard]] bool HasPhotonEnergy() const;
|
|
|
|
[[nodiscard]] uint64_t GetNumberOfImages() const override;
|
|
void Close() override;
|
|
bool ReadRawImage(int64_t image_number, JFJochReaderRawImage &image) override;
|
|
[[nodiscard]] std::vector<SpotToSave> ReadSpots(int64_t image) const override;
|
|
};
|