use ndarray::{Array1, Array2};
use solow_core::error::{Error, Result};
use solow_linalg::eigh;
pub fn cov2corr(cov: &Array2<f64>) -> Array2<f64> {
let (corr, _) = cov2corr_std(cov);
corr
}
pub fn cov2corr_std(cov: &Array2<f64>) -> (Array2<f64>, Array1<f64>) {
let k = cov.nrows();
let std: Array1<f64> = Array1::from_iter((0..k).map(|i| cov[[i, i]].sqrt()));
let mut corr = Array2::<f64>::zeros((k, k));
for i in 0..k {
for j in 0..k {
corr[[i, j]] = cov[[i, j]] / (std[i] * std[j]);
}
}
(corr, std)
}
pub fn corr2cov(corr: &Array2<f64>, std: &Array1<f64>) -> Array2<f64> {
let k = corr.nrows();
let mut cov = Array2::<f64>::zeros((k, k));
for i in 0..k {
for j in 0..k {
cov[[i, j]] = corr[[i, j]] * std[i] * std[j];
}
}
cov
}
fn clip_evals(x: &Array2<f64>, value: f64) -> Result<(Array2<f64>, bool)> {
let (w, v) = eigh(x)?;
let clipped = w.iter().any(|&e| e < value);
let k = w.len();
let mut scaled = v.clone(); for j in 0..k {
let ev = w[j].max(value);
for i in 0..k {
scaled[[i, j]] *= ev;
}
}
let x_new = scaled.dot(&v.t());
Ok((x_new, clipped))
}
pub fn corr_clipped(corr: &Array2<f64>, threshold: f64) -> Result<Array2<f64>> {
let (x_new, clipped) = clip_evals(corr, threshold)?;
if !clipped {
return Ok(corr.clone());
}
let k = x_new.nrows();
let d: Vec<f64> = (0..k).map(|i| x_new[[i, i]].sqrt()).collect();
let mut out = Array2::<f64>::zeros((k, k));
for i in 0..k {
for j in 0..k {
out[[i, j]] = x_new[[i, j]] / (d[i] * d[j]);
}
}
Ok(out)
}
pub fn corr_nearest(corr: &Array2<f64>, threshold: f64, n_fact: usize) -> Result<Array2<f64>> {
let k = corr.nrows();
if corr.ncols() != k {
return Err(Error::Shape("matrix is not square".into()));
}
let mut diff = Array2::<f64>::zeros((k, k));
let mut x_new = corr.clone();
let max_iter = k * n_fact;
for _ in 0..max_iter {
let x_adj = &x_new - &diff;
let (x_psd, clipped) = clip_evals(&x_adj, threshold)?;
if !clipped {
x_new = x_psd;
break;
}
diff = &x_psd - &x_adj;
x_new = x_psd;
for i in 0..k {
x_new[[i, i]] = 1.0;
}
}
Ok(x_new)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NearestMethod {
Clipped,
Nearest,
}
pub fn cov_nearest(
cov: &Array2<f64>,
method: NearestMethod,
threshold: f64,
n_fact: usize,
) -> Result<Array2<f64>> {
let (corr, std) = cov2corr_std(cov);
let corr_fixed = match method {
NearestMethod::Clipped => corr_clipped(&corr, threshold)?,
NearestMethod::Nearest => corr_nearest(&corr, threshold, n_fact)?,
};
Ok(corr2cov(&corr_fixed, &std))
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn cov2corr_roundtrip() {
let cov = array![[4.0, 2.0, 0.0], [2.0, 9.0, -3.0], [0.0, -3.0, 16.0]];
let (corr, std) = cov2corr_std(&cov);
assert!((corr[[0, 0]] - 1.0).abs() < 1e-12);
assert!((corr[[0, 1]] - 2.0 / (2.0 * 3.0)).abs() < 1e-12);
let back = corr2cov(&corr, &std);
for i in 0..3 {
for j in 0..3 {
assert!((back[[i, j]] - cov[[i, j]]).abs() < 1e-12);
}
}
}
#[test]
fn corr_nearest_makes_psd() {
let corr = array![[1.0, 0.9, -0.9], [0.9, 1.0, 0.9], [-0.9, 0.9, 1.0]];
let fixed = corr_nearest(&corr, 1e-7, 100).unwrap();
let (w, _) = eigh(&fixed).unwrap();
assert!(w[0] >= -1e-8, "smallest eigenvalue {} negative", w[0]);
for i in 0..3 {
assert!((fixed[[i, i]] - 1.0).abs() < 1e-6);
}
}
#[test]
fn corr_clipped_psd_passthrough() {
let corr = array![[1.0, 0.2], [0.2, 1.0]];
let fixed = corr_clipped(&corr, 1e-7).unwrap();
for i in 0..2 {
for j in 0..2 {
assert!((fixed[[i, j]] - corr[[i, j]]).abs() < 1e-15);
}
}
}
}