Source code for mathxlab.nt.zeta
"""Numerical helpers for the Riemann zeta function and related quantities.
The implementations are intentionally simple and designed for educational /
exploratory experiments (not for high-precision research computations).
The module uses `mpmath` internally and exposes a small set of helpers for:
- partial Dirichlet/eta series
- truncated Euler products
- the Hardy Z-function
- the main-term zero counting approximation N(T)
"""
from __future__ import annotations
from collections.abc import Iterable, Iterator
from contextlib import contextmanager
from dataclasses import dataclass
import mpmath as mp
__all__ = [
"ZetaEvalSettings",
"chi_factor",
"eta_series_partial",
"euler_product_partial",
"hardy_Z",
"mp_workdps",
"riemann_von_mangoldt_count",
"zeta_series_partial",
"zeta_via_eta",
]
[docs]
@dataclass(frozen=True, slots=True)
class ZetaEvalSettings:
"""
Settings for zeta-related numerical evaluations.
Args:
dps: Decimal digits of precision used by mpmath during the computation.
Examples:
>>> from mathxlab.nt.zeta import ZetaEvalSettings
>>> ZetaEvalSettings # doctest: +SKIP
"""
dps: int = 50
[docs]
@contextmanager
def mp_workdps(dps: int) -> Iterator[None]:
"""
Temporarily set the mpmath precision (decimal digits).
Args:
dps: Decimal digits of precision.
Yields:
None. The previous precision is restored on exit.
Examples:
>>> from mathxlab.nt.zeta import mp_workdps
>>> mp_workdps # doctest: +SKIP
"""
old = mp.mp.dps
mp.mp.dps = int(dps)
try:
yield
finally:
mp.mp.dps = old
[docs]
def zeta_series_partial(
s: complex, n_max: int, *, settings: ZetaEvalSettings | None = None
) -> complex:
"""
Compute the partial Dirichlet series for the Riemann zeta function.
This computes:
sum_{n=1..n_max} n^{-s}
Args:
s: Complex exponent.
n_max: Number of terms.
settings: Optional evaluation settings.
Returns:
Complex partial sum as a Python complex number.
Examples:
>>> from mathxlab.nt.zeta import zeta_series_partial
>>> round(zeta_series_partial(2.0, 2000).real, 3)
1.644
"""
if n_max < 0:
raise ValueError("n_max must be >= 0")
if n_max == 0:
return 0.0 + 0.0j
cfg = settings or ZetaEvalSettings()
with mp_workdps(cfg.dps):
ss = mp.mpc(s)
total = mp.mpc(0)
for n in range(1, n_max + 1):
total += mp.power(n, -ss)
return complex(total)
[docs]
def eta_series_partial(
s: complex, n_max: int, *, settings: ZetaEvalSettings | None = None
) -> complex:
"""
Compute the partial Dirichlet eta series.
This computes:
eta(s) = sum_{n=1..n_max} (-1)^{n-1} n^{-s}
Args:
s: Complex exponent.
n_max: Number of terms.
settings: Optional evaluation settings.
Returns:
Complex partial sum as a Python complex number.
Examples:
>>> from mathxlab.nt.zeta import eta_series_partial
>>> eta_series_partial # doctest: +SKIP
"""
if n_max < 0:
raise ValueError("n_max must be >= 0")
if n_max == 0:
return 0.0 + 0.0j
cfg = settings or ZetaEvalSettings()
with mp_workdps(cfg.dps):
ss = mp.mpc(s)
total = mp.mpc(0)
sign = 1
for n in range(1, n_max + 1):
total += sign * mp.power(n, -ss)
sign = -sign
return complex(total)
[docs]
def zeta_via_eta(s: complex, eta_value: complex) -> complex:
"""
Recover zeta(s) from eta(s) via the identity.
For s != 1:
zeta(s) = eta(s) / (1 - 2^{1-s})
Args:
s: Complex argument.
eta_value: Value of eta(s) (possibly a partial sum).
Returns:
Complex approximation of zeta(s).
Examples:
>>> from mathxlab.nt.zeta import zeta_via_eta
>>> zeta_via_eta # doctest: +SKIP
"""
denom = 1.0 - complex(mp.power(2, 1 - mp.mpc(s)))
if denom == 0:
raise ZeroDivisionError("zeta_via_eta is singular at s=1")
return eta_value / denom
[docs]
def euler_product_partial(
s: complex,
primes: Iterable[int],
*,
settings: ZetaEvalSettings | None = None,
) -> complex:
"""
Compute a partial Euler product approximation of zeta(s).
This computes:
prod_{p in primes} (1 - p^{-s})^{-1}
Args:
s: Complex argument.
primes: Iterable of prime numbers.
settings: Optional evaluation settings.
Returns:
Complex approximation as a Python complex.
Examples:
>>> from mathxlab.nt.zeta import euler_product_partial
>>> euler_product_partial # doctest: +SKIP
"""
cfg = settings or ZetaEvalSettings()
with mp_workdps(cfg.dps):
ss = mp.mpc(s)
prod = mp.mpc(1)
for p in primes:
pp = int(p)
if pp <= 1:
raise ValueError("primes must be integers >= 2")
prod *= 1 / (1 - mp.power(pp, -ss))
return complex(prod)
[docs]
def chi_factor(s: complex, *, settings: ZetaEvalSettings | None = None) -> complex:
"""
Compute the factor chi(s) in the functional equation of zeta.
One convenient form is:
zeta(s) = chi(s) * zeta(1 - s)
where:
chi(s) = 2^s * pi^{s-1} * sin(pi s / 2) * Gamma(1 - s)
Args:
s: Complex argument.
settings: Optional evaluation settings.
Returns:
chi(s) as a Python complex.
Examples:
>>> from mathxlab.nt.zeta import chi_factor
>>> chi_factor # doctest: +SKIP
"""
r"""Compute the factor chi(s) in the functional equation of zeta.
One convenient form is:
zeta(s) = chi(s) * zeta(1 - s)
where:
chi(s) = 2^s * pi^{s-1} * sin(pi s / 2) * Gamma(1 - s)
Args:
s: Complex argument.
settings: Optional evaluation settings.
Returns:
chi(s) as a Python complex.
"""
cfg = settings or ZetaEvalSettings()
with mp_workdps(cfg.dps):
ss = mp.mpc(s)
return complex(
mp.power(2, ss) * mp.power(mp.pi, ss - 1) * mp.sin(mp.pi * ss / 2) * mp.gamma(1 - ss)
)
[docs]
def hardy_Z(t: float, *, settings: ZetaEvalSettings | None = None) -> float:
"""Compute Hardy's Z-function at height t."""
cfg = settings or ZetaEvalSettings()
with mp_workdps(cfg.dps):
return float(mp.siegelz(t))
[docs]
def riemann_von_mangoldt_count(T: float) -> float:
"""Return the Riemann--von Mangoldt main term for N(T)."""
if T <= 0:
raise ValueError("T must be > 0")
x = float(T) / (2.0 * float(mp.pi))
return float(x * (mp.log(x) - 1.0) + 0.875)