generate.py

back to table · edit · history · where entries came from · files · download

9910 bytes, as of the version from 2026-09-22 11:54 (current). Recorded here, not run.

"""Tate parameters q_E of elliptic curves over Q -- numberdb.org/T409.

This generator fills T409 with the p-adic Tate parameters q_E of elliptic
curves over Q with split multiplicative reduction. The curves are the reduced
global minimal models in Sage's mini Cremona database with conductor N <= 100,
and the table stores every split multiplicative prime of each such curve.

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 values are computed by Sage's TateCurve.parameter with absolute p-adic
precision p^n >= 10^50. The integrity check verifies the split multiplicative
filter, the absolute precision, v_p(q_E) = v_p(Delta_E), and the q-expansion
of the j-invariant to the precision left after inverting q_E.
"""

import os
import re
import sys
from functools import lru_cache

import numberdb.sage as numberdb
from sage.databases.cremona import CremonaDatabase, cremona_to_lmfdb
from sage.rings.integer_ring import ZZ
from sage.schemes.elliptic_curves import ell_generic  # noqa: F401
from sage.schemes.elliptic_curves.constructor import EllipticCurve


TABLE = os.environ.get("NUMBERDB_TABLE", "T409")
CONDUCTOR_BOUND = 100
DIGITS = 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
        numberdb.configure(api_key=token)


def _label_key(label):
    found = re.fullmatch(r"([a-z]+)(\d+)", label)
    if found is None:
        return label, 0
    return found.group(1), int(found.group(2))


def target_absolute_precision(p):
    """The least n with p^n >= 10^50."""
    p = ZZ(p)
    n = ZZ(1)
    power = p
    bound = ZZ(10) ** 50
    while power < bound:
        n += 1
        power *= p
    return n


def invariants(ainvs):
    a1, a2, a3, a4, a6 = [ZZ(a) for a in ainvs]
    b2 = a1 * a1 + 4 * a2
    b4 = a1 * a3 + 2 * a4
    b6 = a3 * a3 + 4 * a6
    c4 = b2 * b2 - 24 * b4
    c6 = -b2 * b2 * b2 + 36 * b2 * b4 - 216 * b6
    return c4, c6


def curve_from_ainvs(ainvs):
    return EllipticCurve([ZZ(a) for a in ainvs])


@lru_cache(maxsize=1)
def tate_rows():
    rows = []
    database = CremonaDatabase()
    for conductor in range(1, CONDUCTOR_BOUND + 1):
        curves = database.allcurves(conductor)
        for label, record in sorted(curves.items(), key=lambda item: _label_key(item[0])):
            ainvs, _rank, _torsion_order = record
            ainvs = tuple(ZZ(a) for a in ainvs)
            curve = curve_from_ainvs(ainvs)
            c4, c6 = invariants(ainvs)
            for p in sorted(ZZ(conductor).prime_divisors()):
                p = ZZ(p)
                if not curve.has_split_multiplicative_reduction(p):
                    continue
                rows.append({
                    "N": ZZ(conductor),
                    "label": label,
                    "lmfdb_label": cremona_to_lmfdb("%s%s" % (conductor, label)),
                    "ainvs": ainvs,
                    "c4": c4,
                    "c6": c6,
                    "p": p,
                    "valuation": ZZ(curve.local_data(p).discriminant_valuation()),
                    "target": target_absolute_precision(p),
                })
    return tuple(rows)


def row_for_params(params):
    wanted = (ZZ(params["N"]), ZZ(params["c4"]), ZZ(params["c6"]), ZZ(params["p"]))
    for row in tate_rows():
        found = (row["N"], row["c4"], row["c6"], row["p"])
        if found == wanted:
            return row
    raise KeyError("no Tate parameter row has parameters %s" % (wanted,))


def tate_parameter(row):
    curve = curve_from_ainvs(row["ainvs"])
    relative = max(ZZ(1), row["target"] - row["valuation"])
    return curve.tate_curve(row["p"]).parameter(relative)


def entry_comment(row):
    cremona_label = "%s%s" % (row["N"], row["label"])
    lmfdb_label = row["lmfdb_label"]
    if lmfdb_label.replace(".", "") == cremona_label:
        return "Cremona label %s." % cremona_label
    return "Cremona label %s; LMFDB label %s." % (cremona_label, lmfdb_label)


def _ceil_div(a, b):
    return -(-ZZ(a) // ZZ(b))


def j_from_tate_parameter(q, required):
    """Compute j(q) from E4^3 / Delta, truncated past the required precision."""
    field = q.parent()
    one = field(1)
    q_power = one
    e4 = one
    delta_unit = one
    valuation = ZZ(q.valuation())
    terms = max(ZZ(1), _ceil_div(required, valuation) + 5)
    for n in range(1, int(terms) + 1):
        q_power *= q
        e4 += field(240 * n**3) * q_power / (one - q_power)
        delta_unit *= (one - q_power) ** 24
    return (e4 ** 3) / (q * delta_unit)


def check_j_invariant(row, q):
    curve = curve_from_ainvs(row["ainvs"])
    required = row["target"] - 2 * ZZ(q.valuation())
    if required <= 0:
        return
    found = j_from_tate_parameter(q, required)
    expected = q.parent()(curve.j_invariant())
    difference = found - expected
    if difference != 0 and difference.valuation() < required:
        raise ArithmeticError(
            "%s p=%s: j(q) agrees only to valuation %s, expected at least %s"
            % ("%s%s" % (row["N"], row["label"]), row["p"],
               difference.valuation(), required)
        )


def run_integrity_checks():
    rows = tate_rows()
    if len(rows) != 214:
        raise ArithmeticError("expected 214 Tate parameters, found %d" % len(rows))

    longest = ("", 0)
    by_prime = {}
    for row in rows:
        curve = curve_from_ainvs(row["ainvs"])
        if not curve.has_split_multiplicative_reduction(row["p"]):
            raise ArithmeticError(
                "%s p=%s is not split multiplicative"
                % ("%s%s" % (row["N"], row["label"]), row["p"])
            )
        q = tate_parameter(row)
        if q.precision_absolute() != row["target"]:
            raise ArithmeticError(
                "%s p=%s has absolute precision %s, expected %s"
                % ("%s%s" % (row["N"], row["label"]), row["p"],
                   q.precision_absolute(), row["target"])
            )
        if q.valuation() != row["valuation"]:
            raise ArithmeticError(
                "%s p=%s has valuation %s, expected %s"
                % ("%s%s" % (row["N"], row["label"]), row["p"],
                   q.valuation(), row["valuation"])
            )
        check_j_invariant(row, q)
        key = "%s%s, p=%s" % (row["N"], row["label"], row["p"])
        length = len(str(q))
        if length > longest[1]:
            longest = (key, length)
        by_prime[int(row["p"])] = by_prime.get(int(row["p"]), 0) + 1

    print("integrity checks passed for %d Tate parameters" % len(rows))
    print("checked split multiplicative reduction and absolute p-adic precision")
    print("checked v_p(q_E) = v_p(Delta_E) and the j(q) expansion on every row")
    print("longest value has %d characters at %s" % (longest[1], longest[0]))
    print("entries by prime: %s" % sorted(by_prime.items()))


class TateParametersEllipticCurvesQ(numberdb.Generator):
    table = TABLE
    parameters = ("N", "c4", "c6", "p")
    type = "Qp"
    digits = DIGITS
    rigour = "proven"

    def enumerate(self):
        for row in tate_rows():
            yield {
                "N": str(row["N"]),
                "c4": str(row["c4"]),
                "c6": str(row["c6"]),
                "p": str(row["p"]),
            }

    def value(self, params, digits):
        row = row_for_params(params)
        return {
            "number": tate_parameter(row),
            "comment": entry_comment(row),
        }


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)
        bounded = True if generator.type == "Qp" else None
        _check_rigour(generator, table, identity, value, bounded=bounded)

        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 = TateParametersEllipticCurvesQ()
    run_integrity_checks()
    if "--publish" in sys.argv or os.environ.get("NUMBERDB_PUBLISH") == "1":
        print(fill_draft_once(
            generator,
            message="fill Tate-parameter draft from Sage Tate curves",
        ))
    elif os.environ.get("NUMBERDB_CHECK_ONLY") == "1":
        sys.exit(0)
    else:
        report = generator.verify(sample=None)
        print(report)
        sys.exit(0 if report.ok else 1)