Skip to main content

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}
Last built: 2026-09-08 21:35:55 UTC