copula_core/extreme_value/
mod.rs1use crate::{Copula, CopulaError, Result};
19use nalgebra::DMatrix;
20use rand::{Rng, RngExt};
21
22#[derive(Debug, Clone)]
31pub struct GalambosCopula {
32 theta: f64,
33}
34
35impl GalambosCopula {
36 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 if u1 == 0.0 || u2 == 0.0 {
60 return Ok(0.0);
61 }
62
63 if self.theta.abs() < 1e-10 {
65 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 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 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 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 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#[derive(Debug, Clone)]
146pub struct HuslerReissCopula {
147 lambda: f64,
148}
149
150impl HuslerReissCopula {
151 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 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 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 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 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 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 assert!(cdf >= 0.0);
274 assert!(cdf <= 0.5); }
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 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 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}