copula_core/archimedean/
gumbel.rs1#[cfg(feature = "estimation")]
10use crate::traits::FittableCopula;
11#[cfg(feature = "estimation")]
12use crate::utils::kendall_tau;
13use crate::{ArchimedeanCopula, Copula, CopulaError, Result};
14use nalgebra::DMatrix;
15use rand::{Rng, RngExt};
16
17#[derive(Debug, Clone)]
19pub struct GumbelCopula {
20 theta: f64,
22}
23
24validated_serde!("GumbelCopula", GumbelCopula { theta: f64 } => GumbelCopula::new(theta));
25
26impl GumbelCopula {
27 pub fn new(theta: f64) -> Result<Self> {
29 if theta <= 1.0 || !theta.is_finite() {
30 return Err(CopulaError::invalid_parameter("theta must be > 1"));
31 }
32 Ok(Self { theta })
33 }
34}
35
36impl Copula for GumbelCopula {
37 fn cdf(&self, u: &[f64]) -> Result<f64> {
38 if u.len() != 2 {
39 return Err(CopulaError::dimension_mismatch(2, u.len()));
40 }
41 crate::error::validate_unit_range(u)?;
42
43 let sum = (-u[0].ln()).powf(self.theta) + (-u[1].ln()).powf(self.theta);
44 Ok(crate::utils::clamp_to_frechet_bounds(
45 u,
46 (-sum.powf(1.0 / self.theta)).exp(),
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 ln_u = -u[0].ln();
58 let ln_v = -u[1].ln();
59
60 let a = ln_u.powf(theta) + ln_v.powf(theta);
62
63 let c_uv = (-a.powf(1.0 / theta)).exp();
65
66 let term1 = c_uv / (u[0] * u[1]);
68 let term2 = a.powf(-2.0 + 2.0 / theta);
69 let term3 = (ln_u * ln_v).powf(theta - 1.0);
70 let term4 = theta - 1.0 + a.powf(1.0 / theta);
71
72 Ok(term1 * term2 * term3 * term4)
73 }
74
75 fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
76 let mut samples = DMatrix::<f64>::zeros(n, 2);
77
78 for i in 0..n {
79 let u1: f64 = rng.random::<f64>();
81 let v: f64 = rng.random::<f64>();
82
83 let ln_u1 = -u1.ln();
89 let target = v;
90
91 let mut u2_low: f64 = 1e-10;
93 let mut u2_high: f64 = 1.0 - 1e-10;
94 let mut u2: f64 = 0.5;
95
96 for _ in 0..50 {
97 u2 = (u2_low + u2_high) / 2.0;
99 let ln_u2 = -u2.ln();
100 let a = ln_u1.powf(self.theta) + ln_u2.powf(self.theta);
101 let a_root = a.powf(1.0 / self.theta);
102
103 let c_uv = (-a_root).exp();
105 let cond_cdf = c_uv
106 * a_root.powf(-1.0)
107 * ln_u1.powf(self.theta - 1.0)
108 * a.powf((1.0 - self.theta) / self.theta)
109 / u1;
110
111 if (cond_cdf - target).abs() < 1e-10 {
112 break;
113 }
114
115 if cond_cdf < target {
116 u2_low = u2;
117 } else {
118 u2_high = u2;
119 }
120 }
121
122 samples[(i, 0)] = u1;
123 samples[(i, 1)] = u2;
124 }
125
126 Ok(samples)
127 }
128
129 fn dimension(&self) -> usize {
130 2
131 }
132}
133
134impl ArchimedeanCopula for GumbelCopula {
135 fn phi(&self, t: f64) -> Result<f64> {
136 if t <= 0.0 || t > 1.0 {
137 return Err(CopulaError::invalid_range(vec![t]));
138 }
139 Ok((-t.ln()).powf(self.theta))
140 }
141
142 fn phi_inv(&self, s: f64) -> Result<f64> {
143 if s < 0.0 {
144 return Err(CopulaError::invalid_range(vec![s]));
145 }
146 Ok((-s.powf(1.0 / self.theta)).exp())
147 }
148
149 fn phi_inv_deriv(&self, s: f64, k: usize) -> Result<f64> {
150 if s < 0.0 {
151 return Err(CopulaError::invalid_range(vec![s]));
152 }
153 match k {
154 1 => {
155 let base = s.powf(1.0 / self.theta - 1.0);
156 Ok(-self.phi_inv(s)? * base / self.theta)
157 }
158 2 => {
159 let phi_inv = self.phi_inv(s)?;
160 let term1 = (1.0 / self.theta.powi(2)) * s.powf(2.0 / self.theta - 2.0);
161 let term2 =
162 (1.0 / self.theta) * (1.0 / self.theta - 1.0) * s.powf(1.0 / self.theta - 2.0);
163 Ok(phi_inv * (term1 - term2))
164 }
165 _ => Err(CopulaError::not_implemented("phi_inv_deriv k>2")),
166 }
167 }
168}
169
170#[cfg(feature = "estimation")]
171impl FittableCopula for GumbelCopula {
172 type Parameters = f64;
173
174 fn fit(&mut self, pseudo_obs: &DMatrix<f64>) -> Result<Self::Parameters> {
175 self.fit_moments(pseudo_obs)
176 }
177
178 fn log_likelihood(&self, pseudo_obs: &DMatrix<f64>) -> Result<f64> {
179 if pseudo_obs.ncols() != 2 {
180 return Err(CopulaError::dimension_mismatch(2, pseudo_obs.ncols()));
181 }
182
183 let mut log_lik = 0.0;
184 for i in 0..pseudo_obs.nrows() {
185 let u = [pseudo_obs[(i, 0)], pseudo_obs[(i, 1)]];
186 let pdf_val = self.pdf(&u)?;
187 if pdf_val <= 0.0 {
188 return Err(CopulaError::numerical(
189 "PDF value must be positive for log-likelihood",
190 ));
191 }
192 log_lik += pdf_val.ln();
193 }
194
195 Ok(log_lik)
196 }
197
198 fn fit_moments(&mut self, pseudo_obs: &DMatrix<f64>) -> Result<Self::Parameters> {
199 if pseudo_obs.ncols() != 2 {
200 return Err(CopulaError::dimension_mismatch(2, pseudo_obs.ncols()));
201 }
202 for j in 0..2 {
203 crate::error::validate_unit_range(pseudo_obs.column(j).as_slice())?;
204 }
205 let u: Vec<f64> = pseudo_obs.column(0).iter().copied().collect();
206 let v: Vec<f64> = pseudo_obs.column(1).iter().copied().collect();
207 let tau = kendall_tau(&u, &v)?;
208 if tau >= 1.0 {
209 return Err(CopulaError::invalid_parameter("tau must be < 1"));
210 }
211 let theta = 1.0 / (1.0 - tau);
212 if theta <= 1.0 {
213 return Err(CopulaError::invalid_parameter("theta must be > 1"));
214 }
215 self.theta = theta;
216 Ok(theta)
217 }
218
219 fn parameters(&self) -> Self::Parameters {
220 self.theta
221 }
222
223 fn set_parameters(&mut self, params: Self::Parameters) -> Result<()> {
224 *self = Self::new(params)?;
225 Ok(())
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn cdf_matches_formula() {
235 let cop = GumbelCopula::new(1.5).unwrap();
236 let cdf = cop.cdf(&[0.7, 0.8]).unwrap();
237 let sum = (-0.7_f64.ln()).powf(1.5) + (-0.8_f64.ln()).powf(1.5);
238 let expected = (-sum.powf(1.0 / 1.5)).exp();
239 assert!((cdf - expected).abs() < 1e-10);
240 }
241
242 #[test]
243 fn phi_inverse_derivatives() {
244 let cop = GumbelCopula::new(2.0).unwrap();
245 let s = 0.3;
246 let phi_inv = cop.phi_inv(s).unwrap();
247 let h = 1e-6;
248 let fd = (cop.phi_inv(s + h).unwrap() - phi_inv) / h;
249 let analytic = cop.phi_inv_deriv(s, 1).unwrap();
250 assert!((fd - analytic).abs() < 1e-4);
251 }
252
253 #[test]
254 fn new_rejects_invalid_theta() {
255 assert!(GumbelCopula::new(1.0).is_err());
256 assert!(GumbelCopula::new(f64::NAN).is_err());
257 }
258
259 #[test]
260 fn cdf_validates_input() {
261 let cop = GumbelCopula::new(2.0).unwrap();
262 assert!(cop.cdf(&[0.5]).is_err());
264 assert!(cop.cdf(&[1.2, 0.3]).is_err());
266 }
267}