use crate::error::FdarError;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum NcompMethod {
CumulativeVariance(f64),
Elbow,
Fixed(usize),
Kaiser,
}
#[must_use = "selected ncomp should not be discarded"]
pub fn select_ncomp(eigenvalues: &[f64], method: &NcompMethod) -> Result<usize, FdarError> {
if eigenvalues.is_empty() {
return Err(FdarError::InvalidDimension {
parameter: "eigenvalues",
expected: "at least 1 eigenvalue".to_string(),
actual: "0 eigenvalues".to_string(),
});
}
for (i, &ev) in eigenvalues.iter().enumerate() {
if ev.is_nan() {
return Err(FdarError::InvalidParameter {
parameter: "eigenvalues",
message: format!("eigenvalue[{i}] is NaN"),
});
}
if ev < 0.0 {
return Err(FdarError::InvalidParameter {
parameter: "eigenvalues",
message: format!("eigenvalue[{i}] = {ev} is negative"),
});
}
}
match method {
NcompMethod::CumulativeVariance(threshold) => {
if *threshold <= 0.0 || *threshold > 1.0 {
return Err(FdarError::InvalidParameter {
parameter: "threshold",
message: format!("threshold must be in (0, 1], got {threshold}"),
});
}
cumulative_variance_ncomp(eigenvalues, *threshold)
}
NcompMethod::Elbow => {
if eigenvalues.len() < 3 {
return cumulative_variance_ncomp(eigenvalues, 0.95);
}
Ok(elbow_ncomp(eigenvalues))
}
NcompMethod::Fixed(k) => Ok((*k).max(1).min(eigenvalues.len())),
NcompMethod::Kaiser => {
if eigenvalues.len() < 2 {
return Ok(1); }
Ok(kaiser_ncomp(eigenvalues))
}
}
}
fn cumulative_variance_ncomp(eigenvalues: &[f64], threshold: f64) -> Result<usize, FdarError> {
let total: f64 = eigenvalues.iter().sum();
if total <= 0.0 {
return Err(FdarError::ComputationFailed {
operation: "select_ncomp",
detail: "total variance is non-positive".to_string(),
});
}
let mut cumsum = 0.0;
for (k, &ev) in eigenvalues.iter().enumerate() {
cumsum += ev;
if cumsum / total >= threshold {
return Ok(k + 1);
}
}
Ok(eigenvalues.len())
}
fn kaiser_ncomp(eigenvalues: &[f64]) -> usize {
let mean = eigenvalues.iter().sum::<f64>() / eigenvalues.len() as f64;
let k = eigenvalues.iter().filter(|&&ev| ev > mean).count();
k.max(1) }
fn elbow_ncomp(eigenvalues: &[f64]) -> usize {
let n = eigenvalues.len();
let smoothed: Vec<f64> = (0..n)
.map(|i| {
if i == 0 {
(eigenvalues[0] + eigenvalues[1]) / 2.0
} else if i == n - 1 {
(eigenvalues[n - 2] + eigenvalues[n - 1]) / 2.0
} else {
(eigenvalues[i - 1] + eigenvalues[i] + eigenvalues[i + 1]) / 3.0
}
})
.collect();
let mut best_k = 1;
let mut best_d2 = f64::NEG_INFINITY;
for k in 1..n - 1 {
let d2 = smoothed[k - 1] - 2.0 * smoothed[k] + smoothed[k + 1];
if d2 > best_d2 {
best_d2 = d2;
best_k = k;
}
}
(best_k + 1).max(1).min(eigenvalues.len())
}