checks.py

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

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

"""Independent exact checks for Chiral unitary Weingarten functions, T552.

Download this file beside generate.py. Run:
    sage -python checks.py                 # independently check local values
    sage -python checks.py --stored        # check all stored values (key for draft)
No generator routines are used in the independent Gram systems or moment checks.
The optional built-in control checks run before evaluating table values.
"""
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from itertools import permutations
from collections import Counter
from functools import lru_cache
from math import factorial, comb, prod
import json
import sys
import os


def read_rational(text):
    parts=str(text).split('/')
    assert len(parts) in (1,2)
    return QQ(int(parts[0]))/QQ(int(parts[1]) if len(parts)==2 else 1)


@lru_cache(None)
def symmetric_group(k):
    return tuple(permutations(range(k)))


def cycle_type(p):
    unused=set(range(len(p))); cycles=[]
    while unused:
        j=min(unused); length=0
        while j in unused:
            unused.remove(j); length+=1; j=p[j]
        cycles.append(length)
    return tuple(sorted(cycles,reverse=True))


def compose_inverse(p,q):
    inverse=[0]*len(p)
    for i,j in enumerate(p): inverse[j]=i
    return tuple(inverse[q[i]] for i in range(len(q)))


def gaussian_elimination(rows, rhs):
    a=[[QQ(x) for x in row]+[QQ(y)] for row,y in zip(rows,rhs)]
    size=len(rhs)
    for col in range(size):
        pivot=next(row for row in range(col,size) if a[row][col])
        a[col],a[pivot]=a[pivot],a[col]
        divisor=a[col][col]
        a[col]=[x/divisor for x in a[col]]
        for row in range(size):
            if row!=col:
                multiple=a[row][col]
                a[row]=[x-multiple*y for x,y in zip(a[row],a[col])]
    return [row[-1] for row in a]


@lru_cache(None)
def independent_coefficients(a,b,k):
    group=symmetric_group(k)
    representatives={}
    for p in group: representatives.setdefault(cycle_type(p),p)
    types=sorted(representatives,reverse=True); index={mu:i for i,mu in enumerate(types)}
    rows=[]; rhs=[]; n=a+b
    for mu in types:
        sigma=representatives[mu]; row=[0]*len(types)
        for tau in group:
            row[index[cycle_type(compose_inverse(tau,sigma))]] += n**len(cycle_type(tau))
        rows.append(row)
        rhs.append(prod(n if part%2==0 else a-b for part in mu))
    solution=gaussian_elimination(rows,rhs)
    return dict(zip(types,solution))


def diagonal_moment(a,b,k):
    # Q=sum_{j<=a}|U_{j1}|^2 is Beta(a,b), and W_11=2Q-1.
    # Rising factorial moments E Q^j=(a)_j/(a+b)_j.
    return sum((QQ(comb(k,j)*2**j*(-1)**(k-j)*prod(range(a,a+j)))
                /QQ(prod(range(a+b,a+b+j)))) for j in range(k+1))


def controls():
    assert cycle_type((1,0,2)) == (2,1)
    assert compose_inverse((1,2,0),(1,2,0)) == (0,1,2)
    assert gaussian_elimination([[2,1],[1,3]],[1,2]) == [QQ(1)/QQ(5),QQ(3)/QQ(5)]
    known=independent_coefficients(1,1,2)
    assert known[(1,1)] == -QQ(1)/QQ(3) and known[(2,)] == QQ(2)/QQ(3)
    assert diagonal_moment(1,1,2) == QQ(1)/QQ(3)
    assert diagonal_moment(1,1,4) == QQ(1)/QQ(5)
    assert diagonal_moment(2,1,1) == QQ(1)/QQ(3)
    bad=dict(known); bad[(2,)]+=1
    assert sum(bad[cycle_type(p)] for p in symmetric_group(2)) != diagonal_moment(1,1,2)
    return 8


def validate(values):
    expected={}
    for a in range(1,8):
        for b in range(1,min(a,8-a)+1):
            for k in range(1,min(4,a+b)+1):
                for mu in {cycle_type(p) for p in symmetric_group(k)}:
                    expected[(a,b,mu)]=independent_coefficients(a,b,k)[mu]
    assert len(expected)==163 and set(values)==set(expected)
    for key,value in values.items(): assert value==expected[key], (key,value,expected[key])
    # Full permutation equation, not just a representative of each class.
    equations=0; moments=0; small=0; balanced=0
    for a,b in sorted({(a,b) for a,b,mu in values}):
        n=a+b
        for k in range(1,min(4,n)+1):
            group=symmetric_group(k)
            for sigma in group:
                lhs=sum(QQ(n**len(cycle_type(tau)))*values[(a,b,cycle_type(compose_inverse(tau,sigma)))] for tau in group)
                rhs=prod(n if part%2==0 else a-b for part in cycle_type(sigma))
                assert lhs==rhs,(a,b,k,sigma,lhs,rhs)
                equations+=1
            moment=sum(values[(a,b,cycle_type(p))] for p in group)
            assert moment==diagonal_moment(a,b,k),(a,b,k,moment)
            moments+=1
        known={(1,):QQ(a-b)/QQ(n),(1,1):QQ((a-b)**2-1)/QQ(n*n-1),(2,):QQ(4*a*b)/QQ(n*(n*n-1))}
        for mu,value in known.items():
            assert values[(a,b,mu)]==value
            small+=1
    for (a,b,mu),value in values.items():
        if a==b and sum(mu)%2:
            assert value==0
            balanced+=1
    return {'independently_solved_entries':len(values),'full_permutation_equations':equations,
            'beta_moment_checks':moments,'source_small_cases':small,'balanced_odd_zeros':balanced}


def check_unitary_convolution(values, unitary):
    # T549 is an independently built table in the same Haar normalization.
    block=unitary['Numbers']
    if isinstance(block,dict):
        kernel={(int(n),tuple(map(int,mu.split(',')))):read_rational(value if not isinstance(value,dict) else value['number'])
                for n,mus in block.items() for mu,value in mus.items()}
    else:
        kernel={(int(r['params']['N']),tuple(map(int,r['params']['mu'].split(',')))):read_rational(r['number']) for r in block}
    for (a,b,mu),value in values.items():
        k=sum(mu); n=a+b
        sigma=next(p for p in symmetric_group(k) if cycle_type(p)==mu)
        answer=sum(QQ(prod(n if part%2==0 else a-b for part in cycle_type(tau)))
                   *kernel[(n,cycle_type(compose_inverse(tau,sigma)))] for tau in symmetric_group(k))
        assert answer==value,(a,b,mu,answer,value)
    return len(values)


def main():
    control_count=controls()
    stored='--stored' in sys.argv or os.environ.get('NUMBERDB_CHECK_STORED')=='1'
    if stored:
        if os.environ.get('NUMBERDB_KEY_FROM_STDIN')=='1':
            token=sys.stdin.read().strip()
            if '=' in token and token.split('=',1)[0] in ('NUMBERDB_KEY','NUMBERDB_API_KEY'):
                token=token.split('=',1)[1].strip().strip('\"\'')
            numberdb.configure(api_key=token)
        doc=numberdb.table('T552')
        values={}
        block=doc['Numbers']
        if isinstance(block,dict):
            records=[{'params':{'a':a,'b':b,'mu':mu},'number':value if not isinstance(value,dict) else value['number']}
                     for a,bs in block.items() for b,mus in bs.items() for mu,value in mus.items()]
        else:
            records=block
        for entry in records:
            p=entry['params']; mu=tuple(map(int,str(p['mu']).split(',')))
            key=(int(p['a']),int(p['b']),mu)
            assert key not in values
            values[key]=read_rational(entry['number'])
    else:
        from generate import ChiralUnitaryWeingarten, weingarten
        gen=ChiralUnitaryWeingarten()
        values={(p['a'],p['b'],tuple(map(int,p['mu'].split(',')))):gen.value(p,100) for p in gen.enumerate()}
        for (a,b,mu),value in values.items():
            assert weingarten(b,a,mu)==(-1)**sum(mu)*value
    report=validate(values)
    for (a,b,mu),value in values.items():
        assert independent_coefficients(b,a,sum(mu))[mu] == (-1)**sum(mu)*value
    report['signature_reversal_checks']=len(values)
    if stored:
        report['unitary_convolution_checks']=check_unitary_convolution(values,numberdb.table('T549'))
    report.update(controls=control_count,mode='stored' if stored else 'local',success=True)
    print(json.dumps(report,indent=2))
    return values,report


if __name__=='__main__':
    main()