nlls_gram
Levenberg-Marquardt nonlinear least-squares for JAX pytrees, aimed at
solving systems of equations (i.e., interpolation). The core use case is
underdetermined systems — more parameters than residuals — where something
must select which interpolating solution is returned. The package ships
two solvers with one shared init/update/solve protocol:
RidgeLevenbergMarquardtputs the selection in the objective: it minimizes (|r(x)|^2 + \lambda,|x_m|_W^2) for a user positive-definite metric (W) on the metric block (x_m) of (x = [x_m; x_f]) (the free block (x_f) stays unpenalized), with the ridge weight (\lambda) annealed toward zero, so the limit is the minimum-seminorm (e.g. minimum-RKHS-norm) interpolant by classical nonlinear Tikhonov regularization (Engl–Hanke–Neubauer 1996; Kaltenbacher–Neubauer–Scherzer 2008). Every inner problem is a well-posed NLLS — plain Gauss-Newton alone would converge to some interpolant without selecting the minimal-norm one (Campbell–Kunkel–Bobinyec 2012).LevenbergMarquardtis standard damped LM for general nonlinear least squares — square, tall, or underdetermined — with dense, QR, CG, and matrix-free LSMR linear solvers and a swappable implicit-AD rule.
Both take a residual over (x), (x, args), or (x, args, p) (always in
that order), flatten any pytree x with jax.flatten_util.ravel_pytree,
expose per-step update(...) and an internally jitted solve(...) loop
with callbacks and multi-start, and differentiate solve(...).x with
respect to p through a custom implicit rule.
Install
uv add nlls-gram
For GPU use, install the JAX accelerator build that matches your hardware, for example:
uv add nlls-gram "jax[cuda13]"
Ridge Example: Minimum-RKHS-Norm Interpolation
For kernel coefficient problems
(f_\alpha(x)=\sum_j \alpha_j K(x, x_j)), the squared RKHS norm is
(\alpha^\top K \alpha), so the metric is a RepeatedFactorMetric
(repeats coefficient blocks sharing one upper-triangular factor (F)
with (W = F^\top F), plus unpenalized structural scalars in the free
block — no epsilon shift anywhere):
import jax.numpy as jnp
from nlls_gram import (
RepeatedFactorMetric,
RidgeLevenbergMarquardt,
ridge_continuation,
)
F = jnp.linalg.cholesky(K, upper=True) # W = F'F per coefficient block
metric = RepeatedFactorMetric(F, repeats=3) # free block: len(x) - metric.size
solver = RidgeLevenbergMarquardt(
residual_fn, # (x) | (x, args) | (x, args, p)
metric=metric,
ridge=1e-8, # fixed small ridge; None = dtype default
)
result = solver.solve(theta_0, max_steps=400, gtol=1e-8, atol=1e-8)
# optional homotopy (ridge annealed 1e-4 -> 1e-8 on stationarity):
cb, us0 = ridge_continuation(ridge_floor=1e-8, decrease=0.1)
result = solver.solve(theta_0, max_steps=400, gtol=1e-8, atol=1e-8,
callback=cb, user_state=us0)
The solver runs entirely in the whitened variable
(y = \bar F x) ((\bar F = \mathrm{blockdiag}(F, I))): stock Euclidean
LM on the augmented residual ([r;\sqrt{\lambda},y_m]), where the
penalty rows are constant. The trust-region damping and the selection
weight are fully decoupled, the assembled whitened normal matrix
(\tilde J^\top \tilde J + \lambda E) is cached across rejected steps
and keeps a clean spectral floor at (\lambda) — so the default cholesky
path stays accurate at deep ridge — and a damping-row QR path
(linear_solver=QR()) covers the extreme tiny-ridge/tiny-damping regime.
IdentityMetric(size) is plain ridge; jnp.eye never appears.
info.loss is the ridge objective; info.resid_loss is the equation error.
Stopping is conjunctive: gtol means "stationary at this ridge", atol
additionally demands the equations solved (it never stops the solve alone).
Steps are measured in the W-norm and gradients in the dual W⁻¹-norm, which
makes calibration clean: gtol ~ 1e-3 * ridge * sqrt(q(x*)) with
(q = |x_m|_W^2) the solution's squared seminorm
(info.penalty_grad_norm reports sqrt(penalty_value)).
The Ridge LM docs
derive the selection theorem, the whitened change of variables, the
continuation schedule, and the solver table.
General Nonlinear Least Squares
import jax
import jax.numpy as jnp
from nlls_gram import LevenbergMarquardt
def residual_fn(x, args):
ts, ys = args
return x["a"] * jnp.exp(x["b"] * ts) - ys
ts = jnp.linspace(0.0, 2.0, 20)
ys = 2.0 * jnp.exp(-1.0 * ts)
x = {"a": 1.0, "b": 0.0}
solver = LevenbergMarquardt(residual_fn, init_damping=1e-2)
lm_state = solver.init(x, (ts, ys))
@jax.jit
def train_step(x, lm_state):
return solver.update(x, lm_state, (ts, ys))
for _ in range(50):
x, lm_state, info = train_step(x, lm_state)
print(x["a"], x["b"]) # approximately 2.0, -1.0
For a simple full solve loop:
result = solver.solve(x, (ts, ys), max_steps=50, atol=1e-8)
x = result.x
solve stops on a residual-norm atol, gradient-norm gtol, or
accepted-step-norm xtol (each 0.0 disables), always enforces max_steps,
and takes a traceable callback for custom stopping, epoch-style data
resampling, and per-step history recording; the docs have a cookbook.
solve(...).x also supports custom implicit JVP/VJP with respect to p;
the docs give the metric-minimum-norm formula and a minimal jax.jvp /
jax.vjp example. The default ad_solver="auto" uses a direct solve for
every square system, preserves the forward CG space for nonsquare CG systems,
and uses SVD otherwise. Every method is independently swappable (an lsmr forward solve with
ad_solver="normal_cg" is fully matrix-free end to end). The metric
matters for underdetermined roots because it selects which tangent is the
minimum-norm solution. The per-step update(...) interface does not define
the implicit AD rule. By default, both CONVERGED and MAX_STEPS results are
usable: fixed-step solves retain their implicit derivative while the status
still reports MAX_STEPS. Pass max_steps_is_success=False for strict
failure semantics. A failed solve keeps its primal result and diagnostics but
contributes exactly zero through result.x and result.aux; result.p
remains an identity pass-through. Its linear tangent program is evaluated
safely at differentiation-inert copies of the caller's original
(x0, args, p), so those initial inputs must be JVP-safe for the residual,
aux map, and any metric or preconditioner factory used by the selected AD
method. This also keeps mixed successful/failed vmap lanes finite.
Advanced: metric damping
LevenbergMarquardt optionally accepts a positive-definite parameter-space
metric (or an iterate-aware MetricFactory) that redefines the damping
geometry, so the small-damping Gauss-Newton limit selects
minimum-metric-norm corrections — the algorithmic-bias alternative to the
ridge objective, with repeated_shifted_dense_metric /
repeated_shifted_state_space_metric covering the kernel geometry
(\operatorname{blockdiag}(K,\ldots,K,0_s)+\varepsilon I). See the
metrics and
metric Gauss-Newton
docs pages; for new kernel problems prefer RidgeLevenbergMarquardt.
LevenbergMarquardt Solvers
linear_solver="auto"(the default): resolves at trace time to the smaller dense factorization —gram_choleskywhenn > m,normal_choleskyotherwise. A shape rule, and safely so: the two forms compute the same step.linear_solver="gram_cholesky": densem × mresidual-space Gram solve.linear_solver="normal_cholesky": densen × nwhitened normal solve; its small-damping limit is the minimum-metric-norm least-squares step at every shape and rank.linear_solver="qr": dense QR solve of the whitened-step problem (requires a full-row-rank Jacobian).linear_solver="augmented_qr": direct augmented QR in parameter space; robust to rank-deficient Jacobians when damping is positive and best suited to small systems.linear_solver="gram_cg": matrix-free residual-space CG. Adual_preconditioneris required (e.g.sherman_morrison_preconditioner, or the randomizednystrom_preconditionerfor neural-network duals; passidentity_preconditioner()to run unpreconditioned CG explicitly); on a nonsquare systemad_solver="auto"keepssolve(...).xmatrix-free under AD and requiresad_solver_preconditionerwhen the differentiated solve is traced (an explicitad_solver="gram_cg"validates it eagerly). When the dual operator rotates as LM driftsx, passpreconditioner_factory=PreconditionerFactory(prepare, apply)instead — a θ-adaptive preconditioner rebuilt from the live iterate each step — andrecycle=RecycleConfig(rank=k)to carry a deflation basis across steps, recycling each solve's Krylov subspace into the next.linear_solver="normal_cg": matrix-free CG on the whitened normal system, iterating in parameter space — the matrix-free form for square-to-tall problems. Anormal_preconditioneris required; on rank-deficient problems it must preserverange(Bᵀ)or the minimum-norm selection is lost (identity_preconditioner()always qualifies — the docs give the full requirement).linear_solver="lsmr": matrix-free LSMR on the whitened augmented system, the iterative sibling ofaugmented_qr, using only J/Jᵀ products. It works on the whitened Jacobian rather than a squared Gram/normal operator, so it stays accurate at small damping where those solves hit theireps·condfloor. An optionalwhitened_preconditioner=WhitenedPreconditioner(solve, solve_transpose)right-preconditions the operator to cluster its spectrum; every damped posed subproblem stays exactly the identity-damped whitened one, so the preconditioner changes the iteration path, never the converged step.
All eight solve the same metric-damped linearized subproblem up to the
accuracy of the chosen linear solver. The dense paths materialize the
Jacobian from its small side (jacobian_mode="auto"; "fwd"/"rev"
force one AD mode), so tall systems never build an m × m residual basis.
Docs and Alternatives
Full docs: https://highdimensionaleconlab.github.io/nlls_gram/
Working with an AI assistant? Point it at
docs/tuning_guide.md
if it doesn't pick it up automatically — solver selection, damping heuristics,
inner-solve scheduling, and failure signatures, written to be read by humans
and agents alike (also indexed via the site's llms.txt).
For a broader JAX nonlinear solver library, see
Optimistix. nlls_gram is more
specialized: it focuses on underdetermined nonlinear least-squares, residual
space Gram solves, and explicit parameter-space metrics.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file nlls_gram-2.6.0.tar.gz.
File metadata
- Download URL: nlls_gram-2.6.0.tar.gz
- Upload date:
- Size: 2.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
999f57c392f5df16439b749078064fdd357cc0a48b7079f78524aafa324c9986
|
|
| MD5 |
659bd3591de80dd7a2072161b8733c99
|
|
| BLAKE2b-256 |
d27ebeaf2498f7d4d9ced03a385104115094dbcb95ef1cf5c21d269664f01faf
|
Provenance
The following attestation bundles were made for nlls_gram-2.6.0.tar.gz:
Publisher:
publish.yml on HighDimensionalEconLab/nlls_gram
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nlls_gram-2.6.0.tar.gz -
Subject digest:
999f57c392f5df16439b749078064fdd357cc0a48b7079f78524aafa324c9986 - Sigstore transparency entry: 2243266326
- Sigstore integration time:
-
Permalink:
HighDimensionalEconLab/nlls_gram@f2d570cde462468d82705d8cceb9fc015cedbc3e -
Branch / Tag:
refs/tags/v2.6.0 - Owner: https://github.com/HighDimensionalEconLab
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f2d570cde462468d82705d8cceb9fc015cedbc3e -
Trigger Event:
release
-
Statement type:
File details
Details for the file nlls_gram-2.6.0-py3-none-any.whl.
File metadata
- Download URL: nlls_gram-2.6.0-py3-none-any.whl
- Upload date:
- Size: 106.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7c3ee317f7fc916b6f2098d33179babce33697c653c9dd8afe798cfca77cbe06
|
|
| MD5 |
5d4debbba6f96f8f67fabe87b4f49f52
|
|
| BLAKE2b-256 |
09cfe74fdaa9e6e5e3960a1787868d1f00a63fac9f3112693d4e97faa1812152
|
Provenance
The following attestation bundles were made for nlls_gram-2.6.0-py3-none-any.whl:
Publisher:
publish.yml on HighDimensionalEconLab/nlls_gram
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nlls_gram-2.6.0-py3-none-any.whl -
Subject digest:
7c3ee317f7fc916b6f2098d33179babce33697c653c9dd8afe798cfca77cbe06 - Sigstore transparency entry: 2243266544
- Sigstore integration time:
-
Permalink:
HighDimensionalEconLab/nlls_gram@f2d570cde462468d82705d8cceb9fc015cedbc3e -
Branch / Tag:
refs/tags/v2.6.0 - Owner: https://github.com/HighDimensionalEconLab
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f2d570cde462468d82705d8cceb9fc015cedbc3e -
Trigger Event:
release
-
Statement type: