thermite_special/specialized/generic/inverses.rs
1//! Newton inverses of shipped forwards: `inv_digamma` and `wright_omega`.
2//!
3//! Both are the same shape as [`inv_log_ndtr`](super::ndtr::inv_log_ndtr_impl): a cheap
4//! seed one precision tier down, then `newtons_method` on the forward with its closed-form
5//! derivative, stopping at a residual tolerance a few ulp above the forward's own noise and
6//! capped at eight iterations. Each forward is increasing and concave on its domain, so a
7//! Newton step from either side lands left of the root and the iteration is monotone from
8//! there. The bracket handed to `newtons_method` only keeps a wild seed from crossing zero.
9//!
10//! Newton cannot beat the forward's rounding, and both functions have a region
11//! where that rounding is the whole error. `digamma(x) - y` for large `x` is `ln x - y`
12//! to a rounding of `eps * y`, which is `eps * y` _relative_ in `x`. `w + ln w - x` for
13//! very negative `x` is the difference of two numbers near `x` whose true difference is
14//! `e^x`. Each gets an analytic arm there instead: the Stirling fixed point in the
15//! exponent for `inv_digamma` above `y = 6`, and the Lagrange series `sum (-n)^{n-1}/n!
16//! e^{nx}` for `wright_omega` below `x = -7`.
17
18use thermite::{
19 element::FloatElement,
20 math::{
21 TranscendentalMathWithPolicy as _,
22 algorithms::newtons_method,
23 policy::{
24 Policy,
25 policies::{LessPrecision, MaxIterations},
26 },
27 },
28 prelude::*,
29};
30
31use super::ndtr::residual_tolerance;
32use crate::specialized::SpecializedSpecialMath;
33
34/// The `x > 0` with `digamma(x) = y`.
35///
36/// Seed (Minka, "Estimating a Dirichlet distribution", appendix): `e^y + 1/2` for
37/// `y >= -2.22` and `-1/(y - digamma(1))` below, both from the asymptotics at the two ends
38/// and within a factor of two of the root everywhere. Newton with `trigamma` from there,
39/// bracketed by that factor of two on each side.
40///
41/// Above `y = 3` (`x > 20`) the answer is the Stirling series solved for `x` instead.
42/// `digamma(x) = ln x - t(x)` with `t = 1/(2x) + 1/(12x^2) - 1/(120x^4) + ... - 691/(32760 x^12)`
43/// (5e-18 at `x = 20`), so `x = e^y e^{t(x)}`. Writing `x = e^y u`, the unknown `u = e^{t}`
44/// is the root of `h(u) = u - e^{t(e^y u)}`, which Newton takes quadratically from
45/// `u_0 = e^{t(e^y + 1/2)}` in two steps. Nothing in `h` subtracts `y`, so the result is
46/// within an ulp or two of `e^y`'s own rounding, where Newton on `digamma` is bounded by
47/// `eps * y` relative (the residual `digamma(x) - y` is `ln x - y` to a rounding of
48/// `eps * y`). The plain fixed point `x <- e^y e^{t(x)}` contracts only by `1/(2x)` per
49/// pass and would need seven passes at `x = 20`.
50#[inline(always)]
51pub fn inv_digamma_impl<P, E, V>(y: V) -> V
52where
53 P: Policy,
54 E: FloatElement,
55 V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
56{
57 let finite = y.is_finite();
58 let big = y.cmp_ge(V::splat(<E as FloatElement>::ConstInt::<3>::VALUE));
59 let active = finite & !big;
60
61 // e^y at the seed's tier serves both the seed and the analytic arm.
62 let ey = y.exp_p::<LessPrecision<P>>();
63
64 // Minka's seed, both halves cheap: the seam is at digamma(0.6) ~ -2.22.
65 let seam = V::splat(<E as FloatElement>::ConstRatio::<-222, 100>::VALUE);
66 let low = y.cmp_lt(seam);
67 let x0 = low.select(
68 (y + V::EULER_GAMMA).approx_reciprocal_p::<LessPrecision<P>>().neg(),
69 ey + V::HALF,
70 );
71
72 let mut x = x0;
73 if const { P::POLICY.avoid_branching } || active.any() {
74 let tol = residual_tolerance::<P, E, V>(y.abs().max(V::ONE));
75 let bounds = Some((x0 * V::HALF, x0 + x0));
76 let (r, _) = newtons_method::<V, MaxIterations<P, 8>, _>(x0, tol, active, bounds, |x| {
77 (
78 <V as SpecializedSpecialMath<E>>::digamma::<P>(x) - y,
79 <V as SpecializedSpecialMath<E>>::trigamma::<P>(x),
80 )
81 });
82 x = r;
83 }
84
85 if const { P::POLICY.avoid_branching } || big.any() {
86 let ey = if const { P::POLICY.precision.gt(thermite::math::policy::PrecisionPolicy::Medium) } {
87 y.exp_p::<P>()
88 } else {
89 ey
90 };
91
92 // t(x) and t'(x) from r = 1/x. t' only needs to be right to a few percent: it is
93 // Newton's slope, and h sets the step size, not h'.
94 let stirling = |xs: V| -> (V, V) {
95 let r = xs.approx_reciprocal_p::<P>();
96 let r2 = r * r;
97 // 1/(2x) is the one odd power. The rest is a polynomial in 1/x^2.
98 let even = r2.poly_n_p::<P, _>(&[
99 <E as FloatElement>::ConstRatio::<1, 12>::VALUE,
100 <E as FloatElement>::ConstRatio::<-1, 120>::VALUE,
101 <E as FloatElement>::ConstRatio::<1, 252>::VALUE,
102 <E as FloatElement>::ConstRatio::<-1, 240>::VALUE,
103 <E as FloatElement>::ConstRatio::<1, 132>::VALUE,
104 <E as FloatElement>::ConstRatio::<-691, 32760>::VALUE,
105 ]);
106 let t = r.mul_adde(V::HALF, r2 * even);
107 // t(x) = 1/(2x) + 1/(12x^2) + ..., so t'(x) = -(1/(2x^2))(1 + 1/(3x) + ...).
108 let dt = -(r2 * V::HALF) * r.mul_adde(V::splat(<E as FloatElement>::ConstRatio::<1, 3>::VALUE), V::ONE);
109 (t, dt)
110 };
111
112 let (t0, _) = stirling(ey + V::HALF);
113 let mut u = t0.exp_p::<P>();
114 let steps = const {
115 if P::POLICY.precision.ge(thermite::math::policy::PrecisionPolicy::Best) {
116 3
117 } else {
118 2
119 }
120 };
121 let mut i = 0;
122 while i < steps {
123 let xs = ey * u;
124 let (t, dt) = stirling(xs);
125 let et = t.exp_p::<P>();
126 // h = u - e^t, h' = 1 - e^t t'(x) e^y.
127 let h = u - et;
128 let dh = (et * dt).nmul_adde(ey, V::ONE);
129 u -= h / dh;
130 i += 1;
131 }
132 x = big.select(ey * u, x);
133 }
134
135 // digamma maps (0, inf) onto the whole line: +inf -> +inf, -inf -> 0.
136 let x = y.cmp_eq(V::INFINITY).select(V::INFINITY, x);
137 let x = y.cmp_eq(V::NEG_INFINITY).select(V::ZERO, x);
138 y.is_nan().select(V::NAN, x)
139}
140
141/// The Wright omega function, the `w > 0` with `w + ln w = x`, which is `W_0(e^x)` without
142/// ever forming `e^x`.
143///
144/// Seeds by region, after Lawrence, Corless and Jeffrey (2012, the algorithm SciPy uses):
145/// `q(1 - q(1 - 3q/2))` in `q = e^x` for `x <= -2`, the series about `x = 1` for
146/// `-2 < x < 1`, and `x - ln x + ln x / x` above, each one tier down. Newton on
147/// `w + ln w - x` with `1 + 1/w` from there, bracketed by a factor of two.
148///
149/// Below `x = -7` the residual `w + ln w - x` is the difference of two numbers near `x`
150/// whose true difference is `e^x < 1e-3`, so Newton is bounded by `eps * |x|` relative.
151/// The Lagrange series `w = sum_{n>=1} (-n)^{n-1}/n! q^n` to six terms is `2e-17` relative
152/// there and is the whole answer, one `exp` and five FMAs. Above `x = 1e20` the seed is
153/// the answer to working precision and Newton's first residual is already within tolerance.
154#[inline(always)]
155pub fn wright_omega_impl<P, E, V>(x: V) -> V
156where
157 P: Policy,
158 E: FloatElement,
159 V: FloatVector<Element = E> + SpecializedSpecialMath<E>,
160{
161 let finite = x.is_finite();
162 let series = x.cmp_le(-V::splat(<E as FloatElement>::ConstInt::<7>::VALUE));
163 let active = finite & !series;
164
165 let mut w = V::ZERO;
166
167 if const { P::POLICY.avoid_branching } || series.any() {
168 // (-n)^{n-1}/n!: 1, -1, 3/2, -8/3, 125/24, -54/5.
169 let q = x.exp_p::<P>();
170 let p = q.poly_n_p::<P, _>(&[
171 E::ZERO,
172 E::ONE,
173 -E::ONE,
174 <E as FloatElement>::ConstRatio::<3, 2>::VALUE,
175 <E as FloatElement>::ConstRatio::<-8, 3>::VALUE,
176 <E as FloatElement>::ConstRatio::<125, 24>::VALUE,
177 <E as FloatElement>::ConstRatio::<-54, 5>::VALUE,
178 ]);
179 w = p;
180 }
181
182 if const { P::POLICY.avoid_branching } || active.any() {
183 let left = x.cmp_le(-V::TWO);
184 let right = x.cmp_ge(V::ONE);
185
186 let mut w0 = V::ZERO;
187 if const { P::POLICY.avoid_branching } || left.any() {
188 let q = x.exp_p::<LessPrecision<P>>();
189 let three_halves = V::splat(<E as FloatElement>::ConstRatio::<3, 2>::VALUE);
190 w0 = q * q.nmul_adde(q.nmul_adde(three_halves, V::ONE), V::ONE);
191 }
192 if const { P::POLICY.avoid_branching } || !(left | right).all() {
193 // omega about x = 1, where omega(1) = 1 exactly.
194 let z = x - V::ONE;
195 let mid = z.poly_n_p::<LessPrecision<P>, _>(&[
196 E::ONE,
197 <E as FloatElement>::ConstRatio::<1, 2>::VALUE,
198 <E as FloatElement>::ConstRatio::<1, 16>::VALUE,
199 <E as FloatElement>::ConstRatio::<-1, 192>::VALUE,
200 <E as FloatElement>::ConstRatio::<-1, 3072>::VALUE,
201 <E as FloatElement>::ConstRatio::<13, 61440>::VALUE,
202 ]);
203 w0 = left.select(w0, mid);
204 }
205 if const { P::POLICY.avoid_branching } || right.any() {
206 let lx = x.ln_p::<LessPrecision<P>>();
207 w0 = right.select((x - lx) + lx * x.approx_reciprocal_p::<LessPrecision<P>>(), w0);
208 }
209
210 let tol = residual_tolerance::<P, E, V>(x.abs().max(V::ONE));
211 let bounds = Some((w0 * V::HALF, w0 + w0));
212 let (r, _) = newtons_method::<V, MaxIterations<P, 8>, _>(w0, tol, active, bounds, |w| {
213 let rw = w.approx_reciprocal_p::<P>();
214 (w + w.ln_p::<P>() - x, V::ONE + rw)
215 });
216 w = active.select(r, w);
217 }
218
219 // omega(+inf) = +inf, omega(-inf) = 0.
220 let w = x.cmp_eq(V::INFINITY).select(V::INFINITY, w);
221 x.is_nan().select(V::NAN, w)
222}