Repository navigation
Add ndoptimize.least_squares(), a batched Levenberg-Marquardt fitter. - #4
Merged
Merged
Conversation
`ndoptimize.least_squares()` fits a model to many independent datasets at once, in parallel with Numba, each with its own independent variable, data, noise, initial guess, bounds and Gaussian priors. The noise of neighboring points may be correlated, making the fit generalized least squares through a banded Cholesky whitening. It uses Marquardt damping with Nielsen's gain-ratio update, projected bounds, and the Newton decrement as the convergence test, and returns the parameter covariance. Models are Numba functions compiled with `ndoptimize.model`, which the kernel takes as a typed function pointer so that one cached kernel serves every model. `ndoptimize.models` provides a Gaussian, a Gaussian plus a line, and two Gaussians plus a line. Also sets up the package: a BSD-3-Clause license, citation metadata, tests and typing workflows like `ndfilters`', and the documentation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PZRXiFhGzzrF87vsFg73B9
The repository now has a `CODECOV_TOKEN` secret, so the tests workflow uploads the coverage report like `ndfilters`' does, and the README gets a Codecov badge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PZRXiFhGzzrF87vsFg73B9
Welcome to Codecov 🎉Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests. Thanks for integrating Codecov - We've got you covered ☂️ |
- Models name their parameters, `ndoptimize.model(parameters=...)`, and are `ndoptimize.Model` objects. `least_squares()` checks the guess against them, since a mismatch made the compiled model read and write outside its arrays, and calling a model directly checks the shapes of its arrays. - `ndoptimize.model` takes `cache=False`, for models not defined in a file or reading globals computed at run time. - The ridge in the convergence test is a fraction of each diagonal element of the normal matrix rather than 1e-12, which stopped fits of parameters with a very different scale than the data at their guess, reported as converged. Parameters with no effect on the model are held instead. - The covariance is NaN where the normal matrix is singular, as in a fit with fewer usable points than parameters, rather than finite numbers set by a ridge. - `least_squares()` rejects `lower > upper`, NaN bounds, a non-positive or NaN `prior_sigma`, a non-finite `prior_mean` with a prior, a non-finite `correlation`, a guess of the wrong shape and a negative `max_iterations`. A `prior_mean` without a prior is ignored. - The noise factor is stored as a band, not a dense matrix per thread, and the positive-definite check of `correlation` is a banded Cholesky too. - Arguments shared by every fit are passed to the kernel as views rather than copied. Together with the band, the benchmark of 51,400 fits takes 74 ms instead of 97 ms, with the same results. - `iterations` no longer counts a rejected step, an unreachable branch for a NaN objective is gone, `LeastSquaresResult` no longer generates a broken `__eq__`, and the inputs are annotated as `ArrayLike`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PZRXiFhGzzrF87vsFg73B9
… parameters at once. It runs in parallel with Numba, and broadcasts the batch axes of the points and the parameters like `least_squares()` does, so the best-fit model of every fit is one call. The named-arrays wrapper of `least_squares()` needs it to give back the fitted model. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PZRXiFhGzzrF87vsFg73B9
A copy of a model is the model itself, and a pickled model unpickles as the model of the same name in its module. Before, a deep copy or a pickle round trip made a new `Model`, which isn't equal to the original since models compare by identity. This broke `copy()` and pickling of named-arrays function arrays which keep a model as a field. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PZRXiFhGzzrF87vsFg73B9
roytsmart
added a commit
to sun-data/named-arrays
that referenced
this pull request
Oct 19, 2026
…el`. Along an axis of `axis_model` where the inputs of a `ModelFitFunctionArray` are the edges of bins, with one more element than the outputs, each input which varies along it becomes two inputs of the model, the lower and upper edges of each bin. This is what a model of the average over each bin, like `ndoptimize.models.gaussian_binned`, needs, and what fitting a CTIS reconstruction, whose wavelength coordinates are bin edges, needs. Before, such a fit failed with a shape mismatch. Calling the array with new edges evaluates the model on new bins, with one fewer output than edges, and `predictions` has the shape of the outputs. An input which varies along a vertex axis outside `axis_model`, or along two vertex axes, raises an error that says why. `ndoptimize` is now installed from its `main` branch, since sun-data/ndoptimize#4 has merged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PZRXiFhGzzrF87vsFg73B9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
ndoptimize.least_squares(), which fits a model to many independent datasets at once (every spectrum of a spectral cube, for example) with the Levenberg-Marquardt method, compiled and run in parallel with Numba.Each fit can have its own independent variable, data, noise, initial guess, bounds and Gaussian priors, all broadcast against each other. NaN data points are left out of their fit. The noise of neighboring points may be correlated, as it is in resampled data. That makes the fit generalized least squares, with a banded Cholesky whitening, and gives correct parameter uncertainties. The result is a
LeastSquaresResultwith the parameters, their covariance, χ², the number of points used, the iteration count and a converged flag.Algorithm
A + λ diag(A), with Nielsen's gain-ratio update.gᵀA⁻¹gis the convergence test.ValueError: bounds withlower > upper, a non-positiveprior_sigma, a non-finiteprior_meanwith a prior, acorrelationthat isn't finite and positive definite, or a guess that doesn't match the model.Models
Models are Numba functions that write the model and its Jacobian into output arrays. The new decorator compiles them to one explicit signature and names their parameters:
It returns an
ndoptimize.Model.least_squares()checks the guess against the model's parameter names, since the compiled function doesn't check its indices. Calling aModeldirectly checks the shapes of its arrays. A model is copied and pickled by reference, like a function, so a copy of an object that holds one keeps the same model. The kernel takes the model as a typed function pointer, so one cached kernel serves every model, including user-defined ones. A new process imports and runs its first fit in about 1 s once the cache is built.cache=Falseis for models defined outside a file, or ones that read globals computed at run time.ndoptimize.modelsprovidesgaussian,gaussian_linearandtwo_gaussians_linear.Evaluation
ndoptimize.evaluate(model, x, parameters)evaluates a model for many sets of parameters at once, in parallel. It broadcasts the batch axes of the points and the parameters likeleast_squares()does, so the best-fit model of every fit is one call. The named-arrays wrapper,na.ModelFitFunctionArray(sun-data/named-arrays#271), uses it to evaluate a fit at new inputs and for its predictions.Package setup
CITATION.cff.ndfilters, including the Codecov upload, plus a Codecov badge in the README.least_squaresdocstring has an example that fits 1,000 noisy lines and plots the centroid errors against a unit normal.The 2024 work-in-progress stubs were never committed and aren't included.
Testing
47 tests:
scipy.optimize.least_squaresfor every combination of correlated noise, priors and bounds: the same minimum to 10⁻³σ and the same covariance.exec, and copying and pickling.evaluateagainst the model evaluated one set at a time, with broadcasting, at the best-fit parameters of noiseless data, and with invalid shapes.The suite takes 3 s with Numba and 17 s with
NUMBA_DISABLE_JIT=1, at 99% coverage. The misses are numerical-safety branches. pyright 1.1.411, ruff and black are clean, and the docs build without warnings.On the IRIS Fe XII 2×4-binned raster (51,400 spectra), this kernel reproduces the existing numpy GLS fit (99.9% of parameters within 0.05σ) in about 0.1 s, about 600× faster. A synthetic benchmark of the same size (Gaussian plus line, priors, bounds and correlated noise) takes 74 ms on 48 threads.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PZRXiFhGzzrF87vsFg73B9