Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

 Open In Colab

The Intuition

Stationary methods are a class of iterative solvers that repeatedly apply the exact same mathematical operation at every iteration.

What does it mean for a vector x\mathbf{x} to satisfy Ax=b\mathbf{A}\mathbf{x}=\mathbf{b}? It means that all the elements in x\mathbf{x} are perfectly consistent with each other. If we guess a solution where one element is inconsistent, we can algebraically solve for it based on the other elements!

The Algorithm:

  1. Start with a random guess for x\mathbf{x}.

  2. For each iterative step, identify a single element in x\mathbf{x} and force it to be consistent with the others.

  3. Check the magnitude of the residual (how well the new guess solves the system).

  4. Repeat until the residual falls below a specified tolerance.

Jacobi Iteration

Let’s formalize this by writing out the linear system Ax=b\mathbf{A}\mathbf{x}=\mathbf{b} in index form:

∑jAijxj=bi\begin{aligned} \sum_{j} A_{ij} x_j &= b_i \\ \end{aligned}

If we extract the ii-th row, we can isolate and solve for the diagonal term xix_i:

Aiixi(k+1)+∑j≠iAijxj(k)=bixi(k+1)=1Aii[bi−∑j≠iAijxj(k)]\begin{aligned} A_{ii}x_i^{(k+1)} + \sum_{j \ne i} A_{ij} x_j^{(k)} &= b_i \\ x_i^{(k+1)} &= \frac{1}{A_{ii}}\left[b_i-\sum_{j \ne i} A_{ij} x_j^{(k)}\right] \end{aligned}

This equation explicitly updates xi(k+1)x_i^{(k+1)} to be consistent with all the other elements from the previous iteration x(k)\mathbf{x}^{(k)}.

Matrix Formulation

Since we proceed row-by-row, we can vectorize this operation by pulling apart the coefficient matrix A\mathbf{A} into its diagonal D\mathbf{D}, strictly lower triangular L\mathbf{L}, and strictly upper triangular U\mathbf{U} components:

A=L+D+U\mathbf{A} = \mathbf{L} + \mathbf{D} + \mathbf{U}

NB: This is not a matrix decomposition; we are literally just pulling it apart (blank elements are zeros):

[a11a12a13a21a22a23a31a32a33]=[a21a22a23]+[a11a22a33]+[a12a13a23]\begin{bmatrix} a_{11} & a_{12} & a_{13} \\ a_{21} & a_{22} & a_{23} \\ a_{31} & a_{32} & a_{33} \end{bmatrix} = \begin{bmatrix} & & \\ a_{21} & & \\ a_{22} & a_{23} & \end{bmatrix} + \begin{bmatrix} a_{11} & & \\ & a_{22} & \\ & & a_{33} \end{bmatrix} + \begin{bmatrix} & a_{12} & a_{13} \\ & & a_{23} \\ & & \end{bmatrix}

The algorithm becomes:

Dxk+1=b−[L+U]xkxk+1=D−1[b−[L+U]xk]\begin{align} D x^{k+1} &= b-[L+U]x^k \\ x^{k+1} &= D^{-1} \big[b-[L+U]x^k\big] \end{align}

The Jacobi algorithm elegantly becomes:

Dx(k+1)=b−[L+U]x(k)x(k+1)=D−1[b−[L+U]x(k)]\begin{aligned} \mathbf{D} \mathbf{x}^{(k+1)} &= \mathbf{b} - [\mathbf{L}+\mathbf{U}]\mathbf{x}^{(k)} \\ \mathbf{x}^{(k+1)} &= \mathbf{D}^{-1} \big[\mathbf{b} - [\mathbf{L}+\mathbf{U}]\mathbf{x}^{(k)}\big] \end{aligned}

(Note: Because the new vector x(k+1)\mathbf{x}^{(k+1)} requires the entire previous vector x(k)\mathbf{x}^{(k)}, we must store both in memory. However, because each element update is independent of the others in the same step, Jacobi iteration is incredibly easy to parallelize!)

Jacobi iterations are guaranteed to converge if the matrix A\mathbf{A} is diagonally dominant.

Gauss-Seidel

Gauss-Seidel operates on the exact same premise as Jacobi, but with one critical optimization: as soon as we calculate a new element xi(k+1)x_i^{(k+1)}, we immediately use it to calculate the next elements in the same iteration!

If we move downwards through the rows, the ii-th element is updated using the already-updated rows above it, and the old rows below it:

xi(k+1)=1Aii[bi−∑j=1i−1Aijxj(k+1)−∑j=i+1nAijxj(k)]\begin{aligned} x_i^{(k+1)} &= \frac{1}{A_{ii}}\left[b_i-\sum_{j=1}^{i-1} A_{ij} x_j^{(k+1)} -\sum_{j=i+1}^n A_{ij} x_j^{(k)}\right] \end{aligned}

Matrix Formulation

Written in matrix form, we simply move the lower triangular matrix L\mathbf{L} to the left side since those elements are already updated:

[D+L]x(k+1)=b−Ux(k)\begin{aligned} [\mathbf{D} + \mathbf{L}] \mathbf{x}^{(k+1)} &= \mathbf{b} - \mathbf{U}\mathbf{x}^{(k)} \end{aligned}

Because we are modifying x\mathbf{x} in-place as we go, we only need to store a single vector in memory!

Gauss-Seidel is guaranteed to converge if the matrix is diagonally dominant or symmetric positive definite.

(Note: While Gauss-Seidel is vastly more memory-efficient and generally converges faster than Jacobi, the in-place updates mean each calculation depends on the previous one, making it extremely difficult to parallelize.)

Successive Over Relaxation (SOR)

The convergence of Gauss-Seidel can be accelerated through relaxation. Instead of simply accepting the new xi(k+1)x_i^{(k+1)} value, we take a weighted average of the newly calculated value and the old value using a relaxation parameter ω\omega:

xi←ωAii[bi−∑j≠iAijxj]+(1−ω)xi\begin{aligned} x_i &\leftarrow \frac{\omega}{A_{ii}}\left[b_i-\sum_{j \ne i} A_{ij} x_j \right] + (1-\omega) x_i \end{aligned}
  • If ω=1\omega = 1: The method is exactly Gauss-Seidel.

  • If ω<1\omega < 1: The method is under-relaxed (converges more smoothly but slower).

  • If ω>1\omega > 1: The method is over-relaxed (convergence is artificially accelerated).

Choosing the Relaxation Parameter

SOR with a well-chosen ω\omega will drastically outperform Gauss-Seidel. However, finding the optimal ω\omega dynamically is non-trivial.

One heuristic approach involves monitoring the difference between successive iterations Δx(k)=∣∣x(k)−x(k−1)∣∣\Delta \mathbf{x}^{(k)} = ||\mathbf{x}^{(k)}-\mathbf{x}^{(k-1)}||. After pp iterations, you can estimate the optimal relaxation factor as:

ωopt≈21+1+[Δx(k+p)Δx(k)]p\omega_{opt} \approx \frac{2}{1+\sqrt{1+\left[ \frac{\Delta \mathbf{x}^{(k+p)}}{\Delta \mathbf{x}^{(k)}} \right]^p }}

Performance Benchmarking

How do these stationary methods stack up against a highly optimized direct solver like scipy.linalg.solve?

Let’s generate large diagonally-dominant matrices and time the execution of our iterative loops versus the direct solver. Try changing the ‘n’ field in the code below!