Merge 'origin/main' into feature/cuda_clusterfinder

This commit is contained in:
kferjaoui
2026-07-21 15:21:49 +02:00
64 changed files with 4365 additions and 542 deletions
+8 -1
View File
@@ -29,7 +29,7 @@ pybind11_add_module(_aare NO_EXTRAS src/module.cpp)
target_link_libraries(_aare PRIVATE aare_core aare_compiler_flags)
target_include_directories(
_aare SYSTEM
PRIVATE $<TARGET_PROPERTY:Minuit2::Minuit2,INTERFACE_INCLUDE_DIRECTORIES>)
PRIVATE $<TARGET_PROPERTY:aare::Minuit2,INTERFACE_INCLUDE_DIRECTORIES>)
set_target_properties(
_aare PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/aare
@@ -63,6 +63,7 @@ endif()
# List of python files to be copied to the build directory
set(PYTHON_FILES
aare/__init__.py
aare/_version.py
aare/CtbRawFile.py
aare/ClusterFinder.py
aare/ClusterVector.py
@@ -90,6 +91,8 @@ foreach(FILE ${PYTHON_EXAMPLES})
message(STATUS "Copying ${FILE} to ${CMAKE_BINARY_DIR}/${FILE}")
endforeach(FILE ${PYTHON_EXAMPLES})
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/../VERSION
${CMAKE_BINARY_DIR}/aare/VERSION)
if(AARE_INSTALL_PYTHONEXT)
set(AARE_PY_INSTALL_TARGETS _aare)
if(AARE_CUDA)
@@ -105,4 +108,8 @@ if(AARE_INSTALL_PYTHONEXT)
FILES ${PYTHON_FILES}
DESTINATION aare
COMPONENT python)
install(
FILES ../VERSION
DESTINATION aare
COMPONENT python)
endif()
+15 -2
View File
@@ -30,12 +30,13 @@ from ._aare import corner
# from ._aare import ClusterFinderMT, ClusterCollector, ClusterFileSink, ClusterVector_i
from ._version import __version__
from .ClusterFinder import ClusterFinder, ClusterCollector, ClusterFinderMT, ClusterFileSink, ClusterFile
from .ClusterFinder import ClusterFinderCUDA, _cuda_available
from .ClusterVector import ClusterVector
from .Cluster import Cluster
from ._aare import Gaussian, RisingScurve, FallingScurve, Pol1, Pol2
from ._aare import Gaussian, RisingScurve, FallingScurve, Pol1, Pol2, GaussianErfcPlateau, GaussianChargeSharing, GaussianChargeSharingKb
from ._aare import fit
from ._aare import fit_gaus, fit_pol1, fit_scurve, fit_scurve2
from ._aare import Interpolator
@@ -44,11 +45,13 @@ from ._aare import reduce_to_2x2, reduce_to_3x3
from ._aare import apply_custom_weights
from ._aare import Etai, Etad, Etaf
from .CtbRawFile import CtbRawFile
from .RawFile import RawFile
from .ScanParameters import ScanParameters
from .utils import random_pixels, random_pixel, flat_list, add_colorbar
from .utils import random_pixels, random_pixel, flat_list, add_colorbar, Timer
#make functions available in the top level API
@@ -59,3 +62,13 @@ from ._aare import apply_calibration, count_switching_pixels
from ._aare import calculate_pedestal, calculate_pedestal_float, calculate_pedestal_g0, calculate_pedestal_g0_float
from ._aare import VarClusterFinder
from ._aare import (
PedestalTrackingPixelHistogram,
PixelHistogram,
PixelHistogram_d,
PixelHistogram_f,
PixelHistogram_u8,
PixelHistogram_u16,
PixelHistogram_u32,
PixelHistogram_u64,
)
+8
View File
@@ -0,0 +1,8 @@
# This file is used to get the version of the package from the VERSION file
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
try:
__version__ = version('aare')
except PackageNotFoundError:
__version__ = Path(__file__).parent.joinpath('VERSION').read_text().strip()
+17 -4
View File
@@ -17,6 +17,15 @@ class AdcSar05060708Transform64to16:
return _aare.adc_sar_05_06_07_08decode64to16(data)
class Moench05Transform:
"""
Transforms Moench05 chip data from a buffer of bytes (uint8_t)
to a numpy array of uint16. Assumes data taken with analog samples and assumes adc 1, 9, 13 are enabled.
(e.g. for 10g mode adc 0,1,2,3 and 8,9,10,11 and 12,13,14,15 are enabled but only adc 1,9,13 contain relevant data)
.. note::
A moench05 chip has 160 rows and 50 cols per adc and has dynamic range 16 bit. Each adc sample is encoded in 16 bits.
The transformation thus requires 160*50*16/16 = 8000 analog samples per adc.
"""
#Could be moved to C++ without changing the interface
def __init__(self):
self.pixel_map = _aare.GenerateMoench05PixelMap()
@@ -81,6 +90,10 @@ class Matterhorn10Transform:
A matterhorn chip has 256 columns and 256 rows.
A matterhornchip with dynamic range 16 and 2 counters thus requires
256*256*16*2/(2*64) = 1024 transceiver samples. (Per default 2 channels are enabled per transceiver sample, each channel storing 64 bits)
.. note::
Due to an artefact in the chip, the transformation only fully supports 2 or 4 counters. Also if you enable 2 counters you can only select counter 1 and 2 or 0, 3 to get reasonable results.
Otherwise only the first half of the image is correct.
"""
def __init__(self, dynamic_range : int, num_counters : int):
self.pixel_map = _aare.GenerateMatterhorn10PixelMap(dynamic_range, num_counters)
@@ -105,7 +118,7 @@ class Matterhorn10Transform:
checks if data is compatible for transformation
:param data: data to be transformed, expected to be a 1D numpy array of uint8
:type data: np.ndarray
:type data: np.ndarray(n_counters, n_rows, n_cols)
:raises ValueError: if not compatible
"""
expected_size = (Matterhorn10.nRows*Matterhorn10.nCols*self.num_counters*self.dynamic_range)//8 # read_frame returns data in uint8_t
@@ -118,11 +131,11 @@ class Matterhorn10Transform:
def __call__(self, data):
self.data_compatibility(data)
if self.dynamic_range == 16:
return np.take(data.view(np.uint16), self.pixel_map)
return np.take(data.view(np.uint16), self.pixel_map).reshape(self.num_counters, Matterhorn10.nRows, Matterhorn10.nCols)
elif self.dynamic_range == 8:
return np.take(data.view(np.uint8), self.pixel_map)
return np.take(data.view(np.uint8), self.pixel_map).reshape(self.num_counters, Matterhorn10.nRows, Matterhorn10.nCols)
else: #dynamic range 4
return np.take(_aare.expand4to8bit(data.view(np.uint8)), self.pixel_map)
return np.take(_aare.expand4to8bit(data.view(np.uint8)), self.pixel_map).reshape(self.num_counters, Matterhorn10.nRows, Matterhorn10.nCols)
class Mythen302Transform:
"""
+16 -1
View File
@@ -2,6 +2,7 @@
import numpy as np
import matplotlib.pyplot as plt
from mpl_toolkits.axes_grid1 import make_axes_locatable
import time
def random_pixels(n_pixels, xmin=0, xmax=512, ymin=0, ymax=1024):
"""Return a list of random pixels.
@@ -34,4 +35,18 @@ def add_colorbar(ax, im, size="5%", pad=0.05):
divider = make_axes_locatable(ax)
cax = divider.append_axes("right", size=size, pad=pad)
plt.colorbar(im, cax=cax)
return ax, im, cax
return ax, im, cax
class Timer:
def __init__(self, label="Elapsed time:", verbose=True):
self.label = label
self.verbose = verbose
def __enter__(self):
self.start = time.perf_counter()
return self
def __exit__(self, exc_type, exc, tb):
self.elapsed = time.perf_counter() - self.start
if self.verbose:
print(f"{self.label} {self.elapsed:.3f}s")
+5
View File
@@ -22,4 +22,9 @@ void define_defs_bindings(py::module &m) {
moench04.attr("nPixelsPerSuperColumn") = Moench04::nPixelsPerSuperColumn;
moench04.attr("superColumnWidth") = Moench04::superColumnWidth;
moench04.attr("adcNumbers") = Moench04::adcNumbers;
auto moench05 = py::class_<Moench05>(m, "Moench05");
moench05.attr("nRows") = Moench05::nRows;
moench05.attr("nCols") = Moench05::nCols;
moench05.attr("adcNumbers") = Moench05::adcNumbers;
}
+6 -6
View File
@@ -13,12 +13,12 @@ void define_eta(py::module &m, const std::string &typestr) {
py::class_<Eta2<T>>(m, class_name.c_str())
.def(py::init<>())
.def_readonly("x", &Eta2<T>::x, "eta x value")
.def_readonly("y", &Eta2<T>::y, "eta y value")
.def_readonly("c", &Eta2<T>::c,
"eta corner value cTopLeft, cTopRight, "
"cBottomLeft, cBottomRight")
.def_readonly("sum", &Eta2<T>::sum, "photon energy of cluster");
.def_readwrite("x", &Eta2<T>::x, "eta x value")
.def_readwrite("y", &Eta2<T>::y, "eta y value")
.def_readwrite("c", &Eta2<T>::c,
"eta corner value cTopLeft, cTopRight, "
"cBottomLeft, cBottomRight")
.def_readwrite("sum", &Eta2<T>::sum, "photon energy of cluster");
}
void define_corner_enum(py::module &m) {
+34 -1
View File
@@ -11,17 +11,20 @@
namespace py = pybind11;
// clang-format off
#define REGISTER_INTERPOLATOR_ETA2(T, N, M, U) \
register_interpolate<T, N, M, U, aare::calculate_full_eta2<T, N, M, U>>( \
interpolator, "_full_eta2", "full eta2"); \
register_interpolate<T, N, M, U, aare::calculate_eta2<T, N, M, U>>( \
interpolator, "", "eta2");
interpolator, "", "eta2"); \
register_interpolate_custom_eta<T, N, M, U>(interpolator);
#define REGISTER_INTERPOLATOR_ETA3(T, N, M, U) \
register_interpolate<T, N, M, U, aare::calculate_eta3<T, N, M, U>>( \
interpolator, "_eta3", "full eta3"); \
register_interpolate<T, N, M, U, aare::calculate_cross_eta3<T, N, M, U>>( \
interpolator, "_cross_eta3", "cross eta3");
// clang-format on
template <typename Type, uint8_t CoordSizeX, uint8_t CoordSizeY,
typename CoordType = uint16_t, auto EtaFunction>
@@ -48,6 +51,34 @@ void register_interpolate(py::class_<aare::Interpolator> &interpolator,
docstring.c_str(), py::arg("cluster_vector"));
}
template <typename Type, uint8_t ClusterSizeX, uint8_t ClusterSizeY,
typename CoordType = uint16_t>
void register_interpolate_custom_eta(
py::class_<aare::Interpolator> &interpolator) {
using ClusterType = Cluster<Type, ClusterSizeX, ClusterSizeY, CoordType>;
interpolator.def(
"interpolate",
[](aare::Interpolator &self, const ClusterVector<ClusterType> &clusters,
const std::vector<Eta2<Type>> &etas) {
auto photons = self.interpolate<Type, ClusterSizeX, ClusterSizeY, CoordType>(clusters, etas);
auto *ptr = new std::vector<Photon>{std::move(photons)};
return return_vector(ptr);
},
R"(
Interpolation based on custom eta values provided by the user.
Args:
cluster_vector: vector of clusters to interpolate
etas: vector of eta values for each cluster (must be in the same order as the clusters)
Returns:
interpolated photons
)",
py::arg("cluster_vector"), py::arg("etas"));
}
template <typename Type>
void register_transform_eta_values(
py::class_<aare::Interpolator> &interpolator) {
@@ -65,6 +96,8 @@ void define_interpolation_bindings(py::module &m) {
PYBIND11_NUMPY_DTYPE(aare::Photon, x, y, energy);
PYBIND11_NUMPY_DTYPE(aare::Coordinate2D, x, y);
auto interpolator =
py::class_<aare::Interpolator>(m, "Interpolator")
.def(py::init(
@@ -0,0 +1,245 @@
// SPDX-License-Identifier: MPL-2.0
#include "aare/hist/PedestalTrackingPixelHistogram.hpp"
#include "np_helper.hpp"
#include <cstdint>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
using namespace ::aare;
void define_pedestal_tracking_pixel_histogram_bindings(py::module &m) {
py::class_<PedestalTrackingPixelHistogram>(
m, "PedestalTrackingPixelHistogram",
"A pixel-wise histogram of frame - pedestal residuals, with a "
"per-pixel running pedestal estimate sharded across worker threads")
.def(
py::init<int, int, int, double, double, int, std::size_t, double>(),
R"(
Initialize a PedestalTrackingPixelHistogram.
Args:
rows: Number of rows in the detector
cols: Number of columns in the detector
n_bins: Number of histogram bins along the residual axis
xmin: Minimum residual value (inclusive)
xmax: Maximum residual value (exclusive)
n_threads: Number of worker threads (default: 1). Each
worker owns a disjoint row slice of both the
pedestal and the histogram, so the partition
determines per-thread memory usage.
max_pending: Maximum number of frames that can be
queued for asynchronous filling before
fill_async() applies backpressure
on the caller (default: 16).
n_sigma: Sigma multiplier used as the gate for the
pedestal-update side effect of
fill_async(): a pixel sample is
pushed back into the pedestal estimate iff
``abs(residual) < n_sigma * cached_std``. Set to
``0.0`` to disable the pedestal update and get
histogram-only async behaviour (default: 1.0).
Also exposed live via the ``n_sigma`` property.
)",
py::kw_only(), py::arg("rows"), py::arg("cols"), py::arg("n_bins"),
py::arg("xmin"), py::arg("xmax"), py::arg("n_threads") = 1,
py::arg("max_pending") = std::size_t{16}, py::arg("n_sigma") = 1.0)
.def(
"push_pedestal_no_update",
[](PedestalTrackingPixelHistogram &self,
py::array_t<PedestalTrackingPixelHistogram::FrameType, 0>
frame) {
auto view = make_view_2d(frame);
self.push_pedestal_no_update(view);
},
R"(
Accumulate `frame` into the per-pixel running pedestal
estimate without refreshing the cached mean.
Use repeatedly while bootstrapping the pedestal, then call
update_mean() once before starting to fill the histogram.
Args:
frame: A 2D numpy array of raw pixel values (dtype: uint16)
)",
py::arg("frame").noconvert())
.def("update_mean", &PedestalTrackingPixelHistogram::update_mean,
R"(
Refresh each partial pedestal's cached per-pixel mean from
its running sums. Drains pending async fills first, then
dispatches the update to the worker pool so the writes to
each shard happen on the same thread that reads them in
fill_async().
)",
py::call_guard<py::gil_scoped_release>())
.def(
"pedestal_mean",
[](const PedestalTrackingPixelHistogram &self) {
// pedestal_mean() flushes + locks + memcpys; do all of
// that without the GIL, only reacquire to wrap into a
// numpy array.
NDArray<PedestalTrackingPixelHistogram::AxisType, 2> *ptr =
nullptr;
{
py::gil_scoped_release release;
ptr = new NDArray<PedestalTrackingPixelHistogram::AxisType,
2>(self.pedestal_mean());
}
return return_image_data(ptr);
},
R"(
Snapshot the per-pixel pedestal mean stitched together
from all shards.
Returns:
A 2D numpy array (rows x cols, dtype: float64)
containing the current cached pedestal mean.
)")
.def(
"fill_async",
[](PedestalTrackingPixelHistogram &self,
py::array_t<PedestalTrackingPixelHistogram::FrameType, 0>
image) {
// Copy the numpy buffer into an owned NDArray while we
// still hold the GIL so we don't depend on the array's
// backing storage outliving this call.
auto view = make_view_2d(image);
NDArray<PedestalTrackingPixelHistogram::FrameType, 2> owned(
view);
// Release the GIL while enqueueing -
// fill_async can block on backpressure
// when the queue is full.
py::gil_scoped_release release;
self.fill_async(std::move(owned));
},
R"(
Submit an image for asynchronous filling with sigma-clipped
pedestal tracking.
For each pixel the worker pool:
* histograms the pedestal-subtracted residual when it
falls in ``[xmin, xmax)``, and
* additionally pushes the raw pixel value back into the
per-thread pedestal estimate when
``abs(residual) < n_sigma * cached_std`` (the
sigma-clipped pedestal-update gate).
The cached std is populated by ``update_mean()``, so
``push_pedestal_no_update()`` + ``update_mean()`` must have
run at least once for the pedestal-update side effect to
fire. Setting ``n_sigma = 0`` disables the side effect and
recovers plain histogram-only async filling.
The image is copied into an internal buffer before this call
returns, so the caller may mutate or free the numpy array
immediately. If the internal queue is full this call blocks
(with the GIL released) until a slot becomes available.
Args:
image: A 2D numpy array of raw pixel values (dtype: uint16)
)",
py::arg("image").noconvert())
.def("fill_from_file", &PedestalTrackingPixelHistogram::fill_from_file,
R"(
Fill the histogram from a file.
Args:
file_path: Path to the file to fill from
max_frames: Maximum number of frames to fill from the file (default: -1)
)",
py::call_guard<py::gil_scoped_release>(), py::arg("fname"),
py::arg("max_frames") = -1, py::arg("verbose") = false)
.def("process_pedestal_file",
&PedestalTrackingPixelHistogram::process_pedestal_file,
R"(
Process a pedestal file.
Args:
file_path: Path to the file to process
max_frames: Maximum number of frames to process from the file (default: -1)
)",
py::call_guard<py::gil_scoped_release>(), py::arg("fname"),
py::arg("max_frames") = -1, py::arg("verbose") = false)
.def_property("n_sigma", &PedestalTrackingPixelHistogram::n_sigma,
&PedestalTrackingPixelHistogram::set_n_sigma,
R"(
Sigma multiplier used as the pedestal-update gate in
fill_async(). Atomic; safe to read or write
from any thread. Setting it to 0.0 disables the pedestal
update entirely. The new value takes effect on subsequent
per-pixel evaluations inside the worker pool.
)")
.def("flush", &PedestalTrackingPixelHistogram::flush,
R"(
Block until all images submitted via
fill_async() have been merged into the
accumulators. Cheap when nothing is pending.
)",
py::call_guard<py::gil_scoped_release>())
.def(
"values",
[](const PedestalTrackingPixelHistogram &self) {
// values() implicitly flushes - release the GIL while it
// does so. Allocation/copy into the NDArray runs without
// the GIL too; only the numpy wrapping needs it.
NDArray<PedestalTrackingPixelHistogram::StorageType, 3> *ptr =
nullptr;
{
py::gil_scoped_release release;
ptr =
new NDArray<PedestalTrackingPixelHistogram::StorageType,
3>(self.values());
}
return return_image_data(ptr);
},
R"(
Get the histogram data as a numpy array.
Implicitly flushes any pending asynchronous fills before
returning, so the snapshot is consistent with everything
submitted up to this call.
Returns:
A 3D numpy array (rows x cols x n_bins, dtype: uint16)
containing the histogram bins for each pixel.
)")
.def(
"bin_centers",
[](const PedestalTrackingPixelHistogram &self) {
auto ptr =
new NDArray<PedestalTrackingPixelHistogram::AxisType, 1>(
self.bin_centers());
return return_image_data(ptr);
},
R"(
Get the bin centers along the residual axis.
Returns:
A 1D numpy array (dtype: float32) of bin center values.
)")
.def(
"bin_edges",
[](const PedestalTrackingPixelHistogram &self) {
auto ptr =
new NDArray<PedestalTrackingPixelHistogram::AxisType, 1>(
self.bin_edges());
return return_image_data(ptr);
},
R"(
Get the bin edges along the residual axis.
Returns:
A 1D numpy array (dtype: float32) of bin edge values.
)");
}
+147
View File
@@ -0,0 +1,147 @@
// SPDX-License-Identifier: MPL-2.0
#include "aare/hist/PixelHistogram.hpp"
#include "np_helper.hpp"
#include <cstddef>
#include <cstdint>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <string>
namespace py = pybind11;
using namespace ::aare;
namespace {
template <typename StorageType>
void define_pixel_histogram_binding(py::module &m, const char *class_name,
const char *storage_dtype) {
using Hist = PixelHistogram<StorageType, double>;
const std::string doc =
std::string("A histogram for pixel-wise statistics with float64 input "
"axis and ") +
storage_dtype + " bin storage";
py::class_<Hist>(m, class_name, doc.c_str())
.def(py::init<int, int, int, double, double, int, std::size_t>(),
R"(
Initialize a PixelHistogram.
Args:
rows: Number of rows in the detector
cols: Number of columns in the detector
n_bins: Number of histogram bins
xmin: Minimum value for histogram range
xmax: Maximum value for histogram range
n_threads: Number of threads for parallel filling (default: 1)
max_pending: Maximum number of images that can be queued for
asynchronous filling before fill_async() applies
backpressure on the caller (default: 16)
)",
py::kw_only(), py::arg("rows"), py::arg("cols"), py::arg("n_bins"),
py::arg("xmin"), py::arg("xmax"), py::arg("n_threads") = 1,
py::arg("max_pending") = std::size_t{16})
.def(
"fill_async",
[](Hist &self, py::array_t<double, 0> image) {
// Copy the numpy buffer into an owned NDArray while we
// still hold the GIL so we don't depend on the array's
// backing storage outliving this call.
auto view = make_view_2d(image);
NDArray<double, 2> owned(view);
// Release the GIL while enqueueing - fill_async can block
// on backpressure when the queue is full.
py::gil_scoped_release release;
self.fill_async(std::move(owned));
},
R"(
Submit an image for asynchronous filling.
The image is copied into an internal buffer before this call
returns, so the caller may mutate or free the numpy array
immediately. The actual histogram update happens on a
background thread. If the internal queue is full this call
blocks (with the GIL released) until a slot becomes available.
Args:
image: A 2D numpy array of pixel values (dtype: float64)
)",
py::arg("image").noconvert())
.def("flush", &Hist::flush,
R"(
Block until all images submitted via fill_async() have been
merged into the accumulators. Cheap when nothing is pending.
)",
py::call_guard<py::gil_scoped_release>())
.def(
"values",
[](const Hist &self) {
// values() implicitly flushes - release the GIL while it
// does so. Allocation/copy into the NDArray runs without
// the GIL too; only the numpy wrapping needs it.
NDArray<StorageType, 3> *ptr = nullptr;
{
py::gil_scoped_release release;
ptr = new NDArray<StorageType, 3>(self.values());
}
return return_image_data(ptr);
},
R"(
Get the histogram data as a numpy array.
Implicitly flushes any pending asynchronous fills before
returning, so the snapshot is consistent with everything
submitted up to this call.
Returns:
A 3D numpy array containing the histogram bins for each pixel
)")
.def(
"bin_centers",
[](const Hist &self) {
auto ptr = new NDArray<double, 1>(self.bin_centers());
return return_image_data(ptr);
},
R"(
Get the bin centers along the value axis.
Returns:
A 1D numpy array containing the center values for each histogram bin
)")
.def(
"bin_edges",
[](const Hist &self) {
auto ptr = new NDArray<double, 1>(self.bin_edges());
return return_image_data(ptr);
},
R"(
Get the bin edges along the value axis.
Returns:
A 1D numpy array containing the edge values for the histogram bins
)");
}
} // namespace
void define_pixel_histogram_bindings(py::module &m) {
define_pixel_histogram_binding<double>(m, "PixelHistogram_d", "float64");
define_pixel_histogram_binding<float>(m, "PixelHistogram_f", "float32");
define_pixel_histogram_binding<std::uint64_t>(m, "PixelHistogram_u64",
"uint64");
define_pixel_histogram_binding<std::uint32_t>(m, "PixelHistogram_u32",
"uint32");
define_pixel_histogram_binding<std::uint16_t>(m, "PixelHistogram_u16",
"uint16");
define_pixel_histogram_binding<std::uint8_t>(m, "PixelHistogram_u8",
"uint8");
// Backwards-compatible alias for the generic Python class name.
m.attr("PixelHistogram") = m.attr("PixelHistogram_d");
}
+1
View File
@@ -280,6 +280,7 @@ void define_raw_file_io_bindings(py::module &m) {
.def("tell", &RawFile::tell, R"(
Return the current frame number.)")
.def_property_readonly("total_frames", &RawFile::total_frames)
.def("__len__", &RawFile::total_frames)
.def("rows", static_cast<size_t (RawFile::*)() const>(&RawFile::rows))
.def(
"rows",
+3 -1
View File
@@ -226,5 +226,7 @@ void define_ctb_raw_file_io_bindings(py::module &m) {
.def_property_readonly("image_size_in_bytes",
&CtbRawFile::image_size_in_bytes)
.def_property_readonly("frames_in_file", &CtbRawFile::frames_in_file);
.def_property_readonly("frames_in_file", &CtbRawFile::frames_in_file)
.def_property_readonly("total_frames", &CtbRawFile::total_frames)
.def("__len__", &CtbRawFile::total_frames);
}
+1
View File
@@ -60,6 +60,7 @@ void define_file_io_bindings(py::module &m) {
.def("seek", &File::seek)
.def("tell", &File::tell)
.def_property_readonly("total_frames", &File::total_frames)
.def("__len__", &File::total_frames)
.def_property_readonly("rows", &File::rows)
.def_property_readonly("cols", &File::cols)
.def_property_readonly("bitdepth", &File::bitdepth)
+52 -25
View File
@@ -5,7 +5,6 @@
#include <pybind11/stl.h>
#include <pybind11/stl_bind.h>
#include "aare/Chi2.hpp"
#include "aare/Fit.hpp"
#include "aare/FitModel.hpp"
#include "aare/Models.hpp"
@@ -13,7 +12,7 @@
namespace py = pybind11;
using namespace pybind11::literals;
template <typename Model, typename FCN>
template <typename Model>
py::object
fit_dispatch(const aare::FitModel<Model> &model,
py::array_t<double, py::array::c_style | py::array::forcecast> x,
@@ -22,7 +21,6 @@ fit_dispatch(const aare::FitModel<Model> &model,
template <typename Model> void bind_fit_model(py::module &m, const char *name) {
using FM = aare::FitModel<Model>;
using FCN = aare::func::Chi2Model1DGrad<Model>;
py::class_<FM>(m, name)
.def(py::init<unsigned int, unsigned int, double, bool>(),
py::arg("strategy") = 0, py::arg("max_calls") = 100,
@@ -85,8 +83,7 @@ template <typename Model> void bind_fit_model(py::module &m, const char *name) {
py::array_t<double, py::array::c_style | py::array::forcecast> x,
py::array_t<double, py::array::c_style | py::array::forcecast> y,
py::object y_err_obj, int n_threads) -> py::object {
return fit_dispatch<Model, FCN>(self, x, y, y_err_obj,
n_threads);
return fit_dispatch<Model>(self, x, y, y_err_obj, n_threads);
},
R"doc(
Fit this model to 1D or 3D data using Minuit2.
@@ -145,7 +142,7 @@ py::dict pack_1d_result_dict(const aare::NDArray<double, 1> &result,
}
// Helper: typed dispatch for one Model, handles 1D/3D + y_err logic
template <typename Model, typename FCN>
template <typename Model>
py::object
fit_dispatch(const aare::FitModel<Model> &model,
py::array_t<double, py::array::c_style | py::array::forcecast> x,
@@ -175,9 +172,9 @@ fit_dispatch(const aare::FitModel<Model> &model,
new NDArray<double, 3>({y.shape(0), y.shape(1), npar}, 0.0);
auto y_view_err = make_view_3d(y_err);
aare::fit_3d<Model, FCN>(model, x_view, y_view, y_view_err,
par_out->view(), err_out->view(),
chi2_out->view(), n_threads);
aare::fit_3d<Model>(model, x_view, y_view, y_view_err,
par_out->view(), err_out->view(),
chi2_out->view(), n_threads);
if (model.compute_errors()) {
return py::dict("par"_a = return_image_data(par_out),
@@ -193,9 +190,9 @@ fit_dispatch(const aare::FitModel<Model> &model,
NDView<double, 3> dummy_err{};
NDView<double, 3> dummy_err_out{};
aare::fit_3d<Model, FCN>(model, x_view, y_view, dummy_err,
par_out->view(), dummy_err_out,
chi2_out->view(), n_threads);
aare::fit_3d<Model>(model, x_view, y_view, dummy_err,
par_out->view(), dummy_err_out,
chi2_out->view(), n_threads);
return py::dict("par"_a = return_image_data(par_out),
"chi2"_a = return_image_data(chi2_out));
@@ -217,10 +214,9 @@ fit_dispatch(const aare::FitModel<Model> &model,
}
auto y_view_err = make_view_1d(y_err);
result =
aare::fit_pixel<Model, FCN>(model, x_view, y_view, y_view_err);
result = aare::fit_pixel<Model>(model, x_view, y_view, y_view_err);
} else {
result = aare::fit_pixel<Model, FCN>(model, x_view, y_view);
result = aare::fit_pixel<Model>(model, x_view, y_view);
}
return pack_1d_result_dict<Model>(result, model.compute_errors());
@@ -679,6 +675,11 @@ void define_fit_bindings(py::module &m) {
// ── Bind model classes ──────────────────────────────────────────
bind_fit_model<aare::model::Gaussian>(m, "Gaussian");
bind_fit_model<aare::model::GaussianErfcPlateau>(m, "GaussianErfcPlateau");
bind_fit_model<aare::model::GaussianChargeSharing>(m,
"GaussianChargeSharing");
bind_fit_model<aare::model::GaussianChargeSharingKb>(
m, "GaussianChargeSharingKb");
bind_fit_model<aare::model::RisingScurve>(m, "RisingScurve");
bind_fit_model<aare::model::FallingScurve>(m, "FallingScurve");
bind_fit_model<aare::model::Pol1>(m, "Pol1");
@@ -691,28 +692,54 @@ void define_fit_bindings(py::module &m) {
py::array_t<double, py::array::c_style | py::array::forcecast> y,
py::object y_err_obj, int n_threads) -> py::object {
using namespace aare::model;
using namespace aare::func;
// ── Polynomial of degree 1 ───────
if (py::isinstance<aare::FitModel<Pol1>>(model_obj)) {
const auto &mdl =
model_obj.cast<const aare::FitModel<Pol1> &>();
return fit_dispatch<Pol1, Chi2Pol1>(mdl, x, y, y_err_obj,
n_threads);
return fit_dispatch<Pol1>(mdl, x, y, y_err_obj, n_threads);
}
// ── Polynomial of degree 2 ───────
if (py::isinstance<aare::FitModel<Pol2>>(model_obj)) {
const auto &mdl =
model_obj.cast<const aare::FitModel<Pol2> &>();
return fit_dispatch<Pol2, Chi2Pol2>(mdl, x, y, y_err_obj,
n_threads);
return fit_dispatch<Pol2>(mdl, x, y, y_err_obj, n_threads);
}
// ── Gaussian ───────
if (py::isinstance<aare::FitModel<Gaussian>>(model_obj)) {
const auto &mdl =
model_obj.cast<const aare::FitModel<Gaussian> &>();
return fit_dispatch<Gaussian, Chi2Gaussian>(
return fit_dispatch<Gaussian>(mdl, x, y, y_err_obj, n_threads);
}
// ── GaussianErfcPlateau ───────
if (py::isinstance<aare::FitModel<GaussianErfcPlateau>>(
model_obj)) {
const auto &mdl =
model_obj
.cast<const aare::FitModel<GaussianErfcPlateau> &>();
return fit_dispatch<GaussianErfcPlateau>(mdl, x, y, y_err_obj,
n_threads);
}
// ── GaussianChargeSharing ───────
if (py::isinstance<aare::FitModel<GaussianChargeSharing>>(
model_obj)) {
const auto &mdl =
model_obj
.cast<const aare::FitModel<GaussianChargeSharing> &>();
return fit_dispatch<GaussianChargeSharing>(mdl, x, y, y_err_obj,
n_threads);
}
// ── GaussianChargeSharingKb ───────
if (py::isinstance<aare::FitModel<GaussianChargeSharingKb>>(
model_obj)) {
const auto &mdl = model_obj.cast<
const aare::FitModel<GaussianChargeSharingKb> &>();
return fit_dispatch<GaussianChargeSharingKb>(
mdl, x, y, y_err_obj, n_threads);
}
@@ -720,16 +747,16 @@ void define_fit_bindings(py::module &m) {
if (py::isinstance<aare::FitModel<RisingScurve>>(model_obj)) {
const auto &mdl =
model_obj.cast<const aare::FitModel<RisingScurve> &>();
return fit_dispatch<RisingScurve, Chi2RisingScurve>(
mdl, x, y, y_err_obj, n_threads);
return fit_dispatch<RisingScurve>(mdl, x, y, y_err_obj,
n_threads);
}
// ── Falling Scurve ───────
if (py::isinstance<aare::FitModel<FallingScurve>>(model_obj)) {
const auto &mdl =
model_obj.cast<const aare::FitModel<FallingScurve> &>();
return fit_dispatch<FallingScurve, Chi2FallingScurve>(
mdl, x, y, y_err_obj, n_threads);
return fit_dispatch<FallingScurve>(mdl, x, y, y_err_obj,
n_threads);
}
throw std::runtime_error(
+1
View File
@@ -72,6 +72,7 @@ void define_jungfrau_data_file_io_bindings(py::module &m) {
.def_property_readonly("bitdepth", &JungfrauDataFile::bitdepth)
.def_property_readonly("current_file", &JungfrauDataFile::current_file)
.def_property_readonly("total_frames", &JungfrauDataFile::total_frames)
.def("__len__", &JungfrauDataFile::total_frames)
.def_property_readonly("n_files", &JungfrauDataFile::n_files)
.def("read_frame", &read_dat_frame,
R"(
+4
View File
@@ -12,6 +12,8 @@
#include "bind_Defs.hpp"
#include "bind_Eta.hpp"
#include "bind_Interpolator.hpp"
#include "bind_PedestalTrackingPixelHistogram.hpp"
#include "bind_PixelHistogram.hpp"
#include "bind_PixelMap.hpp"
#include "bind_RawFile.hpp"
#include "bind_calibration.hpp"
@@ -64,6 +66,8 @@ PYBIND11_MODULE(_aare, m) {
define_raw_master_file_bindings(m);
define_var_cluster_finder_bindings(m);
define_pixel_map_bindings(m);
define_pixel_histogram_bindings(m);
define_pedestal_tracking_pixel_histogram_bindings(m);
define_pedestal_bindings<double>(m, "Pedestal_d");
define_pedestal_bindings<float>(m, "Pedestal_f");
define_fit_bindings(m);
+61 -3
View File
@@ -5,6 +5,7 @@
#include <cstdint>
#include <filesystem>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
@@ -12,7 +13,8 @@ namespace py = pybind11;
template <typename SUM_TYPE>
void define_pedestal_bindings(py::module &m, const std::string &name) {
py::class_<Pedestal<SUM_TYPE>>(m, name.c_str())
py::class_<Pedestal<SUM_TYPE>>(m, name.c_str(), py::buffer_protocol())
.def(py::init<int, int, int>())
.def(py::init<int, int>())
.def("mean",
@@ -21,6 +23,23 @@ void define_pedestal_bindings(py::module &m, const std::string &name) {
*mea = self.mean();
return return_image_data(mea);
})
.def("view",
[](py::object self_py) {
auto &self = self_py.cast<Pedestal<SUM_TYPE> &>();
auto v = self.view();
std::array<py::ssize_t, 2> shape{
static_cast<py::ssize_t>(v.shape(0)),
static_cast<py::ssize_t>(v.shape(1))};
std::array<py::ssize_t, 2> byte_strides{
static_cast<py::ssize_t>(v.strides()[0]) *
static_cast<py::ssize_t>(sizeof(SUM_TYPE)),
static_cast<py::ssize_t>(v.strides()[1]) *
static_cast<py::ssize_t>(sizeof(SUM_TYPE))};
auto arr = py::array_t<SUM_TYPE>(shape, byte_strides, v.data(),
self_py);
arr.attr("setflags")(py::arg("write") = false);
return arr;
})
.def("variance",
[](Pedestal<SUM_TYPE> &self) {
auto var = new NDArray<SUM_TYPE, 2>{};
@@ -33,6 +52,23 @@ void define_pedestal_bindings(py::module &m, const std::string &name) {
*std = self.std();
return return_image_data(std);
})
.def(
"__array_ufunc__",
[](py::object self, py::object ufunc, const std::string &method,
py::args inputs, py::kwargs kwargs) -> py::object {
if (method != "__call__" || inputs.size() != 2 ||
inputs[1].ptr() != self.ptr() ||
py::cast<std::string>(ufunc.attr("__name__")) !=
"subtract") {
return py::reinterpret_borrow<py::object>(
Py_NotImplemented);
}
auto mean =
py::module_::import("builtins").attr("memoryview")(self);
return ufunc(inputs[0], mean, **kwargs);
},
"Support subtracting a Pedestal from a NumPy array.")
.def("clear", py::overload_cast<>(&Pedestal<SUM_TYPE>::clear))
.def_property_readonly("rows", &Pedestal<SUM_TYPE>::rows)
.def_property_readonly("cols", &Pedestal<SUM_TYPE>::cols)
@@ -49,6 +85,16 @@ void define_pedestal_bindings(py::module &m, const std::string &name) {
auto v = make_view_2d(f);
pedestal.push(v);
})
.def(
"push_with_threshold",
[](Pedestal<SUM_TYPE> &pedestal,
py::array_t<uint16_t, py::array::c_style> &f,
py::array_t<SUM_TYPE, py::array::c_style> &threshold) {
auto frame_view = make_view_2d(f);
auto threshold_view = make_view_2d(threshold);
pedestal.push_with_threshold(frame_view, threshold_view);
},
py::arg("frame").noconvert(), py::arg("threshold").noconvert())
.def(
"push_no_update",
[](Pedestal<SUM_TYPE> &pedestal,
@@ -57,5 +103,17 @@ void define_pedestal_bindings(py::module &m, const std::string &name) {
pedestal.push_no_update(v);
},
py::arg().noconvert())
.def("update_mean", &Pedestal<SUM_TYPE>::update_mean);
}
.def("update_mean", &Pedestal<SUM_TYPE>::update_mean)
.def_buffer([](Pedestal<SUM_TYPE> &self) {
auto mean = self.view();
return py::buffer_info(
const_cast<SUM_TYPE *>(mean.data()), sizeof(SUM_TYPE),
py::format_descriptor<SUM_TYPE>::format(), 2,
{static_cast<py::ssize_t>(mean.shape(0)),
static_cast<py::ssize_t>(mean.shape(1))},
{static_cast<py::ssize_t>(mean.strides()[0] * sizeof(SUM_TYPE)),
static_cast<py::ssize_t>(mean.strides()[1] *
sizeof(SUM_TYPE))},
true);
});
}
+4
View File
@@ -34,6 +34,10 @@ void define_var_cluster_finder_bindings(py::module &m) {
auto noise_map_span = make_view_2d(noise_map);
self.set_noiseMap(noise_map_span);
})
.def("set_numberOfNeighbours",
&VarClusterFinder<double>::set_numberOfNeighbours)
.def("set_empty_surroundingPixels",
&VarClusterFinder<double>::set_empty_surroundingPixels)
.def("set_peripheralThresholdFactor",
&VarClusterFinder<double>::set_peripheralThresholdFactor)
.def("find_clusters",
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+31
View File
@@ -0,0 +1,31 @@
import pytest
from aare import Interpolator, ClusterVector, Etai, Cluster
import numpy as np
def test_interpolation_api():
eta_distribution = np.zeros((10, 10, 1)) # dummy eta distribution
etax_bins = np.linspace(0, 1.0, 11)
etay_bins = np.linspace(0, 1.0, 11)
e_bins = np.array([0., 10.]) # dummy energy bins
interpolator = Interpolator(eta_distribution, etax_bins, etay_bins, e_bins)
cluster_vector = ClusterVector()
cluster_vector.push_back(Cluster(10, 5, np.ones(shape=9, dtype=np.int32)))
cluster_vector.push_back(Cluster(20, 10, np.ones(shape=9, dtype=np.int32)))
eta1 = Etai()
eta1.x = 0.1
eta1.y = 0.1
eta1.sum = 5
eta2 = Etai()
eta2.x = 0.1
eta2.y = 0.9
eta2.sum = 6
etas = np.array([eta1, eta2]) # dummy etas for the clusters
photons = interpolator.interpolate(cluster_vector, etas)
assert photons.size == cluster_vector.size # should return one photon per cluster
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+40
View File
@@ -0,0 +1,40 @@
import numpy as np
import pytest
from aare import Pedestal_d, Pedestal_f
@pytest.mark.parametrize(
("pedestal_type", "expected_dtype"),
[(Pedestal_d, np.float64), (Pedestal_f, np.float32)],
)
def test_numpy_array_minus_pedestal(pedestal_type, expected_dtype):
pedestal = pedestal_type(2, 3)
pedestal.push(np.array([[2, 4, 6], [8, 10, 12]], dtype=np.uint16))
array = np.array([[12, 14, 16], [18, 20, 22]], dtype=np.uint16)
result = array - pedestal
np.testing.assert_array_equal(
result, np.array([[10, 10, 10], [10, 10, 10]], dtype=expected_dtype)
)
assert result.dtype == expected_dtype
def test_numpy_array_minus_pedestal_rejects_incompatible_shape():
pedestal = Pedestal_d(2, 3)
array = np.zeros((2, 2), dtype=np.float64)
with pytest.raises(ValueError):
array - pedestal
def test_pedestal_exposes_mean_as_read_only_buffer():
pedestal = Pedestal_d(2, 3)
pedestal.push(np.array([[2, 4, 6], [8, 10, 12]], dtype=np.uint16))
mean = np.asarray(pedestal)
np.testing.assert_array_equal(mean, pedestal.view())
assert np.shares_memory(mean, pedestal.view())
assert not mean.flags.writeable
+8 -9
View File
@@ -8,10 +8,10 @@ def test_matterhorn10_16bit(test_data_path):
with CtbRawFile(test_data_path / "raw/Matterhorn10/16bit_master_0.json", transform = transform.Matterhorn10Transform(dynamic_range=16, num_counters=1)) as f:
headers, frames = f.read_frame()
assert frames.shape == (256, 256)
assert frames.shape == (1, 256, 256)
assert frames.dtype == np.uint16
expected_data = np.tile(np.arange(255, -1, -1,dtype=np.uint16), (256, 1)) # TODO: endianess issue ?
expected_data = np.tile(np.arange(255, -1, -1,dtype=np.uint16), (1, 256, 1)) # TODO: endianess issue ?
assert np.all(frames == expected_data)
@@ -22,24 +22,23 @@ def test_matterhorn10_8bit(test_data_path):
with CtbRawFile(test_data_path / "raw/Matterhorn10/8bit_master_1.json", transform = transform.Matterhorn10Transform(dynamic_range=8, num_counters=1)) as f:
headers, frames = f.read_frame()
assert frames.shape == (256, 256)
assert frames.shape == (1, 256, 256)
assert frames.dtype == np.uint8
expected_data = np.tile(np.arange(255, -1, -1,dtype=np.uint8), (256, 1)) # TODO: endianess issue ?
expected_data = np.tile(np.arange(255, -1, -1,dtype=np.uint8), (1, 256, 1)) # TODO: endianess issue ?
assert np.all(frames == expected_data)
@pytest.mark.withdata
def test_matterhorn10_4bit(test_data_path):
""" Matterhorn10Transform 1 counter 4 bit dynamic range """
with CtbRawFile(test_data_path / "raw/Matterhorn10/newnewrun_4bit_1counter_master_0.json", transform = transform.Matterhorn10Transform(dynamic_range=4, num_counters=1)) as f:
headers, frames = f.read_frame()
assert frames.shape == (256, 256)
assert frames.shape == (1, 256, 256)
assert frames.dtype == np.uint8
expected_data = np.tile(np.tile(np.arange(15, -1, -1, dtype=np.uint8), 16), (256, 1)) # TODO: endianess issue ?
expected_data = np.tile(np.tile(np.arange(15, -1, -1, dtype=np.uint8), 16), (1, 256, 1)) # TODO: endianess issue ?
assert np.all(frames == expected_data)
@@ -50,9 +49,9 @@ def test_matterhorn10_16bit_4counters(test_data_path):
with CtbRawFile(test_data_path / "raw/Matterhorn10/4counter_16bit_master_4.json", transform = transform.Matterhorn10Transform(dynamic_range=16, num_counters=4)) as f:
headers, frames = f.read_frame()
assert frames.shape == (4*256, 256)
assert frames.shape == (4, 256, 256)
assert frames.dtype == np.uint16
expected_data = np.tile(np.arange(255, -1, -1,dtype=np.uint16), (4*256, 1)) # TODO: endianess issue ?
expected_data = np.tile(np.arange(255, -1, -1,dtype=np.uint16), (4, 256, 1)) # TODO: endianess issue ?
assert np.all(frames == expected_data)