use crate::cholesky::Cholesky;
use oxiblas_core::scalar::{Field, Real, Scalar};
use oxiblas_matrix::{Mat, MatRef};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GeneralizedEvdError {
EmptyMatrix,
DimensionMismatch {
nrows_a: usize,
ncols_a: usize,
nrows_b: usize,
ncols_b: usize,
},
NotSquare {
nrows: usize,
ncols: usize,
},
BNotPositiveDefinite,
NotConverged,
Singular,
}
impl core::fmt::Display for GeneralizedEvdError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::EmptyMatrix => write!(f, "Matrix is empty"),
Self::DimensionMismatch {
nrows_a,
ncols_a,
nrows_b,
ncols_b,
} => {
write!(
f,
"Dimension mismatch: A is {nrows_a}x{ncols_a}, B is {nrows_b}x{ncols_b}"
)
}
Self::NotSquare { nrows, ncols } => {
write!(f, "Matrix is not square: {nrows} x {ncols}")
}
Self::BNotPositiveDefinite => {
write!(f, "Matrix B is not positive definite")
}
Self::NotConverged => write!(f, "Algorithm did not converge"),
Self::Singular => write!(f, "Matrix is singular"),
}
}
}
impl std::error::Error for GeneralizedEvdError {}
#[derive(Debug, Clone)]
pub struct SymmetricGeneralizedEvd<T: Scalar> {
eigenvalues: Vec<T>,
eigenvectors: Mat<T>,
n: usize,
}
impl<T: Field + Real + bytemuck::Zeroable> SymmetricGeneralizedEvd<T> {
#[allow(dead_code)]
const MAX_ITERATIONS: usize = 100;
pub fn compute(a: MatRef<'_, T>, b: MatRef<'_, T>) -> Result<Self, GeneralizedEvdError> {
let n = a.nrows();
if n == 0 {
return Err(GeneralizedEvdError::EmptyMatrix);
}
if n != a.ncols() {
return Err(GeneralizedEvdError::NotSquare {
nrows: a.nrows(),
ncols: a.ncols(),
});
}
if n != b.nrows() || n != b.ncols() {
return Err(GeneralizedEvdError::DimensionMismatch {
nrows_a: a.nrows(),
ncols_a: a.ncols(),
nrows_b: b.nrows(),
ncols_b: b.ncols(),
});
}
if n == 1 {
let eigenvalue = a[(0, 0)] / b[(0, 0)];
let mut eigenvectors = Mat::zeros(1, 1);
eigenvectors[(0, 0)] = T::one();
return Ok(Self {
eigenvalues: vec![eigenvalue],
eigenvectors,
n,
});
}
let chol = Cholesky::compute(b).map_err(|_| GeneralizedEvdError::BNotPositiveDefinite)?;
let l = chol.l_factor();
let l_ref = l.as_ref();
let mut temp = Mat::zeros(n, n);
for j in 0..n {
let col_a = column_to_vec(&a, j);
let col_temp = solve_lower_triangular(l_ref, &col_a);
for i in 0..n {
temp[(i, j)] = col_temp[i];
}
}
let mut c = Mat::zeros(n, n);
for i in 0..n {
let row_temp: Vec<T> = (0..n).map(|j| temp[(i, j)]).collect();
let row_c = solve_lower_transpose_triangular(l_ref, &row_temp);
for j in 0..n {
c[(i, j)] = row_c[j];
}
}
for i in 0..n {
for j in (i + 1)..n {
let avg = (c[(i, j)] + c[(j, i)]) / (T::one() + T::one());
c[(i, j)] = avg;
c[(j, i)] = avg;
}
}
let evd = super::SymmetricEvd::compute(c.as_ref())
.map_err(|_| GeneralizedEvdError::NotConverged)?;
let y_vecs = evd.eigenvectors();
let mut eigenvectors = Mat::zeros(n, n);
for j in 0..n {
let y_col: Vec<T> = (0..n).map(|i| y_vecs[(i, j)]).collect();
let x_col = solve_lower_transpose_triangular(l_ref, &y_col);
for i in 0..n {
eigenvectors[(i, j)] = x_col[i];
}
}
Ok(Self {
eigenvalues: evd.eigenvalues().to_vec(),
eigenvectors,
n,
})
}
pub fn eigenvalues(&self) -> &[T] {
&self.eigenvalues
}
pub fn eigenvectors(&self) -> MatRef<'_, T> {
self.eigenvectors.as_ref()
}
pub fn n(&self) -> usize {
self.n
}
pub fn verify(&self, a: MatRef<'_, T>, b: MatRef<'_, T>) -> T {
let n = self.n;
let mut max_error = T::zero();
for j in 0..n {
let lambda = self.eigenvalues[j];
for i in 0..n {
let mut av_i = T::zero();
for k in 0..n {
av_i = av_i + a[(i, k)] * self.eigenvectors[(k, j)];
}
let mut bv_i = T::zero();
for k in 0..n {
bv_i = bv_i + b[(i, k)] * self.eigenvectors[(k, j)];
}
let lambda_bv_i = lambda * bv_i;
let error = Scalar::abs(av_i - lambda_bv_i);
if error > max_error {
max_error = error;
}
}
}
max_error
}
}
#[derive(Debug, Clone)]
pub struct GeneralizedEvd<T: Scalar> {
eigenvalues_real: Vec<T>,
eigenvalues_imag: Vec<T>,
right_eigenvectors: Option<Mat<T>>,
#[allow(dead_code)]
left_eigenvectors: Option<Mat<T>>,
n: usize,
is_finite: Vec<bool>,
}
impl<T: Field + Real + bytemuck::Zeroable> GeneralizedEvd<T> {
pub fn compute(a: MatRef<'_, T>, b: MatRef<'_, T>) -> Result<Self, GeneralizedEvdError> {
let n = a.nrows();
if n == 0 {
return Err(GeneralizedEvdError::EmptyMatrix);
}
if n != a.ncols() {
return Err(GeneralizedEvdError::NotSquare {
nrows: a.nrows(),
ncols: a.ncols(),
});
}
if n != b.nrows() || n != b.ncols() {
return Err(GeneralizedEvdError::DimensionMismatch {
nrows_a: a.nrows(),
ncols_a: a.ncols(),
nrows_b: b.nrows(),
ncols_b: b.ncols(),
});
}
if n == 1 {
let b00 = b[(0, 0)];
if Scalar::abs(b00) <= <T as Scalar>::epsilon() {
return Ok(Self {
eigenvalues_real: vec![T::zero()],
eigenvalues_imag: vec![T::zero()],
right_eigenvectors: None,
left_eigenvectors: None,
n,
is_finite: vec![false],
});
}
let eigenvalue = a[(0, 0)] / b00;
return Ok(Self {
eigenvalues_real: vec![eigenvalue],
eigenvalues_imag: vec![T::zero()],
right_eigenvectors: None,
left_eigenvectors: None,
n,
is_finite: vec![true],
});
}
let lu = crate::lu::Lu::compute(b).map_err(|_| GeneralizedEvdError::Singular)?;
let mut c = Mat::zeros(n, n);
for j in 0..n {
let mut col_a = Mat::zeros(n, 1);
for i in 0..n {
col_a[(i, 0)] = a[(i, j)];
}
let col_c = lu
.solve(col_a.as_ref())
.map_err(|_| GeneralizedEvdError::Singular)?;
for i in 0..n {
c[(i, j)] = col_c[(i, 0)];
}
}
let evd = super::GeneralEvd::eigenvalues_only(c.as_ref())
.map_err(|_| GeneralizedEvdError::NotConverged)?;
let eigenvalues = evd.eigenvalues();
let real_parts: Vec<T> = eigenvalues.iter().map(|e| e.real).collect();
let imag_parts: Vec<T> = eigenvalues.iter().map(|e| e.imag).collect();
Ok(Self {
eigenvalues_real: real_parts,
eigenvalues_imag: imag_parts,
right_eigenvectors: None,
left_eigenvectors: None,
n,
is_finite: vec![true; n],
})
}
pub fn compute_with_eigenvectors(
a: MatRef<'_, T>,
b: MatRef<'_, T>,
) -> Result<Self, GeneralizedEvdError> {
let n = a.nrows();
if n == 0 {
return Err(GeneralizedEvdError::EmptyMatrix);
}
if n != a.ncols() {
return Err(GeneralizedEvdError::NotSquare {
nrows: a.nrows(),
ncols: a.ncols(),
});
}
if n != b.nrows() || n != b.ncols() {
return Err(GeneralizedEvdError::DimensionMismatch {
nrows_a: a.nrows(),
ncols_a: a.ncols(),
nrows_b: b.nrows(),
ncols_b: b.ncols(),
});
}
let lu = crate::lu::Lu::compute(b).map_err(|_| GeneralizedEvdError::Singular)?;
let mut c = Mat::zeros(n, n);
for j in 0..n {
let mut col_a = Mat::zeros(n, 1);
for i in 0..n {
col_a[(i, 0)] = a[(i, j)];
}
let col_c = lu
.solve(col_a.as_ref())
.map_err(|_| GeneralizedEvdError::Singular)?;
for i in 0..n {
c[(i, j)] = col_c[(i, 0)];
}
}
let evd = super::GeneralEvd::compute(c.as_ref())
.map_err(|_| GeneralizedEvdError::NotConverged)?;
let eigenvalues = evd.eigenvalues();
let real_parts: Vec<T> = eigenvalues.iter().map(|e| e.real).collect();
let imag_parts: Vec<T> = eigenvalues.iter().map(|e| e.imag).collect();
let right_eigenvectors = if let Some(vecs) = evd.eigenvectors_real() {
let mut v = Mat::zeros(n, n);
for i in 0..n {
for j in 0..n {
v[(i, j)] = vecs[(i, j)];
}
}
Some(v)
} else {
None
};
Ok(Self {
eigenvalues_real: real_parts,
eigenvalues_imag: imag_parts,
right_eigenvectors,
left_eigenvectors: None,
n,
is_finite: vec![true; n],
})
}
pub fn eigenvalues_real(&self) -> &[T] {
&self.eigenvalues_real
}
pub fn eigenvalues_imag(&self) -> &[T] {
&self.eigenvalues_imag
}
pub fn eigenvalues_as_pairs(&self) -> (&[T], &[T]) {
(&self.eigenvalues_real, &self.eigenvalues_imag)
}
pub fn right_eigenvectors(&self) -> Option<MatRef<'_, T>> {
self.right_eigenvectors.as_ref().map(|m| m.as_ref())
}
pub fn is_finite(&self) -> &[bool] {
&self.is_finite
}
pub fn n(&self) -> usize {
self.n
}
}
fn column_to_vec<T: Scalar>(m: &MatRef<'_, T>, j: usize) -> Vec<T> {
(0..m.nrows()).map(|i| m[(i, j)]).collect()
}
fn solve_lower_triangular<T: Field + Real>(l: MatRef<'_, T>, b: &[T]) -> Vec<T> {
let n = l.nrows();
let mut x = b.to_vec();
for i in 0..n {
for j in 0..i {
x[i] = x[i] - l[(i, j)] * x[j];
}
x[i] = x[i] / l[(i, i)];
}
x
}
fn solve_lower_transpose_triangular<T: Field + Real>(l: MatRef<'_, T>, b: &[T]) -> Vec<T> {
let n = l.nrows();
let mut x = b.to_vec();
for i in (0..n).rev() {
for j in (i + 1)..n {
x[i] = x[i] - l[(j, i)] * x[j];
}
x[i] = x[i] / l[(i, i)];
}
x
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_symmetric_generalized_evd() {
let a = Mat::from_rows(&[&[1.0f64, 0.0], &[0.0, 1.0]]);
let b = Mat::from_rows(&[&[1.0f64, 0.0], &[0.0, 1.0]]);
let gevd = SymmetricGeneralizedEvd::compute(a.as_ref(), b.as_ref()).unwrap();
let eigs = gevd.eigenvalues();
assert!((eigs[0] - 1.0).abs() < 1e-10);
assert!((eigs[1] - 1.0).abs() < 1e-10);
}
#[test]
fn test_symmetric_generalized_evd_scaled() {
let a = Mat::from_rows(&[&[2.0f64, 0.0], &[0.0, 2.0]]);
let b = Mat::from_rows(&[&[1.0f64, 0.0], &[0.0, 1.0]]);
let gevd = SymmetricGeneralizedEvd::compute(a.as_ref(), b.as_ref()).unwrap();
let eigs = gevd.eigenvalues();
assert!((eigs[0] - 2.0).abs() < 1e-10);
assert!((eigs[1] - 2.0).abs() < 1e-10);
}
#[test]
fn test_symmetric_generalized_evd_general() {
let a = Mat::from_rows(&[&[5.0f64, 2.0], &[2.0, 3.0]]);
let b = Mat::from_rows(&[&[2.0f64, 0.0], &[0.0, 2.0]]);
let gevd = SymmetricGeneralizedEvd::compute(a.as_ref(), b.as_ref()).unwrap();
let eigs = gevd.eigenvalues();
let expected_1 = (4.0 - 5.0_f64.sqrt()) / 2.0;
let expected_2 = (4.0 + 5.0_f64.sqrt()) / 2.0;
assert!((eigs[0] - expected_1).abs() < 1e-10);
assert!((eigs[1] - expected_2).abs() < 1e-10);
}
#[test]
fn test_symmetric_generalized_evd_verify_identity() {
let a = Mat::from_rows(&[&[2.0f64, 1.0], &[1.0, 2.0]]);
let b = Mat::from_rows(&[&[1.0f64, 0.0], &[0.0, 1.0]]);
let gevd = SymmetricGeneralizedEvd::compute(a.as_ref(), b.as_ref()).unwrap();
let eigs = gevd.eigenvalues();
assert!(
(eigs[0] - 1.0).abs() < 1e-10,
"Expected eigenvalue 1, got {}",
eigs[0]
);
assert!(
(eigs[1] - 3.0).abs() < 1e-10,
"Expected eigenvalue 3, got {}",
eigs[1]
);
let error = gevd.verify(a.as_ref(), b.as_ref());
assert!(error < 1e-8, "Verification error: {error}");
}
#[test]
fn test_generalized_evd_simple() {
let a = Mat::from_rows(&[&[2.0f64, 0.0], &[0.0, 3.0]]);
let b = Mat::from_rows(&[&[1.0f64, 0.0], &[0.0, 1.0]]);
let gevd = GeneralizedEvd::compute(a.as_ref(), b.as_ref()).unwrap();
let real = gevd.eigenvalues_real();
let mut eigs: Vec<f64> = real.to_vec();
eigs.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert!((eigs[0] - 2.0).abs() < 1e-10);
assert!((eigs[1] - 3.0).abs() < 1e-10);
}
#[test]
fn test_generalized_evd_with_eigenvectors() {
let a = Mat::from_rows(&[&[1.0f64, 2.0], &[0.0, 3.0]]);
let b = Mat::from_rows(&[&[1.0f64, 0.0], &[0.0, 1.0]]);
let gevd = GeneralizedEvd::compute_with_eigenvectors(a.as_ref(), b.as_ref()).unwrap();
let real = gevd.eigenvalues_real();
let mut eigs: Vec<f64> = real.to_vec();
eigs.sort_by(|a, b| a.partial_cmp(b).unwrap());
assert!((eigs[0] - 1.0).abs() < 1e-10);
assert!((eigs[1] - 3.0).abs() < 1e-10);
}
#[test]
fn test_generalized_evd_dimension_mismatch() {
let a = Mat::from_rows(&[&[1.0f64, 0.0], &[0.0, 1.0]]);
let b = Mat::from_rows(&[&[1.0f64, 0.0, 0.0], &[0.0, 1.0, 0.0], &[0.0, 0.0, 1.0]]);
let result = GeneralizedEvd::compute(a.as_ref(), b.as_ref());
assert!(matches!(
result,
Err(GeneralizedEvdError::DimensionMismatch { .. })
));
}
#[test]
fn test_symmetric_generalized_evd_non_spd() {
let a = Mat::from_rows(&[&[1.0f64, 0.0], &[0.0, 1.0]]);
let b = Mat::from_rows(&[&[-1.0f64, 0.0], &[0.0, 1.0]]);
let result = SymmetricGeneralizedEvd::compute(a.as_ref(), b.as_ref());
assert!(matches!(
result,
Err(GeneralizedEvdError::BNotPositiveDefinite)
));
}
}