initial implementation

This commit is contained in:
Erik Fröjdh committed 2026-09-24 17:20:44 +02:00
1 parent 4677e1003e
commit b74c0afb97
11 files changed
+634 -154

No files matched your search

+1
View File
@@ -40,6 +40,7 @@ from .Cluster import Cluster
from ._aare import Gaussian, RisingScurve, FallingScurve, Pol1, Pol2, GaussianErfcPlateau, GaussianChargeSharing, GaussianChargeSharingKb
from ._aare import fit
from ._aare import Minimizer
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
+26 -2
View File
@@ -22,9 +22,11 @@ fit_dispatch(const aare::FitModel<Model> &model,
template <typename Model> void bind_fit_model(py::module &m, const char *name) {
using FM = aare::FitModel<Model>;
py::class_<FM>(m, name)
.def(py::init<unsigned int, unsigned int, double, bool>(),
.def(py::init<unsigned int, unsigned int, double, bool,
aare::Minimizer>(),
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,
py::arg("minimizer") = aare::Minimizer::Migrad)
.def("SetParLimits",
py::overload_cast<unsigned int, double, double>(&FM::SetParLimits),
py::arg("idx"), py::arg("lo"), py::arg("hi"))
@@ -59,6 +61,7 @@ template <typename Model> void bind_fit_model(py::module &m, const char *name) {
.def_property("tolerance", &FM::tolerance, &FM::SetTolerance)
.def_property("compute_errors", &FM::compute_errors,
&FM::SetComputeErrors)
.def_property("minimizer", &FM::minimizer, &FM::SetMinimizer)
.def(
"__call__",
[](const FM & /*self*/,
@@ -88,6 +91,9 @@ template <typename Model> void bind_fit_model(py::module &m, const char *name) {
R"doc(
Fit this model to 1D or 3D data using Minuit2.
The minimizer is selected by the ``minimizer`` property
(``Minimizer.Migrad`` by default, or ``Minimizer.Fumili``).
Parameters
----------
x : array_like, shape (n_scan,)
@@ -227,6 +233,22 @@ fit_dispatch(const aare::FitModel<Model> &model,
}
void define_fit_bindings(py::module &m) {
// The enum must exist before it is used as a constructor default below.
py::enum_<aare::Minimizer>(m, "Minimizer", R"doc(
Minuit2 minimizer used by the fit models.
``Migrad`` is the default variable-metric minimizer. With
``compute_errors`` its parameter errors come from Hesse.
``Fumili`` is a Gauss-Newton style minimizer that takes the gradient
and a linearised Hessian from the analytic derivatives of the model.
It usually needs far fewer function evaluations than Migrad. With
``compute_errors`` its parameter errors come from the covariance of
the linearised Hessian, without an extra Hesse step.
)doc")
.value("Migrad", aare::Minimizer::Migrad)
.value("Fumili", aare::Minimizer::Fumili);
// ── Bind model classes ──────────────────────────────────────────
bind_fit_model<aare::model::Gaussian>(m, "Gaussian");
bind_fit_model<aare::model::GaussianErfcPlateau>(m, "GaussianErfcPlateau");
@@ -325,6 +347,8 @@ void define_fit_bindings(py::module &m) {
model : Pol1, Pol2, Gaussian, RisingScurve, or FallingScurve
Configured model object. User-set limits, fixed parameters,
and start values take precedence over automatic estimates.
The model's ``minimizer`` property selects Migrad (default)
or Fumili.
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)
+38
View File
@@ -13,3 +13,41 @@ 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_minimizer_can_be_selected_and_fumili_fits_data():
x = np.linspace(-5.0, 5.0, 51)
expected = np.array([20.0, 0.5, 1.2])
assert aare.Gaussian().minimizer == aare.Minimizer.Migrad
model = aare.Gaussian(minimizer=aare.Minimizer.Fumili)
assert model.minimizer == aare.Minimizer.Fumili
y = model(x, expected)
result = model.fit(x, y)
np.testing.assert_allclose(result["par"], expected, atol=2e-3)
model.minimizer = aare.Minimizer.Migrad
assert model.minimizer == aare.Minimizer.Migrad
def test_fumili_and_migrad_agree_on_3d_data_with_errors():
x = np.linspace(-5.0, 5.0, 51)
expected = np.array([20.0, 0.5, 1.2])
single = aare.Gaussian()(x, expected)
y = np.tile(single, (2, 3, 1))
y_err = np.ones_like(y)
migrad = aare.Gaussian(compute_errors=True)
fumili = aare.Gaussian(compute_errors=True, minimizer=aare.Minimizer.Fumili)
with_migrad = migrad.fit(x, y, y_err, n_threads=2)
with_fumili = fumili.fit(x, y, y_err, n_threads=2)
assert with_fumili["par"].shape == (2, 3, 3)
assert with_fumili["par_err"].shape == (2, 3, 3)
for result in (with_migrad, with_fumili):
np.testing.assert_allclose(result["par"], np.broadcast_to(expected, (2, 3, 3)), atol=2e-3)
np.testing.assert_allclose(result["chi2"], 0.0, atol=1e-3)
np.testing.assert_allclose(with_fumili["par_err"], with_migrad["par_err"], rtol=2e-2)