generate.py

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

5656 bytes, as of the version from 2026-11-01 18:19. Recorded here, not run.

"""Minimal polynomials of monstrous McKay–Thompson CM values -- numberdb.org/T657.

Run with SageMath, with cm-inputs.json beside this file:
    sage -pip install numberdb
    sage -python generate.py
    sage -python generate.py --fill
The default verifies every stored entry. --fill writes entries to the existing
private draft, using NUMBERDB_API_KEY; it never offers or publishes the table.

The inputs are frozen exact H_{-4r} and canonical modular equations from T129
and T309. Let Phi_p(m,j)=A_p(m)-m*j and C=p^(12/(p-1)). Then
E(m)=m^h*H(A_p(m)/m) vanishes at m=C/R. Reciprocal substitution gives an
annihilator of R. Factor it over Q and select the unique factor not excluded
by an Arb enclosure of the eta quotient. Translate for B; take a resultant
with u^2+(b-x)*u+C for A, and again select the only possible factor.
This is exact elimination with certified factor exclusion, not integer-relation
recognition. All outputs are required to be monic irreducible in Z[x].

640 working bits leave considerably more than 100 decimal digits for the
embedding check. No approximation is stored as a polynomial coefficient.
See argument-selection.md, checks.py and numerical-evidence.json for coverage,
controls, independent finite-product checks and measured precision/size.
"""
import json
import os
import sys
from functools import lru_cache
from pathlib import Path

import numberdb.sage as numberdb
from sage.rings.integer_ring import ZZ
from sage.rings.rational_field import QQ
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
from sage.rings.complex_arb import ComplexBallField

BASE = Path(__file__).resolve().parent
INPUTS = json.loads((BASE / 'cm-inputs.json').read_text())
PRIMES = (2, 3, 5, 7, 13)
CLASSES = tuple(str(p) + letter for p in PRIMES for letter in ('A', 'B'))
WORKING_BITS = 640
ZX = PolynomialRing(ZZ, 'x')
x = ZX.gen()
QMX = PolynomialRing(QQ, ('u', 'x'))
u, xx = QMX.gens()
QMJ = PolynomialRing(QQ, ('x', 'y'))


def finite_precise(value, minimum=400):
    assert value.is_finite(), 'nonfinite enclosure'
    assert value.accuracy() >= minimum, 'insufficient enclosure precision'
    return value


@lru_cache(maxsize=None)
def quotient_ball(p, r, bits=WORKING_BITS):
    field = ComplexBallField(bits)
    tau = field.gen(0) * field(r).sqrt()
    quotient = (tau.modular_eta() / (p*tau).modular_eta()) ** (24 // (p-1))
    assert quotient.imag().contains_zero()
    real = finite_precise(quotient.real())
    assert real > 0
    return real


def select_factor(polynomial, enclosure):
    finite_precise(enclosure)
    polynomial = ZX(polynomial)
    candidates = []
    factors = polynomial.factor()
    assert ZX(factors.prod()) == polynomial
    for factor, exponent in factors:
        value = factor(enclosure)
        assert value.is_finite()
        if value.contains_zero():
            candidates.append(factor)
    assert len(candidates) == 1, 'embedding did not select exactly one factor'
    selected = candidates[0].change_ring(QQ).monic()
    selected = ZX(selected)  # refuses nonintegral coefficients
    assert selected.is_monic() and selected.is_irreducible()
    return selected


@lru_cache(maxsize=None)
def quotient_annihilator(p, r):
    phi = QMJ(INPUTS['canonical'][str(p)])
    a = ZX(phi(x, 0))
    assert phi == QMJ(a) - QMJ.gen(0)*QMJ.gen(1)
    assert a.degree() == p+1 and a.is_monic()
    hpoly = ZX(INPUTS['hilbert'][str(r)])
    h = hpoly.degree()
    e = sum((hpoly[i]*a**i*x**(h-i) for i in range(h+1)), ZX.zero())
    c = ZZ(p)**(12 // (p-1))
    n = e.degree()
    reciprocal = sum((e[i]*c**i*x**(n-i) for i in range(n+1)), ZX.zero())
    assert reciprocal.degree() == n and reciprocal[0] != 0
    return ZX([a // reciprocal.content() for a in reciprocal])


@lru_cache(maxsize=None)
def polynomials(p, r):
    b = ZZ(24 // (p-1))
    c = ZZ(p)**(12 // (p-1))
    rb = quotient_ball(p, r)
    mr = select_factor(quotient_annihilator(p, r), rb)
    pb = ZX(mr(x-b))
    resultant = QMX(mr(u)).resultant(u*u + (b-xx)*u + c, u)
    pa = select_factor(ZX(resultant.univariate_polynomial()), rb+b+c/rb)
    assert pb.is_monic() and pb.is_irreducible()
    return pa, pb


class MonstrousCMPolynomials(numberdb.Generator):
    table = 'T657'
    parameters = ('g', 'r')
    type = 'Z[]'
    rigour = 'exact'
    digits = 100
    files = ('generate.py', 'cm-inputs.json', 'checks.py', 'argument-selection.md')

    def enumerate(self):
        for g in CLASSES:
            for r in range(1, 11):
                yield {'g': g, 'r': r}

    def value(self, params, digits):
        g, r = str(params['g']), int(params['r'])
        assert g in CLASSES and 1 <= r <= 10
        pa, pb = polynomials(int(g[:-1]), r)
        polynomial = pa if g[-1] == 'A' else pb
        comment = (r'The selected root is HREF{Values_of_monstrous_McKay_Thompson_series_at_rectangular_CM_points#%s,%s}[$T_{%s}(i\sqrt{%s})$]; its order and level constraints are HREF{Hilbert_class_polynomials#%s}[$H_{%s}$] and HREF{Canonical_modular_polynomials#%s}[$\Phi^c_{%s}$].'
                   % (g, r, g, r, -4*r, -4*r, g[:-1], g[:-1]))
        return {'number': polynomial, 'comment': comment}


if __name__ == '__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN'):
        numberdb.configure(api_key=sys.stdin.read().strip())
    generator = MonstrousCMPolynomials()
    if '--fill' in sys.argv or os.environ.get('NUMBERDB_PUBLISH') == '1':
        print(generator.publish(message='Exact elimination with certified CM factor selection', assisted_by='codex-cli'))
    else:
        report = generator.verify(sample=None)
        print(report)
        sys.exit(0 if report.ok else 1)