back to table · edit · history · where entries came from · files · download
10531 bytes, as of the version from 2026-09-20 07:02 (current). Recorded here, not run.
"""Mahler measures m_k = m(x + x^-1 + y + y^-1 + k) -- numberdb.org/T282.
Run it with SageMath:
$ sage -pip install numberdb # once
$ sage -python generate.py # check the table against this code
$ sage -python generate.py --publish # fill the draft, with NUMBERDB_API_KEY set
The table stores nonnegative integer k, since replacing x and y by -x and
-y gives the same Mahler measure for -k. The values are computed as proven
real enclosures: Jensen's one-variable integral is used for k = 1, 2, 3, the
closed form 4G/pi for k = 4, and the absolutely convergent logarithmic series
for k >= 5.
"""
import os
import sys
import numberdb.sage as numberdb
from sage.arith.misc import binomial
from sage.rings.integer_ring import ZZ
from sage.rings.rational_field import QQ
from sage.rings.real_arb import RealBallField
from sage.rings.complex_arb import ComplexBallField
TABLE = os.environ.get("NUMBERDB_TABLE", "T282")
# Measured before filling the draft: k = 0..100 gives 101 entries, each
# written to 100 digits. The size limits leave room for more rows, but the
# range is meant as a reference for Boyd's integer family rather than a list
# of cheaply computable larger cases.
MAX_K = 100
# Bits beyond the requested digits. At 100 digits, the widest enclosure among
# k = 0..100 has radius below 2e-119 with this guard.
WORKING_GUARD = 96
# Terms are summed until the rigorous geometric tail is smaller than this many
# decimal orders beyond the requested digits.
TAIL_GUARD = 20
def _key_from_stdin():
if os.environ.get("NUMBERDB_KEY_FROM_STDIN") != "1":
return
token = sys.stdin.read().strip()
if "=" in token and token.split("=", 1)[0].isupper():
token = token.split("=", 1)[1].strip().strip("'\"")
if token:
os.environ["NUMBERDB_API_KEY"] = token
def _fields(digits):
bits = numberdb.bits(digits, losing=WORKING_GUARD)
return RealBallField(bits), ComplexBallField(bits)
def _integral(C, integrand, a, b):
value = C.integral(integrand, a, b)
if not value.imag().contains_zero():
raise ArithmeticError("the integral came out complex: %s" % value)
real = value.real()
if not real.is_finite():
raise ArithmeticError("the integral did not return a finite ball")
return real
def _small_k_integral(k, digits):
"""Jensen's formula after a substitution removing the endpoint singularity."""
if k <= 0 or k >= 4:
raise ValueError("the transformed integral is for k = 1, 2, 3")
R, C = _fields(digits)
kk = R(k)
root = kk.sqrt()
pi = R.pi()
def integrand(t, analytic):
sine = t.sin()
cosine = t.cos()
return (2 * root * cosine * (root * cosine / 2).arcsinh(analytic=analytic)
/ (1 - kk * sine * sine / 4).sqrt(analytic=analytic))
return _integral(C, integrand, C(0), C(pi / 2)) / pi
def _four_value(digits):
"""m_4 = 4G/pi, with G = L(2, chi_-4) evaluated by arb's Hurwitz zeta."""
R, _ = _fields(digits)
catalan = (R(2).zeta(R(QQ(1) / 4)) - R(2).zeta(R(QQ(3) / 4))) / 16
return 4 * catalan / R.pi()
def _large_k_series(k, digits):
"""log(k) - sum binomial(2n,n)^2/(2n k^(2n)), with a rigorous tail."""
if k <= 4:
raise ValueError("the large-k series needs k > 4")
R, _ = _fields(digits)
kk = ZZ(k)
k_ball = R(kk)
ratio = R(QQ(16) / QQ(kk * kk))
value = k_ball.log()
total = R(0)
target = R(10) ** (-(digits + TAIL_GUARD))
n = 1
while True:
central = ZZ(binomial(2 * n, n))
total += R(central * central) / (2 * n * k_ball ** (2 * n))
next_n = n + 1
tail = ratio ** next_n / (2 * next_n * (1 - ratio))
if tail < target:
value -= total
return value.add_error(tail.upper())
n = next_n
def mahler_measure(k, digits=100):
k = int(k)
if k < 0:
k = -k
if k == 0:
return ZZ(0)
if k < 4:
return _small_k_integral(k, digits)
if k == 4:
return _four_value(digits)
return _large_k_series(k, digits)
def _direct_jensen_integral(k, digits):
"""Independent one-dimensional Jensen integral, used as a check."""
R, C = _fields(digits)
kk = R(k)
pi = R.pi()
if k == 0:
return R(0)
if k < 4:
return _small_power_series(k, digits)
if k == 4:
def integrand(t, analytic):
return 4 * t.cos().arcsinh(analytic=analytic)
return _integral(C, integrand, C(0), C(pi / 2)) / pi
def integrand(t, analytic):
return ((kk + 2 * t.cos()) / 2).arccosh(analytic=analytic)
return _integral(C, integrand, C(0), C(pi)) / pi
def _small_power_series(k, digits):
"""Small-k power series from expanding the transformed Jensen integral."""
if not 0 <= k < 4:
raise ValueError("the small-k series is used for 0 <= k < 4")
if k == 0:
return ZZ(0)
R, _ = _fields(digits)
kk = ZZ(k)
total = R(0)
target = R(10) ** (-(digits + TAIL_GUARD))
# The summand is indexed by j from the asinh expansion and ell from the
# binomial expansion of (1 - k sin(t)^2/4)^(-1/2). Summing by total degree
# gives a reliable stopping test for k = 1, 2, 3, where the series is
# absolutely convergent.
consecutive_small = 0
n = 0
while True:
diagonal = R(0)
for j in range(n + 1):
ell = n - j
b = j + 1
integral_over_pi = (
QQ(binomial(2 * ell, ell) * binomial(2 * b, b))
/ (2 * QQ(4) ** (ell + b) * QQ(binomial(ell + b, ell))))
coefficient = (
QQ((-1) ** j)
* QQ(binomial(2 * j, j) * binomial(2 * ell, ell))
* integral_over_pi
/ (QQ(16) ** (j + ell) * QQ(2 * j + 1)))
diagonal += R(coefficient) * R(kk) ** (n + 1)
total += diagonal
if diagonal.abs() < target:
consecutive_small += 1
if consecutive_small >= 6:
return total.add_error((6 * target).upper())
else:
consecutive_small = 0
n += 1
if n > 1000:
raise ArithmeticError("small-k power series did not converge")
def _overlap_or_raise(label, left, right):
R = left.parent()
if not left.is_finite() or not right.is_finite():
raise ArithmeticError("%s produced a non-finite comparison" % label)
if not left.overlaps(R(right)):
raise ArithmeticError("%s: %s does not overlap %s" % (label, left, right))
def run_integrity_checks():
"""Check every row against a computation that does not share the main path."""
for k in range(MAX_K + 1):
computed = mahler_measure(k, 100)
if k == 0:
if computed != 0:
raise ArithmeticError("k=0 should be exact zero")
continue
independent = _direct_jensen_integral(k, 100)
_overlap_or_raise("k=%d" % k, computed, independent)
# The formulas should also agree with the public table that already holds
# the k = 4 value as the square-lattice spanning-tree entropy.
_overlap_or_raise("k=4 Catalan check", _four_value(100),
_direct_jensen_integral(4, 100))
def _entry_comment(k):
if k == 0:
return ("Here $x+x^{-1}+y+y^{-1}=x^{-1}y^{-1}(x+y)(xy+1)$, "
"so the Mahler measure is zero.")
if k == 1:
return ("Rogers and Zudilin proved "
"$m_1=\\frac{15}{4\\pi^2}L(E_{15},2)$ for the conductor "
"$15$ elliptic curve $E_{15}$ CITE{RogersZudilin}; by the "
"functional equation this is $L'(E_{15},0)$.")
if k == 4:
return ("This is $4G/\\pi$, the "
"HREF{Entropy_constants_of_lattice_models#spanning-tree,square,entropy}"
"[square-lattice spanning-tree entropy].")
return ""
class SquareLatticeMahlerMeasures(numberdb.Generator):
"""Generator for T282, the table of $m_k$."""
table = TABLE
parameters = ("k",)
type = "R"
digits = 100
rigour = "proven"
def enumerate(self, max_k=MAX_K):
for k in range(max_k + 1):
yield {"k": str(k)}
def value(self, params, digits):
k = int(params["k"])
entry = {"number": mahler_measure(k, digits)}
comment = _entry_comment(k)
if comment:
entry["comment"] = comment
if k == 4:
entry["equals"] = (
"HREF{Entropy_constants_of_lattice_models#spanning-tree,square,entropy}")
return entry
def fill_draft_once(generator, message):
"""Fill a fresh draft without the client's empty upsert probe."""
from numberdb._generate import (
_check_precision,
_check_rigour,
_producer,
_run_name,
_source_files,
)
from numberdb._write import Entries, attach, submit_entries, to_text
table = generator.table
run = _run_name(generator)
entries = Entries(*generator.parameters)
for params in generator.enumerate():
params = dict(params)
wanted = generator.digits_for(params)
entry = generator._entry(params, wanted)
value = entry["number"]
identity = ",".join(str(params[name]) for name in generator.parameters)
_check_rigour(generator, table, identity, value)
written = to_text(value, wanted, generator.format)
_check_precision(table, identity, written, wanted, lowering=False)
record = dict(entry)
record.pop("digits", None)
entries.add(**params, **record, digits=wanted)
answer = submit_entries(
table,
entries,
message=message,
produced_by=_producer(generator, os.environ.get("NUMBERDB_ASSISTED_BY", "")),
upsert=False,
run=run,
rigour=generator.rigour,
)
for name, body in sorted(_source_files(generator).items()):
attach(table, name, body, run=run, message=message,
rigour=generator.rigour)
return answer
if __name__ == "__main__":
_key_from_stdin()
generator = SquareLatticeMahlerMeasures()
run_integrity_checks()
if "--publish" in sys.argv or os.environ.get("NUMBERDB_PUBLISH") == "1":
print(fill_draft_once(
generator,
message="Mahler measures for x+x^-1+y+y^-1+k, k = 0..%d" % MAX_K))
else:
report = generator.verify(sample=None)
print(report)
sys.exit(0 if report.ok else 1)