use crate::archimedean_math::frank;
use crate::errors::{CopulaError, FitError};
pub fn theta_from_tau(tau: f64) -> Result<f64, CopulaError> {
if !tau.is_finite() || tau <= 0.0 || tau >= 1.0 {
return Err(FitError::Failed {
reason: "frank pair fit requires tau in (0, 1)",
}
.into());
}
frank::invert_tau(tau, "frank tau inversion failed to bracket root")
}
fn log_one_minus_exp_neg(x: f64) -> f64 {
if x.is_nan() || x <= 0.0 {
return f64::NEG_INFINITY;
}
if x > std::f64::consts::LN_2 {
(-((-x).exp())).ln_1p()
} else {
(-((-x).exp_m1())).ln()
}
}
fn logsumexp2(a: f64, b: f64) -> f64 {
if a == f64::NEG_INFINITY {
return b;
}
if b == f64::NEG_INFINITY {
return a;
}
let m = a.max(b);
m + ((a - m).exp() + (b - m).exp()).ln()
}
pub fn log_pdf(u1: f64, u2: f64, theta: f64) -> Result<f64, CopulaError> {
if !theta.is_finite() || theta <= 0.0 {
return Err(FitError::Failed {
reason: "frank pair theta must be positive",
}
.into());
}
let log_t1 = -theta * u1 + log_one_minus_exp_neg(theta * u2);
let log_t2 = -theta * u2 + log_one_minus_exp_neg(theta * (1.0 - u2));
let log_den = logsumexp2(log_t1, log_t2);
let log_d = log_one_minus_exp_neg(theta);
Ok(theta.ln() + log_d - theta * (u1 + u2) - 2.0 * log_den)
}
pub fn cond_first_given_second(u1: f64, u2: f64, theta: f64) -> Result<f64, CopulaError> {
if !theta.is_finite() || theta <= 0.0 {
return Err(FitError::Failed {
reason: "frank pair theta must be positive",
}
.into());
}
let log_num = -theta * u2 + log_one_minus_exp_neg(theta * u1);
let log_t1 = -theta * u1 + log_one_minus_exp_neg(theta * u2);
let log_t2 = -theta * u2 + log_one_minus_exp_neg(theta * (1.0 - u2));
let log_den = logsumexp2(log_t1, log_t2);
Ok((log_num - log_den).exp())
}
pub fn cond_second_given_first(u1: f64, u2: f64, theta: f64) -> Result<f64, CopulaError> {
if !theta.is_finite() || theta <= 0.0 {
return Err(FitError::Failed {
reason: "frank pair theta must be positive",
}
.into());
}
let log_num = -theta * u1 + log_one_minus_exp_neg(theta * u2);
let log_t1 = -theta * u2 + log_one_minus_exp_neg(theta * u1);
let log_t2 = -theta * u1 + log_one_minus_exp_neg(theta * (1.0 - u1));
let log_den = logsumexp2(log_t1, log_t2);
Ok((log_num - log_den).exp())
}
pub fn inv_first_given_second(p: f64, u2: f64, theta: f64) -> Result<f64, CopulaError> {
if !theta.is_finite() || theta <= 0.0 {
return Err(FitError::Failed {
reason: "frank pair theta must be positive",
}
.into());
}
let log_p = p.ln();
let log_one_minus_p = (1.0 - p).ln();
let log_num = logsumexp2(log_one_minus_p - theta * u2, log_p - theta);
let log_den = logsumexp2(log_one_minus_p - theta * u2, log_p);
Ok((log_den - log_num) / theta)
}
pub fn inv_second_given_first(u1: f64, p: f64, theta: f64) -> Result<f64, CopulaError> {
if !theta.is_finite() || theta <= 0.0 {
return Err(FitError::Failed {
reason: "frank pair theta must be positive",
}
.into());
}
let log_p = p.ln();
let log_one_minus_p = (1.0 - p).ln();
let log_num = logsumexp2(log_one_minus_p - theta * u1, log_p - theta);
let log_den = logsumexp2(log_one_minus_p - theta * u1, log_p);
Ok((log_den - log_num) / theta)
}
#[cfg(test)]
mod tests {
use super::*;
fn reference_log_pdf(u1: f64, u2: f64, theta: f64) -> f64 {
let d = (-theta).exp_m1();
let a = (-theta * u1).exp_m1();
let b = (-theta * u2).exp_m1();
let den = d + a * b;
theta.ln() + (-d).ln() - theta * (u1 + u2) - 2.0 * den.abs().ln()
}
fn reference_h12(u1: f64, u2: f64, theta: f64) -> f64 {
let a = (-theta * u1).exp() - 1.0;
let b = (-theta * u2).exp() - 1.0;
let d = (-theta).exp() - 1.0;
(-theta * u2).exp() * a / (d + a * b)
}
#[test]
fn stable_log_pdf_matches_naive_for_moderate_theta() {
for &theta in &[0.5_f64, 1.0, 3.0, 7.0, 12.0] {
for u1 in [0.05_f64, 0.2, 0.5, 0.8, 0.95] {
for u2 in [0.05_f64, 0.2, 0.5, 0.8, 0.95] {
let stable = log_pdf(u1, u2, theta).expect("stable log_pdf ok");
let naive = reference_log_pdf(u1, u2, theta);
assert!(
(stable - naive).abs() < 1e-9,
"θ={theta} u1={u1} u2={u2}: stable={stable}, naive={naive}"
);
}
}
}
}
#[test]
fn stable_log_pdf_is_finite_for_large_theta() {
for &theta in &[30.0_f64, 37.45, 60.0, 120.0] {
for u1 in [1e-6, 0.01, 0.1, 0.5, 0.9, 0.99, 1.0 - 1e-6] {
for u2 in [1e-6, 0.01, 0.1, 0.5, 0.9, 0.99, 1.0 - 1e-6] {
let value = log_pdf(u1, u2, theta).expect("stable log_pdf ok");
assert!(
value.is_finite(),
"stable log_pdf must stay finite (θ={theta}, u1={u1}, u2={u2}, value={value})"
);
}
}
}
}
#[test]
fn stable_cond_first_given_second_matches_naive_for_moderate_theta() {
for &theta in &[0.5_f64, 1.0, 3.0, 7.0, 12.0] {
for u1 in [0.05_f64, 0.2, 0.5, 0.8, 0.95] {
for u2 in [0.05_f64, 0.2, 0.5, 0.8, 0.95] {
let stable = cond_first_given_second(u1, u2, theta).expect("ok");
let naive = reference_h12(u1, u2, theta);
assert!(
(stable - naive).abs() < 1e-9,
"θ={theta} u1={u1} u2={u2}: stable={stable}, naive={naive}"
);
}
}
}
}
#[test]
fn inverse_is_inverse_of_conditional_for_moderate_theta() {
for &theta in &[5.0_f64, 15.0] {
for u1 in [0.05_f64, 0.2, 0.5, 0.8, 0.95] {
for u2 in [0.05_f64, 0.2, 0.5, 0.8, 0.95] {
let p = cond_second_given_first(u1, u2, theta).expect("cond ok");
assert!((0.0..=1.0).contains(&p), "h must be in [0,1]: {p}");
let u2_hat = inv_second_given_first(u1, p, theta).expect("inv ok");
assert!(
(u2 - u2_hat).abs() < 1e-6,
"θ={theta} u1={u1} u2={u2}: recovered u2_hat={u2_hat}"
);
}
}
}
}
#[test]
fn inverse_stays_strictly_inside_unit_interval_for_large_theta() {
for &theta in &[15.0_f64, 37.45, 60.0, 120.0] {
for &p in &[1e-6_f64, 1e-3, 0.5, 1.0 - 1e-3, 1.0 - 1e-6] {
for &u_cond in &[1e-6_f64, 1e-3, 0.5, 1.0 - 1e-3, 1.0 - 1e-6] {
let v = inv_second_given_first(u_cond, p, theta).expect("inv ok");
assert!(
v.is_finite() && v > 0.0 && v < 1.0,
"θ={theta}: inv_second_given_first({u_cond}, {p}) = {v} escaped (0,1)"
);
}
}
}
}
#[test]
fn inverse_stays_in_unit_interval_for_pathological_inputs() {
for p in [1e-8, 1e-4, 0.5, 1.0 - 1e-4, 1.0 - 1e-8] {
for u2 in [1e-8, 0.01, 0.5, 0.99, 1.0 - 1e-8] {
let value = inv_first_given_second(p, u2, 37.45).expect("ok");
assert!(
value.is_finite() && (0.0..=1.0).contains(&value),
"inv_first_given_second(p={p}, u2={u2}, θ=37.45) = {value}"
);
}
}
}
}