wholelim.py

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

Cited on: FLATTEN.

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

"""wholelim.py -- does the whole-norm tail's loss stay bounded in r?

QUESTION. vartail.py's tail at order s = r - 3 (d = 2) pays, over the
exact certificate, Lam = 2V mean|R|/||P||_1 times sqrt(E g^2)/E|g|
(P = Q_s c*, R = P/(1 - x), V the sign changes of P). It read 2.93 to
3.84 at ranks 5..10. Bounded in r, a tail from O(J_c), and with it a
uniform bound J_ch <= C r^(3/2), is in reach along this route; growing,
it is not.

BY HAND (the Gaussian limit at fixed r, J -> oo). sigma = sqrt(J)/2;
P_k -> 2^J sigma^(-1-s) f(y)/sqrt(2 pi), f = (He_s + b He_(s+2)) phi
least in L2 (tailsplit.py's limit, b1 = 0 by parity). Since
int He_n phi e^(i w y) dy/sqrt(2 pi) = (i w)^n e^(-w^2/2), at w =
sigma theta |P(e^(i theta))| -> 2^J sigma^(-s) |w|^s e^(-w^2/2)
|1 - b w^2|, and |1 - x| -> |theta|, so
  mean|R| -> 2^J sigma^(-s) 2^(s/2) Gamma(s/2) E|1 - bW|/(2 pi),
  W ~ chi^2_s (U = sin^2(theta/2) -> W/J), ||P||_1 -> 2^J sigma^(-s)
  int|f|/sqrt(2 pi), and the Cauchy-Schwarz factor tends to
  sqrt(E(1 - bW)^2)/E|1 - bW|, E(1 - bW)^2 = 1 - 2bs + b^2 s(s + 2):
  Lam -> 2V 2^(s/2) Gamma(s/2) sqrt(E(1 - bW)^2)/(sqrt(2 pi) int|f|).
Split Lam = A_var A_max A_cs: A_var = 2V max|F|/int|f|, F = (He_(s-1)
+ b He_(s+1)) phi, R's limit shape ((He_(n-1) phi)' = -He_n phi);
A_max = 2^(s/2) Gamma(s/2) E|1 - bW|/(sqrt(2 pi) max|F|) (max|R_k| <=
mean|R|); A_cs the Cauchy-Schwarz factor. By hand int|He_n phi| ~
sqrt(n!) n^(-1/4) and max|He_(s-1) phi|, 2^(s/2) Gamma(s/2) ~ sqrt(s!)
s^(-3/4): A_max, A_cs = O(1), A_var ~ sqrt(r): V counts every sign
change of f, only about sqrt(s) of them in phi's bulk.

PREDICTIONS (frozen before the engine). PL1 A_max and A_cs move under
20% over r = 20..40. PL2 A_var's and Lam's local slopes in log r over
r = 20..40 in [0.4, 0.6]. PL3 V_true = r - 1 at even r.
KILL (printed): Lam with V = V_true (the real roots of He_s + b
He_(s+2)) has local slope log(Lam(40)/Lam(20))/log 2 >= 0.3.
CONTROL (positive, read first): chi_abs against quadrature at s = 5,
37, 57 (added after review: E|1 - bW| cancels in Lam, so the band
below never tests it; it feeds A_max and A_cs). Lam with V = r - 1
within 15% of
vartail.exact_read's 2(r - 1) mean/||P||_1 sqrt(E g^2)/E|g| at
dualtail.py's J_t, ranks 8..10. PLANT: b = 0 in the circle mean and in
E(1 - bW)^2, read against the same band.

FINDINGS (copied from the printed output).
  (1) Control: chi_abs equals its quadrature to 1e-15 at s = 5, 37, 57;
      the limit over the exact loss is 0.999, 1.000, 1.000 at
      ranks 8..10 (exact 3.5524, 3.4780, 3.8357); the plant reads 1.856,
      1.986, 2.109, caught at every rank.
  (2) PL1, PL2, PL3 hold; the KILL fires. Lam(V_true) = 5.1468, 6.2180,
      7.1357, 7.9501, 8.6894 at r = 20, 30, 40, 50, 60; its local slope
      0.471 over r = 20..40 and 0.486 over 40..60. A_var carries it
      (slope 0.503; 3.0742 at r = 20 to 5.3248 at 60); A_max stays
      within 1.305 .. 1.361 at r >= 15 and A_cs rises to 1.2462 (slopes
      -0.045, 0.014).
  (3) V_true is r - 1 at even r and r - 3 at odd r at every rank read
      (vartail's var(P) 7, 6, 9 at ranks 8..10). b reads 2/(2s + 3) to
      the printed digits at every rank (0.28571 at s = 2, 0.01709 at
      s = 57), b1 below 1e-16. By Parseval int He_m phi He_n phi dy =
      i^(m - n) Gamma((m + n + 1)/2) at m + n even, 0 at odd, so b1 = 0
      and b = Gamma(s + 3/2)/Gamma(s + 5/2) = 2/(2s + 3) exactly; the
      grid's b is within 4.4e-16 of it, relative.
  (4) So the whole-norm tail's loss grows as sqrt(r), all of it in the
      variation step: P changes sign about r times, but only about
      sqrt(r) of its runs sit in the Gaussian bulk, and 2V max|R| pays
      for every one at the bulk's height.

RUN RECORD. 4.6 s, numpy grid of step 2e-4 over |y| <= 40 (tailsplit's),
E|1 - bW| through the regularised gamma at 30 digits; the control reads
vartail.exact_read in rationals.
"""
import math
import os
import sys
import time

os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
import numpy as np  # noqa: E402
import mpmath as mp  # noqa: E402
from numpy.polynomial import hermite_e as He  # noqa: E402

mp.mp.dps = 30
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


def chi_abs(s, b):
    """E|1 - bW|, W ~ chi^2_s, exactly through the regularised gamma."""
    s, b = mp.mpf(s), mp.mpf(b)
    m0 = 1 - b * s                       # E(1 - bW)
    if b == 0:
        return abs(m0)
    w0 = 1 / b
    if w0 <= 0:                          # 1 - bW keeps one sign
        return abs(m0)
    # E[(1 - bW) 1(W > w0)]: P(W > w0) and E[W 1(W > w0)] = s P(chi^2_(s+2) > w0)
    tail = mp.gammainc(s / 2, w0 / 2, mp.inf, regularized=True)
    tailw = s * mp.gammainc(s / 2 + 1, w0 / 2, mp.inf, regularized=True)
    neg = tail - b * tailw              # E[(1 - bW) 1(W > w0)] < 0 if b > 0
    return float(m0 - 2 * neg)


def limit(r, plant=False):
    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.linalg.solve(G[1:, 1:], -G[1:, 0])
    b1, b = beta
    f = fs[0] + b1 * fs[1] + b * fs[2]
    F = h(s - 1) + b1 * h(s) + b * h(s + 1)
    l1f = np.sum(np.abs(f)) * DY
    maxF = np.max(np.abs(F))
    # V_true: real roots of He_s + b1 He_(s+1) + b He_(s+2)
    c = np.zeros(s + 3)
    c[s], c[s + 1], c[s + 2] = 1, b1, b
    rts = He.hermeroots(c)
    vtrue = int(np.sum(np.abs(rts.imag) < 1e-7 * (1 + np.abs(rts.real))))
    bb = 0.0 if plant else b
    gam = float(mp.mpf(2) ** (mp.mpf(s) / 2) * mp.gamma(mp.mpf(s) / 2))
    Eabs = chi_abs(s, bb)
    E2 = 1 - 2 * bb * s + bb * bb * s * (s + 2)
    Acs = math.sqrt(E2) / Eabs
    Amax = gam * Eabs / (math.sqrt(2 * math.pi) * maxF)
    unit = gam * math.sqrt(E2) / (math.sqrt(2 * math.pi) * l1f)
    return dict(b=b, b1=b1, vtrue=vtrue, Acs=Acs, Amax=Amax,
                Avar1=2 * (r - 1) * maxF / l1f, Avar=2 * vtrue * maxF / l1f,
                lam1=2 * (r - 1) * unit, lam=2 * vtrue * unit)


def chi_quad(s, b):
    """E|1 - bW| by quadrature of the chi^2_s density, split at 1/b."""
    s, b = mp.mpf(s), mp.mpf(b)
    lc = (s / 2) * mp.log(2) + mp.loggamma(s / 2)
    def dens(w):
        return abs(1 - b * w) * mp.exp((s / 2 - 1) * mp.log(w) - w / 2 - lc)
    return float(mp.quad(dens, [0, 1 / b, mp.inf]))


def control():
    from vartail import exact_read, JT
    ok, caught = True, False
    for s, b in ((5, 2 / 13), (37, 2 / 77), (57, 2 / 117)):
        cq, cf = chi_quad(s, b), chi_abs(s, b)
        ok &= abs(cq - cf) <= 1e-12
        print("CONTROL E|1 - bW| at s = %d: closed %.15f, quadrature %.15f"
              % (s, cf, cq))
    for r in (8, 9, 10):
        J = JT[r]
        e = exact_read(r, r - 3, J)
        l1P = mp.mpf(e["l1P"].numerator) / e["l1P"].denominator
        ex = float(2 * (r - 1) * e["mean"] / l1P * mp.sqrt(e["Eg2"]) / e["Eg"])
        lm = limit(r)["lam1"]
        lp = limit(r, plant=True)["lam1"]
        within = abs(lm / ex - 1) <= 0.15
        pin = abs(lp / ex - 1) <= 0.15
        ok &= within
        caught |= not pin
        print("CONTROL r = %2d J = %4d: exact %.4f, limit %.4f (%.3f), "
              "PLANT %.4f (%.3f) %s; var(P) %d" % (
                  r, J, ex, lm, lm / ex, lp, lp / ex,
                  "in band" if pin else "out", e["V"]), flush=True)
    print("control: %s; plant %s" % ("ok" if ok else "OFF",
                                      "caught" if caught else "MISSED"))
    return ok, caught


def main():
    t0 = time.time()
    ok, caught = control()
    rows = {}
    for r in list(range(5, 11)) + [15, 20, 25, 30, 35, 40, 50, 60]:
        L = limit(r)
        rows[r] = L
        print("r = %2d  b %+.5f  b1 %.1e  V_true %2d  A_var %.4f  A_max %.4f"
              "  A_cs %.4f  Lam(V_true) %.4f  Lam(r - 1) %.4f" % (
                  r, L["b"], L["b1"], L["vtrue"], L["Avar"], L["Amax"],
                  L["Acs"], L["lam"], L["lam1"]), flush=True)

    def slope(key, a=20, z=40):
        return math.log(rows[z][key] / rows[a][key]) / math.log(z / a)
    print("local slopes over r = 20..40: Lam(V_true) %.3f, Lam(r - 1) %.3f,"
          " A_var %.3f, A_max %.3f, A_cs %.3f; over 40..60: Lam %.3f" % (
              slope("lam"), slope("lam1"), slope("Avar"), slope("Amax"),
              slope("Acs"), slope("lam", 40, 60)))
    rb = max(abs(L["b"] * (2 * r - 3) / 2 - 1) for r, L in rows.items())
    print("largest |b (2s + 3)/2 - 1| over the ranks read: %.1e" % rb)
    pl1 = all(max(rows[r][k] for r in (20, 25, 30, 35, 40))
              / min(rows[r][k] for r in (20, 25, 30, 35, 40)) < 1.2
              for k in ("Amax", "Acs"))
    pl2 = all(0.4 <= slope(k) <= 0.6 for k in ("Avar", "lam"))
    pl3 = all(rows[r]["vtrue"] == r - 1 for r in rows if r % 2 == 0)
    print("PL1 %s; PL2 %s; PL3 %s" % tuple("holds" if v else "fails"
                                           for v in (pl1, pl2, pl3)))
    kill = slope("lam") >= 0.3
    print("KILL %s (%.1f s)" % ("fires" if kill else "misses",
                                time.time() - t0))
    return not ok or not caught


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