multicalc
multicalc is a Rust library for single- and multi-variable calculus: derivatives, integrals,
Jacobians and Hessians, vector-field operators, and Taylor approximation in a no-std environment.
Why use it
- Pure, safe Rust.
no_std, with no heap allocation and no panics — it runs on bare-metal and embedded targets.- Generic over the scalar type — use
f32orf64, defaulting tof64. - Transcendental functions come from
libm, so the math works withoutstd. - Every fallible call returns a typed
CalcError; convenience wrappers fill in sensible defaults. - A runnable example for every module, and a test suite covering each error path.
What it does
- Differentiation of any order, total and partial — exact via forward-mode autodiff by default, with finite differences available for black-box functions.
- Integration of any order:
- Iterative rules — Boole, Simpson, Trapezoidal — over finite, semi-infinite, and infinite limits.
- Gaussian quadrature — Gauss-Legendre, Gauss-Hermite, Gauss-Laguerre.
- Jacobian and Hessian matrices.
- Vector calculus — line and flux integrals, curl, and divergence.
- Approximation — linear and quadratic (Taylor) models, with goodness-of-fit metrics.
- Least-squares optimization — Levenberg-Marquardt and Gauss-Newton solvers for nonlinear curve fitting.
- Root finding — bracketed bisection and Newton solvers for scalar equations and square systems, with exact derivatives by default and an optional damped (backtracking) line search.
Install
Enable the alloc feature if you want the heap-based methods (see Heap allocation).
A note on no_std
Methods like f64::sin are only available with std. In a no_std crate, call the libm versions
instead — for example libm::sin(x) in place of x.sin(). multicalc re-exports libm, so you can
reach it as multicalc::libm. The examples below use x.sin() because they assume std.
At a glance
Derivatives
use AutoDiffSingle;
use DerivatorSingleVariable;
use scalar_fn;
let f = scalar_fn!; // f(x) = x^3
let d = default; // forward-mode autodiff, exact
let first = d.get.unwrap; // 12.0
let third = d.get.unwrap; // 6.0
// get_single / get_double are wrappers for orders 1 and 2
For several variables, the derivative order is just the number of indices you pass:
use AutoDiffMulti;
use DerivatorMultiVariable;
use scalar_fn;
// g(x, y, z) = y*sin(x) + x*cos(y) + x*y*e^z
let g = scalar_fn!;
let d = default;
let point = ;
let dx = d.get_single_partial.unwrap; // dg/dx
let mixed = d.get.unwrap; // d(dg/dx)/dy
let third = d.get.unwrap; // d^3 g / dx^2 dy
Pass a finite-difference derivator (FiniteDifferenceSingle / FiniteDifferenceMulti) instead when
the function is a black box you cannot author with scalar_fn!.
Integration
use IntegratorSingleVariable;
use IterativeSingle;
let f = ;
let integrator = default; // Boole's rule, 120 intervals
let area = integrator.get_single.unwrap; // 4.0
// infinite and semi-infinite limits are supported for decaying integrands
let bell = integrator
.get_single
.unwrap; // sqrt(pi)
Choose the rule and interval count with from_parameters:
use IterativeMethod;
let integrator = from_parameters;
Gaussian quadrature
Each Gaussian rule integrates over a fixed domain. Pass the bare integrand f(x) — the weights
already carry the weighting factor.
use IntegratorSingleVariable;
use GaussianSingle;
use GaussianQuadratureMethod;
// Gauss-Hermite integrates f(x) * e^(-x^2) over the whole real line.
let hermite = from_parameters;
let val = hermite
.get_single
.unwrap; // sqrt(pi)/2
| Rule | Computes |
|---|---|
| Gauss-Legendre | $\int_a^b f(x), \mathrm{d}x$ |
| Gauss-Laguerre | $\int_0\infty f(x), e{-x}, \mathrm{d}x$ |
| Gauss-Hermite | $\int_{-\infty}\infty f(x), e{-x^2}, \mathrm{d}x$ |
Jacobian and Hessian
A vector-valued function is authored with scalar_fn_vec!, so its rows differentiate under autodiff:
use Jacobian;
use Hessian;
use c;
use ;
// the vector function (x*y*z, x^2 + y^2)
let f = scalar_fn_vec!;
let jacobian: Jacobian = default;
let j = jacobian.get.unwrap; // [[6, 3, 2], [2, 4, 0]]
// g(x, y) = y*sin(x) + 2*x*e^y
let g = scalar_fn!;
let hessian: Hessian = default;
let h = hessian.get.unwrap;
Vector calculus
Curl and divergence take an explicit derivator; pass AutoDiffMulti::default() for exact results.
Line and flux integrals sample the field, so they take plain closures.
use AutoDiffMulti;
use c;
use scalar_fn_vec;
use ;
// field (2xy, 3cos y)
let field = scalar_fn_vec!;
let curl_2d = get_2d.unwrap;
let div_2d = get_2d.unwrap;
// field (y, -x) along the unit circle (cos t, sin t)
let g: = ;
let curve: = ;
let limit = ;
let line = get_2d.unwrap; // -2*pi
let flux = get_2d.unwrap; // 0
Approximation
use LinearApproximator;
use scalar_fn;
let f = scalar_fn!;
let linear: LinearApproximator = default;
let model = linear.get.unwrap;
let y = model.predict;
// model.get_prediction_metrics(&samples, &f) returns RMSE, R^2, and more
QuadraticApproximator works the same way and captures curvature as well.
Least-squares fitting
Author the residuals model - data with scalar_fn_vec!; the solver differentiates them under
autodiff. LevenbergMarquardt is the robust default; GaussNewton is the faster undamped variant
for well-conditioned problems.
use LevenbergMarquardt;
use AutoDiffMulti;
use c;
use scalar_fn_vec;
// Fit a*e^(b*t) to (0, 100), (1, 50), (2, 25): the minimum is a = 100, b = -ln 2.
let residuals = scalar_fn_vec!;
let report = default
.minimize
.unwrap;
// report.solution ~ [100.0, -0.693]; report.termination says which test converged
For a plain linear least-squares or linear solve, use the QR factorization directly:
use ;
// Least-squares fit of y = a + b*t through (0, 1), (1, 3), (2, 5): a = 1, b = 2.
let a = new;
let b = new;
let x = decompose.unwrap.solve_least_squares.unwrap;
Root finding
Bisection brackets a scalar root and is guaranteed to converge. Newton takes Newton steps with
exact derivatives by default (finite differences on request); with_backtracking(true) adds a damped
line search that rescues far starts. NewtonSystem solves square systems F(x) = 0 with the exact
Jacobian. Every solver takes an iteration budget and reports why it stopped.
use ;
use ;
use c;
use ;
// Bracket a scalar root: f(x) = x^2 - 2 on [0, 2].
let f = scalar_fn!;
let bracketed = default.solve.unwrap; // ~ sqrt(2)
// Newton with exact derivatives; damped Newton adds a backtracking line search.
let quadratic = default.solve.unwrap; // ~ 1.41421356
let damped = default
.with_backtracking
.solve
.unwrap;
// Square system: x^2 + y^2 = 4 and x*y = 1.
let system = scalar_fn_vec!;
let solved = default.solve.unwrap;
// solved.root ~ [1.9319, 0.5176]; solved.termination says which test converged
Linear solves
use ;
// Solve A·x = b.
let a = new;
let b = new;
let x = a.solve.unwrap; // [1, 2, 3]
let lu = a.lu.unwrap;
let det = lu.determinant;
let inv = lu.inverse;
// A symmetric positive-definite matrix has a faster Cholesky path.
let s = new;
let s_inv = s.cholesky.unwrap.inverse;
Singular values and pseudo-inverse
The singular value decomposition (one-sided Jacobi) gives the pseudo-inverse, minimum-norm least-squares solve, rank, and condition number for any shape.
use ;
// Thin SVD of a tall matrix: A = U · diag(σ) · Vᵀ.
let a = new;
let svd = a.svd.unwrap;
let sigma = svd.singular_values; // descending, non-negative
let cond = svd.condition_number; // σ_max / σ_min
// Moore–Penrose pseudo-inverse — tall, square, or wide (M < N) inputs.
let a_pinv = a.pseudo_inverse.unwrap;
// Minimum-norm least-squares solve of A·x = b, without forming A⁺.
let x = svd.solve;
Error handling
Where a sensible default exists, a "safe" wrapper (such as get_single or get_double) returns the
answer directly. Otherwise the call returns Result<T, CalcError> for the working scalar T (f64
by default), so you decide how to handle bad input. All variants are listed in
error_codes.rs.
Heap allocation
By default everything uses fixed-size stack arrays. Enable the alloc feature for Vec-based methods
that handle inputs too large for the stack. This currently covers the Jacobian's get_on_heap, which
returns a Vec<Vec<T>> of the scalar (Vec<Vec<f64>> by default).
Examples
Runnable, self-contained programs for each module live in examples/ — see
examples/README.md. Run one with:
Benchmarks
See benches/README.md for the index, or go straight to calculus, linear_algebra, optimization, or root_finding for accuracy figures and measured latency.
Contributing
See CONTRIBUTIONS.md.
Acknowledgements
The least-squares solvers and QR factorization port the public-domain MINPACK routines lmder,
lmpar, qrfac, and qrsolv (Moré, Garbow, Hillstrom; netlib), following Moré (1978), "The
Levenberg-Marquardt algorithm: Implementation and theory", and Nocedal & Wright, Numerical
Optimization (chapters 4 and 10).
License
multicalc is licensed under the MIT license.