🛠️ project Announcing Basin: A Numerical Optimization Library for Rust
Hi!
I've been working on Basin, a numerical optimization library for Rust. It's heavily inspired by argmin; same overall shape (Executor driver loop, Solver/Problem trait split, per-solver State, pluggable termination). Basin exists because I wanted to push on a few specific design directions that were awkward to retrofit.
What's Different
- Framework-level termination, bound to state shape.
max_iter, the*_tolerancefamily,max_time, eval budgets all live on theExecutorand compose across solvers. Each criterion binds on the minimum state it needs, so asking for aGradientToleranceon a derivative-free solver is a compile error, not a runtime surprise. - First-class constraints. Constraints describe the problem, so they live problem-side (not on the executor, never on state). A constrained problem handed to an unconstrained solver is a compile error; there are opt-in adapters (projection/log-barrier/augmented Lagrangian) to wrap unconstrained solvers. Box bounds and linear (in)equalities today; nonlinear is on the roadmap.
- Backend-generic linear algebra. Solvers are generic over
Vec<f64>(no features), nalgebra, ndarray, and faer. A small universal vector tier keeps first-order/derivative-free solvers backend-generic; a richerlinalgtier holds matrix ops that LA-heavy solvers bound on by the minimum subset they need. - WASM in the default build. No BLAS/LAPACK, no threads, no
std::time::Instantin default paths. CI enforceswasm32-unknown-unknown. Parallelism and BLAS-backed paths are opt-in features.
Current Solvers Supported
- First-order/quasi-Newton: gradient descent (with momentum + pluggable line searches), BFGS, L-BFGS, L-BFGS-B
- Derivative-free: Nelder--Mead, Brent
- Nonlinear least squares: Gauss--Newton, Levenberg--Marquardt, trust-region-reflective
- Global/stochastic: random search, CMA-ES, steady-state GA, memetic combinations
- Constrained: projected gradient, bounded Nelder--Mead/L-BFGS-B/CMA-ES, log-barrier, augmented Lagrangian
Example
use basin::{BasicState, CostFunction, Executor, Gradient, GradientDescent, GradientTolerance};
struct Rosenbrock;
impl CostFunction for Rosenbrock {
type Param = Vec<f64>;
type Output = f64;
type Error = std::convert::Infallible;
fn cost(&self, x: &Vec<f64>) -> Result<f64, std::convert::Infallible> {
Ok((1.0 - x[0]).powi(2) + 100.0 * (x[1] - x[0].powi(2)).powi(2))
}
}
impl Gradient for Rosenbrock {
type Gradient = Vec<f64>;
fn gradient(&self, x: &Vec<f64>) -> Result<Vec<f64>, std::convert::Infallible> {
Ok(vec![
-2.0 * (1.0 - x[0]) - 400.0 * x[0] * (x[1] - x[0].powi(2)),
200.0 * (x[1] - x[0].powi(2)),
])
}
}
let result = Executor::new(Rosenbrock, GradientDescent::new(1e-3), BasicState::new(vec![-1.2, 1.0]))
.max_iter(50_000)
.terminate_on(GradientTolerance(1e-6))
.run()
.unwrap();
Links
- Crate: https://crates.io/crates/basin (
cargo add basin) - Docs: https://basin.bz/docs/, API: https://docs.rs/basin
- In-browser solver visualizer: https://basin.bz/visualizer/
- Benchmarks vs other crates and across backends: https://basin.bz/benchmarks/
- Source: https://github.com/jolars/basin (MIT)
Feedback is very welcome, especially on the trait surface, naming, and any solver/backend combinations you'd want that aren't there yet.
112
Upvotes
5
u/Zogzer 5d ago
We have been using
argminfor a while now. Biggest difference from your example above is that yourCostFunctiontrait doesn't return aResultof any type. Our cost functions are not perfect and sometimes this happens and we want to exit quickly, is there a reason to this change or are there some type tricks that let us dotype Output = Result<f64, ..>;? Thanks