diff --git a/CMakeLists.txt b/CMakeLists.txt index e0c9b6c7..7f0d8651 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -439,7 +439,7 @@ if(AARE_VERBOSE) target_compile_definitions(aare_core PUBLIC AARE_VERBOSE) target_compile_definitions(aare_core PUBLIC AARE_LOG_LEVEL=aare::logDEBUG5) else() - target_compile_definitions(aare_core PUBLIC AARE_LOG_LEVEL=aare::logERROR) + target_compile_definitions(aare_core PUBLIC AARE_LOG_LEVEL=aare::logWARNING) endif() if(AARE_CUSTOM_ASSERT) @@ -451,6 +451,7 @@ set_target_properties(aare_core PROPERTIES ARCHIVE_OUTPUT_DIRECTORY if(AARE_TESTS) set(TestSources + ${CMAKE_CURRENT_SOURCE_DIR}/tests/raw_file_helpers.hpp ${CMAKE_CURRENT_SOURCE_DIR}/src/algorithm.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/calibration.test.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/defs.test.cpp diff --git a/RELEASE.md b/RELEASE.md index bb3cb855..5436d958 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -43,6 +43,19 @@ - ``TimingMode::Auto`` changed to ``TimingMode::AUTO_TIMING``, ``TimingMode::Trigger`` changed to ``TimingMode::TRIGGER_EXPOSURE`` ### Bugfixes: +- ``RawFile`` now derives its frame count from the shortest selected raw + subfile series across all ROIs. Frame-number reads use the same bounds, and + Python ``len(reader)`` returns the adjusted count. A warning is printed when + subfile counts differ or their minimum differs from the recorded master + count. The expected frame count is not used and master metadata is preserved. + Warning-level logging is now enabled in non-verbose builds. +- Raw frame, batch, ROI, and frame-number read errors now include the attempted + frame index and file path in C++ and Python, preserving subfile error details. + Subfile errors omit redundant master-file context and C++ source locations; + out-of-range errors include the available frame count. + Synchronization errors identify the last raw data file for the affected module. + Top-level frame bounds errors state the total frame count and that indices + are zero-based. - Fixed ``ClusterVector`` move operations to transfer storage instead of copying every cluster. - Validate that ``ClusterVector`` masks are one-dimensional, C-contiguous @@ -199,5 +212,3 @@ dhanya.thattil@psi.ch - - diff --git a/docs/src/RawFile.rst b/docs/src/RawFile.rst index 4b7e03ae..bd45ad71 100644 --- a/docs/src/RawFile.rst +++ b/docs/src/RawFile.rst @@ -1,8 +1,40 @@ RawFile =============== +``total_frames()`` is the minimum actual frame count across all selected +``RawSubFile`` objects in all ROIs. Each subfile count includes its complete +series of ``.raw`` files and is determined when the file is opened. Disabled +UDP ports are excluded. An empty subfile makes the count zero; opening a file +with no selected subfiles raises an error. + +A warning is logged once on opening if the subfile counts differ or their +minimum differs from ``master().frames_in_file()``. It reports the subfile count +range, the master count, and the count used for reading. The master metadata +is preserved. Its expected frame count does not affect the count used for +reading. + +Frame, batch, ROI, and frame-number reads use this minimum as their bound. +Frame-number synchronization may still fail within this bound if detector +frame numbers differ between subfiles. + +Read errors include the attempted zero-based frame index and the failing file +path. Errors from a data subfile identify that subfile's path and preserve +the underlying failure reason without adding a master-file wrapper or C++ +source location. Out-of-range subfile errors also report the available frame +count across that subfile's series. This applies to frame, batch, ROI, and +frame-number reads, including reads through ``aare::File``. + +The frame index is the position in the file, not the frame number recorded by +the detector. Batch errors report the index of the frame that failed. + +Synchronization errors identify the last ``.raw`` data file read for the module +that ran out of frames. Requests rejected as out of range before accessing a +subfile report the master path, since there is no single failing data file. +The top-level frame bounds error states the number of frames in the file and +that indices are zero-based. + .. doxygenclass:: aare::RawFile :members: :undoc-members: - :private-members: \ No newline at end of file + :private-members: diff --git a/docs/src/python/file/pyRawFile.rst b/docs/src/python/file/pyRawFile.rst index b81f4138..deeb8f3e 100644 --- a/docs/src/python/file/pyRawFile.rst +++ b/docs/src/python/file/pyRawFile.rst @@ -3,8 +3,38 @@ RawFile .. py:currentmodule:: aare +``total_frames`` and ``len(reader)`` report the minimum actual frame count +across all selected subfiles and ROIs, including each subfile's complete +``.raw`` series. ``read()`` uses this count, and ``read_n()`` and +``read_n_with_roi()`` limit batches to the remaining frames. Counts are +determined when the reader is constructed. +An empty subfile makes the count zero, and construction fails if no subfiles +are selected. + +The C++ logger prints one warning to stderr on opening if the subfile counts +differ or their minimum differs from ``reader.master.frames_in_file``. The +warning includes the count range, the recorded master count, and the count +used for reading. Master metadata is preserved; its expected frame count +does not affect reading bounds. Detector frame-number mismatches can still +cause synchronization errors within these bounds. + +Reading errors raise ``RuntimeError`` with the attempted zero-based frame index +and the failing file path. If a data subfile fails, its path and the underlying +failure reason are reported. Out-of-range subfile errors include the available frame count across +that subfile's series. This applies to ``read_frame()``, ``read_n()``, +and the ROI reading methods. The frame index is the position in the file, not +the frame number recorded by the detector. + +Synchronization errors also identify the last ``.raw`` data file read for the +module that ran out of frames. An out-of-range request rejected before accessing +a subfile reports the master path. + +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)``. + .. autoclass:: RawFile :members: :undoc-members: :show-inheritance: - :inherited-members: \ No newline at end of file + :inherited-members: diff --git a/include/aare/RawFile.hpp b/include/aare/RawFile.hpp index d9cf0d71..3b27e156 100644 --- a/include/aare/RawFile.hpp +++ b/include/aare/RawFile.hpp @@ -31,7 +31,7 @@ class RawFile : public FileInterface { std::vector m_ROI_geometries; - /// @brief total number of frames in file + /// @brief Minimum frame count across the selected raw subfile series. size_t m_frames_in_file{}; public: @@ -102,6 +102,7 @@ class RawFile : public FileInterface { size_t bytes_per_pixel() const; void seek(size_t frame_index) override; size_t tell() override; + /// @brief Minimum actual frame count across all subfiles and ROIs. size_t total_frames() const override; size_t rows() const override; /** @@ -148,6 +149,9 @@ class RawFile : public FileInterface { static DetectorHeader read_header(const std::filesystem::path &fname); private: + std::runtime_error frame_error(size_t frame_index, + const std::string &message) const; + /** * @brief read the frame at the given frame index into the image buffer * @param frame_index frame number to read diff --git a/include/aare/RawMasterFile.hpp b/include/aare/RawMasterFile.hpp index 827bcc70..26ea3b41 100644 --- a/include/aare/RawMasterFile.hpp +++ b/include/aare/RawMasterFile.hpp @@ -130,6 +130,9 @@ class RawMasterFile { RawMasterFile(const std::filesystem::path &fpath); RawMasterFile(std::istream &is, const std::string &fname); // for testing + /// @brief Get the filename including path of the master file. + std::filesystem::path master_fname() const { return m_fnc.master_fname(); } + std::filesystem::path data_fname(size_t mod_id, size_t file_id) const; const std::string &version() const; //!< For example "7.2" @@ -192,4 +195,4 @@ class RawMasterFile { void retrieve_geometry(); }; -} // namespace aare \ No newline at end of file +} // namespace aare diff --git a/include/aare/RawSubFile.hpp b/include/aare/RawSubFile.hpp index 50db61fa..deff6fb7 100644 --- a/include/aare/RawSubFile.hpp +++ b/include/aare/RawSubFile.hpp @@ -84,7 +84,13 @@ class RawSubFile { size_t frames_in_file() const { return m_total_frames; } + /// @brief Path of the current data file in the raw subfile series. + std::filesystem::path current_path() const; + private: + std::runtime_error frame_error(size_t frame_index, + const std::string &message) const; + template void read_with_map(std::byte *image_buf); void parse_fname(const std::filesystem::path &fname); @@ -93,4 +99,4 @@ class RawSubFile { std::filesystem::path fpath(size_t file_index) const; }; -} // namespace aare \ No newline at end of file +} // namespace aare diff --git a/python/aare/RawFile.py b/python/aare/RawFile.py index 988ff3d5..253fcc53 100644 --- a/python/aare/RawFile.py +++ b/python/aare/RawFile.py @@ -43,7 +43,7 @@ class RawFile(_aare.RawFile): Returns: int: Number of frames in file. """ - return super().frames_in_file + return self.total_frames def __enter__(self): return self diff --git a/python/src/bind_RawFile.hpp b/python/src/bind_RawFile.hpp index 85ef155f..2c4e20c8 100644 --- a/python/src/bind_RawFile.hpp +++ b/python/src/bind_RawFile.hpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -21,14 +22,23 @@ namespace py = pybind11; using namespace ::aare; +namespace { +std::runtime_error raw_file_read_error(RawFile &file, + const std::string &message) { + return std::runtime_error(fmt::format( + "Error reading frame index {} from file '{}': {}", file.tell(), + file.master().master_fname().string(), message)); +} +} // namespace + void define_raw_file_io_bindings(py::module &m) { py::class_(m, "RawFile") .def(py::init()) .def("read_frame", [](RawFile &self) { if (self.n_modules_in_roi().size() > 1) { - throw std::runtime_error( - "File contains multiple ROIs - use read_ROIs()"); + throw raw_file_read_error( + self, "File contains multiple ROIs - use read_ROIs()"); } std::vector shape; @@ -51,16 +61,19 @@ void define_raw_file_io_bindings(py::module &m) { "read_n", [](RawFile &self, size_t n_frames) { if (self.n_modules_in_roi().size() > 1) { - throw std::runtime_error( + throw raw_file_read_error( + self, "File contains multiple ROIs - use read_n_ROIs() to " "read a specific ROI or use read_ROIs and " "read one frame at a time."); } // adjust for actual frames left in the file n_frames = - std::min(n_frames, self.total_frames() - self.tell()); + std::min(n_frames, self.tell() < self.total_frames() + ? self.total_frames() - self.tell() + : size_t{0}); if (n_frames == 0) { - throw std::runtime_error("No frames left in file"); + throw raw_file_read_error(self, "No frames left in file"); } std::vector shape{n_frames, self.rows(), self.cols()}; @@ -89,12 +102,13 @@ void define_raw_file_io_bindings(py::module &m) { "read_roi", [](RawFile &self, const size_t roi_index) { if (self.num_rois() == 0) { - throw std::runtime_error(LOCATION + "No ROIs defined."); + throw raw_file_read_error(self, + LOCATION + "No ROIs defined."); } if (roi_index >= self.num_rois()) { - throw std::runtime_error(LOCATION + - "ROI index out of range."); + throw raw_file_read_error( + self, LOCATION + "ROI index out of range."); } // return headers from all subfiles @@ -141,7 +155,8 @@ void define_raw_file_io_bindings(py::module &m) { "read_rois", [](RawFile &self) { if (self.num_rois() == 0) { - throw std::runtime_error(LOCATION + "No ROIs defined."); + throw raw_file_read_error(self, + LOCATION + "No ROIs defined."); } size_t number_of_ROIs = self.num_rois(); @@ -196,19 +211,22 @@ void define_raw_file_io_bindings(py::module &m) { "read_n_with_roi", [](RawFile &self, const size_t num_frames, const size_t roi_index) { if (self.num_rois() == 0) { - throw std::runtime_error(LOCATION + "No ROIs defined."); + throw raw_file_read_error(self, + LOCATION + "No ROIs defined."); } if (roi_index >= self.num_rois()) { - throw std::runtime_error(LOCATION + - "ROI index out of range."); + throw raw_file_read_error( + self, LOCATION + "ROI index out of range."); } // adjust for actual frames left in the file size_t n_frames = - std::min(num_frames, self.total_frames() - self.tell()); + std::min(num_frames, self.tell() < self.total_frames() + ? self.total_frames() - self.tell() + : size_t{0}); if (n_frames == 0) { - throw std::runtime_error("No frames left in file"); + throw raw_file_read_error(self, "No frames left in file"); } std::vector shape{ n_frames, self.roi_geometries(roi_index).pixels_y(), @@ -306,4 +324,4 @@ void define_raw_file_io_bindings(py::module &m) { .def_property_readonly("n_modules", &RawFile::n_modules) .def_property_readonly("n_modules_in_roi", &RawFile::n_modules_in_roi) .def_property_readonly("num_rois", &RawFile::num_rois); -} \ No newline at end of file +} diff --git a/python/tests/test_RawFile.py b/python/tests/test_RawFile.py index 5909c04c..78636a9c 100644 --- a/python/tests/test_RawFile.py +++ b/python/tests/test_RawFile.py @@ -1,8 +1,244 @@ # SPDX-License-Identifier: MPL-2.0 import pytest -from aare import RawFile, ROI, UDPPortPosition +import json +from aare import File, RawFile, RawSubFile, DetectorType, ROI, UDPPortPosition import numpy as np + +@pytest.fixture +def small_raw_file(tmp_path): + master_path = tmp_path / "run_master_0.json" + master_path.write_text(json.dumps({ + "Version": 7.2, + "Detector Type": "Jungfrau", + "Timing Mode": "auto", + "Geometry": {"x": 1, "y": 1}, + "Image Size in bytes": 12, + "Pixels": {"x": 3, "y": 2}, + "Max Frames Per File": 1, + "Total Frames": 2, + "Frames in File": 2, + "Frame Padding": 1, + "Frame Discard Policy": "nodiscard", + })) + for index in range(2): + data_path = tmp_path / f"run_d0_f{index}_0.raw" + # A detector header followed by six uint16 pixels. + data_path.write_bytes(bytes(112) + np.arange(6, dtype=np.uint16).tobytes()) + return master_path + + +@pytest.mark.parametrize("method, args, kwargs", [ + ("read_frame", (), {}), + ("read_n", (1,), {}), + ("read_roi", (0,), {}), + ("read_rois", (), {}), + ("read_n_with_roi", (1,), {"roi_index": 0}), + ("frame_number", (2,), {}), +]) +def test_raw_read_error_context(small_raw_file, method, args, kwargs): + reader = RawFile(small_raw_file) + reader.seek(2) + with pytest.raises(RuntimeError) as error: + getattr(reader, method)(*args, **kwargs) + assert "frame index 2" in str(error.value) + assert str(small_raw_file) in str(error.value) + assert ".raw" not in str(error.value) + if method == "read_frame": + assert "file contains 2 frames (indices are zero-based)" in str(error.value) + + +@pytest.mark.parametrize("frame_index", [2, 17]) +def test_generic_raw_read_error_context(small_raw_file, frame_index): + reader = File(small_raw_file) + with pytest.raises(RuntimeError) as error: + reader.read_frame(frame_index) + assert f"frame index {frame_index}" in str(error.value) + assert str(small_raw_file) in str(error.value) + + +@pytest.mark.parametrize("raw_subfile", [False, True]) +def test_truncated_raw_read_error_context(small_raw_file, raw_subfile): + data_path = small_raw_file.parent / "run_d0_f1_0.raw" + if raw_subfile: + reader = RawSubFile( + small_raw_file.parent / "run_d0_f0_0.raw", + DetectorType.Jungfrau, 2, 3, 16, + ) + else: + reader = RawFile(small_raw_file) + data_path.write_bytes(bytes(112)) + reader.seek(1) + with pytest.raises(RuntimeError) as error: + reader.read_frame() + assert "frame index 1" in str(error.value) + assert str(data_path) in str(error.value) + assert "End of file" in str(error.value) + assert "master" not in str(error.value) + assert "RawSubFile.cpp" not in str(error.value) + + +@pytest.mark.parametrize("method, args", [ + ("read_frame", ()), + ("read_n", (1,)), + ("read_roi", (0,)), + ("frame_number", (1,)), +]) +def test_short_raw_subfile_sets_read_bounds(small_raw_file, method, args): + (small_raw_file.parent / "run_d0_f1_0.raw").unlink() + reader = RawFile(small_raw_file) + reader.seek(1) + with pytest.raises(RuntimeError) as error: + getattr(reader, method)(*args) + assert reader.total_frames == 1 + assert "frame index 1" in str(error.value) + assert str(small_raw_file) in str(error.value) + assert ".raw" not in str(error.value) + + +@pytest.mark.parametrize("counts, master_count, warn", [ + ([2], 2, False), + ([2, 2], 2, False), + ([2, 2], 0, True), + ([2, 2], 1, True), + ([2, 2], 5, True), + ([1, 2], 2, True), + ([2, 1], 2, True), + ([1, 2], 1, True), + ([0, 2], 2, True), + ([0, 0], 0, False), +]) +def test_raw_subfile_frame_counts(small_raw_file, capfd, counts, master_count, warn): + metadata = json.loads(small_raw_file.read_text()) + metadata["Geometry"]["y"] = len(counts) + metadata["Frames in File"] = master_count + metadata["Total Frames"] = 1000 + small_raw_file.write_text(json.dumps(metadata)) + data = (small_raw_file.parent / "run_d0_f0_0.raw").read_bytes() + for module, count in enumerate(counts): + for index in range(2): + path = small_raw_file.parent / f"run_d{module}_f{index}_0.raw" + if index < count: + path.write_bytes(data) + elif index == 0: + path.write_bytes(b"") + else: + path.unlink(missing_ok=True) + + reader = RawFile(small_raw_file) + warning = capfd.readouterr().err + if warn: + assert warning.count("WARNING") == 1 + assert str(small_raw_file) in warning + else: + assert "WARNING" not in warning + + assert reader.total_frames == min(counts) + assert len(reader) == min(counts) + assert reader.master.frames_in_file == master_count + if min(counts): + _, frames = reader.read() + assert frames.shape == (min(counts), 2 * len(counts), 3) + reader.seek(0) + _, frames = reader.read_n(10) + assert len(frames) == min(counts) + assert reader.frame_number(min(counts) - 1) == 0 + with pytest.raises(RuntimeError): + reader.frame_number(min(counts)) + reader.seek(min(counts)) + with pytest.raises(RuntimeError): + reader.read_frame() + assert "WARNING" not in capfd.readouterr().err + + +@pytest.mark.parametrize("short_module", [0, 1]) +def test_raw_frame_count_across_rois(small_raw_file, short_module): + metadata = json.loads(small_raw_file.read_text()) + metadata.update({ + "Version": 8.0, + "Geometry": {"x": 1, "y": 2}, + "Image Size": 12, + "Receiver Rois": [ + {"xmin": 0, "xmax": 2, "ymin": 0, "ymax": 1}, + {"xmin": 0, "xmax": 2, "ymin": 2, "ymax": 3}, + ], + }) + small_raw_file.write_text(json.dumps(metadata)) + for index in range(2): + data = (small_raw_file.parent / f"run_d0_f{index}_0.raw").read_bytes() + (small_raw_file.parent / f"run_d1_f{index}_0.raw").write_bytes(data) + (small_raw_file.parent / f"run_d{short_module}_f1_0.raw").unlink() + + reader = RawFile(small_raw_file) + assert reader.total_frames == 1 + _, rois = reader.read_rois() + assert len(rois) == 2 + assert all(roi.shape == (2, 3) for roi in rois) + for roi_index in range(2): + reader.seek(0) + _, frames = reader.read_n_with_roi(10, roi_index=roi_index) + assert frames.shape == (1, 2, 3) + with pytest.raises(RuntimeError): + reader.read_roi(roi_index) + + +@pytest.mark.parametrize("disabled_port", [0, 1]) +def test_raw_frame_count_with_disabled_udp_port(small_raw_file, disabled_port): + metadata = json.loads(small_raw_file.read_text()) + metadata.update({ + "Geometry": {"x": 1, "y": 2}, + "Frames in File": 0, + "Number of UDP Interfaces": 2, + "UDP Ports Type": ["bottom", "top"], + "UDP Ports Disabled": [disabled_port], + }) + small_raw_file.write_text(json.dumps(metadata)) + data = (small_raw_file.parent / "run_d0_f0_0.raw").read_bytes() + for index in range(2): + (small_raw_file.parent / f"run_d0_f{index}_0.raw").unlink() + path = small_raw_file.parent / f"run_d{1 - disabled_port}_f{index}_0.raw" + path.write_bytes(data) + + reader = RawFile(small_raw_file) + assert reader.total_frames == 2 + assert reader.master.frames_in_file == 0 + assert reader.frame_number(1) == 0 + _, frames = reader.read() + assert frames.shape == (2, 2, 3) + + +def test_raw_frame_count_without_selected_subfiles(small_raw_file): + metadata = json.loads(small_raw_file.read_text()) + metadata.update({"Version": 8.0, "Image Size": 12, "Receiver Rois": []}) + small_raw_file.write_text(json.dumps(metadata)) + with pytest.raises(RuntimeError, match="No raw subfiles selected"): + RawFile(small_raw_file) + + +@pytest.mark.parametrize("lagging_module", [0, 1]) +def test_raw_synchronization_error_identifies_subfile( + small_raw_file, lagging_module +): + metadata = json.loads(small_raw_file.read_text()) + metadata["Geometry"]["y"] = 2 + small_raw_file.write_text(json.dumps(metadata)) + for index in range(2): + data = (small_raw_file.parent / f"run_d0_f{index}_0.raw").read_bytes() + (small_raw_file.parent / f"run_d1_f{index}_0.raw").write_bytes(data) + + ahead_path = small_raw_file.parent / f"run_d{1 - lagging_module}_f1_0.raw" + with ahead_path.open("r+b") as output: + output.write(np.array([1], dtype=np.uint64).tobytes()) + + reader = RawFile(small_raw_file) + reader.seek(1) + with pytest.raises(RuntimeError) as error: + reader.read_frame() + assert "frame index 1" in str(error.value) + assert "synchroniz" in str(error.value) + lagging_path = small_raw_file.parent / f"run_d{lagging_module}_f1_0.raw" + assert str(lagging_path) in str(error.value) + @pytest.mark.withdata def test_read_rawfile_with_roi_spanning_over_one_module(test_data_path): @@ -147,4 +383,4 @@ def test_read_eiger_udp_port_disabled(test_data_path): rois = f.master.rois assert len(rois) == 2 assert rois[0] == ROI(0, 512, 0, 512) - assert rois[1] == ROI(1024, 1536, 0, 512) \ No newline at end of file + assert rois[1] == ROI(1024, 1536, 0, 512) diff --git a/src/RawFile.cpp b/src/RawFile.cpp index de89929b..1d76b622 100644 --- a/src/RawFile.cpp +++ b/src/RawFile.cpp @@ -16,8 +16,15 @@ using json = nlohmann::json; namespace aare { +std::runtime_error RawFile::frame_error(size_t frame_index, + const std::string &message) const { + return std::runtime_error( + fmt::format("Error reading frame index {} from file '{}': {}", + frame_index, m_master.master_fname().string(), message)); +} + RawFile::RawFile(const std::filesystem::path &fname, const std::string &mode) - : m_master(fname), m_frames_in_file(m_master.frames_in_file()) { + : m_master(fname) { m_mode = mode; @@ -32,30 +39,31 @@ RawFile::RawFile(const std::filesystem::path &fname, const std::string &mode) open_subfiles(roi_index); } - // TODO: work around for now - retrieve num_frames from subfiles - if (!m_master.disabled_udp_ports().empty()) { - // TODO: remove frames_from_file from master file? - // retrieve the frame numbers from subfile as frames per file in - // master file 0 if dataprocessor 0 was disabled - std::vector min_subfiles_per_roi(m_subfiles.size()); - std::transform( - m_subfiles.begin(), m_subfiles.end(), - min_subfiles_per_roi.begin(), [](const auto &subfiles) { - return std::min_element( - subfiles.begin(), subfiles.end(), - [](const auto &raw_subfile1, - const auto &raw_subfile2) { - return raw_subfile1->frames_in_file() < - raw_subfile2->frames_in_file(); - }) - ->get() - ->frames_in_file(); - }); - m_frames_in_file = *std::min_element(min_subfiles_per_roi.begin(), - min_subfiles_per_roi.end()); - - LOG(logDEBUG) << "Frames in file: " << m_frames_in_file; + std::optional min_frames; + size_t max_frames = 0; + for (const auto &subfiles : m_subfiles) { + for (const auto &subfile : subfiles) { + const auto count = subfile->frames_in_file(); + min_frames = min_frames ? std::min(*min_frames, count) : count; + max_frames = std::max(max_frames, count); + } } + if (!min_frames) { + throw std::runtime_error(fmt::format( + "No raw subfiles selected by '{}'", fname.string())); + } + m_frames_in_file = *min_frames; + + if (m_frames_in_file != max_frames || + m_frames_in_file != m_master.frames_in_file()) { + LOG(logWARNING) << fmt::format( + "'{}': Different number of frames across subfiles. Expected {} " + "frames but found min/max {}/{}, " + "using {} frames.", + fname.string(), m_master.frames_in_file(), m_frames_in_file, + max_frames, m_frames_in_file); + } + LOG(logDEBUG) << "Frames in file: " << m_frames_in_file; } else { throw std::runtime_error(LOCATION + " Unsupported mode. Can only read RawFiles."); @@ -65,7 +73,8 @@ RawFile::RawFile(const std::filesystem::path &fname, const std::string &mode) Frame RawFile::read_roi(const size_t roi_index) { if (roi_index >= m_master.roi_geometries().size()) { - throw std::runtime_error(LOCATION + "ROI index out of range."); + throw frame_error(m_current_frame, + LOCATION + "ROI index out of range."); } return get_frame(m_current_frame++, roi_index); } @@ -86,15 +95,17 @@ std::vector RawFile::read_rois() { Frame RawFile::read_frame() { if (m_master.roi_geometries().size() > 1) { - throw std::runtime_error(LOCATION + "Multiple ROIs present in file. " - "Use read_ROIs() instead."); + throw frame_error(m_current_frame, LOCATION + + "Multiple ROIs present in file. " + "Use read_ROIs() instead."); } return get_frame(m_current_frame++); } Frame RawFile::read_frame(size_t frame_number) { if (m_master.roi_geometries().size() > 1) { - throw std::runtime_error( + throw frame_error( + frame_number, LOCATION + "Multiple ROIs present in file. " "Use read_ROIs(const size_t frame_number) instead."); } @@ -105,8 +116,8 @@ Frame RawFile::read_frame(size_t frame_number) { void RawFile::read_into(std::byte *image_buf, size_t n_frames) { // TODO: implement this in a more efficient way if (m_master.roi_geometries().size() > 1) { - throw std::runtime_error(LOCATION + - "Cannot use read_into for multiple ROIs."); + throw frame_error(m_current_frame, + LOCATION + "Cannot use read_into for multiple ROIs."); } for (size_t i = 0; i < n_frames; i++) { @@ -117,9 +128,10 @@ void RawFile::read_into(std::byte *image_buf, size_t n_frames) { void RawFile::read_into(std::byte *image_buf) { if (m_master.roi_geometries().size() > 1) { - throw std::runtime_error(LOCATION + - "Cannot use read_into for multiple ROIs. Use " - "read_roi_into() for a single ROI instead."); + throw frame_error(m_current_frame, + LOCATION + + "Cannot use read_into for multiple ROIs. Use " + "read_roi_into() for a single ROI instead."); } return get_frame_into(m_current_frame++, image_buf); } @@ -127,16 +139,17 @@ void RawFile::read_into(std::byte *image_buf) { void RawFile::read_roi_into(std::byte *image_buf, const size_t roi_index, const size_t frame_number, DetectorHeader *header) { if (roi_index >= num_rois()) { - throw std::runtime_error(LOCATION + "ROI index out of range."); + throw frame_error(frame_number, LOCATION + "ROI index out of range."); } return get_frame_into(frame_number, image_buf, roi_index, header); } void RawFile::read_into(std::byte *image_buf, DetectorHeader *header) { if (m_master.roi_geometries().size() > 1) { - throw std::runtime_error(LOCATION + - "Cannot use read_into for multiple ROIs. Use " - "read_roi_into() for a single ROI instead."); + throw frame_error(m_current_frame, + LOCATION + + "Cannot use read_into for multiple ROIs. Use " + "read_roi_into() for a single ROI instead."); } return get_frame_into(m_current_frame++, image_buf, 0, header); } @@ -146,15 +159,16 @@ void RawFile::read_into(std::byte *image_buf, size_t n_frames, // return get_frame_into(m_current_frame++, image_buf, header); if (m_master.roi_geometries().size() > 1) { - throw std::runtime_error( + throw frame_error( + m_current_frame, LOCATION + - "Cannot use read_into for multiple ROIs."); // TODO: maybe - // pass - // roi_index so - // one can use - // read_into for - // a specific - // ROI + "Cannot use read_into for multiple ROIs."); // TODO: maybe + // pass + // roi_index so + // one can use + // read_into for + // a specific + // ROI } for (size_t i = 0; i < n_frames; i++) { @@ -201,7 +215,8 @@ void RawFile::seek(size_t frame_index) { // if frame_number == total_frames, then the next read will throw an // error if (frame_index > total_frames()) { - throw std::runtime_error( + throw frame_error( + frame_index, fmt::format("frame number {} is greater than total frames {}", frame_index, total_frames())); } @@ -281,14 +296,22 @@ DetectorHeader RawFile::read_header(const std::filesystem::path &fname) { DetectorHeader h{}; FILE *fp = fopen(fname.string().c_str(), "r"); if (!fp) - throw std::runtime_error( - fmt::format("Could not open: {} for reading", fname.string())); + throw std::runtime_error(fmt::format( + "Could not open file '{}' for frame index 0", fname.string())); size_t const rc = fread(reinterpret_cast(&h), sizeof(h), 1, fp); - if (rc != 1) - throw std::runtime_error(LOCATION + "Could not read header from file"); + if (rc != 1) { + fclose(fp); + throw std::runtime_error( + LOCATION + fmt::format("Could not read header for frame index 0 " + "from file '{}'", + fname.string())); + } if (fclose(fp)) { - throw std::runtime_error(LOCATION + "Could not close file"); + throw std::runtime_error( + LOCATION + fmt::format("Could not close file '{}' after reading " + "frame index 0", + fname.string())); } return h; @@ -311,7 +334,12 @@ void RawFile::get_frame_into(size_t frame_index, std::byte *frame_buffer, const size_t roi_index, DetectorHeader *header) { LOG(logDEBUG) << "RawFile::get_frame_into(" << frame_index << ")"; if (frame_index >= total_frames()) { - throw std::runtime_error(LOCATION + "Frame number out of range"); + throw frame_error( + frame_index, + LOCATION + + fmt::format("Frame index {} is out of range: file contains {} " + "frames (indices are zero-based)", + frame_index, total_frames())); } std::vector frame_numbers( m_master.roi_geometries().at(roi_index).num_modules_in_roi()); @@ -345,8 +373,17 @@ void RawFile::get_frame_into(size_t frame_index, std::byte *frame_buffer, // 4. if we can't increase its index => throw error if (frame_indices[min_frame_idx] >= total_frames()) { - throw std::runtime_error(LOCATION + - "Frame number out of range"); + throw frame_error( + frame_index, + LOCATION + + fmt::format( + "Frame index {} out of range while synchronizing " + "module {} for ROI {}; last data file '{}'", + frame_indices[min_frame_idx], min_frame_idx, + roi_index, + m_subfiles[roi_index][min_frame_idx] + ->current_path() + .string())); } frame_numbers[min_frame_idx] = @@ -385,14 +422,15 @@ void RawFile::get_frame_into(size_t frame_index, std::byte *frame_buffer, .at(roi_index) .module_indices_in_roi(part_idx)) .origin_x != 0) - throw std::runtime_error( + throw frame_error( + frame_index, LOCATION + - " Implementation error. x pos not 0."); // TODO: - // origin - // can still - // change if - // roi - // changes + " Implementation error. x pos not 0."); // TODO: + // origin + // can still + // change if + // roi + // changes // TODO! What if the files don't match? m_subfiles[roi_index][part_idx]->seek(corrected_idx); m_subfiles[roi_index][part_idx]->read_into(frame_buffer + offset, @@ -451,12 +489,12 @@ void RawFile::get_frame_into(size_t frame_index, std::byte *frame_buffer, std::vector RawFile::read_n(size_t n_frames) { // TODO: implement this in a more efficient way if (num_rois() > 1) { - throw std::runtime_error(LOCATION + - "Multiple ROIs defined in the master " - "file. Use " - "read_num_rois for a specific ROI or use " - "read_ROIs to read one frame after " - "the other."); + throw frame_error(m_current_frame, + LOCATION + "Multiple ROIs defined in the master " + "file. Use " + "read_num_rois for a specific ROI or use " + "read_ROIs to read one frame after " + "the other."); } std::vector frames; @@ -471,7 +509,8 @@ std::vector RawFile::read_n(size_t n_frames) { std::vector RawFile::read_n_with_roi(const size_t n_frames, const size_t roi_index) { if (roi_index >= num_rois()) { - throw std::runtime_error(LOCATION + "ROI index out of range."); + throw frame_error(m_current_frame, + LOCATION + "ROI index out of range."); } std::vector frames; @@ -484,8 +523,8 @@ std::vector RawFile::read_n_with_roi(const size_t n_frames, } size_t RawFile::frame_number(size_t frame_index) { - if (frame_index >= m_master.frames_in_file()) { - throw std::runtime_error(LOCATION + " Frame number out of range"); + if (frame_index >= total_frames()) { + throw frame_error(frame_index, "Frame number out of range"); } return m_subfiles[0][0]->frame_number(frame_index); } diff --git a/src/RawFile.test.cpp b/src/RawFile.test.cpp index da316d52..e798a610 100644 --- a/src/RawFile.test.cpp +++ b/src/RawFile.test.cpp @@ -8,6 +8,7 @@ #include #include +#include "raw_file_helpers.hpp" #include "test_config.hpp" #include "test_macros.hpp" @@ -15,6 +16,167 @@ using aare::File; using aare::RawFile; using namespace aare; +TEST_CASE("RawFile read errors identify the frame index and master path", + "[RawFile][read-errors]") { + TemporaryRawFiles files; + RawFile file(files.master_path()); + REQUIRE(file.frame_number(1) == 101); + file.seek(2); + const auto index_matcher = + Catch::Matchers::ContainsSubstring("frame index 2"); + const auto path_matcher = + Catch::Matchers::ContainsSubstring(files.master_path().string()); + const auto context = index_matcher && path_matcher; + std::vector buffer(file.bytes_per_frame() * 2); + DetectorHeader headers[2]{}; + + SECTION("sequential frame") { + REQUIRE_THROWS_WITH( + file.read_frame(), + index_matcher && path_matcher && + Catch::Matchers::ContainsSubstring( + "file contains 2 frames (indices are zero-based)") && + !Catch::Matchers::ContainsSubstring(".raw")); + } + SECTION("indexed frame at EOF") { + REQUIRE_THROWS_WITH(file.read_frame(2), context); + } + SECTION("indexed frame beyond EOF") { + REQUIRE_THROWS_WITH( + file.read_frame(17), + Catch::Matchers::ContainsSubstring("frame index 17") && + Catch::Matchers::ContainsSubstring( + files.master_path().string())); + } + SECTION("batch crosses EOF") { + file.seek(1); + REQUIRE_THROWS_WITH(file.read_n(2), context); + } + SECTION("read into buffer") { + FileInterface &reader = file; + REQUIRE_THROWS_WITH(reader.read_into(buffer.data()), context); + } + SECTION("batch into buffer") { + file.seek(1); + REQUIRE_THROWS_WITH(file.read_into(buffer.data(), size_t{2}), context); + } + SECTION("read with header") { + REQUIRE_THROWS_WITH(file.read_into(buffer.data(), headers), context); + } + SECTION("batch with headers") { + file.seek(1); + REQUIRE_THROWS_WITH(file.read_into(buffer.data(), 2, headers), context); + } + SECTION("single ROI") { REQUIRE_THROWS_WITH(file.read_roi(0), context); } + SECTION("all ROIs") { REQUIRE_THROWS_WITH(file.read_rois(), context); } + SECTION("ROI into buffer") { + REQUIRE_THROWS_WITH(file.read_roi_into(buffer.data(), 0, 2), context); + } + SECTION("ROI batch") { + file.seek(1); + REQUIRE_THROWS_WITH(file.read_n_with_roi(2, 0), context); + } + SECTION("frame number") { + REQUIRE_THROWS_WITH(file.frame_number(2), context); + } + SECTION("generic File") { + File reader(files.master_path()); + REQUIRE_THROWS_WITH(reader.read_frame(2), context); + } +} + +TEST_CASE("RawFile synchronization errors retain the requested index", + "[RawFile][read-errors]") { + TemporaryRawFiles files(2); + const size_t lagging_module = GENERATE(0, 1); + { + std::fstream output(files.data_path(1 - lagging_module, 1), + std::ios::binary | std::ios::in | std::ios::out); + DetectorHeader header{}; + header.frameNumber = 102; + output.write(reinterpret_cast(&header), sizeof(header)); + } + RawFile file(files.master_path()); + REQUIRE_THROWS_WITH( + file.read_frame(1), + Catch::Matchers::ContainsSubstring("frame index 1") && + Catch::Matchers::ContainsSubstring(files.master_path().string()) && + Catch::Matchers::ContainsSubstring( + files.data_path(lagging_module, 1).string()) && + Catch::Matchers::ContainsSubstring("synchroniz")); +} + +TEST_CASE("RawFile reports the failing data file and requested frame", + "[RawFile][read-errors]") { + TemporaryRawFiles files; + RawFile file(files.master_path()); + std::filesystem::resize_file(files.data_path(0, 1), sizeof(DetectorHeader)); + REQUIRE_THROWS_WITH( + file.read_frame(1), + Catch::Matchers::ContainsSubstring("frame index 1") && + !Catch::Matchers::ContainsSubstring(files.master_path().string()) && + Catch::Matchers::ContainsSubstring( + files.data_path(0, 1).string()) && + Catch::Matchers::ContainsSubstring("End of file")); +} + +TEST_CASE("RawFile bounds use the shortest subfile", + "[RawFile][read-errors][frame-count]") { + TemporaryRawFiles files(2); + const size_t short_module = GENERATE(0, 1); + std::filesystem::remove(files.data_path(short_module, 1)); + RawFile file(files.master_path()); + REQUIRE(file.total_frames() == 1); + REQUIRE(file.master().frames_in_file() == 2); + REQUIRE_NOTHROW(file.read_frame(0)); + const auto expected = Catch::Matchers::ContainsSubstring( + "Error reading frame index 1 from file '" + + files.master_path().string() + "':"); + + SECTION("indexed frame") { + REQUIRE_THROWS_WITH(file.read_frame(1), expected); + } + SECTION("batch") { + file.seek(0); + REQUIRE_THROWS_WITH(file.read_n(2), expected); + } + SECTION("ROI") { + file.seek(1); + REQUIRE_THROWS_WITH(file.read_roi(0), expected); + } + SECTION("generic File") { + File reader(files.master_path()); + REQUIRE_THROWS_WITH(reader.read_frame(1), expected); + } + SECTION("frame number") { + REQUIRE_THROWS_WITH(file.frame_number(1), expected); + } +} + +TEST_CASE("RawFile frame count comes from the complete subfile series", + "[RawFile][frame-count]") { + TemporaryRawFiles files; + const size_t master_frames = GENERATE(0, 1, 2, 5); + auto metadata = nlohmann::json::parse(std::ifstream(files.master_path())); + metadata["Frames in File"] = master_frames; + metadata["Total Frames"] = 1000; + std::ofstream(files.master_path()) << metadata; + + RawFile file(files.master_path()); + REQUIRE(file.total_frames() == 2); + REQUIRE(file.master().frames_in_file() == master_frames); + REQUIRE(file.master().total_frames_expected() == 1000); + REQUIRE(file.frame_number(1) == 101); + REQUIRE_NOTHROW(file.read_frame(1)); + REQUIRE_THROWS(file.frame_number(2)); + REQUIRE_THROWS(file.read_frame(2)); + + File reader(files.master_path()); + REQUIRE(reader.total_frames() == 2); + REQUIRE(reader.frame_number(1) == 101); + REQUIRE_NOTHROW(reader.read_frame(1)); +} + TEST_CASE("Read number of frames from a jungfrau raw file", "[.with-data][RawFile]") { @@ -697,4 +859,4 @@ TEST_CASE("Read Eiger frame with disabled UDP ports", REQUIRE(rois.size() == 1); REQUIRE(rois[0] == ROI{0, 1024, 0, 512}); } -} \ No newline at end of file +} diff --git a/src/RawSubFile.cpp b/src/RawSubFile.cpp index ed60a4a5..7a42bbba 100644 --- a/src/RawSubFile.cpp +++ b/src/RawSubFile.cpp @@ -12,6 +12,17 @@ namespace aare { +std::runtime_error RawSubFile::frame_error(size_t frame_index, + const std::string &message) const { + return std::runtime_error( + fmt::format("Error reading frame index {} from file '{}': {}", + frame_index, current_path().string(), message)); +} + +std::filesystem::path RawSubFile::current_path() const { + return fpath(m_current_file_index + m_offset); +} + RawSubFile::RawSubFile(const std::filesystem::path &fname, DetectorType detector, size_t rows, size_t cols, size_t bitdepth, uint32_t pos_row, uint32_t pos_col) @@ -34,8 +45,9 @@ RawSubFile::RawSubFile(const std::filesystem::path &fname, void RawSubFile::seek(size_t frame_index) { LOG(logDEBUG) << "RawSubFile::seek(" << frame_index << ")"; if (frame_index >= m_total_frames) { - throw std::runtime_error(LOCATION + " Frame index out of range: " + - std::to_string(frame_index)); + throw frame_error( + frame_index, + fmt::format("out of range (available frames: {})", m_total_frames)); } m_current_frame_index = frame_index; auto file_index = first_larger(m_last_frame_in_file, frame_index); @@ -49,6 +61,9 @@ void RawSubFile::seek(size_t frame_index) { auto byte_offset = frame_offset * (m_bytes_per_frame + sizeof(DetectorHeader)); m_file.seekg(byte_offset); + if (m_file.fail()) { + throw frame_error(frame_index, ifstream_error_msg(m_file)); + } } size_t RawSubFile::tell() { @@ -66,7 +81,7 @@ void RawSubFile::read_into(std::byte *image_buf, DetectorHeader *header) { } if (m_file.fail()) { - throw std::runtime_error(LOCATION + ifstream_error_msg(m_file)); + throw frame_error(m_current_frame_index, ifstream_error_msg(m_file)); } // TODO! expand support for different bitdepths @@ -81,8 +96,8 @@ void RawSubFile::read_into(std::byte *image_buf, DetectorHeader *header) { } else if (m_bitdepth == 32) { read_with_map(image_buf); } else { - throw std::runtime_error( - "Unsupported bitdepth for read with pixel map"); + throw frame_error(m_current_frame_index, + "Unsupported bitdepth for read with pixel map"); } } else { @@ -91,7 +106,7 @@ void RawSubFile::read_into(std::byte *image_buf, DetectorHeader *header) { } if (m_file.fail()) { - throw std::runtime_error(LOCATION + ifstream_error_msg(m_file)); + throw frame_error(m_current_frame_index, ifstream_error_msg(m_file)); } ++m_current_frame_index; @@ -135,6 +150,9 @@ size_t RawSubFile::frame_number(size_t frame_index) { seek(frame_index); DetectorHeader h{}; m_file.read(reinterpret_cast(&h), sizeof(DetectorHeader)); + if (m_file.fail()) { + throw frame_error(frame_index, ifstream_error_msg(m_file)); + } return h.frameNumber; } @@ -178,8 +196,8 @@ void RawSubFile::open_file(size_t file_index) { m_file.open(fname, std::ios::binary); if (!m_file.is_open()) { throw std::runtime_error( - LOCATION + - fmt::format("Could not open file {}", fpath(file_index).string())); + fmt::format("Could not open file '{}' for frame index {}", + fname.string(), m_current_frame_index)); } m_current_file_index = file_index; } @@ -208,4 +226,4 @@ void RawSubFile::scan_files() { } } -} // namespace aare \ No newline at end of file +} // namespace aare diff --git a/src/RawSubFile.test.cpp b/src/RawSubFile.test.cpp index 9550ed17..e09dff7e 100644 --- a/src/RawSubFile.test.cpp +++ b/src/RawSubFile.test.cpp @@ -2,11 +2,70 @@ #include "aare/RawSubFile.hpp" #include "aare/File.hpp" #include "aare/NDArray.hpp" +#include "raw_file_helpers.hpp" #include "test_config.hpp" #include +#include +#include using namespace aare; +TEST_CASE("RawSubFile errors identify the frame and actual data path", + "[RawSubFile][read-errors]") { + TemporaryRawFiles files; + const size_t offset = GENERATE(0, 5); + if (offset != 0) { + for (size_t index = 0; index < 2; ++index) { + std::filesystem::rename(files.data_path(0, index), + files.data_path(0, offset + index)); + } + } + RawSubFile file(files.data_path(0, offset), DetectorType::Jungfrau, 2, 3, + 16); + CHECK(file.current_path() == files.data_path(0, offset)); + std::vector buffer(file.bytes_per_frame() * 3); + DetectorHeader header{}; + const auto second_path = files.data_path(0, offset + 1); + const auto index_matcher = + Catch::Matchers::ContainsSubstring("frame index 1"); + const auto path_matcher = + Catch::Matchers::ContainsSubstring(second_path.string()); + const auto context = index_matcher && path_matcher; + + SECTION("truncated header") { + std::filesystem::resize_file(second_path, 0); + file.seek(1); + REQUIRE_THROWS_WITH(file.read_into(buffer.data(), &header), context); + } + SECTION("truncated pixels") { + std::filesystem::resize_file(second_path, sizeof(DetectorHeader)); + file.seek(1); + CHECK(file.current_path() == second_path); + REQUIRE_THROWS_WITH(file.read_into(buffer.data()), context); + } + SECTION("frame number from truncated header") { + std::filesystem::resize_file(second_path, 0); + REQUIRE_THROWS_WITH(file.frame_number(1), context); + } + SECTION("missing next file") { + std::filesystem::remove(second_path); + REQUIRE_THROWS_WITH(file.seek(1), context); + } + SECTION("batch beyond EOF") { + REQUIRE_THROWS_WITH( + file.read_into(buffer.data(), size_t{3}), + Catch::Matchers::ContainsSubstring("frame index 2") && + Catch::Matchers::ContainsSubstring(second_path.string())); + } + SECTION("seek beyond EOF") { + REQUIRE_THROWS_WITH( + file.seek(17), + Catch::Matchers::ContainsSubstring("frame index 17") && + Catch::Matchers::ContainsSubstring( + files.data_path(0, offset).string())); + } +} + TEST_CASE("Read frames directly from a RawSubFile", "[.with-data]") { auto fpath_raw = test_data_path() / "raw/jungfrau" / "jungfrau_single_d0_f0_0.raw"; @@ -77,4 +136,4 @@ TEST_CASE("Read frames directly from a RawSubFile starting at the second file", auto npy_frame = npy.read_frame(); CHECK((image.view() == npy_frame.view())); } -} \ No newline at end of file +} diff --git a/tests/raw_file_helpers.hpp b/tests/raw_file_helpers.hpp new file mode 100644 index 00000000..b1a9b17c --- /dev/null +++ b/tests/raw_file_helpers.hpp @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MPL-2.0 +#pragma once + +#include "aare/defs.hpp" + +#include +#include +#include +#include +#include + +class TemporaryRawFiles { + std::filesystem::path m_directory; + + public: + explicit TemporaryRawFiles(size_t modules = 1) { + 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 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}}}, + {"Max Frames Per File", 1}, + {"Total Frames", 2}, + {"Frames in File", 2}, + {"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::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}; + output.write(reinterpret_cast(&header), + sizeof(header)); + output.write(reinterpret_cast(pixels), + sizeof(pixels)); + } + } + } + + TemporaryRawFiles(const TemporaryRawFiles &) = delete; + TemporaryRawFiles &operator=(const TemporaryRawFiles &) = delete; + ~TemporaryRawFiles() { std::filesystem::remove_all(m_directory); } + + std::filesystem::path master_path() const { + return m_directory / "run_master_0.json"; + } + + std::filesystem::path data_path(size_t module = 0, size_t file = 0) const { + return m_directory / ("run_d" + std::to_string(module) + "_f" + + std::to_string(file) + "_0.raw"); + } +};