use crate::decomposition::svd;
use crate::eigen::standard::eig;
use crate::error::{LinalgError, LinalgResult};
use crate::solve::solve;
use scirs2_core::ndarray::{Array1, Array2, ArrayView2, ScalarOperand};
use scirs2_core::numeric::{Complex, Float, NumAssign};
use scirs2_core::random::prelude::Rng;
use scirs2_core::random::SeedableRng;
use std::iter::Sum;
fn random_unit_vector<F>(n: usize, rng: &mut impl Rng) -> Option<Array1<F>>
where
F: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static,
{
let mut v = Array1::<F>::zeros(n);
for i in 0..n {
let u: f64 = rng.random::<f64>() * 2.0 - 1.0;
v[i] = F::from(u).unwrap_or(F::zero());
}
let norm_sq: F = v.iter().fold(F::zero(), |acc, &x| acc + x * x);
if norm_sq <= F::epsilon() {
return None;
}
let norm = norm_sq.sqrt();
for vi in v.iter_mut() {
*vi /= norm;
}
Some(v)
}
fn rayleigh_quotient_real<F>(a: &ArrayView2<F>, x: &Array1<F>) -> F
where
F: Float + NumAssign + Sum + ScalarOperand,
{
let ax = a.dot(x);
x.iter()
.zip(ax.iter())
.fold(F::zero(), |acc, (&xi, &axi)| acc + xi * axi)
}
fn smallest_singular_value_shifted<F>(
a: &ArrayView2<F>,
z_re: F,
z_im: F,
) -> LinalgResult<F>
where
F: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static,
{
let n = a.nrows();
let mut m = Array2::<F>::zeros((2 * n, 2 * n));
for i in 0..n {
for j in 0..n {
let aij = a[[i, j]];
m[[i, j]] = if i == j { z_re - aij } else { -aij };
m[[i + n, j + n]] = if i == j { z_re - aij } else { -aij };
m[[i, j + n]] = if i == j { -z_im } else { F::zero() };
m[[i + n, j]] = if i == j { z_im } else { F::zero() };
}
}
let (_u, s, _vt) = svd(&m.view(), false, None)?;
let two = F::from(2.0_f64).unwrap_or(F::one() + F::one());
let min_sv = s.iter().cloned().fold(F::infinity(), |a, b| if b < a { b } else { a });
Ok(min_sv / two.sqrt())
}
pub fn field_of_values<F>(
a: &ArrayView2<F>,
n_samples: usize,
) -> LinalgResult<(Vec<F>, Vec<F>)>
where
F: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static,
{
let n = a.nrows();
if n == 0 {
return Err(LinalgError::InvalidInputError(
"field_of_values: matrix must be non-empty".to_string(),
));
}
if a.ncols() != n {
return Err(LinalgError::ShapeError(
"field_of_values: matrix must be square".to_string(),
));
}
if n_samples < 4 {
return Err(LinalgError::ValueError(
"field_of_values: n_samples must be >= 4".to_string(),
));
}
let two = F::from(2.0_f64).unwrap_or(F::one() + F::one());
let mut h = Array2::<F>::zeros((n, n));
let mut s = Array2::<F>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let aij = a[[i, j]];
let aji = a[[j, i]];
h[[i, j]] = (aij + aji) / two;
s[[i, j]] = (aij - aji) / two;
}
}
let pi = F::from(std::f64::consts::PI).unwrap_or(F::one());
let two_pi = two * pi;
let mut re_pts = Vec::with_capacity(n_samples);
let mut im_pts = Vec::with_capacity(n_samples);
for k in 0..n_samples {
let theta = two_pi * F::from(k).unwrap_or(F::zero()) / F::from(n_samples).unwrap_or(F::one());
let cos_t = theta.cos();
let sin_t = theta.sin();
let mut h_theta = Array2::<F>::zeros((n, n));
for i in 0..n {
for j in 0..n {
h_theta[[i, j]] = cos_t * h[[i, j]] + sin_t * s[[i, j]];
}
}
let lambda_max = power_iteration_max_eigenvalue(&h_theta.view(), 200)?;
re_pts.push(lambda_max * cos_t);
im_pts.push(lambda_max * sin_t);
}
Ok((re_pts, im_pts))
}
fn power_iteration_max_eigenvalue<F>(a: &ArrayView2<F>, max_iter: usize) -> LinalgResult<F>
where
F: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static,
{
let n = a.nrows();
if n == 0 {
return Ok(F::zero());
}
let mut rng = scirs2_core::random::rngs::SmallRng::seed_from_u64(12345);
let mut v = loop {
if let Some(u) = random_unit_vector(n, &mut rng) {
break u;
}
};
let tol = F::from(1e-10_f64).unwrap_or(F::epsilon());
let mut lambda_prev = F::zero();
for _iter in 0..max_iter {
let av = a.dot(&v);
let lambda = rayleigh_quotient_real(a, &v);
if (lambda - lambda_prev).abs() < tol {
return Ok(lambda);
}
lambda_prev = lambda;
let norm_sq: F = av.iter().fold(F::zero(), |acc, &x| acc + x * x);
if norm_sq <= F::epsilon() {
break;
}
let norm = norm_sq.sqrt();
v = av.mapv(|x| x / norm);
}
Ok(lambda_prev)
}
pub fn numerical_radius<F>(a: &ArrayView2<F>) -> LinalgResult<F>
where
F: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static,
{
let n_samples = 256;
let (re_pts, im_pts) = field_of_values(a, n_samples)?;
let radius = re_pts
.iter()
.zip(im_pts.iter())
.map(|(&r, &i)| (r * r + i * i).sqrt())
.fold(F::zero(), |acc, v| if v > acc { v } else { acc });
Ok(radius)
}
pub fn spectral_abscissa<F>(a: &ArrayView2<F>) -> LinalgResult<F>
where
F: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static,
{
let n = a.nrows();
if n == 0 {
return Err(LinalgError::InvalidInputError(
"spectral_abscissa: matrix must be non-empty".to_string(),
));
}
if a.ncols() != n {
return Err(LinalgError::ShapeError(
"spectral_abscissa: matrix must be square".to_string(),
));
}
let (eigenvalues, _) = eig(a, None)?;
let alpha = eigenvalues
.iter()
.map(|z| z.re)
.fold(F::neg_infinity(), |acc, re| if re > acc { re } else { acc });
Ok(alpha)
}
pub fn spectral_radius<F>(a: &ArrayView2<F>) -> LinalgResult<F>
where
F: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static,
{
let n = a.nrows();
if n == 0 {
return Err(LinalgError::InvalidInputError(
"spectral_radius: matrix must be non-empty".to_string(),
));
}
if a.ncols() != n {
return Err(LinalgError::ShapeError(
"spectral_radius: matrix must be square".to_string(),
));
}
let (eigenvalues, _) = eig(a, None)?;
let rho = eigenvalues
.iter()
.map(|z| (z.re * z.re + z.im * z.im).sqrt())
.fold(F::zero(), |acc, r| if r > acc { r } else { acc });
Ok(rho)
}
pub fn kreiss_number<F>(a: &ArrayView2<F>, grid_size: usize) -> LinalgResult<F>
where
F: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static,
{
let n = a.nrows();
if n == 0 {
return Err(LinalgError::InvalidInputError(
"kreiss_number: matrix must be non-empty".to_string(),
));
}
if a.ncols() != n {
return Err(LinalgError::ShapeError(
"kreiss_number: matrix must be square".to_string(),
));
}
let gs = grid_size.max(4);
let rho = spectral_radius(a)?;
let scale = (rho + F::one()).max(F::one());
let pi = F::from(std::f64::consts::PI).unwrap_or(F::one());
let half_pi = pi / (F::one() + F::one());
let half = F::from(0.5_f64).unwrap_or(F::one() / (F::one() + F::one()));
let mut kreiss = F::zero();
for ir in 0..gs {
let r_log_min = -3.0_f64 + scale.to_f64().unwrap_or(1.0).log10();
let r_log_max = scale.to_f64().unwrap_or(1.0).log10() + 0.6_f64;
let r_log = r_log_min + (r_log_max - r_log_min) * ir as f64 / (gs - 1) as f64;
let r = F::from(10.0_f64.powf(r_log)).unwrap_or(F::one());
for ia in 0..gs {
let frac = (ia as f64 + 0.5) / gs as f64;
let theta = half_pi * (F::from(frac * 2.0 - 1.0).unwrap_or(F::zero()));
let z_re = r * theta.cos();
let z_im = r * theta.sin();
if z_re <= F::zero() {
continue;
}
let sigma_min = match smallest_singular_value_shifted(a, z_re, z_im) {
Ok(s) => s,
Err(_) => continue,
};
if sigma_min <= F::zero() {
continue;
}
let candidate = z_re / sigma_min;
if candidate > kreiss {
kreiss = candidate;
}
if ia == gs / 2 {
let sigma_real = match smallest_singular_value_shifted(a, r, F::zero()) {
Ok(s) => s,
Err(_) => continue,
};
if sigma_real > F::zero() {
let c = r / sigma_real;
if c > kreiss {
kreiss = c;
}
}
}
}
for ia in 0..4 {
let eps_re = scale * F::from(1e-4_f64 * (ia + 1) as f64).unwrap_or(F::epsilon());
let y = scale * F::from(ia as f64 / 3.0).unwrap_or(F::zero());
let sigma = match smallest_singular_value_shifted(a, eps_re, y) {
Ok(s) => s,
Err(_) => continue,
};
if sigma > F::zero() {
let c = eps_re / sigma;
if c > kreiss {
kreiss = c;
}
}
}
let _ = half;
}
Ok(kreiss)
}
pub struct PseudospectrumResult<F> {
pub x_grid: Vec<F>,
pub y_grid: Vec<F>,
pub sigma_min: Array2<F>,
}
pub fn pseudospectrum<F>(
a: &ArrayView2<F>,
_epsilon_values: &[F],
grid_size: usize,
x_range: Option<(F, F)>,
y_range: Option<(F, F)>,
) -> LinalgResult<PseudospectrumResult<F>>
where
F: Float + NumAssign + Sum + ScalarOperand + Send + Sync + 'static,
{
let n = a.nrows();
if n == 0 {
return Err(LinalgError::InvalidInputError(
"pseudospectrum: matrix must be non-empty".to_string(),
));
}
if a.ncols() != n {
return Err(LinalgError::ShapeError(
"pseudospectrum: matrix must be square".to_string(),
));
}
let gs = grid_size.max(2);
let (eig_re, eig_im) = {
let (eigenvalues, _) = eig(a, None)?;
let re: Vec<F> = eigenvalues.iter().map(|z| z.re).collect();
let im: Vec<F> = eigenvalues.iter().map(|z| z.im).collect();
(re, im)
};
let eig_re_min = eig_re.iter().cloned().fold(F::infinity(), |a, b| if b < a { b } else { a });
let eig_re_max = eig_re.iter().cloned().fold(F::neg_infinity(), |a, b| if b > a { b } else { a });
let eig_im_min = eig_im.iter().cloned().fold(F::infinity(), |a, b| if b < a { b } else { a });
let eig_im_max = eig_im.iter().cloned().fold(F::neg_infinity(), |a, b| if b > a { b } else { a });
let two = F::from(2.0_f64).unwrap_or(F::one() + F::one());
let margin = ((eig_re_max - eig_re_min).abs() + (eig_im_max - eig_im_min).abs()) / two + F::one();
let (x_min, x_max) = x_range.unwrap_or((eig_re_min - margin, eig_re_max + margin));
let (y_min, y_max) = y_range.unwrap_or((eig_im_min - margin, eig_im_max + margin));
let gs_f = F::from(gs - 1).unwrap_or(F::one());
let x_step = (x_max - x_min) / gs_f;
let y_step = (y_max - y_min) / gs_f;
let x_grid: Vec<F> = (0..gs)
.map(|i| x_min + x_step * F::from(i).unwrap_or(F::zero()))
.collect();
let y_grid: Vec<F> = (0..gs)
.map(|i| y_min + y_step * F::from(i).unwrap_or(F::zero()))
.collect();
let mut sigma_min = Array2::<F>::zeros((gs, gs));
for iy in 0..gs {
for ix in 0..gs {
let z_re = x_grid[ix];
let z_im = y_grid[iy];
let s = smallest_singular_value_shifted(a, z_re, z_im)?;
sigma_min[[iy, ix]] = s;
}
}
Ok(PseudospectrumResult {
x_grid,
y_grid,
sigma_min,
})
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{array, Array2};
#[test]
fn test_spectral_abscissa_diagonal() {
let a = array![[-1.0_f64, 0.0], [0.0, -2.0]];
let alpha = spectral_abscissa(&a.view()).expect("spectral_abscissa");
assert!(
(alpha - (-1.0)).abs() < 1e-10,
"expected -1, got {alpha}"
);
}
#[test]
fn test_spectral_radius_rotation() {
let a = array![[0.0_f64, -1.0], [1.0, 0.0]];
let rho = spectral_radius(&a.view()).expect("spectral_radius");
assert!((rho - 1.0).abs() < 1e-10, "expected 1, got {rho}");
}
#[test]
fn test_spectral_radius_diagonal() {
let a = array![[3.0_f64, 0.0], [0.0, -5.0]];
let rho = spectral_radius(&a.view()).expect("spectral_radius");
assert!((rho - 5.0).abs() < 1e-10, "expected 5, got {rho}");
}
#[test]
fn test_spectral_abscissa_stable() {
let a = array![[-2.0_f64, 1.0], [0.0, -3.0]];
let alpha = spectral_abscissa(&a.view()).expect("spectral_abscissa");
assert!(alpha < 0.0, "stable matrix: spectral_abscissa should be <0");
}
#[test]
fn test_field_of_values_symmetric() {
let a = array![[1.0_f64, 0.0], [0.0, 3.0]];
let (re, im) = field_of_values(&a.view(), 64).expect("fov");
for (&rev, &imv) in re.iter().zip(im.iter()) {
let mag = (rev * rev + imv * imv).sqrt();
assert!(mag <= 3.1, "boundary point magnitude out of range: {mag}");
}
let max_mag = re.iter().zip(im.iter())
.map(|(&r, &i)| (r*r + i*i).sqrt())
.fold(0.0_f64, f64::max);
assert!(max_mag >= 2.9, "max boundary magnitude should be ≈3, got {max_mag}");
}
#[test]
fn test_numerical_radius_symmetric_positive() {
let a = array![[2.0_f64, 0.0], [0.0, 4.0]];
let w = numerical_radius(&a.view()).expect("numerical_radius");
assert!((w - 4.0).abs() < 0.2, "expected ≈4, got {w}");
}
#[test]
fn test_kreiss_number_nonnegative() {
let a = array![[0.0_f64, 1.0], [-1.0, 0.0]];
let k = kreiss_number(&a.view(), 8).expect("kreiss_number");
assert!(k >= 0.0, "kreiss_number should be >=0, got {k}");
}
#[test]
fn test_pseudospectrum_shape() {
let a = array![[1.0_f64, 0.0], [0.0, 2.0]];
let result = pseudospectrum(&a.view(), &[0.1_f64], 6, None, None)
.expect("pseudospectrum");
assert_eq!(result.sigma_min.shape(), &[6, 6]);
assert_eq!(result.x_grid.len(), 6);
assert_eq!(result.y_grid.len(), 6);
}
#[test]
fn test_pseudospectrum_on_eigenvalue() {
let a = array![[1.0_f64, 0.0], [0.0, 2.0]];
let result = pseudospectrum(
&a.view(),
&[0.01_f64],
4,
Some((0.5, 1.5)),
Some((-0.5, 0.5)),
)
.expect("pseudospectrum");
let min_sv = result
.sigma_min
.iter()
.cloned()
.fold(f64::INFINITY, f64::min);
assert!(min_sv < 0.6, "σ_min near eigenvalue should be small, got {min_sv}");
}
#[test]
fn test_field_of_values_empty_error() {
let a = Array2::<f64>::zeros((0, 0));
assert!(field_of_values(&a.view(), 64).is_err());
}
#[test]
fn test_field_of_values_nonsquare_error() {
let a = Array2::<f64>::zeros((2, 3));
assert!(field_of_values(&a.view(), 64).is_err());
}
}