use super::ConvergenceError;
use super::power_iteration::{Slice, normalize};
use super::tridiagonal::TridiagonalMatrix;
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 lanczos<S, V, T, const K: usize>(
a: &S,
n: usize,
v0: &V,
tol: T,
basis: &mut Basis<'_, T, K>,
scratch: &mut [T],
) -> Result<TridiagonalMatrix<T, K>, 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 diagonal = [T::zero(); K];
let mut off_diagonal = [T::zero(); K];
if K == 0 {
return Ok(TridiagonalMatrix::new(diagonal, off_diagonal));
}
{
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 })
};
{
let Some(q_j) = basis.vector(j) else {
return Err(ConvergenceError::DimensionMismatch);
};
let Ok(alpha_j) = dot(&Slice { data: q_j }, &Slice { data: &*scratch }) else {
return Err(ConvergenceError::DimensionMismatch);
};
diagonal[j] = alpha_j;
subtract_scaled(scratch, alpha_j, q_j);
}
if j > 0 {
let Some(q_prev) = basis.vector(j - 1) else {
return Err(ConvergenceError::DimensionMismatch);
};
subtract_scaled(scratch, off_diagonal[j - 1], q_prev);
}
for i in 0..=j {
let Some(q_i) = basis.vector(i) else {
return Err(ConvergenceError::DimensionMismatch);
};
let Ok(residue) = dot(&Slice { data: q_i }, &Slice { data: &*scratch }) else {
return Err(ConvergenceError::DimensionMismatch);
};
subtract_scaled(scratch, residue, q_i);
}
let beta_j = norm(&Slice { data: &*scratch });
let probe = beta_j.sub(beta_j);
#[allow(clippy::eq_op)] let non_finite = probe != probe;
if non_finite {
return Err(ConvergenceError::NonFinite);
}
if j + 1 < K {
if beta_j <= tol.mul(norm_aq) {
return Err(ConvergenceError::Breakdown);
}
let Some(q_next) = basis.vector_mut(j + 1) else {
return Err(ConvergenceError::DimensionMismatch);
};
let inv = T::one().div(beta_j);
for (slot, &w_i) in q_next.iter_mut().zip(scratch.iter()) {
*slot = w_i.mul(inv);
}
off_diagonal[j] = beta_j;
}
}
Ok(TridiagonalMatrix::new(diagonal, off_diagonal))
}
#[cfg(test)]
mod tests {
use super::lanczos;
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(a: &[f64], n: usize, basis: &Basis<'_, f64, 3>, 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 tridiagonal_input_from_e1_reproduces_itself() {
let a = StaticStorage::new([2.0, 1.0, 0.0, 1.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 t = lanczos(&a, 3, &v0, 1e-12, &mut basis, &mut scratch).unwrap();
for j in 0..3 {
assert_close(t.diagonal()[j], 2.0, 1e-12);
}
for j in 0..2 {
assert_close(t.off_diagonal()[j], 1.0, 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_projection_matches_the_tridiagonal() {
let a = [4.0, 1.0, 2.0, 1.0, 3.0, 1.0, 2.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 t = lanczos(
&StaticStorage::new(a),
3,
&v0,
1e-12,
&mut basis,
&mut scratch,
)
.unwrap();
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);
let expected = match (r, c) {
_ if r == c => t.diagonal()[r],
_ if r.abs_diff(c) == 1 => t.off_diagonal()[r.min(c)],
_ => 0.0,
};
assert_close(projection_entry(&a, 3, &basis, r, c), expected, 1e-12);
}
}
}
#[test]
fn partial_basis_projects_onto_the_leading_block() {
let a = [4.0, 1.0, 2.0, 1.0, 3.0, 1.0, 2.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 t = lanczos(
&StaticStorage::new(a),
3,
&v0,
1e-12,
&mut basis,
&mut scratch,
)
.unwrap();
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);
let mut projected = [[0.0; 2]; 2];
for (r, q_r) in [q_0, q_1].iter().enumerate() {
for (c, q_c) in [q_0, q_1].iter().enumerate() {
for row in 0..3 {
for col in 0..3 {
projected[r][c] += q_r[row] * a[row * 3 + col] * q_c[col];
}
}
}
}
assert_close(projected[0][0], t.diagonal()[0], 1e-12);
assert_close(projected[1][1], t.diagonal()[1], 1e-12);
assert_close(projected[0][1], t.off_diagonal()[0], 1e-12);
assert_close(projected[1][0], t.off_diagonal()[0], 1e-12);
}
#[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 t = lanczos(&a, 2, &v0, 1e-12, &mut basis, &mut scratch).unwrap();
assert_close(t.diagonal()[0], 34.0 / 25.0, 1e-12);
assert!(t.off_diagonal().is_empty());
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 t = lanczos(&a, 2, &v0, 1e-12, &mut basis, &mut scratch).unwrap();
assert!(t.diagonal().is_empty());
assert!(t.off_diagonal().is_empty());
}
#[test]
fn repeated_eigenvalue_breaks_down_instead_of_panicking() {
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 result = lanczos(&a, 2, &v0, 1e-12, &mut basis, &mut scratch);
assert_eq!(result, Err(ConvergenceError::Breakdown));
}
#[test]
fn v0_inside_an_invariant_subspace_breaks_down_instead_of_panicking() {
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 result = lanczos(&a, 2, &v0, 1e-12, &mut basis, &mut scratch);
assert_eq!(result, Err(ConvergenceError::Breakdown));
}
#[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 = lanczos(&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 = lanczos(&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 = lanczos(&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 = lanczos(&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!(
lanczos(&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!(
lanczos(&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!(
lanczos(&a, 2, &v0, 1e-12, &mut basis_wrong, &mut scratch),
Err(ConvergenceError::DimensionMismatch)
);
let mut scratch_short = [0.0; 1];
assert_eq!(
lanczos(&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!(
lanczos(&a, 2, &v0, 1e-12, &mut basis_deep, &mut scratch),
Err(ConvergenceError::DimensionMismatch)
);
}
}