use std::error::Error;
use ndarray::Array1;
use ndarray::Array2;
use rand::Rng;
use super::CopulaType;
use crate::traits::MultivariateExt;
pub mod pair_copula;
pub use pair_copula::PairCopula;
#[derive(Debug, Clone)]
pub struct DVine {
dim: usize,
pair_copulas: Vec<Vec<PairCopula>>,
}
impl DVine {
pub fn new(dim: usize, pair_copulas: Vec<Vec<PairCopula>>) -> Result<Self, Box<dyn Error>> {
if dim < 2 {
return Err(format!("D-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 pair_copulas: Vec<Vec<PairCopula>> = (0..dim - 1)
.map(|m| vec![PairCopula::Independence; dim - 1 - m])
.collect();
Self::new(dim, pair_copulas)
}
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; 2 * d]; d];
u[0] = w[0];
v[0][0] = w[0];
for i in 1..d {
v[i][0] = w[i];
for k in (1..=i).rev() {
let cop = self.pair_copulas[k - 1][i - k];
v[i][0] = cop.h_inverse(v[i][0], v[i - 1][2 * k - 2]);
}
u[i] = v[i][0];
if i == d - 1 {
break;
}
v[i][1] = self.pair_copulas[0][i - 1].h(v[i - 1][0], v[i][0]);
v[i][2] = self.pair_copulas[0][i - 1].h(v[i][0], v[i - 1][0]);
for k in 2..=i - 1 {
let cop_a = self.pair_copulas[k - 1][i - k];
v[i][2 * k - 1] = cop_a.h(v[i - 1][2 * k - 2 - 1], v[i][2 * k - 2]);
v[i][2 * k] = cop_a.h(v[i][2 * k - 2], v[i - 1][2 * k - 2 - 1]);
}
if i >= 2 {
v[i][2 * i - 1] = self.pair_copulas[i - 1][0].h(v[i - 1][2 * i - 3], v[i][2 * i - 2]);
}
}
u
}
fn log_density_one(&self, u: &[f64]) -> f64 {
let d = self.dim;
let mut v = vec![vec![0.0_f64; 2 * d]; d];
let mut log_c = 0.0_f64;
for (i, &ui) in u.iter().enumerate() {
v[i][0] = ui;
}
for i in 0..d - 1 {
let cop = self.pair_copulas[0][i];
log_c += cop.log_density(v[i][0], v[i + 1][0]);
v[i][1] = cop.h(v[i][0], v[i + 1][0]);
v[i + 1][1] = cop.h(v[i + 1][0], v[i][0]);
}
for m in 1..d - 1 {
for i in 0..d - 1 - m {
let cop = self.pair_copulas[m][i];
log_c += cop.log_density(v[i][2 * m - 1], v[i + m + 1][2 * (m - 1)]);
if m + 1 < d - 1 {
v[i][2 * m + 1] = cop.h(v[i][2 * m - 1], v[i + m + 1][2 * (m - 1)]);
v[i + m + 1][2 * m + 1] = cop.h(v[i + m + 1][2 * (m - 1)], v[i][2 * m - 1]);
}
}
}
log_c
}
}
impl MultivariateExt for DVine {
fn r#type(&self) -> CopulaType {
CopulaType::DVine
}
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(
"DVine::fit not implemented — supply the tree explicitly via DVine::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, D-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 D-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 dvine_independence_three_dim_uniform_marginals() {
let dv = DVine::independence(3).unwrap();
let s = dv.sample(10_000).unwrap();
assert_eq!(s.ncols(), 3);
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 = dv.log_pdf(array![[0.3, 0.6, 0.8]]).unwrap();
assert!(lp[0].abs() < 1e-12);
}
#[test]
fn dvine_two_dim_clayton_matches_bivariate() {
let tree = vec![vec![PairCopula::Clayton { theta: 2.0 }]];
let dv = DVine::new(2, tree).unwrap();
let s = dv.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) D-vine τ_(0,1) = {}, expected ~0.5",
tau[[0, 1]]
);
}
#[test]
fn dvine_three_dim_gaussian_chain() {
let t1 = vec![
PairCopula::Gaussian { rho: 0.6 },
PairCopula::Gaussian { rho: 0.6 },
];
let t2 = vec![PairCopula::Independence];
let dv = DVine::new(3, vec![t1, t2]).unwrap();
let s = dv.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[[1, 2]] - expected_tau).abs() < 0.03,
"τ_(1,2) = {} vs expected {expected_tau}",
tau[[1, 2]]
);
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);
}
}
#[test]
fn dvine_shape_validation() {
let bad1 = vec![vec![PairCopula::Independence]];
assert!(DVine::new(3, bad1).is_err());
let bad2 = vec![
vec![
PairCopula::Independence,
PairCopula::Independence,
PairCopula::Independence,
],
vec![PairCopula::Independence],
];
assert!(DVine::new(3, bad2).is_err());
let bad3 = vec![];
assert!(DVine::new(1, bad3).is_err());
}
#[test]
fn dvine_fit_rejects_with_descriptive_error() {
let mut dv = DVine::independence(3).unwrap();
let data = ndarray::Array2::<f64>::from_elem((10, 3), 0.5);
let res = dv.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}"
);
}
}