Numerical preconditioners for iterative linear solvers, built on top of the faer linear algebra crate.
Every preconditioner implements faer's LinOp, Precond, BiLinOp, and
BiPrecond traits from the matrix_free module, so they plug directly into
faer's Krylov solvers (CG, GMRES, BiCGSTAB, LSMR, ...).
| Preconditioner | Description | Status |
|---|---|---|
JacobiPrecond<T> |
Diagonal (point-Jacobi) scaling — M = diag(A) |
✅ |
BlockJacobiPrecond<T> |
Block-diagonal Jacobi — arbitrary block sizes, LU per block | ✅ |
Ilu0<I, T> |
Zero-fill incomplete LU on a CSC matrix | ✅ |
Ic0<I, T> |
Zero-fill incomplete Cholesky on a CSC Hermitian PD matrix | ✅ |
Ilutp<I, T> |
Threshold ILU with partial pivoting (dual-threshold ILUT + column pivoting) | ✅ |
SolvePrecond<S> |
Adapter wrapping any faer SolveCore factorisation (Llt, Lu, Qr, ...) |
✅ |
There is no single best preconditioner; the right one depends on the structure
of A and how much work you can afford per iteration.
- Start with
JacobiPrecond. Almost free to build and apply. It helps wheneverA's rows differ in scale — diagonally dominant systems, variable-coefficient PDEs, badly-scaled unknowns. IfAhas a constant diagonal it does nothing, so move on. Ic0for symmetric positive-definiteA. The standard choice for SPD problems from PDE discretisations (Laplacians, diffusion, elasticity) solved with conjugate gradient. It cuts iteration counts sharply; each apply is two sparse triangular solves, so it always wins on iteration count and wins on wall-clock time once the problem is ill-conditioned enough.Ilu0for general (nonsymmetric) sparseA. The nonsymmetric counterpart to IC(0), paired with GMRES or BiCGSTAB. Cheap to build, stores nothing beyondA's sparsity pattern.IlutpwhenIlu0is too weak. Threshold ILU with partial pivoting: it adds fill where the factor needs it (tuned by a drop tolerance and a fill budget) and pivots for stability — the robust choice for hard nonsymmetric problems, badly-scaled operators, or matrices with small/zero diagonals. Costs more thanIlu0, and its pattern is value-dependent (no zero-allocation refactorisation). Seeexamples/ilutp.rsfor the fill-vs-iterations tradeoff.BlockJacobiPrecondwhen unknowns cluster into small dense groups. Several fields per mesh node, coupled species, tightly-coupled sub-systems. Inverting those blocks exactly captures the strong local coupling point-Jacobi misses.SolvePrecondto reuse an exact factorisation. Factorise a cheaper approximation ofAonce and let the Krylov method correct the rest.
The examples/speedup.rs example measures all of this end-to-end on a
badly-conditioned diffusion problem — run it with
cargo run --release --example speedup.
- No heap allocation during
apply. Everyapply_scratchreturns eitherStackReq::EMPTYor a precise pre-computed size; all temporary memory flows through theMemStackprovided by faer's trait interface. - Refactorisation reuses storage.
Ilu0::refactorizeandIc0::refactorizemutate the existing value buffer for the next iteration of a nonlinear Krylov driver — no allocation occurs after the first factorisation. - In-place semantics match faer.
applyperformsout = M⁻¹ rhs;apply_in_placeoverwritesrhs.
[dependencies]
faer-precond = "0.2"
faer = "0.24"
dyn-stack = "0.13"use dyn_stack::MemStack;
use faer::{mat, Par};
use faer::matrix_free::Precond;
use faer_precond::JacobiPrecond;
let pc = JacobiPrecond::try_from_diagonal(&[4.0_f64, 2.0, 8.0]).unwrap();
let mut x = mat![[8.0_f64], [6.0], [16.0]];
pc.apply_in_place(x.as_mut(), Par::Seq, MemStack::new(&mut []));
// x is now [2.0, 3.0, 2.0]use dyn_stack::MemStack;
use faer::{mat, Par};
use faer::matrix_free::Precond;
use faer_precond::BlockJacobiPrecond;
// 5x5 matrix; two diagonal blocks of size 2 and 3.
let a = mat![
[4.0_f64, 1.0, 0.0, 0.0, 0.0],
[2.0, 3.0, 0.0, 0.0, 0.0],
[0.0, 0.0, 6.0, 1.0, 2.0],
[0.0, 0.0, 3.0, 5.0, 1.0],
[0.0, 0.0, 2.0, 1.0, 4.0],
];
let pc = BlockJacobiPrecond::try_new(a.as_ref(), &[0, 2, 5]).unwrap();
let mut x = mat![[1.0_f64], [2.0], [3.0], [-1.0], [0.5]];
pc.apply_in_place(x.as_mut(), Par::Seq, MemStack::new(&mut []));use dyn_stack::MemStack;
use faer::sparse::{SparseColMat, Triplet};
use faer::{mat, Par};
use faer::matrix_free::Precond;
use faer_precond::Ilu0;
// 5x5 tridiagonal SPD: diag 4, off -1 (no fill — ILU(0) is exact).
let mut triplets = Vec::new();
for i in 0..5 {
triplets.push(Triplet::new(i, i, 4.0_f64));
if i > 0 {
triplets.push(Triplet::new(i, i - 1, -1.0));
triplets.push(Triplet::new(i - 1, i, -1.0));
}
}
let a = SparseColMat::<usize, f64>::try_new_from_triplets(5, 5, &triplets).unwrap();
let pc = Ilu0::try_new(a.as_ref()).expect("non-singular pattern");
let mut b = mat![[1.0_f64], [0.0], [0.0], [0.0], [0.0]];
pc.apply_in_place(b.as_mut(), Par::Seq, MemStack::new(&mut []));use dyn_stack::MemStack;
use faer::sparse::{SparseColMat, Triplet};
use faer::{mat, Par};
use faer::matrix_free::Precond;
use faer_precond::Ic0;
let mut triplets = Vec::new();
for i in 0..5 {
triplets.push(Triplet::new(i, i, 4.0_f64));
if i > 0 {
triplets.push(Triplet::new(i, i - 1, -1.0));
triplets.push(Triplet::new(i - 1, i, -1.0));
}
}
let a = SparseColMat::<usize, f64>::try_new_from_triplets(5, 5, &triplets).unwrap();
// Hermitian PD input — IC(0) silently ignores the strict upper triangle.
let pc = Ic0::try_new(a.as_ref()).expect("matrix is positive definite");
let mut b = mat![[1.0_f64], [0.0], [0.0], [0.0], [0.0]];
pc.apply_in_place(b.as_mut(), Par::Seq, MemStack::new(&mut []));use dyn_stack::{MemBuffer, MemStack};
use faer::sparse::{SparseColMat, Triplet};
use faer::{mat, Par};
use faer::matrix_free::Precond;
use faer_precond::{FillControl, Ilutp, IlutpParams};
// A non-symmetric tridiagonal: diag 4, sub -2, super -1.
let mut triplets = Vec::new();
for i in 0..5usize {
triplets.push(Triplet::new(i, i, 4.0_f64));
if i > 0 {
triplets.push(Triplet::new(i, i - 1, -2.0));
triplets.push(Triplet::new(i - 1, i, -1.0));
}
}
let a = SparseColMat::<usize, f64>::try_new_from_triplets(5, 5, &triplets).unwrap();
// Tune the drop tolerance, fill budget, and pivot aggressiveness.
let params = IlutpParams {
drop_tol: 1e-3,
fill: FillControl::PerRow(10),
pivot_tol: 0.1,
..Default::default()
};
let pc = Ilutp::try_new_with_params(a.as_ref(), params).unwrap();
// Apply needs a length-n scratch column (it may apply the pivot permutation).
let mut b = mat![[1.0_f64], [0.0], [0.0], [0.0], [0.0]];
let mut buf = MemBuffer::new(pc.apply_in_place_scratch(1, Par::Seq));
pc.apply_in_place(b.as_mut(), Par::Seq, MemStack::new(&mut buf));use dyn_stack::MemStack;
use faer::{mat, Par, Side};
use faer::linalg::solvers::Llt;
use faer::matrix_free::Precond;
use faer_precond::SolvePrecond;
let a = mat![[4.0_f64, 1.0], [1.0, 3.0]];
let llt = Llt::new(a.as_ref(), Side::Lower).expect("matrix is SPD");
let pc = SolvePrecond::new(llt);
let mut x = mat![[1.0_f64], [2.0]];
pc.apply_in_place(x.as_mut(), Par::Seq, MemStack::new(&mut []));
// x is now [1/11, 7/11]When A's sparsity pattern is fixed but its values change between Krylov
iterations, build the symbolic factor once and refactorise repeatedly with
zero allocation:
use faer::sparse::{SparseColMat, Triplet};
use faer_precond::{Ilu0, SymbolicIlu0};
# let triplets: Vec<Triplet<usize, usize, f64>> = (0..3).map(|i| Triplet::new(i, i, 1.0_f64)).collect();
# let a0 = SparseColMat::<usize, f64>::try_new_from_triplets(3, 3, &triplets).unwrap();
let symbolic = SymbolicIlu0::try_new(a0.as_ref().symbolic()).unwrap();
let mut pc = Ilu0::<usize, f64>::new_with_symbolic(symbolic);
// Hot loop — no allocation:
pc.refactorize(a0.as_ref()).unwrap();
// pc.refactorize(a1.as_ref()).unwrap();
// pc.refactorize(a2.as_ref()).unwrap();Every preconditioner implements the full trait hierarchy:
LinOp<T>—apply,conj_applyPrecond<T>—apply_in_place,conj_apply_in_placeBiLinOp<T>—transpose_apply,adjoint_applyBiPrecond<T>—transpose_apply_in_place,adjoint_apply_in_place
rustc 1.88 (edition 2024 + let-chains).
Dual-licensed under either of
- Apache License, Version 2.0 — LICENSE-APACHE
- MIT license — LICENSE-MIT
at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual-licensed as above, without any additional terms or conditions.