use crate::error::{LinalgError, LinalgResult};
use scirs2_core::ndarray::{Array1, Array2};
#[derive(Debug, Clone)]
pub struct BunchKaufmanResult {
pub l: Array2<f64>,
pub d_blocks: Vec<Array2<f64>>,
pub perm: Vec<usize>,
}
fn symmetric_swap(a: &mut Array2<f64>, i: usize, j: usize) {
let n = a.nrows();
if i == j {
return;
}
for k in 0..n {
let tmp = a[[i, k]];
a[[i, k]] = a[[j, k]];
a[[j, k]] = tmp;
}
for k in 0..n {
let tmp = a[[k, i]];
a[[k, i]] = a[[k, j]];
a[[k, j]] = tmp;
}
}
fn col_max_abs(a: &Array2<f64>, col: usize, row_start: usize) -> f64 {
let n = a.nrows();
let mut max = 0.0_f64;
for r in row_start..n {
let v = a[[r, col]].abs();
if v > max {
max = v;
}
}
max
}
pub fn bunch_kaufman(a: &Array2<f64>) -> LinalgResult<BunchKaufmanResult> {
let n = a.nrows();
if a.ncols() != n {
return Err(LinalgError::ShapeError(format!(
"bunch_kaufman: matrix must be square, got {}×{}",
n,
a.ncols()
)));
}
let mut work = a.clone();
let mut l = Array2::<f64>::eye(n);
let mut d_blocks: Vec<Array2<f64>> = Vec::new();
let mut perm: Vec<usize> = (0..n).collect();
let alpha = (1.0_f64 + 17.0_f64.sqrt()) / 8.0_f64;
let mut k = 0usize; while k < n {
let remaining = n - k;
if remaining == 1 {
let pivot_val = work[[k, k]];
let mut blk = Array2::<f64>::zeros((1, 1));
blk[[0, 0]] = pivot_val;
d_blocks.push(blk);
k += 1;
continue;
}
let a_kk = work[[k, k]].abs();
let mut r = k + 1;
let mut a_rk = 0.0_f64;
for i in (k + 1)..n {
let v = work[[i, k]].abs();
if v > a_rk {
a_rk = v;
r = i;
}
}
let use_1x1 = if a_rk == 0.0 {
true
} else {
let a_rr = work[[r, r]].abs();
let mut a_sr = 0.0_f64;
for j in k..n {
if j != r {
let v = work[[r, j]].abs();
if v > a_sr {
a_sr = v;
}
}
}
if a_kk >= alpha * a_rk {
true
} else if a_rr >= alpha * a_sr {
symmetric_swap(&mut work, k, r);
perm.swap(k, r);
for c in 0..k {
let tmp = l[[k, c]];
l[[k, c]] = l[[r, c]];
l[[r, c]] = tmp;
}
true
} else {
false
}
};
if use_1x1 {
let d11 = work[[k, k]];
let mut blk = Array2::<f64>::zeros((1, 1));
blk[[0, 0]] = d11;
d_blocks.push(blk);
if d11.abs() > f64::EPSILON {
for i in (k + 1)..n {
let mult = work[[i, k]] / d11;
l[[i, k]] = mult;
for j in (k + 1)..=i {
work[[i, j]] -= mult * work[[k, j]];
work[[j, i]] = work[[i, j]]; }
}
}
k += 1;
} else {
if r != k + 1 {
symmetric_swap(&mut work, k + 1, r);
perm.swap(k + 1, r);
for c in 0..k {
let tmp = l[[k + 1, c]];
l[[k + 1, c]] = l[[r, c]];
l[[r, c]] = tmp;
}
}
let d11 = work[[k, k]];
let d12 = work[[k, k + 1]];
let d22 = work[[k + 1, k + 1]];
let det = d11 * d22 - d12 * d12;
let mut blk = Array2::<f64>::zeros((2, 2));
blk[[0, 0]] = d11;
blk[[0, 1]] = d12;
blk[[1, 0]] = d12;
blk[[1, 1]] = d22;
d_blocks.push(blk);
if det.abs() > f64::EPSILON {
let inv_det = 1.0 / det;
for i in (k + 2)..n {
let b1 = work[[i, k]];
let b2 = work[[i, k + 1]];
let m1 = (d22 * b1 - d12 * b2) * inv_det;
let m2 = (d11 * b2 - d12 * b1) * inv_det;
l[[i, k]] = m1;
l[[i, k + 1]] = m2;
for j in (k + 2)..=i {
work[[i, j]] -= m1 * work[[k, j]] + m2 * work[[k + 1, j]];
work[[j, i]] = work[[i, j]];
}
}
}
k += 2;
}
}
Ok(BunchKaufmanResult {
l,
d_blocks,
perm,
})
}
pub fn ldlt_solve(bk: &BunchKaufmanResult, b: &Array1<f64>) -> LinalgResult<Array1<f64>> {
let n = bk.l.nrows();
if b.len() != n {
return Err(LinalgError::DimensionError(format!(
"ldlt_solve: rhs length {} does not match matrix size {}",
b.len(),
n
)));
}
let mut y = Array1::<f64>::zeros(n);
for (new_pos, &orig) in bk.perm.iter().enumerate() {
y[new_pos] = b[orig];
}
for i in 1..n {
let mut sum = 0.0;
for j in 0..i {
sum += bk.l[[i, j]] * y[j];
}
y[i] -= sum;
}
let mut z = Array1::<f64>::zeros(n);
let mut block_idx = 0usize;
let mut col = 0usize;
for blk in &bk.d_blocks {
if blk.nrows() == 1 {
let d = blk[[0, 0]];
if d.abs() < f64::EPSILON {
z[col] = 0.0;
} else {
z[col] = y[col] / d;
}
col += 1;
} else {
let d11 = blk[[0, 0]];
let d12 = blk[[0, 1]];
let d22 = blk[[1, 1]];
let det = d11 * d22 - d12 * d12;
if det.abs() < f64::EPSILON * (d11.abs() + d22.abs() + d12.abs() + 1.0) {
return Err(LinalgError::SingularMatrixError(format!(
"ldlt_solve: 2×2 D block {} is (near-)singular",
block_idx
)));
}
let inv_det = 1.0 / det;
z[col] = (d22 * y[col] - d12 * y[col + 1]) * inv_det;
z[col + 1] = (d11 * y[col + 1] - d12 * y[col]) * inv_det;
col += 2;
}
block_idx += 1;
}
let mut x_perm = z;
for i in (0..(n.saturating_sub(1))).rev() {
let mut sum = 0.0;
for j in (i + 1)..n {
sum += bk.l[[j, i]] * x_perm[j];
}
x_perm[i] -= sum;
}
let mut x = Array1::<f64>::zeros(n);
for (new_pos, &orig) in bk.perm.iter().enumerate() {
x[orig] = x_perm[new_pos];
}
Ok(x)
}
pub fn inertia(bk: &BunchKaufmanResult) -> (usize, usize, usize) {
let eps = f64::EPSILON * 100.0;
let mut n_pos = 0usize;
let mut n_neg = 0usize;
let mut n_zero = 0usize;
for blk in &bk.d_blocks {
if blk.nrows() == 1 {
let v = blk[[0, 0]];
if v > eps {
n_pos += 1;
} else if v < -eps {
n_neg += 1;
} else {
n_zero += 1;
}
} else {
let d11 = blk[[0, 0]];
let d12 = blk[[0, 1]];
let d22 = blk[[1, 1]];
let tr = d11 + d22;
let det = d11 * d22 - d12 * d12;
let discriminant = tr * tr - 4.0 * det;
if discriminant < 0.0 {
n_pos += 2;
} else {
let sqrt_disc = discriminant.sqrt();
let lambda1 = (tr + sqrt_disc) / 2.0;
let lambda2 = (tr - sqrt_disc) / 2.0;
for &lam in &[lambda1, lambda2] {
if lam > eps {
n_pos += 1;
} else if lam < -eps {
n_neg += 1;
} else {
n_zero += 1;
}
}
}
}
}
(n_pos, n_neg, n_zero)
}
pub fn modified_cholesky(a: &Array2<f64>) -> LinalgResult<(Array2<f64>, f64)> {
let n = a.nrows();
if a.ncols() != n {
return Err(LinalgError::ShapeError(format!(
"modified_cholesky: matrix must be square, got {}×{}",
n,
a.ncols()
)));
}
let mut frob_sq = 0.0_f64;
for i in 0..n {
for j in 0..n {
frob_sq += a[[i, j]] * a[[i, j]];
}
}
let frob = frob_sq.sqrt();
let eps = (n as f64) * f64::EPSILON * frob.max(1.0);
let mut delta = 0.0_f64;
for i in 0..n {
let diag_val = a[[i, i]];
if diag_val <= eps {
let needed = eps - diag_val;
if needed > delta {
delta = needed;
}
}
}
let mut work = a.clone();
for i in 0..n {
work[[i, i]] += delta;
}
let mut attempts = 0usize;
loop {
let mut l = Array2::<f64>::zeros((n, n));
let mut failed = false;
'outer: for j in 0..n {
let mut diag = work[[j, j]];
for k in 0..j {
diag -= l[[j, k]] * l[[j, k]];
}
if diag <= 0.0 {
failed = true;
break 'outer;
}
let l_jj = diag.sqrt();
l[[j, j]] = l_jj;
for i in (j + 1)..n {
let mut s = work[[i, j]];
for k in 0..j {
s -= l[[i, k]] * l[[j, k]];
}
l[[i, j]] = s / l_jj;
}
}
if !failed {
return Ok((l, delta));
}
if delta == 0.0 {
delta = eps.max(1e-8 * frob.max(1.0));
} else {
delta *= 2.0;
}
for i in 0..n {
work[[i, i]] = a[[i, i]] + delta;
}
attempts += 1;
if attempts > 60 {
return Err(LinalgError::ConvergenceError(
"modified_cholesky: failed to find a positive-definite perturbation".to_string(),
));
}
}
}
pub fn spectral_decomp_indefinite(a: &Array2<f64>) -> LinalgResult<(Array2<f64>, Array1<f64>)> {
let n = a.nrows();
if a.ncols() != n {
return Err(LinalgError::ShapeError(format!(
"spectral_decomp_indefinite: matrix must be square, got {}×{}",
n,
a.ncols()
)));
}
let mut s = a.clone();
let mut v = Array2::<f64>::eye(n);
let max_iter = 100 * n * n;
let tol = f64::EPSILON * (n as f64) * {
let mut fnorm = 0.0_f64;
for i in 0..n {
for j in 0..n {
fnorm += a[[i, j]] * a[[i, j]];
}
}
fnorm.sqrt()
};
for _ in 0..max_iter {
let mut off = 0.0_f64;
for i in 0..n {
for j in (i + 1)..n {
off += s[[i, j]] * s[[i, j]];
}
}
if off <= tol * tol {
break;
}
for p in 0..n {
for q in (p + 1)..n {
let s_pq = s[[p, q]];
if s_pq.abs() < tol {
continue;
}
let theta = if (s[[q, q]] - s[[p, p]]).abs() < f64::EPSILON {
std::f64::consts::FRAC_PI_4
} else {
0.5 * ((2.0 * s_pq) / (s[[q, q]] - s[[p, p]])).atan()
};
let cos_t = theta.cos();
let sin_t = theta.sin();
for r in 0..n {
let sp = cos_t * s[[p, r]] + sin_t * s[[q, r]];
let sq = -sin_t * s[[p, r]] + cos_t * s[[q, r]];
s[[p, r]] = sp;
s[[q, r]] = sq;
}
for r in 0..n {
let sp = cos_t * s[[r, p]] + sin_t * s[[r, q]];
let sq = -sin_t * s[[r, p]] + cos_t * s[[r, q]];
s[[r, p]] = sp;
s[[r, q]] = sq;
}
for r in 0..n {
let vp = cos_t * v[[r, p]] + sin_t * v[[r, q]];
let vq = -sin_t * v[[r, p]] + cos_t * v[[r, q]];
v[[r, p]] = vp;
v[[r, q]] = vq;
}
}
}
}
let mut eigenvalues: Vec<(f64, usize)> = (0..n).map(|i| (s[[i, i]], i)).collect();
eigenvalues.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let evals = Array1::from_vec(eigenvalues.iter().map(|(e, _)| *e).collect());
let mut evecs = Array2::<f64>::zeros((n, n));
for (new_col, (_, orig_col)) in eigenvalues.iter().enumerate() {
for r in 0..n {
evecs[[r, new_col]] = v[[r, *orig_col]];
}
}
Ok((evecs, evals))
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
fn reconstruct_bk(a: &Array2<f64>, bk: &BunchKaufmanResult) -> Array2<f64> {
let n = bk.l.nrows();
let mut d = Array2::<f64>::zeros((n, n));
let mut col = 0;
for blk in &bk.d_blocks {
let sz = blk.nrows();
for i in 0..sz {
for j in 0..sz {
d[[col + i, col + j]] = blk[[i, j]];
}
}
col += sz;
}
let mut ld = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut s = 0.0;
for k in 0..n {
s += bk.l[[i, k]] * d[[k, j]];
}
ld[[i, j]] = s;
}
}
let mut ldlt = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let mut s = 0.0;
for k in 0..n {
s += ld[[i, k]] * bk.l[[j, k]];
}
ldlt[[i, j]] = s;
}
}
let mut pap = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
pap[[i, j]] = a[[bk.perm[i], bk.perm[j]]];
}
}
let mut diff = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
diff[[i, j]] = ldlt[[i, j]] - pap[[i, j]];
}
}
diff
}
#[test]
fn test_bunch_kaufman_spd() {
let a = array![
[4.0_f64, 2.0, 0.0],
[2.0, 3.0, 1.0],
[0.0, 1.0, 2.0],
];
let bk = bunch_kaufman(&a).expect("BK failed");
let diff = reconstruct_bk(&a, &bk);
let err: f64 = diff.iter().map(|x| x * x).sum::<f64>().sqrt();
assert!(err < 1e-10, "reconstruction error too large: {}", err);
}
#[test]
fn test_bunch_kaufman_indefinite() {
let a = array![
[ 2.0_f64, 1.0, 0.0],
[ 1.0, 0.0, -1.0],
[ 0.0, -1.0, 3.0],
];
let bk = bunch_kaufman(&a).expect("BK failed");
let diff = reconstruct_bk(&a, &bk);
let err: f64 = diff.iter().map(|x| x * x).sum::<f64>().sqrt();
assert!(err < 1e-10, "reconstruction error too large: {}", err);
}
#[test]
fn test_ldlt_solve_basic() {
let a = array![
[4.0_f64, 2.0, -1.0],
[2.0, 3.0, 0.0],
[-1.0, 0.0, 2.0],
];
let b = array![1.0_f64, 2.0, 3.0];
let bk = bunch_kaufman(&a).expect("BK failed");
let x = ldlt_solve(&bk, &b).expect("solve failed");
for i in 0..3 {
let ax_i: f64 = (0..3).map(|j| a[[i, j]] * x[j]).sum();
assert!(
(ax_i - b[i]).abs() < 1e-8,
"residual at row {} = {}",
i,
(ax_i - b[i]).abs()
);
}
}
#[test]
fn test_inertia() {
let a = array![
[2.0_f64, 0.0, 0.0],
[0.0, -1.0, 0.0],
[0.0, 0.0, 0.0],
];
let bk = bunch_kaufman(&a).expect("BK failed");
let (pos, neg, zer) = inertia(&bk);
assert_eq!(pos, 1, "expected 1 positive eigenvalue");
assert_eq!(neg, 1, "expected 1 negative eigenvalue");
assert_eq!(zer, 1, "expected 1 zero eigenvalue");
}
#[test]
fn test_inertia_spd() {
let a = array![
[4.0_f64, 1.0, 0.0],
[1.0, 3.0, 0.5],
[0.0, 0.5, 2.0],
];
let bk = bunch_kaufman(&a).expect("BK failed");
let (pos, neg, zer) = inertia(&bk);
assert_eq!(pos, 3, "expected 3 positive eigenvalues for SPD");
assert_eq!(neg, 0);
assert_eq!(zer, 0);
}
#[test]
fn test_modified_cholesky_indefinite() {
let a = array![
[ 4.0_f64, 2.0],
[ 2.0, -1.0],
];
let (l, delta) = modified_cholesky(&a).expect("modified cholesky failed");
assert!(delta >= 0.0, "delta must be non-negative");
let n = 2;
for i in 0..n {
for j in 0..n {
let llt_ij: f64 = (0..=j.min(i)).map(|k| l[[i, k]] * l[[j, k]]).sum();
let expected = a[[i, j]] + if i == j { delta } else { 0.0 };
assert!(
(llt_ij - expected).abs() < 1e-10,
"L L^T [{i},{j}] mismatch: {} vs {}",
llt_ij,
expected
);
}
}
}
#[test]
fn test_modified_cholesky_spd() {
let a = array![
[4.0_f64, 1.0],
[1.0, 3.0],
];
let (_l, delta) = modified_cholesky(&a).expect("modified cholesky failed");
assert_eq!(delta, 0.0, "no perturbation needed for SPD");
}
#[test]
fn test_spectral_decomp_indefinite() {
let a = array![
[ 2.0_f64, 1.0],
[ 1.0, -1.0],
];
let (v, eigenvalues) = spectral_decomp_indefinite(&a).expect("decomp failed");
let n = 2;
for i in 0..n {
for row in 0..n {
let av_i: f64 = (0..n).map(|c| a[[row, c]] * v[[c, i]]).sum();
let lv_i = eigenvalues[i] * v[[row, i]];
assert!(
(av_i - lv_i).abs() < 1e-9,
"eigenvector equation failed at ({row},{i}): {av_i} != {lv_i}"
);
}
}
}
#[test]
fn test_bunch_kaufman_non_square_error() {
let a = array![[1.0_f64, 2.0, 3.0], [4.0, 5.0, 6.0]];
assert!(bunch_kaufman(&a).is_err());
}
#[test]
fn test_bunch_kaufman_1x1() {
let a = array![[5.0_f64]];
let bk = bunch_kaufman(&a).expect("BK 1x1 failed");
assert_eq!(bk.d_blocks.len(), 1);
assert_eq!(bk.d_blocks[0][[0, 0]], 5.0);
}
#[allow(dead_code)]
fn max_offdiag(a: &Array2<f64>) -> f64 {
let n = a.nrows();
let mut max = 0.0_f64;
for i in 0..n {
for j in 0..n {
if i != j {
let v = a[[i, j]].abs();
if v > max {
max = v;
}
}
}
}
max
}
#[test]
fn test_col_max_abs_helper() {
let a = array![
[1.0_f64, 2.0],
[3.0, 4.0],
[5.0, 6.0],
];
assert_eq!(col_max_abs(&a, 0, 1), 5.0);
assert_eq!(col_max_abs(&a, 1, 0), 6.0);
}
}