Combines the stills reader and module work (stills-readers, stills-modgeom, stills-euxfel) onto rc178 with the owner's decisions for this release: - Inputs are files, never a directory; several files of one format are one dataset (the multi-input CLI of the previous commit), so every reader takes an explicit list: JFJochXFELReader::ReadFiles, ReadCXI, ReadPanelTable and JFJochXTCReader::ReadFiles take std::vector<std::string>; ListFiles and xtc::CollectStreams are gone, CanRead* accept a regular file only. - One HDF5 frame reader (JFJochCrystFELReader -> JFJochXFELReader) parameterised by what the file holds: per-shot groups or a 3D stack, a /metadata panel table (posx/posy/angle, pixel size, nominal camera length, pixel mask: as SACLA writes), a Cheetah CXI single panel (square frame; a tiled slab is refused with a message). At most 8 files open; every file checked against the first. Signed 32-bit decode, per-frame 3D masks, panels at their own coffset (from the EuXFEL work). - Frames stored one chunk each are read the way the NXmx reader reads its images: only the chunk's place is asked of HDF5 under its lock (H5Dget_chunk_info_by_coord), the bytes are read through HDF5ImageSource::RawFile (positional, no lock) and decoded outside it - deflate(+shuffle) by zlib, bitshuffle/LZ4 (and zstd, plain LZ4) by CompressedImage's decoders. Bitshuffle/LZ4 used to go through HDF5's filter under the global lock, one frame at a time. - LCLS-I XTC (Rayonix): wavelength from the beam record, pixel size from the Rayonix model and binning, only --detector-distance needed; damaged/compressed frames skipped, a truncated last event dropped; std::ifstream, no POSIX calls: builds with MSVC. No DIALS .expt support at all. - CrystFEL .geom: module geometry only (panel ranges, fs/ss, corners, pixel size, coffset, data path) and optionally the camera length; everything else is ignored with a note in the log. - --mask-good / --mask-bad <file>:<dataset>: a mask array in another HDF5 file, in the frames' stored layout (what a .geom's mask_file used to give). - Gain measured on the background for panel-table and CXI input that states none (--gain overrides). - Detector modules from explicit geometry only (DetectorModules: the reader's panels, an NXmx file with one NXdetector_module per module, or --module-geometry); never from the image. Spots and predictions are mapped through the modules, the image is not re-assembled. - Module refinement in the stills bundle adjustment (Ceres; per-module shift and in-plane turn, gauge on mean shift/turn/scale and on the two tilt-like patterns when the tilt is refined; adopted only where it predicts held-out frames better). Applied for a foreign multi-panel detector (panel table, .geom panels); for a JUNGFRAU measured and reported only. Written to <prefix>_modules.h5 (NXmx NXdetector_module groups), read back with --module-geometry. - A panel-table file's camera length is nominal: with a reference cell it is refined from the cell. - Broker and writer output unchanged: masters keep the single NXdetector_module group, the start message carries no module list. Tests: [CrystFELGeometry], JFJochXFELReader_*, JFJochXTCReader, [JFJochMultiReader], [CellPopulation], SaturationCeiling_*, [DetectorModules], RefineGlobalGeometry_ModulePlacement, [reindex], Sublattice. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015CeEGgpKdfkCTJb3YNMJcu
185 lines
8.9 KiB
C++
185 lines
8.9 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#include <catch2/catch_all.hpp>
|
|
#include <filesystem>
|
|
#include <hdf5.h>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "../reader/JFJochXFELReader.h"
|
|
|
|
// Synthetic SACLA Cheetah and Cheetah CXI files, written with the HDF5 C API the way those pipelines
|
|
// write them: whole frames as single deflated (and byte-shuffled) chunks, so the direct-chunk path is
|
|
// what is read.
|
|
namespace {
|
|
|
|
void WriteFrames(hid_t loc, const std::string &name, hid_t type, const void *data, std::vector<hsize_t> dims) {
|
|
const hid_t space = H5Screate_simple(static_cast<int>(dims.size()), dims.data(), nullptr);
|
|
std::vector<hsize_t> chunk = dims;
|
|
if (chunk.size() == 3)
|
|
chunk[0] = 1;
|
|
const hid_t dcpl = H5Pcreate(H5P_DATASET_CREATE);
|
|
H5Pset_chunk(dcpl, static_cast<int>(chunk.size()), chunk.data());
|
|
H5Pset_shuffle(dcpl);
|
|
H5Pset_deflate(dcpl, 4);
|
|
const hid_t ds = H5Dcreate2(loc, name.c_str(), type, space, H5P_DEFAULT, dcpl, H5P_DEFAULT);
|
|
H5Dwrite(ds, type, H5S_ALL, H5S_ALL, H5P_DEFAULT, data);
|
|
H5Dclose(ds);
|
|
H5Pclose(dcpl);
|
|
H5Sclose(space);
|
|
}
|
|
|
|
void WriteVector(hid_t loc, const std::string &name, const std::vector<double> &v) {
|
|
const hsize_t n = v.size();
|
|
const hid_t space = H5Screate_simple(1, &n, nullptr);
|
|
const hid_t ds = H5Dcreate2(loc, name.c_str(), H5T_NATIVE_DOUBLE, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
|
H5Dwrite(ds, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, v.data());
|
|
H5Dclose(ds);
|
|
H5Sclose(space);
|
|
}
|
|
|
|
void WriteScalar(hid_t loc, const std::string &name, double v) {
|
|
const hid_t space = H5Screate(H5S_SCALAR);
|
|
const hid_t ds = H5Dcreate2(loc, name.c_str(), H5T_NATIVE_DOUBLE, space, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
|
H5Dwrite(ds, H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, &v);
|
|
H5Dclose(ds);
|
|
H5Sclose(space);
|
|
}
|
|
|
|
int32_t PixelAt(JFJochXFELReader &reader, int64_t image, size_t col, size_t row) {
|
|
auto raw = reader.GetRawImage(image);
|
|
REQUIRE(raw);
|
|
return reinterpret_cast<const int32_t *>(raw->image_buffer.data())[row * raw->image.GetWidth() + col];
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("JFJochXFELReader_SACLA", "[portable]") {
|
|
// Two sensors of 8 rows x 4 columns stacked in a 16 x 4 frame, 50 um pixels. The first lies square
|
|
// with its first corner on the beam; the second is turned by 90 degrees with its corner 20 pixels
|
|
// along x (posx = -1000 um), so its fast axis runs along y and its slow axis along -x.
|
|
const std::string name = "xfel_reader_sacla.h5";
|
|
{
|
|
const hid_t f = H5Fcreate(name.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
|
|
const hid_t meta = H5Gcreate2(f, "metadata", H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
|
WriteVector(meta, "posx_in_um", {0, -1000});
|
|
WriteVector(meta, "posy_in_um", {0, 0});
|
|
WriteVector(meta, "posz_in_um", {0, 0});
|
|
WriteVector(meta, "angle_in_rad", {0, 90}); // degrees, whatever the name says
|
|
WriteVector(meta, "pixelsizex_in_um", {50, 50});
|
|
WriteVector(meta, "pixelsizey_in_um", {50, 50});
|
|
WriteScalar(meta, "distance_in_mm", 60.0);
|
|
std::vector<uint16_t> pixelmask(16 * 4, 0);
|
|
pixelmask[1] = 1; // first sensor, row 0, column 1
|
|
WriteFrames(meta, "pixelmask", H5T_NATIVE_UINT16, pixelmask.data(), {16, 4});
|
|
H5Gclose(meta);
|
|
for (int shot = 0; shot < 2; shot++) {
|
|
const std::string group = "tag-" + std::to_string(100 + shot);
|
|
const hid_t g = H5Gcreate2(f, group.c_str(), H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT);
|
|
std::vector<int16_t> frame(16 * 4, 10);
|
|
frame[0] = static_cast<int16_t>(20 + shot); // first sensor (fs 0, ss 0)
|
|
frame[8 * 4] = -5; // second sensor (fs 0, ss 0): below zero
|
|
frame[8 * 4 + 1] = 33; // second sensor (fs 1, ss 0)
|
|
frame[9 * 4] = 44; // second sensor (fs 0, ss 1)
|
|
WriteFrames(g, "data", H5T_NATIVE_INT16, frame.data(), {16, 4});
|
|
WriteScalar(g, "photon_energy_ev", 10000.0 + 10.0 * shot);
|
|
H5Gclose(g);
|
|
}
|
|
H5Fclose(f);
|
|
}
|
|
|
|
REQUIRE(JFJochXFELReader::CanReadPanelTable(name));
|
|
CHECK_FALSE(JFJochXFELReader::CanReadCXI(name));
|
|
JFJochXFELReader reader;
|
|
REQUIRE_NOTHROW(reader.ReadPanelTable({name}));
|
|
CHECK(reader.GetNumberOfImages() == 2);
|
|
CHECK_FALSE(reader.CountsInPhotons());
|
|
const auto x = reader.GetDataset()->experiment;
|
|
CHECK(x.GetIncidentEnergy_keV() == Catch::Approx(10.005));
|
|
CHECK(x.GetDetectorDistance_mm() == Catch::Approx(60.0));
|
|
CHECK(x.GetPixelSize_mm() == Catch::Approx(0.05));
|
|
// The first sensor's first pixel centre is at (0.5, 0.5) from the beam, which the flat image puts on
|
|
// a whole column: the beam at 1.5, the pixel at column and row 2.
|
|
CHECK(x.GetBeamX_pxl() == Catch::Approx(1.5));
|
|
CHECK(x.GetBeamY_pxl() == Catch::Approx(1.5));
|
|
CHECK(PixelAt(reader, 0, 2, 2) == 20);
|
|
CHECK(PixelAt(reader, 1, 2, 2) == 21);
|
|
// Second sensor: (fs 0, ss 0) centre at x = 20 - 0.5, y = 0.5; fs runs along +y, ss along -x.
|
|
CHECK(PixelAt(reader, 0, 21, 2) == -5);
|
|
CHECK(PixelAt(reader, 0, 21, 3) == 33);
|
|
CHECK(PixelAt(reader, 0, 20, 2) == 44);
|
|
// The file's mask holds the first sensor's (fs 1, ss 0), at column 3, row 2.
|
|
const auto &mask = reader.GetDataset()->pixel_mask->GetMask();
|
|
CHECK(mask[2 * x.GetXPixelsNum() + 3] != 0);
|
|
CHECK(mask[2 * x.GetXPixelsNum() + 2] == 0);
|
|
|
|
reader.ApplyGain(2.0f);
|
|
CHECK(reader.CountsInPhotons());
|
|
CHECK(PixelAt(reader, 0, 2, 2) == 10);
|
|
reader.Close();
|
|
std::filesystem::remove(name);
|
|
}
|
|
|
|
TEST_CASE("JFJochXFELReader_CXI", "[portable]") {
|
|
// A single-panel Cheetah CXI file: three 6 x 6 frames of signed 32-bit counts, 75 um pixels,
|
|
// 9 keV stated in joules, a mask_shared with one bad pixel.
|
|
const std::string name = "xfel_reader_test.cxi";
|
|
{
|
|
const hid_t f = H5Fcreate(name.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
|
|
for (const char *g: {"entry_1", "entry_1/data_1", "entry_1/instrument_1", "entry_1/instrument_1/detector_1",
|
|
"entry_1/instrument_1/source_1"})
|
|
H5Gclose(H5Gcreate2(f, g, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT));
|
|
std::vector<int32_t> frames(3 * 36, 5);
|
|
frames[0] = -7; // frame 0, pixel (0, 0): dark-subtracted below zero
|
|
frames[36 + 7] = 70000; // frame 1, pixel (1, 1)
|
|
WriteFrames(f, "entry_1/data_1/data", H5T_NATIVE_INT32, frames.data(), {3, 6, 6});
|
|
const std::string det = "entry_1/instrument_1/detector_1/";
|
|
WriteVector(f, det + "x_pixel_size", {75e-6, 75e-6, 75e-6});
|
|
WriteVector(f, det + "distance", {0.1, 0.1, 0.1});
|
|
WriteVector(f, "entry_1/instrument_1/source_1/energy", std::vector<double>(3, 9000.0 * 1.602176634e-19));
|
|
std::vector<uint16_t> mask(36, 0);
|
|
mask[35] = 8; // dead
|
|
WriteFrames(f, det + "mask_shared", H5T_NATIVE_UINT16, mask.data(), {6, 6});
|
|
H5Fclose(f);
|
|
}
|
|
|
|
REQUIRE(JFJochXFELReader::CanReadCXI(name));
|
|
CHECK_FALSE(JFJochXFELReader::CanReadPanelTable(name));
|
|
CHECK_FALSE(JFJochXFELReader::CXIRefusal(name).has_value());
|
|
JFJochXFELReader reader;
|
|
REQUIRE_NOTHROW(reader.ReadCXI({name}));
|
|
CHECK(reader.GetNumberOfImages() == 3);
|
|
const auto x = reader.GetDataset()->experiment;
|
|
CHECK(x.GetIncidentEnergy_keV() == Catch::Approx(9.0));
|
|
CHECK(x.GetPixelSize_mm() == Catch::Approx(0.075));
|
|
CHECK(x.GetDetectorDistance_mm() == Catch::Approx(100.0));
|
|
// Corner at (-3, -3): the pixel (fs, ss) lands at column and row 2 + fs, 2 + ss.
|
|
CHECK(PixelAt(reader, 0, 2, 2) == -7);
|
|
CHECK(PixelAt(reader, 1, 3, 3) == 70000);
|
|
CHECK(PixelAt(reader, 2, 4, 4) == 5);
|
|
const auto &m = reader.GetDataset()->pixel_mask->GetMask();
|
|
CHECK(m[7 * x.GetXPixelsNum() + 7] != 0);
|
|
CHECK(m[6 * x.GetXPixelsNum() + 6] == 0);
|
|
reader.Close();
|
|
std::filesystem::remove(name);
|
|
}
|
|
|
|
TEST_CASE("JFJochXFELReader_CXI_TiledRefused", "[portable]") {
|
|
// A Cheetah CXI slab of a tiled detector is not square, and is not read as one flat panel.
|
|
const std::string name = "xfel_reader_tiled.cxi";
|
|
{
|
|
const hid_t f = H5Fcreate(name.c_str(), H5F_ACC_TRUNC, H5P_DEFAULT, H5P_DEFAULT);
|
|
for (const char *g: {"entry_1", "entry_1/data_1"})
|
|
H5Gclose(H5Gcreate2(f, g, H5P_DEFAULT, H5P_DEFAULT, H5P_DEFAULT));
|
|
std::vector<int16_t> frames(2 * 8 * 4, 1);
|
|
WriteFrames(f, "entry_1/data_1/data", H5T_NATIVE_INT16, frames.data(), {2, 8, 4});
|
|
H5Fclose(f);
|
|
}
|
|
CHECK_FALSE(JFJochXFELReader::CanReadCXI(name));
|
|
const auto refusal = JFJochXFELReader::CXIRefusal(name);
|
|
REQUIRE(refusal.has_value());
|
|
CHECK(refusal->find("tiled detector") != std::string::npos);
|
|
std::filesystem::remove(name);
|
|
}
|