Expand description
Β§la-stack

Fast, stack-allocated linear algebra for fixed dimensions in Rust.
This crate grew from the need to support delaunay with fast, stack-allocated linear algebra primitives and algorithms
while keeping the API intentionally small and explicit.
Β§π Introduction
la-stack provides a handful of const-generic, stack-backed building blocks:
Vector<const D: usize>for fixed-lengthf64vectors backed by[f64; D]Matrix<const D: usize>for fixed-size squaref64matrices backed by[[f64; D]; D]Lu<const D: usize>for LU factorization with partial pivoting (solve + det)Ldlt<const D: usize>for LDLT factorization without pivoting (solve + det; symmetric SPD/PSD)
Β§β¨ Design goals
- β
Copytypes where possible - β Const-generic dimensions (no dynamic sizes)
- β
const fnwhere possible (compile-time evaluation of determinants, dot products, etc.) - β Explicit algorithms (LU, solve, determinant)
- β
Robust geometric predicates via optional exact arithmetic (
det_sign_exact,det_errbound) - β
Exact linear system solve via optional arbitrary-precision arithmetic (
solve_exact, strict/rounded f64 conversions) - β No runtime dependencies by default (optional features may add deps)
- β Stack storage only (no heap allocation in core types)
- β
unsafeforbidden
See CHANGELOG.md for release history and docs/roadmap.md for current release planning.
Β§π« Anti-goals
- Bare-metal performance: see
blas-src,lapack-src,openblas-src - Comprehensive: use
nalgebraif you need a full-featured library - Large matrices/dimensions with parallelism: use
faerif you need this - Alternate floating-point scalar families:
la-stacksupportsf64and optional exact arithmetic, notf32/f16APIs
Β§β Use this crate when
- Your matrices and vectors have small, fixed dimensions known at compile time
- Stack allocation and
Copyvalue semantics fit your data flow - You want explicit LU / LDLT / determinant APIs rather than a broad algebra toolkit
- You need exact determinants, exact determinant signs, or exact linear solves for fixed-size systems
- Robust predicates matter for geometry-style workloads near degeneracy
- You prefer a default build with no runtime dependencies
Β§π’ Scalar types
The scalar model is intentionally limited to f64 for floating-point work and
exact rationals behind the optional "exact" feature. This matches the crateβs
focus on small, robustness-sensitive numerical and computational geometry
workloads. When f64 precision is insufficient (e.g. near-degenerate geometric
configurations), the optional "exact" feature provides arbitrary-precision
arithmetic via BigRational (see below).
Lower-precision f32 / f16 throughput-oriented workloads are outside the
crateβs scope; they usually indicate large-matrix or accelerator-oriented use
cases better served by broader linear-algebra libraries.
Β§π Quickstart
Add this to your Cargo.toml:
[dependencies]
la-stack = "0.4.3"Β§Feature flags
default: no runtime dependenciesexact:BigRationalexact determinant and solve APIsbench: Criterion, nalgebra, and faer for internal benchmarks
Solve a 5Γ5 system via LU:
use la_stack::prelude::*;
fn main() -> Result<(), LaError> {
// This system requires pivoting (a[0][0] = 0), so it's a good LU demo.
// A = J - I: zeros on diagonal, ones elsewhere.
let a = Matrix::<5>::try_from_rows([
[0.0, 1.0, 1.0, 1.0, 1.0],
[1.0, 0.0, 1.0, 1.0, 1.0],
[1.0, 1.0, 0.0, 1.0, 1.0],
[1.0, 1.0, 1.0, 0.0, 1.0],
[1.0, 1.0, 1.0, 1.0, 0.0],
])?;
let b = Vector::<5>::try_new([14.0, 13.0, 12.0, 11.0, 10.0])?;
let lu = a.lu(DEFAULT_SINGULAR_TOL)?;
let x = lu.solve(b)?.into_array();
// Floating-point rounding is expected; compare with a tolerance.
let expected = [1.0, 2.0, 3.0, 4.0, 5.0];
for (x_i, e_i) in x.iter().zip(expected.iter()) {
assert!((*x_i - *e_i).abs() <= 1e-12);
}
Ok(())
}Compute a determinant for a symmetric SPD matrix via LDLT (no pivoting).
For symmetric positive-definite matrices, LDL^T is essentially a square-root-free form of the Cholesky decomposition
(you can recover a Cholesky factor by absorbing sqrt(D) into L):
use la_stack::prelude::*;
fn main() -> Result<(), LaError> {
// This matrix is symmetric positive-definite (A = L*L^T) so LDLT works without pivoting.
let a = Matrix::<5>::try_from_rows([
[1.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 2.0, 1.0, 0.0, 0.0],
[0.0, 1.0, 2.0, 1.0, 0.0],
[0.0, 0.0, 1.0, 2.0, 1.0],
[0.0, 0.0, 0.0, 1.0, 2.0],
])?;
let ldlt = match a.ldlt(DEFAULT_SINGULAR_TOL) {
Ok(ldlt) => ldlt,
Err(err @ LaError::Asymmetric { row, col, .. }) => {
eprintln!("LDLT requires symmetry; first mismatch at ({row}, {col})");
return Err(err);
}
Err(err) => return Err(err),
};
let det = ldlt.det()?;
assert!((det - 1.0).abs() <= 1e-12);
Ok(())
}β οΈ LDLT invariant: The input matrix must be symmetric. Asymmetric inputs passed to
Matrix::ldltreturn a typedLaError::Asymmetricbefore factorization starts. UseMatrix::first_asymmetryto locate the offending pair, or fall back tolu()if your matrices may not be symmetric at all. Symmetric inputs with a negative LDLT diagonal returnLaError::NotPositiveSemidefinite; zero or too-small non-negative diagonals returnLaError::Singular.
Β§β‘ Compile-time determinants (D β€ 4)
det_direct() is a const fn providing closed-form determinants for D=0β4,
using fused multiply-add where applicable. Matrix::<0>::zero().det_direct()
returns Ok(Some(1.0)) (the empty-product convention). For D=1β4, cofactor
expansion bypasses LU factorization entirely. This enables compile-time
evaluation when inputs are known:
use la_stack::prelude::*;
// Evaluated entirely at compile time β no runtime cost.
const DET: Result<Option<f64>, LaError> = match Matrix::<4>::try_from_rows([
[2.0, 0.0, 0.0, 0.0],
[0.0, 3.0, 0.0, 0.0],
[0.0, 0.0, 5.0, 0.0],
[0.0, 0.0, 0.0, 7.0],
]) {
Ok(matrix) => matrix.det_direct(),
Err(err) => Err(err),
};
fn main() -> Result<(), LaError> {
assert_eq!(DET?, Some(210.0));
Ok(())
}The public det() method automatically dispatches through the closed-form path
for D β€ 4 and falls back to LU for D β₯ 5. Finite inputs return a floating-point
determinant estimate in every dimension; det() does not surface
LaError::Singular. Tiny nonzero determinants are not flattened by a pivot
tolerance. Use lu() directly when you need tolerance-aware singularity
detection or the pivot-column diagnostic from the factorization, and use the
exact determinant APIs when exact singularity classification matters.
Β§π¬ Exact arithmetic ("exact" feature)
The default build has zero runtime dependencies. Enable the optional
exact Cargo feature to add exact arithmetic methods using arbitrary-precision
rationals (this pulls in num-bigint, num-rational, and num-traits for
BigRational):
[dependencies]
la-stack = { version = "0.4.3", features = ["exact"] }Determinants:
det_exact()β returns the exact determinant as aBigRationaldet_exact_f64()β returns the exact determinant asf64only when it is exactly representable (orLaError::Unrepresentableotherwise)det_exact_rounded_f64()β returns the exact determinant rounded to a finitef64det_sign_exact()β returns the provably correct sign (β1, 0, or +1)
Linear system solve:
solve_exact(b)β solvesAx = bexactly, returning[BigRational; D]solve_exact_f64(b)β solvesAx = bexactly, returningVector<D>only when every component is exactly representable asf64solve_exact_rounded_f64(b)β solvesAx = bexactly, returning each component rounded to finitef64
use la_stack::prelude::*;
fn main() -> Result<(), LaError> {
// Exact determinant
let m = Matrix::<3>::try_from_rows([
[1.0, 2.0, 3.0],
[4.0, 5.0, 6.0],
[7.0, 8.0, 9.0],
])?;
assert_eq!(m.det_sign_exact()?, 0); // exactly singular
let det = m.det_exact()?;
assert_eq!(det, BigRational::from_integer(0.into())); // exact zero
let det_f64 = m.det_exact_f64()?;
assert_eq!(det_f64, 0.0);
// If strict exact-to-f64 conversion would require rounding, opt in
// explicitly with the rounded API.
let inexact = Matrix::<2>::try_from_rows([
[1.0 + f64::EPSILON, 0.0],
[0.0, 1.0 - f64::EPSILON],
])?;
let rounded_det = match inexact.det_exact_f64() {
Ok(det) => det,
Err(err) if err.requires_rounding() => inexact.det_exact_rounded_f64()?,
Err(err) => return Err(err),
};
assert_eq!(rounded_det.to_bits(), 1.0f64.to_bits());
// If the exact determinant cannot fit in f64, keep the BigRational value.
let big = f64::MAX / 2.0;
let huge = Matrix::<3>::try_from_rows([
[0.0, 0.0, 1.0],
[big, 0.0, 1.0],
[0.0, big, 1.0],
])?;
let huge_det = huge.det_exact()?;
assert_eq!(
huge.det_exact_f64()
.err()
.and_then(|err| err.unrepresentable_reason()),
Some(UnrepresentableReason::NotFinite)
);
println!("exact determinant = {huge_det}");
// Exact linear system solve
let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
let b = Vector::<2>::try_new([5.0, 11.0])?;
let x = a.solve_exact_f64(b)?.into_array();
assert!((x[0] - 1.0).abs() <= f64::EPSILON);
assert!((x[1] - 2.0).abs() <= f64::EPSILON);
Ok(())
}With the exact feature enabled, BigInt and BigRational are re-exported
from the crate root and prelude, alongside the most commonly needed
num-traits items (FromPrimitive, ToPrimitive, Signed). This lets
consumers construct exact values (BigRational::from_f64, from_i64), query
sign (is_positive / is_negative), and convert back to f64 (to_f64)
with a single use la_stack::prelude::*; β no need to add num-bigint,
num-rational, or num-traits to their own Cargo.toml.
For det_sign_exact(), D β€ 4 matrices use a fast f64 filter (error-bounded
det_direct()) that resolves the sign without allocating. Only near-degenerate
or large (D β₯ 5) matrices fall through to the exact Bareiss algorithm.
Β§Adaptive precision with det_errbound()
det_errbound() returns the conservative absolute error bound used by the fast
filter. This method does NOT require the exact feature β it uses pure f64
arithmetic and is available by default. This enables building custom
adaptive-precision logic for geometric predicates:
use la_stack::prelude::*;
fn main() -> Result<(), LaError> {
let m = Matrix::<3>::identity();
if let Some(bound) = m.det_errbound()? {
if let Some(det) = m.det_direct()? {
if det.abs() > bound {
// f64 sign is guaranteed correct
let sign = det.signum() as i8;
} else {
// Fall back to exact arithmetic (requires `exact` feature)
let sign = m.det_sign_exact()?;
}
}
} else {
// D β₯ 5: no fast filter, use exact directly (requires `exact` feature)
let sign = m.det_sign_exact()?;
}
Ok(())
}The error coefficients (ERR_COEFF_2, ERR_COEFF_3, ERR_COEFF_4) are the
dimension-specific constants behind that bound. In plain terms, they answer:
βhow many machine-epsilon-sized rounding mistakes can this closed-form
determinant formula accumulate?β To get an absolute error bound, det_errbound()
multiplies the coefficient by a size measure of the matrix entries, the
absolute Leibniz sum:
p(|A|) = sum over determinant terms of product of absolute valuesFor a 2Γ2 matrix [[a, b], [c, d]], that scale is |a*d| + |b*c|, so:
|det_direct(A) - det_exact(A)| <= ERR_COEFF_2 * (|a*d| + |b*c|)The coefficients are not tolerances and are not meant to be tuned by callers; they are conservative constants derived from the fixed D β€ 4 formulas and their floating-point rounding chains. They are exposed for advanced users who want to compose the same bound themselves.
Β§π§© API at a glance
| Type | Storage | Purpose | Key methods |
|---|---|---|---|
Vector<D> | [f64; D] | Finite fixed-length vector for input and computation | try_new, zero, dot, norm2_sq |
Matrix<D> | [[f64; D]; D] | Finite square matrix for input and computation | See below |
Lu<D> | Matrix<D> + pivot array | Factorization for solves/det | solve, det |
Ldlt<D> | Matrix<D> | Factorization for symmetric SPD/PSD solves/det | solve, det |
Storage shown above reflects the intentional f64 scalar model.
Matrix<D> key methods: lu, ldlt, det, det_direct, det_errbound,
det_exactΒΉ, det_exact_f64ΒΉ, det_exact_rounded_f64ΒΉ, det_sign_exactΒΉ,
solve_exactΒΉ, solve_exact_f64ΒΉ, solve_exact_rounded_f64ΒΉ.
Matrix and vector constructors validate non-finite inputs at public API
boundaries. After construction, Matrix<D> and Vector<D> carry that
finite-storage invariant directly, so kernels do not revalidate stored entries.
ΒΉ Requires features = ["exact"].
Β§π Benchmarks (vs nalgebra/faer)
Raw data: docs/assets/bench/vs_linalg_lu_solve_median.csv
Representative benchmark: lu_solve factors the matrix and solves one
right-hand side. Median time is lower-is-better, and the βla-stack vs
nalgebra/faerβ columns show the % time reduction relative to each baseline
(positive = la-stack faster). This is not an aggregate score across all
operations.
For the full per-kernel comparison methodology, input construction, and release-comparison workflow details, see docs/BENCHMARKING.md. For the current release-to-release performance snapshot, see docs/PERFORMANCE.md.
| D | la-stack median (ns) | nalgebra median (ns) | faer median (ns) | la-stack vs nalgebra | la-stack vs faer |
|---|---|---|---|---|---|
| 2 | 2.044 | 4.542 | 143.958 | +55.0% | +98.6% |
| 3 | 9.596 | 23.599 | 185.466 | +59.3% | +94.8% |
| 4 | 23.338 | 50.717 | 210.976 | +54.0% | +88.9% |
| 5 | 45.368 | 69.065 | 277.564 | +34.3% | +83.7% |
| 8 | 127.861 | 164.412 | 364.864 | +22.2% | +65.0% |
| 16 | 631.997 | 663.822 | 882.674 | +4.8% | +28.4% |
| 32 | 2,745.604 | 2,424.540 | 2,867.431 | -13.2% | +4.2% |
| 64 | 17,543.034 | 14,747.731 | 12,266.271 | -19.0% | -43.0% |
Β§π Examples
The examples/ directory contains small, runnable programs:
solve_5x5β solve a 5Γ5 system via LU with partial pivotingdet_5x5β determinant of a 5Γ5 matrix via LUldlt_solve_3x3β solve a 3Γ3 symmetric positive definite system via LDLTconst_det_4x4β compile-time 4Γ4 determinant viadet_direct()exact_det_3x3β exact determinant value of a near-singular 3Γ3 matrix (requiresexactfeature)exact_sign_3x3β exact determinant sign of a near-singular 3Γ3 matrix (requiresexactfeature)exact_solve_3x3β exact solve of a near-singular 3Γ3 system vs f64 LU (requiresexactfeature)
just examples
# or individually:
cargo run --example solve_5x5
cargo run --example det_5x5
cargo run --example ldlt_solve_3x3
cargo run --example const_det_4x4
cargo run --features exact --example exact_det_3x3
cargo run --features exact --example exact_sign_3x3
cargo run --features exact --example exact_solve_3x3Β§π€ Contributing
A short contributor workflow:
cargo install just
just setup # install/verify dev tools + sync Python deps
just check # lint/validate (non-mutating)
just fix # apply auto-fixes (mutating)
just ci # lint + tests + examples + bench compileThe repository uses Rust-native tooling for documentation and config checks:
rumdl for Markdown, dprint with pretty_yaml for YAML, taplo for TOML,
and typos for spelling. GitHub Actions references are SHA-pinned, restricted
to an explicit allowlist, and kept with readable version comments for review.
CI runs just ci on Ubuntu, macOS, and Windows to keep platform coverage
aligned with the local comprehensive validation path.
For coverage commands and report locations, see
docs/COVERAGE.md.
For the full contributor workflow, see
CONTRIBUTING.md.
Β§π Citation
If you use this library in academic work, please cite it using CITATION.cff (or GitHubβs βCite this repositoryβ feature). Tagged releases are archived on Zenodo.
Β§π References
For canonical references to the algorithms used by this crate, see REFERENCES.md.
Β§π€ AI Agents
AI coding assistants should read AGENTS.md before proposing or applying changes. See CONTRIBUTING.md for the repositoryβs AI-assisted development note.
Β§π License
BSD 3-Clause License. See LICENSE.
ModulesΒ§
- prelude
- Common imports for ergonomic usage.
MacrosΒ§
- try_
with_ stack_ matrix - Fallibly dispatch a runtime dimension to a concrete stack-allocated matrix.
StructsΒ§
- BigInt
- A big signed integer type.
- Ldlt
- LDLT factorization (
A = L D Lα΅) for symmetric positive (semi)definite matrices. - Lu
- LU decomposition (PA = LU) with partial pivoting.
- Matrix
- Finite fixed-size square matrix
DΓD, stored inline. - Tolerance
- Finite, non-negative tolerance used by numerical predicates and factorizations.
- Vector
- Finite fixed-size vector of length
D, stored inline.
EnumsΒ§
- LaError
- Linear algebra errors.
- Unrepresentable
Reason - Reason an exact result cannot satisfy an exact-to-
f64conversion contract.
ConstantsΒ§
- DEFAULT_
SINGULAR_ TOL - Default absolute threshold used for singularity/degeneracy detection.
- ERR_
COEFF_ 2 - Absolute error coefficient for
Matrix::<2>::det_direct. - ERR_
COEFF_ 3 - Absolute error coefficient for
Matrix::<3>::det_direct. - ERR_
COEFF_ 4 - Absolute error coefficient for
Matrix::<4>::det_direct. - MAX_
STACK_ MATRIX_ DISPATCH_ DIM - Largest dimension supported by
try_with_stack_matrix!.
TraitsΒ§
- From
Primitive - A generic trait for converting a number to a value.
- Signed
- Useful functions for signed numbers (i.e. numbers that can be negative).
- ToPrimitive
- A generic trait for converting a value to a number.
Type AliasesΒ§
- BigRational
- Alias for arbitrary precision rationals.