use crate::error::{SparseError, SparseResult};
use super::types::{estimate_condition, CholUpdateConfig, CholUpdateResult};
pub fn cholesky_rank1_downdate(
l: &[Vec<f64>],
u: &[f64],
alpha: f64,
) -> SparseResult<Vec<Vec<f64>>> {
cholesky_rank1_downdate_with_config(l, u, alpha, &CholUpdateConfig::default())
}
pub fn cholesky_rank1_downdate_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 downdate".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 sqrt_alpha = alpha.sqrt();
let mut p = vec![0.0; n];
for i in 0..n {
let mut s = sqrt_alpha * u[i];
for k in 0..i {
s -= l[i][k] * p[k];
}
if l[i][i].abs() < config.tolerance {
return Err(SparseError::ComputationError(format!(
"Near-zero diagonal at ({},{}): {:.6e}",
i, i, l[i][i]
)));
}
p[i] = s / l[i][i];
}
let p_norm_sq: f64 = p.iter().map(|&v| v * v).sum();
if p_norm_sq >= 1.0 - config.tolerance {
return Err(SparseError::ComputationError(format!(
"Downdate would destroy positive definiteness: ||p||² = {:.6e} >= 1",
p_norm_sq
)));
}
let mut a_prime = 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_prime[i][j] = s - alpha * u[i] * u[j];
}
}
let l_new = super::rank1::cholesky_factorize(&a_prime).map_err(|e| {
SparseError::ComputationError(format!(
"Downdate produced a non-positive-definite matrix: {}",
e
))
})?;
if config.check_positive_definite {
for j in 0..n {
if l_new[j][j] <= config.tolerance {
return Err(SparseError::ComputationError(format!(
"Updated factor not positive definite at ({},{}): {:.6e}",
j, j, l_new[j][j]
)));
}
}
}
Ok(l_new)
}
pub fn cholesky_rank1_downdate_result(
l: &[Vec<f64>],
u: &[f64],
alpha: f64,
config: &CholUpdateConfig,
) -> CholUpdateResult {
match cholesky_rank1_downdate_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,
},
}
}
#[cfg(test)]
fn is_pd_after_downdate(a: &[Vec<f64>], u: &[f64], alpha: f64) -> bool {
let n = a.len();
let mut a2 = a.to_vec();
for i in 0..n {
for j in 0..n {
a2[i][j] -= alpha * u[i] * u[j];
}
}
super::rank1::cholesky_factorize(&a2).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cholesky_update::rank1::{cholesky_factorize, llt};
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_downdate_basic() {
let a = vec![
vec![10.0, 1.0, 0.5],
vec![1.0, 10.0, 1.0],
vec![0.5, 1.0, 10.0],
];
let l = cholesky_factorize(&a).expect("factor");
let u = vec![0.3, 0.2, 0.1];
let alpha = 1.0;
assert!(is_pd_after_downdate(&a, &u, alpha));
let l_new = cholesky_rank1_downdate(&l, &u, alpha).expect("downdate");
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_downdate_rejects_indefinite() {
let a = vec![vec![2.0, 0.0], vec![0.0, 2.0]];
let l = cholesky_factorize(&a).expect("factor");
let u = vec![2.0, 0.0]; let result = cholesky_rank1_downdate(&l, &u, 1.0);
assert!(result.is_err());
}
#[test]
fn test_downdate_dimension_mismatch() {
let l = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let u = vec![1.0];
assert!(cholesky_rank1_downdate(&l, &u, 1.0).is_err());
}
#[test]
fn test_downdate_negative_alpha() {
let l = vec![vec![1.0, 0.0], vec![0.0, 1.0]];
let u = vec![0.1, 0.1];
assert!(cholesky_rank1_downdate(&l, &u, -1.0).is_err());
}
#[test]
fn test_downdate_preserves_lower_triangular() {
let a = vec![vec![10.0, 1.0], vec![1.0, 10.0]];
let l = cholesky_factorize(&a).expect("factor");
let u = vec![0.2, 0.1];
let l_new = cholesky_rank1_downdate(&l, &u, 1.0).expect("downdate");
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_downdate_result_struct() {
let a = vec![vec![10.0, 1.0], vec![1.0, 10.0]];
let l = cholesky_factorize(&a).expect("factor");
let u = vec![0.1, 0.1];
let res = cholesky_rank1_downdate_result(&l, &u, 1.0, &CholUpdateConfig::default());
assert!(res.successful);
assert!(res.condition_estimate > 0.0);
}
#[test]
fn test_downdate_result_failure() {
let a = vec![vec![2.0, 0.0], vec![0.0, 2.0]];
let l = cholesky_factorize(&a).expect("factor");
let u = vec![2.0, 0.0];
let res = cholesky_rank1_downdate_result(&l, &u, 1.0, &CholUpdateConfig::default());
assert!(!res.successful);
}
}