mirror of
https://github.com/slsdetectorgroup/aare.git
synced 2026-10-28 20:31:27 +01:00
first attempt on custom minimizer
This commit is contained in:
1 parent
4677e1003e
commit
f001188dcf
28 files changed
+2078
-533
No files matched your search
@@ -27,10 +27,6 @@ set_target_properties(_aare PROPERTIES LIBRARY_OUTPUT_DIRECTORY
|
||||
${CMAKE_BINARY_DIR})
|
||||
target_link_libraries(_aare PRIVATE aare_core aare_compiler_flags)
|
||||
|
||||
target_include_directories(
|
||||
_aare SYSTEM
|
||||
PRIVATE $<TARGET_PROPERTY:aare::Minuit2,INTERFACE_INCLUDE_DIRECTORIES>)
|
||||
|
||||
# List of python files to be copied to the build directory
|
||||
set(PYTHON_FILES
|
||||
aare/__init__.py
|
||||
|
||||
@@ -4,3 +4,10 @@
|
||||
from ._aare.experimental import MultiThreadedFileReader
|
||||
|
||||
__all__ = ["MultiThreadedFileReader"]
|
||||
|
||||
try:
|
||||
from ._aare.experimental import fit_minuit2
|
||||
|
||||
__all__.append("fit_minuit2")
|
||||
except ImportError: # built without AARE_MINUIT2
|
||||
pass
|
||||
+133
-98
@@ -8,11 +8,41 @@
|
||||
#include "aare/Fit.hpp"
|
||||
#include "aare/FitModel.hpp"
|
||||
#include "aare/Models.hpp"
|
||||
#ifdef AARE_MINUIT2
|
||||
#include "aare/FitMinuit2.hpp"
|
||||
#endif
|
||||
|
||||
namespace py = pybind11;
|
||||
using namespace pybind11::literals;
|
||||
|
||||
template <typename Model>
|
||||
// Backend tags select which fit functions serve a Python call. The built-in
|
||||
// Levenberg-Marquardt solver is always available; the Minuit2 backend only
|
||||
// when aare was configured with AARE_MINUIT2=ON.
|
||||
struct LmBackend {
|
||||
template <typename Model, typename... Args>
|
||||
static aare::NDArray<double, 1> fit_pixel(Args &&...args) {
|
||||
return aare::fit_pixel<Model>(std::forward<Args>(args)...);
|
||||
}
|
||||
template <typename Model, typename... Args>
|
||||
static void fit_3d(Args &&...args) {
|
||||
aare::fit_3d<Model>(std::forward<Args>(args)...);
|
||||
}
|
||||
};
|
||||
|
||||
#ifdef AARE_MINUIT2
|
||||
struct Minuit2Backend {
|
||||
template <typename Model, typename... Args>
|
||||
static aare::NDArray<double, 1> fit_pixel(Args &&...args) {
|
||||
return aare::minuit2::fit_pixel<Model>(std::forward<Args>(args)...);
|
||||
}
|
||||
template <typename Model, typename... Args>
|
||||
static void fit_3d(Args &&...args) {
|
||||
aare::minuit2::fit_3d<Model>(std::forward<Args>(args)...);
|
||||
}
|
||||
};
|
||||
#endif
|
||||
|
||||
template <typename Model, typename Backend>
|
||||
py::object
|
||||
fit_dispatch(const aare::FitModel<Model> &model,
|
||||
py::array_t<double, py::array::c_style | py::array::forcecast> x,
|
||||
@@ -24,7 +54,23 @@ template <typename Model> void bind_fit_model(py::module &m, const char *name) {
|
||||
py::class_<FM>(m, name)
|
||||
.def(py::init<unsigned int, unsigned int, double, bool>(),
|
||||
py::arg("strategy") = 0, py::arg("max_calls") = 100,
|
||||
py::arg("tolerance") = 0.5, py::arg("compute_errors") = false)
|
||||
py::arg("tolerance") = 0.5, py::arg("compute_errors") = false,
|
||||
R"doc(
|
||||
Fit configuration for this model.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
strategy : int
|
||||
Accepted for compatibility and ignored by the built-in solver.
|
||||
max_calls : int
|
||||
Maximum number of model evaluations per pixel.
|
||||
tolerance : float
|
||||
EDM tolerance (Minuit convention): the fit stops when the
|
||||
estimated distance to the minimum is below 0.002 * tolerance.
|
||||
compute_errors : bool
|
||||
Also return Gauss-Newton parameter errors. Fixed parameters
|
||||
and parameters ending on a limit report an error of 0.
|
||||
)doc")
|
||||
.def("SetParLimits",
|
||||
py::overload_cast<unsigned int, double, double>(&FM::SetParLimits),
|
||||
py::arg("idx"), py::arg("lo"), py::arg("hi"))
|
||||
@@ -83,10 +129,12 @@ template <typename Model> void bind_fit_model(py::module &m, const char *name) {
|
||||
py::array_t<double, py::array::c_style | py::array::forcecast> x,
|
||||
py::array_t<double, py::array::c_style | py::array::forcecast> y,
|
||||
py::object y_err_obj, int n_threads) -> py::object {
|
||||
return fit_dispatch<Model>(self, x, y, y_err_obj, n_threads);
|
||||
return fit_dispatch<Model, LmBackend>(self, x, y, y_err_obj,
|
||||
n_threads);
|
||||
},
|
||||
R"doc(
|
||||
Fit this model to 1D or 3D data using Minuit2.
|
||||
Fit this model to 1D or 3D data with the built-in
|
||||
Levenberg-Marquardt solver.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -142,7 +190,7 @@ py::dict pack_1d_result_dict(const aare::NDArray<double, 1> &result,
|
||||
}
|
||||
|
||||
// Helper: typed dispatch for one Model, handles 1D/3D + y_err logic
|
||||
template <typename Model>
|
||||
template <typename Model, typename Backend>
|
||||
py::object
|
||||
fit_dispatch(const aare::FitModel<Model> &model,
|
||||
py::array_t<double, py::array::c_style | py::array::forcecast> x,
|
||||
@@ -172,9 +220,9 @@ fit_dispatch(const aare::FitModel<Model> &model,
|
||||
new NDArray<double, 3>({y.shape(0), y.shape(1), npar}, 0.0);
|
||||
auto y_view_err = make_view_3d(y_err);
|
||||
|
||||
aare::fit_3d<Model>(model, x_view, y_view, y_view_err,
|
||||
par_out->view(), err_out->view(),
|
||||
chi2_out->view(), n_threads);
|
||||
Backend::template fit_3d<Model>(model, x_view, y_view, y_view_err,
|
||||
par_out->view(), err_out->view(),
|
||||
chi2_out->view(), n_threads);
|
||||
|
||||
if (model.compute_errors()) {
|
||||
return py::dict("par"_a = return_image_data(par_out),
|
||||
@@ -190,9 +238,9 @@ fit_dispatch(const aare::FitModel<Model> &model,
|
||||
NDView<double, 3> dummy_err{};
|
||||
NDView<double, 3> dummy_err_out{};
|
||||
|
||||
aare::fit_3d<Model>(model, x_view, y_view, dummy_err,
|
||||
par_out->view(), dummy_err_out,
|
||||
chi2_out->view(), n_threads);
|
||||
Backend::template fit_3d<Model>(model, x_view, y_view, dummy_err,
|
||||
par_out->view(), dummy_err_out,
|
||||
chi2_out->view(), n_threads);
|
||||
|
||||
return py::dict("par"_a = return_image_data(par_out),
|
||||
"chi2"_a = return_image_data(chi2_out));
|
||||
@@ -214,9 +262,10 @@ fit_dispatch(const aare::FitModel<Model> &model,
|
||||
}
|
||||
|
||||
auto y_view_err = make_view_1d(y_err);
|
||||
result = aare::fit_pixel<Model>(model, x_view, y_view, y_view_err);
|
||||
result = Backend::template fit_pixel<Model>(model, x_view, y_view,
|
||||
y_view_err);
|
||||
} else {
|
||||
result = aare::fit_pixel<Model>(model, x_view, y_view);
|
||||
result = Backend::template fit_pixel<Model>(model, x_view, y_view);
|
||||
}
|
||||
|
||||
return pack_1d_result_dict<Model>(result, model.compute_errors());
|
||||
@@ -226,7 +275,39 @@ fit_dispatch(const aare::FitModel<Model> &model,
|
||||
}
|
||||
}
|
||||
|
||||
void define_fit_bindings(py::module &m) {
|
||||
// Resolve the model type behind a Python object and dispatch to Backend.
|
||||
template <typename Backend>
|
||||
py::object dispatch_any_model(
|
||||
py::object model_obj,
|
||||
py::array_t<double, py::array::c_style | py::array::forcecast> x,
|
||||
py::array_t<double, py::array::c_style | py::array::forcecast> y,
|
||||
py::object y_err_obj, int n_threads) {
|
||||
using namespace aare::model;
|
||||
|
||||
#define AARE_DISPATCH_MODEL(Model) \
|
||||
if (py::isinstance<aare::FitModel<Model>>(model_obj)) { \
|
||||
const auto &mdl = model_obj.cast<const aare::FitModel<Model> &>(); \
|
||||
return fit_dispatch<Model, Backend>(mdl, x, y, y_err_obj, n_threads); \
|
||||
}
|
||||
|
||||
AARE_DISPATCH_MODEL(Pol1)
|
||||
AARE_DISPATCH_MODEL(Pol2)
|
||||
AARE_DISPATCH_MODEL(Gaussian)
|
||||
AARE_DISPATCH_MODEL(GaussianErfcPlateau)
|
||||
AARE_DISPATCH_MODEL(GaussianChargeSharing)
|
||||
AARE_DISPATCH_MODEL(GaussianChargeSharingKb)
|
||||
AARE_DISPATCH_MODEL(RisingScurve)
|
||||
AARE_DISPATCH_MODEL(FallingScurve)
|
||||
|
||||
#undef AARE_DISPATCH_MODEL
|
||||
|
||||
throw std::runtime_error(
|
||||
"Unknown model type. Expected Pol1, Pol2, Gaussian, "
|
||||
"GaussianErfcPlateau, GaussianChargeSharing, GaussianChargeSharingKb, "
|
||||
"RisingScurve or FallingScurve.");
|
||||
}
|
||||
|
||||
void define_fit_bindings(py::module &m, py::module &experimental) {
|
||||
// ── Bind model classes ──────────────────────────────────────────
|
||||
bind_fit_model<aare::model::Gaussian>(m, "Gaussian");
|
||||
bind_fit_model<aare::model::GaussianErfcPlateau>(m, "GaussianErfcPlateau");
|
||||
@@ -245,86 +326,20 @@ void define_fit_bindings(py::module &m) {
|
||||
py::array_t<double, py::array::c_style | py::array::forcecast> x,
|
||||
py::array_t<double, py::array::c_style | py::array::forcecast> y,
|
||||
py::object y_err_obj, int n_threads) -> py::object {
|
||||
using namespace aare::model;
|
||||
|
||||
// ── Polynomial of degree 1 ───────
|
||||
if (py::isinstance<aare::FitModel<Pol1>>(model_obj)) {
|
||||
const auto &mdl =
|
||||
model_obj.cast<const aare::FitModel<Pol1> &>();
|
||||
return fit_dispatch<Pol1>(mdl, x, y, y_err_obj, n_threads);
|
||||
}
|
||||
|
||||
// ── Polynomial of degree 2 ───────
|
||||
if (py::isinstance<aare::FitModel<Pol2>>(model_obj)) {
|
||||
const auto &mdl =
|
||||
model_obj.cast<const aare::FitModel<Pol2> &>();
|
||||
return fit_dispatch<Pol2>(mdl, x, y, y_err_obj, n_threads);
|
||||
}
|
||||
|
||||
// ── Gaussian ───────
|
||||
if (py::isinstance<aare::FitModel<Gaussian>>(model_obj)) {
|
||||
const auto &mdl =
|
||||
model_obj.cast<const aare::FitModel<Gaussian> &>();
|
||||
return fit_dispatch<Gaussian>(mdl, x, y, y_err_obj, n_threads);
|
||||
}
|
||||
|
||||
// ── GaussianErfcPlateau ───────
|
||||
if (py::isinstance<aare::FitModel<GaussianErfcPlateau>>(
|
||||
model_obj)) {
|
||||
const auto &mdl =
|
||||
model_obj
|
||||
.cast<const aare::FitModel<GaussianErfcPlateau> &>();
|
||||
return fit_dispatch<GaussianErfcPlateau>(mdl, x, y, y_err_obj,
|
||||
n_threads);
|
||||
}
|
||||
|
||||
// ── GaussianChargeSharing ───────
|
||||
if (py::isinstance<aare::FitModel<GaussianChargeSharing>>(
|
||||
model_obj)) {
|
||||
const auto &mdl =
|
||||
model_obj
|
||||
.cast<const aare::FitModel<GaussianChargeSharing> &>();
|
||||
return fit_dispatch<GaussianChargeSharing>(mdl, x, y, y_err_obj,
|
||||
n_threads);
|
||||
}
|
||||
|
||||
// ── GaussianChargeSharingKb ───────
|
||||
if (py::isinstance<aare::FitModel<GaussianChargeSharingKb>>(
|
||||
model_obj)) {
|
||||
const auto &mdl = model_obj.cast<
|
||||
const aare::FitModel<GaussianChargeSharingKb> &>();
|
||||
return fit_dispatch<GaussianChargeSharingKb>(
|
||||
mdl, x, y, y_err_obj, n_threads);
|
||||
}
|
||||
|
||||
// ── Rising Scurve ───────
|
||||
if (py::isinstance<aare::FitModel<RisingScurve>>(model_obj)) {
|
||||
const auto &mdl =
|
||||
model_obj.cast<const aare::FitModel<RisingScurve> &>();
|
||||
return fit_dispatch<RisingScurve>(mdl, x, y, y_err_obj,
|
||||
n_threads);
|
||||
}
|
||||
|
||||
// ── Falling Scurve ───────
|
||||
if (py::isinstance<aare::FitModel<FallingScurve>>(model_obj)) {
|
||||
const auto &mdl =
|
||||
model_obj.cast<const aare::FitModel<FallingScurve> &>();
|
||||
return fit_dispatch<FallingScurve>(mdl, x, y, y_err_obj,
|
||||
n_threads);
|
||||
}
|
||||
|
||||
throw std::runtime_error(
|
||||
"Unknown model type. Expected Pol1, Pol2, Gaussian, "
|
||||
"RisingScurve or FallingScurve.");
|
||||
return dispatch_any_model<LmBackend>(model_obj, x, y, y_err_obj,
|
||||
n_threads);
|
||||
},
|
||||
R"(
|
||||
Fit a model to 1D or 3D data using Minuit2.
|
||||
Fit a model to 1D or 3D data with the built-in Levenberg-Marquardt
|
||||
solver.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
model : Pol1, Pol2, Gaussian, RisingScurve, or FallingScurve
|
||||
Configured model object. User-set limits, fixed parameters,
|
||||
and start values take precedence over automatic estimates.
|
||||
model : object
|
||||
Configured model object: Pol1, Pol2, Gaussian, GaussianErfcPlateau,
|
||||
GaussianChargeSharing, GaussianChargeSharingKb, RisingScurve or
|
||||
FallingScurve. User-set limits, fixed parameters, and start values
|
||||
take precedence over automatic estimates.
|
||||
x : array_like, shape (n_scan,)
|
||||
Scan points (e.g. energy or threshold values).
|
||||
y : array_like, shape (n_scan,) or (rows, cols, n_scan)
|
||||
@@ -336,16 +351,36 @@ void define_fit_bindings(py::module &m) {
|
||||
|
||||
Returns
|
||||
-------
|
||||
For 1D input:
|
||||
numpy array of shape (2*npar+1,) if compute_errors else (npar+1,).
|
||||
Layout: [params..., (errors...,) chi2].
|
||||
|
||||
For 3D input:
|
||||
dict with keys:
|
||||
"par" : (rows, cols, npar) fitted parameters.
|
||||
"par_err" : (rows, cols, npar) parameter errors (if compute_errors).
|
||||
"chi2" : (rows, cols) chi-squared per pixel.
|
||||
dict
|
||||
``par`` holds the fitted parameters, shape (npar,) for 1D input or
|
||||
(rows, cols, npar) for 3D input. ``chi2`` holds the chi-squared,
|
||||
shape (1,) or (rows, cols). ``par_err`` holds the Gauss-Newton
|
||||
parameter errors when ``compute_errors`` is enabled; fixed
|
||||
parameters and parameters on a limit report 0. A failed fit
|
||||
returns zeros.
|
||||
)",
|
||||
py::arg("model"), py::arg("x"), py::arg("y"),
|
||||
py::arg("y_err") = py::none(), py::arg("n_threads") = 4);
|
||||
|
||||
#ifdef AARE_MINUIT2
|
||||
experimental.def(
|
||||
"fit_minuit2",
|
||||
[](py::object model_obj,
|
||||
py::array_t<double, py::array::c_style | py::array::forcecast> x,
|
||||
py::array_t<double, py::array::c_style | py::array::forcecast> y,
|
||||
py::object y_err_obj, int n_threads) -> py::object {
|
||||
return dispatch_any_model<Minuit2Backend>(model_obj, x, y,
|
||||
y_err_obj, n_threads);
|
||||
},
|
||||
R"(
|
||||
Validation only: fit with the Minuit2 (Migrad + Hesse) backend.
|
||||
|
||||
Available when aare was built with -DAARE_MINUIT2=ON. Takes the same
|
||||
arguments and returns the same dictionary as fit().
|
||||
)",
|
||||
py::arg("model"), py::arg("x"), py::arg("y"),
|
||||
py::arg("y_err") = py::none(), py::arg("n_threads") = 4);
|
||||
#else
|
||||
(void)experimental;
|
||||
#endif
|
||||
}
|
||||
@@ -84,7 +84,7 @@ PYBIND11_MODULE(_aare, m) {
|
||||
define_fast_pedestal_bindings<double>(m, "FastPedestal_d");
|
||||
define_fast_pedestal_bindings<float>(m, "FastPedestal_f");
|
||||
define_fast_pedestal_bindings<int16_t>(m, "FastPedestal_i16");
|
||||
define_fit_bindings(m);
|
||||
define_fit_bindings(m, experimental);
|
||||
define_interpolation_bindings(m);
|
||||
define_jungfrau_data_file_io_bindings(m);
|
||||
|
||||
|
||||
@@ -1,8 +1,21 @@
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import aare
|
||||
|
||||
|
||||
def gaussian(x, A, mu, sigma):
|
||||
return A * np.exp(-0.5 * ((x - mu) / sigma) ** 2)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def gaussian_data():
|
||||
x = np.linspace(-6.0, 6.0, 61)
|
||||
y = gaussian(x, 120.0, 0.8, 1.3)
|
||||
y_err = np.ones_like(y)
|
||||
return x, y, y_err
|
||||
|
||||
|
||||
def test_gaussian_model_evaluates_and_fits_data():
|
||||
x = np.linspace(-5.0, 5.0, 51)
|
||||
@@ -13,3 +26,159 @@ def test_gaussian_model_evaluates_and_fits_data():
|
||||
result = model.fit(x, y)
|
||||
|
||||
np.testing.assert_allclose(result["par"], expected, atol=2e-3)
|
||||
|
||||
|
||||
def test_fit_returns_errors_and_chi2(gaussian_data):
|
||||
x, y, y_err = gaussian_data
|
||||
model = aare.Gaussian(compute_errors=True)
|
||||
result = model.fit(x, y, y_err)
|
||||
|
||||
assert set(result) == {"par", "par_err", "chi2"}
|
||||
assert result["par"].shape == (3,)
|
||||
assert result["par_err"].shape == (3,)
|
||||
assert result["chi2"].shape == (1,)
|
||||
np.testing.assert_allclose(result["par"], [120.0, 0.8, 1.3], rtol=1e-4)
|
||||
np.testing.assert_allclose(
|
||||
result["par_err"], [0.361, 0.00451, 0.00451], rtol=2e-2
|
||||
)
|
||||
assert result["chi2"][0] == pytest.approx(0.0, abs=1e-3)
|
||||
|
||||
without_errors = aare.fit(aare.Gaussian(), x, y)
|
||||
assert set(without_errors) == {"par", "chi2"}
|
||||
|
||||
|
||||
def test_pol1_weighted_errors_match_analytic_formula():
|
||||
x = np.arange(20, dtype=float)
|
||||
y = 2.0 + 0.5 * x
|
||||
y_err = 1.0 + 0.1 * x
|
||||
w = 1.0 / y_err**2
|
||||
S, Sx, Sxx = w.sum(), (w * x).sum(), (w * x * x).sum()
|
||||
delta = S * Sxx - Sx * Sx
|
||||
|
||||
result = aare.Pol1(compute_errors=True).fit(x, y, y_err)
|
||||
|
||||
np.testing.assert_allclose(result["par"], [2.0, 0.5], rtol=1e-6)
|
||||
np.testing.assert_allclose(
|
||||
result["par_err"],
|
||||
[np.sqrt(Sxx / delta), np.sqrt(S / delta)],
|
||||
rtol=1e-6,
|
||||
)
|
||||
|
||||
|
||||
def test_fixed_parameter_reports_zero_error(gaussian_data):
|
||||
x, y, y_err = gaussian_data
|
||||
model = aare.Gaussian(compute_errors=True)
|
||||
model.FixParameter("sigma", 1.5)
|
||||
result = model.fit(x, y, y_err)
|
||||
|
||||
assert result["par"][2] == 1.5
|
||||
assert result["par_err"][2] == 0.0
|
||||
assert result["par"][0] == pytest.approx(111.15, rel=1e-3)
|
||||
assert result["chi2"][0] == pytest.approx(1684.25, rel=1e-3)
|
||||
|
||||
model.ReleaseParameter("sigma")
|
||||
result = model.fit(x, y, y_err)
|
||||
assert result["par"][2] == pytest.approx(1.3, rel=1e-4)
|
||||
|
||||
|
||||
def test_active_limit_ends_on_the_limit(gaussian_data):
|
||||
x, y, _ = gaussian_data
|
||||
model = aare.Gaussian()
|
||||
model.SetParLimits("mu", -5.0, 0.5)
|
||||
result = model.fit(x, y)
|
||||
|
||||
assert result["par"][1] == pytest.approx(0.5, abs=1e-9)
|
||||
assert result["par"][0] == pytest.approx(116.88, rel=1e-3)
|
||||
assert result["chi2"][0] == pytest.approx(4301.95, rel=1e-3)
|
||||
|
||||
inactive = aare.Gaussian()
|
||||
inactive.SetParLimits(1, -5.0, 5.0)
|
||||
np.testing.assert_allclose(
|
||||
inactive.fit(x, y)["par"], [120.0, 0.8, 1.3], rtol=1e-4
|
||||
)
|
||||
|
||||
|
||||
def test_user_start_value(gaussian_data):
|
||||
x, y, _ = gaussian_data
|
||||
model = aare.Gaussian()
|
||||
model.SetParameter("mu", 0.0)
|
||||
np.testing.assert_allclose(model.fit(x, y)["par"], [120.0, 0.8, 1.3], rtol=1e-4)
|
||||
|
||||
|
||||
def test_max_calls_failure_returns_zeros(gaussian_data):
|
||||
x, y, y_err = gaussian_data
|
||||
result = aare.Gaussian(max_calls=3, compute_errors=True).fit(x, y, y_err)
|
||||
assert np.all(result["par"] == 0.0)
|
||||
assert np.all(result["par_err"] == 0.0)
|
||||
assert np.all(result["chi2"] == 0.0)
|
||||
|
||||
|
||||
def test_strategy_is_accepted_and_ignored(gaussian_data):
|
||||
x, y, _ = gaussian_data
|
||||
a = aare.Gaussian(strategy=0).fit(x, y)
|
||||
b = aare.Gaussian(strategy=1).fit(x, y)
|
||||
np.testing.assert_array_equal(a["par"], b["par"])
|
||||
|
||||
|
||||
def test_invalid_parameter_access_raises():
|
||||
model = aare.Gaussian()
|
||||
assert model.par_names == ["A", "mu", "sigma"]
|
||||
assert model.n_par == 3
|
||||
with pytest.raises(RuntimeError):
|
||||
model.SetParameter("nope", 1.0)
|
||||
with pytest.raises(IndexError):
|
||||
model.SetParameter(3, 1.0)
|
||||
with pytest.raises(RuntimeError):
|
||||
model.SetParLimits("mu", 1.0, 1.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("cls", [aare.RisingScurve, aare.FallingScurve])
|
||||
def test_scurve_fit(cls):
|
||||
truth = np.array([10.0, 0.1, 50.0, 3.0, 1000.0, 2.0])
|
||||
x = np.linspace(0.0, 100.0, 100)
|
||||
model = cls(max_calls=500, compute_errors=True)
|
||||
y = model(x, truth)
|
||||
result = model.fit(x, y, np.ones_like(y))
|
||||
|
||||
np.testing.assert_allclose(result["par"], truth, rtol=1e-3)
|
||||
assert np.all(result["par_err"] > 0.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("with_errors", [False, True])
|
||||
def test_fit_3d_matches_fit_1d(with_errors):
|
||||
x = np.linspace(-6.0, 6.0, 61)
|
||||
rows, cols = 4, 3
|
||||
y = np.empty((rows, cols, x.size))
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
y[r, c] = gaussian(x, 80.0 + 10.0 * r, -0.6 + 0.3 * c, 0.9 + 0.1 * r)
|
||||
y_err = np.ones_like(y) if with_errors else None
|
||||
|
||||
model = aare.Gaussian(compute_errors=with_errors)
|
||||
cube = model.fit(x, y, y_err, n_threads=3)
|
||||
|
||||
assert cube["par"].shape == (rows, cols, 3)
|
||||
assert cube["chi2"].shape == (rows, cols)
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
single = model.fit(x, y[r, c], None if y_err is None else y_err[r, c])
|
||||
np.testing.assert_allclose(cube["par"][r, c], single["par"], rtol=1e-10)
|
||||
assert cube["chi2"][r, c] == pytest.approx(single["chi2"][0], abs=1e-10)
|
||||
if with_errors:
|
||||
np.testing.assert_allclose(
|
||||
cube["par_err"][r, c], single["par_err"], rtol=1e-10
|
||||
)
|
||||
np.testing.assert_allclose(cube["par"][3, 2], [110.0, 0.0, 1.2], rtol=1e-4, atol=1e-6)
|
||||
|
||||
|
||||
def test_minuit2_backend_matches_builtin_solver(gaussian_data):
|
||||
fit_minuit2 = getattr(aare.experimental, "fit_minuit2", None)
|
||||
if fit_minuit2 is None:
|
||||
pytest.skip("aare was built without AARE_MINUIT2")
|
||||
x, y, y_err = gaussian_data
|
||||
model = aare.Gaussian(compute_errors=True)
|
||||
model.SetParLimits("mu", -5.0, 0.5)
|
||||
lm = model.fit(x, y, y_err)
|
||||
mn = fit_minuit2(model, x, y, y_err)
|
||||
np.testing.assert_allclose(lm["par"], mn["par"], rtol=1e-3)
|
||||
np.testing.assert_allclose(lm["chi2"], mn["chi2"], rtol=1e-3)
|
||||
Reference in new issue
Block a user