#![allow(clippy::needless_range_loop)]
use super::GpError;
use crate::core::scalar::ControlScalar;
pub fn cholesky<S: ControlScalar, const N: usize>(a: &[[S; N]; N]) -> Result<[[S; N]; N], GpError> {
let mut l = [[S::ZERO; N]; N];
for j in 0..N {
let mut diag = a[j][j];
for k in 0..j {
diag -= l[j][k] * l[j][k];
}
if diag.to_f64() <= 0.0 {
return Err(GpError::NotPositiveDefinite);
}
l[j][j] = S::from_f64(libm::sqrt(diag.to_f64()));
for i in (j + 1)..N {
let mut off = a[i][j];
for k in 0..j {
off -= l[i][k] * l[j][k];
}
l[i][j] = off / l[j][j];
}
}
Ok(l)
}
pub fn forward_sub<S: ControlScalar, const N: usize>(
l: &[[S; N]; N],
b: &[S; N],
) -> Result<[S; N], GpError> {
let mut x = [S::ZERO; N];
for i in 0..N {
if l[i][i].to_f64() == 0.0 {
return Err(GpError::NumericalError);
}
let mut acc = b[i];
for j in 0..i {
acc -= l[i][j] * x[j];
}
x[i] = acc / l[i][i];
}
Ok(x)
}
pub fn backward_sub<S: ControlScalar, const N: usize>(
l: &[[S; N]; N],
b: &[S; N],
) -> Result<[S; N], GpError> {
let mut x = [S::ZERO; N];
for step in 0..N {
let i = N - 1 - step;
if l[i][i].to_f64() == 0.0 {
return Err(GpError::NumericalError);
}
let mut acc = b[i];
for j in (i + 1)..N {
acc -= l[j][i] * x[j];
}
x[i] = acc / l[i][i];
}
Ok(x)
}
pub fn cholesky_solve<S: ControlScalar, const N: usize>(
a: &[[S; N]; N],
b: &[S; N],
) -> Result<[S; N], GpError> {
let l = cholesky(a)?;
let y = forward_sub(&l, b)?;
backward_sub(&l, &y)
}
#[cfg(test)]
mod tests {
use super::*;
const TOL: f64 = 1e-10;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn cholesky_identity() {
let a = [[1.0_f64, 0.0], [0.0, 1.0]];
let l = cholesky(&a).expect("cholesky of identity must succeed");
assert!(approx_eq(l[0][0], 1.0, TOL));
assert!(approx_eq(l[1][0], 0.0, TOL));
assert!(approx_eq(l[1][1], 1.0, TOL));
}
#[test]
fn cholesky_known_2x2() {
let a = [[4.0_f64, 2.0], [2.0, 3.0]];
let l = cholesky(&a).expect("cholesky must succeed");
assert!(approx_eq(l[0][0], 2.0, TOL));
assert!(approx_eq(l[1][0], 1.0, TOL));
assert!(approx_eq(l[1][1], libm::sqrt(2.0_f64), TOL));
}
#[test]
fn cholesky_not_positive_definite() {
let a = [[-1.0_f64, 0.0], [0.0, 1.0]];
assert_eq!(cholesky(&a), Err(GpError::NotPositiveDefinite));
}
#[test]
fn cholesky_round_trip() {
let a = [[6.0_f64, 2.0, 1.0], [2.0, 5.0, 2.0], [1.0, 2.0, 4.0]];
let l = cholesky(&a).expect("cholesky must succeed");
for i in 0..3 {
for j in 0..3 {
let mut entry = 0.0_f64;
for k in 0..3 {
entry += l[i][k] * l[j][k];
}
assert!(approx_eq(entry, a[i][j], 1e-12), "A'[{i}][{j}] mismatch");
}
}
}
#[test]
fn forward_sub_identity() {
let l = [[1.0_f64, 0.0], [0.0, 1.0]];
let b = [3.0_f64, 5.0];
let x = forward_sub(&l, &b).expect("forward_sub must succeed");
assert!(approx_eq(x[0], 3.0, TOL));
assert!(approx_eq(x[1], 5.0, TOL));
}
#[test]
fn forward_sub_known() {
let sqrt2 = libm::sqrt(2.0_f64);
let l = [[2.0_f64, 0.0], [1.0, sqrt2]];
let b = [4.0_f64, 5.0];
let x = forward_sub(&l, &b).expect("forward_sub must succeed");
assert!(approx_eq(x[0], 2.0, TOL));
assert!(approx_eq(x[1], 3.0 / sqrt2, TOL));
}
#[test]
fn backward_sub_identity() {
let l = [[1.0_f64, 0.0], [0.0, 1.0]];
let b = [7.0_f64, -2.0];
let x = backward_sub(&l, &b).expect("backward_sub must succeed");
assert!(approx_eq(x[0], 7.0, TOL));
assert!(approx_eq(x[1], -2.0, TOL));
}
#[test]
fn cholesky_solve_residual() {
let a = [[4.0_f64, 2.0], [2.0, 3.0]];
let b = [1.0_f64, 2.0];
let x = cholesky_solve(&a, &b).expect("cholesky_solve must succeed");
let r0 = a[0][0] * x[0] + a[0][1] * x[1];
let r1 = a[1][0] * x[0] + a[1][1] * x[1];
assert!(approx_eq(r0, b[0], 1e-12), "residual[0] failed: got {r0}");
assert!(approx_eq(r1, b[1], 1e-12), "residual[1] failed: got {r1}");
}
#[test]
fn cholesky_solve_3x3() {
let a = [[6.0_f64, 2.0, 1.0], [2.0, 5.0, 2.0], [1.0, 2.0, 4.0]];
let b = [1.0_f64, 2.0, 3.0];
let x = cholesky_solve(&a, &b).expect("3x3 solve must succeed");
for i in 0..3 {
let mut row = 0.0_f64;
for j in 0..3 {
row += a[i][j] * x[j];
}
assert!(approx_eq(row, b[i], 1e-10), "row {i} residual: got {row}");
}
}
}