generate.py

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

8484 bytes, as of the version from 2026-09-19 13:58. Recorded here, not run.

"""Regulators of elliptic curves over real quadratic fields -- numberdb.org/T293.

Run it with SageMath:

    $ sage -pip install numberdb          # once
    $ sage -python generate.py            # preview the changes
    $ sage -python generate.py --publish  # send them

The file reads the pinned ecnf-data commit named below and writes regulators
of positive-rank elliptic curves over the six real quadratic fields of
smallest discriminant, with conductor norm at most 250. It keeps 35
significant digits from ecnf-data's `reg` field, checks the BSD quotient
against the recorded analytic order of Sha, and checks the D=21 rank-2
regulators by recomputing the height-pairing determinant from the recorded
generators and equations.
"""

import os
import re
import sys
from decimal import Decimal, getcontext
from urllib.request import urlopen

import numberdb.sage as numberdb
from sage.misc.sage_eval import sage_eval
from sage.rings.number_field.number_field import NumberField
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
from sage.rings.rational_field import QQ
from sage.rings.real_mpfr import RR
from sage.schemes.elliptic_curves.constructor import EllipticCurve


COMMIT = "10b28418e80392032b106ea00e6c5aa109d28e7b"
BASE = f"https://raw.githubusercontent.com/JohnCremona/ecnf-data/{COMMIT}/RQF"
FIELDS = (5, 8, 12, 13, 17, 21)
CONDUCTOR_NORM_BOUND = 250
DIGITS = 35
BSD_TOLERANCE = Decimal("1e-25")

getcontext().prec = 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 source_file(kind, D):
    return urlopen(f"{BASE}/{kind}.2.2.{D}.1").read().decode().splitlines()


def key_of(parts):
    return (parts[1], parts[2], parts[3])


def label_of(parts):
    return f"{parts[1]}-{parts[2]}{parts[3]}"


def conductor_norm(parts):
    return int(parts[1].split(".")[0])


def sig_trunc(text, digits=DIGITS):
    sign = ""
    text = text.strip()
    if text.startswith("-"):
        sign, text = "-", text[1:]
    out = []
    count = 0
    started = False
    for char in text:
        if char.isdigit():
            if char != "0" or started:
                started = True
                if count >= digits:
                    continue
                count += 1
            out.append(char)
        else:
            out.append(char)
    result = "".join(out)
    if result.endswith("."):
        result = result[:-1]
    return sign + result


def normalize_latex(text):
    text = text.replace("*w", "w")
    text = text.replace(r"\phi", "w")
    return re.sub(r"(?<![A-Za-z\\])a(?![A-Za-z])", "w", text)


def tamagawa_product(local_data):
    if not local_data or local_data == "[]":
        return 1
    product = 1
    for item in local_data.split(";"):
        if item:
            product *= int(item.split(":")[-1])
    return product


def field_polynomial(D):
    R = PolynomialRing(QQ, "x")
    x = R.gen()
    if D % 4 == 1:
        return x * x - x - QQ(D - 1) / QQ(4)
    return x * x - QQ(D) / QQ(4)


def field(D):
    return NumberField(field_polynomial(D), "w", check=False)


def field_element(K, pair):
    return QQ(pair[0]) + QQ(pair[1]) * K.gen()


def parse_nf_element(K, text):
    a, b = text.split(",")
    return QQ(a) + QQ(b) * K.gen()


def bsd_quotient(row, cp, D):
    r = int(row["rank"])
    torsion_order = Decimal(row["torsion_order"])
    lvalue = Decimal(row["lvalue"])
    omega = Decimal(row["omega"])
    regulator = Decimal(row["reg"])
    numerator = lvalue * torsion_order * torsion_order * Decimal(D).sqrt()
    denominator = (Decimal(2) ** r) * omega * regulator * Decimal(cp)
    return numerator / denominator


def read_rows():
    rows = []
    for D in FIELDS:
        curves = {}
        for line in source_file("curves", D):
            parts = line.split()
            curves[key_of(parts)] = {
                "ideal": normalize_latex(parts[4]),
                "ainvs": parts[6],
                "equation": normalize_latex(parts[10]),
            }
        local = {}
        for line in source_file("local_data", D):
            parts = line.split()
            local[key_of(parts)] = parts[4] if len(parts) > 4 else ""
        for line in source_file("mwdata", D):
            parts = line.split()
            if conductor_norm(parts) > CONDUCTOR_NORM_BOUND:
                continue
            if parts[4] == "?" or int(parts[4]) <= 0:
                continue
            curve = curves[key_of(parts)]
            row = {
                "D": D,
                "label": label_of(parts),
                "rank": parts[4],
                "rank_bounds": parts[5],
                "analytic_rank": parts[6],
                "ngens": parts[7],
                "gens": parts[8],
                "heights": parts[9],
                "reg": parts[10],
                "torsion_order": parts[11],
                "torsion_structure": parts[12],
                "torsion_gens": parts[13],
                "omega": parts[14],
                "lvalue": parts[15],
                "sha": parts[16],
                "ideal": curve["ideal"],
                "ainvs": curve["ainvs"],
                "equation": curve["equation"],
                "cp": tamagawa_product(local[key_of(parts)]),
            }
            check_source_row(row)
            rows.append(row)
    check_rank_two_determinants(rows)
    return rows


def check_source_row(row):
    rank = int(row["rank"])
    if rank == 1:
        height = row["heights"].strip()[1:-1]
        if sig_trunc(height, DIGITS) != sig_trunc(row["reg"], DIGITS):
            raise ValueError(f'{row["D"]} {row["label"]}: rank-1 height disagrees with reg')
    quotient = bsd_quotient(row, row["cp"], row["D"])
    sha = Decimal(row["sha"])
    if abs(quotient - sha) > BSD_TOLERANCE:
        raise ValueError(
            f'{row["D"]} {row["label"]}: BSD quotient {quotient} != Sha {sha}'
        )


def check_rank_two_determinants(rows):
    by_D = {}
    for row in rows:
        if int(row["rank"]) == 2:
            by_D.setdefault(row["D"], []).append(row)
    for D, rank_two_rows in by_D.items():
        K = field(D)
        for row in rank_two_rows:
            ainvs = [parse_nf_element(K, part) for part in row["ainvs"].split(";")]
            E = EllipticCurve(K, ainvs)
            gens = sage_eval(row["gens"], locals={"QQ": QQ})
            points = [
                E([field_element(K, coordinate) for coordinate in point])
                for point in gens
            ]
            determinant = RR(E.height_pairing_matrix(points).det())
            source = RR(row["reg"])
            relative = abs(determinant - source) / max(abs(source), RR("1e-99"))
            if relative > RR("1e-10"):
                raise ValueError(
                    f'{D} {row["label"]}: rank-2 determinant {determinant} '
                    f"disagrees with source {source}"
                )


class RealQuadraticEllipticRegulators(numberdb.Generator):
    table = "T293"
    parameters = ("D", "label")
    type = "R"
    digits = DIGITS
    rigour = "heuristic"

    def __init__(self):
        self.rows = read_rows()

    def enumerate(self):
        for row in self.rows:
            yield {"D": str(row["D"]), "label": row["label"]}

    def value(self, params, digits):
        D = int(params["D"])
        label = params["label"]
        for row in self.rows:
            if row["D"] == D and row["label"] == label:
                field_label = f"2.2.{D}.1-{label}"
                return {
                    "number": sig_trunc(row["reg"], digits),
                    "comment": (
                        f"LMFDB curve {field_label} has conductor ideal "
                        f'${row["ideal"]}$, rank ${row["rank"]}$, and equation '
                        f'${row["equation"]}$.'
                    ),
                }
        raise KeyError(params)


if __name__ == "__main__":
    key_from_stdin()
    generator = RealQuadraticEllipticRegulators()
    if os.environ.get("NUMBERDB_PUBLISH") == "1" or "--publish" in sys.argv:
        print(generator.publish(
            overwrite=False,
            message="refresh rank-2-aware generator for conductor norm <= 250",
        ))
    else:
        print(generator.preview(overwrite=False))