diff --git a/RELEASE.md b/RELEASE.md index 76473988..bfb340c2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -17,6 +17,10 @@ is set, gaps between modules. Gap positions follow detector coordinates before any ROI. Gap pixels receive a fill value or, with ``split_counts``, half of the neighbouring double size pixel. +- ``RawFile`` takes a ``GapPixels`` configuration, in Python the + ``gap_pixels`` argument, and inserts gap pixels while frames are + assembled. ``rows()``, ``cols()`` and every returned frame then include + the gaps, for the full frame and for each ROI. ### Other: diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt index e874a003..f89b57d3 100644 --- a/benchmarks/CMakeLists.txt +++ b/benchmarks/CMakeLists.txt @@ -20,7 +20,7 @@ add_executable(benchmarks) target_sources( benchmarks PRIVATE ndarray_benchmark.cpp ndview_benchmark.cpp calculateeta_benchmark.cpp - reduce_benchmark.cpp clusterfile_benchmark.cpp) + reduce_benchmark.cpp clusterfile_benchmark.cpp rawfile_benchmark.cpp) # Link Google Benchmark and other necessary libraries target_link_libraries(benchmarks PRIVATE benchmark::benchmark aare_core diff --git a/benchmarks/rawfile_benchmark.cpp b/benchmarks/rawfile_benchmark.cpp new file mode 100644 index 00000000..cecd5183 --- /dev/null +++ b/benchmarks/rawfile_benchmark.cpp @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: MPL-2.0 +#include "aare/GapPixels.hpp" +#include "aare/RawFile.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +/// One Jungfrau module, 512x1024 pixels at 16 bit, with every frame in a +/// single data file written to the temporary directory. +class RawFileFixture : public benchmark::Fixture { + protected: + static constexpr size_t rows = 512; + static constexpr size_t cols = 1024; + static constexpr size_t frame_count = 32; + std::filesystem::path m_directory; + + public: + void SetUp(const benchmark::State &) override { + const auto stamp = + std::chrono::steady_clock::now().time_since_epoch().count(); + m_directory = std::filesystem::temp_directory_path() / + ("aare-rawfile-benchmark-" + std::to_string(stamp)); + std::filesystem::create_directory(m_directory); + const nlohmann::json metadata = { + {"Version", 7.2}, + {"Detector Type", "Jungfrau"}, + {"Timing Mode", "auto"}, + {"Geometry", {{"x", 1}, {"y", 1}}}, + {"Image Size in bytes", rows * cols * sizeof(uint16_t)}, + {"Pixels", {{"x", cols}, {"y", rows}}}, + {"Max Frames Per File", frame_count}, + {"Total Frames", frame_count}, + {"Frames in File", frame_count}, + {"Frame Padding", 1}, + {"Frame Discard Policy", "nodiscard"}}; + std::ofstream(master()) << metadata; + + std::vector pixels(rows * cols); + for (size_t i = 0; i < pixels.size(); ++i) { + pixels[i] = static_cast(i % 1000 + 1); + } + std::ofstream data(m_directory / "run_d0_f0_0.raw", std::ios::binary); + for (size_t frame = 0; frame < frame_count; ++frame) { + aare::DetectorHeader header{}; + header.frameNumber = frame + 1; + data.write(reinterpret_cast(&header), sizeof(header)); + data.write( + reinterpret_cast(pixels.data()), + static_cast(pixels.size() * sizeof(uint16_t))); + } + } + + void TearDown(const benchmark::State &) override { + std::error_code error; + std::filesystem::remove_all(m_directory, error); + } + + std::filesystem::path master() const { + return m_directory / "run_master_0.json"; + } + + /// Read every frame of the file into one buffer per iteration + void read_all(benchmark::State &state, + std::optional gaps) const { + aare::RawFile file(master(), "r", gaps); + std::vector buffer(file.bytes_per_frame() * frame_count); + for (auto _ : state) { + file.seek(0); + file.read_into(buffer.data(), frame_count); + benchmark::DoNotOptimize(buffer.data()); + benchmark::ClobberMemory(); + } + state.SetItemsProcessed(static_cast(state.iterations()) * + static_cast(frame_count)); + state.SetBytesProcessed(static_cast(state.iterations()) * + static_cast(buffer.size())); + } +}; + +BENCHMARK_DEFINE_F(RawFileFixture, NoGaps)(benchmark::State &state) { + read_all(state, std::nullopt); +} + +BENCHMARK_DEFINE_F(RawFileFixture, GapsFillValue)(benchmark::State &state) { + read_all(state, aare::GapPixels{}); +} + +BENCHMARK_DEFINE_F(RawFileFixture, GapsSplitCounts)(benchmark::State &state) { + aare::GapPixels gaps; + gaps.split_counts = true; + gaps.seed = 1; + read_all(state, gaps); +} + +BENCHMARK_REGISTER_F(RawFileFixture, NoGaps)->Unit(benchmark::kMillisecond); +BENCHMARK_REGISTER_F(RawFileFixture, GapsFillValue) + ->Unit(benchmark::kMillisecond); +BENCHMARK_REGISTER_F(RawFileFixture, GapsSplitCounts) + ->Unit(benchmark::kMillisecond); + +} // namespace diff --git a/docs/src/GapPixels.rst b/docs/src/GapPixels.rst index 0470878d..f5169e7a 100644 --- a/docs/src/GapPixels.rst +++ b/docs/src/GapPixels.rst @@ -17,6 +17,7 @@ Configuration ``GapPixels`` holds the configuration. The chip size (256 pixels) and the module size (1024x512 pixels) are fixed. An Eiger quad (512x512 pixels) needs no special handling, since its only chip boundaries are at pixel 256. +``RawFile`` rejects other detector types with ``std::invalid_argument``. Gaps of up to 1024 pixels are supported, far beyond the 2 to 10 pixels of a real detector, and detector coordinates are limited to 2^28. @@ -68,6 +69,26 @@ fill value. Splitting only happens where a gap pixel is inserted, so an edge pixel on a ROI border keeps its full value. Split mode requires a chip gap of 2. +Reading files +------------- + +Pass a ``GapPixels`` to :cpp:class:`aare::RawFile`. Frame sizes, +``rows()``, ``cols()`` and ``bytes_per_frame()`` then include the gaps. +Module data is scattered into the gapped frame while it is assembled, so no +extra frame sized buffer is needed. Headers are unchanged. The generic +:cpp:class:`aare::File` does not insert gap pixels; use ``RawFile`` or +apply ``insert_gap_pixels`` to the frames it returns. + +.. code-block:: cpp + + #include "aare/GapPixels.hpp" + #include "aare/RawFile.hpp" + + aare::GapPixels gaps; + gaps.module_gaps = aare::ModuleGaps{8, 36}; + aare::RawFile file("run_master_0.json", "r", gaps); + auto frame = file.read_frame(); // 1064 x 1030 for two stacked modules + Images in memory ---------------- diff --git a/docs/src/RawFile.rst b/docs/src/RawFile.rst index 8ecd30e9..db10ae78 100644 --- a/docs/src/RawFile.rst +++ b/docs/src/RawFile.rst @@ -42,6 +42,14 @@ The top-level frame bounds error states the number of frames in the file and that indices are zero-based. +Pass a :doc:`GapPixels ` configuration as the third constructor +argument to insert gap pixels while frames are assembled. ``rows()``, +``cols()``, ``pixels_per_frame()``, ``bytes_per_frame()`` and every returned +frame then include the gaps, for the full frame and for each ROI. The option +is only supported for Jungfrau and Eiger and throws +``std::invalid_argument`` for other detector types. Headers and frame +numbers are unchanged. + .. doxygenclass:: aare::RawFile :members: :undoc-members: diff --git a/docs/src/python/file/pyGapPixels.rst b/docs/src/python/file/pyGapPixels.rst index 020144c3..58e344a3 100644 --- a/docs/src/python/file/pyGapPixels.rst +++ b/docs/src/python/file/pyGapPixels.rst @@ -46,7 +46,7 @@ defaults and ``(1064, 1030)`` with ``ModuleGaps(8, 36)``. gapped = insert_gap_pixels(image) # module gaps as in the slsDetectorPackage GUI, counts split at chip edges - gaps = GapPixels(module_gaps=ModuleGaps(8, 36), split_counts=True, seed=1) + gaps = GapPixels(module_gaps=ModuleGaps(8, 36), split_counts=True) gapped = insert_gap_pixels(image, gaps) # an image that covers columns 100 to 400 of the first ten rows @@ -59,6 +59,28 @@ defaults and ``(1064, 1030)`` with ``ModuleGaps(8, 36)``. same dtype. Pass ``roi`` when the image covers part of the detector. ``gapped_shape`` returns the shape of the gapped image of a ROI. +Reading files +------------- + +``RawFile`` takes ``gap_pixels``: ``True`` for the defaults or a +``GapPixels``. Frame shapes, ``rows`` and ``cols`` then include the gaps. +Only Jungfrau and Eiger raw files are supported; other detector types raise +``ValueError``. The generic ``File`` does not insert gap pixels; use +``RawFile`` or apply ``insert_gap_pixels`` to the frames it returns. + +.. code-block:: python + + from aare import GapPixels, ModuleGaps, RawFile + + # defaults: chip gaps of 2 filled with 0 + with RawFile("run_master_0.json", gap_pixels=True) as f: + header, image = f.read_frame() + + # module gaps as in the slsDetectorPackage GUI + gaps = GapPixels(module_gaps=ModuleGaps(8, 36), split_counts=False) + with RawFile("run_master_0.json", gap_pixels=gaps) as f: + header, image = f.read_frame() + .. autoclass:: GapPixels :members: :undoc-members: diff --git a/docs/src/python/file/pyRawFile.rst b/docs/src/python/file/pyRawFile.rst index a128d2fe..480560e2 100644 --- a/docs/src/python/file/pyRawFile.rst +++ b/docs/src/python/file/pyRawFile.rst @@ -40,6 +40,12 @@ For a file containing five frames, the valid indices are 0 through 4. Attempting to read index 5 raises an error stating: ``Frame index 5 is out of range: file contains 5 frames (indices are zero-based)``. +``gap_pixels`` inserts :doc:`gap pixels ` while frames are +assembled. Pass ``True`` for the defaults or a ``GapPixels``. ``rows()``, +``cols()`` and every returned frame then include the gaps, for the full frame +and for each ROI. Only Jungfrau and Eiger are supported; other detector +types raise ``ValueError``. Headers are unchanged. + .. autoclass:: RawFile :members: :undoc-members: diff --git a/include/aare/RawFile.hpp b/include/aare/RawFile.hpp index e119fe55..e359ab16 100644 --- a/include/aare/RawFile.hpp +++ b/include/aare/RawFile.hpp @@ -2,6 +2,7 @@ #pragma once #include "aare/FileInterface.hpp" #include "aare/Frame.hpp" +#include "aare/GapPixels.hpp" #include "aare/NDArray.hpp" //for pixel map #include "aare/ROIGeometry.hpp" #include "aare/RawMasterFile.hpp" @@ -11,7 +12,9 @@ #include "../tests/friend_test.hpp" #endif +#include #include +#include namespace aare { @@ -38,15 +41,32 @@ class RawFile : public FileInterface { /// @brief Minimum frame count across the selected raw subfile series. size_t m_frames_in_file{}; + /// @brief Gap pixel configuration, empty when reading without gaps + std::optional m_gap_pixels; + detail::GapLayout m_gap_layout{}; + std::mt19937_64 m_generator; + + /// @brief Detector coordinates of each ROI, in roi_geometries() order + std::vector m_roi_rects; + /// @brief {rows, cols} of the assembled frame of each ROI, including + /// gap pixels when enabled + std::vector> m_roi_shapes; + public: /** * @brief RawFile constructor * @param fname path to the master file (.json) * @param mode file mode (only "r" is supported at the moment) + * @param gap_pixels insert gap pixels into every assembled frame. Frame + * sizes, rows() and cols() then include the gaps. Only supported for + * Jungfrau and Eiger. * @throws std::runtime_error if frame padding is disabled and the frame * discard policy is not DiscardPartial. + * @throws std::invalid_argument if gap pixels are requested for another + * detector type or the gap configuration is invalid */ - RawFile(const std::filesystem::path &fname, const std::string &mode = "r"); + RawFile(const std::filesystem::path &fname, const std::string &mode = "r", + std::optional gap_pixels = std::nullopt); virtual ~RawFile() override = default; Frame read_frame() override; @@ -106,6 +126,10 @@ class RawFile : public FileInterface { */ size_t pixels_per_frame(const size_t roi_index); size_t bytes_per_pixel() const; + + /// @brief Gap pixel configuration, empty when reading without gaps + const std::optional &gap_pixels() const { return m_gap_pixels; } + void seek(size_t frame_index) override; size_t tell() override; /// @brief Minimum actual frame count across all subfiles and ROIs. @@ -168,6 +192,16 @@ class RawFile : public FileInterface { std::vector synchronized_frame_indices(size_t frame_index, size_t roi_index); + /** + * @brief Assemble a frame with gap pixels: fill the gap lines, scatter + * every module part through the scratch buffer and, if configured, + * split the counts of the chip edge pixels. + */ + template + void assemble_with_gaps(std::byte *frame_buffer, size_t roi_index, + const std::vector &frame_indices, + DetectorHeader *header); + /** * @brief read the frame at the given frame index into the image buffer * @param frame_index frame number to read diff --git a/python/aare/RawFile.py b/python/aare/RawFile.py index c769c063..ad876913 100644 --- a/python/aare/RawFile.py +++ b/python/aare/RawFile.py @@ -4,16 +4,18 @@ import numpy as np from .ScanParameters import ScanParameters class RawFile(_aare.RawFile): - """ - Class to read Raw files produced by slsDetectorPackage + def __init__(self, fname, chunk_size = 1, gap_pixels = False): + """Open a raw file for reading. - Parameters: - fname (str): Path to the master file. - chunk_size (int, optional): Number of frames to read at a time. Defaults to 1. - strixeltransform (list, optional): List of strixel transform objects. Defaults to None. - """ - def __init__(self, fname, chunk_size = 1, strixeltransform : list = None): - super().__init__(fname) + Args: + fname: Master file (.json or .raw). + chunk_size: Frames returned per iteration step. + gap_pixels: Insert gap pixels into every frame. True uses the + default GapPixels configuration, or pass a GapPixels. Frame + shapes then include the gaps. Only Jungfrau and Eiger are + supported. + """ + super().__init__(fname, gap_pixels) self._chunk_size = chunk_size self._strixeltransform : list = strixeltransform if self._strixeltransform is not None: diff --git a/python/src/bind_RawFile.hpp b/python/src/bind_RawFile.hpp index 27e6511c..f3b2d672 100644 --- a/python/src/bind_RawFile.hpp +++ b/python/src/bind_RawFile.hpp @@ -3,6 +3,7 @@ #include "aare/CtbRawFile.hpp" #include "aare/File.hpp" #include "aare/Frame.hpp" +#include "aare/GapPixels.hpp" #include "aare/RawFile.hpp" #include "aare/RawMasterFile.hpp" #include "aare/RawSubFile.hpp" @@ -13,6 +14,7 @@ #include #include #include +#include #include #include #include @@ -34,7 +36,33 @@ std::runtime_error raw_file_read_error(RawFile &file, void define_raw_file_io_bindings(py::module &m) { py::class_(m, "RawFile") - .def(py::init()) + .def(py::init([](const std::filesystem::path &fname, + const GapPixels &gap_pixels) { + return std::make_unique(fname, "r", gap_pixels); + }), + py::arg("fname"), py::arg("gap_pixels")) + .def(py::init([](const std::filesystem::path &fname, bool gap_pixels) { + return std::make_unique( + fname, "r", + gap_pixels ? std::optional(GapPixels{}) + : std::nullopt); + }), + py::arg("fname"), py::arg("gap_pixels") = false, R"( + Open a raw file for reading. + + Parameters + ---------- + fname : path + Master file (.json or .raw). + gap_pixels : bool or GapPixels + Insert gap pixels into every frame. True uses the default + GapPixels configuration. Frame shapes, rows() and cols() + then include the gaps. Only Jungfrau and Eiger are + supported. + )") + .def_property_readonly("gap_pixels", &RawFile::gap_pixels, R"( + Gap pixel configuration, or None when reading without gaps. + )") .def("read_frame", [](RawFile &self) { if (self.n_modules_in_roi().size() > 1) { @@ -118,8 +146,8 @@ void define_raw_file_io_bindings(py::module &m) { std::vector shape; shape.reserve(2); - shape.push_back(self.roi_geometries(roi_index).pixels_y()); - shape.push_back(self.roi_geometries(roi_index).pixels_x()); + shape.push_back(self.rows(roi_index)); + shape.push_back(self.cols(roi_index)); py::array image = allocate_image_data(self.bytes_per_pixel(), shape); @@ -177,8 +205,8 @@ void define_raw_file_io_bindings(py::module &m) { for (size_t r = 0; r < number_of_ROIs; r++) { std::vector shape; shape.reserve(2); - shape.push_back(self.roi_geometries(r).pixels_y()); - shape.push_back(self.roi_geometries(r).pixels_x()); + shape.push_back(self.rows(r)); + shape.push_back(self.cols(r)); images[r] = allocate_image_data(self.bytes_per_pixel(), shape); @@ -229,9 +257,8 @@ void define_raw_file_io_bindings(py::module &m) { if (n_frames == 0) { throw raw_file_read_error(self, "No frames left in file"); } - std::vector shape{ - n_frames, self.roi_geometries(roi_index).pixels_y(), - self.roi_geometries(roi_index).pixels_x()}; + std::vector shape{n_frames, self.rows(roi_index), + self.cols(roi_index)}; // return headers from all subfiles auto n_mod = self.n_modules_in_roi()[roi_index]; diff --git a/python/tests/test_RawFile.py b/python/tests/test_RawFile.py index b62044fd..1cadef36 100644 --- a/python/tests/test_RawFile.py +++ b/python/tests/test_RawFile.py @@ -2,6 +2,7 @@ import pytest import json from aare import File, RawFile, RawSubFile, DetectorType, ROI, UDPPortPosition, xy +from aare import GapPixels, ModuleGaps, insert_gap_pixels import numpy as np from aare import strixelremap @@ -31,6 +32,46 @@ def small_raw_file(tmp_path): return master_path +MODULE_ROWS = 2 +MODULE_COLS = 3 + + +def write_module_layout(directory, layout_rows, layout_cols, frames=2, + module_rows=MODULE_ROWS, module_cols=MODULE_COLS, + detector_type="Jungfrau"): + """Write a raw file series with modules in the given layout. + + Modules are numbered as DetectorGeometry orders them, down each column + first, and every module holds distinct values so misplacement is visible. + Returns the master path and the expected image of frame 0. + """ + master_path = directory / "run_master_0.json" + master_path.write_text(json.dumps({ + "Version": 7.2, + "Detector Type": detector_type, + "Timing Mode": "auto", + "Geometry": {"x": layout_cols, "y": layout_rows}, + "Image Size in bytes": module_rows * module_cols * 2, + "Pixels": {"x": module_cols, "y": module_rows}, + "Max Frames Per File": 1, + "Total Frames": frames, + "Frames in File": frames, + "Frame Padding": 1, + "Frame Discard Policy": "nodiscard", + })) + expected = np.zeros((module_rows * layout_rows, module_cols * layout_cols), dtype=np.uint16) + for module in range(layout_rows * layout_cols): + module_row, module_col = module % layout_rows, module // layout_rows + pixels = ((np.arange(module_rows * module_cols, dtype=np.uint32) + module * 10 + 1) % 65536).astype(np.uint16) + expected[module_row * module_rows:(module_row + 1) * module_rows, + module_col * module_cols:(module_col + 1) * module_cols] = pixels.reshape(module_rows, module_cols) + for index in range(frames): + data_path = directory / f"run_d{module}_f{index}_0.raw" + # 112 byte detector header followed by the module pixels + data_path.write_bytes(bytes(112) + pixels.tobytes()) + return master_path, expected + + @pytest.mark.parametrize("reader_type", [RawFile, File]) @pytest.mark.parametrize("legacy_master", [False, True]) @pytest.mark.parametrize("padding, policy, supported", [ @@ -469,4 +510,120 @@ def test_RawFile_with_strixeltransform(): assert len(frames[0]) == 3 assert frames[0][0].shape == (165, 79) assert frames[0][1].shape == (320, 47) - assert frames[0][2].shape == (476, 59) \ No newline at end of file + assert frames[0][2].shape == (476, 59) + +@pytest.mark.parametrize("layout", [(1, 1), (2, 1), (1, 2), (2, 2), (3, 2)]) +@pytest.mark.parametrize("reader_type", [RawFile, File]) +def test_raw_file_assembles_module_layout(tmp_path, layout, reader_type): + layout_rows, layout_cols = layout + master_path, expected = write_module_layout(tmp_path, layout_rows, layout_cols) + + with reader_type(master_path) as reader: + # RawFile binds rows/cols as methods (ROI overloads), File as properties + rows = reader.rows() if reader_type is RawFile else reader.rows + cols = reader.cols() if reader_type is RawFile else reader.cols + assert (rows, cols) == expected.shape + result = reader.read_frame() + image = result[1] if reader_type is RawFile else result + assert image.shape == expected.shape + assert (image == expected).all() + + reader.seek(0) + result = reader.read_n(2) + images = result[1] if reader_type is RawFile else result + assert images.shape == (2,) + expected.shape + assert (images == expected).all() + + +@pytest.mark.parametrize("layout", [(1, 1), (2, 1), (1, 2)]) +def test_raw_file_gap_pixels(tmp_path, layout): + layout_rows, layout_cols = layout + master_path, expected = write_module_layout(tmp_path, layout_rows, layout_cols, + module_rows=512, module_cols=1024) + gaps = GapPixels(fill_value=5) + with RawFile(master_path, gap_pixels=gaps) as reader: + assert reader.gap_pixels == gaps + # one chip boundary per module row, three per module column and no + # gap between modules + assert (reader.rows(), reader.cols()) == ((512 + 2) * layout_rows, (1024 + 6) * layout_cols) + header, image = reader.read_frame() + assert header.size == layout_rows * layout_cols + assert (image == insert_gap_pixels(expected, gaps)).all() + reader.seek(0) + _, images = reader.read_n(2) + assert images.shape == (2, reader.rows(), reader.cols()) + assert (images[1] == image).all() + + with RawFile(master_path) as reader: + assert reader.gap_pixels is None + assert (reader.rows(), reader.cols()) == expected.shape + + with RawFile(master_path, gap_pixels=True) as reader: + assert reader.gap_pixels == GapPixels() + assert (reader.rows(), reader.cols()) == insert_gap_pixels(expected).shape + _, image = reader.read_frame() + assert (image == insert_gap_pixels(expected)).all() + + module_gaps = GapPixels(module_gaps=ModuleGaps(8, 36)) + with RawFile(master_path, gap_pixels=module_gaps) as reader: + assert (reader.rows(), reader.cols()) == insert_gap_pixels(expected, module_gaps).shape + _, image = reader.read_frame() + assert (image == insert_gap_pixels(expected, module_gaps)).all() + + +def test_raw_file_gap_pixels_split_counts(tmp_path): + # one Eiger module: four 256x512 ports in a 2x2 layout. Splitting is + # rejected on raw Jungfrau data. The plain read is the reference since + # Eiger ports are row flipped on reading. + master_path, _ = write_module_layout(tmp_path, 2, 2, module_rows=256, module_cols=512, + detector_type="Eiger") + gaps = GapPixels(split_counts=True, seed=11) + with RawFile(master_path) as reader: + _, plain = reader.read_frame() + with RawFile(master_path, gap_pixels=gaps) as reader: + _, image = reader.read_frame() + assert (image == insert_gap_pixels(plain, gaps)).all() + assert image.sum() == plain.sum() + + +def test_raw_file_gap_pixels_split_counts_rejected_for_jungfrau(tmp_path): + master_path, _ = write_module_layout(tmp_path, 1, 1, module_rows=512, module_cols=1024) + RawFile(master_path, gap_pixels=GapPixels()) + with pytest.raises(ValueError): + RawFile(master_path, gap_pixels=GapPixels(split_counts=True)) + + +def test_raw_file_gap_pixels_unsupported_detector(tmp_path): + master_path, _ = write_module_layout(tmp_path, 1, 1, detector_type="Moench") + RawFile(master_path) + with pytest.raises(ValueError): + RawFile(master_path, gap_pixels=True) + + +@pytest.mark.withdata +def test_raw_file_gap_pixels_recorded_jungfrau(test_data_path): + path = test_data_path / "raw/jungfrau/jungfrau_single_master_0.json" + gaps = GapPixels(fill_value=3) + with RawFile(path, gap_pixels=gaps) as gapped, RawFile(path) as plain: + assert (gapped.rows(), gapped.cols()) == (514, 1030) + _, image = gapped.read_frame() + _, reference = plain.read_frame() + assert image.shape == (514, 1030) + assert (image == insert_gap_pixels(reference, gaps)).all() + + +@pytest.mark.withdata +def test_raw_file_gap_pixels_recorded_rois(test_data_path): + path = test_data_path / "raw/ROITestData/MultipleROIs/run_master_0.json" + with RawFile(path, gap_pixels=True) as gapped, RawFile(path) as plain: + assert gapped.num_rois == 2 + _, images = gapped.read_rois() + _, references = plain.read_rois() + rois = plain.master.rois + assert images[0].shape == (303, 103) + assert images[1].shape == (101, 103) + for image, reference, roi in zip(images, references, rois): + assert (image == insert_gap_pixels(reference, roi=roi)).all() + gapped.seek(0) + _, frames = gapped.read_n_with_roi(2, roi_index=1) + assert frames.shape == (2, 101, 103) diff --git a/src/RawFile.cpp b/src/RawFile.cpp index 54adada6..34c336a2 100644 --- a/src/RawFile.cpp +++ b/src/RawFile.cpp @@ -24,7 +24,8 @@ std::runtime_error RawFile::frame_error(size_t frame_index, frame_index, m_master.master_fname().string(), message)); } -RawFile::RawFile(const std::filesystem::path &fname, const std::string &mode) +RawFile::RawFile(const std::filesystem::path &fname, const std::string &mode, + std::optional gap_pixels) : m_master(fname) { m_mode = mode; @@ -61,6 +62,60 @@ RawFile::RawFile(const std::filesystem::path &fname, const std::string &mode) } } m_part_buffer.resize(max_part_bytes); + + m_roi_rects = m_master.rois(); + if (m_roi_rects.size() != num_rois) { + throw std::runtime_error( + LOCATION + "ROI count differs from ROI geometry count"); + } + m_roi_shapes.reserve(num_rois); + if (gap_pixels) { + const auto detector = m_master.detector_type(); + if (detector != DetectorType::Jungfrau && + detector != DetectorType::Eiger) { + throw std::invalid_argument( + LOCATION + "Gap pixels are only supported for Jungfrau " + "and Eiger detectors"); + } + if (gap_pixels->split_counts && + detector == DetectorType::Jungfrau) { + throw std::invalid_argument( + LOCATION + "split_counts cannot be used on raw Jungfrau " + "data: it would split the gain bits. Convert " + "to energy and use insert_gap_pixels"); + } + // bit depth and fill value are known at open, reject them here + // rather than on the first read + switch (m_master.bitdepth()) { + case 8: + detail::fill_as(gap_pixels->fill_value); + break; + case 16: + detail::fill_as(gap_pixels->fill_value); + break; + case 32: + detail::fill_as(gap_pixels->fill_value); + break; + default: + throw std::invalid_argument( + LOCATION + + fmt::format("Gap pixels are not supported for a bit " + "depth of {}", + m_master.bitdepth())); + } + m_gap_pixels = gap_pixels; + m_gap_layout = detail::gap_layout(*gap_pixels); + m_generator = detail::make_generator(gap_pixels->seed); + for (const auto &rect : m_roi_rects) { + m_roi_shapes.push_back( + detail::gapped_shape(rect, m_gap_layout)); + } + } else { + for (const auto &roi : m_master.roi_geometries()) { + m_roi_shapes.push_back({static_cast(roi.pixels_y()), + static_cast(roi.pixels_x())}); + } + } if (!min_frames) { throw std::runtime_error(fmt::format( "No raw subfiles selected by '{}'", fname.string())); @@ -202,9 +257,7 @@ size_t RawFile::bytes_per_frame() { } size_t RawFile::bytes_per_frame(const size_t roi_index) { - return m_master.roi_geometries().at(roi_index).pixels_x() * - m_master.roi_geometries().at(roi_index).pixels_y() * - m_master.bitdepth() / bits_per_byte; + return pixels_per_frame(roi_index) * m_master.bitdepth() / bits_per_byte; } size_t RawFile::pixels_per_frame() { @@ -217,8 +270,7 @@ size_t RawFile::pixels_per_frame() { } size_t RawFile::pixels_per_frame(const size_t roi_index) { - return m_master.roi_geometries().at(roi_index).pixels_x() * - m_master.roi_geometries().at(roi_index).pixels_y(); + return rows(roi_index) * cols(roi_index); } DetectorType RawFile::detector_type() const { return m_master.detector_type(); } @@ -249,7 +301,7 @@ size_t RawFile::rows() const { return rows(0); } size_t RawFile::rows(const size_t roi_index) const { - return m_master.roi_geometries().at(roi_index).pixels_y(); + return static_cast(m_roi_shapes.at(roi_index)[0]); } size_t RawFile::cols() const { if (m_master.roi_geometries().size() > 1) { @@ -260,7 +312,7 @@ size_t RawFile::cols() const { return cols(0); } size_t RawFile::cols(const size_t roi_index) const { - return m_master.roi_geometries().at(roi_index).pixels_x(); + return static_cast(m_roi_shapes.at(roi_index)[1]); } size_t RawFile::bitdepth() const { return m_master.bitdepth(); } @@ -333,8 +385,8 @@ DetectorHeader RawFile::read_header(const std::filesystem::path &fname) { RawMasterFile RawFile::master() const { return m_master; } Frame RawFile::get_frame(size_t frame_index, const size_t roi_index) { - auto f = Frame(m_master.roi_geometries().at(roi_index).pixels_y(), - m_master.roi_geometries().at(roi_index).pixels_x(), + auto f = Frame(static_cast(rows(roi_index)), + static_cast(cols(roi_index)), Dtype::from_bitdepth(m_master.bitdepth())); std::byte *frame_buffer = f.data(); get_frame_into(frame_index, frame_buffer, roi_index); @@ -400,6 +452,30 @@ void RawFile::get_frame_into(size_t frame_index, std::byte *frame_buffer, const auto frame_indices = synchronized_frame_indices(frame_index, roi_index); + if (m_gap_pixels) { + switch (m_master.bitdepth()) { + case 8: + assemble_with_gaps(frame_buffer, roi_index, frame_indices, + header); + break; + case 16: + assemble_with_gaps(frame_buffer, roi_index, frame_indices, + header); + break; + case 32: + assemble_with_gaps(frame_buffer, roi_index, frame_indices, + header); + break; + default: + throw frame_error(frame_index, + LOCATION + + fmt::format("Gap pixels are not supported " + "for a bit depth of {}", + m_master.bitdepth())); + } + return; + } + const size_t bytes_per_pixel = m_master.bitdepth() / bits_per_byte; const size_t roi_width = roi.pixels_x(); @@ -437,6 +513,43 @@ void RawFile::get_frame_into(size_t frame_index, std::byte *frame_buffer, } } +template +void RawFile::assemble_with_gaps(std::byte *frame_buffer, size_t roi_index, + const std::vector &frame_indices, + DetectorHeader *header) { + const auto &roi = m_master.roi_geometries().at(roi_index); + const ROI &rect = m_roi_rects.at(roi_index); + NDView destination(reinterpret_cast(frame_buffer), + m_roi_shapes.at(roi_index)); + detail::fill_gap_pixels(destination, rect, m_gap_layout, + detail::fill_as(m_gap_pixels->fill_value)); + + for (size_t part_idx = 0; part_idx != roi.num_modules_in_roi(); + ++part_idx) { + const auto &pos = m_master.geometry().get_module_geometries( + roi.module_indices_in_roi(part_idx)); + auto &subfile = *m_subfiles[roi_index][part_idx]; + subfile.seek(frame_indices[part_idx]); + subfile.read_into(m_part_buffer.data(), header); + if (header) { + ++header; + } + // detector coordinates of the part + const ROI part_rect{ + rect.xmin + pos.origin_x, rect.xmin + pos.origin_x + pos.width, + rect.ymin + pos.origin_y, rect.ymin + pos.origin_y + pos.height}; + NDView source( + reinterpret_cast(m_part_buffer.data()), + {pos.height, pos.width}); + detail::copy_with_gaps(source, part_rect, destination, rect, + m_gap_layout); + } + + if (m_gap_pixels->split_counts) { + detail::split_edge_counts(destination, rect, m_gap_layout, m_generator); + } +} + std::vector RawFile::read_n(size_t n_frames) { // TODO: implement this in a more efficient way if (num_rois() > 1) { diff --git a/src/RawFile.test.cpp b/src/RawFile.test.cpp index ee0b9e24..a4d67ec9 100644 --- a/src/RawFile.test.cpp +++ b/src/RawFile.test.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MPL-2.0 #include "aare/RawFile.hpp" #include "aare/File.hpp" +#include "aare/GapPixels.hpp" #include "aare/RawMasterFile.hpp" //needed for ROI #include @@ -222,6 +223,354 @@ TEST_CASE("RawFile frame count comes from the complete subfile series", REQUIRE_NOTHROW(reader.read_frame(1)); } +TEST_CASE("RawFile assembles every module layout", "[RawFile][geometry]") { + const auto [layout_rows, layout_cols] = GENERATE(table({ + {1, 1}, + {2, 1}, + {1, 2}, + {2, 2}, + {3, 2}, + })); + CAPTURE(layout_rows, layout_cols); + TemporaryRawFiles files(xy{layout_rows, layout_cols}); + const size_t rows = TemporaryRawFiles::module_rows * layout_rows; + const size_t cols = TemporaryRawFiles::module_cols * layout_cols; + + auto check = [&](NDView image) { + REQUIRE(image.shape(0) == static_cast(rows)); + REQUIRE(image.shape(1) == static_cast(cols)); + for (size_t row = 0; row < rows; ++row) { + for (size_t col = 0; col < cols; ++col) { + CAPTURE(row, col); + REQUIRE(image(row, col) == files.expected(row, col)); + } + } + }; + + RawFile file(files.master_path()); + REQUIRE(file.rows() == rows); + REQUIRE(file.cols() == cols); + REQUIRE(file.n_modules() == files.n_modules()); + REQUIRE(file.bytes_per_frame() == rows * cols * sizeof(uint16_t)); + + SECTION("read_frame") { + auto frame = file.read_frame(); + check(frame.view()); + } + SECTION("read_into with headers") { + NDArray image( + {static_cast(rows), static_cast(cols)}); + std::vector headers(files.n_modules()); + file.seek(1); + file.read_into(reinterpret_cast(image.data()), + headers.data()); + check(image.view()); + for (size_t module = 0; module < files.n_modules(); ++module) { + CAPTURE(module); + REQUIRE(headers[module].frameNumber == 101); + REQUIRE(headers[module].row == module % layout_rows); + REQUIRE(headers[module].column == module / layout_rows); + } + } + SECTION("read_n") { + auto frames = file.read_n(2); + REQUIRE(frames.size() == 2); + for (auto &frame : frames) { + check(frame.view()); + } + } + SECTION("generic File") { + File reader(files.master_path()); + auto frame = reader.read_frame(1); + check(frame.view()); + } +} + +namespace { + +/// Compare a frame read with gap pixels against the free function applied +/// to the same frame read without gaps. +template +bool matches_gap_oracle(Frame &gapped, Frame &plain, const ROI &roi, + const GapPixels &gaps) { + auto expected = + insert_gap_pixels(NDView(plain.view()), roi, gaps); + auto actual = gapped.view(); + if (expected.shape() != actual.shape()) { + UNSCOPED_INFO("shape " << actual.shape(0) << "x" << actual.shape(1) + << " expected " << expected.shape(0) << "x" + << expected.shape(1)); + return false; + } + for (ssize_t row = 0; row < expected.shape(0); ++row) { + for (ssize_t col = 0; col < expected.shape(1); ++col) { + if (expected(row, col) != actual(row, col)) { + UNSCOPED_INFO("pixel (" << row << ", " << col << ") " + << actual(row, col) << " expected " + << expected(row, col)); + return false; + } + } + } + return true; +} + +bool matches_gap_oracle(Frame &gapped, Frame &plain, const ROI &roi, + const GapPixels &gaps) { + switch (plain.bitdepth()) { + case 8: + return matches_gap_oracle(gapped, plain, roi, gaps); + case 16: + return matches_gap_oracle(gapped, plain, roi, gaps); + case 32: + return matches_gap_oracle(gapped, plain, roi, gaps); + default: + return false; + } +} + +} // namespace + +TEST_CASE("RawFile inserts gap pixels into synthetic Jungfrau modules", + "[RawFile][GapPixels]") { + const auto [layout_rows, layout_cols] = GENERATE(table({ + {1, 1}, + {2, 1}, + {1, 2}, + })); + CAPTURE(layout_rows, layout_cols); + TemporaryRawFiles files(xy{layout_rows, layout_cols}, xy{512, 1024}); + const ROI detector{0, static_cast(files.cols()), 0, + static_cast(files.rows())}; + + GapPixels gaps; + SECTION("defaults") {} + SECTION("module gaps and fill value") { + gaps.module_gaps = ModuleGaps{8, 36}; + gaps.fill_value = 7; + } + const auto layout = detail::gap_layout(gaps); + const auto shape = detail::gapped_shape(detector, layout); + const auto fill = static_cast(gaps.fill_value); + + RawFile file(files.master_path(), "r", gaps); + REQUIRE(file.gap_pixels() == gaps); + REQUIRE(file.rows() == static_cast(shape[0])); + REQUIRE(file.cols() == static_cast(shape[1])); + REQUIRE(file.pixels_per_frame() == + static_cast(shape[0] * shape[1])); + REQUIRE(file.bytes_per_frame() == file.pixels_per_frame() * 2); + + std::vector headers(files.n_modules()); + NDArray image(shape); + file.read_into(reinterpret_cast(image.data()), headers.data()); + for (const auto &header : headers) { + REQUIRE(header.frameNumber == 100); + } + + for (size_t row = 0; row < files.rows(); ++row) { + for (size_t col = 0; col < files.cols(); ++col) { + const auto value = + image(layout.y.gapped(static_cast(row)), + layout.x.gapped(static_cast(col))); + if (value != files.expected(row, col)) { + CAPTURE(row, col); + REQUIRE(value == files.expected(row, col)); + } + } + } + for (const auto boundary : + layout.x.inner_boundaries(detector.xmin, detector.xmax)) { + const auto [first, last] = layout.x.gap_range(boundary, 0); + for (ssize_t col = first; col <= last; ++col) { + for (ssize_t row = 0; row < shape[0]; ++row) { + if (image(row, col) != fill) { + CAPTURE(row, col); + REQUIRE(image(row, col) == fill); + } + } + } + } + for (const auto boundary : + layout.y.inner_boundaries(detector.ymin, detector.ymax)) { + const auto [first, last] = layout.y.gap_range(boundary, 0); + for (ssize_t row = first; row <= last; ++row) { + for (ssize_t col = 0; col < shape[1]; ++col) { + if (image(row, col) != fill) { + CAPTURE(row, col); + REQUIRE(image(row, col) == fill); + } + } + } + } + + auto frame = file.read_frame(); + REQUIRE(frame.rows() == static_cast(shape[0])); + REQUIRE(frame.cols() == static_cast(shape[1])); + REQUIRE(frame.view() == image.view()); +} + +TEST_CASE("RawFile splits counts like the free function", + "[RawFile][GapPixels]") { + // one Eiger module: four 256x512 ports in a 2x2 layout. Splitting is + // rejected on raw Jungfrau data, so the synthetic file is Eiger. + TemporaryRawFiles files(xy{2, 2}, xy{256, 512}); + auto metadata = nlohmann::json::parse(std::ifstream(files.master_path())); + metadata["Detector Type"] = "Eiger"; + std::ofstream(files.master_path()) << metadata; + GapPixels gaps; + gaps.split_counts = true; + gaps.seed = 7; + RawFile gapped(files.master_path(), "r", gaps); + RawFile plain(files.master_path()); + auto gapped_frame = gapped.read_frame(); + auto plain_frame = plain.read_frame(); + const ROI detector{0, 1024, 0, 512}; + REQUIRE(matches_gap_oracle(gapped_frame, plain_frame, detector, gaps)); + + // the next frame continues the generator, so the oracle for frame 1 is + // the free function applied after the same number of draws + auto frames = gapped.read_n(1); + auto second_plain = plain.read_frame(); + auto expected = insert_gap_pixels( + NDView(second_plain.view()), detector, + gaps); + double gapped_total = 0; + double expected_total = 0; + auto view = frames[0].view(); + for (ssize_t row = 0; row < expected.shape(0); ++row) { + for (ssize_t col = 0; col < expected.shape(1); ++col) { + gapped_total += view(row, col); + expected_total += expected(row, col); + } + } + REQUIRE(gapped_total == expected_total); +} + +TEST_CASE("RawFile rejects split counts on raw Jungfrau data", + "[RawFile][GapPixels]") { + TemporaryRawFiles files(xy{1, 1}, xy{512, 1024}); + GapPixels gaps; + gaps.split_counts = true; + REQUIRE_NOTHROW(RawFile(files.master_path(), "r", GapPixels{})); + REQUIRE_THROWS_AS(RawFile(files.master_path(), "r", gaps), + std::invalid_argument); +} + +TEST_CASE("RawFile rejects gap pixels for unsupported detectors", + "[RawFile][GapPixels]") { + TemporaryRawFiles files; + auto metadata = nlohmann::json::parse(std::ifstream(files.master_path())); + metadata["Detector Type"] = "Moench"; + std::ofstream(files.master_path()) << metadata; + REQUIRE_NOTHROW(RawFile(files.master_path())); + REQUIRE_THROWS_AS(RawFile(files.master_path(), "r", GapPixels{}), + std::invalid_argument); +} + +TEST_CASE("Gap pixels on recorded Jungfrau and Eiger files", + "[.with-data][RawFile][GapPixels]") { + struct Case { + const char *path; + std::array shape; + std::optional module_gaps; + }; + const auto test = GENERATE(values({ + {"raw/jungfrau/jungfrau_single_master_0.json", {514, 1030}, {}}, + {"raw/jungfrau/jungfrau_double_master_0.json", {514, 1030}, {}}, + {"raw/eiger/Lab6_20500eV_2deg_20240629_master_7.json", {514, 1030}, {}}, + {"raw/eiger/eiger_500k_16bit_master_0.json", {514, 1030}, {}}, + {"raw/eiger_quad_data/" + "W13_vrpreampscan_m21C_300V_800eV_vthre2000_master_0.json", + {514, 514}, + {}}, + {"raw/virtual/920/jungfrau/two_modules_master_0.json", + {1028, 1030}, + {}}, + {"raw/virtual/920/jungfrau/two_modules_master_0.json", + {1064, 1030}, + ModuleGaps{8, 36}}, + {"raw/ROITestData/MultipleChipROI/run_master_2.json", {301, 103}, {}}, + {"raw/ROITestData/MultipleChipROI/run_master_2.json", + {337, 103}, + ModuleGaps{8, 36}}, + {"raw/virtual/920/jungfrau/" + "single_module_roi_512_767_0_255_master_0.json", + {256, 256}, + {}}, + })); + CAPTURE(test.path); + const auto fpath = test_data_path() / test.path; + REQUIRE(std::filesystem::exists(fpath)); + + GapPixels gaps; + gaps.module_gaps = test.module_gaps; + gaps.fill_value = 3; + RawFile gapped(fpath, "r", gaps); + RawFile plain(fpath); + REQUIRE(gapped.rows() == static_cast(test.shape[0])); + REQUIRE(gapped.cols() == static_cast(test.shape[1])); + REQUIRE(gapped.bytes_per_frame() == + gapped.pixels_per_frame() * gapped.bytes_per_pixel()); + + auto gapped_frame = gapped.read_frame(); + auto plain_frame = plain.read_frame(); + REQUIRE(gapped_frame.rows() == static_cast(test.shape[0])); + REQUIRE(gapped_frame.cols() == static_cast(test.shape[1])); + REQUIRE(matches_gap_oracle(gapped_frame, plain_frame, plain.master().roi(), + gaps)); +} + +TEST_CASE("Split counts on a recorded Eiger file", + "[.with-data][RawFile][GapPixels]") { + const auto fpath = + test_data_path() / "raw/eiger/eiger_500k_16bit_master_0.json"; + GapPixels gaps; + gaps.split_counts = true; + gaps.seed = 2024; + RawFile gapped(fpath, "r", gaps); + RawFile plain(fpath); + auto gapped_frame = gapped.read_frame(); + auto plain_frame = plain.read_frame(); + REQUIRE(matches_gap_oracle(gapped_frame, plain_frame, plain.master().roi(), + gaps)); +} + +TEST_CASE("Split counts are rejected on a recorded Jungfrau file", + "[.with-data][RawFile][GapPixels]") { + const auto fpath = + test_data_path() / "raw/jungfrau/jungfrau_single_master_0.json"; + GapPixels gaps; + gaps.split_counts = true; + REQUIRE_THROWS_AS(RawFile(fpath, "r", gaps), std::invalid_argument); +} + +TEST_CASE("Gap pixels on a file with two ROIs", + "[.with-data][RawFile][GapPixels]") { + const auto fpath = + test_data_path() / "raw/ROITestData/MultipleROIs/run_master_0.json"; + GapPixels gaps; + RawFile gapped(fpath, "r", gaps); + RawFile plain(fpath); + REQUIRE(gapped.num_rois() == 2); + // x 200..300 holds the chip boundary at 256; y 100..400 holds the one at + // 256 while y 600..700 holds none + REQUIRE(gapped.rows(0) == 303); + REQUIRE(gapped.cols(0) == 103); + REQUIRE(gapped.rows(1) == 101); + REQUIRE(gapped.cols(1) == 103); + + auto gapped_frames = gapped.read_rois(); + auto plain_frames = plain.read_rois(); + const auto rois = plain.master().rois(); + REQUIRE(gapped_frames.size() == 2); + for (size_t index = 0; index < 2; ++index) { + CAPTURE(index); + REQUIRE(matches_gap_oracle(gapped_frames[index], plain_frames[index], + rois[index], gaps)); + } +} + TEST_CASE("Read number of frames from a jungfrau raw file", "[.with-data][RawFile]") { @@ -535,7 +884,7 @@ TEST_CASE("Open multi module file with ROI", } TEST_CASE("Open multi module file with ROI spanning over multiple modules", - "[.width-data][read_rois][RawFile]") { + "[.with-data][read_rois][RawFile]") { auto fpath = test_data_path() / "raw/ROITestData/MultipleChipROI/run_master_2.json"; @@ -557,7 +906,7 @@ TEST_CASE("Open multi module file with ROI spanning over multiple modules", } TEST_CASE("Open multi module file with two ROIs", - "[.width-data][read_rois][RawFile]") { + "[.with-data][read_rois][RawFile]") { auto fpath = test_data_path() / "raw/ROITestData/MultipleROIs/run_master_0.json"; diff --git a/tests/raw_file_helpers.hpp b/tests/raw_file_helpers.hpp index b1a9b17c..4b8733e0 100644 --- a/tests/raw_file_helpers.hpp +++ b/tests/raw_file_helpers.hpp @@ -8,44 +8,99 @@ #include #include #include +#include +/** + * @brief Throwaway raw file series with a 2x3 pixel Jungfrau "module" per + * data file. Modules are laid out as DetectorGeometry orders them (module + * index runs down each column first) and every module holds distinct pixel + * values so that misplaced modules are detected. + */ class TemporaryRawFiles { std::filesystem::path m_directory; + aare::xy m_layout; + aare::xy m_module_size; public: - explicit TemporaryRawFiles(size_t modules = 1) { + static constexpr size_t module_rows = 2; + static constexpr size_t module_cols = 3; + static constexpr size_t frames = 2; + + /// Value written to pixel index `pixel` (row major) of module `module` + static uint16_t pixel_value(size_t module, size_t pixel) { + return static_cast((module * 10 + pixel + 1) % 65536); + } + + /// Modules stacked in one column + explicit TemporaryRawFiles(size_t modules = 1) + : TemporaryRawFiles(aare::xy{static_cast(modules), 1}) {} + + /// Modules of `module_size` pixels arranged in `layout.row` rows and + /// `layout.col` columns. The default module is 2x3 pixels; pass + /// {512, 1024} for a full Jungfrau module. + explicit TemporaryRawFiles(aare::xy layout, + aare::xy module_size = {module_rows, + module_cols}) + : m_layout(layout), m_module_size(module_size) { const auto unique = std::chrono::steady_clock::now().time_since_epoch().count(); m_directory = std::filesystem::temp_directory_path() / ("aare-raw-" + std::to_string(unique)); std::filesystem::create_directory(m_directory); + const size_t pixels_per_module = module_size.row * module_size.col; const nlohmann::json metadata = { {"Version", 7.2}, {"Detector Type", "Jungfrau"}, {"Timing Mode", "auto"}, - {"Geometry", {{"x", 1}, {"y", modules}}}, - {"Image Size in bytes", 12}, - {"Pixels", {{"x", 3}, {"y", 2}}}, + {"Geometry", {{"x", layout.col}, {"y", layout.row}}}, + {"Image Size in bytes", pixels_per_module * 2}, + {"Pixels", {{"x", module_size.col}, {"y", module_size.row}}}, {"Max Frames Per File", 1}, - {"Total Frames", 2}, - {"Frames in File", 2}, + {"Total Frames", frames}, + {"Frames in File", frames}, {"Frame Padding", 1}, {"Frame Discard Policy", "nodiscard"}}; std::ofstream(master_path()) << metadata; - for (size_t module = 0; module < modules; ++module) { - for (size_t file = 0; file < 2; ++file) { + std::vector pixels(pixels_per_module); + for (size_t module = 0; module < n_modules(); ++module) { + for (size_t pixel = 0; pixel < pixels_per_module; ++pixel) { + pixels[pixel] = pixel_value(module, pixel); + } + for (size_t file = 0; file < frames; ++file) { std::ofstream output(data_path(module, file), std::ios::binary); aare::DetectorHeader header{}; header.frameNumber = 100 + file; - const uint16_t pixels[6] = {1, 2, 3, 4, 5, 6}; + header.row = static_cast(module % layout.row); + header.column = static_cast(module / layout.row); output.write(reinterpret_cast(&header), sizeof(header)); - output.write(reinterpret_cast(pixels), - sizeof(pixels)); + output.write(reinterpret_cast(pixels.data()), + static_cast(pixels.size() * + sizeof(uint16_t))); } } } + aare::xy layout() const { return m_layout; } + aare::xy module_size() const { return m_module_size; } + size_t n_modules() const { return m_layout.row * m_layout.col; } + size_t rows() const { return m_module_size.row * m_layout.row; } + size_t cols() const { return m_module_size.col * m_layout.col; } + + /// Module index (data file number) of the module at a layout position + size_t module_index(size_t module_row, size_t module_col) const { + return module_col * m_layout.row + module_row; + } + + /// Expected value of pixel (row, col) in the assembled image + uint16_t expected(size_t row, size_t col) const { + const size_t module = + module_index(row / m_module_size.row, col / m_module_size.col); + const size_t pixel = (row % m_module_size.row) * m_module_size.col + + col % m_module_size.col; + return pixel_value(module, pixel); + } + TemporaryRawFiles(const TemporaryRawFiles &) = delete; TemporaryRawFiles &operator=(const TemporaryRawFiles &) = delete; ~TemporaryRawFiles() { std::filesystem::remove_all(m_directory); }