generate.py

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)