use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
use crate::error::{LinalgError, LinalgResult};
fn spectral_norm(a: &ArrayView2<f64>) -> LinalgResult<f64> {
let (_, s, _) = crate::decomposition::svd(a, false, None)?;
Ok(s[0]) }
fn frob_norm_view(a: &ArrayView2<f64>) -> f64 {
a.iter().map(|&v| v * v).sum::<f64>().sqrt()
}
fn matrix_2norm(a: &Array2<f64>) -> LinalgResult<f64> {
spectral_norm(&a.view())
}
#[derive(Debug, Clone)]
pub struct WeylBoundsResult {
pub eigenvalues_a: Array1<f64>,
pub eigenvalues_b: Array1<f64>,
pub absolute_differences: Array1<f64>,
pub weyl_bound: f64,
pub bound_satisfied: bool,
}
pub fn weyl_bounds(
a: &ArrayView2<f64>,
e: &ArrayView2<f64>,
) -> LinalgResult<WeylBoundsResult> {
let n = a.nrows();
if n != a.ncols() {
return Err(LinalgError::ShapeError(format!(
"weyl_bounds: A must be square, got {}×{}",
n,
a.ncols()
)));
}
if e.nrows() != n || e.ncols() != n {
return Err(LinalgError::ShapeError(format!(
"weyl_bounds: E must be {}×{}, got {}×{}",
n, n,
e.nrows(), e.ncols()
)));
}
let (eigenvalues_a, _) = crate::eigen::eigh(a, None)?;
let b: Array2<f64> = a.to_owned() + e.to_owned();
let (eigenvalues_b, _) = crate::eigen::eigh(&b.view(), None)?;
let weyl_bound = spectral_norm(e)?;
let mut absolute_differences = Array1::<f64>::zeros(n);
let mut bound_satisfied = true;
for i in 0..n {
let diff = (eigenvalues_b[i] - eigenvalues_a[i]).abs();
absolute_differences[i] = diff;
if diff > weyl_bound + 1e-10 * (eigenvalues_a[i].abs() + 1.0) {
bound_satisfied = false;
}
}
Ok(WeylBoundsResult {
eigenvalues_a,
eigenvalues_b,
absolute_differences,
weyl_bound,
bound_satisfied,
})
}
#[derive(Debug, Clone)]
pub struct DavisKahanResult {
pub sin_theta_bound: f64,
pub spectral_gap: f64,
pub perturbation_ev: f64,
pub gap_positive: bool,
}
pub fn davis_kahan_bound(
a: &ArrayView2<f64>,
e: &ArrayView2<f64>,
subspace_indices: &[usize],
) -> LinalgResult<DavisKahanResult> {
let n = a.nrows();
if n != a.ncols() {
return Err(LinalgError::ShapeError(format!(
"davis_kahan_bound: A must be square, got {}×{}",
n,
a.ncols()
)));
}
if e.nrows() != n || e.ncols() != n {
return Err(LinalgError::ShapeError(format!(
"davis_kahan_bound: E must be {}×{}, got {}×{}",
n, n,
e.nrows(), e.ncols()
)));
}
if subspace_indices.is_empty() {
return Err(LinalgError::ValueError(
"davis_kahan_bound: subspace_indices must not be empty".to_string(),
));
}
for &idx in subspace_indices {
if idx >= n {
return Err(LinalgError::ValueError(format!(
"davis_kahan_bound: index {} out of range [0, {})",
idx, n
)));
}
}
let (eigenvalues, eigenvectors) = crate::eigen::eigh(a, None)?;
let k = subspace_indices.len();
let mut v = Array2::<f64>::zeros((n, k));
for (col, &idx) in subspace_indices.iter().enumerate() {
for row in 0..n {
v[[row, col]] = eigenvectors[[row, idx]];
}
}
let ev = e.dot(&v);
let ev_norm = frob_norm_view(&ev.view());
let selected_set: std::collections::HashSet<usize> =
subspace_indices.iter().copied().collect();
let selected_eigs: Vec<f64> = subspace_indices.iter().map(|&i| eigenvalues[i]).collect();
let complement_eigs: Vec<f64> = (0..n)
.filter(|i| !selected_set.contains(i))
.map(|i| eigenvalues[i])
.collect();
let spectral_gap = if complement_eigs.is_empty() {
f64::INFINITY
} else {
let mut gap = f64::INFINITY;
for &s in &selected_eigs {
for &c in &complement_eigs {
let d = (s - c).abs();
if d < gap {
gap = d;
}
}
}
gap
};
let gap_positive = spectral_gap > 0.0 && spectral_gap.is_finite();
let sin_theta_bound = if gap_positive {
(ev_norm / spectral_gap).min(1.0)
} else {
1.0 };
Ok(DavisKahanResult {
sin_theta_bound,
spectral_gap,
perturbation_ev: ev_norm,
gap_positive,
})
}
#[derive(Debug, Clone)]
pub struct BauerFikeResult {
pub bauer_fike_bound: f64,
pub condition_number: f64,
pub perturbation_norm: f64,
}
pub fn bauer_fike_bound(
a: &ArrayView2<f64>,
e: &ArrayView2<f64>,
) -> LinalgResult<BauerFikeResult> {
let n = a.nrows();
if n != a.ncols() {
return Err(LinalgError::ShapeError(format!(
"bauer_fike_bound: A must be square, got {}×{}",
n,
a.ncols()
)));
}
if e.nrows() != n || e.ncols() != n {
return Err(LinalgError::ShapeError(format!(
"bauer_fike_bound: E must be {}×{}, got {}×{}",
n, n,
e.nrows(), e.ncols()
)));
}
let (eigenvalues_real, eigvec_real) = crate::eigen::eigh(a, None)?;
let _ = eigenvalues_real;
let (_, s_v, _) = crate::decomposition::svd(&eigvec_real.view(), false, None)?;
let sigma_max = s_v[0];
let sigma_min = s_v[s_v.len() - 1];
let condition_number = if sigma_min < 1e-300 {
f64::INFINITY
} else {
sigma_max / sigma_min
};
let perturbation_norm = spectral_norm(e)?;
let bauer_fike_bound = condition_number * perturbation_norm;
Ok(BauerFikeResult {
bauer_fike_bound,
condition_number,
perturbation_norm,
})
}
#[derive(Debug, Clone)]
pub struct RelativePerturbationResult {
pub relative_bound: f64,
pub scaled_perturbation_norm: f64,
pub min_eigenvalue: f64,
}
pub fn relative_perturbation_bound(
a: &ArrayView2<f64>,
e: &ArrayView2<f64>,
) -> LinalgResult<RelativePerturbationResult> {
let n = a.nrows();
if n != a.ncols() {
return Err(LinalgError::ShapeError(format!(
"relative_perturbation_bound: A must be square, got {}×{}",
n,
a.ncols()
)));
}
if e.nrows() != n || e.ncols() != n {
return Err(LinalgError::ShapeError(format!(
"relative_perturbation_bound: E must be {}×{}, got {}×{}",
n, n,
e.nrows(), e.ncols()
)));
}
let (eigenvalues, eigvecs) = crate::eigen::eigh(a, None)?;
let min_eigenvalue = eigenvalues[0];
if min_eigenvalue <= 0.0 {
return Err(LinalgError::NonPositiveDefiniteError(
"relative_perturbation_bound: A must be positive-definite".to_string(),
));
}
let mut a_inv_sqrt = Array2::<f64>::zeros((n, n));
for k in 0..n {
let lam_inv_sqrt = 1.0 / eigenvalues[k].sqrt();
for i in 0..n {
for j in 0..n {
a_inv_sqrt[[i, j]] += lam_inv_sqrt * eigvecs[[i, k]] * eigvecs[[j, k]];
}
}
}
let m = a_inv_sqrt.dot(&e.to_owned()).dot(&a_inv_sqrt);
let scaled_perturbation_norm = matrix_2norm(&m)?;
let relative_bound = scaled_perturbation_norm;
Ok(RelativePerturbationResult {
relative_bound,
scaled_perturbation_norm,
min_eigenvalue,
})
}
#[derive(Debug, Clone)]
pub struct ConditionSensitivityResult {
pub condition_number: f64,
pub relative_error_bound: f64,
pub relative_perturbation_a: f64,
pub relative_perturbation_b: Option<f64>,
pub sigma_max: f64,
pub sigma_min: f64,
}
pub fn condition_number_sensitivity(
a: &ArrayView2<f64>,
delta_a: Option<&ArrayView2<f64>>,
b_norm: Option<f64>,
delta_b_norm: Option<f64>,
) -> LinalgResult<ConditionSensitivityResult> {
let n = a.nrows();
if n != a.ncols() {
return Err(LinalgError::ShapeError(format!(
"condition_number_sensitivity: A must be square, got {}×{}",
n,
a.ncols()
)));
}
if let Some(da) = delta_a {
if da.nrows() != n || da.ncols() != n {
return Err(LinalgError::ShapeError(format!(
"condition_number_sensitivity: δA must be {}×{}, got {}×{}",
n, n,
da.nrows(), da.ncols()
)));
}
}
let (_, s, _) = crate::decomposition::svd(a, false, None)?;
let sigma_max = s[0];
let sigma_min = s[s.len() - 1];
if sigma_min < 1e-300 {
return Err(LinalgError::SingularMatrixError(
"condition_number_sensitivity: A appears numerically singular".to_string(),
));
}
let condition_number = sigma_max / sigma_min;
let a_norm = sigma_max;
let relative_perturbation_a = if let Some(da) = delta_a {
let da_norm = spectral_norm(da)?;
da_norm / a_norm
} else {
0.0
};
let relative_perturbation_b = match (b_norm, delta_b_norm) {
(Some(bn), Some(dbn)) if bn > 0.0 => Some(dbn / bn),
(Some(_), Some(dbn)) => Some(if dbn == 0.0 { 0.0 } else { f64::INFINITY }),
_ => None,
};
let relative_error_bound = condition_number
* (relative_perturbation_a + relative_perturbation_b.unwrap_or(0.0));
Ok(ConditionSensitivityResult {
condition_number,
relative_error_bound,
relative_perturbation_a,
relative_perturbation_b,
sigma_max,
sigma_min,
})
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_weyl_bounds_diagonal() {
let a = array![[3.0_f64, 0.0], [0.0, 5.0]];
let e = array![[0.1_f64, 0.0], [0.0, -0.1]];
let res = weyl_bounds(&a.view(), &e.view()).expect("weyl_bounds failed");
assert!(res.bound_satisfied, "Weyl bound should be satisfied");
for &d in res.absolute_differences.iter() {
assert!(d <= res.weyl_bound + 1e-10, "diff={} > bound={}", d, res.weyl_bound);
}
}
#[test]
fn test_weyl_bounds_large_perturbation() {
let a = array![[1.0_f64, 0.0], [0.0, 2.0]];
let e = array![[3.0_f64, 0.0], [0.0, -1.0]];
let res = weyl_bounds(&a.view(), &e.view()).expect("failed");
assert!(res.bound_satisfied);
}
#[test]
fn test_davis_kahan_small_perturbation() {
let a = array![[1.0_f64, 0.0, 0.0],
[0.0, 5.0, 0.0],
[0.0, 0.0, 10.0]];
let e = array![[0.01_f64, 0.0, 0.0],
[0.0, 0.01, 0.0],
[0.0, 0.0, 0.01]];
let res = davis_kahan_bound(&a.view(), &e.view(), &[0]).expect("failed");
assert!(res.gap_positive, "gap must be positive");
assert!(res.sin_theta_bound >= 0.0 && res.sin_theta_bound <= 1.0);
}
#[test]
fn test_davis_kahan_large_gap() {
let a = array![[1.0_f64, 0.0], [0.0, 1000.0]];
let e = array![[0.01_f64, 0.0], [0.0, 0.0]];
let res = davis_kahan_bound(&a.view(), &e.view(), &[0]).expect("failed");
assert!(res.spectral_gap > 0.0);
assert!(res.sin_theta_bound < 0.01 / res.spectral_gap + 1e-10);
}
#[test]
fn test_bauer_fike_symmetric() {
let a = array![[4.0_f64, 0.0], [0.0, 9.0]];
let e = array![[0.1_f64, 0.0], [0.0, 0.1]];
let res = bauer_fike_bound(&a.view(), &e.view()).expect("failed");
assert!(
(res.bauer_fike_bound - 0.1).abs() < 1e-7,
"bound = {}",
res.bauer_fike_bound
);
}
#[test]
fn test_relative_perturbation_pd() {
let a = array![[4.0_f64, 0.0], [0.0, 100.0]];
let e = array![[0.01_f64, 0.0], [0.0, 0.1]];
let res = relative_perturbation_bound(&a.view(), &e.view()).expect("failed");
assert!(res.relative_bound >= 0.0);
assert!(res.min_eigenvalue > 0.0);
}
#[test]
fn test_relative_perturbation_identity() {
let a = array![[1.0_f64, 0.0], [0.0, 1.0]];
let e = array![[0.0_f64, 0.0], [0.0, 0.5]];
let res = relative_perturbation_bound(&a.view(), &e.view()).expect("failed");
assert!(
(res.scaled_perturbation_norm - 0.5).abs() < 1e-8,
"scaled norm = {}",
res.scaled_perturbation_norm
);
}
#[test]
fn test_relative_perturbation_non_pd_error() {
let a = array![[1.0_f64, 2.0], [2.0, 1.0]]; let e = array![[0.0_f64, 0.0], [0.0, 0.0]];
let result = relative_perturbation_bound(&a.view(), &e.view());
assert!(result.is_err(), "Should fail for non-PD matrix");
}
#[test]
fn test_condition_sensitivity_identity() {
let a = array![[1.0_f64, 0.0], [0.0, 1.0]];
let res = condition_number_sensitivity(&a.view(), None, Some(1.0), Some(0.01))
.expect("failed");
assert!((res.condition_number - 1.0).abs() < 1e-10);
assert!((res.relative_error_bound - 0.01).abs() < 1e-10);
}
#[test]
fn test_condition_sensitivity_ill_conditioned() {
let a = array![[1.0_f64, 0.0], [0.0, 1e-6_f64]];
let res = condition_number_sensitivity(&a.view(), None, None, None).expect("failed");
assert!(res.condition_number > 1e5, "κ = {}", res.condition_number);
}
#[test]
fn test_condition_sensitivity_with_delta_a() {
let a = array![[2.0_f64, 0.0], [0.0, 2.0]];
let da = array![[0.01_f64, 0.0], [0.0, 0.0]];
let res = condition_number_sensitivity(&a.view(), Some(&da.view()), None, None)
.expect("failed");
assert!(
(res.relative_perturbation_a - 0.005).abs() < 1e-8,
"rel_pert_a = {}",
res.relative_perturbation_a
);
}
}