Skip to main content

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}