1#[cfg(feature = "regression")]
12pub(crate) const Z_975: f64 = 1.959_963_984_540_054;
13
14#[cfg(feature = "regression")]
20pub fn qt(p: f64, df: f64) -> f64 {
21 use anofox_regression::distributions::{ContinuousCDF, StudentsT};
22 if df <= 0.0 {
23 return f64::NAN;
24 }
25 StudentsT::new(0.0, 1.0, df)
26 .map(|d| d.inverse_cdf(p))
27 .unwrap_or(Z_975)
28}
29
30#[cfg(not(feature = "regression"))]
32pub fn qt(p: f64, df: f64) -> f64 {
33 qt_native(p, df)
34}
35
36#[cfg_attr(feature = "regression", allow(dead_code))]
37fn beta_cf(a: f64, b: f64, x: f64) -> f64 {
40 const TINY: f64 = 1e-300;
41 let (qab, qap, qam) = (a + b, a + 1.0, a - 1.0);
42 let mut c = 1.0;
43 let mut d = 1.0 - qab * x / qap;
44 if d.abs() < TINY {
45 d = TINY;
46 }
47 d = 1.0 / d;
48 let mut h = d;
49 for m in 1..=300 {
50 let m = m as f64;
51 let m2 = 2.0 * m;
52 let aa = m * (b - m) * x / ((qam + m2) * (a + m2));
53 d = 1.0 + aa * d;
54 d = if d.abs() < TINY { TINY } else { d };
55 c = 1.0 + aa / c;
56 c = if c.abs() < TINY { TINY } else { c };
57 d = 1.0 / d;
58 h *= d * c;
59 let aa = -(a + m) * (qab + m) * x / ((a + m2) * (qap + m2));
60 d = 1.0 + aa * d;
61 d = if d.abs() < TINY { TINY } else { d };
62 c = 1.0 + aa / c;
63 c = if c.abs() < TINY { TINY } else { c };
64 d = 1.0 / d;
65 let del = d * c;
66 h *= del;
67 if (del - 1.0).abs() < 1e-15 {
68 break;
69 }
70 }
71 h
72}
73
74#[cfg_attr(feature = "regression", allow(dead_code))]
75fn inc_beta(a: f64, b: f64, x: f64) -> f64 {
77 use crate::stat::distribution::ln_gamma;
78 if x <= 0.0 {
79 return 0.0;
80 }
81 if x >= 1.0 {
82 return 1.0;
83 }
84 let front =
85 (ln_gamma(a + b) - ln_gamma(a) - ln_gamma(b) + a * x.ln() + b * (1.0 - x).ln()).exp();
86 if x < (a + 1.0) / (a + b + 2.0) {
87 front * beta_cf(a, b, x) / a
88 } else {
89 1.0 - front * beta_cf(b, a, 1.0 - x) / b
90 }
91}
92
93#[cfg_attr(feature = "regression", allow(dead_code))]
94pub(crate) fn pt_native(t: f64, df: f64) -> f64 {
96 let tail = 0.5 * inc_beta(df / 2.0, 0.5, df / (df + t * t));
97 if t >= 0.0 {
98 1.0 - tail
99 } else {
100 tail
101 }
102}
103
104#[cfg_attr(feature = "regression", allow(dead_code))]
105pub(crate) fn qt_native(p: f64, df: f64) -> f64 {
107 if df.is_nan() || df <= 0.0 || !(0.0..=1.0).contains(&p) {
108 return f64::NAN;
109 }
110 if p == 0.0 {
111 return f64::NEG_INFINITY;
112 }
113 if p == 1.0 {
114 return f64::INFINITY;
115 }
116 let (mut lo, mut hi) = (-1.0, 1.0);
117 while pt_native(lo, df) > p {
118 lo *= 2.0;
119 }
120 while pt_native(hi, df) < p {
121 hi *= 2.0;
122 }
123 for _ in 0..200 {
124 let mid = 0.5 * (lo + hi);
125 if pt_native(mid, df) < p {
126 lo = mid;
127 } else {
128 hi = mid;
129 }
130 if hi - lo < 1e-12 * hi.abs().max(1.0) {
131 break;
132 }
133 }
134 0.5 * (lo + hi)
135}
136
137#[cfg(feature = "regression")]
143pub fn qnorm(p: f64) -> f64 {
144 use anofox_regression::distributions::{ContinuousCDF, Normal};
145 if p <= 0.0 {
146 return f64::NEG_INFINITY;
147 }
148 if p >= 1.0 {
149 return f64::INFINITY;
150 }
151 Normal::new(0.0, 1.0)
152 .map(|d| d.inverse_cdf(p))
153 .unwrap_or(0.0)
154}
155
156#[cfg(not(feature = "regression"))]
159pub fn qnorm(p: f64) -> f64 {
160 if p <= 0.0 {
161 return f64::NEG_INFINITY;
162 }
163 if p >= 1.0 {
164 return f64::INFINITY;
165 }
166 fn rational_approx(t: f64) -> f64 {
168 let (c0, c1, c2) = (2.515_517, 0.802_853, 0.010_328);
169 let (d1, d2, d3) = (1.432_788, 0.189_269, 0.001_308);
170 t - (c0 + c1 * t + c2 * t * t) / (1.0 + d1 * t + d2 * t * t + d3 * t * t * t)
171 }
172 if p < 0.5 {
173 -rational_approx((-2.0 * p.ln()).sqrt())
174 } else if p > 0.5 {
175 rational_approx((-2.0 * (1.0 - p).ln()).sqrt())
176 } else {
177 0.0
178 }
179}
180
181#[cfg(feature = "regression")]
188pub fn ellipse_radius(level: f64, n: usize) -> f64 {
189 use anofox_regression::distributions::{ContinuousCDF, FisherSnedecor};
190 let dfd = (n as f64 - 1.0).max(1.0);
191 match FisherSnedecor::new(2.0, dfd) {
192 Ok(f) => (2.0 * f.inverse_cdf(level)).max(0.0).sqrt(),
193 Err(_) => (-2.0 * (1.0 - level).ln()).sqrt(),
194 }
195}
196
197#[cfg(not(feature = "regression"))]
199pub fn ellipse_radius(level: f64, _n: usize) -> f64 {
200 (-2.0 * (1.0 - level).ln()).sqrt()
201}
202
203#[cfg(all(test, feature = "regression"))]
204mod tests {
205 use super::{ellipse_radius, qnorm, qt};
206
207 #[test]
208 fn qnorm_matches_r() {
209 let cases = [
211 (0.025, -1.959_963_984_540_054),
212 (0.25, -0.674_489_750_196_082),
213 (0.5, 0.0),
214 (0.75, 0.674_489_750_196_082),
215 (0.975, 1.959_963_984_540_054),
216 ];
217 for (p, want) in cases {
218 assert!((qnorm(p) - want).abs() < 1e-9, "qnorm({p})");
219 }
220 }
221
222 #[test]
223 fn ellipse_radius_is_monotonic_and_finite() {
224 let small = ellipse_radius(0.5, 30);
225 let big = ellipse_radius(0.99, 30);
226 assert!(big > small && small > 0.0 && big.is_finite());
227 }
228
229 #[test]
231 fn qt_matches_r() {
232 let cases = [
233 (1.0, 12.706_204_736_432_1),
234 (2.0, 4.302_652_729_912_0),
235 (5.0, 2.570_581_835_636_1),
236 (10.0, 2.228_138_851_986_3),
237 (30.0, 2.042_272_456_301_4),
238 (100.0, 1.983_971_518_449_5),
239 ];
240 for (df, want) in cases {
241 let got = qt(0.975, df);
242 assert!(
243 (got - want).abs() < 1e-6,
244 "qt(0.975,{df}) = {got}, want {want}"
245 );
246 }
247 }
248}
249
250#[cfg(test)]
251mod native_t_tests {
252 use super::qt_native;
253
254 #[test]
255 fn qt_matches_r() {
256 for (p, df, want) in [
258 (0.975, 6.0, 2.446911851),
259 (0.95, 6.0, 1.943180281),
260 (0.995, 3.0, 5.840909310),
261 (0.025, 10.0, -2.228138852),
262 (0.9, 1.0, 3.077683537),
263 (0.975, 1000.0, 1.962339081),
264 (0.975, 2.5, 3.574654842),
265 ] {
266 let got = qt_native(p, df);
267 assert!((got - want).abs() < 1e-7, "qt({p}, {df}) = {got}, R {want}");
268 }
269 }
270}