checks.py

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

8690 bytes, as of the version from 2026-10-27 14:23 (current). Recorded here, not run.

"""Independent checks for Symplectic Weingarten functions -- numberdb.org/T551.

Run: sage -python checks.py
Requires generate.py and check.py beside this file, plus numberdb and Sage.
With --stored, reads the draft (NUMBERDB_API_KEY) instead of generated values.
No character, Jack, or spherical routines from the generator are used to
construct the independent pairing Gram systems or uniform-sphere moments.
"""
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, product
from math import factorial, prod
from pathlib import Path
import json
import os
import sys
import time
from check import exactness, measure, agrees_with, names_its_rings


def partition_list(n, cap=None):
    if n == 0:
        yield ()
        return
    for first in range(min(n, n if cap is None else cap), 0, -1):
        for tail in partition_list(n-first, first):
            yield (first,) + tail


def pairings(vertices):
    if not vertices:
        yield ()
        return
    a = vertices[0]
    for j in range(1, len(vertices)):
        b = vertices[j]
        for rest in pairings(vertices[1:j]+vertices[j+1:]):
            yield ((a, b),) + rest


def signature(p):
    letters = [v for pair in p for v in pair]
    return (-1)**sum(a > b for i, a in enumerate(letters) for b in letters[i+1:])


def representative(mu):
    result = []
    start = 0
    for r in mu:
        result.append((start, start+2*r-1))
        result.extend((a, a+1) for a in range(start+1, start+2*r-1, 2))
        start += 2*r
    return tuple(result)


def components(p, q):
    adjacency = [[] for _ in range(2*len(p))]
    for a, b in p+q:
        adjacency[a].append(b)
        adjacency[b].append(a)
    colors = {}
    sizes = []
    for root in range(len(adjacency)):
        if root in colors:
            continue
        colors[root] = 0
        stack = [root]
        size = 0
        while stack:
            a = stack.pop()
            size += 1
            for b in adjacency[a]:
                if b not in colors:
                    colors[b] = 1-colors[a]
                    stack.append(b)
                assert colors[a] != colors[b]
        sizes.append(size//2)
    # For an oriented edge (a,b), J_{i_a,i_b} has sign (-1)^color[a].
    # Each alternating loop has 2N assignments; flipping all colors preserves
    # the sign because the loop contains an even number of edge factors.
    orientation = (-1)**sum(colors[a] for a, b in p+q)
    return tuple(sorted(sizes, reverse=True)), orientation


def ct(p, q):
    return components(p, q)[0]


def gram_entry(N, p, q):
    mu, orientation = components(p, q)
    return orientation*(2*N)**len(mu)


def direct_tensor_contraction(N, p, q):
    def J(a, b):
        return 1 if a < N and b == a+N else (-1 if b < N and a == b+N else 0)
    return sum(prod(J(labels[a], labels[b]) for a, b in p+q)
               for labels in product(range(2*N), repeat=2*len(p)))


def solve(a, b):
    a = [[QQ(x) for x in row] + [QQ(y)] for row, y in zip(a, b)]
    n = len(a)
    for col in range(n):
        pivot = next(r for r in range(col, n) if a[r][col])
        a[col], a[pivot] = a[pivot], a[col]
        scale = a[col][col]
        a[col] = [v/scale for v in a[col]]
        for r in range(n):
            if r != col:
                scale = a[r][col]
                a[r] = [x-scale*y for x, y in zip(a[r], a[col])]
    return [row[-1] for row in a]


@lru_cache(maxsize=None)
def geometry(k):
    types = tuple(partition_list(k))
    pp = tuple(pairings(tuple(range(2*k))))
    p0 = tuple((2*i, 2*i+1) for i in range(k))
    assert len(pp) == prod(range(1, 2*k, 2))
    base = [components(p0, q) for q in pp]
    signs = [signature(q) for q in pp]
    counts = []
    for mu in types:
        r = representative(mu)
        assert signature(r) == 1 and ct(p0, r) == mu
        counts.append(Counter((ct(q, r), len(base_mu), orientation*sq)
                              for q, (base_mu, orientation), sq in zip(pp, base, signs)))
    return types, pp, counts


def gram_system(N, k):
    types, pp, counts = geometry(k)
    a = [[sum(multiplicity*sign*(2*N)**loops
              for (nu, loops, sign), multiplicity in row.items() if nu == mu)
          for mu in types] for row in counts]
    b = [int(mu == (1,)*k) for mu in types]
    return types, a, b


@lru_cache(maxsize=None)
def moment_counts(k):
    # i=j=(1,...,1,N+1,...,N+1). All nonzero Delta products pair
    # one position from each half and have positive orientation.
    pp = [tuple((a,k+b) for a,b in enumerate(pi)) for pi in permutations(range(k))]
    result = Counter()
    signs = {p:signature(p) for p in pp}
    for p in pp:
        for q in pp:
            result[ct(p,q)] += signs[p]*signs[q]
    return result


def run(values):
    start = time.monotonic()
    assert not exactness(values)
    assert not names_its_rings(str(Path(__file__).with_name('generate.py')))
    assert solve([[2,1],[1,3]], [1,0]) == [QQ(3)/5, -QQ(1)/5]
    assert gram_entry(2, ((0,1),(2,3)), ((0,3),(1,2))) == -4
    for N in (1,2):
        for k in (1,2,3):
            p0=tuple((2*i,2*i+1) for i in range(k))
            for q in pairings(tuple(range(2*k))):
                assert gram_entry(N,p0,q) == direct_tensor_contraction(N,p0,q)
    assert moment_counts(2) == {(1,1):2,(2,):-2}
    print('Known-answer controls passed: rational solve, signed tensor contractions, fourth moment.', flush=True)
    expected={}
    systems=[]
    moment_results=[]
    for k in range(1,7):
        types, pp, counts = geometry(k)
        for N in range(k,7):
            _,a,b=gram_system(N,k)
            recovered=solve(a,b)
            for mu, v in zip(types,recovered):
                expected[(N,mu)]=v
            actual=[values[(N,mu)] for mu in types]
            assert [sum(c*v for c,v in zip(row,actual)) for row in a] == b
            if N == k == 2:
                bad=actual[:];bad[0]+=QQ(1)/1000
                assert [sum(c*v for c,v in zip(row,bad)) for row in a] != b
            moment = sum(c*values[(N,mu)] for mu,c in moment_counts(k).items())
            sphere = QQ(factorial(k))/QQ(prod(2*N+j for j in range(k)))
            assert moment == sphere, (N,k,moment,sphere)
            moment_results.append({'N':N,'k':k,'moment':str(moment)})
            systems.append({'N':N,'k':k,'pairings':len(pp),'coset_types':len(types)})
        print('k=%d: all %d pairing types, %d matchings, Gram recovery and sphere moments passed'
              % (k,len(types),len(pp)),flush=True)
    assert set(values) == set(expected) and len(values)==68
    assert not agrees_with(values,lambda key:expected[key])
    small=0
    for (N,mu), v in values.items():
        if sum(mu)==1:
            assert v == QQ(1)/(2*N);small+=1
        if sum(mu)==2:
            assert v == QQ(2*N-1 if mu==(1,1) else 1)/QQ(4*N*(N-1)*(2*N+1));small+=1
    result={'entries':len(values),'measure':measure(values),'gram_systems':systems,
            'sphere_moments':moment_results,'explicit_small_cases':small,
            'controls':'nonzero exact solve; 38 direct tensor contractions; fourth-moment counts; perturbed coefficient rejected',
            'seconds':round(time.monotonic()-start,3)}
    print('RESULT_JSON '+json.dumps(result,sort_keys=True),flush=True)
    return result


def read_stored():
    document=numberdb.table('T551')
    block=document['Numbers']
    # Accept the flat input format and the nested format observed in the API export.
    if isinstance(block,list):
        return {(int(r['params']['N']),tuple(map(int,r['params']['mu'].split(',')))):QQ(r['number'])
                for r in block}
    if isinstance(block, dict):
        return {(int(N),tuple(map(int,mu.split(',')))):QQ(value)
                for N, by_type in block.items() for mu, value in by_type.items()}
    raise ValueError('Unknown entry format')


if __name__ == '__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN')=='1':
        key=sys.stdin.read().strip()
        if '=' in key:key=key.split('=',1)[1].strip().strip('\"\'')
        numberdb.configure(api_key=key)
    if '--stored' in sys.argv or os.environ.get('NUMBERDB_RESTATING')=='1':
        values=read_stored()
    else:
        from generate import SymplecticWeingarten
        generator=SymplecticWeingarten()
        values={(int(p['N']),tuple(map(int,p['mu'].split(',')))):generator.value(p,100)
                for p in generator.enumerate()}
    run(values)
    print('VALUES_JSON '+json.dumps([{'N':N,'mu':list(mu),'number':str(v)}
                                   for (N,mu),v in values.items()]),flush=True)