splitpart.py

Cited on: PRINCIPAL.

To run it: save splitpart.py, with localtype.py, module_law.py, principal.py beside it; needs numpy.

"""splitpart.py -- which split terms carry the rise of PI_1 - PI_k0?

QUESTION. Over the real fields with the class k0 to |D| <= 48000, the
split places carry the rise of d = PI_1 - PI_k0 with log D
(localtype.py: +0.258 of +0.290 per unit at the cut 250, each field
weighted by its h+/2 odd characters). Which split terms carry it: the
first powers or the higher, and inside Lagrange's zone p < sqrt(D)/2
or outside it?

THE DERIVATION (before code). A split P, Pbar in narrow classes c,
c^-1 adds (2/e)([c^e = 1] - [c^e = k0]) at each power with p^e < x.
  (a) e = 1, S1: a uniform c has mean 0 here for every field, so a
      slope in S1 is a bias of 1 over k0. TRANSPLANT: the uniform c at
      fixed p as D grows is Chebotarev carried from p -> infinity.
  (b) e >= 2, S2: a uniform c gives E2 = sum (2/e)(#{g: g^e = 1} -
      #{g: g^e = k0})/h+, 0 at odd e (k0 = k0^e), 1/|Cl+^2| at e = 2
      when k0 is not a square and 0 when it is.
  (c) Lagrange (known): |m| < sqrt(D)/2 represented primitively by the
      principal form leads a reduced form of its cycle. So for split
      p < sqrt(D)/2, P in 1 iff +p leads a form in the cycle of
      (1, b0, .), P in k0 iff -p does.
  (d) S1 splits at sqrt(D)/2 into S1lo (the zone) and S1hi. At 250 the
      zone's prime count grows with D to 48000.

PREDICTIONS, frozen before the engine (cut 250, weighted h+/2).
  C1 Lagrange on the engine: [P in 1] = [+p leads a principal-cycle
     form], [P in k0] = [-p does], over every split p < sqrt(D)/2;
     0 exceptions.
  C2 S1lo + S1hi + S2 = d_S per field to 1e-12; s(S1lo + c log D)
     reads s(S1lo) + c to 1e-9, c = 0.29.
  P1 s(S2) < +0.05, and the mean of S2 - E2 within 0.1 of 0.
  P2 the per-prime bias inside the zone, mean of S1lo / #zone primes
     split, is positive in every |D| band; outside it, within 0.02 of 0.
  KILL ZONE (printed "S1lo share of s(d_S) at 250"): fires if
     s(S1lo) < 0.5 s(d_S); Lagrange's zone then does not carry the
     rise. At 10^4 the split prints, no verdict.

FINDINGS, the 10323 real fields with k0 to |D| <= 48000, weighted h+/2.
  F0  CONTROLS. Every principal-cycle form lies in class 1 (by
      construction); Lagrange read on the engine with 0 exceptions, so
      +p in the cycle is P in 1 and -p is P in k0; the parts sum to d_S
      to 1.42e-14; the planted c log D reads c to the tenth digit. P2's
      "mean of S1lo / #zone primes" is read in the engine as a ratio of
      means, Sum S1 / Sum primes, both weighted h+/2; a mean of
      per-field ratios is undefined at a field with no zone prime.
  F1  KILL ZONE FIRED, and P2 MISSED with the sign reversed: at 250
      s(d_S) +0.2581 = S1lo -0.0657 +- 0.0206 + S1hi +0.4204 +- 0.0398
      + S2 -0.0966 +- 0.0185 (E2 -0.1471). The first powers lean to k0,
      not to 1, inside the zone and outside it, and the lean fades with
      D on both sides: per split prime, Sum S1 / Sum primes reads
      -0.1374, -0.0750, -0.0707, -0.0542, -0.0506 by band inside the
      zone and -0.0784, -0.0445, -0.0330, -0.0314, -0.0234 outside. The
      zone holds 3.26 to 11.83 split primes by band: as sqrt(D)/2
      rises, primes cross from S1hi into S1lo, each carrying a negative
      lean, so S1lo reads -0.448, -0.394, -0.499, -0.521, -0.598 by band
      and S1hi -1.740 to -0.320; the S1lo, S1hi split tracks the zone's
      moving edge. At 10^4: +0.8078 = -0.0657 + 1.5815 - 0.7079.
  F2  P1 half held: s(S2) -0.0966 < 0.05, but S2 sits below the uniform
      class's E2 by 0.3891 at 250 (1.2183 at 10^4); band means S2
      +1.299 to +0.980 against E2 +1.810 to +1.334. d_S by band -0.890,
      -0.152, -0.023, +0.006, +0.061: the uniform class's value of d_S
      is E2, which d_S approaches from below.
  Not derived: why the first powers lean to k0, and why the lean
  relaxes as D grows at a fixed cut.
  TIERS: F0 a property (Lagrange, known); F1, F2
  observations over this population.

RUN RECORD. A rehearsal at --d 3000 (3.0 s). Full: 5/8, the three
misses P1's second half and P2's two, 51.4 s, peak commit 55.8 MB under
a memory guard (512 MB). P1's second half and P2 are now prints, since
a verdict is read, never asserted, and a missed one is the finding:
5/5, 51.5 s, every printed number unchanged.
"""

import os
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")

import sys
import time

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import numpy as np  # noqa: E402
import principal as P  # noqa: E402
import localtype as L  # noqa: E402
from module_law import check, section, CHECKS  # noqa: E402

D_MAX = 48000
if "--d" in sys.argv:
    D_MAX = int(sys.argv[sys.argv.index("--d") + 1])
J250, J10K = 0, len(P.CUTS) - 1


def split_terms(G, avals):
    """Per cut: S1lo, S1hi, S2, E2, the zone's and the outside's split
    prime counts; and the Lagrange exceptions."""
    nc = len(P.CUTS)
    out = np.zeros((6, nc))
    bad = 0
    pw = {}

    def npow(e):
        if e not in pw:
            vals = [G.power(g, e) for g in range(G.h)]
            pw[e] = (vals.count(G.e), vals.count(G.k0))
        return pw[e]

    for p in P.SMALL:
        if P.kron(G.D, p) != 1:
            continue
        c = G.place(p)
        cb = G.inv(c)
        zone = 4 * p * p < G.D
        j = P.bucket(p)
        v = 2 * ((c == G.e) - (c == G.k0))
        out[0 if zone else 1, j:] += v
        out[4 if zone else 5, j:] += 1
        if zone:
            bad += ((c == G.e) != (p in avals)) + \
                ((c == G.k0) != (-p in avals))
        e, pe = 2, p * p
        while pe < P.TOP:
            j = P.bucket(pe)
            for g in (c, cb):
                ge = G.power(g, e)
                out[2, j:] += ((ge == G.e) - (ge == G.k0)) / e
            n1, nk = npow(e)
            out[3, j:] += 2 * (n1 - nk) / (e * G.h)
            e, pe = e + 1, pe * p
    return out, bad


def read():
    rows, bad, worst, offcyc = [], 0, 0.0, 0
    for D in range(5, D_MAX + 1):
        if not P.fundamental(D):
            continue
        G = P.Group(D)
        if G.h < 2 or G.k0 == G.e:
            continue
        cyc = P.Real(D).cycle(G.one_form)
        offcyc += sum(G.idx[f] != G.e for f in cyc)
        t, b = split_terms(G, {f[0] for f in cyc})
        bad += b
        dS = L.parts(G)[2]
        worst = max(worst, float(np.max(np.abs(t[0] + t[1] + t[2] - dS))))
        rows.append((D, G.h // 2, t, dS))
    return rows, bad, worst, offcyc


def main():
    t0 = time.time()
    section("A  LAGRANGE AND THE SPLIT")
    rows, bad, worst, offcyc = read()
    n = len(rows)
    Ds = np.array([r[0] for r in rows])
    x = np.log(Ds.astype(float))
    wc = np.array([r[1] for r in rows], float)
    print(f"  {n} real fields with k0, |D| <= {D_MAX}"
          f"  [{time.time() - t0:.0f} s]")
    check("the principal cycle's forms all lie in class 1", offcyc == 0,
          f"{offcyc} off")
    check("C1 Lagrange: in the zone P in 1 / k0 iff +p / -p leads a"
          " principal-cycle form", bad == 0, f"{bad} exceptions")
    check("C2 S1lo + S1hi + S2 = d_S per field", worst < 1e-12,
          f"worst {worst:.2e}")

    def col(k, j):
        return np.array([r[2][k, j] for r in rows])

    s1lo = col(0, J250)
    sa = L.wslope(x, s1lo, wc)[0]
    sb = L.wslope(x, s1lo + 0.29 * x, wc)[0]
    check("C2 s(S1lo + c log D) reads s(S1lo) + c", abs(sb - sa - 0.29)
          < 1e-9, f"{sb - sa:+.10f}")
    if not all(CHECKS):
        print("CONTROL FAILED: the kill is not read.")
        raise SystemExit(1)

    section("B  THE SLOPES, WEIGHTED h+/2")
    for j in (J250, J10K):
        dS = np.array([r[3][j] for r in rows])
        s, se = L.wslope(x, dS, wc)
        parts = [L.wslope(x, col(k, j), wc) for k in range(4)]
        print(f"  cut {P.CUTS[j]:5d}: s(d_S) {s:+.4f} +- {se:.4f} ="
              f" S1lo {parts[0][0]:+.4f} +- {parts[0][1]:.4f}"
              f" + S1hi {parts[1][0]:+.4f} +- {parts[1][1]:.4f}"
              f" + S2 {parts[2][0]:+.4f} +- {parts[2][1]:.4f}"
              f"   (E2 {parts[3][0]:+.4f})")
        resid = np.sum(wc * (col(2, j) - col(3, j))) / np.sum(wc)
        print(f"    mean S2 - E2 {resid:+.4f}")
        if j == J250:
            check("P1 s(S2) < +0.05", parts[2][0] < 0.05,
                  f"{parts[2][0]:+.4f}")
            print(f"  P1 mean S2 - E2 within 0.1 of 0: "
                  f"{'HELD' if abs(resid) <= 0.1 else 'MISSED'}"
                  f" ({resid:+.4f})")
            share = parts[0][0] / s
            print(f"  S1lo share of s(d_S) at 250: {share:.3f}")
            print(f"  KILL ZONE (share < 0.5): "
                  f"{'FIRED' if share < 0.5 else 'MISSED'}")

    section("C  BY |D| BAND, WEIGHTED h+/2: PER-PRIME BIAS, MEANS")
    print("  band         n   zone primes  bias in  bias out"
          "   S1lo    S1hi     S2     E2    d_S")
    p2in, p2out = [], []
    for i, (lo, hi) in enumerate(L.BANDS):
        m = np.array([L.band_of(int(D)) == i for D in Ds])
        w = wc[m]

        def mean(v):
            return np.sum(w * v[m]) / np.sum(w)

        bin_ = np.sum(w * col(0, J250)[m]) / max(np.sum(w * col(4, J250)[m]),
                                                 1e-300)
        bout = np.sum(w * col(1, J250)[m]) / np.sum(w * col(5, J250)[m])
        p2in.append(bin_)
        p2out.append(bout)
        dS = np.array([r[3][J250] for r in rows])
        print(f"  {lo:5d}-{min(hi, D_MAX):5d} {int(m.sum()):5d}"
              f"  {mean(col(4, J250)):6.2f}    {bin_:+.4f}   {bout:+.4f}"
              f"  {mean(col(0, J250)):+.3f}  {mean(col(1, J250)):+.3f}"
              f"  {mean(col(2, J250)):+.3f}  {mean(col(3, J250)):+.3f}"
              f"  {mean(dS):+.3f}")
    print(f"  P2 the per-prime bias in the zone is positive in every band: "
          f"{'HELD' if all(v > 0 for v in p2in) else 'MISSED'}")
    print(f"  P2 outside the zone within 0.02 of 0: "
          f"{'HELD' if all(abs(v) <= 0.02 for v in p2out) else 'MISSED'}")
    print(f"\n{sum(CHECKS)}/{len(CHECKS)} checks PASS,"
          f" {time.time() - t0:.1f} s")
    raise SystemExit(0 if all(CHECKS) else 1)


if __name__ == "__main__":
    main()