Skip to main content

u_numflow/
special.rs

1//! Special mathematical functions.
2//!
3//! Numerical approximations of standard mathematical functions used
4//! throughout probability and statistics.
5
6/// 1/√(2π) ≈ 0.3989422804014327
7const FRAC_1_SQRT_2PI: f64 = 0.3989422804014326779399460599343818684758586311649;
8
9/// Approximation of the standard normal CDF Φ(x) = P(Z ≤ x) for Z ~ N(0,1).
10///
11/// # Algorithm
12/// Abramowitz & Stegun formula 26.2.17, polynomial approximation with
13/// Horner evaluation.
14///
15/// Reference: Abramowitz & Stegun (1964), *Handbook of Mathematical
16/// Functions*, formula 26.2.17, p. 932.
17///
18/// # Accuracy
19/// Maximum absolute error < 7.5 × 10⁻⁸.
20///
21/// # Examples
22/// ```
23/// use u_numflow::special::standard_normal_cdf;
24/// assert!((standard_normal_cdf(0.0) - 0.5).abs() < 1e-7);
25/// assert!((standard_normal_cdf(1.96) - 0.975).abs() < 1e-3);
26/// ```
27pub fn standard_normal_cdf(x: f64) -> f64 {
28    if x.is_nan() {
29        return f64::NAN;
30    }
31    if x == f64::INFINITY {
32        return 1.0;
33    }
34    if x == f64::NEG_INFINITY {
35        return 0.0;
36    }
37
38    // Use symmetry: Φ(-x) = 1 - Φ(x)
39    let abs_x = x.abs();
40    let k = 1.0 / (1.0 + 0.2316419 * abs_x);
41
42    // φ(x) = (1/√(2π)) exp(-x²/2)
43    let phi = FRAC_1_SQRT_2PI * (-0.5 * abs_x * abs_x).exp();
44
45    // Horner evaluation of the polynomial
46    // a₅ = 1.330274429, a₄ = -1.821255978, a₃ = 1.781477937,
47    // a₂ = -0.356563782, a₁ = 0.319381530
48    let poly = k
49        * (0.319381530
50            + k * (-0.356563782 + k * (1.781477937 + k * (-1.821255978 + k * 1.330274429))));
51
52    let cdf_abs = 1.0 - phi * poly;
53
54    if x >= 0.0 {
55        cdf_abs
56    } else {
57        1.0 - cdf_abs
58    }
59}
60
61/// Approximation of the inverse standard normal CDF (quantile function).
62///
63/// Given a probability `p ∈ (0, 1)`, returns `z` such that `Φ(z) = p`.
64///
65/// # Algorithm
66/// Abramowitz & Stegun formula 26.2.23, rational approximation.
67///
68/// Reference: Abramowitz & Stegun (1964), *Handbook of Mathematical
69/// Functions*, formula 26.2.23, p. 933.
70///
71/// # Accuracy
72/// Maximum absolute error < 4.5 × 10⁻⁴.
73///
74/// # Returns
75/// - `f64::NAN` if `p` is outside `(0, 1)` or NaN.
76/// - `f64::NEG_INFINITY` if `p == 0.0`.
77/// - `f64::INFINITY` if `p == 1.0`.
78///
79/// # Examples
80/// ```
81/// use u_numflow::special::inverse_normal_cdf;
82/// assert!((inverse_normal_cdf(0.5)).abs() < 1e-4);
83/// assert!((inverse_normal_cdf(0.975) - 1.96).abs() < 0.01);
84/// ```
85pub fn inverse_normal_cdf(p: f64) -> f64 {
86    if p.is_nan() || !(0.0..=1.0).contains(&p) {
87        return f64::NAN;
88    }
89    if p == 0.0 {
90        return f64::NEG_INFINITY;
91    }
92    if p == 1.0 {
93        return f64::INFINITY;
94    }
95
96    // Use symmetry for p > 0.5
97    let (q, sign) = if p > 0.5 { (1.0 - p, 1.0) } else { (p, -1.0) };
98
99    // A&S 26.2.23: t = √(-2 ln(q))
100    let t = (-2.0 * q.ln()).sqrt();
101
102    // Rational approximation coefficients
103    const C0: f64 = 2.515517;
104    const C1: f64 = 0.802853;
105    const C2: f64 = 0.010328;
106    const D1: f64 = 1.432788;
107    const D2: f64 = 0.189269;
108    const D3: f64 = 0.001308;
109
110    let z = t - (C0 + C1 * t + C2 * t * t) / (1.0 + D1 * t + D2 * t * t + D3 * t * t * t);
111
112    sign * z
113}
114
115/// Standard normal PDF φ(x) = (1/√(2π)) exp(-x²/2).
116///
117/// # Examples
118/// ```
119/// use u_numflow::special::standard_normal_pdf;
120/// let peak = standard_normal_pdf(0.0);
121/// assert!((peak - 0.3989422804014327).abs() < 1e-15);
122/// ```
123pub fn standard_normal_pdf(x: f64) -> f64 {
124    if x.is_nan() {
125        return f64::NAN;
126    }
127    FRAC_1_SQRT_2PI * (-0.5 * x * x).exp()
128}
129
130/// Lanczos approximation of ln Γ(x).
131///
132/// Reference: Lanczos (1964), "A Precision Approximation of the Gamma
133/// Function", *SIAM Journal on Numerical Analysis* 1(1).
134///
135/// # Accuracy
136/// Relative error < 2 × 10⁻¹⁰ for x > 0.
137///
138/// # Examples
139/// ```
140/// use u_numflow::special::ln_gamma;
141/// // Γ(5) = 24
142/// assert!((ln_gamma(5.0) - 24.0_f64.ln()).abs() < 1e-10);
143/// ```
144pub fn ln_gamma(x: f64) -> f64 {
145    #[allow(clippy::excessive_precision)]
146    const COEFFICIENTS: [f64; 9] = [
147        0.99999999999980993,
148        676.5203681218851,
149        -1259.1392167224028,
150        771.32342877765313,
151        -176.61502916214059,
152        12.507343278686905,
153        -0.13857109526572012,
154        9.9843695780195716e-6,
155        1.5056327351493116e-7,
156    ];
157    const G: f64 = 7.0;
158
159    if x < 0.5 {
160        let pi = std::f64::consts::PI;
161        return (pi / (pi * x).sin()).ln() - ln_gamma(1.0 - x);
162    }
163
164    let x = x - 1.0;
165    let mut sum = COEFFICIENTS[0];
166    for (i, &c) in COEFFICIENTS[1..].iter().enumerate() {
167        sum += c / (x + i as f64 + 1.0);
168    }
169
170    let t = x + G + 0.5;
171    0.5 * (2.0 * std::f64::consts::PI).ln() + (x + 0.5) * t.ln() - t + sum.ln()
172}
173
174/// Gamma function Γ(x) = exp(ln_gamma(x)).
175///
176/// # Examples
177/// ```
178/// use u_numflow::special::gamma;
179/// // Γ(5) = 4! = 24
180/// assert!((gamma(5.0) - 24.0).abs() < 1e-8);
181/// // Γ(0.5) = √π
182/// assert!((gamma(0.5) - std::f64::consts::PI.sqrt()).abs() < 1e-10);
183/// ```
184pub fn gamma(x: f64) -> f64 {
185    ln_gamma(x).exp()
186}
187
188// ============================================================================
189// Log Beta Function
190// ============================================================================
191
192/// Log of the Beta function: `ln B(a, b) = ln Γ(a) + ln Γ(b) − ln Γ(a+b)`.
193///
194/// # Examples
195/// ```
196/// use u_numflow::special::ln_beta;
197/// // B(1,1) = 1, so ln B(1,1) = 0
198/// assert!(ln_beta(1.0, 1.0).abs() < 1e-10);
199/// ```
200pub fn ln_beta(a: f64, b: f64) -> f64 {
201    ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b)
202}
203
204// ============================================================================
205// Regularized Incomplete Beta Function
206// ============================================================================
207
208/// Regularized incomplete beta function I_x(a, b).
209///
210/// # Definition
211/// ```text
212/// I_x(a, b) = B(x; a, b) / B(a, b)
213/// ```
214/// where B(x; a, b) is the incomplete beta function.
215///
216/// # Algorithm
217/// Uses the continued fraction representation (Lentz's method) with
218/// symmetry relation for convergence optimization.
219///
220/// Reference: Press et al. (2007), *Numerical Recipes*, 3rd ed., §6.4.
221///
222/// # Accuracy
223/// Relative error < 1e-10 for typical parameter ranges.
224///
225/// # Examples
226/// ```
227/// use u_numflow::special::regularized_incomplete_beta;
228/// // I_0(a,b) = 0, I_1(a,b) = 1
229/// assert_eq!(regularized_incomplete_beta(0.0, 2.0, 3.0), 0.0);
230/// assert_eq!(regularized_incomplete_beta(1.0, 2.0, 3.0), 1.0);
231/// // I_0.5(1,1) = 0.5 (uniform)
232/// assert!((regularized_incomplete_beta(0.5, 1.0, 1.0) - 0.5).abs() < 1e-10);
233/// ```
234pub fn regularized_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
235    if x <= 0.0 {
236        return 0.0;
237    }
238    if x >= 1.0 {
239        return 1.0;
240    }
241
242    // Use symmetry relation: I_x(a,b) = 1 - I_{1-x}(b,a)
243    if x > (a + 1.0) / (a + b + 2.0) {
244        return 1.0 - regularized_incomplete_beta(1.0 - x, b, a);
245    }
246
247    let ln_prefix = a * x.ln() + b * (1.0 - x).ln() - ln_beta(a, b);
248    let cf = beta_cf(x, a, b);
249    (ln_prefix.exp() / a) * cf
250}
251
252/// Continued fraction for the incomplete beta function (Lentz's algorithm).
253fn beta_cf(x: f64, a: f64, b: f64) -> f64 {
254    const MAX_ITER: usize = 200;
255    const EPS: f64 = 1e-14;
256    const TINY: f64 = 1e-30;
257
258    let mut c = 1.0;
259    let mut d = 1.0 / (1.0 - (a + b) * x / (a + 1.0)).max(TINY);
260    let mut h = d;
261
262    for m in 1..=MAX_ITER {
263        let m_f = m as f64;
264        let num_even = m_f * (b - m_f) * x / ((a + 2.0 * m_f - 1.0) * (a + 2.0 * m_f));
265        d = 1.0 / (1.0 + num_even * d).max(TINY);
266        c = (1.0 + num_even / c).max(TINY);
267        h *= d * c;
268
269        let num_odd = -(a + m_f) * (a + b + m_f) * x / ((a + 2.0 * m_f) * (a + 2.0 * m_f + 1.0));
270        d = 1.0 / (1.0 + num_odd * d).max(TINY);
271        c = (1.0 + num_odd / c).max(TINY);
272        let delta = d * c;
273        h *= delta;
274
275        if (delta - 1.0).abs() < EPS {
276            break;
277        }
278    }
279    h
280}
281
282// ============================================================================
283// Regularized Lower Incomplete Gamma Function
284// ============================================================================
285
286/// Regularized lower incomplete gamma function P(a, x) = γ(a, x) / Γ(a).
287///
288/// # Algorithm
289/// Uses series expansion for `x < a + 1`, continued fraction otherwise.
290///
291/// # Examples
292/// ```
293/// use u_numflow::special::regularized_lower_gamma;
294/// // P(1, x) = 1 - exp(-x) for the exponential distribution
295/// let p = regularized_lower_gamma(1.0, 2.0);
296/// assert!((p - (1.0 - (-2.0_f64).exp())).abs() < 1e-10);
297/// ```
298pub fn regularized_lower_gamma(a: f64, x: f64) -> f64 {
299    if x <= 0.0 {
300        return 0.0;
301    }
302    if x < a + 1.0 {
303        gamma_series(a, x)
304    } else {
305        1.0 - gamma_cf(a, x)
306    }
307}
308
309/// Series expansion for the regularized lower incomplete gamma.
310fn gamma_series(a: f64, x: f64) -> f64 {
311    let mut term = 1.0 / a;
312    let mut sum = term;
313    let mut ap = a;
314    for _ in 0..200 {
315        ap += 1.0;
316        term *= x / ap;
317        sum += term;
318        if term.abs() < sum.abs() * 1e-14 {
319            break;
320        }
321    }
322    sum * (-x + a * x.ln() - ln_gamma(a)).exp()
323}
324
325/// Continued fraction for the upper incomplete gamma Q(a, x) = 1 − P(a, x).
326fn gamma_cf(a: f64, x: f64) -> f64 {
327    let mut b = x + 1.0 - a;
328    let mut c = 1.0 / 1e-30;
329    let mut d = 1.0 / b;
330    let mut h = d;
331    for i in 1..=200 {
332        let an = -(i as f64) * (i as f64 - a);
333        b += 2.0;
334        d = an * d + b;
335        if d.abs() < 1e-30 {
336            d = 1e-30;
337        }
338        c = b + an / c;
339        if c.abs() < 1e-30 {
340            c = 1e-30;
341        }
342        d = 1.0 / d;
343        let delta = d * c;
344        h *= delta;
345        if (delta - 1.0).abs() < 1e-14 {
346            break;
347        }
348    }
349    h * (-x + a * x.ln() - ln_gamma(a)).exp()
350}
351
352// ============================================================================
353// Error Function
354// ============================================================================
355
356/// Error function erf(x).
357///
358/// # Definition
359/// ```text
360/// erf(x) = (2/√π) ∫₀ˣ exp(-t²) dt
361/// ```
362///
363/// # Algorithm
364/// Abramowitz & Stegun formula 7.1.28, maximum absolute error < 1.5 × 10⁻⁷.
365///
366/// # Examples
367/// ```
368/// use u_numflow::special::erf;
369/// assert!(erf(0.0).abs() < 1e-7);
370/// assert!((erf(1.0) - 0.8427007929).abs() < 1e-6);
371/// ```
372pub fn erf(x: f64) -> f64 {
373    if x.is_nan() {
374        return f64::NAN;
375    }
376    let sign = if x >= 0.0 { 1.0 } else { -1.0 };
377    let x = x.abs();
378
379    // A&S 7.1.28
380    const P: f64 = 0.3275911;
381    const A1: f64 = 0.254829592;
382    const A2: f64 = -0.284496736;
383    const A3: f64 = 1.421413741;
384    const A4: f64 = -1.453152027;
385    const A5: f64 = 1.061405429;
386
387    let t = 1.0 / (1.0 + P * x);
388    let poly = t * (A1 + t * (A2 + t * (A3 + t * (A4 + t * A5))));
389    sign * (1.0 - poly * (-x * x).exp())
390}
391
392/// Complementary error function erfc(x) = 1 − erf(x).
393///
394/// More numerically stable than `1.0 - erf(x)` for large `x`.
395///
396/// # Examples
397/// ```
398/// use u_numflow::special::erfc;
399/// assert!((erfc(0.0) - 1.0).abs() < 1e-7);
400/// assert!((erfc(3.0)).abs() < 0.001);
401/// ```
402pub fn erfc(x: f64) -> f64 {
403    1.0 - erf(x)
404}
405
406// ============================================================================
407// Student's t-Distribution
408// ============================================================================
409
410/// CDF of Student's t-distribution: P(T ≤ t | df).
411///
412/// # Algorithm
413/// Uses the incomplete beta function:
414/// - For t ≥ 0: `F(t) = 1 − I_x(df/2, 1/2) / 2`
415/// - For t < 0: `F(t) = I_x(df/2, 1/2) / 2`
416///
417/// where `x = df / (df + t²)`.
418///
419/// # Returns
420/// - `f64::NAN` if df ≤ 0 or inputs are NaN.
421///
422/// # Examples
423/// ```
424/// use u_numflow::special::t_distribution_cdf;
425/// // CDF at 0 = 0.5 (symmetric)
426/// assert!((t_distribution_cdf(0.0, 10.0) - 0.5).abs() < 1e-10);
427/// // For large df, approaches normal CDF
428/// assert!((t_distribution_cdf(1.96, 1000.0) - 0.975).abs() < 0.002);
429/// ```
430pub fn t_distribution_cdf(t: f64, df: f64) -> f64 {
431    if t.is_nan() || df.is_nan() || df <= 0.0 {
432        return f64::NAN;
433    }
434    if t == 0.0 {
435        return 0.5;
436    }
437    let x = df / (df + t * t);
438    let ib = regularized_incomplete_beta(x, df / 2.0, 0.5);
439    if t >= 0.0 {
440        1.0 - ib / 2.0
441    } else {
442        ib / 2.0
443    }
444}
445
446/// PDF of Student's t-distribution.
447///
448/// # Formula
449/// ```text
450/// f(t; df) = Γ((df+1)/2) / (√(df·π) · Γ(df/2)) · (1 + t²/df)^(−(df+1)/2)
451/// ```
452pub fn t_distribution_pdf(t: f64, df: f64) -> f64 {
453    if t.is_nan() || df.is_nan() || df <= 0.0 {
454        return f64::NAN;
455    }
456    let half_df = df / 2.0;
457    let log_pdf = ln_gamma(half_df + 0.5)
458        - 0.5 * (df * std::f64::consts::PI).ln()
459        - ln_gamma(half_df)
460        - (half_df + 0.5) * (1.0 + t * t / df).ln();
461    log_pdf.exp()
462}
463
464/// Quantile function (inverse CDF) of Student's t-distribution.
465///
466/// Given a probability `p ∈ (0, 1)`, returns `t` such that `P(T ≤ t) = p`.
467///
468/// # Algorithm
469/// Newton-Raphson iteration with initial guess from inverse normal CDF.
470/// Converges in 5–15 iterations for typical inputs.
471///
472/// # Returns
473/// - `f64::NAN` if `p` is outside `(0, 1)` or df ≤ 0.
474///
475/// # Examples
476/// ```
477/// use u_numflow::special::t_distribution_quantile;
478/// // Median = 0
479/// assert!(t_distribution_quantile(0.5, 10.0).abs() < 1e-10);
480/// // df=∞ → normal quantile
481/// assert!((t_distribution_quantile(0.975, 10000.0) - 1.96).abs() < 0.01);
482/// ```
483pub fn t_distribution_quantile(p: f64, df: f64) -> f64 {
484    if p.is_nan() || df.is_nan() || df <= 0.0 || p <= 0.0 || p >= 1.0 {
485        return f64::NAN;
486    }
487    if (p - 0.5).abs() < 1e-15 {
488        return 0.0;
489    }
490
491    // Initial guess from normal approximation
492    let mut t = inverse_normal_cdf(p);
493
494    // Newton-Raphson refinement
495    for _ in 0..50 {
496        let cdf = t_distribution_cdf(t, df);
497        let pdf = t_distribution_pdf(t, df);
498        if pdf.abs() < 1e-300 {
499            break;
500        }
501        let delta = (cdf - p) / pdf;
502        t -= delta;
503        if delta.abs() < 1e-12 * t.abs().max(1.0) {
504            break;
505        }
506    }
507    t
508}
509
510// ============================================================================
511// F-Distribution
512// ============================================================================
513
514/// CDF of the F-distribution: P(X ≤ x | df1, df2).
515///
516/// # Algorithm
517/// Uses the incomplete beta function:
518/// ```text
519/// F(x; d1, d2) = I_y(d1/2, d2/2) where y = d1·x / (d1·x + d2)
520/// ```
521///
522/// # Returns
523/// - `f64::NAN` if df1 ≤ 0, df2 ≤ 0, or inputs are NaN.
524/// - `0.0` if x ≤ 0.
525///
526/// # Examples
527/// ```
528/// use u_numflow::special::f_distribution_cdf;
529/// assert!((f_distribution_cdf(0.0, 5.0, 10.0) - 0.0).abs() < 1e-10);
530/// // F(1.0; 10, 10) ≈ 0.5 (F(1) is median when df1 > 2)
531/// let f = f_distribution_cdf(1.0, 10.0, 10.0);
532/// assert!((f - 0.5).abs() < 0.05);
533/// ```
534pub fn f_distribution_cdf(x: f64, df1: f64, df2: f64) -> f64 {
535    if x.is_nan() || df1.is_nan() || df2.is_nan() || df1 <= 0.0 || df2 <= 0.0 {
536        return f64::NAN;
537    }
538    if x <= 0.0 {
539        return 0.0;
540    }
541    let y = df1 * x / (df1 * x + df2);
542    regularized_incomplete_beta(y, df1 / 2.0, df2 / 2.0)
543}
544
545/// Quantile function (inverse CDF) of the F-distribution.
546///
547/// Given a probability `p ∈ (0, 1)`, returns `x` such that `P(X ≤ x) = p`.
548///
549/// # Algorithm
550/// Bisection method on `[0, upper_bound]`. Robust for all parameter ranges.
551///
552/// # Returns
553/// - `f64::NAN` if `p` is outside `(0, 1)` or df1/df2 ≤ 0.
554pub fn f_distribution_quantile(p: f64, df1: f64, df2: f64) -> f64 {
555    if p.is_nan()
556        || df1.is_nan()
557        || df2.is_nan()
558        || df1 <= 0.0
559        || df2 <= 0.0
560        || p <= 0.0
561        || p >= 1.0
562    {
563        return f64::NAN;
564    }
565
566    // Find upper bound where CDF > p
567    let mut hi = 2.0;
568    while f_distribution_cdf(hi, df1, df2) < p {
569        hi *= 2.0;
570        if hi > 1e15 {
571            return hi;
572        }
573    }
574    let mut lo = 0.0_f64;
575
576    // Bisection
577    for _ in 0..200 {
578        let mid = (lo + hi) / 2.0;
579        if hi - lo < 1e-12 * mid.max(1e-15) {
580            break;
581        }
582        if f_distribution_cdf(mid, df1, df2) < p {
583            lo = mid;
584        } else {
585            hi = mid;
586        }
587    }
588    (lo + hi) / 2.0
589}
590
591// ============================================================================
592// Chi-Squared Distribution CDF
593// ============================================================================
594
595/// CDF of the chi-squared distribution: P(X ≤ x | k).
596///
597/// # Algorithm
598/// Uses the regularized lower incomplete gamma function:
599/// ```text
600/// F(x; k) = P(k/2, x/2) = γ(k/2, x/2) / Γ(k/2)
601/// ```
602///
603/// # Returns
604/// - `f64::NAN` if k ≤ 0 or inputs are NaN.
605/// - `0.0` if x ≤ 0.
606///
607/// # Examples
608/// ```
609/// use u_numflow::special::chi_squared_cdf;
610/// // P(X ≤ 0) = 0
611/// assert_eq!(chi_squared_cdf(0.0, 5.0), 0.0);
612/// // Known: P(X ≤ 3.841) ≈ 0.95 for df=1
613/// assert!((chi_squared_cdf(3.841, 1.0) - 0.95).abs() < 0.01);
614/// ```
615pub fn chi_squared_cdf(x: f64, k: f64) -> f64 {
616    if x.is_nan() || k.is_nan() || k <= 0.0 {
617        return f64::NAN;
618    }
619    if x <= 0.0 {
620        return 0.0;
621    }
622    regularized_lower_gamma(k / 2.0, x / 2.0)
623}
624
625#[cfg(test)]
626mod tests {
627    use super::*;
628
629    // --- standard_normal_cdf ---
630
631    #[test]
632    fn test_cdf_at_zero() {
633        assert!((standard_normal_cdf(0.0) - 0.5).abs() < 1e-7);
634    }
635
636    #[test]
637    fn test_cdf_symmetry() {
638        for &x in &[0.5, 1.0, 1.5, 2.0, 2.5, 3.0] {
639            let sum = standard_normal_cdf(x) + standard_normal_cdf(-x);
640            assert!(
641                (sum - 1.0).abs() < 1e-7,
642                "Φ({x}) + Φ(-{x}) = {sum}, expected 1.0"
643            );
644        }
645    }
646
647    #[test]
648    fn test_cdf_known_values() {
649        // 68-95-99.7 rule
650        assert!((standard_normal_cdf(1.0) - 0.8413).abs() < 0.001);
651        assert!((standard_normal_cdf(2.0) - 0.9772).abs() < 0.001);
652        assert!((standard_normal_cdf(3.0) - 0.9987).abs() < 0.001);
653
654        // Common critical values
655        assert!((standard_normal_cdf(1.645) - 0.95).abs() < 0.001);
656        assert!((standard_normal_cdf(1.96) - 0.975).abs() < 0.001);
657        assert!((standard_normal_cdf(2.576) - 0.995).abs() < 0.001);
658    }
659
660    #[test]
661    fn test_cdf_extremes() {
662        assert_eq!(standard_normal_cdf(f64::INFINITY), 1.0);
663        assert_eq!(standard_normal_cdf(f64::NEG_INFINITY), 0.0);
664        assert!(standard_normal_cdf(f64::NAN).is_nan());
665    }
666
667    #[test]
668    fn test_cdf_monotonic() {
669        let xs: Vec<f64> = (-30..=30).map(|i| i as f64 * 0.1).collect();
670        for w in xs.windows(2) {
671            assert!(
672                standard_normal_cdf(w[0]) <= standard_normal_cdf(w[1]),
673                "CDF not monotonic at x = {}, {}",
674                w[0],
675                w[1]
676            );
677        }
678    }
679
680    // --- inverse_normal_cdf ---
681
682    #[test]
683    fn test_inverse_cdf_at_half() {
684        assert!(inverse_normal_cdf(0.5).abs() < 1e-4);
685    }
686
687    #[test]
688    fn test_inverse_cdf_known_values() {
689        assert!((inverse_normal_cdf(0.8413) - 1.0).abs() < 0.01);
690        assert!((inverse_normal_cdf(0.975) - 1.96).abs() < 0.01);
691        assert!((inverse_normal_cdf(0.95) - 1.645).abs() < 0.01);
692    }
693
694    #[test]
695    fn test_inverse_cdf_symmetry() {
696        for &p in &[0.1, 0.2, 0.3, 0.4] {
697            let z1 = inverse_normal_cdf(p);
698            let z2 = inverse_normal_cdf(1.0 - p);
699            assert!(
700                (z1 + z2).abs() < 1e-3,
701                "Φ⁻¹({p}) + Φ⁻¹({}) = {}, expected ~0",
702                1.0 - p,
703                z1 + z2
704            );
705        }
706    }
707
708    #[test]
709    fn test_inverse_cdf_extremes() {
710        assert_eq!(inverse_normal_cdf(0.0), f64::NEG_INFINITY);
711        assert_eq!(inverse_normal_cdf(1.0), f64::INFINITY);
712        assert!(inverse_normal_cdf(f64::NAN).is_nan());
713        assert!(inverse_normal_cdf(-0.1).is_nan());
714        assert!(inverse_normal_cdf(1.1).is_nan());
715    }
716
717    #[test]
718    fn test_roundtrip_cdf_inverse() {
719        for &p in &[0.01, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.99] {
720            let z = inverse_normal_cdf(p);
721            let p_back = standard_normal_cdf(z);
722            assert!(
723                (p_back - p).abs() < 0.002,
724                "roundtrip failed: p={p}, z={z}, p_back={p_back}"
725            );
726        }
727    }
728
729    // --- standard_normal_pdf ---
730
731    #[test]
732    fn test_pdf_at_zero() {
733        let peak = standard_normal_pdf(0.0);
734        assert!((peak - 0.3989422804014327).abs() < 1e-14);
735    }
736
737    #[test]
738    fn test_pdf_symmetry() {
739        for &x in &[0.5, 1.0, 2.0, 3.0] {
740            let diff = (standard_normal_pdf(x) - standard_normal_pdf(-x)).abs();
741            assert!(diff < 1e-15, "PDF not symmetric at x={x}");
742        }
743    }
744
745    // --- ln_gamma / gamma ---
746
747    #[test]
748    fn test_ln_gamma_integers() {
749        // Γ(n) = (n-1)! for positive integers
750        assert!((ln_gamma(1.0)).abs() < 1e-10); // Γ(1) = 1
751        assert!((ln_gamma(2.0)).abs() < 1e-10); // Γ(2) = 1
752        assert!((ln_gamma(3.0) - 2.0_f64.ln()).abs() < 1e-10); // Γ(3) = 2
753        assert!((ln_gamma(5.0) - 24.0_f64.ln()).abs() < 1e-10); // Γ(5) = 24
754        assert!((ln_gamma(7.0) - 720.0_f64.ln()).abs() < 1e-9); // Γ(7) = 720
755    }
756
757    #[test]
758    fn test_gamma_half_integers() {
759        // Γ(0.5) = √π
760        let sqrt_pi = std::f64::consts::PI.sqrt();
761        assert!((gamma(0.5) - sqrt_pi).abs() < 1e-10);
762        // Γ(1.5) = √π/2
763        assert!((gamma(1.5) - sqrt_pi / 2.0).abs() < 1e-10);
764        // Γ(2.5) = 3√π/4
765        assert!((gamma(2.5) - 3.0 * sqrt_pi / 4.0).abs() < 1e-10);
766    }
767
768    #[test]
769    fn test_gamma_positive() {
770        for &x in &[0.1, 0.5, 1.0, 2.0, 5.0, 10.0] {
771            assert!(gamma(x) > 0.0, "Γ({x}) should be positive");
772        }
773    }
774
775    // --- ln_beta ---
776
777    #[test]
778    fn test_ln_beta_known() {
779        // B(1,1) = 1, ln B(1,1) = 0
780        assert!(ln_beta(1.0, 1.0).abs() < 1e-10);
781        // B(1,2) = 1/2, ln B(1,2) = -ln(2)
782        assert!((ln_beta(1.0, 2.0) - (-2.0_f64.ln())).abs() < 1e-10);
783        // B(a,b) = B(b,a)
784        assert!((ln_beta(3.0, 5.0) - ln_beta(5.0, 3.0)).abs() < 1e-10);
785    }
786
787    // --- regularized_incomplete_beta ---
788
789    #[test]
790    fn test_inc_beta_boundary() {
791        assert_eq!(regularized_incomplete_beta(0.0, 2.0, 3.0), 0.0);
792        assert_eq!(regularized_incomplete_beta(1.0, 2.0, 3.0), 1.0);
793    }
794
795    #[test]
796    fn test_inc_beta_uniform() {
797        // I_x(1,1) = x (Uniform case)
798        for &x in &[0.1, 0.3, 0.5, 0.7, 0.9] {
799            let result = regularized_incomplete_beta(x, 1.0, 1.0);
800            assert!(
801                (result - x).abs() < 1e-10,
802                "I_{x}(1,1) = {result}, expected {x}"
803            );
804        }
805    }
806
807    #[test]
808    fn test_inc_beta_symmetry() {
809        // I_0.5(a,a) = 0.5
810        let result = regularized_incomplete_beta(0.5, 3.0, 3.0);
811        assert!((result - 0.5).abs() < 1e-8);
812    }
813
814    #[test]
815    fn test_inc_beta_known_formula() {
816        // I_x(1,b) = 1 - (1-x)^b
817        for &x in &[0.1, 0.5, 0.9] {
818            let result = regularized_incomplete_beta(x, 1.0, 3.0);
819            let expected = 1.0 - (1.0 - x).powi(3);
820            assert!((result - expected).abs() < 1e-10);
821        }
822    }
823
824    // --- regularized_lower_gamma ---
825
826    #[test]
827    fn test_lower_gamma_exponential() {
828        // P(1, x) = 1 - exp(-x) (exponential distribution CDF)
829        for &x in &[0.5, 1.0, 2.0, 5.0] {
830            let result = regularized_lower_gamma(1.0, x);
831            let expected = 1.0 - (-x).exp();
832            assert!(
833                (result - expected).abs() < 1e-10,
834                "P(1,{x}) = {result}, expected {expected}"
835            );
836        }
837    }
838
839    #[test]
840    fn test_lower_gamma_boundary() {
841        assert_eq!(regularized_lower_gamma(2.0, 0.0), 0.0);
842        assert_eq!(regularized_lower_gamma(2.0, -1.0), 0.0);
843    }
844
845    #[test]
846    fn test_lower_gamma_large_x() {
847        // For large x, P(a, x) → 1
848        let result = regularized_lower_gamma(3.0, 100.0);
849        assert!((result - 1.0).abs() < 1e-10);
850    }
851
852    // --- erf / erfc ---
853
854    #[test]
855    fn test_erf_known_values() {
856        assert!(erf(0.0).abs() < 1e-7);
857        assert!((erf(1.0) - 0.8427007929).abs() < 1e-6);
858        // erf(∞) = 1
859        assert!((erf(10.0) - 1.0).abs() < 1e-7);
860        // erf(-x) = -erf(x)
861        assert!((erf(1.5) + erf(-1.5)).abs() < 1e-7);
862    }
863
864    #[test]
865    fn test_erf_nan() {
866        assert!(erf(f64::NAN).is_nan());
867    }
868
869    #[test]
870    fn test_erfc_complement() {
871        for &x in &[0.0, 0.5, 1.0, 2.0, 3.0] {
872            let sum = erf(x) + erfc(x);
873            assert!((sum - 1.0).abs() < 1e-7, "erf({x}) + erfc({x}) = {sum}");
874        }
875    }
876
877    // --- t-distribution ---
878
879    #[test]
880    fn test_t_cdf_at_zero() {
881        // CDF at 0 = 0.5 (symmetric)
882        for &df in &[1.0, 5.0, 10.0, 30.0, 100.0] {
883            assert!(
884                (t_distribution_cdf(0.0, df) - 0.5).abs() < 1e-10,
885                "t_cdf(0, {df}) should be 0.5"
886            );
887        }
888    }
889
890    #[test]
891    fn test_t_cdf_symmetry() {
892        for &df in &[1.0, 5.0, 10.0] {
893            for &t in &[0.5, 1.0, 2.0] {
894                let sum = t_distribution_cdf(t, df) + t_distribution_cdf(-t, df);
895                assert!(
896                    (sum - 1.0).abs() < 1e-8,
897                    "t_cdf({t},{df}) + t_cdf(-{t},{df}) = {sum}"
898                );
899            }
900        }
901    }
902
903    #[test]
904    fn test_t_cdf_approaches_normal() {
905        // For large df, t-distribution → normal
906        let t_val = 1.96;
907        let result = t_distribution_cdf(t_val, 10000.0);
908        let expected = standard_normal_cdf(t_val);
909        assert!((result - expected).abs() < 0.002);
910    }
911
912    #[test]
913    fn test_t_cdf_known_values() {
914        // t(0.025, df=10) → CDF ≈ 0.025 → t ≈ -2.228
915        let cdf = t_distribution_cdf(-2.228, 10.0);
916        assert!((cdf - 0.025).abs() < 0.002, "t_cdf(-2.228, 10) = {cdf}");
917    }
918
919    #[test]
920    fn test_t_cdf_nan() {
921        assert!(t_distribution_cdf(1.0, -1.0).is_nan());
922        assert!(t_distribution_cdf(f64::NAN, 5.0).is_nan());
923    }
924
925    #[test]
926    fn test_t_pdf_positive() {
927        for &df in &[1.0, 5.0, 10.0] {
928            for &t in &[-2.0, 0.0, 1.0, 3.0] {
929                assert!(t_distribution_pdf(t, df) > 0.0);
930            }
931        }
932    }
933
934    #[test]
935    fn test_t_pdf_symmetry() {
936        for &df in &[1.0, 5.0, 10.0] {
937            for &t in &[0.5, 1.0, 2.0] {
938                let diff = (t_distribution_pdf(t, df) - t_distribution_pdf(-t, df)).abs();
939                assert!(diff < 1e-12, "t_pdf not symmetric at t={t}, df={df}");
940            }
941        }
942    }
943
944    #[test]
945    fn test_t_quantile_median() {
946        // Median should be 0
947        for &df in &[1.0, 5.0, 10.0, 100.0] {
948            assert!(t_distribution_quantile(0.5, df).abs() < 1e-10);
949        }
950    }
951
952    #[test]
953    fn test_t_quantile_roundtrip() {
954        for &df in &[2.0, 5.0, 10.0, 30.0] {
955            for &p in &[0.025, 0.05, 0.1, 0.5, 0.9, 0.95, 0.975] {
956                let t = t_distribution_quantile(p, df);
957                let p_back = t_distribution_cdf(t, df);
958                assert!(
959                    (p_back - p).abs() < 1e-6,
960                    "roundtrip: p={p}, df={df}, t={t}, p_back={p_back}"
961                );
962            }
963        }
964    }
965
966    #[test]
967    fn test_t_quantile_nan() {
968        assert!(t_distribution_quantile(0.0, 5.0).is_nan());
969        assert!(t_distribution_quantile(1.0, 5.0).is_nan());
970        assert!(t_distribution_quantile(0.5, -1.0).is_nan());
971    }
972
973    // --- F-distribution ---
974
975    #[test]
976    fn test_f_cdf_zero() {
977        assert_eq!(f_distribution_cdf(0.0, 5.0, 10.0), 0.0);
978        assert_eq!(f_distribution_cdf(-1.0, 5.0, 10.0), 0.0);
979    }
980
981    #[test]
982    fn test_f_cdf_known() {
983        // F(1.0; 10, 10) ≈ 0.5 (median for equal df when df > 2)
984        let f = f_distribution_cdf(1.0, 10.0, 10.0);
985        assert!((f - 0.5).abs() < 0.05, "F_cdf(1,10,10) = {f}");
986    }
987
988    #[test]
989    fn test_f_cdf_monotonic() {
990        let xs: Vec<f64> = (0..=20).map(|i| i as f64 * 0.5).collect();
991        for w in xs.windows(2) {
992            let c0 = f_distribution_cdf(w[0], 5.0, 10.0);
993            let c1 = f_distribution_cdf(w[1], 5.0, 10.0);
994            assert!(
995                c1 >= c0 - 1e-10,
996                "F CDF not monotonic at {}, {}",
997                w[0],
998                w[1]
999            );
1000        }
1001    }
1002
1003    #[test]
1004    fn test_f_cdf_nan() {
1005        assert!(f_distribution_cdf(1.0, -1.0, 5.0).is_nan());
1006        assert!(f_distribution_cdf(1.0, 5.0, -1.0).is_nan());
1007    }
1008
1009    #[test]
1010    fn test_f_quantile_roundtrip() {
1011        for &(df1, df2) in &[(5.0, 10.0), (10.0, 10.0), (3.0, 20.0)] {
1012            for &p in &[0.05, 0.1, 0.5, 0.9, 0.95] {
1013                let x = f_distribution_quantile(p, df1, df2);
1014                let p_back = f_distribution_cdf(x, df1, df2);
1015                assert!(
1016                    (p_back - p).abs() < 1e-4,
1017                    "F roundtrip: p={p}, df1={df1}, df2={df2}, x={x}, p_back={p_back}"
1018                );
1019            }
1020        }
1021    }
1022
1023    #[test]
1024    fn test_f_quantile_nan() {
1025        assert!(f_distribution_quantile(0.0, 5.0, 10.0).is_nan());
1026        assert!(f_distribution_quantile(1.0, 5.0, 10.0).is_nan());
1027    }
1028
1029    // --- Chi-squared ---
1030
1031    #[test]
1032    fn test_chi2_cdf_zero() {
1033        assert_eq!(chi_squared_cdf(0.0, 5.0), 0.0);
1034        assert_eq!(chi_squared_cdf(-1.0, 5.0), 0.0);
1035    }
1036
1037    #[test]
1038    fn test_chi2_cdf_exponential_special_case() {
1039        // Chi2(2) = Exponential(1/2): CDF(x) = 1 - exp(-x/2)
1040        for &x in &[1.0, 2.0, 5.0, 10.0] {
1041            let result = chi_squared_cdf(x, 2.0);
1042            let expected = 1.0 - (-x / 2.0).exp();
1043            assert!(
1044                (result - expected).abs() < 1e-8,
1045                "chi2_cdf({x}, 2) = {result}, expected {expected}"
1046            );
1047        }
1048    }
1049
1050    #[test]
1051    fn test_chi2_cdf_known_critical() {
1052        // P(X ≤ 3.841) ≈ 0.95 for df=1
1053        assert!((chi_squared_cdf(3.841, 1.0) - 0.95).abs() < 0.01);
1054        // P(X ≤ 5.991) ≈ 0.95 for df=2
1055        assert!((chi_squared_cdf(5.991, 2.0) - 0.95).abs() < 0.01);
1056    }
1057
1058    #[test]
1059    fn test_chi2_cdf_nan() {
1060        assert!(chi_squared_cdf(1.0, -1.0).is_nan());
1061        assert!(chi_squared_cdf(f64::NAN, 5.0).is_nan());
1062    }
1063}
1064
1065#[cfg(test)]
1066mod proptests {
1067    use super::*;
1068    use proptest::prelude::*;
1069
1070    proptest! {
1071        #![proptest_config(ProptestConfig::with_cases(500))]
1072
1073        #[test]
1074        fn cdf_in_zero_one(x in -6.0_f64..6.0) {
1075            let c = standard_normal_cdf(x);
1076            prop_assert!((0.0..=1.0).contains(&c), "CDF({x}) = {c} out of [0,1]");
1077        }
1078
1079        #[test]
1080        fn cdf_is_monotonic(x1 in -6.0_f64..6.0, x2 in -6.0_f64..6.0) {
1081            let (lo, hi) = if x1 <= x2 { (x1, x2) } else { (x2, x1) };
1082            prop_assert!(
1083                standard_normal_cdf(lo) <= standard_normal_cdf(hi) + 1e-15,
1084                "CDF not monotonic"
1085            );
1086        }
1087
1088        #[test]
1089        fn inverse_roundtrip(p in 0.001_f64..0.999) {
1090            let z = inverse_normal_cdf(p);
1091            let p_back = standard_normal_cdf(z);
1092            let err = (p_back - p).abs();
1093            prop_assert!(err < 0.005, "roundtrip error {} for p={}", err, p);
1094        }
1095
1096        #[test]
1097        fn pdf_is_non_negative(x in -10.0_f64..10.0) {
1098            prop_assert!(standard_normal_pdf(x) >= 0.0);
1099        }
1100
1101        #[test]
1102        fn inc_beta_in_01(x in 0.01_f64..0.99, a in 0.5_f64..10.0, b in 0.5_f64..10.0) {
1103            let result = regularized_incomplete_beta(x, a, b);
1104            prop_assert!(
1105                (0.0..=1.0).contains(&result),
1106                "I_{x}({a},{b}) = {result} out of [0,1]"
1107            );
1108        }
1109
1110        #[test]
1111        fn inc_beta_complementary(x in 0.01_f64..0.99, a in 0.5_f64..10.0, b in 0.5_f64..10.0) {
1112            // I_x(a,b) + I_{1-x}(b,a) = 1
1113            let ix = regularized_incomplete_beta(x, a, b);
1114            let i1x = regularized_incomplete_beta(1.0 - x, b, a);
1115            prop_assert!(
1116                (ix + i1x - 1.0).abs() < 1e-8,
1117                "complementary: {ix} + {i1x} != 1"
1118            );
1119        }
1120
1121        #[test]
1122        fn t_cdf_in_01(t in -10.0_f64..10.0, df in 1.0_f64..100.0) {
1123            let c = t_distribution_cdf(t, df);
1124            prop_assert!(
1125                (0.0..=1.0).contains(&c),
1126                "t_cdf({t}, {df}) = {c} out of [0,1]"
1127            );
1128        }
1129
1130        #[test]
1131        fn t_cdf_symmetric(t in 0.01_f64..10.0, df in 1.0_f64..50.0) {
1132            let sum = t_distribution_cdf(t, df) + t_distribution_cdf(-t, df);
1133            prop_assert!(
1134                (sum - 1.0).abs() < 1e-6,
1135                "t symmetry: {sum} != 1 for t={t}, df={df}"
1136            );
1137        }
1138
1139        #[test]
1140        fn erf_odd_symmetry(x in 0.01_f64..5.0) {
1141            let sum = erf(x) + erf(-x);
1142            prop_assert!(sum.abs() < 1e-6, "erf odd symmetry: {sum} for x={x}");
1143        }
1144
1145        #[test]
1146        fn chi2_cdf_in_01(x in 0.01_f64..50.0, k in 0.5_f64..20.0) {
1147            let c = chi_squared_cdf(x, k);
1148            prop_assert!(
1149                (0.0..=1.0).contains(&c),
1150                "chi2_cdf({x}, {k}) = {c} out of [0,1]"
1151            );
1152        }
1153    }
1154}