use scirs2_core::ndarray::{Array1, Array2, ArrayView2};
use scirs2_linalg::compat::{ArrayLinalgExt, UPLO};
use sklears_core::{
error::{Result as SklResult, SklearsError},
types::Float,
};
#[derive(Debug, Clone)]
pub struct EigenConfig {
pub max_iterations: usize,
pub tolerance: Float,
pub use_iterative_refinement: bool,
pub monitor_condition: bool,
pub condition_threshold: Float,
pub adaptive_precision: bool,
pub min_eigenvalue: Float,
}
impl Default for EigenConfig {
fn default() -> Self {
Self {
max_iterations: 100,
tolerance: 1e-12,
use_iterative_refinement: true,
monitor_condition: true,
condition_threshold: 1e12,
adaptive_precision: true,
min_eigenvalue: 1e-14,
}
}
}
#[derive(Debug, Clone)]
pub struct EigenResult {
pub eigenvalues: Array1<Float>,
pub eigenvectors: Array2<Float>,
pub condition_number: Option<Float>,
pub iterations_used: usize,
pub converged: bool,
pub accuracy_metrics: AccuracyMetrics,
}
#[derive(Debug, Clone)]
pub struct AccuracyMetrics {
pub max_residual: Float,
pub orthogonality_error: Float,
pub backward_error: Float,
pub eigenvalue_error: Float,
}
pub struct StableEigen {
config: EigenConfig,
}
impl StableEigen {
pub fn new(config: EigenConfig) -> Self {
Self { config }
}
#[allow(clippy::should_implement_trait)] pub fn default() -> Self {
Self::new(EigenConfig::default())
}
pub fn decompose(&self, matrix: ArrayView2<Float>) -> SklResult<EigenResult> {
self.validate_input(&matrix)?;
let (preprocessed, scaling_factor) = self.preprocess_matrix(&matrix)?;
let mut result = self.compute_initial_decomposition(&preprocessed)?;
result.eigenvalues *= scaling_factor;
if self.config.use_iterative_refinement {
result = self.iterative_refinement(&matrix, result)?;
}
result.accuracy_metrics = self.compute_accuracy_metrics(&matrix, &result);
if self.config.monitor_condition {
result.condition_number = Some(self.estimate_condition_number(&result.eigenvalues));
if let Some(cond) = result.condition_number {
if cond > self.config.condition_threshold {
eprintln!(
"Warning: Matrix is ill-conditioned (condition number: {:.2e})",
cond
);
}
}
}
Ok(result)
}
pub fn decompose_with_rank_detection(
&self,
matrix: ArrayView2<Float>,
) -> SklResult<EigenResult> {
let mut result = self.decompose(matrix)?;
let numerical_rank = self.detect_numerical_rank(&result.eigenvalues);
if numerical_rank < result.eigenvalues.len() {
result.eigenvalues = result
.eigenvalues
.slice(scirs2_core::ndarray::s![..numerical_rank])
.to_owned();
result.eigenvectors = result
.eigenvectors
.slice(scirs2_core::ndarray::s![.., ..numerical_rank])
.to_owned();
}
Ok(result)
}
pub fn generalized_decompose(
&self,
a_matrix: ArrayView2<Float>,
b_matrix: ArrayView2<Float>,
) -> SklResult<EigenResult> {
self.validate_input(&a_matrix)?;
self.validate_input(&b_matrix)?;
if a_matrix.shape() != b_matrix.shape() {
return Err(SklearsError::InvalidParameter {
name: "matrix_shapes".to_string(),
reason: "Matrices A and B must have the same shape".to_string(),
});
}
let b_eigen = self.decompose(b_matrix)?;
if b_eigen.eigenvalues.iter().any(|&x| x <= 0.0) {
return Err(SklearsError::InvalidParameter {
name: "b_matrix".to_string(),
reason: "Matrix B must be positive definite for generalized eigenvalue problem"
.to_string(),
});
}
let l_matrix = self.cholesky_decomposition(&b_matrix)?;
let l_inv = self.matrix_inverse(&l_matrix)?;
let temp = l_inv.dot(&a_matrix);
let c_matrix = temp.dot(&l_inv.t());
let c_result = self.decompose(c_matrix.view())?;
let eigenvectors = l_inv.t().dot(&c_result.eigenvectors);
Ok(EigenResult {
eigenvalues: c_result.eigenvalues,
eigenvectors,
condition_number: c_result.condition_number,
iterations_used: c_result.iterations_used,
converged: c_result.converged,
accuracy_metrics: c_result.accuracy_metrics,
})
}
fn validate_input(&self, matrix: &ArrayView2<Float>) -> SklResult<()> {
let (n, m) = matrix.dim();
if n != m {
return Err(SklearsError::InvalidParameter {
name: "matrix_shape".to_string(),
reason: "Matrix must be square".to_string(),
});
}
if n == 0 {
return Err(SklearsError::InvalidParameter {
name: "matrix_size".to_string(),
reason: "Matrix must have positive size".to_string(),
});
}
for &val in matrix.iter() {
if !val.is_finite() {
return Err(SklearsError::InvalidParameter {
name: "matrix_values".to_string(),
reason: "Matrix contains NaN or infinite values".to_string(),
});
}
}
let symmetry_error = self.check_symmetry(matrix);
if symmetry_error > 1e-10 {
eprintln!(
"Warning: Matrix is not symmetric (error: {:.2e})",
symmetry_error
);
}
Ok(())
}
fn check_symmetry(&self, matrix: &ArrayView2<Float>) -> Float {
let n = matrix.nrows();
let mut max_error: Float = 0.0;
for i in 0..n {
for j in i + 1..n {
let error = (matrix[[i, j]] - matrix[[j, i]]).abs();
max_error = max_error.max(error);
}
}
max_error
}
fn preprocess_matrix(&self, matrix: &ArrayView2<Float>) -> SklResult<(Array2<Float>, Float)> {
let mut processed = matrix.to_owned();
let matrix_norm = self.frobenius_norm(&processed);
let scaling_factor = if matrix_norm > 1e-10 {
1.0 / matrix_norm
} else {
1.0
};
processed *= scaling_factor;
let n = processed.nrows();
for i in 0..n {
for j in i + 1..n {
let avg = (processed[[i, j]] + processed[[j, i]]) * 0.5;
processed[[i, j]] = avg;
processed[[j, i]] = avg;
}
}
Ok((processed, 1.0 / scaling_factor))
}
fn frobenius_norm(&self, matrix: &Array2<Float>) -> Float {
matrix.iter().map(|&x| x * x).sum::<Float>().sqrt()
}
fn compute_initial_decomposition(&self, matrix: &Array2<Float>) -> SklResult<EigenResult> {
let (eigenvalues, eigenvectors) =
matrix
.eigh(UPLO::Lower)
.map_err(|e| SklearsError::InvalidParameter {
name: "eigendecomposition".to_string(),
reason: format!("LAPACK eigendecomposition failed: {}", e),
})?;
let mut eigen_pairs: Vec<(Float, Array1<Float>)> = eigenvalues
.iter()
.zip(eigenvectors.columns())
.map(|(&val, vec)| (val, vec.to_owned()))
.collect();
eigen_pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
let sorted_eigenvalues =
Array1::from_vec(eigen_pairs.iter().map(|(val, _)| *val).collect());
let sorted_eigenvectors = Array2::from_shape_vec(
(matrix.nrows(), eigen_pairs.len()),
eigen_pairs
.into_iter()
.flat_map(|(_, vec)| vec.into_raw_vec_and_offset().0)
.collect(),
)
.map_err(|e| SklearsError::InvalidParameter {
name: "array_construction".to_string(),
reason: format!("Failed to construct eigenvector matrix: {}", e),
})?;
Ok(EigenResult {
eigenvalues: sorted_eigenvalues,
eigenvectors: sorted_eigenvectors,
condition_number: None,
iterations_used: 0,
converged: true,
accuracy_metrics: AccuracyMetrics {
max_residual: 0.0,
orthogonality_error: 0.0,
backward_error: 0.0,
eigenvalue_error: 0.0,
},
})
}
fn iterative_refinement(
&self,
original_matrix: &ArrayView2<Float>,
mut result: EigenResult,
) -> SklResult<EigenResult> {
for iteration in 0..self.config.max_iterations {
let mut max_residual: Float = 0.0;
let mut improved = false;
for i in 0..result.eigenvalues.len() {
let eigenval = result.eigenvalues[i];
let eigenvec = result.eigenvectors.column(i).to_owned();
let ax = original_matrix.dot(&eigenvec);
let lambda_x = eigenvec.mapv(|x| x * eigenval);
let residual = &ax - &lambda_x;
let residual_norm = residual.iter().map(|&x| x * x).sum::<Float>().sqrt();
max_residual = max_residual.max(residual_norm);
if residual_norm > self.config.tolerance {
let improved_pair =
self.refine_eigenpair(original_matrix, eigenval, &eigenvec)?;
if let Some((new_val, new_vec)) = improved_pair {
result.eigenvalues[i] = new_val;
result.eigenvectors.column_mut(i).assign(&new_vec);
improved = true;
}
}
}
result.iterations_used = iteration + 1;
if max_residual < self.config.tolerance {
result.converged = true;
break;
}
if !improved {
break;
}
}
Ok(result)
}
fn refine_eigenpair(
&self,
matrix: &ArrayView2<Float>,
eigenval: Float,
eigenvec: &Array1<Float>,
) -> SklResult<Option<(Float, Array1<Float>)>> {
let n = matrix.nrows();
let sigma = eigenval + 1e-8;
let mut shifted_matrix = matrix.to_owned();
for i in 0..n {
shifted_matrix[[i, i]] -= sigma;
}
let mut y = eigenvec.clone();
for _ in 0..5 {
let mut new_y = Array1::zeros(n);
for i in 0..n {
let mut sum = 0.0;
for j in 0..n {
if i != j {
sum += shifted_matrix[[i, j]] * y[j];
}
}
if shifted_matrix[[i, i]].abs() > 1e-14 {
new_y[i] = (eigenvec[i] - sum) / shifted_matrix[[i, i]];
} else {
new_y[i] = y[i]; }
}
y = new_y;
}
let norm = y.iter().map(|&x| x * x).sum::<Float>().sqrt();
if norm > 1e-14 {
y /= norm;
}
let ax = matrix.dot(&y);
let numerator = y
.iter()
.zip(ax.iter())
.map(|(&x, &ax)| x * ax)
.sum::<Float>();
let denominator = y.iter().map(|&x| x * x).sum::<Float>();
if denominator > 1e-14 {
let refined_eigenval = numerator / denominator;
Ok(Some((refined_eigenval, y)))
} else {
Ok(None)
}
}
fn compute_accuracy_metrics(
&self,
matrix: &ArrayView2<Float>,
result: &EigenResult,
) -> AccuracyMetrics {
let mut max_residual: Float = 0.0;
let mut eigenvalue_errors = Vec::new();
for i in 0..result.eigenvalues.len() {
let eigenval = result.eigenvalues[i];
let eigenvec = result.eigenvectors.column(i);
let ax = matrix.dot(&eigenvec);
let lambda_x = eigenvec.mapv(|x| x * eigenval);
let residual = &ax - &lambda_x;
let residual_norm = residual.iter().map(|&x| x * x).sum::<Float>().sqrt();
max_residual = max_residual.max(residual_norm);
let computed_eigenval = eigenvec.dot(&ax) / eigenvec.dot(&eigenvec);
eigenvalue_errors.push((computed_eigenval - eigenval).abs());
}
let qtq = result.eigenvectors.t().dot(&result.eigenvectors);
let mut orthogonality_error = 0.0;
let n = qtq.nrows();
for i in 0..n {
for j in 0..n {
let expected = if i == j { 1.0 } else { 0.0 };
let error = (qtq[[i, j]] - expected).abs();
orthogonality_error += error * error;
}
}
orthogonality_error = orthogonality_error.sqrt();
let matrix_norm = self.frobenius_norm(&matrix.to_owned());
let backward_error = max_residual / matrix_norm;
let eigenvalue_error = eigenvalue_errors
.iter()
.cloned()
.fold(0.0 as Float, |a, b| a.max(b));
AccuracyMetrics {
max_residual,
orthogonality_error,
backward_error,
eigenvalue_error,
}
}
fn estimate_condition_number(&self, eigenvalues: &Array1<Float>) -> Float {
if eigenvalues.is_empty() {
return 1.0;
}
let max_eigenval = eigenvalues
.iter()
.cloned()
.fold(Float::NEG_INFINITY, |a, b| a.max(b));
let min_eigenval = eigenvalues
.iter()
.cloned()
.fold(Float::INFINITY, |a, b| a.min(b));
if min_eigenval.abs() < 1e-15 {
Float::INFINITY
} else {
max_eigenval.abs() / min_eigenval.abs()
}
}
fn detect_numerical_rank(&self, eigenvalues: &Array1<Float>) -> usize {
if eigenvalues.is_empty() {
return 0;
}
if eigenvalues.len() == 1 {
return if eigenvalues[0].abs() < self.config.min_eigenvalue {
0
} else {
1
};
}
let mut sorted_eigenvals: Vec<Float> = eigenvalues.iter().cloned().collect();
sorted_eigenvals.sort_by(|a, b| {
b.abs()
.partial_cmp(&a.abs())
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut max_gap_ratio = 0.0;
let mut gap_index = sorted_eigenvals.len();
for i in 0..sorted_eigenvals.len() - 1 {
let current = sorted_eigenvals[i].abs();
let next = sorted_eigenvals[i + 1].abs();
if next > 1e-15 {
let gap_ratio = current / next;
if gap_ratio > max_gap_ratio {
max_gap_ratio = gap_ratio;
gap_index = i + 1;
}
} else {
gap_index = i + 1;
break;
}
}
let max_eigenval = sorted_eigenvals[0].abs();
let threshold = max_eigenval * 1e-8;
for (i, &val) in sorted_eigenvals.iter().enumerate() {
if val.abs() < threshold {
return i;
}
}
if max_gap_ratio > 100.0 {
return gap_index;
}
sorted_eigenvals.len()
}
fn cholesky_decomposition(&self, matrix: &ArrayView2<Float>) -> SklResult<Array2<Float>> {
let n = matrix.nrows();
let mut l = Array2::zeros((n, n));
for i in 0..n {
for j in 0..=i {
if i == j {
let mut sum = 0.0;
for k in 0..j {
sum += l[[j, k]] * l[[j, k]];
}
let val = matrix[[j, j]] - sum;
if val <= 0.0 {
return Err(SklearsError::InvalidParameter {
name: "cholesky".to_string(),
reason: "Matrix is not positive definite".to_string(),
});
}
l[[j, j]] = val.sqrt();
} else {
let mut sum = 0.0;
for k in 0..j {
sum += l[[i, k]] * l[[j, k]];
}
l[[i, j]] = (matrix[[i, j]] - sum) / l[[j, j]];
}
}
}
Ok(l)
}
fn matrix_inverse(&self, matrix: &Array2<Float>) -> SklResult<Array2<Float>> {
matrix.inv().map_err(|e| SklearsError::InvalidParameter {
name: "matrix_inverse".to_string(),
reason: format!("Matrix inversion failed: {}", e),
})
}
}
pub struct ManifoldEigen;
impl ManifoldEigen {
pub fn laplacian_eigen(
laplacian: ArrayView2<Float>,
n_components: usize,
) -> SklResult<EigenResult> {
let config = EigenConfig {
min_eigenvalue: 1e-12, ..Default::default()
};
let solver = StableEigen::new(config);
let mut result = solver.decompose(laplacian)?;
let mut eigen_pairs: Vec<(Float, Array1<Float>)> = result
.eigenvalues
.iter()
.zip(result.eigenvectors.columns())
.map(|(&val, vec)| (val, vec.to_owned()))
.collect();
eigen_pairs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
let start_idx = eigen_pairs
.iter()
.position(|(val, _)| val.abs() > 1e-12)
.unwrap_or(0);
let end_idx = (start_idx + n_components).min(eigen_pairs.len());
let selected_pairs: Vec<(Float, Array1<Float>)> = eigen_pairs[start_idx..end_idx].to_vec();
if selected_pairs.is_empty() {
return Err(SklearsError::InvalidParameter {
name: "laplacian_eigen".to_string(),
reason: "No non-zero eigenvalues found".to_string(),
});
}
result.eigenvalues = Array1::from_vec(selected_pairs.iter().map(|(val, _)| *val).collect());
result.eigenvectors = Array2::from_shape_vec(
(laplacian.nrows(), selected_pairs.len()),
selected_pairs
.into_iter()
.flat_map(|(_, vec)| vec.into_raw_vec_and_offset().0)
.collect(),
)
.map_err(|e| SklearsError::InvalidParameter {
name: "array_construction".to_string(),
reason: format!("Failed to construct eigenvector matrix: {}", e),
})?;
Ok(result)
}
pub fn covariance_eigen(
covariance: ArrayView2<Float>,
n_components: usize,
) -> SklResult<EigenResult> {
let config = EigenConfig {
min_eigenvalue: 1e-10, ..Default::default()
};
let solver = StableEigen::new(config);
let mut result = solver.decompose(covariance)?;
if n_components < result.eigenvalues.len() {
result.eigenvalues = result
.eigenvalues
.slice(scirs2_core::ndarray::s![..n_components])
.to_owned();
result.eigenvectors = result
.eigenvectors
.slice(scirs2_core::ndarray::s![.., ..n_components])
.to_owned();
}
Ok(result)
}
}
#[allow(non_snake_case)]
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::array;
#[test]
fn test_stable_eigen_basic() {
let matrix = array![[4.0, 2.0], [2.0, 1.0]];
let config = EigenConfig::default();
let solver = StableEigen::new(config);
let result = solver
.decompose(matrix.view())
.expect("operation should succeed");
assert_eq!(result.eigenvalues.len(), 2);
assert_eq!(result.eigenvectors.shape(), &[2, 2]);
assert!(result.converged);
}
#[test]
fn test_symmetry_check() {
let asymmetric = array![[1.0, 2.0], [3.0, 4.0]];
let config = EigenConfig::default();
let solver = StableEigen::new(config);
let result = solver.decompose(asymmetric.view());
assert!(result.is_ok());
}
#[test]
fn test_condition_monitoring() {
let ill_conditioned = array![[1.0, 1.0], [1.0, 1.0 + 1e-6]]; let config = EigenConfig {
monitor_condition: true,
condition_threshold: 1e4,
..Default::default()
};
let solver = StableEigen::new(config);
let result = solver
.decompose(ill_conditioned.view())
.expect("operation should succeed");
assert!(result.condition_number.is_some());
assert!(result.condition_number.expect("operation should succeed") > 0.0);
}
#[test]
fn test_rank_detection() {
let rank_deficient = array![[1.0, 2.0], [2.0, 4.0]]; let config = EigenConfig::default();
let solver = StableEigen::new(config);
let result = solver
.decompose_with_rank_detection(rank_deficient.view())
.expect("operation should succeed");
assert_eq!(result.eigenvalues.len(), 1);
}
#[test]
fn test_laplacian_eigen() {
let laplacian = array![[1.0, -1.0, 0.0], [-1.0, 2.0, -1.0], [0.0, -1.0, 1.0]];
let result =
ManifoldEigen::laplacian_eigen(laplacian.view(), 2).expect("operation should succeed");
assert_eq!(result.eigenvalues.len(), 2);
for &eigenval in result.eigenvalues.iter() {
assert!(eigenval > 1e-10);
}
}
#[test]
fn test_accuracy_metrics() {
let matrix = array![[2.0, 1.0], [1.0, 2.0]];
let config = EigenConfig::default();
let solver = StableEigen::new(config);
let result = solver
.decompose(matrix.view())
.expect("operation should succeed");
assert!(result.accuracy_metrics.max_residual < 1e-10);
assert!(result.accuracy_metrics.orthogonality_error < 1e-10);
assert!(result.accuracy_metrics.backward_error < 1e-10);
}
}