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

+170
View File
@@ -6,6 +6,7 @@
#include <catch2/catch_approx.hpp>
#include <catch2/catch_test_macros.hpp>
#include <cmath>
#include <vector>
namespace {
@@ -79,3 +80,172 @@ TEST_CASE("Minuit2 fits a Gaussian data cube in parallel", "[fit]") {
}
}
}
TEST_CASE("Fumili 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);
aare::FitModel<aare::model::Gaussian> unweighted_model;
REQUIRE(unweighted_model.minimizer() == aare::Minimizer::Migrad);
unweighted_model.SetMinimizer(aare::Minimizer::Fumili);
REQUIRE(unweighted_model.minimizer() == aare::Minimizer::Fumili);
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, aare::Minimizer::Fumili);
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));
for (ssize_t k = 3; k < 6; ++k)
CHECK(weighted(k) > 0.0);
CHECK(weighted(6) == Catch::Approx(0.0).margin(1e-4));
}
TEST_CASE("Fumili parameter errors agree with Migrad and Hesse", "[fit]") {
aare::NDArray<double, 1> x({n_points});
aare::NDArray<double, 1> y({n_points});
aare::NDArray<double, 1> y_err({n_points}, 2.0);
fill_gaussian(x, y, 120.0, 0.8, 1.3);
const aare::FitModel<aare::model::Gaussian> migrad(0, 100, 0.5, true,
aare::Minimizer::Migrad);
const aare::FitModel<aare::model::Gaussian> fumili(0, 100, 0.5, true,
aare::Minimizer::Fumili);
const auto with_migrad =
aare::fit_pixel(migrad, x.view(), y.view(), y_err.view());
const auto with_fumili =
aare::fit_pixel(fumili, x.view(), y.view(), y_err.view());
REQUIRE(with_migrad.size() == 7);
REQUIRE(with_fumili.size() == 7);
for (ssize_t k = 0; k < 3; ++k)
CHECK(with_fumili(k) == Catch::Approx(with_migrad(k)).epsilon(1e-5));
for (ssize_t k = 3; k < 6; ++k)
CHECK(with_fumili(k) == Catch::Approx(with_migrad(k)).epsilon(1e-2));
}
TEST_CASE("Fumili honours fixed parameters and limits", "[fit]") {
aare::NDArray<double, 1> x({n_points});
aare::NDArray<double, 1> y({n_points});
fill_gaussian(x, y, 120.0, 0.8, 1.3);
aare::FitModel<aare::model::Gaussian> model(0, 100, 0.5, false,
aare::Minimizer::Fumili);
model.FixParameter("mu", 0.8);
model.SetParLimits("sigma", 0.5, 5.0);
const auto result = aare::fit_pixel(model, x.view(), y.view());
REQUIRE(result.size() == 4);
CHECK(result(0) == Catch::Approx(120.0).epsilon(1e-5));
CHECK(result(1) == 0.8);
CHECK(result(2) == Catch::Approx(1.3).epsilon(1e-5));
CHECK(result(3) == Catch::Approx(0.0).margin(1e-4));
}
TEST_CASE("Fumili fits a rising S-curve", "[fit]") {
// f(x) = (p0 + p1*x) + 0.5*(1 + erf((x - mu)/(sqrt(2)*sigma))) * (A + C*(x
// - mu))
const std::vector<double> truth = {5.0, 0.1, 30.0, 4.0, 200.0, 0.5};
aare::NDArray<double, 1> x({n_points});
aare::NDArray<double, 1> y({n_points});
for (ssize_t i = 0; i < n_points; ++i) {
x(i) = static_cast<double>(i);
y(i) = aare::model::RisingScurve::eval(x(i), truth);
}
const aare::FitModel<aare::model::RisingScurve> model(
0, 200, 0.5, false, aare::Minimizer::Fumili);
const auto result = aare::fit_pixel(model, x.view(), y.view());
REQUIRE(result.size() == 7);
for (std::size_t k = 0; k < truth.size(); ++k)
CHECK(result(k) == Catch::Approx(truth[k]).epsilon(1e-3));
}
TEST_CASE("Fumili 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});
aare::NDArray<double, 3> y_err({2, 2, n_points}, 1.0);
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, 3> err({2, 2, 3});
aare::NDArray<double, 2> chi2({2, 2});
const aare::FitModel<aare::model::Gaussian> model(0, 100, 0.5, true,
aare::Minimizer::Fumili);
aare::fit_3d(model, x.view(), y.view(), y_err.view(), par.view(),
err.view(), chi2.view(), 2);
// Fumili stops once the estimated remaining chi2 decrease is below
// tolerance * 1e-4, which leaves deviations of order 1e-5 relative here.
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-4));
CHECK(par(row, col, 1) == Catch::Approx(-0.6).epsilon(1e-4));
CHECK(par(row, col, 2) == Catch::Approx(0.9).epsilon(1e-4));
for (ssize_t k = 0; k < 3; ++k)
CHECK(err(row, col, k) > 0.0);
CHECK(chi2(row, col) == Catch::Approx(0.0).margin(1e-3));
}
}
}
TEST_CASE("Copying a FitModel keeps the minimizer", "[fit]") {
const aare::FitModel<aare::model::Pol1> original(0, 100, 0.5, false,
aare::Minimizer::Fumili);
const aare::FitModel<aare::model::Pol1> copied(original);
aare::FitModel<aare::model::Pol1> assigned;
assigned = original;
CHECK(copied.minimizer() == aare::Minimizer::Fumili);
CHECK(assigned.minimizer() == aare::Minimizer::Fumili);
}
TEST_CASE("Fitting with every parameter fixed returns the fixed values",
"[fit]") {
aare::NDArray<double, 1> x({n_points});
aare::NDArray<double, 1> y({n_points});
fill_gaussian(x, y, 120.0, 0.8, 1.3);
for (const auto minimizer :
{aare::Minimizer::Migrad, aare::Minimizer::Fumili}) {
aare::FitModel<aare::model::Gaussian> model(0, 100, 0.5, true,
minimizer);
model.FixParameter("A", 120.0);
model.FixParameter("mu", 0.8);
model.FixParameter("sigma", 1.3);
const auto result = aare::fit_pixel(model, x.view(), y.view());
REQUIRE(result.size() == 7);
CHECK(result(0) == 120.0);
CHECK(result(1) == 0.8);
CHECK(result(2) == 1.3);
CHECK(result(6) == Catch::Approx(0.0).margin(1e-12));
}
}