use scirs2_core::ndarray::{Array1, Array2};
use scirs2_linalg::eigh;
use crate::kernel_pca::error::{KernelPcaError, KernelPcaResult};
const POSITIVITY_FLOOR: f64 = 1e-10;
#[derive(Debug, Clone)]
pub struct TopKEigen {
pub eigenvalues: Array1<f64>,
pub eigenvectors: Array2<f64>,
}
pub fn symmetric_eigendecomp(
matrix: &Array2<f64>,
n_components: usize,
) -> KernelPcaResult<TopKEigen> {
let (rows, cols) = (matrix.nrows(), matrix.ncols());
if rows != cols {
return Err(KernelPcaError::InvalidInput(format!(
"symmetric_eigendecomp: matrix must be square, got {}x{}",
rows, cols
)));
}
if n_components == 0 {
return Err(KernelPcaError::InvalidInput(
"symmetric_eigendecomp: n_components must be >= 1".to_string(),
));
}
if n_components > rows {
return Err(KernelPcaError::InvalidInput(format!(
"symmetric_eigendecomp: n_components ({}) cannot exceed matrix size ({})",
n_components, rows
)));
}
let (raw_eigenvalues, raw_eigenvectors) = eigh(&matrix.view(), None)
.map_err(|e| KernelPcaError::EigendecompositionFailed(e.to_string()))?;
let n = raw_eigenvalues.len();
if n != rows {
return Err(KernelPcaError::EigendecompositionFailed(format!(
"eigh returned {} eigenvalues for a {}x{} matrix",
n, rows, cols
)));
}
if raw_eigenvectors.nrows() != rows || raw_eigenvectors.ncols() != n {
return Err(KernelPcaError::EigendecompositionFailed(format!(
"eigh returned eigenvector matrix with shape {}x{} (expected {}x{})",
raw_eigenvectors.nrows(),
raw_eigenvectors.ncols(),
rows,
n
)));
}
let mut indexed: Vec<(f64, usize)> = (0..n).map(|idx| (raw_eigenvalues[idx], idx)).collect();
indexed.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let available = indexed
.iter()
.filter(|&&(v, _)| v > POSITIVITY_FLOOR)
.count();
if available < n_components {
return Err(KernelPcaError::InsufficientComponents {
requested: n_components,
available,
});
}
let mut eigenvalues = Array1::<f64>::zeros(n_components);
let mut eigenvectors = Array2::<f64>::zeros((rows, n_components));
for (k, &(lambda, col)) in indexed.iter().take(n_components).enumerate() {
eigenvalues[k] = lambda;
for i in 0..rows {
eigenvectors[(i, k)] = raw_eigenvectors[(i, col)];
}
}
Ok(TopKEigen {
eigenvalues,
eigenvectors,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn diagonal_matrix_eigenvalues_are_sorted_descending() {
let m = Array2::<f64>::from_shape_fn((4, 4), |(i, j)| {
if i == j {
[2.0, 5.0, 1.0, 8.0][i]
} else {
0.0
}
});
let res = symmetric_eigendecomp(&m, 2).expect("eigendecomp");
assert!((res.eigenvalues[0] - 8.0).abs() < 1e-10);
assert!((res.eigenvalues[1] - 5.0).abs() < 1e-10);
assert!(res.eigenvalues[0] >= res.eigenvalues[1]);
}
#[test]
fn eigenvectors_are_orthonormal() {
let a = Array2::<f64>::from_shape_fn((5, 5), |(i, j)| {
(i as f64 * 0.3 + j as f64 * 0.7 + 1.0).sin()
});
let mut m = Array2::<f64>::zeros((5, 5));
for i in 0..5 {
for j in 0..5 {
let mut s = 0.0;
for k in 0..5 {
s += a[(k, i)] * a[(k, j)];
}
m[(i, j)] = s;
}
}
for i in 0..5 {
for j in (i + 1)..5 {
let avg = 0.5 * (m[(i, j)] + m[(j, i)]);
m[(i, j)] = avg;
m[(j, i)] = avg;
}
}
let k = 2;
let res = symmetric_eigendecomp(&m, k).expect("eigendecomp");
for c in 0..k {
let norm_sq: f64 = (0..5).map(|r| res.eigenvectors[(r, c)].powi(2)).sum();
assert!(
(norm_sq - 1.0).abs() < 1e-6,
"col {} norm_sq = {}",
c,
norm_sq
);
}
for c1 in 0..k {
for c2 in (c1 + 1)..k {
let dot: f64 = (0..5)
.map(|r| res.eigenvectors[(r, c1)] * res.eigenvectors[(r, c2)])
.sum();
assert!(dot.abs() < 1e-6, "cols {}/{} dot = {}", c1, c2, dot);
}
}
}
#[test]
fn rejects_n_components_zero_or_too_large() {
let m = Array2::<f64>::eye(3);
assert!(symmetric_eigendecomp(&m, 0).is_err());
assert!(symmetric_eigendecomp(&m, 4).is_err());
}
#[test]
fn insufficient_components_on_low_rank_matrix() {
let m = Array2::<f64>::from_shape_fn((3, 3), |_| 1.0);
let err = symmetric_eigendecomp(&m, 2).expect_err("must fail");
match err {
KernelPcaError::InsufficientComponents {
requested,
available,
} => {
assert_eq!(requested, 2);
assert!(available <= 1, "available = {}", available);
}
other => panic!("wrong variant: {:?}", other),
}
}
}