Why Cholesky Decomposition Matters
A simulation may need to solve the same stiffness system for many loads. A statistical model may need repeated solves with a covariance matrix and its log determinant. When the matrix is symmetric positive definite, Cholesky decomposition uses that structure: a single lower triangular factor and its transpose replace a general pair of triangular factors.
The standard form is $A=LL^T$, where $L$ has positive diagonal entries. Solving $Ax=b$ then requires a forward solve with $L$ and a backward solve with $L^T$. Dense Cholesky uses about half the leading factorization arithmetic of general dense LU, while storing only one triangular factor.
This lesson also makes pivoting explicit. Ordinary Cholesky does not require numerical pivoting for an exactly positive definite matrix. Our implementation uses a symmetric diagonal-pivoted variant: it reorders rows and columns together and computes $PAP^T=LL^T$. We will trace two swaps, preserve earlier factor entries, solve two right-hand sides, and distinguish a valid factorization from a numerical rejection.
For a symmetric positive definite matrix, Cholesky writes the system as a triangular factor times its transpose, so one factor supports repeated solves, determinant calculations, and covariance transformations. Symmetric pivoting reorders the variables while preserving the same structure: $PAP^T=LL^T$.
Core Concepts and Terminology
A real matrix $A$ is symmetric when $A=A^T$. It is positive definite when $v^TAv\gt0$ for every nonzero real vector $v$. Positive diagonal entries are necessary but insufficient. For example, $\begin{pmatrix}1&2\\2&1\end{pmatrix}$ has positive diagonal entries, yet $v=(1,-1)^T$ gives $v^TAv=-2$.
For symmetric positive definite $A\in\mathbb{R}^{n\times n}$, there is a unique lower triangular factor with positive diagonal satisfying $A=LL^T$. With a fixed symmetric permutation, the corresponding factor is also unique. Its diagonal is not generally all ones; the positive square roots are essential.
Our zero-based array $p$ specifies the final variable order. Define $P_{i,p_i}=1$ and all other entries zero. Then $(PAP^T)_{ij}=A_{p_i,p_j}$ and $(Pb)_i=b_{p_i}$. To solve, compute $Ly=Pb$, then $L^Tz=y$, then restore the original order by $x_{p_i}=z_i$. Both the right-hand side and the answer require the appropriate permutation operation.
The input contract is deliberately precise: a nonempty finite square matrix that is exactly symmetric in its supplied floating-point entries, plus a finite relative tolerance $0\lt\tau\lt1$. The code rejects nonpositive or too-small residual pivots. It does not return a low-rank factor for a semidefinite matrix or an indefinite factorization.
The LAPACK DPOTRF reference documents standard lower and upper Cholesky forms. Its interface can read only the selected input triangle; our teaching implementation checks the entire supplied matrix for exact symmetry before working on a copy.
| Term | Meaning | Worked example or role |
|---|---|---|
| Symmetric positive definite, SPD | $A=A^T$ and $v^TAv\gt0$ for all $v\ne0$ | Guarantees a positive-diagonal Cholesky factor in exact arithmetic |
| Lower factor $L$ | Lower triangular with $L_{ii}\gt0$ | Final rows are $(4,0,0)$, $(1,3,0)$, $(2,1,2)$ |
| Transpose $L^T$ | Entry $(i,j)$ equals $L_{ji}$ | Back substitution must read l[j][i] |
| Symmetric permutation $P$ | Reorders both rows and columns | Final $p=(2,0,1)$ means $(PAP^T)_{ij}=A_{p_i,p_j}$ |
| Residual diagonal $d_i$ | Current diagonal after subtracting earlier factor contributions | Before the second pivot, the active values in original units are $5$ and $9$ |
| Schur complement | Unfactored residual block after completed columns are removed | Its diagonal supplies the pivot candidates |
| Diagonal pivoting | Choose the largest remaining residual diagonal | Moves residual pivot $9$ ahead of $5$ |
| Relative threshold | Compare normalized residual pivots with $\tau$ | Equivalent to comparing original-unit pivots with $\tau\max_{i,j}\lvert A_{ij}\rvert$ |
| Positive semidefinite | $v^TAv\ge0$; zero directions may exist | Can have a zero pivot and no unique solve |
| Reusable factorization | Keep $L$ and $p$ while changing $b$ | Each later solve uses two triangular passes |
How Cholesky Decomposition Works, Step by Step
First consider the unpivoted formulas. At column $k$, match the entries of $A=LL^T$:
$$ L_{kk}=\sqrt{A_{kk}-\sum_{j=0}^{k-1}L_{kj}^2}, \qquad L_{ik}=\frac{A_{ik}-\sum_{j=0}^{k-1}L_{ij}L_{kj}}{L_{kk}} \quad(i\gt k). $$
Earlier columns are already available, so these expressions determine the next column from top to bottom. The diagonal expression must be positive before taking its square root. Our pivoted implementation uses the same formulas after reordering the remaining variables.
- Validate and scale. Check the square shape, finite entries, exact symmetry, and tolerance. Set $s=\max_{i,j}\lvert A_{ij}\rvert$ and reject $s=0$. Copy $W=A/s$, initialize the normalized factor $C$ to zero, set $p=(0,1,\ldots,n-1)$, and cache $d_i=W_{ii}$.
- Select a residual pivot. At column $k$, find the largest $d_i$ for $i\ge k$, keeping the first on an exact tie. Reject if that value is at most $\tau$. These are residual diagonals, not the unchanged original diagonal entries.
- Swap symmetrically. If the selected index is $r\ne k$, swap rows $k,r$ and columns $k,r$ of $W$. Swap $p_k,p_r$ and $d_k,d_r$. Swap only columns before $k$ between rows $k,r$ of $C$, moving the history already computed for those variables.
- Take the positive square root. Set $C_{kk}=\sqrt{d_k}$. For each $i\gt k$, compute $C_{ik}=(W_{ik}-\sum_{j\lt k}C_{ij}C_{kj})/C_{kk}$.
- Update the cached diagonals. For every $i\gt k$, set $d_i\leftarrow d_i-C_{ik}^2$. This maintains $d_i=W_{ii}-\sum_{j=0}^{k}C_{ij}^2$ for the next stage, without recomputing all the diagonal sums.
- Finish and rescale. Repeat until every column is complete. Return $L=\sqrt{s}\,C$ and $p$. Since $P(A/s)P^T=CC^T$, rescaling gives $PAP^T=LL^T$.
- Solve and restore order. Evaluate
$$ y_i=\frac{b_{p_i}-\sum_{j=0}^{i-1}L_{ij}y_j}{L_{ii}} \quad\text{from top to bottom}, \qquad z_i=\frac{y_i-\sum_{j=i+1}^{n-1}L_{ji}z_j}{L_{ii}} \quad\text{from bottom to top}. $$
Finally assign $x_{p_i}=z_i$. Empty sums are zero. The second pass uses the transpose of the same factor; neither an explicit inverse nor a separately stored upper factor is needed.
flowchart TD
A["Validate shape, symmetry, finite data, and tolerance"] --> B["Scale A; initialize factor, order, residual diagonals"]
B --> C{"More columns?"}
C -->|Yes| D["Select largest active residual diagonal"]
D --> E{"Pivot exceeds tolerance?"}
E -->|No| F["Report rejected positive pivot"]
E -->|Yes| G["Swap rows and columns, order, diagonals, factor prefix"]
G --> H["Take positive square root; compute lower column"]
H --> I["Update residual diagonals; advance column"]
I --> C
C -->|No| J["Rescale factor; forward solve with permuted b"]
J --> K["Back solve transpose; restore variable order"]
K --> M["Check factor product and original-system residual"]
Worked Example with a Full Trace
Take
$$ A=\begin{pmatrix}10&5&4\\5&9&8\\4&8&16\end{pmatrix}, \qquad b=\begin{pmatrix}32\\47\\68\end{pmatrix}. $$
The code uses $s=16$, stores $C=L/4$ during the computation, and caches residual diagonals divided by $16$. Below, factors and residuals are shown in original units so the integers remain visible. The final product will also independently establish that this symmetric matrix is positive definite.
Initialize. The variable order is $p=(0,1,2)$, the factor is zero, and the original-unit diagonal candidates are $(10,9,16)$.
Stage $k=0$: first symmetric swap. Choose index $2$, whose candidate is $16$. Swap indices $0$ and $2$ in both rows and columns. Now $p=(2,1,0)$ and
$$ B_1=\begin{pmatrix}16&8&4\\8&9&5\\4&5&10\end{pmatrix}. $$
The pivot is $16$, so $L_{00}=4$. Compute $L_{10}=8/4=2$ and $L_{20}=4/4=1$. The next residual diagonal candidates are $9-2^2=5$ and $10-1^2=9$. Thus the partial factor is
$$ L_{\mathrm{partial}}=\begin{pmatrix}4&0&0\\2&0&0\\1&0&0\end{pmatrix}, \qquad d_{\mathrm{active}}=(5,9). $$
Stage $k=1$: move a previously computed entry. Choose residual $9$, at current index $2$. Swap indices $1$ and $2$ symmetrically. Update $p=(2,0,1)$ and swap the completed factor entries $L_{10}=2$ and $L_{20}=1$. Immediately after the swap,
$$ B_2=\begin{pmatrix}16&4&8\\4&10&5\\8&5&9\end{pmatrix}, \qquad L_{\mathrm{partial}}=\begin{pmatrix}4&0&0\\1&0&0\\2&0&0\end{pmatrix}. $$
Take $L_{11}=\sqrt9=3$. Then $L_{21}=(5-L_{20}L_{10})/3=(5-2\cdot1)/3=1$. Update the final residual diagonal to $5-1^2=4$; equivalently, it is $9-2^2-1^2=4$.
Stage $k=2$: last positive pivot. The remaining pivot is $4$, so $L_{22}=2$. All normalized pivot candidates, $1$, $9/16$, and $1/4$, exceed $10^{-12}$. The final factor and its transpose are
$$ L=\begin{pmatrix}4&0&0\\1&3&0\\2&1&2\end{pmatrix}, \qquad L^T=\begin{pmatrix}4&1&2\\0&3&1\\0&0&2\end{pmatrix}. $$
Verify the product. Multiplying gives
$$ LL^T=\begin{pmatrix}16&4&8\\4&10&5\\8&5&9\end{pmatrix} =PAP^T, \qquad P=\begin{pmatrix}0&0&1\\1&0&0\\0&1&0\end{pmatrix}. $$
Since $L$ is invertible, $v^TAv=\lVert L^TPv\rVert_2^2\gt0$ for nonzero $v$. This verifies positive definiteness independently of the input's appearance.
Forward solve. Gather $Pb=(68,32,47)^T$. Compute $y_0=68/4=17$, $y_1=(32-1\cdot17)/3=5$, and $y_2=(47-2\cdot17-1\cdot5)/2=4$.
Transpose solve. Compute $z_2=4/2=2$, $z_1=(5-1\cdot2)/3=1$, and $z_0=(17-1\cdot1-2\cdot2)/4=3$. Thus $z=(3,1,2)^T$ in permuted coordinates. Restore $x_2=z_0=3$, $x_0=z_1=1$, and $x_1=z_2=2$, giving $x=(1,2,3)^T$. Direct multiplication yields $Ax=(32,47,68)^T$.
Reuse and determinant. For $b^{(2)}=(10,5,4)^T$, the same factors give $Pb^{(2)}=(4,10,5)^T$, $y^{(2)}=(1,3,0)^T$, $z^{(2)}=(0,1,0)^T$, and $x^{(2)}=(1,0,0)^T$. Also $\det(A)=(4\cdot3\cdot2)^2=576$. Symmetric permutations do not introduce a determinant sign change because the row and column permutation signs multiply to $1$.
Trace It by Hand
Pause after the first column. The active residuals are $5$ and $9$, and the stored first-column entries below the pivot are $2$ and $1$. Predict which index moves next, where those two entries go, and the last residual pivot.
Reveal: current index $2$ moves to index $1$. The factor prefix moves with it, so $L_{10}$ becomes $1$ and $L_{20}$ becomes $2$. Then $L_{11}=3$, $L_{21}=1$, and the last residual pivot is $4$.
Now solve for $b^{(3)}=(4,8,16)^T$ without refactoring. Check: $Pb^{(3)}=(16,4,8)^T$, $y=(4,0,0)^T$, $z=(1,0,0)^T$, and the restored answer is $x=(0,0,1)^T$. It matches the third column of the original matrix.
Implementation in Python, C++, and Java
Each panel is a complete program with identical sample data, pivot selection, scaling, and output. Run Python 3 directly, use C++14 or newer, or save the Java program as Main.java. The factor routine preserves $A$ and returns $L$ and a final order array $p$; the solve routine preserves its factors and $b$ and expects an unchanged successful factorization result.
The default tolerance is $\tau=10^{-12}$. It applies to residual diagonals of $A/s$, not to the square-root diagonal of $L$. A positive pivot smaller than the threshold is rejected even when the exact matrix is positive definite. For example, $\operatorname{diag}(1,10^{-14})$ is SPD but is rejected at this default tolerance. The threshold is a numerical policy, not a condition-number estimate.
The exact-symmetry check intentionally avoids silently replacing the user's matrix with a different one. If an application supplies approximately symmetric data, choose and document an appropriate symmetry test or symmetrization policy before calling this routine. A nonsymmetric matrix must not be accepted simply because one of its triangles produces plausible factor entries.
The factor diagnostic is $\max_{i,j}\lvert(LL^T)_{ij}-A_{p_i,p_j}\rvert/s$, while each solve diagnostic is $\lVert Ax-b\rVert_\infty$ in original coordinates. The sample checks both against $10^{-10}$ because its magnitudes are modest. General applications need scale-aware solve criteria. Scaling the factorization reduces some overflow hazards, but the implementation uses ordinary floating-point arithmetic, and the simple diagnostic dot products are intended for moderate inputs.
All three programs print these eight lines:
permutation = [2, 0, 1]L = [[4.000000, 0.000000, 0.000000], [1.000000, 3.000000, 0.000000], [2.000000, 1.000000, 2.000000]]x = [1.000000, 2.000000, 3.000000]reused x = [1.000000, 0.000000, 0.000000]relative factor error = 0.000e+00solve residual = 0.000e+00reuse residual = 0.000e+00check = PASS
Complexity, Correctness, and Invariants
For a successful dense $n\times n$ input, these implementations take $\Theta(n^3)$ factorization time in the best, average, and worst cases. They execute the dot-product loops even when entries are zero. Pivot searches, symmetric swaps, prefix swaps, and cached diagonal updates add $O(n^2)$ work overall. The leading factorization arithmetic is about $n^3/3$ operations when multiplication and subtraction are counted separately, compared with about $2n^3/3$ for general dense LU.
The code uses $\Theta(n^2)$ storage for the input copy and explicit square factor arrays, plus $O(n)$ permutation and diagonal arrays. Although only $n(n+1)/2$ factor entries are mathematically needed, the teaching code allocates full nested arrays. Each solve costs $\Theta(n^2)$ time and $\Theta(n)$ extra workspace. Factoring once and solving $q$ right-hand sides costs $\Theta(n^3+qn^2)$ total time. Rejection may stop early, but valid square input validation and copying already require quadratic work.
| Method | Dense factorization time | Each solve after factoring | Factor storage | Main requirement or tradeoff |
|---|---|---|---|---|
| Cholesky, including the diagonal-pivoted version here | $\Theta(n^3)$; leading count about $n^3/3$ | $\Theta(n^2)$ | One triangle mathematically; $\Theta(n^2)$ allocated here | Symmetric positive definite input with accepted pivots |
| Partial-pivoted LU | $\Theta(n^3)$; leading count about $2n^3/3$ | $\Theta(n^2)$ | Two triangles, possibly packed | General nonsingular square systems |
| Symmetric indefinite $LDL^T$ | $\Theta(n^3)$ | $\Theta(n^2)$ | Triangular factor and diagonal or block-diagonal $D$ | Suitable symmetric pivoting; can use $1\times1$ and $2\times2$ blocks |
| Householder QR | $\Theta(n^3)$ for square input | $\Theta(n^2)$ | Reflectors or explicit $Q$, plus $R$ | Also extends naturally to rectangular least squares |
| Conjugate gradients | No dense factorization required | Iterative: $O(kn^2)$ for $k$ dense matrix-vector steps | $O(n)$ vectors beyond the matrix | SPD input; convergence depends on conditioning and preconditioning |
To justify correctness in exact arithmetic, consider the currently permuted, normalized matrix $B=P(A/s)P^T$. After $k$ columns, let $C_k$ contain the completed columns of the normalized factor. The residual relation has the block form
$$ B=C_kC_k^T+\begin{pmatrix}0&0\\0&S_k\end{pmatrix}, $$
where $S_k$ is the remaining Schur complement. The cached diagonal satisfies $d_i=(S_k)_{ii}$ for active indices. Initially there are no completed columns and $S_0=A/s$.
A symmetric swap of active variables permutes both rows and columns of the residual block and the corresponding rows of $C_k$. Swapping the completed factor prefixes therefore preserves the relation. A positive pivot $d$ creates the new column with first active entry $\sqrt d$ and lower entries equal to the residual column divided by $\sqrt d$. Subtracting that column's outer product clears its row and column in the residual, leaving the next Schur complement. The dot products and diagonal updates in the code implement exactly this relation without explicitly storing the entire Schur complement.
For SPD input, every such Schur complement is SPD in exact arithmetic, so its diagonal pivots are positive and the process can continue. The code imposes the additional tolerance requirement. At termination, no residual block remains: $P(A/s)P^T=CC^T$, hence $PAP^T=LL^T$ after rescaling.
The solves then give $LL^Tz=Pb$. Since $z=Px$ and $P$ is invertible, this implies $Ax=b$. Positive factor diagonals make both triangular solves unique. In floating-point arithmetic these identities are approximate; failure on a nearly singular SPD matrix can reflect rounding and the chosen tolerance, while successful reconstruction does not by itself establish a small forward solution error for an ill-conditioned system.
Completed factor columns account for the corresponding rows and columns of the symmetrically permuted matrix, while the remaining error is its Schur complement. Each positive square-root pivot adds one factor column and shrinks that residual block; symmetric swaps must also move the completed factor prefixes to preserve the relation.
Variants, Pitfalls, and Optimizations
Positive diagonal is not enough. The matrix $\begin{pmatrix}1&2\\2&1\end{pmatrix}$ has a first Cholesky entry $1$ and a lower entry $2$, but its next residual pivot is $1-2^2=-3$. Stop before taking a real square root. Replacing the pivot by its absolute value would create $L_{\mathrm{fake}}=\begin{pmatrix}1&0\\2&\sqrt3\end{pmatrix}$, whose product has bottom-right entry $7$ instead of $1$: it factors a different matrix. A zero pivot likewise needs a separate semidefinite or rank-deficient policy.
Preserve symmetry and order. Swapping only rows destroys the symmetric problem. Swap rows and columns together, move earlier factor entries, gather $b_{p_i}$ for the forward solve, and scatter $x_{p_i}=z_i$ afterward. In the transpose solve, use $L_{ji}$ for an upper-triangular entry. A factor may look triangular while a missing prefix swap makes its product wrong.
Pivoted, unpivoted, and indefinite variants. Standard SPD Cholesky usually runs without numerical pivoting, possibly after a structural fill-reducing permutation. The LAPACK DPSTRF reference describes a complete-pivoting variant for semidefinite problems; its convention is $P_{\mathrm{lib}}^TAP_{\mathrm{lib}}=LL^T$, corresponding to our $P=P_{\mathrm{lib}}^T$. Its rank-return and stopping interface differ from our full-factor-or-error routine. For symmetric indefinite input, a suitably pivoted $LDL^T$ factorization is the appropriate different tool, rather than forcing Cholesky to continue.
Blocked, sparse, and approximate factors. Blocked Cholesky groups trailing updates into matrix operations for better cache use and optimized BLAS execution. Sparse Cholesky adds symbolic analysis and ordering to reduce fill-in; the largest-diagonal ordering used here is not a general fill-reduction strategy. Incomplete Cholesky drops selected contributions to build a preconditioner, with separate breakdown and stability considerations. Banded and tridiagonal SPD matrices admit specialized routines with much lower costs than dense storage and loops.
Updates and modeling changes. A rank-one update replaces $A$ by $A+uu^T$ and can update a dense factor in $O(n^2)$ work. A downdate $A-uu^T$ needs a positive-definiteness check and can fail. Adding a diagonal term $\lambda I$ can be an intentional regularization choice, but it changes the system; it must not be an unexplained repair for a failed pivot. Approximately symmetric data likewise require a deliberate policy.
Other uses of the factor. Compute $\log\det(A)=2\sum_i\log L_{ii}$ to avoid multiplying many diagonal values. For covariance $\Sigma$ with $P\Sigma P^T=LL^T$, an independent standard normal vector $g$ gives a correlated draw $P^TLg$. Full-rank covariance is needed for this invertible factor; degenerate covariance can require a semidefinite method. For complex Hermitian positive definite matrices, replace the transpose by the conjugate transpose. Avoid forming $A^{-1}$ merely to solve a system, and remember that forming normal equations can square the condition number of an underlying least-squares matrix.
Check symmetry and the residual pivot before taking a square root; positive original diagonal entries do not establish positive definiteness. Preserve the symmetric permutation through both factorization and solves, and never hide a failed pivot by taking an absolute value or silently changing the matrix.
Practice Problems and Real-World Use
Begin with a known triangular matrix $C$ whose diagonal is positive, form $A=CC^T$, and permute its variables. This guarantees an SPD test matrix in exact arithmetic and lets you check the factor product independently of the solver. To test solves, choose a known $x$, generate $b=Ax$, factor once, and recover several vectors from the same factors. Different pivot orders can yield different valid factors, so always compare with the appropriate symmetrically permuted matrix.
Include one-by-one inputs, a second-stage swap, zero and negative pivots, exact asymmetry, very small accepted pivots under a changed tolerance, and uniformly scaled systems. Verify lower-triangular shape, positive diagonals, input preservation, $LL^T\approx PAP^T$, and the original-system residual. Use a scale-aware residual when the matrix and vectors vary greatly in magnitude.
| Exercise or application | Concrete task and checkpoint | Idea tested | Difficulty |
|---|---|---|---|
| First two-by-two factor | Factor $\begin{pmatrix}4&2\\2&3\end{pmatrix}$ to get rows $(2,0)$ and $(1,\sqrt2)$ | Positive square-root pivot and one off-diagonal update | Beginner |
| Later symmetric swap | Reproduce $p=(2,0,1)$ and the integer factor in this lesson | Move rows, columns, cached diagonals, and factor prefixes together | Intermediate |
| Failure diagnosis | Compare $\begin{pmatrix}1&1/2\\1/2&1\end{pmatrix}$ with $\begin{pmatrix}1&2\\2&1\end{pmatrix}$ | Residual pivots $3/4$ versus $-3$ | Beginner |
| Reusable solve and determinant | Solve the three lesson right-hand sides and recover determinant $576$ | Two substitutions and squared diagonal product | Intermediate |
| Gaussian sampling | Transform independent standard normal samples using $P^TL$ | Covariance factorization and permutation recovery | Applied |
| Kernel system | Solve $(K+\lambda I)x=b$ for PSD $K$ and $\lambda\gt0$ | Deliberately obtain an SPD regularized system | Applied |
| Constrained stiffness system | Solve repeated loads after removing unconstrained rigid-body modes | SPD modeling assumptions and factor reuse | Applied |
| Sparse preconditioner | Compare sparse exact and incomplete Cholesky on an SPD discretization | Fill-in, approximation, and iterative solver tradeoffs | Advanced |