checks.py

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

5932 bytes, as of the version from 2026-10-16 11:15 (current). Recorded here, not run.

"""Reproducible independent checks for Simple Hurwitz numbers -- numberdb.org/T463.

    sage -python checks.py
    sage -python checks.py --stored

Download generate.py, independent.py, check.py and published-examples.json
beside this file. Offline mode checks freshly computed values. --stored
reads the actual database entries and runs the same checks. A private draft
needs NUMBERDB_API_KEY, or NUMBERDB_KEY_FROM_STDIN=1 and a key on stdin.
The checker does not use the generator's cut-and-join code for its expected
answers. It uses symmetric-group characters, direct small-degree monodromy,
genus-zero Hurwitz's formula, and genus-one ELSV intersection theory.
"""
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from fractions import Fraction
from math import factorial, prod, comb
from pathlib import Path
from itertools import combinations_with_replacement
from collections import Counter
import json, os, sys, time
from check import exactness, measure, agrees_with, names_its_rings
import independent as ind
from generate import SimpleHurwitz

HERE=Path(__file__).resolve().parent


def as_qq(value):
    """Compare exact rationals through explicit numerator/denominator conversion."""
    return QQ(value.numerator)/QQ(value.denominator)


def one_part(g,d):
    r=2*g+d-1
    return Fraction(sum((-1)**k*comb(d-1,k)*(d*(d-1-2*k)//2)**r
                        for k in range(d)),d*factorial(d))


def expected_keys():
    out=set()
    for d in range(1,11):
        for n in range(1,d+1):
            for ascending in combinations_with_replacement(range(1,d+1),n):
                if sum(ascending)!=d:continue
                mu=tuple(reversed(ascending))
                for g in range(6):
                    if 2*g-2+n>0:out.add((g,mu))
    return out


def flatten(doc):
    values={}
    for g,rows in doc['Numbers'].items():
        for mu,v in rows.items():
            key=(int(g),tuple(map(int,mu.split(','))))
            assert key not in values
            raw=v['number'] if isinstance(v,dict) else v
            values[key]=as_qq(Fraction(str(raw)))
    return values


def run(values):
    start=time.monotonic()
    report={}
    # Known controls run before the bulk comparisons.
    report['character_orthogonality_equations']=ind.character_controls(10)
    assert ind.hurwitz_character(1,(2,))==Fraction(1,2)
    assert ind.hurwitz_character(1,(1,1))==1
    assert ind.hurwitz_character(0,(1,1,1))==24
    assert ind.hurwitz_character(1,(4,))==160
    assert ind.hurwitz_character(2,(3,1))==45927
    assert ind.elsv_one((4,))==160
    assert ind.elsv_one((1,1))==1
    assert ind.genus_zero((1,1,1))==24
    assert one_part(2,4)==5824
    assert ind.hurwitz_character(1,(1,1))!=Fraction(1,2) # unlabelled control
    assert ind.hurwitz_character(1,(4,))!=Fraction(160,factorial(5)) # r! control
    assert exactness({'bad':0.5})
    assert agrees_with({'bad':QQ(2)},lambda k:QQ(1))
    report['positive_and_negative_controls']='pass'
    assert len(values)==793 and set(values)==expected_keys()
    assert not exactness(values)
    assert not names_its_rings(str(HERE/'generate.py'))
    assert all(v>=0 for v in values.values())
    wrong=agrees_with(values,lambda k:as_qq(ind.hurwitz_character(*k)))
    assert not wrong,wrong[:4]
    report['independent_character_matches']=len(values)
    print('All 793 character comparisons pass.',flush=True)
    genus0={k:v for k,v in values.items() if k[0]==0}
    assert not agrees_with(genus0,lambda k:as_qq(ind.genus_zero(k[1])))
    report['genus_zero_formula_matches']=len(genus0)
    genus1={k:v for k,v in values.items() if k[0]==1}
    assert not agrees_with(genus1,lambda k:as_qq(ind.elsv_one(k[1])))
    report['genus_one_elsv_matches']=len(genus1)
    single={k:v for k,v in values.items() if len(k[1])==1}
    assert not agrees_with(single,lambda k:as_qq(one_part(k[0],k[1][0])))
    report['one_part_formula_matches']=len(single)
    examples=json.loads((HERE/'published-examples.json').read_text())['records']
    for example in examples:
        mu=tuple(map(int,example['mu'].split(',')))
        aut=prod(factorial(c) for c in Counter(mu).values())
        assert values[(example['g'],mu)]==as_qq(aut*Fraction(example['unlabelled'])),example
    report['published_example_matches']=len(examples)
    direct=0
    for d in range(1,5):
        rows={k:v for k,v in values.items() if sum(k[1])==d}
        R=max(2*g-2+d+len(mu) for g,mu in rows)
        counts=ind.direct_monodromy(d,R)
        if d==3:assert counts[2][(1,2,0)]==3 # three minimal factorizations
        for (g,mu),value in rows.items():
            r=2*g-2+d+len(mu)
            count=counts[r].get(ind.fixed_sigma(mu),0)
            assert value==as_qq(Fraction(count,prod(mu))),(g,mu,value,count)
            direct+=1
    report['direct_monodromy_matches']=direct
    # Every n=1,d=1 entry is zero because a degree-one cover is an isomorphism.
    for g in range(1,6):assert values[(g,(1,))]==0
    for g in range(1,6):
        assert values[(g,(2,))]==QQ(1)/QQ(2)
        assert values[(g,(1,1))]==1
    report['degree_one_and_two_checks']=15
    report['domain_entries']=len(values)
    report['measurement']=measure(values)
    report['seconds']=time.monotonic()-start
    print(json.dumps(report,indent=2,default=str),flush=True)
    return report


def main():
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN')=='1':
        os.environ['NUMBERDB_API_KEY']=sys.stdin.read().strip()
    if '--stored' in sys.argv:
        values=flatten(numberdb.table('T463'))
    else:
        gen=SimpleHurwitz()
        pairs=[((int(p['g']),tuple(map(int,p['mu'].split(',')))),gen.value(p)) for p in gen.enumerate()]
        assert len(pairs)==len(dict(pairs))
        values=dict(pairs)
    run(values)
    print('VALUES_JSON='+json.dumps([{'g':g,'mu':','.join(map(str,mu)),'number':str(v)} for (g,mu),v in values.items()]))

if __name__=='__main__':main()