use num_complex::Complex;
use faer::Mat;
#[derive(Clone, Copy, Debug)]
pub struct ArnoldiOptions {
pub count: usize,
pub max_dim: usize,
pub tol: f64,
pub max_restarts: usize,
}
impl Default for ArnoldiOptions {
fn default() -> Self {
Self {
count: 6,
max_dim: 0,
tol: 1e-10,
max_restarts: 8,
}
}
}
const BASIS_BUDGET_BYTES: usize = 256 << 20;
const MAX_KRYLOV_DIM: usize = 1024;
#[derive(Clone, Debug)]
pub struct RitzPair {
pub theta: Complex<f64>,
pub vector: Vec<Complex<f64>>,
pub residual: f64,
pub converged: bool,
}
#[derive(Clone, Debug)]
pub struct ArnoldiResult {
pub pairs: Vec<RitzPair>,
pub restarts: usize,
pub dim: usize,
}
impl ArnoldiResult {
pub fn converged(&self) -> bool {
self.pairs.iter().all(|p| p.converged)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ArnoldiError {
OperatorFailed,
Eigen,
Empty,
}
impl std::fmt::Display for ArnoldiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ArnoldiError::OperatorFailed => {
write!(
f,
"the operator could not be applied — a singular factorization"
)
}
ArnoldiError::Eigen => write!(f, "the projected eigenproblem did not decompose"),
ArnoldiError::Empty => write!(f, "nothing to iterate on"),
}
}
}
impl std::error::Error for ArnoldiError {}
fn dot(a: &[Complex<f64>], b: &[Complex<f64>]) -> Complex<f64> {
a.iter().zip(b).map(|(x, y)| x.conj() * y).sum()
}
fn norm(a: &[Complex<f64>]) -> f64 {
a.iter().map(|x| x.norm_sqr()).sum::<f64>().sqrt()
}
fn start_vector(n: usize) -> Vec<Complex<f64>> {
let mut state = 0x2545_f491_4f6c_dd1du64;
let mut next = || {
state ^= state << 13;
state ^= state >> 7;
state ^= state << 17;
(state >> 11) as f64 / (1u64 << 53) as f64 - 0.5
};
let mut v: Vec<Complex<f64>> = (0..n).map(|_| Complex::new(next(), next())).collect();
let scale = norm(&v);
for value in &mut v {
*value /= scale;
}
v
}
pub fn eigenpairs<F>(
n: usize,
apply: F,
opts: &ArnoldiOptions,
) -> Result<ArnoldiResult, ArnoldiError>
where
F: Fn(&[Complex<f64>]) -> Option<Vec<Complex<f64>>>,
{
if n == 0 || opts.count == 0 {
return Err(ArnoldiError::Empty);
}
let count = opts.count.min(n);
let requested = if opts.max_dim == 0 {
(2 * count + 20).max(30)
} else {
opts.max_dim
};
let cap = dimension_cap(n, count);
let mut m = requested.max(count + 1).min(cap);
let mut v0 = start_vector(n);
let mut restarts = 0usize;
let mut best: Option<(Vec<RitzPair>, usize)> = None;
loop {
let (pairs, dim) = cycle(n, &apply, &v0, m, count, opts.tol)?;
let converged = pairs.iter().all(|p| p.converged);
let improved = match &best {
None => true,
Some((previous, _)) => worst_residual(&pairs) < worst_residual(previous),
};
if improved {
best = Some((pairs.clone(), dim));
}
if converged || dim < m || m >= cap || restarts >= opts.max_restarts {
let (pairs, dim) = best.expect("at least one cycle ran");
return Ok(ArnoldiResult {
pairs,
restarts,
dim,
});
}
m = (2 * m).min(cap);
let mut next = vec![Complex::new(0.0, 0.0); n];
for pair in &pairs {
let weight = if pair.converged { 0.1 } else { 1.0 };
for (acc, value) in next.iter_mut().zip(&pair.vector) {
*acc += weight * value;
}
}
let scale = norm(&next);
if scale > f64::EPSILON {
for value in &mut next {
*value /= scale;
}
v0 = next;
}
restarts += 1;
}
}
fn dimension_cap(n: usize, count: usize) -> usize {
let per_vector = n * std::mem::size_of::<Complex<f64>>();
let by_memory = (BASIS_BUDGET_BYTES / per_vector.max(1)).max(count + 1);
by_memory.min(MAX_KRYLOV_DIM).min(n)
}
fn worst_residual(pairs: &[RitzPair]) -> f64 {
pairs.iter().fold(0.0f64, |m, p| m.max(p.residual))
}
fn cycle<F>(
n: usize,
apply: &F,
v0: &[Complex<f64>],
m_max: usize,
count: usize,
tol: f64,
) -> Result<(Vec<RitzPair>, usize), ArnoldiError>
where
F: Fn(&[Complex<f64>]) -> Option<Vec<Complex<f64>>>,
{
let zero = Complex::new(0.0, 0.0);
let mut basis: Vec<Vec<Complex<f64>>> = Vec::with_capacity(m_max + 1);
basis.push(v0.to_vec());
let mut h: Vec<Vec<Complex<f64>>> = Vec::with_capacity(m_max);
let mut m = m_max;
for j in 0..m_max {
let mut w = apply(&basis[j]).ok_or(ArnoldiError::OperatorFailed)?;
let w_norm_before = norm(&w);
let mut column = vec![zero; j + 2];
for pass in 0..2 {
for (i, vi) in basis.iter().enumerate().take(j + 1) {
let coeff = dot(vi, &w);
for (wk, vk) in w.iter_mut().zip(vi) {
*wk -= coeff * vk;
}
if pass == 0 {
column[i] = coeff;
} else {
column[i] += coeff;
}
}
}
let beta = norm(&w);
column[j + 1] = Complex::new(beta, 0.0);
h.push(column);
if beta <= 1e-14 * w_norm_before.max(1.0) {
m = j + 1;
break;
}
for value in &mut w {
*value /= beta;
}
basis.push(w);
}
let hm = Mat::<Complex<f64>>::from_fn(m, m, |i, j| if i < h[j].len() { h[j][i] } else { zero });
let eigen = hm.eigen().map_err(|_| ArnoldiError::Eigen)?;
let s = eigen.S();
let y = eigen.U();
let mut order: Vec<usize> = (0..m).collect();
order.sort_by(|&a, &b| {
let (fa, fb) = (s[b].norm(), s[a].norm());
fa.partial_cmp(&fb).unwrap_or(std::cmp::Ordering::Equal)
});
order.truncate(count);
let mut pairs: Vec<RitzPair> = Vec::with_capacity(order.len());
for i in order {
let theta = Complex::new(s[i].re, s[i].im);
let mut vector = vec![zero; n];
for k in 0..m {
let coeff = y[(k, i)];
for (acc, value) in vector.iter_mut().zip(&basis[k]) {
*acc += coeff * value;
}
}
let scale = norm(&vector);
if scale > 0.0 {
for value in &mut vector {
*value /= scale;
}
}
let image = apply(&vector).ok_or(ArnoldiError::OperatorFailed)?;
let raw = image
.iter()
.zip(&vector)
.map(|(a, b)| (a - theta * b).norm_sqr())
.sum::<f64>()
.sqrt();
let magnitude = theta.norm();
let converged = magnitude > 0.0 && raw <= tol * magnitude;
let residual = if magnitude > 0.0 {
raw / (magnitude * magnitude)
} else {
f64::INFINITY
};
pairs.push(RitzPair {
theta,
vector,
residual,
converged,
});
}
Ok((pairs, m))
}