checks.py

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

5459 bytes, as of the version from 2026-10-28 06:43. Recorded here, not run.

"""Independent exact checks of Trace moments -- numberdb.org/T574.

sage -python checks.py                 # compare generated entries
sage -python checks.py --stored        # independently check the live draft
A private draft requires NUMBERDB_API_KEY (or NUMBERDB_KEY_FROM_STDIN=1).
This file has no dependency on a NumberDB website checkout.
"""
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from collections import Counter
from functools import lru_cache
from itertools import permutations
from math import factorial, prod
import json
import os
import sys
from generate import InducedTraceMoments, moment


def cycles(perm):
    seen = set()
    count = 0
    for start in range(len(perm)):
        if start in seen:
            continue
        count += 1
        i = start
        while i not in seen:
            seen.add(i)
            i = perm[i]
    return count


@lru_cache(maxsize=None)
def wick_counts(q):
    counts = Counter()
    for sigma in permutations(range(q)):
        gamma_sigma = tuple((sigma[i]+1) % q for i in range(q))
        counts[(cycles(gamma_sigma), cycles(sigma))] += 1
    assert sum(counts.values()) == factorial(q)
    return counts


def wick(N, K, q):
    if q == 0:
        return QQ(N)
    numerator = sum(count*N**i*K**j for (i,j), count in wick_counts(q).items())
    return QQ(numerator)/QQ(prod(N*K+r for r in range(q)))


def beta_integer(a, b):
    return QQ(factorial(a-1)*factorial(b-1))/QQ(factorial(a+b-1))


def qubit_integral(K, q):
    # Integrate x^q against x^(K-2)(1-x)^(K-2)(2x-1)^2 on [0,1].
    def raw(order):
        return (4*beta_integer(K+order+1,K-1)
                -4*beta_integer(K+order,K-1)
                +beta_integer(K+order-1,K-1))
    return 2*raw(q)/raw(0)


def qubit_hs(q):
    return 6*(QQ(4)/QQ(q+3)-QQ(4)/QQ(q+2)+QQ(1)/QQ(q+1))


def run_checks(values):
    # Nonzero known controls precede all data comparisons.
    assert cycles((0,1,2)) == 3 and cycles((1,2,0)) == 1
    assert wick_counts(2) == Counter({(1,2):1,(2,1):1})
    assert beta_integer(1,1) == 1
    assert qubit_integral(2,0) == 2 and qubit_integral(2,1) == 1
    for q, expected in [(2,QQ(4)/QQ(5)),(3,QQ(7)/QQ(10)),(4,QQ(22)/QQ(35))]:
        assert wick(2,2,q) == qubit_integral(2,q) == qubit_hs(q) == expected
    expected_keys={(N,K,q) for N in range(2,9) for K in range(N,65)
                   if N*K <= 64 for q in range(2,9)}
    assert set(values) == expected_keys and len(values) == 560
    counts=Counter()
    for (N,K,q), actual in values.items():
        assert actual == wick(N,K,q), ('Wick',N,K,q,actual,wick(N,K,q))
        counts['wick'] += 1
        assert actual == wick(K,N,q)
        counts['dimension_swap'] += 1
        assert QQ(1)/QQ(N**(q-1)) < actual < 1
        counts['density_bounds'] += 1
        mprev=values[(N,K,q-1)] if q > 2 else QQ(1)
        mprevprev=values[(N,K,q-2)] if q > 3 else (QQ(1) if q==3 else QQ(N))
        assert actual < mprev
        counts['order_monotonicity'] += 1
        left=actual*(N*K+q-1)*(N*K+q-2)*(q+1)
        right=(2*q-1)*(N+K)*(N*K+q-2)*mprev+(q-2)*((q-1)**2-(K-N)**2)*mprevprev
        assert left == right
        counts['recurrence'] += 1
        if q == 2:
            assert actual == QQ(N+K)/QQ(N*K+1)
            counts['purity'] += 1
        if q == 3:
            assert actual == QQ(N*N+3*N*K+K*K+1)/QQ((N*K+1)*(N*K+2))
            counts['third_moment'] += 1
        if q == 4:
            assert actual == QQ(N**3+6*N*N*K+6*N*K*K+K**3+5*(N+K))/QQ((N*K+1)*(N*K+2)*(N*K+3))
            counts['fourth_moment'] += 1
        if N == 2:
            assert actual == qubit_integral(K,q)
            counts['qubit_eigenvalue_integral'] += 1
        if N == K == 2:
            assert actual == qubit_hs(q)
            counts['qubit_hs_formula'] += 1
    for N,K in {(N,K) for N,K,q in values}:
        assert wick(N,K,0) == N and wick(N,K,1) == 1
        assert moment(N,K,0) == N and moment(N,K,1) == 1
        counts['zeroth_and_first_controls'] += 1
    return {'entries':len(values),'counts':dict(counts),'permutations_enumerated':sum(factorial(q) for q in range(2,9)),
            'max_written_length':max(len(str(v)) for v in values.values()),'all_passed':True}


def generated_values():
    generator=InducedTraceMoments()
    return {tuple(p[n] for n in generator.parameters):generator.value(p) for p in generator.enumerate()}


def parse_exact(value):
    text=str(value).strip()
    if '/' in text:
        numerator,denominator=text.split('/')
        return QQ(int(numerator))/QQ(int(denominator))
    return QQ(int(text))


def stored_values():
    document=numberdb.table('T574')
    assert tuple(document['Parameters']) == ('N','K','q')
    records=document['Numbers']
    if isinstance(records,list):
        return {tuple(int(r['params'][n]) for n in ('N','K','q')):parse_exact(r['number']) for r in records}
    # API reads may nest flat submissions by the declared parameter order.
    assert isinstance(records,dict)
    return {(int(N),int(K),int(q)):parse_exact(v['number'] if isinstance(v,dict) else v)
            for N,by_K in records.items() for K,by_q in by_K.items()
            for q,v in by_q.items()}


if __name__ == '__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
        os.environ['NUMBERDB_API_KEY']=sys.stdin.read().strip()
    data=stored_values() if '--stored' in sys.argv else generated_values()
    print(json.dumps(run_checks(data),indent=2))