thermite_special/specialized/generic/erfcx.rs
1//! `erfcx(x) = e^{x^2} erfc(x)`, the scaled complementary error function.
2//!
3//! # Motivation
4//!
5//! `erfc` underflows to zero at `x ~ 27` in binary64 and `x ~ 9` in binary32, where the
6//! true value is `e^{-x^2}/(x sqrt(pi))`, nonzero and merely unrepresentable. Every
7//! Gaussian tail, importance weight and log-likelihood past that point silently becomes
8//! zero. `erfcx` removes the exponential and is `O(1/x)`, so it stays representable for
9//! every finite argument and carries full relative accuracy the whole way.
10//!
11//! # Algorithm
12//!
13//! The Faddeeva function restricted to the imaginary axis: `w(ix) = erfcx(x)` exactly.
14//! Weideman's approximation (see [`crate::tables::weideman`]) is
15//!
16//! ```text
17//! Z = (L + iz)/(L - iz), w(z) = 1/(sqrt(pi)(L - iz)) + 2 P(Z)/(L - iz)^2
18//! ```
19//!
20//! with `P` real. Substituting `z = ix` for real `x` makes `L - iz = L + x` and
21//! `Z = (L - x)/(L + x)`, **both real**: every complex operation in the method
22//! disappears and what is left is one reciprocal and one real Horner. There are no
23//! transcendentals at all on the non-negative side, which makes this cheaper than the
24//! `erfc` it complements.
25//!
26//! The domain is well conditioned throughout. `L + x >= L > 0` for every finite `x >= 0`,
27//! so the reciprocal needs no guard, and `Z` runs monotonically over `(-1, 1]`, so the
28//! Horner stays inside the unit disc the coefficients were fitted on. Only the
29//! infinities fall outside that, `Z` there being `(L - inf) * 0`, and they are named
30//! explicitly under `check_overflow`.
31//!
32//! Measured against mpmath at 50 digits with the `N = 40` table, the worst relative
33//! error over `x` from 0 to `1e15` is 1.22 ulp.
34//!
35//! # Negative arguments
36//!
37//! `erfcx(-x) = 2 e^{x^2} - erfcx(x)`, which genuinely overflows for `x` below about
38//! -26.6 (binary64). `erfcx` grows like `e^{x^2}` to the left, so the infinity is the
39//! correct answer rather than a failure. This is the only branch, and it is the only
40//! place an `exp` appears.
41
42use thermite::{
43 element::FloatElement,
44 math::{
45 CoreMathWithPolicy as _, TranscendentalMathWithPolicy as _, policy::Policy,
46 specialized::SpecializedTranscendentalMath,
47 },
48 prelude::*,
49};
50
51use crate::tables::weideman::{Weideman, WeidemanTables, weideman_n};
52
53/// `erfcx` by the `N`-term Weideman approximation on the imaginary axis.
54///
55/// `N` is a literal at every call site (the ladder in [`erfcx_internal`] instantiates it
56/// as one of 8/16/24/32/40), which is what lets the trip count and the coefficient loads
57/// fold.
58#[inline(always)]
59pub fn erfcx_with<V, E, P, const N: usize>(x: V, l: E, a: &[E; N]) -> V
60where
61 E: FloatElement,
62 V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
63 P: Policy,
64{
65 let l = V::splat(l);
66 let ax = x.abs();
67
68 // L + |x| >= L > 0: the one reciprocal, and it needs no guard.
69 let r = (l + ax).approx_reciprocal_p::<P>();
70 let z = (l - ax) * r;
71
72 let p = z.poly_rev_n_p::<P, _>(a);
73
74 // w = r/sqrt(pi) + 2 P r^2, grouped so the second `r` multiplies once.
75 let mut y = r * (p + p).mul_adde(r, V::FRAC_1_SQRT_PI);
76
77 // erfcx(-|x|) = 2 e^{x^2} - erfcx(|x|). Overflows to +inf below x ~ -26.6 in
78 // binary64, which is the true behaviour of the function and not a guard failure.
79 let neg = x.cmp_lt(V::ZERO);
80 if const { P::POLICY.avoid_branching } || thermite::unlikely(neg.any()) {
81 let refl = (ax * ax).exp_p::<P>();
82 y = neg.select(refl + refl - y, y);
83 }
84
85 if const { P::POLICY.check_overflow } {
86 // Both infinities need saying. At +inf the reciprocal is 0 but `Z` is
87 // `(L - inf) * 0`, i.e. NaN, which the Horner then spreads, and the limit is 0. At
88 // -inf the reflection is `inf - NaN` for the same reason, and the limit is +inf.
89 y = x.cmp_eq(V::INFINITY).select(V::ZERO, y);
90 y = x.cmp_eq(V::NEG_INFINITY).select(V::INFINITY, y);
91 }
92
93 y
94}
95
96/// [`erfcx_with`], with `N` and the table chosen by the precision policy.
97#[inline(always)]
98pub fn erfcx_internal<V, E, P>(x: V) -> V
99where
100 E: FloatElement + WeidemanTables,
101 V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
102 P: Policy,
103{
104 macro_rules! tier {
105 ($n:literal) => {
106 erfcx_with::<V, E, P, $n>(x, <E as Weideman<$n>>::L, &<E as Weideman<$n>>::A)
107 };
108 }
109
110 // Spelled out at each arm rather than bound to a `let`: a `const` block cannot
111 // capture a local, even one whose initializer is itself constant.
112 macro_rules! is {
113 ($n:literal) => {
114 const { weideman_n(P::POLICY.precision, <E as WeidemanTables>::MAX_N) <= $n }
115 };
116 }
117
118 if is!(8) {
119 tier!(8)
120 } else if is!(16) {
121 tier!(16)
122 } else if is!(24) {
123 tier!(24)
124 } else if is!(32) {
125 tier!(32)
126 } else {
127 tier!(40)
128 }
129}