use num_bigint::BigInt;
use num_traits::{Signed, ToPrimitive};
use super::data::Q;
use super::family::Distribution;
use crate::api::context::Context;
use crate::api::expr::Ex;
use crate::base::errors::SymplexError;
use crate::domains::optimize::{RootOpts, brent_root};
use crate::output::codegen::numeric_rt::erfcinv;
pub(crate) fn qi(n: i64) -> Q {
Q::from_integer(BigInt::from(n))
}
pub(crate) fn qu(n: usize) -> Q {
Q::from_integer(BigInt::from(n))
}
pub(crate) fn ex(ctx: &Context, q: &Q) -> Ex {
ctx.from_ratio(q.clone())
}
pub(crate) fn ex_usize(ctx: &Context, n: usize) -> Ex {
ctx.from_bigint(BigInt::from(n))
}
pub(crate) fn q_to_f64(q: &Q) -> f64 {
q.to_f64().unwrap_or(f64::NAN)
}
pub(crate) fn invalid(op: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::invalid_argument(op, reason)
}
pub(crate) fn check_unit_open(op: &'static str, name: &str, v: f64) -> Result<(), SymplexError> {
if v > 0.0 && v < 1.0 {
Ok(())
} else {
Err(invalid(
op,
format!("{name} must lie strictly between 0 and 1, got {v}"),
))
}
}
pub(crate) fn check_confidence(op: &'static str, confidence: f64) -> Result<(), SymplexError> {
check_unit_open(op, "confidence", confidence)
}
pub(crate) fn check_alpha(op: &'static str, alpha: f64) -> Result<(), SymplexError> {
check_unit_open(op, "alpha", alpha)
}
pub(crate) fn check_finite(op: &'static str, name: &str, v: f64) -> Result<(), SymplexError> {
if v.is_finite() {
Ok(())
} else {
Err(invalid(op, format!("{name} must be finite, got {v}")))
}
}
pub(crate) fn check_sample(
op: &'static str,
name: &str,
x: &[Q],
min: usize,
) -> Result<(), SymplexError> {
if x.len() < min {
return Err(invalid(
op,
format!(
"{name} needs at least {min} observation{}, got {}",
if min == 1 { "" } else { "s" },
x.len()
),
));
}
Ok(())
}
pub(crate) fn usize_to_i64(op: &'static str, n: usize) -> Result<i64, SymplexError> {
i64::try_from(n).map_err(|_| invalid(op, format!("{n} does not fit in an i64")))
}
pub(crate) fn chi_squared_sf(ctx: &Context, df: usize, x: &Ex) -> Ex {
let half_df = ex_usize(ctx, df) / ctx.int(2);
(x / ctx.int(2)).uppergamma(&half_df) / half_df.gamma()
}
pub(crate) fn chi_squared_sf_q(ctx: &Context, df: usize, x: &Q) -> Ex {
if !x.is_positive() {
return ctx.one();
}
chi_squared_sf(ctx, df, &ex(ctx, x))
}
pub(crate) fn f_sf(ctx: &Context, d1: usize, d2: usize, f: &Q) -> Ex {
if !f.is_positive() {
return ctx.one();
}
let z = qu(d2) / (qu(d2) + qu(d1) * f);
ex(ctx, &z).betainc_regularized(
&(ex_usize(ctx, d2) / ctx.int(2)),
&(ex_usize(ctx, d1) / ctx.int(2)),
&ctx.zero(),
)
}
pub(crate) fn f_sf_rational(ctx: &Context, d1: &Q, d2: &Q, f: &Q) -> Ex {
if !f.is_positive() {
return ctx.one();
}
let z = d2 / (d2 + d1 * f);
ex(ctx, &z).betainc_regularized(
&ex(ctx, &(d2 / qi(2))),
&ex(ctx, &(d1 / qi(2))),
&ctx.zero(),
)
}
pub(crate) fn norm_isf(alpha: f64) -> f64 {
std::f64::consts::SQRT_2 * erfcinv(2.0 * alpha)
}
pub(crate) fn norm_ppf(p: f64) -> f64 {
-norm_isf(p)
}
pub(crate) fn z_two_sided(confidence: f64) -> f64 {
norm_isf((1.0 - confidence) / 2.0)
}
pub(crate) fn student_t_cdf_f64(ctx: &Context, df: f64, t: f64) -> Result<f64, SymplexError> {
let nu = ctx.from_f64(df)?;
let z = &nu / (ctx.from_f64(t * t)? + &nu);
let tail = ctx.rational(1, 2)
* z.betainc_regularized(&(&nu / ctx.int(2)), &ctx.rational(1, 2), &ctx.zero());
let tail = tail.eval_f64()?;
Ok(if t < 0.0 { tail } else { 1.0 - tail })
}
pub(crate) fn student_t_quantile_f64(
op: &'static str,
ctx: &Context,
df: f64,
p: f64,
) -> Result<f64, SymplexError> {
if p < 0.5 {
return student_t_quantile_f64(op, ctx, df, 1.0 - p).map(|t| -t);
}
if p == 0.5 {
return Ok(0.0);
}
let g = |t: f64| student_t_cdf_f64(ctx, df, t).unwrap_or(f64::NAN) - p;
let mut hi = 1.0;
for _ in 0..64 {
let v = g(hi);
if v.is_nan() {
return Err(SymplexError::computation_failed(
op,
"the Student-t distribution function could not be evaluated",
));
}
if v >= 0.0 {
break;
}
hi *= 2.0;
}
let root = brent_root(g, 0.0, hi, &RootOpts::default())
.map_err(|e| SymplexError::computation_failed(op, e.to_string()))?;
if root.is_finite() {
Ok(root)
} else {
Err(SymplexError::computation_failed(
op,
"the Student-t quantile did not converge",
))
}
}
pub(crate) fn t_two_sided(op: &'static str, df: f64, confidence: f64) -> Result<f64, SymplexError> {
let ctx = Context::new();
student_t_quantile_f64(op, &ctx, df, 1.0 - (1.0 - confidence) / 2.0)
}
pub(crate) fn standard_normal() -> Distribution {
let ctx = Context::new();
Distribution::normal(ctx.int(0), ctx.int(1))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn normal_quantiles() {
assert!((z_two_sided(0.95) - 1.959963984540054).abs() < 1e-12);
assert!((norm_ppf(0.975) - 1.959963984540054).abs() < 1e-12);
assert!((norm_isf(0.025) - 1.959963984540054).abs() < 1e-12);
}
#[test]
fn student_t_quantile() {
assert!((t_two_sided("test", 5.0, 0.95).unwrap() - 2.5705818356363146).abs() < 1e-9);
}
#[test]
fn checks() {
assert!(check_confidence("op", 0.95).is_ok());
assert!(check_confidence("op", 1.0).is_err());
assert!(check_alpha("op", 0.0).is_err());
assert!(check_sample("op", "x", &[qi(1)], 2).is_err());
assert!(check_sample("op", "x", &[qi(1), qi(2)], 2).is_ok());
assert_eq!(q_to_f64(&qi(3)), 3.0);
}
}