use nalgebra::DVector;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MpError {
#[error("ratio q must be positive, got {0}")]
InvalidQ(f64),
#[error("empty eigenvalue vector")]
EmptyEigenvalues,
#[error("σ² fitting did not converge after {max_iter} iterations (residual: {residual})")]
ConvergenceFailure {
max_iter: usize,
residual: f64,
},
}
#[derive(Debug, Clone)]
pub struct MpFit {
pub sigma_sq: f64,
pub lambda_plus: f64,
pub lambda_minus: f64,
pub signal_count: usize,
pub noise_count: usize,
pub q: f64,
}
#[must_use]
pub fn mp_bounds(sigma_sq: f64, q: f64) -> (f64, f64) {
if q <= 0.0 || !q.is_finite() || !sigma_sq.is_finite() {
return (0.0, 0.0);
}
let sqrt_inv_q = (1.0 / q).sqrt();
let lambda_plus = sigma_sq * (1.0 + sqrt_inv_q).powi(2);
let lambda_minus = sigma_sq * (1.0 - sqrt_inv_q).powi(2);
(lambda_minus, lambda_plus)
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn mp_density(lambda: f64, sigma_sq: f64, q: f64) -> f64 {
if q <= 0.0 || sigma_sq <= 0.0 || lambda <= 0.0 {
return 0.0;
}
let (lambda_minus, lambda_plus) = mp_bounds(sigma_sq, q);
if lambda < lambda_minus || lambda > lambda_plus {
return 0.0;
}
let numerator = ((lambda_plus - lambda) * (lambda - lambda_minus)).sqrt();
let denominator = 2.0 * std::f64::consts::PI * sigma_sq * lambda / q;
if denominator.abs() < f64::EPSILON {
return 0.0;
}
numerator / denominator
}
#[allow(clippy::cast_precision_loss)]
pub fn fit_sigma_sq(eigenvalues: &DVector<f64>, q: f64) -> Result<MpFit, MpError> {
if q <= 0.0 || !q.is_finite() {
return Err(MpError::InvalidQ(q));
}
let num_eigenvalues = eigenvalues.len();
if num_eigenvalues == 0 {
return Err(MpError::EmptyEigenvalues);
}
let max_iter = 1000;
let tol = 1e-10;
let mut sigma_sq = eigenvalues.mean();
for _ in 0..max_iter {
let (_, lambda_plus) = mp_bounds(sigma_sq, q);
let noise_eigenvalues: Vec<f64> = eigenvalues
.iter()
.filter(|&&ev| ev <= lambda_plus)
.copied()
.collect();
if noise_eigenvalues.is_empty() {
sigma_sq = f64::EPSILON;
break;
}
let new_sigma_sq = noise_eigenvalues.iter().sum::<f64>() / noise_eigenvalues.len() as f64;
if (new_sigma_sq - sigma_sq).abs() < tol {
sigma_sq = new_sigma_sq;
break;
}
sigma_sq = new_sigma_sq;
}
let (lambda_minus, lambda_plus) = mp_bounds(sigma_sq, q);
let signal_count = eigenvalues.iter().filter(|&&ev| ev > lambda_plus).count();
let noise_count = num_eigenvalues - signal_count;
Ok(MpFit {
sigma_sq,
lambda_plus,
lambda_minus,
signal_count,
noise_count,
q,
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_mp_bounds_q1() {
let (lambda_minus, lambda_plus) = mp_bounds(1.0, 1.0);
assert_relative_eq!(lambda_plus, 4.0, epsilon = 1e-10);
assert_relative_eq!(lambda_minus, 0.0, epsilon = 1e-10);
}
#[test]
fn test_mp_bounds_q2() {
let (lambda_minus, lambda_plus) = mp_bounds(1.0, 2.0);
let sqrt_half = (0.5_f64).sqrt();
assert_relative_eq!(lambda_plus, (1.0 + sqrt_half).powi(2), epsilon = 1e-10);
assert_relative_eq!(lambda_minus, (1.0 - sqrt_half).powi(2), epsilon = 1e-10);
}
#[test]
fn test_mp_bounds_sigma_scaling() {
let (lm1, lp1) = mp_bounds(1.0, 2.0);
let (lm2, lp2) = mp_bounds(2.0, 2.0);
assert_relative_eq!(lp2, 2.0 * lp1, epsilon = 1e-10);
assert_relative_eq!(lm2, 2.0 * lm1, epsilon = 1e-10);
}
#[test]
fn test_mp_bounds_invalid_q() {
assert_eq!(mp_bounds(1.0, 0.0), (0.0, 0.0));
assert_eq!(mp_bounds(1.0, -1.0), (0.0, 0.0));
}
#[test]
fn test_mp_density_outside_support() {
let sigma_sq = 1.0;
let q = 2.0;
let (lambda_minus, lambda_plus) = mp_bounds(sigma_sq, q);
assert_relative_eq!(mp_density(lambda_minus - 0.01, sigma_sq, q), 0.0);
assert_relative_eq!(mp_density(lambda_plus + 0.01, sigma_sq, q), 0.0);
assert_relative_eq!(mp_density(-1.0, sigma_sq, q), 0.0);
}
#[test]
fn test_mp_density_inside_support() {
let sigma_sq = 1.0;
let q = 2.0;
let (lambda_minus, lambda_plus) = mp_bounds(sigma_sq, q);
let mid = f64::midpoint(lambda_minus, lambda_plus);
let density = mp_density(mid, sigma_sq, q);
assert!(density > 0.0, "density at midpoint should be positive");
}
#[test]
fn test_mp_density_integrates_to_one() {
let sigma_sq = 1.0;
let q = 2.0;
let (lambda_minus, lambda_plus) = mp_bounds(sigma_sq, q);
let steps = 10_000;
let dl = (lambda_plus - lambda_minus) / f64::from(steps);
let integral: f64 = (0..steps)
.map(|step| {
let lambda = lambda_minus + (f64::from(step) + 0.5) * dl;
mp_density(lambda, sigma_sq, q) * dl
})
.sum();
assert_relative_eq!(integral, 1.0, epsilon = 1e-3);
}
#[test]
fn test_fit_pure_noise() {
let eigenvalues = DVector::from_element(100, 1.0);
let fit = fit_sigma_sq(&eigenvalues, 2.0).unwrap();
assert_relative_eq!(fit.sigma_sq, 1.0, epsilon = 0.1);
assert_eq!(fit.signal_count, 0);
assert_eq!(fit.noise_count, 100);
}
#[test]
fn test_fit_signal_plus_noise() {
let mut vals = vec![1.0; 99];
vals.insert(0, 20.0); let eigenvalues = DVector::from_vec(vals);
let fit = fit_sigma_sq(&eigenvalues, 2.0).unwrap();
assert!(
fit.signal_count >= 1,
"should detect at least 1 signal eigenvalue"
);
assert_relative_eq!(fit.sigma_sq, 1.0, epsilon = 0.15);
}
#[test]
fn test_fit_invalid_q() {
let eigenvalues = DVector::from_element(10, 1.0);
assert!(fit_sigma_sq(&eigenvalues, 0.0).is_err());
assert!(fit_sigma_sq(&eigenvalues, -1.0).is_err());
}
#[test]
fn test_fit_empty() {
let eigenvalues = DVector::from_vec(vec![]);
assert!(fit_sigma_sq(&eigenvalues, 2.0).is_err());
}
}