generate.py

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

5446 bytes, as of the version from 2026-10-16 09:47 (current). Recorded here, not run.

"""Weil-Petersson volume polynomials -- numberdb.org/T460.

Run with SageMath (or numberdb plus passagemath):
    sage -pip install numberdb
    sage -python generate.py                 # verify the stored draft/table
    sage -python generate.py --fill          # fill this draft with your key

A draft requires NUMBERDB_API_KEY even to verify. --fill sends entries but
never offers, reviews, or publishes a table. The campaign wrapper may provide
NUMBERDB_KEY_FROM_STDIN=1 and NUMBERDB_PUBLISH=1 instead of command arguments.

The polynomial variables are L1,...,Ln,p, with p representing pi formally.
All coefficients are exact. Pure psi intersections use the DVV recurrence;
Lemma 2.2 of Liu-Xu (arXiv:0708.0565) converts kappa_1 powers to them.
Mirzakhani's coefficient formula then gives the volume, with the orbifold
normalization V11=(L1^2+4*p^2)/48. See checks.py for an independent computation
by the geometric Mirzakhani recursion and checks against published values.
"""
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing

from collections import Counter
from fractions import Fraction as F
from functools import lru_cache
from math import factorial, prod
import os
import sys

MAX_COMPLEXITY = 3
MAX_BOUNDARIES = 5


def df(n):
    return prod(range(n, 0, -2))


def psi(g, d):
    return _psi(int(g), tuple(sorted(d)))


@lru_cache(None)
def _psi(g, d):
    n = len(d)
    if g < 0 or 2*g-2+n <= 0 or any(x < 0 for x in d):
        return F(0)
    if sum(d) != 3*g-3+n:
        return F(0)
    if g == 0:
        return F(factorial(n-3), prod(factorial(x) for x in d))
    if d == (1,) and g == 1:
        return F(1, 24)
    if d[0] == 0:
        tail = d[1:]
        return sum((psi(g, tail[:j]+(x-1,)+tail[j+1:])
                    for j,x in enumerate(tail) if x), F(0))
    if d[0] == 1:
        return (2*g-2+n-1)*psi(g, d[1:])
    k, tail = d[-1], d[:-1]
    total = F(0)
    for j,x in enumerate(tail):
        total += F(df(2*k+2*x-1), df(2*x-1))*psi(
            g, tail[:j]+(k+x-1,)+tail[j+1:])
    for a in range(k-1):
        b = k-2-a
        split = psi(g-1, (a,b)+tail)
        for mask in range(1 << len(tail)):
            left = tuple(x for i,x in enumerate(tail) if mask & (1 << i))
            right = tuple(x for i,x in enumerate(tail) if not mask & (1 << i))
            for h in range(g+1):
                split += psi(h,(a,)+left)*psi(g-h,(b,)+right)
        total += F(df(2*a+1)*df(2*b+1),2)*split
    return total / df(2*k+1)


def partitions(m, minimum=1):
    if m == 0:
        yield ()
    for a in range(minimum,m+1):
        for rest in partitions(m-a,a):
            yield (a,)+rest


@lru_cache(None)
def mixed(g, d, m):
    """Integral of kappa_1^m times the given psi powers."""
    answer = F(0)
    for blocks in partitions(m):
        multiplicities = Counter(blocks)
        denominator = prod(factorial(b) for b in blocks)
        denominator *= prod(factorial(c) for c in multiplicities.values())
        multiplicity = factorial(m) // denominator
        answer += (-1)**(m-len(blocks))*multiplicity*psi(
            g, d+tuple(b+1 for b in blocks))
    return answer


def compositions_at_most(total, n):
    if n == 0:
        yield ()
    else:
        for a in range(total+1):
            for rest in compositions_at_most(total-a,n-1):
                yield (a,)+rest


def volume(g, n):
    """Return V_{g,n} in QQ[L1,...,Ln,p], with orbifold weights."""
    g,n = int(g),int(n)
    if g < 0 or n < 0 or 2*g-2+n <= 0:
        raise ValueError('g,n must be a stable nonnegative pair')
    R = PolynomialRing(QQ, names=tuple('L%d'%i for i in range(1,n+1))+('p',))
    dimension = 3*g-3+n
    coefficients = {}
    for d in compositions_at_most(dimension,n):
        m = dimension-sum(d)
        coefficient = mixed(g,tuple(sorted(d)),m)*F(
            2**m, 2**sum(d)*factorial(m)*prod(factorial(a) for a in d))
        assert coefficient > 0
        exponents = tuple(2*a for a in d)+(2*m,)
        # Sage's one-variable constructor takes integer rather than tuple keys.
        key = exponents if n else exponents[0]
        coefficients[key] = QQ(coefficient.numerator)/QQ(coefficient.denominator)
    return R(coefficients)


class WeilPeterssonPolynomials(numberdb.Generator):
    table = 'T460'
    parameters = ('g','n')
    type = 'Q[]'
    rigour = 'exact'

    def enumerate(self, complexity=MAX_COMPLEXITY, boundaries=MAX_BOUNDARIES):
        for g in range((complexity+2)//2+1):
            for n in range(boundaries+1):
                if 0 < 2*g-2+n <= complexity:
                    yield {'g':str(g),'n':str(n)}

    def value(self, params, digits=100):
        return volume(int(params['g']),int(params['n']))


if __name__ == '__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
        token = sys.stdin.read().strip()
        if '=' in token and token.split('=',1)[0].isupper():
            token = token.split('=',1)[1].strip().strip('\'"')
        numberdb.configure(api_key=token)
    generator = WeilPeterssonPolynomials()
    if '--fill' in sys.argv or os.environ.get('NUMBERDB_PUBLISH') == '1':
        print(generator.publish(message='Exact orbifold volume polynomials, checked by an independent geometric recursion.'))
    else:
        result = generator.verify(sample=None)
        print(result)
        sys.exit(0 if result.ok else 1)