Merge branch 'main' into dev/cluster-vector

This commit is contained in:
Erik Fröjdh
2026-09-01 18:44:38 +02:00
committed by GitHub
43 changed files with 2188 additions and 302 deletions
+2
View File
@@ -39,7 +39,9 @@ set(PYTHON_FILES
aare/ClusterFinder.py
aare/ClusterVector.py
aare/Cluster.py
aare/FastPedestal.py
aare/calibration.py
aare/factory.py
aare/experimental.py
aare/RawFile.py
aare/transform.py
+2 -2
View File
@@ -1,6 +1,6 @@
from . import _aare
import numpy as np
from .ClusterFinder import _type_to_char
from .factory import _type_to_char
def Cluster(x : int, y : int, data, cluster_size=(3,3), dtype = np.int32):
@@ -21,4 +21,4 @@ def Cluster(x : int, y : int, data, cluster_size=(3,3), dtype = np.int32):
except AttributeError:
raise ValueError(f"Unsupported combination of type and cluster size: {dtype}/{cluster_size} when requesting {class_name}")
return cls(x, y, data)
return cls(x, y, data)
+1 -12
View File
@@ -1,21 +1,10 @@
# SPDX-License-Identifier: MPL-2.0
from . import _aare
import numpy as np
from .factory import _type_to_char
_supported_cluster_sizes = [(2,2), (3,3), (5,5), (7,7), (9,9),]
def _type_to_char(dtype):
if dtype == np.int32:
return 'i'
elif dtype == np.float32:
return 'f'
elif dtype == np.float64:
return 'd'
elif dtype == np.int16:
return 'i16'
else:
raise ValueError(f"Unsupported dtype: {dtype}. Only np.int32, np.float32, and np.float64 are supported.")
def _get_class(name, cluster_size, dtype):
"""
Helper function to get the class based on the name, cluster size, and dtype.
+56
View File
@@ -0,0 +1,56 @@
# SPDX-License-Identifier: MPL-2.0
import numpy as np
from .factory import _get_typed_class
def _get_fast_pedestal_class(dtype):
return _get_typed_class("FastPedestal", dtype)
def FastPedestal(rows, cols, n_samples=1000, dtype=np.float64):
"""Create an empty per-pixel running pedestal.
This factory hides the dtype suffix used by the templated C++ bindings.
Call ``add_init_frame()`` exactly ``n_samples`` times before using the
statistics or calling ``push_ema()``. Subsequent frames have weight
``1 / n_samples`` in the running mean and population variance.
Args:
rows: Number of image rows.
cols: Number of image columns.
n_samples: Initialization frame count and steady-state update-weight
denominator.
dtype: Output dtype for the mean, variance, and standard deviation.
Supported values are ``np.float64``, ``np.float32``, and
``np.int16``.
"""
cls = _get_fast_pedestal_class(dtype)
return cls(rows, cols, n_samples)
def from_file(filename, n_samples=1000, skip_first=0, dtype=np.float64):
"""Create a FastPedestal from frames in a file.
After ignoring ``skip_first`` frames, the next ``n_samples`` frames
initialize the pedestal. Every remaining frame is then applied as a
steady-state update. Input frames are read as uint16 data.
Args:
filename: Input image file.
n_samples: Number of frames used for initialization.
skip_first: Number of leading frames to ignore.
dtype: Output dtype for the mean, variance, and standard deviation.
Raises:
RuntimeError: If fewer than ``n_samples`` frames remain after
``skip_first`` or ``n_samples`` is zero.
"""
cls = _get_fast_pedestal_class(dtype)
return cls.from_file(
filename, n_samples=n_samples, skip_first=skip_first
)
FastPedestal.from_file = from_file
+11
View File
@@ -5,6 +5,16 @@ from . import _aare
from . import transform
from . import experimental
from ._aare import (
FastPedestal_d,
FastPedestal_f,
FastPedestal_i16,
Pedestal_d,
Pedestal_f,
Pedestal_i16,
ClusterFinder_Cluster3x3i,
VarClusterFinder,
)
from ._aare import (
File,
JungfrauDataFile,
@@ -20,6 +30,7 @@ from ._aare import corner
# from ._aare import ClusterFinderMT, ClusterCollector, ClusterFileSink, ClusterVector_i
from ._version import __version__
from .FastPedestal import FastPedestal
from .ClusterFinder import ClusterFinder, ClusterCollector, ClusterFinderMT, ClusterFileSink, ClusterFile
from .ClusterVector import ClusterVector
from .Cluster import Cluster
+36
View File
@@ -0,0 +1,36 @@
# SPDX-License-Identifier: MPL-2.0
import numpy as np
from . import _aare
_TYPE_TO_CHAR = {
np.dtype(np.int32): "i",
np.dtype(np.float32): "f",
np.dtype(np.float64): "d",
np.dtype(np.int16): "i16",
}
def _type_to_char(dtype):
"""Return the suffix used by bindings instantiated for ``dtype``."""
try:
return _TYPE_TO_CHAR[np.dtype(dtype)]
except (KeyError, TypeError):
supported = ", ".join(str(dtype) for dtype in _TYPE_TO_CHAR)
raise ValueError(
f"Unsupported dtype: {dtype}. Supported dtypes are {supported}."
) from None
def _get_typed_class(name, dtype):
"""Return a bound class named ``<name>_<dtype suffix>``."""
class_name = f"{name}_{_type_to_char(dtype)}"
try:
return getattr(_aare, class_name)
except AttributeError:
raise ValueError(
f"Unsupported dtype for {name}: {dtype} "
f"(binding {class_name} is not available)."
) from None
-1
View File
@@ -10,7 +10,6 @@
#include <pybind11/stl_bind.h>
namespace py = pybind11;
using pd_type = double;
using namespace aare;
+3 -2
View File
@@ -6,6 +6,8 @@
#include "aare/ClusterVector.hpp"
#include "aare/NDView.hpp"
#include "aare/Pedestal.hpp"
#include "module_config.hpp"
#include "np_helper.hpp"
#include <cstdint>
@@ -15,7 +17,6 @@
#include <pybind11/stl_bind.h>
namespace py = pybind11;
using pd_type = double;
using namespace aare;
@@ -30,7 +31,7 @@ void define_ClusterCollector(py::module &m, const std::string &typestr) {
using ClusterType = Cluster<T, ClusterSizeX, ClusterSizeY, CoordType>;
py::class_<ClusterCollector<ClusterType>>(m, class_name.c_str())
.def(py::init<ClusterFinderMT<ClusterType, uint16_t, double> *>())
.def(py::init<ClusterFinderMT<ClusterType, uint16_t, pd_type> *>())
.def("stop", &ClusterCollector<ClusterType>::stop)
.def(
"steal_clusters",
+2 -2
View File
@@ -15,8 +15,6 @@
#include <pybind11/stl_bind.h>
namespace py = pybind11;
using pd_type = double;
using namespace aare;
#pragma GCC diagnostic push
@@ -29,6 +27,8 @@ void define_ClusterFileSink(py::module &m, const std::string &typestr) {
using ClusterType = Cluster<T, ClusterSizeX, ClusterSizeY, CoordType>;
// TODO! adapt to set pedestal type (needs templating of ClusterFileSink)
// or maybe access through base class?
py::class_<ClusterFileSink<ClusterType>>(m, class_name.c_str())
.def(py::init<ClusterFinderMT<ClusterType, uint16_t, double> *,
const std::filesystem::path &>())
+4 -1
View File
@@ -6,6 +6,8 @@
#include "aare/ClusterVector.hpp"
#include "aare/NDView.hpp"
#include "aare/Pedestal.hpp"
#include "module_config.hpp"
#include "np_helper.hpp"
#include <cstdint>
@@ -15,7 +17,6 @@
#include <pybind11/stl_bind.h>
namespace py = pybind11;
using pd_type = double;
using namespace aare;
@@ -48,6 +49,8 @@ void define_ClusterFinder(py::module &m, const std::string &typestr) {
})
.def("clear_pedestal",
&ClusterFinder<ClusterType, uint16_t, pd_type>::clear_pedestal)
.def("update_threshold",
&ClusterFinder<ClusterType, uint16_t, pd_type>::update_threshold)
.def_property_readonly(
"pedestal",
[](ClusterFinder<ClusterType, uint16_t, pd_type> &self) {
+7 -4
View File
@@ -6,6 +6,8 @@
#include "aare/ClusterVector.hpp"
#include "aare/NDView.hpp"
#include "aare/Pedestal.hpp"
#include "module_config.hpp"
#include "np_helper.hpp"
#include <cstdint>
@@ -15,7 +17,6 @@
#include <pybind11/stl_bind.h>
namespace py = pybind11;
using pd_type = double;
using namespace aare;
@@ -31,9 +32,10 @@ void define_ClusterFinderMT(py::module &m, const std::string &typestr) {
py::class_<ClusterFinderMT<ClusterType, uint16_t, pd_type>>(
m, class_name.c_str())
.def(py::init<Shape<2>, pd_type, size_t, size_t>(),
.def(py::init<Shape<2>, pd_type, size_t, size_t, size_t>(),
py::arg("image_size"), py::arg("n_sigma") = 5.0,
py::arg("capacity") = 2048, py::arg("n_threads") = 3)
py::arg("capacity") = 2048, py::arg("n_threads") = 3,
py::arg("queue_depth") = 16)
.def("push_pedestal_frame",
[](ClusterFinderMT<ClusterType, uint16_t, pd_type> &self,
py::array_t<uint16_t> frame) {
@@ -46,7 +48,6 @@ void define_ClusterFinderMT(py::module &m, const std::string &typestr) {
py::array_t<uint16_t> frame, uint64_t frame_number) {
auto view = make_view_2d(frame);
self.find_clusters(view, frame_number);
return;
},
py::arg(), py::arg("frame_number") = 0)
.def_property_readonly(
@@ -56,6 +57,8 @@ void define_ClusterFinderMT(py::module &m, const std::string &typestr) {
})
.def("clear_pedestal",
&ClusterFinderMT<ClusterType, uint16_t, pd_type>::clear_pedestal)
.def("update_threshold",
&ClusterFinderMT<ClusterType, uint16_t, pd_type>::update_threshold)
.def("sync", &ClusterFinderMT<ClusterType, uint16_t, pd_type>::sync)
.def("stop", &ClusterFinderMT<ClusterType, uint16_t, pd_type>::stop)
.def("start", &ClusterFinderMT<ClusterType, uint16_t, pd_type>::start)
-1
View File
@@ -15,7 +15,6 @@
#include <pybind11/stl_bind.h>
namespace py = pybind11;
using pd_type = double;
using namespace aare;
+147
View File
@@ -0,0 +1,147 @@
// SPDX-License-Identifier: MPL-2.0
#include "aare/FastPedestal.hpp"
#include "np_helper.hpp"
#include <cstdint>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
namespace py = pybind11;
template <typename SUM_TYPE>
void define_fast_pedestal_bindings(py::module &m, const std::string &name) {
py::class_<FastPedestal<SUM_TYPE>>(
m, name.c_str(),
"Maintain a per-pixel running mean, population variance, and "
"standard deviation.",
py::buffer_protocol())
.def(py::init<uint32_t, uint32_t, uint32_t>(), py::arg("rows"),
py::arg("cols"), py::arg("n_samples"),
"Construct an empty pedestal. It becomes ready after n_samples "
"calls to add_init_frame().")
.def(py::init<uint32_t, uint32_t>(), py::arg("rows"), py::arg("cols"),
"Construct an empty pedestal with n_samples=1000.")
.def(
"mean",
[](FastPedestal<SUM_TYPE> &self) {
auto mean = new NDArray<SUM_TYPE, 2>{};
*mean = self.mean();
return return_image_data(mean);
},
"Return a copy of the cached mean. The pedestal must be ready.")
.def(
"var",
[](FastPedestal<SUM_TYPE> &self) {
auto variance = new NDArray<SUM_TYPE, 2>{};
*variance = self.variance();
return return_image_data(variance);
},
"Return the population variance, normalized by n_samples, as a "
"NumPy array. The pedestal must be ready.")
.def(
"std",
[](FastPedestal<SUM_TYPE> &self) {
auto standard_deviation = new NDArray<SUM_TYPE, 2>{};
*standard_deviation = self.std();
return return_image_data(standard_deviation);
},
"Return the population standard deviation as a NumPy array. The "
"pedestal must be ready.")
.def(
"view",
[](py::object self_py) {
return py::module_::import("numpy").attr("asarray")(self_py);
},
"Return a non-owning, non-writable NumPy view of the cached mean. "
"The pedestal must be ready.")
// We need to buffer protocol to allow for numpy operations using the
// pedestal mean
.def_buffer([](FastPedestal<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);
})
// Subtracting a FastPedestal from a NumPy array
.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 FastPedestal from a NumPy array.")
.def("clear", py::overload_cast<>(&FastPedestal<SUM_TYPE>::clear),
"Reset all statistics and initialization state to zero.")
.def_property_readonly("rows", &FastPedestal<SUM_TYPE>::rows,
"Number of image rows.")
.def_property_readonly("cols", &FastPedestal<SUM_TYPE>::cols,
"Number of image columns.")
.def_property_readonly("cur_samples",
&FastPedestal<SUM_TYPE>::cur_samples,
"Number of initialization frames accumulated. "
"Steady-state pushes do not change it.")
.def_property_readonly(
"ready", &FastPedestal<SUM_TYPE>::ready,
"Whether n_samples initialization frames have been accumulated.")
.def_property_readonly(
"n_samples", &FastPedestal<SUM_TYPE>::n_samples,
"Initialization frame count and steady-state update-weight "
"denominator.")
.def(
"clone",
[](FastPedestal<SUM_TYPE> &pedestal) {
return FastPedestal<SUM_TYPE>(pedestal);
},
"Return an independent copy of the pedestal and its state.")
.def(
"push_ema",
[](FastPedestal<SUM_TYPE> &pedestal,
py::array_t<uint16_t, py::array::c_style> &frame) {
pedestal.push_ema(make_view_2d(frame));
},
py::arg("frame").noconvert(),
"Update exponential moving average. The pedstal must "
"already be ready for this update.")
.def(
"add_init_frame",
[](FastPedestal<SUM_TYPE> &pedestal,
py::array_t<uint16_t, py::array::c_style> &frame) {
pedestal.add_init_frame(make_view_2d(frame));
},
py::arg("frame").noconvert(),
"Accumulate one uint16 initialization frame. Call exactly "
"n_samples times to make the pedestal ready.")
.def_static(
"from_file",
[](const std::filesystem::path &filename, uint32_t n_samples,
uint32_t skip_first) {
return FastPedestal<SUM_TYPE>::template from_file<uint16_t>(
filename, n_samples, skip_first);
},
py::arg("filename"), py::arg("n_samples") = 1000,
py::arg("skip_first") = 0,
"Create a pedestal from a uint16 file. Skip skip_first frames, use "
"the next n_samples for initialization, then apply every remaining "
"frame as a steady-state update.");
}
@@ -153,9 +153,13 @@ void define_pedestal_tracking_pixel_histogram_bindings(py::module &m) {
Args:
file_path: Path to the file to fill from
max_frames: Maximum number of frames to fill from the file (default: -1)
reader_threads: Number of parallel file reader workers (default: 2)
reader_chunk_size: Frames claimed by each reader worker per batch (default: 4)
)",
py::call_guard<py::gil_scoped_release>(), py::arg("fname"),
py::arg("max_frames") = -1, py::arg("verbose") = false)
py::arg("max_frames") = -1, py::arg("verbose") = false,
py::arg("reader_threads") = std::size_t{2},
py::arg("reader_chunk_size") = std::size_t{4})
.def("process_pedestal_file",
&PedestalTrackingPixelHistogram::process_pedestal_file,
R"(
+9
View File
@@ -1,6 +1,8 @@
// SPDX-License-Identifier: MPL-2.0
// Files with bindings to the different classes
#include "module_config.hpp"
// New style file naming
#include "bind_Cluster.hpp"
#include "bind_ClusterCollector.hpp"
@@ -11,6 +13,7 @@
#include "bind_ClusterVector.hpp"
#include "bind_Defs.hpp"
#include "bind_Eta.hpp"
#include "bind_FastPedestal.hpp"
#include "bind_Interpolator.hpp"
#include "bind_MultiThreadedFileReader.hpp"
#include "bind_PedestalTrackingPixelHistogram.hpp"
@@ -30,6 +33,7 @@
#include "var_cluster.hpp"
// Pybind stuff
#include <cstdint>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
@@ -75,6 +79,10 @@ PYBIND11_MODULE(_aare, m) {
define_pedestal_tracking_pixel_histogram_bindings(m);
define_pedestal_bindings<double>(m, "Pedestal_d");
define_pedestal_bindings<float>(m, "Pedestal_f");
define_pedestal_bindings<int16_t>(m, "Pedestal_i16");
define_fast_pedestal_bindings<double>(m, "FastPedestal_d");
define_fast_pedestal_bindings<float>(m, "FastPedestal_f");
define_fast_pedestal_bindings<int16_t>(m, "FastPedestal_i16");
define_fit_bindings(m);
define_interpolation_bindings(m);
define_jungfrau_data_file_io_bindings(m);
@@ -106,6 +114,7 @@ PYBIND11_MODULE(_aare, m) {
DEFINE_BINDINGS_CLUSTERFINDER(int, 3, 3, uint16_t, i);
DEFINE_BINDINGS_CLUSTERFINDER(double, 3, 3, uint16_t, d);
DEFINE_BINDINGS_CLUSTERFINDER(float, 3, 3, uint16_t, f);
DEFINE_BINDINGS_CLUSTERFINDER(int16_t, 3, 3, uint16_t, i16);
DEFINE_BINDINGS_CLUSTERFINDER(int, 5, 5, uint16_t, i);
DEFINE_BINDINGS_CLUSTERFINDER(double, 5, 5, uint16_t, d);
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include <cstdint>
// Configure module wide pedestal type for cluster finding
using pd_type = double;
+6
View File
@@ -52,6 +52,12 @@ void define_pedestal_bindings(py::module &m, const std::string &name) {
*std = self.std();
return return_image_data(std);
})
.def("cached_std",
[](Pedestal<SUM_TYPE> &self) {
auto standard_deviation = new NDArray<SUM_TYPE, 2>{};
*standard_deviation = self.cached_std();
return return_image_data(standard_deviation);
})
.def(
"__array_ufunc__",
[](py::object self, py::object ufunc, const std::string &method,
+10 -7
View File
@@ -103,15 +103,18 @@ def test_max_sum():
def test_cluster_finder():
"""Test ClusterFinder"""
shape = [100,100]
cf = _aare.ClusterFinder_Cluster3x3i(shape)
clusterfinder = _aare.ClusterFinder_Cluster3x3i([100,100])
#Push 1000 frames to the pedestal
for i in range(1000):
frame = np.random.normal(loc = 100, scale = 5, size = shape).astype(np.uint16)
cf.push_pedestal_frame(frame)
cf.update_threshold()
frame = np.zeros(shape=shape, dtype=np.uint16)
cf.find_clusters(frame)
#frame = np.random.rand(100,100)
frame = np.zeros(shape=[100,100])
clusterfinder.find_clusters(frame)
clusters = clusterfinder.steal_clusters(False) #conversion does not work
clusters = cf.steal_clusters(False) #conversion does not work
assert clusters.size == 0
+145
View File
@@ -0,0 +1,145 @@
import numpy as np
import pytest
from aare import (
FastPedestal,
FastPedestal_d,
FastPedestal_f,
FastPedestal_i16,
)
@pytest.mark.parametrize(
("dtype", "pedestal_type"),
[
(np.float64, FastPedestal_d),
(np.float32, FastPedestal_f),
(np.int16, FastPedestal_i16),
],
)
def test_fast_pedestal_factory(dtype, pedestal_type):
pedestal = FastPedestal(2, 3, n_samples=4, dtype=dtype)
assert isinstance(pedestal, pedestal_type)
assert pedestal.rows == 2
assert pedestal.cols == 3
assert pedestal.n_samples == 4
def test_fast_pedestal_factory_defaults_to_double():
assert isinstance(FastPedestal(2, 3), FastPedestal_d)
def test_fast_pedestal_factory_rejects_unbound_dtype():
with pytest.raises(ValueError, match="Unsupported dtype for FastPedestal"):
FastPedestal(2, 3, dtype=np.int32)
@pytest.mark.parametrize(
("kwargs", "expected_n_samples"),
[
({"rows": 2, "cols": 3}, 1000),
({"rows": 2, "cols": 3, "n_samples": 4}, 4),
],
)
def test_fast_pedestal_binding_accepts_constructor_keywords(
kwargs, expected_n_samples
):
pedestal = FastPedestal_d(**kwargs)
assert pedestal.rows == 2
assert pedestal.cols == 3
assert pedestal.n_samples == expected_n_samples
@pytest.mark.parametrize(
("dtype", "pedestal_type", "expected_dtype"),
[
(np.float64, FastPedestal_d, np.float64),
(np.float32, FastPedestal_f, np.float32),
(np.int16, FastPedestal_i16, np.int16),
],
)
def test_fast_pedestal_factory_from_file(
tmp_path, dtype, pedestal_type, expected_dtype
):
frames = np.array(
[[[100, 100]], [[2, 4]], [[4, 6]], [[5, 7]]], dtype=np.uint16
)
filename = tmp_path / "frames.npy"
np.save(filename, frames)
pedestal = FastPedestal.from_file(
filename, n_samples=2, skip_first=1, dtype=dtype
)
assert isinstance(pedestal, pedestal_type)
assert pedestal.ready
assert pedestal.cur_samples == 2
assert pedestal.mean().dtype == expected_dtype
np.testing.assert_array_equal(pedestal.mean(), [[4, 6]])
def test_fast_pedestal_factory_from_file_rejects_unbound_dtype():
with pytest.raises(ValueError, match="Unsupported dtype for FastPedestal"):
FastPedestal.from_file("unused.npy", dtype=np.int32)
def test_fast_pedestal_from_file_rejects_skip_beyond_end(tmp_path):
filename = tmp_path / "frames.npy"
np.save(filename, np.zeros((1, 1, 1), dtype=np.uint16))
with pytest.raises(RuntimeError, match="less frames"):
FastPedestal.from_file(filename, n_samples=1, skip_first=2)
@pytest.mark.parametrize(
("pedestal_type", "expected_dtype"),
[(FastPedestal_d, np.float64), (FastPedestal_f, np.float32)],
)
def test_fast_pedestal_initialization(pedestal_type, expected_dtype):
pedestal = pedestal_type(2, 3, 2)
first = np.array([[2, 4, 6], [8, 10, 12]], dtype=np.uint16)
second = np.array([[4, 6, 8], [10, 12, 14]], dtype=np.uint16)
pedestal.add_init_frame(first)
pedestal.add_init_frame(second)
expected_mean = np.array(
[[3, 5, 7], [9, 11, 13]], dtype=expected_dtype
)
np.testing.assert_array_equal(pedestal.mean(), expected_mean)
np.testing.assert_array_equal(pedestal.std(), np.ones((2, 3)))
def test_fast_pedestal_steady_state_push_ema():
pedestal = FastPedestal_d(1, 2, 2)
pedestal.add_init_frame(np.array([[2, 4]], dtype=np.uint16))
pedestal.add_init_frame(np.array([[4, 6]], dtype=np.uint16))
pedestal.push_ema(np.array([[6, 8]], dtype=np.uint16))
np.testing.assert_array_equal(pedestal.mean(), [[4.5, 6.5]])
def test_fast_pedestal_exposes_read_only_buffer_and_subtraction():
pedestal = FastPedestal_d(1, 2, 1)
pedestal.add_init_frame(np.array([[2, 4]], dtype=np.uint16))
view = np.asarray(pedestal)
result = np.array([[12, 14]], dtype=np.uint16) - pedestal
np.testing.assert_array_equal(view, [[2, 4]])
np.testing.assert_array_equal(result, [[10, 10]])
assert np.shares_memory(view, pedestal.view())
assert not view.flags.writeable
def test_fast_pedestal_rejects_wrong_shape():
pedestal = FastPedestal_d(2, 3)
with pytest.raises(RuntimeError, match="shape"):
pedestal.add_init_frame(np.zeros((2, 2), dtype=np.uint16))