Skip to content

Commit bfb5336

Browse files
99991cclausspre-commit-ci[bot]
authored
Add cholesky_decomposition.py (#11848)
* Add cholesky_decomposition * Simplify equations, rename variables * Enforce symmetry on A * Fix typo * Rename variables * Rename variables * updating DIRECTORY.md * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --------- Co-authored-by: Christian Clauss <cclauss@me.com> Co-authored-by: cclauss <cclauss@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 21fea6a commit bfb5336

2 files changed

Lines changed: 121 additions & 0 deletions

File tree

DIRECTORY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@
720720
* [Chebyshev Distance](maths/chebyshev_distance.py)
721721
* [Check Polygon](maths/check_polygon.py)
722722
* [Chinese Remainder Theorem](maths/chinese_remainder_theorem.py)
723+
* [Cholesky Decomposition](maths/cholesky_decomposition.py)
723724
* [Chudnovsky Algorithm](maths/chudnovsky_algorithm.py)
724725
* [Collatz Sequence](maths/collatz_sequence.py)
725726
* [Combinations](maths/combinations.py)

maths/cholesky_decomposition.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import numpy as np
2+
3+
4+
def cholesky_decomposition(matrix: np.ndarray) -> np.ndarray:
5+
"""Return a Cholesky decomposition of the matrix A.
6+
7+
The Cholesky decomposition decomposes the square, positive definite matrix A
8+
into a lower triangular matrix L such that A = L L^T.
9+
10+
https://en.wikipedia.org/wiki/Cholesky_decomposition
11+
12+
Arguments:
13+
A -- a numpy.ndarray of shape (n, n)
14+
15+
>>> A = np.array([[4, 12, -16], [12, 37, -43], [-16, -43, 98]], dtype=float)
16+
>>> L = cholesky_decomposition(A)
17+
>>> np.allclose(L, np.array([[2, 0, 0], [6, 1, 0], [-8, 5, 3]]))
18+
True
19+
20+
>>> # check that the decomposition is correct
21+
>>> np.allclose(L @ L.T, A)
22+
True
23+
24+
>>> # check that L is lower triangular
25+
>>> np.allclose(np.tril(L), L)
26+
True
27+
28+
The Cholesky decomposition can be used to solve the linear system A x = y.
29+
30+
>>> x_true = np.array([1, 2, 3], dtype=float)
31+
>>> y = A @ x_true
32+
>>> x = solve_cholesky(L, y)
33+
>>> np.allclose(x, x_true)
34+
True
35+
36+
It can also be used to solve multiple equations A X = Y simultaneously.
37+
38+
>>> X_true = np.random.rand(3, 3)
39+
>>> Y = A @ X_true
40+
>>> X = solve_cholesky(L, Y)
41+
>>> np.allclose(X, X_true)
42+
True
43+
"""
44+
45+
assert matrix.shape[0] == matrix.shape[1], (
46+
f"Input matrix is not square, {matrix.shape=}"
47+
)
48+
assert np.allclose(matrix, matrix.T), "Input matrix must be symmetric"
49+
50+
n = matrix.shape[0]
51+
lower_triangle = np.tril(matrix)
52+
53+
for i in range(n):
54+
for j in range(i + 1):
55+
lower_triangle[i, j] -= np.sum(
56+
lower_triangle[i, :j] * lower_triangle[j, :j]
57+
)
58+
59+
if i == j:
60+
if lower_triangle[i, i] <= 0:
61+
raise ValueError("Matrix A is not positive definite")
62+
63+
lower_triangle[i, i] = np.sqrt(lower_triangle[i, i])
64+
else:
65+
lower_triangle[i, j] /= lower_triangle[j, j]
66+
67+
return lower_triangle
68+
69+
70+
def solve_cholesky(
71+
lower_triangle: np.ndarray,
72+
right_hand_side: np.ndarray,
73+
) -> np.ndarray:
74+
"""Given a Cholesky decomposition L L^T = A of a matrix A, solve the
75+
system of equations A X = Y where the right-hand side Y is either
76+
a matrix or a vector.
77+
78+
>>> L = np.array([[2, 0], [3, 4]], dtype=float)
79+
>>> Y = np.array([[22, 54], [81, 193]], dtype=float)
80+
>>> X = solve_cholesky(L, Y)
81+
>>> np.allclose(X, np.array([[1, 3], [3, 7]], dtype=float))
82+
True
83+
"""
84+
85+
assert lower_triangle.shape[0] == lower_triangle.shape[1], (
86+
f"Matrix L is not square, {lower_triangle.shape=}"
87+
)
88+
assert np.allclose(np.tril(lower_triangle), lower_triangle), (
89+
"Matrix L is not lower triangular"
90+
)
91+
92+
# Handle vector case by reshaping to matrix and then flattening again
93+
if len(right_hand_side.shape) == 1:
94+
return solve_cholesky(lower_triangle, right_hand_side.reshape(-1, 1)).ravel()
95+
96+
n = right_hand_side.shape[0]
97+
98+
# Solve L W = Y for W
99+
w = right_hand_side.copy()
100+
for i in range(n):
101+
for j in range(i):
102+
w[i] -= lower_triangle[i, j] * w[j]
103+
104+
w[i] /= lower_triangle[i, i]
105+
106+
# Solve L^T X = W for X
107+
x = w
108+
for i in reversed(range(n)):
109+
for j in range(i + 1, n):
110+
x[i] -= lower_triangle[j, i] * x[j]
111+
112+
x[i] /= lower_triangle[i, i]
113+
114+
return x
115+
116+
117+
if __name__ == "__main__":
118+
import doctest
119+
120+
doctest.testmod()

0 commit comments

Comments
 (0)