copula_core/archimedean/
joe.rs1use crate::{ArchimedeanCopula, Copula, CopulaError, Result};
10use nalgebra::DMatrix;
11use rand::{Rng, RngExt};
12
13#[derive(Debug, Clone)]
15pub struct JoeCopula {
16 theta: f64,
18}
19
20validated_serde!("JoeCopula", JoeCopula { theta: f64 } => JoeCopula::new(theta));
21
22impl JoeCopula {
23 pub fn new(theta: f64) -> Result<Self> {
25 if !theta.is_finite() || theta <= 1.0 {
26 return Err(CopulaError::invalid_parameter(
27 "theta must be finite and > 1",
28 ));
29 }
30 Ok(Self { theta })
31 }
32}
33
34impl Copula for JoeCopula {
35 fn cdf(&self, u: &[f64]) -> Result<f64> {
36 if u.len() != 2 {
37 return Err(CopulaError::dimension_mismatch(2, u.len()));
38 }
39 crate::error::validate_unit_range(u)?;
40
41 let u1 = (1.0 - u[0]).powf(self.theta);
42 let u2 = (1.0 - u[1]).powf(self.theta);
43 let sum = u1 + u2 - u1 * u2;
44 Ok(crate::utils::clamp_to_frechet_bounds(
45 u,
46 1.0 - sum.powf(1.0 / self.theta),
47 ))
48 }
49
50 fn pdf(&self, u: &[f64]) -> Result<f64> {
51 if u.len() != 2 {
52 return Err(CopulaError::dimension_mismatch(2, u.len()));
53 }
54 crate::error::validate_unit_range(u)?;
55
56 let theta = self.theta;
57 let u1_bar = 1.0 - u[0];
58 let u2_bar = 1.0 - u[1];
59 let u1_bar_theta = u1_bar.powf(theta);
60 let u2_bar_theta = u2_bar.powf(theta);
61
62 let sum = u1_bar_theta + u2_bar_theta - u1_bar_theta * u2_bar_theta;
64 let sum_root = sum.powf(1.0 / theta);
65
66 let term1 = u1_bar.powf(theta - 1.0) * u2_bar.powf(theta - 1.0);
69 let term2 = sum.powf(1.0 / theta - 2.0);
70 let term3 = theta - 1.0 + sum_root;
71 let term4 = 1.0 - sum.powf(1.0 / theta - 1.0);
72
73 Ok(theta * term1 * term2 * term3 * term4.max(1e-15))
74 }
75
76 fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
77 let mut samples = DMatrix::<f64>::zeros(n, 2);
78
79 for i in 0..n {
80 let u1: f64 = rng.random::<f64>();
81 let v: f64 = rng.random::<f64>();
82
83 let mut u2_low: f64 = 1e-10;
85 let mut u2_high: f64 = 1.0 - 1e-10;
86 let mut u2: f64 = 0.5;
87
88 for _ in 0..50 {
89 u2 = (u2_low + u2_high) / 2.0;
90
91 let u1_bar = 1.0 - u1;
92 let u2_bar = 1.0 - u2;
93 let u1_bar_theta = u1_bar.powf(self.theta);
94 let u2_bar_theta = u2_bar.powf(self.theta);
95 let sum = u1_bar_theta + u2_bar_theta - u1_bar_theta * u2_bar_theta;
96
97 let cond_cdf = u1_bar.powf(self.theta - 1.0)
99 * sum.powf(1.0 / self.theta - 1.0)
100 * (1.0 - u2_bar_theta);
101
102 if (cond_cdf - v).abs() < 1e-10 {
103 break;
104 }
105
106 if cond_cdf < v {
107 u2_high = u2;
108 } else {
109 u2_low = u2;
110 }
111 }
112
113 samples[(i, 0)] = u1;
114 samples[(i, 1)] = u2;
115 }
116
117 Ok(samples)
118 }
119
120 fn dimension(&self) -> usize {
121 2
122 }
123}
124
125impl ArchimedeanCopula for JoeCopula {
126 fn phi(&self, t: f64) -> Result<f64> {
127 if t <= 0.0 || t > 1.0 {
128 return Err(CopulaError::invalid_range(vec![t]));
129 }
130 let inner = 1.0 - (1.0 - t).powf(self.theta);
132 if inner <= 0.0 {
133 return Err(CopulaError::numerical("phi argument out of valid range"));
134 }
135 Ok(-inner.ln())
136 }
137
138 fn phi_inv(&self, s: f64) -> Result<f64> {
139 if s < 0.0 {
140 return Err(CopulaError::invalid_range(vec![s]));
141 }
142 let exp_neg_s = (-s).exp();
144 Ok(1.0 - (1.0 - exp_neg_s).powf(1.0 / self.theta))
145 }
146
147 fn phi_inv_deriv(&self, s: f64, k: usize) -> Result<f64> {
148 if s < 0.0 {
149 return Err(CopulaError::invalid_range(vec![s]));
150 }
151
152 let exp_neg_s = (-s).exp();
153 let base = 1.0 - exp_neg_s;
154
155 match k {
156 1 => {
157 Ok((1.0 / self.theta) * exp_neg_s * base.powf(1.0 / self.theta - 1.0))
159 }
160 2 => {
161 let term1 = -(1.0 / self.theta) * exp_neg_s * base.powf(1.0 / self.theta - 1.0);
163 let term2 = (1.0 / self.theta)
164 * (1.0 / self.theta - 1.0)
165 * exp_neg_s.powi(2)
166 * base.powf(1.0 / self.theta - 2.0);
167 Ok(term1 + term2)
168 }
169 _ => Err(CopulaError::not_implemented("phi_inv_deriv k>2")),
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn new_rejects_invalid_theta() {
180 assert!(JoeCopula::new(1.0).is_err());
181 }
182
183 #[test]
184 fn valid_new_returns_copula() {
185 let cop = JoeCopula::new(2.0).unwrap();
186 assert_eq!(cop.dimension(), 2);
187 }
188
189 #[test]
190 fn cdf_matches_formula() {
191 let cop = JoeCopula::new(1.5).unwrap();
192 let cdf = cop.cdf(&[0.4, 0.6]).unwrap();
193 let u1 = (1.0 - 0.4_f64).powf(1.5);
194 let u2 = (1.0 - 0.6_f64).powf(1.5);
195 let expected = 1.0 - (u1 + u2 - u1 * u2).powf(1.0 / 1.5);
196 assert!((cdf - expected).abs() < 1e-12);
197 }
198}