thermite_special/specialized/generic/phi.rs
1//! The phi-functions of exponential integrators, `phi_N(z) = sum z^n/(n+N)!`, shared
2//! by every element type. `ps.rs`/`pd.rs` supply a compile-time series length, while
3//! the element-agnostic default iterates to the element's own epsilon.
4
5use thermite::{
6 LargeInt,
7 element::FloatElement,
8 math::{FloatConsts, policy::Policy, specialized::SpecializedTranscendentalMath},
9 prelude::*,
10};
11
12/// Series terms `phi_N` needs on `|z| < N` to converge to a relative `eps`.
13///
14/// The n-th term of `sum z^n/(n+N)!` relative to the leading `1/N!` is `z^n N!/(N+n)!`.
15/// At `z = N` every factor `N/(N+n)` is below one, so the terms fall monotonically and
16/// the first one under `eps` bounds the whole tail (the ratio there is small, so the
17/// tail is barely more than that term). Bounded input, so this is a compile-time count.
18pub const fn phi_series_terms(n: usize, eps: f64) -> usize {
19 let t = n as f64;
20 let mut term = 1.0;
21 let mut k = 0;
22 while term > eps {
23 k += 1;
24 term *= t / (n + k) as f64;
25 }
26 k
27}
28
29/// How many orders the runtime-order `phi` has its series length precomputed for. See
30/// [`phi_terms_table`].
31pub const PHI_TABLE_ORDERS: usize = 33;
32
33/// [`phi_series_terms`] for every order below [`PHI_TABLE_ORDERS`], capped at
34/// `max_iterations`: the table the f32/f64 runtime-order entries build in a `const` block
35/// per policy, so the term count is an index per call rather than a search.
36pub const fn phi_terms_table(eps: f64, max_iterations: usize) -> [usize; PHI_TABLE_ORDERS] {
37 let mut t = [0; PHI_TABLE_ORDERS];
38 let mut n = 0;
39 while n < PHI_TABLE_ORDERS {
40 let needed = phi_series_terms(n, eps);
41 t[n] = if needed < max_iterations {
42 needed
43 } else {
44 max_iterations
45 };
46 n += 1;
47 }
48 t
49}
50
51/// The runtime-order twin of [`phi_internal_n`]: the same two arms with `N` as a value. The
52/// `1/N!` prefactor and the per-term ratios are the same running products, so the two forms
53/// agree to the bit for the same `terms`.
54#[inline(always)]
55pub fn phi_internal<V, E, P, const ADAPTIVE: bool>(z: V, n: u32, terms: usize) -> V
56where
57 E: FloatElement,
58 V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
59 P: Policy,
60{
61 if n == 0 {
62 return V::exp::<P>(z);
63 }
64
65 if n == 1 {
66 let mut r = V::approx_div::<P>(V::exp_m1::<P>(z), z);
67
68 if const { P::POLICY.check_overflow } {
69 r = z.is_zero().select(V::ONE, r);
70 r = z.cmp_eq(V::INFINITY).select(V::INFINITY, r);
71 }
72
73 return r;
74 }
75
76 let n = n as usize;
77
78 let mut inv_fact = E::ONE;
79 let mut k = 2;
80 while k <= n {
81 inv_fact = inv_fact * E::from_ratio(1, k as LargeInt);
82 k += 1;
83 }
84
85 let near = z.abs().cmp_lt(V::splat(E::from_int(n as LargeInt)));
86
87 // Series arm.
88 let mut s = V::ZERO;
89 if const { P::POLICY.avoid_branching } || thermite::unlikely(near.any()) {
90 let tol = <V as FloatConsts>::EPSILON * V::HALF;
91 let mut term = V::splat(inv_fact);
92 s = term;
93 let mut k = 1;
94 while k <= terms {
95 term *= z * V::splat(E::from_ratio(1, (n + k) as LargeInt));
96 s += term;
97 if const { ADAPTIVE } && term.abs().cmp_le(tol * s.abs()).all() {
98 break;
99 }
100 k += 1;
101 }
102 }
103
104 // Recurrence arm.
105 let mut p = V::ZERO;
106 if const { P::POLICY.avoid_branching } || thermite::unlikely(!near.all()) {
107 let inv = V::ONE / z;
108 p = V::exp_m1::<P>(z) * inv;
109 let mut inv_kfact = E::ONE;
110 let mut k = 1;
111 while k < n {
112 p = (p - V::splat(inv_kfact)) * inv;
113 k += 1;
114 inv_kfact = inv_kfact * E::from_ratio(1, k as LargeInt);
115 }
116 }
117
118 let mut r = near.select(s, p);
119
120 if const { P::POLICY.check_overflow } {
121 r = z.cmp_eq(V::INFINITY).select(V::INFINITY, r);
122 }
123
124 r
125}
126
127/// `phi_N(z) = sum_{n>=0} z^n/(n+N)!`, the exponential-integrator functions.
128///
129/// `N = 0` is `exp` and `N = 1` is `expm1(z)/z`. Beyond that, two arms split at `|z| = N`:
130///
131/// * Below, the series, summed forward from `1/N!` with each term the previous times
132/// `z/(N+k)`. Every coefficient is one small ratio, so this stays exact for any element
133/// type. `terms` bounds the loop, and with `ADAPTIVE` it also stops as soon as the term
134/// it just added is under half an ulp of the sum, which is how an element whose precision
135/// is not known statically (`Compensated`) converges to its own epsilon.
136/// * Above, the recurrence `phi_{k+1} = (phi_k - 1/k!)/z` upward from
137/// `phi_1 = expm1(z)/z`. Each step subtracts a constant from something that is only
138/// just larger than it while `|z|` is small (that is the cancellation the series
139/// exists to avoid), but the amplification per step is `phi_k/(phi_k - 1/k!)`, which
140/// is bounded once `|z| >= k`. `|z| >= N` covers every step, and measured against
141/// mpmath the recurrence stays under 6 ulp for `N <= 8` in both f32 and f64. The same
142/// bound is why the series arm stops at `N`: its terms are monotone there, so the
143/// alternating negative side does not cancel either.
144///
145/// Both arms overflow gracefully: `expm1` saturates to `+inf` and each division by `z`
146/// leaves it there, and `-inf` gives `-1 * -0` and then a run of `+0`s, the limit. Only
147/// `+inf` itself, `inf * (1/inf)`, and the `0/0` of `N = 1` at the origin need patching.
148#[inline(always)]
149pub fn phi_internal_n<V, E, P, const N: usize, const ADAPTIVE: bool>(z: V, terms: usize) -> V
150where
151 E: FloatElement,
152 V: FloatVector<Element = E> + SpecializedTranscendentalMath<E>,
153 P: Policy,
154{
155 if const { N == 0 } {
156 return V::exp::<P>(z);
157 }
158
159 if const { N == 1 } {
160 let mut r = V::approx_div::<P>(V::exp_m1::<P>(z), z);
161
162 if const { P::POLICY.check_overflow } {
163 // 0/0 at the origin, where the limit is 1, and inf/inf at +inf. -inf needs
164 // nothing: expm1 gives -1, and -1 / -inf = 0 is already the limit. A
165 // large finite z overflows expm1 to inf, and inf/z is likewise right.
166 r = z.is_zero().select(V::ONE, r);
167 r = z.cmp_eq(V::INFINITY).select(V::INFINITY, r);
168 }
169
170 return r;
171 }
172
173 // 1/N! as a running product of small ratios, exact-ish for any element and never
174 // an integer overflow.
175 let mut inv_fact = E::ONE;
176 let mut k = 2;
177 while k <= N {
178 inv_fact = inv_fact * E::from_ratio(1, k as LargeInt);
179 k += 1;
180 }
181
182 let near = z.abs().cmp_lt(V::splat(E::from_int(N as LargeInt)));
183
184 // Series arm.
185 let mut s = V::ZERO;
186 if const { P::POLICY.avoid_branching } || thermite::unlikely(near.any()) {
187 // FloatConsts, not FloatVector: `Compensated`'s FloatVector::EPSILON is the
188 // single-width one, and its consts table carries the real 2^-105.
189 let tol = <V as FloatConsts>::EPSILON * V::HALF;
190 let mut term = V::splat(inv_fact);
191 s = term;
192 let mut k = 1;
193 while k <= terms {
194 term *= z * V::splat(E::from_ratio(1, (N + k) as LargeInt));
195 s += term;
196 if const { ADAPTIVE } && term.abs().cmp_le(tol * s.abs()).all() {
197 break;
198 }
199 k += 1;
200 }
201 }
202
203 // Recurrence arm.
204 let mut p = V::ZERO;
205 if const { P::POLICY.avoid_branching } || thermite::unlikely(!near.all()) {
206 let inv = V::ONE / z;
207 p = V::exp_m1::<P>(z) * inv;
208 let mut inv_kfact = E::ONE; // 1/k!
209 let mut k = 1;
210 while k < N {
211 p = (p - V::splat(inv_kfact)) * inv;
212 k += 1;
213 inv_kfact = inv_kfact * E::from_ratio(1, k as LargeInt);
214 }
215 }
216
217 let mut r = near.select(s, p);
218
219 if const { P::POLICY.check_overflow } {
220 r = z.cmp_eq(V::INFINITY).select(V::INFINITY, r);
221 }
222
223 r
224}