use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use crate::error::{LinalgError, LinalgResult};
pub fn eigenvalue_perturbation(
a: &ArrayView2<f64>,
e: &ArrayView2<f64>,
) -> LinalgResult<Array1<f64>> {
let (n, nc) = a.dim();
if n != nc {
return Err(LinalgError::ShapeError(format!(
"eigenvalue_perturbation: A must be square, got ({n}×{nc})"
)));
}
let (en, enc) = e.dim();
if en != n || enc != n {
return Err(LinalgError::ShapeError(format!(
"eigenvalue_perturbation: E must be ({n}×{n}), got ({en}×{enc})"
)));
}
let (_eigenvalues, eigvecs) = crate::eigen::eigh(a, None)?;
let mut deltas = Array1::<f64>::zeros(n);
for i in 0..n {
let vi = eigvecs.column(i);
let ev = matvec_f64(e, &vi);
let mut d = 0.0_f64;
for k in 0..n {
d += vi[k] * ev[k];
}
deltas[i] = d;
}
Ok(deltas)
}
pub fn davis_kahan(
a: &ArrayView2<f64>,
b: &ArrayView2<f64>,
delta: f64,
) -> LinalgResult<f64> {
let (n, nc) = a.dim();
if n != nc {
return Err(LinalgError::ShapeError(format!(
"davis_kahan: A must be square, got ({n}×{nc})"
)));
}
let (bn, bnc) = b.dim();
if bn != n || bnc != n {
return Err(LinalgError::ShapeError(format!(
"davis_kahan: B must be ({n}×{n}), got ({bn}×{bnc})"
)));
}
if delta <= 0.0 {
return Err(LinalgError::ValueError(
"davis_kahan: delta (spectral gap) must be strictly positive".to_string(),
));
}
let mut frob_sq = 0.0_f64;
for i in 0..n {
for j in 0..n {
let diff = a[[i, j]] - b[[i, j]];
frob_sq += diff * diff;
}
}
let frob = frob_sq.sqrt();
let bound = (frob / delta).min(1.0_f64);
Ok(bound)
}
pub fn weyl_bound(
eigenvalues_a: &ArrayView1<f64>,
eigenvalues_b: &ArrayView1<f64>,
) -> LinalgResult<f64> {
let n = eigenvalues_a.len();
if eigenvalues_b.len() != n {
return Err(LinalgError::ShapeError(format!(
"weyl_bound: eigenvalue arrays have different lengths: {} vs {}",
n,
eigenvalues_b.len()
)));
}
if n == 0 {
return Err(LinalgError::ValueError(
"weyl_bound: eigenvalue arrays must be non-empty".to_string(),
));
}
let max_diff = (0..n)
.map(|i| (eigenvalues_a[i] - eigenvalues_b[i]).abs())
.fold(0.0_f64, f64::max);
Ok(max_diff)
}
pub fn relative_perturbation(
a: &ArrayView2<f64>,
e: &ArrayView2<f64>,
) -> LinalgResult<f64> {
let (n, nc) = a.dim();
if n != nc {
return Err(LinalgError::ShapeError(format!(
"relative_perturbation: A must be square, got ({n}×{nc})"
)));
}
let (en, enc) = e.dim();
if en != n || enc != n {
return Err(LinalgError::ShapeError(format!(
"relative_perturbation: E must be ({n}×{n}), got ({en}×{enc})"
)));
}
let (eigenvalues, eigvecs) = crate::eigen::eigh(a, None)?;
let mut max_rel = 0.0_f64;
for i in 0..n {
let vi = eigvecs.column(i);
let ev = matvec_f64(e, &vi);
let mut delta_i = 0.0_f64;
for k in 0..n {
delta_i += vi[k] * ev[k];
}
let lam_i = eigenvalues[i].abs();
let rel_i = if lam_i < f64::EPSILON {
delta_i.abs()
} else {
delta_i.abs() / lam_i
};
if rel_i > max_rel {
max_rel = rel_i;
}
}
Ok(max_rel)
}
pub fn condition_eigenvalue(a: &ArrayView2<f64>, i: usize) -> LinalgResult<f64> {
let (n, nc) = a.dim();
if n != nc {
return Err(LinalgError::ShapeError(format!(
"condition_eigenvalue: A must be square, got ({n}×{nc})"
)));
}
if n == 0 {
return Err(LinalgError::ValueError(
"condition_eigenvalue: matrix must be non-empty".to_string(),
));
}
if i >= n {
return Err(LinalgError::IndexError(format!(
"condition_eigenvalue: index {i} out of range for {n}×{n} matrix"
)));
}
let (eigenvalues, _) = crate::eigen::eigh(a, None)?;
let lam_i = eigenvalues[i];
let gap = eigenvalues
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, &lam_j)| (lam_i - lam_j).abs())
.fold(f64::INFINITY, f64::min);
if gap < f64::EPSILON {
Ok(f64::INFINITY)
} else {
Ok(1.0 / gap)
}
}
pub fn pseudospectrum(
a: &ArrayView2<f64>,
epsilon: f64,
grid_size: usize,
real_range: (f64, f64),
imag_range: (f64, f64),
) -> LinalgResult<Array2<bool>> {
let (n, nc) = a.dim();
if n != nc {
return Err(LinalgError::ShapeError(format!(
"pseudospectrum: A must be square, got ({n}×{nc})"
)));
}
if epsilon <= 0.0 {
return Err(LinalgError::ValueError(
"pseudospectrum: epsilon must be strictly positive".to_string(),
));
}
if grid_size == 0 {
return Err(LinalgError::ValueError(
"pseudospectrum: grid_size must be at least 1".to_string(),
));
}
let (r_min, r_max) = real_range;
let (im_min, im_max) = imag_range;
if r_max <= r_min {
return Err(LinalgError::ValueError(
"pseudospectrum: real_range must have max > min".to_string(),
));
}
if im_max <= im_min {
return Err(LinalgError::ValueError(
"pseudospectrum: imag_range must have max > min".to_string(),
));
}
let dr = (r_max - r_min) / (grid_size as f64 - 1.0).max(1.0);
let di = (im_max - im_min) / (grid_size as f64 - 1.0).max(1.0);
let mut grid = Array2::<bool>::from_elem((grid_size, grid_size), false);
for gi in 0..grid_size {
let re = r_min + gi as f64 * dr;
for gj in 0..grid_size {
let im = im_min + gj as f64 * di;
let sigma_min = smallest_singular_value_complex_shift(a, re, im);
if sigma_min < epsilon {
grid[[gi, gj]] = true;
}
}
}
Ok(grid)
}
fn matvec_f64(m: &ArrayView2<f64>, v: &scirs2_core::ndarray::ArrayView1<f64>) -> Vec<f64> {
let (rows, cols) = m.dim();
let mut out = vec![0.0_f64; rows];
for i in 0..rows {
let mut s = 0.0_f64;
for j in 0..cols {
s += m[[i, j]] * v[j];
}
out[i] = s;
}
out
}
fn smallest_singular_value_complex_shift(a: &ArrayView2<f64>, re: f64, im: f64) -> f64 {
let n = a.nrows();
let nn = 2 * n;
let mut m = Array2::<f64>::zeros((nn, nn));
for i in 0..n {
for j in 0..n {
let a_ij = a[[i, j]];
m[[i, j]] = if i == j { re - a_ij } else { -a_ij };
m[[i + n, j + n]] = if i == j { re - a_ij } else { -a_ij };
}
m[[i, i + n]] = -im;
m[[i + n, i]] = im;
}
let mtm = compute_ata(&m.view());
let trace_mtm: f64 = (0..nn).map(|i| mtm[[i, i]]).sum::<f64>() / nn as f64;
let shift = trace_mtm * 1.05;
let sigma_min_sq = smallest_eigenvalue_via_shifted_power(&mtm, shift, nn);
sigma_min_sq.max(0.0_f64).sqrt()
}
fn compute_ata(a: &ArrayView2<f64>) -> Array2<f64> {
let (m, n) = a.dim();
let mut ata = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..=i {
let mut s = 0.0_f64;
for k in 0..m {
s += a[[k, i]] * a[[k, j]];
}
ata[[i, j]] = s;
ata[[j, i]] = s;
}
}
ata
}
fn smallest_eigenvalue_via_shifted_power(
b: &Array2<f64>,
shift: f64,
n: usize,
) -> f64 {
let mut c = b.clone();
c.mapv_inplace(|x| -x);
for i in 0..n {
c[[i, i]] += shift;
}
let mut v = vec![1.0_f64; n];
let norm0: f64 = v.iter().map(|x| x * x).sum::<f64>().sqrt().max(f64::MIN_POSITIVE);
for x in v.iter_mut() {
*x /= norm0;
}
let mut lam_c = 0.0_f64;
const MAX_ITER: usize = 30;
for _ in 0..MAX_ITER {
let mut w = vec![0.0_f64; n];
for i in 0..n {
let mut s = 0.0_f64;
for j in 0..n {
s += c[[i, j]] * v[j];
}
w[i] = s;
}
let rq: f64 = v.iter().zip(w.iter()).map(|(vi, wi)| vi * wi).sum();
let norm_w: f64 = w.iter().map(|x| x * x).sum::<f64>().sqrt().max(f64::MIN_POSITIVE);
for (vi, wi) in v.iter_mut().zip(w.iter()) {
*vi = *wi / norm_w;
}
lam_c = rq;
}
shift - lam_c
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use scirs2_core::ndarray::array;
fn diag_matrix(eigs: &[f64]) -> Array2<f64> {
let n = eigs.len();
let mut m = Array2::<f64>::zeros((n, n));
for (i, &e) in eigs.iter().enumerate() {
m[[i, i]] = e;
}
m
}
#[test]
fn test_eigenvalue_perturbation_diagonal() {
let a = diag_matrix(&[2.0, 5.0, 8.0]);
let e = diag_matrix(&[0.1, -0.2, 0.3]);
let delta = eigenvalue_perturbation(&a.view(), &e.view()).expect("failed to create delta");
assert_relative_eq!(delta[0], 0.1, epsilon = 1e-9);
assert_relative_eq!(delta[1], -0.2, epsilon = 1e-9);
assert_relative_eq!(delta[2], 0.3, epsilon = 1e-9);
}
#[test]
fn test_eigenvalue_perturbation_shape_error_a() {
let a = array![[1.0_f64, 2.0, 3.0]];
let e = array![[0.1_f64, 0.0, 0.0]];
assert!(eigenvalue_perturbation(&a.view(), &e.view()).is_err());
}
#[test]
fn test_eigenvalue_perturbation_shape_error_e() {
let a = array![[1.0_f64, 0.0], [0.0, 2.0]];
let e = array![[0.1_f64, 0.0, 0.0], [0.0, 0.1, 0.0], [0.0, 0.0, 0.1]];
assert!(eigenvalue_perturbation(&a.view(), &e.view()).is_err());
}
#[test]
fn test_eigenvalue_perturbation_sum_zero() {
let a = diag_matrix(&[1.0, 3.0]);
let e = array![[0.5_f64, 0.0], [0.0, -0.5]];
let delta = eigenvalue_perturbation(&a.view(), &e.view()).expect("failed to create delta");
assert_relative_eq!(delta[0] + delta[1], 0.0, epsilon = 1e-9);
}
#[test]
fn test_davis_kahan_zero_perturbation() {
let a = array![[1.0_f64, 0.0], [0.0, 5.0]];
let bound = davis_kahan(&a.view(), &a.view(), 4.0).expect("failed to create bound");
assert_relative_eq!(bound, 0.0, epsilon = 1e-14);
}
#[test]
fn test_davis_kahan_small_perturbation() {
let a = array![[1.0_f64, 0.0], [0.0, 10.0]];
let b = array![[1.01_f64, 0.0], [0.0, 10.01]];
let gap = 9.0_f64;
let bound = davis_kahan(&a.view(), &b.view(), gap).expect("failed to create bound");
assert!(bound < 0.01);
}
#[test]
fn test_davis_kahan_large_perturbation_clamped() {
let a = array![[1.0_f64, 0.0], [0.0, 5.0]];
let b = array![[100.0_f64, 0.0], [0.0, 200.0]];
let bound = davis_kahan(&a.view(), &b.view(), 0.001).expect("failed to create bound");
assert_relative_eq!(bound, 1.0, epsilon = 1e-14);
}
#[test]
fn test_davis_kahan_invalid_delta() {
let a = array![[1.0_f64, 0.0], [0.0, 5.0]];
assert!(davis_kahan(&a.view(), &a.view(), 0.0).is_err());
assert!(davis_kahan(&a.view(), &a.view(), -1.0).is_err());
}
#[test]
fn test_davis_kahan_shape_error() {
let a = array![[1.0_f64, 2.0, 3.0]];
let b = array![[1.0_f64, 2.0, 3.0]];
assert!(davis_kahan(&a.view(), &b.view(), 1.0).is_err());
}
#[test]
fn test_weyl_bound_equal() {
let la = array![1.0_f64, 2.0, 3.0];
let lb = la.clone();
let bound = weyl_bound(&la.view(), &lb.view()).expect("failed to create bound");
assert_relative_eq!(bound, 0.0, epsilon = 1e-14);
}
#[test]
fn test_weyl_bound_known() {
let la = array![1.0_f64, 3.0, 7.0];
let lb = array![1.1_f64, 2.9, 7.2];
let bound = weyl_bound(&la.view(), &lb.view()).expect("failed to create bound");
assert_relative_eq!(bound, 0.2, epsilon = 1e-12);
}
#[test]
fn test_weyl_bound_length_mismatch() {
let la = array![1.0_f64, 2.0];
let lb = array![1.0_f64, 2.0, 3.0];
assert!(weyl_bound(&la.view(), &lb.view()).is_err());
}
#[test]
fn test_weyl_bound_empty() {
let la = Array1::<f64>::zeros(0);
let lb = Array1::<f64>::zeros(0);
assert!(weyl_bound(&la.view(), &lb.view()).is_err());
}
#[test]
fn test_relative_perturbation_diagonal() {
let a = diag_matrix(&[4.0, 9.0]);
let e = diag_matrix(&[0.04, 0.09]);
let rel = relative_perturbation(&a.view(), &e.view()).expect("failed to create rel");
assert_relative_eq!(rel, 0.01, epsilon = 1e-10);
}
#[test]
fn test_relative_perturbation_zero_perturbation() {
let a = diag_matrix(&[1.0, 4.0, 9.0]);
let e = Array2::<f64>::zeros((3, 3));
let rel = relative_perturbation(&a.view(), &e.view()).expect("failed to create rel");
assert_relative_eq!(rel, 0.0, epsilon = 1e-14);
}
#[test]
fn test_condition_eigenvalue_diagonal() {
let a = diag_matrix(&[1.0, 5.0]);
let kappa0 = condition_eigenvalue(&a.view(), 0).expect("failed to create kappa0");
assert_relative_eq!(kappa0, 0.25, epsilon = 1e-10);
let kappa1 = condition_eigenvalue(&a.view(), 1).expect("failed to create kappa1");
assert_relative_eq!(kappa1, 0.25, epsilon = 1e-10);
}
#[test]
fn test_condition_eigenvalue_three_eigenvalues() {
let a = diag_matrix(&[1.0, 3.0, 10.0]);
let k0 = condition_eigenvalue(&a.view(), 0).expect("failed to create k0");
assert_relative_eq!(k0, 0.5, epsilon = 1e-10);
let k1 = condition_eigenvalue(&a.view(), 1).expect("failed to create k1");
assert_relative_eq!(k1, 0.5, epsilon = 1e-10);
let k2 = condition_eigenvalue(&a.view(), 2).expect("failed to create k2");
assert_relative_eq!(k2, 1.0 / 7.0, epsilon = 1e-10);
}
#[test]
fn test_condition_eigenvalue_repeated_is_inf() {
let a = diag_matrix(&[2.0, 2.0, 5.0]);
let k = condition_eigenvalue(&a.view(), 0).expect("failed to create k");
assert!(k.is_infinite());
}
#[test]
fn test_condition_eigenvalue_out_of_bounds() {
let a = diag_matrix(&[1.0, 2.0]);
assert!(condition_eigenvalue(&a.view(), 2).is_err());
}
#[test]
fn test_condition_eigenvalue_non_square() {
let a = array![[1.0_f64, 2.0, 3.0]];
assert!(condition_eigenvalue(&a.view(), 0).is_err());
}
#[test]
fn test_pseudospectrum_shape() {
let a = diag_matrix(&[0.0, 1.0]);
let ps = pseudospectrum(&a.view(), 0.3, 10, (-0.5, 1.5), (-1.0, 1.0)).expect("failed to create ps");
assert_eq!(ps.dim(), (10, 10));
}
#[test]
fn test_pseudospectrum_contains_eigenvalue_neighborhood() {
let a = diag_matrix(&[0.0, 3.0]);
let eps = 0.4;
let ps = pseudospectrum(&a.view(), eps, 30, (-1.0, 4.0), (-0.5, 0.5)).expect("failed to create ps");
let mut any_true = false;
for gi in 0..30 {
for gj in 0..30 {
if ps[[gi, gj]] {
any_true = true;
}
}
}
assert!(any_true, "pseudospectrum should contain at least one true entry near eigenvalues");
}
#[test]
fn test_pseudospectrum_invalid_epsilon() {
let a = diag_matrix(&[1.0, 2.0]);
assert!(pseudospectrum(&a.view(), -0.1, 5, (0.0, 2.0), (-1.0, 1.0)).is_err());
assert!(pseudospectrum(&a.view(), 0.0, 5, (0.0, 2.0), (-1.0, 1.0)).is_err());
}
#[test]
fn test_pseudospectrum_invalid_ranges() {
let a = diag_matrix(&[1.0, 2.0]);
assert!(pseudospectrum(&a.view(), 0.1, 5, (2.0, 1.0), (-1.0, 1.0)).is_err());
assert!(pseudospectrum(&a.view(), 0.1, 5, (0.0, 2.0), (1.0, -1.0)).is_err());
}
#[test]
fn test_pseudospectrum_non_square() {
let a = array![[1.0_f64, 2.0, 3.0]];
assert!(pseudospectrum(&a.view(), 0.1, 5, (0.0, 2.0), (-1.0, 1.0)).is_err());
}
}