Skip to main content

thermite_special/specialized/generic/
probit.rs

1use thermite::{
2    element::{FloatElement, FloatElementWithBits},
3    math::{
4        CoreMathWithPolicy as _, TranscendentalMathWithPolicy as _,
5        policy::{Policy, PrecisionPolicy},
6    },
7    prelude::*,
8};
9
10use crate::specialized::SpecializedSpecialMath;
11
12/// Shared Acklam normal-quantile (probit) core for all real element types.
13///
14/// `probit(p) = Phi^-1(p)`, via Peter John Acklam's rational approximation
15/// (a central region plus a `q = sqrt(-2 ln p)` tail branch):
16/// <https://web.archive.org/web/20151030215612/http://home.online.no/~pjacklam/notes/invnorm/>
17///
18/// `REFINE` enables a single Halley step that polishes Acklam's ~1.15e-9 fit up to
19/// full double precision (used by f64; f32 is already at its precision limit without
20/// it). The step only runs when the policy precision is `Best` or higher.
21#[inline(always)]
22pub fn probit_acklam<P, E, V, const REFINE: bool>(p_in: V, a: &[E; 6], b: &[E; 6], c: &[E; 6], d: &[E; 5]) -> V
23where
24    P: Policy,
25    E: FloatElementWithBits,
26    V: FloatVectorWithBits<Element = E> + SpecializedSpecialMath<E>,
27{
28    let p = p_in.min(V::ONE - p_in); // reflect to (0, 0.5]
29    // lower tail if p < 0.02425 (= 97/4000), upper tail if p > 0.97575
30    let is_tail = p.cmp_lt(V::splat(<E as FloatElement>::ConstRatio::<97, 4000>::VALUE));
31
32    let q = p - V::HALF;
33    let mut y = q * (q * q).poly_rational_n_p::<P, _, _>(a, b);
34
35    if const { P::POLICY.avoid_branching } || is_tail.any() {
36        let q = (-V::TWO * p.ln_p::<P>()).sqrt();
37        let t = q.poly_rational_n_p::<P, _, _>(c, d);
38
39        y = is_tail.select(t, y);
40    }
41
42    let mut x = y.copysign(p_in - V::HALF);
43
44    // Acklam's rational fit is only good to ~1.15e-9, which is ~30 of 53 bits and
45    // not what an Average-or-better tier should be handing back. One Halley step on
46    // the DEFINING equation refines it to full precision, so above `Medium` the
47    // returned value is a root of `Phi(x) = p` rather than a fit to one. With
48    // f(x) = Phi(x) - p, u = f/f' = (Phi(x) - p)/phi(x):
49    //   Phi(x) = 0.5*erfc(-x/sqrt2),  1/phi(x) = sqrt(2pi)*exp(x^2/2)
50    //   x <- x - u / (1 + x*u/2)
51    //
52    // The closed form `sqrt(2) * erfinv(2p - 1)` is the other way to "use the real
53    // formula" and it is measurably worse: `2p - 1` rounds to exactly -1 once
54    // p < 2^-54, so it returns -inf below p = 1e-17 and is already 4.4e-07 relative
55    // at p = 1e-12, where this path is machine-precision. Acklam's `sqrt(-2 ln p)`
56    // tail branch is what makes the far tail work at all, so it stays as the seed.
57    if const { REFINE && P::POLICY.precision.ge(PrecisionPolicy::Average) } {
58        // Halley is cubic, so one step takes Acklam's ~1e-9 past full precision and
59        // a second has nothing left to converge on. `Reference` runs three anyway,
60        // being the tier where cost is not a consideration and the extra steps are
61        // free insurance if a future seed change makes the first step insufficient.
62        //
63        // They do NOT buy accuracy here. The step converges to the root of the
64        // COMPUTED Phi(x) - p, so once the residual is dominated by `erfc`'s own
65        // error rather than by the seed, iterating cannot move it. At p ~ 0.45 and
66        // one step, 44 ulp at `Precision` against 6 ulp at `Reference` on the same
67        // iteration count, and the whole gap is `erfc`/`exp` being libm-backed at
68        // `Reference`. The remedy for that region is a better erfc, not more Halley.
69        let steps = const {
70            if P::POLICY.precision.ge(PrecisionPolicy::Reference) {
71                3
72            } else {
73                1
74            }
75        };
76
77        let mut i = 0;
78        while i < steps {
79            let e = <V as SpecializedSpecialMath<E>>::erfc::<P>(x * -V::FRAC_1_SQRT_2).mul_sube(V::HALF, p_in);
80            let u = e * V::SQRT_TAU * (x * x * V::HALF).exp_p::<P>();
81            x -= u / x.mul_adde(u * V::HALF, V::ONE);
82            i += 1;
83        }
84    }
85
86    x
87}
Last built: 2026-09-08 21:35:55 UTC