use num_bigint::BigInt;
use num_traits::{One, Signed, Zero};
use crate::api::context::Context;
use crate::api::expr::Ex;
use crate::base::errors::SymplexError;
use crate::base::interval::Interval;
use crate::domains::optimize::{RootOpts, bisect};
use super::common::{check_alpha, check_unit_open, ex_usize, invalid, q_to_f64};
use super::data::Q;
fn ln_ratio(num: &Q, den: &Q) -> f64 {
let ratio = num / den;
let x = q_to_f64(&ratio);
if (0.5..=2.0).contains(&x) {
return q_to_f64(&((num - den) / den)).ln_1p();
}
if x.is_finite() && x >= f64::MIN_POSITIVE {
return x.ln();
}
let bits = |n: &BigInt| i64::try_from(n.bits()).unwrap_or(i64::MAX);
let k = bits(ratio.numer()).saturating_sub(bits(ratio.denom()));
let shift = usize::try_from(k.unsigned_abs()).unwrap_or(usize::MAX);
let pow = Q::from_integer(BigInt::one() << shift);
let scaled = if k >= 0 { ratio / pow } else { ratio * pow };
q_to_f64(&scaled).ln() + k as f64 * std::f64::consts::LN_2
}
fn exact(v: f64) -> Q {
Q::from_float(v).unwrap_or_else(Q::zero)
}
fn check_rates(op: &'static str, alpha: f64, beta: f64) -> Result<(), SymplexError> {
check_alpha(op, alpha)?;
check_unit_open(op, "beta", beta)?;
if alpha + beta >= 1.0 {
return Err(invalid(
op,
format!(
"alpha + beta must be below 1 for the boundaries to be ordered, got {alpha} + {beta}"
),
));
}
Ok(())
}
fn check_probability(op: &'static str, p: &Q, what: &str) -> Result<(), SymplexError> {
if !p.is_positive() || p >= &Q::one() {
return Err(invalid(
op,
format!("{what} must lie strictly in (0, 1), got {p}"),
));
}
Ok(())
}
pub fn wald_boundaries(alpha: f64, beta: f64) -> Result<Interval<f64>, SymplexError> {
check_rates("wald_boundaries", alpha, beta)?;
let (a, b, one) = (exact(alpha), exact(beta), Q::one());
Ok(Interval::open(
ln_ratio(&b, &(&one - &a)),
ln_ratio(&(&one - &b), &a),
))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Decision {
Continue,
AcceptH0,
AcceptH1,
}
#[derive(Clone, Debug, PartialEq)]
enum Model {
Bernoulli { p0: Q, p1: Q },
NormalMean { mu0: Q, mu1: Q, sigma: Q },
}
#[derive(Clone, Debug, PartialEq)]
pub struct Sprt {
model: Model,
alpha: f64,
beta: f64,
boundaries: Interval<f64>,
observations: usize,
successes: usize,
sum: Q,
stopped: Option<Stop>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct Stop {
decision: Decision,
at: usize,
}
impl Sprt {
pub fn bernoulli(p0: &Q, p1: &Q, alpha: f64, beta: f64) -> Result<Self, SymplexError> {
const OP: &str = "Sprt::bernoulli";
check_probability(OP, p0, "p0")?;
check_probability(OP, p1, "p1")?;
if p0 == p1 {
return Err(invalid(OP, "p0 and p1 must differ"));
}
check_rates(OP, alpha, beta)?;
let boundaries = wald_boundaries(alpha, beta)?;
Ok(Sprt {
model: Model::Bernoulli {
p0: p0.clone(),
p1: p1.clone(),
},
alpha,
beta,
boundaries,
observations: 0,
successes: 0,
sum: Q::zero(),
stopped: None,
})
}
pub fn normal_mean(
mu0: &Q,
mu1: &Q,
sigma: &Q,
alpha: f64,
beta: f64,
) -> Result<Self, SymplexError> {
const OP: &str = "Sprt::normal_mean";
if !sigma.is_positive() {
return Err(invalid(OP, format!("sigma must be positive, got {sigma}")));
}
if mu0 == mu1 {
return Err(invalid(OP, "mu0 and mu1 must differ"));
}
check_rates(OP, alpha, beta)?;
let boundaries = wald_boundaries(alpha, beta)?;
Ok(Sprt {
model: Model::NormalMean {
mu0: mu0.clone(),
mu1: mu1.clone(),
sigma: sigma.clone(),
},
alpha,
beta,
boundaries,
observations: 0,
successes: 0,
sum: Q::zero(),
stopped: None,
})
}
pub fn update(&mut self, success: bool) -> Decision {
self.observations += 1;
if success {
self.successes += 1;
self.sum += Q::one();
}
self.settle()
}
pub fn observe(&mut self, x: &Q) -> Result<Decision, SymplexError> {
match &self.model {
Model::Bernoulli { .. } => {
if x.is_one() {
Ok(self.update(true))
} else if x.is_zero() {
Ok(self.update(false))
} else {
Err(invalid(
"Sprt::observe",
format!("a Bernoulli observation must be 0 or 1, got {x}"),
))
}
}
Model::NormalMean { .. } => {
self.observations += 1;
if x.is_positive() {
self.successes += 1;
}
self.sum += x;
Ok(self.settle())
}
}
}
fn settle(&mut self) -> Decision {
if let Some(stop) = self.stopped {
return stop.decision;
}
let decision = self.current();
if decision != Decision::Continue {
self.stopped = Some(Stop {
decision,
at: self.observations,
});
}
decision
}
fn current(&self) -> Decision {
let llr = self.log_likelihood_ratio_f64();
if llr >= self.boundaries.upper {
Decision::AcceptH1
} else if llr <= self.boundaries.lower {
Decision::AcceptH0
} else {
Decision::Continue
}
}
pub fn decision(&self) -> Decision {
match self.stopped {
Some(stop) => stop.decision,
None => self.current(),
}
}
pub fn is_decided(&self) -> bool {
self.stopped.is_some()
}
pub fn stopped_at(&self) -> Option<usize> {
self.stopped.map(|s| s.at)
}
pub fn reset(&mut self) {
self.observations = 0;
self.successes = 0;
self.sum = Q::zero();
self.stopped = None;
}
pub fn boundaries(&self) -> Interval<f64> {
self.boundaries
}
pub fn alpha(&self) -> f64 {
self.alpha
}
pub fn beta(&self) -> f64 {
self.beta
}
pub fn observations(&self) -> usize {
self.observations
}
pub fn successes(&self) -> usize {
self.successes
}
pub fn failures(&self) -> usize {
self.observations - self.successes
}
pub fn sum(&self) -> &Q {
&self.sum
}
pub fn log_likelihood_ratio(&self, ctx: &Context) -> Ex {
match &self.model {
Model::Bernoulli { p0, p1 } => {
let s = ex_usize(ctx, self.successes);
let f = ex_usize(ctx, self.failures());
let one = Q::one();
s * ctx.from_ratio(p1 / p0).ln()
+ f * ctx.from_ratio((&one - p1) / (&one - p0)).ln()
}
Model::NormalMean { .. } => ctx.from_ratio(self.normal_llr_exact()),
}
}
pub fn log_likelihood_ratio_f64(&self) -> f64 {
match &self.model {
Model::Bernoulli { p0, p1 } => {
let inc = BernoulliIncrements::new(p0, p1);
self.successes as f64 * inc.success + self.failures() as f64 * inc.failure
}
Model::NormalMean { .. } => q_to_f64(&self.normal_llr_exact()),
}
}
fn normal_llr_exact(&self) -> Q {
match &self.model {
Model::NormalMean { mu0, mu1, sigma } => {
let var = sigma * sigma;
let n = Q::from_integer(self.observations.into());
let two = Q::from_integer(2.into());
(mu1 - mu0) / &var * &self.sum - n * (mu1 * mu1 - mu0 * mu0) / (&two * &var)
}
Model::Bernoulli { .. } => Q::zero(),
}
}
}
#[derive(Clone, Copy, Debug)]
struct BernoulliIncrements {
success: f64,
failure: f64,
}
impl BernoulliIncrements {
fn new(p0: &Q, p1: &Q) -> Self {
let one = Q::one();
BernoulliIncrements {
success: ln_ratio(p1, p0),
failure: ln_ratio(&(&one - p1), &(&one - p0)),
}
}
fn checked(op: &'static str, p: f64, p0: &Q, p1: &Q) -> Result<Self, SymplexError> {
check_probability(op, p0, "p0")?;
check_probability(op, p1, "p1")?;
if p0 == p1 {
return Err(invalid(op, "p0 and p1 must differ"));
}
if !(0.0..=1.0).contains(&p) {
return Err(invalid(op, format!("p must lie in [0, 1], got {p}")));
}
Ok(Self::new(p0, p1))
}
}
fn w_expm1(w: f64, z: f64) -> f64 {
if z <= 1.0 {
w * z.exp_m1()
} else if z <= 700.0 {
w * z.exp() - w
} else {
(w.ln() + z).exp() - w
}
}
fn remainder_over_h2(w: f64, l: f64, h: f64) -> f64 {
let z = h * l;
if z.abs() <= 1.0 {
let (mut term, mut sum) = (0.5_f64, 0.5_f64);
for k in 1..=30 {
term *= z / f64::from(k + 2);
sum += term;
if term.abs() <= 1e-17 * sum {
break;
}
}
w * l * l * sum
} else {
(w_expm1(w, z) - w * z) / (h * h)
}
}
fn wald_h(
op: &'static str,
p: f64,
inc: BernoulliIncrements,
drift: f64,
) -> Result<f64, SymplexError> {
if drift == 0.0 {
return Ok(0.0);
}
let (ls, lf) = (inc.success, inc.failure);
let g = |h: f64| w_expm1(p, h * ls) + w_expm1(1.0 - p, h * lf);
let phi = |h: f64| if h == 0.0 { drift } else { g(h) / h };
let end = if (drift < 0.0) == (ls > 0.0) {
-p.ln() / ls
} else {
-(-p).ln_1p() / lf
};
if !end.is_finite() || end == 0.0 {
return Err(SymplexError::computation_failed(
op,
format!("the root of Wald's identity is outside the f64 range (bracket end {end})"),
));
}
if g(end) <= 0.0 {
return Ok(end);
}
let (lo, hi) = if end > 0.0 { (0.0, end) } else { (end, 0.0) };
let tight = RootOpts {
xtol: 0.0,
max_iter: 200,
..RootOpts::default()
};
bisect(phi, lo, hi, &tight).map_err(|e| SymplexError::computation_failed(op, e.to_string()))
}
#[derive(Clone, Copy, Debug)]
struct WaldApprox {
oc: f64,
asn: f64,
}
fn wald_approx(
op: &'static str,
p: f64,
inc: BernoulliIncrements,
a: f64,
b: f64,
) -> Result<WaldApprox, SymplexError> {
let (ls, lf) = (inc.success, inc.failure);
if p <= 0.0 || p >= 1.0 {
let z = if p >= 1.0 { ls } else { lf };
return Ok(if z > 0.0 {
WaldApprox {
oc: 0.0,
asn: b / z,
}
} else {
WaldApprox {
oc: 1.0,
asn: a / z,
}
});
}
let drift = p * ls + (1.0 - p) * lf;
let h = wald_h(op, p, inc, drift)?;
let err_remainders = 4.0 * f64::EPSILON * (h * ls).abs().max((h * lf).abs());
let err_drift = f64::EPSILON * ((p * ls).abs() + ((1.0 - p) * lf).abs()) / drift.abs();
let s3 = if h != 0.0 && err_drift < err_remainders {
-drift / h
} else {
remainder_over_h2(p, ls, h) + remainder_over_h2(1.0 - p, lf, h)
};
if h.abs() * (b - a) <= 1.0 {
let exprel = |z: f64| if z == 0.0 { 1.0 } else { z.exp_m1() / z };
let (xb, xa) = (b * exprel(b * h), -a * exprel(a * h));
let s1 = remainder_over_h2(1.0, b, h) / b - remainder_over_h2(1.0, a, h) / a;
let s2 = xb + xa;
return Ok(WaldApprox {
oc: xb / s2,
asn: -a * b * s1 / (s2 * s3),
});
}
let (eb, ea) = ((b * h).exp_m1().abs(), (a * h).exp_m1().abs());
let (oc, oc_c) = if eb >= ea {
let r = ea / eb;
(1.0 / (1.0 + r), r / (1.0 + r))
} else {
let r = eb / ea;
(r / (1.0 + r), 1.0 / (1.0 + r))
};
Ok(WaldApprox {
oc,
asn: (oc * a + oc_c * b) / (-h * s3),
})
}
pub fn operating_characteristic_bernoulli(
p: f64,
p0: &Q,
p1: &Q,
alpha: f64,
beta: f64,
) -> Result<f64, SymplexError> {
const OP: &str = "operating_characteristic_bernoulli";
let inc = BernoulliIncrements::checked(OP, p, p0, p1)?;
check_rates(OP, alpha, beta)?;
let bounds = wald_boundaries(alpha, beta)?;
Ok(wald_approx(OP, p, inc, bounds.lower, bounds.upper)?.oc)
}
pub fn expected_sample_size_bernoulli(
p: f64,
p0: &Q,
p1: &Q,
alpha: f64,
beta: f64,
) -> Result<f64, SymplexError> {
const OP: &str = "expected_sample_size_bernoulli";
let inc = BernoulliIncrements::checked(OP, p, p0, p1)?;
check_rates(OP, alpha, beta)?;
let bounds = wald_boundaries(alpha, beta)?;
Ok(wald_approx(OP, p, inc, bounds.lower, bounds.upper)?.asn)
}