checks.py

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

7520 bytes, as of the version from 2026-10-31 10:48 (current). Recorded here, not run.

"""Independent checks for T635, including all entries fetched from the draft.

    sage -python checks.py             # offline checks against generate.py
    sage -python checks.py --stored    # read T635 using NUMBERDB_API_KEY

This checker reconstructs the input series from canonical modular equations
(T309) and archived j coefficients (T79). It does not use the generator's eta
products, divisor sums, convolution helper, or Faber recurrence. All source
snapshots needed here are in check-data.json. A triangular Laurent cancellation
recovers each polynomial independently. Positive Laurent coefficients also
satisfy the twisted Hecke relation for prime-order Monster classes.
"""
import json
import os
import sys
from pathlib import Path
from functools import lru_cache
import numberdb.sage as numberdb
from sage.rings.integer_ring import ZZ
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing

PR = PolynomialRing(ZZ, 'X')
X = PR.gen()
QR = PolynomialRing(ZZ, 'q')
q = QR.gen()
MR = PolynomialRing(ZZ, names=('x', 'y'))
x, y = MR.gens()
CLASSES = ('1A', '2A', '2B', '3A', '3B', '5A', '5B', '7A', '7B', '13A', '13B')
DATA = json.loads(Path(__file__).with_name('check-data.json').read_text())
J = {int(k): ZZ(v) for k,v in DATA['j_coefficients'].items()}


@lru_cache(None)
def independent_coefficients(g, bound=60):
    if g == '1A':
        return {**{k:J[k] for k in range(-1,bound+1)},0:ZZ(0)}
    p = int(g[:-1]); exponent=24//(p-1); scale=ZZ(p)**(12//(p-1))
    phi = MR(DATA['canonical_equations'][str(p)])
    assert phi.degree(y)==1 and phi.monomial_coefficient(x*y)==-1
    a = phi + x*y
    assert all(b==0 for (i,b) in a.dict())
    # Phi(m,j)=A(m)-m*j=0, m=C*q+..., determines one new coefficient at a time.
    coeffs = {int(i):v for (i,b),v in a.dict().items()}
    m = QR(0)
    for k in range(1,bound+3):
        am = QR(0)
        for i in range(max(coeffs),-1,-1):
            am = (am*m+coeffs.get(i,0)).truncate(k)
        known = sum((J[i]*m[k-1-i] for i in range(k-1)), ZZ(0))
        m += (am[k-1]-known)*q**k
    assert m[1]==scale
    assert all(v % scale == 0 for v in m.list())
    h = [m[k+1]//scale for k in range(bound+2)]
    inv = [ZZ(1)]
    for k in range(1,bound+2):
        inv.append(-sum((h[i]*inv[k-i] for i in range(1,k+1)), ZZ(0)))
    assert inv[1]+exponent == 0
    c = {-1:ZZ(1),0:ZZ(0)}
    for k in range(1,bound+1):
        c[k] = inv[k+1] + (m[k] if g[-1]=='A' else 0)
    return c


def laurent_mul(a,b,upper):
    result = {}
    for i,ai in a.items():
        for j,bj in b.items():
            if i+j <= upper:
                result[i+j] = result.get(i+j,ZZ(0))+ai*bj
    return {k:v for k,v in result.items() if v}


@lru_cache(None)
def powers(g,max_degree=20):
    c = independent_coefficients(g)
    # After any intermediate product at most max_degree further factors occur,
    # each with minimum exponent -1. Keeping through max_degree+3 is enough
    # to certify final coefficients through q^3.
    upper=max_degree+3
    series={k:v for k,v in c.items() if k<=upper and v}
    result=[{0:ZZ(1)}]
    for n in range(1,max_degree+1):
        result.append(laurent_mul(result[-1],series,upper))
    return result


def evaluate_laurent(poly,g):
    result={}
    for i,coef in enumerate(poly.list()):
        for e,v in powers(g)[i].items():
            result[e]=result.get(e,ZZ(0))+coef*v
    return {e:v for e,v in result.items() if v}


def independent_polynomial(g,n):
    ps=powers(g); residual=dict(ps[n]); result=X**n
    for k in range(n-1,-1,-1):
        coefficient=residual.get(-k,ZZ(0))
        result-=coefficient*X**k
        for e,v in ps[k].items():
            residual[e]=residual.get(e,ZZ(0))-coefficient*v
    return result


def replication_coefficient(g,n,r):
    p = 1 if g=='1A' else int(g[:-1])
    answer=ZZ(0)
    for a in range(1,min(n,r)+1):
        if n%a==0 and r%a==0:
            power_class='1A' if p==1 or a%p==0 else g
            answer+=(n//a)*independent_coefficients(power_class)[n*r//(a*a)]
    return answer


def check_one(poly,g,n):
    assert poly.degree()==n and poly[n]==1
    assert poly[n-1]==0
    assert poly == independent_polynomial(g,n), (g,n,'triangular cancellation')
    substituted=evaluate_laurent(poly,g)
    assert {e:v for e,v in substituted.items() if e<=0} == {-n:ZZ(1)}, (g,n,'principal part')
    for r in range(1,4):
        assert substituted.get(r,0)==replication_coefficient(g,n,r), (g,n,r,'replication')


def run_checks(values):
    # Positive controls establish the input-series normalization and a nonzero
    # polynomial answer. Deliberate corruptions must fail before checking data.
    assert independent_coefficients('1A')[1]==196884
    assert independent_coefficients('2A')[1]==4372
    assert independent_coefficients('2B')[2]==-2048
    assert independent_coefficients('13A')[1]==12
    assert independent_coefficients('13B')[1]==-1
    assert independent_polynomial('1A',2)==X**2-393768
    assert independent_polynomial('2A',3)==X**3-13116*X-288768
    for bad in [X**2-393767,X**2+X-393768,PR(0)]:
        try: check_one(bad,'1A',2)
        except AssertionError: pass
        else: raise AssertionError('Negative control accepted')
    expected={(g,n) for g in CLASSES for n in range(2,21)}
    assert set(values)==expected
    for (g,n),poly in values.items():
        check_one(poly,g,n)
    # Explicit examples in Bannai-Kojima-Miezaki pp.24-25 use t_1A=T_1A+744
    # and t_2A=T_2A+104. Substitute X+shift to compare our zero-constant form.
    published={
      '1A':(744,[X**2-1488*X+159768,
         X**3-2232*X**2+1069956*X-36866976,
         X**4-2976*X**3+2533680*X**2-561444608*X+8507424792,
         X**5-3720*X**4+4550940*X**3-2028551200*X**2+246683410950*X-1963211493744,
         X**6-4464*X**5+7121736*X**4-4850017536*X**3+1304194222980*X**2-96687754014528*X+453039686271072]),
      '2A':(104,[X**2-208*X+2072,
         X**3-312*X**2+19332*X-49568,
         X**4-416*X**3+47408*X**2-1246976*X+1146904,
         X**5-520*X**4+86300*X**3-4909600*X**2+65094150*X-26542704,
         X**6-624*X**5+136008*X**4-12162304*X**3+397216644*X**2-2958154560*X+614211680])}
    for g,(shift,polys) in published.items():
        for n,poly in enumerate(polys,2):
            assert values[g,n]==poly(X+shift), (g,n,'published example')
    return {'entries':len(values),'principal_part_checks':209,
            'replication_coefficients':627,'published_polynomial_examples':10,
            'negative_controls_rejected':3,'series_source':'T79 and T309, independent of generator eta recurrence'}


def read_stored():
    from numberdb._generate import _stored_entries
    doc=numberdb.table('T635')
    # Use the client parser used by verify, preserving nested parameter groups.
    return {(identity.split(',')[0],int(identity.split(',')[1])):PR(value)
            for identity,value in _stored_entries(doc).items()}


if __name__=='__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN')=='1':
        key=sys.stdin.read().strip()
        if '=' in key and key.split('=',1)[0].strip().isidentifier():
            key=key.split('=',1)[1].strip().strip('\"\'')
        numberdb.configure(api_key=key)
    if '--stored' in sys.argv or os.environ.get('NUMBERDB_CHECK_STORED')=='1':
        values=read_stored()
    else:
        from generate import MonstrousFaber
        generator=MonstrousFaber()
        values={(p['g'],p['n']):generator.value(p,100) for p in generator.enumerate()}
    print(json.dumps(run_checks(values),indent=2))