use super::super::{Cdf, LogCdf, Moments, Pdf, Quantile, Sample, bisection_quantile, count_to_f64};
use crate::distributions::TDistribution;
use crate::rng::SplitMix64;
use crate::special::{betai, ln_betai_lower, ln_gamma};
use std::f64::consts::{LN_2, PI};
impl TDistribution {
fn nu(&self) -> f64 {
count_to_f64(self.degrees_of_freedom)
}
}
impl Pdf for TDistribution {
fn pdf(&self, x: f64) -> f64 {
let nu = self.nu();
let gamma_ratio = ln_gamma(0.5 * (nu + 1.0)) - ln_gamma(0.5 * nu);
let log_norm = 0.5f64.mul_add(-(nu * PI).ln(), gamma_ratio);
let ln_kernel = -0.5 * (nu + 1.0) * x.mul_add(x / nu, 1.0).ln();
(log_norm + ln_kernel).exp()
}
}
impl Cdf for TDistribution {
fn cdf(&self, x: f64) -> f64 {
let nu = self.nu();
let ib = betai(0.5 * nu, 0.5, nu / x.mul_add(x, nu));
if x >= 0.0 {
0.5f64.mul_add(-ib, 1.0)
} else {
0.5 * ib
}
}
}
impl LogCdf for TDistribution {
fn logsf(&self, x: f64) -> f64 {
self.logcdf(-x)
}
fn logcdf(&self, x: f64) -> f64 {
let nu = self.nu();
let ln_ib = ln_betai_lower(0.5 * nu, 0.5, nu / x.mul_add(x, nu));
if x <= 0.0 {
ln_ib - LN_2
} else {
(-0.5 * ln_ib.exp()).ln_1p()
}
}
}
impl Quantile for TDistribution {
fn quantile(&self, p: f64) -> f64 {
if (p - 0.5).abs() < f64::EPSILON {
return 0.0;
}
if p < 0.5 {
return -self.quantile(1.0 - p);
}
bisection_quantile(p, 0.0, 1.0e6, |x| self.cdf(x))
}
}
impl Moments for TDistribution {
fn mean(&self) -> Option<f64> {
if self.degrees_of_freedom > 1 {
Some(0.0)
} else {
None
}
}
fn variance(&self) -> Option<f64> {
let nu = self.nu();
if self.degrees_of_freedom > 2 {
Some(nu / (nu - 2.0))
} else {
None
}
}
}
impl Sample for TDistribution {
fn sample(&self, rng: &mut SplitMix64) -> f64 {
let z = rng.standard_normal();
let mut chi2 = 0.0;
for _ in 0..self.degrees_of_freedom.max(1) {
let g = rng.standard_normal();
chi2 = g.mul_add(g, chi2);
}
z / (chi2 / self.nu()).sqrt()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn density_is_symmetric() {
let d = TDistribution {
degrees_of_freedom: 7,
..Default::default()
};
assert!((d.pdf(1.3) - d.pdf(-1.3)).abs() < 1e-12);
}
#[test]
fn cdf_at_zero_is_half() {
let d = TDistribution {
degrees_of_freedom: 4,
..Default::default()
};
assert!((d.cdf(0.0) - 0.5).abs() < 1e-12);
}
#[test]
fn logsf_matches_scipy_and_stays_finite() {
let d = TDistribution {
degrees_of_freedom: 5,
..Default::default()
};
let body = d.logsf(1.0);
assert!(
((body - (1.0 - d.cdf(1.0)).ln()) / body.abs()).abs() < 1e-9,
"logsf body was {body}"
);
let tail = d.logsf(50.0);
let want = -17.314_140_361_404_83; assert!(tail.is_finite(), "logsf(50) was {tail}");
assert!(
((tail - want) / want).abs() < 1e-9,
"logsf(50) = {tail}, want {want}"
);
}
}