copula_core/archimedean/
frank.rs1use crate::{ArchimedeanCopula, Copula, CopulaError, Result};
10use nalgebra::DMatrix;
11use rand::{Rng, RngExt};
12
13#[derive(Debug, Clone)]
15pub struct FrankCopula {
16 theta: f64,
18}
19
20validated_serde!("FrankCopula", FrankCopula { theta: f64 } => FrankCopula::new(theta));
21
22impl FrankCopula {
23 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 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 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 let theta = self.theta;
90 let exp_neg_theta = (-theta).exp();
91
92 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 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 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 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 let num = exp_neg_s * denominator;
166 let denom = theta * (1.0 + exp_neg_s * denominator);
167 Ok(num / denom)
168 }
169 2 => {
170 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}