xingo-sparse
Sparse linear algebra for optimization solvers, in pure Rust: the factorizations a simplex method and an interior-point method need, over compressed sparse containers generic in the scalar type. It is the linear algebra layer of Xingó, a linear and mixed-integer programming solver, and depends on nothing in it: the solver depends on this crate, never the other way round.
It is not a general sparse or dense linear algebra library. There is no sparse matrix product, no QR or SVD, no eigenvalues, no iterative solvers, no complex scalars (see "Scope" below); faer, nalgebra and ndarray cover that ground. What is here is what those do not offer and an optimization solver cannot do without.
Three entry points
1. The basis of a simplex method — a dynamic LU with threshold Markowitz
pivoting (Suhl & Suhl 1990), Forrest–Tomlin updates when one column of
the basis changes, and hypersparse FTRAN/BTRAN (Gilbert–Peierls reach,
Hall–McKinnon density policy). The basis is never formed: it is a list of
references into [A | ±I] plus the constraint matrix A.
use ;
// A 2 × 3 constraint matrix by coordinates, then its CSC form.
let mut t = new;
for in
let a = t.to_csc?;
// A basis: column a_0 at position 0, the slack of row 1 at position 1.
let mut heading = vec!;
let mut lu = default; // Forrest–Tomlin is the default update policy
lu.factorize?;
// FTRAN: x = B⁻¹ b (row-indexed in, position-indexed out).
let mut x = from_dense;
lu.ftran;
assert_eq!;
// Exchange: a_2 enters at position 1. `ftran_for_update` records the spike.
let mut alpha = new;
Structural.for_each_entry;
lu.ftran_for_update;
assert_eq!;
heading = Structural;
// The factor never refactorizes on its own; it asks (`RefactorNeeded`) and
// the caller, who owns the columns, calls `factorize` again.
# Ok::
2. Fill-reducing orderings — our own Amd (Amestoy, Davis & Duff 1996)
and Colamd (Davis, Gilbert, Larimore & Ng 2004) behind the Ordering
trait, on a value-free Pattern; Pattern::cholesky_nnz counts the fill an
ordering yields. Fill within 1.05× of SuiteSparse's on Netlib and
Harwell–Boeing (331 comparisons, worst 1.029×).
use ;
// The arrowhead: a dense first row and column plus the diagonal.
let n = 20;
let mut t = new;
for i in 0..n as u32
let s = t.to_csc?.pattern;
let band = s.cholesky_nnz?;
let amd = s.cholesky_nnz?;
assert_eq!; // pivoting on the hub first fills everything
assert_eq!; // minimum degree leaves it for last
# Ok::
3. The KKT systems of an interior-point method — the KktSolver trait
in the CHOLMOD shape (analyze once, factorize many, solve), with the
regularization policy (static shifts ρ, δ in the equilibrated matrix
plus dynamic pivot bumping, Altman & Gondzio 1999) and iterative
refinement (right-preconditioned GMRES, best iterate kept) in the trait's
provided methods, so every backend shares them. The 0.1 backend is
FaerLdl, faer's sparse LDLᵀ/LBLᵀ ordered by our AMD (feature faer).
use ;
// K = [−D Aᵀ; A δI] for A = [1 1 0; 0 1 1], D = diag(1, 2, 3): lower triangle.
let = ;
let part = from_sizes?; // primal block, then dual
let dual = part.row_block.start as u32;
let mut t = new;
for in .into_iter.enumerate
for in
let k = t.to_csc?;
let form = Augmented ;
let solver = default;
let sym = solver.analyze?; // once per pattern
let = solver.factorize?; // every iteration
assert_eq!; // m positive, n negative pivots
let b = ;
let mut x = ;
// Refine against the unregularized K: the shifts' bias is removed.
let report = solver.solve_refined;
assert!;
# Ok::
The three programs in examples/ run these end to end
(cargo run -p xingo-sparse --example basis_lu, ordering,
kkt_ipm_step), each ending in a check of its result.
Features
faer(default) — brings in faer 0.24 (pure Rust, MIT; norayon: the factorizations run sequentially and the results do not depend on threads) for the KKT backendFaerLdland the dense kernelFaerDense.- Without it (
default-features = false) the crate has no dependency at all: the containers, the LU with its updates and solves, the orderings, the scaling, theKktSolvertrait with its regularization and refinement, and the reference dense kernelNaiveDenseremain. The Xingó solver builds it this way: the simplex side never touches faer.
Scope
In: CSC/CSR containers and a sparse work vector, generic in T: Real
(f64 today; double-double and quad-double are planned in a sibling
crate); the coordinate builder Triplets and BlockPartition; row/column
scaling (geometric in powers of two, Curtis–Reid, ∞-norm); the dynamic LU
with a reusable pivot sequence (SymbolicLu) for refactorizations of the
same pattern; Forrest–Tomlin updates; AMD and COLAMD; the regularized,
refined KKT solver.
Out, by decision: a complete dense library; sparse matrix products, QR, SVD, eigenvalues; iterative solvers and preconditioners beyond the refinement loop; complex scalars; METIS or any C binding; GPU. Bartels–Golub updates and an own supernodal LDLᵀ are specified in the crate's contract and scheduled after 0.1.
Known limitations of 0.1.0
Measured, not guessed (the decision log of the solver records each):
FaerLdlis implemented forf64only; its Bunch–Kaufman pivoting is intranodal and used only for an unregularized augmented system — the regularized path does not pivot (it is quasi-definite and needs none).- The default regularization (
ρ = 10⁻⁶,δ = 10⁻⁸in the equilibrated matrix) was calibrated on the 93 Netlib LPs at three stages of an interior-point method (558 systems, plus 3 × 558 with other random diagonals): residual against the unregularizedKat most10⁻¹⁰with the default refinement in all but one case (that one at1.6·10⁻¹⁰). A running interior-point method will reviewρ, δ. - Our
Amdis about 1.25× slower than faer's AMD (geometric mean on the ten largest Netlib systems, at most 1.42×), with the same fill; it does the same algorithmic work and stores its lists differently. - The count of bumped pivots is exact for the simplicial structure; for
the supernodal one, negative-sign bumps are counted by value and may be
overcounted by a pivot that equals
−βexactly. - The LU declares singular a column whose entries are all at or below the
absolute pivot tolerance (
10⁻¹⁰), without scaling; the Harwell–Boeing matrix sherman3 needsScalingfirst (or a lowerabs_pivot_tol). Refactorization with a reused pivot sequence speeds up dense factors less than 1.5× (it saves the analysis, not the arithmetic). The replay threshold defaults to the Markowitz thresholdu;u/10is available. - A
KktSolver::solveallocates once per factor, at its first call (the lazy initialization of a mutex on macOS);solve_refined_withwith a reusedRefineWorkspaceallocates nothing afterwards. Triplets::to_cscdoes not store an entry whose sum is exactly zero (the containers forbid explicit zeros); zeros are accepted as input.
Design notes
- Literate. Every module is a chapter: notation, the algorithm in
numbered steps, invariants (each a
debug_assert!or acheck_invariants()), references. The crate documentation is the text; start at the crate root on docs.rs, or read Chapter 5 of the solver's book, which includes these sources verbatim. - Read, not copied. HiGHS (
HFactor,HVector), SuiteSparse (AMD, COLAMD, CHOLMOD) and faer were studied as references and oracles; the code here is written from the papers. No GPL or LGPL code is used or linked; faer (MIT) is the only dependency, and only with thefaerfeature. - Deterministic. Same input, same options ⇒ same result, bit for bit: no hashing, no threads, no allocation-order dependence anywhere in the numerical paths.
- Errors, not panics, on data: malformed containers, singular bases, wrong inertia and non-finite factors are reported with what the caller needs to recover. Panics are reserved for contract violations (a vector of the wrong length).
- No
unsafein this crate (faer has its own, audited, kernels).
Repository, issues and citation
The crate is developed inside the Xingó monorepo
(StrategicProjects/xingo,
directory crates/sparse), where every change is validated together with
the solver that uses it. This stand-alone repository,
StrategicProjects/xingo-sparse,
is a read-only mirror of that directory (git subtree split, full history),
updated at every release; it exists so that the library can be archived and
cited on its own. Issues and pull requests about the crate are welcome in
either repository. Each release of the mirror is archived on Zenodo; the DOI
of the version you use is on the release page, and CITATION.cff carries
the concept DOI once the first archive exists.
Minimum supported Rust version
1.90 (edition 2024), tested in CI together with stable.
License
Licensed under either of the Apache License, Version 2.0 or the MIT license, at your option.