use crate::{
owens_t::{owens_t_dispatch, owens_t_znorm1, owens_t_znorm2},
util::*,
};
use libm::{asin, erfc};
fn one_minus_phi(x: f64) -> f64 {
0.5 * erfc(x * FRAC_1_SQRT_2)
}
pub fn biv_norm(x: f64, y: f64, rho: f64) -> f64 {
biv_norm_inner(x.into(), y.into(), rho.into())
}
#[derive(Copy, Clone, Default, Debug)]
pub struct BivNormArg {
val: f64,
val_recip: f64,
one_minus_phi_val: f64,
}
impl From<f64> for BivNormArg {
fn from(src: f64) -> Self {
Self {
one_minus_phi_val: one_minus_phi(src),
val_recip: src.recip(),
val: src,
}
}
}
impl AsRef<f64> for BivNormArg {
fn as_ref(&self) -> &f64 {
&self.val
}
}
#[derive(Copy, Clone, Default, Debug)]
pub struct BivNormRho {
rho: f64,
sqrt_1_minus_rho_sq_recip: f64,
}
impl From<f64> for BivNormRho {
fn from(rho: f64) -> Self {
Self {
sqrt_1_minus_rho_sq_recip: sqrt(1.0 - rho * rho).recip(),
rho,
}
}
}
pub fn biv_norm_inner(
BivNormArg {
val: x,
val_recip: x_recip,
one_minus_phi_val: one_minus_phi_x,
}: BivNormArg,
BivNormArg {
val: y,
val_recip: y_recip,
one_minus_phi_val: one_minus_phi_y,
}: BivNormArg,
BivNormRho {
rho,
sqrt_1_minus_rho_sq_recip,
}: BivNormRho,
) -> f64 {
debug_assert!(-1.0 <= rho, "{rho}");
debug_assert!(1.0 >= rho, "{rho}");
if x == 0.0 && y == 0.0 {
return 0.25 + ONE_DIV_TWO_PI * asin(rho);
}
if rho == 0.0 {
return one_minus_phi_x * one_minus_phi_y;
} else if rho == 1.0 {
return if x < y {
one_minus_phi_y
} else {
one_minus_phi_x
};
} else if rho == -1.0 {
return f64::max(one_minus_phi_x + one_minus_phi_y - 1.0, 0.0);
};
let (q_x, c_x) = if x == 0.0 {
(0.0, 0.0)
} else {
let r_x = (y * x_recip - rho) * sqrt_1_minus_rho_sq_recip;
q(x, r_x, one_minus_phi_x)
};
let (q_y, c_y) = if y == 0.0 {
(0.0, 0.0)
} else {
let r_y = (x * y_recip - rho) * sqrt_1_minus_rho_sq_recip;
q(y, r_y, one_minus_phi_y)
};
let beta = if (x * y) > 0.0 {
0.0
} else if (x * y) < 0.0 {
0.5
} else {
0.0
};
q_x + q_y + (c_x + c_y - beta)
}
fn q(h: f64, a: f64, one_minus_phi_h: f64) -> (f64, f64) {
let s_a = a.signum();
let h_abs = h.abs();
let a_abs = a.abs();
let ah_abs = h_abs * a_abs;
if a_abs <= 1.0 {
(
0.5 * one_minus_phi_h - (s_a * owens_t_dispatch(h_abs, a_abs, ah_abs, None)),
0.0,
)
} else {
let phi_h_minus_half = if h_abs <= 0.67 {
owens_t_znorm1(h)
} else {
0.5 - one_minus_phi_h
};
let one_minus_phi_ah = owens_t_znorm2(a * h);
let one_half_if_a_negative = (s_a - 1.0) * -0.25;
(
(s_a * owens_t_dispatch(ah_abs, a_abs.recip(), h_abs, Some(phi_h_minus_half.abs())))
- phi_h_minus_half * one_minus_phi_ah,
one_half_if_a_negative,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::{BvndTestPoint, get_axis_test_points, get_burkardt_nbs_test_points};
use assert_within::assert_within;
use rand::Rng;
use rand_pcg::{Pcg64Mcg, rand_core::SeedableRng};
#[test]
fn spot_check_biv_norm_against_axis_points() {
for (n, BvndTestPoint { x, y, r, expected }) in get_axis_test_points().enumerate() {
let eps = 1e-15;
let val = biv_norm(x, y, r);
assert_within!(+eps, biv_norm(y, x, r), val, "n = {n}, x = {x}, y = {y}, rho = {r}");
assert_within!(+eps, val, expected, "n = {n}, x = {x}, y = {y}, rho = {r}")
}
}
#[test]
fn spot_check_biv_norm_against_burkardt_nbs_points() {
for (n, BvndTestPoint { x, y, r, expected }) in get_burkardt_nbs_test_points().enumerate() {
let val = biv_norm(x, y, r);
let eps = if x > 0.0 && y > 0.0 {
1e-10
} else if x == 0.0 || y == 0.0 {
1e-6
} else if x < 0.0 && y < 0.0 {
1e-9
} else {
1e-7
};
assert_within!(+eps, biv_norm(y,x,r), val);
assert_within!(+eps, val, expected, "n = {n}, x = {x}, y = {y}, rho = {r}")
}
}
fn to_three_decimals(x: f64) -> f64 {
(x * 1000.0).round() / 1000.0
}
#[test]
fn check_symmetry_conditions() {
let mut rng = Pcg64Mcg::seed_from_u64(9);
for n in 0..10000 {
let x = to_three_decimals(4.0 * rng.random::<f64>() - 2.0);
let y = to_three_decimals(4.0 * rng.random::<f64>() - 2.0);
let r = to_three_decimals(rng.random::<f64>());
let val = biv_norm(x, y, r);
let eps = 1e-15;
assert_within!(+eps, val, biv_norm(y,x,r), "n = {n}, x = {x}, y = {y}, rho = {r}");
let eps = 1e-15;
assert_within!(+eps, val, one_minus_phi(x) - biv_norm(x,-y,-r), "n = {n}, x = {x}, y = {y}, rho = {r}");
let eps = 1e-15;
assert_within!(+eps, val, one_minus_phi(x) - one_minus_phi(-y) + biv_norm(-x,-y,r), "n = {n}, x = {x}, y = {y}, rho = {r}");
}
}
#[test]
fn check_symmetry_conditions_wider_range() {
let mut rng = Pcg64Mcg::seed_from_u64(9);
for n in 0..10000 {
let x = to_three_decimals(8.0 * rng.random::<f64>() - 4.0);
let y = to_three_decimals(8.0 * rng.random::<f64>() - 4.0);
let r = to_three_decimals(rng.random::<f64>());
let val = biv_norm(x, y, r);
let eps = 1e-15;
assert_within!(+eps, val, biv_norm(y,x,r), "n = {n}, x = {x}, y = {y}, rho = {r}");
let eps = 1e-15;
assert_within!(+eps, val, one_minus_phi(x) - biv_norm(x,-y,-r), "n = {n}, x = {x}, y = {y}, rho = {r}");
let eps = 1e-15;
assert_within!(+eps, val, one_minus_phi(x) - one_minus_phi(-y) + biv_norm(-x,-y,r), "n = {n}, x = {x}, y = {y}, rho = {r}");
}
}
}