Translated from the author's Korean original. Read the original (한국어)
Choosing a CFD Linear Solver: Comparing Convergence Speeds from Jacobi to BiCGSTAB
CG finishes in N steps only while the matrix stays SPD. Once advection breaks the symmetry, the solver name has to change.
In 1952 Lanczos, Hestenes, and Stiefel walked into the same meeting with the same algorithm, written apart. Symmetric positive-definite (SPD) matrices, finished in iterations. The name that stuck is conjugate gradient (CG). Seventy years later the pressure equation in a CFD code still hunts for its answer in that subspace.
Drag grid size N from 20 to 80 and the four residual curves are drawn again from scratch. Raise the iteration cap and the order in which they hit the floor changes.
Near N=40, CG kinks downward while Jacobi lies almost flat on the log axis. Push N to 80 and the gap widens. The teeth on BiCGSTAB are a residual that will not die smoothly.
Why CFD Solves Linear Systems Iteratively#
Discretize Navier–Stokes and every step drops . is the coefficient matrix, the cell-centered unknowns, the right-hand side. A million cells makes a sparse matrix. LU factorization, the split into lower and upper triangular factors, burns memory and time together at that size. OpenFOAM, SU2, and Gerris iterate.
Start from a guess and keep editing until the residual drops under the tolerance. The definition of "a little" is the solver's name.
The Idea of the Krylov Subspace#
Repeated products of with the initial residual fill a space.
is the Krylov subspace, its dimension, the set of linear combinations. The update stays in , and the leftover has to meet an optimality condition.
The only required operation is a matrix-vector product, so the sparsity pattern survives. CG minimizes the A-norm error in that space. GMRES minimizes the 2-norm residual. The second one happens in the subspace Arnoldi builds by stacking vectors.
Differences in Solver Characteristics#
| Solver | Required Matrix | Memory | Advantages | Disadvantages |
|---|---|---|---|---|
| Jacobi | Diagonal dominance recommended | Very low | Easy to parallelize, 10 lines of implementation | Slow (spectral radius ) |
| Gauss-Seidel | Arbitrary | Very low | About twice as fast as Jacobi | Inherently sequential, disadvantageous for GPU |
| CG | SPD required | 4 vectors | Optimal for SPD, lightweight with 3-term recurrence | Cannot handle non-symmetric matrices |
| BiCGSTAB | Arbitrary | 7 vectors | Non-symmetric OK, transpose not required | Fluctuating convergence, risk of breakdown |
The pressure Poisson problem is SPD, so CG fits. Momentum with advection is nonsymmetric, so the choice moves to BiCGSTAB or GMRES. OpenFOAM's PCG/PBiCGStab and SU2's BCGSTAB are that split, written into the option names.
Convergence Speed with Python#
The model problem is 1D Poisson, , with . Finite differences leave a tridiagonal matrix. At , Jacobi and CG share the tolerance .
import numpy as np
def build_poisson_1d(n):
A = np.zeros((n, n))
for i in range(n):
A[i, i] = 2.0
if i > 0: A[i, i - 1] = -1.0
if i < n - 1: A[i, i + 1] = -1.0
return A, np.ones(n)
def jacobi_sweep(A, b, tol=1e-10, max_iter=2000):
D = np.diag(A)
LU = A - np.diag(D)
x = np.zeros_like(b)
b0 = np.linalg.norm(b)
history = []
for _ in range(max_iter):
x = (b - LU @ x) / D
res = np.linalg.norm(b - A @ x) / b0
history.append(res)
if res < tol:
break
return x, history
def cg_sweep(A, b, tol=1e-10, max_iter=2000):
x = np.zeros_like(b)
r = b - A @ x
p = r.copy()
rr = r @ r
b0 = np.linalg.norm(b)
history = [np.sqrt(rr) / b0]
for _ in range(max_iter):
Ap = A @ p
alpha = rr / (p @ Ap)
x += alpha * p
r -= alpha * Ap
rr_new = r @ r
history.append(np.sqrt(rr_new) / b0)
if history[-1] < tol:
break
p = r + (rr_new / rr) * p
rr = rr_new
return x, history
A, b = build_poisson_1d(n=60)
_, hist_j = jacobi_sweep(A, b)
_, hist_cg = cg_sweep(A, b)
print(f"Jacobi: {len(hist_j):4d} iterations, final {hist_j[-1]:.2e}")
print(f"CG: {len(hist_cg):4d} iterations, final {hist_cg[-1]:.2e}")The run prints this.
Jacobi: 2000 iterations, final 2.11e-02
CG: 60 iterations, final 1.87e-13Raise the log-condition slider and the four curves stall in a different place inside one iteration budget. The iteration cap decides where the plot is cut.
조건수 κ를 키우면 (그림에서 오른쪽으로) Jacobi는 거의 멈추고 CG/GMRES는 √κ 한계를 따른다. BiCGSTAB는 비대칭에 강하지만 잔차가 출렁인다.
At the default and about 80 iterations, Jacobi (red) is nearly horizontal and CG (cyan) is steeper. Push toward and Jacobi still has no floor at 200 iterations. The printed case is one point on that picture: Jacobi at 2.11e-2 after 2000 sweeps, CG at 1.87e-13 after 60.
Turn the printed residual into a per-step factor. From 1 down to 2.11e-2 in 2000 sweeps is a contraction of 0.998 per sweep, the 2000th root of 0.0211. That is the scale of the spectral radius in the table. At , is 1/3600, about 0.00028, and 0.998 is that term with a small constant in front.
Keep the factor and costs about 12,000 iterations. From 2.11e-2, about 10,000 sweeps are still missing. Double and falls by four, so the same residual costs four times as many iterations.
CG's 60 iterations equal , so the ceiling of at most steps is used up. 1.87e-13 is about times smaller than 2.11e-2. Double precision adds about to 1, and two or three digits wore off. A million cells at 8 bytes is 8 MB a vector: 32 MB for four CG vectors, 56 MB for seven BiCGSTAB vectors. Storage is cheap beside the matrix. The tail is not.
Bare iterations, preconditioner off#
Those curves are bare iterations. Turn a preconditioner on and one tick on the horizontal axis means something else. A vertical spike in BiCGSTAB is breakdown: an inner product in a denominator has stuck at zero. More iterations do not walk that back. Change the preconditioner, or move to GMRES. A restart period near 30 is the usual compromise between memory and that spike.
Practical Solver Selection Guide#
Pressure Poisson, and heat transfer that is only diffusion: CG with ILU(0). If symmetry is intact, another name just stores more vectors.
Nonsymmetric momentum starts on BiCGSTAB. When the residual wobbles, switch to GMRES(). Here is the restart period.
Gauss-Seidel is sequential, so it goes limp on a GPU. The pattern of the last decade is a Jacobi smoother plus algebraic multigrid (AMG).
SU2 also carries LU-SGS (Lower-Upper Symmetric Gauss-Seidel), a stationary iteration beside Krylov. Tied to implicit time stepping on compressible flow, its memory footprint stays smaller. It is not as sturdy as Krylov.
The preconditioner moves the iteration count more than the solver name does. PCG plus AMG is often 100 times faster than bare CG. That factor is whether the ten-thousand-sweep tail gets cut.
Calling 2.11e-2 a converged residual#
Hand the next time step a Poisson residual that is still 2.11e-2 after 2000 sweeps, and that pressure error walks into the next nonlinear iteration. While the matrix stays SPD, the slot belongs to CG. Once advection tips the matrix nonsymmetric, CG is no longer eligible and the handoff is BiCGSTAB. Picking the name first and measuring the condition number later is the failure.
The open question is whether to stack another ten thousand iterations. A V-cycle that clears the error on a coarse grid may cut this tail, or it may call Jacobi again as the fine-grid smoother. This residual does not answer that.
Related
Share if you found it helpful.