use arpack::Error;
use arpack::Which;
use arpack::arnoldi::{Options, eigenpairs_c64};
use num_complex::Complex64;
fn c(re: f64, im: f64) -> Complex64 {
Complex64::new(re, im)
}
fn sorted_by_re(eigenvalues: &[Complex64]) -> Vec<Complex64> {
let mut sorted = eigenvalues.to_vec();
sorted.sort_by(|a, b| a.re.partial_cmp(&b.re).expect("eigenvalues are finite"));
sorted
}
#[test]
fn eigenpairs_smallest_real_part_three_laplacian() {
let n = 32usize;
let nev = 3;
let pi = std::f64::consts::PI;
let lambda = |k: usize| 2.0 - 2.0 * (k as f64 * pi / (n as f64 + 1.0)).cos();
let expected = [lambda(1), lambda(2), lambda(3)];
let solution = eigenpairs_c64(
n,
nev,
Which::SmallestRealPart,
|x, y| {
for i in 0..n {
let center = c(2.0, 0.0) * x[i];
let left = if i > 0 {
c(-1.0, 0.0) * x[i - 1]
} else {
c(0.0, 0.0)
};
let right = if i + 1 < n {
c(-1.0, 0.0) * x[i + 1]
} else {
c(0.0, 0.0)
};
y[i] = center + left + right;
}
},
&Options {
tol: 1e-12,
max_iter: 500,
ncv: None,
},
)
.expect("driver should converge");
assert!(
solution.nconv >= nev,
"expected full convergence (nconv >= nev); nconv = {}, nev = {}",
solution.nconv,
nev
);
assert_eq!(solution.eigenvalues.len(), nev);
assert_eq!(solution.eigenvectors.len(), nev);
let sorted = sorted_by_re(&solution.eigenvalues);
for (k, (&got, &exp)) in sorted.iter().zip(expected.iter()).enumerate() {
assert!(
got.im.abs() < 1e-9,
"Hermitian eigenvalue should be real; got im = {}",
got.im
);
let rel_err = (got.re - exp).abs() / exp.abs();
assert!(
rel_err < 1e-9,
"eigenvalue[{k}].re = {}, expected {exp}, rel_err = {rel_err}",
got.re
);
}
}
#[test]
fn eigenpairs_largest_real_part_three_laplacian() {
let n = 32usize;
let nev = 3;
let pi = std::f64::consts::PI;
let lambda = |k: usize| 2.0 - 2.0 * (k as f64 * pi / (n as f64 + 1.0)).cos();
let expected = [lambda(n - 2), lambda(n - 1), lambda(n)];
let solution = eigenpairs_c64(
n,
nev,
Which::LargestRealPart,
|x, y| {
for i in 0..n {
let center = c(2.0, 0.0) * x[i];
let left = if i > 0 {
c(-1.0, 0.0) * x[i - 1]
} else {
c(0.0, 0.0)
};
let right = if i + 1 < n {
c(-1.0, 0.0) * x[i + 1]
} else {
c(0.0, 0.0)
};
y[i] = center + left + right;
}
},
&Options {
tol: 1e-12,
max_iter: 500,
ncv: None,
},
)
.expect("driver should converge");
assert!(
solution.nconv >= nev,
"expected full convergence (nconv >= nev); nconv = {}, nev = {}",
solution.nconv,
nev
);
assert_eq!(solution.eigenvalues.len(), nev);
assert_eq!(solution.eigenvectors.len(), nev);
let sorted = sorted_by_re(&solution.eigenvalues);
for (k, (&got, &exp)) in sorted.iter().zip(expected.iter()).enumerate() {
let rel_err = (got.re - exp).abs() / exp.abs();
assert!(
rel_err < 1e-9,
"eigenvalue[{k}].re = {}, expected {exp}, rel_err = {rel_err}",
got.re
);
}
}
#[test]
fn eigenpairs_smallest_magnitude_complex_diag() {
let diag = [
c(-5.0, 0.0),
c(-3.0, 0.0),
c(1.0, 0.0),
c(2.0, 0.0),
c(4.0, 0.0),
c(7.0, 0.0),
c(10.0, 0.0),
c(15.0, 0.0),
];
let n = diag.len();
let nev = 3;
let solution = eigenpairs_c64(
n,
nev,
Which::SmallestMagnitude,
|x, y| {
for i in 0..n {
y[i] = diag[i] * x[i];
}
},
&Options {
tol: 1e-12,
max_iter: 300,
ncv: None,
},
)
.expect("driver should converge");
assert!(
solution.nconv >= nev,
"expected full convergence (nconv >= nev); nconv = {}, nev = {}",
solution.nconv,
nev
);
assert_eq!(solution.eigenvalues.len(), nev);
assert_eq!(solution.eigenvectors.len(), nev);
let sorted = sorted_by_re(&solution.eigenvalues);
let expected_re = [-3.0_f64, 1.0, 2.0];
for (k, (&got, &exp)) in sorted.iter().zip(expected_re.iter()).enumerate() {
assert!(
(got.re - exp).abs() < 1e-9 && got.im.abs() < 1e-9,
"eigenvalue[{k}] = {got} (expected {exp} + 0i)"
);
}
}
#[test]
fn eigenpairs_hermitian_imaginary_off_diagonals_nev3() {
let n = 8usize;
let nev = 3;
let pi = std::f64::consts::PI;
let lambda = |k: usize| 2.0 - 2.0 * (k as f64 * pi / (n as f64 + 1.0)).cos();
let expected = [lambda(1), lambda(2), lambda(3)];
let im = c(0.0, 1.0);
let solution = eigenpairs_c64(
n,
nev,
Which::SmallestRealPart,
|x, y| {
for i in 0..n {
let center = c(2.0, 0.0) * x[i];
let upper = if i + 1 < n {
im * x[i + 1]
} else {
c(0.0, 0.0)
};
let lower = if i > 0 { -im * x[i - 1] } else { c(0.0, 0.0) };
y[i] = center + upper + lower;
}
},
&Options {
tol: 1e-12,
max_iter: 500,
ncv: None,
},
)
.expect("driver should converge");
assert!(
solution.nconv >= nev,
"expected full convergence (nconv >= nev); nconv = {}, nev = {}",
solution.nconv,
nev
);
assert_eq!(solution.eigenvalues.len(), nev);
assert_eq!(solution.eigenvectors.len(), nev);
let sorted = sorted_by_re(&solution.eigenvalues);
for (k, (&got, &exp)) in sorted.iter().zip(expected.iter()).enumerate() {
assert!(
got.im.abs() < 1e-9,
"eigenvalue[{k}].im = {} (expected ~0 for Hermitian)",
got.im
);
let rel_err = (got.re - exp).abs() / exp.abs();
assert!(
rel_err < 1e-9,
"eigenvalue[{k}].re = {}, expected {exp}, rel_err = {rel_err}",
got.re
);
}
let total_imag: f64 = solution
.eigenvectors
.iter()
.flat_map(|v| v.iter())
.map(|c| c.im.abs())
.sum();
assert!(
total_imag > 1e-2,
"expected non-trivial imaginary content in eigenvectors; sum |im| = {total_imag}"
);
}
#[test]
fn eigenpairs_rejects_symmetric_only_which_for_complex() {
let n = 8;
let result = eigenpairs_c64(
n,
2,
Which::SmallestAlgebraic,
|_x, _y| unreachable!(),
&Options::default(),
);
assert!(matches!(result, Err(Error::InvalidParam(_))));
}
#[test]
fn eigenpairs_huge_nev_is_rejected_without_overflow() {
let n = 8;
let result = eigenpairs_c64(
n,
usize::MAX,
Which::SmallestRealPart,
|_x, _y| unreachable!("matvec should not run when nev overflows c_int"),
&Options::default(),
);
assert!(matches!(result, Err(Error::InvalidParam(_))));
}
#[test]
fn eigenpairs_nev_zero_is_rejected() {
let n = 8;
let result = eigenpairs_c64(
n,
0,
Which::SmallestRealPart,
|_x, _y| unreachable!(),
&Options::default(),
);
assert!(matches!(result, Err(Error::InvalidParam(_))));
}
#[test]
fn eigenpairs_max_iter_one_yields_max_iter_reached_or_partial() {
let n = 64usize;
let nev = 3;
let matvec_fn = |x: &[Complex64], y: &mut [Complex64]| {
for i in 0..n {
let center = c(2.0, 0.0) * x[i];
let left = if i > 0 {
c(-1.0, 0.0) * x[i - 1]
} else {
c(0.0, 0.0)
};
let right = if i + 1 < n {
c(-1.0, 0.0) * x[i + 1]
} else {
c(0.0, 0.0)
};
y[i] = center + left + right;
}
};
let result = eigenpairs_c64(
n,
nev,
Which::SmallestRealPart,
matvec_fn,
&Options {
tol: 1e-15,
max_iter: 1,
ncv: None,
},
);
match result {
Err(Error::MaxIterReached { nconv, iters, .. }) => {
assert_eq!(nconv, 0, "MaxIterReached must carry nconv = 0");
assert!(iters >= 1);
}
Ok(solution) => {
assert!(
solution.nconv >= 1 && solution.nconv <= nev,
"partial-Ok branch requires 0 < nconv <= nev, got nconv = {}",
solution.nconv
);
assert_eq!(solution.eigenvalues.len(), solution.nconv);
assert_eq!(solution.eigenvectors.len(), solution.nconv);
}
other => panic!("expected MaxIterReached or Ok partial, got {other:?}"),
}
}