// SPDX-License-Identifier: MPL-2.0 #pragma once #include "aare/CalculateEta.hpp" #include "aare/ClusterFile.hpp" #include "aare/defs.hpp" #include #include #include #include #include #include #include #include #include namespace py = pybind11; using namespace ::aare; template class ClusterFileIterator { Range m_range; std::optional m_iterator; bool m_done{false}; public: explicit ClusterFileIterator(Range range) : m_range(std::move(range)) {} typename Range::Iterator::value_type next() { if (m_done) { throw py::stop_iteration(); } // Advance before returning the next result, never after yielding it. if (m_iterator) { ++*m_iterator; } else { m_iterator.emplace(m_range.begin()); } if (*m_iterator == m_range.end()) { m_done = true; throw py::stop_iteration(); } return std::move(**m_iterator); } }; template void define_cluster_file_iterator(py::module &m, const std::string &name) { py::class_(m, name.c_str()) .def( "__iter__", [](Iterator &self) -> Iterator & { return self; }, py::return_value_policy::reference_internal) .def("__next__", &Iterator::next); } template void define_ClusterFile(py::module &m, const std::string &typestr) { using ClusterType = Cluster; using File = ClusterFile; using FrameIterator = ClusterFileIterator; using ChunkIterator = ClusterFileIterator; auto class_name = fmt::format("ClusterFile_{}", typestr); define_cluster_file_iterator(m, class_name + "_FrameIterator"); define_cluster_file_iterator(m, class_name + "_ChunkIterator"); py::class_>( m, class_name.c_str(), "Read and write legacy binary cluster files. The format contains no " "cluster type or shape metadata, so this class must match the file.") .def(py::init(), py::arg("fname"), py::arg("chunk_size") = 1000, py::arg("mode") = "r", "Open a cluster file. Mode must be 'r' to read, 'w' to truncate " "and write, or 'a' to append.") .def( "frames", [](File &self) { return FrameIterator(self.frames()); }, py::keep_alive<0, 1>(), "Iterate over complete frames from the current position, including " "empty and fully filtered frames with their stored frame numbers. " "Results own their storage. The iterator keeps the file alive but " "shares its cursor; use only one traversal at a time.") .def( "chunks", [](File &self) { return ChunkIterator(self.chunks()); }, py::keep_alive<0, 1>(), "Iterate from the current position using the constructor's chunk " "size, which must be positive. Results own their storage. The " "iterator keeps the file alive but shares its cursor; use only " "one traversal at a time.") .def( "chunks", [](File &self, size_t chunk_size) { return ChunkIterator(self.chunks(chunk_size)); }, py::arg("chunk_size"), py::keep_alive<0, 1>(), "Iterate over chunks of up to chunk_size selected clusters. The " "size must be positive and does not change the constructor's " "default. Chunks may span frames; their frame numbers are not " "per-cluster metadata. Only the final chunk can be short.") .def( "read_clusters", [](ClusterFile &self, size_t n_clusters) { auto v = new ClusterVector( self.read_clusters(n_clusters)); return v; }, py::return_value_policy::take_ownership, py::arg("n_clusters"), "Read up to n_clusters without preserving frame boundaries. The " "result may combine frames, so its frame number is not reliable " "per-cluster metadata.") .def( "read_frame", [](ClusterFile &self) -> py::object { auto clusters = self.read_frame(); if (!clusters) { return py::none(); } return py::cast( new ClusterVector(std::move(*clusters)), py::return_value_policy::take_ownership); }, "Read and return the next complete frame with its frame number, " "or None at end of file.") .def("set_roi", &ClusterFile::set_roi, py::arg("roi"), "Select clusters whose centers lie within the half-open ROI.") .def("tell", &ClusterFile::tell, "Return the current byte position in the file.") .def("estimate_n_clusters", &ClusterFile::estimate_n_clusters, "Estimate the number of clusters from the file size. Frame " "headers can make this larger than the actual count.") .def( "set_noise_map", // conversions ok [](ClusterFile &self, py::array_t noise_map) { auto view = make_view_2d(noise_map); self.set_noise_map(view); }, py::arg("noise_map"), "Set a two-dimensional, C-contiguous int32 noise map indexed as " "[y, x]. The map must cover every cluster center coordinate.") .def( "set_gain_map", // conversions ok [](ClusterFile &self, py::array_t gain_map) { auto view = make_view_2d(gain_map); self.set_gain_map(view); }, py::arg("gain_map"), "Set a two-dimensional, C-contiguous float64 gain map in " "ADU/energy, indexed as [y, x]. Clusters whose complete footprint " "extends beyond the map are retained with all data values set to " "zero.") .def("close", &ClusterFile::close, "Close the file. Calling close more than once is safe.") .def("write_frame", &ClusterFile::write_frame, py::arg("clusters"), "Write one ClusterVector, including its frame number.") .def("__enter__", [](ClusterFile &self) { return &self; }) .def("__exit__", [](ClusterFile &self, [[maybe_unused]] const std::optional &exc_type, [[maybe_unused]] const std::optional &exc_value, [[maybe_unused]] const std::optional &traceback) { self.close(); }) .def("__iter__", [](ClusterFile &self) { return &self; }) .def("__next__", [](File &self) { return ChunkIterator(self.chunks()).next(); }); }