ILI285 - Computación Científica I / INF285 - Computación Científica

Linear Systems of Equations

[S]cientific [C]omputing [T]eam

Version: 1.12


In [39]:
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline

Introduction

In our last Jupyter Notebook we learned how to solve 1D equations. Now, we'll go to the next level and will learn how to solve not just one equation, but a system of linear equations. This is a set of $n$ equations involving $n$ variables wherein all the equations must be satisfied at the same time. You probably know how to solve small 2D systems with methods such as substitution and reduction, but in practical real-life situations it's very likely that you'll find problems of bigger dimensions. As usual, we'll present some useful methods for solving systems of linear equations below.

Direct Methods

Firstly, we will study direct methods. They compute the analytic solution of the system (from here comes the name direct) limited only by the loss of numerical precision, because of the arithmetic operations performed by the computer. Their counterpart is the iterative methods, which calculate an approximate solution that evolves iteratively converging to the real solution.

LU decomposition

Given the matrix $A \in \mathbb{R}^{n \times n}$ square and non singular, the main goal of this method involves finding a decomposition like $A = L U$ where $L,U \in \mathbb{R}^{n \times n}$ are lower and upper triangular matrices respectively.

The algorithm to perform this decomposition is basically a modified version of Gaussian Elimination. It basically iterates through the first $n-1$ columns, making $0$ all the entries below the main diagonal. This is accomplished by performing row operations.


In [40]:
def lu_decomp(A, show=False):
    N,_ = A.shape
    U = np.copy(A)
    L = np.identity(N)
    if show:
        print('Initial matrices')
        print('L = '); print(np.array_str(L, precision=2, suppress_small=True))
        print('U = '); print(np.array_str(U, precision=2, suppress_small=True))
        print('----------------------------------------')
    #iterating through columns
    for j in range(N-1):
        #iterating through rows
        for i in range(j+1,N):
            L[i,j] = U[i,j]/U[j,j]
            U[i] -= L[i,j]*U[j] 
            if show:
                print('L = '); print(np.array_str(L, precision=2, suppress_small=True))
                print('U = '); print(np.array_str(U, precision=2, suppress_small=True))
                print('----------------------------------------')
    return L,U

Once the decomposition is done, solving a linear system like $A x = b$ is straightforward:

$$A x = b \rightarrow L U x = b \ \ \text{ if we set } \ \ U x = c \rightarrow L c = b \ \ \text{ (solve for c) } \ \rightarrow U x = c$$

and as you might know, solving lower and upper triangular systems can be easily performed by back-substitution and forward-subsitution respectively.


In [41]:
"""
Solves a linear system A x = b, where A is a
triangular (upper or lower) matrix
"""
def solve_triangular(A, b, upper=True):
    n = b.shape[0]
    x = np.zeros_like(b)
    if upper==True:
        #perform back-substitution
        x[-1] = (1./A[-1,-1]) * b[-1]
        for i in range(n-2, -1, -1):
            x[i] = (1./A[i,i]) * (b[i] - np.sum(A[i,i+1:] * x[i+1:]))
    else:
        #perform forward-substitution
        x[0] = (1./A[0,0]) * b[0]
        for i in range(1,n):
            x[i] = (1./A[i,i]) * (b[i] - np.sum(A[i,:i] * x[:i]))
    return x

def solve_lu(A, b, show=False):
    L,U = lu_decomp(A, show)
    # L.c = b with c = U.x
    c = solve_triangular(L, b, upper=False)
    x = solve_triangular(U, c)
    return x

Let's now try our implementations. We begin by creating a random 100$\times$100 linear system:


In [42]:
A = np.random.random((3,3))
b = np.ones(3)

and then we compute the solution with our LU solver, and aditionally with the NumPy solver which computes the solution using LAPACK routines.


In [43]:
lu_sol = solve_lu(A,b, show=True)
np_sol = np.linalg.solve(A,b)


Initial matrices
L = 
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
U = 
[[0.71 0.15 0.51]
 [0.51 0.34 0.48]
 [0.72 0.95 0.81]]
----------------------------------------
L = 
[[1.   0.   0.  ]
 [0.72 1.   0.  ]
 [0.   0.   1.  ]]
U = 
[[0.71 0.15 0.51]
 [0.   0.23 0.11]
 [0.72 0.95 0.81]]
----------------------------------------
L = 
[[1.   0.   0.  ]
 [0.72 1.   0.  ]
 [1.02 0.   1.  ]]
U = 
[[0.71 0.15 0.51]
 [0.   0.23 0.11]
 [0.   0.8  0.28]]
----------------------------------------
L = 
[[1.   0.   0.  ]
 [0.72 1.   0.  ]
 [1.02 3.41 1.  ]]
U = 
[[ 0.71  0.15  0.51]
 [ 0.    0.23  0.11]
 [ 0.    0.   -0.08]]
----------------------------------------

in order to compare these huge vectors, we use the Euclidean metric as follows:


In [44]:
np.linalg.norm(lu_sol - np_sol)


Out[44]:
2.4242552967656972e-14

which is a very good result!

This method has two important facts to be noted:

  1. Computing the LU decomposition requires $2n^3/3$ floating point operations. Can you check that?
  2. When computing the LU decomposition you can see the instruction L[i,j] = U[i,j]/U[j,j]. Here we divide an entry below the main diagonal by the pivot value. What happens if the pivot equals 0? How can we prevent that? Answer: PALU.

PALU decomposition

As you might've noted previously, LU has a problem when a pivot has the value of $0$. To handle this problem, we add row permutations to the original LU algorithm. The procedure is as follows:

  1. When visiting the row $j$, search for $\max(|a_{j,j}|,\ |a_{j+1,j}|,\ \ldots,\ |a_{N-1,j}|,\ |a_{N,j}|)$ (the maximum between the pivot and the entries below it).
  2. If such maximum is $|a_{j,k}| \neq |a_{j,j}|$, permutate rows $i$ and $k$ making $a_{j,k}$ the new pivot.

To keep track of all the permutations performed, we use the permutation matrix $P$. It's inicially an identity matrix which permutes its rows in the same way the algorithm does on the resulting matrix.


In [45]:
#permutation between rows i and j on matrix A
def row_perm(A, i, j):
    tmp = np.copy(A[i])
    A[i] = A[j]
    A[j] = tmp

def palu_decomp(A, show=False):
    N,_ = A.shape
    P = np.identity(N)
    L = np.zeros((N,N))
    U = np.copy(A)
    if show:
        print('Initial matrices')
        print('P = '); print(np.array_str(P, precision=2, suppress_small=True))
        print('L = '); print(np.array_str(L, precision=2, suppress_small=True))
        print('U = '); print(np.array_str(U, precision=2, suppress_small=True))
        print('----------------------------------------')
    #iterating through columns
    for j in range(N-1):
        #determine the new pivot
        p_index = np.argmax(np.abs(U[j:,j]))
        if p_index != 0:
            row_perm(P, j, j+p_index)
            row_perm(U, j, j+p_index)
            row_perm(L, j, j+p_index)
            if show:
                print('A permutation has been made')
                print('P = '); print(np.array_str(P, precision=2, suppress_small=True))
                print('L = '); print(np.array_str(L, precision=2, suppress_small=True))
                print('U = '); print(np.array_str(U, precision=2, suppress_small=True))
                print('----------------------------------------')
        #iterating through rows
        for i in range(j+1,N):
            L[i,j] = U[i,j]/U[j,j]
            U[i] -= L[i,j]*U[j]
            if show:
                print('P = '); print(np.array_str(P, precision=2, suppress_small=True))
                print('L = '); print(np.array_str(L, precision=2, suppress_small=True))
                print('U = '); print(np.array_str(U, precision=2, suppress_small=True))
                print('----------------------------------------')
    np.fill_diagonal(L,1)
    return P,L,U

The procedure to solve the system $Ax=b$ remains almost the same. We have to add the efect of the permutation matrix $P$:

$$A x = b \rightarrow P A x = P b \rightarrow L U x = b' \ \ \text{ if we set } \ \ U x = c \rightarrow L c = b' \ \ \text{ (solve for c) } \ \rightarrow U x = c$$

In [46]:
def solve_palu(A, b, show=False):
    P,L,U = palu_decomp(A, show)
    #A.x = b -> P.A.x = P.b = b'
    b = np.dot(P,b)
    # L.c = b' with c = U.x
    c = solve_triangular(L, b, upper=False)
    x = solve_triangular(U, c)
    return x

Let's test this new method against the LU and NumPy solvers


In [47]:
palu_sol = solve_palu(A, b, show=True)


Initial matrices
P = 
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 0. 1.]]
L = 
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
U = 
[[0.71 0.15 0.51]
 [0.51 0.34 0.48]
 [0.72 0.95 0.81]]
----------------------------------------
A permutation has been made
P = 
[[0. 0. 1.]
 [0. 1. 0.]
 [1. 0. 0.]]
L = 
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
U = 
[[0.72 0.95 0.81]
 [0.51 0.34 0.48]
 [0.71 0.15 0.51]]
----------------------------------------
P = 
[[0. 0. 1.]
 [0. 1. 0.]
 [1. 0. 0.]]
L = 
[[0.   0.   0.  ]
 [0.71 0.   0.  ]
 [0.   0.   0.  ]]
U = 
[[ 0.72  0.95  0.81]
 [ 0.   -0.33 -0.09]
 [ 0.71  0.15  0.51]]
----------------------------------------
P = 
[[0. 0. 1.]
 [0. 1. 0.]
 [1. 0. 0.]]
L = 
[[0.   0.   0.  ]
 [0.71 0.   0.  ]
 [0.98 0.   0.  ]]
U = 
[[ 0.72  0.95  0.81]
 [ 0.   -0.33 -0.09]
 [ 0.   -0.78 -0.28]]
----------------------------------------
A permutation has been made
P = 
[[0. 0. 1.]
 [1. 0. 0.]
 [0. 1. 0.]]
L = 
[[0.   0.   0.  ]
 [0.98 0.   0.  ]
 [0.71 0.   0.  ]]
U = 
[[ 0.72  0.95  0.81]
 [ 0.   -0.78 -0.28]
 [ 0.   -0.33 -0.09]]
----------------------------------------
P = 
[[0. 0. 1.]
 [1. 0. 0.]
 [0. 1. 0.]]
L = 
[[0.   0.   0.  ]
 [0.98 0.   0.  ]
 [0.71 0.42 0.  ]]
U = 
[[ 0.72  0.95  0.81]
 [ 0.   -0.78 -0.28]
 [ 0.    0.    0.02]]
----------------------------------------

In [48]:
np.linalg.norm(palu_sol - lu_sol)


Out[48]:
4.782987168225453e-15

In [49]:
np.linalg.norm(palu_sol - np_sol)


Out[49]:
2.897150329470593e-14

Here are some questions about PALU:

  1. How much computational complexity has been added to the original $2n^3/3$ of LU?
  2. Clearly PALU is more robust than LU, but given a non sigular matrix $A$ will it always be possible to perform the PALU decomposition?

Cholesky

This is another direct method only applicable to symmetric positive-definite matrices. In order to try this algorithm we have to create this kind of matrices. The next function generates random symmetric positive-definite matrices.


In [50]:
"""
Randomly generates an nxn symmetric positive-
definite matrix A.
"""
def generate_spd_matrix(n, flag=True):
    if flag:
        A = np.random.random((n,n))
        # Constructing symmetry
        A += A.T 
        # A = np.dot(A.T,A) # Another way
        #symmetric+diagonally dominant -> symmetric positive-definite
        deltas = 0.1*np.random.random(n)
        row_sum = A.sum(axis=1)-np.diag(A)
        np.fill_diagonal(A, row_sum+deltas)
    else:
        B = np.random.random((n,n))
        # A way to make sure the quadratic form is greater or equal to zero:
        # this means x^T*B^T\B*x >= ||B*x||, but if B is singular, it could be zero.
        A = np.dot(B.T,B)
        # To avoid a being singular, we just add a positive diagonal matrix
        A = A + np.eye(n)
    return A

Given a symmetric positive-definite matrix $A \in \mathbb{R}^{n \times n}$, the Cholesky decomposition is of the form $A =R^T R$, with $R$ being an upper triangular matrix. This method takes advantage of the properties of symmetric matrices, reaching approximately twice the efficiency of LU.


In [51]:
def cholesky_decomp(A, show=False):
    N,_ = A.shape
    A = np.copy(A)
    R = np.zeros((N,N))
    if show:
        print('Initial matrix')
        print('A = '); print(np.array_str(A, precision=2, suppress_small=True))
        print('R = '); print(np.array_str(R, precision=2, suppress_small=True))
        print('----------------------------------------')
    for i in range(N):
        R[i,i] = np.sqrt(A[i,i])
        u = (1./R[i,i])*A[i,i+1:]
        R[i,i+1:] = u
        A[i+1:,i+1:] -= np.outer(u,u)
        if show:
            print('A = '); print(np.array_str(A, precision=2, suppress_small=True))
            print('R = '); print(np.array_str(R, precision=2, suppress_small=True))
            print('----------------------------------------')
    return R

The solve stage remains the same as LU:


In [52]:
def solve_cholesky(A, b, show=False):
    R = cholesky_decomp(A, show)
    #R^T.R.x = b -> R^T.c = b with R.x = c
    c = solve_triangular(R.T, b, upper=False)
    x = solve_triangular(R, c)
    return x

Now we test our implementation, comparing time execution with LU and PALU on two different linear systems


In [53]:
A = generate_spd_matrix(3)
b = np.ones(3)

In [54]:
b=np.array([4,2,0])
solve_cholesky(A, b, show=True)


Initial matrix
A = 
[[2.22 0.94 1.25]
 [0.94 1.67 0.63]
 [1.25 0.63 1.9 ]]
R = 
[[0. 0. 0.]
 [0. 0. 0.]
 [0. 0. 0.]]
----------------------------------------
A = 
[[2.22 0.94 1.25]
 [0.94 1.27 0.11]
 [1.25 0.11 1.2 ]]
R = 
[[1.49 0.63 0.84]
 [0.   0.   0.  ]
 [0.   0.   0.  ]]
----------------------------------------
A = 
[[2.22 0.94 1.25]
 [0.94 1.27 0.11]
 [1.25 0.11 1.19]]
R = 
[[1.49 0.63 0.84]
 [0.   1.13 0.09]
 [0.   0.   0.  ]]
----------------------------------------
A = 
[[2.22 0.94 1.25]
 [0.94 1.27 0.11]
 [1.25 0.11 1.19]]
R = 
[[1.49 0.63 0.84]
 [0.   1.13 0.09]
 [0.   0.   1.09]]
----------------------------------------
Out[54]:
array([1, 0, 0])

In [55]:
A = generate_spd_matrix(100)
b = np.ones(100)

In [56]:
%timeit solve_cholesky(A, b)
%timeit solve_lu(A, b)
%timeit solve_palu(A, b)


3.77 ms ± 108 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
20.2 ms ± 462 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)
20.6 ms ± 613 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

In [57]:
A = generate_spd_matrix(1000)
b = np.ones(1000)

In [58]:
%timeit solve_cholesky(A, b)
%timeit solve_lu(A, b)
%timeit solve_palu(A, b)


848 ms ± 9.13 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
2.79 s ± 214 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
2.84 s ± 193 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Iterative Methods


In [59]:
"""
Randomly generates an nxn strictly diagonally 
dominant matrix A.
"""
def generate_dd_matrix(n):
    A = np.random.random((n,n))
    deltas = 0.1*np.random.random(n)
    row_sum = A.sum(axis=1)-np.diag(A)
    np.fill_diagonal(A, row_sum+deltas)
    return A

"""
Computes relative error between each row on 
X matrix and y vector. 
"""
def error(X, y):
    D = X-y
    err = np.linalg.norm(D, axis=1, ord=np.inf)
    return err

As before we will create a linear system $A x = b$, with $A$ as a diagonally dominant matrix, which is a sufficient condition for the methods we will study in this section converge


In [60]:
A = np.array([[3, -1, 0, 0, 0, 0.5],[-1, 3, -1, 0, 0.5, 0],[0, -1, 3, -1, 0, 0],[0, 0, -1, 3, -1, 0],
              [0, 0.5, 0, -1, 3, -1],[0.5, 0, 0, 0, -1, 3]])
b = np.array([2.5, 1.5, 1., 1., 1.5, 2.5])
print ('A='); print (A)
print ('b='); print (b)


A=
[[ 3.  -1.   0.   0.   0.   0.5]
 [-1.   3.  -1.   0.   0.5  0. ]
 [ 0.  -1.   3.  -1.   0.   0. ]
 [ 0.   0.  -1.   3.  -1.   0. ]
 [ 0.   0.5  0.  -1.   3.  -1. ]
 [ 0.5  0.   0.   0.  -1.   3. ]]
b=
[2.5 1.5 1.  1.  1.5 2.5]

and find the solution $x$ through np.linalg.solve to use it as the reference solution-


In [61]:
np_sol = np.linalg.solve(A,b)

Jacobi


In [62]:
"""
Iterative methods implementations returns an array X
with the the solutions at each iteration
"""
def jacobi(A, b, n_iter=50):
    n = A.shape[0]
    #array with solutions
    X = np.empty((n_iter, n))
    #initial guess
    X[0] = np.zeros(n)
    #submatrices
    D = np.diag(A)
    Dinv = D**-1
    R = A - np.diag(D) # R = (L+U)
    for i in range(1, n_iter):
        # X[i] = Dinv*(b - np.dot(R, X[i-1]))
        # v1.12
        ri = b - np.dot(A, X[i-1])
        X[i] = X[i-1]+Dinv*ri
    return X
def jacobi_M(A):
    L = np.tril(A,-1)
    U = np.triu(A,1)
    D = np.diag(np.diag(A))
    M = -np.dot(np.linalg.inv(D),L+U)
    return M

# \mathbf{x}_{n+1}=M\,\mathbf{x}_{n}+\mathbf{c}

Now let's resolve the same linear system with Jacobi method!


In [63]:
jac_sol = jacobi(A,b, n_iter=50)

In [64]:
jac_err = error(jac_sol, np_sol)
it = np.linspace(1, 50, 50)

In [65]:
plt.figure(figsize=(12,6))
plt.semilogy(it, jac_err, marker='o', linestyle='--', color='b')
plt.grid(True)
plt.xlabel('Iterations')
plt.ylabel('Error')
plt.title('Infinity norm error for Jacobi method')
plt.show()



In [66]:
Mj = jacobi_M(A)
print(np.linalg.norm(Mj))


1.1055415967851334

In [67]:
np.linalg.eigvals(Mj)


Out[67]:
array([-0.56734117, -0.53694624, -0.03039493,  0.03039493,  0.56734117,
        0.53694624])

In [68]:
np.abs(np.linalg.eigvals(Mj))


Out[68]:
array([0.56734117, 0.53694624, 0.03039493, 0.03039493, 0.56734117,
       0.53694624])

In [69]:
np.max(np.abs(np.linalg.eigvals(Mj)))


Out[69]:
0.5673411660774046

Gauss Seidel


In [70]:
def gauss_seidel(A, b, n_iter=50):
    n = A.shape[0]
    #array with solutions
    X = np.empty((n_iter, n))
    #initial guess
    X[0] = np.zeros(n)
    #submatrices
    R = np.tril(A) #R=(L+D)
    U = A-R
    for i in range(1, n_iter):
        #X[i] = solve_triangular(R, b-np.dot(U, X[i-1]), upper=False)
        # v1.11
        X[i] = X[i-1]+solve_triangular(R, b-np.dot(A, X[i-1]), upper=False)
    return X
def gauss_seidel_M(A):
    L = np.tril(A,-1)
    U = np.triu(A,1)
    D = np.diag(np.diag(A))
    M = -np.dot(np.linalg.inv(L+D),U)
    return M

Now let's resolve the same linear system with Gauss-Seidel method!


In [71]:
gauss_sol = gauss_seidel(A,b)

In [72]:
gauss_err = error(gauss_sol, np_sol)

In [73]:
plt.figure(figsize=(12,6))
plt.semilogy(it, gauss_err, marker='o', linestyle='--', color='r')
plt.grid(True)
plt.xlabel('Iterations')
plt.ylabel('Error')
plt.title('Infinity norm error for Gauss method')
plt.show()


Here are some questions about Gauss-Seidel:

  • Can you explain what the differences between this and Jacobi method are?
  • Why do we use solve_triangular instead of np.linalg.solve or something similar?

In [74]:
Mgs = gauss_seidel_M(A)
print(np.linalg.norm(Mgs))


0.8351230731594124

In [75]:
np.max(np.abs(np.linalg.eigvals(Mgs)))


Out[75]:
0.37372799877238394

SOR


In [76]:
def sor(A, b, w=1.05, n_iter=50):
    n = A.shape[0]
    #array with solutions
    X = np.empty((n_iter, n))
    #initial guess
    X[0] = np.zeros(n)
    #submatrices
    R = np.tril(A) #R=(L+D)
    U = A-R
    # v1.11
    L = np.tril(A,-1)
    D = np.diag(np.diag(A))
    M = L+D/w
    for i in range(1, n_iter):
        #X_i = solve_triangular(R, b-np.dot(U, X[i-1]), upper=False)
        #X[i] = w*X_i + (1-w)*X[i-1]
        # v1.11
        X[i] = X[i-1]+solve_triangular(M, b-np.dot(A, X[i-1]), upper=False)
    return X
def sor_M(A,w=1.05):
    L = np.tril(A,-1)
    U = np.triu(A,1)
    D = np.diag(np.diag(A))
    M = np.dot(np.linalg.inv(w*L + D),((1-w)*D -w*U))
    return M

Now let's resolve the same linear system with Jacobi method!


In [77]:
sor_sol = sor(A, b, w=1.15)

In [78]:
sor_err = error(sor_sol, np_sol)

In [79]:
plt.figure(figsize=(12,6))
plt.semilogy(it, sor_err, marker='o', linestyle='--', color='g')
plt.grid(True)
plt.xlabel('Iterations')
plt.ylabel('Error')
plt.title('Infinity norm error for SOR method')
plt.show()



In [80]:
Msor = sor_M(A)
print(np.linalg.norm(Msor))


0.850053072081257

In [81]:
np.max(np.abs(np.linalg.eigvals(Msor)))


Out[81]:
0.33434770548114945

How can we choose a good value of $\omega$? Well there are some methods you could search, but for now we will try a naive way, i.e, computing the solution for a range $\omega \in [1,1.3]$ as follows:


In [82]:
n = 30 #width of subdivisions
sor_solutions = list()
for w in np.linspace(1., 1.3, n):
    sor_solutions.append(sor(A, b, w, n_iter=5)[-1])
np.asarray(sor_solutions)

#now compute error solutions with each w
sor_errors = error(sor_solutions, np_sol)
w = np.linspace(1., 1.3, n)

as you can see, we compute the SOR solution with 5 iterations for each $\omega$ on the given range.


In [83]:
plt.figure(figsize=(12,6))
plt.semilogy(w, sor_errors, marker='o', linestyle='--', color='g')
plt.grid(True)
plt.xlabel('w')
plt.ylabel('Error')
plt.title('Infinity norm error after 5 steps of SOR as a function of w')
plt.show()


Here are some questions about SOR:

  • Why can averaging the current solution with the Gauss-Seidel solution improve convergence?
  • Why do we use $\omega > 1$ and not $\omega < 1$?
  • Could you describe a method to find the best value of $\omega$ (the one which optimizes convergence)?
  • Would it be a better option to re-compute $\omega$ at each iteration?

Convergence Analysis

Let's see convergence plots all together


In [84]:
plt.figure(figsize=(12,6))
plt.semilogy(it, jac_err, marker='o', linestyle='--', color='b', label='Jacobi')
plt.semilogy(it, gauss_err, marker='o', linestyle='--', color='r', label='Gauss-Seidel')
plt.semilogy(it, sor_err, marker='o', linestyle='--', color='g', label='SOR')
plt.grid(True)
plt.xlabel('Iterations')
plt.ylabel('Error')
plt.title('Infinity norm error for all methods')
plt.legend(loc=0)
plt.show()


Exercises

Now that you know how to solve systems of linear equations problem with these methods, let's try to answer a few questions!

$a)$ Find the values of $\alpha$ that make possible to do a LU descomposition of the following matrix:

$$ \begin{bmatrix} \alpha & 2 \\[0.3em] 1 & \alpha \end{bmatrix} $$

$b)$- Let $A$ be the following matrix:

$$ A = \begin{bmatrix} 2 & 4 & 2 \\[0.3em] -1 & 1 & 2 \\[0.3em] -1 & -3 & -1 \end{bmatrix} $$

  • Find the PALU descomposition of the matrix $A$.

  • Solve the system of equations $Ax = \[1 , \frac{1}{2}, \frac{1}{3}\]^T$.

$c)$ Considering this matrix:

$$ \begin{bmatrix} 1 & 1 & 0 \\[0.3em] 1 & 5 & 2 \\[0.3em] 0 & 2 & 3 \end{bmatrix} $$
  • Find the LU descomposition.

  • Find the Cholesky descomposition.

  • Compare the efficiency of both methods.

$d)$ Use Jacobi, Gauss Seidel, and SOR to solve the following system of equations (number of iterations = 2):

$$2x + y = 3$$$$x + 2y = 2$$
  • Which is the best method to solve this problem (with better results)?

$e)$ Explain the pros and cons of using iterative methods instead of the direct ones.

Extra: Hilbert Matrix


In [85]:
from scipy.linalg import hilbert

In [86]:
N=20
errors=np.zeros(N+1)
kappas=np.zeros(N+1)
my_range=np.arange(5,N+1)
for n in my_range:
    A=hilbert(n)
    x_exact=np.ones(n)
    b=np.dot(A,x_exact)
    x=np.linalg.solve(A,b)
    errors[n]=np.linalg.norm(x-x_exact,ord=np.inf)
    kappas[n]=np.linalg.cond(A,2)

In [87]:
plt.figure(figsize=(12,6))
plt.semilogy(my_range, 10.**(-16+np.log10(kappas[my_range])), marker='o', linestyle='--', color='b',label='Estimated forward error')
plt.semilogy(my_range, errors[my_range], marker='o', linestyle='--', color='r',label='Forward error')
plt.grid(True)
plt.xlabel('n')
#plt.ylabel('$\kappa$ and errors')
plt.title('')
plt.legend(loc=0)
plt.show()


(1/Kappa(A))||b-A x_a||/||b|| <= ||x-x_a||/||x||<= Kappa(A)||b-A x_a||/||b||


In [88]:
n=200
A=hilbert(n)
x_exact=np.ones(n)
b=np.dot(A,x_exact)
x=np.linalg.solve(A,b)
kappa=np.linalg.cond(A,2)
print(np.log10(kappa))


19.814106208776867

In [89]:
np.linalg.norm(b-np.dot(A,x))


Out[89]:
1.339099724944585e-13

In [90]:
kappa


Out[90]:
6.517877723650372e+19

In [91]:
np.linalg.norm(x-x_exact)


Out[91]:
9653.459067648608

In [92]:
x_exact[0]


Out[92]:
1.0

In [93]:
x


Out[93]:
array([ 1.00000665e+00,  9.98750690e-01,  1.05609273e+00, -5.14904933e-02,
        1.11565147e+01, -5.44909507e+01,  1.77079698e+02, -3.10039439e+02,
        2.52290754e+02, -6.18643339e+00, -8.46142339e+01,  1.58745346e+02,
       -3.90995456e+02,  1.52586006e+02,  1.61201278e+02,  7.17163400e+01,
        2.87833782e+02, -4.96853279e+02,  5.71029708e+00, -8.55677027e+01,
       -2.81006315e+02,  7.37144440e+01,  8.47670286e+02, -2.51821751e+02,
       -4.01263048e+01,  1.13778089e+02, -2.73176943e+02, -4.96215668e+01,
        5.54820641e+01,  2.86732668e+02, -4.00106753e+02, -4.83750609e+02,
        4.24250382e+02, -2.75718791e+02, -1.61323316e+02,  7.49846984e+02,
        9.76782056e+01, -4.98498641e+02,  1.93499727e+02,  6.13198603e+02,
        5.29735239e+02, -8.47667734e+02,  1.56680387e+02,  4.84095571e+01,
       -1.30630864e+02, -1.99077267e+01, -1.45806500e+02, -8.29331429e+02,
       -8.44094280e+01, -7.36246701e+01,  1.77793660e+02,  6.80464259e+00,
       -1.32368252e+00,  3.86312603e+02, -1.93019853e+02,  3.84235362e+02,
        2.19594340e+02, -4.62564061e+01,  6.78305495e+02, -2.49007244e+02,
        4.61044254e+02,  3.73587071e+01, -9.65972951e-01, -6.59901562e+01,
       -1.14385924e+03,  1.73079092e+02,  1.24768319e+02, -7.92924214e+00,
        1.14098555e+02, -4.88750667e+02, -8.97675305e+01,  5.00316496e+02,
        6.43865275e+02, -4.39561099e+01, -1.28718498e+03,  4.75814920e+01,
        3.42639107e+02, -3.72555510e+02, -7.11008667e+02, -1.63563631e+02,
        4.57680984e+01, -4.26455441e+02, -1.12022734e+02,  3.62000309e+02,
        1.56623759e+03,  2.43049100e+02,  1.13009447e+03,  6.23419351e+02,
       -7.38812487e+02, -3.71331596e+02, -3.63320456e+01, -3.81181177e+01,
       -1.71559576e+02,  6.41874141e+02,  1.03793050e+03, -1.17290313e+02,
       -7.58407807e+02, -5.19793731e+02, -4.69198106e+02, -7.02644717e+02,
       -2.11964372e+02, -1.51445917e+02, -7.48495310e+02, -1.94036548e+02,
        5.35214822e+02,  8.18057059e+02, -1.72246340e+03,  9.66285474e+02,
        8.67925652e+02, -8.44711140e+01, -5.14694938e+02, -1.34040451e+03,
        1.13961784e+03,  1.17710456e+02, -8.86960406e+02,  1.43471937e+03,
       -2.73058790e+02,  1.27186187e+03,  1.66296842e+02,  2.27653843e+02,
        1.53615417e+03, -8.97108984e+02, -9.36941444e+02,  6.52726989e+02,
        2.63588535e+02, -2.70396420e+02,  1.33930489e+02, -1.50475619e+03,
       -1.43047952e+03,  6.89976330e+02,  6.74375226e+01, -1.88906825e+03,
        4.18938652e+02,  1.21193198e+03,  5.01197898e+02,  8.10671929e+02,
        3.21354454e+02, -3.49865988e+02, -3.62963040e+02, -7.19776727e+02,
        6.48692274e+02, -9.35634452e+02,  6.41490822e+02,  1.99170846e+02,
        1.02321761e+03, -5.64029729e+02,  7.19899462e+01, -9.33472270e+02,
       -5.97579457e+02,  8.30781590e+02,  4.48147252e+02,  4.53324620e+02,
        9.51088705e+02,  1.52544307e+02, -3.48433087e+02, -8.07482055e+02,
       -7.24706155e+02,  1.24831179e+03, -3.81328089e+02, -2.00904160e+02,
       -7.35405420e+02,  1.25105289e+03, -8.42038773e+02,  2.28646820e+02,
       -5.91082108e+02, -9.02251341e+02, -1.94190823e+03,  3.06129279e+02,
       -1.20356724e+03,  2.00782139e+02,  1.70049107e+03,  4.89738888e+02,
        2.18831003e+03,  5.07485690e+02,  3.87556307e+02, -1.26893956e+02,
        8.23144999e+01, -8.97337566e+02,  9.89190976e+02,  5.49768305e+02,
       -6.31474577e+01,  2.60562145e+02, -2.56731867e+02, -1.84314906e+02,
       -1.93304261e+03,  4.80959141e+02, -9.19920973e+02, -1.18978074e+03,
        7.43313404e+01,  1.96504331e+01, -2.39542973e+01, -6.46737579e+02,
        1.16653906e+03,  6.90088263e+02,  2.68258341e+02,  6.91811802e+02,
        1.23660035e+03, -4.23930662e+02, -1.61578608e+03,  2.70684837e+02])

In [94]:
plt.plot(x,'.')
plt.show()


Acknowledgements

  • Material created by professor Claudio Torres (ctorres@inf.utfsm.cl) and assistants: Laura Bermeo, Alvaro Salinas, Axel Simonsen and Martín Villanueva. DI UTFSM. April 2016.
  • Update May 2020 - v1.11 - C.Torres : Fixing formatting issues.

In [95]:
lu_decomp(np.array([[1e-20, 1],[1,2]]), show=True)


Initial matrices
L = 
[[1. 0.]
 [0. 1.]]
U = 
[[0. 1.]
 [1. 2.]]
----------------------------------------
L = 
[[1.e+00 0.e+00]
 [1.e+20 1.e+00]]
U = 
[[ 1.e-20  1.e+00]
 [ 0.e+00 -1.e+20]]
----------------------------------------
Out[95]:
(array([[1.e+00, 0.e+00],
        [1.e+20, 1.e+00]]),
 array([[ 1.e-20,  1.e+00],
        [ 0.e+00, -1.e+20]]))

In [ ]: