generate.py

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

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

"""Weil--Petersson volumes of hyperbolic moduli spaces -- numberdb.org/T459.

Run with SageMath after installing the client (sage -pip install numberdb):
    sage -python generate.py            # verify every stored entry
    sage -python checks.py              # independent exact and interval checks
    sage -python generate.py --fill     # fill the existing draft

For a private draft, set NUMBERDB_API_KEY or pipe the key on stdin with
NUMBERDB_KEY_FROM_STDIN=1. Filling writes numbers and attachments only.
It does not publish or offer the table for review.

The rational kappa_1 integrals use Theorem 1.1 and equation (3) of Liu-Xu,
arXiv:0708.0565v2. All arithmetic before pi uses QQ. The volume is
(2*pi^2)^m/m! times the integral, m=3*g-3+n. The orbifold normalisation
includes generic automorphisms, and all n punctures are labelled.

A 64-bit guard beyond bits(100) is checked by measuring the relative
accuracy of every output ball. The independent checker uses a partition
transform to pure psi integrals and MPFI for the final pi evaluation.
"""
import os
import sys
from functools import lru_cache
from math import factorial, comb
from collections import Counter
from itertools import product

import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from sage.rings.integer_ring import ZZ
from sage.rings.real_arb import RealBallField

WORKING_GUARD = 64
MAX_GENUS = 3
MAX_PUNCTURES = 8

@lru_cache(None)
def odd_factorial(k):
    if k <= 0:
        return ZZ(1)
    return ZZ(k)*odd_factorial(k-2)

@lru_cache(None)
def splits(ds):
    """All labelled subsets, compressed by equal exponents."""
    counts = sorted(Counter(ds).items())
    out = []
    for chosen in product(*(range(c+1) for _, c in counts)):
        left, right, ways = [], [], 1
        for (d,c), j in zip(counts,chosen):
            left.extend([d]*j)
            right.extend([d]*(c-j))
            ways *= comb(c,j)
        out.append((tuple(left),tuple(right),ways))
    return tuple(out)

def mixed(g,k,ds):
    return _mixed(int(g),int(k),tuple(sorted(ds)))

@lru_cache(None)
def _mixed(g,k,ds):
    """Integral of kappa_1^k times the psi powers ds."""
    n=len(ds)
    if g<0 or k<0 or any(d<0 for d in ds) or 2*g-2+n<=0:
        return QQ(0)
    if k+sum(ds)!=3*g-3+n:
        return QQ(0)
    if (g,k,ds)==(0,0,(0,0,0)):
        return QQ(1)
    if g==1 and n==1 and k+ds[0]==1:
        return QQ(1)/24
    if n==0:
        # Liu-Xu equation (3), adding a marked point.
        return sum((QQ((-1)**j*comb(k,j))*mixed(g,k-j,(j+1,))
                    for j in range(k+1)),QQ(0))/(2*g-2)
    d=ds[-1]
    tail=ds[:-1]
    total=QQ(0)
    # Isolate the L=0 term on the left of Liu-Xu Theorem 1.1.
    for j in range(1,k+1):
        total -= (QQ((-1)**j*comb(k,j))*odd_factorial(2*d+2*j+1)
                  /odd_factorial(2*j+1)*mixed(g,k-j,tail+(d+j,)))
    for i,e in enumerate(tail):
        total += (odd_factorial(2*(d+e)-1)/odd_factorial(2*e-1)
                  *mixed(g,k,tail[:i]+tail[i+1:]+(d+e-1,)))
    for a in range(d-1):
        b=d-2-a
        term=mixed(g-1,k,tail+(a,b))
        for left,right,ways in splits(tail):
            for h in range(g+1):
                # Dimension forces the kappa power on the first component.
                ka=3*h-3+len(left)+1-a-sum(left)
                if 0<=ka<=k:
                    term += (ways*comb(k,ka)*mixed(h,ka,left+(a,))
                             *mixed(g-h,k-ka,right+(b,)))
        total += odd_factorial(2*a+1)*odd_factorial(2*b+1)*term/2
    return total/odd_factorial(2*d+1)

def coefficient(g,n):
    """Exact coefficient of pi^(2m) in V_{g,n}."""
    if g<0 or n<0 or 2*g-2+n<=0:
        raise ValueError('requires stable nonnegative genus and punctures')
    m=3*g-3+n
    return QQ(2**m)*mixed(g,m,(0,)*n)/factorial(m)

def volume(g,n,digits=100):
    c=coefficient(g,n)
    m=3*g-3+n
    if m==0:
        return c
    field=RealBallField(numberdb.bits(digits,losing=WORKING_GUARD))
    ans=field(c)*field.pi()**(2*m)
    assert ans.is_finite() and ans>0 and ans.rad()>0
    return ans

class WeilPeterssonVolumes(numberdb.Generator):
    table='T459'
    parameters=('g','n')
    type='R'
    digits=100
    rigour='proven'
    files=('generate.py','checks.py','check.py','numerical-evidence.json')

    def enumerate(self,max_genus=MAX_GENUS,max_punctures=MAX_PUNCTURES):
        for g in range(max_genus+1):
            for n in range(max_punctures+1):
                if 2*g-2+n>0:
                    yield {'g':g,'n':n}

    def value(self,params,digits):
        g,n=int(params['g']),int(params['n'])
        v=volume(g,n,digits)
        if (g,n)==(0,3):
            return {'number':v,'comment':'The thrice-punctured sphere has a unique hyperbolic structure, and its moduli space has volume $1$.'}
        return v

if __name__=='__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN')=='1':
        numberdb.configure(api_key=sys.stdin.read().strip())
    gen=WeilPeterssonVolumes()
    if '--fill' in sys.argv or os.environ.get('NUMBERDB_PUBLISH')=='1':
        print(gen.publish(message='add orbifold Weil-Petersson volumes with exact coefficient checks'))
    else:
        report=gen.verify(sample=None)
        print(report)
        sys.exit(0 if report.ok else 1)