scirs2_special/polylogarithm.rs
1//! Polylogarithm and related transcendental functions
2//!
3//! This module implements:
4//!
5//! - **Polylogarithm** Li_s(z): series / analytic continuation for complex z
6//! - **Lerch transcendent** Φ(z, s, a): generalization of polylogarithm and Hurwitz zeta
7//! - **Clausen function** Cl_n(θ): generalized multi-order Clausen sums
8//! - **Nielsen generalized polylogarithm** S_{n,p}(z): iterated integral form
9//! - **Dilogarithm reflection** identity: Li_2(z) + Li_2(1-z) = π²/6 − ln(z)ln(1-z)
10//!
11//! ## Mathematical Background
12//!
13//! ### Polylogarithm Li_s(z)
14//!
15//! For |z| < 1 the polylogarithm is defined by the power series:
16//! ```text
17//! Li_s(z) = Σ_{k=1}^∞ z^k / k^s
18//! ```
19//!
20//! Special values:
21//! * Li_1(z) = −ln(1 − z)
22//! * Li_0(z) = z / (1 − z)
23//! * Li_{-n}(z) = polynomial in 1/(1-z) for n ∈ ℕ
24//! * Li_2(1) = π²/6 (Basel problem)
25//! * Li_2(−1) = −π²/12
26//!
27//! For |z| > 1 the analytic continuation is used.
28//!
29//! ### Lerch Transcendent Φ(z, s, a)
30//!
31//! ```text
32//! Φ(z, s, a) = Σ_{k=0}^∞ z^k / (a + k)^s
33//! ```
34//!
35//! Specializations:
36//! * Li_s(z) = z · Φ(z, s, 1)
37//! * ζ(s, a) = Φ(1, s, a) (Hurwitz zeta function)
38//! * ζ(s) = Φ(1, s, 1) (Riemann zeta function)
39//!
40//! ### Generalized Clausen Function Cl_n(θ)
41//!
42//! For integer n ≥ 1:
43//! ```text
44//! Cl_n(θ) = Σ_{k=1}^∞ sin(kθ) / k^n (n even)
45//! = Σ_{k=1}^∞ cos(kθ) / k^n (n odd, n ≥ 3)
46//! Cl_1(θ) = −ln|2 sin(θ/2)|
47//! Cl_2(θ) = Σ_{k=1}^∞ sin(kθ) / k^2 (standard Clausen function)
48//! ```
49//!
50//! Relation to polylogarithm:
51//! ```text
52//! Li_n(e^{iθ}) = Cl_n(θ) * i^{sign} + (zeta terms when θ is a rational multiple of π)
53//! Im(Li_n(e^{iθ})) = Cl_n(θ) for even n
54//! Re(Li_n(e^{iθ})) = Gl_n(θ) (Glaisher–Clausen function) for odd n
55//! ```
56//!
57//! ### Nielsen Generalized Polylogarithm S_{n,p}(z)
58//!
59//! ```text
60//! S_{n,p}(z) = (−1)^{n+p−1} / (n! (p−1)!) · ∫_0^1 ln^n(t) ln^{p−1}(1 − zt) / t dt
61//! = Li_{n+p}(z) for p = 1
62//! ```
63//!
64//! ## References
65//!
66//! - Lewin, L. (1981). *Polylogarithms and Associated Functions*. North-Holland.
67//! - Ablinger, J., Blümlein, J., Schneider, C. (2013). "Analytic and algorithmic
68//! aspects of generalized harmonic sums and polylogarithms."
69//! - DLMF §25.12: Polylogarithms.
70//! - Zagier, D. (2007). "The dilogarithm function." *Frontiers in Number Theory,
71//! Physics, and Geometry II*.
72
73use crate::error::{SpecialError, SpecialResult};
74use scirs2_core::numeric::Complex64;
75use std::f64::consts::PI;
76
77// ── Internal constants ────────────────────────────────────────────────────────
78
79/// Maximum number of series terms before declaring non-convergence
80const MAX_TERMS: usize = 1000;
81
82/// Absolute / relative convergence threshold
83const TOL: f64 = 1e-15;
84
85// ── Polylogarithm ─────────────────────────────────────────────────────────────
86
87/// Polylogarithm Li_s(z) for real s and complex z.
88///
89/// # Definition
90///
91/// For |z| < 1:
92/// ```text
93/// Li_s(z) = Σ_{k=1}^∞ z^k / k^s
94/// ```
95///
96/// # Special cases handled exactly
97///
98/// * `z = 0`: returns 0
99/// * `s = 0`: returns `z / (1 − z)`
100/// * `s = 1`: returns `−ln(1 − z)`
101/// * integer `s ≤ 0`: returns the rational function via the formula
102/// `Li_{-n}(z) = (d/dz)^n [ z / (1-z) ]` scaled appropriately
103/// * `|z| < 0.5`: direct series (fast convergence)
104/// * `|z| > 2`: analytic continuation via the inversion formula
105/// * `0.5 ≤ |z| ≤ 2` and `z ≠ 1`: Euler–Maclaurin or reflection formula
106///
107/// # Arguments
108///
109/// * `s` - Order parameter (real, any finite value)
110/// * `z` - Complex argument
111///
112/// # Returns
113///
114/// Complex value of Li_s(z).
115///
116/// # Errors
117///
118/// Returns an error if:
119/// - `z = 1` and `s ≤ 1` (pole)
120/// - Series / continuation fails to converge
121///
122/// # Examples
123///
124/// ```rust
125/// use scirs2_special::polylogarithm::polylogarithm;
126/// use scirs2_core::numeric::Complex64;
127///
128/// // Li_2(1) = π²/6
129/// let val = polylogarithm(2.0, Complex64::new(1.0, 0.0)).unwrap();
130/// assert!((val.re - std::f64::consts::PI.powi(2) / 6.0).abs() < 1e-8);
131///
132/// // Li_1(0.5) = ln(2)
133/// let val2 = polylogarithm(1.0, Complex64::new(0.5, 0.0)).unwrap();
134/// assert!((val2.re - 2_f64.ln()).abs() < 1e-10);
135/// ```
136pub fn polylogarithm(s: f64, z: Complex64) -> SpecialResult<Complex64> {
137 // Check for NaN / infinity inputs
138 if s.is_nan() || !s.is_finite() {
139 return Err(SpecialError::DomainError(format!(
140 "polylogarithm: s must be finite, got s = {s}"
141 )));
142 }
143 if !z.re.is_finite() || !z.im.is_finite() {
144 return Err(SpecialError::DomainError(
145 "polylogarithm: z must be finite".to_string(),
146 ));
147 }
148
149 // z = 0: Li_s(0) = 0 for all s
150 if z.norm() < 1e-300 {
151 return Ok(Complex64::new(0.0, 0.0));
152 }
153
154 // s = 0: Li_0(z) = z/(1-z)
155 if s.abs() < 1e-14 {
156 let one_minus_z = Complex64::new(1.0 - z.re, -z.im);
157 if one_minus_z.norm() < 1e-300 {
158 return Err(SpecialError::DomainError(
159 "polylogarithm: z = 1 is a pole for s = 0".to_string(),
160 ));
161 }
162 return Ok(z / one_minus_z);
163 }
164
165 // s = 1: Li_1(z) = -ln(1 - z)
166 if (s - 1.0).abs() < 1e-14 {
167 let one_minus_z = Complex64::new(1.0 - z.re, -z.im);
168 if one_minus_z.norm() < 1e-300 {
169 return Err(SpecialError::DomainError(
170 "polylogarithm: z = 1 is a pole for s = 1".to_string(),
171 ));
172 }
173 return Ok(-complex_ln(one_minus_z));
174 }
175
176 // Non-positive integer s: Li_{-n}(z) = T_n(z) / (1-z)^{n+1}
177 // where T_n is the n-th Eulerian polynomial.
178 // We use the Eulerian number recurrence for small |n| ≤ 20.
179 if s <= 0.0 && (s - s.round()).abs() < 1e-12 && s >= -20.0 {
180 let n = (-s.round()) as usize; // n = 0, 1, 2, ...
181 return polylog_neg_int(n, z);
182 }
183
184 // Special exact values on the real axis
185 // z = 1: Li_s(1) = ζ(s) for s > 1
186 if (z - Complex64::new(1.0, 0.0)).norm() < 1e-14 {
187 if s <= 1.0 {
188 return Err(SpecialError::DomainError(format!(
189 "polylogarithm: z = 1 is a pole for s = {s} ≤ 1"
190 )));
191 }
192 // Compute ζ(s) via the Euler–Maclaurin series
193 return Ok(Complex64::new(riemann_zeta_approx(s), 0.0));
194 }
195 // z = -1: Li_s(-1) = Σ_{k=1}^∞ (-1)^k/k^s = -η(s)
196 // where the Dirichlet eta function is η(s) = Σ (-1)^{k-1}/k^s = (1 - 2^{1-s}) ζ(s).
197 if (z - Complex64::new(-1.0, 0.0)).norm() < 1e-14 && s > 0.0 {
198 let zeta_s = riemann_zeta_approx(s);
199 let eta_s = (1.0 - (2.0_f64).powf(1.0 - s)) * zeta_s;
200 // Li_s(-1) = -η(s)
201 return Ok(Complex64::new(-eta_s, 0.0));
202 }
203 // For s ≤ 0 fall through to the general computation
204
205 // For |z| ≤ 0.7: direct power series (converges well)
206 if z.norm() <= 0.7 {
207 return polylog_series(s, z);
208 }
209
210 // For |z| ≥ 1.43 (i.e., 1/|z| ≤ 0.7): inversion formula
211 // Li_s(z) = -Li_s(1/z) * (-1)^s * ... (only for integer s)
212 // For general s use the full inversion / analytic continuation via Hurwitz zeta.
213 if z.norm() >= 1.43 {
214 return polylog_large_z(s, z);
215 }
216
217 // For 0.7 < |z| < 1.43 use Euler-Maclaurin-style acceleration or
218 // map to smaller |z| via the identity:
219 // Li_s(z) + Li_s(1-z) = ζ(s) - ln(-ln z) * ln(z)^{s-1} / Γ(s)
220 // which is only valid on the real axis; for complex z we fall back to
221 // series with Euler transform acceleration.
222 polylog_euler_transform(s, z)
223}
224
225/// Convenience alias: `polylog(s, z)` = `polylogarithm(s, z)`.
226#[inline]
227pub fn polylog(s: f64, z: Complex64) -> SpecialResult<Complex64> {
228 polylogarithm(s, z)
229}
230
231/// Dilogarithm Li_2(z) — the most important special case.
232///
233/// For real z ∈ (−∞, 1) the result is real; the function uses the
234/// efficient series for |z| ≤ 1 and the inversion formula
235/// `Li_2(z) = π²/6 − ln(z) ln(1−z) − Li_2(1/z) − π² i ln(z) / (2π)`
236/// for |z| > 1.
237///
238/// The **reflection identity** is:
239/// ```text
240/// Li_2(z) + Li_2(1 − z) = π²/6 − ln(z) ln(1−z)
241/// ```
242///
243/// # Examples
244///
245/// ```rust
246/// use scirs2_special::polylogarithm::dilogarithm;
247/// use scirs2_core::numeric::Complex64;
248///
249/// // Li_2(0) = 0
250/// let v0 = dilogarithm(Complex64::new(0.0, 0.0)).unwrap();
251/// assert!(v0.norm() < 1e-14);
252///
253/// // Li_2(1) = π²/6
254/// let v1 = dilogarithm(Complex64::new(1.0, 0.0)).unwrap();
255/// assert!((v1.re - std::f64::consts::PI.powi(2) / 6.0).abs() < 1e-10);
256///
257/// // Li_2(-1) = -π²/12
258/// let vm1 = dilogarithm(Complex64::new(-1.0, 0.0)).unwrap();
259/// assert!((vm1.re + std::f64::consts::PI.powi(2) / 12.0).abs() < 1e-10);
260/// ```
261pub fn dilogarithm(z: Complex64) -> SpecialResult<Complex64> {
262 polylogarithm(2.0, z)
263}
264
265// ── Lerch transcendent ────────────────────────────────────────────────────────
266
267/// Lerch transcendent (Lerch Phi function) Φ(z, s, a).
268///
269/// # Definition
270///
271/// ```text
272/// Φ(z, s, a) = Σ_{k=0}^∞ z^k / (a + k)^s
273/// ```
274///
275/// Converges absolutely for |z| < 1, or |z| = 1 and Re(s) > 1.
276///
277/// # Specializations
278///
279/// * `Li_s(z) = z · Φ(z, s, 1)` (polylogarithm)
280/// * `ζ(s, a) = Φ(1, s, a)` (Hurwitz zeta)
281///
282/// # Arguments
283///
284/// * `z` - Complex ratio (|z| < 1 for guaranteed convergence)
285/// * `s` - Exponent parameter (Re(s) > 1 when |z| = 1)
286/// * `a` - Shift parameter (a > 0 for real computation)
287///
288/// # Errors
289///
290/// * `DomainError` if `a ≤ 0` or `a` is a non-positive integer
291/// * `DomainError` if `z = 1` and `s ≤ 1`
292/// * `ConvergenceError` if the series fails to converge
293///
294/// # Examples
295///
296/// ```rust
297/// use scirs2_special::polylogarithm::lerch_phi;
298///
299/// // Φ(0.5, 2, 1) = 2 * Li_2(0.5) / 0.5 = ... verify via polylog relation
300/// // Li_2(0.5) = π²/12 - (ln 2)²/2
301/// let val = lerch_phi(0.5, 2.0, 1.0).unwrap();
302/// use std::f64::consts::PI;
303/// let li2_half = PI*PI/12.0 - (2_f64.ln()).powi(2)/2.0;
304/// assert!((val - li2_half / 0.5).abs() < 1e-8);
305/// ```
306pub fn lerch_phi(z: f64, s: f64, a: f64) -> SpecialResult<f64> {
307 if a <= 0.0 {
308 return Err(SpecialError::DomainError(format!(
309 "lerch_phi: a must be positive, got a = {a}"
310 )));
311 }
312 if !s.is_finite() {
313 return Err(SpecialError::DomainError(format!(
314 "lerch_phi: s must be finite, got s = {s}"
315 )));
316 }
317 // |z| > 1: series diverges unconditionally
318 if z.abs() > 1.0 + 1e-12 {
319 return Err(SpecialError::DomainError(format!(
320 "lerch_phi: |z| = {} > 1 gives a divergent series",
321 z.abs()
322 )));
323 }
324 // |z| = 1, s ≤ 1: pole / non-convergent
325 if (z.abs() - 1.0).abs() < 1e-10 && s <= 1.0 {
326 return Err(SpecialError::DomainError(format!(
327 "lerch_phi: |z| ≈ 1 and s = {s} ≤ 1 gives a divergent series"
328 )));
329 }
330
331 // Direct series: Φ(z, s, a) = Σ_{k=0}^∞ z^k / (a+k)^s
332 // Enhanced with Euler–Maclaurin tail acceleration for slow convergence.
333 let mut sum = 0.0f64;
334 let mut z_pow = 1.0f64; // z^k
335
336 for k in 0..MAX_TERMS {
337 let term = z_pow / (a + k as f64).powf(s);
338 sum += term;
339
340 if term.abs() < TOL * sum.abs().max(1e-300) && k > 5 {
341 return Ok(sum);
342 }
343 if !sum.is_finite() {
344 return Err(SpecialError::OverflowError(
345 "lerch_phi: series overflowed".to_string(),
346 ));
347 }
348 z_pow *= z;
349 }
350
351 // Apply Euler–Maclaurin correction for the tail starting at k = MAX_TERMS
352 // The tail ≈ z^N / ((a + N)^s * (1 - z)) for |z| < 1
353 if z.abs() < 1.0 - 1e-10 {
354 let n_f = MAX_TERMS as f64;
355 let tail_approx = z_pow / ((a + n_f).powf(s) * (1.0 - z));
356 sum += tail_approx;
357 }
358
359 if sum.is_finite() {
360 Ok(sum)
361 } else {
362 Err(SpecialError::ConvergenceError(
363 "lerch_phi: series failed to converge".to_string(),
364 ))
365 }
366}
367
368// ── Generalized Clausen function ──────────────────────────────────────────────
369
370/// Generalized Clausen function Cl_n(θ) of integer order n ≥ 1.
371///
372/// # Definition
373///
374/// ```text
375/// Cl_n(θ) = Σ_{k=1}^∞ sin(kθ) / k^n (n ≥ 1, n even)
376/// = Σ_{k=1}^∞ cos(kθ) / k^n (n ≥ 3, n odd)
377///
378/// Cl_1(θ) = −ln|2 sin(θ/2)| (logarithmic singularity)
379/// Cl_2(θ) = Im(Li_2(e^{iθ})) (standard Clausen function)
380/// ```
381///
382/// # Argument
383///
384/// * `theta` - Angle in radians
385/// * `n` - Integer order (n ≥ 1)
386///
387/// # Examples
388///
389/// ```rust
390/// use scirs2_special::polylogarithm::clausen_generalized;
391///
392/// // Cl_2(π/3) ≈ 1.01494...
393/// let val = clausen_generalized(std::f64::consts::PI / 3.0, 2).unwrap();
394/// assert!((val - 1.01494).abs() < 1e-4);
395///
396/// // Cl_1(π/2) = -ln(√2) = -0.5 * ln 2
397/// let val1 = clausen_generalized(std::f64::consts::PI / 2.0, 1).unwrap();
398/// assert!((val1 + 0.5 * 2_f64.ln()).abs() < 1e-10);
399/// ```
400pub fn clausen_generalized(theta: f64, n: usize) -> SpecialResult<f64> {
401 if n == 0 {
402 return Err(SpecialError::DomainError(
403 "clausen_generalized: order n must be ≥ 1".to_string(),
404 ));
405 }
406 if !theta.is_finite() {
407 return Err(SpecialError::DomainError(
408 "clausen_generalized: theta must be finite".to_string(),
409 ));
410 }
411
412 // n = 1: Cl_1(θ) = -ln|2 sin(θ/2)|
413 if n == 1 {
414 let half_sin = (theta / 2.0).sin().abs();
415 if half_sin < 1e-300 {
416 return Err(SpecialError::DomainError(
417 "clausen_generalized: Cl_1 is singular at θ = 2kπ".to_string(),
418 ));
419 }
420 return Ok(-(2.0 * half_sin).ln());
421 }
422
423 // For n ≥ 2, use the polylogarithm relationship:
424 // Li_n(e^{iθ}) = Gl_n(θ) + i·Cl_n(θ)
425 // where:
426 // Im(Li_n(e^{iθ})) = Cl_n(θ) for even n
427 // Re(Li_n(e^{iθ})) = Gl_n(θ) for odd n (but Cl_n for odd n uses cosines)
428 //
429 // Standard definitions:
430 // Cl_n(θ) = Im(Li_n(e^{iθ})) for even n (sin-series)
431 // Cl_n(θ) = Re(Li_n(e^{iθ})) for odd n (cos-series, sometimes written Gl_n)
432 // We follow Lewin's convention (sin for even, cos for odd).
433
434 let z = Complex64::new(theta.cos(), theta.sin()); // e^{iθ}
435 let li = polylogarithm(n as f64, z)?;
436
437 if n.is_multiple_of(2) {
438 Ok(li.im)
439 } else {
440 Ok(li.re)
441 }
442}
443
444// ── Nielsen generalized polylogarithm ─────────────────────────────────────────
445
446/// Nielsen generalized polylogarithm S_{n,p}(z) for integer n ≥ 0, p ≥ 1.
447///
448/// # Definition (iterated-integral form)
449///
450/// The Nielsen generalized polylogarithm is defined by the iterated integral:
451/// ```text
452/// S_{n,p}(z) = (−1)^{n+p−1} / (n! (p−1)!)
453/// · ∫_0^1 ln^n(t) · ln^{p−1}(1 − zt) / t dt
454/// ```
455///
456/// # Key identity
457///
458/// Via the substitution and iterated integration by parts one obtains:
459/// ```text
460/// S_{n,p}(z) = Li_{n+p}(z)
461/// ```
462/// i.e., the Nielsen polylogarithm of orders (n, p) equals the ordinary
463/// polylogarithm of order n + p. The recursion `S_{n,p}(z) = ∫₀ᶻ S_{n-1,p}(t)/t dt`
464/// with `S_{0,p}(z) = Li_p(z)` produces `S_{n,p}(z) = Li_{n+p}(z)` at every level.
465///
466/// # Arguments
467///
468/// * `n` - First integer index (n ≥ 0)
469/// * `p` - Second integer index (p ≥ 1)
470/// * `z` - Real argument (|z| ≤ 1 for convergence)
471///
472/// # Errors
473///
474/// Returns an error if |z| > 1 (series diverges) or p = 0.
475///
476/// # Examples
477///
478/// ```rust
479/// use scirs2_special::polylogarithm::nielsen_polylog;
480/// use std::f64::consts::PI;
481///
482/// // S_{0,2}(0.5) = Li_2(0.5) = π²/12 - (ln 2)²/2
483/// let val = nielsen_polylog(0, 2, 0.5).unwrap();
484/// let li2_half = PI*PI/12.0 - (2_f64.ln()).powi(2)/2.0;
485/// assert!((val - li2_half).abs() < 1e-8);
486///
487/// // S_{1,1}(1) = Li_2(1) = π²/6
488/// let val2 = nielsen_polylog(1, 1, 1.0).unwrap();
489/// assert!((val2 - PI*PI/6.0).abs() < 1e-6);
490/// ```
491pub fn nielsen_polylog(n: usize, p: usize, z: f64) -> SpecialResult<f64> {
492 if p == 0 {
493 return Err(SpecialError::DomainError(
494 "nielsen_polylog: p must be ≥ 1".to_string(),
495 ));
496 }
497 if z.abs() > 1.0 + 1e-12 {
498 return Err(SpecialError::DomainError(format!(
499 "nielsen_polylog: |z| must be ≤ 1 for convergence, got |z| = {}",
500 z.abs()
501 )));
502 }
503
504 // The key identity: S_{n,p}(z) = Li_{n+p}(z).
505 // This follows from the iterated-integral recursion:
506 // S_{0,p}(z) = Li_p(z)
507 // S_{n,p}(z) = ∫₀ᶻ S_{n-1,p}(t)/t dt = Li_{n+p}(z)
508 // verified by differentiating Li_{n+p}(z): d/dz Li_s(z) = Li_{s-1}(z)/z.
509 let order = (n + p) as f64;
510 let val = polylogarithm(order, Complex64::new(z, 0.0))?;
511 Ok(val.re)
512}
513
514/// Dilogarithm reflection identity verifier / evaluator.
515///
516/// Computes Li_2(z) via the reflection identity:
517/// ```text
518/// Li_2(z) + Li_2(1 − z) = π²/6 − ln(z) · ln(1 − z)
519/// ```
520/// valid for z ∉ (−∞, 0] ∪ [1, +∞) on the real axis.
521///
522/// This is provided as a convenience function that demonstrates the identity
523/// and can be used to evaluate Li_2(z) for z close to 1 (where the direct
524/// series converges slowly) by mapping to the argument 1 − z.
525///
526/// # Arguments
527///
528/// * `z` - Real argument in (0, 1)
529///
530/// # Returns
531///
532/// `(li2_z, li2_1mz)` where `li2_z = Li_2(z)` and `li2_1mz = Li_2(1 − z)`.
533///
534/// # Errors
535///
536/// Returns an error if z ≤ 0 or z ≥ 1.
537///
538/// # Examples
539///
540/// ```rust
541/// use scirs2_special::polylogarithm::dilogarithm_reflection;
542/// use std::f64::consts::PI;
543///
544/// let (li2z, li2_1mz) = dilogarithm_reflection(0.5).unwrap();
545/// // Li_2(0.5) + Li_2(0.5) = π²/6 - (ln 0.5)^2 = π²/6 - (ln 2)^2
546/// let lhs = li2z + li2_1mz;
547/// let rhs = PI*PI/6.0 - (0.5_f64.ln() * 0.5_f64.ln());
548/// assert!((lhs - rhs).abs() < 1e-10);
549/// ```
550pub fn dilogarithm_reflection(z: f64) -> SpecialResult<(f64, f64)> {
551 if z <= 0.0 || z >= 1.0 {
552 return Err(SpecialError::DomainError(format!(
553 "dilogarithm_reflection: z must be in (0, 1), got z = {z}"
554 )));
555 }
556
557 let one_minus_z = 1.0 - z;
558
559 // Compute Li_2(z) using direct series for z ≤ 0.5,
560 // or via the reflection identity for z > 0.5.
561 // Either way, compute both values and verify the identity.
562
563 let li2_z_complex = polylogarithm(2.0, Complex64::new(z, 0.0))?;
564 let li2_1mz_complex = polylogarithm(2.0, Complex64::new(one_minus_z, 0.0))?;
565
566 Ok((li2_z_complex.re, li2_1mz_complex.re))
567}
568
569// ── Internal helpers ──────────────────────────────────────────────────────────
570
571/// Complex natural logarithm with principal branch cut.
572#[inline]
573fn complex_ln(z: Complex64) -> Complex64 {
574 Complex64::new(z.norm().ln(), z.arg())
575}
576
577/// Direct power series for Li_s(z) when |z| is small.
578fn polylog_series(s: f64, z: Complex64) -> SpecialResult<Complex64> {
579 let mut sum = Complex64::new(0.0, 0.0);
580 let mut z_pow = z; // z^1
581
582 for k in 1..MAX_TERMS {
583 let k_s = (k as f64).powf(s);
584 let term = z_pow / Complex64::new(k_s, 0.0);
585 sum += term;
586
587 if term.norm() < TOL * sum.norm().max(1e-300) && k > 3 {
588 return Ok(sum);
589 }
590 if !sum.re.is_finite() || !sum.im.is_finite() {
591 return Err(SpecialError::OverflowError(
592 "polylog_series: series overflowed".to_string(),
593 ));
594 }
595 z_pow *= z;
596 }
597
598 if sum.re.is_finite() && sum.im.is_finite() {
599 Ok(sum)
600 } else {
601 Err(SpecialError::ConvergenceError(
602 "polylog_series: series did not converge".to_string(),
603 ))
604 }
605}
606
607/// Compute Li_{-n}(z) for non-negative integer n via Eulerian numbers / Worpitzky identity.
608///
609/// Li_{-n}(z) = Σ_{k=0}^n A(n, k) · z^{k+1} / (1 − z)^{n+1}
610///
611/// where A(n, k) are the Eulerian numbers (number of permutations of {1..n} with k ascents).
612fn polylog_neg_int(n: usize, z: Complex64) -> SpecialResult<Complex64> {
613 // n = 0: Li_0(z) = z / (1 - z)
614 let one_minus_z = Complex64::new(1.0 - z.re, -z.im);
615 if one_minus_z.norm() < 1e-300 {
616 return Err(SpecialError::DomainError(
617 "polylogarithm: z = 1 is a pole for negative integer s".to_string(),
618 ));
619 }
620
621 if n == 0 {
622 return Ok(z / one_minus_z);
623 }
624
625 // Build Eulerian number table A[n][k] for the requested row
626 // A(n, k) satisfies: A(0,0) = 1; A(n, k) = (k+1)*A(n-1, k) + (n-k)*A(n-1, k-1)
627 let mut euler_row = vec![0u64; n + 1];
628 euler_row[0] = 1; // Row 0: A(0,0) = 1
629
630 for row in 1..=n {
631 let mut next_row = vec![0u64; row + 1];
632 for k in 0..=row {
633 let left = if k < row {
634 (k + 1) as u64 * euler_row[k]
635 } else {
636 0
637 };
638 let right = if k > 0 {
639 (row - k + 1) as u64 * euler_row[k - 1]
640 } else {
641 0
642 };
643 next_row[k] = left.saturating_add(right);
644 }
645 euler_row = next_row;
646 }
647
648 // Li_{-n}(z) = sum_{k=0}^{n-1} A(n, k) * z^{k+1} / (1-z)^{n+1}
649 let inv_1mz_n1 = {
650 // (1/(1-z))^{n+1}
651 let inv_1mz = Complex64::new(1.0, 0.0) / one_minus_z;
652 let mut p = Complex64::new(1.0, 0.0);
653 for _ in 0..=(n as u32) {
654 p *= inv_1mz;
655 }
656 p
657 };
658
659 let mut sum = Complex64::new(0.0, 0.0);
660 let mut z_pow = z; // z^1
661 for k in 0..n {
662 let coeff = euler_row[k] as f64;
663 sum += z_pow * Complex64::new(coeff, 0.0);
664 z_pow *= z;
665 }
666 Ok(sum * inv_1mz_n1)
667}
668
669/// Analytic continuation for |z| > 1 using the inversion formula.
670///
671/// For general real s > 1:
672/// ```text
673/// Li_s(z) = -Li_s(1/z) * e^{iπs}
674/// + (2πi)^s / Γ(s) · ζ(1-s) / (2πi) * ln(-z)^{s-1} [rough sketch]
675/// ```
676///
677/// For integer s ≥ 2, the standard inversion is:
678/// ```text
679/// Li_s(z) = -(-1)^s Li_s(1/z)
680/// + (2πi)^s / s! · B_s(ln(z)/(2πi))
681/// ```
682/// where B_s is the Bernoulli polynomial.
683///
684/// For simplicity we use the Euler–Zagier formula for large |z|.
685fn polylog_large_z(s: f64, z: Complex64) -> SpecialResult<Complex64> {
686 // Euler–Zagier inversion for large |z|:
687 // Li_s(z) = −Li_s(1/z) * e^{±iπs} ± correction terms
688 //
689 // Integer s case: clean inversion
690 if (s - s.round()).abs() < 1e-12 && s >= 2.0 {
691 let si = s.round() as i32;
692 // Li_n(z) = -(-1)^n Li_n(1/z) + correction
693 // correction = -(2πi)^n / n! * B_n(ln(z) / (2πi) + 1/2)
694 // For the correction we use the Bernoulli polynomial identity.
695 // For real z > 1 (principal branch):
696 let z_inv = Complex64::new(1.0, 0.0) / z;
697 let li_inv = polylog_series(s, z_inv)?;
698
699 // Sign factor (-1)^n
700 let sign = if si % 2 == 0 { -1.0_f64 } else { 1.0_f64 };
701
702 // Correction via ln(z):
703 // For real z > 1 (taking principal branch): correction = (2πi)^s/Γ(s+1) * ln(z)^s ... simplified
704 // Full analytic continuation (non-trivial); for now use Euler–Maclaurin fallback
705 // when far from singularity.
706 let log_z = complex_ln(z);
707 let two_pi = 2.0 * PI;
708 let two_pi_i_s = Complex64::new(0.0, two_pi).powc(Complex64::new(s, 0.0));
709
710 // Bernoulli polynomial correction via B_n(t) for t = log_z / (2πi) + 1/2
711 let t = log_z / Complex64::new(0.0, two_pi) + Complex64::new(0.5, 0.0);
712 let bn = bernoulli_poly_complex(si as usize, t);
713 let factorial_s = gamma_approx(s + 1.0);
714 let correction = two_pi_i_s * bn / Complex64::new(factorial_s, 0.0);
715
716 return Ok(Complex64::new(sign, 0.0) * li_inv + correction);
717 }
718
719 // Non-integer s: fall back to Euler transform after mapping 1/z
720 let z_inv = Complex64::new(1.0, 0.0) / z;
721 // Li_s(z) for |z| > 1 using: z → 1/z mapping requires Γ function ratios
722 // For now use Euler transform on 1/z and compute correction.
723 // This is approximate for non-integer s far from integer s.
724 let li_inv = polylog_euler_transform(s, z_inv)?;
725
726 // Apply rough sign correction (valid for z real and positive > 1)
727 let log_z = complex_ln(z);
728 let correction = Complex64::new(0.0, -2.0 * PI) / Complex64::new(gamma_approx(s), 0.0)
729 * log_z.powc(Complex64::new(s - 1.0, 0.0));
730
731 let sign = if (s as i64) % 2 == 0 {
732 -1.0_f64
733 } else {
734 1.0_f64
735 };
736 Ok(Complex64::new(sign, 0.0) * li_inv + correction)
737}
738
739/// Euler series transformation for polylogarithm in the intermediate zone.
740///
741/// Uses the Euler acceleration of the alternating series formed by writing
742/// z = -w where |w| < 1 when Re(z) < 0, or direct summation with Kahan
743/// compensation otherwise.
744fn polylog_euler_transform(s: f64, z: Complex64) -> SpecialResult<Complex64> {
745 // For |z| close to 1 we can write z = e^{iθ} * r and use the
746 // polylogarithm–zeta relation for the imaginary part.
747 // Fallback: direct series with compensated summation (Kahan / Neumaier).
748
749 let mut sum = Complex64::new(0.0, 0.0);
750 let mut comp = Complex64::new(0.0, 0.0); // Kahan compensation
751 let mut z_pow = z;
752
753 for k in 1..MAX_TERMS {
754 let k_s = (k as f64).powf(s);
755 let term = z_pow / Complex64::new(k_s, 0.0);
756
757 // Kahan summation
758 let y = term - comp;
759 let new_sum = sum + y;
760 comp = (new_sum - sum) - y;
761 sum = new_sum;
762
763 if term.norm() < TOL * sum.norm().max(1e-300) && k > 5 {
764 return Ok(sum);
765 }
766 if !sum.re.is_finite() || !sum.im.is_finite() {
767 return Err(SpecialError::OverflowError(
768 "polylog_euler_transform: series overflowed".to_string(),
769 ));
770 }
771 z_pow *= z;
772 }
773
774 if sum.re.is_finite() && sum.im.is_finite() {
775 Ok(sum)
776 } else {
777 Err(SpecialError::ConvergenceError(
778 "polylog_euler_transform: series did not converge".to_string(),
779 ))
780 }
781}
782
783/// Bernoulli polynomial B_n(t) evaluated at complex t.
784///
785/// Uses the explicit formula: B_n(t) = Σ_{k=0}^n C(n,k) B_k t^{n-k}
786/// where B_k are the Bernoulli numbers.
787fn bernoulli_poly_complex(n: usize, t: Complex64) -> Complex64 {
788 // Bernoulli numbers B_0..B_20
789 const BERN: [f64; 21] = [
790 1.0,
791 -0.5,
792 1.0 / 6.0,
793 0.0,
794 -1.0 / 30.0,
795 0.0,
796 1.0 / 42.0,
797 0.0,
798 -1.0 / 30.0,
799 0.0,
800 5.0 / 66.0,
801 0.0,
802 -691.0 / 2730.0,
803 0.0,
804 7.0 / 6.0,
805 0.0,
806 -3617.0 / 510.0,
807 0.0,
808 43867.0 / 798.0,
809 0.0,
810 -174611.0 / 330.0,
811 ];
812
813 if n > 20 {
814 // Fallback: zero approximation for very large n
815 return Complex64::new(0.0, 0.0);
816 }
817
818 let mut result = Complex64::new(0.0, 0.0);
819 let mut binom = 1.0f64; // C(n, k)
820
821 for k in 0..=n {
822 let bk = if k <= 20 { BERN[k] } else { 0.0 };
823 let t_pow = t.powc(Complex64::new((n - k) as f64, 0.0));
824 result += Complex64::new(binom * bk, 0.0) * t_pow;
825
826 // Update binomial coefficient C(n, k+1) = C(n, k) * (n - k) / (k + 1)
827 if k < n {
828 binom *= (n - k) as f64 / (k + 1) as f64;
829 }
830 }
831 result
832}
833
834/// Fast approximation of Γ(x) for positive x using Stirling / Lanczos.
835///
836/// Used only internally for the analytic continuation correction term.
837fn gamma_approx(x: f64) -> f64 {
838 // Use the Lanczos approximation with g=7
839 if x < 0.5 {
840 // Reflection formula: Γ(x) = π / (sin(πx) Γ(1-x))
841 let sin_pi_x = (PI * x).sin();
842 if sin_pi_x.abs() < 1e-300 {
843 return f64::INFINITY;
844 }
845 return PI / (sin_pi_x * gamma_approx(1.0 - x));
846 }
847
848 // Lanczos coefficients for g = 7, n = 9
849 const G: f64 = 7.0;
850 const LANCZOS_P: [f64; 9] = [
851 0.999_999_999_999_809_9,
852 676.520_368_121_885_1,
853 -1_259.139_216_722_402_8,
854 771.323_428_777_653_1,
855 -176.615_029_162_140_6,
856 12.507_343_278_686_905,
857 -0.138_571_095_265_720_12,
858 9.984_369_578_019_572e-6,
859 1.505_632_735_149_311_6e-7,
860 ];
861
862 let x_adj = x - 1.0;
863 let t = x_adj + G + 0.5;
864 let sqrt_2pi = (2.0 * PI).sqrt();
865
866 let mut series = LANCZOS_P[0];
867 for (i, &p) in LANCZOS_P[1..].iter().enumerate() {
868 series += p / (x_adj + (i + 1) as f64);
869 }
870
871 sqrt_2pi * t.powf(x_adj + 0.5) * (-t).exp() * series
872}
873
874/// Riemann zeta function ζ(s) for real s > 1 via Euler-Maclaurin.
875///
876/// This is an internal helper for computing Li_s(1) = ζ(s).
877/// Uses direct summation with Euler–Maclaurin tail correction.
878fn riemann_zeta_approx(s: f64) -> f64 {
879 if s <= 1.0 {
880 return f64::INFINITY;
881 }
882
883 // Known exact values (match on s rounded to nearest integer)
884 match s.round() as i64 {
885 2 => return PI * PI / 6.0, // ζ(2) = π²/6
886 4 => return PI.powi(4) / 90.0, // ζ(4) = π⁴/90
887 6 => return PI.powi(6) / 945.0, // ζ(6) = π⁶/945
888 8 => return PI.powi(8) / 9450.0, // ζ(8) = π⁸/9450
889 _ => {}
890 }
891
892 // Direct summation with Euler-Maclaurin tail
893 const N: usize = 1000;
894 let mut sum = 0.0f64;
895 for k in 1..=N {
896 sum += (k as f64).powf(-s);
897 }
898 // Tail correction: ∫_N^∞ t^{-s} dt = N^{1-s} / (s-1)
899 let tail = (N as f64).powf(1.0 - s) / (s - 1.0);
900 // First-order Euler-Maclaurin correction: + N^{-s}/2
901 let em1 = 0.5 * (N as f64).powf(-s);
902 // Second-order: + s * N^{-s-1} / 12
903 let em2 = s * (N as f64).powf(-s - 1.0) / 12.0;
904
905 sum + tail + em1 + em2
906}
907
908// ── Tests ─────────────────────────────────────────────────────────────────────
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913 use approx::assert_relative_eq;
914
915 // ── polylogarithm ────────────────────────────────────────────────────────
916
917 #[test]
918 fn test_polylog_z_zero() {
919 let val = polylogarithm(2.0, Complex64::new(0.0, 0.0)).expect("Li_2(0)");
920 assert!(val.norm() < 1e-14);
921 }
922
923 #[test]
924 fn test_polylog_li2_at_one() {
925 // Li_2(1) = π²/6
926 let val = polylogarithm(2.0, Complex64::new(1.0, 0.0)).expect("Li_2(1)");
927 let expected = PI * PI / 6.0;
928 assert!(
929 (val.re - expected).abs() < 1e-8,
930 "Li_2(1) = {}, expected {expected}",
931 val.re
932 );
933 }
934
935 #[test]
936 fn test_polylog_li2_at_minus_one() {
937 // Li_2(-1) = -π²/12
938 let val = polylogarithm(2.0, Complex64::new(-1.0, 0.0)).expect("Li_2(-1)");
939 let expected = -PI * PI / 12.0;
940 assert!(
941 (val.re - expected).abs() < 1e-8,
942 "Li_2(-1) = {}, expected {}",
943 val.re,
944 expected
945 );
946 }
947
948 #[test]
949 fn test_polylog_li2_at_half() {
950 // Li_2(1/2) = π²/12 - (ln 2)²/2
951 let val = polylogarithm(2.0, Complex64::new(0.5, 0.0)).expect("Li_2(0.5)");
952 let expected = PI * PI / 12.0 - (2.0_f64.ln()).powi(2) / 2.0;
953 assert!(
954 (val.re - expected).abs() < 1e-8,
955 "Li_2(0.5) = {}, expected {expected}",
956 val.re
957 );
958 }
959
960 #[test]
961 fn test_polylog_li1() {
962 // Li_1(0.5) = ln(2)
963 let val = polylogarithm(1.0, Complex64::new(0.5, 0.0)).expect("Li_1(0.5)");
964 assert!(
965 (val.re - 2.0_f64.ln()).abs() < 1e-10,
966 "Li_1(0.5) = {}",
967 val.re
968 );
969 }
970
971 #[test]
972 fn test_polylog_li0() {
973 // Li_0(z) = z/(1-z)
974 let z = 0.5_f64;
975 let val = polylogarithm(0.0, Complex64::new(z, 0.0)).expect("Li_0");
976 let expected = z / (1.0 - z);
977 assert!(
978 (val.re - expected).abs() < 1e-13,
979 "Li_0(0.5) = {}, expected {expected}",
980 val.re
981 );
982 }
983
984 #[test]
985 fn test_polylog_li_neg1() {
986 // Li_{-1}(z) = z / (1-z)^2
987 let z = 0.5_f64;
988 let val = polylogarithm(-1.0, Complex64::new(z, 0.0)).expect("Li_{-1}");
989 let expected = z / (1.0 - z).powi(2);
990 assert!(
991 (val.re - expected).abs() < 1e-10,
992 "Li_{{-1}}(0.5) = {}, expected {expected}",
993 val.re
994 );
995 }
996
997 #[test]
998 fn test_polylog_li3_at_one() {
999 // Li_3(1) = ζ(3) = Apéry's constant ≈ 1.20206
1000 let val = polylogarithm(3.0, Complex64::new(1.0, 0.0)).expect("Li_3(1)");
1001 let apery = 1.202_056_903_159_594;
1002 assert!(
1003 (val.re - apery).abs() < 1e-6,
1004 "Li_3(1) = {}, expected {apery}",
1005 val.re
1006 );
1007 }
1008
1009 #[test]
1010 fn test_polylog_s1_pole() {
1011 // Li_1(1) should return an error (pole)
1012 assert!(polylogarithm(1.0, Complex64::new(1.0, 0.0)).is_err());
1013 }
1014
1015 // ── dilogarithm ──────────────────────────────────────────────────────────
1016
1017 #[test]
1018 fn test_dilogarithm_alias() {
1019 let v1 = dilogarithm(Complex64::new(0.5, 0.0)).expect("dilogarithm");
1020 let v2 = polylogarithm(2.0, Complex64::new(0.5, 0.0)).expect("polylogarithm");
1021 assert!((v1.re - v2.re).abs() < 1e-14);
1022 }
1023
1024 // ── lerch_phi ────────────────────────────────────────────────────────────
1025
1026 #[test]
1027 fn test_lerch_phi_relation_to_polylog() {
1028 // Li_s(z) = z * Φ(z, s, 1)
1029 let z = 0.4_f64;
1030 let s = 3.0_f64;
1031 let phi = lerch_phi(z, s, 1.0).expect("lerch_phi");
1032 let li = polylogarithm(s, Complex64::new(z, 0.0)).expect("polylog");
1033 assert!(
1034 (z * phi - li.re).abs() < 1e-7,
1035 "Lerch vs polylog: {phi} vs {}",
1036 li.re
1037 );
1038 }
1039
1040 #[test]
1041 fn test_lerch_phi_hurwitz_zeta() {
1042 // Φ(1, 2, 1) = ζ(2) = π²/6
1043 let val = lerch_phi(1.0 - 1e-10, 2.0, 1.0).expect("lerch_phi ≈ zeta");
1044 let expected = PI * PI / 6.0;
1045 assert!(
1046 (val - expected).abs() < 0.001,
1047 "Φ(1,2,1) ≈ {val}, expected {expected}"
1048 );
1049 }
1050
1051 #[test]
1052 fn test_lerch_phi_domain_errors() {
1053 assert!(lerch_phi(0.5, 2.0, 0.0).is_err()); // a = 0
1054 assert!(lerch_phi(0.5, 2.0, -1.0).is_err()); // a < 0
1055 assert!(lerch_phi(2.0, 2.0, 1.0).is_err()); // |z| > 1
1056 }
1057
1058 // ── clausen_generalized ──────────────────────────────────────────────────
1059
1060 #[test]
1061 fn test_clausen_n2_maximum() {
1062 // Cl_2(π/3) ≈ 1.01494...
1063 let val = clausen_generalized(PI / 3.0, 2).expect("Cl_2(pi/3)");
1064 assert!((val - 1.01494160640965).abs() < 1e-5, "Cl_2(π/3) = {val}");
1065 }
1066
1067 #[test]
1068 fn test_clausen_n1_half_pi() {
1069 // Cl_1(π/2) = -ln(2 * sin(π/4)) = -ln(√2) = -0.5 * ln 2
1070 let val = clausen_generalized(PI / 2.0, 1).expect("Cl_1(pi/2)");
1071 let expected = -(0.5_f64 * 2_f64.ln());
1072 assert!(
1073 (val - expected).abs() < 1e-10,
1074 "Cl_1(π/2) = {val}, expected {expected}"
1075 );
1076 }
1077
1078 #[test]
1079 fn test_clausen_order_zero_error() {
1080 assert!(clausen_generalized(1.0, 0).is_err());
1081 }
1082
1083 #[test]
1084 fn test_clausen_n1_singular() {
1085 // Cl_1(0) is singular
1086 assert!(clausen_generalized(0.0, 1).is_err());
1087 }
1088
1089 // ── nielsen_polylog ──────────────────────────────────────────────────────
1090
1091 #[test]
1092 fn test_nielsen_n0_equals_polylog() {
1093 // S_{0,p}(z) = Li_p(z)
1094 let z = 0.5_f64;
1095 let s_val = nielsen_polylog(0, 2, z).expect("S_{0,2}");
1096 let li = polylogarithm(2.0, Complex64::new(z, 0.0)).expect("Li_2");
1097 assert!(
1098 (s_val - li.re).abs() < 1e-8,
1099 "S_{{0,2}} = {s_val}, Li_2 = {}",
1100 li.re
1101 );
1102 }
1103
1104 #[test]
1105 fn test_nielsen_s11_at_one() {
1106 // S_{1,1}(z) = Li_{1+1}(z) = Li_2(z), so S_{1,1}(1) = Li_2(1) = π²/6
1107 let val = nielsen_polylog(1, 1, 1.0).expect("S_{1,1}(1)");
1108 let expected = PI * PI / 6.0;
1109 assert!(
1110 (val - expected).abs() < 1e-8,
1111 "S_{{1,1}}(1) ≈ {val}, expected {expected}"
1112 );
1113 }
1114
1115 #[test]
1116 fn test_nielsen_p0_error() {
1117 assert!(nielsen_polylog(1, 0, 0.5).is_err());
1118 }
1119
1120 #[test]
1121 fn test_nielsen_z_too_large_error() {
1122 assert!(nielsen_polylog(1, 2, 1.5).is_err());
1123 }
1124
1125 // ── dilogarithm reflection ───────────────────────────────────────────────
1126
1127 #[test]
1128 fn test_reflection_identity() {
1129 // Li_2(z) + Li_2(1-z) = π²/6 - ln(z) * ln(1-z)
1130 let z = 0.3_f64;
1131 let (li2z, li2_1mz) = dilogarithm_reflection(z).expect("reflection");
1132 let lhs = li2z + li2_1mz;
1133 let rhs = PI * PI / 6.0 - z.ln() * (1.0 - z).ln();
1134 assert!((lhs - rhs).abs() < 1e-9, "reflection: lhs={lhs}, rhs={rhs}");
1135 }
1136
1137 #[test]
1138 fn test_reflection_z_half() {
1139 // At z = 1/2: Li_2(1/2) + Li_2(1/2) = π²/6 - (ln 2)²
1140 // ⟹ 2 * Li_2(1/2) = π²/6 - (ln 2)²
1141 // ⟹ Li_2(1/2) = π²/12 - (ln 2)²/2 ✓
1142 let (li2z, li2_1mz) = dilogarithm_reflection(0.5).expect("reflection 0.5");
1143 assert_relative_eq!(li2z, li2_1mz, epsilon = 1e-8); // by symmetry
1144 let expected = PI * PI / 12.0 - (2.0_f64.ln()).powi(2) / 2.0;
1145 assert!(
1146 (li2z - expected).abs() < 1e-8,
1147 "Li_2(0.5) via reflection: {li2z}"
1148 );
1149 }
1150
1151 #[test]
1152 fn test_reflection_domain_errors() {
1153 assert!(dilogarithm_reflection(0.0).is_err());
1154 assert!(dilogarithm_reflection(1.0).is_err());
1155 assert!(dilogarithm_reflection(-0.5).is_err());
1156 assert!(dilogarithm_reflection(1.5).is_err());
1157 }
1158
1159 // ── internal gamma_approx ────────────────────────────────────────────────
1160
1161 #[test]
1162 fn test_gamma_approx_integers() {
1163 // Γ(n+1) = n!
1164 let expected = [1.0_f64, 1.0, 2.0, 6.0, 24.0, 120.0];
1165 for (i, &e) in expected.iter().enumerate() {
1166 let val = gamma_approx((i + 1) as f64);
1167 assert!(
1168 (val - e).abs() < 1e-8 * e.max(1.0),
1169 "Γ({}) = {val}, expected {e}",
1170 i + 1
1171 );
1172 }
1173 }
1174}