use std::error::Error;
use std::f64;
use ndarray::Array1;
use ndarray::Array2;
use crate::bivariate::CopulaType;
use crate::traits::BivariateExt;
#[derive(Debug, Clone)]
pub struct Fgm {
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 Fgm {
fn default() -> Self {
Self {
r#type: CopulaType::Fgm,
theta: None,
tau: None,
theta_bounds: (-1.0, 1.0),
invalid_thetas: vec![],
}
}
}
impl Fgm {
pub fn new() -> Self {
Self::default()
}
}
impl BivariateExt for Fgm {
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>> {
Err("FGM is not Archimedean — generator not defined".into())
}
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];
out[i] = 1.0 + theta * (1.0 - 2.0 * u) * (1.0 - 2.0 * v);
}
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];
out[i] = u * v + theta * u * (1.0 - u) * v * (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];
out[i] = u + theta * u * (1.0 - u) * (1.0 - 2.0 * v);
}
Ok(out)
}
fn compute_theta(&self) -> f64 {
let tau = self.tau.unwrap();
(9.0 * tau / 2.0).clamp(-1.0, 1.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 fgm_independence_theta_zero_reproduces_product_cdf() {
let mut c = Fgm::new();
c.set_theta(0.0);
let x = array![[0.3_f64, 0.7], [0.5, 0.5]];
let cdf = c.cdf(&x).unwrap();
assert!(approx(cdf[0], 0.21, 1e-12));
assert!(approx(cdf[1], 0.25, 1e-12));
}
#[test]
fn fgm_pdf_at_center_equals_one_for_zero_theta() {
let mut c = Fgm::new();
c.set_theta(0.0);
let x = array![[0.5_f64, 0.5]];
let pdf = c.pdf(&x).unwrap();
assert!(approx(pdf[0], 1.0, 1e-12));
}
#[test]
fn fgm_compute_theta_inverts_tau_exactly() {
let mut c = Fgm::new();
c.set_tau(0.1);
assert!(approx(c.compute_theta(), 0.45, 1e-12));
c.set_tau(-0.15);
assert!(approx(c.compute_theta(), -0.675, 1e-12));
}
#[test]
fn fgm_compute_theta_saturates_at_extreme_tau() {
let mut c = Fgm::new();
c.set_tau(0.5);
assert!(approx(c.compute_theta(), 1.0, 1e-12));
c.set_tau(-0.5);
assert!(approx(c.compute_theta(), -1.0, 1e-12));
}
#[test]
fn fgm_generator_returns_err_not_archimedean() {
let c = Fgm::new();
let t = array![0.5_f64, 0.8];
assert!(c.generator(&t).is_err());
}
}