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