Merge branch 'main' into dev/filereading_for_disabled_udp
Build on RHEL9 / build (push) Successful in 2m52s
Build on RHEL8 / build (push) Successful in 3m16s
Run tests using data on local RHEL8 / build (push) Successful in 4m0s

This commit is contained in:
Erik Fröjdh
2026-08-28 10:46:41 +02:00
committed by GitHub
13 changed files with 264 additions and 12 deletions
+166
View File
@@ -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.
+4 -1
View File
@@ -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
@@ -11,6 +13,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<T, Ndim>`` now converts to ``NDView<const T, Ndim>``;
``expand4to8bit`` and ``expand24to32bit`` accept const input views.
### Bugfixes:
- Fixed broken reading of old (pre reordering) Moench03
@@ -166,4 +170,3 @@ dhanya.thattil@psi.ch
+13
View File
@@ -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
+10 -1
View File
@@ -12,6 +12,7 @@
#include <iostream>
#include <numeric>
#include <stdexcept>
#include <type_traits>
#include <vector>
namespace aare {
@@ -92,6 +93,12 @@ class NDView : public ArrayExpr<NDView<T, Ndim>, Ndim> {
: buffer_(buffer), strides_(c_strides<Ndim>(shape)), shape_(shape),
size_(num_elements(shape)) {}
template <typename U, std::enable_if_t<
std::is_convertible_v<U (*)[], T (*)[]>, int> = 0>
NDView(const NDView<U, Ndim> &other) noexcept
: buffer_(other.buffer_), strides_(other.strides_),
shape_(other.shape_), size_(other.size_) {}
template <typename... Ix>
std::enable_if_t<sizeof...(Ix) == Ndim, T &> operator()(Ix... index) {
return buffer_[element_offset(strides_, index...)];
@@ -216,6 +223,8 @@ class NDView : public ArrayExpr<NDView<T, Ndim>, Ndim> {
}
private:
template <typename, ssize_t> friend class NDView;
T *buffer_{nullptr};
std::array<ssize_t, Ndim> strides_{};
std::array<ssize_t, Ndim> shape_{};
@@ -263,4 +272,4 @@ template <typename T> NDView<T, 1> make_view(std::vector<T> &vec) {
return NDView<T, 1>(vec.data(), {static_cast<ssize_t>(vec.size())});
}
} // namespace aare
} // namespace aare
+2 -2
View File
@@ -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<uint8_t, 1> input, NDView<uint32_t, 1> output,
void expand24to32bit(NDView<const uint8_t, 1> input, NDView<uint32_t, 1> output,
BitOffset offset = {});
/**
@@ -43,7 +43,7 @@ void expand24to32bit(NDView<uint8_t, 1> input, NDView<uint32_t, 1> output,
* @param input input buffer with 4 bit values packed into 8 bit
* @param output output buffer with 8 bit values
*/
void expand4to8bit(NDView<uint8_t, 1> input, NDView<uint8_t, 1> output);
void expand4to8bit(NDView<const uint8_t, 1> input, NDView<uint8_t, 1> output);
/**
* @brief Apply custom weights to a 16-bit input value. Will sum up
+3 -1
View File
@@ -47,6 +47,8 @@ void define_ClusterFile(py::module &m, const std::string &typestr) {
})
.def("set_roi", &ClusterFile<ClusterType>::set_roi, py::arg("roi"))
.def("tell", &ClusterFile<ClusterType>::tell)
.def("estimate_n_clusters",
&ClusterFile<ClusterType>::estimate_n_clusters)
.def(
"set_noise_map",
[](ClusterFile<ClusterType> &self, py::array_t<int32_t> noise_map) {
@@ -82,4 +84,4 @@ void define_ClusterFile(py::module &m, const std::string &typestr) {
});
}
#pragma GCC diagnostic pop
#pragma GCC diagnostic pop
+2 -1
View File
@@ -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<ClusterType>::size)
.def("empty", &ClusterVector<ClusterType>::empty)
.def("item_size", &ClusterVector<ClusterType>::item_size)
.def_property_readonly("fmt",
[typestr](ClusterVector<ClusterType> &self) {
@@ -169,4 +170,4 @@ void define_3x3_reduction(py::module &m) {
py::arg("clustervector"));
}
#pragma GCC diagnostic pop
#pragma GCC diagnostic pop
+3 -1
View File
@@ -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
assert hist_aare == hist_py
+3 -1
View File
@@ -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()
assert (cv_masked_array[0]["data"] == np.ones((3,3),dtype=np.int32)).all()
+4
View File
@@ -17,6 +17,8 @@ TEST_CASE("Read one frame from a cluster file", "[.with-data]") {
REQUIRE(std::filesystem::exists(fpath));
ClusterFile<Cluster<int32_t, 3, 3>> 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<ClusterType> 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);
+16 -1
View File
@@ -5,12 +5,16 @@
#include <cstddef>
#include <iostream>
#include <numeric>
#include <type_traits>
#include <vector>
using aare::NDView;
using aare::num_elements;
using aare::Shape;
static_assert(std::is_convertible_v<NDView<int, 2>, NDView<const int, 2>>);
static_assert(!std::is_convertible_v<NDView<const int, 2>, NDView<int, 2>>);
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<int> vec{1, 2, 3, 4};
NDView<int, 2> mutable_view(vec.data(), Shape<2>{2, 2});
NDView<const int, 2> 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<int> 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});
}
}
+2 -2
View File
@@ -144,7 +144,7 @@ uint32_t mask32to24bits(uint32_t input, BitOffset offset) {
return (input >> offset.value()) & mask24bits;
}
void expand4to8bit(NDView<uint8_t, 1> input, NDView<uint8_t, 1> output) {
void expand4to8bit(NDView<const uint8_t, 1> input, NDView<uint8_t, 1> output) {
if (2 * input.size() != output.size())
throw std::runtime_error(
@@ -162,7 +162,7 @@ void expand4to8bit(NDView<uint8_t, 1> input, NDView<uint8_t, 1> output) {
}
}
void expand24to32bit(NDView<uint8_t, 1> input, NDView<uint32_t, 1> output,
void expand24to32bit(NDView<const uint8_t, 1> input, NDView<uint32_t, 1> output,
BitOffset bit_offset) {
ssize_t bytes_per_channel = 3; // 24bit
+36 -1
View File
@@ -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<const uint8_t, 1> input(buffer, {9});
aare::NDArray<uint32_t, 1> 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]);
}
}
}
}
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<const uint8_t, 1> input(&buffer[0], {6});
aare::NDArray<uint8_t, 1> 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]);
}
}
}