thermite_special/specialized/generic/polylog.rs
1//! The polylogarithm `$\mathrm{Li}_s(z) = \sum_{k \ge 1} z^k / k^s$` of a real argument, at
2//! a scalar real order.
3//!
4//! Two things live here: the **order plan** (every order-dependent coefficient, computed
5//! once per call in the element type through the scalar math surface, and shared with
6//! thermite-complex's kernel) and the **real kernel**, which returns the real part of the
7//! principal value for a real vector. Nothing in the real kernel is a complex number.
8//! Where the mathematics is complex (the negative axis, the cut `$z > 1$`, the far-field
9//! roots) the real part is taken analytically: the polynomial parts by a Goertzel
10//! recurrence, the transcendental parts in polar form.
11//!
12//! # Regions
13//!
14//! With `$\mu = \ln z$` (principal: `$\ln|z| + i\pi$` on the negative axis) and
15//! `$t = |\mu| / 2\pi$`, per lane:
16//!
17//! 1. **Defining series** where `$2\pi|z| < |\mu|$` (Roughan's rule, which crosses the
18//! positive axis at `z = 0.2323` and the negative axis at `z = -0.5113`), at a fixed
19//! term count set by the worst ratio `0.5113`.
20//! 2. **Unity series** where `$t \le 0.512$` (Wood 9.3, Crandall 1.4, Roughan Series 2):
21//! ```math
22//! \mathrm{Li}_s(z) = \Gamma(1-s)(-\mu)^{s-1} + \sum_{k \ge 0} \zeta(s-k)\,\frac{\mu^k}{k!}
23//! ```
24//! whose tail falls like `$(|\mu|/2\pi)^k$`. For `$s = n + \varepsilon$` with `$n \ge 1$`
25//! the `$k = n-1$` term and the `$\Gamma$` term each have a pole that the other cancels.
26//! They are fused **algebraically** into `$\mu^{n-1} Q_{n-1}(L, \varepsilon)/(n-1)!$`
27//! with `$L = \ln(-\mu)$` and
28//! ```math
29//! Q_m(L, \varepsilon) = \Big[\zeta(1+\varepsilon) - \tfrac{1}{\varepsilon}\Big]
30//! + \Big[(-1)^m m!\,\Gamma(-m-\varepsilon) + \tfrac{1}{\varepsilon}\Big] e^{\varepsilon L}
31//! - \frac{e^{\varepsilon L} - 1}{\varepsilon}.
32//! ```
33//! Both brackets are scalars, finite and smooth for every `$\varepsilon$` (the second is
34//! `$-\mathrm{expm1}(u)/\varepsilon$` with
35//! `$u = \ln\Gamma(1-\varepsilon) - \sum_{k \le m}\ln(1 + \varepsilon/k)$`), so there is
36//! no near-integer threshold and no Taylor arm. Roughan's Series 3 and its `1e-3`
37//! switch are what this replaces. At `$\varepsilon = 0$` it collapses to Wood 9.5's
38//! `$H_m - L$` exactly, and every coefficient of the integer case is a table read.
39//! 3. **Far field**, otherwise. Integer order takes the inversion formula (Crandall 1.3),
40//! `$\mathrm{Li}_n(z) = -(-1)^n \mathrm{Li}_n(1/z) - \frac{(2\pi i)^n}{n!} B_n\!\big(\tfrac{\mu}{2\pi i}\big) - \sigma(z)\,\frac{2\pi i\,\mu^{n-1}}{(n-1)!}$`,
41//! with `$\mathrm{Li}_n(1/z)$` from the defining series. The step term is imaginary and
42//! drops out of a real part. Real order takes Wood's m-th-root identity
43//! `$\mathrm{Li}_s(z) = m^{s-1}\sum_{j} \mathrm{Li}_s(z^{1/m}\,e^{2\pi i j/m})$`
44//! (Roughan 2026 Section 4.6): `m` is chosen so every root's `$\mu_j$` satisfies
45//! `$|\mu_j| \le 1.2\pi$`, the coefficient sweep is shared across the roots, each root
46//! runs region 2 (the fused form included, so this arm serves near-integer orders too),
47//! and for a real argument the roots come in conjugate pairs with equal real parts, so
48//! only half of them are evaluated. Negative integer orders run regions 1 and 2 as they
49//! stand and reflect `$\mathrm{Li}_{-p}(z) = -(-1)^p \mathrm{Li}_{-p}(1/z)$` (Wood 10.3) in
50//! the far field only.
51//! 4. `$\mathrm{Li}_1 = -\ln(1-z)$` and `$\mathrm{Li}_0 = z/(1-z)$` are closed forms.
52//!
53//! Every arm is fixed-length, so a packet pays the count once rather than its worst lane's
54//! convergence. The counts scale with the policy's precision tier.
55//!
56//! # Real parts without complex numbers
57//!
58//! A real polynomial at `$x = a + ib$` divides by `$(t-x)(t-\bar x) = t^2 - 2a\,t + |x|^2$`.
59//! The Goertzel recurrence `$B_k = c_k + 2a B_{k+1} - |x|^2 B_{k+2}$` leaves
60//! `$\mathrm{Re}\,P(x) = B_0 - a B_1$` and `$\mathrm{Im}\,P(x) = b B_1$`, two real FMAs per
61//! term. The lead terms use `$r = |\mu|$`, `$\theta = \arg(-\mu)$`, `$\varphi = \arg\mu$`:
62//! `$\mathrm{Re}\,\Gamma(1-s)(-\mu)^{s-1} = \Gamma(1-s)\,r^{s-1}\cos((s-1)\theta)$`, and for
63//! the fused term `$\mathrm{Re}[\mu^m Q] = r^m(\cos m\varphi\,\mathrm{Re}\,Q - \sin m\varphi\,\mathrm{Im}\,Q)$`
64//! with `$e^{\varepsilon L} = r^\varepsilon e^{i\varepsilon\theta}$` and the difference
65//! quotient spelled `$\ln r\,\varphi_1(\varepsilon \ln r)\cos\varepsilon\theta - \mathrm{versin}(\varepsilon\theta)/\varepsilon$`
66//! so that `$\varepsilon \to 0$` is exact. Angles are carried as multiples of `$\pi$` and go
67//! through the `_pi` trig, which is exact on the axes. On the cut near `$z = 1$` the lead
68//! term's imaginary part is enormous and a radian phase error leaks it into the real part.
69//! All verified against mpmath before being written (scratch `goertzel.py`, 2026-09-02).
70
71use thermite::{
72 LargeInt,
73 element::{FloatElement, FloatElementWithBits, SignedElement, SignedIntegerElement},
74 math::{
75 RealMathWithPolicy, ScalarMathWithPolicy,
76 policy::{Policy, PrecisionPolicy},
77 },
78 prelude::*,
79};
80use thermite::{const_element, const_splat};
81
82use crate::polylog::PolylogOrder;
83use crate::tables::bernoulli::BernoulliNumbers;
84use crate::tables::polylog::PolylogConsts;
85
86/// Longest series any arm runs: binary64 at the root arm's ratio `0.6` needs 74 terms.
87/// Also caps `|n|` for the inversion polynomial.
88pub const KMAX: usize = 80;
89
90/// Root-count cap. `|z| = 1e308` asks for 341.
91pub const MMAX: usize = 512;
92
93/// Unity-series boundary on `|mu| / 2pi`. Roughan's 0.512 rather than 0.5: the extra sliver
94/// is what closes the gap the series-1 rule leaves near the negative axis.
95#[inline(always)]
96pub fn t1<E: FloatElement>() -> E {
97 const_element!(ratio <E>: 512 / 1000)
98}
99
100/// `-ln 0.5113`, the worst ratio the defining series meets (the negative-axis crossing).
101#[inline(always)]
102fn ln_inv_series_ratio<E: FloatElement>() -> E {
103 const_element!(ratio <E>: 6708 / 10000)
104}
105
106/// `-ln 0.512` and `-ln 0.6`: the unity series' ratio in region 2 and in the root arm
107/// (`alpha / 2` with Roughan's `alpha = 1.2`).
108#[inline(always)]
109fn ln_inv_unity_ratio<E: FloatElement>(root_lanes: bool) -> E {
110 if root_lanes {
111 const_element!(ratio <E>: 5108 / 10000)
112 } else {
113 const_element!(ratio <E>: 6694 / 10000)
114 }
115}
116
117/// Mantissa bits the policy's precision tier asks the series to reach.
118#[inline(always)]
119fn effective_bits<E: FloatElementWithBits>(precision: PrecisionPolicy) -> u32 {
120 let m = E::MANTISSA_BITS + 1;
121 match precision {
122 PrecisionPolicy::Worst => m / 2,
123 PrecisionPolicy::Medium => m * 3 / 4,
124 _ => m,
125 }
126}
127
128#[inline(always)]
129fn int<E: FloatElement>(k: usize) -> E {
130 E::from_int(k as LargeInt)
131}
132
133/// Terms for a tail `r^k k^-s` (`ln_inv_ratio = -ln r`) to fall below `2^-bits`: the
134/// smallest `k` with `k ln(1/r) + s ln k >= bits ln 2`, plus two. Non-negative orders drop
135/// the helping `s ln k` term and take the plain geometric count, but negative orders grow
136/// before they decay (`s = -3.7`, `z = -1/2` needs 77 terms, not 57) and keep it. The
137/// search is a comparison loop in `E`, so no count is ever converted out of a float.
138#[inline(always)]
139fn terms<P: Policy, E>(bits: u32, ln_inv_ratio: E, s: E) -> usize
140where
141 E: FloatElement + ScalarMathWithPolicy,
142{
143 let target = E::from_int(bits as LargeInt) * E::LN_2;
144 let mut k = 1usize;
145 if s < E::ZERO {
146 while k < KMAX && int::<E>(k) * ln_inv_ratio + s * int::<E>(k).scalar_ln_p::<P>() < target {
147 k += 1;
148 }
149 } else {
150 while k < KMAX && int::<E>(k) * ln_inv_ratio < target {
151 k += 1;
152 }
153 }
154 (k + 2).min(KMAX)
155}
156
157/// `zeta(n)` at an integer: the table for `n >= 2` (exactly 1 past it), `-1/2` at zero,
158/// `-B_{j+1}/(j+1)` at `-j` (zero for even `j`).
159///
160/// Past the Bernoulli table it returns **zero**, not infinity: the table ends where
161/// `B_{2n}` overflows the format, and every use here divides by a `k!` that overflowed
162/// earlier still, so the true coefficient is far below the format's precision and the
163/// honest value of `inf/inf` is 0, not NaN. Reached in binary32 from `k = 58`.
164#[inline(always)]
165pub fn zeta_int<E: FloatElement + PolylogConsts + BernoulliNumbers>(n: isize) -> E {
166 if n >= 2 {
167 return E::ZETA_INT.get((n - 2) as usize).copied().unwrap_or(E::ONE);
168 }
169 if n == 1 {
170 return E::ORDER_MAX;
171 }
172 if n == 0 {
173 return -const_element!(ratio <E>: 1 / 2);
174 }
175 let j = (-n) as usize;
176 if j.is_multiple_of(2) {
177 return E::ZERO;
178 }
179 // B_{j+1} with j+1 even: table entry i holds B_{2i+2}.
180 match E::B2N.get(j.div_ceil(2) - 1) {
181 Some(&b) => -b / int::<E>(j + 1),
182 None => E::ZERO,
183 }
184}
185
186/// Finite and not NaN (a NaN fails the comparison on its own).
187#[inline(always)]
188fn finite<E: FloatElement>(x: E) -> bool {
189 SignedElement::abs(x) < E::ORDER_MAX
190}
191
192/// The number of m-th roots the far field needs when the widest lane's `Re ln z` is
193/// `re_mu_max`: the smallest `m >= 2` with `Re mu / m <= sqrt(alpha^2 - 1) pi = 2.0839`
194/// (`alpha = 1.2`), capped. The identity holds for any `m`, so the widest lane sets it for
195/// the packet. A comparison loop, so no count is converted out of a float.
196#[inline(always)]
197pub fn root_count<E: FloatElement>(re_mu_max: E) -> usize {
198 let step = const_element!(ratio <E>: 20839 / 10000);
199 let mut m = 2usize;
200 while m < MMAX && int::<E>(m) * step < re_mu_max {
201 m += 1;
202 }
203 m
204}
205
206/// Everything about the order, computed once per call in the element type. Shared with
207/// the complex kernel in thermite-complex. Not a stable surface.
208#[doc(hidden)]
209pub struct PolylogPlan<E> {
210 pub s: E,
211 /// `round(s)`. The fused slot is `m = n - 1` when `n >= 1`.
212 pub n: isize,
213 pub eps: E,
214 pub integer: bool,
215 /// Integer order whose inversion coefficients are representable. False past `n = 79`
216 /// (binary64) / `n = 34` (binary32), where the far field answers NaN.
217 pub inversion: bool,
218 pub fused: bool,
219 pub m: usize,
220 /// Defining-series length, and the one used on `1/z` in the inversion arm.
221 pub k1: usize,
222 pub k_inv: usize,
223 /// Unity-series length actually swept into `c`.
224 pub k2: usize,
225 /// `d[k-1] = k^-s`.
226 pub d: [E; KMAX],
227 /// `c[k] = zeta(s-k)/k!`, with `c[m] = 0` when fused.
228 pub c: [E; KMAX],
229 /// `ln m!`: the fused lead is `exp(m ln mu - ln m!) Q`, never `mu^m / m!`.
230 pub ln_fact_m: E,
231 /// The two scalar brackets of `Q_m`. For integer order `b1 = H_m` and `b2 = 0`.
232 pub b1: E,
233 pub b2: E,
234 /// `Gamma(1-s)`, for the unfused lead term (`n <= 0`).
235 pub gamma_1ms: E,
236 /// `Li_s(1)`.
237 pub at_one: E,
238 /// Inversion arm: coefficient of `x^j` in `B_n(x)`, the `(2 pi i)^n / n!` factor as
239 /// `(re, im)`, and `2 pi / (n-1)!` for the step term.
240 pub bp: [E; KMAX],
241 pub bp_scale: (E, E),
242 /// Read only by the complex kernel, as a real part never sees the step term.
243 #[allow(dead_code)]
244 pub sigma_scale: E,
245}
246
247/// The element bounds the plan's scalar precompute needs.
248pub trait PolylogElement:
249 FloatElementWithBits + PolylogConsts + BernoulliNumbers + ScalarMathWithPolicy + crate::ScalarSpecialMathWithPolicy
250{
251}
252impl<E> PolylogElement for E where
253 E: FloatElementWithBits
254 + PolylogConsts
255 + BernoulliNumbers
256 + ScalarMathWithPolicy
257 + crate::ScalarSpecialMathWithPolicy
258{
259}
260
261impl<E: PolylogElement> PolylogPlan<E> {
262 /// `s = n + eps` with `n >= 1`: the brackets of `Q_{n-1}`.
263 #[inline(always)]
264 fn brackets<P: Policy>(m: usize, eps: E) -> (E, E) {
265 let mut h_m = E::ZERO;
266 for k in 1..=m {
267 h_m = h_m + E::ONE / int::<E>(k);
268 }
269 if eps == E::ZERO {
270 return (h_m, E::ZERO);
271 }
272 let small = SignedElement::abs(eps) <= const_element!(ratio <E>: 1 / 10);
273
274 // b1 = zeta(1+eps) - 1/eps. The Laurent remainder is entire, so the Stieltjes series
275 // converges everywhere, and is used where the direct difference would cancel.
276 let b1 = if small {
277 let mut acc = E::ZERO;
278 let mut i = E::STIELTJES.len();
279 while i > 0 {
280 i -= 1;
281 // sum (-1)^k gamma_k eps^k / k!, Horner in eps with the 1/k! folded in.
282 acc = E::STIELTJES[i] - acc * eps / int::<E>(i + 1);
283 }
284 acc
285 } else {
286 (E::ONE + eps).scalar_zeta_p::<P>() - E::ONE / eps
287 };
288
289 // b2 = -expm1(u)/eps with u = lgamma(1-eps) - sum_{k<=m} log1p(eps/k). Small eps takes
290 // u's own series, u = eps (gamma - H_m) + sum_{j>=2} eps^j [zeta(j) + (-1)^j H_{m,j}] / j,
291 // because lgamma near 1 and log1p do not hold the relative accuracy u/eps needs.
292 let u = if small {
293 let mut acc = E::ZERO;
294 let mut j = 18usize;
295 while j >= 2 {
296 // H_{m,j} = sum_{k<=m} k^-j
297 let mut hmj = E::ZERO;
298 for k in 1..=m {
299 let r = E::ONE / int::<E>(k);
300 let mut p = r;
301 for _ in 1..j {
302 p = p * r;
303 }
304 hmj = hmj + p;
305 }
306 let zj = zeta_int::<E>(j as isize);
307 let coeff = if j.is_multiple_of(2) { zj + hmj } else { zj - hmj } / int::<E>(j);
308 acc = coeff + acc * eps;
309 j -= 1;
310 }
311 eps * (E::STIELTJES[0] - h_m + acc * eps)
312 } else {
313 let mut u = (E::ONE - eps).scalar_lgamma_p::<P>();
314 for k in 1..=m {
315 u = u - (eps / int::<E>(k)).scalar_ln_1p_p::<P>();
316 }
317 u
318 };
319 let b2 = -u.scalar_exp_m1_p::<P>() / eps;
320 (b1, b2)
321 }
322
323 /// Build the plan from a simplified order. `root_lanes` says whether any lane will run
324 /// the root arm, whose unity series is longer (ratio 0.6 against 0.512).
325 #[inline(always)]
326 pub fn build<P: Policy, S: SignedIntegerElement>(order: PolylogOrder<E, S>, root_lanes: bool) -> Self {
327 let bits = effective_bits::<E>(P::POLICY.precision);
328 let (integer, s, n): (bool, E, isize) = match order {
329 PolylogOrder::Integer(k) => {
330 // The signed lane element's own sanctioned narrowing. Far beyond the table
331 // reach in either direction is the same "unsupported" as the cap below.
332 let n: isize = k
333 .try_into()
334 .unwrap_or(if k < S::ZERO { isize::MIN / 2 } else { isize::MAX / 2 });
335 (
336 true,
337 E::from_int(n.clamp(-(KMAX as isize) * 4, (KMAX as isize) * 4) as LargeInt),
338 n,
339 )
340 }
341 PolylogOrder::Real(s) => {
342 // n = round(s) as an integer, found by comparison rather than conversion. Only
343 // |n| up to the table reach matters (beyond it the fused slot is outside the
344 // sweep and the lead term underflows), so the search is bounded.
345 let r = FloatElement::round(s);
346 let mut n = 0isize;
347 let bound = KMAX as isize * 4;
348 if r > E::ZERO {
349 while n < bound && int::<E>(n as usize) < r {
350 n += 1;
351 }
352 } else {
353 while n > -bound && -int::<E>((-n) as usize) > r {
354 n -= 1;
355 }
356 }
357 (false, s, n)
358 }
359 };
360 let eps = if integer {
361 E::ZERO
362 } else {
363 s - E::from_int(n as LargeInt)
364 };
365 let fused = n >= 1;
366 let m = if fused { (n - 1) as usize } else { 0 };
367 let m_capped = m.min(KMAX * 4);
368
369 // Series lengths. The unity sweep is as long as the longest lane needs: 0.512 for a
370 // packet that stays in region 2, the root arm's 0.6 otherwise. The inversion arm's
371 // inner series runs at 1/z, which on the negative axis is as large as 1/2 (the far
372 // field starts at z = -2 there), so it takes the full count.
373 let k1 = terms::<P, E>(bits, ln_inv_series_ratio::<E>(), s);
374 let k_inv = k1;
375 let k2 = terms::<P, E>(bits, ln_inv_unity_ratio::<E>(root_lanes), s);
376
377 let mut d = [E::ZERO; KMAX];
378 for k in 1..=k1 {
379 let kf = int::<E>(k);
380 d[k - 1] = if integer {
381 kf.scalar_powi_p::<P>(-(n as i32))
382 } else {
383 kf.scalar_powf_p::<P>(-s)
384 };
385 }
386
387 let mut c = [E::ZERO; KMAX];
388 let mut fact = E::ONE;
389 for k in 0..k2 {
390 if k > 0 {
391 fact = fact * int::<E>(k);
392 }
393 let zk = if integer {
394 zeta_int::<E>(n - k as isize)
395 } else {
396 (s - int::<E>(k)).scalar_zeta_p::<P>()
397 };
398 // A non-finite zeta (the reflection's Gamma overflowing the format, binary32 from
399 // k ~ 34 at negative s) sits where k! has overflowed too: the coefficient is
400 // negligible and the honest value is 0, not inf/inf.
401 c[k] = if (fused && k == m) || !finite(zk) {
402 E::ZERO
403 } else {
404 zk / fact
405 };
406 }
407
408 // ln m!, so the fused lead is exp(m ln r - ln m!) rather than r^m / m!, whose
409 // two factors overflow separately (binary32 at m = 35) while the ratio is fine.
410 let mut ln_fact_m = E::ZERO;
411 let mut fact_m = E::ONE;
412 for k in 1..=m_capped {
413 ln_fact_m = ln_fact_m + int::<E>(k).scalar_ln_p::<P>();
414 fact_m = fact_m * int::<E>(k);
415 }
416 let (b1, b2) = if fused {
417 Self::brackets::<P>(m_capped, eps)
418 } else {
419 (E::ZERO, E::ZERO)
420 };
421 let gamma_1ms = if fused {
422 E::ZERO
423 } else {
424 (E::ONE - s).scalar_tgamma_p::<P>()
425 };
426
427 let at_one = if s > E::ONE {
428 if integer {
429 zeta_int::<E>(n)
430 } else {
431 s.scalar_zeta_p::<P>()
432 }
433 } else {
434 E::ORDER_MAX
435 };
436
437 // Inversion arm (integer n >= 1, capped by the array): B_n(x) = sum_k C(n,k) B_k x^{n-k}
438 // with B_1 = -1/2, so the coefficient of x^j is C(n, j) B_{n-j}.
439 let mut bp = [E::ZERO; KMAX];
440 let mut bp_scale = (E::ZERO, E::ZERO);
441 let mut sigma_scale = E::ZERO;
442 // The inversion formula needs n! and B_n in the format: binary64 to n = 79 (the
443 // array), binary32 to n = 34 (n! overflows at 35). Past that the far field is NaN.
444 let inversion = integer && n >= 1 && (n as usize) < KMAX && finite(fact_m * int::<E>(n as usize));
445 if inversion {
446 let nu = n as usize;
447 let mut binom = E::ONE; // C(n, j), walked up in j
448 for j in 0..=nu {
449 if j > 0 {
450 binom = binom * int::<E>(nu - j + 1) / int::<E>(j);
451 }
452 let i = nu - j;
453 let b = if i == 0 {
454 E::ONE
455 } else if i == 1 {
456 -const_element!(ratio <E>: 1 / 2)
457 } else if !i.is_multiple_of(2) {
458 E::ZERO
459 } else {
460 E::B2N.get(i / 2 - 1).copied().unwrap_or(E::ORDER_MAX)
461 };
462 bp[j] = binom * b;
463 }
464 // (2 pi i)^n / n! = (2 pi)^n / n! * i^n
465 let mag = E::TAU.scalar_powi_p::<P>(n as i32) / (fact_m * int::<E>(nu));
466 bp_scale = match nu % 4 {
467 0 => (mag, E::ZERO),
468 1 => (E::ZERO, mag),
469 2 => (-mag, E::ZERO),
470 _ => (E::ZERO, -mag),
471 };
472 sigma_scale = E::TAU / fact_m;
473 }
474
475 Self {
476 s,
477 n,
478 eps,
479 integer,
480 inversion,
481 fused,
482 m: m_capped,
483 k1,
484 k_inv,
485 k2,
486 d,
487 c,
488 ln_fact_m,
489 b1,
490 b2,
491 gamma_1ms,
492 at_one,
493 bp,
494 bp_scale,
495 sigma_scale,
496 }
497 }
498}
499
500/// The defining series `sum_{k=1}^{terms} d_k z^k` at a real `z`, Horner.
501#[inline(always)]
502fn series<E, V>(z: V, d: &[E; KMAX], terms: usize) -> V
503where
504 E: FloatElement,
505 V: FloatVector<Element = E>,
506{
507 let mut acc = V::splat(d[terms - 1]);
508 let mut k = terms - 1;
509 while k > 0 {
510 V::_loop_hint();
511 k -= 1;
512 acc = acc.mul_adde(z, V::splat(d[k]));
513 }
514 acc * z
515}
516
517/// `(Re, Im)` of `sum_{k < terms} c_k x^k` at `x = a + ib`, by Goertzel: `q = |x|^2`.
518#[inline(always)]
519fn goertzel<E, V>(c: &[E; KMAX], terms: usize, a: V, b: V, q: V) -> (V, V)
520where
521 E: FloatElement,
522 V: FloatVector<Element = E>,
523{
524 let p = a + a;
525 let mut b1 = V::ZERO;
526 let mut b2 = V::ZERO;
527 let mut k = terms;
528 while k > 0 {
529 V::_loop_hint();
530 k -= 1;
531 let b0 = q.nmul_adde(b2, p.mul_adde(b1, V::splat(c[k])));
532 b2 = b1;
533 b1 = b0;
534 }
535 (a.nmul_adde(b2, b1), b * b2)
536}
537
538/// `(e^x - 1)/x`, exactly 1 at the origin.
539#[inline(always)]
540fn phi1<P, E, V>(x: V) -> V
541where
542 P: Policy,
543 E: FloatElement,
544 V: FloatVector<Element = E> + RealMathWithPolicy<Element = E>,
545{
546 x.cmp_eq(V::ZERO).select(V::ONE, x.exp_m1_p::<P>() / x)
547}
548
549/// The real part of the unity series at `mu = a + ib`, including the fused or plain lead
550/// term. `q = |mu|^2`. See the module docs for the identities.
551#[inline(always)]
552pub fn unity_re<P, E, V>(a: V, b: V, q: V, plan: &PolylogPlan<E>) -> V
553where
554 P: Policy,
555 E: FloatElement,
556 V: FloatVector<Element = E> + RealMathWithPolicy<Element = E>,
557{
558 let (poly, _) = goertzel::<E, V>(&plan.c, plan.k2, a, b, q);
559
560 let ln_r = q.ln_p::<P>() * V::HALF;
561 // Angles as multiples of pi: exact on the axes, and the `_pi` trig is exact at
562 // half-integers, which the cut near z = 1 needs (see the module docs).
563 let theta_pi = (-b).atan2_p::<P>(-a) * V::FRAC_1_PI; // arg(-mu)/pi
564
565 if plan.fused {
566 let m = V::splat(int::<E>(plan.m));
567 let phi_pi = b.atan2_p::<P>(a) * V::FRAC_1_PI; // arg(mu)/pi
568 // r^m / m! as one exponential of a difference: neither factor need be representable.
569 let r_m = (ln_r * m - V::splat(plan.ln_fact_m)).exp_p::<P>();
570 let (s_m, c_m) = (phi_pi * m).sincos_pi_p::<P>();
571
572 let (q_re, q_im) = if plan.eps == E::ZERO {
573 (V::splat(plan.b1) - ln_r, -(theta_pi * V::PI))
574 } else {
575 let eps = V::splat(plan.eps);
576 let r_eps = (ln_r * eps).exp_p::<P>();
577 let et_pi = theta_pi * eps;
578 let (s_e, c_e) = et_pi.sincos_pi_p::<P>();
579 // D = (e^{eps L} - 1)/eps, spelled to be exact as eps -> 0: versin(x) = 2 sin^2(x/2)
580 // and sin(x)/x through sinc_pi.
581 let half_s = (et_pi * V::HALF).sin_pi_p::<P>();
582 let versin = (half_s * half_s) * V::TWO;
583 let d_re = (ln_r * phi1::<P, E, V>(ln_r * eps)).mul_sube(c_e, versin / eps);
584 let d_im = r_eps * (theta_pi * V::PI) * et_pi.sinc_pi_p::<P>();
585 let b2e = r_eps * V::splat(plan.b2);
586 (b2e.mul_adde(c_e, V::splat(plan.b1)) - d_re, b2e * s_e - d_im)
587 };
588 let lead = r_m * (c_m * q_re - s_m * q_im);
589 poly + lead
590 } else {
591 // Gamma(1-s) r^{s-1} cos((s-1) theta)
592 let sm1 = V::splat(plan.s - E::ONE);
593 let lead = (ln_r * sm1).exp_p::<P>() * (theta_pi * sm1).cos_pi_p::<P>() * V::splat(plan.gamma_1ms);
594 poly + lead
595 }
596}
597
598/// The real part of `Li_s(z)` for real `z`, every real order. See the [module
599/// documentation](self).
600#[inline(always)]
601pub fn polylog_impl<P, E, V>(z_in: V, order: PolylogOrder<E, E::Signed>) -> V
602where
603 P: Policy,
604 E: PolylogElement,
605 V: FloatVectorWithBits<Element = E> + RealMathWithPolicy<Element = E>,
606 V::Signed: GenericVector<Element = E::Signed>,
607{
608 let order = order.simplify::<V>();
609 let one = V::ONE;
610
611 // Closed forms: Li_1 = -ln(1 - z) (on the cut the real part is -ln(z - 1)), Li_0 = z/(1-z).
612 if let PolylogOrder::Integer(k) = order {
613 if k == E::Signed::ONE {
614 let below = z_in.cmp_lt(one);
615 return below.select(-(-z_in).ln_1p_p::<P>(), -(z_in - one).ln_p::<P>());
616 }
617 if k == E::Signed::ZERO {
618 return z_in / (one - z_in);
619 }
620 }
621
622 // mu = ln z = a + ib with b = pi on the negative axis. Near |z| = 1 the log is taken as
623 // ln_1p(|z| - 1), the subtraction being exact there: a plain `ln` carries an absolute
624 // error of an ulp of 1 into `a`, and every arm amplifies the relative error of `a` by
625 // the order (the lead term is a power of it).
626 let az = z_in.abs();
627 let near_one = az.cmp_ge(V::HALF) & az.cmp_le(V::TWO);
628 let a_in = near_one.select((az - one).ln_1p_p::<P>(), az.ln_p::<P>());
629 let b = z_in.is_negative().select(V::PI, V::ZERO);
630 let q = a_in.mul_adde(a_in, b * b);
631
632 // Region masks: series where 2 pi |z| < |mu|, unity where |mu| <= 2 pi T1, far otherwise.
633 let tau_t1 = V::TAU * V::splat(t1::<E>());
634 let use_series = ((z_in * z_in) * (V::TAU * V::TAU)).cmp_lt(q);
635 let in_unity = q.cmp_le(tau_t1 * tau_t1);
636 let far = !(use_series | in_unity);
637
638 // Negative integer order: the series and unity arms serve as they stand (every
639 // coefficient is a tabulated zeta value, the lead term is `(-n)! (-mu)^{n-1}`), and the
640 // far field is the exact reflection `Li_{-p}(z) = -(-1)^p Li_{-p}(1/z)` (Wood 10.3),
641 // which lands every far lane in the series region. Only far lanes reflect: next to
642 // z = 1 the rounding of 1/z is amplified by (p+1)/|mu| (measured 2.5e-13 at 1.001), and
643 // the unity series needs no help there. `ln(1/z) = -ln z` exactly, so `a` just flips.
644 // The closed rational form in z/(1-z) was tried first and cancels ~2000x near |z| = 1
645 // at p = 6, as Wood warns.
646 let negative = matches!(order, PolylogOrder::Integer(k) if k < E::Signed::ZERO);
647 let none = V::ZERO.cmp_ne(V::ZERO);
648 let reflect = if negative { far } else { none };
649 let z = reflect.select(one / z_in, z_in);
650 let a = a_in.neg_c(reflect);
651 let use_series = use_series | reflect;
652 let use_far = if negative { none } else { far };
653 let branchy = !P::POLICY.avoid_branching;
654
655 let any_far = if branchy { use_far.any() } else { true };
656 let plan = PolylogPlan::<E>::build::<P, E::Signed>(order, any_far);
657
658 let mut result = series::<E, V>(z, &plan.d, plan.k1);
659
660 if !branchy || !use_series.all() {
661 let u = unity_re::<P, E, V>(a, b, q, &plan);
662 // z = 1 exactly: mu = 0 makes the lead term 0 * inf.
663 let u = q.cmp_eq(V::ZERO).select(V::splat(plan.at_one), u);
664 result = use_series.select(result, u);
665 }
666
667 if any_far {
668 let far_value = if plan.integer {
669 // Inversion, real part. The step term is imaginary and drops out.
670 let n = plan.n;
671 if !plan.inversion {
672 V::NAN
673 } else {
674 let inner = series::<E, V>(one / z, &plan.d, plan.k_inv);
675 let inner = if n % 2 == 0 { -inner } else { inner };
676
677 // x = mu / (2 pi i) = (b - i a) / 2 pi
678 let inv_tau = V::FRAC_1_TAU;
679 let xr = b * inv_tau;
680 let xi = -(a * inv_tau);
681 let (pr, pi) = goertzel::<E, V>(&plan.bp, n as usize + 1, xr, xi, q * inv_tau * inv_tau);
682 let bterm = pr.mul_sube(V::splat(plan.bp_scale.0), pi * V::splat(plan.bp_scale.1));
683 inner - bterm
684 }
685 } else {
686 // m-th roots, conjugate pairs folded: for k = 0..=m/2 the root's imaginary part
687 // is (b + 2 pi k)/m, weighted 2 for a proper pair, 1 for the self-conjugate roots
688 // at 0 and pi, 0 past pi.
689 let m_roots = root_count::<E>(a.max_element());
690 let inv_m = V::splat(E::ONE / int::<E>(m_roots));
691 let a_m = a * inv_m;
692 let tol = V::PI * const_splat!(ratio <E>: 1 / 1000000000);
693 let mut sum = V::ZERO;
694 for k in 0..=(m_roots / 2) {
695 let b_j = (b + V::TAU * V::splat(int::<E>(k))) * inv_m;
696 let q_j = a_m.mul_adde(a_m, b_j * b_j);
697 let over = b_j.cmp_gt(V::PI + tol);
698 let single = b_j.cmp_eq(V::ZERO) | (b_j - V::PI).abs().cmp_le(tol);
699 let weight = over.select(V::ZERO, single.select(one, V::TWO));
700 sum = weight.mul_adde(unity_re::<P, E, V>(a_m, b_j, q_j, &plan), sum);
701 }
702 sum * V::splat(int::<E>(m_roots).scalar_powf_p::<P>(plan.s - E::ONE))
703 };
704 result = use_far.select(far_value, result);
705 }
706
707 if negative && plan.n % 2 == 0 {
708 result = result.neg_c(reflect);
709 }
710
711 if const { P::POLICY.check_overflow } {
712 result = z_in.is_finite().select(result, V::NAN);
713 }
714
715 result
716}