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>
Bring the dependency-free Levenberg-Marquardt solver from dev/custom-fit in
as Minimizer::LevenbergMarquardt next to Minuit2's Migrad (default) and
Fumili, selected through the FitModel constructor, SetMinimizer() or the
Python `minimizer` argument and property.
- FitModel becomes plain data (limits, fixed flags, start values, minimizer
settings) with index and limit validation. Minuit2 user parameters are
built per pixel in src/FitMinuit2.cpp, the only translation unit that
includes Minuit2 headers.
- fit_3d fits through a per-thread PixelFitter so the LM path allocates
nothing per pixel, and no minimizer allocates a per-pixel result array.
- Fixed parameters and parameters ending on a limit report an error of 0
with every minimizer; user start values are applied before the validity
check and clamped into the limits.
- Minuit2's Fumili cannot converge once a two-sided limit is active, so
such pixels are refitted with Migrad. The Minuit2 minimizers keep a wide
two-sided range for one-sided limits because Minuit2's one-sided
transform converges markedly slower.
- The [fit] tests run every scenario through all three minimizers and
check that they agree; the benchmark gains LM and data-cube cases.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- Move Chi2.hpp from include/aare/ to src/ (private)
- Pimpl on FitModel<Model>: MnUserParameters/MnStrategy behind opaque
src/FitModelImpl.hpp, no Minuit2 includes in public headers
- Move fit_pixel/fit_3d bodies to Fit.cpp with explicit instantiations
for all 8 models; drop FCN template param from public API
- CMake: aare::Minuit2 wrapped in $<BUILD_INTERFACE:...> (hidden from
exported targets, same pattern as lmfit), MINUIT2_INSTALL OFF, Chi2.hpp
removed from PUBLICHEADERS
- Update python bindings and benchmark callsites accordingly
---------
Co-authored-by: Erik Fröjdh <erik.frojdh@psi.ch>
Co-authored-by: Alice <alice.mazzoleni@psi.ch>
To improve codebase quality and reduce human error, this PR introduces
the pre-commit framework. This ensures that all code adheres to project
standards before it is even committed, maintaining a consistent style
and catching common mistakes early.
Key Changes:
- Code Formatting: Automated C++ formatting using clang-format (based on
the project's .clang-format file).
- Syntax Validation: Basic checks for file integrity and syntax.
- Spell Check: Automated scanning for typos in source code and comments.
- CMake Formatting: Standardization of CMakeLists.txt and .cmake
configuration files.
- GitHub Workflow: Added a CI action that validates every Pull Request
against the pre-commit configuration to ensure compliance.
The configuration includes a [ci] block to handle automated fixes within
the PR. Currently, this is disabled. If we want the CI to automatically
commit formatting fixes back to the PR branch, this can be toggled to
true in .pre-commit-config.yaml.
```yaml
ci:
autofix_commit_msg: [pre-commit] auto fixes from pre-commit hooks
autofix_prs: false
autoupdate_schedule: monthly
```
The last large commit with the fit functions, for example, was not
formatted according to the clang-format rules. This PR would allow to
avoid similar mistakes in the future.
Python fomat with `ruff` for tests and sanitiser for `.ipynb` notebooks
can be added as well.
## Unified Minuit2 fitting framework with FitModel API
### Models (`Models.hpp`)
Consolidate all model structs (Gaussian, RisingScurve, FallingScurve)
into a
single header. Each model provides: `eval`, `eval_and_grad`, `is_valid`,
`estimate_par`, `compute_steps`, and `param_info` metadata. No Minuit2
dependency.
### Chi2 functors (`Chi2.hpp`)
Generic `Chi2Model1DGrad` (analytic gradient) templated on the model
struct.
Replaces the separate Chi2Gaussian, Chi2GaussianGradient,
Chi2Scurves, and Chi2ScurvesGradient headers.
### FitModel (`FitModel.hpp`)
Configuration object wrapping `MnUserParameters`, strategy, tolerance,
and
user-override tracking. User constraints (fixed parameters, start
values, limits)
always take precedence over automatic data-driven estimates.
### Fit functions (`Fit.hpp`)
- `fit_pixel<Model, FCN>(model, x, y, y_err)` -> single-pixel,
self-contained
- `fit_pixel<Model, FCN>(model, upar_local, x, y, y_err)` -> pre-cloned
upar for hot loops
- `fit_3d<Model, FCN>(model, x, y, y_err, ..., n_threads)` ->
row-parallel over pixel grid
### Python bindings
- `Pol1`, `Pol2`, `Gaussian`, `RisingScurve`, `FallingScurve` model
classes with
`FixParameter`, `SetParLimits`, `SetParameter`, and properties for
`max_calls`, `tolerance`, `compute_errors`
- Single `fit(model, x, y, y_err, n_threads)` dispatch replacing the old
`fit_gaus_minuit`, `fit_gaus_minuit_grad`, `fit_scurve_minuit_grad`,
etc.
### Benchmarks
- Updated `fit_benchmark.cpp` (Google Benchmark) to use the new FitModel
API
- Jupyter notebooks for 1D and 3D S-curve fitting (lmfit vs Minuit2
analytic)
- ~1.8x speedup over lmfit, near-linear thread scaling up to physical
core count
---------
Co-authored-by: Erik Fröjdh <erik.frojdh@psi.ch>