1use crate::archimedean::{AMHCopula, ClaytonCopula, FrankCopula, GumbelCopula, JoeCopula};
42use crate::elliptical::{GaussianCopula, StudentTCopula};
43use crate::{Copula, CopulaError, Result};
44use nalgebra::DMatrix;
45use rand::{Rng, RngExt};
46use statrs::distribution::{ContinuousCDF, Normal, StudentsT};
47
48const BOUNDARY_EPS: f64 = 1e-12;
51
52fn interior(p: f64) -> f64 {
53 p.clamp(BOUNDARY_EPS, 1.0 - BOUNDARY_EPS)
54}
55
56#[derive(Clone)]
61pub enum CopulaType {
62 Clayton(ClaytonCopula),
64 Gumbel(GumbelCopula),
66 Frank(FrankCopula),
68 Joe(JoeCopula),
70 AMH(AMHCopula),
72 Gaussian(GaussianCopula),
74 StudentT(StudentTCopula),
76}
77
78impl CopulaType {
79 fn dimension(&self) -> usize {
80 match self {
81 CopulaType::Clayton(c) => c.dimension(),
82 CopulaType::Gumbel(c) => c.dimension(),
83 CopulaType::Frank(c) => c.dimension(),
84 CopulaType::Joe(c) => c.dimension(),
85 CopulaType::AMH(c) => c.dimension(),
86 CopulaType::Gaussian(c) => c.dimension(),
87 CopulaType::StudentT(c) => c.dimension(),
88 }
89 }
90
91 fn cdf(&self, u: &[f64]) -> Result<f64> {
93 match self {
94 CopulaType::Clayton(c) => c.cdf(u),
95 CopulaType::Gumbel(c) => c.cdf(u),
96 CopulaType::Frank(c) => c.cdf(u),
97 CopulaType::Joe(c) => c.cdf(u),
98 CopulaType::AMH(c) => c.cdf(u),
99 CopulaType::Gaussian(c) => c.cdf(u),
100 CopulaType::StudentT(c) => c.cdf(u),
101 }
102 }
103}
104
105#[derive(Clone)]
109pub struct PairCopula {
110 copula: CopulaType,
112 var1: usize,
114 var2: usize,
116 conditioning_set: Vec<usize>,
118}
119
120impl PairCopula {
121 pub fn new(copula: CopulaType, var1: usize, var2: usize, conditioning_set: Vec<usize>) -> Self {
123 Self {
124 copula,
125 var1,
126 var2,
127 conditioning_set,
128 }
129 }
130
131 pub fn var1(&self) -> usize {
133 self.var1
134 }
135
136 pub fn var2(&self) -> usize {
138 self.var2
139 }
140
141 pub fn conditioning_set(&self) -> &[usize] {
143 &self.conditioning_set
144 }
145
146 fn h_function(&self, u: f64, v: f64) -> Result<f64> {
148 match &self.copula {
149 CopulaType::Gaussian(c) => gaussian_h(u, v, c.correlation()[(0, 1)]),
150 CopulaType::StudentT(c) => student_t_h(u, v, c.correlation()[(0, 1)], c.df()),
151 _ => self.numerical_h(u, v),
152 }
153 }
154
155 fn h_inv(&self, w: f64, v: f64) -> Result<f64> {
158 match &self.copula {
159 CopulaType::Gaussian(c) => gaussian_h_inv(w, v, c.correlation()[(0, 1)]),
160 CopulaType::StudentT(c) => student_t_h_inv(w, v, c.correlation()[(0, 1)], c.df()),
161 _ => self.bisect_h_inv(w, v),
162 }
163 }
164
165 fn numerical_h(&self, u: f64, v: f64) -> Result<f64> {
170 const STEP: f64 = 1e-6;
171 let u = u.clamp(0.0, 1.0);
172 let cdf = |t: f64| -> Result<f64> {
173 if u == 0.0 || t == 0.0 {
174 Ok(0.0)
175 } else if t == 1.0 {
176 Ok(u)
177 } else if u == 1.0 {
178 Ok(t)
179 } else {
180 self.copula.cdf(&[u, t])
181 }
182 };
183 let lo = (v - STEP).max(0.0);
184 let hi = (v + STEP).min(1.0);
185 Ok(((cdf(hi)? - cdf(lo)?) / (hi - lo)).clamp(0.0, 1.0))
186 }
187
188 fn bisect_h_inv(&self, w: f64, v: f64) -> Result<f64> {
189 let mut lo = 1e-10;
190 let mut hi = 1.0 - 1e-10;
191
192 for _ in 0..50 {
193 let mid = (lo + hi) / 2.0;
194 let h_val = self.numerical_h(mid, v)?;
195
196 if (h_val - w).abs() < 1e-10 {
197 return Ok(mid);
198 }
199
200 if h_val < w {
201 lo = mid;
202 } else {
203 hi = mid;
204 }
205 }
206
207 Ok((lo + hi) / 2.0)
208 }
209}
210
211fn standard_normal() -> Result<Normal> {
212 Normal::new(0.0, 1.0).map_err(|_| CopulaError::computation("failed to create Normal(0,1)"))
213}
214
215fn standard_t(df: f64) -> Result<StudentsT> {
216 StudentsT::new(0.0, 1.0, df)
217 .map_err(|_| CopulaError::computation("failed to create Student's t distribution"))
218}
219
220fn gaussian_h(u: f64, v: f64, rho: f64) -> Result<f64> {
222 let normal = standard_normal()?;
223 let x = normal.inverse_cdf(interior(u));
224 let y = normal.inverse_cdf(interior(v));
225 Ok(normal.cdf((x - rho * y) / (1.0 - rho * rho).sqrt()))
226}
227
228fn gaussian_h_inv(w: f64, v: f64, rho: f64) -> Result<f64> {
230 let normal = standard_normal()?;
231 let x = normal.inverse_cdf(interior(w));
232 let y = normal.inverse_cdf(interior(v));
233 Ok(normal.cdf(x * (1.0 - rho * rho).sqrt() + rho * y))
234}
235
236fn student_t_h(u: f64, v: f64, rho: f64, df: f64) -> Result<f64> {
240 let t = standard_t(df)?;
241 let t_next = standard_t(df + 1.0)?;
242 let x = t.inverse_cdf(interior(u));
243 let y = t.inverse_cdf(interior(v));
244 let scale = ((df + y * y) * (1.0 - rho * rho) / (df + 1.0)).sqrt();
245 Ok(t_next.cdf((x - rho * y) / scale))
246}
247
248fn student_t_h_inv(w: f64, v: f64, rho: f64, df: f64) -> Result<f64> {
250 let t = standard_t(df)?;
251 let t_next = standard_t(df + 1.0)?;
252 let y = t.inverse_cdf(interior(v));
253 let scale = ((df + y * y) * (1.0 - rho * rho) / (df + 1.0)).sqrt();
254 Ok(t.cdf(t_next.inverse_cdf(interior(w)) * scale + rho * y))
255}
256
257fn validate_trees(kind: &str, dimension: usize, trees: &[Vec<PairCopula>]) -> Result<()> {
260 if dimension < 2 {
261 return Err(CopulaError::invalid_parameter(
262 "dimension must be >= 2 for vine copulas",
263 ));
264 }
265
266 if trees.len() != dimension - 1 {
267 return Err(CopulaError::invalid_parameter(format!(
268 "{} with dimension {} should have {} trees, got {}",
269 kind,
270 dimension,
271 dimension - 1,
272 trees.len()
273 )));
274 }
275
276 for (level, tree) in trees.iter().enumerate() {
277 let expected_pairs = dimension - level - 1;
278 if tree.len() != expected_pairs {
279 return Err(CopulaError::invalid_parameter(format!(
280 "Tree {} should have {} pair-copulas, got {}",
281 level + 1,
282 expected_pairs,
283 tree.len()
284 )));
285 }
286 for (edge, pair) in tree.iter().enumerate() {
287 let pair_dim = pair.copula.dimension();
288 if pair_dim != 2 {
289 return Err(CopulaError::invalid_parameter(format!(
290 "pair-copula {} of tree {} must be bivariate, got dimension {}",
291 edge + 1,
292 level + 1,
293 pair_dim
294 )));
295 }
296 }
297 }
298
299 Ok(())
300}
301
302#[derive(Clone)]
314pub struct CVineCopula {
315 dimension: usize,
316 trees: Vec<Vec<PairCopula>>,
319}
320
321impl CVineCopula {
322 pub fn new(dimension: usize, trees: Vec<Vec<PairCopula>>) -> Result<Self> {
331 validate_trees("C-vine", dimension, &trees)?;
332 Ok(Self { dimension, trees })
333 }
334}
335
336impl Copula for CVineCopula {
337 fn cdf(&self, u: &[f64]) -> Result<f64> {
338 if u.len() != self.dimension {
339 return Err(CopulaError::dimension_mismatch(self.dimension, u.len()));
340 }
341 crate::error::validate_unit_range(u)?;
342
343 Err(CopulaError::not_implemented(
345 "C-vine CDF requires specialized numerical methods",
346 ))
347 }
348
349 fn pdf(&self, u: &[f64]) -> Result<f64> {
350 if u.len() != self.dimension {
351 return Err(CopulaError::dimension_mismatch(self.dimension, u.len()));
352 }
353 crate::error::validate_unit_range(u)?;
354
355 Err(CopulaError::not_implemented(
358 "C-vine PDF computation not yet implemented",
359 ))
360 }
361
362 fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
371 let d = self.dimension;
372 let mut samples = DMatrix::<f64>::zeros(n, d);
373
374 for row in 0..n {
375 let w: Vec<f64> = (0..d).map(|_| rng.random::<f64>()).collect();
376
377 for i in 0..d {
378 let mut value = w[i];
379 for k in (0..i).rev() {
380 value = self.trees[k][i - k - 1].h_inv(value, w[k])?;
381 }
382 samples[(row, i)] = value;
383 }
384 }
385
386 Ok(samples)
387 }
388
389 fn dimension(&self) -> usize {
390 self.dimension
391 }
392}
393
394#[derive(Clone)]
406pub struct DVineCopula {
407 dimension: usize,
408 trees: Vec<Vec<PairCopula>>,
410}
411
412impl DVineCopula {
413 pub fn new(dimension: usize, trees: Vec<Vec<PairCopula>>) -> Result<Self> {
422 validate_trees("D-vine", dimension, &trees)?;
423 Ok(Self { dimension, trees })
424 }
425}
426
427impl Copula for DVineCopula {
428 fn cdf(&self, u: &[f64]) -> Result<f64> {
429 if u.len() != self.dimension {
430 return Err(CopulaError::dimension_mismatch(self.dimension, u.len()));
431 }
432 crate::error::validate_unit_range(u)?;
433
434 Err(CopulaError::not_implemented(
435 "D-vine CDF requires specialized numerical methods",
436 ))
437 }
438
439 fn pdf(&self, u: &[f64]) -> Result<f64> {
440 if u.len() != self.dimension {
441 return Err(CopulaError::dimension_mismatch(self.dimension, u.len()));
442 }
443 crate::error::validate_unit_range(u)?;
444
445 Err(CopulaError::not_implemented(
446 "D-vine PDF computation not yet implemented",
447 ))
448 }
449
450 fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
468 let d = self.dimension;
469 let mut samples = DMatrix::<f64>::zeros(n, d);
470 let mut fwd = vec![vec![0.0; d + 1]; d];
471 let mut bwd = vec![vec![0.0; d + 1]; d];
472
473 for row in 0..n {
474 let w: Vec<f64> = (0..d).map(|_| rng.random::<f64>()).collect();
475
476 for i in 0..d {
477 let mut value = w[i];
478 for k in (1..=i).rev() {
479 value = self.trees[k - 1][i - k].h_inv(value, bwd[i - k][k])?;
480 fwd[i][k] = value;
481 }
482 fwd[i][1] = value;
483 bwd[i][1] = value;
484 samples[(row, i)] = value;
485
486 for k in 1..=i {
487 bwd[i - k][k + 1] =
488 self.trees[k - 1][i - k].h_function(bwd[i - k][k], fwd[i][k])?;
489 }
490 }
491 }
492
493 Ok(samples)
494 }
495
496 fn dimension(&self) -> usize {
497 self.dimension
498 }
499}
500
501#[cfg(test)]
502mod tests {
503 use super::*;
504
505 #[test]
506 fn test_pair_copula_creation() {
507 let clayton = CopulaType::Clayton(ClaytonCopula::new(2.0).unwrap());
508 let pair = PairCopula::new(clayton, 0, 1, vec![]);
509
510 assert_eq!(pair.var1(), 0);
511 assert_eq!(pair.var2(), 1);
512 assert!(pair.conditioning_set().is_empty());
513 }
514
515 #[test]
516 fn test_cvine_creation() {
517 let c12 = CopulaType::Clayton(ClaytonCopula::new(2.0).unwrap());
519 let c13 = CopulaType::Clayton(ClaytonCopula::new(1.5).unwrap());
520 let c23_1 = CopulaType::Clayton(ClaytonCopula::new(1.0).unwrap());
521
522 let tree1 = vec![
523 PairCopula::new(c12, 0, 1, vec![]),
524 PairCopula::new(c13, 0, 2, vec![]),
525 ];
526
527 let tree2 = vec![PairCopula::new(c23_1, 1, 2, vec![0])];
528
529 let cvine = CVineCopula::new(3, vec![tree1, tree2]).unwrap();
530 assert_eq!(cvine.dimension(), 3);
531 }
532
533 #[test]
534 fn test_cvine_wrong_num_trees() {
535 let c12 = CopulaType::Clayton(ClaytonCopula::new(2.0).unwrap());
536 let tree1 = vec![PairCopula::new(c12, 0, 1, vec![])];
537
538 let result = CVineCopula::new(3, vec![tree1]);
540 assert!(result.is_err());
541 }
542
543 #[test]
544 fn test_cvine_wrong_num_pairs_in_tree() {
545 let clayton = || CopulaType::Clayton(ClaytonCopula::new(2.0).unwrap());
546 let tree1 = vec![PairCopula::new(clayton(), 0, 1, vec![])];
548 let tree2 = vec![PairCopula::new(clayton(), 1, 2, vec![0])];
549 assert!(CVineCopula::new(3, vec![tree1, tree2]).is_err());
550 }
551
552 #[test]
553 fn test_dvine_wrong_num_trees() {
554 let c12 = CopulaType::Clayton(ClaytonCopula::new(2.0).unwrap());
555 let tree1 = vec![PairCopula::new(c12, 0, 1, vec![])];
556 assert!(DVineCopula::new(3, vec![tree1]).is_err());
557 }
558
559 #[test]
560 fn test_dvine_wrong_num_pairs_in_tree() {
561 let clayton = || CopulaType::Clayton(ClaytonCopula::new(2.0).unwrap());
562 let tree1 = vec![PairCopula::new(clayton(), 0, 1, vec![])];
564 let tree2 = vec![PairCopula::new(clayton(), 0, 2, vec![1])];
565 assert!(DVineCopula::new(3, vec![tree1, tree2]).is_err());
566 }
567
568 #[test]
569 fn test_dvine_creation() {
570 let c12 = CopulaType::Clayton(ClaytonCopula::new(2.0).unwrap());
572 let c23 = CopulaType::Clayton(ClaytonCopula::new(1.5).unwrap());
573 let c13_2 = CopulaType::Clayton(ClaytonCopula::new(1.0).unwrap());
574
575 let tree1 = vec![
576 PairCopula::new(c12, 0, 1, vec![]),
577 PairCopula::new(c23, 1, 2, vec![]),
578 ];
579
580 let tree2 = vec![PairCopula::new(c13_2, 0, 2, vec![1])];
581
582 let dvine = DVineCopula::new(3, vec![tree1, tree2]).unwrap();
583 assert_eq!(dvine.dimension(), 3);
584 }
585
586 fn gaussian_pair(rho: f64) -> PairCopula {
587 let corr = DMatrix::from_row_slice(2, 2, &[1.0, rho, rho, 1.0]);
588 PairCopula::new(
589 CopulaType::Gaussian(GaussianCopula::new(corr).unwrap()),
590 0,
591 0,
592 vec![],
593 )
594 }
595
596 fn student_t_pair(rho: f64, df: f64) -> PairCopula {
597 let corr = DMatrix::from_row_slice(2, 2, &[1.0, rho, rho, 1.0]);
598 PairCopula::new(
599 CopulaType::StudentT(StudentTCopula::new(corr, df).unwrap()),
600 0,
601 0,
602 vec![],
603 )
604 }
605
606 fn clayton_pair(theta: f64) -> PairCopula {
607 PairCopula::new(
608 CopulaType::Clayton(ClaytonCopula::new(theta).unwrap()),
609 0,
610 0,
611 vec![],
612 )
613 }
614
615 fn unpartial(r_ij_given_k: f64, r_ik: f64, r_jk: f64) -> f64 {
617 r_ij_given_k * ((1.0 - r_ik * r_ik) * (1.0 - r_jk * r_jk)).sqrt() + r_ik * r_jk
618 }
619
620 fn partial(r_ij: f64, r_ik: f64, r_jk: f64) -> f64 {
622 (r_ij - r_ik * r_jk) / ((1.0 - r_ik * r_ik) * (1.0 - r_jk * r_jk)).sqrt()
623 }
624
625 fn normal_score_correlation(samples: &DMatrix<f64>) -> DMatrix<f64> {
627 let normal = Normal::new(0.0, 1.0).unwrap();
628 let (n, d) = samples.shape();
629 let z = DMatrix::from_fn(n, d, |i, j| normal.inverse_cdf(samples[(i, j)]));
630 let centered = DMatrix::from_fn(n, d, |i, j| z[(i, j)] - z.column(j).mean());
631 let cov = centered.transpose() * ¢ered;
632 DMatrix::from_fn(d, d, |a, b| {
633 cov[(a, b)] / (cov[(a, a)] * cov[(b, b)]).sqrt()
634 })
635 }
636
637 fn assert_correlations(samples: &DMatrix<f64>, expected: &[[f64; 4]; 4], tol: f64) {
638 let actual = normal_score_correlation(samples);
639 for a in 0..4 {
640 for b in 0..4 {
641 assert!(
642 (actual[(a, b)] - expected[a][b]).abs() < tol,
643 "corr({a},{b}) = {:.4}, expected {:.4}",
644 actual[(a, b)],
645 expected[a][b]
646 );
647 }
648 }
649 }
650
651 fn column_tau(samples: &DMatrix<f64>, a: usize, b: usize) -> f64 {
652 let x: Vec<f64> = samples.column(a).iter().copied().collect();
653 let y: Vec<f64> = samples.column(b).iter().copied().collect();
654 crate::utils::kendall_tau(&x, &y).unwrap()
655 }
656
657 #[test]
663 fn gaussian_cvine_samples_match_implied_correlations() {
664 use rand::{rngs::StdRng, SeedableRng};
665 let (r01, r02, r03) = (0.6, 0.4, -0.3);
666 let (r12_0, r13_0) = (0.5, 0.2);
667 let r23_01 = -0.4;
668 let r12 = unpartial(r12_0, r01, r02);
669 let r13 = unpartial(r13_0, r01, r03);
670 let r23 = unpartial(unpartial(r23_01, r12_0, r13_0), r02, r03);
671 let expected = [
672 [1.0, r01, r02, r03],
673 [r01, 1.0, r12, r13],
674 [r02, r12, 1.0, r23],
675 [r03, r13, r23, 1.0],
676 ];
677
678 let vine = CVineCopula::new(
679 4,
680 vec![
681 vec![gaussian_pair(r01), gaussian_pair(r02), gaussian_pair(r03)],
682 vec![gaussian_pair(r12_0), gaussian_pair(r13_0)],
683 vec![gaussian_pair(r23_01)],
684 ],
685 )
686 .unwrap();
687 let samples = vine.sample(5000, &mut StdRng::seed_from_u64(7)).unwrap();
688 assert_correlations(&samples, &expected, 0.05);
689 }
690
691 #[test]
692 fn gaussian_dvine_samples_match_implied_correlations() {
693 use rand::{rngs::StdRng, SeedableRng};
694 let (r01, r12, r23) = (0.6, 0.5, -0.3);
695 let (r02_1, r13_2) = (0.4, 0.3);
696 let r03_12 = 0.5;
697 let r02 = unpartial(r02_1, r01, r12);
698 let r13 = unpartial(r13_2, r12, r23);
699 let r03_1 = unpartial(r03_12, r02_1, partial(r23, r12, r13));
700 let r03 = unpartial(r03_1, r01, r13);
701 let expected = [
702 [1.0, r01, r02, r03],
703 [r01, 1.0, r12, r13],
704 [r02, r12, 1.0, r23],
705 [r03, r13, r23, 1.0],
706 ];
707
708 let vine = DVineCopula::new(
709 4,
710 vec![
711 vec![gaussian_pair(r01), gaussian_pair(r12), gaussian_pair(r23)],
712 vec![gaussian_pair(r02_1), gaussian_pair(r13_2)],
713 vec![gaussian_pair(r03_12)],
714 ],
715 )
716 .unwrap();
717 let samples = vine.sample(5000, &mut StdRng::seed_from_u64(7)).unwrap();
718 assert_correlations(&samples, &expected, 0.05);
719 }
720
721 #[test]
723 fn student_t_vine_first_tree_pairs_match_kendall_tau() {
724 use rand::{rngs::StdRng, SeedableRng};
725 let tau_of = |rho: f64| 2.0 / std::f64::consts::PI * rho.asin();
726
727 let dvine = DVineCopula::new(
728 3,
729 vec![
730 vec![student_t_pair(0.7, 3.0), student_t_pair(-0.4, 3.0)],
731 vec![student_t_pair(0.3, 4.0)],
732 ],
733 )
734 .unwrap();
735 let s = dvine.sample(2000, &mut StdRng::seed_from_u64(3)).unwrap();
736 assert!((column_tau(&s, 0, 1) - tau_of(0.7)).abs() < 0.05);
737 assert!((column_tau(&s, 1, 2) - tau_of(-0.4)).abs() < 0.05);
738
739 let cvine = CVineCopula::new(
740 3,
741 vec![
742 vec![student_t_pair(0.5, 5.0), student_t_pair(0.6, 5.0)],
743 vec![student_t_pair(-0.2, 6.0)],
744 ],
745 )
746 .unwrap();
747 let s = cvine.sample(2000, &mut StdRng::seed_from_u64(4)).unwrap();
748 assert!((column_tau(&s, 0, 1) - tau_of(0.5)).abs() < 0.05);
749 assert!((column_tau(&s, 0, 2) - tau_of(0.6)).abs() < 0.05);
750 }
751
752 #[test]
755 fn clayton_dvine_matches_first_tree_and_depends_on_second_tree() {
756 use rand::{rngs::StdRng, SeedableRng};
757 let sample_with_tree2 = |theta: f64| {
758 DVineCopula::new(
759 3,
760 vec![
761 vec![clayton_pair(2.0), clayton_pair(4.0)],
762 vec![clayton_pair(theta)],
763 ],
764 )
765 .unwrap()
766 .sample(2000, &mut StdRng::seed_from_u64(5))
767 .unwrap()
768 };
769
770 let weak = sample_with_tree2(0.5);
771 let strong = sample_with_tree2(6.0);
772 for s in [&weak, &strong] {
773 assert!((column_tau(s, 0, 1) - 0.5).abs() < 0.05);
774 assert!((column_tau(s, 1, 2) - 2.0 / 3.0).abs() < 0.05);
775 }
776 assert!(column_tau(&strong, 0, 2) - column_tau(&weak, 0, 2) > 0.1);
777 }
778
779 #[test]
780 fn gaussian_closed_form_h_matches_numerical_derivative() {
781 let pair = gaussian_pair(0.6);
784 for &u in &[0.1, 0.4, 0.8] {
785 for &v in &[0.2, 0.5, 0.9] {
786 let closed = pair.h_function(u, v).unwrap();
787 let numerical = pair.numerical_h(u, v).unwrap();
788 assert!(
789 (closed - numerical).abs() < 1e-6,
790 "h({u}|{v}): closed {closed}, numerical {numerical}"
791 );
792 }
793 }
794 }
795
796 #[test]
797 fn h_inverse_round_trips() {
798 let pairs = [
799 gaussian_pair(-0.5),
800 student_t_pair(0.4, 3.0),
801 clayton_pair(2.0),
802 ];
803 for pair in &pairs {
804 for &w in &[0.05, 0.3, 0.7, 0.95] {
805 for &v in &[0.1, 0.5, 0.9] {
806 let u = pair.h_inv(w, v).unwrap();
807 let back = pair.h_function(u, v).unwrap();
808 assert!((back - w).abs() < 1e-6, "h(h_inv({w}|{v})) = {back}");
809 }
810 }
811 }
812 }
813
814 #[test]
815 fn h_function_is_finite_on_the_unit_square_boundary() {
816 let pairs = [
817 gaussian_pair(0.5),
818 student_t_pair(0.5, 4.0),
819 clayton_pair(2.0),
820 PairCopula::new(
821 CopulaType::Gumbel(GumbelCopula::new(2.0).unwrap()),
822 0,
823 0,
824 vec![],
825 ),
826 PairCopula::new(
827 CopulaType::Frank(FrankCopula::new(3.0).unwrap()),
828 0,
829 0,
830 vec![],
831 ),
832 PairCopula::new(CopulaType::Joe(JoeCopula::new(2.0).unwrap()), 0, 0, vec![]),
833 PairCopula::new(CopulaType::AMH(AMHCopula::new(0.5).unwrap()), 0, 0, vec![]),
834 ];
835 let edges = [0.0, 1e-9, 0.5, 1.0 - 1e-9, 1.0];
836 for pair in &pairs {
837 for &u in &edges {
838 for &v in &edges {
839 let h = pair.h_function(u, v).unwrap();
840 assert!((0.0..=1.0).contains(&h), "h({u}|{v}) = {h}");
841 }
842 }
843 }
844 }
845
846 #[test]
847 fn vine_rejects_pair_copula_that_is_not_bivariate() {
848 let trivariate = PairCopula::new(
849 CopulaType::Gaussian(GaussianCopula::new_identity(3).unwrap()),
850 0,
851 1,
852 vec![],
853 );
854 assert!(CVineCopula::new(2, vec![vec![trivariate.clone()]]).is_err());
855 assert!(DVineCopula::new(2, vec![vec![trivariate]]).is_err());
856 }
857
858 #[test]
859 fn test_cvine_sample() {
860 let mut rng = rand::rng();
861
862 let c12 = CopulaType::Clayton(ClaytonCopula::new(2.0).unwrap());
864 let tree1 = vec![PairCopula::new(c12, 0, 1, vec![])];
865
866 let cvine = CVineCopula::new(2, vec![tree1]).unwrap();
867 let samples = cvine.sample(10, &mut rng).unwrap();
868
869 assert_eq!(samples.nrows(), 10);
870 assert_eq!(samples.ncols(), 2);
871
872 for i in 0..10 {
874 for j in 0..2 {
875 assert!(samples[(i, j)] >= 0.0 && samples[(i, j)] <= 1.0);
876 }
877 }
878 }
879}