use std::f64;
use stochastic_rs_distributions::special::beta_i;
use stochastic_rs_distributions::special::ln_gamma;
use stochastic_rs_distributions::special::ndtri;
use stochastic_rs_distributions::special::norm_cdf;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PairCopula {
Independence,
Gaussian {
rho: f64,
},
Clayton {
theta: f64,
},
Frank {
theta: f64,
},
StudentT {
rho: f64,
nu: f64,
},
}
impl PairCopula {
pub fn h(&self, u: f64, v: f64) -> f64 {
let u = u.clamp(EPS, 1.0 - EPS);
let v = v.clamp(EPS, 1.0 - EPS);
let h = match *self {
PairCopula::Independence => u,
PairCopula::Gaussian { rho } => {
let x = ndtri(u);
let y = ndtri(v);
let arg = (x - rho * y) / (1.0 - rho * rho).sqrt();
norm_cdf(arg)
}
PairCopula::Clayton { theta } => {
let s = u.powf(-theta) + v.powf(-theta) - 1.0;
v.powf(-theta - 1.0) * s.powf(-(theta + 1.0) / theta)
}
PairCopula::Frank { theta } => {
let eu = (-theta * u).exp();
let ev = (-theta * v).exp();
let e1 = (-theta).exp();
let num = ev * (eu - 1.0);
let den = (e1 - 1.0) + (eu - 1.0) * (ev - 1.0);
num / den
}
PairCopula::StudentT { rho, nu } => {
let x = student_t_quantile(u, nu);
let y = student_t_quantile(v, nu);
let scale = ((nu + 1.0) / ((nu + y * y) * (1.0 - rho * rho))).sqrt();
student_t_cdf((x - rho * y) * scale, nu + 1.0)
}
};
h.clamp(EPS, 1.0 - EPS)
}
pub fn h_inverse(&self, p: f64, v: f64) -> f64 {
let p = p.clamp(EPS, 1.0 - EPS);
let v = v.clamp(EPS, 1.0 - EPS);
let u = match *self {
PairCopula::Independence => p,
PairCopula::Gaussian { rho } => {
let z = ndtri(p);
let y = ndtri(v);
let arg = z * (1.0 - rho * rho).sqrt() + rho * y;
norm_cdf(arg)
}
PairCopula::Clayton { theta } => {
let base = p * v.powf(theta + 1.0);
let inner = base.powf(-theta / (theta + 1.0)) - v.powf(-theta) + 1.0;
inner.max(1.0 + EPS).powf(-1.0 / theta)
}
PairCopula::Frank { theta } => {
let y = (-theta * v).exp() - 1.0;
let a = (-theta).exp() - 1.0;
let x_val = p * a / (1.0 + y * (1.0 - p));
let arg = x_val + 1.0;
if arg <= 0.0 {
return EPS;
}
-arg.ln() / theta
}
PairCopula::StudentT { rho, nu } => {
let z = student_t_quantile(p, nu + 1.0);
let y = student_t_quantile(v, nu);
let scale = ((nu + y * y) * (1.0 - rho * rho) / (nu + 1.0)).sqrt();
let x = z * scale + rho * y;
student_t_cdf(x, nu)
}
};
u.clamp(EPS, 1.0 - EPS)
}
pub fn log_density(&self, u: f64, v: f64) -> f64 {
let u = u.clamp(EPS, 1.0 - EPS);
let v = v.clamp(EPS, 1.0 - EPS);
match *self {
PairCopula::Independence => 0.0,
PairCopula::Gaussian { rho } => {
let x = ndtri(u);
let y = ndtri(v);
let one_minus_rho2 = 1.0 - rho * rho;
let q = rho * rho * (x * x + y * y) - 2.0 * rho * x * y;
-0.5 * one_minus_rho2.ln() - 0.5 * q / one_minus_rho2
}
PairCopula::Clayton { theta } => {
let s = u.powf(-theta) + v.powf(-theta) - 1.0;
(1.0 + theta).ln()
+ (-theta - 1.0) * u.ln()
+ (-theta - 1.0) * v.ln()
+ (-1.0 / theta - 2.0) * s.ln()
}
PairCopula::Frank { theta } => {
let a = 1.0 - (-theta).exp();
let eu = (-theta * u).exp();
let ev = (-theta * v).exp();
let denom = a - (1.0 - eu) * (1.0 - ev);
(theta * a).ln() + (-theta * (u + v)) - 2.0 * denom.abs().ln()
}
PairCopula::StudentT { rho, nu } => {
let x = student_t_quantile(u, nu);
let y = student_t_quantile(v, nu);
let one_minus_rho2 = 1.0 - rho * rho;
let log_norm = ln_gamma(0.5 * (nu + 2.0)) + ln_gamma(0.5 * nu)
- 2.0 * ln_gamma(0.5 * (nu + 1.0))
- 0.5 * one_minus_rho2.ln();
let log_joint = -0.5
* (nu + 2.0)
* (1.0 + (x * x - 2.0 * rho * x * y + y * y) / (nu * one_minus_rho2)).ln();
let log_marg = 0.5 * (nu + 1.0) * ((1.0 + x * x / nu).ln() + (1.0 + y * y / nu).ln());
log_norm + log_joint + log_marg
}
}
}
pub fn density(&self, u: f64, v: f64) -> f64 {
self.log_density(u, v).exp()
}
}
const EPS: f64 = 1e-12;
fn student_t_cdf(x: f64, nu: f64) -> f64 {
if !x.is_finite() {
return if x > 0.0 { 1.0 } else { 0.0 };
}
let t = nu / (nu + x * x);
let half = 0.5 * beta_i(0.5 * nu, 0.5, t);
if x >= 0.0 { 1.0 - half } else { half }
}
fn student_t_log_pdf(x: f64, nu: f64) -> f64 {
let log_norm =
ln_gamma(0.5 * (nu + 1.0)) - 0.5 * (nu * f64::consts::PI).ln() - ln_gamma(0.5 * nu);
let log_kernel = -0.5 * (nu + 1.0) * (1.0 + x * x / nu).ln();
log_norm + log_kernel
}
fn student_t_quantile(p: f64, nu: f64) -> f64 {
if p <= 0.0 {
return f64::NEG_INFINITY;
}
if p >= 1.0 {
return f64::INFINITY;
}
let z = ndtri(p);
let mut x = z * (1.0 + (z * z + 1.0) / (4.0 * nu));
for _ in 0..40 {
let cdf = student_t_cdf(x, nu);
let f = cdf - p;
let pdf = student_t_log_pdf(x, nu).exp();
if pdf <= 0.0 {
break;
}
let dx = f / pdf;
let new_x = x - dx;
if (new_x - x).abs() < 1e-14 * (1.0 + x.abs()) {
return new_x;
}
x = new_x;
}
x
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn independence_h_identity() {
let p = PairCopula::Independence;
for u in [0.1, 0.3, 0.5, 0.7, 0.9] {
for v in [0.1, 0.5, 0.9] {
assert!((p.h(u, v) - u).abs() < 1e-12);
assert!((p.h_inverse(u, v) - u).abs() < 1e-12);
}
}
}
#[test]
fn h_inverse_round_trips_all_families() {
let cases: Vec<(PairCopula, f64)> = vec![
(PairCopula::Gaussian { rho: 0.5 }, 1e-6),
(PairCopula::Gaussian { rho: -0.3 }, 1e-6),
(PairCopula::Clayton { theta: 2.0 }, 1e-10),
(PairCopula::Clayton { theta: 0.5 }, 1e-10),
(PairCopula::Frank { theta: 3.0 }, 1e-10),
(PairCopula::Frank { theta: -2.0 }, 1e-10),
(PairCopula::StudentT { rho: 0.4, nu: 4.0 }, 1e-6),
(PairCopula::StudentT { rho: -0.5, nu: 8.0 }, 1e-6),
];
for (cop, tol) in cases {
for u in [0.1, 0.25, 0.5, 0.75, 0.9] {
for v in [0.1, 0.5, 0.9] {
let p = cop.h(u, v);
let u_back = cop.h_inverse(p, v);
assert!(
(u_back - u).abs() < tol,
"{cop:?}: u={u}, v={v}, h(u|v)={p}, h⁻¹(p|v)={u_back}, err={}",
(u_back - u).abs()
);
}
}
}
}
#[test]
fn gaussian_h_at_zero_rho_is_identity() {
let p = PairCopula::Gaussian { rho: 0.0 };
for u in [0.1, 0.5, 0.9] {
assert!(
(p.h(u, 0.4) - u).abs() < 1e-6,
"h(u={u}, v=0.4, ρ=0) = {} should ≈ {u}",
p.h(u, 0.4)
);
}
}
#[test]
fn clayton_density_closed_form() {
let p = PairCopula::Clayton { theta: 2.0 };
let theta: f64 = 2.0;
let u = 0.5_f64;
let v = 0.5_f64;
let s = u.powf(-theta) + v.powf(-theta) - 1.0;
let expected = (1.0 + theta) * (u * v).powf(-theta - 1.0) * s.powf(-1.0 / theta - 2.0);
let got = p.density(u, v);
assert!(
(got - expected).abs() / expected < 1e-10,
"Clayton(θ=2) density at (0.5, 0.5): got {got}, expected {expected}"
);
}
#[test]
fn student_t_h_large_nu_approaches_gaussian() {
let g = PairCopula::Gaussian { rho: 0.4 };
let t = PairCopula::StudentT {
rho: 0.4,
nu: 200.0,
};
for u in [0.2, 0.5, 0.8] {
for v in [0.2, 0.5, 0.8] {
let hg = g.h(u, v);
let ht = t.h(u, v);
assert!(
(ht - hg).abs() < 0.01,
"ν=200 t-h({u},{v})={ht} vs Gaussian-h={hg}"
);
}
}
}
}