use super::ConvergenceError;
use super::hessenberg::HessenbergMatrix;
use super::power_iteration::{Slice, normalize};
use crate::algorithm::matrix::mul_vector;
use crate::algorithm::vector::{dot, norm};
use crate::scalar::Scalar;
use crate::storage::{Basis, Storage};
fn subtract_scaled<T: Scalar>(y: &mut [T], coefficient: T, x: &[T]) {
for (slot, &x_i) in y.iter_mut().zip(x.iter()) {
*slot = slot.sub(coefficient.mul(x_i));
}
}
pub fn arnoldi<S, V, T, const K: usize>(
a: &S,
n: usize,
v0: &V,
tol: T,
basis: &mut Basis<'_, T, K>,
scratch: &mut [T],
) -> Result<(HessenbergMatrix<T, K>, usize), ConvergenceError>
where
S: Storage<Item = T>,
V: Storage<Item = T>,
T: Scalar + PartialOrd,
{
if a.len() != n * n || v0.len() != n || basis.vector_len() != n || scratch.len() != n || K > n {
return Err(ConvergenceError::DimensionMismatch);
}
for (i, slot) in scratch.iter_mut().enumerate() {
let Some(&x) = v0.get(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
*slot = x;
}
normalize(scratch)?;
let mut data = [[T::zero(); K]; K];
if K == 0 {
return Ok((HessenbergMatrix::new(data), 0));
}
{
let Some(q_0) = basis.vector_mut(0) else {
return Err(ConvergenceError::DimensionMismatch);
};
q_0.copy_from_slice(scratch);
}
for j in 0..K {
let norm_aq = {
let Some(q_j) = basis.vector(j) else {
return Err(ConvergenceError::DimensionMismatch);
};
if mul_vector(a, n, n, &Slice { data: q_j }, scratch).is_err() {
return Err(ConvergenceError::DimensionMismatch);
}
norm(&Slice { data: &*scratch })
};
for (i, row) in data.iter_mut().enumerate().take(j + 1) {
let Some(q_i) = basis.vector(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
let Ok(h_ij) = dot(&Slice { data: q_i }, &Slice { data: &*scratch }) else {
return Err(ConvergenceError::DimensionMismatch);
};
row[j] = h_ij;
subtract_scaled(scratch, h_ij, q_i);
}
let h_next = norm(&Slice { data: &*scratch });
let probe = h_next.sub(h_next);
#[allow(clippy::eq_op)] let non_finite = probe != probe;
if non_finite {
return Err(ConvergenceError::NonFinite);
}
if h_next <= tol.mul(norm_aq) {
return Ok((HessenbergMatrix::new(data), j + 1));
}
if j + 1 < K {
let Some(q_next) = basis.vector_mut(j + 1) else {
return Err(ConvergenceError::DimensionMismatch);
};
let inv = T::one().div(h_next);
for (slot, &w_i) in q_next.iter_mut().zip(scratch.iter()) {
*slot = w_i.mul(inv);
}
data[j + 1][j] = h_next;
}
}
Ok((HessenbergMatrix::new(data), K))
}
#[cfg(test)]
mod tests {
use super::arnoldi;
use crate::krylov::ConvergenceError;
use crate::storage::{Basis, StaticStorage};
fn assert_close(actual: f64, expected: f64, tol: f64) {
assert!(
(actual - expected).abs() < tol,
"expected {expected}, got {actual}"
);
}
fn projection_entry<const K: usize>(
a: &[f64],
n: usize,
basis: &Basis<'_, f64, K>,
r: usize,
c: usize,
) -> f64 {
let q_r = basis.vector(r).unwrap();
let q_c = basis.vector(c).unwrap();
let mut sum = 0.0;
for row in 0..n {
for col in 0..n {
sum += q_r[row] * a[row * n + col] * q_c[col];
}
}
sum
}
#[test]
fn hessenberg_input_from_e1_reproduces_itself() {
let a = StaticStorage::new([2.0, 1.0, 0.0, 3.0, 2.0, 1.0, 0.0, 1.0, 2.0]);
let v0 = StaticStorage::new([1.0, 0.0, 0.0]);
let mut buffer = [0.0; 9];
let mut basis = Basis::<f64, 3>::new(&mut buffer, 3).unwrap();
let mut scratch = [0.0; 3];
let (h, reached) = arnoldi(&a, 3, &v0, 1e-12, &mut basis, &mut scratch).unwrap();
assert_eq!(reached, 3);
let expected = [[2.0, 1.0, 0.0], [3.0, 2.0, 1.0], [0.0, 1.0, 2.0]];
for (r, expected_row) in expected.iter().enumerate() {
for (c, &expected_val) in expected_row.iter().enumerate() {
assert_close(h.entry(r, c).unwrap(), expected_val, 1e-12);
}
}
for k in 0..3 {
let q_k = basis.vector(k).unwrap();
for (i, &entry) in q_k.iter().enumerate() {
assert_close(entry, if i == k { 1.0 } else { 0.0 }, 1e-12);
}
}
}
#[test]
fn basis_is_orthonormal_and_hessenberg_structure_holds() {
let a = [4.0, 1.0, 2.0, 3.0, 3.0, 1.0, 5.0, 1.0, 5.0];
let v0 = StaticStorage::new([1.0, 1.0, 1.0]);
let mut buffer = [0.0; 9];
let mut basis = Basis::<f64, 3>::new(&mut buffer, 3).unwrap();
let mut scratch = [0.0; 3];
let (h, reached) = arnoldi(
&StaticStorage::new(a),
3,
&v0,
1e-12,
&mut basis,
&mut scratch,
)
.unwrap();
assert_eq!(reached, 3);
for r in 0..3 {
for c in 0..3 {
let q_r = basis.vector(r).unwrap();
let q_c = basis.vector(c).unwrap();
let inner: f64 = q_r.iter().zip(q_c.iter()).map(|(x, y)| x * y).sum();
assert_close(inner, if r == c { 1.0 } else { 0.0 }, 1e-12);
}
}
assert_close(h.entry(2, 0).unwrap(), 0.0, 1e-10);
assert_close(
projection_entry(&a, 3, &basis, 0, 0),
h.entry(0, 0).unwrap(),
1e-10,
);
assert_close(
projection_entry(&a, 3, &basis, 1, 0),
h.entry(1, 0).unwrap(),
1e-10,
);
assert_close(
projection_entry(&a, 3, &basis, 2, 1),
h.entry(2, 1).unwrap(),
1e-10,
);
}
#[test]
fn partial_basis_projects_onto_the_leading_block() {
let a = [4.0, 1.0, 2.0, 3.0, 3.0, 1.0, 5.0, 1.0, 5.0];
let v0 = StaticStorage::new([1.0, 1.0, 1.0]);
let mut buffer = [0.0; 6];
let mut basis = Basis::<f64, 2>::new(&mut buffer, 3).unwrap();
let mut scratch = [0.0; 3];
let (h, reached) = arnoldi(
&StaticStorage::new(a),
3,
&v0,
1e-12,
&mut basis,
&mut scratch,
)
.unwrap();
assert_eq!(reached, 2);
let q_0 = basis.vector(0).unwrap();
let q_1 = basis.vector(1).unwrap();
let inner: f64 = q_0.iter().zip(q_1.iter()).map(|(x, y)| x * y).sum();
assert_close(inner, 0.0, 1e-12);
assert_close(
projection_entry(&a, 3, &basis, 0, 0),
h.entry(0, 0).unwrap(),
1e-10,
);
assert_close(
projection_entry(&a, 3, &basis, 0, 1),
h.entry(0, 1).unwrap(),
1e-10,
);
assert_close(
projection_entry(&a, 3, &basis, 1, 0),
h.entry(1, 0).unwrap(),
1e-10,
);
assert_close(
projection_entry(&a, 3, &basis, 1, 1),
h.entry(1, 1).unwrap(),
1e-10,
);
}
#[test]
fn k_equals_one_yields_the_rayleigh_quotient() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([3.0, 4.0]);
let mut buffer = [0.0; 2];
let mut basis = Basis::<f64, 1>::new(&mut buffer, 2).unwrap();
let mut scratch = [0.0; 2];
let (h, reached) = arnoldi(&a, 2, &v0, 1e-12, &mut basis, &mut scratch).unwrap();
assert_eq!(reached, 1);
assert_close(h.entry(0, 0).unwrap(), 34.0 / 25.0, 1e-12);
assert_close(basis.vector(0).unwrap()[0], 0.6, 1e-12);
assert_close(basis.vector(0).unwrap()[1], 0.8, 1e-12);
}
#[test]
fn k_zero_produces_an_empty_projection() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([1.0, 0.0]);
let mut buffer: [f64; 0] = [];
let mut basis = Basis::<f64, 0>::new(&mut buffer, 2).unwrap();
let mut scratch = [0.0; 2];
let (h, reached) = arnoldi(&a, 2, &v0, 1e-12, &mut basis, &mut scratch).unwrap();
assert_eq!(reached, 0);
assert_eq!(h.entry(0, 0), None);
}
#[test]
fn repeated_eigenvalue_breaks_down_as_success_not_error() {
let a = StaticStorage::new([1.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([1.0, 1.0]);
let mut buffer = [0.0; 4];
let mut basis = Basis::<f64, 2>::new(&mut buffer, 2).unwrap();
let mut scratch = [0.0; 2];
let (h, reached) = arnoldi(&a, 2, &v0, 1e-12, &mut basis, &mut scratch).unwrap();
assert_eq!(reached, 1);
assert_close(h.entry(0, 0).unwrap(), 1.0, 1e-12);
}
#[test]
fn v0_inside_an_invariant_subspace_breaks_down_as_success_not_error() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([1.0, 0.0]);
let mut buffer = [0.0; 4];
let mut basis = Basis::<f64, 2>::new(&mut buffer, 2).unwrap();
let mut scratch = [0.0; 2];
let (h, reached) = arnoldi(&a, 2, &v0, 1e-12, &mut basis, &mut scratch).unwrap();
assert_eq!(reached, 1);
assert_close(h.entry(0, 0).unwrap(), 2.0, 1e-12);
}
#[test]
fn rank_deficient_matrix_breaks_down_early() {
let a = StaticStorage::new([1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]);
let v0 = StaticStorage::new([1.0, 1.0, 1.0]);
let mut buffer = [0.0; 9];
let mut basis = Basis::<f64, 3>::new(&mut buffer, 3).unwrap();
let mut scratch = [0.0; 3];
let (_, reached) = arnoldi(&a, 3, &v0, 1e-10, &mut basis, &mut scratch).unwrap();
assert_eq!(reached, 1);
}
#[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 buffer = [0.0; 4];
let mut basis = Basis::<f64, 2>::new(&mut buffer, 2).unwrap();
let mut scratch = [0.0; 2];
let result = arnoldi(&a, 2, &v0, 1e-12, &mut basis, &mut scratch);
assert_eq!(result, Err(ConvergenceError::ZeroVector));
}
#[test]
fn zero_initial_vector_is_an_error_even_when_k_is_zero() {
let a = StaticStorage::new([2.0, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([0.0, 0.0]);
let mut buffer: [f64; 0] = [];
let mut basis = Basis::<f64, 0>::new(&mut buffer, 2).unwrap();
let mut scratch = [0.0; 2];
let result = arnoldi(&a, 2, &v0, 1e-12, &mut basis, &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 buffer = [0.0; 4];
let mut basis = Basis::<f64, 2>::new(&mut buffer, 2).unwrap();
let mut scratch = [0.0; 2];
let result = arnoldi(&a, 2, &v0, 1e-12, &mut basis, &mut scratch);
assert_eq!(result, Err(ConvergenceError::NonFinite));
}
#[test]
fn non_finite_matrix_entry_is_an_error_not_a_breakdown() {
for poison in [f64::NAN, f64::INFINITY] {
let a = StaticStorage::new([poison, 0.0, 0.0, 1.0]);
let v0 = StaticStorage::new([1.0, 1.0]);
let mut buffer = [0.0; 4];
let mut basis = Basis::<f64, 2>::new(&mut buffer, 2).unwrap();
let mut scratch = [0.0; 2];
let result = arnoldi(&a, 2, &v0, 1e-12, &mut basis, &mut scratch);
assert_eq!(result, Err(ConvergenceError::NonFinite));
}
}
#[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 v0_3 = StaticStorage::new([1.0, 0.0, 0.0]);
let mut buffer_3 = [0.0; 6];
let mut basis_3 = Basis::<f64, 2>::new(&mut buffer_3, 3).unwrap();
let mut scratch_3 = [0.0; 3];
assert_eq!(
arnoldi(&a, 3, &v0_3, 1e-12, &mut basis_3, &mut scratch_3),
Err(ConvergenceError::DimensionMismatch)
);
let v0_short = StaticStorage::new([1.0]);
let mut buffer = [0.0; 4];
let mut basis = Basis::<f64, 2>::new(&mut buffer, 2).unwrap();
let mut scratch = [0.0; 2];
assert_eq!(
arnoldi(&a, 2, &v0_short, 1e-12, &mut basis, &mut scratch),
Err(ConvergenceError::DimensionMismatch)
);
let mut buffer_wrong = [0.0; 2];
let mut basis_wrong = Basis::<f64, 2>::new(&mut buffer_wrong, 1).unwrap();
assert_eq!(
arnoldi(&a, 2, &v0, 1e-12, &mut basis_wrong, &mut scratch),
Err(ConvergenceError::DimensionMismatch)
);
let mut scratch_short = [0.0; 1];
assert_eq!(
arnoldi(&a, 2, &v0, 1e-12, &mut basis, &mut scratch_short),
Err(ConvergenceError::DimensionMismatch)
);
let mut buffer_deep = [0.0; 6];
let mut basis_deep = Basis::<f64, 3>::new(&mut buffer_deep, 2).unwrap();
assert_eq!(
arnoldi(&a, 2, &v0, 1e-12, &mut basis_deep, &mut scratch),
Err(ConvergenceError::DimensionMismatch)
);
}
}