1use crate::{Copula, CopulaError, Result};
12use nalgebra::DMatrix;
13use rand::seq::IndexedRandom;
14use rand::Rng;
15
16#[derive(Debug, Clone)]
18pub struct MarshallOlkinCopula {
19 alpha: f64,
20 beta: f64,
21}
22
23validated_serde!("MarshallOlkinCopula", MarshallOlkinCopula { alpha: f64, beta: f64 } => MarshallOlkinCopula::new(alpha, beta));
24
25impl MarshallOlkinCopula {
26 pub fn new(alpha: f64, beta: f64) -> Result<Self> {
28 if !(0.0..1.0).contains(&alpha) || !(0.0..1.0).contains(&beta) {
29 return Err(CopulaError::invalid_parameter(
30 "alpha and beta must be in [0,1)",
31 ));
32 }
33 Ok(Self { alpha, beta })
34 }
35}
36
37impl Copula for MarshallOlkinCopula {
38 fn cdf(&self, u: &[f64]) -> Result<f64> {
39 if u.len() != 2 {
40 return Err(CopulaError::dimension_mismatch(2, u.len()));
41 }
42 crate::error::validate_unit_range(u)?;
43 let u1 = u[0];
44 let u2 = u[1];
45 let term1 = u1.powf(1.0 - self.alpha) * u2;
46 let term2 = u1 * u2.powf(1.0 - self.beta);
47 Ok(crate::utils::clamp_to_frechet_bounds(u, term1.min(term2)))
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 u1 = u[0];
61 let u2 = u[1];
62
63 let term1 = u1.powf(1.0 - self.alpha) * u2;
65 let term2 = u1 * u2.powf(1.0 - self.beta);
66
67 if (term1 - term2).abs() < 1e-10 {
70 return Ok(1e6);
73 }
74
75 if term1 < term2 {
77 Ok((1.0 - self.alpha) * u1.powf(-self.alpha) * u2.powf(0.0))
78 } else {
79 Ok((1.0 - self.beta) * u1.powf(0.0) * u2.powf(-self.beta))
80 }
81 }
82
83 fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
84 use rand_distr::{Distribution, Exp};
85
86 let mut samples = DMatrix::<f64>::zeros(n, 2);
87
88 let exp_dist =
93 Exp::new(1.0).map_err(|_| CopulaError::computation("failed to create Exp(1)"))?;
94
95 for i in 0..n {
96 let x1 = exp_dist.sample(rng);
97 let x2 = exp_dist.sample(rng);
98 let x12 = exp_dist.sample(rng);
99
100 let u1 = (-x1 / (1.0 - self.alpha) - x12).exp();
102 let u2 = (-x2 / (1.0 - self.beta) - x12).exp();
103
104 samples[(i, 0)] = u1.clamp(1e-10, 1.0 - 1e-10);
105 samples[(i, 1)] = u2.clamp(1e-10, 1.0 - 1e-10);
106 }
107
108 Ok(samples)
109 }
110
111 fn dimension(&self) -> usize {
112 2
113 }
114}
115
116#[derive(Debug, Clone)]
118pub struct EmpiricalCopula {
119 data: DMatrix<f64>,
120}
121
122validated_serde!("EmpiricalCopula", EmpiricalCopula { data: DMatrix<f64> } => EmpiricalCopula::new(data));
123
124impl EmpiricalCopula {
125 pub fn new(data: DMatrix<f64>) -> Result<Self> {
127 crate::utils::validate_pseudo_observations(&data)?;
128 Ok(Self { data })
129 }
130}
131
132impl Copula for EmpiricalCopula {
133 fn cdf(&self, u: &[f64]) -> Result<f64> {
134 let (n_rows, n_cols) = self.data.shape();
135 if u.len() != n_cols {
136 return Err(CopulaError::dimension_mismatch(n_cols, u.len()));
137 }
138 crate::error::validate_unit_range(u)?;
139 let count = (0..n_rows)
140 .filter(|&i| (0..n_cols).all(|j| self.data[(i, j)] <= u[j]))
141 .count();
142 Ok(count as f64 / n_rows as f64)
143 }
144
145 fn pdf(&self, u: &[f64]) -> Result<f64> {
146 let (_n_rows, n_cols) = self.data.shape();
147 if u.len() != n_cols {
148 return Err(CopulaError::dimension_mismatch(n_cols, u.len()));
149 }
150 crate::error::validate_unit_range(u)?;
151
152 Err(CopulaError::not_implemented(
155 "Empirical copula PDF is not well-defined (discrete distribution). Use CDF instead or implement kernel density estimation."
156 ))
157 }
158
159 fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
160 let (n_rows, n_cols) = self.data.shape();
161 let mut samples = DMatrix::<f64>::zeros(n, n_cols);
162
163 let indices: Vec<usize> = (0..n_rows).collect();
165
166 for i in 0..n {
167 let &idx = indices
168 .choose(rng)
169 .ok_or_else(|| CopulaError::computation("failed to sample from indices"))?;
170
171 for j in 0..n_cols {
172 samples[(i, j)] = self.data[(idx, j)];
173 }
174 }
175
176 Ok(samples)
177 }
178
179 fn dimension(&self) -> usize {
180 self.data.ncols()
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn marshall_olkin_cdf_product() {
190 let cop = MarshallOlkinCopula::new(0.3, 0.4).unwrap();
191 let cdf = cop.cdf(&[0.4, 0.5]).unwrap();
192 let term1 = 0.4_f64.powf(0.7) * 0.5;
193 let term2 = 0.4 * 0.5_f64.powf(0.6);
194 let expected = term1.min(term2);
195 assert!((cdf - expected).abs() < 1e-12);
196 }
197
198 #[test]
199 fn empirical_cdf_product() {
200 let data = DMatrix::from_row_slice(3, 2, &[0.2, 0.3, 0.4, 0.6, 0.9, 0.8]);
201 let cop = EmpiricalCopula::new(data).unwrap();
202 let cdf = cop.cdf(&[0.5, 0.7]).unwrap();
203 assert!((cdf - 2.0 / 3.0).abs() < 1e-12);
205 }
206
207 #[test]
208 fn marshall_olkin_rejects_invalid_params() {
209 assert!(MarshallOlkinCopula::new(-0.1, 0.5).is_err());
210 assert!(MarshallOlkinCopula::new(0.5, 1.0).is_err());
211 assert!(MarshallOlkinCopula::new(1.0, 0.5).is_err());
212 }
213
214 #[test]
215 fn marshall_olkin_dimension() {
216 let cop = MarshallOlkinCopula::new(0.3, 0.4).unwrap();
217 assert_eq!(cop.dimension(), 2);
218 }
219
220 #[test]
221 fn marshall_olkin_cdf_validates_input() {
222 let cop = MarshallOlkinCopula::new(0.3, 0.4).unwrap();
223 assert!(cop.cdf(&[0.5]).is_err());
224 assert!(cop.cdf(&[0.5, 1.1]).is_err());
225 }
226
227 #[test]
228 fn marshall_olkin_sampling() {
229 let mut rng = rand::rng();
230 let cop = MarshallOlkinCopula::new(0.3, 0.4).unwrap();
231 let samples = cop.sample(50, &mut rng).unwrap();
232 assert_eq!(samples.nrows(), 50);
233 assert_eq!(samples.ncols(), 2);
234 for i in 0..50 {
235 for j in 0..2 {
236 let v = samples[(i, j)];
237 assert!(v > 0.0 && v < 1.0);
238 }
239 }
240 }
241
242 #[test]
243 fn empirical_copula_rejects_invalid_data() {
244 let data = DMatrix::from_row_slice(2, 2, &[1.0, 0.5, 0.3, 0.7]);
245 assert!(EmpiricalCopula::new(data).is_err()); }
247
248 #[test]
249 fn empirical_copula_dimension() {
250 let data = DMatrix::from_row_slice(3, 3, &[0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.1]);
251 let cop = EmpiricalCopula::new(data).unwrap();
252 assert_eq!(cop.dimension(), 3);
253 }
254
255 #[test]
256 fn empirical_copula_cdf_validates_dimension() {
257 let data = DMatrix::from_row_slice(3, 2, &[0.2, 0.3, 0.5, 0.6, 0.8, 0.9]);
258 let cop = EmpiricalCopula::new(data).unwrap();
259 assert!(cop.cdf(&[0.5]).is_err());
260 assert!(cop.cdf(&[0.5, 0.5, 0.5]).is_err());
261 }
262
263 #[test]
264 fn empirical_copula_pdf_not_implemented() {
265 let data = DMatrix::from_row_slice(3, 2, &[0.2, 0.3, 0.5, 0.6, 0.8, 0.9]);
266 let cop = EmpiricalCopula::new(data).unwrap();
267 assert!(cop.pdf(&[0.5, 0.5]).is_err());
268 }
269
270 #[test]
271 fn empirical_copula_sampling() {
272 let mut rng = rand::rng();
273 let data = DMatrix::from_row_slice(3, 2, &[0.2, 0.3, 0.5, 0.6, 0.8, 0.9]);
274 let cop = EmpiricalCopula::new(data).unwrap();
275 let samples = cop.sample(10, &mut rng).unwrap();
276 assert_eq!(samples.nrows(), 10);
277 assert_eq!(samples.ncols(), 2);
278 }
279}