Skip to main content

multicalc/linear_algebra/
cholesky.rs

1//! Cholesky factorization for symmetric positive-definite matrices.
2//!
3//! A fixed-size `no_std` implementation of the standard Cholesky–Banachiewicz
4//! algorithm on this crate's own [`Vector`] and [`Matrix`] types; results are checked against
5//! numpy/LAPACK reference values.
6
7use crate::linear_algebra::{Matrix, Vector};
8use crate::scalar::Numeric;
9use crate::utils::error_codes::CalcError;
10
11/// A Cholesky factorization `A = L·Lᵀ`, as produced by [`Matrix::cholesky`].
12///
13/// `L` is lower-triangular with a strictly positive diagonal; the entries above the diagonal are
14/// zero. It exists only for a symmetric positive-definite `A`.
15#[derive(Debug, Clone, Copy)]
16#[must_use]
17pub struct Cholesky<const N: usize, T = f64> {
18    /// Lower-triangular factor `L`, where `A = L·Lᵀ`.
19    pub(crate) l: Matrix<N, N, T>,
20}
21
22impl<const N: usize, T: Numeric> Matrix<N, N, T> {
23    /// Factorizes `self` as `L·Lᵀ` by the Cholesky–Banachiewicz algorithm.
24    ///
25    /// Only the lower triangle is read; `self` is assumed symmetric. Returns
26    /// [`CalcError::NotPositiveDefinite`] if a diagonal radicand is not strictly positive — the
27    /// matrix is not positive definite — rather than taking a root of it.
28    ///
29    /// ```
30    /// use multicalc::linear_algebra::Matrix;
31    /// let a = Matrix::<3, 3>::new([[4.0, 12.0, -16.0], [12.0, 37.0, -43.0], [-16.0, -43.0, 98.0]]);
32    /// let l = a.cholesky().unwrap().l();
33    /// // L·Lᵀ == A.
34    /// let prod = l * l.transpose();
35    /// for r in 0..3 {
36    ///     for c in 0..3 {
37    ///         assert!((prod[(r, c)] - a[(r, c)]).abs() < 1e-12);
38    ///     }
39    /// }
40    /// ```
41    pub fn cholesky(self) -> Result<Cholesky<N, T>, CalcError> {
42        let mut l = Matrix::zeros();
43
44        for j in 0..N {
45            // Diagonal entry: subtract the squares already placed in row j.
46            let mut d = self[(j, j)];
47            for k in 0..j {
48                d -= l[(j, k)] * l[(j, k)];
49            }
50            if d <= T::ZERO {
51                return Err(CalcError::NotPositiveDefinite);
52            }
53            let ljj = d.sqrt();
54            l[(j, j)] = ljj;
55
56            // Below-diagonal entries of column j.
57            for i in (j + 1)..N {
58                let mut s = self[(i, j)];
59                for k in 0..j {
60                    s -= l[(i, k)] * l[(j, k)];
61                }
62                l[(i, j)] = s / ljj;
63            }
64        }
65
66        Ok(Cholesky { l })
67    }
68}
69
70impl<const N: usize, T: Numeric> Cholesky<N, T> {
71    /// The lower-triangular factor `L`, where `A = L·Lᵀ`.
72    pub fn l(&self) -> Matrix<N, N, T> {
73        self.l
74    }
75
76    /// The determinant, `Π L[i][i]²`.
77    ///
78    /// ```
79    /// use multicalc::linear_algebra::Matrix;
80    /// let a = Matrix::<2, 2>::new([[4.0, 2.0], [2.0, 3.0]]);
81    /// assert!((a.cholesky().unwrap().determinant() - a.determinant()).abs() < 1e-12);
82    /// ```
83    #[inline]
84    #[must_use]
85    pub fn determinant(&self) -> T {
86        let mut det = T::ONE;
87        for i in 0..N {
88            det *= self.l[(i, i)] * self.l[(i, i)];
89        }
90        det
91    }
92
93    /// Solves `A·x = b` for `x`, reusing this factorization.
94    ///
95    /// Infallible: the factorization already guaranteed every `L` diagonal entry is positive.
96    ///
97    /// ```
98    /// use multicalc::linear_algebra::{Matrix, Vector};
99    /// let a = Matrix::<2, 2>::new([[4.0, 2.0], [2.0, 3.0]]);
100    /// // A·x = b has the exact solution x = [1, 2].
101    /// let x = a.cholesky().unwrap().solve(Vector::new([8.0, 8.0]));
102    /// assert!((x[0] - 1.0).abs() < 1e-12);
103    /// assert!((x[1] - 2.0).abs() < 1e-12);
104    /// ```
105    pub fn solve(&self, b: Vector<N, T>) -> Vector<N, T> {
106        let mut x: [T; N] = core::array::from_fn(|i| b[i]);
107
108        // Forward substitution for L·y = b.
109        for i in 0..N {
110            let mut sum = x[i];
111            for (j, &xj) in x.iter().enumerate().take(i) {
112                sum -= self.l[(i, j)] * xj;
113            }
114            x[i] = sum / self.l[(i, i)];
115        }
116
117        // Back substitution for Lᵀ·x = y, where Lᵀ[i][j] = L[j][i].
118        for i in (0..N).rev() {
119            let mut sum = x[i];
120            for (j, &xj) in x.iter().enumerate().skip(i + 1) {
121                sum -= self.l[(j, i)] * xj;
122            }
123            x[i] = sum / self.l[(i, i)];
124        }
125
126        Vector::new(x)
127    }
128
129    /// Solves `A·X = B` for `X`, one column at a time, reusing this factorization.
130    ///
131    /// ```
132    /// use multicalc::linear_algebra::Matrix;
133    /// let a = Matrix::<2, 2>::new([[4.0, 2.0], [2.0, 3.0]]);
134    /// // Solving A·X = I gives X = A⁻¹.
135    /// let x = a.cholesky().unwrap().solve_matrix(Matrix::<2, 2>::identity());
136    /// let p = a * x;
137    /// assert!((p[(0, 0)] - 1.0).abs() < 1e-12);
138    /// assert!((p[(1, 1)] - 1.0).abs() < 1e-12);
139    /// ```
140    pub fn solve_matrix<const K: usize>(&self, b: Matrix<N, K, T>) -> Matrix<N, K, T> {
141        let mut result = Matrix::zeros();
142        for c in 0..K {
143            let x = self.solve(b.column(c));
144            for r in 0..N {
145                result[(r, c)] = x[r];
146            }
147        }
148        result
149    }
150
151    /// The inverse of the factorized matrix, from solving `A·X = I`.
152    ///
153    /// ```
154    /// use multicalc::linear_algebra::Matrix;
155    /// let a = Matrix::<3, 3>::new([[4.0, 12.0, -16.0], [12.0, 37.0, -43.0], [-16.0, -43.0, 98.0]]);
156    /// let p = a * a.cholesky().unwrap().inverse();
157    /// for r in 0..3 {
158    ///     for c in 0..3 {
159    ///         let expected = if r == c { 1.0 } else { 0.0 };
160    ///         assert!((p[(r, c)] - expected).abs() < 1e-12);
161    ///     }
162    /// }
163    /// ```
164    pub fn inverse(&self) -> Matrix<N, N, T> {
165        self.solve_matrix(Matrix::identity())
166    }
167}