back to table · edit · history · where entries came from · files · download
8274 bytes, as of the version from 2026-09-23 19:18. Recorded here, not run.
"""Values of the Jacobi theta constant theta_4(0,q) -- numberdb.org/T426.
This generator fills T426 with theta_4(0,q) at every rational nome q = a/b in
lowest terms with b <= 12 and 0 < q <= 9/10 -- the arguments a reader is
likely to have written down, rather than the ones base ten makes short. The
bound matches T425, the nome itself, so the two tables are indexed alike.
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
Before publishing, the script compares every value with the defining q-series
using an explicit geometric tail bound, and checks the Jacobi identity
theta_3(0,q)^4 = theta_2(0,q)^4 + theta_4(0,q)^4 across the stored range.
"""
import os
import sys
import numberdb.sage as numberdb
from sage.rings.complex_arb import ComplexBallField
from sage.rings.rational_field import QQ
from sage.rings.real_arb import RealBallField
TABLE = os.environ.get("NUMBERDB_TABLE", "T426")
DIGITS = 100
WORKING_GUARD = 80
CHECK_GUARD = 256
#: Rationals of small height, not a decimal grid. Same bound as T425, which
#: holds q(m) itself: the two are read together and should be indexed alike.
#: The upper limit stays where it was -- beyond q = 9/10 the constant falls
#: below 1e-10 and keeps falling, and those are not values a computation hands
#: back.
DENOMINATOR_BOUND = 12
MAX_ARGUMENT = QQ(9) / QQ(10)
SERIES_TERMS = 100
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 _complex_field(digits, guard=WORKING_GUARD):
return ComplexBallField(numberdb.bits(digits, losing=guard))
def _real_field(digits, guard=WORKING_GUARD):
return RealBallField(numberdb.bits(digits, losing=guard))
def _arguments():
seen = set()
for denominator in range(2, DENOMINATOR_BOUND + 1):
for numerator in range(1, denominator):
value = QQ(numerator) / QQ(denominator)
if value <= MAX_ARGUMENT:
seen.add(value)
for value in sorted(seen):
yield value
def _tau_from_q(q, field):
return field(0, -1) * field(q).log() / field.pi()
def theta4_arb(q, digits, guard=WORKING_GUARD):
field = _complex_field(digits, guard)
value = field(0).jacobi_theta(_tau_from_q(QQ(q), field))[3]
if not (value.real().is_finite() and value.imag().is_finite()):
raise ArithmeticError("theta4(0,%s) produced a non-finite ball: %s" % (q, value))
if not value.imag().contains_zero():
raise ArithmeticError("theta4(0,%s) has nonzero imaginary part: %s" % (q, value.imag()))
real = value.real()
if not real.is_finite():
raise ArithmeticError("theta4(0,%s) produced a non-finite real ball: %s" % (q, real))
return real
def _series_tail_square(q, start, field):
q = field(q)
return 2 * q ** (start * start) / (1 - q ** (2 * start + 1))
def theta3_series(q, digits, terms=SERIES_TERMS, guard=CHECK_GUARD):
field = _real_field(digits, guard)
q = field(q)
value = field(1)
for n in range(1, terms):
value += 2 * q ** (n * n)
return value.add_error(_series_tail_square(q, terms, field))
def theta4_series(q, digits, terms=SERIES_TERMS, guard=CHECK_GUARD):
field = _real_field(digits, guard)
q = field(q)
value = field(1)
sign = -1
for n in range(1, terms):
value += 2 * sign * q ** (n * n)
sign *= -1
return value.add_error(_series_tail_square(q, terms, field))
def theta2_series(q, digits, terms=SERIES_TERMS, guard=CHECK_GUARD):
field = _real_field(digits, guard)
q = field(q)
value = field(0)
for n in range(terms):
value += q ** (n * (n + 1))
leading = q.sqrt().sqrt()
tail = 2 * leading * q ** (terms * (terms + 1)) / (1 - q ** (2 * terms + 2))
return (2 * leading * value).add_error(tail)
class JacobiTheta4ConstantValues(numberdb.Generator):
"""Generator for T426, values of theta_4(0,q)."""
table = TABLE
parameters = ("q",)
type = "R"
digits = DIGITS
rigour = "proven"
files = ("generate.py", "table.yaml")
def enumerate(self):
for q in _arguments():
yield {"q": str(q)}
def value(self, params, digits):
return theta4_arb(QQ(params["q"]), digits)
def run_integrity_checks():
field = _real_field(DIGITS, CHECK_GUARD)
worst_value_radius = field(0)
worst_value_at = None
worst_series_radius = field(0)
worst_series_at = None
worst_identity_radius = field(0)
worst_identity_at = None
for q in _arguments():
value = theta4_arb(q, DIGITS, guard=CHECK_GUARD)
series = theta4_series(q, DIGITS)
difference = value - series
if not difference.contains_zero():
raise ArithmeticError(
"theta4 q-series check failed at q=%s: %s vs %s" % (q, value, series)
)
theta2 = theta2_series(q, DIGITS)
theta3 = theta3_series(q, DIGITS)
theta4 = series
identity = theta3 ** 4 - theta2 ** 4 - theta4 ** 4
if not identity.contains_zero():
raise ArithmeticError("Jacobi identity failed at q=%s: %s" % (q, identity))
value_radius = field(value.rad())
if value_radius > worst_value_radius:
worst_value_radius = value_radius
worst_value_at = q
series_radius = field(series.rad())
if series_radius > worst_series_radius:
worst_series_radius = series_radius
worst_series_at = q
identity_radius = field(identity.rad())
if identity_radius > worst_identity_radius:
worst_identity_radius = identity_radius
worst_identity_at = q
print("integrity checks passed")
print("widest theta4 value ball radius: %s at q=%s" % (worst_value_radius, worst_value_at))
print("widest q-series check radius: %s at q=%s" % (worst_series_radius, worst_series_at))
print("widest identity check radius: %s at q=%s" % (worst_identity_radius, worst_identity_at))
def fill_draft_once(generator, message):
"""Fill a fresh prose 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 = JacobiTheta4ConstantValues()
run_integrity_checks()
if os.environ.get("NUMBERDB_PUBLISH") == "1" or "--publish" in sys.argv:
print(fill_draft_once(
generator,
message="Jacobi theta4 constants at rationals of small height",
))
else:
report = generator.verify(sample=None)
print(report)
sys.exit(0 if report.ok else 1)