faer-precond
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, ...).
Preconditioners
| 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, ...) |
✅ |
Choosing a preconditioner
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.
Design contract
- 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.
Install
[]
= "0.2"
= "0.24"
= "0.13"
Quick start
Point-Jacobi
use MemStack;
use ;
use Precond;
use JacobiPrecond;
let pc = try_from_diagonal.unwrap;
let mut x = mat!;
pc.apply_in_place;
// x is now [2.0, 3.0, 2.0]
Block-Jacobi
use MemStack;
use ;
use Precond;
use BlockJacobiPrecond;
// 5x5 matrix; two diagonal blocks of size 2 and 3.
let a = mat!;
let pc = try_new.unwrap;
let mut x = mat!;
pc.apply_in_place;
ILU(0)
use MemStack;
use ;
use ;
use Precond;
use Ilu0;
// 5x5 tridiagonal SPD: diag 4, off -1 (no fill — ILU(0) is exact).
let mut triplets = Vecnew;
for i in 0..5
let a = try_new_from_triplets.unwrap;
let pc = try_new.expect;
let mut b = mat!;
pc.apply_in_place;
IC(0)
use MemStack;
use ;
use ;
use Precond;
use Ic0;
let mut triplets = Vecnew;
for i in 0..5
let a = try_new_from_triplets.unwrap;
// Hermitian PD input — IC(0) silently ignores the strict upper triangle.
let pc = try_new.expect;
let mut b = mat!;
pc.apply_in_place;
ILUTP (threshold + partial pivoting)
use ;
use ;
use ;
use Precond;
use ;
// A non-symmetric tridiagonal: diag 4, sub -2, super -1.
let mut triplets = Vecnew;
for i in 0..5usize
let a = try_new_from_triplets.unwrap;
// Tune the drop tolerance, fill budget, and pivot aggressiveness.
let params = IlutpParams ;
let pc = try_new_with_params.unwrap;
// Apply needs a length-n scratch column (it may apply the pivot permutation).
let mut b = mat!;
let mut buf = new;
pc.apply_in_place;
Wrapping a faer factorisation as a preconditioner
use MemStack;
use ;
use Llt;
use Precond;
use SolvePrecond;
let a = mat!;
let llt = new.expect;
let pc = new;
let mut x = mat!;
pc.apply_in_place;
// x is now [1/11, 7/11]
Repeated factorisation (nonlinear Krylov)
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 ;
use ;
# let triplets: = .map.collect;
# let a0 = try_new_from_triplets.unwrap;
let symbolic = try_new.unwrap;
let mut pc = new_with_symbolic;
// Hot loop — no allocation:
pc.refactorize.unwrap;
// pc.refactorize(a1.as_ref()).unwrap();
// pc.refactorize(a2.as_ref()).unwrap();
Trait coverage
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
Minimum supported Rust version
rustc 1.88 (edition 2024 + let-chains).
License
Dual-licensed under either of
- Apache License, Version 2.0 — LICENSE-APACHE
- MIT license — LICENSE-MIT
at your option.
Contribution
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.