use crate::error::{SparseError, SparseResult};
use crate::host_csr::HostCsr;
const LCG_MULT: u64 = 6_364_136_223_846_793_005;
const LCG_INCR: u64 = 1_442_695_040_888_963_407;
const MU_FLOOR: f64 = 1e-300;
const VEC_FLOOR: f64 = 1e-300;
#[derive(Debug, Clone)]
pub struct ShiftInvertResult {
pub eigenvalue: f64,
pub eigenvector: Vec<f64>,
pub iters: usize,
pub converged: bool,
pub residual: f64,
}
pub fn shift_invert(
a: &HostCsr,
sigma: f64,
max_iter: usize,
tol: f64,
) -> SparseResult<ShiftInvertResult> {
if a.nrows != a.ncols {
return Err(SparseError::DimensionMismatch(format!(
"shift-invert requires a square matrix, got {}x{}",
a.nrows, a.ncols
)));
}
let n = a.nrows;
if n == 0 {
return Err(SparseError::InvalidArgument(
"shift-invert requires a non-empty matrix".to_string(),
));
}
if max_iter == 0 {
return Err(SparseError::InvalidArgument(
"shift-invert requires max_iter >= 1".to_string(),
));
}
let mut shifted = a.to_dense();
for i in 0..n {
shifted[i * n + i] -= sigma;
}
let lu = DenseLu::factor(shifted, n)?;
let mut rng = Lcg::new(0x51f7_1234_abcd_9e01);
let mut v = vec![0.0f64; n];
for slot in v.iter_mut() {
*slot = rng.next_signed();
}
if !normalize(&mut v) {
v.iter_mut().for_each(|x| *x = 0.0);
v[0] = 1.0;
}
let mut eigenvalue = sigma;
let mut residual = f64::INFINITY;
let mut converged = false;
let mut iters = 0usize;
let mut pending: Option<Vec<f64>> = None;
for it in 0..max_iter {
iters = it + 1;
if let Some(w) = pending.take() {
let mut next = w;
if !normalize(&mut next) {
return Err(SparseError::ConvergenceFailure(
"shift-invert iterate collapsed to zero".to_string(),
));
}
v = next;
}
let w = lu.solve(&v);
let mu = dot(&v, &w);
if mu.abs() < MU_FLOOR {
return Err(SparseError::ConvergenceFailure(
"shift-invert Rayleigh quotient underflowed".to_string(),
));
}
eigenvalue = sigma + 1.0 / mu;
let av = a.matvec(&v);
residual = av
.iter()
.zip(v.iter())
.map(|(&av_i, &v_i)| {
let d = av_i - eigenvalue * v_i;
d * d
})
.sum::<f64>()
.sqrt();
if residual < tol {
converged = true;
break;
}
if norm(&w) < VEC_FLOOR {
return Err(SparseError::ConvergenceFailure(
"shift-invert solve produced a zero vector".to_string(),
));
}
pending = Some(w);
}
Ok(ShiftInvertResult {
eigenvalue,
eigenvector: v,
iters,
converged,
residual,
})
}
struct DenseLu {
n: usize,
lu: Vec<f64>,
piv: Vec<usize>,
}
impl DenseLu {
fn factor(mut a: Vec<f64>, n: usize) -> SparseResult<Self> {
let mut piv: Vec<usize> = (0..n).collect();
for col in 0..n {
let mut pivot_row = col;
let mut pivot_mag = a[col * n + col].abs();
for r in (col + 1)..n {
let mag = a[r * n + col].abs();
if mag > pivot_mag {
pivot_mag = mag;
pivot_row = r;
}
}
if pivot_mag < 1e-300 {
return Err(SparseError::SingularMatrix);
}
if pivot_row != col {
for c in 0..n {
a.swap(col * n + c, pivot_row * n + c);
}
piv.swap(col, pivot_row);
}
let pivot = a[col * n + col];
for r in (col + 1)..n {
let factor = a[r * n + col] / pivot;
a[r * n + col] = factor; if factor != 0.0 {
for c in (col + 1)..n {
a[r * n + c] -= factor * a[col * n + c];
}
}
}
}
Ok(Self { n, lu: a, piv })
}
fn solve(&self, b: &[f64]) -> Vec<f64> {
let n = self.n;
let mut x: Vec<f64> = self.piv.iter().map(|&p| b[p]).collect();
for i in 0..n {
let lrow = &self.lu[i * n..i * n + i];
let dotp: f64 = lrow.iter().zip(x[..i].iter()).map(|(&l, &xj)| l * xj).sum();
x[i] -= dotp;
}
for i in (0..n).rev() {
let urow = &self.lu[i * n + i + 1..i * n + n];
let dotp: f64 = urow
.iter()
.zip(x[i + 1..].iter())
.map(|(&u, &xj)| u * xj)
.sum();
x[i] = (x[i] - dotp) / self.lu[i * n + i];
}
x
}
}
fn dot(a: &[f64], b: &[f64]) -> f64 {
a.iter().zip(b.iter()).map(|(&x, &y)| x * y).sum()
}
fn norm(a: &[f64]) -> f64 {
dot(a, a).sqrt()
}
fn normalize(v: &mut [f64]) -> bool {
let nrm = norm(v);
if nrm < VEC_FLOOR {
return false;
}
let inv = 1.0 / nrm;
for x in v.iter_mut() {
*x *= inv;
}
true
}
struct Lcg {
state: u64,
}
impl Lcg {
fn new(seed: u64) -> Self {
Self {
state: seed.wrapping_add(1),
}
}
fn next_u32(&mut self) -> u32 {
self.state = self.state.wrapping_mul(LCG_MULT).wrapping_add(LCG_INCR);
(self.state >> 32) as u32
}
fn next_signed(&mut self) -> f64 {
let u = self.next_u32() as f64 / (u32::MAX as f64 + 1.0);
2.0 * u - 1.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::host_csr::laplacian_1d;
use std::f64::consts::PI;
fn laplacian_1d_eigs(n: usize) -> Vec<f64> {
(1..=n)
.map(|j| 2.0 - 2.0 * ((j as f64) * PI / ((n + 1) as f64)).cos())
.collect()
}
#[test]
fn finds_interior_eigenvalue_not_dominant() {
let n = 5;
let a = laplacian_1d(n);
let eigs = laplacian_1d_eigs(n);
let dominant = eigs[n - 1];
let res = shift_invert(&a, 1.9, 200, 1e-10).expect("shift-invert");
assert!(res.converged, "did not converge");
assert!(
(res.eigenvalue - eigs[2]).abs() < 1e-7,
"expected {} got {}",
eigs[2],
res.eigenvalue
);
assert!(
(res.eigenvalue - dominant).abs() > 1.0,
"shift-invert returned the dominant eigenvalue"
);
assert!(res.residual < 1e-9);
assert!(res.eigenvalue.is_finite());
assert!(res.eigenvector.iter().all(|v| v.is_finite()));
}
#[test]
fn eigenvector_residual_small() {
let n = 5;
let a = laplacian_1d(n);
let res = shift_invert(&a, 0.9, 200, 1e-11).expect("shift-invert");
let av = a.matvec(&res.eigenvector);
let r: f64 = av
.iter()
.zip(res.eigenvector.iter())
.map(|(&av_i, &v_i)| {
let d = av_i - res.eigenvalue * v_i;
d * d
})
.sum::<f64>()
.sqrt();
assert!(r < 1e-9, "residual too large: {r}");
let nrm: f64 = res.eigenvector.iter().map(|&x| x * x).sum::<f64>().sqrt();
assert!((nrm - 1.0).abs() < 1e-10);
}
#[test]
fn different_shifts_target_different_eigenvalues() {
let n = 5;
let a = laplacian_1d(n);
let eigs = laplacian_1d_eigs(n);
let cases = [
(0.3, eigs[0]), (0.9, eigs[1]), (1.9, eigs[2]), (2.9, eigs[3]), (3.8, eigs[4]), ];
for (sigma, expected) in cases {
let res = shift_invert(&a, sigma, 300, 1e-10).expect("shift-invert");
assert!(res.converged, "sigma {sigma} did not converge");
assert!(
(res.eigenvalue - expected).abs() < 1e-6,
"sigma {sigma}: expected {expected} got {}",
res.eigenvalue
);
}
}
#[test]
fn finds_smallest_eigenvalue() {
let n = 6;
let a = laplacian_1d(n);
let eigs = laplacian_1d_eigs(n);
let res = shift_invert(&a, -0.5, 300, 1e-10).expect("shift-invert");
assert!(res.converged);
assert!(
(res.eigenvalue - eigs[0]).abs() < 1e-6,
"expected smallest {} got {}",
eigs[0],
res.eigenvalue
);
}
#[test]
fn converges_within_max_iter() {
let n = 5;
let a = laplacian_1d(n);
let res = shift_invert(&a, 1.9, 50, 1e-9).expect("shift-invert");
assert!(res.converged);
assert!(res.iters <= 50);
}
#[test]
fn rejects_non_square() {
let a = HostCsr::new(2, 3, vec![0, 1, 2], vec![0, 1], vec![1.0, 1.0]).expect("rect");
assert!(shift_invert(&a, 0.0, 10, 1e-8).is_err());
}
#[test]
fn rejects_zero_max_iter() {
let a = laplacian_1d(4);
assert!(shift_invert(&a, 0.5, 0, 1e-8).is_err());
}
#[test]
fn singular_shift_errors() {
let a =
HostCsr::new(3, 3, vec![0, 1, 2, 3], vec![0, 1, 2], vec![1.0, 2.0, 3.0]).expect("diag");
assert!(shift_invert(&a, 2.0, 50, 1e-8).is_err());
}
#[test]
fn diagonal_matrix_exact() {
let a = HostCsr::new(
4,
4,
vec![0, 1, 2, 3, 4],
vec![0, 1, 2, 3],
vec![10.0, 20.0, 30.0, 40.0],
)
.expect("diag");
let res = shift_invert(&a, 19.0, 100, 1e-10).expect("shift-invert");
assert!(res.converged);
assert!((res.eigenvalue - 20.0).abs() < 1e-8);
}
#[test]
fn dense_lu_solves_correctly() {
let lu = DenseLu::factor(vec![2.0, 1.0, 1.0, 3.0], 2).expect("factor");
let x = lu.solve(&[3.0, 5.0]);
assert!((x[0] - 0.8).abs() < 1e-12);
assert!((x[1] - 1.4).abs() < 1e-12);
}
}