generate.py

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

5724 bytes, as of the version from 2026-10-27 21:27 (current). Recorded here, not run.

"""Area Siegel–Veech constants of Abelian strata -- numberdb.org/T561.

Run with SageMath (or a modular Sage installation):
    sage -pip install numberdb
    sage -python generate.py             # verify every stored entry
    sage -python checks.py               # independent checks, no writes

To fill this same draft, an authorized contributor can use
    AreaSiegelVeech().publish(client=numberdb.Client(api_key=...))
The method writes entries; it does not publish or offer the draft.

The exact rightmost column of Eskin–Kontsevich–Zorich, arXiv:1112.5872v6,
Appendix A, pp. 100–101, supplies the genus-three and genus-four Lyapunov
sums. Genus two uses Corollary 2, p. 15. Theorem 1, equation (2.1), gives
c_area = 3*(L - sum(m*(m+2)/(m+1))/12)/pi**2. Only the exact sums are used,
never the experimental individual Lyapunov exponents in adjacent columns.

All 25 components of all positive zero partitions in genera 2–4 are included.
The default enumeration generates partitions and applies the KZ classification;
it is not a Cartesian product of profiles and component labels. See the attached
argument-selection.md. Every arithmetic division is between exact Sage values
or certified balls. A 64-bit guard is measured by checks.py at 100 digits.
"""
import os
import sys
import numberdb.sage as numberdb
from sage.rings.integer_ring import ZZ
from sage.rings.rational_field import QQ
from sage.rings.real_arb import RealBallField

WORKING_GUARD = 64
# (genus, positive zero profile, component, exact sum of nonnegative exponents)
SOURCE_ROWS = (
    (2, (2,), 'hyp', '4/3'),
    (2, (1, 1), 'hyp', '3/2'),
    (3, (4,), 'hyp', '9/5'),
    (3, (4,), 'odd', '8/5'),
    (3, (3, 1), 'connected', '7/4'),
    (3, (2, 2), 'hyp', '2'),
    (3, (2, 2), 'odd', '5/3'),
    (3, (2, 1, 1), 'connected', '11/6'),
    (3, (1, 1, 1, 1), 'connected', '53/28'),
    (4, (6,), 'hyp', '16/7'),
    (4, (6,), 'even', '2'),
    (4, (6,), 'odd', '13/7'),
    (4, (5, 1), 'connected', '2'),
    (4, (4, 2), 'even', '32/15'),
    (4, (4, 2), 'odd', '29/15'),
    (4, (4, 1, 1), 'connected', '1137/550'),
    (4, (3, 3), 'hyp', '5/2'),
    (4, (3, 3), 'nonhyp', '2'),
    (4, (3, 2, 1), 'connected', '25/12'),
    (4, (3, 1, 1, 1), 'connected', '66/31'),
    (4, (2, 2, 2), 'even', '737/333'),
    (4, (2, 2, 2), 'odd', '2'),
    (4, (2, 2, 1, 1), 'connected', '5045/2358'),
    (4, (2, 1, 1, 1, 1), 'connected', '131/60'),
    (4, (1, 1, 1, 1, 1, 1), 'connected', '839/377'),
)
SUMS = {(g, mu, c): QQ(total) for g, mu, c, total in SOURCE_ROWS}


def partitions(total, largest=None):
    """Positive partitions, in decreasing lexicographic order."""
    if total == 0:
        yield ()
        return
    largest = total if largest is None else min(total, largest)
    for first in range(largest, 0, -1):
        for tail in partitions(total - first, first):
            yield (first,) + tail


def components(g, mu):
    """KZ component classification for positive profiles and g >= 2."""
    if g < 2 or not mu or tuple(sorted(mu, reverse=True)) != mu \
            or min(mu) < 1 or sum(mu) != 2*g-2:
        raise ValueError('Use a positive, non-increasing partition of 2*g-2.')
    if g == 2:
        return ('hyp',)
    special = mu in ((2*g-2,), (g-1, g-1))
    if g == 3 and special:
        return ('hyp', 'odd')
    if all(m % 2 == 0 for m in mu):
        return ('hyp', 'even', 'odd') if special else ('even', 'odd')
    if mu == (g-1, g-1):
        return ('hyp', 'nonhyp')
    return ('connected',)


def coefficient(g, mu, component):
    """Return the exact rational pi**2*c_area from the EKZ identity."""
    g, mu = int(g), tuple(int(m) for m in mu)
    if component not in components(g, mu):
        raise ValueError('This profile has no component with that label.')
    total = SUMS[(g, mu, component)]  # Source coverage is deliberately finite.
    kappa = sum((QQ(m*(m+2)) / QQ(m+1) for m in mu), QQ(0)) / QQ(12)
    return QQ(3) * (total - kappa)


def area_constant(g, mu, component, digits=100):
    field = RealBallField(numberdb.bits(digits, losing=WORKING_GUARD))
    return field(coefficient(g, mu, component)) / field.pi()**2


class AreaSiegelVeech(numberdb.Generator):
    table = 'T561'
    parameters = ('g', 'mu', 'component')
    type = 'R'
    digits = 100
    rigour = 'proven'
    files = ('generate.py', 'checks.py', 'check.py', 'table_records.py',
             'argument-selection.md',
             'source-evidence.md', 'numerical-evidence.json')

    def enumerate(self):
        for g in range(2, 5):
            for mu in partitions(2*g-2):
                for component in components(g, mu):
                    yield {'g': g, 'mu': ','.join(map(str, mu)), 'component': component}

    def value(self, params, digits):
        mu = tuple(int(m) for m in str(params['mu']).split(','))
        value = area_constant(params['g'], mu, params['component'], digits)
        if (int(params['g']), mu, params['component']) == (2, (2,), 'hyp'):
            return {'number': value, 'comment': r'The value is $10/(3\pi^2)$ CITE{EKZ}, Corollary 2.'}
        if (int(params['g']), mu, params['component']) == (2, (1, 1), 'hyp'):
            return {'number': value, 'comment': r'The value is $15/(4\pi^2)$ CITE{EKZ}, Corollary 2.'}
        return value


if __name__ == '__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
        key = sys.stdin.read().strip()
        if key.startswith('NUMBERDB_'):
            key = key.split('=', 1)[1].strip().strip('"').strip("'")
        client = numberdb.Client(api_key=key)
    else:
        client = None
    result = AreaSiegelVeech().verify(sample=None, client=client)
    print(result)
    raise SystemExit(0 if result.ok else 1)