Skip to main content

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