Merge branch 'main' into feature/cuda_clusterfinder
Build on RHEL8 / build (push) Successful in 3m34s
Build on RHEL9 / build (push) Successful in 3m42s
Run tests using data on local RHEL8 / build (push) Failing after 4m46s

This commit is contained in:
Khalil Ferjaoui authored and GitHub committed 2026-09-02 14:18:37 +02:00
commit 1d963a778b
100 files changed
+4748 -2464

No files matched your search

+3 -1
View File
@@ -68,8 +68,10 @@ set(PYTHON_FILES
aare/ClusterFinder.py
aare/ClusterVector.py
aare/Cluster.py
aare/FastPedestal.py
aare/calibration.py
aare/func.py
aare/factory.py
aare/experimental.py
aare/RawFile.py
aare/transform.py
aare/ScanParameters.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
+20 -7
View File
@@ -20,8 +20,24 @@ except ImportError:
pass
from . import transform
from . import experimental
from ._aare import File, RawMasterFile, RawSubFile, JungfrauDataFile
from ._aare import (
FastPedestal_d,
FastPedestal_f,
FastPedestal_i16,
Pedestal_d,
Pedestal_f,
Pedestal_i16,
ClusterFinder_Cluster3x3i,
VarClusterFinder,
)
from ._aare import (
File,
JungfrauDataFile,
RawMasterFile,
RawSubFile,
)
from ._aare import Pedestal_d, Pedestal_f, ClusterFinder_Cluster3x3i, VarClusterFinder
from ._aare import DetectorType, ReadoutMode
from ._aare import hitmap
@@ -31,14 +47,14 @@ from ._aare import corner
# from ._aare import ClusterFinderMT, ClusterCollector, ClusterFileSink, ClusterVector_i
from ._version import __version__
from .ClusterFinder import ClusterFinder, ClusterFinderFrozen, ClusterCollector, ClusterFinderMT, ClusterFileSink, ClusterFile
from .ClusterFinder import ClusterFinderCUDA, ClusterFinderCUDAGraph, _cuda_available, find_cluster_views_batched_iter
from .FastPedestal import FastPedestal
from .ClusterFinder import (ClusterFinder, ClusterFinderFrozen, ClusterCollector, ClusterFinderMT, ClusterFileSink, ClusterFile,
ClusterFinderCUDA, ClusterFinderCUDAGraph, _cuda_available, find_cluster_views_batched_iter)
from .ClusterVector import ClusterVector
from .Cluster import Cluster
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
from ._aare import calculate_eta2, calculate_eta3, calculate_cross_eta3, calculate_full_eta2
from ._aare import reduce_to_2x2, reduce_to_3x3
@@ -54,9 +70,6 @@ from .ScanParameters import ScanParameters
from .utils import random_pixels, random_pixel, flat_list, add_colorbar, Timer
#make functions available in the top level API
from .func import *
from .calibration import *
from ._aare import apply_calibration, count_switching_pixels
from ._aare import calculate_pedestal, calculate_pedestal_float, calculate_pedestal_g0, calculate_pedestal_g0_float
+6
View File
@@ -0,0 +1,6 @@
# SPDX-License-Identifier: MPL-2.0
"""Experimental APIs that may change without notice."""
from ._aare.experimental import MultiThreadedFileReader
__all__ = ["MultiThreadedFileReader"]
+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
-2
View File
@@ -1,2 +0,0 @@
# SPDX-License-Identifier: MPL-2.0
from ._aare import gaus, pol1, scurve, scurve2
+42 -70
View File
@@ -1,106 +1,78 @@
# SPDX-License-Identifier: MPL-2.0
import matplotlib.pyplot as plt
import numpy as np
import sys
sys.path.insert(0, '/home/kferjaoui/sw/aare/build')
from aare import fit_gaus, fit_pol1
from aare import Gaussian, fit
from aare import pol1
textpm = f"±" #
textmu = f"μ" #
textsigma = f"σ" #
from aare import Gaussian, Pol1
textpm = "±"
textmu = "μ"
textsigma = "σ"
# ================================= Gauss fit =================================
# Parameters
mu = np.random.uniform(1, 100) # Mean of Gaussian
sigma = np.random.uniform(4, 20) # Standard deviation
num_points = 10000 # Number of points for smooth distribution
# noise_sigma = 10
# Generate Gaussian distribution
data = np.random.normal(mu, sigma, num_points)
mu = np.random.uniform(1, 100)
sigma = np.random.uniform(4, 20)
data = np.random.normal(mu, sigma, 10000)
counts, edges = np.histogram(data, bins=100)
x = 0.5 * (edges[:-1] + edges[1:]) # proper bin centers
x = 0.5 * (edges[:-1] + edges[1:])
y = counts.astype(np.float64)
# Poisson noise
yerr = np.sqrt(np.maximum(y, 1))
# yerr = np.abs(np.random.normal(0, noise_sigma, len(x)))
# Create subplot
fig0, ax0 = plt.subplots(1, 1, num=0, figsize=(12, 8))
# Add the errors as error bars in the step plot
ax0.errorbar(x, y, yerr=yerr, fmt=". ", capsize=5)
ax0.grid()
# Fit with lmfit
result_lm = fit_gaus(x, y, yerr)
par_lm = result_lm["par"]
err_lm = result_lm["par_err"]
chi2_lm = result_lm["chi2"]
print("[lmfit] fit_gaus: ", par_lm, err_lm, chi2_lm)
gaussian = Gaussian(compute_errors=True)
result = gaussian.fit(x, y, yerr)
par = result["par"]
err = result["par_err"]
chi2 = result["chi2"]
print(f"Gaussian.fit: par={par}, err={err}, chi2={chi2}")
# Fit with Minuit2 + analytic gradient + Hesse errors
gaussian = Gaussian()
gaussian.compute_errors = True
result_m2 = gaussian.fit(x, y, yerr)
par_m2 = result_m2['par']
err_m2 = result_m2['par_err']
chi2_m2 = result_m2['chi2']
print(f"[minuit2] gaussian.fit: par={par_m2}, err={err_m2}, chi2={chi2_m2}")
x = np.linspace(x[0], x[-1], 1000)
ax0.plot(x, gaussian(x, par_lm), marker="", label="fit_gaus")
ax0.plot(x, gaussian(x, par_m2), marker="", linestyle=":", label="fit_gaus_minuit_grad")
x_plot = np.linspace(x[0], x[-1], 1000)
ax0.plot(x_plot, gaussian(x_plot, par), marker="", label="Gaussian.fit")
ax0.legend()
ax0.set(xlabel="x", ylabel="Counts",
ax0.set(
xlabel="x",
ylabel="Counts",
title=(
f"fit_gaus: A={par_lm[0]:0.2f}{textpm}{err_lm[0]:0.2f} "
f"{textmu}={par_lm[1]:0.2f}{textpm}{err_lm[1]:0.2f} "
f"{textsigma}={par_lm[2]:0.2f}{textpm}{err_lm[2]:0.2f}\n"
f"minuit_grad: A={par_m2[0]:0.2f}{textpm}{err_m2[0]:0.2f} "
f"{textmu}={par_m2[1]:0.2f}{textpm}{err_m2[1]:0.2f} "
f"{textsigma}={par_m2[2]:0.2f}{textpm}{err_m2[2]:0.2f}\n"
f"A={par[0]:0.2f}{textpm}{err[0]:0.2f} "
f"{textmu}={par[1]:0.2f}{textpm}{err[1]:0.2f} "
f"{textsigma}={par[2]:0.2f}{textpm}{err[2]:0.2f}\n"
f"(truth: {textmu}={mu:0.2f}, {textsigma}={sigma:0.2f})"
),
)
fig0.tight_layout()
# ================================= pol1 fit =================================
# Parameters
# ================================= Pol1 fit =================================
n_points = 40
# Generate random slope and intercept (origin)
slope = np.random.uniform(-10, 10) # Random slope between 0.5 and 2.0
intercept = np.random.uniform(-10, 10) # Random intercept between -10 and 10
# Generate random x values
slope = np.random.uniform(-10, 10)
intercept = np.random.uniform(-10, 10)
x_values = np.random.uniform(-10, 10, n_points)
# Calculate y values based on the linear function y = mx + b + error
errors = np.abs(np.random.normal(0, np.random.uniform(1, 5), n_points))
var_points = np.random.normal(0, np.random.uniform(0.1, 2), n_points)
y_values = slope * x_values + intercept + var_points
y_values = slope * x_values + intercept + np.random.normal(0, 1, n_points)
fig1, ax1 = plt.subplots(1, 1, num=1, figsize=(12, 8))
ax1.errorbar(x_values, y_values, yerr=errors, fmt=". ", capsize=5)
result_pol = fit_pol1(x_values, y_values, errors)
par = result_pol["par"]
err = result_pol["par_err"]
x = np.linspace(np.min(x_values), np.max(x_values), 1000)
ax1.plot(x, pol1(x, par), marker="")
ax1.set(xlabel="x", ylabel="y", title=f"a = {par[0]:0.2f}{textpm}{err[0]:0.2f}\n"
f"b = {par[1]:0.2f}{textpm}{err[1]:0.2f}\n"
f"(init: {slope:0.2f}, {intercept:0.2f})")
pol1 = Pol1(compute_errors=True)
result = pol1.fit(x_values, y_values, errors)
par = result["par"]
err = result["par_err"]
x_plot = np.linspace(np.min(x_values), np.max(x_values), 1000)
ax1.plot(x_plot, pol1(x_plot, par), marker="")
ax1.set(
xlabel="x",
ylabel="y",
title=(
f"intercept = {par[0]:0.2f}{textpm}{err[0]:0.2f}\n"
f"slope = {par[1]:0.2f}{textpm}{err[1]:0.2f}\n"
f"(truth: {intercept:0.2f}, {slope:0.2f})"
),
)
fig1.tight_layout()
plt.show()
-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",
+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 -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)
+2 -2
View File
@@ -15,7 +15,6 @@
#include <pybind11/stl_bind.h>
namespace py = pybind11;
using pd_type = double;
using namespace aare;
@@ -72,6 +71,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 +169,4 @@ void define_3x3_reduction(py::module &m) {
py::arg("clustervector"));
}
#pragma GCC diagnostic pop
#pragma GCC diagnostic pop
+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.");
}
+175
View File
@@ -0,0 +1,175 @@
// SPDX-License-Identifier: MPL-2.0
#pragma once
#include "aare/MultiThreadedFileReader.hpp"
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <optional>
#include <stdexcept>
#include <vector>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <pybind11/stl/filesystem.h>
namespace py = pybind11;
inline py::dtype multi_threaded_reader_numpy_dtype(const aare::Dtype &dtype) {
using aare::Dtype;
if (dtype == Dtype::INT8)
return py::dtype::of<int8_t>();
if (dtype == Dtype::UINT8)
return py::dtype::of<uint8_t>();
if (dtype == Dtype::INT16)
return py::dtype::of<int16_t>();
if (dtype == Dtype::UINT16)
return py::dtype::of<uint16_t>();
if (dtype == Dtype::INT32)
return py::dtype::of<int32_t>();
if (dtype == Dtype::UINT32)
return py::dtype::of<uint32_t>();
if (dtype == Dtype::INT64)
return py::dtype::of<int64_t>();
if (dtype == Dtype::UINT64)
return py::dtype::of<uint64_t>();
if (dtype == Dtype::FLOAT)
return py::dtype::of<float>();
if (dtype == Dtype::DOUBLE)
return py::dtype::of<double>();
throw std::runtime_error("Unsupported pixel data type");
}
inline py::array
multi_threaded_reader_read(aare::experimental::MultiThreadedFileReader &reader,
bool read_all) {
const size_t n_frames =
read_all ? reader.remaining_frames() : reader.next_read_frames();
const std::vector<py::ssize_t> shape{
static_cast<py::ssize_t>(n_frames),
static_cast<py::ssize_t>(reader.rows()),
static_cast<py::ssize_t>(reader.cols())};
py::array image(multi_threaded_reader_numpy_dtype(reader.dtype()), shape);
auto *destination = reinterpret_cast<std::byte *>(image.mutable_data());
{
py::gil_scoped_release release;
if (read_all) {
size_t offset = 0;
while (reader.remaining_frames() != 0) {
const size_t frames_read =
reader.read_into(destination + offset);
offset += frames_read * reader.bytes_per_frame();
}
} else {
reader.read_into(destination);
}
}
return image;
}
inline void define_multi_threaded_file_reader_bindings(py::module_ &m) {
using aare::experimental::MultiThreadedFileReader;
auto reader =
py::class_<MultiThreadedFileReader>(m, "MultiThreadedFileReader");
reader.attr("__module__") = "aare.experimental";
reader
.def(py::init<std::filesystem::path, size_t, size_t,
std::optional<size_t>>(),
py::arg("fname"), py::arg("n_threads"), py::arg("chunk_size"),
py::arg("total_frames") = py::none(),
R"doc(
Read chunks of detector frames concurrently.
Each worker opens an independent File. The returned array is
ordered by frame index even though chunks are read in parallel.
Args:
fname: Path accepted by File.
n_threads: Maximum number of worker threads.
chunk_size: Number of frames read per claimed chunk.
total_frames: Optional frame limit. None reads all frames.
)doc")
.def(
"read",
[](MultiThreadedFileReader &self) {
return multi_threaded_reader_read(self, false);
},
R"doc(
Read one chunk per active worker into a NumPy array.
Returns:
An array containing at most n_threads * chunk_size frames.
An empty array is returned at the end of the configured
frame range. The GIL is released while file data is read.
)doc")
.def(
"read_all",
[](MultiThreadedFileReader &self) {
return multi_threaded_reader_read(self, true);
},
R"doc(
Read all frames remaining from the current position.
)doc")
.def_property_readonly("n_threads", &MultiThreadedFileReader::n_threads)
.def_property_readonly("chunk_size",
&MultiThreadedFileReader::chunk_size)
.def_property_readonly("total_frames",
&MultiThreadedFileReader::total_frames)
.def_property_readonly("source_total_frames",
&MultiThreadedFileReader::source_total_frames)
.def_property_readonly("rows", &MultiThreadedFileReader::rows)
.def_property_readonly("cols", &MultiThreadedFileReader::cols)
.def_property_readonly("bitdepth", &MultiThreadedFileReader::bitdepth)
.def_property_readonly("dtype",
[](const MultiThreadedFileReader &self) {
return multi_threaded_reader_numpy_dtype(
self.dtype());
})
.def_property_readonly("bytes_per_frame",
&MultiThreadedFileReader::bytes_per_frame)
.def_property_readonly("total_bytes",
&MultiThreadedFileReader::total_bytes)
.def_property_readonly("remaining_frames",
&MultiThreadedFileReader::remaining_frames)
.def_property_readonly("next_read_frames",
&MultiThreadedFileReader::next_read_frames)
.def_property_readonly("next_read_bytes",
&MultiThreadedFileReader::next_read_bytes)
.def("seek", &MultiThreadedFileReader::seek, py::arg("frame_index"))
.def("tell", &MultiThreadedFileReader::tell)
.def("close", &MultiThreadedFileReader::close,
"Close all worker files. Safe to call more than once.")
.def_property_readonly(
"closed",
[](const MultiThreadedFileReader &self) { return !self.is_open(); })
.def("__len__", &MultiThreadedFileReader::total_frames)
.def(
"__enter__",
[](MultiThreadedFileReader &self) -> MultiThreadedFileReader * {
if (!self.is_open()) {
throw std::runtime_error(
"Cannot enter a closed MultiThreadedFileReader");
}
return &self;
},
py::return_value_policy::reference_internal)
.def("__exit__",
[](MultiThreadedFileReader &self, const py::object &,
const py::object &, const py::object &) {
self.close();
return false;
})
.def(
"__iter__", [](MultiThreadedFileReader &self) { return &self; },
py::return_value_policy::reference_internal)
.def("__next__", [](MultiThreadedFileReader &self) {
if (self.remaining_frames() == 0) {
throw py::stop_iteration();
}
return multi_threaded_reader_read(self, false);
});
}
@@ -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"(
-1
View File
@@ -9,7 +9,6 @@
#include "aare/decode.hpp"
#include "aare/defs.hpp"
// #include "aare/fClusterFileV2.hpp"
#include "np_helper.hpp"
+1 -50
View File
@@ -7,7 +7,6 @@
#include "aare/RawSubFile.hpp"
#include "aare/defs.hpp"
// #include "aare/fClusterFileV2.hpp"
#include <cstdint>
#include <filesystem>
@@ -195,52 +194,4 @@ void define_file_io_bindings(py::module &m) {
});
#pragma GCC diagnostic pop
// py::class_<ClusterHeader>(m, "ClusterHeader")
// .def(py::init<>())
// .def_readwrite("frame_number", &ClusterHeader::frame_number)
// .def_readwrite("n_clusters", &ClusterHeader::n_clusters)
// .def("__repr__", [](const ClusterHeader &a) { return "<ClusterHeader:
// " + a.to_string() + ">"; });
// py::class_<ClusterV2_>(m, "ClusterV2_")
// .def(py::init<>())
// .def_readwrite("x", &ClusterV2_::x)
// .def_readwrite("y", &ClusterV2_::y)
// .def_readwrite("data", &ClusterV2_::data)
// .def("__repr__", [](const ClusterV2_ &a) { return "<ClusterV2_: " +
// a.to_string(false) + ">"; });
// py::class_<ClusterV2>(m, "ClusterV2")
// .def(py::init<>())
// .def_readwrite("cluster", &ClusterV2::cluster)
// .def_readwrite("frame_number", &ClusterV2::frame_number)
// .def("__repr__", [](const ClusterV2 &a) { return "<ClusterV2: " +
// a.to_string() + ">"; });
// py::class_<ClusterFileV2>(m, "ClusterFileV2")
// .def(py::init<const std::filesystem::path &, const std::string &>())
// .def("read", py::overload_cast<>(&ClusterFileV2::read))
// .def("read", py::overload_cast<int>(&ClusterFileV2::read))
// .def("frame_number", &ClusterFileV2::frame_number)
// .def("write", py::overload_cast<std::vector<ClusterV2> const
// &>(&ClusterFileV2::write))
// .def("close", &ClusterFileV2::close);
// m.def("to_clustV2", [](std::vector<DynamicCluster> &clusters, const int
// frame_number) {
// std::vector<ClusterV2> clusters_;
// for (auto &c : clusters) {
// ClusterV2 cluster;
// cluster.cluster.x = c.x;
// cluster.cluster.y = c.y;
// int i=0;
// for(auto &d : cluster.cluster.data) {
// d=c.get<double>(i++);
// }
// cluster.frame_number = frame_number;
// clusters_.push_back(cluster);
// }
// return clusters_;
// });
}
}
+1 -447
View File
@@ -227,452 +227,6 @@ fit_dispatch(const aare::FitModel<Model> &model,
}
void define_fit_bindings(py::module &m) {
// TODO! Evaluate without converting to double
m.def(
"gaus",
[](py::array_t<double, py::array::c_style | py::array::forcecast> x,
py::array_t<double, py::array::c_style | py::array::forcecast> par) {
auto x_view = make_view_1d(x);
auto par_view = make_view_1d(par);
auto y = new NDArray<double, 1>{aare::func::gaus(x_view, par_view)};
return return_image_data(y);
},
R"(
Evaluate a 1D Gaussian function for all points in x using parameters par.
Parameters
----------
x : array_like
The points at which to evaluate the Gaussian function.
par : array_like
The parameters of the Gaussian function. The first element is the amplitude, the second element is the mean, and the third element is the standard deviation.
)",
py::arg("x"), py::arg("par"));
m.def(
"pol1",
[](py::array_t<double, py::array::c_style | py::array::forcecast> x,
py::array_t<double, py::array::c_style | py::array::forcecast> par) {
auto x_view = make_view_1d(x);
auto par_view = make_view_1d(par);
auto y = new NDArray<double, 1>{aare::func::pol1(x_view, par_view)};
return return_image_data(y);
},
R"(
Evaluate a 1D polynomial function for all points in x using parameters par. (p0+p1*x)
Parameters
----------
x : array_like
The points at which to evaluate the polynomial function.
par : array_like
The parameters of the polynomial function. The first element is the intercept, and the second element is the slope.
)",
py::arg("x"), py::arg("par"));
m.def(
"scurve",
[](py::array_t<double, py::array::c_style | py::array::forcecast> x,
py::array_t<double, py::array::c_style | py::array::forcecast> par) {
auto x_view = make_view_1d(x);
auto par_view = make_view_1d(par);
auto y =
new NDArray<double, 1>{aare::func::scurve(x_view, par_view)};
return return_image_data(y);
},
R"(
Evaluate a 1D scurve function for all points in x using parameters par.
Parameters
----------
x : array_like
The points at which to evaluate the scurve function.
par : array_like
The parameters of the scurve function. The first element is the background slope, the second element is the background intercept, the third element is the mean, the fourth element is the standard deviation, the fifth element is inflexion point count number, and the sixth element is C.
)",
py::arg("x"), py::arg("par"));
m.def(
"scurve2",
[](py::array_t<double, py::array::c_style | py::array::forcecast> x,
py::array_t<double, py::array::c_style | py::array::forcecast> par) {
auto x_view = make_view_1d(x);
auto par_view = make_view_1d(par);
auto y =
new NDArray<double, 1>{aare::func::scurve2(x_view, par_view)};
return return_image_data(y);
},
R"(
Evaluate a 1D scurve2 function for all points in x using parameters par.
Parameters
----------
x : array_like
The points at which to evaluate the scurve function.
par : array_like
The parameters of the scurve2 function. The first element is the background slope, the second element is the background intercept, the third element is the mean, the fourth element is the standard deviation, the fifth element is inflexion point count number, and the sixth element is C.
)",
py::arg("x"), py::arg("par"));
m.def(
"fit_gaus",
[](py::array_t<double, py::array::c_style | py::array::forcecast> x,
py::array_t<double, py::array::c_style | py::array::forcecast> y,
int n_threads) {
if (y.ndim() == 3) {
auto par = new NDArray<double, 3>{};
auto y_view = make_view_3d(y);
auto x_view = make_view_1d(x);
*par = aare::fit_gaus(x_view, y_view, n_threads);
return return_image_data(par);
} else if (y.ndim() == 1) {
auto par = new NDArray<double, 1>{};
auto y_view = make_view_1d(y);
auto x_view = make_view_1d(x);
*par = aare::fit_gaus(x_view, y_view);
return return_image_data(par);
} else {
throw std::runtime_error("Data must be 1D or 3D");
}
},
R"(
Fit a 1D Gaussian to data.
Parameters
----------
x : array_like
The x values.
y : array_like
The y values.
n_threads : int, optional
The number of threads to use. Default is 4.
)",
py::arg("x"), py::arg("y"), py::arg("n_threads") = 4);
m.def(
"fit_gaus",
[](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::array_t<double, py::array::c_style | py::array::forcecast> y_err,
int n_threads) {
if (y.ndim() == 3) {
// Allocate memory for the output
// Need to have pointers to allow python to manage
// the memory
auto par = new NDArray<double, 3>({y.shape(0), y.shape(1), 3});
auto par_err =
new NDArray<double, 3>({y.shape(0), y.shape(1), 3});
auto chi2 = new NDArray<double, 2>({y.shape(0), y.shape(1)});
// Make views of the numpy arrays
auto y_view = make_view_3d(y);
auto y_view_err = make_view_3d(y_err);
auto x_view = make_view_1d(x);
aare::fit_gaus(x_view, y_view, y_view_err, par->view(),
par_err->view(), chi2->view(), n_threads);
return py::dict("par"_a = return_image_data(par),
"par_err"_a = return_image_data(par_err),
"chi2"_a = return_image_data(chi2),
"Ndf"_a = y.shape(2) - 3);
} else if (y.ndim() == 1) {
// Allocate memory for the output
// Need to have pointers to allow python to manage
// the memory
auto par = new NDArray<double, 1>({3});
auto par_err = new NDArray<double, 1>({3});
// Decode the numpy arrays
auto y_view = make_view_1d(y);
auto y_view_err = make_view_1d(y_err);
auto x_view = make_view_1d(x);
double chi2 = 0;
aare::fit_gaus(x_view, y_view, y_view_err, par->view(),
par_err->view(), chi2);
return py::dict("par"_a = return_image_data(par),
"par_err"_a = return_image_data(par_err),
"chi2"_a = chi2, "Ndf"_a = y.size() - 3);
} else {
throw std::runtime_error("Data must be 1D or 3D");
}
},
R"(
Fit a 1D Gaussian to data with error estimates.
Parameters
----------
x : array_like
The x values.
y : array_like
The y values.
y_err : array_like
The error in the y values.
n_threads : int, optional
The number of threads to use. Default is 4.
)",
py::arg("x"), py::arg("y"), py::arg("y_err"), py::arg("n_threads") = 4);
m.def(
"fit_pol1",
[](py::array_t<double, py::array::c_style | py::array::forcecast> x,
py::array_t<double, py::array::c_style | py::array::forcecast> y,
int n_threads) {
if (y.ndim() == 3) {
auto par = new NDArray<double, 3>{};
auto x_view = make_view_1d(x);
auto y_view = make_view_3d(y);
*par = aare::fit_pol1(x_view, y_view, n_threads);
return return_image_data(par);
} else if (y.ndim() == 1) {
auto par = new NDArray<double, 1>{};
auto x_view = make_view_1d(x);
auto y_view = make_view_1d(y);
*par = aare::fit_pol1(x_view, y_view);
return return_image_data(par);
} else {
throw std::runtime_error("Data must be 1D or 3D");
}
},
py::arg("x"), py::arg("y"), py::arg("n_threads") = 4);
m.def(
"fit_pol1",
[](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::array_t<double, py::array::c_style | py::array::forcecast> y_err,
int n_threads) {
if (y.ndim() == 3) {
auto par = new NDArray<double, 3>({y.shape(0), y.shape(1), 2});
auto par_err =
new NDArray<double, 3>({y.shape(0), y.shape(1), 2});
auto y_view = make_view_3d(y);
auto y_view_err = make_view_3d(y_err);
auto x_view = make_view_1d(x);
auto chi2 = new NDArray<double, 2>({y.shape(0), y.shape(1)});
aare::fit_pol1(x_view, y_view, y_view_err, par->view(),
par_err->view(), chi2->view(), n_threads);
return py::dict("par"_a = return_image_data(par),
"par_err"_a = return_image_data(par_err),
"chi2"_a = return_image_data(chi2),
"Ndf"_a = y.shape(2) - 2);
} else if (y.ndim() == 1) {
auto par = new NDArray<double, 1>({2});
auto par_err = new NDArray<double, 1>({2});
auto y_view = make_view_1d(y);
auto y_view_err = make_view_1d(y_err);
auto x_view = make_view_1d(x);
double chi2 = 0;
aare::fit_pol1(x_view, y_view, y_view_err, par->view(),
par_err->view(), chi2);
return py::dict("par"_a = return_image_data(par),
"par_err"_a = return_image_data(par_err),
"chi2"_a = chi2, "Ndf"_a = y.size() - 2);
} else {
throw std::runtime_error("Data must be 1D or 3D");
}
},
R"(
Fit a 1D polynomial to data with error estimates.
Parameters
----------
x : array_like
The x values.
y : array_like
The y values.
y_err : array_like
The error in the y values.
n_threads : int, optional
The number of threads to use. Default is 4.
)",
py::arg("x"), py::arg("y"), py::arg("y_err"), py::arg("n_threads") = 4);
//=========
m.def(
"fit_scurve",
[](py::array_t<double, py::array::c_style | py::array::forcecast> x,
py::array_t<double, py::array::c_style | py::array::forcecast> y,
int n_threads) {
if (y.ndim() == 3) {
auto par = new NDArray<double, 3>{};
auto x_view = make_view_1d(x);
auto y_view = make_view_3d(y);
*par = aare::fit_scurve(x_view, y_view, n_threads);
return return_image_data(par);
} else if (y.ndim() == 1) {
auto par = new NDArray<double, 1>{};
auto x_view = make_view_1d(x);
auto y_view = make_view_1d(y);
*par = aare::fit_scurve(x_view, y_view);
return return_image_data(par);
} else {
throw std::runtime_error("Data must be 1D or 3D");
}
},
py::arg("x"), py::arg("y"), py::arg("n_threads") = 4);
m.def(
"fit_scurve",
[](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::array_t<double, py::array::c_style | py::array::forcecast> y_err,
int n_threads) {
if (y.ndim() == 3) {
auto par = new NDArray<double, 3>({y.shape(0), y.shape(1), 6});
auto par_err =
new NDArray<double, 3>({y.shape(0), y.shape(1), 6});
auto y_view = make_view_3d(y);
auto y_view_err = make_view_3d(y_err);
auto x_view = make_view_1d(x);
auto chi2 = new NDArray<double, 2>({y.shape(0), y.shape(1)});
aare::fit_scurve(x_view, y_view, y_view_err, par->view(),
par_err->view(), chi2->view(), n_threads);
return py::dict("par"_a = return_image_data(par),
"par_err"_a = return_image_data(par_err),
"chi2"_a = return_image_data(chi2),
"Ndf"_a = y.shape(2) - 2);
} else if (y.ndim() == 1) {
auto par = new NDArray<double, 1>({2});
auto par_err = new NDArray<double, 1>({2});
auto y_view = make_view_1d(y);
auto y_view_err = make_view_1d(y_err);
auto x_view = make_view_1d(x);
double chi2 = 0;
aare::fit_scurve(x_view, y_view, y_view_err, par->view(),
par_err->view(), chi2);
return py::dict("par"_a = return_image_data(par),
"par_err"_a = return_image_data(par_err),
"chi2"_a = chi2, "Ndf"_a = y.size() - 2);
} else {
throw std::runtime_error("Data must be 1D or 3D");
}
},
R"(
Fit a 1D polynomial to data with error estimates.
Parameters
----------
x : array_like
The x values.
y : array_like
The y values.
y_err : array_like
The error in the y values.
n_threads : int, optional
The number of threads to use. Default is 4.
)",
py::arg("x"), py::arg("y"), py::arg("y_err"), py::arg("n_threads") = 4);
m.def(
"fit_scurve2",
[](py::array_t<double, py::array::c_style | py::array::forcecast> x,
py::array_t<double, py::array::c_style | py::array::forcecast> y,
int n_threads) {
if (y.ndim() == 3) {
auto par = new NDArray<double, 3>{};
auto x_view = make_view_1d(x);
auto y_view = make_view_3d(y);
*par = aare::fit_scurve2(x_view, y_view, n_threads);
return return_image_data(par);
} else if (y.ndim() == 1) {
auto par = new NDArray<double, 1>{};
auto x_view = make_view_1d(x);
auto y_view = make_view_1d(y);
*par = aare::fit_scurve2(x_view, y_view);
return return_image_data(par);
} else {
throw std::runtime_error("Data must be 1D or 3D");
}
},
py::arg("x"), py::arg("y"), py::arg("n_threads") = 4);
m.def(
"fit_scurve2",
[](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::array_t<double, py::array::c_style | py::array::forcecast> y_err,
int n_threads) {
if (y.ndim() == 3) {
auto par = new NDArray<double, 3>({y.shape(0), y.shape(1), 6});
auto par_err =
new NDArray<double, 3>({y.shape(0), y.shape(1), 6});
auto y_view = make_view_3d(y);
auto y_view_err = make_view_3d(y_err);
auto x_view = make_view_1d(x);
auto chi2 = new NDArray<double, 2>({y.shape(0), y.shape(1)});
aare::fit_scurve2(x_view, y_view, y_view_err, par->view(),
par_err->view(), chi2->view(), n_threads);
return py::dict("par"_a = return_image_data(par),
"par_err"_a = return_image_data(par_err),
"chi2"_a = return_image_data(chi2),
"Ndf"_a = y.shape(2) - 2);
} else if (y.ndim() == 1) {
auto par = new NDArray<double, 1>({6});
auto par_err = new NDArray<double, 1>({6});
auto y_view = make_view_1d(y);
auto y_view_err = make_view_1d(y_err);
auto x_view = make_view_1d(x);
double chi2 = 0;
aare::fit_scurve2(x_view, y_view, y_view_err, par->view(),
par_err->view(), chi2);
return py::dict("par"_a = return_image_data(par),
"par_err"_a = return_image_data(par_err),
"chi2"_a = chi2, "Ndf"_a = y.size() - 2);
} else {
throw std::runtime_error("Data must be 1D or 3D");
}
},
R"(
Fit a 1D polynomial to data with error estimates.
Parameters
----------
x : array_like
The x values.
y : array_like
The y values.
y_err : array_like
The error in the y values.
n_threads : int, optional
The number of threads to use. Default is 4.
)",
py::arg("x"), py::arg("y"), py::arg("y_err"), py::arg("n_threads") = 4);
// ── Bind model classes ──────────────────────────────────────────
bind_fit_model<aare::model::Gaussian>(m, "Gaussian");
bind_fit_model<aare::model::GaussianErfcPlateau>(m, "GaussianErfcPlateau");
@@ -794,4 +348,4 @@ void define_fit_bindings(py::module &m) {
)",
py::arg("model"), py::arg("x"), py::arg("y"),
py::arg("y_err") = py::none(), py::arg("n_threads") = 4);
}
}
+14
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"
@@ -12,7 +14,9 @@
#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"
#include "bind_PixelHistogram.hpp"
#include "bind_PixelMap.hpp"
@@ -30,6 +34,7 @@
#include "var_cluster.hpp"
// Pybind stuff
#include <cstdint>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
@@ -61,7 +66,11 @@ double, 'f' for float)
define_ClusterCollector<T, N, M, U>(m, "Cluster" #N "x" #M #TYPE_CODE);
PYBIND11_MODULE(_aare, m) {
auto experimental = m.def_submodule(
"experimental", "Experimental APIs that may change without notice");
define_file_io_bindings(m);
define_multi_threaded_file_reader_bindings(experimental);
define_raw_file_io_bindings(m);
define_raw_sub_file_io_bindings(m);
define_ctb_raw_file_io_bindings(m);
@@ -72,6 +81,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);
@@ -103,6 +116,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,
-1
View File
@@ -8,7 +8,6 @@
#include "aare/RawSubFile.hpp"
#include "aare/defs.hpp"
// #include "aare/fClusterFileV2.hpp"
#include <cstdint>
#include <filesystem>
-1
View File
@@ -7,7 +7,6 @@
#include "aare/RawSubFile.hpp"
#include "aare/defs.hpp"
// #include "aare/fClusterFileV2.hpp"
#include <cstdint>
#include <filesystem>
-6
View File
@@ -1,17 +1,11 @@
// SPDX-License-Identifier: MPL-2.0
#include "aare/VarClusterFinder.hpp"
#include "np_helper.hpp"
// #include "aare/defs.hpp"
// #include "aare/fClusterFileV2.hpp"
#include <cstdint>
// #include <filesystem>
#include <pybind11/numpy.h>
// #include <pybind11/iostream.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
// #include <pybind11/stl/filesystem.h>
// #include <string>
namespace py = pybind11;
using namespace ::aare;
+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
+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()
+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))
+15
View File
@@ -0,0 +1,15 @@
# SPDX-License-Identifier: MPL-2.0
import numpy as np
import aare
def test_gaussian_model_evaluates_and_fits_data():
x = np.linspace(-5.0, 5.0, 51)
expected = np.array([20.0, 0.5, 1.2])
model = aare.Gaussian()
y = model(x, expected)
result = model.fit(x, y)
np.testing.assert_allclose(result["par"], expected, atol=2e-3)
+408 -483
View File
@@ -1,488 +1,413 @@
{
"cells":
[
{
"cell_type": "code",
"execution_count": 2,
"id": "efef8e20-6571-4561-8f0b-6048b57907de",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"import random\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.gridspec import GridSpec\n",
"import sys\n",
"sys.path.insert(0, '/home/ferjao_k/aare/build')\n",
"from aare import fit_gaus # lmfit\n",
"from aare import Gaussian, fit # minuit2"
]
"cells": [
{
"cell_type": "code",
"execution_count": null,
"id": "efef8e20-6571-4561-8f0b-6048b57907de",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"import random\n",
"import numpy as np\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.gridspec import GridSpec\n",
"from aare import Gaussian\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "35772e2c-37c6-4986-a9c0-7dca4a034827",
"metadata": {},
"outputs": [],
"source": [
"ROWS = 100\n",
"COLS = 100\n",
"N_SCAN = 100\n",
"NOISE_FRAC = 0.05\n",
"SEED = 42\n",
"N_THREADS = 4\n",
"\n",
"N_REPEATS = 7\n",
"N_WARMUP = 3 # untimed iterations (icache + branch predictor warmup)\n",
"COOLDOWN = 2.0 # seconds between (method, thread_count) pairs"
]
},
{
"cell_type": "markdown",
"id": "be455445-7df8-47cc-9f39-d2a38856e0bd",
"metadata": {},
"source": [
"## Data generator"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "06c8fddb-56d9-4f84-8b21-4ffd8b7bd26f",
"metadata": {},
"outputs": [],
"source": [
"def generate_3d_data(rows, cols, n_scan, noise_frac, seed):\n",
" \"\"\"\n",
" Generate a synthetic detector image stack where each pixel has a\n",
" Gaussian response curve with per-pixel variation in A, mu, sigma.\n",
"\n",
" Returns x (n_scan,), y (rows, cols, n_scan), y_err (rows, cols, n_scan),\n",
" and the ground-truth parameter arrays.\n",
" \"\"\"\n",
" rng = np.random.default_rng(seed)\n",
"\n",
" # Per-pixel true params each of shape: [rows, cols, 1]\n",
" A_true = rng.uniform(200, 1000, size=(rows, cols))\n",
" mu_true = rng.uniform(20, 80, size=(rows, cols))\n",
" sig_true = rng.uniform(3, 12, size=(rows, cols))\n",
" \n",
" # One common binned energy array\n",
" x = np.linspace(0, 100, n_scan) # shape [1, 1, nscan]\n",
"\n",
" # Build ground truth signals per-pixel\n",
" exponent = -0.5 * ((x[None, None, :] - mu_true[:, :, None]) / sig_true[:,:, None])**2 # shape [rows, cols, nscan]\n",
" y_clean = A_true[:, :, None] * np.exp(exponent)\n",
"\n",
" # Perturb with noise\n",
" noise_sigma = noise_frac * A_true[:, :, None] * np.ones_like(y_clean) # shape [rows, cols, nscan]\n",
" noise = rng.normal(0, noise_sigma)\n",
" y = y_clean + noise\n",
"\n",
" y_err = noise_sigma.copy()\n",
"\n",
" return x, y, y_err, A_true, mu_true, sig_true "
]
},
{
"cell_type": "markdown",
"id": "30ed6bb8-4798-497f-9990-4c518f885855",
"metadata": {},
"source": [
"## Profiling function"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dc0b52b6-9fc3-453d-b6eb-e325a2b8342e",
"metadata": {},
"outputs": [],
"source": [
"def bench(fn, n_warmup=N_WARMUP, n_repeats=N_REPEATS):\n",
" \"\"\"\n",
" Warmup then time `fn` over `n_repeats` calls.\n",
" Returns (last_result, list_of_walltimes_in_seconds).\n",
" \"\"\"\n",
" # warmup: primes icache, branch predictor, and lets CPU ramp to boost clock\n",
" for _ in range(n_warmup):\n",
" res = fn()\n",
"\n",
" times = []\n",
" for _ in range(n_repeats):\n",
" t0 = time.perf_counter()\n",
" res = fn()\n",
" t1 = time.perf_counter()\n",
" times.append(t1 - t0)\n",
" return res, times"
]
},
{
"cell_type": "markdown",
"id": "377fc820-95b2-48aa-b104-a272c50e4103",
"metadata": {},
"source": [
"# Quick check on small (2x2) frame"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9cc999b9-e0d8-4deb-ae29-e8bd0141534b",
"metadata": {},
"outputs": [],
"source": [
"# Generate 2 x 2 dataset of Gaussian-like profiles for each pixel\n",
"x2, y2, yerr2, true_A2, true_mu2, true_sig2 = generate_3d_data(\n",
" 2, 2, N_SCAN, NOISE_FRAC, SEED\n",
")\n",
"model_g = Gaussian()\n",
"model_g.compute_errors = True\n",
"result = model_g.fit(x2, y2, yerr2)\n",
"\n",
"from pprint import pprint\n",
"print(\"== True Gaussian params == \")\n",
"print(\"A_true = \\n\", true_A2)\n",
"print(\"mu_true = \\n\", true_mu2)\n",
"print(\"sig_true = \\n\",true_sig2)\n",
"print(\"\\n\")\n",
"\n",
"print(\"== Fit results ==\")\n",
"par = result['par']\n",
"# print(par)\n",
"A_fit = par[:, :, 0]\n",
"mu_fit = par[:, :, 1]\n",
"sig_fit = par[:, :, 2]\n",
"print(\"A_fit = \\n\", A_fit)\n",
"print(\"mu_fit = \\n\", mu_fit)\n",
"print(\"sig_fit = \\n\", sig_fit)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a98a41f3-fd2e-4bfc-9ec6-0d23dd38e896",
"metadata": {},
"outputs": [],
"source": [
"fig, ax = plt.subplots(2, 2, figsize=(12,8))\n",
"\n",
"# Gaussians in 2x2 frame: True vs Fit\n",
"for row in range(2):\n",
" for col in range(2):\n",
" ax[row, col].plot(x2, y2[row, col,:], label=\"data\")\n",
" ax[row, col].plot(x2, model_g(x2, result['par'][row, col,:]), linewidth=1, color=\"green\", label=\"minuit\")\n",
" ax[row, col].set_title(f\"Gaussian Fit to data in pixel [{row}, {col}]\")\n",
" ax[row, col].legend()"
]
},
{
"cell_type": "markdown",
"id": "fcf52481-0278-4f95-8676-829a6d61eff8",
"metadata": {},
"source": [
"## Fit data with different backends"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1f6bc651-80c1-41dd-8a05-7f15aba006aa",
"metadata": {},
"outputs": [],
"source": [
"# ===============\n",
"# DATA GENERATION\n",
"# ===============\n",
"print(f\"Generating synthetic data: {ROWS}x{COLS} pixels, \"\n",
" f\"{N_SCAN} scan points, noise_frac={NOISE_FRAC}\\n\")\n",
"\n",
"x, y, yerr, true_A, true_mu, true_sig = generate_3d_data(\n",
" ROWS, COLS, N_SCAN, NOISE_FRAC, SEED\n",
")\n",
"\n",
"model = Gaussian()\n",
"print(f\"model.max_calls = {model.max_calls}\")\n",
"print(f\"model.tolerance = {model.tolerance}\")\n",
"print(\"model.compute_errors =\", model.compute_errors)\n",
"METHOD_DEFS = [\n",
" (\"Minuit2 (obj API)\",\n",
" lambda nt: lambda: model.fit(x, y, n_threads=nt),\n",
" \"#FF9800\", {\"linewidth\": 2.5, \"linestyle\": \":\"}),\n",
"]\n",
"\n",
"colors = {label: c for label, _, c, _ in METHOD_DEFS}\n",
"styles = {label: s for label, _, _, s in METHOD_DEFS}"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5a417145-ce42-4c3a-a7bf-05ff97ba0450",
"metadata": {},
"outputs": [],
"source": [
"# ====================================\n",
"# SINGLE-CALL BENCHMARK (at N_THREADS)\n",
"# ====================================\n",
"def extract_result(label, res):\n",
" \"\"\"Normalize return values across fitters into a common dict.\"\"\"\n",
" if isinstance(res, dict):\n",
" out = {\"par\": res[\"par\"]}\n",
" if \"par_err\" in res:\n",
" out[\"par_err\"] = res[\"par_err\"]\n",
" if \"chi2\" in res:\n",
" out[\"chi2\"] = res[\"chi2\"]\n",
" return out\n",
" \n",
"methods = {}\n",
"for label, factory, _, _ in METHOD_DEFS:\n",
" time.sleep(COOLDOWN)\n",
" res, times = bench(factory(N_THREADS))\n",
" entry = extract_result(label, res)\n",
" entry[\"times\"] = times\n",
" methods[label] = entry\n",
"\n",
"# ---- Print summary ----\n",
"ndf = N_SCAN - 3\n",
"print(f\"{'Method':24s} {'time (ms)':>10s} {'med|dA|':>10s} {'med|dMu|':>10s} {'med|dSig|':>10s}\")\n",
"print(\"-\" * 80)\n",
"for name, m in methods.items():\n",
" par = m[\"par\"]\n",
" med_t = np.median(m[\"times\"]) * 1e3\n",
" dA = np.median(np.abs(par[:,:,0] - true_A))\n",
" dMu = np.median(np.abs(par[:,:,1] - true_mu))\n",
" dSig = np.median(np.abs(par[:,:,2] - true_sig))\n",
"\n",
" chi2_str = \"\"\n",
" if \"chi2\" in m:\n",
" chi2_str = f\" chi2/ndf={np.median(m['chi2'] / ndf):.4f}\"\n",
"\n",
" print(f\"[{name:22s}] {med_t:8.2f} ms \"\n",
" f\"{dA:10.3f} {dMu:10.4f} {dSig:10.4f}{chi2_str}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "55ecc77c-0823-408d-a811-8e4f4900f332",
"metadata": {
"scrolled": true
},
{
"cell_type": "code",
"execution_count": 3,
"id": "35772e2c-37c6-4986-a9c0-7dca4a034827",
"metadata": {},
"outputs": [],
"source": [
"ROWS = 100\n",
"COLS = 100\n",
"N_SCAN = 100\n",
"NOISE_FRAC = 0.05\n",
"SEED = 42\n",
"N_THREADS = 4\n",
"\n",
"N_REPEATS = 7\n",
"N_WARMUP = 3 # untimed iterations (icache + branch predictor warmup)\n",
"COOLDOWN = 2.0 # seconds between (method, thread_count) pairs"
]
},
{
"cell_type": "markdown",
"id": "be455445-7df8-47cc-9f39-d2a38856e0bd",
"metadata": {},
"source": [
"## Data generator"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "06c8fddb-56d9-4f84-8b21-4ffd8b7bd26f",
"metadata": {},
"outputs": [],
"source": [
"def generate_3d_data(rows, cols, n_scan, noise_frac, seed):\n",
" \"\"\"\n",
" Generate a synthetic detector image stack where each pixel has a\n",
" Gaussian response curve with per-pixel variation in A, mu, sigma.\n",
"\n",
" Returns x (n_scan,), y (rows, cols, n_scan), y_err (rows, cols, n_scan),\n",
" and the ground-truth parameter arrays.\n",
" \"\"\"\n",
" rng = np.random.default_rng(seed)\n",
"\n",
" # Per-pixel true params each of shape: [rows, cols, 1]\n",
" A_true = rng.uniform(200, 1000, size=(rows, cols))\n",
" mu_true = rng.uniform(20, 80, size=(rows, cols))\n",
" sig_true = rng.uniform(3, 12, size=(rows, cols))\n",
" \n",
" # One common binned energy array\n",
" x = np.linspace(0, 100, n_scan) # shape [1, 1, nscan]\n",
"\n",
" # Build ground truth signals per-pixel\n",
" exponent = -0.5 * ((x[None, None, :] - mu_true[:, :, None]) / sig_true[:,:, None])**2 # shape [rows, cols, nscan]\n",
" y_clean = A_true[:, :, None] * np.exp(exponent)\n",
"\n",
" # Perturb with noise\n",
" noise_sigma = noise_frac * A_true[:, :, None] * np.ones_like(y_clean) # shape [rows, cols, nscan]\n",
" noise = rng.normal(0, noise_sigma)\n",
" y = y_clean + noise\n",
"\n",
" y_err = noise_sigma.copy()\n",
"\n",
" return x, y, y_err, A_true, mu_true, sig_true "
]
},
{
"cell_type": "markdown",
"id": "30ed6bb8-4798-497f-9990-4c518f885855",
"metadata": {},
"source": [
"## Profiling function"
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "dc0b52b6-9fc3-453d-b6eb-e325a2b8342e",
"metadata": {},
"outputs": [],
"source": [
"def bench(fn, n_warmup=N_WARMUP, n_repeats=N_REPEATS):\n",
" \"\"\"\n",
" Warmup then time `fn` over `n_repeats` calls.\n",
" Returns (last_result, list_of_walltimes_in_seconds).\n",
" \"\"\"\n",
" # warmup: primes icache, branch predictor, and lets CPU ramp to boost clock\n",
" for _ in range(n_warmup):\n",
" res = fn()\n",
"\n",
" times = []\n",
" for _ in range(n_repeats):\n",
" t0 = time.perf_counter()\n",
" res = fn()\n",
" t1 = time.perf_counter()\n",
" times.append(t1 - t0)\n",
" return res, times"
]
},
{
"cell_type": "markdown",
"id": "377fc820-95b2-48aa-b104-a272c50e4103",
"metadata": {},
"source": [
"# Quick check on small (2x2) frame"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "9cc999b9-e0d8-4deb-ae29-e8bd0141534b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"== True Gaussian params == \n",
"A_true = \n",
" [[819.16483884 551.1027518 ]\n",
" [886.87833593 757.89442325]]\n",
"mu_true = \n",
" [[25.65064087 78.5373411 ]\n",
" [65.66838212 67.16385832]]\n",
"sig_true = \n",
" [[ 4.15302269 7.05347344]\n",
" [ 6.33718222 11.3408849 ]]\n",
"\n",
"\n",
"== Fit results ==\n",
"A_fit = \n",
" [[812.09277132 559.04069721]\n",
" [899.09335849 759.24481682]]\n",
"mu_fit = \n",
" [[25.6598209 78.40461782]\n",
" [65.52261318 66.84540995]]\n",
"sig_fit = \n",
" [[ 4.2778026 7.041045 ]\n",
" [ 6.29190225 11.34233504]]\n"
]
}
],
"source": [
"# Generate 2 x 2 dataset of Gaussian-like profiles for each pixel\n",
"x2, y2, yerr2, true_A2, true_mu2, true_sig2 = generate_3d_data(\n",
" 2, 2, N_SCAN, NOISE_FRAC, SEED\n",
")\n",
"model_g = Gaussian()\n",
"model_g.compute_errors = True\n",
"result = model_g.fit(x2, y2, yerr2)\n",
"\n",
"from pprint import pprint\n",
"print(\"== True Gaussian params == \")\n",
"print(\"A_true = \\n\", true_A2)\n",
"print(\"mu_true = \\n\", true_mu2)\n",
"print(\"sig_true = \\n\",true_sig2)\n",
"print(\"\\n\")\n",
"\n",
"print(\"== Fit results ==\")\n",
"par = result['par']\n",
"# print(par)\n",
"A_fit = par[:, :, 0]\n",
"mu_fit = par[:, :, 1]\n",
"sig_fit = par[:, :, 2]\n",
"print(\"A_fit = \\n\", A_fit)\n",
"print(\"mu_fit = \\n\", mu_fit)\n",
"print(\"sig_fit = \\n\", sig_fit)"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "a98a41f3-fd2e-4bfc-9ec6-0d23dd38e896",
"metadata": {},
"outputs": [
{
"data": {
"image/png":
"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 truncated
"text/plain": [
"<Figure size 1200x800 with 4 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"fig, ax = plt.subplots(2, 2, figsize=(12,8))\n",
"\n",
"# Gaussians in 2x2 frame: True vs Fit\n",
"for row in range(2):\n",
" for col in range(2):\n",
" ax[row, col].plot(x2, y2[row, col,:], label=\"data\")\n",
" ax[row, col].plot(x2, model_g(x2, result['par'][row, col,:]), linewidth=1, color=\"green\", label=\"minuit\")\n",
" ax[row, col].set_title(f\"Gaussian Fit to data in pixel [{row}, {col}]\")\n",
" ax[row, col].legend()"
]
},
{
"cell_type": "markdown",
"id": "fcf52481-0278-4f95-8676-829a6d61eff8",
"metadata": {},
"source": [
"## Fit data with different backends"
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "1f6bc651-80c1-41dd-8a05-7f15aba006aa",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Generating synthetic data: 100x100 pixels, 100 scan points, noise_frac=0.05\n",
"\n",
"model.max_calls = 100\n",
"model.tolerance = 0.5\n",
"model.compute_errors = False\n"
]
}
],
"source": [
"# ===============\n",
"# DATA GENERATION\n",
"# ===============\n",
"print(f\"Generating synthetic data: {ROWS}x{COLS} pixels, \"\n",
" f\"{N_SCAN} scan points, noise_frac={NOISE_FRAC}\\n\")\n",
"\n",
"x, y, yerr, true_A, true_mu, true_sig = generate_3d_data(\n",
" ROWS, COLS, N_SCAN, NOISE_FRAC, SEED\n",
")\n",
"\n",
"model = Gaussian()\n",
"print(f\"model.max_calls = {model.max_calls}\")\n",
"print(f\"model.tolerance = {model.tolerance}\")\n",
"print(\"model.compute_errors =\", model.compute_errors)\n",
"METHOD_DEFS = [\n",
" (\"lmfit (LM)\",\n",
" lambda nt: lambda: fit_gaus(x, y, n_threads=nt),\n",
" \"#2196F3\", {\"linewidth\": 3.0, \"linestyle\": \"-\"}),\n",
"\n",
" (\"Minuit2 (obj API)\",\n",
" lambda nt: lambda: model.fit(x, y, n_threads=nt),\n",
" \"#FF9800\", {\"linewidth\": 2.5, \"linestyle\": \":\"}),\n",
"]\n",
"\n",
"colors = {label: c for label, _, c, _ in METHOD_DEFS}\n",
"styles = {label: s for label, _, _, s in METHOD_DEFS}"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "5a417145-ce42-4c3a-a7bf-05ff97ba0450",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Method time (ms) med|dA| med|dMu| med|dSig|\n",
"--------------------------------------------------------------------------------\n",
"[lmfit (LM) ] 90.30 ms 6.272 0.0940 0.0949\n",
"[Minuit2 (obj API) ] 59.60 ms 6.272 0.0940 0.0949 chi2/ndf=880.9946\n"
]
}
],
"source":
[
"# ====================================\n",
"# SINGLE-CALL BENCHMARK (at N_THREADS)\n",
"# ====================================\n",
"def extract_result(label, res):\n",
" \"\"\"Normalize return values across fitters into a common dict.\"\"\"\n",
" if isinstance(res, dict):\n",
" out = {\"par\": res[\"par\"]}\n",
" if \"par_err\" in res:\n",
" out[\"par_err\"] = res[\"par_err\"]\n",
" if \"chi2\" in res:\n",
" out[\"chi2\"] = res[\"chi2\"]\n",
" return out\n",
" # fit_gaus without y_err returns a raw array\n",
" return {\"par\": res}\n",
" \n",
"methods = {}\n",
"for label, factory, _, _ in METHOD_DEFS:\n",
" time.sleep(COOLDOWN)\n",
" res, times = bench(factory(N_THREADS))\n",
" entry = extract_result(label, res)\n",
" entry[\"times\"] = times\n",
" methods[label] = entry\n",
"\n",
"# ---- Print summary ----\n",
"ndf = N_SCAN - 3\n",
"print(f\"{'Method':24s} {'time (ms)':>10s} {'med|dA|':>10s} {'med|dMu|':>10s} {'med|dSig|':>10s}\")\n",
"print(\"-\" * 80)\n",
"for name, m in methods.items():\n",
" par = m[\"par\"]\n",
" med_t = np.median(m[\"times\"]) * 1e3\n",
" dA = np.median(np.abs(par[:,:,0] - true_A))\n",
" dMu = np.median(np.abs(par[:,:,1] - true_mu))\n",
" dSig = np.median(np.abs(par[:,:,2] - true_sig))\n",
"\n",
" chi2_str = \"\"\n",
" if \"chi2\" in m:\n",
" chi2_str = f\" chi2/ndf={np.median(m['chi2'] / ndf):.4f}\"\n",
"\n",
" print(f\"[{name:22s}] {med_t:8.2f} ms \"\n",
" f\"{dA:10.3f} {dMu:10.4f} {dSig:10.4f}{chi2_str}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "55ecc77c-0823-408d-a811-8e4f4900f332",
"metadata": {
"scrolled": true
},
"outputs": [],
"source": [
"# ===============\n",
"# THREAD SCALING\n",
"# ===============\n",
"thread_counts = [1, 2, 4, 8, 16]\n",
"\n",
"thread_times = {label: [] for label, _, _, _ in METHOD_DEFS}\n",
"ttimes_stddev = {label: [] for label, _, _, _ in METHOD_DEFS}\n",
"\n",
"for nt in thread_counts:\n",
" # shuffle method order per thread count to decorrelate thermal bias\n",
" run_order = list(METHOD_DEFS)\n",
" random.shuffle(run_order)\n",
"\n",
" for label, factory, _, _ in run_order:\n",
" time.sleep(COOLDOWN)\n",
" _, times = bench(factory(nt))\n",
"\n",
" med = np.median(times) * 1e3\n",
" std = np.std(times) * 1e3\n",
" thread_times[label].append(med)\n",
" ttimes_stddev[label].append(std)\n",
"\n",
" per_px = med / (ROWS * COLS) * 1e3\n",
" per_px_std = std / (ROWS * COLS) * 1e3\n",
" print(f\" {label:22s} n_threads={nt:2d} \"\n",
" f\"{med:8.2f} ± {std:6.2f} ms \"\n",
" f\"({per_px:.4f} ± {per_px_std:.4f} μs/pixel)\")\n",
" print(\"\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d5f3152d-be84-420e-8045-9c42ac5c24cb",
"metadata": {},
"outputs": [],
"source": [
"# =============================\n",
"# FIGURE 1: Residual histograms\n",
"# =============================\n",
"param_names = [\"A\", \"μ\", \"σ\"]\n",
"param_truths = [true_A, true_mu, true_sig]\n",
"\n",
"fig1, axes1 = plt.subplots(1, 3, figsize=(15, 5))\n",
"fig1.suptitle(f\"Parameter Residuals — {ROWS}×{COLS} pixels, {N_SCAN} scan points\",\n",
" fontsize=14, fontweight=\"bold\")\n",
"\n",
"for col, (pname, truth) in enumerate(zip(param_names, param_truths)):\n",
" ax = axes1[col]\n",
"\n",
" # collect residuals across all methods for shared bin edges\n",
" res_by_method = {}\n",
" all_res = []\n",
" for mname, m in methods.items():\n",
" residual = (m[\"par\"][:, :, col] - truth).ravel()\n",
" res_by_method[mname] = residual\n",
" all_res.append(residual)\n",
" all_res = np.concatenate(all_res)\n",
"\n",
" lo, hi = np.percentile(all_res, [0.5, 99.5])\n",
" edges = np.linspace(lo, hi, 101)\n",
"\n",
" for mname, r in res_by_method.items():\n",
" ax.hist(r, bins=edges, histtype=\"step\", label=mname,\n",
" color=colors[mname],\n",
" linewidth=styles[mname][\"linewidth\"],\n",
" linestyle=styles[mname][\"linestyle\"])\n",
"\n",
" ax.axvline(0, color=\"k\", linestyle=\"--\", linewidth=1, alpha=0.7)\n",
" ax.set_xlabel(f\"Fitted {pname} − True {pname}\")\n",
" ax.set_ylabel(\"Pixel count\")\n",
" ax.set_title(f\"Δ{pname}\")\n",
" ax.legend(fontsize=8)\n",
" ax.grid(alpha=0.3)\n",
"\n",
"fig1.tight_layout()\n",
"# fig1.savefig(\"fig1_residual_histograms.png\", dpi=150, bbox_inches=\"tight\")\n",
"# print(\"\\nSaved fig1_residual_histograms.png\")\n",
"\n",
"# ====================================================\n",
"# FIGURE 2: Performance — bar chart + thread scaling\n",
"# ====================================================\n",
"fig2 = plt.figure(figsize=(14, 5))\n",
"gs = GridSpec(1, 2, figure=fig2, width_ratios=[1, 1.3])\n",
"\n",
"# -- Left: bar chart at N_THREADS --\n",
"ax2a = fig2.add_subplot(gs[0])\n",
"names = list(methods.keys())\n",
"medians = [np.median(methods[n][\"times\"]) * 1e3 for n in names]\n",
"bars = ax2a.barh(names, medians,\n",
" color=[colors[n] for n in names],\n",
" edgecolor=\"white\", height=0.5)\n",
"ax2a.set_xlabel(\"Median wall time (ms)\")\n",
"ax2a.set_title(f\"Single call — {ROWS}×{COLS} px, {N_THREADS} threads\")\n",
"for bar, val in zip(bars, medians):\n",
" ax2a.text(bar.get_width() + max(medians) * 0.02,\n",
" bar.get_y() + bar.get_height() / 2,\n",
" f\"{val:.1f} ms\", va=\"center\", fontsize=10)\n",
"ax2a.grid(axis=\"x\", alpha=0.3)\n",
"ax2a.set_xlim(0, max(medians) * 1.25)\n",
"\n",
"# -- Right: thread scaling with error bars --\n",
"ax2b = fig2.add_subplot(gs[1])\n",
"for label, _, _, _ in METHOD_DEFS:\n",
" tt = thread_times[label]\n",
" sd = ttimes_stddev[label]\n",
" speedup = [tt[0] / t for t in tt]\n",
" # propagate uncertainty: S = t0/t → δS/S = sqrt((δt0/t0)² + (δt/t)²)\n",
" speedup_err = [\n",
" s * np.sqrt((sd[0] / tt[0])**2 + (sd[i] / tt[i])**2)\n",
" for i, s in enumerate(speedup)\n",
" ]\n",
" ax2b.errorbar(thread_counts, speedup, yerr=speedup_err,\n",
" fmt=\"o-\", label=label, color=colors[label],\n",
" linewidth=2, markersize=7, capsize=4)\n",
"\n",
"ax2b.plot(thread_counts, thread_counts, \"k--\", alpha=0.4, label=\"Ideal linear\")\n",
"ax2b.set_xlabel(\"Number of threads\")\n",
"ax2b.set_ylabel(\"Speedup vs 1 thread\")\n",
"ax2b.set_title(\"Thread scaling\")\n",
"ax2b.set_xticks(thread_counts)\n",
"ax2b.legend(fontsize=9)\n",
"ax2b.grid(alpha=0.3)\n",
"\n",
"fig2.tight_layout()\n",
"# fig2.savefig(\"fig2_performance.png\", dpi=150, bbox_inches=\"tight\")\n",
"# print(\"Saved fig2_performance.png\")\n",
"\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "92a8c724-e139-45d4-a354-1b8408637ead",
"metadata": {},
"outputs": [],
"source": []
}
],
"outputs": [],
"source": [
"# ===============\n",
"# THREAD SCALING\n",
"# ===============\n",
"thread_counts = [1, 2, 4, 8, 16]\n",
"\n",
"thread_times = {label: [] for label, _, _, _ in METHOD_DEFS}\n",
"ttimes_stddev = {label: [] for label, _, _, _ in METHOD_DEFS}\n",
"\n",
"for nt in thread_counts:\n",
" # shuffle method order per thread count to decorrelate thermal bias\n",
" run_order = list(METHOD_DEFS)\n",
" random.shuffle(run_order)\n",
"\n",
" for label, factory, _, _ in run_order:\n",
" time.sleep(COOLDOWN)\n",
" _, times = bench(factory(nt))\n",
"\n",
" med = np.median(times) * 1e3\n",
" std = np.std(times) * 1e3\n",
" thread_times[label].append(med)\n",
" ttimes_stddev[label].append(std)\n",
"\n",
" per_px = med / (ROWS * COLS) * 1e3\n",
" per_px_std = std / (ROWS * COLS) * 1e3\n",
" print(f\" {label:22s} n_threads={nt:2d} \"\n",
" f\"{med:8.2f} \u00b1 {std:6.2f} ms \"\n",
" f\"({per_px:.4f} \u00b1 {per_px_std:.4f} \u03bcs/pixel)\")\n",
" print(\"\\n\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "d5f3152d-be84-420e-8045-9c42ac5c24cb",
"metadata": {},
"outputs": [],
"source": [
"# =============================\n",
"# FIGURE 1: Residual histograms\n",
"# =============================\n",
"param_names = [\"A\", \"\u03bc\", \"\u03c3\"]\n",
"param_truths = [true_A, true_mu, true_sig]\n",
"\n",
"fig1, axes1 = plt.subplots(1, 3, figsize=(15, 5))\n",
"fig1.suptitle(f\"Parameter Residuals \u2014 {ROWS}\u00d7{COLS} pixels, {N_SCAN} scan points\",\n",
" fontsize=14, fontweight=\"bold\")\n",
"\n",
"for col, (pname, truth) in enumerate(zip(param_names, param_truths)):\n",
" ax = axes1[col]\n",
"\n",
" # collect residuals across all methods for shared bin edges\n",
" res_by_method = {}\n",
" all_res = []\n",
" for mname, m in methods.items():\n",
" residual = (m[\"par\"][:, :, col] - truth).ravel()\n",
" res_by_method[mname] = residual\n",
" all_res.append(residual)\n",
" all_res = np.concatenate(all_res)\n",
"\n",
" lo, hi = np.percentile(all_res, [0.5, 99.5])\n",
" edges = np.linspace(lo, hi, 101)\n",
"\n",
" for mname, r in res_by_method.items():\n",
" ax.hist(r, bins=edges, histtype=\"step\", label=mname,\n",
" color=colors[mname],\n",
" linewidth=styles[mname][\"linewidth\"],\n",
" linestyle=styles[mname][\"linestyle\"])\n",
"\n",
" ax.axvline(0, color=\"k\", linestyle=\"--\", linewidth=1, alpha=0.7)\n",
" ax.set_xlabel(f\"Fitted {pname} \u2212 True {pname}\")\n",
" ax.set_ylabel(\"Pixel count\")\n",
" ax.set_title(f\"\u0394{pname}\")\n",
" ax.legend(fontsize=8)\n",
" ax.grid(alpha=0.3)\n",
"\n",
"fig1.tight_layout()\n",
"# fig1.savefig(\"fig1_residual_histograms.png\", dpi=150, bbox_inches=\"tight\")\n",
"# print(\"\\nSaved fig1_residual_histograms.png\")\n",
"\n",
"# ====================================================\n",
"# FIGURE 2: Performance \u2014 bar chart + thread scaling\n",
"# ====================================================\n",
"fig2 = plt.figure(figsize=(14, 5))\n",
"gs = GridSpec(1, 2, figure=fig2, width_ratios=[1, 1.3])\n",
"\n",
"# -- Left: bar chart at N_THREADS --\n",
"ax2a = fig2.add_subplot(gs[0])\n",
"names = list(methods.keys())\n",
"medians = [np.median(methods[n][\"times\"]) * 1e3 for n in names]\n",
"bars = ax2a.barh(names, medians,\n",
" color=[colors[n] for n in names],\n",
" edgecolor=\"white\", height=0.5)\n",
"ax2a.set_xlabel(\"Median wall time (ms)\")\n",
"ax2a.set_title(f\"Single call \u2014 {ROWS}\u00d7{COLS} px, {N_THREADS} threads\")\n",
"for bar, val in zip(bars, medians):\n",
" ax2a.text(bar.get_width() + max(medians) * 0.02,\n",
" bar.get_y() + bar.get_height() / 2,\n",
" f\"{val:.1f} ms\", va=\"center\", fontsize=10)\n",
"ax2a.grid(axis=\"x\", alpha=0.3)\n",
"ax2a.set_xlim(0, max(medians) * 1.25)\n",
"\n",
"# -- Right: thread scaling with error bars --\n",
"ax2b = fig2.add_subplot(gs[1])\n",
"for label, _, _, _ in METHOD_DEFS:\n",
" tt = thread_times[label]\n",
" sd = ttimes_stddev[label]\n",
" speedup = [tt[0] / t for t in tt]\n",
" # propagate uncertainty: S = t0/t \u2192 \u03b4S/S = sqrt((\u03b4t0/t0)\u00b2 + (\u03b4t/t)\u00b2)\n",
" speedup_err = [\n",
" s * np.sqrt((sd[0] / tt[0])**2 + (sd[i] / tt[i])**2)\n",
" for i, s in enumerate(speedup)\n",
" ]\n",
" ax2b.errorbar(thread_counts, speedup, yerr=speedup_err,\n",
" fmt=\"o-\", label=label, color=colors[label],\n",
" linewidth=2, markersize=7, capsize=4)\n",
"\n",
"ax2b.plot(thread_counts, thread_counts, \"k--\", alpha=0.4, label=\"Ideal linear\")\n",
"ax2b.set_xlabel(\"Number of threads\")\n",
"ax2b.set_ylabel(\"Speedup vs 1 thread\")\n",
"ax2b.set_title(\"Thread scaling\")\n",
"ax2b.set_xticks(thread_counts)\n",
"ax2b.legend(fontsize=9)\n",
"ax2b.grid(alpha=0.3)\n",
"\n",
"fig2.tight_layout()\n",
"# fig2.savefig(\"fig2_performance.png\", dpi=150, bbox_inches=\"tight\")\n",
"# print(\"Saved fig2_performance.png\")\n",
"\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "92a8c724-e139-45d4-a354-1b8408637ead",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
+24 -136
View File
@@ -2,26 +2,19 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"id": "efef8e20-6571-4561-8f0b-6048b57907de",
"metadata": {},
"outputs": [],
"source": [
"import time\n",
"import random\n",
"import numpy as np\n",
"np.set_printoptions(suppress=True, precision=6)\n",
"\n",
"import matplotlib.pyplot as plt\n",
"from scipy.special import erf\n",
"\n",
"import sys\n",
"sys.path.insert(0, '/home/ferjao_k/sw/aare/build')\n",
"\n",
"from aare import fit_scurve, fit_scurve2 # lmfit\n",
"from aare import RisingScurve, FallingScurve, fit # minuit2 (object based API)\n",
"\n",
"from pprint import pprint"
"from aare import RisingScurve, FallingScurve\n",
"from pprint import pprint\n"
]
},
{
@@ -34,7 +27,7 @@
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "35772e2c-37c6-4986-a9c0-7dca4a034827",
"metadata": {},
"outputs": [],
@@ -58,7 +51,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "06c8fddb-56d9-4f84-8b21-4ffd8b7bd26f",
"metadata": {},
"outputs": [],
@@ -92,22 +85,10 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "9a0b3292-7c57-4483-aecc-7ef683ff0b62",
"metadata": {},
"outputs": [
{
"data": {
"image/png":
"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 truncated
"text/plain": [
"<Figure size 1200x400 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"outputs": [],
"source": [
"fig, ax = plt.subplots(1, 2, figsize=(12,4), sharex=True)\n",
"\n",
@@ -134,38 +115,11 @@
},
{
"cell_type": "code",
"execution_count": 18,
"execution_count": null,
"id": "37d2fda4-40b8-4ea2-8f2c-6c3f2384d158",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Paramter list of the Scurve:\n",
"['p0', 'p1', 'mu', 'sigma', 'A', 'C']\n",
"6 \n",
"\n",
"== Tuned fit settings ==\n",
"max_calls : 500\n",
"tolerance : 0.01\n",
"compute_erros: True\n",
"\n",
"\n",
"== Results ==\n",
"True rising params: [100. 0.25 60. 6. 120. 1. ]\n",
"lmfit rising result: [ 99.467831 0.287017 60.078405 5.715461 117.257939 0.967234]\n",
"minuit_grad (obj api ) rising result:\n",
"{'par': array([100. , 0.269137, 60.050193, 5.729932, 117.777635,\n",
" 0.984073]),\n",
" 'par_err': array([1. , 0.054406, 0.747753, 0.909864, 7.887499, 0.174058]),\n",
" 'chi2': array([7.351743])}\n"
]
}
],
"outputs": [],
"source": [
"res_r_lmfit = fit_scurve(x, y_rising)\n",
"\n",
"model_r = RisingScurve()\n",
"print(\"Paramter list of the Scurve:\")\n",
"# print(model_r.GetParNames())\n",
@@ -187,7 +141,6 @@
"res_r_munuit_obj = model_r.fit(x, y_rising, np.sqrt(y_rising))\n",
"\n",
"print(\"True rising params: \", p_true_rising)\n",
"print(\"lmfit rising result: \", res_r_lmfit)\n",
"print(\"minuit_grad (obj api ) rising result:\")\n",
"pprint(res_r_munuit_obj, sort_dicts=False)"
]
@@ -202,26 +155,11 @@
},
{
"cell_type": "code",
"execution_count": 19,
"execution_count": null,
"id": "5ce22d2d-1adb-47d9-bf36-6cb535fa8b77",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"True falling params: [100. 0.25 60. 6. 120. -1. ]\n",
"lmfit falling result: [101.73448 0.225786 60.251971 6.092915 118.476735 -0.998677]\n",
"minuit_grad (obj api ) falling result\n",
"{'par': array([101.741754, 0.225723, 60.251604, 6.092887, 118.473268,\n",
" -0.998565]),\n",
" 'chi2': array([1865.728797])}\n"
]
}
],
"outputs": [],
"source": [
"res_f_lmfit = fit_scurve2(x, y_falling)\n",
"\n",
"model_f = FallingScurve()\n",
"model_f.SetParameter(2, 20) # set start p2 = 20\n",
"model_f.SetParameter(4, 90) # set start p4 = 90\n",
@@ -230,36 +168,22 @@
"res_f_munuit_obj = model_f.fit(x, y_falling)\n",
"\n",
"print(\"True falling params: \", p_true_falling)\n",
"print(\"lmfit falling result: \", res_f_lmfit)\n",
"print(\"minuit_grad (obj api ) falling result\")\n",
"pprint(res_f_munuit_obj, sort_dicts=False)"
]
},
{
"cell_type": "code",
"execution_count": 20,
"execution_count": null,
"id": "fe72503a-fc77-4b17-9b17-b98055e2fc87",
"metadata": {},
"outputs": [
{
"data": {
"image/png":
"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p06duuR9appGmzZt8i2rVasWhw4d8jxft26dp015Pfnkk5e0D7PZzMKFC9m5cyeTJk3iiSee4OTJk4wdO5aqVauyZ88ewD1P2uFw5HsA7N27l3///ZdevXrh5eV1ycd2MQ888EC+JHMANWvW5K677mLmzJmeZbt27eKPP/6gZ8+enmU//vgjNWrU4M4778zX3hYtWly37O533303sbGxvP3222zcuBG73X7N9ymEEOLKzZ49m02bNuV7GAwGHA4H77zzDtWqVcNkMmEwGDCZTOzbt49du3YV2f6bNm2aL1dKREQE4eHhnnN+ZmYmf/75J+3atcNkMnnW8/PzO+9aoTD16tVj//79LF26lDfeeIMGDRqwatUqunXrxqOPPurJy5Lbu5/7yJ2//fPPPwPQr1+/IjnmXB06dDhvWc+ePcnOzmb+/PmeZTNnzsRsNtOlSxfAPax/9+7dPPXUUwD52tyqVSvi4+M91zHXyp133onJZKJPnz7MmjWLAwcOXNP9iVuPBNrippd7Al69ejXPPfccu3btuqRg9umnn2bGjBkcOnSIDh06EB4ezj333MOKFSsu+l4fH5/zglez2YzVavU8P336NBEREee9t6BlF1K1alUGDhzInDlzOHz4MBMnTuT06dO8+eabAMyaNQuj0ZjvAe65yECBw6uvRmFzyHr27Mlvv/3G7t27gbMn3bw/ixMnTrBt27bz2muxWFBKXdbNjis1f/58unfvzvTp02nQoAHBwcF069aNhISEa75vIYQQl69q1arUrVs33wNg0KBBvPnmm7Rr147Fixfz+++/s2nTJu644w6ys7OLbP8hISHnLTObzZ59JCcno5S66nO+0WikRYsWjB07lmXLlnHkyBGaNGnCjz/+6AmkH3zwwXznz9yb2SdPnkSv1xMZGXklh1iogs751atXp169ep6b606nkzlz5tC2bVuCg4OBs3lyhgwZct45v2/fvgDX/JxfsWJFVq5cSXh4OP369aNixYpUrFiRyZMnX9P9iluHzNEWN73cEzC47zo7nU6mT5/OggUL6Nix4wXf+8wzz/DMM8+QmZnJL7/8wsiRI2ndujV79+4lJibmqtoVEhLCH3/8cd7yqwnoNE3j5ZdfZsyYMZ4M3G3atGHTpk3nrZs7J/no0aMX3KaXl9d5Sdyg8BNgYXPGnnzySQYNGkRsbCxjx47liy++oF27dvl6v0NDQ/H29mbGjBkFbiM0NPSCbS0KoaGhfPDBB3zwwQccPnyYH374gddff53ExESWLl16zfcvhBCiaMyZM4du3brxzjvv5Ft+6tSp61rmKSgoCE3TCpyPfTXn/JCQEAYOHMjatWvZvn07rVq14pNPPiE9Pd2zTuLine truncated
"text/plain": [
"<Figure size 1200x400 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"outputs": [],
"source": [
"fig, ax = plt.subplots(1, 2, figsize=(12,4))\n",
"\n",
"# rising\n",
"ax[0].plot(x, y_true_rising, label=\"true\")\n",
"ax[0].scatter(x, y_rising, s=12, label=\"data\")\n",
"ax[0].scatter(x, scurve(x, res_r_lmfit[:6]), s=20, marker=\"x\", label=\"lmfit\")\n",
"ax[0].plot(x, model_r(x, res_r_munuit_obj['par']), linewidth=1, color=\"green\", label=\"minuit\")\n",
"ax[0].set_title(\"Rising S-curve fits\")\n",
"ax[0].legend()\n",
@@ -267,7 +191,6 @@
"# falling\n",
"ax[1].plot(x, y_true_falling, label=\"true\")\n",
"ax[1].scatter(x, y_falling, s=12, label=\"data\")\n",
"ax[1].scatter(x, scurve2(x, res_f_lmfit[:6]), s=20, marker=\"x\", label=\"lmfit\")\n",
"ax[1].plot(x, model_f(x, res_f_munuit_obj['par']), linewidth=1, color=\"green\", label=\"minuit\")\n",
"ax[1].set_title(\"Falling S-curve fits\")\n",
"ax[1].legend()\n",
@@ -285,29 +208,13 @@
},
{
"cell_type": "code",
"execution_count": 21,
"execution_count": null,
"id": "3ba40e9a-0c64-4ef6-ba90-b1116f99b862",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Rising abs error lmfit : [0.532169 0.037017 0.078405 0.284539 2.742061 0.032766]\n",
"Rising abs error minuit_grad: [0. 0.019137 0.050193 0.270068 2.222365 0.015927]\n",
"\n",
"\n",
"Falling abs error lmfit : [1.73448 0.024214 0.251971 0.092915 1.523265 0.001323]\n",
"Falling abs error minuit_grad: [1.741754 0.024277 0.251604 0.092887 1.526732 0.001435]\n"
]
}
],
"outputs": [],
"source": [
"print(\"Rising abs error lmfit : \", np.abs(res_r_lmfit[:6] - p_true_rising))\n",
"print(\"Rising abs error minuit_grad: \", np.abs(res_r_munuit_obj['par'] - p_true_rising))\n",
"print(\"\\n\")\n",
"print(\"Falling abs error lmfit : \", np.abs(res_f_lmfit[:6] - p_true_falling))\n",
"print(\"Falling abs error minuit_grad: \", np.abs(res_f_munuit_obj['par'] - p_true_falling))"
"print(\"Rising absolute parameter error:\", np.abs(res_r_munuit_obj['par'] - p_true_rising))\n",
"print(\"Falling absolute parameter error:\", np.abs(res_f_munuit_obj['par'] - p_true_falling))\n"
]
},
{
@@ -320,25 +227,12 @@
},
{
"cell_type": "code",
"execution_count": 22,
"execution_count": null,
"id": "dc0b52b6-9fc3-453d-b6eb-e325a2b8342e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Rising lmfit : 0.334 ms\n",
"Rising minuit2 : 0.239 ms\n",
"\n",
"Falling lmfit : 0.294 ms\n",
"Falling minuit2 : 0.214 ms\n"
]
}
],
"outputs": [],
"source": [
"def bench(fn, n_repeats=200):\n",
" # warmup\n",
" for _ in range(3):\n",
" fn()\n",
"\n",
@@ -348,20 +242,14 @@
" t1 = time.perf_counter()\n",
" return res, (t1 - t0) / n_repeats\n",
"\n",
"model_rising = RisingScurve()\n",
"model_rising = RisingScurve()\n",
"model_falling = FallingScurve()\n",
"\n",
"res_r_lmfit, t_r_lmfit = bench(lambda: fit_scurve(x, y_rising))\n",
"res_r_minuit, t_r_minuit_grad = bench(lambda: model_rising.fit(x, y_rising))\n",
"_, t_rising = bench(lambda: model_rising.fit(x, y_rising))\n",
"_, t_falling = bench(lambda: model_falling.fit(x, y_falling))\n",
"\n",
"res_f_lmfit, t_f_lmfit = bench(lambda: fit_scurve2(x, y_falling))\n",
"res_f_minuit, t_f_minuit_grad = bench(lambda: model_falling.fit(x, y_falling))\n",
"\n",
"print(f\"Rising lmfit : {1e3*t_r_lmfit:.3f} ms\")\n",
"print(f\"Rising minuit2 : {1e3*t_r_minuit_grad:.3f} ms\")\n",
"print()\n",
"print(f\"Falling lmfit : {1e3*t_f_lmfit:.3f} ms\")\n",
"print(f\"Falling minuit2 : {1e3*t_f_minuit_grad:.3f} ms\")"
"print(f\"Rising Minuit2: {1e3*t_rising:.3f} ms\")\n",
"print(f\"Falling Minuit2: {1e3*t_falling:.3f} ms\")\n"
]
},
{
+22 -121
View File
@@ -2,7 +2,7 @@
"cells": [
{
"cell_type": "code",
"execution_count": 1,
"execution_count": null,
"id": "efef8e20-6571-4561-8f0b-6048b57907de",
"metadata": {},
"outputs": [],
@@ -13,30 +13,15 @@
"from scipy.special import erf\n",
"import matplotlib.pyplot as plt\n",
"from matplotlib.gridspec import GridSpec\n",
"import sys\n",
"sys.path.insert(0, '/home/ferjao_k/aare/build')\n",
"\n",
"from aare import fit_scurve2\n",
"from aare import FallingScurve, fit"
"from aare import FallingScurve\n"
]
},
{
"cell_type": "code",
"execution_count": 2,
"execution_count": null,
"id": "2536939d-8f34-438e-850a-6f3bdf48f658",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"'/home/ferjao_k/aare/build/aare/__init__.py'"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"outputs": [],
"source": [
"import aare\n",
"aare.__file__"
@@ -44,7 +29,7 @@
},
{
"cell_type": "code",
"execution_count": 3,
"execution_count": null,
"id": "35772e2c-37c6-4986-a9c0-7dca4a034827",
"metadata": {},
"outputs": [],
@@ -71,7 +56,7 @@
},
{
"cell_type": "code",
"execution_count": 4,
"execution_count": null,
"id": "06c8fddb-56d9-4f84-8b21-4ffd8b7bd26f",
"metadata": {},
"outputs": [],
@@ -127,7 +112,7 @@
},
{
"cell_type": "code",
"execution_count": 5,
"execution_count": null,
"id": "dc0b52b6-9fc3-453d-b6eb-e325a2b8342e",
"metadata": {},
"outputs": [],
@@ -154,19 +139,10 @@
},
{
"cell_type": "code",
"execution_count": 6,
"execution_count": null,
"id": "13bcd5a6-8f95-4300-a002-04023d0f9bab",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Generating synthetic data: 100x100 pixels, 100 scan points, noise_frac=0.05\n",
"\n"
]
}
],
"outputs": [],
"source": [
"print(f\"Generating synthetic data: {ROWS}x{COLS} pixels, \"\n",
" f\"{N_SCAN} scan points, noise_frac={NOISE_FRAC}\\n\")\n",
@@ -186,17 +162,13 @@
},
{
"cell_type": "code",
"execution_count": 7,
"execution_count": null,
"id": "1f6bc651-80c1-41dd-8a05-7f15aba006aa",
"metadata": {},
"outputs": [],
"source": [
"model = FallingScurve()\n",
"METHOD_DEFS = [\n",
" (\"lmfit (LM)\",\n",
" lambda nt: lambda: fit_scurve2(x, y, n_threads=nt),\n",
" \"#FF9800\", {\"linewidth\": 3.0, \"linestyle\": \"-\"}),\n",
"\n",
" (\"Minuit2 (analytic)\",\n",
" lambda nt: lambda: model.fit(x, y, n_threads=nt),\n",
" \"#4CAF50\", {\"linewidth\": 4.0, \"linestyle\": \":\"}),\n",
@@ -216,27 +188,13 @@
},
{
"cell_type": "code",
"execution_count": 8,
"execution_count": null,
"id": "5a417145-ce42-4c3a-a7bf-05ff97ba0450",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Method time (ms) med|dp0| med|dp1| med|dp2| med|dp3| med|dp4| med|dp5|\n",
"--------------------------------------------------------------------------------------------------------------\n",
"[lmfit (LM) ] 359.31 ms 18.6946 0.2249 0.2177 0.2663 11.9347 0.3635\n",
"[Minuit2 (analytic) ] 257.18 ms 17.1035 0.2147 0.2293 0.2668 11.9449 0.3518 chi2/ndf=596.8385\n"
]
}
],
"outputs": [],
"source": [
"PARAM_NAMES = [\"p0\", \"p1\", \"p2\", \"p3\", \"p4\", \"p5\"]\n",
"ndf = N_SCAN - len(PARAM_NAMES)\n",
"# NOTE: fit_scurve returns Ndf = n_scan - 2 in its dict, which looks wrong\n",
"# for a 6-parameter model. We use our own ndf = n_scan - 6 everywhere.\n",
"\n",
"def extract_result(label, res):\n",
" if isinstance(res, dict):\n",
" out = {\"par\": res[\"par\"]}\n",
@@ -245,8 +203,6 @@
" if \"chi2\" in res:\n",
" out[\"chi2\"] = res[\"chi2\"]\n",
" return out\n",
" # fallback: raw array, assume shape (rows, cols, 6)\n",
" return {\"par\": res}\n",
"\n",
"methods = {}\n",
"for label, factory, _, _ in METHOD_DEFS:\n",
@@ -288,35 +244,12 @@
},
{
"cell_type": "code",
"execution_count": 9,
"execution_count": null,
"id": "55ecc77c-0823-408d-a811-8e4f4900f332",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
" lmfit (LM) n_threads= 1 1404.39 ± 11.48 ms (140.4392 ± 1.1478 µs/pixel)\n",
" Minuit2 (analytic) n_threads= 1 1022.42 ± 1.69 ms (102.2417 ± 0.1690 µs/pixel)\n",
"\n",
"\n",
" lmfit (LM) n_threads= 2 708.78 ± 6.14 ms (70.8775 ± 0.6142 µs/pixel)\n",
" Minuit2 (analytic) n_threads= 2 521.15 ± 0.96 ms (52.1151 ± 0.0962 µs/pixel)\n",
"\n",
"\n",
" Minuit2 (analytic) n_threads= 4 256.73 ± 2.40 ms (25.6728 ± 0.2399 µs/pixel)\n",
" lmfit (LM) n_threads= 4 352.83 ± 0.77 ms (35.2833 ± 0.0767 µs/pixel)\n",
"\n",
"\n",
" lmfit (LM) n_threads= 8 226.62 ± 1.48 ms (22.6616 ± 0.1479 µs/pixel)\n",
" Minuit2 (analytic) n_threads= 8 164.39 ± 0.51 ms (16.4386 ± 0.0509 µs/pixel)\n",
"\n",
"\n"
]
}
],
"outputs": [],
"source": [
"thread_counts = [1, 2, 4, 8]\n",
" \n",
@@ -339,8 +272,8 @@
" per_px = med / (ROWS * COLS) * 1e3\n",
" per_px_std = std / (ROWS * COLS) * 1e3\n",
" print(f\" {label:22s} n_threads={nt:2d} \"\n",
" f\"{med:8.2f} ± {std:6.2f} ms \"\n",
" f\"({per_px:.4f} ± {per_px_std:.4f} µs/pixel)\")\n",
" f\"{med:8.2f} \u00b1 {std:6.2f} ms \"\n",
" f\"({per_px:.4f} \u00b1 {per_px_std:.4f} \u00b5s/pixel)\")\n",
" \n",
" print(\"\\n\")"
]
@@ -355,49 +288,17 @@
},
{
"cell_type": "code",
"execution_count": 10,
"execution_count": null,
"id": "d5f3152d-be84-420e-8045-9c42ac5c24cb",
"metadata": {},
"outputs": [
{
"data": {
"image/png":
"iVBORw0KGgoAAAANSUhEUgAABjUAAAN2CAYAAACxdwtsAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjgsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvwVt1zgAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3Qd0VNXWwPGdQioBQkjo3RCQoogIogKKFBXQh4oVbE95gigKYsGCigqIgB9gQyyAgKgPlfcUxQYqVREVxYCCCNIC0tOT+dY+vDvMnUxCyiRT8v+tdTOZO3fu3DYzZ84+Z58Qh8PhEAAAAAAAAAAAAD8X6usNAAAAAAAAAAAAKA6CGgAAAAAAAAAAICAQ1AAAAAAAAAAAAAGBoAYAAAAAAAAAAAgIBDUAAAAAAAAAAEBAIKgBAAAAAAAAAAACAkENAAAAAAAAAAAQEAhqAAAAAAAAAACAgEBQAwAAAAAAAAAABASCGgAAACjUjTfeKCEhIWbq3r17sY+ULms9T9fhC19++aVzG3T6448/fLIdQGFef/112zXqC66vr9sDBNr3DQAAqHwIagAA4KfcK2Rdp6pVq8qpp54qw4cPly1btvh6UwOCv1bcFXaew8LCpEaNGnLGGWfIfffdJ7t37/b1psLHli5dKnfddZd06dJFYmJiShSwWbhwofTs2VMSEhIkMjJSmjRpIrfccots3ry50Ofs2rXLvF5ycrJER0dLfHy8nHvuufLyyy9LXl5eOeyhyMyZM+Xmm2+Wtm3bSnh4uHP/dHuLkpmZKRMnTpQOHTpItWrVJDY2Vtq0aSMPPfSQHDp0qNDnffvtt3L11VdLvXr1zHGpXbu29O/fXz799NNy2Dt4cvDgQZkwYYJceeWV5jy7XtcnC4iW9hotzfsBgcn1mho7dqyvNwcAAHhJuLdWBAAAKs6xY8dk48aNZnr11Vfl/ffflwsvvJBTEETy8/NNZez3339vptmzZ8uaNWukYcOGFbodWuGrlcOqol8bdjNmzDDv9ZJwOBxy0003yRtvvGGbv23bNvPZMW/ePHn33Xfl4osvLlDZ37t3b/n7779tgYNvvvnGTP/+97/lvffek6ioKK+epnvvvbfIIIQn+/fvNxXU+j5x9fPPP5vpzTfflM8//1yaNm1qe/yVV16RIUOGmPeaZe/evbJ48WIzPfLII/LYY49JeerYsaM888wzUplpQO7+++8v8fNKc42W9v2AisH3DQAAKC6CGgAABIirrrpKzjzzTMnOzpaVK1fKf/7zHzM/PT1dBg0aZCqGtMWpt+Xm5kpOTo5pBYuS0Qo27XFRpUqVEp/nw4cPmwq5n376yczXnhpTpkyRyZMnV+hp6NOnj5nge9rSuEGDBub60FboWvFenECIawWuVhpqL68FCxbIL7/8Yq7Ra6+91vyvvRWsoOkVV1zhrCzWYJZWBOs1OGvWLPPaH3/8sTz88MMFKuR1ff/85z9lzJgx0qpVK4/bpM9r3769DBgwoMBj+n7R5+k+6rW/fv36k+6jBiasgIZ+Tul9rcjWXh8a8NDPxmuuuUZWrFghoaHHO6rrem+//XZnQKNz587St29fUxn+0UcfmXmPP/64nHXWWXLJJZdIeWndurWZKjs9X+3atTPnff78+XLgwIEily/tNVqa9wMqDt83AACg2BwAAMAvffHFFw79qram1157zfb4ddddZ3v8s88+M/OXLl3quOmmmxynn366o3bt2o6IiAhHdHS045RTTjHzf/zxxwKvdcMNNzjX061bN8fvv//uGDhwoCMhIcEREhJitkW9+OKLjiuuuMKRkpJiHgsPD3fExcWZ17rvvvscaWlpBdbduHFj57offfRRx4cffujo3Lmz2ab69es7xowZ48jOzjbLvvDCC45WrVo5IiMjHU2bNnU8+eSTjvz8/ALrzM3Ndbz++uuOHj16OGrVqmW2IzEx0dG/f3/H559/bltW98f1OLlPun2udu7cafalbdu2jqpVq5ptSU5Odtx9992OXbt2FdgW1/XrcVy3bp3joosuctSoUcPM27p1a6nP88GDB835sx7r3bt3oeu48sorHQ0aNDDLV6tWzXHuuec6XnnlFUdeXl6B5ZcvX+647LLLHPXq1XNUqVLFERsba45Dnz59zDnS1y3s2nD373//29GxY0dHVFSUIykpyXHzzTc79uzZU+C4FLa/7sensGOh6xw1apTj/PPPdzRq1MicG912fc2ePXs65syZU+BaKeq1jh496njssccc7du3N+uyrqHTTjvN8c9//tPx0UcfOfxNenq68389NkUdR+t9oufYWubaa691PrZ//37z3rUe02ve8vzzzzvn6/v/119/dT724IMPOh/Tc/7333/bXvOqq64yj9WpU8eRmppaYJse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 truncated
"text/plain": [
"<Figure size 1600x900 with 6 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"text/plain": [
"<Figure size 1400x500 with 0 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"image/png":
"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 truncated
"text/plain": [
"<Figure size 1400x500 with 2 Axes>"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"outputs": [],
"source": [
"# FIGURE 1: Residual histograms (6 panels)\n",
"truth_arrays = [truths[pn] for pn in PARAM_NAMES]\n",
" \n",
"fig1, axes1 = plt.subplots(2, 3, figsize=(16, 9))\n",
"fig1.suptitle(\n",
" f\"Parameter Residuals — {ROWS}×{COLS} pixels, {N_SCAN} scan points\",\n",
" f\"Parameter Residuals \u2014 {ROWS}\u00d7{COLS} pixels, {N_SCAN} scan points\",\n",
" fontsize=14, fontweight=\"bold\")\n",
" \n",
"for idx, (pname, truth) in enumerate(zip(PARAM_NAMES, truth_arrays)):\n",
@@ -421,9 +322,9 @@
" linestyle=styles[mname][\"linestyle\"])\n",
" \n",
" ax.axvline(0, color=\"k\", linestyle=\"--\", linewidth=1, alpha=0.7)\n",
" ax.set_xlabel(f\"Fitted {pname} − True {pname}\")\n",
" ax.set_xlabel(f\"Fitted {pname} \u2212 True {pname}\")\n",
" ax.set_ylabel(\"Pixel count\")\n",
" ax.set_title(f\"Δ{pname}\")\n",
" ax.set_title(f\"\u0394{pname}\")\n",
" ax.legend(fontsize=8)\n",
" ax.grid(alpha=0.3)\n",
" \n",
@@ -444,7 +345,7 @@
" color=[colors[n] for n in names],\n",
" edgecolor=\"white\", height=0.5)\n",
"ax2a.set_xlabel(\"Median wall time (ms)\")\n",
"ax2a.set_title(f\"Single call — {ROWS}×{COLS} px, {N_THREADS} threads\")\n",
"ax2a.set_title(f\"Single call \u2014 {ROWS}\u00d7{COLS} px, {N_THREADS} threads\")\n",
"for bar, val in zip(bars, medians):\n",
" ax2a.text(bar.get_width() + max(medians) * 0.02,\n",
" bar.get_y() + bar.get_height() / 2,\n",
@@ -0,0 +1,147 @@
# SPDX-License-Identifier: MPL-2.0
import numpy as np
import pytest
import aare
from aare.experimental import MultiThreadedFileReader
@pytest.fixture
def frame_file(tmp_path):
data = np.arange(10 * 2 * 3, dtype=np.uint16).reshape(10, 2, 3)
path = tmp_path / "frames.npy"
np.save(path, data)
return path, data
def test_experimental_import_path():
assert aare.experimental.MultiThreadedFileReader is MultiThreadedFileReader
assert MultiThreadedFileReader.__module__ == "aare.experimental"
assert not hasattr(aare, "MultiThreadedFileReader")
def test_reads_all_frames_in_order(frame_file):
path, expected = frame_file
reader = MultiThreadedFileReader(path, n_threads=2, chunk_size=3)
first = reader.read()
second = reader.read()
exhausted = reader.read()
assert np.array_equal(first, expected[:6])
assert np.array_equal(second, expected[6:])
assert exhausted.shape == (0, 2, 3)
assert first.dtype == np.uint16
assert reader.n_threads == 2
assert reader.chunk_size == 3
assert reader.total_frames == 10
assert reader.source_total_frames == 10
assert reader.rows == 2
assert reader.cols == 3
assert reader.bitdepth == 16
assert reader.dtype == np.dtype(np.uint16)
assert reader.bytes_per_frame == 12
assert reader.total_bytes == expected.nbytes
assert len(reader) == 10
assert reader.tell() == 10
assert reader.remaining_frames == 0
assert reader.next_read_frames == 0
assert reader.next_read_bytes == 0
def test_total_frame_limit(frame_file):
path, expected = frame_file
reader = MultiThreadedFileReader(
path, n_threads=2, chunk_size=2, total_frames=7
)
assert np.array_equal(reader.read(), expected[:4])
assert np.array_equal(reader.read_all(), expected[4:7])
def test_iteration_yields_one_chunk_per_thread(frame_file):
path, expected = frame_file
reader = MultiThreadedFileReader(path, n_threads=2, chunk_size=2)
batches = list(reader)
assert [len(batch) for batch in batches] == [4, 4, 2]
assert np.array_equal(np.concatenate(batches), expected)
def test_seek_resets_iteration_position(frame_file):
path, expected = frame_file
reader = MultiThreadedFileReader(path, n_threads=2, chunk_size=2)
reader.read()
assert reader.tell() == 4
reader.seek(1)
assert reader.tell() == 1
assert np.array_equal(reader.read(), expected[1:5])
with pytest.raises(IndexError):
reader.seek(11)
def test_context_manager_closes_worker_files(frame_file):
path, expected = frame_file
with MultiThreadedFileReader(path, n_threads=2, chunk_size=2) as reader:
assert not reader.closed
assert np.array_equal(reader.read(), expected[:4])
assert reader.closed
reader.close()
with pytest.raises(RuntimeError):
reader.read()
with pytest.raises(RuntimeError):
reader.seek(0)
with pytest.raises(RuntimeError):
with reader:
pass
def test_explicit_zero_frame_limit(frame_file):
path, expected = frame_file
reader = MultiThreadedFileReader(
path, n_threads=8, chunk_size=3, total_frames=0
)
actual = reader.read()
assert actual.shape == (0, *expected.shape[1:])
assert actual.dtype == expected.dtype
@pytest.mark.parametrize(
"dtype", [np.int8, np.int32, np.uint64, np.float32, np.float64]
)
def test_preserves_numpy_dtype(tmp_path, dtype):
expected = np.arange(4 * 2 * 3, dtype=dtype).reshape(4, 2, 3)
path = tmp_path / "typed-frames.npy"
np.save(path, expected)
reader = MultiThreadedFileReader(path, n_threads=2, chunk_size=3)
actual = reader.read()
assert actual.dtype == expected.dtype
assert reader.dtype == expected.dtype
assert np.array_equal(actual, expected)
@pytest.mark.parametrize(
("n_threads", "chunk_size", "total_frames"),
[(0, 1, None), (1, 0, None), (1, 1, 11)],
)
def test_invalid_configuration(
frame_file, n_threads, chunk_size, total_frames
):
path, _ = frame_file
with pytest.raises(ValueError):
MultiThreadedFileReader(
path,
n_threads=n_threads,
chunk_size=chunk_size,
total_frames=total_frames,
)
+101
View File
@@ -0,0 +1,101 @@
# SPDX-License-Identifier: MPL-2.0
import numpy as np
import pytest
from aare import PixelHistogram
def _random_frames(rows, cols, n, xmin, xmax, seed=0):
rng = np.random.default_rng(seed)
return [rng.uniform(xmin - 0.25, xmax + 0.25, size=(rows, cols)).astype(np.float64)
for _ in range(n)]
def _reference_hdata(frames, rows, cols, n_bins, xmin, xmax):
expected = np.zeros((rows, cols, n_bins), dtype=np.uint16)
inv_range = n_bins / (xmax - xmin)
for img in frames:
for r in range(rows):
for c in range(cols):
v = float(img[r, c])
if not (xmin <= v < xmax):
continue
b = int((v - xmin) * inv_range)
if b >= n_bins:
b = n_bins - 1
expected[r, c, b] += 1
return expected
def test_async_fill_matches_reference():
rows, cols, n_bins = 5, 7, 8
xmin, xmax = 0.0, 2.0
frames = _random_frames(rows, cols, n=3, xmin=xmin, xmax=xmax, seed=1)
hist = PixelHistogram(rows=rows, cols=cols, n_bins=n_bins, xmin=xmin, xmax=xmax)
for img in frames:
hist.fill_async(img)
np.testing.assert_array_equal(
hist.values(),
_reference_hdata(frames, rows, cols, n_bins, xmin, xmax),
)
def test_fill_async_copies_buffer():
# After fill_async returns, the caller should be free to mutate the
# numpy array without affecting the pending fill.
rows, cols, n_bins = 4, 4, 4
xmin, xmax = 0.0, 1.0
hist = PixelHistogram(rows=rows, cols=cols, n_bins=n_bins, xmin=xmin, xmax=xmax, n_threads=1, max_pending=8)
img = np.full((rows, cols), 0.1, dtype=np.float64) # falls in bin 0
hist.fill_async(img)
# Mutate the original array immediately; this must not affect the
# value that was already enqueued.
img[:] = 0.9 # would be bin 3
hist.flush()
h = hist.values()
assert h.shape == (rows, cols, n_bins)
# Every pixel saw one value in bin 0, none elsewhere.
assert (h[:, :, 0] == 1).all()
assert (h[:, :, 1:] == 0).all()
def test_fill_async_rejects_wrong_shape():
hist = PixelHistogram(8, 8, 4, 0.0, 1.0)
bad = np.zeros((4, 4), dtype=np.float32)
with pytest.raises(ValueError):
hist.fill_async(bad)
def test_hdata_flushes_pending():
# Submit several frames with a tiny queue and read hdata() without an
# explicit flush(); hdata() must drain everything first.
rows, cols, n_bins = 3, 3, 4
xmin, xmax = 0.0, 1.0
hist = PixelHistogram(rows=rows, cols=cols, n_bins=n_bins, xmin=xmin, xmax=xmax,
n_threads=1, max_pending=1)
frames = _random_frames(rows, cols, n=8, xmin=xmin, xmax=xmax, seed=3)
for img in frames:
hist.fill_async(img)
h = hist.values() # no explicit flush()
np.testing.assert_array_equal(
h, _reference_hdata(frames, rows, cols, n_bins, xmin, xmax)
)
def test_bin_centers_and_edges():
n_bins = 5
xmin, xmax = 0.0, 1.0
hist = PixelHistogram(rows=2, cols=2, n_bins=n_bins, xmin=xmin, xmax=xmax)
edges = hist.bin_edges()
centers = hist.bin_centers()
assert edges.shape == (n_bins + 1,)
assert centers.shape == (n_bins,)
np.testing.assert_allclose(edges, np.linspace(xmin, xmax, n_bins + 1), atol=1e-6)
np.testing.assert_allclose(centers, 0.5 * (edges[:-1] + edges[1:]), atol=1e-6)