copula_core/traits.rs
1// src/traits.rs
2
3//! Core traits that define the interface for all copula types.
4//!
5//! This module defines the fundamental traits that all copulas must implement,
6//! as well as specialized traits for specific copula families and capabilities.
7
8use crate::error::{CopulaError, Result};
9use nalgebra::DMatrix;
10use rand::Rng;
11
12#[cfg(feature = "serde")]
13use serde::{Deserialize, Serialize};
14
15/// Core trait that all copulas must implement.
16///
17/// This trait defines the essential operations that any copula must support:
18/// evaluating the cumulative distribution function (CDF), probability density
19/// function (PDF), generating random samples, and providing dimension information.
20///
21/// # Mathematical Background
22///
23/// A copula C: [0,1]ⁿ → [0,1] is a multivariate distribution function whose
24/// univariate margins are uniform on [0,1]. For any n-dimensional copula:
25///
26/// 1. **Grounding**: C(u₁, ..., uᵢ₋₁, 0, uᵢ₊₁, ..., uₙ) = 0
27/// 2. **Marginality**: C(1, ..., 1, uᵢ, 1, ..., 1) = uᵢ
28/// 3. **2-increasing**: For all rectangles in [0,1]ⁿ, the C-volume is non-negative
29///
30/// # Examples
31///
32/// ```rust
33/// use copula_core::{Copula, ClaytonCopula};
34/// use rand::thread_rng;
35///
36/// let copula = ClaytonCopula::new(2.0)?;
37///
38/// // Evaluate CDF
39/// let cdf = copula.cdf(&[0.5, 0.7])?;
40///
41/// // Evaluate PDF
42/// let pdf = copula.pdf(&[0.5, 0.7])?;
43///
44/// // Generate samples
45/// let mut rng = thread_rng();
46/// let samples = copula.sample(100, &mut rng)?;
47/// # Ok::<(), copula_core::CopulaError>(())
48/// ```
49pub trait Copula {
50 /// Evaluate the copula cumulative distribution function (CDF) at point u.
51 ///
52 /// For a bivariate copula, this computes C(u₁, u₂) = P(U₁ ≤ u₁, U₂ ≤ u₂)
53 /// where U₁, U₂ are uniform random variables with the copula dependence structure.
54 ///
55 /// # Arguments
56 ///
57 /// * `u` - Point at which to evaluate the CDF. All values must be in [0,1].
58 ///
59 /// # Returns
60 ///
61 /// The CDF value C(u), which is in [0,1].
62 ///
63 /// # Errors
64 ///
65 /// Returns [`CopulaError::InvalidRange`] if any value in `u` is outside [0,1].
66 /// Returns [`CopulaError::DimensionMismatch`] if the length of `u` doesn't match
67 /// the copula's dimension.
68 fn cdf(&self, u: &[f64]) -> Result<f64>;
69
70 /// Evaluate the copula probability density function (PDF) at point u.
71 ///
72 /// For a bivariate copula, this computes c(u₁, u₂) = ∂²C(u₁, u₂)/(∂u₁∂u₂).
73 ///
74 /// # Arguments
75 ///
76 /// * `u` - Point at which to evaluate the PDF. All values must be in [0,1].
77 ///
78 /// # Returns
79 ///
80 /// The PDF value c(u), which is non-negative.
81 ///
82 /// # Errors
83 ///
84 /// Returns [`CopulaError::InvalidRange`] if any value in `u` is outside [0,1].
85 /// Returns [`CopulaError::DimensionMismatch`] if the length of `u` doesn't match
86 /// the copula's dimension.
87 fn pdf(&self, u: &[f64]) -> Result<f64>;
88
89 /// Generate random samples from the copula.
90 ///
91 /// # Arguments
92 ///
93 /// * `n` - Number of samples to generate
94 /// * `rng` - Random number generator
95 ///
96 /// # Returns
97 ///
98 /// An n × d matrix where each row is a sample from the copula and d is the dimension.
99 ///
100 /// # Errors
101 ///
102 /// Returns [`CopulaError::NumericalError`] if sampling fails due to numerical issues.
103 fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>>;
104
105 /// Get the dimension of the copula.
106 ///
107 /// # Returns
108 ///
109 /// The number of variables (dimension) of the copula.
110 fn dimension(&self) -> usize;
111
112 /// Compute the conditional copula CDF given some variables.
113 ///
114 /// This computes C(u₁, ..., uₙ | uⱼ for j ∈ given), which is needed for
115 /// vine copula constructions and conditional sampling.
116 ///
117 /// # Arguments
118 ///
119 /// * `u` - Point at which to evaluate the conditional CDF
120 /// * `given` - Indices of variables to condition on
121 ///
122 /// # Returns
123 ///
124 /// The conditional CDF value.
125 ///
126 /// # Errors
127 ///
128 /// Returns [`CopulaError::NotImplemented`] if the copula doesn't support
129 /// conditional evaluation.
130 fn conditional_cdf(&self, u: &[f64], given: &[usize]) -> Result<f64> {
131 let _ = (u, given);
132 Err(CopulaError::not_implemented(format!(
133 "conditional_cdf for {}",
134 std::any::type_name::<Self>()
135 )))
136 }
137
138 /// Compute the tail dependence coefficients.
139 ///
140 /// For a bivariate copula, the tail dependence coefficients are:
141 /// - Lower tail: λₗ = lim_{t→0⁺} C(t,t)/t
142 /// - Upper tail: λᵤ = lim_{t→1⁻} (1-2t+C(t,t))/(1-t)
143 ///
144 /// # Returns
145 ///
146 /// A tuple (λₗ, λᵤ) of lower and upper tail dependence coefficients,
147 /// each in [0,1]. A value of 0 indicates no tail dependence.
148 ///
149 /// # Errors
150 ///
151 /// Returns [`CopulaError::NotImplemented`] if tail dependence computation
152 /// is not available for this copula family.
153 fn tail_dependence(&self) -> Result<(f64, f64)> {
154 Err(CopulaError::not_implemented(format!(
155 "tail_dependence for {}",
156 std::any::type_name::<Self>()
157 )))
158 }
159
160 /// Compute Kendall's tau for this copula.
161 ///
162 /// Kendall's tau is a measure of rank correlation that can be computed
163 /// analytically for many copula families.
164 ///
165 /// # Returns
166 ///
167 /// Kendall's tau coefficient in [-1, 1].
168 ///
169 /// # Errors
170 ///
171 /// Returns [`CopulaError::NotImplemented`] if analytical computation
172 /// is not available. In this case, users should estimate it from samples.
173 fn kendall_tau(&self) -> Result<f64> {
174 Err(CopulaError::not_implemented(format!(
175 "kendall_tau for {}",
176 std::any::type_name::<Self>()
177 )))
178 }
179
180 /// Compute Spearman's rho for this copula.
181 ///
182 /// Spearman's rho is another measure of rank correlation.
183 ///
184 /// # Returns
185 ///
186 /// Spearman's rho coefficient in [-1, 1].
187 ///
188 /// # Errors
189 ///
190 /// Returns [`CopulaError::NotImplemented`] if analytical computation
191 /// is not available.
192 fn spearman_rho(&self) -> Result<f64> {
193 Err(CopulaError::not_implemented(format!(
194 "spearman_rho for {}",
195 std::any::type_name::<Self>()
196 )))
197 }
198
199 /// Check if the copula has analytical forms for CDF and PDF.
200 ///
201 /// Some copulas may only have closed-form expressions for certain operations.
202 fn has_closed_form(&self) -> (bool, bool) {
203 (true, true) // Default assumption: both CDF and PDF are available
204 }
205
206 /// Get a string identifier for the copula family.
207 fn family_name(&self) -> &'static str {
208 std::any::type_name::<Self>()
209 }
210}
211
212/// Trait for copulas that can be fitted to data.
213///
214/// This trait extends the basic [`Copula`] trait with parameter estimation
215/// capabilities. Copulas implementing this trait can learn their parameters
216/// from observed data.
217///
218/// # Examples
219///
220/// ```rust
221/// use copula_core::{FittableCopula, GaussianCopula, to_pseudo_observations};
222/// use nalgebra::DMatrix;
223///
224/// // Create copula and fit to data
225/// let mut copula = GaussianCopula::new_identity(2)?;
226/// let data = DMatrix::from_row_slice(100, 2, &[/* your data */]);
227/// let pseudo_obs = to_pseudo_observations(&data);
228///
229/// let params = copula.fit(&pseudo_obs)?;
230/// println!("Fitted parameters: {:?}", params);
231/// # Ok::<(), copula_core::CopulaError>(())
232/// ```
233#[cfg(feature = "estimation")]
234#[cfg_attr(docsrs, doc(cfg(feature = "estimation")))]
235pub trait FittableCopula: Copula {
236 /// Type representing the copula's parameters.
237 ///
238 /// This could be a single value (for one-parameter families like Clayton),
239 /// a matrix (for Gaussian copulas), or a more complex structure.
240 type Parameters: Clone + std::fmt::Debug;
241
242 /// Fit copula parameters to pseudo-observations using maximum likelihood estimation.
243 ///
244 /// The input data should be transformed to pseudo-observations (uniform margins)
245 /// before fitting. Use [`crate::to_pseudo_observations`] for this transformation.
246 ///
247 /// # Arguments
248 ///
249 /// * `pseudo_obs` - Matrix of pseudo-observations where each row is an observation
250 /// and each column is a variable. All values should be in (0,1).
251 ///
252 /// # Returns
253 ///
254 /// The estimated parameters.
255 ///
256 /// # Errors
257 ///
258 /// Returns [`CopulaError::OptimizationError`] if the optimization fails to converge.
259 /// Returns [`CopulaError::DataError`] if the data is invalid.
260 fn fit(&mut self, pseudo_obs: &DMatrix<f64>) -> Result<Self::Parameters>;
261
262 /// Compute the log-likelihood of the data given current parameters.
263 ///
264 /// # Arguments
265 ///
266 /// * `pseudo_obs` - Matrix of pseudo-observations
267 ///
268 /// # Returns
269 ///
270 /// The log-likelihood value.
271 fn log_likelihood(&self, pseudo_obs: &DMatrix<f64>) -> Result<f64>;
272
273 /// Fit parameters using method of moments.
274 ///
275 /// This is often faster than MLE but may be less efficient statistically.
276 ///
277 /// # Arguments
278 ///
279 /// * `pseudo_obs` - Matrix of pseudo-observations
280 ///
281 /// # Returns
282 ///
283 /// The estimated parameters.
284 fn fit_moments(&mut self, pseudo_obs: &DMatrix<f64>) -> Result<Self::Parameters> {
285 // Default implementation falls back to MLE
286 self.fit(pseudo_obs)
287 }
288
289 /// Get current parameters of the copula.
290 fn parameters(&self) -> Self::Parameters;
291
292 /// Set parameters of the copula.
293 ///
294 /// # Arguments
295 ///
296 /// * `params` - New parameters to set
297 ///
298 /// # Errors
299 ///
300 /// Returns [`CopulaError::InvalidParameter`] if parameters are invalid.
301 fn set_parameters(&mut self, params: Self::Parameters) -> Result<()>;
302
303 /// Compute standard errors of parameter estimates.
304 ///
305 /// This typically uses the Fisher information matrix from MLE.
306 ///
307 /// # Arguments
308 ///
309 /// * `pseudo_obs` - The data used for estimation
310 ///
311 /// # Returns
312 ///
313 /// Standard errors corresponding to the parameters.
314 fn standard_errors(&self, _pseudo_obs: &DMatrix<f64>) -> Result<Self::Parameters> {
315 Err(CopulaError::not_implemented("standard_errors"))
316 }
317
318 /// Compute confidence intervals for parameters.
319 ///
320 /// # Arguments
321 ///
322 /// * `pseudo_obs` - The data used for estimation
323 /// * `confidence_level` - Confidence level (e.g., 0.95 for 95% CI)
324 ///
325 /// # Returns
326 ///
327 /// Confidence intervals as (lower, upper) bounds.
328 fn confidence_intervals(
329 &self,
330 _pseudo_obs: &DMatrix<f64>,
331 _confidence_level: f64,
332 ) -> Result<(Self::Parameters, Self::Parameters)> {
333 Err(CopulaError::not_implemented("confidence_intervals"))
334 }
335}
336
337/// Trait for Archimedean copulas.
338///
339/// Archimedean copulas are defined by a generator function φ: [0,1] → [0,∞]
340/// such that C(u₁, ..., uₙ) = φ⁻¹(φ(u₁) + ... + φ(uₙ)).
341///
342/// This trait provides access to the generator function and its properties.
343///
344/// # Mathematical Background
345///
346/// The generator φ must satisfy:
347/// 1. φ(1) = 0
348/// 2. φ'(t) < 0 for t ∈ (0,1) (strictly decreasing)
349/// 3. φ''(t) > 0 for t ∈ (0,1) (convex)
350///
351/// # Examples
352///
353/// ```rust
354/// use copula_core::{ArchimedeanCopula, ClaytonCopula};
355///
356/// let copula = ClaytonCopula::new(2.0)?;
357///
358/// // Evaluate generator function
359/// let phi_val = copula.phi(0.5)?;
360///
361/// // Evaluate inverse generator
362/// let phi_inv_val = copula.phi_inv(1.0)?;
363/// # Ok::<(), copula_core::CopulaError>(())
364/// ```
365pub trait ArchimedeanCopula: Copula {
366 /// Evaluate the generator function φ(t).
367 ///
368 /// # Arguments
369 ///
370 /// * `t` - Value in [0,1] at which to evaluate φ
371 ///
372 /// # Returns
373 ///
374 /// φ(t) ∈ [0,∞]
375 fn phi(&self, t: f64) -> Result<f64>;
376
377 /// Evaluate the inverse generator function φ⁻¹(s).
378 ///
379 /// # Arguments
380 ///
381 /// * `s` - Value in [0,∞] at which to evaluate φ⁻¹
382 ///
383 /// # Returns
384 ///
385 /// φ⁻¹(s) ∈ [0,1]
386 fn phi_inv(&self, s: f64) -> Result<f64>;
387
388 /// Evaluate the k-th derivative of the inverse generator φ⁻¹.
389 ///
390 /// This is needed for computing PDFs and higher-order derivatives.
391 ///
392 /// # Arguments
393 ///
394 /// * `s` - Value at which to evaluate the derivative
395 /// * `k` - Order of derivative (1 for first derivative, 2 for second, etc.)
396 ///
397 /// # Returns
398 ///
399 /// The k-th derivative of φ⁻¹ at s.
400 fn phi_inv_deriv(&self, s: f64, k: usize) -> Result<f64>;
401
402 /// Check if the generator satisfies Archimedean properties.
403 ///
404 /// This can be used for validation during construction.
405 fn validate_generator(&self) -> Result<()> {
406 // Check φ(1) = 0
407 let phi_1 = self.phi(1.0)?;
408 if (phi_1).abs() > 1e-10 {
409 return Err(CopulaError::invalid_parameter(
410 "Generator function must satisfy φ(1) = 0",
411 ));
412 }
413
414 // Check φ(0) = ∞ (or very large)
415 let phi_0 = self.phi(1e-10)?;
416 if !phi_0.is_infinite() && phi_0 < 1e6 {
417 return Err(CopulaError::invalid_parameter(
418 "Generator function must satisfy φ(0) = ∞",
419 ));
420 }
421
422 Ok(())
423 }
424
425 /// Get the parameter value(s) for single-parameter Archimedean families.
426 ///
427 /// Many Archimedean copulas are single-parameter families.
428 fn parameter(&self) -> f64 {
429 f64::NAN // Default for multi-parameter families
430 }
431}
432
433/// Trait for extreme value copulas.
434///
435/// Extreme value copulas arise as limits of copulas of component-wise maxima.
436/// They are characterized by their Pickands dependence function.
437pub trait ExtremeValueCopula: Copula {
438 /// Evaluate the Pickands dependence function A(t).
439 ///
440 /// The Pickands function satisfies:
441 /// 1. A(0) = A(1) = 1
442 /// 2. max(t, 1-t) ≤ A(t) ≤ 1 for t ∈ [0,1]
443 /// 3. A is convex
444 ///
445 /// # Arguments
446 ///
447 /// * `t` - Value in [0,1]
448 ///
449 /// # Returns
450 ///
451 /// A(t) ∈ [0.5, 1]
452 fn pickands_function(&self, t: f64) -> Result<f64>;
453
454 /// Check if the Pickands function is valid.
455 fn validate_pickands(&self) -> Result<()> {
456 // Check boundary conditions
457 let a_0 = self.pickands_function(0.0)?;
458 let a_1 = self.pickands_function(1.0)?;
459
460 if (a_0 - 1.0).abs() > 1e-10 || (a_1 - 1.0).abs() > 1e-10 {
461 return Err(CopulaError::invalid_parameter(
462 "Pickands function must satisfy A(0) = A(1) = 1",
463 ));
464 }
465
466 Ok(())
467 }
468}
469
470/// Trait for copulas that support vine constructions.
471///
472/// Vine copulas build high-dimensional distributions from bivariate copulas
473/// arranged in a tree structure. This trait provides the necessary operations
474/// for vine decomposition and construction.
475pub trait VineCopula: Copula {
476 /// Compute h-function: h(u|v) = ∂C(u,v)/∂v.
477 ///
478 /// This is the conditional distribution function needed for vine sampling.
479 ///
480 /// # Arguments
481 ///
482 /// * `u` - First variable
483 /// * `v` - Second variable (conditioning variable)
484 ///
485 /// # Returns
486 ///
487 /// h(u|v) = P(U ≤ u | V = v)
488 fn h_function(&self, u: f64, v: f64) -> Result<f64>;
489
490 /// Compute inverse h-function: h⁻¹(p|v).
491 ///
492 /// This inverts the h-function and is needed for vine sampling.
493 ///
494 /// # Arguments
495 ///
496 /// * `p` - Probability value in [0,1]
497 /// * `v` - Conditioning variable
498 ///
499 /// # Returns
500 ///
501 /// u such that h(u|v) = p
502 fn h_function_inv(&self, p: f64, v: f64) -> Result<f64>;
503}
504
505/// Trait for meta-distributions that can use any copula.
506///
507/// This allows for constructions like meta-elliptical distributions where
508/// the dependence structure is specified by a copula.
509pub trait MetaDistribution {
510 /// Type of the underlying copula
511 type CopulaType: Copula;
512
513 /// Get reference to the underlying copula
514 fn copula(&self) -> &Self::CopulaType;
515
516 /// Get mutable reference to the underlying copula
517 fn copula_mut(&mut self) -> &mut Self::CopulaType;
518}
519
520/// Marker trait for copulas that have symmetric dependence structure.
521///
522/// Symmetric copulas satisfy C(u₁, u₂) = C(u₂, u₁).
523pub trait SymmetricCopula: Copula {}
524
525/// Marker trait for copulas that are exchangeable.
526///
527/// Exchangeable copulas have the same dependence structure regardless
528/// of variable ordering.
529pub trait ExchangeableCopula: Copula {}
530
531/// Trait for copulas that support parameter bounds and constraints.
532pub trait BoundedParameters {
533 /// Get the valid parameter bounds as (min, max) pairs.
534 fn parameter_bounds() -> Vec<(f64, f64)>;
535
536 /// Check if parameters are within valid bounds.
537 fn check_bounds(&self) -> Result<()>;
538}
539
540/// Trait for serializable copulas.
541///
542/// This enables saving and loading copula models.
543#[cfg(feature = "serde")]
544#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
545pub trait SerializableCopula: Copula + Serialize + for<'de> Deserialize<'de> {
546 /// Serialize the copula to a JSON string.
547 fn to_json(&self) -> Result<String> {
548 serde_json::to_string(self).map_err(|e| CopulaError::SerializationError {
549 message: format!("JSON serialization failed: {}", e),
550 })
551 }
552
553 /// Deserialize a copula from a JSON string.
554 fn from_json(json: &str) -> Result<Self>
555 where
556 Self: Sized,
557 {
558 serde_json::from_str(json).map_err(|e| CopulaError::SerializationError {
559 message: format!("JSON deserialization failed: {}", e),
560 })
561 }
562}
563
564/// Utility trait for converting between different copula parameter representations.
565pub trait ParameterConversion<T> {
566 /// Convert from Kendall's tau to copula parameters.
567 fn from_kendall_tau(tau: f64) -> Result<T>;
568
569 /// Convert from Spearman's rho to copula parameters.
570 fn from_spearman_rho(rho: f64) -> Result<T>;
571
572 /// Convert from copula parameters to Kendall's tau.
573 fn to_kendall_tau(&self) -> Result<f64>;
574
575 /// Convert from copula parameters to Spearman's rho.
576 fn to_spearman_rho(&self) -> Result<f64>;
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 // Mock copula for testing trait implementations
584 struct MockCopula {
585 dimension: usize,
586 }
587
588 impl Copula for MockCopula {
589 fn cdf(&self, u: &[f64]) -> Result<f64> {
590 if u.len() != self.dimension {
591 return Err(CopulaError::dimension_mismatch(self.dimension, u.len()));
592 }
593 Ok(u.iter().product()) // Independence copula
594 }
595
596 fn pdf(&self, u: &[f64]) -> Result<f64> {
597 if u.len() != self.dimension {
598 return Err(CopulaError::dimension_mismatch(self.dimension, u.len()));
599 }
600 Ok(1.0) // Independence copula
601 }
602
603 fn sample<R: Rng + ?Sized>(&self, n: usize, rng: &mut R) -> Result<DMatrix<f64>> {
604 use rand_distr::{Distribution, Uniform};
605
606 let uniform = Uniform::new(0.0, 1.0);
607 let mut samples = DMatrix::<f64>::zeros(n, self.dimension);
608
609 for i in 0..n {
610 for j in 0..self.dimension {
611 samples[(i, j)] = uniform.sample(rng);
612 }
613 }
614
615 Ok(samples)
616 }
617
618 fn dimension(&self) -> usize {
619 self.dimension
620 }
621
622 fn family_name(&self) -> &'static str {
623 "Mock"
624 }
625 }
626
627 #[test]
628 fn test_mock_copula_basic_operations() {
629 let copula = MockCopula { dimension: 2 };
630
631 // Test CDF
632 let cdf = copula.cdf(&[0.5, 0.5]).unwrap();
633 assert_eq!(cdf, 0.25);
634
635 // Test PDF
636 let pdf = copula.pdf(&[0.5, 0.5]).unwrap();
637 assert_eq!(pdf, 1.0);
638
639 // Test dimension
640 assert_eq!(copula.dimension(), 2);
641
642 // Test dimension mismatch
643 assert!(copula.cdf(&[0.5]).is_err());
644 }
645
646 #[test]
647 fn test_trait_default_implementations() {
648 let copula = MockCopula { dimension: 2 };
649
650 // Test default implementations return NotImplemented
651 assert!(copula.conditional_cdf(&[0.5, 0.5], &[0]).is_err());
652 assert!(copula.tail_dependence().is_err());
653 assert!(copula.kendall_tau().is_err());
654 assert!(copula.spearman_rho().is_err());
655 }
656}