special version with pedestal tracking
Build on RHEL9 / build (push) Successful in 2m33s
Build on RHEL8 / build (push) Successful in 3m19s
Run tests using data on local RHEL8 / build (push) Successful in 4m8s

This commit is contained in:
Lars Erik Fröjd
2026-05-27 15:18:13 +02:00
parent e7bc1253ea
commit 7b55a94dd9
8 changed files with 968 additions and 1 deletions
@@ -0,0 +1,246 @@
// SPDX-License-Identifier: MPL-2.0
#include "aare/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_with_threshold() applies backpressure
on the caller (default: 16).
n_sigma: Sigma multiplier used as the gate for the
pedestal-update side effect of
fill_async_with_threshold(): 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::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().
)",
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",
[](PedestalTrackingPixelHistogram &self,
py::array_t<PedestalTrackingPixelHistogram::FrameType, 0>
image) {
auto view = make_view_2d(image);
self.fill(view);
},
R"(
Fill the histogram with image data (blocking).
The pedestal-subtracted residual `image - pedestal_mean`
is computed per pixel inside the worker loop, so the
pedestal must already have been bootstrapped via
push_pedestal_no_update() + update_mean() for this to be
meaningful.
Args:
image: A 2D numpy array of raw pixel values (dtype: uint16)
)",
py::arg("image").noconvert())
.def("fill_async_with_threshold",
[](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_with_threshold can block on backpressure
// when the queue is full.
py::gil_scoped_release release;
self.fill_async_with_threshold(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_property("n_sigma", &PedestalTrackingPixelHistogram::n_sigma,
&PedestalTrackingPixelHistogram::set_n_sigma,
R"(
Sigma multiplier used as the pedestal-update gate in
fill_async_with_threshold(). 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_with_threshold() have been merged into the
accumulators. Cheap when nothing is pending.
)",
py::call_guard<py::gil_scoped_release>())
.def("pending", &PedestalTrackingPixelHistogram::pending,
R"(
Return the number of images either waiting in the queue or
currently being processed by the background thread (i.e.
still in flight after fill_async_with_threshold()). Useful
for monitoring/diagnostics.
)")
.def("hdata",
[](const PedestalTrackingPixelHistogram &self) {
// hdata() 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.hdata());
}
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.
)");
}
+2
View File
@@ -12,6 +12,7 @@
#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"
@@ -66,6 +67,7 @@ PYBIND11_MODULE(_aare, 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);
+27
View File
@@ -21,6 +21,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>{};
@@ -49,6 +66,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,