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 MarshallOlkin {
pub r#type: CopulaType,
pub theta: Option<f64>,
pub tau: Option<f64>,
pub theta_bounds: (f64, f64),
pub invalid_thetas: Vec<f64>,
pub alpha: Option<f64>,
pub beta: Option<f64>,
}
impl Default for MarshallOlkin {
fn default() -> Self {
Self {
r#type: CopulaType::MarshallOlkin,
theta: None,
tau: None,
theta_bounds: (0.0, 1.0),
invalid_thetas: vec![],
alpha: None,
beta: None,
}
}
}
impl MarshallOlkin {
pub fn new() -> Self {
Self::default()
}
pub fn with_alpha_beta(alpha: f64, beta: f64) -> Self {
assert!(
alpha > 0.0 && alpha <= 1.0,
"alpha must lie in (0, 1], got {alpha}"
);
assert!(
beta > 0.0 && beta <= 1.0,
"beta must lie in (0, 1], got {beta}"
);
Self {
alpha: Some(alpha),
beta: Some(beta),
..Self::default()
}
}
fn resolve_params(&self) -> (f64, f64) {
match (self.alpha, self.beta) {
(Some(a), Some(b)) => (a, b),
_ => {
let theta = self
.theta
.expect("Marshall-Olkin: neither (alpha, beta) nor theta is set");
(theta, theta)
}
}
}
}
impl BivariateExt for MarshallOlkin {
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);
self.alpha = None;
self.beta = None;
}
fn generator(&self, _t: &Array1<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
Err("Marshall-Olkin is not Archimedean — generator not defined".into())
}
fn pdf(&self, x: &Array2<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
let (alpha, beta) = self.resolve_params();
let u_col = x.column(0);
let v_col = x.column(1);
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 lhs = u.powf(alpha);
let rhs = v.powf(beta);
if (lhs - rhs).abs() < 1e-15 {
out[i] = 0.0;
} else if lhs > rhs {
out[i] = (1.0 - alpha) * u.powf(-alpha);
} else {
out[i] = (1.0 - beta) * v.powf(-beta);
}
}
Ok(out)
}
fn cdf(&self, x: &Array2<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
let (alpha, beta) = self.resolve_params();
let u_col = x.column(0);
let v_col = x.column(1);
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;
}
let lhs = u.powf(alpha);
let rhs = v.powf(beta);
out[i] = if lhs >= rhs {
u.powf(1.0 - alpha) * v
} else {
u * v.powf(1.0 - beta)
};
}
Ok(out)
}
fn partial_derivative(&self, x: &Array2<f64>) -> Result<Array1<f64>, Box<dyn Error>> {
let (alpha, beta) = self.resolve_params();
let u_col = x.column(0);
let v_col = x.column(1);
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 {
out[i] = 0.0;
continue;
}
if u >= 1.0 {
out[i] = 1.0;
continue;
}
let lhs = u.powf(alpha);
let rhs = v.powf(beta);
out[i] = if lhs >= rhs {
(1.0 - alpha) * u.powf(-alpha) * v
} else {
v.powf(1.0 - beta)
};
}
Ok(out)
}
fn compute_theta(&self) -> f64 {
let tau = self.tau.unwrap();
if tau <= 0.0 {
return 0.0_f64.max(f64::EPSILON);
}
if tau >= 1.0 - 1e-12 {
return 1.0;
}
(2.0 * tau / (1.0 + tau)).clamp(0.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 mo_cdf_marginal_recovers_input() {
let c = MarshallOlkin::with_alpha_beta(0.5, 0.3);
let x = array![[0.4_f64, 1.0], [1.0, 0.7]];
let cdf = c.cdf(&x).unwrap();
assert!(approx(cdf[0], 0.4, 1e-12));
assert!(approx(cdf[1], 0.7, 1e-12));
}
#[test]
fn mo_alpha_eq_one_beta_eq_one_is_comonotone() {
let c = MarshallOlkin::with_alpha_beta(1.0, 1.0);
let x = array![[0.3_f64, 0.7], [0.6, 0.2], [0.5, 0.5]];
let cdf = c.cdf(&x).unwrap();
for i in 0..x.nrows() {
let expected = x[[i, 0]].min(x[[i, 1]]);
assert!(approx(cdf[i], expected, 1e-12), "row {i}");
}
}
#[test]
fn mo_alpha_zero_or_beta_zero_is_independence() {
let c = MarshallOlkin::with_alpha_beta(1e-12, 0.5);
let x = array![[0.4_f64, 0.6]];
let cdf = c.cdf(&x).unwrap();
assert!(approx(cdf[0], 0.24, 1e-6), "α→0: got {}", cdf[0]);
}
#[test]
fn mo_compute_theta_via_symmetric_inversion() {
let mut c = MarshallOlkin::new();
c.set_tau(1.0 / 3.0);
let theta = c.compute_theta();
assert!(approx(theta, 0.5, 1e-12), "expected θ=0.5, got {theta}");
}
#[test]
fn mo_singular_curve_total_mass_matches_paper() {
let alpha = 0.6_f64;
let beta = 0.4_f64;
let mass_singular_paper = alpha * beta / (alpha + beta - alpha * beta);
let c = MarshallOlkin::with_alpha_beta(alpha, beta);
let n = 200usize;
let h = 1.0 / n as f64;
let mut points = Array2::<f64>::zeros((n * n, 2));
for i in 0..n {
for j in 0..n {
let row = i * n + j;
points[[row, 0]] = (i as f64 + 0.5) * h;
points[[row, 1]] = (j as f64 + 0.5) * h;
}
}
let pdf_vals = c.pdf(&points).unwrap();
let mass_abs: f64 = pdf_vals.iter().sum::<f64>() * h * h;
let mass_singular_grid = 1.0 - mass_abs;
assert!(
(mass_singular_grid - mass_singular_paper).abs() < 0.02,
"singular mass grid={mass_singular_grid:.4}, paper={mass_singular_paper:.4}"
);
}
#[test]
fn mo_partial_derivative_in_each_sector() {
let c = MarshallOlkin::with_alpha_beta(0.5, 0.5);
let x = array![[0.9_f64, 0.5]];
let pd = c.partial_derivative(&x).unwrap();
let expected = 0.5_f64 * 0.9_f64.powf(-0.5) * 0.5;
assert!(
approx(pd[0], expected, 1e-12),
"pd={}, expected {expected}",
pd[0]
);
}
}