Skip to main content

bicmath_statistics/
mathfn.rs

1//! Special functions and probability distributions implemented from scratch.
2//!
3//! The implementation follows the standard series and continued-fraction
4//! methods for the regularized incomplete gamma and beta functions
5//! (Numerical Recipes style, with explicit iteration bounds and a structured
6//! `NonConvergence` error). No `statrs` or platform `erf` is used; on wasm32
7//! every transcendental falls back to `libm`.
8
9use bicmath_core::error::{EngineError, ErrorCode};
10
11const EPS: f64 = 1e-16;
12const FPMIN: f64 = f64::MIN_POSITIVE / EPS;
13const MAX_ITER: u64 = 100_000;
14const LN_2_PI: f64 = 1.837_877_066_409_345_3;
15const SQRT_2PI: f64 = 2.506_628_274_631_000_7;
16const SQRT_2: f64 = std::f64::consts::SQRT_2;
17
18// ---------------------------------------------------------------------------
19// libm fallbacks for wasm32
20// ---------------------------------------------------------------------------
21
22#[cfg(target_arch = "wasm32")]
23pub fn exp(x: f64) -> f64 {
24    libm::exp(x)
25}
26#[cfg(not(target_arch = "wasm32"))]
27pub fn exp(x: f64) -> f64 {
28    x.exp()
29}
30
31#[cfg(target_arch = "wasm32")]
32pub fn ln(x: f64) -> f64 {
33    libm::log(x)
34}
35#[cfg(not(target_arch = "wasm32"))]
36pub fn ln(x: f64) -> f64 {
37    x.ln()
38}
39
40#[cfg(target_arch = "wasm32")]
41pub fn sqrt(x: f64) -> f64 {
42    libm::sqrt(x)
43}
44#[cfg(not(target_arch = "wasm32"))]
45pub fn sqrt(x: f64) -> f64 {
46    x.sqrt()
47}
48
49#[cfg(target_arch = "wasm32")]
50pub fn sin(x: f64) -> f64 {
51    libm::sin(x)
52}
53#[cfg(not(target_arch = "wasm32"))]
54pub fn sin(x: f64) -> f64 {
55    x.sin()
56}
57
58#[cfg(target_arch = "wasm32")]
59pub fn abs(x: f64) -> f64 {
60    libm::fabs(x)
61}
62#[cfg(not(target_arch = "wasm32"))]
63pub fn abs(x: f64) -> f64 {
64    x.abs()
65}
66
67fn non_convergence(what: &str) -> EngineError {
68    EngineError::new(
69        ErrorCode::NonConvergence,
70        format!("{what} did not converge within {MAX_ITER} iterations"),
71    )
72}
73
74// ---------------------------------------------------------------------------
75// Gamma and log-gamma
76// ---------------------------------------------------------------------------
77
78const LANCZOS: [f64; 9] = [
79    0.999_999_999_999_809_9,
80    676.520_368_121_885_1,
81    -1_259.139_216_722_402_8,
82    771.323_428_777_653_1,
83    -176.615_029_162_140_6,
84    12.507_343_278_686_905,
85    -0.138_571_095_265_720_12,
86    9.984_369_578_019_572e-6,
87    1.505_632_735_149_311_6e-7,
88];
89
90/// Lanczos approximation to ln|Gamma(x)|.
91pub fn lgamma(x: f64) -> f64 {
92    if x < 0.5 {
93        // Reflection: Gamma(x) Gamma(1-x) = pi / sin(pi x).
94        let pi = std::f64::consts::PI;
95        return ln(pi / abs(sin(pi * x))) - lgamma(1.0 - x);
96    }
97    let z = x - 1.0;
98    let mut series = LANCZOS[0];
99    for (index, coefficient) in LANCZOS.iter().enumerate().skip(1) {
100        series += coefficient / (z + index as f64);
101    }
102    let t = z + 7.5;
103    0.5 * LN_2_PI + (z + 0.5) * ln(t) - t + ln(series)
104}
105
106// ---------------------------------------------------------------------------
107// Regularized incomplete gamma
108// ---------------------------------------------------------------------------
109
110/// Regularized lower incomplete gamma `P(a, x)`.
111pub fn gamma_p(a: f64, x: f64) -> Result<f64, EngineError> {
112    if a <= 0.0 || !a.is_finite() {
113        return Err(EngineError::domain("incomplete gamma requires a > 0"));
114    }
115    if x <= 0.0 {
116        return Ok(0.0);
117    }
118    if x < a + 1.0 {
119        gamma_p_series(a, x)
120    } else {
121        Ok(1.0 - gamma_q_cf(a, x)?)
122    }
123}
124
125/// Regularized upper incomplete gamma `Q(a, x)`.
126pub fn gamma_q(a: f64, x: f64) -> Result<f64, EngineError> {
127    if a <= 0.0 || !a.is_finite() {
128        return Err(EngineError::domain("incomplete gamma requires a > 0"));
129    }
130    if x <= 0.0 {
131        return Ok(1.0);
132    }
133    if x < a + 1.0 {
134        Ok(1.0 - gamma_p_series(a, x)?)
135    } else {
136        gamma_q_cf(a, x)
137    }
138}
139
140fn gamma_p_series(a: f64, x: f64) -> Result<f64, EngineError> {
141    let mut ap = a;
142    let mut sum = 1.0 / a;
143    let mut del = sum;
144    let mut iterations = 0u64;
145    loop {
146        iterations += 1;
147        if iterations > MAX_ITER {
148            return Err(non_convergence("incomplete gamma series"));
149        }
150        ap += 1.0;
151        del *= x / ap;
152        sum += del;
153        if abs(del) < abs(sum) * EPS {
154            break;
155        }
156    }
157    let prefactor = exp(-x + a * ln(x) - lgamma(a));
158    Ok(sum * prefactor)
159}
160
161fn gamma_q_cf(a: f64, x: f64) -> Result<f64, EngineError> {
162    let mut b = x + 1.0 - a;
163    let mut c = 1.0 / FPMIN;
164    let mut d = 1.0 / b;
165    let mut h = d;
166    let mut iterations = 0u64;
167    loop {
168        iterations += 1;
169        if iterations > MAX_ITER {
170            return Err(non_convergence("incomplete gamma continued fraction"));
171        }
172        let an = -(iterations as f64) * (iterations as f64 - a);
173        b += 2.0;
174        d = an * d + b;
175        if abs(d) < FPMIN {
176            d = FPMIN;
177        }
178        c = b + an / c;
179        if abs(c) < FPMIN {
180            c = FPMIN;
181        }
182        d = 1.0 / d;
183        let delta = d * c;
184        h *= delta;
185        if abs(delta - 1.0) < EPS {
186            break;
187        }
188    }
189    let prefactor = exp(-x + a * ln(x) - lgamma(a));
190    Ok(prefactor * h)
191}
192
193// ---------------------------------------------------------------------------
194// Regularized incomplete beta
195// ---------------------------------------------------------------------------
196
197/// Regularized incomplete beta `I_x(a, b)`.
198pub fn ibeta(a: f64, b: f64, x: f64) -> Result<f64, EngineError> {
199    if a <= 0.0 || b <= 0.0 || !a.is_finite() || !b.is_finite() {
200        return Err(EngineError::domain(
201            "incomplete beta requires finite positive shape parameters",
202        ));
203    }
204    if x <= 0.0 {
205        return Ok(0.0);
206    }
207    if x >= 1.0 {
208        return Ok(1.0);
209    }
210    let front = exp(lgamma(a + b) - lgamma(a) - lgamma(b) + a * ln(x) + b * ln(1.0 - x));
211    if x < (a + 1.0) / (a + b + 2.0) {
212        Ok(front * betacf(a, b, x)? / a)
213    } else {
214        Ok(1.0 - front * betacf(b, a, 1.0 - x)? / b)
215    }
216}
217
218fn betacf(a: f64, b: f64, x: f64) -> Result<f64, EngineError> {
219    let qab = a + b;
220    let qap = a + 1.0;
221    let qam = a - 1.0;
222    let mut c = 1.0;
223    let mut d = 1.0 - qab * x / qap;
224    if abs(d) < FPMIN {
225        d = FPMIN;
226    }
227    d = 1.0 / d;
228    let mut h = d;
229    let mut m = 0u64;
230    loop {
231        m += 1;
232        if m > MAX_ITER {
233            return Err(non_convergence("incomplete beta continued fraction"));
234        }
235        let mf = m as f64;
236        let m2 = 2.0 * mf;
237        let aa = mf * (b - mf) * x / ((qam + m2) * (a + m2));
238        d = 1.0 + aa * d;
239        if abs(d) < FPMIN {
240            d = FPMIN;
241        }
242        c = 1.0 + aa / c;
243        if abs(c) < FPMIN {
244            c = FPMIN;
245        }
246        d = 1.0 / d;
247        h *= d * c;
248        let aa = -(a + mf) * (qab + mf) * x / ((a + m2) * (qap + m2));
249        d = 1.0 + aa * d;
250        if abs(d) < FPMIN {
251            d = FPMIN;
252        }
253        c = 1.0 + aa / c;
254        if abs(c) < FPMIN {
255            c = FPMIN;
256        }
257        d = 1.0 / d;
258        let delta = d * c;
259        h *= delta;
260        if abs(delta - 1.0) < EPS {
261            return Ok(h);
262        }
263    }
264}
265
266// ---------------------------------------------------------------------------
267// Error function
268// ---------------------------------------------------------------------------
269
270/// Error function, computed from the regularized incomplete gamma:
271/// `erf(x) = P(1/2, x^2)` for `x >= 0`.
272pub fn erf(x: f64) -> Result<f64, EngineError> {
273    if x == 0.0 {
274        return Ok(0.0);
275    }
276    let magnitude = gamma_p(0.5, x * x)?;
277    Ok(if x > 0.0 { magnitude } else { -magnitude })
278}
279
280/// Complementary error function, computed from the regularized upper
281/// incomplete gamma so that the positive tail keeps full relative accuracy.
282pub fn erfc(x: f64) -> Result<f64, EngineError> {
283    if x >= 0.0 {
284        gamma_q(0.5, x * x)
285    } else {
286        Ok(2.0 - gamma_q(0.5, x * x)?)
287    }
288}
289
290// ---------------------------------------------------------------------------
291// Normal distribution
292// ---------------------------------------------------------------------------
293
294fn normal_pdf_std(x: f64) -> f64 {
295    exp(-0.5 * x * x) / SQRT_2PI
296}
297
298pub fn normal_pdf(x: f64, mean: f64, sd: f64) -> Result<f64, EngineError> {
299    if !x.is_finite() || !mean.is_finite() {
300        return Err(EngineError::domain("normal_pdf requires finite inputs"));
301    }
302    if sd <= 0.0 || !sd.is_finite() {
303        return Err(EngineError::domain("normal_pdf requires sd > 0"));
304    }
305    let z = (x - mean) / sd;
306    Ok(normal_pdf_std(z) / sd)
307}
308
309pub fn normal_cdf(x: f64, mean: f64, sd: f64) -> Result<f64, EngineError> {
310    if !x.is_finite() || !mean.is_finite() {
311        return Err(EngineError::domain("normal_cdf requires finite inputs"));
312    }
313    if sd <= 0.0 || !sd.is_finite() {
314        return Err(EngineError::domain("normal_cdf requires sd > 0"));
315    }
316    Ok(0.5 * erfc(-(x - mean) / (sd * SQRT_2))?)
317}
318
319/// Upper tail `P(X > x)`, computed with `erfc` so large positive `x` keeps
320/// relative accuracy (never `1 - cdf`).
321pub fn normal_sf(x: f64, mean: f64, sd: f64) -> Result<f64, EngineError> {
322    if !x.is_finite() || !mean.is_finite() {
323        return Err(EngineError::domain("normal_sf requires finite inputs"));
324    }
325    if sd <= 0.0 || !sd.is_finite() {
326        return Err(EngineError::domain("normal_sf requires sd > 0"));
327    }
328    Ok(0.5 * erfc((x - mean) / (sd * SQRT_2))?)
329}
330
331pub fn normal_quantile(p: f64, mean: f64, sd: f64) -> Result<f64, EngineError> {
332    if !(0.0..1.0).contains(&p) {
333        return Err(EngineError::domain(
334            "normal_quantile requires p strictly between 0 and 1",
335        ));
336    }
337    if !mean.is_finite() || sd <= 0.0 || !sd.is_finite() {
338        return Err(EngineError::domain(
339            "normal_quantile requires finite mean and sd > 0",
340        ));
341    }
342    if p == 0.5 {
343        return Ok(mean);
344    }
345    let z = invert_monotone(
346        p,
347        -1.0,
348        1.0,
349        &mut |x| normal_cdf(x, 0.0, 1.0),
350        &mut normal_pdf_std,
351    )?;
352    Ok(mean + sd * z)
353}
354
355// ---------------------------------------------------------------------------
356// Student's t distribution
357// ---------------------------------------------------------------------------
358
359fn student_t_pdf_raw(x: f64, df: f64) -> f64 {
360    let half = 0.5 * df;
361    let ln_pdf = lgamma(half + 0.5)
362        - lgamma(half)
363        - 0.5 * ln(df * std::f64::consts::PI)
364        - (half + 0.5) * ln(1.0 + x * x / df);
365    exp(ln_pdf)
366}
367
368pub fn student_t_pdf(x: f64, df: f64) -> Result<f64, EngineError> {
369    if !x.is_finite() {
370        return Err(EngineError::domain("student_t_pdf requires a finite x"));
371    }
372    if df <= 0.0 || !df.is_finite() {
373        return Err(EngineError::domain("student_t_pdf requires df > 0"));
374    }
375    Ok(student_t_pdf_raw(x, df))
376}
377
378pub fn student_t_cdf(x: f64, df: f64) -> Result<f64, EngineError> {
379    if !x.is_finite() {
380        return Err(EngineError::domain("student_t_cdf requires a finite x"));
381    }
382    if df <= 0.0 || !df.is_finite() {
383        return Err(EngineError::domain("student_t_cdf requires df > 0"));
384    }
385    if x == 0.0 {
386        return Ok(0.5);
387    }
388    let y = df / (df + x * x);
389    let tail = ibeta(0.5 * df, 0.5, y)?;
390    if x > 0.0 {
391        Ok(1.0 - 0.5 * tail)
392    } else {
393        Ok(0.5 * tail)
394    }
395}
396
397pub fn student_t_sf(x: f64, df: f64) -> Result<f64, EngineError> {
398    if !x.is_finite() {
399        return Err(EngineError::domain("student_t_sf requires a finite x"));
400    }
401    if df <= 0.0 || !df.is_finite() {
402        return Err(EngineError::domain("student_t_sf requires df > 0"));
403    }
404    if x == 0.0 {
405        return Ok(0.5);
406    }
407    let y = df / (df + x * x);
408    let tail = ibeta(0.5 * df, 0.5, y)?;
409    if x > 0.0 {
410        Ok(0.5 * tail)
411    } else {
412        Ok(1.0 - 0.5 * tail)
413    }
414}
415
416pub fn student_t_quantile(p: f64, df: f64) -> Result<f64, EngineError> {
417    if !(0.0..1.0).contains(&p) {
418        return Err(EngineError::domain(
419            "student_t_quantile requires p strictly between 0 and 1",
420        ));
421    }
422    if df <= 0.0 || !df.is_finite() {
423        return Err(EngineError::domain("student_t_quantile requires df > 0"));
424    }
425    if p == 0.5 {
426        return Ok(0.0);
427    }
428    invert_monotone(p, -1.0, 1.0, &mut |x| student_t_cdf(x, df), &mut |x| {
429        student_t_pdf_raw(x, df)
430    })
431}
432
433// ---------------------------------------------------------------------------
434// Binomial distribution
435// ---------------------------------------------------------------------------
436
437pub fn binomial_pmf(k: f64, n: f64, p: f64) -> Result<f64, EngineError> {
438    if !k.is_finite() || !n.is_finite() || !p.is_finite() {
439        return Err(EngineError::domain("binomial_pmf requires finite inputs"));
440    }
441    if !(0.0..=1.0).contains(&p) {
442        return Err(EngineError::domain("binomial_pmf requires p in [0, 1]"));
443    }
444    if k < 0.0 || k > n {
445        return Ok(0.0);
446    }
447    if p == 0.0 {
448        return Ok(if k == 0.0 { 1.0 } else { 0.0 });
449    }
450    if p == 1.0 {
451        return Ok(if k == n { 1.0 } else { 0.0 });
452    }
453    if k == 0.0 {
454        return Ok(exp(n * ln(1.0 - p)));
455    }
456    if k == n {
457        return Ok(exp(n * ln(p)));
458    }
459    let ln_pmf =
460        lgamma(n + 1.0) - lgamma(k + 1.0) - lgamma(n - k + 1.0) + k * ln(p) + (n - k) * ln(1.0 - p);
461    Ok(exp(ln_pmf))
462}
463
464pub fn binomial_cdf(k: f64, n: f64, p: f64) -> Result<f64, EngineError> {
465    if !k.is_finite() || !n.is_finite() || !p.is_finite() {
466        return Err(EngineError::domain("binomial_cdf requires finite inputs"));
467    }
468    if !(0.0..=1.0).contains(&p) {
469        return Err(EngineError::domain("binomial_cdf requires p in [0, 1]"));
470    }
471    if k < 0.0 {
472        return Ok(0.0);
473    }
474    if k >= n {
475        return Ok(1.0);
476    }
477    ibeta(n - k, k + 1.0, 1.0 - p)
478}
479
480// ---------------------------------------------------------------------------
481// Chi-square distribution
482// ---------------------------------------------------------------------------
483
484pub fn chi_square_pdf(x: f64, df: f64) -> Result<f64, EngineError> {
485    if !x.is_finite() {
486        return Err(EngineError::domain("chi_square_pdf requires a finite x"));
487    }
488    if df <= 0.0 || !df.is_finite() {
489        return Err(EngineError::domain("chi_square_pdf requires df > 0"));
490    }
491    if x < 0.0 {
492        return Ok(0.0);
493    }
494    if x == 0.0 {
495        return match df.partial_cmp(&2.0) {
496            Some(std::cmp::Ordering::Less) => Err(EngineError::domain(
497                "chi_square_pdf is unbounded at x = 0 for df < 2",
498            )),
499            Some(std::cmp::Ordering::Equal) => Ok(0.5),
500            _ => Ok(0.0),
501        };
502    }
503    let half = 0.5 * df;
504    let ln_pdf = (half - 1.0) * ln(x) - 0.5 * x - half * ln(2.0) - lgamma(half);
505    Ok(exp(ln_pdf))
506}
507
508pub fn chi_square_cdf(x: f64, df: f64) -> Result<f64, EngineError> {
509    if !x.is_finite() {
510        return Err(EngineError::domain("chi_square_cdf requires a finite x"));
511    }
512    if df <= 0.0 || !df.is_finite() {
513        return Err(EngineError::domain("chi_square_cdf requires df > 0"));
514    }
515    if x <= 0.0 {
516        return Ok(0.0);
517    }
518    gamma_p(0.5 * df, 0.5 * x)
519}
520
521pub fn chi_square_sf(x: f64, df: f64) -> Result<f64, EngineError> {
522    if !x.is_finite() {
523        return Err(EngineError::domain("chi_square_sf requires a finite x"));
524    }
525    if df <= 0.0 || !df.is_finite() {
526        return Err(EngineError::domain("chi_square_sf requires df > 0"));
527    }
528    if x <= 0.0 {
529        return Ok(1.0);
530    }
531    gamma_q(0.5 * df, 0.5 * x)
532}
533
534pub fn chi_square_quantile(p: f64, df: f64) -> Result<f64, EngineError> {
535    if !(0.0..1.0).contains(&p) {
536        return Err(EngineError::domain(
537            "chi_square_quantile requires p in [0, 1)",
538        ));
539    }
540    if df <= 0.0 || !df.is_finite() {
541        return Err(EngineError::domain("chi_square_quantile requires df > 0"));
542    }
543    if p == 0.0 {
544        return Ok(0.0);
545    }
546    invert_monotone(p, 0.0, 1.0, &mut |x| chi_square_cdf(x, df), &mut |x| {
547        chi_square_pdf_raw(x, df)
548    })
549}
550
551fn chi_square_pdf_raw(x: f64, df: f64) -> f64 {
552    if x <= 0.0 {
553        return 0.0;
554    }
555    let half = 0.5 * df;
556    exp((half - 1.0) * ln(x) - 0.5 * x - half * ln(2.0) - lgamma(half))
557}
558
559// ---------------------------------------------------------------------------
560// Poisson distribution
561// ---------------------------------------------------------------------------
562
563pub fn poisson_pmf(k: f64, lambda: f64) -> Result<f64, EngineError> {
564    if !k.is_finite() || !lambda.is_finite() {
565        return Err(EngineError::domain("poisson_pmf requires finite inputs"));
566    }
567    if lambda <= 0.0 {
568        return Err(EngineError::domain("poisson_pmf requires lambda > 0"));
569    }
570    if k < 0.0 {
571        return Ok(0.0);
572    }
573    if k == 0.0 {
574        return Ok(exp(-lambda));
575    }
576    let ln_pmf = -lambda + k * ln(lambda) - lgamma(k + 1.0);
577    Ok(exp(ln_pmf))
578}
579
580pub fn poisson_cdf(k: f64, lambda: f64) -> Result<f64, EngineError> {
581    if !k.is_finite() || !lambda.is_finite() {
582        return Err(EngineError::domain("poisson_cdf requires finite inputs"));
583    }
584    if lambda <= 0.0 {
585        return Err(EngineError::domain("poisson_cdf requires lambda > 0"));
586    }
587    if k < 0.0 {
588        return Ok(0.0);
589    }
590    gamma_q(k + 1.0, lambda)
591}
592
593// ---------------------------------------------------------------------------
594// Exponential distribution
595// ---------------------------------------------------------------------------
596
597pub fn exponential_pdf(x: f64, rate: f64) -> Result<f64, EngineError> {
598    if !x.is_finite() || !rate.is_finite() {
599        return Err(EngineError::domain(
600            "exponential_pdf requires finite inputs",
601        ));
602    }
603    if rate <= 0.0 {
604        return Err(EngineError::domain("exponential_pdf requires rate > 0"));
605    }
606    if x < 0.0 {
607        return Ok(0.0);
608    }
609    Ok(rate * exp(-rate * x))
610}
611
612pub fn exponential_cdf(x: f64, rate: f64) -> Result<f64, EngineError> {
613    if !x.is_finite() || !rate.is_finite() {
614        return Err(EngineError::domain(
615            "exponential_cdf requires finite inputs",
616        ));
617    }
618    if rate <= 0.0 {
619        return Err(EngineError::domain("exponential_cdf requires rate > 0"));
620    }
621    if x <= 0.0 {
622        return Ok(0.0);
623    }
624    Ok(1.0 - exp(-rate * x))
625}
626
627/// Upper tail `P(X > x)` computed from `exp` directly so large `x` keeps
628/// relative accuracy (never `1 - cdf`).
629pub fn exponential_sf(x: f64, rate: f64) -> Result<f64, EngineError> {
630    if !x.is_finite() || !rate.is_finite() {
631        return Err(EngineError::domain("exponential_sf requires finite inputs"));
632    }
633    if rate <= 0.0 {
634        return Err(EngineError::domain("exponential_sf requires rate > 0"));
635    }
636    if x <= 0.0 {
637        return Ok(1.0);
638    }
639    Ok(exp(-rate * x))
640}
641
642pub fn exponential_quantile(p: f64, rate: f64) -> Result<f64, EngineError> {
643    if !(0.0..1.0).contains(&p) {
644        return Err(EngineError::domain(
645            "exponential_quantile requires p in [0, 1)",
646        ));
647    }
648    if rate <= 0.0 || !rate.is_finite() {
649        return Err(EngineError::domain(
650            "exponential_quantile requires rate > 0",
651        ));
652    }
653    if p == 0.0 {
654        return Ok(0.0);
655    }
656    Ok(-ln(1.0 - p) / rate)
657}
658
659// ---------------------------------------------------------------------------
660// Uniform distribution
661// ---------------------------------------------------------------------------
662
663fn uniform_bounds(lower: f64, upper: f64) -> Result<(), EngineError> {
664    if !lower.is_finite() || !upper.is_finite() {
665        return Err(EngineError::domain("uniform bounds must be finite"));
666    }
667    if upper <= lower {
668        return Err(EngineError::domain("uniform requires upper > lower"));
669    }
670    Ok(())
671}
672
673pub fn uniform_pdf(x: f64, lower: f64, upper: f64) -> Result<f64, EngineError> {
674    if !x.is_finite() {
675        return Err(EngineError::domain("uniform_pdf requires a finite x"));
676    }
677    uniform_bounds(lower, upper)?;
678    if x < lower || x > upper {
679        return Ok(0.0);
680    }
681    Ok(1.0 / (upper - lower))
682}
683
684pub fn uniform_cdf(x: f64, lower: f64, upper: f64) -> Result<f64, EngineError> {
685    if !x.is_finite() {
686        return Err(EngineError::domain("uniform_cdf requires a finite x"));
687    }
688    uniform_bounds(lower, upper)?;
689    Ok(((x - lower) / (upper - lower)).clamp(0.0, 1.0))
690}
691
692pub fn uniform_quantile(p: f64, lower: f64, upper: f64) -> Result<f64, EngineError> {
693    if !(0.0..=1.0).contains(&p) {
694        return Err(EngineError::domain("uniform_quantile requires p in [0, 1]"));
695    }
696    uniform_bounds(lower, upper)?;
697    Ok(lower + p * (upper - lower))
698}
699
700// ---------------------------------------------------------------------------
701// Log-normal distribution
702// ---------------------------------------------------------------------------
703
704pub fn lognormal_pdf(x: f64, mu: f64, sigma: f64) -> Result<f64, EngineError> {
705    if !x.is_finite() || !mu.is_finite() || !sigma.is_finite() {
706        return Err(EngineError::domain("lognormal_pdf requires finite inputs"));
707    }
708    if sigma <= 0.0 {
709        return Err(EngineError::domain("lognormal_pdf requires sigma > 0"));
710    }
711    if x <= 0.0 {
712        return Ok(0.0);
713    }
714    let z = (ln(x) - mu) / sigma;
715    Ok(exp(-0.5 * z * z) / (x * sigma * SQRT_2PI))
716}
717
718pub fn lognormal_cdf(x: f64, mu: f64, sigma: f64) -> Result<f64, EngineError> {
719    if !x.is_finite() || !mu.is_finite() || !sigma.is_finite() {
720        return Err(EngineError::domain("lognormal_cdf requires finite inputs"));
721    }
722    if sigma <= 0.0 {
723        return Err(EngineError::domain("lognormal_cdf requires sigma > 0"));
724    }
725    if x <= 0.0 {
726        return Ok(0.0);
727    }
728    normal_cdf((ln(x) - mu) / sigma, 0.0, 1.0)
729}
730
731pub fn lognormal_quantile(p: f64, mu: f64, sigma: f64) -> Result<f64, EngineError> {
732    if !(0.0..1.0).contains(&p) {
733        return Err(EngineError::domain(
734            "lognormal_quantile requires p strictly between 0 and 1",
735        ));
736    }
737    if !mu.is_finite() || sigma <= 0.0 || !sigma.is_finite() {
738        return Err(EngineError::domain(
739            "lognormal_quantile requires finite mu and sigma > 0",
740        ));
741    }
742    let z = normal_quantile(p, 0.0, 1.0)?;
743    Ok(exp(mu + sigma * z))
744}
745
746// ---------------------------------------------------------------------------
747// Gamma distribution
748// ---------------------------------------------------------------------------
749
750fn gamma_pdf_raw(x: f64, shape: f64, rate: f64) -> f64 {
751    if x <= 0.0 {
752        return 0.0;
753    }
754    exp(shape * ln(rate) + (shape - 1.0) * ln(x) - rate * x - lgamma(shape))
755}
756
757pub fn gamma_pdf(x: f64, shape: f64, rate: f64) -> Result<f64, EngineError> {
758    if !x.is_finite() || !shape.is_finite() || !rate.is_finite() {
759        return Err(EngineError::domain("gamma_pdf requires finite inputs"));
760    }
761    if shape <= 0.0 {
762        return Err(EngineError::domain("gamma_pdf requires shape > 0"));
763    }
764    if rate <= 0.0 {
765        return Err(EngineError::domain("gamma_pdf requires rate > 0"));
766    }
767    if x < 0.0 {
768        return Ok(0.0);
769    }
770    if x == 0.0 {
771        return match shape.partial_cmp(&1.0) {
772            Some(std::cmp::Ordering::Less) => Err(EngineError::domain(
773                "gamma_pdf is unbounded at x = 0 for shape < 1",
774            )),
775            Some(std::cmp::Ordering::Equal) => Ok(rate),
776            _ => Ok(0.0),
777        };
778    }
779    Ok(gamma_pdf_raw(x, shape, rate))
780}
781
782pub fn gamma_cdf(x: f64, shape: f64, rate: f64) -> Result<f64, EngineError> {
783    if !x.is_finite() || !shape.is_finite() || !rate.is_finite() {
784        return Err(EngineError::domain("gamma_cdf requires finite inputs"));
785    }
786    if shape <= 0.0 {
787        return Err(EngineError::domain("gamma_cdf requires shape > 0"));
788    }
789    if rate <= 0.0 {
790        return Err(EngineError::domain("gamma_cdf requires rate > 0"));
791    }
792    if x <= 0.0 {
793        return Ok(0.0);
794    }
795    gamma_p(shape, rate * x)
796}
797
798pub fn gamma_quantile(p: f64, shape: f64, rate: f64) -> Result<f64, EngineError> {
799    if !(0.0..1.0).contains(&p) {
800        return Err(EngineError::domain("gamma_quantile requires p in [0, 1)"));
801    }
802    if shape <= 0.0 || !shape.is_finite() {
803        return Err(EngineError::domain("gamma_quantile requires shape > 0"));
804    }
805    if rate <= 0.0 || !rate.is_finite() {
806        return Err(EngineError::domain("gamma_quantile requires rate > 0"));
807    }
808    if p == 0.0 {
809        return Ok(0.0);
810    }
811    invert_monotone(p, 0.0, 1.0, &mut |x| gamma_cdf(x, shape, rate), &mut |x| {
812        gamma_pdf_raw(x, shape, rate)
813    })
814}
815
816// ---------------------------------------------------------------------------
817// Beta distribution
818// ---------------------------------------------------------------------------
819
820fn beta_pdf_raw(x: f64, alpha: f64, beta: f64) -> f64 {
821    exp((alpha - 1.0) * ln(x) + (beta - 1.0) * ln(1.0 - x)
822        - (lgamma(alpha) + lgamma(beta) - lgamma(alpha + beta)))
823}
824
825fn beta_shapes(alpha: f64, beta: f64) -> Result<(), EngineError> {
826    if alpha <= 0.0 || beta <= 0.0 || !alpha.is_finite() || !beta.is_finite() {
827        return Err(EngineError::domain(
828            "beta distribution requires finite positive shape parameters",
829        ));
830    }
831    Ok(())
832}
833
834pub fn beta_pdf(x: f64, alpha: f64, beta: f64) -> Result<f64, EngineError> {
835    if !x.is_finite() {
836        return Err(EngineError::domain("beta_pdf requires a finite x"));
837    }
838    beta_shapes(alpha, beta)?;
839    if !(0.0..=1.0).contains(&x) {
840        return Ok(0.0);
841    }
842    if x == 0.0 {
843        return match alpha.partial_cmp(&1.0) {
844            Some(std::cmp::Ordering::Less) => Err(EngineError::domain(
845                "beta_pdf is unbounded at x = 0 for alpha < 1",
846            )),
847            Some(std::cmp::Ordering::Equal) => Ok(beta),
848            _ => Ok(0.0),
849        };
850    }
851    if x == 1.0 {
852        return match beta.partial_cmp(&1.0) {
853            Some(std::cmp::Ordering::Less) => Err(EngineError::domain(
854                "beta_pdf is unbounded at x = 1 for beta < 1",
855            )),
856            Some(std::cmp::Ordering::Equal) => Ok(alpha),
857            _ => Ok(0.0),
858        };
859    }
860    Ok(beta_pdf_raw(x, alpha, beta))
861}
862
863pub fn beta_cdf(x: f64, alpha: f64, beta: f64) -> Result<f64, EngineError> {
864    if !x.is_finite() {
865        return Err(EngineError::domain("beta_cdf requires a finite x"));
866    }
867    beta_shapes(alpha, beta)?;
868    if x <= 0.0 {
869        return Ok(0.0);
870    }
871    if x >= 1.0 {
872        return Ok(1.0);
873    }
874    ibeta(alpha, beta, x)
875}
876
877/// Upper tail `P(X > x)`, computed from the mirrored regularized incomplete
878/// beta function rather than `1 - cdf`.
879pub fn beta_sf(x: f64, alpha: f64, beta: f64) -> Result<f64, EngineError> {
880    if !x.is_finite() {
881        return Err(EngineError::domain("beta_sf requires a finite x"));
882    }
883    beta_shapes(alpha, beta)?;
884    if x <= 0.0 {
885        return Ok(1.0);
886    }
887    if x >= 1.0 {
888        return Ok(0.0);
889    }
890    ibeta(beta, alpha, 1.0 - x)
891}
892
893pub fn beta_quantile(p: f64, alpha: f64, beta: f64) -> Result<f64, EngineError> {
894    if !(0.0..=1.0).contains(&p) {
895        return Err(EngineError::domain("beta_quantile requires p in [0, 1]"));
896    }
897    beta_shapes(alpha, beta)?;
898    if p == 0.0 {
899        return Ok(0.0);
900    }
901    if p == 1.0 {
902        return Ok(1.0);
903    }
904    invert_monotone(p, 0.0, 1.0, &mut |x| beta_cdf(x, alpha, beta), &mut |x| {
905        beta_pdf_raw(x, alpha, beta)
906    })
907}
908
909// ---------------------------------------------------------------------------
910// F distribution
911// ---------------------------------------------------------------------------
912
913fn f_pdf_raw(x: f64, df1: f64, df2: f64) -> f64 {
914    let half1 = 0.5 * df1;
915    let half2 = 0.5 * df2;
916    exp(half1 * ln(df1) + half2 * ln(df2) + (half1 - 1.0) * ln(x)
917        - (half1 + half2) * ln(df2 + df1 * x)
918        - (lgamma(half1) + lgamma(half2) - lgamma(half1 + half2)))
919}
920
921fn f_degrees(df1: f64, df2: f64) -> Result<(), EngineError> {
922    if df1 <= 0.0 || df2 <= 0.0 || !df1.is_finite() || !df2.is_finite() {
923        return Err(EngineError::domain(
924            "F distribution requires finite positive degrees of freedom",
925        ));
926    }
927    Ok(())
928}
929
930pub fn f_pdf(x: f64, df1: f64, df2: f64) -> Result<f64, EngineError> {
931    if !x.is_finite() {
932        return Err(EngineError::domain("f_pdf requires a finite x"));
933    }
934    f_degrees(df1, df2)?;
935    if x < 0.0 {
936        return Ok(0.0);
937    }
938    if x == 0.0 {
939        return match df1.partial_cmp(&2.0) {
940            Some(std::cmp::Ordering::Less) => Err(EngineError::domain(
941                "f_pdf is unbounded at x = 0 for df1 < 2",
942            )),
943            Some(std::cmp::Ordering::Equal) => Ok(1.0),
944            _ => Ok(0.0),
945        };
946    }
947    Ok(f_pdf_raw(x, df1, df2))
948}
949
950pub fn f_cdf(x: f64, df1: f64, df2: f64) -> Result<f64, EngineError> {
951    if !x.is_finite() {
952        return Err(EngineError::domain("f_cdf requires a finite x"));
953    }
954    f_degrees(df1, df2)?;
955    if x <= 0.0 {
956        return Ok(0.0);
957    }
958    let y = df1 * x / (df1 * x + df2);
959    ibeta(0.5 * df1, 0.5 * df2, y)
960}
961
962/// Upper tail `P(X > x)`, computed directly from the regularized incomplete
963/// beta function so large `x` keeps relative accuracy.
964pub fn f_sf(x: f64, df1: f64, df2: f64) -> Result<f64, EngineError> {
965    if !x.is_finite() {
966        return Err(EngineError::domain("f_sf requires a finite x"));
967    }
968    f_degrees(df1, df2)?;
969    if x <= 0.0 {
970        return Ok(1.0);
971    }
972    let y = df2 / (df2 + df1 * x);
973    ibeta(0.5 * df2, 0.5 * df1, y)
974}
975
976pub fn f_quantile(p: f64, df1: f64, df2: f64) -> Result<f64, EngineError> {
977    if !(0.0..1.0).contains(&p) {
978        return Err(EngineError::domain("f_quantile requires p in [0, 1)"));
979    }
980    f_degrees(df1, df2)?;
981    if p == 0.0 {
982        return Ok(0.0);
983    }
984    let q = beta_quantile(p, 0.5 * df1, 0.5 * df2)?;
985    if q >= 1.0 {
986        return Err(EngineError::domain(
987            "f_quantile could not resolve the incomplete beta quantile",
988        ));
989    }
990    Ok(df2 * q / (df1 * (1.0 - q)))
991}
992
993// ---------------------------------------------------------------------------
994// Negative binomial distribution
995// ---------------------------------------------------------------------------
996
997fn negative_binomial_parameters(r: f64, p: f64) -> Result<(), EngineError> {
998    if !r.is_finite() || !p.is_finite() {
999        return Err(EngineError::domain(
1000            "negative binomial requires finite inputs",
1001        ));
1002    }
1003    if r <= 0.0 {
1004        return Err(EngineError::domain("negative binomial requires r > 0"));
1005    }
1006    if !(0.0..=1.0).contains(&p) {
1007        return Err(EngineError::domain(
1008            "negative binomial requires p in [0, 1]",
1009        ));
1010    }
1011    Ok(())
1012}
1013
1014pub fn negative_binomial_pmf(k: f64, r: f64, p: f64) -> Result<f64, EngineError> {
1015    if !k.is_finite() {
1016        return Err(EngineError::domain(
1017            "negative_binomial_pmf requires a finite k",
1018        ));
1019    }
1020    negative_binomial_parameters(r, p)?;
1021    if k < 0.0 {
1022        return Ok(0.0);
1023    }
1024    if p == 0.0 {
1025        return Ok(0.0);
1026    }
1027    if p == 1.0 {
1028        return Ok(if k == 0.0 { 1.0 } else { 0.0 });
1029    }
1030    if k == 0.0 {
1031        return Ok(exp(r * ln(p)));
1032    }
1033    let ln_pmf = lgamma(k + r) - lgamma(k + 1.0) - lgamma(r) + r * ln(p) + k * ln(1.0 - p);
1034    Ok(exp(ln_pmf))
1035}
1036
1037pub fn negative_binomial_cdf(k: f64, r: f64, p: f64) -> Result<f64, EngineError> {
1038    if !k.is_finite() {
1039        return Err(EngineError::domain(
1040            "negative_binomial_cdf requires a finite k",
1041        ));
1042    }
1043    negative_binomial_parameters(r, p)?;
1044    if k < 0.0 {
1045        return Ok(0.0);
1046    }
1047    if p == 0.0 {
1048        return Ok(0.0);
1049    }
1050    if p == 1.0 {
1051        return Ok(1.0);
1052    }
1053    ibeta(r, k + 1.0, p)
1054}
1055
1056// ---------------------------------------------------------------------------
1057// Generic monotone CDF inversion
1058// ---------------------------------------------------------------------------
1059
1060/// Invert a strictly increasing CDF by bracket expansion, bisection, and
1061/// Newton polishing. The tolerance is relative (`1e-15`), comfortably inside
1062/// the documented `1e-12` requirement.
1063pub fn invert_monotone(
1064    p: f64,
1065    mut lo: f64,
1066    mut hi: f64,
1067    cdf: &mut dyn FnMut(f64) -> Result<f64, EngineError>,
1068    pdf: &mut dyn FnMut(f64) -> f64,
1069) -> Result<f64, EngineError> {
1070    let mut expansions = 0u32;
1071    while cdf(lo)? > p {
1072        hi = lo;
1073        lo = if lo < 0.0 { lo * 2.0 } else { -1.0 };
1074        expansions += 1;
1075        if expansions > 4_000 || !lo.is_finite() {
1076            return Err(non_convergence("quantile bracket expansion"));
1077        }
1078    }
1079    while cdf(hi)? < p {
1080        lo = hi;
1081        hi = if hi > 0.0 { hi * 2.0 } else { 1.0 };
1082        expansions += 1;
1083        if expansions > 4_000 || !hi.is_finite() {
1084            return Err(non_convergence("quantile bracket expansion"));
1085        }
1086    }
1087    let mut x = 0.5 * (lo + hi);
1088    for _ in 0..400 {
1089        let mid = 0.5 * (lo + hi);
1090        x = mid;
1091        if cdf(mid)? < p {
1092            lo = mid;
1093        } else {
1094            hi = mid;
1095        }
1096        if (hi - lo) <= 1e-15 * (1.0 + abs(mid)) {
1097            break;
1098        }
1099    }
1100    for _ in 0..5 {
1101        let value = cdf(x)?;
1102        let density = pdf(x);
1103        if density <= 0.0 || !density.is_finite() {
1104            break;
1105        }
1106        let step = (value - p) / density;
1107        if !step.is_finite() {
1108            break;
1109        }
1110        let mut next = x - step;
1111        if !next.is_finite() {
1112            break;
1113        }
1114        if next < lo {
1115            next = 0.5 * (lo + x);
1116        } else if next > hi {
1117            next = 0.5 * (hi + x);
1118        }
1119        if abs(next - x) <= 1e-16 * (1.0 + abs(next)) {
1120            x = next;
1121            break;
1122        }
1123        x = next;
1124    }
1125    if !x.is_finite() {
1126        return Err(non_convergence("quantile inversion"));
1127    }
1128    Ok(x)
1129}
1130
1131#[cfg(test)]
1132mod tests {
1133    use super::*;
1134
1135    fn close(actual: f64, expected: f64, tolerance: f64) {
1136        assert!(
1137            (actual - expected).abs() <= tolerance,
1138            "expected {expected}, got {actual} (tolerance {tolerance})"
1139        );
1140    }
1141
1142    #[test]
1143    fn normal_reference_values() {
1144        close(
1145            normal_cdf(1.96, 0.0, 1.0).unwrap(),
1146            0.975_002_104_851_779_5,
1147            1e-12,
1148        );
1149        close(
1150            normal_quantile(0.975, 0.0, 1.0).unwrap(),
1151            1.959_963_984_540_054,
1152            1e-9,
1153        );
1154        // The commonly published 7.619853e-24 is the 7-digit rounded value;
1155        // compare against the full-precision reference within 1e-9 relative.
1156        let tail = normal_sf(10.0, 0.0, 1.0).unwrap();
1157        close(tail / 7.619_853_024_160_593e-24, 1.0, 1e-9);
1158        close(normal_cdf(0.0, 0.0, 1.0).unwrap(), 0.5, 0.0);
1159        close(normal_quantile(0.5, 0.0, 1.0).unwrap(), 0.0, 0.0);
1160    }
1161
1162    #[test]
1163    fn student_t_reference_value() {
1164        // Provenance: for even df the Student-t CDF has the closed form
1165        // P = 1 - 0.5 * I_x(df/2, 1/2) with x = df/(df + t^2), and for integer
1166        // df/2 the incomplete beta reduces to a finite polynomial in
1167        // sqrt(1 - x). The reference below was computed independently in Python
1168        // with 60-digit Decimal arithmetic from that closed form
1169        // (student_t_cdf(2, 10) = 0.96330598261462981719...), not with this
1170        // crate's continued-fraction implementation.
1171        close(
1172            student_t_cdf(2.0, 10.0).unwrap(),
1173            0.963_305_982_614_629_8,
1174            1e-12,
1175        );
1176        close(
1177            student_t_sf(2.0, 10.0).unwrap(),
1178            0.036_694_017_385_370_18,
1179            1e-12,
1180        );
1181        let q = student_t_quantile(0.975, 10.0).unwrap();
1182        close(q, 2.228_138_851_964_938_5, 1e-8);
1183        close(student_t_cdf(q, 10.0).unwrap(), 0.975, 1e-10);
1184    }
1185
1186    #[test]
1187    fn chi_square_reference_values() {
1188        close(
1189            chi_square_cdf(3.841_458_820_694_124, 1.0).unwrap(),
1190            0.95,
1191            1e-12,
1192        );
1193        let q = chi_square_quantile(0.95, 1.0).unwrap();
1194        close(q, 3.841_458_820_694_124, 1e-9);
1195        close(chi_square_cdf(q, 1.0).unwrap(), 0.95, 1e-10);
1196        close(chi_square_pdf(0.0, 2.0).unwrap(), 0.5, 0.0);
1197    }
1198
1199    #[test]
1200    fn binomial_reference_value() {
1201        close(binomial_cdf(5.0, 10.0, 0.5).unwrap(), 0.623_046_875, 1e-15);
1202        close(binomial_pmf(0.0, 0.0, 0.5).unwrap(), 1.0, 0.0);
1203        close(binomial_pmf(5.0, 10.0, 0.5).unwrap(), 252.0 / 1024.0, 1e-15);
1204    }
1205
1206    #[test]
1207    fn erfc_tail_is_relative_accurate() {
1208        // erfc(5) = 1.5374597944280351e-12
1209        close(erfc(5.0).unwrap(), 1.537_459_794_428_035_1e-12, 1e-24);
1210        close(erf(1.0).unwrap(), 0.842_700_792_949_715, 1e-15);
1211    }
1212
1213    #[test]
1214    fn binomial_edge_cases_are_exact() {
1215        close(binomial_pmf(0.0, 0.0, 0.5).unwrap(), 1.0, 0.0);
1216        close(
1217            binomial_pmf(0.0, 10.0, 0.3).unwrap(),
1218            0.7f64.powi(10),
1219            1e-18,
1220        );
1221        close(
1222            binomial_pmf(10.0, 10.0, 0.3).unwrap(),
1223            0.3f64.powi(10),
1224            1e-18,
1225        );
1226        close(binomial_cdf(-1.0, 10.0, 0.5).unwrap(), 0.0, 0.0);
1227        close(binomial_cdf(10.0, 10.0, 0.5).unwrap(), 1.0, 0.0);
1228    }
1229
1230    #[test]
1231    fn round_trips_are_tight() {
1232        for p in [1e-6, 0.01, 0.25, 0.5, 0.75, 0.99, 1.0 - 1e-6] {
1233            let q = normal_quantile(p, 0.0, 1.0).unwrap();
1234            close(normal_cdf(q, 0.0, 1.0).unwrap(), p, 1e-10);
1235            let t = student_t_quantile(p, 7.0).unwrap();
1236            close(student_t_cdf(t, 7.0).unwrap(), p, 1e-10);
1237            let c = chi_square_quantile(p, 3.0).unwrap();
1238            close(chi_square_cdf(c, 3.0).unwrap(), p, 1e-10);
1239        }
1240    }
1241}