thermite_special/specialized/generic/zeta.rs
1//! The Riemann zeta function, as `$\zeta(s) - 1$` with `$\zeta$` built on top.
2//!
3//! # Which one is the primitive
4//!
5//! `$\zeta(s) \to 1$` fast: `$\zeta(40) - 1$` is about `$9.1\times10^{-13}$`, already far below
6//! the mantissa of `$\zeta$` itself, and by `$s = 80$` the complement is `8.3e-25`. So a caller
7//! who wants the complement cannot get it by subtracting: measured at `s = 80`, forming
8//! `zeta(s)` and taking away 1 is **100% wrong**, and by `s = 200` it returns a flat zero
9//! where the true value is `1e-61`.
10//!
11//! The Euler-Maclaurin sum below opens with the `$n = 1$` term, which _is_ that 1, so the
12//! complement comes from **omitting** it rather than cancelling it (exact, with no subtraction
13//! anywhere), and still carries digits at `s = 700` where the value is around `1e-211`. That
14//! makes [`zetac`](Self) the primitive here and `$\zeta = 1 + \zeta_c$` the derived form, the
15//! same relationship `exp_m1` has to `exp`.
16//!
17//! # Algorithm
18//!
19//! Euler-Maclaurin, truncated at [`N`] direct terms with [`bernoulli_terms`] correction terms:
20//!
21//! ```math
22//! \zeta(s) = \sum_{n=1}^{N-1} n^{-s} + \frac{N^{1-s}}{s-1} + \frac{N^{-s}}{2}
23//! + \sum_{k\ge1} \frac{B_{2k}}{(2k)!}\,(s)_{2k-1}\,N^{-(s+2k-1)}
24//! ```
25//!
26//! The usual alternative is a table of minimax rationals over five or six intervals in `s`
27//! (this is what Boost does, in about a thousand lines). That is excellent scalar code and the
28//! wrong shape for a vector unit, where selecting a coefficient _table_ per lane means either a
29//! gather or evaluating every interval and discarding all but one. Euler-Maclaurin is one
30//! straight-line expression for the whole positive axis instead.
31//!
32//! Borwein's accelerated eta series was the other candidate, and is the more famous one because
33//! it converges in the critical strip and over the complex plane. Measured, it needs **22 terms
34//! to match this at 10**, more than twice the transcendental calls for the same answer, so it
35//! lost on cost. If complex `s` is ever wanted, it becomes interesting again.
36//!
37//! # Four exponentials, not nine
38//!
39//! Every direct term is `$n^{-s} = 2^{-s\log_2 n}$` with `$\log_2 n$` a compile-time constant,
40//! which reads as one `exp2` per term. But the Dirichlet terms **factor over the primes**: with
41//! `$p_n = n^{-s}$` evaluated for `n` in 2, 3, 5, 7, the rest are products:
42//! `$p_4 = p_2^2$`, `$p_6 = p_2p_3$`, `$p_8 = p_2^3$`, `$p_9 = p_3^2$`, and
43//! `$N^{-s} = p_2p_5$` needs no call of its own. Four transcendentals and five multiplies cover
44//! all of `n = 2..10`, at measured accuracy indistinguishable from nine separate calls
45//! (4.89e-16 against 4.41e-16).
46//!
47//! The count is `$\pi(N)$`, the prime-counting function, not `N`, so raising `N` to tighten
48//! the critical strip is cheaper than it looks: `N = 16` costs six, `N = 20` costs eight.
49//!
50//! The correction sum needs **no transcendentals at all**. `$N^{-(s+2k-1)}$` is `$N^{-s}$` times
51//! a constant, and `$(s)_{2k-1}/(2k)!$` advances by a two-factor recurrence whose denominator is
52//! a compile-time integer, so the whole tail is one multiply-accumulate ladder over the shipped
53//! Bernoulli table.
54//!
55//! # Accuracy
56//!
57//! Against mpmath at 40 digits, worst relative error with `N = 10` and 8 correction terms:
58//! 4.4e-16 for `s` in `[1.5, 5]`, 4.3e-16 for `[5, 40]`, 2.3e-15 through the critical strip
59//! `[0.1, 0.9]`, and 4.6e-16 approaching the pole. The strip is the weak region, and `N` is the
60//! lever if it ever matters.
61//!
62//! Negative `s` is **not** reachable by adding terms. The expansion is asymptotic, and its
63//! error there gets _worse_ with larger `N` (measured 3.7e-9 at `N = 10`, 5.4e-8 at `N = 16`).
64//! It takes the functional equation instead, which lands at `$1 - s > 1$`, back in the region
65//! where the series is at its best.
66
67use thermite::{
68 const_splat,
69 element::FloatElement,
70 math::{
71 CoreMathWithPolicy as _, TranscendentalMathWithPolicy as _,
72 policy::{Policy, PrecisionPolicy},
73 specialized::SpecializedTranscendentalMath,
74 },
75 prelude::*,
76};
77
78use crate::specialized::SpecializedSpecialMath;
79use crate::tables::bernoulli::BernoulliNumbers;
80
81/// The truncation point of the direct sum. Ten is the knee: eight direct terms and six
82/// correction terms leave 2.4e-14, ten and eight reach 4.4e-16, and more of either buys nothing
83/// (12 and 8 measured 2.7e-16). Because the terms factor over the primes, the transcendental
84/// cost is `pi(10) = 4` rather than 9.
85#[allow(dead_code)] // named in the docs as the truncation point; the value is inlined below
86pub const N: usize = 10;
87
88/// Correction terms by precision tier. The dropped term bounds the error directly, and the
89/// series is convergent-then-asymptotic in this range, so the tiers are: 8 terms is 4.4e-16, 4
90/// is around 1e-11, and 2 is around 1e-7, which straddles f32's floor, where the whole tail is
91/// nearly free anyway.
92#[inline(always)]
93pub const fn bernoulli_terms(precision: PrecisionPolicy) -> usize {
94 match precision {
95 PrecisionPolicy::Worst => 2,
96 PrecisionPolicy::Medium => 4,
97 _ => 8,
98 }
99}
100
101/// Per-element constants: the base-2 logarithms of the primes under `N`. `log2(pi)` for
102/// the functional equation comes from `FloatConsts`. Declared for `f32`/`f64`. Add more as needed.
103pub trait ZetaConsts {
104 /// `log2(3)`, for `3^-s = exp2(-s log2 3)`.
105 const LOG2_3: Self;
106 /// `log2(5)`.
107 const LOG2_5: Self;
108 /// `log2(7)`.
109 const LOG2_7: Self;
110}
111
112impl ZetaConsts for f32 {
113 const LOG2_3: f32 = 1.5849624872207642;
114 const LOG2_5: f32 = 2.321928024291992;
115 const LOG2_7: f32 = 2.8073549270629883;
116}
117
118impl ZetaConsts for f64 {
119 const LOG2_3: f64 = 1.584962500721156;
120 const LOG2_5: f64 = 2.321928094887362;
121 const LOG2_7: f64 = 2.807354922057604;
122}
123
124/// `zeta(s) - 1` by Euler-Maclaurin for `s > 0`, and optionally `zeta'(s)` alongside it. The
125/// leading `n = 1` term is simply never added, which is what makes the complement exact rather
126/// than a cancellation. Since the two functions differ by a constant, one derivative
127/// serves both.
128///
129/// `DERIV` is a compile-time flag, so the whole derivative half folds away when it is not
130/// wanted. It shares every transcendental with the value: the `log n` weights are constants
131/// (`log n = log2 n * ln 2`, and the composite ones are sums of the prime ones), and the
132/// correction sum's derivative rides the same ladder with a second accumulator.
133///
134/// That second accumulator is not optional. Dropping the correction sum's own
135/// `d/ds (s)_{2k-1}` term (the tempting simplification, since the correction is already tiny)
136/// was measured at **8.9e-5** relative against **2.4e-15** for the full form. The tail is small
137/// but its derivative is not small in the same way.
138#[inline(always)]
139fn zetac_positive<P, E, V, const DERIV: bool>(s: V) -> (V, V)
140where
141 E: FloatElement + ZetaConsts + BernoulliNumbers,
142 V: FloatVector<Element = E> + SpecializedTranscendentalMath<E> + SpecializedSpecialMath<E>,
143 P: Policy,
144{
145 let neg_s = -s;
146
147 // The four primes below N. Everything else is a product of these.
148 let p2 = neg_s.exp2_p::<P>();
149 let p3 = (neg_s * V::splat(<E as ZetaConsts>::LOG2_3)).exp2_p::<P>();
150 let p5 = (neg_s * V::splat(<E as ZetaConsts>::LOG2_5)).exp2_p::<P>();
151 let p7 = (neg_s * V::splat(<E as ZetaConsts>::LOG2_7)).exp2_p::<P>();
152
153 let p4 = p2 * p2;
154 let p6 = p2 * p3;
155 let p8 = p4 * p2;
156 let p9 = p3 * p3;
157 let n_s = p2 * p5; // 10^-s, and the fifth call it saves
158
159 // n = 2..9, ordered small-to-large so the accumulation adds like magnitudes together.
160 let direct = ((p9 + p8) + (p7 + p6)) + ((p5 + p4) + (p3 + p2));
161
162 // N^{1-s}/(s-1) + N^-s/2, sharing the single N^-s.
163 let ten: V = const_splat!(int <E>: 10);
164 let boundary = n_s * (ten.approx_div_p::<P>(s - V::ONE) + V::HALF);
165
166 // The correction sum. `u` carries (s)_{2k-1}/(2k)! * N^-(s+2k-1) and advances by
167 // u_{k+1} = u_k (s + 2k - 1)(s + 2k) / ((2k+1)(2k+2) N^2),
168 // whose denominator is a compile-time integer, so no transcendental appears here at all.
169 let recur: [E; 8] = [
170 <E as FloatElement>::ConstRatio::<1, 1200>::VALUE,
171 <E as FloatElement>::ConstRatio::<1, 3000>::VALUE,
172 <E as FloatElement>::ConstRatio::<1, 5600>::VALUE,
173 <E as FloatElement>::ConstRatio::<1, 9000>::VALUE,
174 <E as FloatElement>::ConstRatio::<1, 13200>::VALUE,
175 <E as FloatElement>::ConstRatio::<1, 18200>::VALUE,
176 <E as FloatElement>::ConstRatio::<1, 24000>::VALUE,
177 <E as FloatElement>::ConstRatio::<1, 30600>::VALUE,
178 ];
179 let terms = const { bernoulli_terms(P::POLICY.precision) };
180
181 // u_1 = (s)_1/2! * N^-(s+1) = s N^-s / 20, and its derivative.
182 let ln_n = V::LN_10;
183 let twentieth: V = const_splat!(ratio <E>: 1 / 20);
184 let mut u = s * n_s * twentieth;
185 let mut du = (n_s - s * ln_n * n_s) * twentieth;
186 let mut a = s + V::ONE; // s + 2k - 1 at k = 1
187 let mut tail = V::ZERO;
188 let mut dtail = V::ZERO;
189
190 let mut k = 0;
191 while k < terms {
192 V::_loop_hint();
193
194 let b = V::splat(E::B2N[k]);
195 tail = u.mul_adde(b, tail);
196 if const { DERIV } {
197 dtail = du.mul_adde(b, dtail);
198 }
199
200 // u_{k+1} = u_k a(a+1) r_k, so du_{k+1} = [du_k a(a+1) + u_k (2a+1)] r_k. The r_k are
201 // pure constants (every bit of the s-dependence lives in u_1), so nothing else enters.
202 let step = a * (a + V::ONE);
203 let r = V::splat(recur[k]);
204 if const { DERIV } {
205 du = du.mul_adde(step, u * (a + a + V::ONE)) * r;
206 }
207 u *= step * r;
208 a += V::TWO;
209 k += 1;
210 }
211
212 let value = (direct + boundary) + tail;
213
214 let deriv = if const { DERIV } {
215 // d/ds n^-s = -(ln n) n^-s. Every log is a constant, and the composite ones are sums of
216 // the prime ones: ln 4 = 2 ln 2, ln 6 = ln 2 + ln 3, and so on.
217 let l2 = V::LN_2;
218 let l3 = V::splat(<E as ZetaConsts>::LOG2_3) * l2;
219 let l5 = V::splat(<E as ZetaConsts>::LOG2_5) * l2;
220 let l7 = V::splat(<E as ZetaConsts>::LOG2_7) * l2;
221
222 let d_direct = -((l3 * p3 + l2 * p2)
223 + ((l2 + l2) * p4 + l5 * p5)
224 + ((l2 + l3) * p6 + l7 * p7)
225 + ((l2 + l2 + l2) * p8 + (l3 + l3) * p9));
226
227 // d/ds [N^-s (N/(s-1) + 1/2)] = -ln(N) * boundary - N^-s N/(s-1)^2.
228 let sm1 = s - V::ONE;
229 let d_boundary = -(ln_n * boundary) - n_s * ten.approx_div_p::<P>(sm1 * sm1);
230
231 (d_direct + d_boundary) + dtail
232 } else {
233 V::ZERO
234 };
235
236 (value, deriv)
237}
238
239/// `zeta(s)` (`ZETAC = false`) or `zeta(s) - 1` (`ZETAC = true`), and its derivative when
240/// `DERIV` is set. `zeta` and `zetac` differ by a constant, so the one derivative serves both.
241#[inline(always)]
242pub fn zeta_core<P, E, V, const ZETAC: bool, const DERIV: bool>(s: V) -> (V, V)
243where
244 E: FloatElement + ZetaConsts + BernoulliNumbers,
245 V: FloatVector<Element = E> + SpecializedTranscendentalMath<E> + SpecializedSpecialMath<E>,
246 P: Policy,
247{
248 // Negative arguments reflect. The expansion is asymptotic, so this is not a matter of
249 // taking more terms. Its error there grows with N. The functional equation maps s < 0 to
250 // 1 - s > 1, which is where the series is at its best.
251 let reflect = s.cmp_lt(V::ZERO);
252 let arg = reflect.select(V::ONE - s, s);
253
254 let (zc, dz_arg) = zetac_positive::<P, E, V, DERIV>(arg);
255 let full = V::ONE + zc;
256
257 let mut result = if const { ZETAC } { zc } else { full };
258 // The reflected lanes hold d/d(arg), and arg = 1 - s there, so the chain rule's -1 is
259 // applied inside the reflected branch rather than here.
260 let mut deriv = dz_arg;
261
262 if const { P::POLICY.avoid_branching } || thermite::unlikely(reflect.any()) {
263 // zeta(s) = chi(s) zeta(1-s), chi(s) = 2^s pi^(s-1) sin(pi s/2) Gamma(1-s).
264 //
265 // 2^s pi^(s-1) is one exp2, not two powers: 2^(s + (s-1) log2 pi).
266 let base = (s - V::ONE).mul_adde(V::LOG2_PI, s).exp2_p::<P>()
267 * <V as SpecializedSpecialMath<E>>::tgamma::<P>(V::ONE - s);
268 let (sin_h, cos_h) = s
269 .scale(<E as FloatElement>::ConstRatio::<1, 2>::VALUE)
270 .sincos_pi_p::<P>();
271
272 let reflected = base * sin_h * full;
273
274 // Away from the positive axis zeta is nowhere near 1, so taking the complement here is
275 // an ordinary subtraction rather than the cancellation `zetac` exists to avoid.
276 let out = if const { ZETAC } { reflected - V::ONE } else { reflected };
277 result = reflect.select(out, result);
278
279 if const { DERIV } {
280 // Differentiating chi(s) zeta(1-s) gives
281 // zeta'(s) = chi(s)[ln 2 + ln pi + (pi/2)cot(pi s/2) - psi(1-s)] zeta(1-s)
282 // - chi(s) zeta'(1-s).
283 //
284 // Written that way the cotangent blows up at every even negative integer: exactly
285 // the trivial zeros, where zeta(s) is 0, so the product is 0 * inf. Folding chi's
286 // own sine into it instead leaves `base * (pi/2) * cos(pi s/2)`, which is finite
287 // there and needs no guard.
288 let logs = V::LN_2 + V::LN_PI - <V as SpecializedSpecialMath<E>>::digamma::<P>(V::ONE - s);
289 // `dz_arg` is zeta'(1-s), the derivative with respect to its own argument. The
290 // chain rule's d(1-s)/ds = -1 is what makes this term subtract.
291 let d_reflected = base * ((logs * sin_h + V::FRAC_PI_2 * cos_h) * full - sin_h * dz_arg);
292 deriv = reflect.select(d_reflected, deriv);
293 }
294 }
295
296 if const { P::POLICY.check_overflow } {
297 // The simple pole. The boundary term already divides by s - 1 and produces the correct
298 // infinity from the right. This pins the exact hit, where the two-sided limit does not
299 // exist and the sign would otherwise come from the zero's.
300 let pole = s.cmp_eq(V::ONE);
301 result = pole.select(V::INFINITY, result);
302 if const { DERIV } {
303 deriv = pole.select(V::NEG_INFINITY, deriv);
304 }
305 }
306
307 (result, deriv)
308}
309
310/// `zeta(s)` (`ZETAC = false`) or `zeta(s) - 1` (`ZETAC = true`), for real `s`.
311#[inline(always)]
312pub fn zeta_impl<P, E, V, const ZETAC: bool>(s: V) -> V
313where
314 E: FloatElement + ZetaConsts + BernoulliNumbers,
315 V: FloatVector<Element = E> + SpecializedTranscendentalMath<E> + SpecializedSpecialMath<E>,
316 P: Policy,
317{
318 zeta_core::<P, E, V, ZETAC, false>(s).0
319}