qfall_math/rational/mat_q/
cholesky_decomp.rs

1// Copyright © 2024 Marvin Beckmann
2//
3// This file is part of qFALL-math.
4//
5// qFALL-math is free software: you can redistribute it and/or modify it under
6// the terms of the Mozilla Public License Version 2.0 as published by the
7// Mozilla Foundation. See <https://mozilla.org/en-US/MPL/2.0/>.
8
9//! This module implements the Cholesky Decomposition for Hermitian positive-definite
10//! matrices.
11
12use super::MatQ;
13use crate::{
14    rational::Q,
15    traits::{
16        Concatenate, MatrixDimensions, MatrixGetEntry, MatrixGetSubmatrix, MatrixSetEntry,
17        MatrixSetSubmatrix,
18    },
19};
20
21impl MatQ {
22    /// This function performs the Cholesky decomposition (the Cholesky algorithm) and
23    /// produces a matrix `L` such that `self = L * L^T`.
24    /// This function relies on the precision of [`Q::sqrt`] and will not provide
25    /// perfect results in all cases.
26    /// Furthermore, the Cholesky decomposition requires a Hermitian positive-definite
27    /// matrix.
28    ///
29    /// Returns the Cholesky decomposition of a Hermitian positive-definite matrix.
30    ///
31    /// # Examples
32    /// ```
33    /// use qfall_math::rational::MatQ;
34    /// use std::str::FromStr;
35    ///
36    /// let matrix = MatQ::from_str("[[4, 12, -16],[12,37,-43],[-16,-43,98]]").unwrap();
37    ///
38    /// let l = matrix.cholesky_decomposition();
39    /// assert_eq!(matrix, &l * l.transpose());
40    /// ```
41    ///
42    /// # Panics ...
43    /// - if `self` is not a symmetric matrix,
44    /// - if `self` has eigenvalues smaller than `0`.
45    pub fn cholesky_decomposition(&self) -> MatQ {
46        assert!(self.is_symmetric(), "The provided matrix is not symmetric.");
47        let n = self.get_num_columns();
48
49        let mut a = self.clone();
50        let mut l = MatQ::identity(n, n);
51
52        for i in 0..n {
53            // get first entry and ith column
54            let a_ii = unsafe { a.get_entry_unchecked(0, 0) };
55            assert!(a_ii > Q::ZERO, "The matrix is not positive-definite.");
56            let column_a_i = match i {
57                0 => unsafe { a.get_column_unchecked(0) },
58                _ => unsafe { MatQ::new(i, 1).get_column_unchecked(0) }
59                    .concat_vertical(&unsafe { a.get_column_unchecked(0) })
60                    .unwrap(),
61            } * (1 / (a_ii.sqrt()));
62            // in the previous line: sqrt panics if `a_ii` is negative, i.e. if an
63            // eigenvalue is negative.
64
65            // produce L matrix recursively
66            let mut l_i = MatQ::identity(n, n);
67            unsafe { l_i.set_column_unchecked(i, &column_a_i, 0) };
68            l = l * l_i;
69
70            // update matrix A recursively
71            if i < n - 1 {
72                let b = a.get_submatrix(1, -1, 1, -1).unwrap();
73                let b_minus = (1 / a_ii)
74                    * a.get_submatrix(1, -1, 0, 0).unwrap()
75                    * a.get_submatrix(0, 0, 1, -1).unwrap();
76                a = b - b_minus;
77            }
78        }
79        l
80    }
81
82    /// This function implements the Cholesky decomposition according to FLINTs
83    /// implementation. As FLINTs algorithm is not (yet) accessible through flint-sys,
84    /// this implementation follows the implementation of the algorithm from FLINT.
85    /// This, however, also means that we will work with less precision as we will work
86    /// with conversions to [`f64`] and not use [`Q`].
87    /// In turn, this makes the function much more efficient, but *not* applicable to
88    /// large numbers.
89    ///
90    /// This function relies on the precision of [`f64::sqrt`] and will not provide
91    /// perfect results in all cases.
92    /// Furthermore, the Cholesky decomposition requires a Hermitian positive-definite
93    /// matrix.
94    ///
95    /// Returns the Cholesky decomposition of a Hermitian positive-definite matrix.
96    ///
97    /// # Examples
98    /// ```
99    /// use qfall_math::rational::MatQ;
100    /// use std::str::FromStr;
101    ///
102    /// let matrix = MatQ::from_str("[[4, 12, -16],[12,37,-43],[-16,-43,98]]").unwrap();
103    ///
104    /// let l = matrix.cholesky_decomposition_flint();
105    /// assert_eq!(matrix, &l * l.transpose());
106    /// ```
107    ///
108    /// # Panics ...
109    /// - if `self` is not a symmetric matrix,
110    /// - if `self` has eigenvalues smaller than `0`.
111    #[allow(clippy::needless_range_loop)]
112    pub fn cholesky_decomposition_flint(&self) -> MatQ {
113        assert!(self.is_symmetric(), "The provided matrix is not symmetric.");
114        let mat_dimension = self.get_num_rows() as usize;
115
116        let mut out = vec![vec![0.0; mat_dimension]; mat_dimension];
117        let mat = self.collect_entries_f64();
118
119        // This code snippet originates from [flint](https://github.com/flintlib/flint/blob/main/src/fmpz_mat/chol_d.c)
120        // it is not part of [flint-sys] as it requires a specific data-type `d_mat_t`
121        for i in 0..mat_dimension {
122            for j in 0..=i {
123                let mut s: f64 = 0.0;
124                for k in 0..j {
125                    s += out[i][k] * out[j][k]
126                }
127                if i == j {
128                    // Find this requirement in https://en.wikipedia.org/wiki/Cholesky_decomposition#The_Cholesky_algorithm
129                    // a_ii > 0 as `self` needs to be positive definite
130                    assert!(
131                        mat[i][i] > s,
132                        "The provided matrix is not positive definite."
133                    );
134
135                    out[i][j] = (mat[i][i] - s).sqrt();
136                } else {
137                    out[i][j] = (mat[i][j] - s) / out[j][j];
138                }
139            }
140        }
141
142        // Assemble Cholesky decomposition as MatQ
143        let mut res = MatQ::new(mat_dimension, mat_dimension);
144        for (i, row) in out.iter().enumerate().take(mat_dimension) {
145            for (j, entry) in row.iter().enumerate().take(mat_dimension) {
146                unsafe { res.set_entry_unchecked(i as i64, j as i64, *entry) };
147            }
148        }
149
150        res
151    }
152}
153
154#[cfg(test)]
155mod test_cholesky_decomposition {
156    use crate::{
157        rational::{MatQ, Q},
158        traits::MatrixSetEntry,
159    };
160    use std::str::FromStr;
161
162    /// Ensure that a basic example (from Wikipedia) works.
163    #[test]
164    fn valid_input() {
165        let matrix = MatQ::from_str("[[4, 12, -16],[12,37,-43],[-16,-43,98]]").unwrap();
166
167        let l = MatQ::from_str("[[2, 0, 0],[6, 1, 0],[-8, 5, 3]]").unwrap();
168        assert_eq!(l, matrix.cholesky_decomposition());
169    }
170
171    /// Ensure that the function panics if a non-square matrix is provided
172    #[test]
173    #[should_panic]
174    fn non_square() {
175        let matrix = MatQ::new(3, 2);
176
177        matrix.cholesky_decomposition();
178    }
179
180    /// Ensure that the function panics if a matrix with negative eigenvalues is provided
181    #[test]
182    #[should_panic]
183    fn non_positive_definite() {
184        let matrix: MatQ = -1 * MatQ::identity(2, 2);
185
186        matrix.cholesky_decomposition();
187    }
188
189    /// Ensure that the function panics if a non-symmetric matrix is provided
190    #[test]
191    #[should_panic]
192    fn non_symmetric() {
193        let mut matrix: MatQ = MatQ::identity(2, 2);
194        matrix.set_entry(1, 0, Q::MINUS_ONE).unwrap();
195
196        matrix.cholesky_decomposition();
197    }
198
199    /// Ensure that the function works with large entries
200    #[test]
201    fn large_entries() {
202        // matrix = [[1,-2^32],[-2^{32},2^64+1]] -> L = [[1,0],[-2^32,1]]
203        let matrix: MatQ = MatQ::from_str(&format!(
204            "[[{},-{}],[-{},{}]]",
205            -1,
206            2_i64.pow(32),
207            2_i64.pow(32),
208            u64::MAX
209        ))
210        .unwrap()
211            + 2 * MatQ::identity(2, 2);
212
213        assert_eq!(
214            matrix,
215            (matrix.cholesky_decomposition() * matrix.cholesky_decomposition().transpose())
216        );
217    }
218}
219
220#[cfg(test)]
221mod test_cholesky_decomposition_flint {
222    use crate::{
223        rational::{MatQ, Q},
224        traits::MatrixSetEntry,
225    };
226    use std::str::FromStr;
227
228    /// Ensure that a basic example (from Wikipedia) works.
229    #[test]
230    fn valid_input() {
231        let matrix = MatQ::from_str("[[4, 12, -16],[12,37,-43],[-16,-43,98]]").unwrap();
232
233        let l = MatQ::from_str("[[2, 0, 0],[6, 1, 0],[-8, 5, 3]]").unwrap();
234        assert_eq!(l, matrix.cholesky_decomposition_flint());
235    }
236
237    /// Ensure that the function panics if a non-square matrix is provided
238    #[test]
239    #[should_panic]
240    fn non_square() {
241        let matrix = MatQ::new(3, 2);
242
243        matrix.cholesky_decomposition_flint();
244    }
245
246    /// Ensure that the function panics if a matrix with negative eigenvalues is provided
247    #[test]
248    #[should_panic]
249    fn non_positive_definite() {
250        let matrix: MatQ = -1 * MatQ::identity(2, 2);
251
252        matrix.cholesky_decomposition_flint();
253    }
254
255    /// Ensure that the function panics if a non-symmetric matrix is provided
256    #[test]
257    #[should_panic]
258    fn non_symmetric() {
259        let mut matrix: MatQ = MatQ::identity(2, 2);
260        matrix.set_entry(1, 0, Q::MINUS_ONE).unwrap();
261
262        matrix.cholesky_decomposition_flint();
263    }
264}