generate.py

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

5665 bytes, as of the version from 2026-10-28 09:23 (current). Recorded here, not run.

"""Zonal polynomials -- numberdb.org/T576

Run with SageMath (numberdb must be installed):
    sage -pip install numberdb
    sage -python generate.py                 # verify the stored draft/table
    sage -python generate.py --fill          # write entries with an authorized key

The default enumeration consists of complete low-degree partition slices.
See argument-selection.md for the bounds and their measured readability.
All arithmetic is in QQ[x1,...,xn]. The generator builds monic triangular
Laplace--Beltrami eigenpolynomials, then decomposes (x1+...+xn)^d in this
basis to impose the real zonal C normalization. It does not call Sage's Jack
implementation or use the hook-product conversion used in checks.py.
There is no floating-point arithmetic and no numerical precision choice.

Sources: Jiu and Koutschan, arXiv:2001.11599, Definition 2.11 and (1.3);
DLMF 35.4. The first source fixes the differential operator, the second fixes
the real-matrix normalization. A partition is recorded without zero padding.
"""
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
from functools import lru_cache
from itertools import permutations
import os
import sys

# Complete degree slices chosen from the measured expanded lengths.
DEGREE_LIMITS = ((2, 6), (3, 6), (4, 5), (5, 4))


def partitions(weight, ceiling=None):
    """All partitions in descending lexicographic order."""
    if weight == 0:
        yield ()
        return
    for first in range(min(weight, ceiling or weight), 0, -1):
        for tail in partitions(weight-first, first):
            yield (first,) + tail


@lru_cache(None)
def ring(n):
    return PolynomialRing(QQ, n, names=['x%d' % i for i in range(1, n+1)])


def dominates(lam, mu):
    return all(sum(lam[:i]) >= sum(mu[:i])
               for i in range(1, max(len(lam), len(mu))+1))


def monomial_symmetric(n, lam):
    R = ring(n)
    return R({tuple(exponents): QQ(1)
              for exponents in set(permutations(lam + (0,)*(n-len(lam))))})


def laplace_beltrami(f):
    R = f.parent()
    x = R.gens()
    df = [f.derivative(v) for v in x]
    answer = sum((v*v*df[i].derivative(v) for i,v in enumerate(x)), R.zero())
    for i in range(len(x)):
        for j in range(i+1,len(x)):
            numerator = x[i]**2*df[i] - x[j]**2*df[j]
            quotient, remainder = numerator.quo_rem(x[i]-x[j])
            assert not remainder, 'symmetry must cancel the denominator'
            answer += quotient
    return answer


@lru_cache(None)
def degree_slice(n, d):
    """Return every C_lambda of degree d in n alphabet variables."""
    R = ring(n)
    shapes = tuple(lam for lam in partitions(d) if len(lam) <= n)
    monomials = [monomial_symmetric(n,lam) for lam in shapes]
    exp = [lam+(0,)*(n-len(lam)) for lam in shapes]
    eigenvalues = [sum(a*(a-1+n-i) for i,a in enumerate(lam,1)) for lam in shapes]
    actions = [{tuple(e): c for e,c in laplace_beltrami(m).dict().items()}
               for m in monomials]
    transition = [[a.get(e,QQ(0)) for e in exp] for a in actions]
    monic = []
    for k,lam in enumerate(shapes):
        coeff = [QQ(0)]*len(shapes)
        coeff[k] = QQ(1)
        for j in range(k+1,len(shapes)):
            numerator = sum((coeff[i]*transition[i][j] for i in range(k,j)),QQ(0))
            if not dominates(lam,shapes[j]):
                assert numerator == 0
                continue
            denominator = QQ(eigenvalues[k]-eigenvalues[j])
            assert denominator > 0
            coeff[j] = numerator/denominator
        f = sum((c*m for c,m in zip(coeff,monomials)),R.zero())
        assert laplace_beltrami(f) == eigenvalues[k]*f, (n,d,lam,coeff,transition,f,laplace_beltrami(f),eigenvalues[k])
        monic.append(f)
    remainder = sum(R.gens(),R.zero())**d
    result = {}
    for lam,e,f in zip(shapes,exp,monic):
        scale = {tuple(a): c for a,c in remainder.dict().items()}.get(e,QQ(0))
        assert scale > 0
        result[lam] = R(scale*f)
        remainder -= result[lam]
    assert remainder == 0
    return result


class ZonalPolynomials(numberdb.Generator):
    table = 'T576'
    parameters = ('n', 'lambda')
    type = 'Q[]'
    rigour = 'exact'
    files = ('generate.py', 'checks.py', 'check.py', 'dry_run.py',
             'argument-selection.md')

    def enumerate(self):
        for n,limit in DEGREE_LIMITS:
            for d in range(2,limit+1):
                for lam in partitions(d):
                    if len(lam) <= n:
                        yield {'n':str(n),'lambda':','.join(map(str,lam))}

    def value(self, params, digits):
        n = int(params['n'])
        lam = tuple(int(a) for a in params['lambda'].split(','))
        if n < 1 or not lam or tuple(sorted(lam,reverse=True)) != lam \
                or min(lam) <= 0 or len(lam) > n:
            raise ValueError('expected a positive alphabet size and a partition of length at most n')
        return degree_slice(n,sum(lam))[lam]


if __name__ == '__main__':
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
        key = sys.stdin.read().strip()
        if '=' in key and key.split('=',1)[0].isupper():
            key = key.split('=',1)[1].strip().strip('\"\'')
        os.environ['NUMBERDB_API_KEY'] = key
    generator = ZonalPolynomials()
    if '--fill' in sys.argv or os.environ.get('NUMBERDB_PUBLISH') == '1':
        print(generator.publish(message='Fill exact real zonal C polynomials after independent checks'))
    else:
        result = generator.verify(sample=None)
        print(result)
        sys.exit(0 if result.ok else 1)