use std::error::Error;
use std::f64;
use ndarray::Array1;
use ndarray::Array2;
use roots::SimpleConvergency;
use roots::find_root_brent;
use crate::bivariate::CopulaType;
use crate::traits::BivariateExt;
#[derive(Debug, Clone)]
pub struct Amh {
pub r#type: CopulaType,
pub theta: Option<f64>,
pub tau: Option<f64>,
pub theta_bounds: (f64, f64),
pub invalid_thetas: Vec<f64>,
}
impl Default for Amh {
fn default() -> Self {
Self {
r#type: CopulaType::Amh,
theta: None,
tau: None,
theta_bounds: (-1.0, 1.0),
invalid_thetas: vec![],
}
}
}
impl Amh {
pub fn new() -> Self {
Self::default()
}
fn tau_residual(tau_target: f64, theta: f64) -> f64 {
let tau_theta = if theta.abs() < 1e-4 {
let t2 = theta * theta;
let t3 = t2 * theta;
let t4 = t2 * t2;
2.0 / 9.0 * theta + t2 / 18.0 + 2.0 / 75.0 * t3 + 5.0 / 294.0 * t4
} else {
let one_minus_theta = 1.0 - theta;
let bracket = theta + one_minus_theta * one_minus_theta * one_minus_theta.ln();
1.0 - 2.0 * bracket / (3.0 * theta * theta)
};
tau_theta - tau_target
}
}
impl BivariateExt for Amh {
fn r#type(&self) -> CopulaType {
self.r#type
}
fn tau(&self) -> Option<f64> {
self.tau
}
fn set_tau(&mut self, tau: f64) {
self.tau = Some(tau);
}
fn theta(&self) -> Option<f64> {
self.theta
}
fn theta_bounds(&self) -> (f64, f64) {
self.theta_bounds
}
fn invalid_thetas(&self) -> Vec<f64> {
self.invalid_thetas.clone()
}
fn set_theta(&mut self, theta: f64) {
self.theta = Some(theta);
}
fn generator(&self, t: &Array1<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
self.check_fit()?;
let theta = self.theta.unwrap();
let mut out = Array1::<f64>::zeros(t.len());
for i in 0..t.len() {
let ti = t[i];
if ti <= 0.0 {
out[i] = f64::INFINITY;
continue;
}
out[i] = ((1.0 - theta * (1.0 - ti)) / ti).ln();
}
Ok(out)
}
fn pdf(&self, x: &Array2<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
self.check_fit()?;
let u_col = x.column(0);
let v_col = x.column(1);
let theta = self.theta.unwrap();
let mut out = Array1::<f64>::zeros(u_col.len());
for i in 0..u_col.len() {
let u = u_col[i];
let v = v_col[i];
if u <= 0.0 || u >= 1.0 || v <= 0.0 || v >= 1.0 {
out[i] = 0.0;
continue;
}
let d = 1.0 - theta * (1.0 - u) * (1.0 - v);
let num = (1.0 - theta) * d + 2.0 * theta * u * v;
out[i] = num / (d * d * d);
}
Ok(out)
}
fn cdf(&self, x: &Array2<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
self.check_fit()?;
let u_col = x.column(0);
let v_col = x.column(1);
let theta = self.theta.unwrap();
let mut out = Array1::<f64>::zeros(u_col.len());
for i in 0..u_col.len() {
let u = u_col[i];
let v = v_col[i];
if u <= 0.0 || v <= 0.0 {
out[i] = 0.0;
continue;
}
if u >= 1.0 {
out[i] = v;
continue;
}
if v >= 1.0 {
out[i] = u;
continue;
}
out[i] = u * v / (1.0 - theta * (1.0 - u) * (1.0 - v));
}
Ok(out)
}
fn partial_derivative(&self, x: &Array2<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
self.check_fit()?;
let u_col = x.column(0);
let v_col = x.column(1);
let theta = self.theta.unwrap();
let mut out = Array1::<f64>::zeros(u_col.len());
for i in 0..u_col.len() {
let u = u_col[i];
let v = v_col[i];
if v <= 0.0 {
out[i] = 0.0;
continue;
}
if v >= 1.0 {
out[i] = 1.0;
continue;
}
let d = 1.0 - theta * (1.0 - u) * (1.0 - v);
out[i] = v * (1.0 - theta * (1.0 - v)) / (d * d);
}
Ok(out)
}
fn compute_theta(&self) -> f64 {
let tau = self.tau.unwrap();
let tau_lo = (5.0 - 8.0 * 2.0_f64.ln()) / 3.0;
let tau_hi = 1.0 / 3.0;
if tau <= tau_lo {
return -1.0;
}
if tau >= tau_hi {
return 1.0 - 1e-9;
}
let residual = |theta: f64| Self::tau_residual(tau, theta);
let mut convergency = SimpleConvergency {
eps: 1e-10,
max_iter: 100,
};
find_root_brent(-1.0 + 1e-9, 1.0 - 1e-9, residual, &mut convergency).unwrap_or(0.0)
}
}
#[cfg(test)]
mod tests {
use ndarray::array;
use super::*;
fn approx(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol
}
#[test]
fn amh_independence_theta_zero_reproduces_product_cdf() {
let mut c = Amh::new();
c.set_theta(0.0);
let x = array![[0.3_f64, 0.7], [0.5, 0.5], [0.9, 0.1]];
let cdf = c.cdf(&x).unwrap();
for i in 0..x.nrows() {
let expected = x[[i, 0]] * x[[i, 1]];
assert!(approx(cdf[i], expected, 1e-12));
}
}
#[test]
fn amh_pdf_at_interior_matches_closed_form_fixture() {
let mut c = Amh::new();
c.set_theta(0.5);
let x = array![[0.5_f64, 0.5]];
let pdf = c.pdf(&x).unwrap();
let expected = 0.6875_f64 / 0.875_f64.powi(3);
assert!(
approx(pdf[0], expected, 1e-12),
"pdf={}, expected {expected}",
pdf[0]
);
}
#[test]
fn amh_marginal_recovers_input() {
let mut c = Amh::new();
c.set_theta(0.7);
let x = array![[0.4_f64, 1.0], [1.0, 0.2]];
let cdf = c.cdf(&x).unwrap();
assert!(approx(cdf[0], 0.4, 1e-12));
assert!(approx(cdf[1], 0.2, 1e-12));
}
#[test]
fn amh_tau_residual_matches_paper_bounds() {
let res_lo = Amh::tau_residual(0.0, -0.999_999);
let res_hi = Amh::tau_residual(0.0, 0.999_999);
let tau_lo = (5.0 - 8.0 * 2.0_f64.ln()) / 3.0;
assert!(
approx(res_lo, tau_lo, 1e-3),
"τ(-1) ≈ {tau_lo}, got {res_lo}"
);
assert!(approx(res_hi, 1.0 / 3.0, 1e-3), "τ(1) ≈ 1/3, got {res_hi}");
}
#[test]
fn amh_compute_theta_inverts_tau_within_bracket() {
let target_theta = 0.6_f64;
let tau = Amh::tau_residual(0.0, target_theta);
let mut c = Amh::new();
c.set_tau(tau);
let recovered = c.compute_theta();
assert!(
approx(recovered, target_theta, 1e-6),
"expected {target_theta}, got {recovered}"
);
}
#[test]
fn amh_tau_zero_recovers_independence() {
let mut c = Amh::new();
c.set_tau(0.0);
let theta = c.compute_theta();
assert!(approx(theta, 0.0, 1e-6), "τ=0 should give θ=0, got {theta}");
}
}