generate.py

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

5029 bytes, as of the version from 2026-11-01 06:59 (current). Recorded here, not run.

"""Zagier traces of singular moduli -- numberdb.org/T643.

Run with SageMath and the numberdb package:
    sage -pip install numberdb           # once
    sage -python generate.py              # verify all stored entries
    sage -python generate.py --fill       # fill the existing draft
The fill operation needs NUMBERDB_API_KEY. It does not publish the draft.
For stdin credentials set NUMBERDB_KEY_FROM_STDIN=1. The campaign wrapper
also accepts NUMBERDB_PUBLISH=1 to select filling rather than verification.

Exact class polynomials are computed by FLINT/Arb via Sage. For every square
f^2 dividing d with e=d/f^2 a positive discriminant magnitude, compute the
trace of P_m(x-744) in Q[x]/H_{-e}, weighted by 1/3 for e=3, 1/2 for e=4,
and 1 otherwise. The square-divisor sum includes all imprimitive forms.
The trace is the sum of diagonal coefficients of multiplication by P_m(x-744).
All final rationals are required to be integers, with no floating rounding.
Faber polynomials are built from E4^3/Delta using finite integer products.

Default enumeration is m=1..4, then d=3..200 with d=0 or 3 modulo 4.
These are noncanonical finite extents of discrete and arithmetic families;
see argument-selection.md. No downloaded data or helper file is required.
"""
import os
import sys
from functools import lru_cache
from math import isqrt

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.schemes.elliptic_curves.cm import hilbert_class_polynomial

M_MAX = 4
D_MAX = 200
ZX = PolynomialRing(ZZ, 'X')
X = ZX.gen()
Qx = PolynomialRing(QQ, 'x')
x = Qx.gen()


def multiply(a, b, bound):
    out = [ZZ(0)] * (bound + 1)
    for i, ai in enumerate(a[:bound + 1]):
        for j, bj in enumerate(b[:bound + 1 - i]):
            out[i + j] += ai * bj
    return out


@lru_cache(None)
def j_positive_coefficients(limit):
    """Return coefficients c(n) of J=q^-1+sum c(n)q^n, 1<=n<=limit."""
    bound = limit + 1
    e4 = [ZZ(1)] + [ZZ(0)] * bound
    for divisor in range(1, bound + 1):
        for n in range(divisor, bound + 1, divisor):
            e4[n] += 240 * ZZ(divisor)**3
    numerator = multiply(multiply(e4, e4, bound), e4, bound)
    denominator = [ZZ(1)] + [ZZ(0)] * bound
    for n in range(1, bound + 1):
        for repetition in range(24):
            for k in range(bound, n - 1, -1):
                denominator[k] -= denominator[k - n]
    quotient = [ZZ(1)] + [ZZ(0)] * bound
    for n in range(1, bound + 1):
        quotient[n] = numerator[n] - sum(
            denominator[k] * quotient[n - k] for k in range(1, n + 1))
    assert quotient[1] == 744
    return tuple(quotient[n + 1] for n in range(1, limit + 1))


@lru_cache(None)
def faber(m):
    m = int(m)
    if m < 1:
        raise ValueError('Hecke order must be positive')
    c = (ZZ(0),) + j_positive_coefficients(m - 1)
    polynomials = {1: X}
    for n in range(2, m + 1):
        polynomials[n] = (X * polynomials[n - 1]
                          - sum(c[k] * polynomials[n - 1 - k]
                                for k in range(1, n - 1)) - n * c[n - 1])
    return polynomials[m]


@lru_cache(None)
def class_polynomial(e):
    return Qx(hilbert_class_polynomial(-ZZ(e), algorithm='arb'))


@lru_cache(None)
def primitive_trace(m, e):
    hpoly = class_polynomial(e)
    degree = hpoly.degree()
    expression = Qx(faber(m)(x - 744)) % hpoly
    trace = QQ(0)
    for k in range(degree):
        trace += expression[k]
        expression = (x * expression) % hpoly
    return trace


def trace(m, d):
    m, d = int(m), int(d)
    if m < 1 or d < 3 or d % 4 not in (0, 3):
        raise ValueError('Expected m>=1 and d>0 congruent to 0 or 3 modulo 4')
    total = QQ(0)
    for f in range(1, isqrt(d) + 1):
        if d % (f * f):
            continue
        e = d // (f * f)
        if e % 4 not in (0, 3):
            continue
        weight = 3 if e == 3 else 2 if e == 4 else 1
        total += primitive_trace(m, e) / QQ(weight)
    assert total.denominator() == 1, (m, d, total)
    return ZZ(total)


class ZagierTraces(numberdb.Generator):
    table = 'T643'
    parameters = ('m', 'd')
    type = 'Z'
    rigour = 'exact'

    def enumerate(self, m_max=M_MAX, d_max=D_MAX):
        for m in range(1, m_max + 1):
            for d in range(3, d_max + 1):
                if d % 4 in (0, 3):
                    yield {'m': m, 'd': d}

    def value(self, params, digits):
        return trace(params['m'], params['d'])


if __name__ == '__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
        numberdb.configure(api_key=sys.stdin.read().strip())
    generator = ZagierTraces()
    if '--fill' in sys.argv or os.environ.get('NUMBERDB_PUBLISH') == '1':
        print(generator.publish(message='Fill the draft with exact weighted CM traces'))
    else:
        report = generator.verify(sample=None)
        print(report)
        sys.exit(0 if report.ok else 1)