Skip to main content

copula_core/archimedean/
joe.rs

1//! Joe copula implementation.
2//!
3//! ## Bibliography
4//! - Joe, H. (1997). *Multivariate Models and Dependence Concepts*. Chapman &
5//!   Hall.
6//! - Nelsen, R. B. (2006). *An Introduction to Copulas*. Springer.
7//! - Joe, H. (2014). *Dependence Modeling with Copulas*. CRC Press.
8
9use crate::{ArchimedeanCopula, Copula, CopulaError, Result};
10use nalgebra::DMatrix;
11use rand::{Rng, RngExt};
12
13/// Joe copula with parameter `theta > 1`.
14#[derive(Debug, Clone)]
15pub struct JoeCopula {
16    /// Copula parameter θ > 1
17    theta: f64,
18}
19
20validated_serde!("JoeCopula", JoeCopula { theta: f64 } => JoeCopula::new(theta));
21
22impl JoeCopula {
23    /// Create a new Joe copula with parameter `theta`.
24    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        // Joe copula PDF is complex. Using numerical differentiation from CDF
63        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        // PDF: c(u,v) involves complex derivatives
67        // Simplified: ∂²C/∂u∂v
68        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            // Binary search for u2 using conditional CDF
84            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                // Conditional CDF (derivative w.r.t. u1)
98                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        // φ(t) = -ln[1 - (1-t)^θ]
131        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        // φ^(-1)(s) = 1 - (1 - e^(-s))^(1/θ)
143        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                // First derivative: (1/θ) × e^(-s) × (1 - e^(-s))^(1/θ - 1)
158                Ok((1.0 / self.theta) * exp_neg_s * base.powf(1.0 / self.theta - 1.0))
159            }
160            2 => {
161                // Second derivative (more complex)
162                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}