use crate::error::{SparseError, SparseResult};
use super::types::{estimate_condition, CholUpdateConfig, CholUpdateResult};
pub fn cholesky_factorize(a: &[Vec<f64>]) -> SparseResult<Vec<Vec<f64>>> {
let n = a.len();
if n == 0 {
return Ok(Vec::new());
}
for (i, row) in a.iter().enumerate() {
if row.len() != n {
return Err(SparseError::ComputationError(format!(
"Row {} has length {} but matrix dimension is {}",
i,
row.len(),
n
)));
}
}
let mut l = vec![vec![0.0; n]; n];
for j in 0..n {
let mut sum = 0.0;
for k in 0..j {
sum += l[j][k] * l[j][k];
}
let diag = a[j][j] - sum;
if diag <= 0.0 {
return Err(SparseError::ComputationError(format!(
"Matrix is not positive definite: diagonal element at ({},{}) is {:.6e}",
j, j, diag
)));
}
l[j][j] = diag.sqrt();
for i in (j + 1)..n {
let mut s = 0.0;
for k in 0..j {
s += l[i][k] * l[j][k];
}
l[i][j] = (a[i][j] - s) / l[j][j];
}
}
Ok(l)
}
pub fn cholesky_rank1_update(l: &[Vec<f64>], u: &[f64], alpha: f64) -> SparseResult<Vec<Vec<f64>>> {
cholesky_rank1_update_with_config(l, u, alpha, &CholUpdateConfig::default())
}
pub fn cholesky_rank1_update_with_config(
l: &[Vec<f64>],
u: &[f64],
alpha: f64,
config: &CholUpdateConfig,
) -> SparseResult<Vec<Vec<f64>>> {
let n = l.len();
if alpha <= 0.0 {
return Err(SparseError::ValueError(
"alpha must be positive for a rank-1 update".into(),
));
}
if u.len() != n {
return Err(SparseError::DimensionMismatch {
expected: n,
found: u.len(),
});
}
for (i, row) in l.iter().enumerate() {
if row.len() != n {
return Err(SparseError::ComputationError(format!(
"Factor row {} has length {} but expected {}",
i,
row.len(),
n
)));
}
}
if n == 0 {
return Ok(Vec::new());
}
let mut l_new = l.to_vec();
let sqrt_alpha = alpha.sqrt();
let mut w: Vec<f64> = u.iter().map(|&v| sqrt_alpha * v).collect();
for j in 0..n {
let lj = l_new[j][j];
let wj = w[j];
let r = (lj * lj + wj * wj).sqrt();
if r < config.tolerance {
return Err(SparseError::ComputationError(format!(
"Numerical breakdown at column {}: r = {:.6e}",
j, r
)));
}
let c = lj / r; let s = wj / r;
l_new[j][j] = r;
for i in (j + 1)..n {
let li = l_new[i][j];
let wi = w[i];
l_new[i][j] = c * li + s * wi;
w[i] = -s * li + c * wi;
}
}
if config.check_positive_definite {
for j in 0..n {
if l_new[j][j] <= config.tolerance {
return Err(SparseError::ComputationError(format!(
"Updated factor is not positive definite at diagonal ({},{}): {:.6e}",
j, j, l_new[j][j]
)));
}
}
}
Ok(l_new)
}
pub fn cholesky_rank1_update_result(
l: &[Vec<f64>],
u: &[f64],
alpha: f64,
config: &CholUpdateConfig,
) -> CholUpdateResult {
match cholesky_rank1_update_with_config(l, u, alpha, config) {
Ok(factor) => {
let cond = estimate_condition(&factor, config.tolerance);
CholUpdateResult {
factor,
successful: true,
condition_estimate: cond,
}
}
Err(_) => CholUpdateResult {
factor: l.to_vec(),
successful: false,
condition_estimate: f64::INFINITY,
},
}
}
pub(crate) fn llt(l: &[Vec<f64>]) -> Vec<Vec<f64>> {
let n = l.len();
let mut a = vec![vec![0.0; n]; n];
for i in 0..n {
for j in 0..n {
let mut s = 0.0;
for k in 0..n {
s += l[i][k] * l[j][k];
}
a[i][j] = s;
}
}
a
}
#[cfg(test)]
mod tests {
use super::*;
fn spd_3x3() -> (Vec<Vec<f64>>, Vec<Vec<f64>>) {
let a = vec![
vec![4.0, 2.0, 1.0],
vec![2.0, 5.0, 3.0],
vec![1.0, 3.0, 6.0],
];
let l = cholesky_factorize(&a).expect("factorize");
(a, l)
}
fn assert_matrices_close(a: &[Vec<f64>], b: &[Vec<f64>], tol: f64) {
assert_eq!(a.len(), b.len());
for i in 0..a.len() {
assert_eq!(a[i].len(), b[i].len());
for j in 0..a[i].len() {
assert!(
(a[i][j] - b[i][j]).abs() < tol,
"Mismatch at ({},{}): {} vs {} (diff {})",
i,
j,
a[i][j],
b[i][j],
(a[i][j] - b[i][j]).abs()
);
}
}
}
#[test]
fn test_cholesky_factorize_basic() {
let (a, l) = spd_3x3();
let reconstructed = llt(&l);
assert_matrices_close(&a, &reconstructed, 1e-12);
}
#[test]
fn test_factorize_not_spd() {
let a = vec![vec![1.0, 0.0], vec![0.0, -1.0]];
assert!(cholesky_factorize(&a).is_err());
}
#[test]
fn test_rank1_update_correctness() {
let (a, l) = spd_3x3();
let u = vec![1.0, 0.5, -0.3];
let alpha = 2.0;
let l_new = cholesky_rank1_update(&l, &u, alpha).expect("update");
let n = a.len();
let mut a_prime = a.clone();
for i in 0..n {
for j in 0..n {
a_prime[i][j] += alpha * u[i] * u[j];
}
}
let reconstructed = llt(&l_new);
assert_matrices_close(&a_prime, &reconstructed, 1e-10);
}
#[test]
fn test_rank1_update_identity() {
let l = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let u = vec![0.0, 1.0];
let alpha = 1.0;
let l_new = cholesky_rank1_update(&l, &u, alpha).expect("update");
let a_new = llt(&l_new);
assert!((a_new[0][0] - 1.0).abs() < 1e-12);
assert!((a_new[1][1] - 2.0).abs() < 1e-12);
assert!((a_new[0][1]).abs() < 1e-12);
}
#[test]
fn test_rank1_update_preserves_lower_triangular() {
let (_, l) = spd_3x3();
let u = vec![1.0, 2.0, 3.0];
let l_new = cholesky_rank1_update(&l, &u, 1.0).expect("update");
for i in 0..l_new.len() {
for j in (i + 1)..l_new[i].len() {
assert!(
l_new[i][j].abs() < 1e-14,
"Non-zero upper triangle at ({},{}): {}",
i,
j,
l_new[i][j]
);
}
}
}
#[test]
fn test_rank1_update_dimension_mismatch() {
let l = vec![vec![1.0, 0.0], vec![0.5, 1.0]];
let u = vec![1.0, 2.0, 3.0]; assert!(cholesky_rank1_update(&l, &u, 1.0).is_err());
}
#[test]
fn test_rank1_update_negative_alpha() {
let l = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let u = vec![1.0, 0.0];
assert!(cholesky_rank1_update(&l, &u, -1.0).is_err());
}
#[test]
fn test_rank1_update_result_struct() {
let (_, l) = spd_3x3();
let u = vec![0.5, 0.5, 0.5];
let res = cholesky_rank1_update_result(&l, &u, 1.0, &CholUpdateConfig::default());
assert!(res.successful);
assert!(res.condition_estimate > 0.0);
assert!(res.condition_estimate < 1e6);
}
#[test]
fn test_rank1_update_large_20x20() {
let n = 20;
let mut a = vec![vec![0.0; n]; n];
for i in 0..n {
a[i][i] = (n as f64) + 1.0;
for j in 0..n {
if i != j {
a[i][j] = 1.0 / ((1 + i).abs_diff(1 + j) as f64 + 1.0);
a[j][i] = a[i][j];
}
}
}
let l = cholesky_factorize(&a).expect("factorize 20x20");
let u: Vec<f64> = (0..n).map(|i| (i as f64 + 1.0) * 0.1).collect();
let alpha = 0.5;
let l_new = cholesky_rank1_update(&l, &u, alpha).expect("update 20x20");
let mut a_prime = a.clone();
for i in 0..n {
for j in 0..n {
a_prime[i][j] += alpha * u[i] * u[j];
}
}
let reconstructed = llt(&l_new);
assert_matrices_close(&a_prime, &reconstructed, 1e-8);
}
}