use super::eigen::EigenDecomposition;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MatrixHealth {
Healthy,
Acceptable,
Unstable,
}
#[derive(Debug, Clone)]
pub struct ConditionReport {
pub condition_number: f64,
pub health: MatrixHealth,
pub lambda_max: f64,
pub lambda_min: f64,
}
#[derive(Debug, Clone)]
pub struct ConditionImprovement {
pub before: ConditionReport,
pub after: ConditionReport,
pub improvement_factor: f64,
}
#[must_use]
pub fn classify(condition_number: f64) -> MatrixHealth {
if condition_number < 100.0 {
MatrixHealth::Healthy
} else if condition_number < 1000.0 {
MatrixHealth::Acceptable
} else {
MatrixHealth::Unstable
}
}
#[must_use]
pub fn condition_report(eigen: &EigenDecomposition) -> ConditionReport {
let lambda_max = eigen.eigenvalues[0];
let lambda_min = eigen.eigenvalues[eigen.n - 1];
let condition_number = if lambda_min > f64::EPSILON {
lambda_max / lambda_min
} else {
f64::INFINITY
};
ConditionReport {
condition_number,
health: classify(condition_number),
lambda_max,
lambda_min,
}
}
#[must_use]
pub fn compare(before: &EigenDecomposition, after: &EigenDecomposition) -> ConditionImprovement {
let before_report = condition_report(before);
let after_report = condition_report(after);
let improvement_factor = if after_report.condition_number > f64::EPSILON {
before_report.condition_number / after_report.condition_number
} else {
f64::INFINITY
};
ConditionImprovement {
before: before_report,
after: after_report,
improvement_factor,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::eigen::eigendecompose;
use approx::assert_relative_eq;
use nalgebra::DMatrix;
#[test]
fn test_classify_healthy() {
assert_eq!(classify(1.0), MatrixHealth::Healthy);
assert_eq!(classify(50.0), MatrixHealth::Healthy);
assert_eq!(classify(99.9), MatrixHealth::Healthy);
}
#[test]
fn test_classify_acceptable() {
assert_eq!(classify(100.0), MatrixHealth::Acceptable);
assert_eq!(classify(500.0), MatrixHealth::Acceptable);
assert_eq!(classify(999.9), MatrixHealth::Acceptable);
}
#[test]
fn test_classify_unstable() {
assert_eq!(classify(1000.0), MatrixHealth::Unstable);
assert_eq!(classify(10_000.0), MatrixHealth::Unstable);
assert_eq!(classify(f64::INFINITY), MatrixHealth::Unstable);
}
#[test]
fn test_identity_condition() {
let identity = DMatrix::identity(4, 4);
let eigen = eigendecompose(&identity).unwrap();
let report = condition_report(&eigen);
assert_relative_eq!(report.condition_number, 1.0, epsilon = 1e-10);
assert_eq!(report.health, MatrixHealth::Healthy);
}
#[test]
fn test_known_condition_number() {
#[rustfmt::skip]
let corr = DMatrix::from_row_slice(3, 3, &[
1.0, 0.9, 0.9,
0.9, 1.0, 0.9,
0.9, 0.9, 1.0,
]);
let eigen = eigendecompose(&corr).unwrap();
let report = condition_report(&eigen);
assert_relative_eq!(report.condition_number, 28.0, epsilon = 1e-10);
assert_eq!(report.health, MatrixHealth::Healthy);
}
#[test]
fn test_improvement_factor() {
#[rustfmt::skip]
let raw = DMatrix::from_row_slice(3, 3, &[
1.0, 0.99, 0.99,
0.99, 1.0, 0.99,
0.99, 0.99, 1.0,
]);
#[rustfmt::skip]
let cleaned = DMatrix::from_row_slice(3, 3, &[
1.0, 0.5, 0.5,
0.5, 1.0, 0.5,
0.5, 0.5, 1.0,
]);
let eigen_raw = eigendecompose(&raw).unwrap();
let eigen_cleaned = eigendecompose(&cleaned).unwrap();
let result = compare(&eigen_raw, &eigen_cleaned);
assert!(
result.improvement_factor > 1.0,
"denoising should improve condition: factor={}",
result.improvement_factor
);
}
#[test]
fn test_no_improvement() {
let corr = DMatrix::identity(3, 3);
let eigen = eigendecompose(&corr).unwrap();
let result = compare(&eigen, &eigen);
assert_relative_eq!(result.improvement_factor, 1.0, epsilon = 1e-10);
}
}