use super::{abs, n_as_scalar};
use crate::scalar::{FloatTolerance, Scalar};
use crate::storage::Storage;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CholeskyError {
DimensionMismatch,
NotSymmetric,
NotPositiveDefinite,
}
pub fn cholesky<S, T>(a: &S, rows: usize, cols: usize, out_l: &mut [T]) -> Result<(), CholeskyError>
where
S: Storage<Item = T>,
T: Scalar + FloatTolerance + PartialOrd,
{
let n = rows;
let mut scale = T::zero();
for j in 0..n.min(cols) {
let Some(&a_jj) = a.get(j * cols + j) else {
return Err(CholeskyError::DimensionMismatch);
};
let a_jj_abs = abs(a_jj);
if a_jj_abs > scale {
scale = a_jj_abs;
}
}
let tolerance = n_as_scalar::<T>(n).mul(T::epsilon()).mul(scale);
cholesky_decompose(a, rows, cols, out_l, tolerance)
}
pub fn cholesky_decompose<S, T>(
a: &S,
rows: usize,
cols: usize,
out_l: &mut [T],
tolerance: T,
) -> Result<(), CholeskyError>
where
S: Storage<Item = T>,
T: Scalar + PartialOrd,
{
if rows != cols {
return Err(CholeskyError::DimensionMismatch);
}
let n = rows;
let len = n * n;
if a.len() != len || out_l.len() != len {
return Err(CholeskyError::DimensionMismatch);
}
let zero = T::zero();
for i in 0..n {
for j in (i + 1)..n {
let Some(&a_ij) = a.get(i * n + j) else {
return Err(CholeskyError::DimensionMismatch);
};
let Some(&a_ji) = a.get(j * n + i) else {
return Err(CholeskyError::DimensionMismatch);
};
if abs(a_ij.sub(a_ji)) > tolerance {
return Err(CholeskyError::NotSymmetric);
}
}
}
for slot in out_l.iter_mut() {
*slot = zero;
}
for j in 0..n {
let mut diag_sum = zero;
for k in 0..j {
let l_jk = out_l[j * n + k];
diag_sum = diag_sum.add(l_jk.mul(l_jk));
}
let Some(&a_jj) = a.get(j * n + j) else {
return Err(CholeskyError::DimensionMismatch);
};
let diag_sq = a_jj.sub(diag_sum);
if diag_sq < zero.sub(tolerance) {
return Err(CholeskyError::NotPositiveDefinite);
}
let l_jj = diag_sq.sqrt();
out_l[j * n + j] = l_jj;
if l_jj == zero {
continue;
}
for i in (j + 1)..n {
let mut sum = zero;
for k in 0..j {
sum = sum.add(out_l[i * n + k].mul(out_l[j * n + k]));
}
let Some(&a_ij) = a.get(i * n + j) else {
return Err(CholeskyError::DimensionMismatch);
};
out_l[i * n + j] = a_ij.sub(sum).div(l_jj);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::{CholeskyError, cholesky, cholesky_decompose};
use crate::algorithm::matrix::{mul_matrix, transpose};
use crate::storage::StaticStorage;
#[test]
fn cholesky_of_known_2x2_positive_definite_matrix_l_times_l_transpose_reconstructs_a() {
let a = StaticStorage::new([4.0_f64, 2.0, 2.0, 2.0]);
let mut l = [0.0; 4];
assert_eq!(cholesky_decompose(&a, 2, 2, &mut l, 1e-9), Ok(()));
assert_eq!(l, [2.0, 0.0, 1.0, 1.0]);
let mut l_t = [0.0; 4];
transpose(&StaticStorage::new(l), 2, 2, &mut l_t).unwrap();
let mut l_l_t = [0.0; 4];
mul_matrix(
&StaticStorage::new(l),
2,
2,
&StaticStorage::new(l_t),
2,
2,
&mut l_l_t,
)
.unwrap();
for (actual, expected) in l_l_t.iter().zip([4.0, 2.0, 2.0, 2.0]) {
assert!((actual - expected).abs() < 1e-9);
}
}
#[test]
fn cholesky_of_known_3x3_positive_definite_matrix_l_times_l_transpose_reconstructs_a() {
let a = StaticStorage::new([4.0_f64, 12.0, -16.0, 12.0, 37.0, -43.0, -16.0, -43.0, 98.0]);
let mut l = [0.0; 9];
assert_eq!(cholesky_decompose(&a, 3, 3, &mut l, 1e-9), Ok(()));
assert_eq!(l, [2.0, 0.0, 0.0, 6.0, 1.0, 0.0, -8.0, 5.0, 3.0]);
let mut l_t = [0.0; 9];
transpose(&StaticStorage::new(l), 3, 3, &mut l_t).unwrap();
let mut l_l_t = [0.0; 9];
mul_matrix(
&StaticStorage::new(l),
3,
3,
&StaticStorage::new(l_t),
3,
3,
&mut l_l_t,
)
.unwrap();
let expected_a = [4.0, 12.0, -16.0, 12.0, 37.0, -43.0, -16.0, -43.0, 98.0];
for (actual, expected) in l_l_t.iter().zip(expected_a) {
assert!((actual - expected).abs() < 1e-9);
}
}
#[test]
fn cholesky_decompose_of_non_symmetric_matrix_is_an_error() {
let a = StaticStorage::new([4.0_f64, 99.0, 2.0, 2.0]);
let mut l = [0.0; 4];
assert_eq!(
cholesky_decompose(&a, 2, 2, &mut l, 1e-9),
Err(CholeskyError::NotSymmetric)
);
}
#[test]
fn cholesky_decompose_nearly_symmetric_within_tolerance_is_accepted() {
let a = StaticStorage::new([4.0_f64, 2.0 + 1e-12, 2.0, 2.0]);
let mut l = [0.0; 4];
assert_eq!(cholesky_decompose(&a, 2, 2, &mut l, 1e-9), Ok(()));
}
#[test]
fn cholesky_high_level_of_non_symmetric_matrix_is_an_error() {
let a = StaticStorage::new([4.0_f64, 99.0, 2.0, 2.0]);
let mut l = [0.0; 4];
assert_eq!(cholesky(&a, 2, 2, &mut l), Err(CholeskyError::NotSymmetric));
}
#[test]
fn cholesky_of_non_positive_definite_matrix_is_an_error() {
let a = StaticStorage::new([1.0, 2.0, 2.0, 1.0]);
let mut l = [0.0; 4];
assert_eq!(
cholesky_decompose(&a, 2, 2, &mut l, 1e-9),
Err(CholeskyError::NotPositiveDefinite)
);
}
#[test]
fn cholesky_of_non_square_matrix_is_an_error_not_a_panic() {
let a = StaticStorage::new([1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
let mut l = [0.0; 9];
assert_eq!(
cholesky_decompose(&a, 2, 3, &mut l, 1e-9),
Err(CholeskyError::DimensionMismatch)
);
}
#[test]
fn cholesky_mismatched_output_length_is_an_error_not_a_panic() {
let a = StaticStorage::new([4.0, 2.0, 2.0, 2.0]);
let mut l = [0.0; 3];
assert_eq!(
cholesky_decompose(&a, 2, 2, &mut l, 1e-9),
Err(CholeskyError::DimensionMismatch)
);
}
#[test]
fn cholesky_matches_cholesky_decompose() {
let a = StaticStorage::new([4.0, 2.0, 2.0, 2.0]);
let mut l_high_level = [0.0; 4];
assert_eq!(cholesky(&a, 2, 2, &mut l_high_level), Ok(()));
let mut l_explicit = [0.0; 4];
assert_eq!(cholesky_decompose(&a, 2, 2, &mut l_explicit, 1e-9), Ok(()));
assert_eq!(l_high_level, l_explicit);
}
#[test]
fn cholesky_default_tolerance_absorbs_rounding_noise_on_a_genuinely_psd_matrix() {
let a = StaticStorage::new([3.0_f64, 3.0, 3.0, 3.0]);
let mut l = [0.0; 4];
assert_eq!(cholesky(&a, 2, 2, &mut l), Ok(()));
assert_eq!(l[3], 0.0);
}
#[test]
fn cholesky_explicit_zero_tolerance_rejects_the_same_rounding_noise() {
let a = StaticStorage::new([3.0_f64, 3.0, 3.0, 3.0]);
let mut l = [0.0; 4];
assert_eq!(
cholesky_decompose(&a, 2, 2, &mut l, 0.0),
Err(CholeskyError::NotPositiveDefinite)
);
}
#[test]
fn cholesky_rejects_upper_triangle_mismatch_across_larger_matrix() {
let a = StaticStorage::new([
4.0_f64, 99.0, -16.0, 2.0, 2.0, 43.0, -8.0, 5.0, 98.0, ]);
let mut l = [0.0; 9];
assert_eq!(
cholesky_decompose(&a, 3, 3, &mut l, 1e-9),
Err(CholeskyError::NotSymmetric)
);
}
}