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