Skip to main content

thermite_special/
polylog.rs

1//! The polylogarithm order.
2//!
3//! [`polylog`](crate::SpecialMath::polylog) takes its order as a [`PolylogOrder`]: a
4//! **scalar**, uniform across the packet, tagged by the class of order it carries.
5//! `$\mathrm{Li}_n$` at whole-number `n` is a table lookup: every coefficient of the unity
6//! series is a tabulated `$\zeta$` value, the leading `$\Gamma(1-s)(-\mu)^{s-1}$` term
7//! collapses into `$H_{n-1} - \ln(-\mu)$`, and the far
8//! field is the Bernoulli-polynomial inversion formula. At arbitrary real `s` the same
9//! series needs a sweep of live `$\zeta(s-k)$` evaluations, a real power, and Wood's
10//! m-th-root identity in the far field. Those are different algorithms with costs an
11//! order of magnitude apart.
12//!
13//! # Why a scalar, not a vector
14//!
15//! Every order-dependent quantity (`$\zeta(s-k)/k!$`, `$k^{-s}$`, `$\Gamma(1-s)$`, the
16//! near-integer brackets) is a _per-call_ scalar precompute, splatted once. A per-lane
17//! order would pay that sweep per lane, and the regime choices that depend on `s` (near an
18//! integer, non-positive) would become masks over arms every lane then has to evaluate.
19//! A caller with several orders runs several calls.
20//!
21//! # The payload types
22//!
23//! `PolylogOrder<E, S>` carries the invoking vector's own element types: `E` is its
24//! `Element` (`f64` on an `f64` vector, `Complex<f64>` on a complex one, a `Dual` element on
25//! a dual one) and `S` is its `Signed` lane element (`i64` on an `f64` vector, `i32` on an
26//! `f32` one), so an order is spelled in the arithmetic of the type it is used with and
27//! nothing is ever converted. The trait signature is
28//! `PolylogOrder<Self::Element, <Self::Signed as GenericVector>::Element>`.
29//!
30//! # Downgrading
31//!
32//! [`simplify`](PolylogOrder::simplify) narrows [`Real`](PolylogOrder::Real) to
33//! [`Integer`](PolylogOrder::Integer) when the value is _exactly_ whole. Nothing narrower
34//! is attempted: unlike the Bessel order there is no half-integer shortcut (Wood's
35//! half-integer series is the general one with tabulated constants). A near-integer
36//! order is a **correctness** hazard for the general arm rather than a cost choice, which
37//! is why the general kernel fuses the two cancelling poles algebraically instead of
38//! trusting the caller to have snapped.
39
40use thermite::element::{FloatElement, SignedIntegerElement};
41use thermite::math::scalar::Unwrap;
42use thermite::prelude::*;
43
44/// The order `$s$` of a polylogarithm, tagged with the class of order it carries. See the
45/// [module documentation](self).
46#[derive(Debug, Clone, Copy, PartialEq)]
47pub enum PolylogOrder<E, S> {
48    /// `$s = n$`, a whole number of either sign, in the vector's signed lane type. The
49    /// tabulated arm for `$n \ge 1$`, closed forms at `0` and `1`, the reflected series for
50    /// `$n < 0$`.
51    Integer(S),
52
53    /// Arbitrary real `$s$`, in the vector's element type. The general algorithm.
54    Real(E),
55}
56
57impl<E: FloatElement, S: SignedIntegerElement> PolylogOrder<E, S> {
58    /// Narrow [`Real`](Self::Real) to [`Integer`](Self::Integer) when the value is exactly
59    /// a whole number. Never changes the value of `$s$`.
60    ///
61    /// `V` is any real vector over `E` (the one the order is about to be used with) and
62    /// only supplies the float-to-integer lane conversion.
63    #[inline(always)]
64    pub fn simplify<V>(self) -> Self
65    where
66        V: FloatVector<Element = E>,
67        V::Signed: GenericVector<Element = S>,
68    {
69        match self {
70            Self::Integer(_) => self,
71            Self::Real(s) => {
72                let r = FloatElement::round(s);
73                if r == s {
74                    Self::Integer(V::splat(r).to_signed_integer().extract::<0>())
75                } else {
76                    self
77                }
78            }
79        }
80    }
81}
82
83/// The order is a scalar in both the vector and the scalar spelling of `polylog`, so it
84/// crosses the scalar surface unchanged.
85impl<E, S> Unwrap for PolylogOrder<E, S> {
86    type Unwrapped = Self;
87
88    #[inline(always)]
89    fn wrap(value: Self) -> Self {
90        value
91    }
92
93    #[inline(always)]
94    fn unwrap(self) -> Self {
95        self
96    }
97}
Last built: 2026-09-08 21:35:55 UTC