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}