Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

 Open In Colab

What is LU Decomposition?

Any square matrix can be decomposed into the product of a lower triangular matrix (L\mathbf{L}) and an upper triangular matrix (U\mathbf{U}):

A=LU\mathbf{A} = \mathbf{L}\mathbf{U}

Where:

  • L\mathbf{L} has 1 s on the diagonal and the multipliers used during elimination below the diagonal.

  • U\mathbf{U} is exactly the upper-triangular matrix that remains after forward Gaussian elimination!

This means LU Decomposition is literally just Gaussian Elimination, but instead of throwing away the elimination steps, we save them inside the L\mathbf{L} matrix.

Assembling the L Matrix

Let’s look back at the 3×33 \times 3 system we solved in the Gaussian Elimination chapter. Our original coefficient matrix was:

A=[43−5−2−45880]\mathbf{A} = \begin{bmatrix} 4 & 3 & -5 \\ -2 & -4 & 5 \\ 8 & 8 & 0 \end{bmatrix}

During elimination, we used three multipliers:

  • m2,1=−0.5m_{2,1} = -0.5

  • m3,1=2m_{3,1} = 2

  • m3,2=−0.8m_{3,2} = -0.8

The lower triangular matrix L\mathbf{L} is constructed by simply placing these exact multipliers into their respective (i,j)(i, j) positions below the main diagonal (with 1s on the diagonal):

L=[100m2,110m3,1m3,21]=[100−0.5102−0.81]\mathbf{L} = \begin{bmatrix} 1 & 0 & 0 \\ m_{2,1} & 1 & 0 \\ m_{3,1} & m_{3,2} & 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 0 \\ -0.5 & 1 & 0 \\ 2 & -0.8 & 1 \end{bmatrix}

The upper triangular matrix U\mathbf{U} is simply the final matrix produced at the end of the forward elimination phase:

U=[43−50−2.52.50012]\mathbf{U} = \begin{bmatrix} 4 & 3 & -5 \\ 0 & -2.5 & 2.5 \\ 0 & 0 & 12 \end{bmatrix}

If we multiply these two matrices together, we perfectly recover our original A\mathbf{A} matrix! $$ \mathbf{L}\mathbf{U} = \begin{bmatrix} 1 & 0 & 0 \ -0.5 & 1 & 0 \ 2 & -0.8 & 1 \end{bmatrix}

[43−50−2.52.50012]\begin{bmatrix} 4 & 3 & -5 \\ 0 & -2.5 & 2.5 \\ 0 & 0 & 12 \end{bmatrix}

= \begin{bmatrix} 4 & 3 & -5 \ -2 & -4 & 5 \ 8 & 8 & 0 \end{bmatrix} = \mathbf{A} $$

Why store the elimination steps?

If we decompose A\mathbf{A} into LU\mathbf{L}\mathbf{U}, solving Ax=b\mathbf{A}\mathbf{x} = \mathbf{b} becomes a two-step process:

(LU)x=bL(Ux)=b\begin{aligned} (\mathbf{L}\mathbf{U})\mathbf{x} &= \mathbf{b} \\ \mathbf{L}(\mathbf{U}\mathbf{x}) &= \mathbf{b} \end{aligned}
  1. Let y=Ux\mathbf{y} = \mathbf{U}\mathbf{x}. First, solve Ly=b\mathbf{L}\mathbf{y} = \mathbf{b} using forward substitution (since L\mathbf{L} is lower triangular).

  2. Then, solve Ux=y\mathbf{U}\mathbf{x} = \mathbf{y} using backward substitution (since U\mathbf{U} is upper triangular).

The advantage: If you need to solve the same system for 100 different b\mathbf{b} vectors (e.g. testing different loads on a bridge), you only have to perform the expensive O(n3)\mathcal{O}(n^3) elimination once! The forward and backward substitutions only take O(n2)\mathcal{O}(n^2).

PLU Decomposition in Python

In reality, standard LU decomposition fails if a zero pivot is encountered. Robust algorithms use pivoting (row swapping), which introduces a Permutation matrix (P\mathbf{P}):

A=PLU\mathbf{A} = \mathbf{P}\mathbf{L}\mathbf{U}

Let’s decompose a matrix using Scipy!

Computational Complexity

Although forward and back substitution both occur in O(n2)\mathcal{O}(n^2), the actual LU\mathbf{L}\mathbf{U} decomposition itself still requires O(n3)\mathcal{O}(n^3) operations.

However, there are a number of major reasons why this is the preferred approach over standard Gaussian elimination:

  • The O(n3)\mathcal{O}(n^3) decomposition only needs to occur once, and the system can then be solved for multiple b\mathbf{b} vectors efficiently in O(n2)\mathcal{O}(n^2).

  • There are highly parallelized algorithms for distributed, GPU computing which can speed up the decomposition wall-time significantly.

  • The L\mathbf{L} and U\mathbf{U} factors preserve sparsity patterns inherently better than calculating exact inverses.

  • The factors are useful for other operations (e.g., calculating the determinant of A\mathbf{A} is simply the product of the diagonals of U\mathbf{U}).

Direct solvers scaling with O(n3)\mathcal{O}(n^3) is terrible. This implies doubling the number of variables increases the computational time by a factor of ~8!

Here sparsity comes to your rescue. Depending on the type of sparsity, one can typically get to algorithms ~ O(n2)\mathcal{O}(n^2) or better (depending on the structure of the sparsity)!

Sparse LU and the danger of “Fill-in”

If solving multiple b\mathbf{b} vectors is the goal, why don’t we just calculate the exact inverse A−1=U−1L−1\mathbf{A}^{-1} = \mathbf{U}^{-1} \mathbf{L}^{-1} once and just do x=A−1b\mathbf{x} = \mathbf{A}^{-1}\mathbf{b}?

The answer is Fill-in.

Fill-in is the phenomenon where, during computation, zeros in a sparse matrix become non-zeros. This destroys the sparsity pattern, requires massive amounts of memory, and drastically slows down the computation.

Let’s look at what happens when you try to take the inverse of a sparse banded matrix.

Yikes!

The exact inverse of a sparse banded matrix is almost always completely dense. If this matrix was 10,000×10,00010,000 \times 10,000, storing the inverse would instantly crash your computer’s RAM!

The LU Solution

This is why we use LU decomposition!

  • The LU decomposition of a banded matrix produces L\mathbf{L} and U\mathbf{U} matrices that are also banded!

  • LU decomposition completely prevents catastrophic fill-in, preserving our memory and computational speed.

Standard Software Solvers

In practice, you rarely write your own LU decomposition solver.

Numpy and Scipy both default to highly optimized (P)LU decomposition under the hood when you call their solve functions:

  • np.linalg.solve(A, b)

  • scipy.linalg.solve(A, b)

Sparse Software Solvers

If you know your matrix is sparse, you must explicitly use a sparse solver to prevent it from being treated as dense.

The advent of distributed computing motivated incredible algorithms suited for massive sparse systems:

  • PARADISO (PARallel Direct SOlver)

  • SuperLU (Supernodal LU)

  • UMFPACK (Unsymmetric-pattern MultiFrontal method)

Scipy provides scipy.sparse.linalg.spsolve, which routes to SuperLU or UMFPACK automatically! Let’s see the speed difference.