tailsplit.py

Checks: FLATTEN: Every rank's band of failing depths ends theorem; rule; pattern.

Cited on: FLATTEN.

To run it: save tailsplit.py, with bandend.py, flatten.py beside it; needs mpmath, numpy, sympy.

"""tailsplit.py -- which loss makes the dual tail's start J_t outgrow
r^(3/2)?

QUESTION. dualtail.py's tail passes at order s when m_s > I_r sum_m
|b_m| Lh_(s+m), Lh_n = 2n I_n (n >= 1), 2^J (n = 0); the exact
certificate passes when m_s > H_ch ||Q_s c*||_1. So the tail's test is
T = A/(l1 l2 l3): A = DUAL_s/H_ch, l1 = I_r/H_ch, l2 = sum |b_m|
L_(s+m)/||Q_s c*||_1 (the triangle inequality), l3 = sum |b_m|
Lh_(s+m)/sum |b_m| L_(s+m) (the variation lemma), L_n = ||(1 + x)^J
(1 - x)^n||_1. Read at the binding order s = r - 3.

BY HAND (the Gaussian limit at fixed r, a TRANSPLANT; closed forms by
quadrature, limits(), f = He_s phi + b2 He_(s+2) phi, b1 = 0 by
parity): l1 ->
sqrt(2 pi) 2^(r/2-1) Gamma(r/2)/(pi max|He_(r-1) phi|), 1 at odd r; l2
-> sum |b_m| int|He_(s+m) phi|/int|f|; l3 -> sum |b_m| 2n 2^(n/2-1)
Gamma(n/2)/pi over sum |b_m| int|He_n phi|/sqrt(2 pi). Their product
Lam is 4.39 .. 13.76 at ranks 5..10 against J_t/J_c = 5.11 .. 14.95; at
r = 20, 30, 40 l2 is 7.58, 9.43, 10.96 and l3 3.78, 4.71, 5.48.

PREDICTIONS (frozen before the engine). Q1 the exact l1, l2, l3 at
J = J_t(r) within 15% of their limits at ranks 5..10. Q2 l1 in
[0.95, 1.2] there. Q3 Lam(J_t)/(J_t/J_c) in [0.75, 1.25].
KILL: l1 carries the growth: l1(J_t(10))/l1(J_t(5)) > 1.5, or an l1 at
J_t outside [0.8, 1.5].
CONTROL, read first: the exact T >= 1 at J_t(r) and < 1 at 0.9 J_t(r),
ranks 5..10. THE PLANT: Lh_n = n I_n must lift T(0.9 J_t) above 1 at
some rank.

FINDINGS (copied from the printed output).
  (1) Control: T(J_t) = 1.0030, 1.0021, 1.0018, 1.0009, 1.0006, 1.0004
      and T(0.9 J_t) = 0.8975 .. 0.8983 at ranks 5..10: the exact split
      is the tail's own test, its rational bounds losing under 0.3%. The
      plant reads 1.7950 .. 1.7966, caught at every rank.
  (2) The kill misses; Q1, Q2, Q3 hold. At J_t, ranks 5..10: l1 =
      1.0000, 1.1054, 1.0043, 1.0795, 1.0024, 1.0625; l2 = 3.1758,
      4.0202, 4.0715, 4.7702, 4.8021, 5.4077; l3 = 1.5488, 1.7910,
      2.0063, 2.1986, 2.3728, 2.5336; Lam = 4.919, 7.959, 8.204,
      11.321, 11.422, 14.557 against J_t/J_c = 5.106 .. 14.953 (Lam over
      it 0.949 .. 0.975). Exact over limit: l1 and l3 within 0.4%, l2
      1.116 falling to 1.058. The plant reads nothing: n I_n halves l3
      and doubles T by arithmetic (2 x 0.8983 = 1.7966); the control's
      content is T at J_t and 0.9 J_t.
  (3) So J_t is about Lam J_c: the tail's start is the certificate's
      times the triangle inequality and the variation lemma; the
      champion's mean over its height, l1, stays within 11% of 1. In the
      limit (limits(), r = 5..10, 20, 30, 40) l2 = 2.8448 .. 10.9643 and
      l3 = 1.5431 .. 5.4799, their local slopes in log r falling from
      0.846, 0.715 (r = 5..10) to 0.532, 0.535 (r = 20..40): about
      sqrt(r) each, Lam = 61.011 at r = 40. A tail from O(J_c) needs
      ||Q_s c*||_1 bounded within a constant, not term by term.

RUN RECORD. 2.4 s, every rank exact in rationals but the Beta values
(30 digits). The limits first ran as a one-off quadrature outside the
script; limits() now prints them, the same to the printed digits; 3.2 s
in all.
"""
import math
import os
import sys
import time
from fractions import Fraction
from math import comb

os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
import mpmath as mp  # noqa: E402
from bandend import Q, pmul, champ_height, gram, solve  # noqa: E402

mp.mp.dps = 30
JT = {5: 240, 6: 478, 7: 817, 8: 1267, 9: 1839, 10: 2542}  # dualtail.py
JC = {5: 47, 6: 57, 7: 97, 8: 108, 9: 157, 10: 170}        # bandend.py


def limits(r):
    """(l1, l2, l3) in the Gaussian limit at s = r - 3, by quadrature on
    a grid of step 2e-4 over |y| <= 40: f = He_s phi + b1 He_(s+1) phi +
    b2 He_(s+2) phi least in L2 (b1 = 0 by parity)."""
    import numpy as np
    from numpy.polynomial import hermite_e as He
    y = np.linspace(-40, 40, 400001)
    dy = y[1] - y[0]
    phi = np.exp(-y ** 2 / 2)

    def h(n):
        c = np.zeros(n + 1)
        c[n] = 1
        return He.hermeval(y, c) * phi
    s = r - 3
    fs = [h(s + m) for m in range(3)]
    G = np.array([[np.sum(fs[i] * fs[j]) * dy for j in range(3)]
                  for i in range(3)])
    beta = np.r_[1, np.linalg.solve(G[1:, 1:], -G[1:, 0])]
    f = sum(beta[m] * fs[m] for m in range(3))
    l1s = [np.sum(np.abs(g)) * dy for g in fs]
    l2 = (sum(abs(beta[m]) * l1s[m] for m in range(3))
          / (np.sum(np.abs(f)) * dy))
    l1 = (math.sqrt(2 * math.pi) * 2 ** (r / 2 - 1) * math.gamma(r / 2)
          / (math.pi * np.max(np.abs(h(r - 1)))))

    def ih(n):
        return 2 ** (n / 2 - 1) * math.gamma(n / 2) / math.pi
    l3 = (sum(abs(beta[m]) * 2 * (s + m) * ih(s + m) for m in range(3))
          / sum(abs(beta[m]) * l1s[m] / math.sqrt(2 * math.pi)
                for m in range(3)))
    return float(l1), float(l2), float(l3)


def I(n, J):
    """2^(J+n-1) B((J + 1)/2, n/2)/pi, the mean modulus on the circle."""
    return (mp.mpf(2) ** (J + n - 1) * mp.beta(mp.mpf(J + 1) / 2,
                                               mp.mpf(n) / 2) / mp.pi)


def split(r, J, plant=False):
    s = r - 3
    d = r - 1 - s
    y = solve(gram(J, s, d), [1] * (d + 1))
    tot = sum(y)
    c = [v / tot for v in y]
    m = 1 / tot
    b = [(-1) ** k * sum(comb(i, k) * c[i] for i in range(k, d + 1))
         for k in range(d + 1)]
    l1q = sum(abs(v) for v in pmul(Q(J, s), c))
    H = champ_height(r, J)
    L = [sum(abs(v) for v in Q(J, s + k)) for k in range(d + 1)]
    k2 = 1 if plant else 2
    Lh = [k2 * (s + k) * I(s + k, J) if s + k else mp.mpf(2) ** J
          for k in range(d + 1)]
    fr = lambda q: mp.mpf(q.numerator) / q.denominator  # noqa: E731
    A = fr(m / (l1q * H))
    l1 = I(r, J) / H
    sb = sum(abs(fr(b[k])) * L[k] for k in range(d + 1))
    l2 = sb / fr(l1q)
    l3 = sum(abs(fr(b[k])) * Lh[k] for k in range(d + 1)) / sb
    return float(A), float(l1), float(l2), float(l3), float(A / (l1 * l2 * l3))


def main():
    t0 = time.time()
    ctl, plant = True, False
    rows = {}
    for r in range(5, 11):
        at = split(r, JT[r])
        lo = split(r, int(0.9 * JT[r]))
        pl = split(r, int(0.9 * JT[r]), plant=True)
        ctl &= at[4] >= 1 and lo[4] < 1
        plant |= pl[4] >= 1
        rows[r] = at
        print("r = %2d  T(J_t) %.4f  T(0.9 J_t) %.4f  plant %.4f  [%.1f s]"
              % (r, at[4], lo[4], pl[4], time.time() - t0), flush=True)
    print("CONTROL T >= 1 at J_t and < 1 at 0.9 J_t, ranks 5..10: %s;"
          " the plant lifts T(0.9 J_t) above 1 at some rank: %s"
          % (ctl, plant), flush=True)
    LIM = {}
    for r in (5, 6, 7, 8, 9, 10, 20, 30, 40):
        LIM[r] = limits(r)
        a, b, c = LIM[r]
        print("limit r = %2d  l1 %.4f  l2 %.4f  l3 %.4f  Lam %.3f"
              % (r, a, b, c, a * b * c), flush=True)
    for r1, r2 in ((5, 10), (10, 20), (20, 40)):
        print("local slope over r = %d..%d: l2 %.3f, l3 %.3f" % (
            r1, r2, *(math.log(LIM[r2][i] / LIM[r1][i]) / math.log(r2 / r1)
                      for i in (1, 2))))
    q1 = q2 = q3 = True
    for r, (A, l1, l2, l3, T) in rows.items():
        lam = l1 * l2 * l3
        rel = [x / y for x, y in zip((l1, l2, l3), LIM[r])]
        q1 &= all(0.85 <= v <= 1.15 for v in rel)
        q2 &= 0.95 <= l1 <= 1.2
        q3 &= 0.75 <= lam / (JT[r] / JC[r]) <= 1.25
        print("r = %2d  J_t %4d  A %.3f  l1 %.4f  l2 %.4f  l3 %.4f  Lam %.3f"
              "  J_t/J_c %.3f (Lam over it %.3f)  exact/limit %.3f %.3f %.3f"
              % (r, JT[r], A, l1, l2, l3, lam, JT[r] / JC[r],
                 lam / (JT[r] / JC[r]), *rel))
    kill = rows[10][1] / rows[5][1] > 1.5 or any(
        not 0.8 <= v[1] <= 1.5 for v in rows.values())
    print("Q1 within 15%% of the limits: %s; Q2 l1 in [0.95, 1.2]: %s; Q3"
          " Lam/(J_t/J_c) in [0.75, 1.25]: %s; KILL (l1 carries the"
          " growth) %s  [%.1f s]" % (q1, q2, q3, "fires" if kill else
                                     "misses", time.time() - t0))
    return not (ctl and plant) or kill


if __name__ == "__main__":
    sys.exit(1 if main() else 0)