classes.py

Checks: READING: The real support theorem.

Cited on: READING.

To run it: save classes.py, with real_support.py beside it; needs mpmath, numpy.

"""
classes.py -- the real support read by residue classes mod P = d - 1:
L(d) = W(d) + E(d), W the class sums' norm in closed form, E the mass
against each class's sign.

QUESTION R. With h the bounded inverse of Delta(z) = 1 - z - ... - z^d
(real_support.py), H_r = sum over m = r mod P of h_m and W = sum_r |H_r|,
is E = L - W at most A/d, A small enough that the real support closes
the carry bound past a computed d, and where in m does E sit?

BY HAND.
  (D1) At w^P = 1, w != 1, the w^j, j = 1..P, sum to 0 and w^d = w, so
       Delta(w) = 1 - w; Delta(1) = -P.
  (D2) h is in l1, so H_r = (1/P) sum_k w^(-kr)/Delta(w^k), w =
       e^(2 pi i/P); the k = 0 term is -1/P^2 and the DFT of r -> r gives
       the rest: H_r = ((P - 1)/2 - r)/P - 1/P^2, r = 0..P-1.
  (D3) W = P/4 at P even; W = (P^2 - 1)/(4P) + 1/P^2 at P odd. So W <=
       (d - 1)/4 at every d >= 3 but d = 4 (P odd needs 1/P^2 <= 1/(4P)),
       with equality at odd d; at d = 4, W = 7/9 > 3/4.
  (D4) With s_r the sign of H_r, E = sum_m e_m, e_m = |h_m| - s_r(m) h_m
       = 2 (s_r(m) h_m)^- >= 0: E is a sum over m, so its share by band
       of m is defined.
  (D5) At d = 0 mod 4 (P = 3 mod 4), C_d + 1 - W = 1/4 + 1/(4P) - 1/P^2,
       so past a computed d the bound needs E < 1/4 less the sup over t's
       excess.

PREDICTIONS AND KILL (fixed before the runs).
  R1 CONTROL: the FFT class sums equal D2's sawtooth to 1e-12 at d =
     3..100, and W equals the triangle wave's carry P/4 at d = 9, 13, 21.
  R2 d E(d) lies in [4, 7] at every d = 21..100 and falls at every step
     within each class of d mod 4. KILL of the route: d E rising over
     d = 60..100 in some class mod 4.
  R3 the share of E from |m| < 2P, printed at d = 21, 40, 60, 100; above
     1/2 at d = 100 means the edge carries E.
  R4 (fixed after R1-R3's run, before its print) at d = 40, 60, 100 more
     than 90% of E lies in the classes within 3 of r = 0 mod P or of
     (P - 1)/2, and E's shares over m/P^3 in [0, 1/16), [1/16, 1/8),
     [1/8, 1/4), [1/4, inf) agree between d = 60 and 100 to 0.02.

FINDINGS (copied from the runs' prints).
  - R1 holds: the class sums equal D2's sawtooth to 5.7e-14 and W D3's
    closed form to 1.2e-12 at d = 3..100; W = 2, 3, 5 at d = 9, 13, 21,
    the wave's P/4.
  - R2 holds: d E runs 4.6739 .. 5.6594 over d = 21..100 and falls at
    every step of 4; the kill does not fire. It splits by parity: at d =
    99, 100, E = 4.721e-02, 4.914e-02, d E = 4.6739, 4.9140 (L =
    24.547210794, 24.796717085, W = 24.5, 24.747576778).
  - R3: the edge carries at most 0.036 of E at d = 21 and none to the
    third decimal at d = 40, 60, 100. The share of E at 0 <= m < 2P is 0.032
    at d = 21 and 0.000 at d = 40, 60, 100; 2P <= m < P^3/8 holds 0.783,
    0.826, 0.828, 0.829 and m >= P^3/8 the rest, m < 0 at most 0.004.
  - R4 holds: within 3 classes of r = 0 or (P - 1)/2 lie 0.9634, 0.9661,
    0.9678 of E at d = 40, 60, 100; E's shares by m/P^3 over the four
    bands are 0.3994, 0.4297, 0.1664, 0.0046 at d = 100, within 0.0024
    of d = 60's. All three d are 0 mod 4 (P odd, 14 classes in the
    near set, its first half r = 0..3 and P - 3..P - 1 about the jump);
    d = 2 mod 4 and odd d are not printed. So at d = 0 mod 4 E reads as
    a scaling profile in m/P^3 at the sawtooth's two zeros (observation,
    three d).

HEAT FLOW (BY HAND after R1-R3, before R4; a reading, not a proof, its
root expansion a TRANSPLANT from k much below P). For m >= 0, h_m = sum_k
c_k x_k^m over the d - 1 roots inside the disc, x^d (x - 2) = -1, x_k
near e^(i theta_k), theta_k = 2 pi k/P: (d - 1) phi = 2 pi k - phi^3 +
..., |x|^d = (5 - 4 cos phi)^(-1/2), so log x_k ~ -theta_k^2/P +
i(theta_k - theta_k^3/P). At m = qP + r, x_k^m ~ e^(i theta_k r)
e^(-4 pi^2 k^2 m/P^3): in each class the leading term is the sawtooth H_r
under the heat flow for time m/P^3. The sawtooth is odd about r = -1/2
(its jump, between classes P - 1 and 0) and about r = (P - 1)/2 (its
zero), so the flow pins both zeros and adds none, and the class's sign
holds; E comes from the O(1/P) corrections (the drift theta^3/P,
c_k's corrections, the -1/P^2) moving a zero by O(1) classes: E ~ c/P,
split by parity (a class sits on the zero (P - 1)/2 only when P is odd,
d even, its H_r = -1/P^2). R4 reads it at P odd only.

RUN RECORD: the bare run 7.5 s, peak commit 198.4 MB.
"""

import os
import sys

os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
import numpy as np  # noqa: E402
from real_support import inverse_fft  # noqa: E402

CHECKS = []
READS = {}


def check(name, ok, detail=""):
    CHECKS.append(bool(ok))
    print(f"  [{'PASS' if ok else 'FAIL'}] {name}" +
          (f" -- {detail}" if detail else ""), flush=True)


def closed_H(P):
    r = np.arange(P)
    return ((P - 1) / 2 - r) / P - 1 / P ** 2


def closed_W(P):
    return P / 4 if P % 2 == 0 else (P * P - 1) / (4 * P) + 1 / P ** 2


def read(d):
    """L, W (numeric), the class sums' error against D2, E and its
    pointwise split e_m by band, kept in READS."""
    if d in READS:
        return READS[d]
    P = d - 1
    hf, hb, alias = inverse_fft(d)
    m = np.concatenate([-np.arange(len(hb) - 1, 0, -1), np.arange(len(hf))])
    h = np.concatenate([hb[:0:-1], hf])
    del hf, hb
    r = m % P
    H = np.bincount(r, weights=h, minlength=P)
    err = np.abs(H - closed_H(P)).max()
    s = np.sign(H)
    e = np.abs(h) - s[r] * h
    L = np.abs(h).sum()
    W = np.abs(H).sum()
    E = e.sum()
    c = np.minimum(r, P - r)                    # distance to 0 mod P
    c = np.minimum(c, np.abs(r - (P - 1) / 2))   # or to (P - 1)/2
    near = e[c <= 3].sum()
    tau = m / P ** 3
    prof = [e[(tau >= a) & (tau < b)].sum()
            for a, b in ((0, 1 / 16), (1 / 16, 1 / 8), (1 / 8, 1 / 4),
                         (1 / 4, np.inf))]
    bands = {
        "m < 0": e[m < 0].sum(),
        "0 <= m < 2P": e[(m >= 0) & (m < 2 * P)].sum(),
        "2P <= m < P^3/8": e[(m >= 2 * P) & (m < P ** 3 / 8)].sum(),
        "m >= P^3/8": e[m >= P ** 3 / 8].sum(),
    }
    READS[d] = (L, W, err, E, bands, alias, near, prof)
    return READS[d]


def section_control():
    """R1: the class sums against D2's sawtooth and W against D3, d =
    3..100; W against the triangle wave's carry at d = 9, 13, 21."""
    print("R1/R2 THE CLASSES MOD d - 1, d = 3..100")
    errs, Wgap = [], []
    for d in range(3, 101):
        L, W, err, E, bands, alias, near, prof = read(d)
        P = d - 1
        errs.append(err)
        Wgap.append(abs(W - closed_W(P)))
        print(f"  d = {d:3d}: L {L:.9f}  W {W:.9f}  E {E:.3e}  d E "
              f"{d * E:.4f}  class err {err:.1e}  alias {alias:.1e}",
              flush=True)
    check("R1 class sums = ((P - 1)/2 - r)/P - 1/P^2 at d = 3..100",
          max(errs) < 1e-12, f"max error {max(errs):.1e}")
    check("R1 W = D3's closed form at d = 3..100", max(Wgap) < 1e-11,
          f"max gap {max(Wgap):.1e}")
    check("R1 control: W = the triangle wave's P/4 at d = 9, 13, 21",
          all(abs(read(d)[1] - (d - 1) / 4) < 1e-11 for d in (9, 13, 21)),
          " ".join(f"{read(d)[1]:.12f}" for d in (9, 13, 21)))


def section_e():
    """R2-R4: d E over d = 21..100, where in m E sits, and the classes
    and scale that carry it at d = 40, 60, 100."""
    dE = {d: d * read(d)[3] for d in range(21, 101)}
    band = [d for d in range(21, 101) if not 4 <= dE[d] <= 7]
    check("R2 d E in [4, 7] at d = 21..100", not band,
          f"outside at {band}; range {min(dE.values()):.4f}"
          f" .. {max(dE.values()):.4f}")
    rises = [d for d in range(25, 101) if dE[d] >= dE[d - 4]]
    check("R2 d E falls at every step of 4, d = 21..100", not rises,
          f"rises at {rises}")
    late = {c: [d for d in range(64, 101) if d % 4 == c and dE[d] >= dE[d - 4]]
            for c in range(4)}
    check("R2 KILL not fired: no class mod 4 rises over d = 60..100",
          not any(late.values()), f"{late}")
    print("R3 WHERE E SITS")
    for d in (21, 40, 60, 100):
        L, W, err, E, bands, alias, near, prof = read(d)
        print(f"  d = {d}: E {E:.6f}; " + "; ".join(
            f"{k} {v / E:.3f}" for k, v in bands.items()))
    print("R4 THE CLASSES AND THE SCALE THAT CARRY E")
    for d in (40, 60, 100):
        L, W, err, E, bands, alias, near, prof = read(d)
        print(f"  d = {d}: share within 3 of 0 or (P - 1)/2 {near / E:.4f};"
              f" by m/P^3 " + " ".join(f"{v / E:.4f}" for v in prof))
    check("R4 more than 90% of E within 3 classes of r = 0 or (P - 1)/2, "
          "d = 40, 60, 100", all(read(d)[6] > 0.9 * read(d)[3]
                             for d in (40, 60, 100)))
    gap = max(abs(a / read(60)[3] - b / read(100)[3])
              for a, b in zip(read(60)[7], read(100)[7]))
    check("R4 E's profile in m/P^3 agrees at d = 60 and 100 to 0.02",
          gap < 0.02, f"largest gap {gap:.4f}")


def main():
    section_control()
    section_e()
    print(f"{sum(CHECKS)}/{len(CHECKS)} checks passed")
    sys.exit(0 if all(CHECKS) else 1)


if __name__ == "__main__":
    main()