use std::f64::consts::PI;
pub fn stdtr(df: i32, t: f64) -> f64 {
assert!(df >= 1, "stdtr requires integer df >= 1, got {df}");
if t == 0.0 {
return 0.5;
}
let abs_t = t.abs();
let z = 1.0 + abs_t * abs_t / df as f64;
let p = if df % 2 == 1 {
let u = abs_t / (df as f64).sqrt();
let mut p = u.atan();
if df > 1 {
let mut f = 1.0_f64;
let mut tz = 1.0_f64;
let mut j = 3i32;
while j <= df - 2 {
tz *= (j as f64 - 1.0) / (z * j as f64);
f += tz;
j += 2;
}
p += f * u / z;
}
p * 2.0 / PI
} else {
let mut f = 1.0_f64;
let mut tz = 1.0_f64;
let mut j = 2i32;
while j <= df - 2 {
tz *= (j as f64 - 1.0) / (z * j as f64);
f += tz;
j += 2;
}
f * abs_t / (z * df as f64).sqrt()
};
if t < 0.0 {
0.5 - 0.5 * p
} else {
0.5 + 0.5 * p
}
}
pub fn stdtrit(df: i32, p: f64) -> f64 {
assert!(df >= 1, "stdtrit requires integer df >= 1, got {df}");
if p == 0.5 {
return 0.0;
}
assert!(p > 0.0 && p < 1.0, "stdtrit requires 0 < p < 1, got {p}");
if df == 1 {
return if p > 0.5 {
(PI * (1.0 - p)).cos() / (PI * (1.0 - p)).sin()
} else {
-(PI * p).cos() / (PI * p).sin()
};
}
if df == 2 {
return (2.0 * p - 1.0) / (2.0 * p * (1.0 - p)).sqrt();
}
let sign = if p > 0.5 { 1.0 } else { -1.0 };
let q = if p > 0.5 { p } else { 1.0 - p };
let mut lo = 0.0_f64;
let mut hi = 1.0_f64;
while stdtr(df, hi) < q {
hi *= 2.0;
}
let log_norm =
ln_gamma(0.5 * (df as f64 + 1.0)) - 0.5 * (df as f64 * PI).ln() - ln_gamma(0.5 * df as f64);
let mut t = 0.5 * (lo + hi);
let eps = 1e-10;
for _ in 0..50 {
let f_val = stdtr(df, t);
if f_val < q {
lo = t;
} else {
hi = t;
}
let log_f = log_norm - 0.5 * (df as f64 + 1.0) * (t * t / df as f64).ln_1p();
let f = log_f.exp();
let t_newton = t - (f_val - q) / f;
t = if lo <= t_newton && t_newton <= hi {
t_newton
} else {
0.5 * (lo + hi)
};
if hi - lo < eps * (1.0 + t.abs()) {
break;
}
}
sign * t
}
fn ln_gamma(x: f64) -> f64 {
const COEFFS: [f64; 9] = [
0.999_999_999_999_809_9,
676.520_368_121_885_1,
-1_259.139_216_722_402_8,
771.323_428_777_653_1,
-176.615_029_162_140_6,
12.507_343_278_686_905,
-0.138_571_095_265_720_12,
9.984_369_578_019_572e-6,
1.505_632_735_149_311_6e-7,
];
let z = x - 1.0;
let mut sum = COEFFS[0];
for (i, &c) in COEFFS.iter().enumerate().skip(1) {
sum += c / (z + i as f64);
}
let t = z + 7.5;
0.5 * (2.0 * PI).ln() + (z + 0.5) * t.ln() - t + sum.ln()
}
fn gamma(x: f64) -> f64 {
ln_gamma(x).exp()
}
pub trait Distribution {
fn cdf(&self, x: f64) -> f64;
fn inverse_cdf(&self, u: f64) -> f64;
fn pdf(&self, x: f64) -> f64;
}
pub struct UniformDistribution {
half_width: f64,
}
impl UniformDistribution {
pub fn new(half_width: f64) -> Self {
Self { half_width }
}
}
impl Distribution for UniformDistribution {
fn cdf(&self, x: f64) -> f64 {
if x < -self.half_width {
return 0.0;
}
if x > self.half_width {
return 1.0;
}
(x + self.half_width) / (2.0 * self.half_width)
}
fn inverse_cdf(&self, u: f64) -> f64 {
-self.half_width + 2.0 * self.half_width * u
}
fn pdf(&self, x: f64) -> f64 {
if -self.half_width <= x && x <= self.half_width {
1.0 / (2.0 * self.half_width)
} else {
0.0
}
}
}
pub struct LogStudentTDistribution {
scale_bits: f64,
df: i32,
t_coef: f64,
}
impl LogStudentTDistribution {
pub fn new(scale_bits: f64, df: i32) -> Self {
let t_coef =
gamma((df as f64 + 1.0) / 2.0) / ((df as f64 * PI).sqrt() * gamma(df as f64 / 2.0));
Self {
scale_bits,
df,
t_coef,
}
}
}
const LN_2: f64 = std::f64::consts::LN_2;
impl Distribution for LogStudentTDistribution {
fn cdf(&self, x: f64) -> f64 {
let y = (1.0 + x.abs()).log2().copysign(x) / self.scale_bits;
stdtr(self.df, y)
}
fn inverse_cdf(&self, u: f64) -> f64 {
let mut y = self.scale_bits * stdtrit(self.df, u);
y = y.clamp(-1023.0, 1023.0);
(y.abs() * LN_2).exp_m1().copysign(y)
}
fn pdf(&self, x: f64) -> f64 {
let y = (1.0 + x.abs()).log2().copysign(x) / self.scale_bits;
let f_t =
self.t_coef * (1.0 + y * y / self.df as f64).powf(-((self.df as f64 + 1.0) / 2.0));
f_t / (self.scale_bits * (1.0 + x.abs()) * LN_2)
}
}
pub struct PiecewiseDistribution<I: Distribution, O: Distribution> {
inner: I,
outer: O,
switchover: f64,
alpha: f64,
beta: f64,
inner_g_neg: f64,
outer_g_pos: f64,
inner_mass: f64,
left_mass: f64,
}
impl<I: Distribution, O: Distribution> PiecewiseDistribution<I, O> {
pub fn new(inner: I, outer: O, switchover: f64) -> Self {
let inner_g_neg = inner.cdf(-switchover);
let inner_g_pos = inner.cdf(switchover);
let outer_g_neg = outer.cdf(-switchover);
let outer_g_pos = outer.cdf(switchover);
let outer_outer_mass = 1.0 - (outer_g_pos - outer_g_neg);
let inner_inner_mass = inner_g_pos - inner_g_neg;
let inner_pdf = inner.pdf(switchover);
let outer_pdf = outer.pdf(switchover);
assert!(
inner_pdf != 0.0,
"inner.pdf(switchover={switchover}) == 0; cannot density-match"
);
let alpha = 1.0 / (outer_pdf * inner_inner_mass / inner_pdf + outer_outer_mass);
let beta = alpha * outer_pdf / inner_pdf;
let inner_mass = beta * inner_inner_mass;
let left_mass = alpha * outer_g_neg;
Self {
inner,
outer,
switchover,
alpha,
beta,
inner_g_neg,
outer_g_pos,
inner_mass,
left_mass,
}
}
}
impl<I: Distribution, O: Distribution> Distribution for PiecewiseDistribution<I, O> {
fn cdf(&self, x: f64) -> f64 {
if x <= -self.switchover {
self.alpha * self.outer.cdf(x)
} else if x < self.switchover {
self.left_mass + self.beta * (self.inner.cdf(x) - self.inner_g_neg)
} else {
self.left_mass + self.inner_mass + self.alpha * (self.outer.cdf(x) - self.outer_g_pos)
}
}
fn inverse_cdf(&self, u: f64) -> f64 {
if u <= self.left_mass {
self.outer.inverse_cdf(u / self.alpha)
} else if u < self.left_mass + self.inner_mass {
let target = self.inner_g_neg + (u - self.left_mass) / self.beta;
self.inner.inverse_cdf(target)
} else {
self.outer
.inverse_cdf((u - self.left_mass - self.inner_mass) / self.alpha + self.outer_g_pos)
}
}
fn pdf(&self, x: f64) -> f64 {
if x.abs() < self.switchover {
self.beta * self.inner.pdf(x)
} else {
self.alpha * self.outer.pdf(x)
}
}
}
#[cfg(test)]
#[path = "../../tests/embedded/native/statistics_tests.rs"]
mod tests;