Add Minimizer::VarPro, a variable projection minimizer

Every bundled model now declares the parameters it is linear in
(linear_par, basis_and_grad) and, except the polynomials, a vectorisable
bulk basis pass (basis_columns) built on the new model::fast_exp. The
Levenberg-Marquardt iteration is factored into a shared DampedGaussNewton
driver with unchanged results; VarPro solves the linear parameters exactly
at every trial point and iterates over the nonlinear ones, rejecting
degenerate trial points and polishing the converged point with one
undamped step. Limits on linear parameters, pixels that do not converge
and models without the separable structure fall back to
LevenbergMarquardt. Parameter errors are the same Gauss-Newton estimates.
The [fit] tests run VarPro through every configuration and check the
separable structure of each model against its derivatives.

Measured on an M1 Max, single thread: FallingScurve population 5.5 us per
fit against 11.7 us with LevenbergMarquardt, and 2 wrong minima in 4096
pixels against 30; Gaussian 3.3 vs 4.3 us; GaussianChargeSharingKb 33 vs
49 us; Pol2 0.34 vs 1.52 us. Migrad stays the default.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik FröjdhandClaude Fable 5.1 committed 2026-09-25 21:36:53 +02:00
1 parent d1bfc85c8f
commit a211a3e8e2
12 files changed
+1929 -422

No files matched your search

+11 -2
View File
@@ -8,6 +8,7 @@ MINIMIZERS = [
aare.Minimizer.Migrad,
aare.Minimizer.Fumili,
aare.Minimizer.LevenbergMarquardt,
aare.Minimizer.VarPro,
]
@@ -16,7 +17,10 @@ def gaussian(x, A, mu, sigma):
def uses_minuit2(minimizer):
return minimizer != aare.Minimizer.LevenbergMarquardt
return minimizer not in (
aare.Minimizer.LevenbergMarquardt,
aare.Minimizer.VarPro,
)
@pytest.fixture(params=MINIMIZERS, ids=lambda m: m.name)
@@ -45,7 +49,12 @@ def test_gaussian_model_evaluates_and_fits_data(minimizer):
def test_minimizer_can_be_selected():
assert aare.Gaussian().minimizer == aare.Minimizer.Migrad
assert set(aare.Minimizer.__members__) == {"Migrad", "Fumili", "LevenbergMarquardt"}
assert set(aare.Minimizer.__members__) == {
"Migrad",
"Fumili",
"LevenbergMarquardt",
"VarPro",
}
for minimizer in MINIMIZERS:
assert aare.Gaussian(minimizer=minimizer).minimizer == minimizer