1use prospicio_core::{Error, Result, StreamRng};
9use prospicio_math::linalg::{cholesky, lower_mul, lower_solve};
10use prospicio_math::special::{ln_gamma, norm_cdf, norm_quantile, student_t_cdf};
11
12use crate::distribution::Distribution;
13use crate::gamma::standard_gamma;
14use crate::predictive::{ComponentKey, PredictiveDistribution};
15use crate::provenance::Provenance;
16
17pub trait Copula: Sync {
19 fn dim(&self) -> usize;
21
22 fn sample(&self, rng: &mut StreamRng, u: &mut [f64]);
25}
26
27#[derive(Debug, Clone, PartialEq)]
44pub struct GaussianCopula {
45 dim: usize,
46 chol: Vec<f64>,
47}
48
49impl GaussianCopula {
50 pub fn new(correlation: &[f64], dim: usize) -> Result<Self> {
53 Ok(Self {
54 dim,
55 chol: correlation_factor(correlation, dim)?,
56 })
57 }
58
59 pub fn factor(&self) -> &[f64] {
61 &self.chol
62 }
63}
64
65impl Copula for GaussianCopula {
66 fn dim(&self) -> usize {
67 self.dim
68 }
69
70 fn sample(&self, rng: &mut StreamRng, u: &mut [f64]) {
71 correlated_normals(&self.chol, rng, u);
72 for x in u.iter_mut() {
73 *x = open01(norm_cdf(*x));
74 }
75 }
76}
77
78#[derive(Debug, Clone, PartialEq)]
87pub struct StudentTCopula {
88 dim: usize,
89 chol: Vec<f64>,
90 nu: f64,
91}
92
93impl StudentTCopula {
94 pub fn new(correlation: &[f64], dim: usize, nu: f64) -> Result<Self> {
97 if !nu.is_finite() || nu <= 0.0 {
98 return Err(invalid("nu", nu, "must be finite and positive"));
99 }
100 Ok(Self {
101 dim,
102 chol: correlation_factor(correlation, dim)?,
103 nu,
104 })
105 }
106
107 pub fn nu(&self) -> f64 {
109 self.nu
110 }
111}
112
113impl Copula for StudentTCopula {
114 fn dim(&self) -> usize {
115 self.dim
116 }
117
118 fn sample(&self, rng: &mut StreamRng, u: &mut [f64]) {
119 correlated_normals(&self.chol, rng, u);
120 let w = 2.0 * standard_gamma(rng, 0.5 * self.nu);
121 let scale = (w / self.nu).sqrt();
122 for x in u.iter_mut() {
123 *x = open01(student_t_cdf(*x / scale, self.nu));
124 }
125 }
126}
127
128#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum Archimedean {
131 Clayton,
132 Gumbel,
133 Frank,
134 Joe,
135}
136
137#[derive(Debug, Clone, PartialEq)]
164pub struct ArchimedeanCopula {
165 family: Archimedean,
166 theta: f64,
167 dim: usize,
168}
169
170impl ArchimedeanCopula {
171 pub fn new(family: Archimedean, theta: f64, dim: usize) -> Result<Self> {
173 let ok = theta.is_finite()
174 && match family {
175 Archimedean::Clayton | Archimedean::Frank => theta > 0.0,
176 Archimedean::Gumbel | Archimedean::Joe => theta >= 1.0,
177 };
178 if !ok {
179 let reason = match family {
180 Archimedean::Clayton | Archimedean::Frank => "must be finite and positive",
181 Archimedean::Gumbel | Archimedean::Joe => "must be finite and at least 1",
182 };
183 return Err(invalid("theta", theta, reason));
184 }
185 if dim == 0 {
186 return Err(invalid("dim", 0.0, "must be positive"));
187 }
188 Ok(Self { family, theta, dim })
189 }
190
191 pub fn family(&self) -> Archimedean {
192 self.family
193 }
194
195 pub fn theta(&self) -> f64 {
196 self.theta
197 }
198
199 pub fn generator(&self, t: f64) -> f64 {
201 let th = self.theta;
202 match self.family {
203 Archimedean::Clayton => (-(t.ln_1p()) / th).exp(),
204 Archimedean::Gumbel => (-t.powf(1.0 / th)).exp(),
205 Archimedean::Frank => -((-t).exp() * (-th).exp_m1()).ln_1p() / th,
207 Archimedean::Joe => -((-(-t).exp_m1()).ln() / th).exp_m1(),
209 }
210 }
211
212 fn frailty(&self, rng: &mut StreamRng) -> f64 {
213 let th = self.theta;
214 match self.family {
215 Archimedean::Clayton => standard_gamma(rng, 1.0 / th),
216 Archimedean::Gumbel => positive_stable(rng, 1.0 / th),
217 Archimedean::Frank => logarithmic(rng, th),
218 Archimedean::Joe => sibuya(rng, 1.0 / th),
219 }
220 }
221}
222
223impl Copula for ArchimedeanCopula {
224 fn dim(&self) -> usize {
225 self.dim
226 }
227
228 fn sample(&self, rng: &mut StreamRng, u: &mut [f64]) {
229 let v = self.frailty(rng);
230 for x in u.iter_mut() {
231 let e = -rng.next_open01().ln();
232 *x = open01(self.generator(e / v));
233 }
234 }
235}
236
237pub fn simulate(
264 copula: &dyn Copula,
265 marginals: &[&(dyn Distribution + Sync)],
266 dims: Vec<String>,
267 components: Vec<ComponentKey>,
268 n_sims: usize,
269 seed: u64,
270 provenance: Provenance,
271) -> Result<PredictiveDistribution> {
272 if marginals.len() != copula.dim() {
273 return Err(invalid(
274 "marginals",
275 marginals.len() as f64,
276 "must have one marginal per copula dimension",
277 ));
278 }
279 let parallel = marginals.iter().all(|m| m.is_parallel_safe());
280 PredictiveDistribution::simulate_with(
281 parallel,
282 dims,
283 components,
284 n_sims,
285 seed,
286 provenance,
287 |rng, row| {
288 copula.sample(rng, row);
289 for (x, m) in row.iter_mut().zip(marginals) {
290 *x = m.quantile(*x).expect("copula uniforms lie in (0, 1)");
291 }
292 },
293 )
294}
295
296pub(crate) fn target_ranks(
301 n: usize,
302 m: usize,
303 correlation: &[f64],
304 seed: u64,
305) -> Result<Vec<Vec<usize>>> {
306 let target = correlation_factor(correlation, m)?;
307 if n < m + 1 {
308 return Err(invalid(
309 "n_sims",
310 n as f64,
311 "must exceed the number of components",
312 ));
313 }
314
315 let scores: Vec<f64> = (1..=n)
317 .map(|i| norm_quantile(i as f64 / (n + 1) as f64))
318 .collect();
319 let mut cols: Vec<Vec<f64>> = (0..m)
320 .map(|j| {
321 let mut c = scores.clone();
322 if j > 0 {
323 shuffle(&mut c, &mut StreamRng::new(seed, j as u64));
324 }
325 c
326 })
327 .collect();
328
329 let actual = correlation_factor(&sample_correlation(&cols), m)
331 .map_err(|_| invalid("n_sims", n as f64, "too few to decorrelate the scores"))?;
332 let (mut row, mut y, mut t) = (vec![0.0; m], vec![0.0; m], vec![0.0; m]);
333 for i in 0..n {
334 for (r, col) in row.iter_mut().zip(&cols) {
335 *r = col[i];
336 }
337 lower_solve(&actual, &row, &mut y);
338 lower_mul(&target, &y, &mut t);
339 for (col, v) in cols.iter_mut().zip(&t) {
340 col[i] = *v;
341 }
342 }
343
344 Ok(cols
345 .iter()
346 .map(|col| {
347 let mut order: Vec<usize> = (0..n).collect();
348 order.sort_by(|&a, &b| col[a].total_cmp(&col[b]));
349 let mut rank = vec![0; n];
350 for (r, &i) in order.iter().enumerate() {
351 rank[i] = r;
352 }
353 rank
354 })
355 .collect())
356}
357
358pub fn iman_conover(
394 pd: &PredictiveDistribution,
395 correlation: &[f64],
396 seed: u64,
397) -> Result<PredictiveDistribution> {
398 let m = pd.n_components();
399 let n = pd.n_sims();
400 let ranks = target_ranks(n, m, correlation, seed)?;
401
402 let mut draws = vec![0.0; n * m];
404 for (j, rank) in ranks.iter().enumerate() {
405 let mut sorted: Vec<f64> = (0..n).map(|i| pd.row(i).expect("in range")[j]).collect();
406 sorted.sort_by(f64::total_cmp);
407 for (i, &r) in rank.iter().enumerate() {
408 draws[i * m + j] = sorted[r];
409 }
410 }
411 let provenance = pd
412 .provenance()
413 .clone()
414 .param("iman_conover_correlation", format!("{correlation:?}"))
415 .param("iman_conover_seed", seed);
416 PredictiveDistribution::from_draws(
417 pd.dims().to_vec(),
418 pd.components().to_vec(),
419 draws,
420 provenance,
421 )
422}
423
424fn sample_correlation(cols: &[Vec<f64>]) -> Vec<f64> {
426 let m = cols.len();
427 let n = cols[0].len() as f64;
428 let centred: Vec<Vec<f64>> = cols
429 .iter()
430 .map(|c| {
431 let mean = c.iter().sum::<f64>() / n;
432 c.iter().map(|x| x - mean).collect()
433 })
434 .collect();
435 let norms: Vec<f64> = centred
436 .iter()
437 .map(|c| c.iter().map(|x| x * x).sum::<f64>().sqrt())
438 .collect();
439 let mut r = vec![0.0; m * m];
440 for i in 0..m {
441 r[i * m + i] = 1.0;
442 for j in 0..i {
443 let dot: f64 = centred[i].iter().zip(¢red[j]).map(|(a, b)| a * b).sum();
444 let v = dot / (norms[i] * norms[j]);
445 r[i * m + j] = v;
446 r[j * m + i] = v;
447 }
448 }
449 r
450}
451
452fn open01(u: f64) -> f64 {
455 u.clamp(f64::MIN_POSITIVE, 1.0 - f64::EPSILON / 2.0)
456}
457
458fn positive_stable(rng: &mut StreamRng, alpha: f64) -> f64 {
462 if alpha == 1.0 {
463 return 1.0;
464 }
465 let theta = std::f64::consts::PI * rng.next_open01();
466 let w = -rng.next_open01().ln();
467 let a = (alpha * theta).sin() / theta.sin().powf(1.0 / alpha);
468 let b = (((1.0 - alpha) * theta).sin() / w).powf((1.0 - alpha) / alpha);
469 a * b
470}
471
472fn logarithmic(rng: &mut StreamRng, theta: f64) -> f64 {
475 let p = -(-theta).exp_m1();
476 let v = rng.next_open01();
477 let u = rng.next_open01();
478 if v > p {
479 return 1.0;
480 }
481 let q = -(-theta * u).exp_m1();
483 if v < q * q {
484 (1.0 + v.ln() / q.ln()).floor()
485 } else if v > q {
486 1.0
487 } else {
488 2.0
489 }
490}
491
492fn sibuya(rng: &mut StreamRng, alpha: f64) -> f64 {
496 let u = rng.next_open01();
497 if alpha == 1.0 || u <= alpha {
498 return 1.0;
499 }
500 let target = (1.0 - u).ln();
501 let ln_survival =
502 |k: f64| ln_gamma(k + 1.0 - alpha) - ln_gamma(k + 1.0) - ln_gamma(1.0 - alpha);
503 let (mut lo, mut hi) = (1.0, 2.0);
505 while ln_survival(hi) > target {
506 lo = hi;
507 hi *= 2.0;
508 if hi > 1e300 {
509 return hi;
510 }
511 }
512 while hi - lo > 1.0 {
513 let mid = (lo + (hi - lo) / 2.0).floor();
514 if ln_survival(mid) > target {
515 lo = mid;
516 } else {
517 hi = mid;
518 }
519 }
520 hi
521}
522
523fn shuffle(x: &mut [f64], rng: &mut StreamRng) {
525 for i in (1..x.len()).rev() {
526 let j = ((rng.next_open01() * (i + 1) as f64) as usize).min(i);
527 x.swap(i, j);
528 }
529}
530
531fn correlation_factor(r: &[f64], dim: usize) -> Result<Vec<f64>> {
533 if dim == 0 || r.len() != dim * dim {
534 return Err(invalid(
535 "correlation",
536 r.len() as f64,
537 "must be a non-empty dim × dim matrix",
538 ));
539 }
540 for i in 0..dim {
541 if r[i * dim + i] != 1.0 {
542 return Err(invalid("correlation", r[i * dim + i], "diagonal must be 1"));
543 }
544 for j in 0..i {
545 let (a, b) = (r[i * dim + j], r[j * dim + i]);
546 if a != b {
547 return Err(invalid("correlation", a, "must be symmetric"));
548 }
549 if !(-1.0..=1.0).contains(&a) {
550 return Err(invalid("correlation", a, "entries must be in [-1, 1]"));
551 }
552 }
553 }
554 cholesky(r, dim).ok_or_else(|| invalid("correlation", 0.0, "must be positive definite"))
555}
556
557fn correlated_normals(chol: &[f64], rng: &mut StreamRng, out: &mut [f64]) {
560 let z: Vec<f64> = (0..out.len())
561 .map(|_| norm_quantile(rng.next_open01()))
562 .collect();
563 lower_mul(chol, &z, out);
564}
565
566fn invalid(name: &'static str, value: f64, reason: &'static str) -> Error {
567 Error::InvalidParameter {
568 name,
569 value,
570 reason,
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use std::f64::consts::PI;
578
579 fn draws(c: &dyn Copula, n: usize, seed: u64) -> Vec<Vec<f64>> {
580 (0..n)
581 .map(|i| {
582 let mut u = vec![0.0; c.dim()];
583 c.sample(&mut StreamRng::new(seed, i as u64), &mut u);
584 u
585 })
586 .collect()
587 }
588
589 fn kendall_tau(x: &[f64], y: &[f64]) -> f64 {
590 let n = x.len();
591 let mut s = 0.0;
592 for i in 0..n {
593 for j in 0..i {
594 s += ((x[i] - x[j]) * (y[i] - y[j])).signum();
595 }
596 }
597 s / (n * (n - 1) / 2) as f64
598 }
599
600 fn ks_uniform(mut u: Vec<f64>) -> f64 {
602 u.sort_by(f64::total_cmp);
603 let n = u.len() as f64;
604 u.iter()
605 .enumerate()
606 .map(|(i, &x)| (x - i as f64 / n).max((i + 1) as f64 / n - x))
607 .fold(0.0, f64::max)
608 }
609
610 const R: [f64; 9] = [1.0, 0.6, -0.3, 0.6, 1.0, 0.1, -0.3, 0.1, 1.0];
611
612 #[test]
613 fn kendall_tau_matches_the_arcsine_law() {
614 let gauss = GaussianCopula::new(&R, 3).unwrap();
615 let t = StudentTCopula::new(&R, 3, 4.0).unwrap();
616 let t_half = StudentTCopula::new(&R, 3, 0.7).unwrap();
617 let n = 4_000;
618 for c in [&gauss as &dyn Copula, &t, &t_half] {
619 let u = draws(c, n, 9);
620 for (i, j) in [(0, 1), (0, 2), (1, 2)] {
621 let x: Vec<f64> = u.iter().map(|r| r[i]).collect();
622 let y: Vec<f64> = u.iter().map(|r| r[j]).collect();
623 let want = 2.0 / PI * R[i * 3 + j].asin();
624 let got = kendall_tau(&x, &y);
626 assert!((got - want).abs() < 0.04, "({i}, {j}): {got} vs {want}");
627 }
628 }
629 }
630
631 #[test]
632 fn margins_are_uniform() {
633 let n = 50_000;
634 for c in [
635 &GaussianCopula::new(&R, 3).unwrap() as &dyn Copula,
636 &StudentTCopula::new(&R, 3, 3.0).unwrap(),
637 &StudentTCopula::new(&R, 3, 0.7).unwrap(),
638 ] {
639 let u = draws(c, n, 2);
640 for j in 0..3 {
641 let d = ks_uniform(u.iter().map(|r| r[j]).collect());
642 assert!(d < 1.95 / (n as f64).sqrt(), "dimension {j}: {d}");
644 assert!(u.iter().all(|r| r[j] > 0.0 && r[j] < 1.0));
645 }
646 }
647 }
648
649 #[test]
650 fn t_copula_has_joint_extremes() {
651 let r = [1.0, 0.5, 0.5, 1.0];
652 let n = 200_000;
653 let q = 0.995;
654 let joint = |c: &dyn Copula| {
655 draws(c, n, 4)
656 .iter()
657 .filter(|u| u[0] > q && u[1] > q)
658 .count() as f64
659 / (n as f64 * (1.0 - q))
660 };
661 let gauss = joint(&GaussianCopula::new(&r, 2).unwrap());
662 let t = joint(&StudentTCopula::new(&r, 2, 3.0).unwrap());
663 let lambda = 2.0 * student_t_cdf(-(4.0f64 * 0.5 / 1.5).sqrt(), 4.0);
665 assert!((lambda - 0.3125).abs() < 1e-3, "{lambda}");
666 assert!(t > 1.5 * gauss, "t {t} vs Gaussian {gauss}");
667 assert!((t - lambda).abs() < 0.1, "{t} vs {lambda}");
668 }
669
670 #[test]
671 fn gamma_moments() {
672 for shape in [0.35, 1.0, 2.5, 40.0] {
673 let n = 100_000;
674 let mut rng = StreamRng::new(3, 0);
675 let x: Vec<f64> = (0..n).map(|_| standard_gamma(&mut rng, shape)).collect();
676 let mean = x.iter().sum::<f64>() / n as f64;
677 let var = x.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n as f64;
678 let se = (shape / n as f64).sqrt();
679 assert!(
680 (mean - shape).abs() < 4.0 * se,
681 "shape {shape}: mean {mean}"
682 );
683 assert!((var / shape - 1.0).abs() < 0.05, "shape {shape}: var {var}");
684 }
685 }
686
687 #[test]
688 fn rejects_bad_correlations() {
689 assert!(GaussianCopula::new(&[1.0, 0.5, 0.4, 1.0], 2).is_err());
690 assert!(GaussianCopula::new(&[1.0, 1.5, 1.5, 1.0], 2).is_err());
691 assert!(GaussianCopula::new(&[2.0, 0.0, 0.0, 1.0], 2).is_err());
692 assert!(GaussianCopula::new(&[1.0, 0.0, 0.0], 2).is_err());
693 let r = [1.0, 0.9, -0.9, 0.9, 1.0, 0.9, -0.9, 0.9, 1.0];
695 assert!(GaussianCopula::new(&r, 3).is_err());
696 assert!(StudentTCopula::new(&[1.0], 1, 0.0).is_err());
697 }
698
699 #[test]
700 fn simulate_is_reproducible_and_checks_dimensions() {
701 use crate::{KeyValue, Lognormal};
702 let a = Lognormal::from_mean_cv(10.0, 0.5).unwrap();
703 let b = Lognormal::from_mean_cv(20.0, 0.5).unwrap();
704 let c = StudentTCopula::new(&[1.0, 0.3, 0.3, 1.0], 2, 5.0).unwrap();
705 let keys = || vec![vec![KeyValue::Int(0)], vec![KeyValue::Int(1)]];
706 let run = || {
707 simulate(
708 &c,
709 &[&a, &b],
710 vec!["lob".into()],
711 keys(),
712 500,
713 8,
714 Provenance::new("t"),
715 )
716 .unwrap()
717 };
718 let (x, y) = (run(), run());
719 assert_eq!(x.draw_matrix(), y.draw_matrix());
720 let mut u = [0.0; 2];
722 c.sample(&mut StreamRng::new(8, 17), &mut u);
723 assert_eq!(
724 x.row(17).unwrap(),
725 [a.quantile(u[0]).unwrap(), b.quantile(u[1]).unwrap()]
726 );
727 assert!(
728 simulate(
729 &c,
730 &[&a],
731 vec!["lob".into()],
732 keys(),
733 10,
734 1,
735 Provenance::new("t")
736 )
737 .is_err()
738 );
739 }
740
741 fn rank_correlation(x: &[f64], y: &[f64], f: fn(f64) -> f64) -> f64 {
743 let n = x.len();
744 let scores = |v: &[f64]| {
745 let mut order: Vec<usize> = (0..n).collect();
746 order.sort_by(|&a, &b| v[a].total_cmp(&v[b]));
747 let mut r = vec![0.0; n];
748 for (k, &i) in order.iter().enumerate() {
749 r[i] = f((k + 1) as f64 / (n + 1) as f64);
750 }
751 r
752 };
753 sample_correlation(&[scores(x), scores(y)])[1]
754 }
755
756 #[test]
757 fn iman_conover_keeps_marginals_and_reaches_the_target() {
758 use crate::{Empirical, KeyValue, Lognormal};
759 let a = Lognormal::from_mean_cv(100.0, 0.3).unwrap();
760 let b = Lognormal::from_mean_cv(50.0, 1.5).unwrap();
761 let c = Lognormal::from_mean_cv(10.0, 0.8).unwrap();
762 let independent =
763 GaussianCopula::new(&[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0], 3).unwrap();
764 let keys: Vec<ComponentKey> = (0..3).map(|j| vec![KeyValue::Int(j)]).collect();
765 let n = 10_000;
766 let pd = simulate(
767 &independent,
768 &[&a, &b, &c],
769 vec!["lob".into()],
770 keys.clone(),
771 n,
772 1,
773 Provenance::new("t"),
774 )
775 .unwrap();
776 let joined = iman_conover(&pd, &R, 7).unwrap();
777 for key in &keys {
778 assert_eq!(
779 joined.marginal(key).unwrap().sorted(),
780 pd.marginal(key).unwrap().sorted()
781 );
782 }
783 let col = |j: usize| -> Vec<f64> { (0..n).map(|i| joined.row(i).unwrap()[j]).collect() };
784 for (i, j) in [(0, 1), (0, 2), (1, 2)] {
785 let r = R[i * 3 + j];
786 let normal_scores = rank_correlation(&col(i), &col(j), norm_quantile);
787 assert!(
788 (normal_scores - r).abs() < 0.01,
789 "({i}, {j}): {normal_scores} vs {r}"
790 );
791 let spearman = rank_correlation(&col(i), &col(j), |u| u);
792 let want = 6.0 / PI * (r / 2.0).asin();
793 assert!(
794 (spearman - want).abs() < 0.01,
795 "({i}, {j}): {spearman} vs {want}"
796 );
797 }
798 assert_eq!(
800 iman_conover(&pd, &R, 7).unwrap().draw_matrix(),
801 joined.draw_matrix()
802 );
803 assert!(
804 joined
805 .provenance()
806 .parameters
807 .iter()
808 .any(|(k, _)| k == "iman_conover_seed")
809 );
810 assert!(iman_conover(&pd, &[1.0, 0.5, 0.5, 1.0], 7).is_err());
812 }
813
814 fn frank_tau(theta: f64) -> f64 {
815 let n = 20_000;
817 let h = theta / n as f64;
818 let f = |t: f64| if t == 0.0 { 1.0 } else { t / t.exp_m1() };
819 let mut sum = f(0.0) + f(theta);
820 for i in 1..n {
821 sum += f(i as f64 * h) * if i % 2 == 1 { 4.0 } else { 2.0 };
822 }
823 let d1 = sum * h / 3.0 / theta;
824 1.0 + 4.0 * (d1 - 1.0) / theta
825 }
826
827 fn joe_tau(theta: f64) -> f64 {
828 let s: f64 = (1..2_000_000)
829 .map(|k| {
830 let k = k as f64;
831 1.0 / (k * (theta * k + 2.0) * (theta * (k - 1.0) + 2.0))
832 })
833 .sum();
834 1.0 - 4.0 * s
835 }
836
837 #[test]
838 fn archimedean_kendall_tau() {
839 use Archimedean::*;
840 let n = 3_000;
841 for (family, theta, want) in [
842 (Clayton, 2.0, 0.5),
843 (Clayton, 0.3, 0.3 / 2.3),
844 (Gumbel, 1.0, 0.0),
845 (Gumbel, 2.5, 0.6),
846 (Frank, 5.0, frank_tau(5.0)),
847 (Frank, 0.5, frank_tau(0.5)),
848 (Joe, 2.0, joe_tau(2.0)),
849 (Joe, 6.0, joe_tau(6.0)),
850 ] {
851 let c = ArchimedeanCopula::new(family, theta, 3).unwrap();
852 let u = draws(&c, n, 21);
853 for (i, j) in [(0, 1), (1, 2)] {
854 let x: Vec<f64> = u.iter().map(|r| r[i]).collect();
855 let y: Vec<f64> = u.iter().map(|r| r[j]).collect();
856 let got = kendall_tau(&x, &y);
857 assert!(
858 (got - want).abs() < 0.04,
859 "{family:?}({theta}) ({i}, {j}): {got} vs {want}"
860 );
861 }
862 }
863 assert!((frank_tau(5.0) - 0.4567).abs() < 1e-3);
865 }
866
867 #[test]
868 fn archimedean_margins_are_uniform() {
869 use Archimedean::*;
870 let n = 30_000;
871 for (family, theta) in [(Clayton, 1.5), (Gumbel, 3.0), (Frank, 8.0), (Joe, 4.0)] {
872 let c = ArchimedeanCopula::new(family, theta, 2).unwrap();
873 let u = draws(&c, n, 6);
874 for j in 0..2 {
875 let d = ks_uniform(u.iter().map(|r| r[j]).collect());
876 assert!(
877 d < 1.95 / (n as f64).sqrt(),
878 "{family:?} dimension {j}: {d}"
879 );
880 }
881 }
882 }
883
884 #[test]
885 fn archimedean_tails() {
886 use Archimedean::*;
887 let n = 200_000;
888 let q = 0.995;
889 let upper = |c: &dyn Copula| {
890 draws(c, n, 13)
891 .iter()
892 .filter(|u| u[0] > q && u[1] > q)
893 .count() as f64
894 / (n as f64 * (1.0 - q))
895 };
896 let lower = |c: &dyn Copula| {
897 draws(c, n, 13)
898 .iter()
899 .filter(|u| u[0] < 1.0 - q && u[1] < 1.0 - q)
900 .count() as f64
901 / (n as f64 * (1.0 - q))
902 };
903 let clayton = ArchimedeanCopula::new(Clayton, 2.0, 2).unwrap();
904 let gumbel = ArchimedeanCopula::new(Gumbel, 2.0, 2).unwrap();
905 assert!((lower(&clayton) - 0.5f64.sqrt()).abs() < 0.08);
907 assert!(upper(&clayton) < 0.2);
908 assert!((upper(&gumbel) - (2.0 - 2f64.sqrt())).abs() < 0.08);
910 assert!(lower(&gumbel) < 0.2);
911 }
912
913 #[test]
914 fn frailty_samplers() {
915 let mut rng = StreamRng::new(17, 0);
916 let n = 200_000;
917 let theta: f64 = 3.0;
919 let p = 1.0 - (-theta).exp();
920 let mean = (0..n).map(|_| logarithmic(&mut rng, theta)).sum::<f64>() / n as f64;
921 let want = p / ((1.0 - p) * theta);
922 assert!((mean / want - 1.0).abs() < 0.02, "{mean} vs {want}");
923 let alpha = 0.4;
925 let v: Vec<f64> = (0..n).map(|_| sibuya(&mut rng, alpha)).collect();
926 let share = |k: f64| v.iter().filter(|&&x| x == k).count() as f64 / n as f64;
927 assert!((share(1.0) - alpha).abs() < 0.005);
928 assert!((share(2.0) - alpha * (1.0 - alpha) / 2.0).abs() < 0.005);
929 assert!(v.iter().all(|&x| x >= 1.0 && x.fract() == 0.0));
930 for alpha in [0.3, 0.7] {
932 let m = (0..n)
933 .map(|_| (-positive_stable(&mut rng, alpha)).exp())
934 .sum::<f64>()
935 / n as f64;
936 assert!((m - (-1f64).exp()).abs() < 0.005, "alpha {alpha}: {m}");
937 }
938 }
939
940 #[test]
941 fn archimedean_rejects_bad_parameters() {
942 use Archimedean::*;
943 assert!(ArchimedeanCopula::new(Clayton, 0.0, 2).is_err());
944 assert!(ArchimedeanCopula::new(Gumbel, 0.9, 2).is_err());
945 assert!(ArchimedeanCopula::new(Frank, -1.0, 2).is_err());
946 assert!(ArchimedeanCopula::new(Joe, f64::INFINITY, 2).is_err());
947 assert!(ArchimedeanCopula::new(Clayton, 1.0, 0).is_err());
948 let g = ArchimedeanCopula::new(Gumbel, 2.0, 2).unwrap();
949 assert_eq!(g.generator(0.0), 1.0);
950 assert!(g.generator(1e6) < 1e-300);
951 }
952}