generate.py

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

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

"""Unitary Weingarten functions -- numberdb.org/T549.

Run with SageMath (install numberdb once with sage -pip install numberdb):
    sage -python generate.py                 # verify every stored entry
    sage -python checks.py                  # independent exact checks
    sage -python generate.py --fill          # write entries to this draft

Writing needs NUMBERDB_API_KEY. It fills the existing table; it does not
publish or offer the draft. NUMBERDB_KEY_FROM_STDIN=1 reads the key from stdin,
and NUMBERDB_PUBLISH=1 is the campaign wrapper's equivalent of --fill.

Matsumoto (2013), arXiv:1301.5401, equation (2.1):
    Wg_N(mu) = 1/k! sum_{lambda |- k, len(lambda)<=N}
                 f^lambda chi^lambda(mu) / product_{(i,j) in lambda}(N+j-i).
Characters are computed by the Murnaghan-Nakayama rule, removing connected
skew rim hooks with no 2 by 2 square, with sign (-1)^(height-1).
Every division has a Sage rational operand. No decimal approximation occurs.
The default selection and its noncanonical finite bounds are justified in
argument-selection.md. The formula also supports positive N below k, using
only partitions of length at most N (the pseudoinverse convention).
"""
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from functools import lru_cache
from math import factorial, prod
import os
import sys


@lru_cache(None)
def partitions(n, ceiling=None):
    """All partitions in reverse lexicographic order, including () for zero."""
    if n == 0:
        return ((),)
    bound = n if ceiling is None else min(n, ceiling)
    return tuple((first,) + tail for first in range(bound, 0, -1)
                 for tail in partitions(n - first, first))


@lru_cache(None)
def cells(shape):
    return frozenset((i, j) for i, row in enumerate(shape)
                     for j in range(row))


@lru_cache(None)
def character(shape, cycle_type):
    """An irreducible symmetric-group character, by rim-hook removal."""
    if not cycle_type:
        return int(not shape)
    r, tail = cycle_type[0], cycle_type[1:]
    result = 0
    for inner in partitions(sum(shape) - r):
        if len(inner) > len(shape) or any(a > b for a, b in zip(inner, shape)):
            continue
        skew = cells(shape) - cells(inner)
        if any({(i, j+1), (i+1, j), (i+1, j+1)} <= skew for i, j in skew):
            continue
        todo = [next(iter(skew))]
        reached = set(todo)
        while todo:
            i, j = todo.pop()
            for neighbor in ((i-1,j), (i+1,j), (i,j-1), (i,j+1)):
                if neighbor in skew and neighbor not in reached:
                    reached.add(neighbor)
                    todo.append(neighbor)
        if len(reached) != r:
            continue
        height = len({i for i, j in skew})
        result += (-1)**(height - 1) * character(inner, tail)
    return result


def dimension(shape):
    hooks = prod(shape[i] - j + sum(row > j for row in shape[i+1:])
                 for i, j in cells(shape))
    numerator = factorial(sum(shape))
    assert numerator % hooks == 0
    return numerator // hooks


def weingarten(N, mu):
    N = int(N)
    mu = tuple(int(x) for x in mu)
    if N < 1 or not mu or any(x <= 0 for x in mu) or tuple(sorted(mu, reverse=True)) != mu:
        raise ValueError('N must be positive and mu a nonempty decreasing partition')
    k = sum(mu)
    result = QQ(0)
    for shape in partitions(k):
        if len(shape) > N:
            continue
        denominator = prod(N + j - i for i, j in cells(shape))
        result += QQ(dimension(shape) * character(shape, mu)) / QQ(denominator)
    return result / QQ(factorial(k))


class UnitaryWeingarten(numberdb.Generator):
    table = 'T549'
    parameters = ('N', 'mu')
    type = 'Q'
    rigour = 'exact'
    digits = 100  # Exact fractions are never rounded.
    files = ('generate.py', 'checks.py', 'check.py', 'program.py', 'argument-selection.md',
             'numerical-evidence.md', 'skill-manifest.json', 'checks-output.json')

    def enumerate(self, max_dimension=12, max_degree=6):
        for N in range(1, max_dimension + 1):
            for k in range(1, min(max_degree, N) + 1):
                for mu in partitions(k):
                    yield {'N': str(N), 'mu': ','.join(map(str, mu))}

    def value(self, params, digits=100):
        return weingarten(int(params['N']), tuple(map(int, params['mu'].split(','))))


def configure_key():
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
        key = sys.stdin.read().strip()
        if '=' in key and key.split('=', 1)[0].isidentifier():
            key = key.split('=', 1)[1].strip().strip('\"\'')
        numberdb.configure(api_key=key)


if __name__ == '__main__':
    configure_key()
    generator = UnitaryWeingarten()
    if '--fill' in sys.argv or os.environ.get('NUMBERDB_PUBLISH') == '1':
        print(generator.publish(message='Fill 242 exact unitary Weingarten coefficients; independently checked by Gram inversion'))
    else:
        report = generator.verify(sample=None)
        print(report)
        sys.exit(0 if report.ok else 1)