\(\newcommand{\re}{\mathbb{R}}\)
Gaussian elimination#
Given \(A \in \re^{m \times m}\), we want to write it as
where \(L\) is lower triangular and \(U\) is upper triangular.
import numpy as np
Gaussian elimination without pivoting#
We now implement Algorithm 20.1.
def LU(A):
m = A.shape[0]
U = A.copy()
L = np.eye(m)
for k in range(m-1):
for j in range(k+1,m):
L[j,k] = U[j,k] / U[k,k]
U[j,k:] = U[j,k:] - L[j,k] * U[k,k:]
return L, U
Generate a random matrix and compute the decomposition
m = 10
A = 2 * np.random.rand(m,m) - 1
L, U = LU(A)
Test that \(A = L U\) is satisfied, compute \(\max| A - L U |\) over all elements
print('max|A-LU| = ', np.abs(A - L @ U).max())
max|A-LU| = 5.995204332975845e-15
Solution using LU decomposition#
We find the solution of
in two steps.
Step 1: Solve
using forward substitution
Note that \(L_{ii}=1\), so we can drop the division step.
Step 2: Solve
using backward substitution
def LUSolve(L,U,b):
m = L.shape[0]
# solve Ly = b
y = np.empty_like(b)
for i in range(m):
y[i] = b[i] - np.dot(L[i,0:i], y[0:i])
# solve Ux = y
x = np.empty_like(b)
for i in range(m-1,-1,-1):
x[i] = (y[i] - np.dot(U[i,i+1:m],x[i+1:m])) / U[i,i]
return x
Create a random right hand side vector \(b\) and solve \(A x = b\).
b = np.random.rand(m)
x = LUSolve(L,U,b)
print('||Ax-b|| = ',np.linalg.norm(A@x - b, np.inf))
||Ax-b|| = 5.773159728050814e-15
Minimizing memory: We have used two vectors x
and y
to implement the solution but we can manage with only one. We solve as
Step 1: \(x = L^{-1} b\)
Step 2:
\[ x_i = \frac{1}{U_{ii}}\left[x_i - \sum_{j=i+1}^{m-1} U_{ij} x_j \right], \qquad i=m-1,m-2,\ldots,0 \]
In the second step, we can overwrite into \(x\) during backward substition since \(U\) is upper triangular. The following code does this:
def LUSolve(L,U,b):
m = L.shape[0]
x = np.empty_like(b) # Single vector
# solve Ly = b
y = x # y is a pointer to x
for i in range(m):
y[i] = b[i] - np.dot(L[i,0:i], y[0:i])
# solve Ux = y
for i in range(m-1,-1,-1):
x[i] = (y[i] - np.dot(U[i,i+1:m],x[i+1:m])) / U[i,i]
return x
Further memory savings can be obtained by modifying the matrix \(A\) in place so that \(L\) is stored in the lower triangular part of \(A\) and \(U\) is stored in upper triangular part of \(A\), rather than creating two new matrices \(L,U\).
Example: failure of Gaussian elimination#
Solve \(A x = b\) with
whose exact solution is
A = np.array([[1.0e-20, 1.0],
[1.0, 1.0]])
b = np.array([1.0, 0.0])
L, U = LU(A)
x = LUSolve(L,U,b)
print('x = ', x)
print('Ax-b = ', A@x-b)
x = [0. 1.]
Ax-b = [0. 1.]
The solution is totally wrong in the first component. If we interchange the two equations, then
A = np.array([[1.0, 1.0],
[1.0e-20, 1.0]])
b = np.array([0.0, 1.0])
L, U = LU(A)
x = LUSolve(L,U,b)
print('x = ', x)
print('Ax-b = ', A@x-b)
x = [-1. 1.]
Ax-b = [0. 0.]
we get the correct solution. This is known as row pivoting.
Gaussian elimination with partial pivoting#
The rows are swapped in the pivoting step and the decomposition is of the form
where \(P\) is a permutation matrix. In the code, we store a permutation vector rather than a matrix.
def PLU(A):
m = A.shape[0]
U = A.copy()
L = np.eye(m)
P = np.arange(m,dtype=int) # Permutation matrix
for k in range(m-1):
i = np.argmax(np.abs(U[k:m,k])) + k
U[[k,i],k:m] = U[[i,k],k:m] # swap row i and k
L[[k,i],0:k] = L[[i,k],0:k] # swap row i and k
P[[k,i]] = P[[i,k]] # swap row i and k
for j in range(k+1,m):
L[j,k] = U[j,k] / U[k,k]
U[j,k:m] = U[j,k:m] - L[j,k] * U[k,k:m]
return P, L, U
Now we test the above function for LU decomposition.
m = 3
A = np.random.rand(m, m)
P, L, U = PLU(A)
print("A =\n", A)
print("L =\n", L)
print("U =\n", U)
print("P =\n", P)
# Permute rows of A
PA = np.empty_like(A)
for i in range(m):
PA[P[i],:] = A[i,:]
print("PA-LU =\n", PA - L@U)
A =
[[0.48340333 0.70438618 0.73757663]
[0.49333255 0.28432849 0.62347304]
[0.16637833 0.36625499 0.43186578]]
L =
[[1. 0. 0. ]
[0.97987315 1. 0. ]
[0.33725391 0.63498495 1. ]]
U =
[[0.49333255 0.28432849 0.62347304]
[0. 0.42578032 0.12665213]
[0. 0. 0.14117486]]
P =
[1 0 2]
PA-LU =
[[0. 0. 0.]
[0. 0. 0.]
[0. 0. 0.]]
From \(L U = P A\) and \(A x = b\) we get
which can be solved as
When we solve, we have to permute the right hand side vector \(b\).
def PLUSolve(L,U,P,b):
m = L.shape[0]
# solve Ly = Pb
y = np.empty_like(b)
for i in range(m):
y[i] = b[P[i]] - np.dot(L[i,0:i], y[0:i])
# solve Ux = y
x = np.empty_like(b)
for i in range(m-1,-1,-1):
x[i] = (y[i] - np.dot(U[i,i+1:m], x[i+1:m])) / U[i,i]
return x
Example: revisit failure case#
A = np.array([[1.0e-20, 1.0],
[1.0, 1.0]])
P, L, U = PLU(A)
b = np.array([1.0, 0.0])
x = PLUSolve(L,U,P,b)
print('P = ',P)
print('x = ', x)
print('Ax-b = ', A@x-b)
P = [1 0]
x = [-1. 1.]
Ax-b = [0. 0.]
Example#
We try a problem with a random matrix and right hand side.
m = 10
A = 2 * np.random.rand(m,m) - 1
P, L, U = PLU(A)
b = np.random.rand(m)
x = PLUSolve(L,U,P,b)
print('||Ax-b|| = ',np.linalg.norm(A@x - b, np.inf))
||Ax-b|| = 6.661338147750939e-16
Here is the permutation vector.
print('P = ',P)
P = [8 6 5 4 2 9 3 0 7 1]