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)