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

+33 -14
View File
@@ -34,23 +34,27 @@ template <typename Model> void bind_fit_model(py::module &m, const char *name) {
----------
strategy : int
Minuit2 strategy (0 = fast, 1 = Minuit2's default). Ignored
by ``Minimizer.LevenbergMarquardt``.
by ``Minimizer.LevenbergMarquardt`` and
``Minimizer.VarPro``.
max_calls : int
Work budget per pixel: Minuit2 function calls, or model
evaluations for ``Minimizer.LevenbergMarquardt`` (one per
iteration, two when a step crosses a limit). 0 selects
Minuit's default budget.
evaluations for ``Minimizer.LevenbergMarquardt`` and
``Minimizer.VarPro`` (one per iteration, two
when a step crosses a limit). 0 selects Minuit's default
budget.
tolerance : float
EDM tolerance in Minuit's convention. Migrad and
LevenbergMarquardt stop below ``0.002 * tolerance``, Fumili
below ``1e-4 * tolerance``.
EDM tolerance in Minuit's convention. Migrad,
LevenbergMarquardt and VarPro stop below
``0.002 * tolerance``, Fumili below ``1e-4 * tolerance``.
compute_errors : bool
Also return parameter errors: from Hesse with Migrad, from
the linearised covariance with Fumili and LevenbergMarquardt.
Fixed parameters and parameters ending on a limit report 0.
the linearised covariance with Fumili, LevenbergMarquardt
and VarPro. Fixed parameters and parameters
ending on a limit report 0.
minimizer : Minimizer
``Minimizer.Migrad`` (default), ``Minimizer.Fumili`` or
``Minimizer.LevenbergMarquardt``.
``Minimizer.Migrad`` (default), ``Minimizer.Fumili``,
``Minimizer.LevenbergMarquardt`` or
``Minimizer.VarPro``.
)doc")
.def("SetParLimits",
py::overload_cast<unsigned int, double, double>(&FM::SetParLimits),
@@ -118,8 +122,9 @@ template <typename Model> void bind_fit_model(py::module &m, const char *name) {
Fit this model to 1D or 3D data using Minuit2.
The minimizer is selected by the ``minimizer`` property
(``Minimizer.Migrad`` by default, ``Minimizer.Fumili`` or
``Minimizer.LevenbergMarquardt``).
(``Minimizer.Migrad`` by default, ``Minimizer.Fumili``,
``Minimizer.LevenbergMarquardt`` or
``Minimizer.VarPro``).
Parameters
----------
@@ -319,12 +324,26 @@ void define_fit_bindings(py::module &m) {
``sqrt(diag((J^T J)^-1))``; ``max_calls`` counts model evaluations
and ``strategy`` is ignored.
``VarPro`` is the built-in variable projection solver.
Every bundled model is linear in some of its parameters (the
amplitude of a Gaussian, the baseline and amplitude of an S-curve,
every parameter of a polynomial); each trial point solves those
exactly and the Levenberg-Marquardt iteration runs over the
remaining nonlinear parameters only. It needs fewer evaluations than
``LevenbergMarquardt`` and cannot be misled by poor start values of
the linear parameters, which it ignores. A pixel whose linear
solution violates a limit on a linear parameter, or that does not
converge, is refitted with ``LevenbergMarquardt``. Its parameter
errors are the same Gauss-Newton estimates; ``max_calls`` counts
evaluations and ``strategy`` is ignored.
With every minimizer, fixed parameters and parameters that end on a
limit report an error of 0 and a failed fit returns zeros.
)doc")
.value("Migrad", aare::Minimizer::Migrad)
.value("Fumili", aare::Minimizer::Fumili)
.value("LevenbergMarquardt", aare::Minimizer::LevenbergMarquardt);
.value("LevenbergMarquardt", aare::Minimizer::LevenbergMarquardt)
.value("VarPro", aare::Minimizer::VarPro);
// ── Bind model classes ──────────────────────────────────────────
bind_fit_model<aare::model::Gaussian>(m, "Gaussian");