use std::error::Error;
use ndarray::Array1;
use ndarray::Array2;
use rand::Rng;
use super::CopulaType;
use super::dvine::PairCopula;
use crate::traits::MultivariateExt;
#[derive(Debug, Clone)]
pub struct CVine {
dim: usize,
pair_copulas: Vec<Vec<PairCopula>>,
}
impl CVine {
pub fn new(dim: usize, pair_copulas: Vec<Vec<PairCopula>>) -> Result<Self, Box<dyn Error>> {
if dim < 2 {
return Err(format!("C-vine requires dim ≥ 2, got {dim}").into());
}
if pair_copulas.len() != dim - 1 {
return Err(
format!(
"Expected {} trees for dim={dim}, got {}",
dim - 1,
pair_copulas.len()
)
.into(),
);
}
for (m, tree) in pair_copulas.iter().enumerate() {
let expected = dim - 1 - m;
if tree.len() != expected {
return Err(
format!(
"Tree T_{} should have {expected} edges (got {})",
m + 1,
tree.len()
)
.into(),
);
}
}
Ok(Self { dim, pair_copulas })
}
pub fn independence(dim: usize) -> Result<Self, Box<dyn Error>> {
let pc: Vec<Vec<PairCopula>> = (0..dim - 1)
.map(|m| vec![PairCopula::Independence; dim - 1 - m])
.collect();
Self::new(dim, pc)
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn pair_copulas(&self) -> &[Vec<PairCopula>] {
&self.pair_copulas
}
fn sample_one<R: Rng + ?Sized>(&self, rng: &mut R) -> Array1<f64> {
let d = self.dim;
let w: Vec<f64> = (0..d).map(|_| rng.random::<f64>()).collect();
let mut u = Array1::<f64>::zeros(d);
let mut v = vec![vec![0.0_f64; d + 1]; d + 1];
u[0] = w[0];
v[0][0] = w[0];
for i in 1..d {
v[i][0] = w[i];
for k in (0..i).rev() {
let cop = self.pair_copulas[k][i - k - 1];
v[i][0] = cop.h_inverse(v[i][0], v[k][k]);
}
u[i] = v[i][0];
if i == d - 1 {
break;
}
for j in 0..i {
let cop = self.pair_copulas[j][i - j - 1];
v[i][j + 1] = cop.h(v[i][j], v[j][j]);
}
}
u
}
fn log_density_one(&self, u: &[f64]) -> f64 {
let d = self.dim;
let mut v = vec![vec![0.0_f64; d + 1]; d + 1];
let mut log_c = 0.0_f64;
for (i, &ui) in u.iter().enumerate() {
v[i][0] = ui;
}
for m in 0..d - 1 {
for i in 0..d - 1 - m {
let cop = self.pair_copulas[m][i];
log_c += cop.log_density(v[m][m], v[m + i + 1][m]);
}
if m + 1 == d - 1 {
break;
}
for i in 0..d - 1 - m {
let cop = self.pair_copulas[m][i];
v[m + i + 1][m + 1] = cop.h(v[m + i + 1][m], v[m][m]);
}
}
log_c
}
}
impl MultivariateExt for CVine {
fn r#type(&self) -> CopulaType {
CopulaType::CVine
}
fn sample(&self, n: usize) -> Result<Array2<f64>, Box<dyn Error>> {
let mut rng = rand::rng();
let mut out = Array2::<f64>::zeros((n, self.dim));
for r in 0..n {
let row = self.sample_one(&mut rng);
for c in 0..self.dim {
out[[r, c]] = row[c].clamp(1e-12, 1.0 - 1e-12);
}
}
Ok(out)
}
fn fit(&mut self, _X: Array2<f64>) -> Result<(), Box<dyn Error>> {
Err(
"CVine::fit not implemented — supply the tree explicitly via CVine::new \
and seed each PairCopula parameter from pairwise Kendall τ. Sequential MLE + \
AIC/BIC family selection (Dißmann 2013) is not yet implemented."
.into(),
)
}
fn check_fit(&self, X: &Array2<f64>) -> Result<(), Box<dyn Error>> {
if X.ncols() != self.dim {
return Err(
format!(
"Dimension mismatch: X has {} columns, C-vine has dim {}",
X.ncols(),
self.dim
)
.into(),
);
}
if X.iter().any(|&v| !(0.0..=1.0).contains(&v)) {
return Err("Input X must be in [0,1] for the C-vine".into());
}
Ok(())
}
fn pdf(&self, X: Array2<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
self.check_fit(&X)?;
let mut out = Array1::<f64>::zeros(X.nrows());
for (i, row) in X.rows().into_iter().enumerate() {
let u: Vec<f64> = row.iter().copied().collect();
out[i] = self.log_density_one(&u).exp();
}
Ok(out)
}
fn log_pdf(&self, X: Array2<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
self.check_fit(&X)?;
let mut out = Array1::<f64>::zeros(X.nrows());
for (i, row) in X.rows().into_iter().enumerate() {
let u: Vec<f64> = row.iter().copied().collect();
out[i] = self.log_density_one(&u);
}
Ok(out)
}
fn cdf(&self, X: Array2<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
self.check_fit(&X)?;
let m = 4_000usize;
let sample = self.sample(m)?;
let mut out = Array1::<f64>::zeros(X.nrows());
for (i, row) in X.rows().into_iter().enumerate() {
let u: Vec<f64> = row.iter().copied().collect();
let mut count = 0usize;
for r in 0..m {
let mut all_le = true;
for c in 0..self.dim {
if sample[[r, c]] > u[c] {
all_le = false;
break;
}
}
if all_le {
count += 1;
}
}
out[i] = count as f64 / m as f64;
}
Ok(out)
}
}
#[cfg(test)]
mod tests {
use ndarray::array;
use super::*;
#[test]
fn cvine_independence_three_dim_uniform_marginals() {
let cv = CVine::independence(3).unwrap();
let s = cv.sample(10_000).unwrap();
for j in 0..3 {
let col = s.column(j);
let m: f64 = col.iter().sum::<f64>() / col.len() as f64;
assert!(
(m - 0.5).abs() < 0.02,
"marginal {j} mean = {m}, expected ~0.5"
);
}
let lp = cv.log_pdf(array![[0.2, 0.4, 0.7]]).unwrap();
assert!(lp[0].abs() < 1e-12);
}
#[test]
fn cvine_two_dim_clayton_matches_bivariate() {
let cv = CVine::new(2, vec![vec![PairCopula::Clayton { theta: 2.0 }]]).unwrap();
let s = cv.sample(10_000).unwrap();
use crate::correlation::kendall_tau;
let tau = kendall_tau(&s);
assert!(
(tau[[0, 1]] - 0.5).abs() < 0.04,
"2-dim Clayton(θ=2) C-vine τ_(0,1) = {}, expected ~0.5",
tau[[0, 1]]
);
}
#[test]
fn cvine_three_dim_gaussian_star() {
let t1 = vec![
PairCopula::Gaussian { rho: 0.6 }, PairCopula::Gaussian { rho: 0.6 }, ];
let t2 = vec![PairCopula::Independence]; let cv = CVine::new(3, vec![t1, t2]).unwrap();
let s = cv.sample(10_000).unwrap();
use crate::correlation::kendall_tau;
let tau = kendall_tau(&s);
let expected_tau = (2.0 / std::f64::consts::PI) * 0.6_f64.asin();
assert!(
(tau[[0, 1]] - expected_tau).abs() < 0.03,
"τ_(0,1) = {} vs expected {expected_tau}",
tau[[0, 1]]
);
assert!(
(tau[[0, 2]] - expected_tau).abs() < 0.03,
"τ_(0,2) = {} vs expected {expected_tau}",
tau[[0, 2]]
);
assert!(
tau[[1, 2]] > 0.15 && tau[[1, 2]] < 0.32,
"τ_(1,2) = {} should sit between [0.15, 0.32] (Gaussian common-factor)",
tau[[1, 2]]
);
for j in 0..3 {
let col = s.column(j);
let mean: f64 = col.iter().sum::<f64>() / col.len() as f64;
assert!((mean - 0.5).abs() < 0.02);
}
}
#[test]
fn cvine_shape_validation() {
assert!(CVine::new(3, vec![vec![PairCopula::Independence]]).is_err());
assert!(CVine::new(1, vec![]).is_err());
}
#[test]
fn cvine_fit_rejects_with_descriptive_error() {
let mut cv = CVine::independence(3).unwrap();
let data = ndarray::Array2::<f64>::from_elem((10, 3), 0.5);
let res = cv.fit(data);
assert!(res.is_err());
let msg = res.unwrap_err().to_string();
assert!(
msg.contains("not implemented") || msg.contains("MLE"),
"fit error should point at the unimplemented sequential MLE; got: {msg}"
);
}
}