From 26d11f74a897f880c8b25595933f91576ee2ee94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Fr=C3=B6jdh?= Date: Tue, 25 Aug 2026 08:51:41 +0200 Subject: [PATCH 1/3] Fix/expand const buffer (#354) Minor API change: - NDView implicitly converts to NDView (as std::span in C++20) - expand4to8bit and expand24to32bit now takes a view over const uint8_t to show that the input is not changes closes #291 --- RELEASE.md | 3 ++- include/aare/NDView.hpp | 11 ++++++++++- include/aare/decode.hpp | 4 ++-- src/NDView.test.cpp | 17 ++++++++++++++++- src/decode.cpp | 4 ++-- src/decode.test.cpp | 37 ++++++++++++++++++++++++++++++++++++- 6 files changed, 68 insertions(+), 8 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 74d799b1..8d647df4 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -11,6 +11,8 @@ - Removed the legacy ``gaus``, ``pol1``, ``scurve``, and ``scurve2`` function evaluators. Model objects are callable and provide the replacement, for example ``Gaussian()(x, par)``. +- ``NDView`` now converts to ``NDView``; + ``expand4to8bit`` and ``expand24to32bit`` accept const input views. ### Bugfixes: - Fixed broken reading of old (pre reordering) Moench03 @@ -165,4 +167,3 @@ dhanya.thattil@psi.ch - diff --git a/include/aare/NDView.hpp b/include/aare/NDView.hpp index 8c2432d4..c507b41f 100644 --- a/include/aare/NDView.hpp +++ b/include/aare/NDView.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include namespace aare { @@ -92,6 +93,12 @@ class NDView : public ArrayExpr, Ndim> { : buffer_(buffer), strides_(c_strides(shape)), shape_(shape), size_(num_elements(shape)) {} + template , int> = 0> + NDView(const NDView &other) noexcept + : buffer_(other.buffer_), strides_(other.strides_), + shape_(other.shape_), size_(other.size_) {} + template std::enable_if_t operator()(Ix... index) { return buffer_[element_offset(strides_, index...)]; @@ -216,6 +223,8 @@ class NDView : public ArrayExpr, Ndim> { } private: + template friend class NDView; + T *buffer_{nullptr}; std::array strides_{}; std::array shape_{}; @@ -263,4 +272,4 @@ template NDView make_view(std::vector &vec) { return NDView(vec.data(), {static_cast(vec.size())}); } -} // namespace aare \ No newline at end of file +} // namespace aare diff --git a/include/aare/decode.hpp b/include/aare/decode.hpp index c3397093..a6efd187 100644 --- a/include/aare/decode.hpp +++ b/include/aare/decode.hpp @@ -35,7 +35,7 @@ uint32_t mask32to24bits(uint32_t input, BitOffset offset = {}); * @param offset Offset within the first byte to where the data starts (0-7 * bits) */ -void expand24to32bit(NDView input, NDView output, +void expand24to32bit(NDView input, NDView output, BitOffset offset = {}); /** @@ -43,7 +43,7 @@ void expand24to32bit(NDView input, NDView output, * @param input input buffer with 4 bit values packed into 8 bit * @param output output buffer with 8 bit values */ -void expand4to8bit(NDView input, NDView output); +void expand4to8bit(NDView input, NDView output); /** * @brief Apply custom weights to a 16-bit input value. Will sum up diff --git a/src/NDView.test.cpp b/src/NDView.test.cpp index b89d0cd5..1a8c6a2b 100644 --- a/src/NDView.test.cpp +++ b/src/NDView.test.cpp @@ -5,12 +5,16 @@ #include #include #include +#include #include using aare::NDView; using aare::num_elements; using aare::Shape; +static_assert(std::is_convertible_v, NDView>); +static_assert(!std::is_convertible_v, NDView>); + TEST_CASE("Calculate size from a shape") { Shape<3> shape{2, 3, 4}; REQUIRE(num_elements(shape) == 24); @@ -88,6 +92,17 @@ TEST_CASE("Element reference 1D with a const NDView") { } } +TEST_CASE("Convert a mutable NDView to a const NDView") { + std::vector vec{1, 2, 3, 4}; + NDView mutable_view(vec.data(), Shape<2>{2, 2}); + + NDView const_view = mutable_view; + + REQUIRE(const_view.data() == mutable_view.data()); + REQUIRE(const_view.shape() == mutable_view.shape()); + REQUIRE(const_view.strides() == mutable_view.strides()); +} + TEST_CASE("Element reference 2D") { std::vector vec(12); std::iota(vec.begin(), vec.end(), 0); @@ -279,4 +294,4 @@ TEST_CASE("NDView over byte") { auto v = aare::make_view(buf); REQUIRE(v.shape()[0] == 5); REQUIRE(v[0] == std::byte{0}); -} \ No newline at end of file +} diff --git a/src/decode.cpp b/src/decode.cpp index 157fed25..afe261f9 100644 --- a/src/decode.cpp +++ b/src/decode.cpp @@ -144,7 +144,7 @@ uint32_t mask32to24bits(uint32_t input, BitOffset offset) { return (input >> offset.value()) & mask24bits; } -void expand4to8bit(NDView input, NDView output) { +void expand4to8bit(NDView input, NDView output) { if (2 * input.size() != output.size()) throw std::runtime_error( @@ -162,7 +162,7 @@ void expand4to8bit(NDView input, NDView output) { } } -void expand24to32bit(NDView input, NDView output, +void expand24to32bit(NDView input, NDView output, BitOffset bit_offset) { ssize_t bytes_per_channel = 3; // 24bit diff --git a/src/decode.test.cpp b/src/decode.test.cpp index d7f3901d..bab7c38b 100644 --- a/src/decode.test.cpp +++ b/src/decode.test.cpp @@ -154,6 +154,20 @@ TEST_CASE("Expand container with 24 bit data to 32") { } } +TEST_CASE("Expand 24 bit values to 32 bit values from a const buffer") { + const uint8_t buffer[] = { + 0x0F, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0xFF, 0xFF, + }; + + aare::NDView input(buffer, {9}); + aare::NDArray out({3}); + aare::expand24to32bit(input, out.view()); + + CHECK(out(0) == 0xF); + CHECK(out(1) == 0xFF); + CHECK(out(2) == 0xFFFFFF); +} + TEST_CASE("Expand 4 bit values packed into 8 bit to 8 bit values") { { uint8_t buffer[] = { @@ -172,4 +186,25 @@ TEST_CASE("Expand 4 bit values packed into 8 bit to 8 bit values") { CHECK(out(i) == expected_output[i]); } } -} \ No newline at end of file +} + +TEST_CASE("Expand 4 bit values packed into 8 bit to 8 bit values from a const " + "buffer") { + { + const uint8_t buffer[] = { + 0x00, 0xF0, 0xFF, 0x00, 0xF0, 0xFF, + }; + + aare::NDView input(&buffer[0], {6}); + aare::NDArray out({12}); + aare::expand4to8bit(input, out.view()); + + uint8_t expected_output[] = { + 0x0, 0x0, 0x0, 0xF, 0xF, 0xF, + 0x0, 0x0, 0x0, 0xF, 0xF, 0xF}; // assuming little endian + + for (size_t i = 0; i < 12; ++i) { + CHECK(out(i) == expected_output[i]); + } + } +} From 85f9d0176a455cc9daa04f5aba441ab4fe815707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Fr=C3=B6jdh?= Date: Tue, 25 Aug 2026 09:15:38 +0200 Subject: [PATCH 2/3] Dev/guess clusters (#355) - exposing ClusterVector::empty in python - added member guess_n_clusters --- RELEASE.md | 2 ++ include/aare/ClusterFile.hpp | 13 +++++++++++++ python/src/bind_ClusterFile.hpp | 4 +++- python/src/bind_ClusterVector.hpp | 3 ++- python/tests/test_ClusterFile.py | 4 +++- python/tests/test_ClusterVector.py | 4 +++- src/ClusterFile.test.cpp | 4 ++++ 7 files changed, 30 insertions(+), 4 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index 8d647df4..638b8b3b 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -4,6 +4,8 @@ ### API Changes: +- Exposed ``ClusterVector.empty()`` in the Python API. +- Added ClusterVector.estimate_n_clusters - Removed the lmfit dependency and the legacy ``fit_gaus``, ``fit_pol1``, ``fit_scurve``, and ``fit_scurve2`` APIs. Use ``Gaussian``, ``Pol1``, ``RisingScurve``, or ``FallingScurve`` and call ``model.fit(...)`` (or diff --git a/include/aare/ClusterFile.hpp b/include/aare/ClusterFile.hpp index 885d4b45..4e416c23 100644 --- a/include/aare/ClusterFile.hpp +++ b/include/aare/ClusterFile.hpp @@ -144,6 +144,19 @@ class ClusterFile { */ size_t chunk_size() const { return m_chunk_size; } + /** + * @brief Estimate the number of clusters in the file from its size + * + * Frame headers are included in the estimate, so the result may be slightly + * larger than the actual number of clusters. The file position is not + * changed. + * + * @throws std::runtime_error if the file is not opened for reading + */ + size_t estimate_n_clusters() const { + return std::filesystem::file_size(m_filename) / sizeof(ClusterType); + } + /** * @brief Set the region of interest to use when reading * clusters. If set only clusters within the ROI will be diff --git a/python/src/bind_ClusterFile.hpp b/python/src/bind_ClusterFile.hpp index fbdd1909..0831d64f 100644 --- a/python/src/bind_ClusterFile.hpp +++ b/python/src/bind_ClusterFile.hpp @@ -47,6 +47,8 @@ void define_ClusterFile(py::module &m, const std::string &typestr) { }) .def("set_roi", &ClusterFile::set_roi, py::arg("roi")) .def("tell", &ClusterFile::tell) + .def("estimate_n_clusters", + &ClusterFile::estimate_n_clusters) .def( "set_noise_map", [](ClusterFile &self, py::array_t noise_map) { @@ -82,4 +84,4 @@ void define_ClusterFile(py::module &m, const std::string &typestr) { }); } -#pragma GCC diagnostic pop \ No newline at end of file +#pragma GCC diagnostic pop diff --git a/python/src/bind_ClusterVector.hpp b/python/src/bind_ClusterVector.hpp index 254f96a9..a3d59f57 100644 --- a/python/src/bind_ClusterVector.hpp +++ b/python/src/bind_ClusterVector.hpp @@ -72,6 +72,7 @@ void define_ClusterVector(py::module &m, const std::string &typestr) { R"(calculates sum of 2x2 subcluster with highest energy and index relative to cluster center 0: top_left, 1: top_right, 2: bottom_left, 3: bottom_right )") .def_property_readonly("size", &ClusterVector::size) + .def("empty", &ClusterVector::empty) .def("item_size", &ClusterVector::item_size) .def_property_readonly("fmt", [typestr](ClusterVector &self) { @@ -169,4 +170,4 @@ void define_3x3_reduction(py::module &m) { py::arg("clustervector")); } -#pragma GCC diagnostic pop \ No newline at end of file +#pragma GCC diagnostic pop diff --git a/python/tests/test_ClusterFile.py b/python/tests/test_ClusterFile.py index 5e032009..3e2fb81d 100644 --- a/python/tests/test_ClusterFile.py +++ b/python/tests/test_ClusterFile.py @@ -14,6 +14,8 @@ from conftest import test_data_path def test_cluster_file(test_data_path): """Test ClusterFile""" f = ClusterFile(test_data_path / "clust/single_frame_97_clustrers.clust") + assert f.estimate_n_clusters() == 97 + assert f.tell() == 0 cv = f.read_clusters(10) #conversion does not work @@ -62,4 +64,4 @@ def test_read_clusters_and_fill_histogram(test_data_path): hist_py = pickle.load(f) #Compare the two histograms - assert hist_aare == hist_py \ No newline at end of file + assert hist_aare == hist_py diff --git a/python/tests/test_ClusterVector.py b/python/tests/test_ClusterVector.py index 0ae96c3e..30f934fa 100644 --- a/python/tests/test_ClusterVector.py +++ b/python/tests/test_ClusterVector.py @@ -16,6 +16,7 @@ def test_create_cluster_vector(): assert cv.cluster_size_x == 3 assert cv.cluster_size_y == 3 assert cv.size == 0 + assert cv.empty() def test_push_back_on_cluster_vector(): @@ -27,6 +28,7 @@ def test_push_back_on_cluster_vector(): cluster = _aare.Cluster2x2i(19, 22, np.ones(4, dtype=np.int32)) cv.push_back(cluster) assert cv.size == 1 + assert not cv.empty() arr = np.array(cv, copy=False) assert arr[0]['x'] == 19 @@ -127,4 +129,4 @@ def test_masking(): assert cv_masked_array[0]["x"] == 1 assert cv_masked_array[0]["y"] == 2 - assert (cv_masked_array[0]["data"] == np.ones((3,3),dtype=np.int32)).all() \ No newline at end of file + assert (cv_masked_array[0]["data"] == np.ones((3,3),dtype=np.int32)).all() diff --git a/src/ClusterFile.test.cpp b/src/ClusterFile.test.cpp index 78ba3e61..15caf6bb 100644 --- a/src/ClusterFile.test.cpp +++ b/src/ClusterFile.test.cpp @@ -17,6 +17,8 @@ TEST_CASE("Read one frame from a cluster file", "[.with-data]") { REQUIRE(std::filesystem::exists(fpath)); ClusterFile> f(fpath); + CHECK(f.estimate_n_clusters() == 97); + CHECK(f.tell() == 0); auto clusters = f.read_frame(); CHECK(clusters.size() == 97); CHECK(clusters.frame_number() == 135); @@ -250,6 +252,8 @@ TEST_CASE("Read cluster from multiple frame file", "[.with-data]") { SECTION("Read clusters from both frames") { ClusterFile f(fpath); + CHECK(f.estimate_n_clusters() == 8); + CHECK(f.tell() == 0); auto clusters = f.read_clusters(2); REQUIRE(clusters.size() == 2); REQUIRE(clusters.frame_number() == 0); From 568f94926b822bf344d16749d6208fd3710ac6a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Fr=C3=B6jdh?= Date: Fri, 28 Aug 2026 09:22:29 +0200 Subject: [PATCH 3/3] baseline AGENTS.md (#356) - AI generated baseline for AGENTS.md - Meant to be edited in the future --- AGENTS.md | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..be6fda68 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,166 @@ +# Repository guidelines + +## Scope and project goals + +These instructions apply to the entire repository unless a more specific +`AGENTS.md` exists in a subdirectory. + +Aare is a data-analysis library for PSI hybrid detectors. The C++17 core is +the canonical implementation, while Python is the main user-facing interface +through pybind11. Changes should preserve the project's priorities: fast and +efficient processing, simple interfaces, API stability, and a small dependency +footprint. + +## Repository layout + +- `include/aare/` contains the public C++ API and header-defined templates. +- `src/` contains C++ implementations and colocated Catch2 tests named + `*.test.cpp`. +- `tests/` contains the C++ test executable, configuration, and shared helpers. +- `python/src/` contains pybind11 bindings and module registration. +- `python/aare/` contains Python facades, convenience APIs, and public exports. +- `python/tests/` contains the pytest suite. +- `docs/src/` contains Sphinx/reStructuredText documentation. Doxygen and + Breathe integrate the C++ API into the Sphinx site. +- `benchmarks/` contains Google Benchmark programs. +- `cmake/`, `conda-recipe/`, and `pyproject.toml` support builds and packaging. +- `RELEASE.md` contains pending and published release notes. + +## Build environment + +The project requires CMake 3.15 or newer, C++17 with compiler extensions +disabled, and Python 3.11 or newer. `etc/dev-env.yml` defines the Conda +development environment. + +CMake fetches several dependencies by default. Use +`-DAARE_SYSTEM_LIBRARIES=ON` only when all required system or Conda packages +are available. + +Use this configuration for normal development: + +```bash +cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Debug \ + -DAARE_TESTS=ON \ + -DAARE_PYTHON_BINDINGS=ON +cmake --build build -j4 +``` + +Useful optional settings include `AARE_DOCS`, `AARE_BENCHMARKS`, `AARE_ASAN`, +and `AARE_WARNINGS_AS_ERRORS`. Reconfigure an existing build directory instead +of creating alternate in-tree build layouts unless isolation is needed. + +## Implementation conventions + +- Put public declarations in `include/aare/` and private implementation details + in `src/`. +- Add new compiled headers, sources, and C++ tests to the explicit lists in the + root `CMakeLists.txt`. +- Keep template implementations in headers unless the supported types are + explicitly instantiated. +- Follow the naming in adjacent code. Broadly, use the `aare` namespace, + CamelCase types, and snake_case functions. +- Treat ownership, lifetime, const-correctness, array shapes, and buffer + contiguity as part of the API when working with `NDArray`, `NDView`, or NumPy + bindings. +- Avoid unnecessary allocations and copies in detector-data and per-pixel + processing paths. Add or update a benchmark when performance is central to a + change. +- Place APIs that may change without notice under the existing experimental + namespace/module. +- Start new source files with `SPDX-License-Identifier: MPL-2.0`, using the + appropriate comment syntax. +- Prefer descriptive names to comments that only restate the code. +- Do not add a dependency unless the benefit justifies the packaging and + deployment cost. + +## Python-facing changes + +A Python-facing feature can require coordinated changes in several layers: + +1. Update the C++ public API and implementation. +2. Add or update its binding in `python/src/`. +3. Register new bindings in `python/src/module.cpp`. +4. Update the facade or public exports in `python/aare/`. +5. If adding a Python module, add it to `PYTHON_FILES` in + `python/CMakeLists.txt` so it is copied and installed. +6. Add Python tests and update user documentation. + +Bindings must validate NumPy dimensions and data types before constructing +views. Do not return a view whose backing C++ or Python storage can expire while +the view remains reachable. + +## Tests + +Run tests that do not require external detector data with: + +```bash +ctest --test-dir build --output-on-failure -j4 +PYTHONPATH="$PWD/build" python -m pytest python/tests +``` + +For focused runs, use a Catch2 tag or an individual pytest file/test: + +```bash +build/run_tests "[tag]" +PYTHONPATH="$PWD/build" python -m pytest python/tests/test_example.py +``` + +Large detector test files live outside this repository. To include data-backed +tests, set `AARE_TEST_DATA` and opt in explicitly: + +```bash +export AARE_TEST_DATA=/path/to/aare-test-data +build/run_tests "[.with-data]" +PYTHONPATH="$PWD/build" python -m pytest python/tests --with-data +``` + +- Start bug fixes with a failing regression test when practical. +- Put C++ tests beside the relevant implementation as `src/Thing.test.cpp`. +- Mark C++ tests requiring external files with `[.with-data]`. +- Mark Python tests requiring external files with `@pytest.mark.withdata`. +- Run both suites for changes that cross the C++/Python boundary. +- Do not silently skip required data-backed coverage. Report when the external + test data is unavailable. + +## Formatting and static analysis + +Before handing off a broad change, run the relevant checks: + +```bash +pre-commit run --all-files +cmake --build build --target check-format +cmake --build build --target clang-tidy +``` + +C++ formatting follows `.clang-format` (four-space indentation and an 80-column +limit). CMake files are checked by `cmake-format`. Avoid formatting unrelated +code as part of a focused change. + +## Documentation and release notes + +- Update the relevant `.rst` pages for public behavior or API changes. +- Add new pages to `docs/src/index.rst` or the relevant nested toctree. +- Update `RELEASE.md` under `## Next` for user-visible features, bug fixes, and + API changes. +- Do not change `VERSION` unless performing an explicitly requested release. +- Preserve compatibility with existing detector formats and older recorded + files where practical. Call out intentional API or format incompatibilities. + +Build the documentation with: + +```bash +cmake -S . -B build \ + -DAARE_DOCS=ON \ + -DAARE_PYTHON_BINDINGS=ON +cmake --build build --target docs +``` + +## Working practices and handoff + +- Inspect the adjacent implementation, tests, and documentation before editing. +- Keep changes focused and preserve unrelated modifications in the worktree. +- Do not edit generated files or fetched dependency sources under `build/`. +- For large features, prefer independently testable increments. +- At handoff, summarize the behavior and important files changed, checks run, + checks not run and why, and any compatibility or performance considerations.