Code
%%julia
using LinearAlgebra
using PlotsMany matrices typically encountered in scientific computing have special structure. It can be very helpful to understand and exploit these special structures!
An \(n \times n\) matrix \(\mathbf{A}\) is strictly (row) diagonally dominant if \[|A_{ii}| > \sum_{j=1//j\not=i}^n |A_{ij}| \text{ for each } i=1, \cdots, n.\] This says that the magnitude of entries on the diagonal are larger than the sum of magnitudes of entries in the same row off-diagonal.
Suppose \(\mathbf{A} \in \mathbb{R}^{n \times n}\) and \(\mathbf{x} \in \mathbb{R}^n\). Note that \(\mathbf{x}^\top \mathbf{A}\mathbf{x}\) is scalar valued – this is called a quadratic form.
This definition is equivalent to assuming that \(\mathbf{A} \in \mathbb{R}^n\) has:
Today we’ll see some classical iterative methods for solving linear systems. Several of these are particular examples of splitting methods.
The first method hinges on the observation that if \(\mathbf{A}\mathbf{x} = \mathbf{b}\) then \[A_{i1}x_1 + A_{i2}x_2 + \cdots + A_{ii}x_i + \cdots + A_{in} x_n = b_i \text{ for } i = 1, \cdots, n.\]
This can be arranged as \[x_i = \frac{1}{A_{ii}} \left[ \sum_{j = 1\\j\not= i}^n (-A_{ij}x_j) + b_i \right] \text{ for } i=1, \cdots, n.\]
Jacobi’s method uses this observation iteratively, forcing the \(i\)th component of the current approximation to satisfy the \(i\)th equation, that is \(\mathbf{x}^{(k)}\) satisfies \[x_i^{(k)} = \frac{1}{A_{ii}} \left[ \sum_{j = 1\\j\not= i}^n (-A_{ij}x_j^{(k-1)}) + b_i \right] \text{ for } i=1, \cdots, n.\]
You should also think about what happens here if the order of the equations is changed or if the initial iterate is somewhere else. Spoiler alert: Jacobi’s method doesn’t always converge!
We can collect all of the updates to the coordinates of \(\mathbf{x}^{(k)}\) together into a vector update.
Note that we can rewrite \(\mathbf{A}\mathbf{x} = \mathbf{b}\) as \[\mathbf{D}(\mathbf{A})\mathbf{x} + \mathbf{L}(\mathbf{A})\mathbf{x} + \mathbf{U}(\mathbf{A})\mathbf{x} = \mathbf{b}\] where \(\mathbf{D}(\mathbf{A})\) is the diagonal matrix containing the diagonal elements of \(\mathbf{A}\), \(\mathbf{L}(\mathbf{A})\) is the strictly lower-triangular matrix containing the strictly lower-triangular elements of \(\mathbf{A}\), and \(\mathbf{U}(\mathbf{A})\) is the strictly upper-triangular matrix containing the strictly upper-triangular elements of \(\mathbf{A}\).
In this notation, the Jacobi iterates satisfy \[\mathbf{x}^{(k)} = -\mathbf{D}(\mathbf{A})^{-1} [\mathbf{L}(\mathbf{A}) + \mathbf{U}(\mathbf{A})] \mathbf{x}^{(k-1)} + \mathbf{D}(\mathbf{A})^{-1} \mathbf{b}.\]
%%julia
using LinearAlgebra
using Plots%%julia
"""
jacobi(A,b,x_0,N)
Run N iterations of Jacobi's method on system Ax=b with initial iterate x_0.
"""
function jacobi(A,b,x_0,N,x_true)
D = Diagonal(A);
L = tril(A,-1);
U = triu(A,1);
x_old = x_0
errs = zeros(N+1)
errs[1] = norm(x_old - x_true)/norm(x_true)
for i in 1:N
x_new = D\(-(L+U)*x_old + b);
errs[i+1] = norm(x_new - x_true)/norm(x_true);
x_old = x_new;
end
return x, errs
endjacobi
Now, we’ll run a small experiment to check that our code works!
%%julia
n = 100
#A = randn(n,n)
A = diagm(ones(n)) + 0.05*randn(n,n)
x_true = ones(n)
b = A*x_true;
x = zeros(n);%%julia
N = 100
x, errs = jacobi(A,b,zeros(n),N,x_true);%%julia
plot(1:N+1,errs,yscale=:log10,label="Jacobi error",xaxis="Iteration (k)", yaxis="Relative error")To prove this theorem, we need Gershgorin’s disc theorem.
We can possibly improve Jacobi’s method by taking a second look at the equation \[x_i^{(k)} = \frac{1}{A_{ii}} \left[ \sum_{j = 1\\j\not= i}^n (-A_{ij}x_j^{(k-1)}) + b_i \right] \text{ for } i=1, \cdots, n.\]
If we think about doing the updates to the entries of \(\mathbf{x}^{(k)}\) sequentially, we see that we update \(x^{(k)}_1\) before updating \(x^{(k)}_2\), but in the update of \(x^{(k)}_2\), we use \(x^{(k-1)}_1\), despite having the (likely better) estimate for the first entry computed in \(x^{(k)}_1\).
This motivates using the most up-to-date estimates available in this update: \[x_i^{(k)} = \frac{1}{A_{ii}} \left[ \sum_{j = 1}^{i-1} (-A_{ij}x_j^{(k)}) \sum_{j = i+1}^{n} (-A_{ij}x_j^{(k-1)})+ b_i \right] \text{ for } i=1, \cdots, n.\]
In the same way we did for Jacobi, we can succinctly write the Gauss-Seidel update in matrix-vector form.
Noting, now, that the update \[x_i^{(k)} = \frac{1}{A_{ii}} \left[ \sum_{j = 1}^{i-1} (-A_{ij}x_j^{(k)}) \sum_{j = i+1}^{n} (-A_{ij}x_j^{(k-1)})+ b_i \right] \text{ for } i=1, \cdots, n\] involves the entire lower triangular part of \(\mathbf{A}\) interacting with entries of \(\mathbf{x}^{(k)}\),
we can rewrite this as \[[\mathbf{D}(\mathbf{A}) + \mathbf{L}(\mathbf{A})]\mathbf{x}^{(k)} + \mathbf{U}(\mathbf{A}) \mathbf{x}^{(k-1)} = \mathbf{b}.\]
Thus, the update is \[\mathbf{x}^{(k)} = -[\mathbf{D}(\mathbf{A}) + \mathbf{L}(\mathbf{A})]^{-1} \mathbf{U}(\mathbf{A}) \mathbf{x}^{(k-1)} + [\mathbf{D}(\mathbf{A}) + \mathbf{L}(\mathbf{A})]^{-1} \mathbf{b}.\]
To prove this theorem, we need the Householder-John theorem.
%%julia
"""
gaussseidel(A,b,x_0,N)
Run N iterations of the Gauss-Seidel method on system Ax=b with initial iterate x_0.
"""
function gaussseidel(A,b,x_0,N,x_true)
D = Diagonal(A);
L = tril(A,-1);
U = triu(A,1);
x_old = x_0
errs = zeros(N+1)
errs[1] = norm(x_old - x_true)/norm(x_true)
for i in 1:N
x_new = ############################################### complete this code!
errs[i+1] = norm(x_new - x_true)/norm(x_true);
x_old = x_new;
end
return x, errs
endgaussseidel
Gauss-Seidel already improves on Jacobi by using each coordinate’s most up-to-date value as soon as it’s available. A natural next question: once we have the Gauss-Seidel update, could we do even better by moving further in that direction — or, if Gauss-Seidel overshoots, by damping the step?
Write the Gauss-Seidel value for coordinate \(i\) as \[x_i^{\text{GS}} = \frac{1}{A_{ii}} \left[ \sum_{j=1}^{i-1}(-A_{ij}x_j^{(k)}) + \sum_{j=i+1}^n (-A_{ij}x_j^{(k-1)}) + b_i \right].\]
The successive over-relaxation (SOR) method introduces a relaxation parameter \(\omega > 0\) and takes the new iterate to be a weighted average of the old value \(x_i^{(k-1)}\) and the Gauss-Seidel value \(x_i^{\text{GS}}\): \[x_i^{(k)} = (1-\omega)\,x_i^{(k-1)} + \omega\, x_i^{\text{GS}}.\]
Substituting the Gauss-Seidel expression gives the SOR update \[x_i^{(k)} = (1-\omega)x_i^{(k-1)} + \frac{\omega}{A_{ii}} \left[ \sum_{j=1}^{i-1}(-A_{ij}x_j^{(k)}) + \sum_{j=i+1}^n (-A_{ij}x_j^{(k-1)}) + b_i \right] \text{ for } i=1,\cdots,n.\]
Notice that \(\omega = 1\) recovers Gauss-Seidel exactly. Taking \(\omega > 1\) is called over-relaxation (we push past the Gauss-Seidel point, hoping to accelerate convergence), while \(0 < \omega < 1\) is under-relaxation (we damp the step, which can help stabilize an otherwise oscillatory iteration).
As with Jacobi and Gauss-Seidel, we can collect the SOR update into matrix-vector form.
Multiplying through by \(A_{ii}\) and separating known (\(k-1\)) from unknown (\(k\)) terms, the update becomes \[[\mathbf{D}(\mathbf{A}) + \omega\mathbf{L}(\mathbf{A})]\mathbf{x}^{(k)} = [(1-\omega)\mathbf{D}(\mathbf{A}) - \omega\mathbf{U}(\mathbf{A})]\mathbf{x}^{(k-1)} + \omega\mathbf{b}.\]
Thus, the update is \[\mathbf{x}^{(k)} = [\mathbf{D}(\mathbf{A}) + \omega\mathbf{L}(\mathbf{A})]^{-1}[(1-\omega)\mathbf{D}(\mathbf{A}) - \omega\mathbf{U}(\mathbf{A})] \mathbf{x}^{(k-1)} + \omega[\mathbf{D}(\mathbf{A}) + \omega\mathbf{L}(\mathbf{A})]^{-1} \mathbf{b}.\]
When \(\omega = 1\) this is exactly the Gauss-Seidel splitting from before.
In order to optimize performance, one would hope to choose the optimal value \(\omega\). This is difficult, but in certain situations one can exactly evaluate the spectral radius of the SOR update matrix in terms of the spectral radius of the Jacobi update matrix, and thus choose the optimal \(\omega\).
The Jacobi and Gauss-Seidel methods update using individual coordinate direction updates onto the hyperplanes defined by the individual equations. This is not the only way that the iterates can move towards these hyperplanes!
Perhaps the most natural updating strategy is to project each iterate onto a hyperplane to produce the subsequent iterate. This strategy is known as the Kaczmarz method. Note that this method is not a splitting method – it is a different, and common type of method – a projection method!
The update for this type of method is given by \(\mathbf{x}^{(k)} = \mathbf{x}^{(k-1)} + \frac{b_i - \mathbf{a}_i^\top \mathbf{x}^{(k-1)}}{\|\mathbf{a}_i\|^2} \mathbf{a}_i\) where \(\mathbf{a}_i\) is the \(i\)th row of \(\mathbf{A}\) and \(i\) is chosen from \(1, \cdots, n\).