use crate::{norminv, erfcinv, binocdf, binopdf};
pub fn binoinv(y: f64, n: f64, p: f64) -> f64 {
if y.is_nan() || n.is_nan() || p.is_nan() || n < 0.0 || p < 0.0 || p > 1.0 || n != n.round() || y < 0.0 || y > 1.0 {
return f64::NAN;
}
if y == 1.0 || (y != 0.0 && p == 1.0) { return n; }
if y == 0.0 || (y != 1.0 && p == 0.0) { return 0.0; }
let eta = erfcinv(2.0 * y) * (2.0 / (n + 1.0)).sqrt();
let xi = xi_asymptotic_inversion_of_binomial_cdf(eta, p);
let cannot_asymptotic = eta < -(-2.0 * (1.0 - p).ln()).sqrt()
|| eta > (-2.0 * p.ln()).sqrt()
|| xi <= 0.0 || xi >= 1.0
|| (eta >= 0.0 && p < xi)
|| (eta < 0.0 && p > xi);
let mut x_approx = if !cannot_asymptotic {
asymptotic_inversion_of_binomial_cdf(eta, xi, n, p)
} else {
cornish_fisher_for_binomial_inverse(y, n, p)
};
x_approx = x_approx.clamp(0.0, n);
let mut x = x_approx;
let mut cum_dist = binocdf(x, n, p, false);
let mut pdf_val = binopdf(x, n, p);
let eps_val = |v: f64| v.next_up() - v;
if cum_dist + 10.0 * eps_val(pdf_val) < y {
while x < n && cum_dist + 10.0 * eps_val(pdf_val) < y {
x += 1.0;
pdf_val = binopdf(x, n, p);
cum_dist += pdf_val;
}
} else {
while x > 0.0 {
pdf_val = binopdf(x, n, p);
let c_temp = cum_dist - pdf_val;
if c_temp + 10.0 * eps_val(pdf_val) < y {
break;
}
cum_dist = c_temp;
x -= 1.0;
}
}
x
}
fn xi_asymptotic_inversion_of_binomial_cdf(eta: f64, p: f64) -> f64 {
let a1 = 1.0;
let a2 = (1.0 / 6.0) * (2.0 * p - 1.0);
let a3 = (1.0 / 72.0) * (2.0 * p.powi(2) - 2.0 * p - 1.0);
let a4 = (-1.0 / 540.0) * (2.0 * p.powi(3) - 3.0 * p.powi(2) - 3.0 * p + 2.0);
let a5 = (1.0 / 17280.0) * (4.0 * p.powi(4) - 8.0 * p.powi(3) - 48.0 * p.powi(2) + 52.0 * p - 23.0);
let etahat = eta / (p * (1.0 - p)).sqrt();
let xi = p - p * (1.0 - p) * (
a1 * etahat
+ a2 * etahat.powi(2)
+ a3 * etahat.powi(3)
+ a4 * etahat.powi(4)
+ a5 * etahat.powi(5)
);
xi
}
fn cornish_fisher_for_binomial_inverse(y: f64, n: f64, p: f64) -> f64 {
let q = 1.0 - p;
let mu = n * p;
let sigma = (n * p * q).sqrt();
let gamma = (q - p) / sigma; let z = norminv(y, 0.0, 1.0);
(mu + sigma * (z + gamma * (z.powi(2) - 1.0) / 6.0) + 0.5).floor()
}
pub(crate) fn eta_taylor_expansion_for_binomial_inverse(eta0: f64, xi: f64) -> f64 {
let lambda = (xi * (1.0 - xi)).sqrt();
let term1 = -(1.0 - 2.0 * xi) / (3.0 * lambda);
let term2 = -((5.0 * xi.powi(2) - 5.0 * xi - 1.0) / (36.0 * lambda.powi(2))) * eta0;
let term3 = (((2.0 * xi - 1.0) * (23.0 * xi.powi(2) - 23.0 * xi - 1.0)) / (1620.0 * lambda.powi(3)))
* eta0.powi(2);
let term4 = -((31.0 * xi.powi(4) - 62.0 * xi.powi(3) + 33.0 * xi.powi(2) - 2.0 * xi + 7.0)
/ (6480.0 * lambda.powi(4)))
* eta0.powi(3);
term1 + term2 + term3 + term4
}
pub(crate) fn asymptotic_inversion_of_binomial_cdf(eta0: f64, xi: f64, n: f64, p: f64) -> f64 {
let threshold = 100.0 * f64::EPSILON * xi.abs().max(f64::EPSILON);
let use_formula = (p - xi).abs() >= threshold;
let eta1 = if use_formula {
let feta0 = eta0 * (xi * (1.0 - xi)).sqrt() / (p - xi);
(1.0 / eta0) * feta0.ln()
} else {
eta_taylor_expansion_for_binomial_inverse(eta0, xi)
};
let eta2 = eta0 + eta1 / (n + 1.0);
let xi2 = xi_asymptotic_inversion_of_binomial_cdf(eta2, p);
(xi2 * (n + 1.0) - 1.0).ceil()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_binopdf_basic() {
let tol = 1e-15;
assert!((binopdf(2.0, 4.0, 0.5) - 0.375).abs() < tol);
assert!((binopdf(0.0, 1.0, 0.5) - 0.5).abs() < tol);
}
#[test]
fn test_binopdf_boundaries() {
let tol = 1e-15;
assert!((binopdf(0.0, 10.0, 0.1) - 0.3486784401).abs() < 1e-10);
assert!((binopdf(10.0, 10.0, 0.1) - 1e-10).abs() < tol);
assert_eq!(binopdf(0.0, 5.0, 0.0), 1.0);
assert_eq!(binopdf(1.0, 5.0, 0.0), 0.0);
assert_eq!(binopdf(5.0, 5.0, 1.0), 1.0);
assert_eq!(binopdf(4.0, 5.0, 1.0), 0.0);
}
#[test]
fn test_binopdf_large_n() {
let val = binopdf(50.0, 100.0, 0.5);
assert!((val - 0.07958923738717816).abs() < 1e-14);
}
#[test]
fn test_binopdf_invalid() {
assert!(binopdf(f64::NAN, 10.0, 0.5).is_nan());
assert!(binopdf(5.0, -1.0, 0.5).is_nan());
assert!(binopdf(5.0, 10.0, -0.1).is_nan());
assert!(binopdf(5.0, 10.0, 1.1).is_nan());
assert_eq!(binopdf(2.5, 4.0, 0.5), 0.0);
assert!(binopdf(2.0, 4.5, 0.5).is_nan());
assert_eq!(binopdf(-1.0, 5.0, 0.5), 0.0);
assert_eq!(binopdf(6.0, 5.0, 0.5), 0.0);
}
#[test]
fn test_binopdf_symmetry() {
let n = 20.0;
let p = 0.3;
let x = 7.0;
assert!((binopdf(x, n, p) - binopdf(n - x, n, 1.0 - p)).abs() < 1e-14);
}
}