tame.py

Checks: CLOCK: The width theorem; OBSERVATORY: The tame readout theorem.

Cited on: CLOCK, OBSERVATORY.

To run it: save tame.py, with clock.py, module_law.py, triangle.py beside it; standard library only.

"""tame.py -- at an odd prime, where a class of units lands past the bend,
read off one coefficient level of the Eisenstein polynomial, and what
that makes of a head's width.

QUESTION. Let p be odd and K/Q_p totally ramified of degree e, cut out
by F = x^e + sum_{i=1}^{e-1} p b_i x^i - p d with root pi, u = p/pi^e
(triangle.py), and let the bend s = e/(p - 1) be an integer. A CLASS is
a pair (c, m) with c p^m = s, m >= 0: a unit z of level exactly c,
v(z - 1) = c, reaches level s after m p-th powers, and its LANDING is
v(z^(p^(m+1)) - 1), where it lands past the bend. Where do the units of
a class land, and what does the least landing read?

THE OBJECT READ. The DEPARTURE of F is
    l = v(u + 1) = min( e v_p(d + 1),  min_i ( e v_p(b_i) + i ) ),
the least level (triangle.py) of a nonzero coefficient digit of
(F - x^e - p)/p: u + 1 = (1 + D)/D with D = d - sum b_i pi^i, and the
terms of 1 + D sit at distinct levels mod e. So l = 0 iff d != -1 mod p
(ubar != -1), l is infinite at F = x^e + p (u = -1), and every value
between is the level of one digit.

THE ARGUMENT (written before the engine).
  (1) BELOW THE BEND. Let z = 1 + pi^c t, t a unit of residue a, and
      x_j = z^(p^j) - 1, so v(x_j) = c p^j for j <= m (the levels stay
      below the bend, where psi multiplies; clock.py). Then x_(j+1) =
      x_j^p (1 + rho_j) with rho_j = (p x_j + middle binomials)/x_j^p of
      valuation (p - 1)(s - c p^j) >= c (p - 1)^2 p^(m-1) for j < m.
      Raising a 1-unit to a p-th power never lowers its level, so
          x_m = pi^s t^(p^m) (1 + eta),   v(eta) >= c(p-1)^2 p^(m-1),
      and c(p - 1)^2 > p at every odd p, so v(eta) > p^m (eta = 0 at
      m = 0).
  (2) THE LANDING. y = 1 + x_m, and
          y^p - 1 = (p x_m / u) * B,
          B = u + x_m^(p-1)/p * u + u sum_{k=2}^{p-1} C(p, k)/p x_m^(k-1),
      with v(p x_m / u) = e + s = p s. Since x_m^(p-1) = pi^e t^((p-1)p^m)
      (1 + eta)^(p-1) and p = u pi^e, the second term of B is
      W := t^((p-1)p^m) (1 + eta)^(p-1). The k-th binomial term sits at
      level (k - 1)s >= p^m, equality only at k = 2 and c = 1. Write
      t = a + t_1 pi + ... with a in {1, ..., p - 1}; a^(p-1) = 1 mod p,
      a level e > p^m, so t^(p-1) = 1 - (t_1/a) pi + ..., and its
      p^m-th power is 1 - (t_1/a) pi^(p^m) + deeper, since below the
      bend each p-th power takes a level-r leading digit gamma to
      gamma^p = gamma at level p r. So
          B = (u + 1) + (-t_1/a + [c = 1] const) pi^(p^m) + deeper,
      const a fixed residue, and -t_1/a runs over all of F_p with t_1.
  (3) THE READOUT. If l < p^m, v(B) = l for every unit of the class,
      which lands RIGIDLY at p s + l. If l >= p^m, every landing is at
      least p s + p^m and one value of t_1 makes the level-p^m digit of
      B nonzero. Hence the least landing of the class (c, m) is
          L(c, m) = p s + min(l, p^m),
      the whole class landing there when l < p^m.
  (4) THE WIDTH. At a place with a head (f = 1, s = p^n, ubar = -1;
      clock.py's head criterion) the class (1, n) is the level-1 units, its
      least landing is clock.py's arrival A, and w = A - s - e = min(l, s), since
      (p - 1)s = e. So at every odd p the width is the departure capped
      at the bend, a function of one coefficient level of any Eisenstein
      polynomial of the field.

TRANSPLANTS. The formula is the record this script replaces, there
written on -p/pi^e, whose defect v(-p/pi^e - 1) is l. The
claim that deeper coefficient digits do not move L is (3) read, not
assumed: the digits above it are drawn at random.

PREDICTIONS, fixed before the run.
  C  CONTROL. (a) l from the coefficients equals v(u + 1) read from
     triangle.py's u at every polynomial used. (b) The class minimum over
     z mod pi^(c+2) equals the minimum over z mod pi^(c+3) at every
     class of p = 3, e <= 6 and p = 5, e = 4. (c) Every level-c unit
     reaches level exactly c p^j after j p-th powers, j <= m.
  L  THE READOUT. At (p, e) = (3, 2), (3, 4), (3, 6), (3, 12), (3, 18),
     (5, 4), (5, 8), (5, 20), (7, 6), (7, 12), every class (c, m), and
     polynomials with l = 0, every l from 1 to s + 2, and l infinite
     (x^e + p), each with random digits above its departure level, plus
     random polynomials: the least landing over the class is
     p s + min(l, p^m), and when l < p^m every unit of the class lands
     there.
  W  THE WIDTH. At the odd-p places of clock.py with a head and at
     designed ones with s = 3 (p = 3, e = 6) and s = 5 (p = 5, e = 20)
     at l = 1, 2, ..., s + 1: the width clock.py reads off the tick
     ladder (via the group exponent, not the landing) is min(l, s); and
     the translate F(x - p), whose root pi + p is another uniformizer of
     the same field, has its own departure l' with min(l', s) = min(l, s).
KILLS, as printed observables: any C line off (nothing below is read);
one (polynomial, class) whose least landing is off p s + min(l, p^m),
or a rigid class (l < p^m) with a unit landing elsewhere; one W place
whose ladder width is off min(l, s), or a translate whose capped
departure differs.

FINDINGS. Every prediction landed: 14/14 checks PASS, no kill fired.
C(a) reads its own draw, and the readout sections
redraw and use the designed l without recomputing it, so a wrong
design shows only as a landing miss; Cc accumulates in the readout
sections and is checked beside their verdicts, not before them.
  C  l off the coefficients is v(u + 1) and the designed level at 92
     polynomials, 0 off; the class minimum over two digits equals that
     over three at 40 (polynomial, class), 0 off; every level-c unit
     reaches level c p^j after j p-th powers, 0 off.
  L  154 (polynomial, class) readings over the ten (p, e), from s = 1
     to s = 9 (classes (9, 0), (3, 1), (1, 2) at (3, 18)): the least
     landing is p s + min(l, p^m) at every one, and at the 58 rigid
     readings (l < p^m) every unit of the class read (digits at levels
     c and c + 1) lands there; C(b)'s depth check covers (3, 2), (3, 4),
     (3, 6) and (5, 4), and L's other six (p, e) lie past it.
  W  At the five odd-p places of clock.py with a head (x^2 + 3, x^2 - 6,
     x^6 + 3, x^4 + 5, x^6 + 7) and ten designed ones, the ladder width
     is min(l, s): 1, 2, 3, 3 at l = 1..4 over (3, 6), and 1, 2, 3, 4,
     5, 5 at l = 1..6 over (5, 20). The translates move l itself, not
     only above e (x^2 + 3 reads l = infinity and its translate l' = 1,
     since pi' / pi = 1 + p/pi sits at level e - 1), and min(l', s) =
     min(l, s) at all 15.
  Tiers: (3) and (4) a theorem at every odd p, their proof (1)-(3); the
  departure's reading off the coefficients a property.

RUN RECORD. 14/14, 0.5 s wall, peak working set 13.8 MB under
a memory guard, green on the first run.
"""

import os
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
import random
from math import comb

from module_law import Local, check, section, CHECKS, vp
from triangle import Field
import clock

SEED = 1424
INF = 10 ** 9


def departure(p, e, b, d):
    """l = v(u + 1), off the coefficients."""
    best = INF if d + 1 == 0 else e * vp(d + 1, p)
    for i in range(1, e):
        if b[i]:
            best = min(best, e * vp(b[i], p) + i)
    return best


def designed(rng, p, e, l, top):
    """(b, d) whose departure is exactly l: the digits of
    (F - x^e - p)/p are zero below level l, nonzero at l, random above
    (to level top); l = INF gives x^e + p."""
    b, dp1 = [0] * e, 0
    if l < INF:
        for r in range(l, top + 1):
            i, k = r % e, r // e
            if r == l:
                c = rng.randrange(2, p) if r == 0 else rng.randrange(1, p)
            else:
                c = rng.randrange(p)
            if i == 0:
                dp1 += c * p ** k
            else:
                b[i] += c * p ** k
    return b, dp1 - 1


def local(p, e, b, d):
    return Local(p, [-p * d] + [p * b[i] for i in range(1, e)] + [1],
                 "eis", f"F({p},{e})")


def classes(p, e):
    s = e // (p - 1)
    out, m = [], 0
    while s % p ** m == 0:
        out.append((s // p ** m, m))
        m += 1
    return out


def landings(loc, c, m, K):
    """{z: v(z^(p^(m+1)) - 1)} over z = 1 + sum_(j<K) a_j pi^(c+j),
    a_0 != 0, with the levels of z^(p^j) - 1 for j <= m."""
    p, e = loc.p, loc.e
    s = e // (p - 1)
    N = (p * s + s + e + 2) // e + 2
    mod = p ** N
    pis = [loc.pi_pow(c + j, mod) for j in range(K)]
    out, levels_ok = [], True
    total = (p - 1) * p ** (K - 1)
    for idx in range(total):
        digs = [1 + idx % (p - 1)]
        rest = idx // (p - 1)
        for _ in range(K - 1):
            digs.append(rest % p)
            rest //= p
        z = [1] + [0] * (e - 1)
        for a, t in zip(digs, pis):
            z = [(x + a * y) % mod for x, y in zip(z, t)]
        y = z
        for j in range(m + 1):
            levels_ok &= loc.val([y[0] - 1] + y[1:], N) == c * p ** j
            y = loc.power(y, p, mod)
        out.append(loc.val([y[0] - 1] + y[1:], N))
    return out, levels_ok, e * N


# ------------------------------------------------------------ sections

GRID = [(3, 2), (3, 4), (3, 6), (3, 12), (3, 18), (5, 4), (5, 8),
        (5, 20), (7, 6), (7, 12)]


def polys(rng, p, e):
    s = e // (p - 1)
    top = 2 * e + s
    out = [(l, designed(rng, p, e, l, top)) for l in range(0, s + 3)]
    out.append((INF, designed(rng, p, e, INF, top)))
    for _ in range(2):
        b = [0] + [rng.randrange(p ** 4) for _ in range(1, e)]
        d = rng.choice([x for x in range(1, p ** 4) if x % p])
        out.append((departure(p, e, b, d), (b, d)))
    return out


def section_control(rng):
    section("C  CONTROL: the departure, the digit depth, the levels below")
    seen = bad = 0
    for (p, e) in GRID:
        for l, (b, d) in polys(rng, p, e):
            f = Field(p, e, b, d, 3 * e)
            v = f.val([(f.u[0] + 1) % f.mod] + f.u[1:])
            seen += 1
            bad += departure(p, e, b, d) != min(v, INF) and not (
                l == INF and v >= f.e * f.N)
            bad += l != departure(p, e, b, d)
    check("Ca l off the coefficients = v(u + 1) = the designed level",
          bad == 0, f"{seen} polynomials, {bad} off")
    seen = bad = 0
    for (p, e) in [(3, 2), (3, 4), (3, 6), (5, 4)]:
        for l, (b, d) in polys(rng, p, e):
            loc = local(p, e, b, d)
            for (c, m) in classes(p, e):
                two = min(landings(loc, c, m, 2)[0])
                three = min(landings(loc, c, m, 3)[0])
                seen += 1
                bad += two != three
    check("Cb the class minimum over two digits = over three",
          bad == 0, f"{seen} (polynomial, class), {bad} off")


def section_readout(rng):
    section("L  THE READOUT: least landing p s + min(l, p^m)")
    total = rigid = 0
    lv_bad = 0
    for (p, e) in GRID:
        s = e // (p - 1)
        seen = bad = 0
        for l, (b, d) in polys(rng, p, e):
            loc = local(p, e, b, d)
            for (c, m) in classes(p, e):
                land, lv_ok, _ = landings(loc, c, m, 2)
                lv_bad += not lv_ok
                want = p * s + min(l, p ** m)
                seen += 1
                ok = min(land) == want
                if l < p ** m:
                    rigid += 1
                    ok &= set(land) == {want}
                bad += not ok
                if not ok:
                    print(f"    OFF p={p} e={e} l={l} class ({c},{m}): "
                          f"min {min(land)} want {want}, "
                          f"spectrum {sorted(set(land))[:6]}")
        total += seen
        check(f"L (p, e) = ({p}, {e}), s = {s}, classes "
              f"{classes(p, e)}", bad == 0,
              f"{seen} (polynomial, class), {bad} off")
    check("Cc every level-c unit reaches level c p^j after j powers",
          lv_bad == 0, f"{lv_bad} off")
    print(f"  {total} (polynomial, class) readings, {rigid} of them rigid")


def ladder_width(loc):
    n = clock.bend_power(loc)
    s = loc.e // (loc.p - 1)
    lad = clock.ladder(loc, loc.e + 2 * s + 2)
    return lad[n + 1] - s - loc.e


def translate(p, e, b, d):
    """F(x - p) as (b', d'): expand and read the coefficients."""
    F = [-p * d] + [p * b[i] for i in range(1, e)] + [1]
    G = [0] * (e + 1)
    for j, a in enumerate(F):
        for i in range(j + 1):
            G[i] += a * comb(j, i) * (-p) ** (j - i)
    assert G[e] == 1 and all(g % p == 0 for g in G[:e])
    return [0] + [G[i] // p for i in range(1, e)], -G[0] // p


def section_width(rng):
    section("W  THE WIDTH: clock.py's ladder width = min(l, s) at odd p")
    rows = []
    for loc in clock.PLACES:
        if loc.p > 2 and loc.kind == "eis" and clock.criterion(loc):
            e, p = loc.e, loc.p
            b = [0] + [loc.g[i] // p for i in range(1, e)]
            rows.append((loc.name, p, e, b, -loc.g[0] // p))
    for (p, e) in [(3, 6), (5, 20)]:
        s = e // (p - 1)
        for l in range(1, s + 2):
            b, d = designed(rng, p, e, l, e + s)
            rows.append((f"designed l={l}", p, e, b, d))
    bad = 0
    for name, p, e, b, d in rows:
        s = e // (p - 1)
        l = departure(p, e, b, d)
        w = ladder_width(local(p, e, b, d))
        b2, d2 = translate(p, e, b, d)
        l2 = departure(p, e, b2, d2)
        ok = w == min(l, s) and min(l2, s) == min(l, s)
        bad += not ok
        print(f"  {name:18s} p={p} e={e:2d} s={s} l={'inf' if l >= INF else l}"
              f" l'={'inf' if l2 >= INF else l2} ladder w={w}"
              f"{'' if ok else '  OFF'}")
    check("W the ladder's width is min(l, s), and the translate agrees",
          bad == 0, f"{len(rows)} places, {bad} off")


def main():
    rng = random.Random(SEED)
    section_control(rng)
    if not all(CHECKS):
        print("\ncontrol failed: nothing below is read")
        raise SystemExit(1)
    section_readout(rng)
    section_width(rng)
    print()
    print(f"{sum(CHECKS)}/{len(CHECKS)} checks PASS")
    if not all(CHECKS):
        raise SystemExit(1)


if __name__ == "__main__":
    main()