use super::ConvergenceError;
use super::power_iteration::{Slice, normalize};
use crate::algorithm::matrix::{abs, mul_vector};
use crate::algorithm::vector::dot;
use crate::scalar::Scalar;
use crate::storage::Storage;
#[allow(clippy::too_many_arguments)] pub fn inverse_power_iteration<S, V, T>(
a: &S,
n: usize,
v0: &V,
shift: T,
max_iter: usize,
tol: T,
singular_tol: T,
out_eigenvector: &mut [T],
factor: &mut [T],
pivots: &mut [usize],
scratch: &mut [T],
) -> Result<T, ConvergenceError>
where
S: Storage<Item = T>,
V: Storage<Item = T>,
T: Scalar + PartialOrd,
{
if a.len() != n * n
|| v0.len() != n
|| out_eigenvector.len() != n
|| factor.len() != n * n
|| pivots.len() != n
|| scratch.len() != n
{
return Err(ConvergenceError::DimensionMismatch);
}
for (i, slot) in factor.iter_mut().enumerate() {
let Some(&x) = a.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
*slot = if i % (n + 1) == 0 { x.sub(shift) } else { x };
}
factorize(factor, pivots, n, singular_tol)?;
for (i, slot) in out_eigenvector.iter_mut().enumerate() {
let Some(&x) = v0.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
*slot = x;
}
normalize(out_eigenvector)?;
let mut prev_mu: Option<T> = None;
for _ in 0..max_iter {
for (slot, &x) in scratch.iter_mut().zip(out_eigenvector.iter()) {
*slot = x;
}
solve_in_place(factor, pivots, n, scratch);
let Ok(mu) = dot(
&Slice {
data: &*out_eigenvector,
},
&Slice { data: &*scratch },
) else {
return Err(ConvergenceError::DimensionMismatch);
};
let mut residual_sq = T::zero();
for (&w_i, &v_i) in scratch.iter().zip(out_eigenvector.iter()) {
let r = w_i.sub(mu.mul(v_i));
residual_sq = residual_sq.add(r.mul(r));
}
let residual = residual_sq.sqrt();
for (slot, &w_i) in out_eigenvector.iter_mut().zip(scratch.iter()) {
*slot = w_i;
}
normalize(out_eigenvector)?;
let scale = abs(mu);
let mu_stable = match prev_mu {
Some(prev) => abs(mu.sub(prev)) <= tol.mul(scale),
None => false,
};
if mu_stable && residual <= tol.mul(scale) {
if mul_vector(
a,
n,
n,
&Slice {
data: &*out_eigenvector,
},
scratch,
)
.is_err()
{
return Err(ConvergenceError::DimensionMismatch);
}
let Ok(eigenvalue) = dot(
&Slice {
data: &*out_eigenvector,
},
&Slice { data: &*scratch },
) else {
return Err(ConvergenceError::DimensionMismatch);
};
return Ok(eigenvalue);
}
prev_mu = Some(mu);
}
Err(ConvergenceError::MaxIterationsExceeded)
}
fn factorize<T: Scalar + PartialOrd>(
factor: &mut [T],
pivots: &mut [usize],
n: usize,
singular_tol: T,
) -> Result<(), ConvergenceError> {
let mut largest = T::zero();
for &x in factor.iter() {
let magnitude = abs(x);
if magnitude > largest {
largest = magnitude;
}
}
let pivot_floor = singular_tol.mul(largest);
if n == 0 || largest == T::zero() {
return Err(ConvergenceError::SingularShift);
}
for k in 0..n {
let mut best_row = k;
let mut best_abs = abs(factor[k * n + k]);
for r in (k + 1)..n {
let candidate_abs = abs(factor[r * n + k]);
if candidate_abs > best_abs {
best_abs = candidate_abs;
best_row = r;
}
}
pivots[k] = best_row;
if best_row != k {
for c in 0..n {
factor.swap(k * n + c, best_row * n + c);
}
}
let pivot = factor[k * n + k];
if abs(pivot) <= pivot_floor {
return Err(ConvergenceError::SingularShift);
}
for i in (k + 1)..n {
let multiplier = factor[i * n + k].div(pivot);
factor[i * n + k] = multiplier;
for c in (k + 1)..n {
let term = multiplier.mul(factor[k * n + c]);
factor[i * n + c] = factor[i * n + c].sub(term);
}
}
}
Ok(())
}
fn solve_in_place<T: Scalar + PartialOrd>(factor: &[T], pivots: &[usize], n: usize, b: &mut [T]) {
for (k, &p) in pivots.iter().enumerate() {
if p != k {
b.swap(k, p);
}
}
for i in 1..n {
let mut sum = b[i];
for j in 0..i {
sum = sum.sub(factor[i * n + j].mul(b[j]));
}
b[i] = sum;
}
for i in (0..n).rev() {
let mut sum = b[i];
for j in (i + 1)..n {
sum = sum.sub(factor[i * n + j].mul(b[j]));
}
b[i] = sum.div(factor[i * n + i]);
}
}
#[cfg(test)]
mod tests {
use super::inverse_power_iteration;
use crate::krylov::ConvergenceError;
use crate::storage::StaticStorage;
fn assert_close(actual: f64, expected: f64, tol: f64) {
assert!(
(actual - expected).abs() < tol,
"expected {expected}, got {actual}"
);
}
#[test]
fn shift_selects_the_smaller_eigenvalue_power_iteration_cannot_reach() {
let a = StaticStorage::new([2.0, 1.0, 1.0, 2.0]);
let v0 = StaticStorage::new([1.0, 0.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let eigenvalue = inverse_power_iteration(
&a,
2,
&v0,
0.9,
200,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
)
.unwrap();
assert_close(eigenvalue, 1.0, 1e-9);
let inv_sqrt2 = 1.0 / core::f64::consts::SQRT_2;
assert_close(eigenvector[0].abs(), inv_sqrt2, 1e-6);
assert_close(eigenvector[1].abs(), inv_sqrt2, 1e-6);
assert!(eigenvector[0] * eigenvector[1] < 0.0);
}
#[test]
fn shift_near_the_dominant_eigenvalue_selects_it() {
let a = StaticStorage::new([2.0, 1.0, 1.0, 2.0]);
let v0 = StaticStorage::new([1.0, 0.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let eigenvalue = inverse_power_iteration(
&a,
2,
&v0,
2.9,
200,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
)
.unwrap();
assert_close(eigenvalue, 3.0, 1e-9);
assert!(eigenvector[0] * eigenvector[1] > 0.0);
}
#[test]
fn zero_shift_finds_the_smallest_magnitude_eigenvalue() {
let a = StaticStorage::new([
5.0, 0.0, 0.0, 0.0, 0.5, 0.0, 0.0, 0.0, -2.0,
]);
let v0 = StaticStorage::new([1.0, 1.0, 1.0]);
let mut eigenvector = [0.0; 3];
let mut factor = [0.0; 9];
let mut pivots = [0_usize; 3];
let mut scratch = [0.0; 3];
let eigenvalue = inverse_power_iteration(
&a,
3,
&v0,
0.0,
500,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
)
.unwrap();
assert_close(eigenvalue, 0.5, 1e-9);
assert_close(eigenvector[0], 0.0, 1e-6);
assert_close(eigenvector[1].abs(), 1.0, 1e-6);
assert_close(eigenvector[2], 0.0, 1e-6);
}
#[test]
fn one_by_one_matrix() {
let a = StaticStorage::new([5.0]);
let v0 = StaticStorage::new([-2.0]);
let mut eigenvector = [0.0; 1];
let mut factor = [0.0; 1];
let mut pivots = [0_usize; 1];
let mut scratch = [0.0; 1];
let eigenvalue = inverse_power_iteration(
&a,
1,
&v0,
1.0,
10,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
)
.unwrap();
assert_close(eigenvalue, 5.0, 1e-12);
assert_close(eigenvector[0].abs(), 1.0, 1e-12);
}
#[test]
fn non_symmetric_matrix_satisfies_the_eigenpair_equation() {
let a = [3.0, 2.0, 1.0, 2.0];
let v0 = StaticStorage::new([1.0, 0.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let eigenvalue = inverse_power_iteration(
&StaticStorage::new(a),
2,
&v0,
0.5,
500,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
)
.unwrap();
assert_close(eigenvalue, 1.0, 1e-9);
assert!(eigenvector[0] * eigenvector[1] < 0.0);
for row in 0..2 {
let av = a[row * 2] * eigenvector[0] + a[row * 2 + 1] * eigenvector[1];
assert_close(av, eigenvalue * eigenvector[row], 1e-8);
}
}
#[test]
fn shift_far_outside_the_spectrum_still_selects_the_nearest_eigenvalue() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([1.0, 1.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let eigenvalue = inverse_power_iteration(
&a,
2,
&v0,
100.0,
20_000,
1e-8,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
)
.unwrap();
assert_close(eigenvalue, 2.0, 1e-6);
}
#[test]
fn shift_exactly_at_an_eigenvalue_is_a_singular_shift_error() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([1.0, 1.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let result = inverse_power_iteration(
&a,
2,
&v0,
2.0,
100,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
);
assert_eq!(result, Err(ConvergenceError::SingularShift));
}
#[test]
fn singular_matrix_with_zero_shift_is_a_singular_shift_error() {
let a = StaticStorage::new([1.0, 1.0, 1.0, 1.0]);
let v0 = StaticStorage::new([1.0, 0.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let result = inverse_power_iteration(
&a,
2,
&v0,
0.0,
100,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
);
assert_eq!(result, Err(ConvergenceError::SingularShift));
}
#[test]
fn near_singular_shift_past_the_tolerance_converges_fast_not_badly() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([1.0, 1.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let eigenvalue = inverse_power_iteration(
&a,
2,
&v0,
2.0 - 1e-8,
100,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
)
.unwrap();
assert_close(eigenvalue, 2.0, 1e-6);
assert_close(eigenvector[0].abs(), 1.0, 1e-6);
assert_close(eigenvector[1], 0.0, 1e-6);
}
#[test]
fn zero_initial_vector_is_an_error_not_a_panic() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([0.0, 0.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let result = inverse_power_iteration(
&a,
2,
&v0,
0.5,
100,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
);
assert_eq!(result, Err(ConvergenceError::ZeroVector));
}
#[test]
fn non_finite_initial_vector_is_a_distinct_error_from_zero_vector() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([f64::NAN, 0.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let result = inverse_power_iteration(
&a,
2,
&v0,
0.5,
100,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
);
assert_eq!(result, Err(ConvergenceError::NonFinite));
}
#[test]
fn too_small_iteration_budget_is_an_error_not_a_panic() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([1.0, 0.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
for max_iter in [0, 1] {
let result = inverse_power_iteration(
&a,
2,
&v0,
1.9,
max_iter,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
);
assert_eq!(result, Err(ConvergenceError::MaxIterationsExceeded));
}
}
#[test]
fn mismatched_dimensions_are_an_error_not_a_panic() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([1.0, 0.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let v0_short = StaticStorage::new([1.0]);
assert_eq!(
inverse_power_iteration(
&a,
2,
&v0_short,
0.5,
10,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
),
Err(ConvergenceError::DimensionMismatch)
);
let mut factor_short = [0.0; 3];
assert_eq!(
inverse_power_iteration(
&a,
2,
&v0,
0.5,
10,
1e-12,
1e-12,
&mut eigenvector,
&mut factor_short,
&mut pivots,
&mut scratch,
),
Err(ConvergenceError::DimensionMismatch)
);
let mut pivots_short = [0_usize; 1];
assert_eq!(
inverse_power_iteration(
&a,
2,
&v0,
0.5,
10,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots_short,
&mut scratch,
),
Err(ConvergenceError::DimensionMismatch)
);
let mut eigenvector_short = [0.0; 1];
assert_eq!(
inverse_power_iteration(
&a,
2,
&v0,
0.5,
10,
1e-12,
1e-12,
&mut eigenvector_short,
&mut factor,
&mut pivots,
&mut scratch,
),
Err(ConvergenceError::DimensionMismatch)
);
let mut scratch_short = [0.0; 1];
assert_eq!(
inverse_power_iteration(
&a,
2,
&v0,
0.5,
10,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch_short,
),
Err(ConvergenceError::DimensionMismatch)
);
}
#[test]
fn pivoting_handles_a_zero_landing_on_the_shifted_diagonal() {
let a = StaticStorage::new([1.0, 1.0, 1.0, 0.0]);
let v0 = StaticStorage::new([1.0, 0.0]);
let mut eigenvector = [0.0; 2];
let mut factor = [0.0; 4];
let mut pivots = [0_usize; 2];
let mut scratch = [0.0; 2];
let eigenvalue = inverse_power_iteration(
&a,
2,
&v0,
1.0,
500,
1e-12,
1e-12,
&mut eigenvector,
&mut factor,
&mut pivots,
&mut scratch,
)
.unwrap();
let golden_ratio = (1.0 + 5.0_f64.sqrt()) / 2.0;
assert_close(eigenvalue, golden_ratio, 1e-9);
}
}