Skip to main content

rustymath_core/
exp.rs

1//! e^x and e^x - 1. The f64 reduction and the form of the final step come from SLEEF's `xexp`, and the f32 polynomial of `exp` and the f32 split of ln 2 from Arm Optimized Routines' AdvSIMD `expf`. The other polynomials are minimax fits. See `THIRD-PARTY.md`.
2use crate::{dd::Double, float::{Native, horner, sealed::Core}};
3
4/// The coefficients of h(r) = (e^r - 1)/r, highest degree first, for |r| up to about ln 2 / 2. `exp` needs h only to within its error in e^r = 1 + r h, which shrinks with r.
5const EXP_64: [f64; 10] = [
6    2.749197269771299e-07, 2.7639186521124947e-06, 2.4801900316905035e-05, 0.00019841173089486715, 0.0013888888529025865, 0.008333333382940285,
7    0.04166666666820299, 0.16666666666564642, 0.4999999999999798, 1.0000000000000056,
8];
9const EXP_32: [f64; 5] = [0.008247390389442444, 0.04189976677298546, 0.16668395698070526, 0.4999912679195404, 0.9999994039535522];
10/// The same, to about 2^-28 relative in e^r, for f32 and f16 results computed in f64. At that accuracy, a power whose exact result is a narrower float comes out exactly.
11const EXP_NARROW: [f64; 6] = [0.0013941108433972674, 0.008375126398153335, 0.04166635289677515, 0.16666415514653277, 0.5000000047117757, 1.000000037716214];
12/// h to within a fraction of an ulp relative to h itself, for `expm1`, whose result near 0 is r h.
13const H_64: [f64; 11] = [
14    2.5052099500253996e-08, 2.762626280199822e-07, 2.7557414780064832e-06, 2.4801504602137864e-05, 0.00019841269707477827, 0.001388888893225886,
15    0.008333333333397829, 0.041666666666573905, 0.16666666666666563, 0.5000000000000006, 1.0,
16];
17const H_32: [f64; 6] = [0.0013888871762901545, 0.00836906861513853, 0.041667137295007706, 0.16666506230831146, 0.4999999701976776, 1.0];
18
19/// x limited to ±l. Comparisons with NaN fail, so NaN passes through.
20#[inline]
21pub(crate) fn clamp<R: Native>(x: R, l: R) -> R { if x < -l { -l } else if x > l { l } else { x } }
22
23/// n and r with x = n ln 2 + r, n whole and |r| at most about ln 2 / 2, for |x| up to `limit`.
24#[inline]
25fn reduce<R: Native, const FUSED: bool>(x: R) -> (R, R) { reduce_with(&crate::recipe::NativeOps::<FUSED>::DEFAULT, x) }
26
27/// e^(x + lo) × 2^k, for a lo far below x and a whole k. lo joins the reduced argument, except where `clamp` changes x and the result is 0 or ∞ whatever lo is. Scaling by 2^k inside keeps e^x/2 finite where e^x would overflow. A caller passes -0 for a part it lacks. The compiler drops such a part, because adding -0 changes no value.
28#[inline]
29fn exp_parts<R: Native, const FUSED: bool>(x: R, lo: R, k: R) -> R { parts_with(&crate::recipe::NativeOps::<FUSED>::DEFAULT, x, lo, k) }
30
31#[inline]
32pub(crate) fn exp<R: Native, const FUSED: bool>(x: R) -> R { exp_parts::<R, FUSED>(x, R::neg_zero(), R::neg_zero()) }
33
34/// [`exp`](crate::exp) as a function object.
35#[derive(Clone, Copy, Debug)]
36pub struct Exp;
37
38/// e^x × 2^k, for a whole k.
39#[inline]
40pub(crate) fn exp_times<R: Native, const FUSED: bool>(x: R, k: R) -> R { exp_parts::<R, FUSED>(x, R::neg_zero(), k) }
41
42#[inline]
43pub(crate) fn expm1<R: Native, const FUSED: bool>(x: R) -> R { expm1_times::<R, FUSED>(x, R::zero()) }
44
45/// [`expm1`](crate::expm1) as a function object.
46#[derive(Clone, Copy, Debug)]
47pub struct Expm1;
48
49/// (e^x - 1) × 2^k, for a whole k. Below -40 at f64 and -18 at f32, e^x - 1 rounds to -1. n can exceed the greatest exponent by one while the result is still finite, so 2^n splits into 2^m and 2^(n-m), with m no greater than that exponent.
50#[inline]
51pub(crate) fn expm1_times<R: Native, const FUSED: bool>(x: R, k: R) -> R { minus_one_formula(&crate::recipe::NativeOps::<FUSED>::DEFAULT, x, k) }
52
53/// e^(hi + lo) for any double word. It renormalizes the word first, because `exp_parts` needs `lo` far below `hi`.
54#[inline]
55pub(crate) fn exp_plus<R: Native, const FUSED: bool>(x: Double<R>) -> R { let x = Double::sum(x.hi, x.lo); exp_parts::<R, FUSED>(x.hi, x.lo, R::neg_zero()) }
56
57/// e^x in f64 for an f32 or f16 result. Beyond 110 in magnitude, e^x is 0 or ∞ at those widths. x stops there. One power of two then scales the result.
58#[inline]
59pub(crate) fn exp_narrow<const FUSED: bool>(x: f64) -> f64 {
60    let (n, r) = reduce::<f64, FUSED>(clamp(x, 110.0));
61    r.mla::<FUSED>(horner::<f64, FUSED, 6>(r, EXP_NARROW), 1.0) * f64::pow2(n + f64::SHIFT)
62}
63
64#[inline(always)]
65fn reduce_with<F: crate::Float, O: crate::recipe::Ops<F> + ?Sized>(o: &O, x: O::V) -> (O::V, O::V) {
66    let shift = crate::recipe::rounding_shift(o);
67    let n = o.subtract(o.multiply_add(x, o.number(F::narrow(std::f64::consts::LOG2_E)), shift), shift);
68    let (hi, lo) = crate::recipe::ln2(o);
69    (n, o.multiply_add(n, o.negate(lo), o.multiply_add(n, o.negate(hi), x)))
70}
71
72#[inline(always)]
73fn reduced_with<F: crate::Float, O: crate::recipe::Ops<F> + ?Sized>(o: &O, r: O::V) -> O::V {
74    let h = crate::recipe::horner(o, r, if o.width() == crate::buffer::FloatWidth::F64 { &EXP_64 } else { &EXP_32 });
75    o.multiply_add(r, h, o.number(F::one()))
76}
77
78#[inline(always)]
79fn clamp_with<F: crate::Float, O: crate::recipe::Ops<F> + ?Sized>(o: &O, x: O::V) -> O::V {
80    let limit = o.number(F::narrow(if o.width() == crate::buffer::FloatWidth::F64 { 1100.0 } else { 150.0 }));
81    let low = o.negate(limit);
82    o.select(o.less(x, low), low, o.select(o.less(limit, x), limit, x))
83}
84
85#[inline(always)]
86fn parts_with<F: crate::Float, O: crate::recipe::Ops<F> + ?Sized>(o: &O, x: O::V, lo: O::V, k: O::V) -> O::V {
87    let c = clamp_with(o, x);
88    let (n, r) = reduce_with(o, c);
89    let r = o.select(o.equal(c, x), o.add(r, lo), r);
90    crate::recipe::ldexp(o, reduced_with(o, r), o.add(n, k))
91}
92
93#[inline(always)]
94pub(crate) fn formula<F: crate::Float, O: crate::recipe::Ops<F> + ?Sized>(o: &O, x: O::V) -> O::V {
95    let zero = o.number(-F::zero());
96    parts_with(o, x, zero, zero)
97}
98
99#[inline(always)]
100fn minus_one_with<F: crate::Float, O: crate::recipe::Ops<F> + ?Sized>(o: &O, r: O::V, scale: O::V) -> O::V {
101    let h = crate::recipe::horner(o, r, if o.width() == crate::buffer::FloatWidth::F64 { &H_64 } else { &H_32 });
102    o.multiply_add(o.multiply(r, h), scale, o.subtract(scale, o.number(F::one())))
103}
104
105#[inline]
106pub(crate) fn scaled_formula<F: crate::Float, O: crate::recipe::Ops<F> + ?Sized>(o: &O, x: O::V, scale: O::V) -> O::V {
107    parts_with(o, x, o.number(-F::zero()), scale)
108}
109
110#[inline]
111pub(crate) fn minus_one_formula<F: crate::Float, O: crate::recipe::Ops<F> + ?Sized>(o: &O, x: O::V, scale: O::V) -> O::V {
112    let (low, top) = if o.width() == crate::buffer::FloatWidth::F64 { (-40.0, 1023.0) } else { (-18.0, 127.0) };
113    let low = o.number(F::narrow(low));
114    let (n, r) = reduce_with(o, clamp_with(o, o.select(o.less(x, low), low, x)));
115    let top = o.number(F::narrow(top));
116    let m = o.select(o.less(top, n), top, n);
117    let shift = crate::recipe::rounding_shift(o);
118    let first = minus_one_with(o, r, crate::recipe::pow2(o, o.add(m, shift)));
119    o.multiply(first, crate::recipe::pow2(o, o.add(o.add(o.subtract(n, m), scale), shift)))
120}