Skip to content

PolyGamma(m, z) at a complex z; Part of #340 - #359

Merged
arnog merged 3 commits into
cortex-js:mainfrom
enumeratio:polygamma-complex
Sep 28, 2026
Merged

arnog merged 3 commits into
cortex-js:mainfrom
enumeratio:polygamma-complex

Conversation

@enumeratio

Copy link
Copy Markdown
Contributor

Part of #340

PolyGamma(m, z) now evaluates at a complex z for every integer order. For m ≥ 1 it uses ψ⁽ᵐ⁾(z) = (−1)^(m+1) m! ζ(m+1, z) (DLMF 5.15.2) on the complex HurwitzZeta kernel; the digamma gets its own shift and asymptotic series. A non-positive integer z is still the pole at every order, and real z is unchanged.

At a high order and large negative Re(z), the Euler–Maclaurin sum cancels past what doubles can hold. There it stays unevaluated rather than answer, using a guard on the ratio of the largest term to the result. Checked against mpmath polygamma for m ≤ 8 over |z| ≤ 50 in every quadrant, near the poles and between them: no answered value off by more than 4e-13.

Also: the compiled JavaScript PolyGamma is now marked real-only, as Zeta and HurwitzZeta are. Before, a complex-typed argument compiled and ran the real kernel on it.

…5.15.2) and a digamma asymptotic series for m = 0; stay unevaluated where Euler-Maclaurin cancellation leaves no accurate digits; mark the JS compile lane real-only to match. Part of cortex-js#340
arnog and others added 2 commits September 28, 2026 16:31
Dual review (Codex + Claude), every value checked against mpmath.

Correctness:
- The shift loops ran once per unit of -Re(z) with no cap: PolyGamma(0,
  -1e12 + i).N() hung the thread, and the cancellation guard declined 11%
  of a left-half-plane grid. Both are replaced by the reflection formula
  for Re(z) < 1/2 (DLMF 5.15.6), with the m-th derivative of cot(pi z)
  taken from whichever of three series loses the fewest digits (partial
  fractions, Fourier, or the cot polynomial); cot(pi z) is built from
  e^{2 pi i z} so it does not cancel for a large |Im z|. Every declined grid
  point now answers within 1e-14, and the cost no longer depends on Re(z).
- m! and zeta were combined outside the double range: PolyGamma(171, 1+i)
  gave ComplexInfinity (a false pole; the value is -1.6e283 + 9e248i) and
  PolyGamma(100, 10000+i) a false 0 (the value is -9.4e-245). Values are
  carried as a mantissa times a power of two; an unrepresentable result
  stays symbolic. Two overflow/underflow causes in the complex helpers were
  fixed on the way (log of a huge argument, abs of a tiny one).
- The digamma tail formed w^2 … w^28 explicitly and overflowed for |z| above
  1e154 (PolyGamma(0, 1e160+i) was ComplexInfinity); it uses powers of
  1/w^2.
- Digamma(z) and Trigamma(z) now use the same complex kernel instead of
  staying symbolic while PolyGamma(0|1, z) evaluated.
- An order above 10 000 stays symbolic (the cost grows linearly with it).

Tests and documentation: the decline test pins the mpmath value; the
"answers accurately" test has an oracle value; CHANGELOG under
[Unreleased]; the remaining decline region (orders above 100 within 1e-5 of
a half-integer on the real axis) is in ROADMAP; `isComplex` spelling; the
unrelated Clamp reformat in error-model.test.ts is dropped.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@arnog
arnog merged commit c8596b3 into cortex-js:main Sep 28, 2026
@arnog

arnog commented Sep 29, 2026

Copy link
Copy Markdown
Member

Thanks, merged, with a review pass on your branch as cd010c21. The main corrections:

  • The shift loops ran once per unit of $−Re(z)$ with no cap (PolyGamma(0, -1e12 + i).N() hung), and the cancellation guard declined 11% of a left-half-plane grid. Both are replaced by the reflection formula for $Re(z) &lt; 1/2$ (DLMF 5.15.6); the m-th derivative of $\cot(\pi z)$ comes from whichever of three series loses the fewest digits, and $\cot(\pi z)$ is built from $e^{2\pi iz}$ so it does not cancel for a large $|Im z|$. Every declined grid point now answers within $1e−14$ and the cost no longer depends on $Re(z)$.
  • $m!$ and $ζ$ were combined outside the double range: PolyGamma(171, 1+i) gave ComplexInfinity (the value is $−1.6e283 + 9e248i$) and PolyGamma(100, 10000+i) a false $0$. Values are carried as a mantissa times a power of two; an unrepresentable result stays symbolic.
  • The digamma tail overflowed for $|z|$ above $1e154$; Digamma(z) and Trigamma(z) now use the same complex kernel; an order above $10,000$ stays symbolic.

The one region that still declines (orders above 100 within $1e−5$ of a half-integer on the real axis) are tracked in ROADMAP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants