Skip to main content

copula_core/archimedean/
frank.rs

1//! Frank copula implementation.
2//!
3//! ## Bibliography
4//! - Frank, M. J. (1979). On the simultaneous associativity of f(x,y) and
5//!   x+y−f(x,y). *Aequationes Mathematicae*, 19(1), 194-226.
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/// Frank copula with non-zero parameter `theta`.
14#[derive(Debug, Clone)]
15pub struct FrankCopula {
16    /// Copula parameter θ ≠ 0
17    theta: f64,
18}
19
20validated_serde!("FrankCopula", FrankCopula { theta: f64 } => FrankCopula::new(theta));
21
22impl FrankCopula {
23    /// Create a new Frank copula with parameter `theta`.
24    pub fn new(theta: f64) -> Result<Self> {
25        if !theta.is_finite() || theta.abs() < f64::EPSILON {
26            return Err(CopulaError::invalid_parameter(
27                "theta must be finite and non-zero",
28            ));
29        }
30        Ok(Self { theta })
31    }
32}
33
34impl Copula for FrankCopula {
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        // C(u, v) = -ln(1 + a) / theta with
42        // a = (e^{-theta u} - 1)(e^{-theta v} - 1) / (e^{-theta} - 1).
43        // For large theta and u, v near 1, a approaches -1 and 1 + a cancels
44        // (an error of 1.6e-3 at theta = 35, and NaN at theta = 60). In that
45        // regime use the expanded form
46        // 1 + a = (e^{-theta} - e^{-theta u} - e^{-theta v} + e^{-theta (u + v)}) / (e^{-theta} - 1).
47        let theta = self.theta;
48        let a = (-theta * u[0]).exp_m1() * (-theta * u[1]).exp_m1() / (-theta).exp_m1();
49        let value = if a > -0.5 {
50            -a.ln_1p() / theta
51        } else {
52            let expanded = (-theta).exp() - (-theta * u[0]).exp() - (-theta * u[1]).exp()
53                + (-theta * (u[0] + u[1])).exp();
54            -(expanded / (-theta).exp_m1()).ln() / theta
55        };
56        Ok(crate::utils::clamp_to_frechet_bounds(u, value))
57    }
58
59    fn pdf(&self, u: &[f64]) -> Result<f64> {
60        if u.len() != 2 {
61            return Err(CopulaError::dimension_mismatch(2, u.len()));
62        }
63        crate::error::validate_unit_range(u)?;
64
65        let theta = self.theta;
66        let exp_neg_theta = (-theta).exp();
67        let exp_neg_theta_u = (-theta * u[0]).exp();
68        let exp_neg_theta_v = (-theta * u[1]).exp();
69        let exp_neg_theta_sum = (-theta * (u[0] + u[1])).exp();
70
71        // c(u,v) = θ(1 - e^(-θ)) × e^(-θ(u+v)) / [(e^(-θu) - 1)(e^(-θv) - 1) + (e^(-θ) - 1)]^2
72        // Note: For θ > 0, (1 - e^(-θ)) > 0 ensures positive PDF
73        let numerator = theta * (1.0 - exp_neg_theta) * exp_neg_theta_sum;
74        let term1 = (exp_neg_theta_u - 1.0) * (exp_neg_theta_v - 1.0);
75        let term2 = exp_neg_theta - 1.0;
76        let denominator = (term1 + term2).powi(2);
77
78        Ok(numerator / denominator)
79    }
80
81    fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
82        let mut samples = DMatrix::<f64>::zeros(n, 2);
83
84        for i in 0..n {
85            let u1: f64 = rng.random::<f64>();
86            let v: f64 = rng.random::<f64>();
87
88            // Use conditional distribution method similar to Gumbel
89            let theta = self.theta;
90            let exp_neg_theta = (-theta).exp();
91
92            // Binary search for u2
93            let mut u2_low: f64 = 1e-10;
94            let mut u2_high: f64 = 1.0 - 1e-10;
95            let mut u2: f64 = 0.5;
96
97            for _ in 0..50 {
98                u2 = (u2_low + u2_high) / 2.0;
99
100                // Conditional CDF for Frank copula
101                let exp_u1 = (-theta * u1).exp();
102                let exp_u2 = (-theta * u2).exp();
103                let num = (exp_u1 - 1.0) * (exp_u2 - 1.0);
104                let denom_base = num + (exp_neg_theta - 1.0);
105
106                let cond_cdf = (exp_u2 - 1.0) * (exp_neg_theta - 1.0) / denom_base;
107
108                if (cond_cdf - v).abs() < 1e-10 {
109                    break;
110                }
111
112                if cond_cdf < v {
113                    u2_low = u2;
114                } else {
115                    u2_high = u2;
116                }
117            }
118
119            samples[(i, 0)] = u1;
120            samples[(i, 1)] = u2;
121        }
122
123        Ok(samples)
124    }
125
126    fn dimension(&self) -> usize {
127        2
128    }
129}
130
131impl ArchimedeanCopula for FrankCopula {
132    fn phi(&self, t: f64) -> Result<f64> {
133        if t <= 0.0 || t > 1.0 {
134            return Err(CopulaError::invalid_range(vec![t]));
135        }
136        let theta = self.theta;
137        // φ(t) = -ln[(e^(-θt) - 1)/(e^(-θ) - 1)]
138        let num = (-theta * t).exp() - 1.0;
139        let denom = (-theta).exp() - 1.0;
140        Ok(-(num / denom).ln())
141    }
142
143    fn phi_inv(&self, s: f64) -> Result<f64> {
144        if s < 0.0 {
145            return Err(CopulaError::invalid_range(vec![s]));
146        }
147        let theta = self.theta;
148        // φ^(-1)(s) = -(1/θ) ln[1 + e^(-s)(e^(-θ) - 1)]
149        let inner = 1.0 + (-s).exp() * ((-theta).exp() - 1.0);
150        Ok(-(1.0 / theta) * inner.ln())
151    }
152
153    fn phi_inv_deriv(&self, s: f64, k: usize) -> Result<f64> {
154        if s < 0.0 {
155            return Err(CopulaError::invalid_range(vec![s]));
156        }
157        let theta = self.theta;
158        let exp_neg_s = (-s).exp();
159        let exp_neg_theta = (-theta).exp();
160        let denominator = exp_neg_theta - 1.0;
161
162        match k {
163            1 => {
164                // First derivative: φ^(-1)'(s) = e^(-s)(e^(-θ) - 1) / [θ(1 + e^(-s)(e^(-θ) - 1))]
165                let num = exp_neg_s * denominator;
166                let denom = theta * (1.0 + exp_neg_s * denominator);
167                Ok(num / denom)
168            }
169            2 => {
170                // Second derivative (more complex)
171                let inner = 1.0 + exp_neg_s * denominator;
172                let term1 = -exp_neg_s * denominator / (theta * inner);
173                let term2 = exp_neg_s.powi(2) * denominator.powi(2) / (theta * inner.powi(2));
174                Ok(term1 + term2)
175            }
176            _ => Err(CopulaError::not_implemented("phi_inv_deriv k>2")),
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn new_rejects_zero_theta() {
187        assert!(FrankCopula::new(0.0).is_err());
188    }
189
190    #[test]
191    fn valid_new_returns_copula() {
192        let cop = FrankCopula::new(1.0).unwrap();
193        assert_eq!(cop.dimension(), 2);
194    }
195
196    #[test]
197    fn cdf_matches_formula() {
198        let cop = FrankCopula::new(2.0).unwrap();
199        let cdf = cop.cdf(&[0.3, 0.4]).unwrap();
200        let theta = 2.0;
201        let num = ((-theta * 0.3_f64).exp() - 1.0) * ((-theta * 0.4_f64).exp() - 1.0);
202        let denom = (-theta).exp() - 1.0;
203        let expected = -(1.0 / theta) * (1.0 + num / denom).ln();
204        assert!((cdf - expected).abs() < 1e-12);
205    }
206}