1use crate::error::{StatsError, StatsResult};
23use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
24use std::f64::consts::PI;
25
26pub trait Copula: std::fmt::Debug {
32 fn cdf(&self, u: f64, v: f64) -> f64;
34
35 fn pdf(&self, u: f64, v: f64) -> f64;
37
38 fn log_pdf(&self, u: f64, v: f64) -> f64 {
40 let d = self.pdf(u, v);
41 if d > 0.0 {
42 d.ln()
43 } else {
44 f64::NEG_INFINITY
45 }
46 }
47
48 fn conditional_v_given_u(&self, u: f64, v: f64) -> f64;
50
51 fn conditional_u_given_v(&self, u: f64, v: f64) -> f64;
53
54 fn upper_tail_dependence(&self) -> f64;
56
57 fn lower_tail_dependence(&self) -> f64;
59
60 fn kendalls_tau(&self) -> f64;
62
63 fn family_name(&self) -> &str;
65
66 fn n_params(&self) -> usize;
68
69 fn params(&self) -> Vec<f64>;
71}
72
73fn norm_cdf(x: f64) -> f64 {
79 0.5 * (1.0 + erf_approx(x / std::f64::consts::SQRT_2))
80}
81
82fn erf_approx(x: f64) -> f64 {
84 let sign = if x >= 0.0 { 1.0 } else { -1.0 };
85 let x = x.abs();
86 let t = 1.0 / (1.0 + 0.3275911 * x);
87 let poly = t
88 * (0.254_829_592
89 + t * (-0.284_496_736
90 + t * (1.421_413_741 + t * (-1.453_152_027 + t * 1.061_405_429))));
91 sign * (1.0 - poly * (-x * x).exp())
92}
93
94fn norm_ppf(p: f64) -> f64 {
97 if p <= 0.0 {
98 return f64::NEG_INFINITY;
99 }
100 if p >= 1.0 {
101 return f64::INFINITY;
102 }
103 if (p - 0.5).abs() < 1e-15 {
104 return 0.0;
105 }
106 let a = [
108 -3.969_683_028_665_376e1,
109 2.209_460_984_245_205e2,
110 -2.759_285_104_469_687e2,
111 1.383_577_518_672_690e2,
112 -3.066_479_806_614_716e1,
113 2.506_628_277_459_239,
114 ];
115 let b = [
116 -5.447_609_879_822_406e1,
117 1.615_858_368_580_409e2,
118 -1.556_989_798_598_866e2,
119 6.680_131_188_771_972e1,
120 -1.328_068_155_288_572e1,
121 ];
122 let c = [
123 -7.784_894_002_430_293e-3,
124 -3.223_964_580_411_365e-1,
125 -2.400_758_277_161_838,
126 -2.549_732_539_343_734,
127 4.374_664_141_464_968,
128 2.938_163_982_698_783,
129 ];
130 let d = [
131 7.784_695_709_041_462e-3,
132 3.224_671_290_700_398e-1,
133 2.445_134_137_142_996,
134 3.754_408_661_907_416,
135 ];
136
137 let p_low = 0.02425;
138 let p_high = 1.0 - p_low;
139
140 if p < p_low {
141 let q = (-2.0 * p.ln()).sqrt();
142 (((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
143 / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
144 } else if p <= p_high {
145 let q = p - 0.5;
146 let r = q * q;
147 (((((a[0] * r + a[1]) * r + a[2]) * r + a[3]) * r + a[4]) * r + a[5]) * q
148 / (((((b[0] * r + b[1]) * r + b[2]) * r + b[3]) * r + b[4]) * r + 1.0)
149 } else {
150 let q = (-2.0 * (1.0 - p).ln()).sqrt();
151 -(((((c[0] * q + c[1]) * q + c[2]) * q + c[3]) * q + c[4]) * q + c[5])
152 / ((((d[0] * q + d[1]) * q + d[2]) * q + d[3]) * q + 1.0)
153 }
154}
155
156fn norm_pdf(x: f64) -> f64 {
158 let inv_sqrt_2pi = 1.0 / (2.0 * PI).sqrt();
159 inv_sqrt_2pi * (-0.5 * x * x).exp()
160}
161
162fn student_t_cdf(x: f64, df: f64) -> f64 {
164 if df <= 0.0 {
165 return 0.5;
166 }
167 let t = df / (df + x * x);
170 let ib = regularized_incomplete_beta(t, df / 2.0, 0.5);
171 if x >= 0.0 {
172 1.0 - 0.5 * ib
173 } else {
174 0.5 * ib
175 }
176}
177
178fn regularized_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
180 if x <= 0.0 {
181 return 0.0;
182 }
183 if x >= 1.0 {
184 return 1.0;
185 }
186 if x > (a + 1.0) / (a + b + 2.0) {
188 return 1.0 - regularized_incomplete_beta(1.0 - x, b, a);
189 }
190 let lnbeta = ln_beta(a, b);
192 let front = (x.ln() * a + (1.0 - x).ln() * b - lnbeta).exp() / a;
193 let mut f = 1.0;
194 let mut c = 1.0;
195 let mut d = 1.0 - (a + b) * x / (a + 1.0);
196 if d.abs() < 1e-30 {
197 d = 1e-30;
198 }
199 d = 1.0 / d;
200 f = d;
201 for m in 1..200 {
202 let mf = m as f64;
203 let num_even = mf * (b - mf) * x / ((a + 2.0 * mf - 1.0) * (a + 2.0 * mf));
205 d = 1.0 + num_even * d;
206 if d.abs() < 1e-30 {
207 d = 1e-30;
208 }
209 c = 1.0 + num_even / c;
210 if c.abs() < 1e-30 {
211 c = 1e-30;
212 }
213 d = 1.0 / d;
214 f *= d * c;
215 let num_odd = -(a + mf) * (a + b + mf) * x / ((a + 2.0 * mf) * (a + 2.0 * mf + 1.0));
217 d = 1.0 + num_odd * d;
218 if d.abs() < 1e-30 {
219 d = 1e-30;
220 }
221 c = 1.0 + num_odd / c;
222 if c.abs() < 1e-30 {
223 c = 1e-30;
224 }
225 d = 1.0 / d;
226 let delta = d * c;
227 f *= delta;
228 if (delta - 1.0).abs() < 1e-12 {
229 break;
230 }
231 }
232 front * f
233}
234
235fn ln_beta(a: f64, b: f64) -> f64 {
237 ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
238}
239
240fn ln_gamma(x: f64) -> f64 {
242 if x <= 0.0 {
243 return f64::INFINITY;
244 }
245 let coef = [
247 0.999_999_999_999_809_93,
248 676.520_368_121_885_1,
249 -1_259.139_216_722_403,
250 771.323_428_777_653_1,
251 -176.615_029_162_140_6,
252 12.507_343_278_686_905,
253 -0.138_571_095_265_720_12,
254 9.984_369_578_019_572e-6,
255 1.505_632_735_149_311_6e-7,
256 ];
257 let g = 7.0;
258 let xx = x - 1.0;
259 let mut sum = coef[0];
260 for i in 1..9 {
261 sum += coef[i] / (xx + i as f64);
262 }
263 let t = xx + g + 0.5;
264 0.5 * (2.0 * PI).ln() + (xx + 0.5) * t.ln() - t + sum.ln()
265}
266
267fn student_t_ppf(p: f64, df: f64) -> f64 {
269 if p <= 0.0 {
270 return f64::NEG_INFINITY;
271 }
272 if p >= 1.0 {
273 return f64::INFINITY;
274 }
275 let mut x = norm_ppf(p);
277 for _ in 0..30 {
279 let cdf = student_t_cdf(x, df);
280 let pdf = student_t_pdf(x, df);
281 if pdf.abs() < 1e-30 {
282 break;
283 }
284 let delta = (cdf - p) / pdf;
285 x -= delta;
286 if delta.abs() < 1e-12 {
287 break;
288 }
289 }
290 x
291}
292
293fn student_t_pdf(x: f64, df: f64) -> f64 {
295 let half_df = df / 2.0;
296 let coef = (ln_gamma(half_df + 0.5) - ln_gamma(half_df) - 0.5 * (df * PI).ln()).exp();
297 coef * (1.0 + x * x / df).powf(-(df + 1.0) / 2.0)
298}
299
300#[derive(Debug, Clone)]
306pub struct GaussianCopula {
307 pub rho: f64,
309}
310
311impl GaussianCopula {
312 pub fn new(rho: f64) -> StatsResult<Self> {
314 if rho <= -1.0 || rho >= 1.0 {
315 return Err(StatsError::InvalidArgument(format!(
316 "rho must be in (-1, 1), got {}",
317 rho
318 )));
319 }
320 Ok(Self { rho })
321 }
322}
323
324impl Copula for GaussianCopula {
325 fn cdf(&self, u: f64, v: f64) -> f64 {
326 let u = u.max(1e-10).min(1.0 - 1e-10);
327 let v = v.max(1e-10).min(1.0 - 1e-10);
328 let x = norm_ppf(u);
329 let y = norm_ppf(v);
330 bivariate_normal_cdf(x, y, self.rho)
332 }
333
334 fn pdf(&self, u: f64, v: f64) -> f64 {
335 let u = u.max(1e-10).min(1.0 - 1e-10);
336 let v = v.max(1e-10).min(1.0 - 1e-10);
337 let x = norm_ppf(u);
338 let y = norm_ppf(v);
339 let r = self.rho;
340 let det = 1.0 - r * r;
341 if det <= 0.0 {
342 return 0.0;
343 }
344 let exponent = -(r * r * (x * x + y * y) - 2.0 * r * x * y) / (2.0 * det);
345 exponent.exp() / det.sqrt()
346 }
347
348 fn conditional_v_given_u(&self, u: f64, v: f64) -> f64 {
349 let u = u.max(1e-10).min(1.0 - 1e-10);
350 let v = v.max(1e-10).min(1.0 - 1e-10);
351 let x = norm_ppf(u);
352 let y = norm_ppf(v);
353 let r = self.rho;
354 let det = 1.0 - r * r;
355 if det <= 0.0 {
356 return v;
357 }
358 norm_cdf((y - r * x) / det.sqrt())
359 }
360
361 fn conditional_u_given_v(&self, u: f64, v: f64) -> f64 {
362 let u = u.max(1e-10).min(1.0 - 1e-10);
363 let v = v.max(1e-10).min(1.0 - 1e-10);
364 let x = norm_ppf(u);
365 let y = norm_ppf(v);
366 let r = self.rho;
367 let det = 1.0 - r * r;
368 if det <= 0.0 {
369 return u;
370 }
371 norm_cdf((x - r * y) / det.sqrt())
372 }
373
374 fn upper_tail_dependence(&self) -> f64 {
375 0.0 }
377
378 fn lower_tail_dependence(&self) -> f64 {
379 0.0
380 }
381
382 fn kendalls_tau(&self) -> f64 {
383 (2.0 / PI) * self.rho.asin()
384 }
385
386 fn family_name(&self) -> &str {
387 "Gaussian"
388 }
389
390 fn n_params(&self) -> usize {
391 1
392 }
393
394 fn params(&self) -> Vec<f64> {
395 vec![self.rho]
396 }
397}
398
399fn bivariate_normal_cdf(x: f64, y: f64, rho: f64) -> f64 {
401 if rho.abs() < 1e-10 {
402 return norm_cdf(x) * norm_cdf(y);
403 }
404 if rho.abs() > 0.9999 {
405 if rho > 0.0 {
406 return norm_cdf(x.min(y));
407 } else {
408 return (norm_cdf(x) + norm_cdf(-y) - 1.0).max(0.0);
409 }
410 }
411 let a = -(x * x + y * y - 2.0 * rho * x * y) / (2.0 * (1.0 - rho * rho));
413 let sign = if rho >= 0.0 { 1.0 } else { -1.0 };
414 let base = norm_cdf(x) * norm_cdf(y);
416 let correction = sign * norm_pdf(x) * norm_pdf(y) * rho / (1.0 - rho * rho).sqrt();
417 (base + correction * (1.0 - (-a).exp()).max(0.0).min(1.0))
418 .max(0.0)
419 .min(1.0)
420}
421
422#[derive(Debug, Clone)]
428pub struct StudentTCopula {
429 pub rho: f64,
431 pub df: f64,
433}
434
435impl StudentTCopula {
436 pub fn new(rho: f64, df: f64) -> StatsResult<Self> {
438 if rho <= -1.0 || rho >= 1.0 {
439 return Err(StatsError::InvalidArgument(format!(
440 "rho must be in (-1, 1), got {}",
441 rho
442 )));
443 }
444 if df <= 0.0 {
445 return Err(StatsError::InvalidArgument(format!(
446 "df must be positive, got {}",
447 df
448 )));
449 }
450 Ok(Self { rho, df })
451 }
452}
453
454impl Copula for StudentTCopula {
455 fn cdf(&self, u: f64, v: f64) -> f64 {
456 let u = u.max(1e-10).min(1.0 - 1e-10);
457 let v = v.max(1e-10).min(1.0 - 1e-10);
458 let x = student_t_ppf(u, self.df);
459 let y = student_t_ppf(v, self.df);
460 bivariate_normal_cdf(norm_cdf(x) * 2.0 - 1.0, norm_cdf(y) * 2.0 - 1.0, self.rho)
462 .max(0.0)
463 .min(1.0)
464 }
465
466 fn pdf(&self, u: f64, v: f64) -> f64 {
467 let u = u.max(1e-10).min(1.0 - 1e-10);
468 let v = v.max(1e-10).min(1.0 - 1e-10);
469 let x = student_t_ppf(u, self.df);
470 let y = student_t_ppf(v, self.df);
471 let r = self.rho;
472 let nu = self.df;
473 let det = 1.0 - r * r;
474 if det <= 0.0 {
475 return 0.0;
476 }
477 let q = (x * x - 2.0 * r * x * y + y * y) / det;
480 let biv_t = (ln_gamma((nu + 2.0) / 2.0)
481 - ln_gamma(nu / 2.0)
482 - (nu * PI).ln()
483 - 0.5 * det.ln()
484 - ((nu + 2.0) / 2.0) * (1.0 + q / nu).ln())
485 .exp();
486 let fx = student_t_pdf(x, nu);
488 let fy = student_t_pdf(y, nu);
489 let denom = fx * fy;
490 if denom < 1e-30 {
491 return 0.0;
492 }
493 biv_t / denom
494 }
495
496 fn conditional_v_given_u(&self, u: f64, v: f64) -> f64 {
497 let u = u.max(1e-10).min(1.0 - 1e-10);
498 let v = v.max(1e-10).min(1.0 - 1e-10);
499 let x = student_t_ppf(u, self.df);
500 let y = student_t_ppf(v, self.df);
501 let r = self.rho;
502 let nu = self.df;
503 let adj_df = nu + 1.0;
504 let scale = ((nu + x * x) * (1.0 - r * r) / adj_df).sqrt();
505 if scale < 1e-15 {
506 return v;
507 }
508 student_t_cdf((y - r * x) / scale, adj_df)
509 }
510
511 fn conditional_u_given_v(&self, u: f64, v: f64) -> f64 {
512 self.conditional_v_given_u(v, u)
514 }
515
516 fn upper_tail_dependence(&self) -> f64 {
517 let nu = self.df;
518 let r = self.rho;
519 let arg = -((nu + 1.0) * (1.0 - r) / (1.0 + r)).sqrt();
521 2.0 * student_t_cdf(arg, nu + 1.0)
522 }
523
524 fn lower_tail_dependence(&self) -> f64 {
525 self.upper_tail_dependence()
527 }
528
529 fn kendalls_tau(&self) -> f64 {
530 (2.0 / PI) * self.rho.asin()
531 }
532
533 fn family_name(&self) -> &str {
534 "Student-t"
535 }
536
537 fn n_params(&self) -> usize {
538 2
539 }
540
541 fn params(&self) -> Vec<f64> {
542 vec![self.rho, self.df]
543 }
544}
545
546#[derive(Debug, Clone)]
553pub struct ClaytonCopula {
554 pub theta: f64,
556}
557
558impl ClaytonCopula {
559 pub fn new(theta: f64) -> StatsResult<Self> {
561 if theta < -1.0 || (theta.abs() < 1e-15) {
562 return Err(StatsError::InvalidArgument(format!(
563 "Clayton theta must be > 0 (or in [-1, 0)), got {}",
564 theta
565 )));
566 }
567 Ok(Self { theta })
568 }
569}
570
571impl Copula for ClaytonCopula {
572 fn cdf(&self, u: f64, v: f64) -> f64 {
573 let u = u.max(1e-10).min(1.0 - 1e-10);
574 let v = v.max(1e-10).min(1.0 - 1e-10);
575 let th = self.theta;
576 let val = u.powf(-th) + v.powf(-th) - 1.0;
577 if val <= 0.0 {
578 return 0.0;
579 }
580 val.powf(-1.0 / th).max(0.0).min(1.0)
581 }
582
583 fn pdf(&self, u: f64, v: f64) -> f64 {
584 let u = u.max(1e-10).min(1.0 - 1e-10);
585 let v = v.max(1e-10).min(1.0 - 1e-10);
586 let th = self.theta;
587 let a = u.powf(-th) + v.powf(-th) - 1.0;
588 if a <= 0.0 {
589 return 0.0;
590 }
591 (1.0 + th) * (u * v).powf(-th - 1.0) * a.powf(-2.0 - 1.0 / th)
592 }
593
594 fn conditional_v_given_u(&self, u: f64, v: f64) -> f64 {
595 let u = u.max(1e-10).min(1.0 - 1e-10);
596 let v = v.max(1e-10).min(1.0 - 1e-10);
597 let th = self.theta;
598 let a = u.powf(-th) + v.powf(-th) - 1.0;
599 if a <= 0.0 {
600 return v;
601 }
602 u.powf(-th - 1.0) * a.powf(-1.0 - 1.0 / th)
603 }
604
605 fn conditional_u_given_v(&self, u: f64, v: f64) -> f64 {
606 let u = u.max(1e-10).min(1.0 - 1e-10);
608 let v = v.max(1e-10).min(1.0 - 1e-10);
609 let th = self.theta;
610 let a = u.powf(-th) + v.powf(-th) - 1.0;
611 if a <= 0.0 {
612 return u;
613 }
614 v.powf(-th - 1.0) * a.powf(-1.0 - 1.0 / th)
615 }
616
617 fn upper_tail_dependence(&self) -> f64 {
618 0.0 }
620
621 fn lower_tail_dependence(&self) -> f64 {
622 if self.theta > 0.0 {
623 2.0_f64.powf(-1.0 / self.theta)
624 } else {
625 0.0
626 }
627 }
628
629 fn kendalls_tau(&self) -> f64 {
630 self.theta / (self.theta + 2.0)
631 }
632
633 fn family_name(&self) -> &str {
634 "Clayton"
635 }
636
637 fn n_params(&self) -> usize {
638 1
639 }
640
641 fn params(&self) -> Vec<f64> {
642 vec![self.theta]
643 }
644}
645
646#[derive(Debug, Clone)]
653pub struct GumbelCopula {
654 pub theta: f64,
656}
657
658impl GumbelCopula {
659 pub fn new(theta: f64) -> StatsResult<Self> {
661 if theta < 1.0 {
662 return Err(StatsError::InvalidArgument(format!(
663 "Gumbel theta must be >= 1, got {}",
664 theta
665 )));
666 }
667 Ok(Self { theta })
668 }
669}
670
671impl Copula for GumbelCopula {
672 fn cdf(&self, u: f64, v: f64) -> f64 {
673 let u = u.max(1e-10).min(1.0 - 1e-10);
674 let v = v.max(1e-10).min(1.0 - 1e-10);
675 let th = self.theta;
676 let a = (-u.ln()).powf(th) + (-v.ln()).powf(th);
677 (-a.powf(1.0 / th)).exp()
678 }
679
680 fn pdf(&self, u: f64, v: f64) -> f64 {
681 let u = u.max(1e-10).min(1.0 - 1e-10);
682 let v = v.max(1e-10).min(1.0 - 1e-10);
683 let th = self.theta;
684 let lu = -u.ln();
685 let lv = -v.ln();
686 let lu_th = lu.powf(th);
687 let lv_th = lv.powf(th);
688 let a = lu_th + lv_th;
689 let c_val = (-a.powf(1.0 / th)).exp();
690 if c_val < 1e-30 {
691 return 0.0;
692 }
693 let a_inv = a.powf(1.0 / th);
694 let term1 = (lu * lv).powf(th - 1.0);
695 let term2 = a.powf(2.0 / th - 2.0);
696 let term3 = a_inv + th - 1.0;
697 c_val * term1 * term2 * term3 / (u * v)
698 }
699
700 fn conditional_v_given_u(&self, u: f64, v: f64) -> f64 {
701 let u = u.max(1e-10).min(1.0 - 1e-10);
702 let v = v.max(1e-10).min(1.0 - 1e-10);
703 let th = self.theta;
704 let lu = -u.ln();
705 let lv = -v.ln();
706 let a = lu.powf(th) + lv.powf(th);
707 let c_val = (-a.powf(1.0 / th)).exp();
708 c_val * lu.powf(th - 1.0) * a.powf(1.0 / th - 1.0) / u
709 }
710
711 fn conditional_u_given_v(&self, u: f64, v: f64) -> f64 {
712 let u = u.max(1e-10).min(1.0 - 1e-10);
713 let v = v.max(1e-10).min(1.0 - 1e-10);
714 let th = self.theta;
715 let lu = -u.ln();
716 let lv = -v.ln();
717 let a = lu.powf(th) + lv.powf(th);
718 let c_val = (-a.powf(1.0 / th)).exp();
719 c_val * lv.powf(th - 1.0) * a.powf(1.0 / th - 1.0) / v
720 }
721
722 fn upper_tail_dependence(&self) -> f64 {
723 2.0 - 2.0_f64.powf(1.0 / self.theta)
724 }
725
726 fn lower_tail_dependence(&self) -> f64 {
727 0.0 }
729
730 fn kendalls_tau(&self) -> f64 {
731 1.0 - 1.0 / self.theta
732 }
733
734 fn family_name(&self) -> &str {
735 "Gumbel"
736 }
737
738 fn n_params(&self) -> usize {
739 1
740 }
741
742 fn params(&self) -> Vec<f64> {
743 vec![self.theta]
744 }
745}
746
747#[derive(Debug, Clone)]
754pub struct FrankCopula {
755 pub theta: f64,
757}
758
759impl FrankCopula {
760 pub fn new(theta: f64) -> StatsResult<Self> {
762 if theta.abs() < 1e-15 {
763 return Err(StatsError::InvalidArgument(
764 "Frank theta must be != 0".into(),
765 ));
766 }
767 Ok(Self { theta })
768 }
769}
770
771impl Copula for FrankCopula {
772 fn cdf(&self, u: f64, v: f64) -> f64 {
773 let u = u.max(1e-10).min(1.0 - 1e-10);
774 let v = v.max(1e-10).min(1.0 - 1e-10);
775 let th = self.theta;
776 let num = ((-th * u).exp() - 1.0) * ((-th * v).exp() - 1.0);
777 let denom = (-th).exp() - 1.0;
778 if denom.abs() < 1e-30 {
779 return u * v;
780 }
781 -(1.0 + num / denom).max(1e-30).ln() / th
782 }
783
784 fn pdf(&self, u: f64, v: f64) -> f64 {
785 let u = u.max(1e-10).min(1.0 - 1e-10);
786 let v = v.max(1e-10).min(1.0 - 1e-10);
787 let th = self.theta;
788 let e_th = (-th).exp();
789 let e_u = (-th * u).exp();
790 let e_v = (-th * v).exp();
791 let num = -th * (e_th - 1.0) * (-th * (u + v)).exp();
792 let denom_inner = (e_th - 1.0) + (e_u - 1.0) * (e_v - 1.0);
793 if denom_inner.abs() < 1e-30 {
794 return 0.0;
795 }
796 let denom = denom_inner * denom_inner;
797 (num / denom).abs()
798 }
799
800 fn conditional_v_given_u(&self, u: f64, v: f64) -> f64 {
801 let u = u.max(1e-10).min(1.0 - 1e-10);
802 let v = v.max(1e-10).min(1.0 - 1e-10);
803 let th = self.theta;
804 let e_u = (-th * u).exp();
805 let e_v = (-th * v).exp();
806 let e_th = (-th).exp();
807 let num = e_u * (e_v - 1.0);
808 let denom = (e_th - 1.0) + (e_u - 1.0) * (e_v - 1.0);
809 if denom.abs() < 1e-30 {
810 return v;
811 }
812 (num / denom).max(0.0).min(1.0)
813 }
814
815 fn conditional_u_given_v(&self, u: f64, v: f64) -> f64 {
816 let u = u.max(1e-10).min(1.0 - 1e-10);
817 let v = v.max(1e-10).min(1.0 - 1e-10);
818 let th = self.theta;
819 let e_u = (-th * u).exp();
820 let e_v = (-th * v).exp();
821 let e_th = (-th).exp();
822 let num = e_v * (e_u - 1.0);
823 let denom = (e_th - 1.0) + (e_u - 1.0) * (e_v - 1.0);
824 if denom.abs() < 1e-30 {
825 return u;
826 }
827 (num / denom).max(0.0).min(1.0)
828 }
829
830 fn upper_tail_dependence(&self) -> f64 {
831 0.0
832 }
833
834 fn lower_tail_dependence(&self) -> f64 {
835 0.0
836 }
837
838 fn kendalls_tau(&self) -> f64 {
839 let th = self.theta;
840 if th.abs() < 0.01 {
841 return th / 9.0; }
843 let d1 = debye_1(th);
846 1.0 - 4.0 * (1.0 - d1) / th
847 }
848
849 fn family_name(&self) -> &str {
850 "Frank"
851 }
852
853 fn n_params(&self) -> usize {
854 1
855 }
856
857 fn params(&self) -> Vec<f64> {
858 vec![self.theta]
859 }
860}
861
862fn debye_1(x: f64) -> f64 {
869 if x.abs() < 1e-10 {
870 return 1.0;
871 }
872 let n = 100;
875 let h = x / (n as f64); let mut sum = 0.0;
877 for i in 0..=n {
878 let t = (i as f64) * h; let f = if t.abs() < 1e-10 {
880 1.0 } else {
882 t / (t.exp() - 1.0)
883 };
884 let w = if i == 0 || i == n {
885 1.0
886 } else if i % 2 == 0 {
887 2.0
888 } else {
889 4.0
890 };
891 sum += w * f;
892 }
893 sum * h / (3.0 * x)
897}
898
899#[derive(Debug, Clone)]
905pub struct TailDependence {
906 pub upper: f64,
908 pub lower: f64,
910}
911
912pub fn tail_dependence(copula: &dyn Copula) -> TailDependence {
914 TailDependence {
915 upper: copula.upper_tail_dependence(),
916 lower: copula.lower_tail_dependence(),
917 }
918}
919
920#[derive(Debug, Clone)]
926pub struct CopulaFitResult {
927 pub params: Vec<f64>,
929 pub log_likelihood: f64,
931 pub aic: f64,
933 pub bic: f64,
935 pub family: String,
937}
938
939pub fn fit_gaussian_copula(
957 u: &ArrayView1<f64>,
958 v: &ArrayView1<f64>,
959) -> StatsResult<CopulaFitResult> {
960 if u.len() != v.len() {
961 return Err(StatsError::DimensionMismatch(
962 "u and v must have the same length".into(),
963 ));
964 }
965 let n = u.len();
966 if n < 3 {
967 return Err(StatsError::InsufficientData(
968 "need at least 3 observations for copula fitting".into(),
969 ));
970 }
971 let mut best_rho = 0.0;
973 let mut best_ll = f64::NEG_INFINITY;
974 for i in -19..20 {
976 let rho = (i as f64) * 0.05;
977 if rho.abs() >= 0.999 {
978 continue;
979 }
980 let cop = match GaussianCopula::new(rho) {
981 Ok(c) => c,
982 Err(_) => continue,
983 };
984 let ll = pseudo_log_likelihood(u, v, &cop);
985 if ll > best_ll {
986 best_ll = ll;
987 best_rho = rho;
988 }
989 }
990 let mut step = 0.025;
992 for _ in 0..20 {
993 let ll_plus = {
994 let r = (best_rho + step).min(0.999);
995 let cop = match GaussianCopula::new(r) {
996 Ok(c) => c,
997 Err(_) => continue,
998 };
999 pseudo_log_likelihood(u, v, &cop)
1000 };
1001 let ll_minus = {
1002 let r = (best_rho - step).max(-0.999);
1003 let cop = match GaussianCopula::new(r) {
1004 Ok(c) => c,
1005 Err(_) => continue,
1006 };
1007 pseudo_log_likelihood(u, v, &cop)
1008 };
1009 if ll_plus > best_ll {
1010 best_rho = (best_rho + step).min(0.999);
1011 best_ll = ll_plus;
1012 } else if ll_minus > best_ll {
1013 best_rho = (best_rho - step).max(-0.999);
1014 best_ll = ll_minus;
1015 }
1016 step *= 0.5;
1017 }
1018
1019 let nf = n as f64;
1020 let k = 1.0;
1021 Ok(CopulaFitResult {
1022 params: vec![best_rho],
1023 log_likelihood: best_ll,
1024 aic: -2.0 * best_ll + 2.0 * k,
1025 bic: -2.0 * best_ll + k * nf.ln(),
1026 family: "Gaussian".into(),
1027 })
1028}
1029
1030pub fn fit_clayton_copula(
1032 u: &ArrayView1<f64>,
1033 v: &ArrayView1<f64>,
1034) -> StatsResult<CopulaFitResult> {
1035 fit_archimedean(u, v, "Clayton")
1036}
1037
1038pub fn fit_gumbel_copula(u: &ArrayView1<f64>, v: &ArrayView1<f64>) -> StatsResult<CopulaFitResult> {
1040 fit_archimedean(u, v, "Gumbel")
1041}
1042
1043pub fn fit_frank_copula(u: &ArrayView1<f64>, v: &ArrayView1<f64>) -> StatsResult<CopulaFitResult> {
1045 fit_archimedean(u, v, "Frank")
1046}
1047
1048fn fit_archimedean(
1049 u: &ArrayView1<f64>,
1050 v: &ArrayView1<f64>,
1051 family: &str,
1052) -> StatsResult<CopulaFitResult> {
1053 if u.len() != v.len() {
1054 return Err(StatsError::DimensionMismatch(
1055 "u and v must have the same length".into(),
1056 ));
1057 }
1058 let n = u.len();
1059 if n < 3 {
1060 return Err(StatsError::InsufficientData(
1061 "need at least 3 observations".into(),
1062 ));
1063 }
1064 let tau = sample_kendalls_tau(u, v)?;
1066
1067 let theta_init = match family {
1069 "Clayton" => {
1070 if tau <= 0.0 {
1072 0.1
1073 } else {
1074 (2.0 * tau / (1.0 - tau).max(0.01)).max(0.01)
1075 }
1076 }
1077 "Gumbel" => {
1078 (1.0 / (1.0 - tau).max(0.01)).max(1.0)
1080 }
1081 "Frank" => {
1082 if tau.abs() < 0.01 {
1084 0.1
1085 } else {
1086 tau * 9.0
1087 } }
1089 _ => 1.0,
1090 };
1091
1092 let mut best_theta = theta_init;
1094 let mut best_ll = f64::NEG_INFINITY;
1095 let (lo, hi, step_count) = match family {
1096 "Clayton" => (0.01, 20.0, 200),
1097 "Gumbel" => (1.0, 20.0, 200),
1098 "Frank" => (-20.0, 20.0, 200),
1099 _ => (0.01, 20.0, 200),
1100 };
1101 for i in 0..=step_count {
1102 let theta = lo + (hi - lo) * (i as f64) / (step_count as f64);
1103 let copula: Box<dyn Copula> = match family {
1104 "Clayton" => match ClaytonCopula::new(theta) {
1105 Ok(c) => Box::new(c),
1106 Err(_) => continue,
1107 },
1108 "Gumbel" => match GumbelCopula::new(theta) {
1109 Ok(c) => Box::new(c),
1110 Err(_) => continue,
1111 },
1112 "Frank" => match FrankCopula::new(theta) {
1113 Ok(c) => Box::new(c),
1114 Err(_) => continue,
1115 },
1116 _ => continue,
1117 };
1118 let ll = pseudo_log_likelihood(u, v, copula.as_ref());
1119 if ll > best_ll && ll.is_finite() {
1120 best_ll = ll;
1121 best_theta = theta;
1122 }
1123 }
1124
1125 let nf = n as f64;
1126 let k = 1.0;
1127 Ok(CopulaFitResult {
1128 params: vec![best_theta],
1129 log_likelihood: best_ll,
1130 aic: -2.0 * best_ll + 2.0 * k,
1131 bic: -2.0 * best_ll + k * nf.ln(),
1132 family: family.into(),
1133 })
1134}
1135
1136fn pseudo_log_likelihood(u: &ArrayView1<f64>, v: &ArrayView1<f64>, copula: &dyn Copula) -> f64 {
1138 let n = u.len();
1139 let mut ll = 0.0;
1140 for i in 0..n {
1141 let ui = u[i].max(1e-6).min(1.0 - 1e-6);
1142 let vi = v[i].max(1e-6).min(1.0 - 1e-6);
1143 let log_c = copula.log_pdf(ui, vi);
1144 if log_c.is_finite() {
1145 ll += log_c;
1146 } else {
1147 ll += -50.0; }
1149 }
1150 ll
1151}
1152
1153fn sample_kendalls_tau(u: &ArrayView1<f64>, v: &ArrayView1<f64>) -> StatsResult<f64> {
1155 let n = u.len();
1156 if n < 2 {
1157 return Err(StatsError::InsufficientData(
1158 "need at least 2 observations for Kendall's tau".into(),
1159 ));
1160 }
1161 let mut concordant = 0_i64;
1162 let mut discordant = 0_i64;
1163 for i in 0..n {
1164 for j in (i + 1)..n {
1165 let du = u[i] - u[j];
1166 let dv = v[i] - v[j];
1167 let prod = du * dv;
1168 if prod > 0.0 {
1169 concordant += 1;
1170 } else if prod < 0.0 {
1171 discordant += 1;
1172 }
1173 }
1174 }
1175 let total = concordant + discordant;
1176 if total == 0 {
1177 return Ok(0.0);
1178 }
1179 Ok((concordant - discordant) as f64 / total as f64)
1180}
1181
1182pub fn pseudo_observations(
1186 x: &ArrayView1<f64>,
1187 y: &ArrayView1<f64>,
1188) -> StatsResult<(Array1<f64>, Array1<f64>)> {
1189 let n = x.len();
1190 if n != y.len() {
1191 return Err(StatsError::DimensionMismatch(
1192 "x and y must have the same length".into(),
1193 ));
1194 }
1195 if n < 2 {
1196 return Err(StatsError::InsufficientData(
1197 "need at least 2 observations".into(),
1198 ));
1199 }
1200 let u = rank_transform(x);
1201 let v = rank_transform(y);
1202 Ok((u, v))
1203}
1204
1205fn rank_transform(x: &ArrayView1<f64>) -> Array1<f64> {
1206 let n = x.len();
1207 let mut indices: Vec<usize> = (0..n).collect();
1208 indices.sort_by(|&a, &b| x[a].partial_cmp(&x[b]).unwrap_or(std::cmp::Ordering::Equal));
1209 let mut ranks = Array1::<f64>::zeros(n);
1210 for (rank, &idx) in indices.iter().enumerate() {
1211 ranks[idx] = (rank as f64 + 1.0) / (n as f64 + 1.0);
1212 }
1213 ranks
1214}
1215
1216#[cfg(test)]
1221mod tests {
1222 use super::*;
1223 use scirs2_core::ndarray::Array1;
1224
1225 #[test]
1226 fn test_gaussian_copula_cdf_bounds() {
1227 let cop = GaussianCopula::new(0.5).expect("should create Gaussian copula");
1228 let c = cop.cdf(0.5, 0.5);
1229 assert!(c >= 0.0 && c <= 1.0, "CDF should be in [0,1], got {}", c);
1230 let c00 = cop.cdf(0.0, 0.0);
1231 assert!(c00 >= 0.0);
1232 let c11 = cop.cdf(1.0, 1.0);
1233 assert!(c11 <= 1.0 + 1e-10);
1234 }
1235
1236 #[test]
1237 fn test_gaussian_copula_pdf_positive() {
1238 let cop = GaussianCopula::new(0.3).expect("should create");
1239 let d = cop.pdf(0.5, 0.5);
1240 assert!(d > 0.0, "PDF should be positive at interior, got {}", d);
1241 }
1242
1243 #[test]
1244 fn test_gaussian_copula_no_tail_dependence() {
1245 let cop = GaussianCopula::new(0.8).expect("should create");
1246 assert!((cop.upper_tail_dependence()).abs() < 1e-10);
1247 assert!((cop.lower_tail_dependence()).abs() < 1e-10);
1248 }
1249
1250 #[test]
1251 fn test_gaussian_copula_kendalls_tau() {
1252 let cop = GaussianCopula::new(0.0).expect("should create");
1253 assert!((cop.kendalls_tau()).abs() < 1e-10);
1254 let cop2 = GaussianCopula::new(0.5).expect("should create");
1255 let tau = cop2.kendalls_tau();
1256 assert!(tau > 0.0 && tau < 1.0);
1257 }
1258
1259 #[test]
1260 fn test_gaussian_copula_invalid_rho() {
1261 assert!(GaussianCopula::new(1.0).is_err());
1262 assert!(GaussianCopula::new(-1.0).is_err());
1263 assert!(GaussianCopula::new(1.5).is_err());
1264 }
1265
1266 #[test]
1267 fn test_student_t_copula_basic() {
1268 let cop = StudentTCopula::new(0.5, 4.0).expect("should create");
1269 let c = cop.cdf(0.5, 0.5);
1270 assert!(c >= 0.0 && c <= 1.0);
1271 let d = cop.pdf(0.5, 0.5);
1272 assert!(d > 0.0);
1273 }
1274
1275 #[test]
1276 fn test_student_t_copula_tail_dependence() {
1277 let cop = StudentTCopula::new(0.5, 4.0).expect("should create");
1278 let lambda_u = cop.upper_tail_dependence();
1279 let lambda_l = cop.lower_tail_dependence();
1280 assert!(lambda_u > 0.0, "t-copula should have upper tail dep");
1281 assert!(lambda_l > 0.0, "t-copula should have lower tail dep");
1282 assert!((lambda_u - lambda_l).abs() < 1e-10);
1284 }
1285
1286 #[test]
1287 fn test_student_t_invalid() {
1288 assert!(StudentTCopula::new(0.5, 0.0).is_err());
1289 assert!(StudentTCopula::new(1.0, 4.0).is_err());
1290 }
1291
1292 #[test]
1293 fn test_clayton_copula_cdf() {
1294 let cop = ClaytonCopula::new(2.0).expect("should create");
1295 let c = cop.cdf(0.5, 0.5);
1296 assert!(c >= 0.0 && c <= 1.0);
1297 let c_u1 = cop.cdf(0.3, 0.9999);
1299 assert!((c_u1 - 0.3).abs() < 0.05, "C(u,1) ~ u, got {}", c_u1);
1300 }
1301
1302 #[test]
1303 fn test_clayton_lower_tail_dependence() {
1304 let cop = ClaytonCopula::new(2.0).expect("should create");
1305 let lambda_l = cop.lower_tail_dependence();
1306 assert!(lambda_l > 0.0);
1307 assert!((cop.upper_tail_dependence()).abs() < 1e-10);
1308 }
1309
1310 #[test]
1311 fn test_clayton_kendalls_tau() {
1312 let cop = ClaytonCopula::new(2.0).expect("should create");
1313 let tau = cop.kendalls_tau();
1314 assert!(
1315 (tau - 0.5).abs() < 1e-10,
1316 "tau = theta/(theta+2) = 2/4 = 0.5, got {}",
1317 tau
1318 );
1319 }
1320
1321 #[test]
1322 fn test_clayton_invalid() {
1323 assert!(ClaytonCopula::new(0.0).is_err());
1324 }
1325
1326 #[test]
1327 fn test_gumbel_copula_cdf() {
1328 let cop = GumbelCopula::new(2.0).expect("should create");
1329 let c = cop.cdf(0.5, 0.5);
1330 assert!(c >= 0.0 && c <= 1.0);
1331 let ind = GumbelCopula::new(1.0).expect("should create");
1333 let c_ind = ind.cdf(0.5, 0.5);
1334 assert!(
1335 (c_ind - 0.25).abs() < 0.01,
1336 "theta=1 => independence, got {}",
1337 c_ind
1338 );
1339 }
1340
1341 #[test]
1342 fn test_gumbel_upper_tail_dependence() {
1343 let cop = GumbelCopula::new(2.0).expect("should create");
1344 let lambda_u = cop.upper_tail_dependence();
1345 assert!(lambda_u > 0.0);
1346 assert!((cop.lower_tail_dependence()).abs() < 1e-10);
1347 }
1348
1349 #[test]
1350 fn test_gumbel_kendalls_tau() {
1351 let cop = GumbelCopula::new(2.0).expect("should create");
1352 let tau = cop.kendalls_tau();
1353 assert!(
1354 (tau - 0.5).abs() < 1e-10,
1355 "tau = 1 - 1/theta = 0.5, got {}",
1356 tau
1357 );
1358 }
1359
1360 #[test]
1361 fn test_gumbel_invalid() {
1362 assert!(GumbelCopula::new(0.5).is_err());
1363 }
1364
1365 #[test]
1366 fn test_frank_copula_cdf() {
1367 let cop = FrankCopula::new(5.0).expect("should create");
1368 let c = cop.cdf(0.5, 0.5);
1369 assert!(c >= 0.0 && c <= 1.0);
1370 }
1371
1372 #[test]
1373 fn test_frank_copula_pdf_positive() {
1374 let cop = FrankCopula::new(5.0).expect("should create");
1375 let d = cop.pdf(0.5, 0.5);
1376 assert!(d > 0.0);
1377 }
1378
1379 #[test]
1380 fn test_frank_no_tail_dependence() {
1381 let cop = FrankCopula::new(10.0).expect("should create");
1382 assert!((cop.upper_tail_dependence()).abs() < 1e-10);
1383 assert!((cop.lower_tail_dependence()).abs() < 1e-10);
1384 }
1385
1386 #[test]
1387 fn test_frank_invalid() {
1388 assert!(FrankCopula::new(0.0).is_err());
1389 }
1390
1391 #[test]
1392 fn test_frank_negative_dependence() {
1393 let cop = FrankCopula::new(-5.0).expect("should create");
1394 let tau = cop.kendalls_tau();
1395 assert!(tau < 0.0, "negative theta => negative tau, got {}", tau);
1396 }
1397
1398 #[test]
1399 fn test_tail_dependence_helper() {
1400 let cop = ClaytonCopula::new(3.0).expect("should create");
1401 let td = tail_dependence(&cop);
1402 assert!(td.lower > 0.0);
1403 assert!(td.upper < 1e-10);
1404 }
1405
1406 #[test]
1407 fn test_pseudo_observations() {
1408 let x = Array1::from_vec(vec![10.0, 20.0, 30.0, 40.0, 50.0]);
1409 let y = Array1::from_vec(vec![50.0, 40.0, 30.0, 20.0, 10.0]);
1410 let (u, v) = pseudo_observations(&x.view(), &y.view()).expect("should succeed");
1411 assert_eq!(u.len(), 5);
1412 assert_eq!(v.len(), 5);
1413 assert!((u[0] - 1.0 / 6.0).abs() < 1e-10);
1415 assert!((u[4] - 5.0 / 6.0).abs() < 1e-10);
1417 }
1418
1419 #[test]
1420 fn test_fit_gaussian_copula() {
1421 let u = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]);
1423 let v = Array1::from_vec(vec![0.15, 0.25, 0.28, 0.45, 0.55, 0.62, 0.73, 0.82, 0.88]);
1424 let result = fit_gaussian_copula(&u.view(), &v.view());
1425 assert!(result.is_ok());
1426 let r = result.expect("fit should succeed");
1427 assert!(
1428 r.params[0] > 0.0,
1429 "rho should be positive, got {}",
1430 r.params[0]
1431 );
1432 assert!(r.log_likelihood.is_finite());
1433 }
1434
1435 #[test]
1436 fn test_fit_clayton_copula() {
1437 let u = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]);
1438 let v = Array1::from_vec(vec![0.15, 0.18, 0.35, 0.42, 0.55, 0.58, 0.72, 0.85, 0.92]);
1439 let result = fit_clayton_copula(&u.view(), &v.view());
1440 assert!(result.is_ok());
1441 let r = result.expect("fit should succeed");
1442 assert!(r.params[0] > 0.0);
1443 }
1444
1445 #[test]
1446 fn test_fit_gumbel_copula() {
1447 let u = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9]);
1448 let v = Array1::from_vec(vec![0.15, 0.25, 0.35, 0.52, 0.68, 0.82, 0.88]);
1449 let result = fit_gumbel_copula(&u.view(), &v.view());
1450 assert!(result.is_ok());
1451 let r = result.expect("fit should succeed");
1452 assert!(r.params[0] >= 1.0);
1453 }
1454
1455 #[test]
1456 fn test_fit_frank_copula() {
1457 let u = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.5, 0.7, 0.8, 0.9]);
1458 let v = Array1::from_vec(vec![0.15, 0.25, 0.32, 0.48, 0.72, 0.78, 0.92]);
1459 let result = fit_frank_copula(&u.view(), &v.view());
1460 assert!(result.is_ok());
1461 }
1462
1463 #[test]
1464 fn test_fit_insufficient_data() {
1465 let u = Array1::from_vec(vec![0.5]);
1466 let v = Array1::from_vec(vec![0.5]);
1467 assert!(fit_gaussian_copula(&u.view(), &v.view()).is_err());
1468 }
1469
1470 #[test]
1471 fn test_fit_length_mismatch() {
1472 let u = Array1::from_vec(vec![0.1, 0.2, 0.3]);
1473 let v = Array1::from_vec(vec![0.1, 0.2]);
1474 assert!(fit_gaussian_copula(&u.view(), &v.view()).is_err());
1475 }
1476
1477 #[test]
1478 fn test_conditional_gaussian() {
1479 let cop = GaussianCopula::new(0.5).expect("should create");
1480 let c = cop.conditional_v_given_u(0.5, 0.5);
1481 assert!(
1482 c >= 0.0 && c <= 1.0,
1483 "conditional should be in [0,1], got {}",
1484 c
1485 );
1486 assert!((c - 0.5).abs() < 0.15);
1488 }
1489
1490 #[test]
1491 fn test_sample_kendalls_tau() {
1492 let u = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4, 0.5]);
1493 let v = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4, 0.5]);
1494 let tau = sample_kendalls_tau(&u.view(), &v.view()).expect("should succeed");
1495 assert!(
1496 (tau - 1.0).abs() < 1e-10,
1497 "perfect concordance => tau=1, got {}",
1498 tau
1499 );
1500 }
1501
1502 #[test]
1503 fn test_sample_kendalls_tau_negative() {
1504 let u = Array1::from_vec(vec![0.1, 0.2, 0.3, 0.4, 0.5]);
1505 let v = Array1::from_vec(vec![0.5, 0.4, 0.3, 0.2, 0.1]);
1506 let tau = sample_kendalls_tau(&u.view(), &v.view()).expect("should succeed");
1507 assert!(
1508 (tau - (-1.0)).abs() < 1e-10,
1509 "perfect discordance => tau=-1, got {}",
1510 tau
1511 );
1512 }
1513
1514 #[test]
1515 fn test_norm_ppf_roundtrip() {
1516 for &p in &[0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99] {
1517 let x = norm_ppf(p);
1518 let q = norm_cdf(x);
1519 assert!(
1520 (q - p).abs() < 1e-6,
1521 "roundtrip failed for p={}: got {}",
1522 p,
1523 q
1524 );
1525 }
1526 }
1527
1528 #[test]
1529 fn test_copula_families_comprehensive() {
1530 let families: Vec<Box<dyn Copula>> = vec![
1532 Box::new(GaussianCopula::new(0.5).expect("create")),
1533 Box::new(StudentTCopula::new(0.5, 5.0).expect("create")),
1534 Box::new(ClaytonCopula::new(2.0).expect("create")),
1535 Box::new(GumbelCopula::new(2.0).expect("create")),
1536 Box::new(FrankCopula::new(5.0).expect("create")),
1537 ];
1538 for cop in &families {
1539 let c = cop.cdf(0.5, 0.5);
1540 assert!(
1541 c >= 0.0 && c <= 1.0,
1542 "{} CDF out of bounds: {}",
1543 cop.family_name(),
1544 c
1545 );
1546 let d = cop.pdf(0.5, 0.5);
1547 assert!(d >= 0.0, "{} PDF negative: {}", cop.family_name(), d);
1548 let tau = cop.kendalls_tau();
1549 assert!(tau.is_finite(), "{} tau not finite", cop.family_name());
1550 let lu = cop.upper_tail_dependence();
1551 let ll = cop.lower_tail_dependence();
1552 assert!(
1553 lu >= 0.0 && lu <= 1.0,
1554 "{} upper tail dep: {}",
1555 cop.family_name(),
1556 lu
1557 );
1558 assert!(
1559 ll >= 0.0 && ll <= 1.0,
1560 "{} lower tail dep: {}",
1561 cop.family_name(),
1562 ll
1563 );
1564 }
1565 }
1566}