use legume_numeric::matrix::traits::RandomizedAlgs;
use nalgebra::{DMatrix, DVector};
use rand::Rng;
use rand_distr::{Distribution, Normal};
#[derive(Debug, Clone)]
pub struct CopulaCovariance {
pub factor: DMatrix<f32>,
pub ridge_sd: DVector<f32>,
}
impl CopulaCovariance {
pub fn dim(&self) -> usize {
self.factor.nrows()
}
pub fn rank(&self) -> usize {
self.factor.ncols()
}
pub fn fit(z: &DMatrix<f32>, rank: usize, regularization: f32) -> anyhow::Result<Self> {
let g = z.nrows();
let n = z.ncols();
if n < 2 {
anyhow::bail!("need ≥2 cells to fit a copula covariance");
}
let r_eff = rank.max(1).min(g).min(n);
let (u, sigmas, _vt) = z.rsvd(r_eff)?;
let actual_rank = u.ncols().min(sigmas.len());
let scale = 1.0 / (n as f32).sqrt();
let mut factor = if u.ncols() > actual_rank {
u.columns(0, actual_rank).into_owned()
} else {
u
};
for k in 0..actual_rank {
let w = sigmas[k] * scale;
factor.column_mut(k).scale_mut(w);
}
let lambda = regularization.max(0.0);
let mut ridge_sd = DVector::<f32>::zeros(g);
for h in 0..g {
let row_norm_sq: f32 = (0..actual_rank).map(|k| factor[(h, k)].powi(2)).sum();
let v_h = row_norm_sq + lambda;
if v_h > 1e-12 {
let inv_sd = 1.0 / v_h.sqrt();
for k in 0..actual_rank {
factor[(h, k)] *= inv_sd;
}
ridge_sd[h] = lambda.sqrt() * inv_sd;
}
}
Ok(Self { factor, ridge_sd })
}
pub fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> DVector<f32> {
let normal = Normal::new(0.0_f32, 1.0_f32).unwrap();
let eta = DVector::from_fn(self.rank(), |_, _| normal.sample(rng));
let mut z = &self.factor * eta;
for i in 0..self.dim() {
z[i] += self.ridge_sd[i] * normal.sample(rng);
}
z
}
pub fn truncate_rank(&self, new_rank: usize) -> Self {
let g = self.dim();
let r_keep = new_rank.min(self.rank());
let factor = if r_keep == 0 {
DMatrix::<f32>::zeros(g, 0)
} else {
self.factor.columns(0, r_keep).into_owned()
};
let mut ridge_sd = DVector::<f32>::zeros(g);
for h in 0..g {
let row_norm_sq: f32 = (0..r_keep).map(|k| factor[(h, k)].powi(2)).sum();
ridge_sd[h] = (1.0 - row_norm_sq).max(0.0).sqrt();
}
Self { factor, ridge_sd }
}
pub fn random_low_rank<R: Rng + ?Sized>(g: usize, rank: usize, rng: &mut R) -> Self {
let normal = Normal::new(0.0_f32, 1.0_f32).unwrap();
let mut factor = DMatrix::<f32>::from_fn(g, rank, |_, _| normal.sample(rng));
const ROW_NORM_SQ_CAP: f32 = 0.99;
let mut ridge_sd = DVector::<f32>::zeros(g);
for h in 0..g {
let row_norm_sq: f32 = (0..rank).map(|k| factor[(h, k)].powi(2)).sum();
let target_norm = ROW_NORM_SQ_CAP.sqrt();
let scale = if row_norm_sq > 1e-12 {
target_norm / row_norm_sq.sqrt()
} else {
0.0
};
for k in 0..rank {
factor[(h, k)] *= scale;
}
let new_norm_sq = if rank > 0 { ROW_NORM_SQ_CAP } else { 0.0 };
ridge_sd[h] = (1.0 - new_norm_sq).max(0.0).sqrt();
}
Self { factor, ridge_sd }
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::SeedableRng;
#[test]
fn identity_recovery_full_rank() {
let g = 5;
let n = 5000;
let normal = Normal::new(0.0_f32, 1.0_f32).unwrap();
let mut rng = rand::rngs::StdRng::seed_from_u64(7);
let z = DMatrix::from_fn(g, n, |_, _| normal.sample(&mut rng));
let cov = CopulaCovariance::fit(&z, g, 0.0).unwrap();
let recovered = &cov.factor * cov.factor.transpose();
for i in 0..g {
for j in 0..g {
let expected = if i == j { 1.0 } else { 0.0 };
assert!(
(recovered[(i, j)] - expected).abs() < 0.1,
"Σ[{},{}]={} expected {}",
i,
j,
recovered[(i, j)],
expected
);
}
}
}
#[test]
fn correlation_round_trip() {
let g = 3;
let mut sigma = DMatrix::<f32>::identity(g, g);
sigma[(0, 1)] = 0.7;
sigma[(1, 0)] = 0.7;
let l = sigma.cholesky().unwrap().l();
let n = 10_000;
let normal = Normal::new(0.0_f32, 1.0_f32).unwrap();
let mut rng = rand::rngs::StdRng::seed_from_u64(13);
let mut z = DMatrix::<f32>::zeros(g, n);
for j in 0..n {
let eta = DVector::from_fn(g, |_, _| normal.sample(&mut rng));
z.set_column(j, &(&l * eta));
}
let cov = CopulaCovariance::fit(&z, g, 0.0).unwrap();
let recovered = &cov.factor * cov.factor.transpose();
assert!(
(recovered[(0, 1)] - 0.7).abs() < 0.05,
"off-diag={}",
recovered[(0, 1)]
);
assert!(
(recovered[(0, 0)] - 1.0).abs() < 0.05,
"diag[0,0]={}",
recovered[(0, 0)]
);
}
#[test]
fn sample_shape_and_rank_cap() {
let g = 50;
let n = 200;
let normal = Normal::new(0.0_f32, 1.0_f32).unwrap();
let mut rng = rand::rngs::StdRng::seed_from_u64(1);
let z = DMatrix::from_fn(g, n, |_, _| normal.sample(&mut rng));
let cov = CopulaCovariance::fit(&z, 200, 1e-3).unwrap();
assert!(cov.rank() <= g);
assert_eq!(cov.dim(), g);
let s = cov.sample(&mut rng);
assert_eq!(s.len(), g);
}
#[test]
fn rank_one_recovery() {
let g = 4;
let n = 8000;
let v = DVector::<f32>::from_vec(vec![1.0, 1.0, 0.0, 0.0]);
let normal = Normal::new(0.0_f32, 1.0_f32).unwrap();
let mut rng = rand::rngs::StdRng::seed_from_u64(99);
let mut z = DMatrix::<f32>::zeros(g, n);
for k in 0..n {
let eta: f32 = normal.sample(&mut rng);
z.set_column(k, &(eta * &v));
}
let cov = CopulaCovariance::fit(&z, 1, 0.0).unwrap();
let recovered = &cov.factor * cov.factor.transpose();
assert!(
(recovered[(0, 0)] - 1.0).abs() < 0.1,
"Σ̂[0,0]={}",
recovered[(0, 0)]
);
assert!(
(recovered[(0, 1)] - 1.0).abs() < 0.1,
"Σ̂[0,1]={}",
recovered[(0, 1)]
);
assert!(
recovered[(2, 2)].abs() < 0.05,
"Σ̂[2,2]={} (should be ~0)",
recovered[(2, 2)]
);
}
#[test]
fn unit_marginal_variance_on_subunit_input() {
let g = 30;
let n = 5000;
let mut rng = rand::rngs::StdRng::seed_from_u64(2026);
let row_sds: Vec<f32> = (0..g)
.map(|h| 0.05 + 0.95 * (h as f32) / (g as f32 - 1.0))
.collect();
let mut z = DMatrix::<f32>::zeros(g, n);
for h in 0..g {
let normal = Normal::new(0.0_f32, row_sds[h]).unwrap();
for j in 0..n {
z[(h, j)] = normal.sample(&mut rng);
}
}
let cov = CopulaCovariance::fit(&z, 5, 1e-3).unwrap();
let n_samples = 20_000;
let mut s2 = vec![0.0_f64; g];
for _ in 0..n_samples {
let s = cov.sample(&mut rng);
for h in 0..g {
let v = s[h] as f64;
s2[h] += v * v;
}
}
for h in 0..g {
let var_h = s2[h] / n_samples as f64;
assert!(
(var_h - 1.0).abs() < 0.08,
"row {} (input σ={:.2}): Var(z*[{}]) = {:.3} (expected ≈1)",
h,
row_sds[h],
h,
var_h
);
}
}
#[test]
fn correlation_preserved_under_rescale() {
let g = 4;
let n = 10_000;
let mut rng = rand::rngs::StdRng::seed_from_u64(11);
let normal = Normal::new(0.0_f32, 1.0_f32).unwrap();
let mut z = DMatrix::<f32>::zeros(g, n);
for j in 0..n {
let eta1: f32 = normal.sample(&mut rng);
let eps0: f32 = normal.sample(&mut rng);
let eps1: f32 = normal.sample(&mut rng);
z[(0, j)] = 0.2 * (0.9 * eta1 + 0.44 * eps0); z[(1, j)] = 1.0 * (0.9 * eta1 + 0.44 * eps1); z[(2, j)] = 0.5 * normal.sample(&mut rng);
z[(3, j)] = 1.5 * normal.sample(&mut rng);
}
let cov = CopulaCovariance::fit(&z, 4, 1e-4).unwrap();
let n_samples = 30_000;
let mut sum = vec![0.0_f64; g];
let mut sum2 = vec![0.0_f64; g];
let mut sxy = vec![vec![0.0_f64; g]; g];
for _ in 0..n_samples {
let s = cov.sample(&mut rng);
for h in 0..g {
let vh = s[h] as f64;
sum[h] += vh;
sum2[h] += vh * vh;
for k in 0..g {
sxy[h][k] += vh * (s[k] as f64);
}
}
}
let nf = n_samples as f64;
let mean: Vec<f64> = sum.iter().map(|s| s / nf).collect();
let var: Vec<f64> = (0..g).map(|h| sum2[h] / nf - mean[h] * mean[h]).collect();
let corr =
|h: usize, k: usize| (sxy[h][k] / nf - mean[h] * mean[k]) / (var[h] * var[k]).sqrt();
let c01 = corr(0, 1);
assert!(c01 > 0.6, "corr(z*[0], z*[1])={:.3} expected >0.6", c01);
let c23 = corr(2, 3);
assert!(c23.abs() < 0.1, "corr(z*[2], z*[3])={:.3} expected ≈0", c23);
let c02 = corr(0, 2);
assert!(c02.abs() < 0.1, "corr(z*[0], z*[2])={:.3} expected ≈0", c02);
for (h, &v) in var.iter().enumerate().take(g) {
assert!(
(v - 1.0).abs() < 0.05,
"Var(z*[{}])={:.3}, expected ≈1",
h,
v
);
}
}
#[test]
fn truncate_rank_preserves_unit_variance() {
let g = 25;
let n = 4_000;
let mut rng = rand::rngs::StdRng::seed_from_u64(2027);
let normal = Normal::new(0.0_f32, 1.0_f32).unwrap();
let z = DMatrix::from_fn(g, n, |_, _| normal.sample(&mut rng));
let full = CopulaCovariance::fit(&z, 10, 1e-3).unwrap();
for new_rank in [0usize, 1, 3, 7] {
let truncated = full.truncate_rank(new_rank);
assert_eq!(truncated.dim(), g);
assert_eq!(truncated.rank(), new_rank.min(full.rank()));
let n_samples = 15_000;
let mut s2 = vec![0.0_f64; g];
for _ in 0..n_samples {
let s = truncated.sample(&mut rng);
for h in 0..g {
let v = s[h] as f64;
s2[h] += v * v;
}
}
for (h, ss) in s2.iter().enumerate().take(g) {
let var_h = ss / n_samples as f64;
assert!(
(var_h - 1.0).abs() < 0.08,
"rank={}, row {}: Var(z*) = {:.3} (expected ≈1)",
new_rank,
h,
var_h
);
}
}
}
#[test]
fn random_low_rank_unit_variance() {
let g = 30;
let mut rng = rand::rngs::StdRng::seed_from_u64(2028);
for rank in [0usize, 1, 3, 5] {
let cov = CopulaCovariance::random_low_rank(g, rank, &mut rng);
assert_eq!(cov.dim(), g);
assert_eq!(cov.rank(), rank);
let n_samples = 15_000;
let mut s2 = vec![0.0_f64; g];
for _ in 0..n_samples {
let s = cov.sample(&mut rng);
for h in 0..g {
let v = s[h] as f64;
s2[h] += v * v;
}
}
for (h, ss) in s2.iter().enumerate().take(g) {
let var_h = ss / n_samples as f64;
assert!(
(var_h - 1.0).abs() < 0.08,
"rank={}, row {}: Var(z*) = {:.3} (expected ≈1)",
rank,
h,
var_h
);
}
}
}
}