use ariadnetor_core::Scalar;
use ariadnetor_tensor::{ComputeBackendTensorExt, DenseTensor, Host, linear_combine};
use num_traits::{Float, One, ToPrimitive, Zero};
use rand::SeedableRng;
use rand::rngs::{StdRng, SysRng};
use super::lanczos_kernels::{
inner, random_unit_vector, solve_tridiagonal_smallest, sub_complex_axpy, sub_real_axpy,
sub_two_real_axpy,
};
pub trait LinearOp<T: Scalar> {
fn apply(&self, v: &DenseTensor<T>) -> DenseTensor<T>;
}
impl<T, F> LinearOp<T> for F
where
T: Scalar,
F: Fn(&DenseTensor<T>) -> DenseTensor<T>,
{
fn apply(&self, v: &DenseTensor<T>) -> DenseTensor<T> {
self(v)
}
}
#[derive(Debug, Clone)]
pub struct LanczosParams {
pub max_iter: usize,
pub tol: f64,
pub seed: Option<u64>,
}
impl Default for LanczosParams {
fn default() -> Self {
Self {
max_iter: 200,
tol: 1e-10,
seed: None,
}
}
}
#[derive(Debug, Clone)]
pub struct LanczosResult<T: Scalar> {
pub eigenvalue: T::Real,
pub eigenvector: DenseTensor<T>,
pub iters: usize,
pub residual: T::Real,
pub converged: bool,
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LanczosError {
#[error(
"Lanczos produced a non-finite result after {iters} iterations: \
eigenvalue = {eigenvalue}, residual = {residual}"
)]
NonFinite {
iters: usize,
eigenvalue: f64,
residual: f64,
},
}
pub fn lanczos_smallest<T, Op>(
op: &Op,
dim: usize,
params: &LanczosParams,
) -> Result<LanczosResult<T>, LanczosError>
where
T: Scalar,
T::Real: Scalar<Real = T::Real>,
Op: LinearOp<T>,
{
assert!(dim >= 1, "lanczos: dim must be >= 1");
assert!(params.max_iter >= 1, "lanczos: max_iter must be >= 1");
assert!(
params.tol.is_finite() && params.tol >= 0.0,
"lanczos: tol must be a finite non-negative f64, got {}",
params.tol,
);
let max_iter = params.max_iter.min(dim);
let tol_real: T::Real =
crate::numeric::try_real_from_f64::<T>(params.tol).unwrap_or_else(|| {
panic!(
"try_real_from_f64: tol = {} is not representable in T::Real",
params.tol
)
});
let beta_floor: T::Real = T::Real::min_positive_value();
let mut rng = match params.seed {
Some(s) => StdRng::seed_from_u64(s),
None => StdRng::try_from_rng(&mut SysRng).expect("OS RNG must be available"),
};
let v0 = random_unit_vector::<T>(dim, &mut rng);
let mut basis: Vec<DenseTensor<T>> = vec![v0];
let mut alphas: Vec<T::Real> = Vec::new();
let mut betas: Vec<T::Real> = Vec::new();
let mut iters = 0usize;
let mut converged_lambda: T::Real = T::Real::zero();
let mut converged_z: DenseTensor<T::Real> = Host::shared().dense(vec![T::Real::one()], vec![1]);
for j in 0..max_iter {
iters = j + 1;
let v_j = basis[j].clone();
let w_raw = op.apply(&v_j);
assert_eq!(
w_raw.shape(),
&[dim],
"LinearOp::apply must return a rank-1 tensor of shape [dim]",
);
let mut w = if w_raw.order() == v_j.order() {
w_raw
} else {
w_raw.reordered(v_j.order())
};
let alpha = inner(&v_j, &w).re();
alphas.push(alpha);
if j == 0 {
w = sub_real_axpy(&w, alpha, &v_j);
} else {
let beta_prev = betas[j - 1];
let v_prev = &basis[j - 1];
w = sub_two_real_axpy(&w, alpha, &v_j, beta_prev, v_prev);
}
for _ in 0..2 {
for v_k in basis.iter().take(j + 1) {
let gamma = inner(v_k, &w);
w = sub_complex_axpy(&w, gamma, v_k);
}
}
let beta = w.norm();
let m = j + 1;
let (lambda, z) = solve_tridiagonal_smallest::<T>(&alphas, &betas, m);
converged_lambda = lambda;
converged_z = z;
let z_last = Float::abs(converged_z.data_slice()[m - 1]);
let residual_estimate = beta * z_last;
if residual_estimate <= tol_real {
break;
}
if j + 1 == max_iter {
break;
}
if beta <= beta_floor {
break;
}
let inv = T::Real::one() / beta;
let v_next_data: Vec<T> = w.data_slice().iter().map(|&x| x.scale_real(inv)).collect();
basis.push(Host::shared().dense(v_next_data, vec![dim]));
betas.push(beta);
}
let m = converged_z.len();
let basis_refs: Vec<&DenseTensor<T>> = basis.iter().take(m).collect();
let coefs: Vec<T> = converged_z
.data_slice()
.iter()
.map(|&zk| T::from_real_imag(zk, T::Real::zero()))
.collect();
let mut psi = linear_combine(&basis_refs, &coefs).expect("linear_combine on Lanczos basis");
psi.normalize();
let h_psi_raw = op.apply(&psi);
assert_eq!(
h_psi_raw.shape(),
&[dim],
"LinearOp::apply must return a rank-1 tensor of shape [dim]",
);
let h_psi = if h_psi_raw.order() == psi.order() {
h_psi_raw
} else {
h_psi_raw.reordered(psi.order())
};
let lambda_t = T::from_real_imag(converged_lambda, T::Real::zero());
let neg_lambda = lambda_t.scale_real(-T::Real::one());
let residual_vec =
linear_combine(&[&h_psi, &psi], &[T::one(), neg_lambda]).expect("residual lc");
let residual = residual_vec.norm();
if !converged_lambda.is_finite() || !residual.is_finite() {
return Err(LanczosError::NonFinite {
iters,
eigenvalue: converged_lambda.to_f64().unwrap_or(f64::NAN),
residual: residual.to_f64().unwrap_or(f64::NAN),
});
}
let converged = residual <= tol_real;
Ok(LanczosResult {
eigenvalue: converged_lambda,
eigenvector: psi,
iters,
residual,
converged,
})
}