Linear Solvers
Solve Ax = b with dense, sparse, and FEM solvers
Solving linear systems Ax = b is at the heart of numerical computing — from fitting models to simulating physics. Equana offers three solver backends, each suited to different problem sizes and matrix types:
- NumWasm — dense direct solvers (LU, QR, Cholesky, least-squares) for small-to-moderate systems
- SciWasm — sparse iterative solvers (GMRES, BiCGSTAB, PCG, LSQR) for large sparse systems
- mfemwasm — object-oriented FEM solvers with preconditioner support for finite element problems
This tutorial walks through each backend, explains when to use which solver, and provides runnable examples. Run the cells in order — variables persist across the notebook.
For sparse matrix creation and manipulation, see the Sparse Matrices tutorial. For dense matrix basics, see the Working with Matrices tutorial.
Choosing the Right Solver
The right solver depends on your matrix properties and problem size:
| Matrix Properties | Recommended Solver | Backend |
|---|---|---|
| Dense, small-to-moderate (n < 5000) | A \ b or linsolve(A, b) | NumWasm |
| Dense, overdetermined (m > n) | lstsq(A, b) | NumWasm |
| Dense, SPD, need factorization | chol(A) then back-substitution | NumWasm |
| Sparse, symmetric positive definite | pcg(A, b) | SciWasm |
| Sparse, general nonsymmetric | gmres(A, b) or bicgstab(A, b) | SciWasm |
| Sparse, rectangular / least-squares | lsqr(A, b) | SciWasm |
| FEM system with preconditioner | mfem.linear.CGSolver etc. | mfemwasm |
Direct vs. iterative: Direct solvers (LU, QR, Cholesky) compute an exact answer in a fixed number of operations — O(n³) for dense matrices. Iterative solvers approximate the solution through successive refinement and are much faster for large sparse systems where most entries are zero.
Dense Direct Solvers (NumWasm)
Dense solvers compute the exact solution (up to floating-point precision) by factoring the matrix. They are best for systems up to a few thousand unknowns.
Backslash Operator and linsolve
The backslash operator A \ b, linsolve(A, b), and mldivide(A, b) are three equivalent ways to solve Ax = b directly:
# Create a 3x3 system
A = [2, 1, -1; -3, -1, 2; -2, 1, 2]
b = [8; -11; -3]
# Three equivalent ways to solve Ax = b
x1 = A \ b
x2 = linsolve(A, b)
x3 = mldivide(A, b)
# All give the same answer
x1
# Verify: A*x should equal b
norm(A * x1 - b)LU Decomposition
LU decomposition factors A into a lower triangular matrix L, an upper triangular matrix U, and a permutation matrix P such that P·A = L·U. Once factored, solving for different right-hand sides is cheap (forward and back substitution):
# LU decomposition with partial pivoting
(L, U, P) = lu(A)
# Verify the factorization: P*A = L*U
norm(P * A - L * U)QR Decomposition
QR decomposes A into an orthogonal matrix Q (where Q'·Q = I) and an upper triangular matrix R. QR is numerically more stable than LU and is the basis for least-squares solvers:
# QR decomposition: A = Q*R
(Q, R) = qr(A)
# Q is orthogonal: Q'*Q should be the identity
Q' * Q
# Verify: A = Q*R
norm(A - Q * R)Cholesky Factorization
For symmetric positive definite (SPD) matrices, Cholesky computes an upper triangular R such that R'·R = A. It requires half the work of LU and guarantees numerical stability:
# Create a symmetric positive definite matrix
B = [4, 2, 1; 2, 5, 3; 1, 3, 6]
# Cholesky: B = R'*R
R = chol(B)
# Verify the factorization
norm(R' * R - B)Least-Squares Solutions
When a system is overdetermined (more equations than unknowns, m > n), no exact solution exists. lstsq(A, b) finds the x that minimizes ||Ax - b||₂. With multiple outputs, it also returns residuals, rank, and singular values:
# Overdetermined system: 5 equations, 3 unknowns
A_tall = [1, 0, 0; 0, 1, 0; 0, 0, 1; 1, 1, 0; 0, 1, 1]
b_tall = [1; 2; 3; 3.1; 4.9]
# Least-squares: minimize ||A*x - b||
x_ls = lstsq(A_tall, b_tall)
# Residual — how far A*x is from b
r = A_tall * x_ls - b_tall
norm(r)# Multi-output form: also get residuals, rank, singular values
(x_ls2, residuals, rnk, s) = lstsq(A_tall, b_tall)
rnk # matrix rank
s # singular valuesSparse Iterative Solvers (SciWasm)
For large sparse systems, direct solvers become impractical — a 10,000×10,000 dense LU costs ~667 billion operations. Iterative solvers approximate the solution through successive refinement, exploiting the sparsity to perform each iteration in O(nnz) time.
Available Solvers
| Solver | Best For | Requires |
|---|---|---|
gmres(A, b) | General nonsymmetric systems | Nothing special |
bicgstab(A, b) | General nonsymmetric systems | Nothing special |
pcg(A, b) | Symmetric positive definite (SPD) | A must be SPD |
lsqr(A, b) | Rectangular / least-squares | Nothing special |
cgs(A, b) | General nonsymmetric systems | Nothing special |
bicg(A, b) | General nonsymmetric (uses A') | Nothing special |
minres(A, b) | Symmetric indefinite | A must be symmetric |
tfqmr(A, b) | General nonsymmetric (transpose-free) | Nothing special |
All solvers support optional tolerance and iteration-limit parameters. With multiple outputs, they return convergence diagnostics:
(x, flag, relres, iter) = solver(A, b, ...)
| Flag | Meaning |
|---|---|
| 0 | Converged to the requested tolerance |
| 1 | Did not converge within the maximum iterations |
Setting Up a Test Problem
We build a classic tridiagonal Laplacian — symmetric positive definite and well-suited for benchmarking:
# Build a 100x100 SPD tridiagonal Laplacian: [-1, 2, -1]
n = 100
e = ones(n, 1)
T = spdiags([-e, 2*e, -e], [-1, 0, 1], n, n)
# Random right-hand side
b_sp = rand(n, 1)
# Matrix properties
issparse(T) # 1
nnz(T) # 298 (3n - 2)GMRES — General Systems
GMRES (Generalized Minimum Residual) works for any square nonsingular matrix. The optional restart parameter limits memory usage by restarting the Krylov subspace after restart iterations:
# Solve with GMRES (restart=30, tol=1e-10, maxit=200)
(x_gm, flag_gm, relres_gm, iter_gm) = gmres(T, b_sp, 30, 1e-10, 200)
flag_gm # 0 = converged
relres_gm # relative residual
iter_gm # iterations usedBiCGSTAB — Nonsymmetric Systems
BiCGSTAB (Bi-Conjugate Gradient Stabilized) is another choice for general nonsymmetric systems. It uses less memory than GMRES (no restart needed) and often has smoother convergence:
# Solve with BiCGSTAB
(x_bi, flag_bi, relres_bi, iter_bi) = bicgstab(T, b_sp, 1e-10, 200)
flag_bi # 0 = converged
relres_bi
iter_biPCG — Symmetric Positive Definite Systems
Preconditioned Conjugate Gradient is the most efficient solver when the matrix is symmetric and positive definite (SPD). It uses half the work per iteration of BiCGSTAB and is guaranteed to converge for SPD matrices. Many physical problems — diffusion, elasticity, Poisson — produce SPD systems:
# PCG — best choice when A is SPD
(x_cg, flag_cg, relres_cg, iter_cg) = pcg(T, b_sp, 1e-10, 200)
flag_cg # 0 = converged
relres_cg
iter_cg
# Verify the solution
norm(full(T * x_cg) - b_sp)LSQR — Least-Squares Problems
lsqr solves overdetermined or underdetermined sparse systems in the least-squares sense (minimizes ||Ax - b||₂). It works on rectangular sparse matrices where direct methods and square solvers cannot be applied:
# LSQR for a square system (also works on rectangular)
(x_lsqr, flag_lsqr) = lsqr(T, b_sp, 1e-10, 200)
flag_lsqr # 0 = convergedCGS — Conjugate Gradient Squared
CGS is another Krylov method for general nonsymmetric systems. It converges faster than BiCGSTAB in some cases but can have irregular convergence behavior:
# Solve with CGS
(x_cgs, flag_cgs, relres_cgs, iter_cgs) = cgs(T, b_sp, 1e-10, 200)
flag_cgs
iter_cgsSparse Direct Solvers
For moderate-sized sparse systems where iterative convergence is problematic, sparse direct solvers factorize the matrix. On desktop, Equana uses SuiteSparse (UMFPACK/CHOLMOD) for native-speed factorization:
# Sparse Cholesky factorization (A must be SPD)
L = spchol(T)
# Sparse LU factorization with pivoting
(L_lu, U_lu, p_lu) = splu(T)
# Direct solve: automatically selects LU or Cholesky
x_direct = spsolve(T, b_sp)
norm(full(T * x_direct) - b_sp)MFEM Solvers
For finite element problems, MFEM provides an object-oriented solver framework. You create a solver object, configure tolerances and iteration limits, optionally attach a preconditioner, then solve. The solver operates on the assembled system matrix and vectors.
Setting Up a FEM Test Problem
We build a small 2D Poisson problem (-∇²u = 1 with u = 0 on boundary) as a test system:
# Create a 4x4 quad mesh on the unit square
mesh = mfem.mesh.makeCartesian2D(4, 4)
# Linear H1 finite element space (order 1)
fespace = mfem.fespace.createH1(mesh, 1, 1)
ndofs = fespace.getVDim() * fespace.getNDofs()
println("DOFs: ${ndofs}")
# Bilinear form: a(u,v) = ∫ ∇u·∇v dx
a = mfem.bilinearForms.create(fespace)
mfem.bilinearForms.addDiffusionIntegrator(a)
a.assemble()
# Linear form: L(v) = ∫ 1·v dx
f = mfem.coefficients.constant(1.0)
b_fem = mfem.linearForms.create(fespace)
mfem.linearForms.addDomainIntegrator(b_fem, f)
b_fem.assemble()
# Grid function for the solution (initialized to zero)
x_fem = mfem.gridfunc.create(fespace)
mfem.gridfunc.projectConstant(x_fem, 0.0)
# Apply homogeneous Dirichlet BCs on all boundaries
ess_tdof_list = fespace.getBoundaryDofs()
mfem.boundaryConditions.applyEssentialBC(a, ess_tdof_list, x_fem, b_fem)
a.finalize()
println("System assembled and BCs applied")CG Solver (Object-Oriented API)
Create a CG solver, configure it, set the operator (system matrix), and call mult(b, x) to solve. After solving, query iteration count and residual:
# Create CG solver
cg = mfem.linear.CGSolver.create()
# Configure solver
cg.setRelTol(1e-10)
cg.setAbsTol(0.0)
cg.setMaxIter(200)
cg.setPrintLevel(1)
# Set the system matrix
A_ptr = a.getSystemMatrixPointer()
cg.setOperator(A_ptr)
# Solve: find x such that A*x = b
cg.mult(b_fem.getVectorPointer(), x_fem.getPointer())
# Check convergence
num_iters = cg.getNumIterations()
converged = cg.getConverged()
final_norm = cg.getFinalNorm()
println("CG iterations: ${num_iters}")
println("Converged: ${converged}")
println("Final norm: ${final_norm}")
# Visualize the CG solution
vtk.colormap("viridis")
vtk.edges("on")
vtk.gridfunction(mesh, x_fem)
title("Poisson Solution (CG)")GMRES Solver
For nonsymmetric FEM systems (e.g., advection-diffusion), use GMRES. The kdim parameter controls the Krylov subspace dimension (restart value):
# Reset the solution
mfem.gridfunc.projectConstant(x_fem, 0.0)
# Create GMRES solver with restart dimension 50
gm = mfem.linear.GMRESSolver.create(50)
gm.setRelTol(1e-10)
gm.setAbsTol(0.0)
gm.setMaxIter(200)
gm.setPrintLevel(1)
# Set operator and solve
gm.setOperator(A_ptr)
gm.mult(b_fem.getVectorPointer(), x_fem.getPointer())
println("GMRES iterations: ${gm.getNumIterations()}")
println("Converged: ${gm.getConverged()}")
# Solution statistics
sol_data = x_fem.getData()
println("Solution range: [${min(sol_data)}, ${max(sol_data)}]")
# Visualize the GMRES solution
vtk.colormap("viridis")
vtk.edges("on")
vtk.gridfunction(mesh, x_fem)
title("Poisson Solution (GMRES)")Available MFEM Solver and Preconditioner Classes
Iterative Solvers:
| Class | Method | Best For |
|---|---|---|
mfem.linear.CGSolver | Conjugate Gradient | SPD systems |
mfem.linear.GMRESSolver(kdim) | GMRES | General nonsymmetric |
mfem.linear.FGMRESSolver(kdim) | Flexible GMRES | Variable preconditioner |
mfem.linear.BiCGSTABSolver | BiCGSTAB | Nonsymmetric, low memory |
mfem.linear.MINRESSolver | MINRES | Symmetric indefinite |
mfem.linear.SLISolver | Stationary iteration | Simple smoothing |
Direct Solvers:
| Class | Description |
|---|---|
mfem.linear.UMFPackSolver | UMFPACK sparse direct solver |
mfem.linear.KLUSolver | KLU sparse direct solver |
Preconditioners:
| Class | Description |
|---|---|
mfem.linear.JacobiSmoother | Diagonal (Jacobi) scaling |
mfem.linear.GSSmoother | Gauss-Seidel (symmetric, forward, or backward) |
mfem.linear.DSmoother | Diagonal smoother |
mfem.linear.ChebyshevSmoother | Polynomial acceleration |
mfem.linear.BlockILU | Block incomplete LU |
Solver Configuration Methods:
| Method | Description |
|---|---|
solver.setRelTol(rtol) | Relative convergence tolerance |
solver.setAbsTol(atol) | Absolute convergence tolerance |
solver.setMaxIter(n) | Maximum number of iterations |
solver.setPrintLevel(level) | Verbosity (0 = silent, 1 = summary) |
solver.setOperator(A) | Set the system matrix |
solver.setPreconditioner(P) | Attach a preconditioner |
solver.mult(b, x) | Solve Ax = b |
solver.getNumIterations() | Iterations used |
solver.getConverged() | 1 if converged, 0 otherwise |
solver.getFinalNorm() | Final residual norm |
Quick Reference
Dense Solvers (NumWasm)
| Function | Description |
|---|---|
A \ b / linsolve(A, b) / mldivide(A, b) | Solve Ax = b directly |
lstsq(A, b) | Least-squares solution (overdetermined systems) |
(L, U, P) = lu(A) | LU decomposition with partial pivoting |
(Q, R) = qr(A) | QR decomposition |
R = chol(A) | Cholesky factorization (SPD matrices) |
Sparse Iterative Solvers (SciWasm)
| Function | System Type | Key Parameter |
|---|---|---|
gmres(A, b, restart, tol, maxit) | General | restart controls memory |
bicgstab(A, b, tol, maxit) | General | Less memory than GMRES |
pcg(A, b, tol, maxit) | SPD only | Fastest for SPD |
lsqr(A, b, tol, maxit) | Rectangular | Least-squares |
cgs(A, b, tol, maxit) | General | Fast, irregular convergence |
bicg(A, b, tol, maxit) | General | Requires A' |
minres(A, b, tol, maxit) | Symmetric | Works for indefinite |
tfqmr(A, b, tol, maxit) | General | Transpose-free |
Sparse Direct Solvers
| Function | Description |
|---|---|
spsolve(A, b) | Auto-select LU or Cholesky and solve |
spchol(A) | Sparse Cholesky factorization (SPD) |
(L, U, p) = splu(A) | Sparse LU with pivoting |
sprank(A) | Structural rank |
MFEM Solvers
| Class | Method |
|---|---|
mfem.linear.CGSolver | Conjugate Gradient (SPD) |
mfem.linear.GMRESSolver(kdim) | GMRES (general) |
mfem.linear.FGMRESSolver(kdim) | Flexible GMRES |
mfem.linear.BiCGSTABSolver | BiCGSTAB |
mfem.linear.MINRESSolver | MINRES (symmetric indefinite) |
mfem.linear.UMFPackSolver | Direct (UMFPACK) |
mfem.linear.KLUSolver | Direct (KLU) |
Summary
This tutorial covered all three solver backends in Equana:
- Dense direct solvers (NumWasm) —
linsolve,mldivide, the backslash operatorA \ b, plus LU, QR, and Cholesky factorizations, andlstsqfor overdetermined systems - Sparse iterative solvers (SciWasm) —
gmres,bicgstab,pcg,lsqr,cgs,bicg,minres, andtfqmrwith convergence diagnostics (flag,relres,iter) - Sparse direct solvers —
spsolve,spchol,splufor moderate-sized sparse systems - MFEM solvers — object-oriented CG, GMRES, and other Krylov solvers with configurable tolerances, preconditioners, and iteration limits for finite element systems
Next Steps
- Explore the Sparse Matrices tutorial for creating and manipulating sparse matrices
- Try the Finite Element Method tutorial for a complete FEM workflow with solvers
- See the Linear Elasticity tutorial for solving structural problems with CG