Skip to main content

copula_core/
traits.rs

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