checks.py

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

5962 bytes, as of the version from 2026-10-27 19:33. Recorded here, not run.

"""Independent exact checks for Umemura polynomials -- numberdb.org/T558.

Run: sage -python checks.py
Readback: sage -python checks.py --stored (NUMBERDB_API_KEY for the draft).
Campaign readback also accepts NUMBERDB_CHECK_STORED=1, or a stored.json file
beside this script. The latter permits offline verification of a saved document.

The determinant is reconstructed from the coefficient sum for
(1+t)^mu exp(x*t), with no calls to the generator's recurrence. It uses the
Leibniz expansion over permutations, independently of polynomial division.
Sources: Clarkson, Law and Lin, SIGMA 19 (2023), 080, Theorems 2.3 and 2.5,
Remarks 2.6 and Table 1; Kajiwara and Masuda, solv-int/9903015, Theorem 1.
"""
import itertools
import json
import math
import os
from pathlib import Path
import sys
import numberdb.sage as numberdb
from sage.rings.rational_field import QQ
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
from check import exactness, measure, agrees_with, names_its_rings

Q = PolynomialRing(QQ, names=('x', 'mu'), order='lex')
x, mu = Q.gens()


def determinant(rows):
    n = len(rows)
    result = Q.zero()
    for perm in itertools.permutations(range(n)):
        sign = (-1)**sum(perm[i] > perm[j] for i in range(n) for j in range(i+1,n))
        term = Q(sign)
        for i in range(n):
            term *= rows[i][perm[i]]
        result += term
    return result


def determinant_polynomial(n):
    if n < 0:
        return Q.one()
    p = {}
    for k in range(2*n):
        falling = Q.one()
        p[k] = Q.zero()
        for j in range(k+1):
            if j:
                falling *= mu-j+1
            p[k] += falling*x**(k-j) * (QQ(1)/QQ(math.factorial(j)*math.factorial(k-j)))
    rows = [[p.get(2*j+1-i,Q.zero()) for j in range(n)] for i in range(n)]
    scale = math.prod((2*j+1)**(n-j) for j in range(1,n+1))
    return scale*determinant(rows)


def reverse_bessel(n):
    return sum((QQ(math.factorial(n+k))/QQ(math.factorial(n-k)*math.factorial(k)*2**k))*x**(n-k) for k in range(n+1))


def painleve_residual(current, previous, n):
    a = current(x,mu-1)*previous
    b = current*previous(x,mu-1)
    d = a.derivative(x)*b-a*b.derivative(x)
    e = d.derivative(x)*b-2*d*b.derivative(x)
    alpha, beta = 2*n+2*mu-1, 2*n-2*mu+1
    return x*a*e-x*d**2+a*b*d-a*b*(alpha*a**2+beta*b**2)-x*a**4+x*b**4


def check_values(values):
    values = {int(n):Q(v) for n,v in values.items()}
    assert sorted(values) == list(range(6)), 'Expected every n from 0 through 5'
    assert not exactness(values)
    # Controls first: empty determinant, nonzero determinant, zero determinant,
    # and known nontrivial first polynomials. A perturbed input must fail.
    assert determinant([]) == 1
    assert determinant([[Q(2),Q(3)],[Q(5),Q(7)]]) == -1
    assert determinant([[Q(2),Q(4)],[Q(1),Q(2)]]) == 0
    assert determinant_polynomial(1) == x+mu
    assert determinant_polynomial(2) == (x+mu)**3-mu
    assert agrees_with({2:values[2]+1},determinant_polynomial)
    assert painleve_residual(x+mu,Q.one(),1) == 0
    assert painleve_residual(x+mu+1,Q.one(),1) != 0
    assert not agrees_with(values, determinant_polynomial)
    # Published Table 1, coefficientwise comparison of all five printed values.
    z = x+mu
    printed = {
      1:z,
      2:z**3-mu,
      3:z**6-5*mu*z**3+9*mu*z-5*mu**2,
      4:z**10-15*mu*z**7+63*mu*z**5-225*mu*z**3+315*mu**2*z**2-175*mu**3*z+36*mu**2,
      5:z**15-35*mu*z**12+252*mu*z**10+175*mu**2*z**9-2025*mu*z**8+945*mu**2*z**7-1225*mu*(mu**2-9)*z**6-26082*mu**2*z**5+33075*mu**3*z**4-350*mu**2*(35*mu**2+36)*z**3+11340*mu**3*z**2-225*mu**2*(49*mu**2-36)*z+7*mu**3*(875*mu**2-828)
    }
    for n,p in printed.items():
        assert p == values[n], ('printed polynomial', n)
    extended = {-1:Q.one(),**values,6:determinant_polynomial(6)}
    for n,p in values.items():
        degree = n*(n+1)//2
        assert p.degree(x) == degree
        assert p.coefficient({x:degree}) == 1
        assert all(c.denominator()==1 for c in p.coefficients())
        assert p(x,0) == x**degree
        assert p(x,1) == x**(n*(n-1)//2)*reverse_bessel(n)
        derivative = p.derivative(x)
        assert extended[n+1]*extended[n-1] == (x+mu)*p**2-x*(p*derivative.derivative(x)-derivative**2)-p*derivative
        previous = extended[n-1]
        shifted = previous(x,mu-1)
        assert p(x,mu-1)*previous == p*shifted+shifted.derivative(x)*p-shifted*derivative
        assert painleve_residual(p,previous,n) == 0, ('Painleve III', n)
    return {'entries_checked':6,'indices':list(range(6)),
            'independent_wronskians':6,'published_explicit_polynomials':5,
            'recurrence_degree_monicity_integrality':True,
            'mu_zero_and_reverse_bessel_specializations':True,
            'painleve_III_and_log_derivative_identities':True,
            'known_and_perturbed_controls':True,
            'measure':measure(values)}


if __name__ == '__main__':
    from generate import Umemura
    if os.environ.get('NUMBERDB_KEY_FROM_STDIN') == '1':
        key=sys.stdin.read().strip()
        if '=' in key and key.split('=',1)[0].replace('_','').isupper():
            key=key.split('=',1)[1].strip().strip('\"\'')
        os.environ['NUMBERDB_API_KEY']=key
    stored_path=Path(__file__).with_name('stored.json')
    stored='--stored' in sys.argv or os.environ.get('NUMBERDB_CHECK_STORED')=='1' or stored_path.exists()
    if stored:
        document=json.loads(stored_path.read_text()) if stored_path.exists() else numberdb.table('T558')
        values={int(row['params']['n']):Q(row['number'].replace('^','**')) for row in document['Numbers']}
    else:
        g=Umemura(); values={p['n']:g.value(p,100) for p in g.enumerate()}
    assert not names_its_rings(str(Path(__file__).with_name('generate.py')))
    report=check_values(values);report['source']='database readback' if stored else 'local generator'
    print(json.dumps(report,indent=2,default=str))