Worksheet 6

In this worksheet we will focus on solving the linear system given by \begin{equation} Ax = b, \end{equation} where $A$ is a non-singular $n\times n$ matrix.

In tis worksheet you will find the $LU$ factorization of the Matrix $A$, such that \begin{equation} L\cdot U = A, \end{equation} where $L$ is lower triangular and $U$ is upper triangular.

If the LU factors are known, then solving the linear system can be performed in two triangular solves. \begin{equation} Lp = b, \end{equation} and \begin{equation} Ux = p. \end{equation}

To warm up we will compute the LU factorization of a $2\times 2$ matrix.
We define the matrix


In [13]:
A = [1. 3.; 2. 5.]


Out[13]:
2x2 Array{Float64,2}:
 1.0  3.0
 2.0  5.0

We check that the matrix is invertible by computing its determinant, which should be non-zero.


In [14]:
det(A)


Out[14]:
-1.0

We make space for the LU factors


In [15]:
L = zeros(Float64, 2, 2);
U = zeros(Float64, 2, 2);

We have that

\begin{equation} A = \left [ \begin{array}{cc} a_{1,1} & a_{1,2} \\ a_{2,1} & a_{2,2} \end{array} \right ], \qquad L = \left [ \begin{array}{cc} l_{1,1} & 0 \\ l_{2,1} & l_{2,2} \end{array} \right ], \qquad U = \left [ \begin{array}{cc} u_{1,1} & u_{1,2} \\ 0 & u_{2,2} \end{array} \right ]. \end{equation}

Then we have that \begin{equation} A = \left [ \begin{array}{cc} a_{1,1} & a_{1,2} \\ a_{2,1} & a_{2,2} \end{array} \right ] = \left [ \begin{array}{cc} l_{1,1}u_{1,1} & l_{1,1}u_{1,2} \\ l_{2,1}u_{1,1} & l_{2,1}u_{1,2} + u_{2,2}l_{2,2} \end{array} \right ]. \end{equation}

We need to find a combination of $l_{1,1}u_{1,1} = a_{1,1}$.

  • We always suppose that $l_{i,i} = 1$, then $u_{1,1} = a_{1,1}$.
  • This means that $u_{1,2} = a_{1,2}$ and $l_{2,1} = a_{2,1}/u_{1,1} = a_{2,1}/a_{1,1}$.

In [16]:
L[:,1] = A[:,1]/A[1,1]
U[1,:] = A[1,:]
println("U = \n",U)
println("L = \n",L)


U = 
[1.0 3.0
 0.0 0.0]
L = 
[1.0 0.0
 2.0 0.0]

We have already factorized the first column and row of $A$. So we can continue with the next row and column.
In this case we have that \begin{equation} l_{2,1}u_{1,2} + u_{2,2}l_{2,2} = a_{2,2}, \end{equation} or, given that $l_{2,1}$ and $u_{1,2}$ are known we have that \begin{equation} u_{2,2}l_{2,2} = a_{2,2} - l_{2,1}u_{1,2}. \end{equation} By convention we have that $l_{2,2} = 1$, then we have that $u_{2,2} = a_{2,2} - l_{2,1}u_{1,2}$.
Which can be written in julia as


In [17]:
L[2,2] = 1;
U[2,2] = A[2,2] - L[2,1]*U[1,2];

And we can test that the factorization is accurate by


In [18]:
vecnorm(L*U - A)


Out[18]:
0.0

General Factorization

Now we will generalize the LU factorization for any matrix, for simplicity this will performed recursively.

Let $A$ be a $n\times n$ matrix given by \begin{equation} A = \left [ \begin{array}{cc} a_{1,1} & A_{1,2} \\ A_{2,1} & A_{2,2} \end{array} \right ] \end{equation} where

  • $A_{2,2}$ is a $ (n-1 \times n-1)$ matrix,
  • $A_{1,2}$ is a $ (1 \times n-1)$ vector, and
  • $A_{2,1}$ is a $ (n-1 \times 1)$ vector.

We have \begin{equation} L = \left [ \begin{array}{cc} l_{1,1} & 0 \\ L_{2,1} & L_{2,2} \end{array} \right ], \qquad U = \left [ \begin{array}{cc} u_{1,1} & U_{1,2} \\ 0 & U_{2,2} \end{array} \right ]. \end{equation} Where

  • $L_{2,2}$ is a lower triangular matrix, and
  • $U_{2,2}$ is an upper triangular matrix.

In [27]:
m = 4
A = rand(m,m) + m*eye(m)
L = zeros(A)
U = zeros(A)


Out[27]:
4x4 Array{Float64,2}:
 0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0

Given that we will modify A, we will save a copy to make the comparison afterwards.


In [28]:
Acopy = copy(A)


Out[28]:
4x4 Array{Float64,2}:
 4.5158    0.147346  0.922938   0.602747
 0.460922  4.59042   0.396809   0.264758
 0.581176  0.54636   4.67243    0.76578 
 0.852285  0.817758  0.0627713  4.78422 

As before we have that $l_{1,1} = 1$ so $u_{1,1} = a_{1,1}$.
But we have that \begin{equation} A = \left [ \begin{array}{cc} a_{1,1} & A_{1,2} \\ A_{2,1} & A_{2,2} \end{array} \right ] = \left [ \begin{array}{cc} l_{1,1}u_{1,1} & l_{1,1}U_{1,2} \\ L_{2,1}u_{1,1} & L_{2,1}U_{1,2} + L_{2,2}U_{2,2} \end{array} \right ]. \end{equation} which means that $L_{2,1} = A_{2,1}/A_{1,1}$ and $U_{1,2} = A_{1,2}$.

We can then save the first row of $U$ and the first columns of $L$ using


In [29]:
U[1,:] = A[1,:];
L[:,1] = A[:,1]/A[1,1];
println("U = \n",U)
println("L = \n",L)

Then we have that \begin{equation} L_{2,2}U_{2,2} = A_{2,2} - L_{2,1}U_{1,2}, \end{equation} where $L_{2,1}$ and $U_{1,2}$ are known.
This means that $L_{2,2}$ and $U_{2,2}$ are the LU factors of the $(n-1\times n-1)$ matrix $A_{2,2} - L_{2,1}U_{1,2}$.

We have "eliminated" the first rown and column of A, and we have reduced to problem to finding the LU factors of a smaller matrix.

You can write this in Julia as


In [30]:
A[1:end,1:end] = A[1:end,1:end] - L[1:end,1]*U[1,1:end]


Out[30]:
4x4 Array{Float64,2}:
 0.0          0.0        0.0       0.0     
 0.0          4.57538    0.302606  0.203237
 1.11022e-16  0.527397   4.55365   0.688207
 0.0          0.789949  -0.111419  4.67047 

You can see that the first column and row of $A$ have been eliminated. Now we can apply the same procedure to the modified matrix.
We eliminate the second row and second columns.


In [32]:
U[2,2:end] = A[2,2:end]
L[2:end,2] = A[2:end,2]/A[2,2]
println("U = \n",U)
println("L = \n",L)


U = 
[4.515795732150141 0.1473462992329202 0.9229382868909721 0.6027473024579797
 0.0 4.5753757918540625 0.3026061920442243 0.20323681100839353
 0.0 0.0 0.0 0.0
 0.0 0.0 0.0 0.0]
L = 
[1.0 0.0 0.0 0.0
 0.10206877123232858 1.0 0.0 0.0
 0.1286983851631945 0.1152684829955739 0.0 0.0
 0.18873408331909516 0.17265229665492562 0.0 0.0]

And we compute the smaller matrix


In [33]:
A[2:end,2:end] = A[2:end,2:end] -  L[2:end,2]*U[2,2:end]
println("A = \n",A)


A_{2,2} = 
[0.0 0.0 0.0 0.0
 0.0 0.0 0.0 0.0
 1.1102230246251565e-16 0.0 4.518765388800081 0.6647805365294117
 0.0 0.0 -0.16366428712176515 4.63537643958818]

And we continue


In [35]:
U[3,3:end] = A[3,3:end]
L[3:end,3] = A[3:end,3]/A[3,3]
println("U = \n",U)
println("L = \n",L)


U = 
[4.515795732150141 0.1473462992329202 0.9229382868909721 0.6027473024579797
 0.0 4.5753757918540625 0.3026061920442243 0.20323681100839353
 0.0 0.0 4.518765388800081 0.6647805365294117
 0.0 0.0 0.0 0.0]
L = 
[1.0 0.0 0.0 0.0
 0.10206877123232858 1.0 0.0 0.0
 0.1286983851631945 0.1152684829955739 1.0 0.0
 0.18873408331909516 0.17265229665492562 -0.036218806032154896 0.0]

We eliminate another colum and row


In [37]:
A[3:end,3:end] = A[3:end,3:end] -  L[3:end,3]*U[3,3:end]
println("A = ",A)


Out[37]:
2x2 Array{Float64,2}:
 0.0  0.0    
 0.0  4.65945

In [ ]:
We need to compute the LU factors of just a number,

In [38]:
L[end,end] = 1
 U[end,end] = A[end,end]


8.881784197001252e-16

Finally, we check that our LU factorization is accurate


In [39]:
println(maximum(abs(L*U-Acopy)))


8.881784197001252e-16

Sparse Matrices

For homework 7, you will need to manipulate a very simple sparse matrix.

A sparse matrix, is a matrix which is sparsely populated, in other words, it has a few non-zero entries per row. When you have a matrix like that, storing all the zeros can become extremely expensive. In order to overcome this problem, we can represent sparse matrices without storing the zeros explicitely.

On typical example is the identity matrix, that has a special contructor


In [41]:
Isp = speye(m)


Out[41]:
4x4 sparse matrix with 4 Float64 entries:
	[1, 1]  =  1.0
	[2, 2]  =  1.0
	[3, 3]  =  1.0
	[4, 4]  =  1.0

You can observe that the sparse matrix contains only the non zero elements.

This fact is extremely valuable not only for storage (a sparse matrix uses much less memory than its dense counterpart), but for any operation. If a $n\times n$ sparse matrix has $\mathcal{O}(1)$ non-zeros per row, or it has only $\mathcal{O}(n)$ non-zeros entries, then a matrix vector multiplication can be performed in $\mathcal{O}(n)$ instead of $\mathcal{O}(n^2)$.

You can run the snippet below, and see how the run-times scale with respect to $n$, as $n$ increases


In [56]:
n = 10000
r = rand(n)
Msp =  spdiagm(r, 0);
M   =   diagm(r,0);
b = rand(n,1)
@time xsp = Msp*b;
@time x =M*b;
norm(x-xsp)


elapsed time: 0.00011385 seconds (80128 bytes allocated)
elapsed time: 0.195414481 seconds (80176 bytes allocated)
Out[56]:
0.0

In julia Sparse matrices behave exaxtly as normal matrices. You can index them, slice them and reorder them.
For example you can swap rows by writting


In [64]:
i = 2;
j = 3;

Isp = speye(4)
# we use full to convert back the sparse amtrix to a dense matrix 
# so it "prints" better in your console
println("original matrix \n ",full(Isp))
buffer = Isp[i,:]
Isp[i,:] = Isp[j,:]
Isp[j,:] = buffer
println("permuted matrix \n",full(Isp))


original matrix 
 [1.0 0.0 0.0 0.0
 0.0 1.0 0.0 0.0
 0.0 0.0 1.0 0.0
 0.0 0.0 0.0 1.0]
permuted matrix 
[1.0 0.0 0.0 0.0
 0.0 0.0 1.0 0.0
 0.0 1.0 0.0 0.0
 0.0 0.0 0.0 1.0]

In [ ]: