1use crate::{Copula, CopulaError, Result};
17use nalgebra::{DMatrix, DVector};
18use rand::Rng;
19use rand_distr::{Distribution, Normal};
20
21#[derive(Debug, Clone)]
30pub struct OneFactorGaussianCopula {
31 loadings: Vec<f64>,
33 dimension: usize,
34}
35
36impl OneFactorGaussianCopula {
37 pub fn new(loadings: Vec<f64>) -> Result<Self> {
45 if loadings.is_empty() {
46 return Err(CopulaError::invalid_parameter("loadings cannot be empty"));
47 }
48
49 for (i, &loading) in loadings.iter().enumerate() {
50 if loading < 0.0 || loading > 1.0 {
51 return Err(CopulaError::invalid_parameter(&format!(
52 "loading[{}] = {} must be in [0, 1]",
53 i, loading
54 )));
55 }
56 }
57
58 let dimension = loadings.len();
59 Ok(Self { loadings, dimension })
60 }
61
62 pub fn correlation(&self, i: usize, j: usize) -> Result<f64> {
66 if i >= self.dimension || j >= self.dimension {
67 return Err(CopulaError::invalid_parameter("index out of bounds"));
68 }
69 Ok(self.loadings[i] * self.loadings[j])
70 }
71
72 pub fn correlation_matrix(&self) -> DMatrix<f64> {
74 let d = self.dimension;
75 let mut corr = DMatrix::<f64>::zeros(d, d);
76
77 for i in 0..d {
78 for j in 0..d {
79 if i == j {
80 corr[(i, j)] = 1.0;
81 } else {
82 corr[(i, j)] = self.loadings[i] * self.loadings[j];
83 }
84 }
85 }
86
87 corr
88 }
89
90 fn phi(x: f64) -> f64 {
92 use statrs::distribution::{ContinuousCDF, Normal};
93 Normal::new(0.0, 1.0).expect("standard normal parameters are always valid").cdf(x)
94 }
95
96 fn phi_inv(p: f64) -> f64 {
98 use statrs::distribution::{ContinuousCDF, Normal};
99 Normal::new(0.0, 1.0).expect("standard normal parameters are always valid").inverse_cdf(p)
100 }
101}
102
103impl Copula for OneFactorGaussianCopula {
104 fn cdf(&self, u: &[f64]) -> Result<f64> {
105 if u.len() != self.dimension {
106 return Err(CopulaError::dimension_mismatch(self.dimension, u.len()));
107 }
108 crate::error::validate_unit_range(u)?;
109
110 let n_points = 50;
113 let z_min = -5.0;
114 let z_max = 5.0;
115 let h = (z_max - z_min) / (n_points - 1) as f64;
116
117 let mut integral = 0.0;
118
119 for k in 0..n_points {
120 let z = z_min + k as f64 * h;
121 let weight = if k == 0 || k == n_points - 1 { 0.5 } else { 1.0 };
122
123 let mut cond_prob = 1.0;
125 for i in 0..self.dimension {
126 let x_i = Self::phi_inv(u[i]);
127 let loading = self.loadings[i];
128 let idio_std = (1.0 - loading * loading).sqrt();
129
130 let cond_cdf = Self::phi((x_i - loading * z) / idio_std);
132 cond_prob *= cond_cdf;
133 }
134
135 let phi_z = (-z * z / 2.0).exp() / (2.0 * std::f64::consts::PI).sqrt();
137 integral += weight * cond_prob * phi_z;
138 }
139
140 Ok(integral * h)
141 }
142
143 fn pdf(&self, u: &[f64]) -> Result<f64> {
144 if u.len() != self.dimension {
145 return Err(CopulaError::dimension_mismatch(self.dimension, u.len()));
146 }
147 crate::error::validate_unit_range(u)?;
148
149 let h = 1e-6;
151 let mut grad_product = 1.0;
152
153 for i in 0..self.dimension {
154 let mut u_plus = u.to_vec();
155 u_plus[i] += h;
156
157 if u_plus[i] > 1.0 {
158 u_plus[i] = 1.0;
159 }
160
161 let cdf_plus = self.cdf(&u_plus)?;
162 let cdf_base = self.cdf(u)?;
163
164 grad_product *= (cdf_plus - cdf_base) / h;
165 }
166
167 Ok(grad_product.max(0.0))
168 }
169
170 fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
171 let normal = Normal::new(0.0, 1.0)
172 .map_err(|_| CopulaError::computation("failed to create Normal(0,1)"))?;
173
174 let mut samples = DMatrix::<f64>::zeros(n, self.dimension);
175
176 for i in 0..n {
177 let z: f64 = normal.sample(rng);
179
180 for j in 0..self.dimension {
182 let loading = self.loadings[j];
183 let idio_std = (1.0 - loading * loading).sqrt();
184
185 let epsilon: f64 = normal.sample(rng);
187
188 let x = loading * z + idio_std * epsilon;
190
191 samples[(i, j)] = Self::phi(x);
193 }
194 }
195
196 Ok(samples)
197 }
198
199 fn dimension(&self) -> usize {
200 self.dimension
201 }
202}
203
204#[derive(Debug, Clone)]
209pub struct MultiFactorGaussianCopula {
210 loadings: DMatrix<f64>,
212 dimension: usize,
213 num_factors: usize,
214}
215
216impl MultiFactorGaussianCopula {
217 pub fn new(loadings: DMatrix<f64>) -> Result<Self> {
225 let dimension = loadings.nrows();
226 let num_factors = loadings.ncols();
227
228 if dimension == 0 || num_factors == 0 {
229 return Err(CopulaError::invalid_parameter(
230 "loadings matrix cannot be empty",
231 ));
232 }
233
234 for i in 0..dimension {
236 let mut sum_sq = 0.0;
237 for k in 0..num_factors {
238 sum_sq += loadings[(i, k)].powi(2);
239 }
240 if sum_sq > 1.0 + 1e-10 {
241 return Err(CopulaError::invalid_parameter(&format!(
242 "row {} has sum of squared loadings > 1",
243 i
244 )));
245 }
246 }
247
248 Ok(Self {
249 loadings,
250 dimension,
251 num_factors,
252 })
253 }
254
255 pub fn correlation_matrix(&self) -> DMatrix<f64> {
257 let lambda_lambda_t = &self.loadings * self.loadings.transpose();
260
261 let mut corr = lambda_lambda_t;
262 for i in 0..self.dimension {
263 corr[(i, i)] = 1.0;
264 }
265
266 corr
267 }
268
269 fn phi(x: f64) -> f64 {
271 use statrs::distribution::{ContinuousCDF, Normal};
272 Normal::new(0.0, 1.0).expect("standard normal parameters are always valid").cdf(x)
273 }
274
275 fn phi_inv(p: f64) -> f64 {
277 use statrs::distribution::{ContinuousCDF, Normal};
278 Normal::new(0.0, 1.0).expect("standard normal parameters are always valid").inverse_cdf(p)
279 }
280}
281
282impl Copula for MultiFactorGaussianCopula {
283 fn cdf(&self, u: &[f64]) -> Result<f64> {
284 if u.len() != self.dimension {
285 return Err(CopulaError::dimension_mismatch(self.dimension, u.len()));
286 }
287 crate::error::validate_unit_range(u)?;
288
289 Err(CopulaError::not_implemented(
292 "Multi-factor CDF requires Monte Carlo integration",
293 ))
294 }
295
296 fn pdf(&self, u: &[f64]) -> Result<f64> {
297 if u.len() != self.dimension {
298 return Err(CopulaError::dimension_mismatch(self.dimension, u.len()));
299 }
300 crate::error::validate_unit_range(u)?;
301
302 Err(CopulaError::not_implemented(
303 "Multi-factor PDF requires Monte Carlo or numerical methods",
304 ))
305 }
306
307 fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
308 let normal = Normal::new(0.0, 1.0)
309 .map_err(|_| CopulaError::computation("failed to create Normal(0,1)"))?;
310
311 let mut samples = DMatrix::<f64>::zeros(n, self.dimension);
312
313 for i in 0..n {
314 let mut factors = DVector::<f64>::zeros(self.num_factors);
316 for k in 0..self.num_factors {
317 factors[k] = normal.sample(rng);
318 }
319
320 for j in 0..self.dimension {
322 let mut factor_contribution = 0.0;
324 let mut sum_sq_loadings = 0.0;
325
326 for k in 0..self.num_factors {
327 let loading = self.loadings[(j, k)];
328 factor_contribution += loading * factors[k];
329 sum_sq_loadings += loading * loading;
330 }
331
332 let idio_std = (1.0 - sum_sq_loadings).max(0.0).sqrt();
334
335 let epsilon: f64 = normal.sample(rng);
337
338 let x = factor_contribution + idio_std * epsilon;
340
341 samples[(i, j)] = Self::phi(x);
343 }
344 }
345
346 Ok(samples)
347 }
348
349 fn dimension(&self) -> usize {
350 self.dimension
351 }
352}
353
354#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn test_one_factor_new() {
360 let loadings = vec![0.5, 0.6, 0.7];
361 let cop = OneFactorGaussianCopula::new(loadings).unwrap();
362 assert_eq!(cop.dimension(), 3);
363 }
364
365 #[test]
366 fn test_one_factor_invalid_loading() {
367 let loadings = vec![0.5, 1.5]; assert!(OneFactorGaussianCopula::new(loadings).is_err());
369 }
370
371 #[test]
372 fn test_one_factor_correlation() {
373 let loadings = vec![0.6, 0.8];
374 let cop = OneFactorGaussianCopula::new(loadings).unwrap();
375 let rho = cop.correlation(0, 1).unwrap();
376 assert!((rho - 0.48).abs() < 1e-10); }
378
379 #[test]
380 fn test_one_factor_sample() {
381 use rand::thread_rng;
382 let mut rng = thread_rng();
383
384 let loadings = vec![0.7, 0.7, 0.7];
385 let cop = OneFactorGaussianCopula::new(loadings).unwrap();
386 let samples = cop.sample(100, &mut rng).unwrap();
387
388 assert_eq!(samples.nrows(), 100);
389 assert_eq!(samples.ncols(), 3);
390
391 for i in 0..100 {
393 for j in 0..3 {
394 assert!(samples[(i, j)] >= 0.0 && samples[(i, j)] <= 1.0);
395 }
396 }
397 }
398
399 #[test]
400 fn test_multi_factor_new() {
401 let loadings = DMatrix::from_row_slice(3, 2, &[
402 0.5, 0.3, 0.6, 0.4, 0.7, 0.2, ]);
406 let cop = MultiFactorGaussianCopula::new(loadings).unwrap();
407 assert_eq!(cop.dimension(), 3);
408 }
409
410 #[test]
411 fn test_multi_factor_sample() {
412 use rand::thread_rng;
413 let mut rng = thread_rng();
414
415 let loadings = DMatrix::from_row_slice(2, 2, &[
416 0.6, 0.3,
417 0.5, 0.4,
418 ]);
419 let cop = MultiFactorGaussianCopula::new(loadings).unwrap();
420 let samples = cop.sample(50, &mut rng).unwrap();
421
422 assert_eq!(samples.nrows(), 50);
423 assert_eq!(samples.ncols(), 2);
424
425 for i in 0..50 {
427 for j in 0..2 {
428 assert!(samples[(i, j)] >= 0.0 && samples[(i, j)] <= 1.0);
429 }
430 }
431 }
432}