Skip to content

Cholesky Decomposition

Decompose positive definite matrices as \(A = LL^T\).

np.linalg.cholesky

1. Basic Usage

import numpy as np

def main():
    # Symmetric positive definite matrix
    A = np.array([[4, 2, 1],
                  [2, 5, 2],
                  [1, 2, 6]])

    L = np.linalg.cholesky(A)

    print("A =")
    print(A)
    print()
    print("L (lower triangular) =")
    print(L.round(4))

if __name__ == "__main__":
    main()

2. Mathematical Form

\[A = LL^T\]

where \(L\) is lower triangular.

3. Verify Result

import numpy as np

def main():
    A = np.array([[4, 2, 1],
                  [2, 5, 2],
                  [1, 2, 6]])

    L = np.linalg.cholesky(A)

    # Reconstruct
    A_reconstructed = L @ L.T

    print("Original A:")
    print(A)
    print()
    print("L @ L^T:")
    print(A_reconstructed.round(10))
    print()
    print(f"Match: {np.allclose(A, A_reconstructed)}")

if __name__ == "__main__":
    main()

Requirements

1. Symmetric Matrix

import numpy as np

def main():
    A = np.array([[4, 2],
                  [2, 3]])

    is_symmetric = np.allclose(A, A.T)
    print(f"A is symmetric: {is_symmetric}")

    L = np.linalg.cholesky(A)
    print(f"Cholesky succeeded: L =\n{L}")

if __name__ == "__main__":
    main()

2. Positive Definite

All eigenvalues must be positive.

import numpy as np

def main():
    A = np.array([[4, 2],
                  [2, 3]])

    eigenvalues = np.linalg.eigvalsh(A)
    is_positive_definite = np.all(eigenvalues > 0)

    print(f"Eigenvalues: {eigenvalues}")
    print(f"Positive definite: {is_positive_definite}")

if __name__ == "__main__":
    main()

3. Error for Non-PD

import numpy as np

def main():
    # Not positive definite (negative eigenvalue)
    A = np.array([[1, 2],
                  [2, 1]])

    print(f"Eigenvalues: {np.linalg.eigvalsh(A)}")

    try:
        L = np.linalg.cholesky(A)
    except np.linalg.LinAlgError as e:
        print(f"Error: {e}")

if __name__ == "__main__":
    main()

Creating PD Matrices

1. From Random Matrix

import numpy as np

def main():
    np.random.seed(42)

    # Random matrix
    B = np.random.randn(3, 3)

    # A = B @ B^T is positive semi-definite
    A = B @ B.T

    # Add small diagonal for strict positive definite
    A = A + 0.01 * np.eye(3)

    print("A =")
    print(A.round(3))
    print()
    print(f"Eigenvalues: {np.linalg.eigvalsh(A).round(4)}")

    L = np.linalg.cholesky(A)
    print("Cholesky succeeded!")

if __name__ == "__main__":
    main()

2. Covariance Matrix

import numpy as np

def main():
    np.random.seed(42)

    # Sample data
    X = np.random.randn(100, 3)

    # Sample covariance (symmetric, typically PD)
    cov = np.cov(X.T)

    print("Covariance matrix:")
    print(cov.round(3))
    print()

    L = np.linalg.cholesky(cov)
    print("Cholesky factor:")
    print(L.round(3))

if __name__ == "__main__":
    main()

3. Regularization

import numpy as np

def main():
    np.random.seed(42)

    # Nearly singular covariance
    X = np.random.randn(10, 5)
    cov = np.cov(X.T)

    # Add regularization
    reg = 1e-6
    cov_reg = cov + reg * np.eye(cov.shape[0])

    L = np.linalg.cholesky(cov_reg)
    print("Regularized Cholesky succeeded!")

if __name__ == "__main__":
    main()

Applications

1. Solving Linear Systems

Faster than general solve for PD matrices.

import numpy as np
from scipy import linalg

def main():
    A = np.array([[4, 2, 1],
                  [2, 5, 2],
                  [1, 2, 6]])
    b = np.array([1, 2, 3])

    # Method 1: Standard solve
    x1 = np.linalg.solve(A, b)

    # Method 2: Cholesky-based solve
    L = np.linalg.cholesky(A)
    # Solve L @ y = b
    y = linalg.solve_triangular(L, b, lower=True)
    # Solve L^T @ x = y
    x2 = linalg.solve_triangular(L.T, y, lower=False)

    print(f"Standard solve: {x1}")
    print(f"Cholesky solve: {x2}")
    print(f"Match: {np.allclose(x1, x2)}")

if __name__ == "__main__":
    main()

2. Multivariate Normal

Generate correlated random samples.

import numpy as np
import matplotlib.pyplot as plt

def main():
    np.random.seed(42)

    # Mean and covariance
    mean = np.array([0, 0])
    cov = np.array([[1, 0.8],
                    [0.8, 1]])

    # Cholesky factor
    L = np.linalg.cholesky(cov)

    # Generate samples: x = mean + L @ z
    n_samples = 1000
    z = np.random.randn(2, n_samples)
    samples = mean.reshape(-1, 1) + L @ z

    fig, ax = plt.subplots()
    ax.scatter(samples[0], samples[1], alpha=0.3, s=10)
    ax.set_xlabel('X1')
    ax.set_ylabel('X2')
    ax.set_title('Correlated Normal Samples via Cholesky')
    ax.set_aspect('equal')
    plt.show()

if __name__ == "__main__":
    main()

3. Log Determinant

Numerically stable computation.

import numpy as np

def main():
    A = np.array([[4, 2, 1],
                  [2, 5, 2],
                  [1, 2, 6]])

    L = np.linalg.cholesky(A)

    # log|A| = 2 * sum(log(diag(L)))
    log_det_cholesky = 2 * np.sum(np.log(np.diag(L)))

    # Direct computation
    sign, log_det_direct = np.linalg.slogdet(A)

    print(f"Log det (Cholesky): {log_det_cholesky:.6f}")
    print(f"Log det (slogdet):  {log_det_direct:.6f}")

if __name__ == "__main__":
    main()

scipy.linalg

1. Lower and Upper

import numpy as np
from scipy import linalg

def main():
    A = np.array([[4, 2],
                  [2, 3]])

    # Lower triangular (default)
    L = linalg.cholesky(A, lower=True)
    print("Lower Cholesky:")
    print(L)
    print()

    # Upper triangular
    U = linalg.cholesky(A, lower=False)
    print("Upper Cholesky:")
    print(U)
    print()

    # Verify: A = L @ L^T = U^T @ U
    print(f"L @ L^T matches A: {np.allclose(L @ L.T, A)}")
    print(f"U^T @ U matches A: {np.allclose(U.T @ U, A)}")

if __name__ == "__main__":
    main()

2. Cholesky Solve

import numpy as np
from scipy import linalg

def main():
    A = np.array([[4, 2, 1],
                  [2, 5, 2],
                  [1, 2, 6]])
    b = np.array([1, 2, 3])

    # Direct Cholesky solve
    L = linalg.cholesky(A, lower=True)
    x = linalg.cho_solve((L, True), b)

    print(f"Solution: {x}")
    print(f"Verify A @ x = {A @ x}")

if __name__ == "__main__":
    main()

3. Check for PD

import numpy as np
from scipy import linalg

def main():
    A = np.array([[4, 2],
                  [2, 3]])

    try:
        L = linalg.cholesky(A, lower=True, check_finite=True)
        print("Matrix is positive definite")
    except linalg.LinAlgError:
        print("Matrix is NOT positive definite")

if __name__ == "__main__":
    main()

Performance

1. Efficiency

Cholesky is ~2x faster than LU decomposition for PD matrices.

import numpy as np
import time

def main():
    n = 1000

    # Create PD matrix
    B = np.random.randn(n, n)
    A = B @ B.T + np.eye(n)

    # Cholesky timing
    start = time.perf_counter()
    L = np.linalg.cholesky(A)
    cholesky_time = time.perf_counter() - start

    # General solve timing (uses LU)
    b = np.random.randn(n)
    start = time.perf_counter()
    x = np.linalg.solve(A, b)
    solve_time = time.perf_counter() - start

    print(f"Cholesky decomposition: {cholesky_time:.4f} sec")
    print(f"General solve: {solve_time:.4f} sec")

if __name__ == "__main__":
    main()

2. Memory

Lower triangular storage uses half the memory.

3. Numerical Stability

Cholesky is numerically stable for PD matrices.