Expand description
A dense trust-region-reflective (TRF) nonlinear least-squares solver that
reproduces scipy.optimize.least_squares (method='trf', 2-point
Jacobian) bit-for-bit. It covers SciPy’s dense unbounded path for arbitrary
problem dimension n and every SciPy loss (linear, soft_l1, huber,
cauchy, arctan) with the f_scale robust-reweighting parameter; see
Scope for the supported dimensionality.
It is built to be a general-purpose solver, not a one-off port: give it a
residual r: R^n -> R^m and a starting point and it runs the same
trust-region Newton iteration SciPy does, down to the last bit. The
linear-algebra
operations that determine the last bits of the trajectory – the thin SVD of
the scaled Jacobian and the small BLAS reductions around it – are injected
through the trf::ThinSvd trait. Supplying an implementation backed by a
host LAPACK/BLAS (see the optional hostlapack module) lets the solver
reproduce that backend’s numerical trajectory exactly, which is what makes
bit-for-bit agreement with a pinned SciPy/NumPy runtime achievable rather
than merely tolerance-close.
Use it when you need scipy-identical least-squares results in Rust: porting a Python pipeline, cross-checking a Rust solver against SciPy, or pinning a numerical result so it cannot silently drift between language runtimes.
§Scope
trf: the dense unbounded trust-region-reflective iteration matchingscipy.optimize._lsq.trf.trf_no_bounds, with the injectabletrf::ThinSvdSVD/BLAS seam and theloss/f_scalerobust path. Ships the default in-cratetrf::NalgebraThinSvdbackend and the convenience [trf::trf_solve] entry point so the solver runs out of the box.model: themodel::ResidualModeltrait andmodel::solve_model, so a residual (and any per-evaluation corrections) can live entirely Rust-side instead of behind a closure boundary.data: data-drivendata::BuiltinResidualkinds anddata::DataProblem/data::solve_data_problem: a host-language binding selects a kind, passes data arrays, and the trust-region loop never re-enters the host language.batch: parallel leave-one-out (batch::solve_drop_one) and multi-start (batch::solve_perturbed) re-solves over arayonpool, with per-index results bit-identical to the equivalent serial solve. Serial leave-one-out twins (batch::solve_drop_one_serialand friends) give single-threaded /wasm32consumers the same report without rayon.loss: SciPy’s robust loss functions (construct_loss_function+IMPLEMENTED_LOSSES) andscale_for_robust_loss_function, reproduced bit-for-bit.numdiff: the dense two-point finite-difference Jacobian matching SciPy’s_numdiff.approx_derivative(..., method="2-point")path.parity: hex-bit fixture helpers, feature-gated trace output, and first-divergence reporting for diagnosing where two trajectories split.hostlapack: atrf::ThinSvdimplementation backed by a dynamically loaded host LAPACK/BLAS, used to reproduce a pinned SciPy runtime’s exact SVD/BLAS results bit-for-bit. Compiled into the default build; point it at the host LAPACK/BLAS at runtime to get bit-exact parity.
The iteration is general in n: give it a residual r: R^n -> R^m for any
n >= 1 (with m >= n for the dense exact trust-region solve) and it runs
SciPy’s trf_no_bounds trajectory bit-for-bit. All five SciPy losses are
supported at every dimension. Bit-exact parity is enforced by committed
fixtures across n in {2, 3, 4, 5, 6, 8} times each loss; see the
fixtures-generators directory and the tests suite.
§Example
The solver runs out of the box: hand model::solve_model a residual model
and a starting point and it drives the trust-region iteration through the
default in-crate SVD. This fits the overdetermined system whose
least-squares solution is [1.0, 2.0]:
use trust_region_least_squares::model::{solve_model, ResidualModel};
use trust_region_least_squares::trf::TrfOptions;
// r(x) = [x0 - 1, x1 - 2, x0 + x1 - 3]; minimized at x = [1, 2].
struct Consistent;
impl ResidualModel for Consistent {
fn residual(&self, x: &[f64], out: &mut Vec<f64>) {
out.clear();
out.push(x[0] - 1.0);
out.push(x[1] - 2.0);
out.push(x[0] + x[1] - 3.0);
}
// The Jacobian defaults to the bit-exact 2-point finite difference.
}
let result = solve_model(&Consistent, &[0.0, 0.0], &TrfOptions::default())
.expect("solve");
assert!(result.success());
assert!((result.x[0] - 1.0).abs() < 1e-6);
assert!((result.x[1] - 2.0).abs() < 1e-6);The thin SVD is still an injectable seam. The default
trf::NalgebraThinSvd is a legitimate independent SVD but is not
bit-identical to a pinned SciPy/NumPy runtime; for bit-for-bit parity inject
the hostlapack::LapackSvd backend (the *_with entry points take a
trf::ThinSvd). The iteration is identical, only the SVD/BLAS seam
changes.
Modules§
- batch
- Parallel leave-one-out and multi-start re-solves.
- data
- Data-driven built-in residuals: the path a host-language binding drives without ever re-entering the host language inside the solver loop.
- hostlapack
- Host LAPACK and BLAS bridge for pinned SciPy 1.18.0 parity runs.
- loss
- SciPy robust loss functions, reproduced bit-for-bit.
- model
- A Rust-side residual model, so the residual, its Jacobian, and any per-evaluation corrections live in Rust instead of behind a closure boundary supplied by the caller.
- numdiff
- Dense 2-point finite differences matching SciPy 1.11
_numdiff. - parity
- Bit-level fixture and trace utilities for reference parity tests.
- trf
- Dense unbounded trust-region reflective least-squares matching
scipy.optimize._lsq.trf.trf_no_boundsfor arbitrary problem dimensionn.