Removing deprecated use of lmfit (#344)
Build on RHEL9 / build (push) Successful in 2m44s
Build on RHEL8 / build (push) Successful in 3m9s
Run tests using data on local RHEL8 / build (push) Successful in 3m58s
Build on local RHEL8 / build (push) Successful in 2m44s

Removed the lmfit based fitting and the old API for calling functions

closes #296
This commit is contained in:
Erik Fröjdh authored and GitHub committed 2026-08-14 10:31:07 +02:00
1 parent e26db97b5c
commit 4f6b69fba6
18 files changed
+635 -1923

No files matched your search

+4 -60
View File
@@ -62,7 +62,6 @@ option(AARE_FETCH_PYBIND11 "Use FetchContent to download pybind11" ON)
option(AARE_FETCH_CATCH "Use FetchContent to download catch2" ON)
option(AARE_FETCH_JSON "Use FetchContent to download nlohmann::json" ON)
option(AARE_FETCH_ZMQ "Use FetchContent to download libzmq" ON)
option(AARE_FETCH_LMFIT "Use FetchContent to download lmfit" ON)
option(AARE_FETCH_MINUIT2 "Use FetchContent to download Minuit2" ON)
option(AARE_WARNINGS_AS_ERRORS "Treat warnings as errors during compilation"
@@ -87,8 +86,8 @@ if(AARE_SYSTEM_LIBRARIES)
set(AARE_FETCH_ZMQ
OFF
CACHE BOOL "Disabled FetchContent for libzmq" FORCE)
# Still fetch lmfit and Minuit2 when setting AARE_SYSTEM_LIBRARIES since these
# are not available on conda-forge
# Still fetch Minuit2 when setting AARE_SYSTEM_LIBRARIES since it is not
# available on conda-forge
endif()
if(AARE_BENCHMARKS)
@@ -97,61 +96,6 @@ endif()
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
if(AARE_FETCH_LMFIT)
# TODO! Should we fetch lmfit from the web or inlcude a tar.gz in the repo?
set(LMFIT_PATCH_COMMAND git apply
${CMAKE_CURRENT_SOURCE_DIR}/patches/lmfit.patch)
# For cmake < 3.28 we can't supply EXCLUDE_FROM_ALL to FetchContent_Declare so
# we need this workaround
if(${CMAKE_VERSION} VERSION_LESS "3.28")
FetchContent_Declare(
lmfit
GIT_REPOSITORY https://jugit.fz-juelich.de/mlz/lmfit.git
GIT_TAG main
PATCH_COMMAND ${LMFIT_PATCH_COMMAND}
UPDATE_DISCONNECTED 1)
else()
FetchContent_Declare(
lmfit
GIT_REPOSITORY https://jugit.fz-juelich.de/mlz/lmfit.git
GIT_TAG main
PATCH_COMMAND ${LMFIT_PATCH_COMMAND}
UPDATE_DISCONNECTED 1
EXCLUDE_FROM_ALL 1)
endif()
# Disable what we don't need from lmfit
set(BUILD_TESTING
OFF
CACHE BOOL "")
set(LMFIT_CPPTEST
OFF
CACHE BOOL "")
set(LIB_MAN
OFF
CACHE BOOL "")
set(LMFIT_CPPTEST
OFF
CACHE BOOL "")
set(BUILD_SHARED_LIBS
OFF
CACHE BOOL "")
if(${CMAKE_VERSION} VERSION_LESS "3.28")
if(NOT lmfit_POPULATED)
FetchContent_Populate(lmfit)
add_subdirectory(${lmfit_SOURCE_DIR} ${lmfit_BINARY_DIR} EXCLUDE_FROM_ALL)
endif()
else()
FetchContent_MakeAvailable(lmfit)
endif()
set_property(TARGET lmfit PROPERTY POSITION_INDEPENDENT_CODE ON)
else()
find_package(lmfit REQUIRED)
endif()
if(AARE_FETCH_MINUIT2)
# We are building Minuit2 from sources.
@@ -457,8 +401,7 @@ target_link_libraries(
aare_core
PUBLIC fmt::fmt nlohmann_json::nlohmann_json ${STD_FS_LIB} # from
# helpers.cmake
PRIVATE aare_compiler_flags Threads::Threads $<BUILD_INTERFACE:lmfit>
$<BUILD_INTERFACE:aare::Minuit2>)
PRIVATE aare_compiler_flags Threads::Threads $<BUILD_INTERFACE:aare::Minuit2>)
target_include_directories(
aare_core SYSTEM
@@ -490,6 +433,7 @@ if(AARE_TESTS)
${CMAKE_CURRENT_SOURCE_DIR}/src/defs.test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/decode.test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Dtype.test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Fit.test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Frame.test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/DetectorGeometry.test.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Interpolation.test.cpp
+12 -1
View File
@@ -1,5 +1,17 @@
# Release notes
## Next
### API Changes:
- Removed the lmfit dependency and the legacy ``fit_gaus``, ``fit_pol1``,
``fit_scurve``, and ``fit_scurve2`` APIs. Use ``Gaussian``, ``Pol1``,
``RisingScurve``, or ``FallingScurve`` and call ``model.fit(...)`` (or
``fit(model, ...)``) instead.
- Removed the legacy ``gaus``, ``pol1``, ``scurve``, and ``scurve2`` function
evaluators. Model objects are callable and provide the replacement, for
example ``Gaussian()(x, par)``.
## 2026.7.2
@@ -151,4 +163,3 @@ dhanya.thattil@psi.ch
+3 -22
View File
@@ -75,24 +75,7 @@ static void report_accuracy(benchmark::State &state, const TestCase &tc,
// Benchmarks
// ----------
// 1. lmcurve
static void BM_FitGausLm(benchmark::State &state) {
const auto &tc = get_test_cases()[state.range(0)];
auto data = generate_gaussian_data(tc);
auto xv = data.x.view();
auto yv = data.y.view();
aare::NDArray<double, 1> result;
for (auto _ : state) {
result = aare::fit_gaus(xv, yv);
benchmark::DoNotOptimize(result.data());
}
report_accuracy(state, tc, result);
state.SetLabel(tc.name);
}
// 2. Minuit2, analytic gradient (no Hesse)
// Minuit2, analytic gradient (no Hesse)
static void BM_FitGausMinuitGrad(benchmark::State &state) {
const auto &tc = get_test_cases()[state.range(0)];
auto data = generate_gaussian_data(tc);
@@ -115,7 +98,7 @@ static void BM_FitGausMinuitGrad(benchmark::State &state) {
state.SetLabel(tc.name);
}
// 3. Minuit2, analytic gradient + Hesse
// Minuit2, analytic gradient + Hesse
static void BM_FitGausMinuitGradHesse(benchmark::State &state) {
const auto &tc = get_test_cases()[state.range(0)];
auto data = generate_gaussian_data(tc);
@@ -145,8 +128,6 @@ static void BM_FitGausMinuitGradHesse(benchmark::State &state) {
state.SetLabel(tc.name);
}
BENCHMARK(BM_FitGausLm)->DenseRange(0, 5)->Unit(benchmark::kMicrosecond);
BENCHMARK(BM_FitGausMinuitGrad)
->DenseRange(0, 5)
->Unit(benchmark::kMicrosecond);
@@ -155,4 +136,4 @@ BENCHMARK(BM_FitGausMinuitGradHesse)
->DenseRange(0, 5)
->Unit(benchmark::kMicrosecond);
BENCHMARK_MAIN();
BENCHMARK_MAIN();
+2 -2
View File
@@ -14,7 +14,7 @@ Requirements
To simplify deployment we build and statically link a few libraries.
- fmt
- lmfit - https://jugit.fz-juelich.de/mlz/lmfit
- Minuit2
- nlohmann_json
- pybind11
- ZeroMQ
@@ -23,4 +23,4 @@ To simplify deployment we build and statically link a few libraries.
- Sphinx
- Breathe
- Doxygen
- Doxygen
+20 -9
View File
@@ -1,19 +1,30 @@
Fit
========
Fitting
-------
.. py:currentmodule:: aare
Aare fits one-dimensional scans and three-dimensional pixel data with
Minuit2. Create a model object and call its :meth:`fit` method::
**Functions**
model = Gaussian(compute_errors=True)
result = model.fit(x, y, y_err)
.. autofunction:: gaus
The model object is also callable, which evaluates it at the supplied points::
.. autofunction:: pol1
fitted_y = model(x, result["par"])
The available models are ``Gaussian``, ``GaussianErfcPlateau``,
``GaussianChargeSharing``, ``GaussianChargeSharingKb``, ``Pol1``, ``Pol2``,
``RisingScurve``, and ``FallingScurve``. The module-level :func:`fit` function
accepts the same model objects when a functional interface is preferred.
**Fitting**
For three-dimensional data, pass an array with shape
``(rows, columns, scan_points)`` and select the worker count with
``n_threads``::
.. autofunction:: fit_gaus
result = model.fit(x, image_data, image_errors, n_threads=8)
.. autofunction:: fit_pol1
The result dictionary contains ``par`` and ``chi2``. It also contains
``par_err`` when ``compute_errors`` is enabled.
.. autofunction:: fit
-100
View File
@@ -1,111 +1,11 @@
// SPDX-License-Identifier: MPL-2.0
#pragma once
#include <cmath>
#include <vector>
#include "aare/FitModel.hpp"
#include "aare/NDArray.hpp"
#include "aare/utils/par.hpp"
#include "aare/utils/task.hpp"
namespace aare {
namespace func {
double gaus(const double x, const double *par);
NDArray<double, 1> gaus(NDView<double, 1> x, NDView<double, 1> par);
double pol1(const double x, const double *par);
NDArray<double, 1> pol1(NDView<double, 1> x, NDView<double, 1> par);
double scurve(const double x, const double *par);
NDArray<double, 1> scurve(NDView<double, 1> x, NDView<double, 1> par);
double scurve2(const double x, const double *par);
NDArray<double, 1> scurve2(NDView<double, 1> x, NDView<double, 1> par);
} // namespace func
static constexpr int DEFAULT_NUM_THREADS = 4;
/**
* @brief Fit a 1D Gaussian to data.
* @param data data to fit
* @param x x values
*/
NDArray<double, 1> fit_gaus(NDView<double, 1> x, NDView<double, 1> y);
/**
* @brief Fit a 1D Gaussian to each pixel. Data layout [row, col, values]
* @param x x values
* @param y y values, layout [row, col, values]
* @param n_threads number of threads to use
*/
NDArray<double, 3> fit_gaus(NDView<double, 1> x, NDView<double, 3> y,
int n_threads = DEFAULT_NUM_THREADS);
/**
* @brief Fit a 1D Gaussian with error estimates
* @param x x values
* @param y y values, layout [row, col, values]
* @param y_err error in y, layout [row, col, values]
* @param par_out output parameters
* @param par_err_out output error parameters
*/
void fit_gaus(NDView<double, 1> x, NDView<double, 1> y, NDView<double, 1> y_err,
NDView<double, 1> par_out, NDView<double, 1> par_err_out,
double &chi2);
/**
* @brief Fit a 1D Gaussian to each pixel with error estimates. Data layout
* [row, col, values]
* @param x x values
* @param y y values, layout [row, col, values]
* @param y_err error in y, layout [row, col, values]
* @param par_out output parameters, layout [row, col, values]
* @param par_err_out output parameter errors, layout [row, col, values]
* @param n_threads number of threads to use
*/
void fit_gaus(NDView<double, 1> x, NDView<double, 3> y, NDView<double, 3> y_err,
NDView<double, 3> par_out, NDView<double, 3> par_err_out,
NDView<double, 2> chi2_out, int n_threads = DEFAULT_NUM_THREADS);
NDArray<double, 1> fit_pol1(NDView<double, 1> x, NDView<double, 1> y);
NDArray<double, 3> fit_pol1(NDView<double, 1> x, NDView<double, 3> y,
int n_threads = DEFAULT_NUM_THREADS);
void fit_pol1(NDView<double, 1> x, NDView<double, 1> y, NDView<double, 1> y_err,
NDView<double, 1> par_out, NDView<double, 1> par_err_out,
double &chi2);
// TODO! not sure we need to offer the different version in C++
void fit_pol1(NDView<double, 1> x, NDView<double, 3> y, NDView<double, 3> y_err,
NDView<double, 3> par_out, NDView<double, 3> par_err_out,
NDView<double, 2> chi2_out, int n_threads = DEFAULT_NUM_THREADS);
NDArray<double, 1> fit_scurve(NDView<double, 1> x, NDView<double, 1> y);
NDArray<double, 3> fit_scurve(NDView<double, 1> x, NDView<double, 3> y,
int n_threads);
void fit_scurve(NDView<double, 1> x, NDView<double, 1> y,
NDView<double, 1> y_err, NDView<double, 1> par_out,
NDView<double, 1> par_err_out, double &chi2);
void fit_scurve(NDView<double, 1> x, NDView<double, 3> y,
NDView<double, 3> y_err, NDView<double, 3> par_out,
NDView<double, 3> par_err_out, NDView<double, 2> chi2_out,
int n_threads);
NDArray<double, 1> fit_scurve2(NDView<double, 1> x, NDView<double, 1> y);
NDArray<double, 3> fit_scurve2(NDView<double, 1> x, NDView<double, 3> y,
int n_threads);
void fit_scurve2(NDView<double, 1> x, NDView<double, 1> y,
NDView<double, 1> y_err, NDView<double, 1> par_out,
NDView<double, 1> par_err_out, double &chi2);
void fit_scurve2(NDView<double, 1> x, NDView<double, 3> y,
NDView<double, 3> y_err, NDView<double, 3> par_out,
NDView<double, 3> par_err_out, NDView<double, 2> chi2_out,
int n_threads);
// ---------------------------------------------------------------------------
// Minuit2-based pixel fitting.
// Template bodies and explicit instantiations live in src/Fit.cpp.
-13
View File
@@ -1,13 +0,0 @@
diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt
index 4efb7ed..6533660 100644
--- a/lib/CMakeLists.txt
+++ b/lib/CMakeLists.txt
@@ -11,7 +11,7 @@ target_compile_definitions(${lib} PRIVATE "LMFIT_EXPORT") # for Windows DLL expo
target_include_directories(${lib}
PUBLIC
- $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/>
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/>
$<INSTALL_INTERFACE:include/>
)
-1
View File
@@ -41,7 +41,6 @@ set(PYTHON_FILES
aare/Cluster.py
aare/calibration.py
aare/experimental.py
aare/func.py
aare/RawFile.py
aare/transform.py
aare/ScanParameters.py
-4
View File
@@ -26,7 +26,6 @@ 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
@@ -42,9 +41,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
-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 -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);
}
}
+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":
"iVBORw0KGgoAAAANSUhEUgAAA9oAAAF0CAYAAADcnkHwAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAADV1UlEQVR4nOzdeZxN5R/A8c+562x39hVjLMmuiEIKJSJCREkIqSwltIhsJSU/8lO0iCGJUpKSfUk/KaFkJ8Y6YzD7cuduz++Pa64ZZqzDDL7v1+u+uOeee85z7tyZc77neZ7vV1NKKYQQQgghhBBCCFEkdMXdACGEEEIIIYQQ4mYigbYQQgghhBBCCFGEJNAWQgghhBBCCCGKkATaQgghhBBCCCFEEZJAWwghhBBCCCGEKEISaAshhBBCCCGEEEVIAm0hhBBCCCGEEKIISaAthBBCCCGEEEIUIQm0hRBCCCGEEEKIIiSBtrhpxcbGomma52EwGIiKiuKJJ55g3759563fpEkTmjRpcln7WLt2LZqmsXbt2qJp9GXatWsXTz/9NBUqVMDLy4vQ0FDq1KlD//79SUtLK5Y2Fbfhw4dTtmxZDAYDgYGBwPk/26ysLEaNGlVsPzchhBBX79zzfN7HkCFDLnk7cXFxaJpGbGzseduOi4vzLOvRowflypUrugO4DEop5s2bx3333Ud4eDheXl6UKVOGFi1aMH369GJpU3GLi4vjkUceITg4GE3TGDhwYIE/yw0bNjBq1ChSUlKKra3i1mQo7gYIca3NnDmTKlWqYLVa+d///sfYsWNZs2YNu3fvJigoyLPe1KlTL3vbderU4bfffqNatWpF2eRLsnXrVu69916qVq3KiBEjKFeuHKdOneLvv/9m3rx5DBkyBH9//+veruK0aNEixo4dy7Bhw2jZsiVmsxk4/2eblZXF6NGjAS775ooQQoiSJfc8n1epUqWKfD9vvvkmL730UpFv91IMHTqU9957j2effZZXXnkFi8XCoUOHWL16NYsWLaJ3797F0q7i9PLLL/P7778zY8YMIiMjiYqKIjIykt9++42KFSt61tuwYQOjR4+mR48enhvwQlwPEmiLm16NGjWoW7cu4A6qnE4nI0eO5Pvvv+eZZ57xrHclwbK/vz/169cvsrZejg8++ACdTsfatWuxWCye5R07duStt95CKVUs7cpLKYXVasXb2/u67G/79u0AvPjii4SHh3uWF8eNECGEENdH3vP8tZQ3eLuesrOz+eCDD+jWrRuffvppvtd69OiBy+UqlnadKzs7Gy8vLzRNuy772759O3fffTft2rXLt7y4rsuEOJcMHRe3nNyT8YkTJ/ItL2jo+LRp07jjjjvw8/PDYrFQpUoV3njjDc/rBQ0d79GjB35+fuzfv59WrVrh5+dHdHQ0gwcPJicnJ9/2jx49SseOHbFYLAQGBvLUU0+xadOm84Y9FeT06dP4+/vj5+dX4OuXeqL7/fffadOmDSEhIXh5eVGxYkUGDhyY73gKGio3atSo8/ahaRr9+/fn448/pmrVqpjNZqZPn054eDhPP/30edtISUnB29ubQYMGeZalpaUxZMgQypcvj8lkonTp0gwcOJDMzMwLHke5cuUYPnw4ABEREWiaxqhRo4D8P9u4uDjCwsIAGD16tGeYYY8ePQA4efIkffr0ITo6GrPZTFhYGPfeey8rV6684P6FEEKULPv37+eZZ56hUqVK+Pj4ULp0adq0acM///xzRdsr6HyYe9774osvqFq1Kj4+Ptxxxx38+OOP571/0aJF1KpVC7PZTIUKFZg8eXKB59JzZWZmkpOTQ1RUVIGv63SXdjnvcrmYMmUKd955J97e3gQGBlK/fn1++OGHfMeTe+7Mq1y5cp7zJJwdWr98+XJ69uxJWFgYPj4+zJ8/H03TWLVq1XnbmDZtGpqmsW3bNs+yP//8k0cffZTg4GC8vLyoXbs2X3/99QWPI/faa//+/fz888+e83hcXNx5Q8dHjRrFK6+8AkD58uU96+Zet61evZomTZoQEhKCt7c3ZcuWpUOHDmRlZV3SZyrEhUiPtrjlHDx4EIDbb7/9guvNmzePvn37MmDAACZMmIBOp2P//v3s3Lnzovuw2+08+uij9OrVi8GDB/PLL7/w1ltvERAQwIgRIwD3ibNp06YkJSXx3nvvcdttt7F06VI6d+58ScfRoEEDfvrpJ5566imee+457r777svuOV62bBlt2rShatWqTJw4kbJlyxIXF8fy5csvazt5ff/996xfv54RI0YQGRlJeHg4Bw8e5OOPP+ajjz7KN5z9q6++wmq1ekYWZGVl0bhxY44ePcobb7xBrVq12LFjByNGjOCff/5h5cqVhV6QLFy4kI8++ojPP/+cpUuXEhAQQJkyZc5bLyoqiqVLl/Lwww/Tq1cvz3C73OD76aefZsuWLYwdO5bbb7+dlJQUtmzZwunTp6/4MxFCCHHtOJ1OHA5HvmUGg4Hjx48TEhLCu+++S1hYGElJScyaNYt77rmHrVu3Urly5SLZ/08//cSmTZsYM2YMfn5+jB8/nvbt27Nnzx4qVKgAwNKlS3nssce4//77mT9/Pg6HgwkTJpx3078goaGh3HbbbUydOpXw8HBatWpF5cqVL7vnuEePHsyZM4devXoxZswYTCYTW7ZsyTcP/XL17NmTRx55hC+++ILMzExat25NeHg4M2fO5MEHH8y3bmxsLHXq1KFWrVoArFmzhocffph77rmHjz/+mICAAObNm0fnzp3JysrKF9jnlTttr3379lSsWJEJEyYA7vN7fHx8vnV79+5NUlISU6ZM4bvvvvPcrKhWrZpnjvd9993HjBkzCAwM5NixYyxduhSbzYaPj88Vfy5CAKCEuEnNnDlTAWrjxo3Kbrer9PR0tXTpUhUZGanuv/9+Zbfb863fuHFj1bhxY8/z/v37q8DAwAvuY82aNQpQa9as8Szr3r27AtTXX3+db91WrVqpypUre55/9NFHClA///xzvvWee+45BaiZM2decN9Wq1W1a9dOAQpQer1e1a5dWw0bNkwlJiZe8L25KlasqCpWrKiys7MLXad79+4qJibmvOUjR45U5/4JAVRAQIBKSkrKt3zbtm0KUJ9++mm+5Xfffbe66667PM/HjRundDqd2rRpU771FixYoAC1ZMmSCx5PbptOnjyZb/m5P9uTJ08qQI0cOfK8bfj5+amBAwdecD9CCCGKX+55vqDHued4pZRyOBzKZrOpSpUqqZdfftmz/ODBg+edd3O3ffDgQc+ygs6HgIqIiFBpaWmeZQkJCUqn06lx48Z5ltWrV09FR0ernJwcz7L09HQVEhJy3rm0IH/88YcqW7as5/gsFotq3bq1mj17tnK5XBd9/y+//KIANWzYsAuuV9i5MSYmRnXv3t3zPPfz6dat23nrDho0SHl7e6uUlBTPsp07dypATZkyxbOsSpUqqnbt2uf9rFq3bq2ioqKU0+m8YFtjYmLUI488km9ZQT/L999//7yfpVJnry3++uuvC+5HiCslQ8fFTa9+/foYjUYsFgsPP/wwQUFBLFq0CIPhwgM67r77blJSUnjyySdZtGgRp06duuR9appGmzZt8i2rVasWhw4d8jxft26dp015Pfnkk5e0D7PZzMKFC9m5cyeTJk3iiSee4OTJk4wdO5aqVauyZ88ewD1P2uFw5HsA7N27l3///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",
+1 -452
View File
@@ -4,7 +4,6 @@
#include "Minuit2/FunctionMinimum.h"
#include "Minuit2/MnHesse.h"
#include "Minuit2/MnMigrad.h"
#include "Minuit2/MnPrint.h"
#include "Minuit2/MnStrategy.h"
#include "Minuit2/MnUserParameters.h"
#include "aare/Models.hpp"
@@ -12,461 +11,11 @@
#include "aare/utils/task.hpp"
#include <array>
#include <cmath>
#include <lmcurve2.h>
#include <lmfit.hpp>
#include <memory>
#include <stdexcept>
#include <thread>
#include <type_traits>
namespace aare {
namespace func {
double gaus(const double x, const double *par) {
return par[0] * exp(-pow(x - par[1], 2) / (2 * pow(par[2], 2)));
}
NDArray<double, 1> gaus(NDView<double, 1> x, NDView<double, 1> par) {
NDArray<double, 1> y({x.shape(0)}, 0);
for (ssize_t i = 0; i < x.size(); i++) {
y(i) = gaus(x(i), par.data());
}
return y;
}
double pol1(const double x, const double *par) { return par[0] * x + par[1]; }
NDArray<double, 1> pol1(NDView<double, 1> x, NDView<double, 1> par) {
NDArray<double, 1> y({x.shape()}, 0);
for (ssize_t i = 0; i < x.size(); i++) {
y(i) = pol1(x(i), par.data());
}
return y;
}
double scurve(const double x, const double *par) {
return (par[0] + par[1] * x) +
0.5 * (1 + erf((x - par[2]) / (sqrt(2) * par[3]))) *
(par[4] + par[5] * (x - par[2]));
}
NDArray<double, 1> scurve(NDView<double, 1> x, NDView<double, 1> par) {
NDArray<double, 1> y({x.shape()}, 0);
for (ssize_t i = 0; i < x.size(); i++) {
y(i) = scurve(x(i), par.data());
}
return y;
}
double scurve2(const double x, const double *par) {
return (par[0] + par[1] * x) +
0.5 * (1 - erf((x - par[2]) / (sqrt(2) * par[3]))) *
(par[4] + par[5] * (x - par[2]));
}
NDArray<double, 1> scurve2(NDView<double, 1> x, NDView<double, 1> par) {
NDArray<double, 1> y({x.shape()}, 0);
for (ssize_t i = 0; i < x.size(); i++) {
y(i) = scurve2(x(i), par.data());
}
return y;
}
} // namespace func
NDArray<double, 1> fit_gaus(NDView<double, 1> x, NDView<double, 1> y) {
NDArray<double, 1> result = model::Gaussian::estimate_par(x, y);
lm_status_struct status;
lmcurve(result.size(), result.data(), x.size(), x.data(), y.data(),
aare::func::gaus, &lm_control_double, &status);
return result;
}
NDArray<double, 3> fit_gaus(NDView<double, 1> x, NDView<double, 3> y,
int n_threads) {
NDArray<double, 3> result({y.shape(0), y.shape(1), 3}, 0);
auto process = [&x, &y, &result](ssize_t first_row, ssize_t last_row) {
for (ssize_t row = first_row; row < last_row; row++) {
for (ssize_t col = 0; col < y.shape(1); col++) {
NDView<double, 1> values(&y(row, col, 0), {y.shape(2)});
auto res = fit_gaus(x, values);
result(row, col, 0) = res(0);
result(row, col, 1) = res(1);
result(row, col, 2) = res(2);
}
}
};
auto tasks = split_task(0, y.shape(0), n_threads);
RunInParallel(process, tasks);
return result;
}
void fit_gaus(NDView<double, 1> x, NDView<double, 1> y, NDView<double, 1> y_err,
NDView<double, 1> par_out, NDView<double, 1> par_err_out,
double &chi2) {
// Check that we have the correct sizes
if (y.size() != x.size() || y.size() != y_err.size() ||
par_out.size() != 3 || par_err_out.size() != 3) {
throw std::runtime_error("Data, x, data_err must have the same size "
"and par_out, par_err_out must have size 3");
}
// /* Collection of output parameters for status info. */
// typedef struct {
// double fnorm; /* norm of the residue vector fvec. */
// int nfev; /* actual number of iterations. */
// int outcome; /* Status indicator. Nonnegative values are used as
// index
// for the message text lm_infmsg, set in lmmin.c. */
// int userbreak; /* Set when function evaluation requests termination.
// */
// } lm_status_struct;
lm_status_struct status;
par_out = model::Gaussian::estimate_par(x, y);
std::array<double, 9> cov{0, 0, 0, 0, 0, 0, 0, 0, 0};
// void lmcurve2( const int n_par, double *par, double *parerr, double
// *covar, const int m_dat, const double *t, const double *y, const double
// *dy, double (*f)( const double ti, const double *par ), const
// lm_control_struct *control, lm_status_struct *status); n_par - Number of
// free variables. Length of parameter vector par. par - Parameter vector.
// On input, it must contain a reasonable guess. On output, it contains the
// solution found to minimize ||r||. parerr - Parameter uncertainties
// vector. Array of length n_par or NULL. On output, unless it or covar is
// NULL, it contains the weighted parameter uncertainties for the found
// parameters. covar - Covariance matrix. Array of length n_par * n_par or
// NULL. On output, unless it is NULL, it contains the covariance matrix.
// m_dat - Number of data points. Length of vectors t, y, dy. Must statisfy
// n_par <= m_dat. t - Array of length m_dat. Contains the abcissae (time,
// or "x") for which function f will be evaluated. y - Array of length
// m_dat. Contains the ordinate values that shall be fitted. dy - Array of
// length m_dat. Contains the standard deviations of the values y. f - A
// user-supplied parametric function f(ti;par). control - Parameter
// collection for tuning the fit procedure. In most cases, the default
// &lm_control_double is adequate. If f is only computed with
// single-precision accuracy, &lm_control_float should be used. Parameters
// are explained in lmmin2(3). status - A record used to return information
// about the minimization process: For details, see lmmin2(3).
lmcurve2(par_out.size(), par_out.data(), par_err_out.data(), cov.data(),
x.size(), x.data(), y.data(), y_err.data(), aare::func::gaus,
&lm_control_double, &status);
// Calculate chi2
chi2 = 0;
for (ssize_t i = 0; i < y.size(); i++) {
chi2 +=
std::pow((y(i) - func::gaus(x(i), par_out.data())) / y_err(i), 2);
}
}
void fit_gaus(NDView<double, 1> x, NDView<double, 3> y, NDView<double, 3> y_err,
NDView<double, 3> par_out, NDView<double, 3> par_err_out,
NDView<double, 2> chi2_out,
int n_threads) {
auto process = [&](ssize_t first_row, ssize_t last_row) {
for (ssize_t row = first_row; row < last_row; row++) {
for (ssize_t col = 0; col < y.shape(1); col++) {
NDView<double, 1> y_view(&y(row, col, 0), {y.shape(2)});
NDView<double, 1> y_err_view(&y_err(row, col, 0),
{y_err.shape(2)});
NDView<double, 1> par_out_view(&par_out(row, col, 0),
{par_out.shape(2)});
NDView<double, 1> par_err_out_view(&par_err_out(row, col, 0),
{par_err_out.shape(2)});
fit_gaus(x, y_view, y_err_view, par_out_view, par_err_out_view,
chi2_out(row, col));
}
}
};
auto tasks = split_task(0, y.shape(0), n_threads);
RunInParallel(process, tasks);
}
void fit_pol1(NDView<double, 1> x, NDView<double, 1> y, NDView<double, 1> y_err,
NDView<double, 1> par_out, NDView<double, 1> par_err_out,
double &chi2) {
// Check that we have the correct sizes
if (y.size() != x.size() || y.size() != y_err.size() ||
par_out.size() != 2 || par_err_out.size() != 2) {
throw std::runtime_error("Data, x, data_err must have the same size "
"and par_out, par_err_out must have size 2");
}
lm_status_struct status;
par_out = model::Pol1::estimate_par(x, y);
std::array<double, 4> cov{0, 0, 0, 0};
lmcurve2(par_out.size(), par_out.data(), par_err_out.data(), cov.data(),
x.size(), x.data(), y.data(), y_err.data(), aare::func::pol1,
&lm_control_double, &status);
// Calculate chi2
chi2 = 0;
for (ssize_t i = 0; i < y.size(); i++) {
chi2 +=
std::pow((y(i) - func::pol1(x(i), par_out.data())) / y_err(i), 2);
}
}
void fit_pol1(NDView<double, 1> x, NDView<double, 3> y, NDView<double, 3> y_err,
NDView<double, 3> par_out, NDView<double, 3> par_err_out,
NDView<double, 2> chi2_out, int n_threads) {
auto process = [&](ssize_t first_row, ssize_t last_row) {
for (ssize_t row = first_row; row < last_row; row++) {
for (ssize_t col = 0; col < y.shape(1); col++) {
NDView<double, 1> y_view(&y(row, col, 0), {y.shape(2)});
NDView<double, 1> y_err_view(&y_err(row, col, 0),
{y_err.shape(2)});
NDView<double, 1> par_out_view(&par_out(row, col, 0),
{par_out.shape(2)});
NDView<double, 1> par_err_out_view(&par_err_out(row, col, 0),
{par_err_out.shape(2)});
fit_pol1(x, y_view, y_err_view, par_out_view, par_err_out_view,
chi2_out(row, col));
}
}
};
auto tasks = split_task(0, y.shape(0), n_threads);
RunInParallel(process, tasks);
}
NDArray<double, 1> fit_pol1(NDView<double, 1> x, NDView<double, 1> y) {
// // Check that we have the correct sizes
// if (y.size() != x.size() || y.size() != y_err.size() ||
// par_out.size() != 2 || par_err_out.size() != 2) {
// throw std::runtime_error("Data, x, data_err must have the same size "
// "and par_out, par_err_out must have size 2");
// }
NDArray<double, 1> par = model::Pol1::estimate_par(x, y);
lm_status_struct status;
lmcurve(par.size(), par.data(), x.size(), x.data(), y.data(),
aare::func::pol1, &lm_control_double, &status);
return par;
}
NDArray<double, 3> fit_pol1(NDView<double, 1> x, NDView<double, 3> y,
int n_threads) {
NDArray<double, 3> result({y.shape(0), y.shape(1), 2}, 0);
auto process = [&](ssize_t first_row, ssize_t last_row) {
for (ssize_t row = first_row; row < last_row; row++) {
for (ssize_t col = 0; col < y.shape(1); col++) {
NDView<double, 1> values(&y(row, col, 0), {y.shape(2)});
auto res = fit_pol1(x, values);
result(row, col, 0) = res(0);
result(row, col, 1) = res(1);
}
}
};
auto tasks = split_task(0, y.shape(0), n_threads);
RunInParallel(process, tasks);
return result;
}
// ~~ S-CURVES ~~
// - No error
NDArray<double, 1> fit_scurve(NDView<double, 1> x, NDView<double, 1> y) {
NDArray<double, 1> result = model::RisingScurve::estimate_par(x, y);
lm_status_struct status;
lmcurve(result.size(), result.data(), x.size(), x.data(), y.data(),
aare::func::scurve, &lm_control_double, &status);
return result;
}
NDArray<double, 3> fit_scurve(NDView<double, 1> x, NDView<double, 3> y,
int n_threads) {
NDArray<double, 3> result({y.shape(0), y.shape(1), 6}, 0);
auto process = [&x, &y, &result](ssize_t first_row, ssize_t last_row) {
for (ssize_t row = first_row; row < last_row; row++) {
for (ssize_t col = 0; col < y.shape(1); col++) {
NDView<double, 1> values(&y(row, col, 0), {y.shape(2)});
auto res = fit_scurve(x, values);
result(row, col, 0) = res(0);
result(row, col, 1) = res(1);
result(row, col, 2) = res(2);
result(row, col, 3) = res(3);
result(row, col, 4) = res(4);
result(row, col, 5) = res(5);
}
}
};
auto tasks = split_task(0, y.shape(0), n_threads);
RunInParallel(process, tasks);
return result;
}
// - Error
void fit_scurve(NDView<double, 1> x, NDView<double, 1> y,
NDView<double, 1> y_err, NDView<double, 1> par_out,
NDView<double, 1> par_err_out, double &chi2) {
// Check that we have the correct sizes
if (y.size() != x.size() || y.size() != y_err.size() ||
par_out.size() != 6 || par_err_out.size() != 6) {
throw std::runtime_error("Data, x, data_err must have the same size "
"and par_out, par_err_out must have size 6");
}
lm_status_struct status;
par_out = model::RisingScurve::estimate_par(x, y);
std::array<double, 36> cov = {0}; // size 6x6
// std::array<double, 4> cov{0, 0, 0, 0};
lmcurve2(par_out.size(), par_out.data(), par_err_out.data(), cov.data(),
x.size(), x.data(), y.data(), y_err.data(), aare::func::scurve,
&lm_control_double, &status);
// Calculate chi2
chi2 = 0;
for (ssize_t i = 0; i < y.size(); i++) {
chi2 +=
std::pow((y(i) - func::pol1(x(i), par_out.data())) / y_err(i), 2);
}
}
void fit_scurve(NDView<double, 1> x, NDView<double, 3> y,
NDView<double, 3> y_err, NDView<double, 3> par_out,
NDView<double, 3> par_err_out, NDView<double, 2> chi2_out,
int n_threads) {
auto process = [&](ssize_t first_row, ssize_t last_row) {
for (ssize_t row = first_row; row < last_row; row++) {
for (ssize_t col = 0; col < y.shape(1); col++) {
NDView<double, 1> y_view(&y(row, col, 0), {y.shape(2)});
NDView<double, 1> y_err_view(&y_err(row, col, 0),
{y_err.shape(2)});
NDView<double, 1> par_out_view(&par_out(row, col, 0),
{par_out.shape(2)});
NDView<double, 1> par_err_out_view(&par_err_out(row, col, 0),
{par_err_out.shape(2)});
fit_scurve(x, y_view, y_err_view, par_out_view,
par_err_out_view, chi2_out(row, col));
}
}
};
auto tasks = split_task(0, y.shape(0), n_threads);
RunInParallel(process, tasks);
}
// SCURVE2 ---
// - No error
NDArray<double, 1> fit_scurve2(NDView<double, 1> x, NDView<double, 1> y) {
NDArray<double, 1> result = model::FallingScurve::estimate_par(x, y);
lm_status_struct status;
lmcurve(result.size(), result.data(), x.size(), x.data(), y.data(),
aare::func::scurve2, &lm_control_double, &status);
return result;
}
NDArray<double, 3> fit_scurve2(NDView<double, 1> x, NDView<double, 3> y,
int n_threads) {
NDArray<double, 3> result({y.shape(0), y.shape(1), 6}, 0);
auto process = [&x, &y, &result](ssize_t first_row, ssize_t last_row) {
for (ssize_t row = first_row; row < last_row; row++) {
for (ssize_t col = 0; col < y.shape(1); col++) {
NDView<double, 1> values(&y(row, col, 0), {y.shape(2)});
auto res = fit_scurve2(x, values);
result(row, col, 0) = res(0);
result(row, col, 1) = res(1);
result(row, col, 2) = res(2);
result(row, col, 3) = res(3);
result(row, col, 4) = res(4);
result(row, col, 5) = res(5);
}
}
};
auto tasks = split_task(0, y.shape(0), n_threads);
RunInParallel(process, tasks);
return result;
}
// - Error
void fit_scurve2(NDView<double, 1> x, NDView<double, 1> y,
NDView<double, 1> y_err, NDView<double, 1> par_out,
NDView<double, 1> par_err_out, double &chi2) {
// Check that we have the correct sizes
if (y.size() != x.size() || y.size() != y_err.size() ||
par_out.size() != 6 || par_err_out.size() != 6) {
throw std::runtime_error("Data, x, data_err must have the same size "
"and par_out, par_err_out must have size 6");
}
lm_status_struct status;
par_out = model::FallingScurve::estimate_par(x, y);
std::array<double, 36> cov = {0}; // size 6x6
// std::array<double, 4> cov{0, 0, 0, 0};
lmcurve2(par_out.size(), par_out.data(), par_err_out.data(), cov.data(),
x.size(), x.data(), y.data(), y_err.data(), aare::func::scurve2,
&lm_control_double, &status);
// Calculate chi2
chi2 = 0;
for (ssize_t i = 0; i < y.size(); i++) {
chi2 +=
std::pow((y(i) - func::pol1(x(i), par_out.data())) / y_err(i), 2);
}
}
void fit_scurve2(NDView<double, 1> x, NDView<double, 3> y,
NDView<double, 3> y_err, NDView<double, 3> par_out,
NDView<double, 3> par_err_out, NDView<double, 2> chi2_out,
int n_threads) {
auto process = [&](ssize_t first_row, ssize_t last_row) {
for (ssize_t row = first_row; row < last_row; row++) {
for (ssize_t col = 0; col < y.shape(1); col++) {
NDView<double, 1> y_view(&y(row, col, 0), {y.shape(2)});
NDView<double, 1> y_err_view(&y_err(row, col, 0),
{y_err.shape(2)});
NDView<double, 1> par_out_view(&par_out(row, col, 0),
{par_out.shape(2)});
NDView<double, 1> par_err_out_view(&par_err_out(row, col, 0),
{par_err_out.shape(2)});
fit_scurve2(x, y_view, y_err_view, par_out_view,
par_err_out_view, chi2_out(row, col));
}
}
};
auto tasks = split_task(0, y.shape(0), n_threads);
RunInParallel(process, tasks);
}
// ============================================================================
// FitModel<Model> — method definitions
// (constructor, destructor, copy, and all methods that touch Minuit2 state)
@@ -756,4 +305,4 @@ AARE_INSTANTIATE_FIT(model::FallingScurve)
#undef AARE_INSTANTIATE_FIT
// NOLINTEND
} // namespace aare
} // namespace aare
+81
View File
@@ -0,0 +1,81 @@
// SPDX-License-Identifier: MPL-2.0
#include "aare/Fit.hpp"
#include "aare/FitModel.hpp"
#include "aare/Models.hpp"
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <cmath>
namespace {
constexpr ssize_t n_points = 61;
void fill_gaussian(aare::NDArray<double, 1> &x, aare::NDArray<double, 1> &y,
double amplitude, double mean, double sigma) {
for (ssize_t i = 0; i < n_points; ++i) {
x(i) = -6.0 + 0.2 * static_cast<double>(i);
const double z = (x(i) - mean) / sigma;
y(i) = amplitude * std::exp(-0.5 * z * z);
}
}
} // namespace
TEST_CASE("Minuit2 fits weighted and unweighted Gaussian data", "[fit]") {
aare::NDArray<double, 1> x({n_points});
aare::NDArray<double, 1> y({n_points});
aare::NDArray<double, 1> y_err({n_points}, 1.0);
fill_gaussian(x, y, 120.0, 0.8, 1.3);
const aare::FitModel<aare::model::Gaussian> unweighted_model;
const auto unweighted =
aare::fit_pixel(unweighted_model, x.view(), y.view());
REQUIRE(unweighted.size() == 4);
CHECK(unweighted(0) == Catch::Approx(120.0).epsilon(1e-5));
CHECK(unweighted(1) == Catch::Approx(0.8).epsilon(1e-5));
CHECK(unweighted(2) == Catch::Approx(1.3).epsilon(1e-5));
CHECK(unweighted(3) == Catch::Approx(0.0).margin(1e-4));
const aare::FitModel<aare::model::Gaussian> weighted_model(0, 100, 0.5,
true);
const auto weighted =
aare::fit_pixel(weighted_model, x.view(), y.view(), y_err.view());
REQUIRE(weighted.size() == 7);
CHECK(weighted(0) == Catch::Approx(120.0).epsilon(1e-5));
CHECK(weighted(1) == Catch::Approx(0.8).epsilon(1e-5));
CHECK(weighted(2) == Catch::Approx(1.3).epsilon(1e-5));
CHECK(weighted(6) == Catch::Approx(0.0).margin(1e-4));
}
TEST_CASE("Minuit2 fits a Gaussian data cube in parallel", "[fit]") {
aare::NDArray<double, 1> x({n_points});
aare::NDArray<double, 1> values({n_points});
fill_gaussian(x, values, 80.0, -0.6, 0.9);
aare::NDArray<double, 3> y({2, 2, n_points});
for (ssize_t row = 0; row < 2; ++row) {
for (ssize_t col = 0; col < 2; ++col) {
for (ssize_t i = 0; i < n_points; ++i)
y(row, col, i) = values(i);
}
}
aare::NDArray<double, 3> par({2, 2, 3});
aare::NDArray<double, 2> chi2({2, 2});
const aare::FitModel<aare::model::Gaussian> model;
aare::fit_3d(model, x.view(), y.view(), aare::NDView<double, 3>{},
par.view(), aare::NDView<double, 3>{}, chi2.view(), 2);
for (ssize_t row = 0; row < 2; ++row) {
for (ssize_t col = 0; col < 2; ++col) {
CHECK(par(row, col, 0) == Catch::Approx(80.0).epsilon(1e-5));
CHECK(par(row, col, 1) == Catch::Approx(-0.6).epsilon(1e-5));
CHECK(par(row, col, 2) == Catch::Approx(0.9).epsilon(1e-5));
CHECK(chi2(row, col) == Catch::Approx(0.0).margin(1e-8));
}
}
}