stats_claw/distributions/sampling/
chi_squared.rs1use super::super::{Cdf, LogCdf, Moments, Pdf, Quantile, Sample, bisection_quantile, count_to_f64};
24use crate::distributions::ChiSquaredDistribution;
25use crate::rng::SplitMix64;
26use crate::special::{gamma_p, ln_gamma, ln_gamma_p, ln_gamma_q};
27
28impl ChiSquaredDistribution {
29 fn k(&self) -> f64 {
31 count_to_f64(self.degrees_of_freedom)
32 }
33}
34
35impl Pdf for ChiSquaredDistribution {
36 fn pdf(&self, x: f64) -> f64 {
37 if x < 0.0 {
38 return 0.0;
39 }
40 if x == 0.0 {
41 return if (self.k() - 2.0).abs() < 1e-12 {
44 0.5
45 } else {
46 0.0
47 };
48 }
49 let half_k = 0.5 * self.k();
50 let log_norm = half_k.mul_add(2.0_f64.ln(), ln_gamma(half_k));
51 let tail = (-0.5f64).mul_add(x, -log_norm);
52 let ln_density = (half_k - 1.0).mul_add(x.ln(), tail);
53 ln_density.exp()
54 }
55}
56
57impl Cdf for ChiSquaredDistribution {
58 fn cdf(&self, x: f64) -> f64 {
59 if x <= 0.0 {
60 0.0
61 } else {
62 gamma_p(0.5 * self.k(), 0.5 * x)
63 }
64 }
65}
66
67impl LogCdf for ChiSquaredDistribution {
68 fn logsf(&self, x: f64) -> f64 {
69 if x <= 0.0 {
71 0.0
72 } else {
73 ln_gamma_q(0.5 * self.k(), 0.5 * x)
74 }
75 }
76 fn logcdf(&self, x: f64) -> f64 {
77 if x <= 0.0 {
79 f64::NEG_INFINITY
80 } else {
81 ln_gamma_p(0.5 * self.k(), 0.5 * x)
82 }
83 }
84}
85
86impl Quantile for ChiSquaredDistribution {
87 fn quantile(&self, p: f64) -> f64 {
88 let k = self.k();
89 let hi = 20.0f64.mul_add((2.0 * k).sqrt(), k).max(1.0);
90 bisection_quantile(p, 0.0, hi, |x| self.cdf(x))
91 }
92}
93
94impl Moments for ChiSquaredDistribution {
95 fn mean(&self) -> Option<f64> {
96 Some(self.k())
97 }
98 fn variance(&self) -> Option<f64> {
99 Some(2.0 * self.k())
100 }
101}
102
103impl Sample for ChiSquaredDistribution {
104 fn sample(&self, rng: &mut SplitMix64) -> f64 {
105 let mut sum = 0.0;
106 for _ in 0..self.degrees_of_freedom.max(0) {
107 let z = rng.standard_normal();
108 sum = z.mul_add(z, sum);
109 }
110 sum
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 #[test]
120 fn moments_are_df_and_twice_df() {
121 let d = ChiSquaredDistribution {
122 degrees_of_freedom: 5,
123 ..Default::default()
124 };
125 assert_eq!(d.mean(), Some(5.0));
126 assert_eq!(d.variance(), Some(10.0));
127 }
128
129 #[test]
131 fn cdf_saturates() {
132 let d = ChiSquaredDistribution {
133 degrees_of_freedom: 3,
134 ..Default::default()
135 };
136 assert!(d.cdf(0.0).abs() < 1e-12);
137 assert!(d.cdf(1e6) > 1.0 - 1e-9);
138 }
139
140 #[test]
143 fn logsf_matches_scipy_and_stays_finite() {
144 let d = ChiSquaredDistribution {
145 degrees_of_freedom: 3,
146 ..Default::default()
147 };
148 let body = d.logsf(2.0);
149 assert!(
150 ((body - (1.0 - d.cdf(2.0)).ln()) / body.abs()).abs() < 1e-9,
151 "logsf body was {body}"
152 );
153 let tail = d.logsf(200.0);
154 let want = -97.571_669_639_663_45; assert!(tail.is_finite(), "logsf(200) was {tail}");
156 assert!(
157 ((tail - want) / want).abs() < 1e-9,
158 "logsf(200) = {tail}, want {want}"
159 );
160 }
161}