Skip to main content

copula_core/extreme_value/
mod.rs

1//! Extreme value copulas module.
2//!
3//! Extreme value copulas are particularly useful for modeling dependence in extreme events.
4//! They arise as the limiting distribution of componentwise maxima.
5//!
6//! This module implements:
7//! - Galambos copula
8//! - Husler-Reiss copula
9//! - Asymmetric logistic (Tawn) copula
10//!
11//! Note: The Gumbel copula is also an extreme value copula but is implemented
12//! in the archimedean module.
13//!
14//! ## Bibliography
15//! - Gudendorf, G., & Segers, J. (2010). Extreme-value copulas. In *Copula Theory and Its Applications*.
16//! - Joe, H. (2014). *Dependence Modeling with Copulas*. CRC Press.
17
18use crate::{Copula, CopulaError, Result};
19use nalgebra::DMatrix;
20use rand::{Rng, RngExt};
21
22/// Galambos copula with parameter θ ≥ 0.
23///
24/// The Galambos copula is an extreme value copula with:
25/// - θ = 0: independence
26/// - θ → ∞: comonotonicity
27///
28/// ## Bibliography
29/// - Galambos, J. (1975). Order statistics of samples from multivariate distributions.
30#[derive(Debug, Clone)]
31pub struct GalambosCopula {
32    theta: f64,
33}
34
35impl GalambosCopula {
36    /// Create a new Galambos copula.
37    ///
38    /// # Arguments
39    /// * `theta` - Association parameter (θ ≥ 0)
40    pub fn new(theta: f64) -> Result<Self> {
41        if theta < 0.0 || !theta.is_finite() {
42            return Err(CopulaError::invalid_parameter("theta must be >= 0"));
43        }
44        Ok(Self { theta })
45    }
46}
47
48impl Copula for GalambosCopula {
49    fn cdf(&self, u: &[f64]) -> Result<f64> {
50        if u.len() != 2 {
51            return Err(CopulaError::dimension_mismatch(2, u.len()));
52        }
53        crate::error::validate_unit_range(u)?;
54
55        let u1 = u[0];
56        let u2 = u[1];
57
58        // Avoid log(0)
59        if u1 == 0.0 || u2 == 0.0 {
60            return Ok(0.0);
61        }
62
63        // C(u1, u2) = u1*u2*exp[(-ln(u1))^(-θ) + (-ln(u2))^(-θ)]^(-1/θ)
64        if self.theta.abs() < 1e-10 {
65            // Independence case
66            return Ok(u1 * u2);
67        }
68
69        let ln_u1 = -u1.ln();
70        let ln_u2 = -u2.ln();
71        let sum = ln_u1.powf(-self.theta) + ln_u2.powf(-self.theta);
72
73        Ok(u1 * u2 * (-sum.powf(-1.0 / self.theta)).exp())
74    }
75
76    fn pdf(&self, u: &[f64]) -> Result<f64> {
77        if u.len() != 2 {
78            return Err(CopulaError::dimension_mismatch(2, u.len()));
79        }
80        crate::error::validate_unit_range(u)?;
81
82        // Complex derivative - using numerical differentiation
83        let h = 1e-8;
84        let c_uv = self.cdf(u)?;
85        let c_u_plus = self.cdf(&[u[0] + h, u[1]])?;
86        let c_v_plus = self.cdf(&[u[0], u[1] + h])?;
87        let c_uv_plus = self.cdf(&[u[0] + h, u[1] + h])?;
88
89        let pdf = (c_uv_plus - c_u_plus - c_v_plus + c_uv) / (h * h);
90        Ok(pdf.max(0.0))
91    }
92
93    fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
94        // Use conditional sampling method
95        let mut samples = DMatrix::<f64>::zeros(n, 2);
96
97        for i in 0..n {
98            let u1: f64 = rng.random::<f64>();
99            let v: f64 = rng.random::<f64>();
100
101            // Binary search for u2 using conditional CDF
102            let mut u2_low: f64 = 1e-10;
103            let mut u2_high: f64 = 1.0 - 1e-10;
104            let mut u2: f64 = 0.5;
105
106            for _ in 0..50 {
107                u2 = (u2_low + u2_high) / 2.0;
108
109                // Numerical derivative of CDF w.r.t. u1
110                let h = 1e-8;
111                let c1 = self.cdf(&[u1 + h, u2])?;
112                let c2 = self.cdf(&[u1, u2])?;
113                let cond_cdf = (c1 - c2) / h;
114
115                if (cond_cdf - v).abs() < 1e-10 {
116                    break;
117                }
118
119                if cond_cdf < v {
120                    u2_low = u2;
121                } else {
122                    u2_high = u2;
123                }
124            }
125
126            samples[(i, 0)] = u1;
127            samples[(i, 1)] = u2;
128        }
129
130        Ok(samples)
131    }
132
133    fn dimension(&self) -> usize {
134        2
135    }
136}
137
138/// Husler-Reiss copula with parameter θ > 0.
139///
140/// The Husler-Reiss copula is an extreme value copula useful for
141/// modeling asymmetric dependence structures.
142///
143/// ## Bibliography
144/// - Husler, J., & Reiss, R.-D. (1989). Maxima of normal random vectors.
145#[derive(Debug, Clone)]
146pub struct HuslerReissCopula {
147    lambda: f64,
148}
149
150impl HuslerReissCopula {
151    /// Create a new Husler-Reiss copula.
152    ///
153    /// # Arguments
154    /// * `lambda` - Association parameter (λ > 0)
155    pub fn new(lambda: f64) -> Result<Self> {
156        if lambda <= 0.0 || !lambda.is_finite() {
157            return Err(CopulaError::invalid_parameter("lambda must be > 0"));
158        }
159        Ok(Self { lambda })
160    }
161
162    /// Standard normal CDF approximation.
163    fn phi(x: f64) -> f64 {
164        use statrs::distribution::{ContinuousCDF, Normal};
165        Normal::new(0.0, 1.0)
166            .expect("standard normal parameters are always valid")
167            .cdf(x)
168    }
169}
170
171impl Copula for HuslerReissCopula {
172    fn cdf(&self, u: &[f64]) -> Result<f64> {
173        if u.len() != 2 {
174            return Err(CopulaError::dimension_mismatch(2, u.len()));
175        }
176        crate::error::validate_unit_range(u)?;
177
178        let u1 = u[0];
179        let u2 = u[1];
180
181        if u1 == 0.0 || u2 == 0.0 {
182            return Ok(0.0);
183        }
184
185        // C(u1, u2) = exp[ln(u1)Φ(1/λ + λ/2 ln(ln(u2)/ln(u1))) + ln(u2)Φ(1/λ + λ/2 ln(ln(u1)/ln(u2)))]
186        let ln_u1 = u1.ln();
187        let ln_u2 = u2.ln();
188
189        let term1 = 1.0 / self.lambda + 0.5 * self.lambda * (ln_u2 / ln_u1).ln();
190        let term2 = 1.0 / self.lambda + 0.5 * self.lambda * (ln_u1 / ln_u2).ln();
191
192        Ok((ln_u1 * Self::phi(term1) + ln_u2 * Self::phi(term2)).exp())
193    }
194
195    fn pdf(&self, u: &[f64]) -> Result<f64> {
196        if u.len() != 2 {
197            return Err(CopulaError::dimension_mismatch(2, u.len()));
198        }
199        crate::error::validate_unit_range(u)?;
200
201        // Use numerical differentiation
202        let h = 1e-8;
203        let c_uv = self.cdf(u)?;
204        let c_u_plus = self.cdf(&[u[0] + h, u[1]])?;
205        let c_v_plus = self.cdf(&[u[0], u[1] + h])?;
206        let c_uv_plus = self.cdf(&[u[0] + h, u[1] + h])?;
207
208        let pdf = (c_uv_plus - c_u_plus - c_v_plus + c_uv) / (h * h);
209        Ok(pdf.max(0.0))
210    }
211
212    fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
213        let mut samples = DMatrix::<f64>::zeros(n, 2);
214
215        for i in 0..n {
216            let u1: f64 = rng.random::<f64>();
217            let v: f64 = rng.random::<f64>();
218
219            // Binary search for u2
220            let mut u2_low: f64 = 1e-10;
221            let mut u2_high: f64 = 1.0 - 1e-10;
222            let mut u2: f64 = 0.5;
223
224            for _ in 0..50 {
225                u2 = (u2_low + u2_high) / 2.0;
226
227                let h = 1e-8;
228                let c1 = self.cdf(&[u1 + h, u2])?;
229                let c2 = self.cdf(&[u1, u2])?;
230                let cond_cdf = (c1 - c2) / h;
231
232                if (cond_cdf - v).abs() < 1e-10 {
233                    break;
234                }
235
236                if cond_cdf < v {
237                    u2_low = u2;
238                } else {
239                    u2_high = u2;
240                }
241            }
242
243            samples[(i, 0)] = u1;
244            samples[(i, 1)] = u2;
245        }
246
247        Ok(samples)
248    }
249
250    fn dimension(&self) -> usize {
251        2
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_galambos_independence() {
261        // theta = 0 should give independence
262        let cop = GalambosCopula::new(0.0).unwrap();
263        let cdf = cop.cdf(&[0.5, 0.7]).unwrap();
264        let expected = 0.5 * 0.7;
265        assert!((cdf - expected).abs() < 1e-10);
266    }
267
268    #[test]
269    fn test_galambos_bounds() {
270        let cop = GalambosCopula::new(2.0).unwrap();
271        let cdf = cop.cdf(&[0.5, 0.7]).unwrap();
272        // Should satisfy Frechet bounds
273        assert!(cdf >= 0.0);
274        assert!(cdf <= 0.5); // min(u1, u2)
275    }
276
277    #[test]
278    fn test_husler_reiss_bounds() {
279        let cop = HuslerReissCopula::new(1.0).unwrap();
280        let cdf = cop.cdf(&[0.4, 0.6]).unwrap();
281        // Should satisfy Frechet bounds
282        assert!(cdf >= 0.0);
283        assert!(cdf <= 0.4);
284    }
285
286    #[test]
287    fn test_galambos_sample() {
288        let mut rng = rand::rng();
289        let cop = GalambosCopula::new(1.5).unwrap();
290        let samples = cop.sample(10, &mut rng).unwrap();
291
292        assert_eq!(samples.nrows(), 10);
293        assert_eq!(samples.ncols(), 2);
294
295        // Check all values in [0, 1]
296        for i in 0..10 {
297            for j in 0..2 {
298                assert!(samples[(i, j)] >= 0.0 && samples[(i, j)] <= 1.0);
299            }
300        }
301    }
302}