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}