use super::Cdf;
use super::gamma::CumulativeGammaDistribution;
use crate::errors::QlResult;
use crate::require;
use crate::types::Real;
#[derive(Clone, Copy, Debug)]
pub struct CumulativeChiSquareDistribution {
gamma: CumulativeGammaDistribution,
}
impl CumulativeChiSquareDistribution {
pub fn new(df: Real) -> QlResult<Self> {
require!(
df.is_finite() && df > 0.0,
"chi-square degrees of freedom must be a finite positive number, got {df}"
);
Ok(CumulativeChiSquareDistribution {
gamma: CumulativeGammaDistribution::new(0.5 * df)?,
})
}
}
impl Cdf for CumulativeChiSquareDistribution {
fn cdf(&self, x: Real) -> Real {
self.gamma.cdf(0.5 * x)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_close(got: Real, expected: Real, tol: Real) {
assert!(
(got - expected).abs() <= tol,
"got {got}, expected {expected}, diff {}",
(got - expected).abs()
);
}
#[test]
fn boundary_values_at_support_edges() {
let dist = CumulativeChiSquareDistribution::new(3.0).unwrap();
assert_eq!(dist.cdf(0.0), 0.0);
assert_eq!(dist.cdf(-2.0), 0.0);
assert_eq!(dist.cdf(Real::INFINITY), 1.0);
}
#[test]
fn df_eq_2_is_exponential() {
let dist = CumulativeChiSquareDistribution::new(2.0).unwrap();
for x in [0.5, 1.0, 2.0, 5.0, 20.0_f64] {
assert_close(dist.cdf(x), 1.0 - (-0.5 * x).exp(), 1e-12);
}
}
#[test]
fn agrees_with_gamma_at_half_arguments() {
for df in [1.0, 4.0, 9.0] {
let chi = CumulativeChiSquareDistribution::new(df).unwrap();
let gamma = CumulativeGammaDistribution::new(0.5 * df).unwrap();
for x in [0.5, 2.0, 7.0, 15.0] {
assert_eq!(chi.cdf(x), gamma.cdf(0.5 * x));
}
}
}
#[test]
fn stays_in_unit_interval_and_increases() {
let dist = CumulativeChiSquareDistribution::new(5.0).unwrap();
let mut prev = 0.0;
let mut x = 0.0;
while x < 50.0 {
x += 0.1;
let p = dist.cdf(x);
assert!((0.0..=1.0).contains(&p), "cdf({x}) = {p}");
assert!(p >= prev, "not increasing at x={x}: {prev} -> {p}");
prev = p;
}
}
#[test]
fn large_df_near_mean_does_not_panic() {
let d1000 = CumulativeChiSquareDistribution::new(1000.0).unwrap();
assert!((d1000.cdf(1000.0) - 0.5059471460854907).abs() < 1e-12);
assert!((d1000.cdf(1200.0) - 0.9999877440576714).abs() < 1e-12);
for df in [400.0, 1000.0, 5000.0] {
let dist = CumulativeChiSquareDistribution::new(df).unwrap();
let mut prev = 0.0;
let mut x = 0.5 * df;
while x <= 1.5 * df {
let p = dist.cdf(x);
assert!((0.0..=1.0).contains(&p), "cdf({x}) = {p} for df={df}");
assert!(p >= prev, "not increasing at x={x} for df={df}");
prev = p;
x += 0.02 * df;
}
}
}
#[test]
fn nan_x_is_nan() {
assert!(
CumulativeChiSquareDistribution::new(3.0)
.unwrap()
.cdf(Real::NAN)
.is_nan()
);
}
#[test]
fn new_rejects_nonpositive_nan_and_infinite_df() {
assert!(CumulativeChiSquareDistribution::new(0.0).is_err());
assert!(CumulativeChiSquareDistribution::new(-1.0).is_err());
assert!(CumulativeChiSquareDistribution::new(Real::NAN).is_err());
assert!(CumulativeChiSquareDistribution::new(Real::INFINITY).is_err());
}
}