LU Decomposition

This notebook presents an experimental implementation of the LU decomposition. It is not meant for use in the wild. Its purpose is to provide a tool to actually see LU decomposition as it arises during Gaussian elimination. It uses lists of lists, rather than NumPy arrays, so that you can experiment with objects of various precision - like Decimal.

lu_step

lu_step is the main function allowing you to see the action. It accepts four mandatory inputs:

  • LLPartial: The multipliers below the diagonal of ones in current L matrix
  • Upartial: The current U matrix obtained by Gaussian elimination
  • row_col: The current step or position along the diagonal that we're woring on
  • pivot: The pivot position you want to use.

In addition, there are two inputs with default values:

  • show: Set to True to print the current results
  • width: A parameter to tweak the pretty printing.

Here's the function:

In [1]:
import pprint as pp
from copy import deepcopy
def lu_step(Lpartial, Upartial, row_col, pivot, show = True, width = 50):
    L = deepcopy(Lpartial) # Need to deepcopy these to avoid the contents
    U = deepcopy(Upartial) # of the input from being overwritten.
    dim = len(U)
    for i in range(dim):
        L[i][i] = 0
    multipliers = []
    row,col = row_col,row_col
    perm = list(range(dim))
    temp = perm[pivot]
    perm[pivot] = perm[row]
    perm[row] = temp
    U = [U[i] for i in perm]
    L = [L[i] for i in perm]
    pivot_term = U[row][col]
    for i in range(row+1,dim):
        multiplier = -U[i][col]/pivot_term
        multipliers.append(multiplier)
        for j in range(dim):
            U[i][j] = U[i][j]+multiplier*U[row][j]
    for i in range(row+1,dim):
        L[i][col] = -multipliers[i-(row+1)]
    for i in range(dim):
        L[i][i] = 1
    if show == True:
        pp.pprint('-------')
        pp.pprint('Step ' + str(row_col))
        pp.pprint('-------')
        pp.pprint('L = ')
        pp.pprint(L, width = width)
        pp.pprint('-------')
        pp.pprint('U = ')
        pp.pprint(U, width = width)
    return L,U

And here's how to use it. Note that we are using NumPy simply for the convenience in some spots, like the setup of an identity matrix. It is not used in the computations.

In [2]:
import numpy as np
A = [
    [10,-7,0],
    [-3,2,6],
    [5,-1,5]
]
Linit = np.identity((len(A))).tolist()
L,U = lu_step(Linit, A, 0, 0, width = 40)
'-------'
'Step 0'
'-------'
'L = '
[[1, 0.0, 0.0],
 [-0.3, 1, 0.0],
 [0.5, 0.0, 1]]
'-------'
'U = '
[[10, -7, 0],
 [0.0, -0.10000000000000009, 6.0],
 [0.0, 2.5, 5.0]]

Note that the resulting L matrix has the additive inverses of the multipliers used in Gaussian elimination below the ones on the diagonal and the resulting U matrix contains the result of the Gaussian elimination. Next, we can use the L and U computed to this point to take the next step. We also need to indicate that we've moved from row_col zero to row_col one and we'll need to indicate where we want the pivot to be. The pivot should be at least as large as row_col but not larger than the dimension of the matrix. Looking at the current state of U, the best choice of a pivot should be 2 - right? Let's see what happens if we use naive Gaussian elimination and use 1 as the pivot.

In [3]:
L,U = lu_step(L, U, 1, 1, width = 40)
'-------'
'Step 1'
'-------'
'L = '
[[1, 0.0, 0.0],
 [-0.3, 1, 0.0],
 [0.5, -24.99999999999998, 1]]
'-------'
'U = '
[[10, -7, 0],
 [0.0, -0.10000000000000009, 6.0],
 [0.0, 0.0, 154.9999999999999]]
In [4]:
np.array(L).dot(np.array(U))
Out[4]:
array([[ 10.,  -7.,   0.],
       [ -3.,   2.,   6.],
       [  5.,  -1.,   5.]])

It appears to have worked!

Here's an example that (I think) shows what goes wrong when the wrong pivot is chosen.

In [5]:
from decimal import Decimal, getcontext
context = getcontext()
context.prec = 5

n = 8
A = [[10**(-n), 1], [1,1]]

dim = len(A)
for i in range(dim):
    for j in range(dim):
        A[i][j] = Decimal(A[i][j])
zero = [[Decimal(0) for i in range(dim)] for j in range(dim)]

L,U = lu_step([[1,0],[0,1]],A,0,0)
'-------'
'Step 0'
'-------'
'L = '
[[1, 0], [Decimal('1.0000E+8'), 1]]
'-------'
'U = '
[[Decimal('1.0000000000000000209225608301284726753266340892878361046314239501953125E-8'),
  Decimal('1')],
 [Decimal('0.0000'), Decimal('-1.0000E+8')]]
In [6]:
L = np.array(L)
U = np.array(U)
L.dot(U)
Out[6]:
array([[Decimal('1.0000E-8'), Decimal('1')],
       [Decimal('1.0000'), Decimal('0E+4')]], dtype=object)

The full decomposition

The function lu_decomp allows you to visualize the whole decompostion, rather than just a single step. Of course, the choice of the pivot should be automated.

In [7]:
def choose_pivot(M,k):
    row,col = k,k
    dim = len(M)
    indexed_elements = [(abs(M[i][col]),i) for i in range(row,dim)]
    pivot = max(indexed_elements)[1]
    return pivot

def lu_decomp(M, show = True, width = 40):
    U = deepcopy(M)
    dim = len(U)
    U = [[float(U[i][j]) for j in range(dim)] for i in range(dim)]
    L = np.identity(dim).tolist()
    perm = list(range(dim))
    for k in range(dim):
        pivot = choose_pivot(U,k)
        temp = perm[pivot]
        perm[pivot] = perm[k]
        perm[k] = temp
        L,U = lu_step(L, U, k, pivot, show=show, width=width)
        if show == True:
            pp.pprint('perm = ' + str(perm))
    return L,U,np.identity(dim)[perm].transpose().tolist()
In [8]:
from numpy.random import seed, randint
seed(1)
A = randint(-8,8,(4,4))
A
Out[8]:
array([[-3,  3,  4,  0],
       [ 7,  1,  3, -3],
       [ 7, -8, -8, -7],
       [ 4, -1,  5,  4]])
In [9]:
L,U,P = lu_decomp(A, width = 60)
'-------'
'Step 0'
'-------'
'L = '
[[1, 0.0, 0, 0.0],
 [1.0, 1, 0.0, 0.0],
 [-0.42857142857142855, 0.0, 1, 0.0],
 [0.5714285714285714, 0.0, 0.0, 1]]
'-------'
'U = '
[[7.0, -8.0, -8.0, -7.0],
 [0.0, 9.0, 11.0, 4.0],
 [0.0, -0.4285714285714284, 0.5714285714285716, -3.0],
 [0.0, 3.571428571428571, 9.571428571428571, 8.0]]
'perm = [2, 1, 0, 3]'
'-------'
'Step 1'
'-------'
'L = '
[[1, 0.0, 0, 0.0],
 [1.0, 1, 0.0, 0.0],
 [-0.42857142857142855, -0.047619047619047596, 1, 0.0],
 [0.5714285714285714, 0.3968253968253968, 0.0, 1]]
'-------'
'U = '
[[7.0, -8.0, -8.0, -7.0],
 [0.0, 9.0, 11.0, 4.0],
 [0.0, 0.0, 1.0952380952380951, -2.8095238095238098],
 [0.0, 0.0, 5.2063492063492065, 6.412698412698413]]
'perm = [2, 1, 0, 3]'
'-------'
'Step 2'
'-------'
'L = '
[[1, 0.0, 0, 0.0],
 [1.0, 1, 0.0, 0.0],
 [0.5714285714285714, 0.3968253968253968, 1, 0],
 [-0.42857142857142855,
  -0.047619047619047596,
  0.21036585365853655,
  1]]
'-------'
'U = '
[[7.0, -8.0, -8.0, -7.0],
 [0.0, 9.0, 11.0, 4.0],
 [0.0, 0.0, 5.2063492063492065, 6.412698412698413],
 [0.0, 0.0, 0.0, -4.158536585365853]]
'perm = [2, 1, 3, 0]'
'-------'
'Step 3'
'-------'
'L = '
[[1, 0.0, 0, 0.0],
 [1.0, 1, 0.0, 0.0],
 [0.5714285714285714, 0.3968253968253968, 1, 0],
 [-0.42857142857142855,
  -0.047619047619047596,
  0.21036585365853655,
  1]]
'-------'
'U = '
[[7.0, -8.0, -8.0, -7.0],
 [0.0, 9.0, 11.0, 4.0],
 [0.0, 0.0, 5.2063492063492065, 6.412698412698413],
 [0.0, 0.0, 0.0, -4.158536585365853]]
'perm = [2, 1, 3, 0]'
In [10]:
L = np.array(L)
U = np.array(U)
P = np.array(P)
P.dot(L.dot(U))
Out[10]:
array([[-3.,  3.,  4.,  0.],
       [ 7.,  1.,  3., -3.],
       [ 7., -8., -8., -7.],
       [ 4., -1.,  5.,  4.]])