Equana

Back to Tutorials

Linear Solvers

Solve Ax = b with dense, sparse, and FEM solvers

Run AllReset

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:

  1. NumWasm — dense direct solvers (LU, QR, Cholesky, least-squares) for small-to-moderate systems
  2. SciWasm — sparse iterative solvers (GMRES, BiCGSTAB, PCG, LSQR) for large sparse systems
  3. 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 PropertiesRecommended SolverBackend
Dense, small-to-moderate (n < 5000)A \ b or linsolve(A, b)NumWasm
Dense, overdetermined (m > n)lstsq(A, b)NumWasm
Dense, SPD, need factorizationchol(A) then back-substitutionNumWasm
Sparse, symmetric positive definitepcg(A, b)SciWasm
Sparse, general nonsymmetricgmres(A, b) or bicgstab(A, b)SciWasm
Sparse, rectangular / least-squareslsqr(A, b)SciWasm
FEM system with preconditionermfem.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:

Code [1]Run
# 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):

Code [2]Run
# 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:

Code [3]Run
# 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:

Code [4]Run
# 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:

Code [5]Run
# 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)
Code [6]Run
# Multi-output form: also get residuals, rank, singular values
(x_ls2, residuals, rnk, s) = lstsq(A_tall, b_tall)
rnk    # matrix rank
s      # singular values

Sparse 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

SolverBest ForRequires
gmres(A, b)General nonsymmetric systemsNothing special
bicgstab(A, b)General nonsymmetric systemsNothing special
pcg(A, b)Symmetric positive definite (SPD)A must be SPD
lsqr(A, b)Rectangular / least-squaresNothing special
cgs(A, b)General nonsymmetric systemsNothing special
bicg(A, b)General nonsymmetric (uses A')Nothing special
minres(A, b)Symmetric indefiniteA 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, ...)

FlagMeaning
0Converged to the requested tolerance
1Did 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:

Code [7]Run
# 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:

Code [8]Run
# 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 used

BiCGSTAB — 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:

Code [9]Run
# 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_bi

PCG — 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:

Code [10]Run
# 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:

Code [11]Run
# LSQR for a square system (also works on rectangular)
(x_lsqr, flag_lsqr) = lsqr(T, b_sp, 1e-10, 200)
flag_lsqr     # 0 = converged

CGS — 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:

Code [12]Run
# Solve with CGS
(x_cgs, flag_cgs, relres_cgs, iter_cgs) = cgs(T, b_sp, 1e-10, 200)
flag_cgs
iter_cgs

Sparse 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:

Code [13]Run
# 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:

Code [14]Run
# 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:

Code [15]Run
# 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):

Code [16]Run
# 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:

ClassMethodBest For
mfem.linear.CGSolverConjugate GradientSPD systems
mfem.linear.GMRESSolver(kdim)GMRESGeneral nonsymmetric
mfem.linear.FGMRESSolver(kdim)Flexible GMRESVariable preconditioner
mfem.linear.BiCGSTABSolverBiCGSTABNonsymmetric, low memory
mfem.linear.MINRESSolverMINRESSymmetric indefinite
mfem.linear.SLISolverStationary iterationSimple smoothing

Direct Solvers:

ClassDescription
mfem.linear.UMFPackSolverUMFPACK sparse direct solver
mfem.linear.KLUSolverKLU sparse direct solver

Preconditioners:

ClassDescription
mfem.linear.JacobiSmootherDiagonal (Jacobi) scaling
mfem.linear.GSSmootherGauss-Seidel (symmetric, forward, or backward)
mfem.linear.DSmootherDiagonal smoother
mfem.linear.ChebyshevSmootherPolynomial acceleration
mfem.linear.BlockILUBlock incomplete LU

Solver Configuration Methods:

MethodDescription
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)

FunctionDescription
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)

FunctionSystem TypeKey Parameter
gmres(A, b, restart, tol, maxit)Generalrestart controls memory
bicgstab(A, b, tol, maxit)GeneralLess memory than GMRES
pcg(A, b, tol, maxit)SPD onlyFastest for SPD
lsqr(A, b, tol, maxit)RectangularLeast-squares
cgs(A, b, tol, maxit)GeneralFast, irregular convergence
bicg(A, b, tol, maxit)GeneralRequires A'
minres(A, b, tol, maxit)SymmetricWorks for indefinite
tfqmr(A, b, tol, maxit)GeneralTranspose-free

Sparse Direct Solvers

FunctionDescription
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

ClassMethod
mfem.linear.CGSolverConjugate Gradient (SPD)
mfem.linear.GMRESSolver(kdim)GMRES (general)
mfem.linear.FGMRESSolver(kdim)Flexible GMRES
mfem.linear.BiCGSTABSolverBiCGSTAB
mfem.linear.MINRESSolverMINRES (symmetric indefinite)
mfem.linear.UMFPackSolverDirect (UMFPACK)
mfem.linear.KLUSolverDirect (KLU)

Summary

This tutorial covered all three solver backends in Equana:

  • Dense direct solvers (NumWasm) — linsolve, mldivide, the backslash operator A \ b, plus LU, QR, and Cholesky factorizations, and lstsq for overdetermined systems
  • Sparse iterative solvers (SciWasm) — gmres, bicgstab, pcg, lsqr, cgs, bicg, minres, and tfqmr with convergence diagnostics (flag, relres, iter)
  • Sparse direct solversspsolve, spchol, splu for 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

Workbench

Clear
No variables in workbench

Next Steps