fieldtop.py

Checks: THERMAL: The F₂ place need not top theorem; observation.

Cited on: THERMAL.

To run it: save fieldtop.py, with growth.py, memory.py, spectrum.py beside it; needs mpmath, numpy, scipy.

"""fieldtop.py -- which place tops the thermal place spectrum over a
number field: quadratic fields, the abelian fields K(m, d) inside
Q(zeta_m), and the cyclic fields of degree 4 and 8 with one place of
residue field F_2.

QUESTION. Over Z the 2-adic column has the largest critical point of
the place spectrum, and over Z[sqrt(-5)] the place with residue field
F_2 is again on top at the four columns read (spectrum.py). Does a
place with residue field F_2 top the spectrum over every number field
that has one, and where it does not, what decides the race?

THE SETTING (spectrum.py's (5)). At a place P of norm N over p, the
limit headroom C_P of the column P^a holds every place R != P up to
the largest j with lambda(R^j) | (N - 1) p^oo, lambda(R^j) the unit
exponent of O/R^j; a place over p itself enters without bound iff
N(R) - 1 | N - 1, else not at all. The column's ideal point beta_P is
the root of zeta_K - sigma_b(C_P) = 1.

THE ARGUMENT.
  (1) THE COMPARISON. zeta_K is shared by every column, and each
      Psi_P = zeta_K - sigma_b(C_P) is a sum of N^-b over a fixed set
      of ideals, so strictly decreasing. So beta_P < beta_Q iff
      Psi_P(beta_Q) < 1 = Psi_Q(beta_Q), iff sigma(C_P) > sigma(C_Q)
      at b = beta_Q: a column sits below the F_2 place's iff its
      headroom sum is the larger at the F_2 point.
  (2) THE F_2 COLUMN. There N - 1 = 1: an odd place R enters iff
      N(R) - 1 is a power of 2, so N(R) is 3, 5, 9, 17, 257, 65537 or
      a Fermat prime past them, at depth 1 (lambda(R^2) carries r);
      every other place of norm 2 enters without bound; no other place
      over 2 enters.
  (3) THE WILD CHAIN. At a place Q over 2 with f = 1 and e = 2^s,
      v(2) = e. A 1-unit 1 + y with v(y) = t squares to 1 + 2y + y^2,
      of valuation min(2t, e + t) where the two differ and at least 2e
      at t = e. So after k <= s squarings every 1-unit has valuation at
      least 2^k and 1 + pi exactly 2^k, and after s + 1 every 1-unit
      has at least 2e: lambda(Q^j), the exponent of U1/U^j, is
      2^ceil(log2 j) for j <= 2e (wild). An odd column of norm N holds
      Q to depth 2^v, v = v_2(N - 1), while 2^v < 2e, and to at least
      2e past that.
  (4) THE RACE. Where 2 splits, the chain at each place of norm 2 is
      (Z/2^j)^*'s, 1, 2, 2, 4, ..., so an odd column holds every one of
      them to depth v + 2 >= 3, one factor 1 + x + x^2 + x^3 at least
      (x = 2^-b) against each conjugate's 1/(1 - x) in the F_2 column.
      Where 2 is totally ramified there is one F_2 place, and an odd
      column with v = 1 holds it only to depth 2. Then by (1) the race
      is the F_2 column's places of Fermat norm, at depth 1, against
      the odd column's 1 + x + x^2 and its own conjugates.
  (5) THE MENUS STOP AT 10^6. A place missed past it can only raise
      its column's headroom sum, so lower its point. The F_2 column's
      missed places have Fermat-prime norm past 65537, at least
      2^32 + 1, at most [K:Q] of them per norm: a mass below 2^-27 at
      b >= 1 and degree <= 16, printed (MISSED) beside the gap at each
      field section O races, the bound alone elsewhere. A
      winning odd column of norm N over p misses only places of norm
      n = d p^i + 1 > 10^6 with d | N - 1, bounded by section T.

DESIGN.
  Q  The quadratic field of discriminant D. The ring Z[w], w^2 = t w -
     n (w = (1 + sqrt D)/2 at D = 1 mod 4, sqrt(D/4) else); ideals as
     Hermite lattices closed under w, as in spectrum.py. The places
     over r from the roots of x^2 - t x + n mod r. Local chains:
     (N(R) - 1) r^(j-1) at unramified odd r, (r - 1) r^ceil((j-1)/2)
     at ramified odd r, but 2 3^max(1, ceil((j-2)/2)) at j >= 2 where
     Q_3(sqrt D) = Q_3(zeta_3) (U1 = mu_3 x U2; -D/3 = 1 mod 3), and
     brute force over O/R^j at r = 2; the odd formulas against brute
     force at every power of norm <= 3000 over r < 50, and chi_D
     against the root counts below 1000, per field. The menu runs r
     below 10^6. D = -7, 17 (2 split), -8 (2 ramified), and 161 = 7 *
     23, with 2, 5, 17 split, 3 inert and two ramified primes = 3 mod
     4. Every place of norm <= 50 is read. CONTROL: D = -20 reprints
     spectrum.py's four points P2 1.70395, P3 1.54922, Q7 1.45633,
     (13) 1.31983 to 1e-5, before any field.
  C  The corner of degree 2: every fundamental D, |D| < 400, with 2
     and 3 ramified and 5 split.
  A  K(m, d), the degree-d subfield of Q(zeta_m), m prime, d | m - 1,
     the fixed field of the d-th powers H: every m < 5000 and d in
     {4, 6, 8, 10, 12, 16} with 2 and 5 in H, so 2 splits completely.
     Over r != m, f = r's order in G/H, d/f places of norm r^f; at m,
     one tame place, chain (m - 1) m^ceil((j-1)/d); zeta_K = zeta times
     the d - 1 L(chi^k), chi of order d trivial on H, by Hurwitz zeta.
     CONTROLS: K(7, 2) and K(17, 2) reprint Q's lines for D = -7, 17;
     zeta_K(3) against the Euler product of the splitting law over
     r < 10^5, every field, to 1e-10 (so a wrong f is seen at the
     small primes only).
  O  One F_2 place. chi = phi^a psi of order d mod 4dm, phi(5) =
     zeta_d and phi(-1) = 1 on (Z/4d)^*, psi of order d on (Z/m)^*,
     a = -ind(5) mod d odd, so chi(5) = 1: 2 and m totally ramified
     (e = d) and 5 split completely. With phi(-1) = 1 the field is one
     per (m, d): another psi is psi^s, s odd, and chi(5) = 1 forces
     chi' = chi^s. The character phi chi_-4 in place of phi cuts out a
     second field per (m, d), with the same ramification at 2 and m and
     5 split completely; it is not read here.
     d = 8: m = 17, 73, 97, the m below 100 with m = 1 mod 8 and
     m = +-2 mod 5; d = 4: m = 13, 17, 37, 53, 73, 97. Tame chain at
     m: (m - 1) m^ceil((j-1)/d); the wild chain (3) at 2. Two arms:
     beta+ holds the place over 2 without bound in every odd column,
     so beta+ <= beta (a loss on it uses no wild chain); EXACT holds
     it to the depth (3) gives. CONTROLS: (C1) the quadratic
     characters of conductor 7, 17 reprint Q's lines; (C2) the
     8th-power character mod 2969 reprints A's least gap; (C3) at each
     field, e = d at 2 and at m, 5 split completely (at d = 8 the
     primes below 100 split completely are 5, 47 | 5, 23, 29, 53 |
     5, 47), zeta_K(3) = Euler to 1e-10; (C4) brute element orders in
     Z_2[zeta_2e]/pi^j, j <= 2e, equal wild, at e = 4 and 8.
  T  The tail past 10^6 at each column that beats the F_2 place: at
     the F_2 point, Psi of the winner with every place of norm
     n = d p^i + 1 > 10^6, d | N - 1, added at full count [K:Q] and
     unbounded depth, its log mass printed.

PREDICTIONS, fixed before each run.
  Q  (q1) the F_2 place tops at all four fields: every odd column holds
     both places over 2 to depth v_2(N - 1) + 2 >= 3 where 2 splits.
     (q2) The runner-up has N = 3 mod 4: the place over 7 at -7, over
     7 or 23 at 161, a place over 11 at -8, over 19 at 17. (q3) The gap
     exceeds 0.03 everywhere, least at 161. (q4) At -8 the F_2 column
     holds no place over 2.
  C  (s1) the F_2 place tops at all; (s2) the runner-up the column over
     3; (s3) the least gap below 0.06.
  A  (a1) the F_2 place tops at every field; (a2) beta_F2 rises with d
     at fixed m; (a3) the least gap where 17 splits completely.
  O  d = 8: (r1) the F_2 place loses at all three; (r2) the top other
     of norm 47 at 17 and 97, 23 at 73; (r3) every column of norm 5
     below beta_F2; (r4) gap+ < -0.02. d = 4: (t1) the F_2 place loses
     where f(3) = 2 (two places of norm 9 in the F_2 column), tops
     where f(3) = 4; (t2) the runner-up N = 3 mod 4.
  T  the log mass below 1e-8 at every winner, and the losses stand.

FINDINGS.
  Q  CONTROL: N=2 1.70395, N=3 1.54922, N=7 1.45633, N=169 1.31983.
     chi_D = the root counts at the 168 primes below 1000, and the odd
     chain formulas = brute at 37, 33, 46, 36 prime powers. The chain
     over a split 2 is (Z/2^j)^*'s, 1, 2, 2, 4, 8, ..., 2048; over the
     ramified 2 at -8 it is 1, 2, 4, 4, 4, 8, 8, 16, 16, ..., 64.
       D = -7: F2 1.66168 | N=7 1.43224 | gap 0.22944
       D = 17: F2 1.60213 | N=47 1.38428 | gap 0.21785
       D = -8: F2 1.63177 | N=3 1.49122 | gap 0.14055
       D = 161: F2 1.69138 | N=23 1.56317 | gap 0.12821
     (q1) holds. (q2) holds as to class, the runner-up's N = 3 mod 4
     everywhere (7, 47, 3, 23), and misses two of its named places: at
     -8 the place over 3 (its conjugate unbounded) beats 11, at 17 the
     place over 47 beats 19. (q3) holds: least gap 0.12821 at 161.
     (q4) holds: the F_2 column at -8 holds 3^1 twice, 17^1 twice,
     257^1 twice, 65537^1 twice, nothing over 2. At 161 the runner-up's
     headroom is the two places over 2 at depth 3 and nothing else,
     the bare part every odd column holds where 2 splits.
  C  D = 24, 156, 204, 264, -24, -84, -264, -276; gaps 0.03995,
     0.09298, 0.07688, 0.03025, 0.08221, 0.05549, 0.07027, 0.07926,
     the runner-up the column over 3 at all eight. (s1)-(s3) hold. At
     D = 264 the F_2 column holds 3, 5 twice, 17 twice, 65537 twice;
     the column over 3 holds the place over 2 at depth 2 and the
     places over 19, 487, 1459, 39367, two each, and nothing else
     below 10^6. The brute check refused the tame formula at D = 24,
     204, -84, -264 until the case Q_3(sqrt D) = Q_3(zeta_3) was
     added: there U1 = mu_3 x U2, brute 6 and 18 at depths 4 and 6
     against the tame formula's 18 and 54.
  A  CONTROL: K(7, 2) F2 1.66168, N=7 1.43224; K(17, 2) F2 1.60213,
     N=47 1.38428. 24 fields, none at d = 12 or 16 below 5000; zeta_K
     = Euler to 4.3e-12. The F_2 place tops every one: least gap
     0.07596 at d = 8, m = 2969 (N=47), then 0.08652 at d = 4, m =
     4201 (N=47), 0.10393 at d = 6, m = 4519 (N=31); the gap is 0.33
     to 0.61 wherever the runner-up is a place of norm 5. (a1) holds.
     (a2) holds at all seven m read at two d: m = 2441, 2.02811 ->
     2.52399 (d = 4 -> 8); 2969, 2.02190 -> 2.49345; 1609, 2.08155 ->
     2.56428; 3121, 2.02185 -> 2.67349 (d = 10); 3449, 4201, 4409 the
     same. (a3) misses: at 2969 the prime 17 has f = 8. Where 2 splits
     completely the F_2 column's conjugates are repaid by g factors
     1 + x + x^2 + x^3, and the point climbs with g.
  O  CONTROLS: C1 F2 1.66168, N=7 1.43224 and F2 1.60213, N=47
     1.38428; C2 F2 2.49345, N=47 2.41748, gap 0.07596; C4 exponents
     1, 2, 4, 4, 8, 8, 8, 8 at e = 4 and 1, 2, 4, 4, 8 (x4), 16 (x8)
     at e = 8; C3 e, f = (d, 1) at 2 and at m, the split primes as
     named, Euler to 4.2e-12.
       d=8 m=17: F2 1.65129 | N=47 beta+ 1.73433 | gap+ -0.08304
       d=8 m=73: F2 1.72004 | N=23 beta+ 1.76814 | gap+ -0.04810
       d=8 m=97: F2 1.64717 | N=47 beta+ 1.72708 | gap+ -0.07991
     EXACT: N=47 1.74920, N=23 1.78138, N=47 1.74229; gaps -0.09791,
     -0.06134, -0.09511. (r1)-(r4) hold: the F_2 place loses at all
     three on the beta+ arm, with no wild chain used. The F_2 column
     holds the eight places of norm 5 at depth 1 (and the place over
     17 at m = 17); the winner holds the place over 2 at depth 2
     (v_2(46) = v_2(22) = 1) and its seven conjugates without bound,
     nothing else below 10^6. The columns of norm 5 sit at 1.40026,
     1.50080, 1.38449, held down by their seven conjugates.
     At d = 4 the beta+ arm decides nothing: gap+ 0.06358, 0.01421,
     0.05213, 0.09787, 0.06329, 0.05175 (m = 13, 17, 37, 53, 73, 97).
     EXACT: gaps 0.04043, -0.00593, 0.03210, 0.08425, 0.04440, 0.02718,
     the top other N=23, 47, 47, 47, 23, 47. At K4(17), f(3) = 2, the
     place of norm 47 tops the F_2 place, 1.54929 against 1.54337: the
     F_2 column holds 5 four times, 9 twice and 17; the column of norm
     47 holds the place over 2 at depth 2 (wild(4, 3) = 4 does not
     divide 46) and its three conjugates. (t1) holds at the four m
     with f(3) = 4 and at the one with f(3) = 2, and does not speak at
     m = 53, f(3) = 1 (gap 0.08425); (t2) holds. This loss rests on
     the wild chain (3), proved above and brute at Z_2[zeta_8].
  T  log mass past 10^6 4.1e-10, 9.3e-11, 4.3e-10 at d = 8, m = 17,
     73, 97 and 1.0e-9 at K4(17); Psi of the winner at the F_2 point
     with that mass added 1.29857, 1.18654, 1.28680, 1.01832, each
     above 1, so every loss stands.

RUN RECORD. `python fieldtop.py`: 46 of 46 checks, 23.6 s, peak commit
72.4 MB; --quad, --corner, --deg and --one run one section each. Every
line of Q, C, A and O reprints its first engine's to 1e-5 with L read
by double-precision Hurwitz zeta in place of a 20-digit sum. With
(5)'s MISSED lines: 46 of 46, 23.7 s, 72.6 MB; the mass below 3.7e-09
at degree 8 and 1.9e-09 at 4, the least gap beside it 5.9e-03.
"""

import math
import os
import sys
from math import gcd

os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))

import numpy as np  # noqa: E402
from scipy.special import zeta as hz  # noqa: E402

from growth import factor, is_prime_mr  # noqa: E402
from spectrum import RPRIMES, bisect, egcd, lcm, zeta  # noqa: E402

LIM = 10 ** 6                   # the menus read every r below this
CHECKS = []


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


def guard():
    """A failed control stops the run before any verdict."""
    if not all(CHECKS):
        print("a control failed: no verdict")
        sys.exit(1)


def kronecker(D, n):
    """(D / n) for n >= 1."""
    if gcd(D, n) != 1:
        return 0
    s = 1
    while n % 2 == 0:
        n //= 2
        s *= 1 if D % 8 in (1, 7) else -1
    a, m = D % n, n
    while a:
        while a % 2 == 0:
            a //= 2
            if m % 8 in (3, 5):
                s = -s
        a, m = m, a
        if a % 4 == 3 and m % 4 == 3:
            s = -s
        a %= m
    return s if m == 1 else 0


# --------------------------------------------- Q, C: quadratic fields

class Field:
    def __init__(self, D):
        self.D = D
        if D % 4 == 1:
            self.t, self.n = 1, (1 - D) // 4
        else:
            self.t, self.n = 0, -(D // 4)
        m = abs(D)
        self.chi = [kronecker(D, a) for a in range(m + 1)]
        units = [a for a in range(1, m) if self.chi[a]]
        self.args = np.array(units) / m
        self.chv = np.array([self.chi[a] for a in units], dtype=float)
        self.kinds = {}

    def emul(self, a, b):
        bd = a[1] * b[1]
        return (a[0] * b[0] - self.n * bd,
                a[0] * b[1] + a[1] * b[0] + self.t * bd)

    def roots(self, r):
        return [s for s in range(r) if (s * s - self.t * s + self.n) % r == 0]

    def kind(self, r):
        if r not in self.kinds:
            if r == 2 or self.D % r == 0:
                k = len(self.roots(r))
                k = {2: "split", 1: "ram", 0: "inert"}[k]
            else:
                k = "split" if pow(self.D % r, (r - 1) // 2, r) == 1 \
                    else "inert"
            self.kinds[r] = k
        return self.kinds[r]

    def primes_over(self, r):
        k = self.kind(r)
        if k == "inert":
            return [Ideal(self, [(r, 0)])]
        return [Ideal(self, [(r, 0), (-s, 1)]) for s in self.roots(r)]

    def L(self, b):
        return abs(self.D) ** -b * float((self.chv * hz(b, self.args)).sum())

    def zK(self, b):
        return zeta(b) * self.L(b)


class Ideal:
    """spectrum.py's Hermite lattice (A, 0), (B, C), over Field F."""

    def __init__(self, F, gens):
        self.F = F
        vecs = []
        for g in gens:
            vecs += [g, F.emul(g, (0, 1))]
        C, v = 0, (0, 0)
        for u in vecs:
            g, s, t = egcd(C, u[1])
            if g != C:
                v = (s * v[0] + t * u[0], s * v[1] + t * u[1])
                C = g
        A = 0
        for u in vecs:
            A = gcd(A, u[0] - (u[1] // C) * v[0])
        self.A, self.B, self.C = A, v[0] % A, C

    def norm(self):
        return self.A * self.C

    def red(self, z):
        t = z[1] // self.C
        return ((z[0] - t * self.B) % self.A, z[1] - t * self.C)

    def gens(self):
        return [(self.A, 0), (self.B, self.C)]

    def __mul__(self, other):
        return Ideal(self.F, [self.F.emul(g, h) for g in self.gens()
                              for h in other.gens()])


def epow(F, z, n, I):
    r, z = I.red((1, 0)), I.red(z)
    while n:
        if n & 1:
            r = I.red(F.emul(r, z))
        z = I.red(F.emul(z, z))
        n >>= 1
    return r


def brute_lam(R, j):
    """The unit exponent of O/R^j by element orders."""
    F, I = R.F, R
    for _ in range(j - 1):
        I = I * R
    N, NR = I.norm(), R.norm()
    G = N - N // NR
    pf = factor(G)
    one = I.red((1, 0))
    L = 1
    for x in range(I.A):
        for y in range(I.C):
            if R.red((x, y)) == (0, 0):
                continue
            o = G
            for p in pf:
                while o % p == 0 and epow(F, (x, y), o // p, I) == one:
                    o //= p
            L = lcm(L, o)
    return L


def lam_formula(F, r, j):
    k = F.kind(r)
    if k == "split":
        return (r - 1) * r ** (j - 1)
    if k == "inert":
        return (r * r - 1) * r ** (j - 1)
    if r == 3 and (-F.D // 3) % 3 == 1:
        # Q_3(sqrt D) = Q_3(zeta_3): U1 = mu_3 x U2, U2's v_k = 2 + 2k
        return 2 if j == 1 else 2 * 3 ** max(1, (j - 1) // 2)
    return (r - 1) * r ** ((j - 1 + 1) // 2)


def chain2(F, jmax):
    """Brute chains of the places over 2, up to norm 2^13."""
    out = []
    for R in F.primes_over(2):
        ch, j = [], 1
        while j <= jmax and R.norm() ** j <= 2 ** 13:
            ch.append(brute_lam(R, j))
            j += 1
        out.append((R.norm(), ch))
    return out


def fits(lam, N, p):
    """lambda | (N - 1) p^oo."""
    while lam % p == 0:
        lam //= p
    return (N - 1) % lam == 0


def menu(F, col, ch2):
    """Headroom entrants (r, N(R), depth or None) of the column at a
    place of norm N over p, the column's own place left out."""
    p, N = col
    out = []
    for r in RPRIMES:
        k = F.kind(r)
        NR = r * r if k == "inert" else r
        cnt = 2 if k == "split" else 1
        if r == p:
            cnt -= 1
            if cnt and fits(NR - 1, N, p):
                out.append((r, NR, None))
            continue
        if r == 2:
            for NR2, ch in ch2:
                j = 0
                while j < len(ch) and fits(ch[j], N, p):
                    j += 1
                assert j < len(ch), ("chain over 2 too short", F.D, col)
                if j:
                    out.append((2, NR2, j))
            continue
        lam1 = lam_formula(F, r, 1)
        if not fits(lam1, N, p):
            continue
        j = 1
        while fits(lam_formula(F, r, j + 1), N, p):
            j += 1
        for _ in range(cnt):
            out.append((r, NR, j))
    return out


def sigma(men, b):
    s = 1.0
    for r, NR, cap in men:
        x = NR ** -b
        s *= 1 / (1 - x) if cap is None else sum(x ** k
                                                 for k in range(cap + 1))
    return s


def point(F, men):
    return bisect(lambda b: F.zK(b) - sigma(men, b) - 1, 1.01, 3, 1e-9)


def places_le(F, bound):
    """(p, N, count, kind) for every rational p with places of norm
    <= bound."""
    out = []
    for r in range(2, bound + 1):
        if not is_prime_mr(r):
            continue
        k = F.kind(r)
        if k == "inert":
            if r * r <= bound:
                out.append((r, r * r, 1, k))
        else:
            out.append((r, r, 2 if k == "split" else 1, k))
    return out


def ingredients(F):
    bad = n = 0
    for r in range(2, 1000):
        if not is_prime_mr(r):
            continue
        c = kronecker(F.D, r)
        bad += {2: 1, 1: 0, 0: -1}[len(F.roots(r))] != c
        bad += {"split": 1, "ram": 0, "inert": -1}[F.kind(r)] != c
        n += 1
    check(f"D={F.D} chi_D(r) = the splitting read off the roots, r < 1000",
          bad == 0, f"{n} primes")
    bad = n = 0
    for r in range(3, 50):
        if not is_prime_mr(r):
            continue
        for R in F.primes_over(r):
            j = 1
            while R.norm() ** j <= 3000:
                bad += brute_lam(R, j) != lam_formula(F, r, j)
                n += 1
                j += 1
    check(f"D={F.D} odd local chains = brute", bad == 0,
          f"{n} prime powers")


def summary(men, top=8):
    fin = sorted(men, key=lambda e: e[1])[:top]
    return ", ".join(f"{NR}^{'oo' if c is None else c}" for _, NR, c in fin)


def run_field(D):
    F = Field(D)
    print(f"\nD = {D}  (t, n) = ({F.t}, {F.n}); 2 is {F.kind(2)}")
    ingredients(F)
    ch2 = chain2(F, 16)
    for NR, ch in ch2:
        print(f"  chain over 2, norm {NR}: {ch}")
    pts = []
    for p, N, cnt, k in places_le(F, 50):
        men = menu(F, (p, N), ch2)
        b = point(F, men)
        pts.append((p, N, cnt, k, b, men))
    pts.sort(key=lambda e: -e[4])
    for p, N, cnt, k, b, men in pts:
        print(f"  N={N:3d} over {p:2d} ({k:5s} x{cnt}) point {b:.5f}  "
              f"headroom {summary(men)}")
    return F, pts


def control_q():
    print("CONTROL: D = -20 through the engine")
    F = Field(-20)
    ch2 = chain2(F, 16)
    want = {(2, 2): 1.70395, (3, 3): 1.54922, (7, 7): 1.45633,
            (13, 169): 1.31983}
    got = {c: point(F, menu(F, c, ch2)) for c in want}
    print("  " + ", ".join(f"N={c[1]}:{got[c]:.5f}" for c in want))
    check("CONTROL D=-20 reprints spectrum.py's four points",
          all(abs(got[c] - want[c]) < 1e-5 for c in want))


def section_q(Ds, title):
    print(f"\n=== {title}: D = {list(Ds)}")
    verdict = []
    for D in Ds:
        before = len(CHECKS)
        F, pts = run_field(D)
        if not all(CHECKS[before:]):
            print(f"  D={D}: an ingredient check failed, no verdict")
            continue
        f2 = [e for e in pts if e[1] == 2]
        oth = [e for e in pts if e[1] != 2]
        bf, bo = max(e[4] for e in f2), oth[0]
        g = bf - bo[4]
        print(f"  TOP LINE D={D}: F2 {bf:.5f} | top other N={bo[1]} "
              f"{bo[4]:.5f} | gap {g:.5f}")
        verdict.append((D, g))
    print()
    for D, g in verdict:
        v = "loses" if g < -1e-5 else ("ties" if g <= 1e-5 else "tops")
        print(f"  the F2 place at D={D}: {v} (gap {g:.5f})")


def corner_ds():
    """Fundamental D, |D| < 400, with 2 and 3 ramified and 5 split."""
    return [D for s in (1, -1) for k in range(1, 100) if (D := 4 * s * k)
            and (s * k) % 4 in (2, 3) and k % 3 == 0
            and all(k % (q * q) for q in (2, 3, 5, 7))
            and kronecker(D, 5) == 1]


# ------------------------------------------------ A: K(m, d) abelian

class Abelian:
    """The degree-d subfield of Q(zeta_m), m prime, d | m - 1: the
    fixed field of H, the d-th powers in (Z/m)^*."""

    def __init__(self, m, d):
        self.m, self.d, self.e = m, d, (m - 1) // d
        g = next(a for a in range(2, m) if all(
            pow(a, (m - 1) // q, m) != 1 for q in factor(m - 1)))
        ind = [0] * m
        x = 1
        for k in range(m - 1):
            ind[x] = k
            x = x * g % m
        a = np.arange(1, m)
        self.args = a / m
        idx = np.array([ind[i] for i in range(1, m)])
        self.chis = [np.exp(2j * np.pi * k * idx / d) for k in range(1, d)]
        self.fs = {}

    def f(self, r):
        if r not in self.fs:
            self.fs[r] = next(f for f in range(1, self.d + 1)
                              if self.d % f == 0
                              and pow(r, f * self.e, self.m) == 1)
        return self.fs[r]

    def zK(self, b):
        z = hz(b, self.args) * self.m ** -b
        out = hz(b, 1.0)
        for c in self.chis:
            out *= complex((c * z).sum())
        return out.real

    def lam(self, r, j):
        if r == 2:
            return 1 if j == 1 else 2 ** max(1, j - 2)
        if r == self.m:
            return (r - 1) * r ** (-(-(j - 1) // self.d))
        return (r ** self.f(r) - 1) * r ** (j - 1)

    def places(self, r):
        """(norm, count) of the places over r."""
        if r == self.m:
            return r, 1
        f = self.f(r)
        return r ** f, self.d // f


def menu_ab(K, col):
    p, N = col
    out = []
    for r in RPRIMES:
        NR, cnt = K.places(r)
        if r == p:
            if cnt > 1 and fits(NR - 1, N, p):
                out += [(r, NR, None)] * (cnt - 1)
            continue
        if not fits(K.lam(r, 1), N, p):
            continue
        j = 1
        while fits(K.lam(r, j + 1), N, p):
            j += 1
            assert j < 64
        out += [(r, NR, j)] * cnt
    return out


def point_ab(K, men):
    return bisect(lambda b: K.zK(b) - sigma(men, b) - 1, 1.01, 8, 1e-9)


def run_ab(m, d, show=False):
    K = Abelian(m, d)
    assert K.f(2) == 1
    pts = []
    for r in range(2, 51):
        if not is_prime_mr(r):
            continue
        NR, cnt = K.places(r)
        if NR <= 50:
            men = menu_ab(K, (r, NR))
            pts.append((r, NR, cnt, point_ab(K, men), men))
    pts.sort(key=lambda e: -e[3])
    if show:
        for r, NR, cnt, b, men in pts:
            print(f"    N={NR:3d} over {r:2d} (x{cnt}) point {b:.5f}  "
                  f"headroom {summary(men)}")
    f2 = max(e[3] for e in pts if e[1] == 2)
    oth = [e for e in pts if e[1] != 2]
    return K, f2, oth[0] if oth else None


def growth_primes(n):
    return [p for p in RPRIMES if p < n]


def section_a():
    print("\n=== A: K(m, d), 2 split completely")
    print("CONTROL: K(7, 2) = Q(sqrt -7), K(17, 2) = Q(sqrt 17)")
    for m, want in ((7, (1.66168, 7, 1.43224)), (17, (1.60213, 47, 1.38428))):
        K, f2, o = run_ab(m, 2, show=True)
        check(f"CONTROL K({m}, 2) reprints Q's F2 and top-other line",
              abs(f2 - want[0]) < 1e-5 and o[1] == want[1]
              and abs(o[3] - want[2]) < 1e-5,
              f"F2 {f2:.5f}, N={o[1]} {o[3]:.5f}")
    guard()
    rows, worst = [], 0.0
    for d in (4, 6, 8, 10, 12, 16):
        for m in growth_primes(5000):
            if (m - 1) % d or m == 5:
                continue
            e = (m - 1) // d
            if pow(2, e, m) != 1 or pow(5, e, m) != 1:
                continue
            K, f2, o = run_ab(m, d)
            fs = {r: K.f(r) for r in (3, 17) if r != m}
            g = f2 - o[3]
            rows.append((g, d, m))
            eul = 1 / (1 - m ** -3.0)
            for r in growth_primes(10 ** 5):
                if r != m:
                    NR, cnt = K.places(r)
                    eul *= (1 - NR ** -3.0) ** -cnt
            worst = max(worst, abs(K.zK(3.0) / eul - 1))
            print(f"  TOP LINE d={d:2d} m={m:4d} f3={fs.get(3)} "
                  f"f17={fs.get(17)}: F2 {f2:.5f} | top other N={o[1]} "
                  f"{o[3]:.5f} | gap {g:.5f}")
    check("A zeta_K at b = 3 = the Euler product of the splitting law to "
          "1e-10, every field (an error in f at a prime below a few "
          "thousand moves it past that)", worst < 1e-10,
          f"worst {worst:.1e}")
    rows.sort()
    print(f"\n  {len(rows)} fields; least gap {rows[0][0]:.5f} at d = "
          f"{rows[0][1]}, m = {rows[0][2]}")
    lost = [r for r in rows if r[0] < -1e-5]
    tie = [r for r in rows if abs(r[0]) <= 1e-5]
    print(f"  the F2 place loses at {len(lost)} fields, ties at {len(tie)}, "
          f"tops at {len(rows) - len(lost) - len(tie)}")


# --------------------------------- O: one F_2 place, a cyclic field

class Cyclic:
    """The cyclic field of degree d cut out by chi, chi(a) =
    zeta_d^ex[a] on (Z/n)^*, ex[a] = -1 off the units. Every chi^k must
    be ramified at every prime of n, so each L read mod n is primitive."""

    def __init__(self, n, d, ex):
        self.n, self.d, self.ex = n, d, ex
        units = np.array([a for a in range(1, n) if ex[a] >= 0])
        self.args = units / n
        e = np.array([ex[a] for a in units])
        self.chis = [np.exp(2j * np.pi * k * e / d) for k in range(1, d)]
        self.ram = {}
        for r in factor(n):
            rv = r
            while n % (rv * r) == 0:
                rv *= r
            n1 = n // rv
            g0 = d
            for a in range(1, n, n1):
                if ex[a] >= 0:
                    g0 = gcd(g0, ex[a])
            fr = next(a for a in range(n) if a % n1 == r % n1
                      and a % rv == 1)
            f = next(t for t in range(1, d + 1) if t * ex[fr] % g0 == 0)
            self.ram[r] = (d // g0, f)
            assert d // g0 == d, ("chi^k unramified at", r)
        self.fs = {}

    def e(self, r):
        return self.ram[r][0] if r in self.ram else 1

    def f(self, r):
        if r in self.ram:
            return self.ram[r][1]
        if r not in self.fs:
            self.fs[r] = self.d // gcd(self.d, self.ex[r % self.n])
        return self.fs[r]

    def places(self, r):
        f = self.f(r)
        return r ** f, self.d // (self.e(r) * f)

    def zK(self, b):
        z = hz(b, self.args) * self.n ** -b
        out = hz(b, 1.0)
        for c in self.chis:
            out *= complex((c * z).sum())
        return out.real

    def lam(self, r, j):
        e, f = self.e(r), self.f(r)
        if r == 2:
            assert e == 1 and f == 1, "2 unramified only with f = 1"
            return 1 if j == 1 else 2 ** max(1, j - 2)
        if e == 1:
            return (r ** f - 1) * r ** (j - 1)
        assert e < r - 1
        return (r ** f - 1) * r ** (-(-(j - 1) // e))


def wild(e, j):
    """The exponent of U1/U^j at a place over 2 with f = 1 and e a power
    of 2, j <= 2e: the argument's (3), controlled by wild_control."""
    assert e & (e - 1) == 0 and 1 <= j <= 2 * e
    return 2 ** (j - 1).bit_length()


def menu_cy(K, col, arm):
    """arm 'plus': an odd column holds a ramified place over 2 without
    bound; 'exact': to depth 2^v, v = v_2(N - 1), exact while 2^v < 2e,
    else 2e (a lower bracket)."""
    p, N = col
    out = []
    for r in RPRIMES:
        NR, cnt = K.places(r)
        if r == p:
            if cnt > 1 and fits(NR - 1, N, p):
                out += [(r, NR, None)] * (cnt - 1)
            continue
        if r == 2 and K.e(2) > 1:
            e = K.e(2)
            assert K.f(2) == 1
            v = ((N - 1) & -(N - 1)).bit_length() - 1
            j = max(i for i in range(1, 2 * e + 1) if fits(wild(e, i), N, p))
            assert j == min(2 ** v, 2 * e)
            out += [(2, 2, None if arm == "plus" else j)] * cnt
            continue
        if not fits(K.lam(r, 1), N, p):
            continue
        j = 1
        while fits(K.lam(r, j + 1), N, p):
            j += 1
            assert j < 64
        out += [(r, NR, j)] * cnt
    return out


def run_cy(K, show=True):
    """Every place of norm <= 50: (r, N, count, beta+, beta exact,
    exact menu)."""
    pts = []
    for r in range(2, 51):
        if not is_prime_mr(r):
            continue
        NR, cnt = K.places(r)
        if NR > 50:
            continue
        mp_ = menu_cy(K, (r, NR), "plus")
        me = menu_cy(K, (r, NR), "exact")
        pts.append((r, NR, cnt, point_ab(K, mp_), point_ab(K, me), me))
    pts.sort(key=lambda e: -e[3])
    if show:
        for r, NR, cnt, bp, bx, me in pts:
            print(f"    N={NR:3d} over {r:2d} (x{cnt}) beta+ {bp:.5f} "
                  f"exact {bx:.5f}  headroom {summary(me, 10)}")
    return pts


def wild_control(e):
    """Brute element orders in Z_2[zeta_2e]/pi^j, j <= 2e, pi = zeta_2e
    - 1, whose minimal polynomial (pi + 1)^e + 1 is Eisenstein: the
    pi-adic digits 0, 1 give O/pi^j, and v(sum c_i pi^i) = min
    e v_2(c_i) + i."""
    from math import comb
    phi = [2] + [comb(e, i) for i in range(1, e)]
    M = 16                                      # O/16 = O/pi^(4e)

    def mul(a, b):
        c = [0] * (2 * e - 1)
        for i, x in enumerate(a):
            if x:
                for k, y in enumerate(b):
                    c[i + k] += x * y
        for k in range(2 * e - 2, e - 1, -1):
            t = c[k]
            if t:
                c[k] = 0
                for i in range(e):
                    c[k - e + i] -= t * phi[i]
        return [x % M for x in c[:e]]

    def val(a):
        v = 4 * e
        for i, x in enumerate(a):
            if x:
                v = min(v, e * ((x & -x).bit_length() - 1) + i)
        return v

    J = 2 * e
    one = [1] + [0] * (e - 1)
    pw = [one]
    for _ in range(J - 1):
        pw.append(mul(pw[-1], [0, 1] + [0] * (e - 2)))
    expo = [1] * (J + 1)
    for bits in range(2 ** (J - 1)):
        u = one
        for i in range(1, J):
            if bits >> (i - 1) & 1:
                u = [(x + y) % M for x, y in zip(u, pw[i])]
        vs, y = [], u
        for _ in range(6):
            vs.append(val([(y[0] - 1) % M] + y[1:]))
            y = mul(y, y)
        for j in range(1, J + 1):
            k = next(k for k in range(6) if vs[k] >= j)
            expo[j] = max(expo[j], 2 ** k)
    got = expo[1:]
    want = [wild(e, j) for j in range(1, J + 1)]
    print(f"  Z2[zeta{2 * e}]/pi^j, j = 1..{J}: exponents {got}")
    check(f"C4 wild chain at e = {e}: brute orders in Z2[zeta{2 * e}] = "
          f"2^ceil(log2 j)", got == want)


def cyc_char(m, d):
    """chi = phi^a psi mod 4dm, d = 4 or 8: phi(5) = zeta_d, phi(-1) = 1
    on (Z/4d)^*, psi(g) = zeta_d at the least primitive root g mod m,
    a = -ind(5) mod d."""
    g = next(a for a in range(2, m) if all(
        pow(a, (m - 1) // q, m) != 1 for q in factor(m - 1)))
    ind = [0] * m
    x = 1
    for k in range(m - 1):
        ind[x] = k
        x = x * g % m
    w, i2 = 4 * d, {}
    x = 1
    for t in range(d):
        i2[x], i2[w - x] = t, t
        x = x * 5 % w
    a = -ind[5] % d
    assert a % 2 == 1, ("ind(5) even at", m)
    n = w * m
    ex = [-1] * n
    for u in range(n):
        if u % 2 and u % m:
            ex[u] = (a * i2[u % w] + ind[u % m]) % d
    return Cyclic(n, d, ex)


def power_char(m, d):
    """The characters of (Z/m)^* trivial on the d-th powers."""
    g = next(a for a in range(2, m) if all(
        pow(a, (m - 1) // q, m) != 1 for q in factor(m - 1)))
    ex = [-1] * m
    x = 1
    for k in range(m - 1):
        ex[x] = k % d
        x = x * g % m
    return Cyclic(m, d, ex)


def tail(K, col, men, b):
    """T: (log mass, Psi) of the column at b with every place of norm
    n = d p^i + 1 > LIM, d | N - 1, added at count [K:Q] and unbounded
    depth: the most the menus' stop at LIM can hide."""
    p, N = col
    s = 0.0
    for d in (d for d in range(1, N) if (N - 1) % d == 0):
        i = 0
        while True:
            n = d * p ** i + 1
            if n > LIM:
                x = n ** -b
                if n > 10 ** 40:            # geometric rest, ratio p^-b
                    s += K.d * x / (1 - x) / (1 - p ** -b)
                    break
                s += K.d * x / (1 - x)
            i += 1
    return s, K.zK(b) - sigma(men, b) * math.exp(s)


def race(K, m):
    d, e = K.d, K.e(2)
    print(f"\nK{d}({m}): conductor {K.n}, f(3) = {K.f(3)}, f(7) = {K.f(7)}")
    pts = run_cy(K)
    f2 = [e_ for e_ in pts if e_[1] == 2][0]
    oth = [e_ for e_ in pts if e_[1] != 2]
    top = max(oth, key=lambda e_: e_[3])
    g = f2[3] - top[3]
    v = "loses" if g < -1e-5 else ("ties" if g <= 1e-5 else "undecided")
    print(f"  TOP LINE d={d} m={m}: F2 {f2[3]:.5f} | top other N={top[1]} "
          f"beta+ {top[3]:.5f} | gap+ {g:.5f}  -> {v}")
    topx = max(oth, key=lambda e_: e_[4])
    gx = f2[4] - topx[4]
    brk = f" (a bracket: 2^v >= {2 * e})" if (topx[1] - 1) % (2 * e) == 0 \
        else ""
    print(f"  EXACT d={d} m={m}: F2 {f2[4]:.5f} | top other N={topx[1]} "
          f"{topx[4]:.5f} | gap {gx:.5f}{brk}")
    # (5): at most d places per Fermat norm past 65537, each F_n^-b
    # below 2^(-2^n), n >= 5, a sum below 2 2^-32
    print(f"  MISSED d={d} m={m}: the F2 column's mass past 10^6 below "
          f"{d * 2.0 ** -31:.1e}, against the gap {abs(gx):.1e}")
    if gx < -1e-5:
        s, psi = tail(K, (topx[0], topx[1]), topx[5], f2[4])
        print(f"  TAIL d={d} m={m} N={topx[1]}: log mass past 10^6 "
              f"{s:.1e}; Psi at the F2 point {psi:.8f}")
        check(f"T d={d} m={m}: the winner stays above the F2 point with "
              f"the tail at full count", psi > 1 and s < 1e-8)
    return g, gx


def section_o():
    print("\n=== O: one F2 place")
    print("CONTROL C1: the quadratic characters of conductor 7 and 17")
    for m, want in ((7, (1.66168, 7, 1.43224)), (17, (1.60213, 47, 1.38428))):
        pts = run_cy(power_char(m, 2))
        f2 = max(e[3] for e in pts if e[1] == 2)
        o = [e for e in pts if e[1] != 2][0]
        check(f"C1 conductor {m} reprints Q's line",
              abs(f2 - want[0]) < 1e-5 and o[1] == want[1]
              and abs(o[3] - want[2]) < 1e-5,
              f"F2 {f2:.5f}, N={o[1]} {o[3]:.5f}")
    print("CONTROL C2: the 8th-power character mod 2969")
    pts = run_cy(power_char(2969, 8), show=False)
    f2 = max(e[3] for e in pts if e[1] == 2)
    o = [e for e in pts if e[1] != 2][0]
    check("C2 d = 8, m = 2969 reprints A's least-gap line",
          abs(f2 - 2.49345) < 1e-5 and o[1] == 47
          and abs(f2 - o[3] - 0.07596) < 1e-5,
          f"F2 {f2:.5f}, N={o[1]} {o[3]:.5f}, gap {f2 - o[3]:.5f}")
    print("CONTROL C4: the wild chain")
    wild_control(4)
    wild_control(8)
    fields = [(8, m, cyc_char(m, 8)) for m in (17, 73, 97)]
    fields += [(4, m, cyc_char(m, 4)) for m in (13, 17, 37, 53, 73, 97)]
    want = {17: [5, 47], 73: [5, 23, 29, 53], 97: [5, 47]}
    for d, m, K in fields:
        sc = [r for r in range(3, 100) if is_prime_mr(r)
              and K.places(r)[1] == d]
        eul = 1.0
        for r in growth_primes(10 ** 5):
            NR, cnt = K.places(r)
            eul *= (1 - NR ** -3.0) ** -cnt
        err = abs(K.zK(3.0) / eul - 1)
        check(f"C3 d={d} m={m}: e, f at 2 = {K.ram[2]}, at m = {K.ram[m]}; "
              f"split completely below 100 {sc}; zeta_K(3) = Euler",
              K.ram[2] == (d, 1) and K.ram[m] == (d, 1) and 5 in sc
              and (d == 4 or sc == want[m]) and err < 1e-10, f"{err:.1e}")
    guard()
    for d, m, K in fields:
        race(K, m)


def main():
    sec = [a for a in ("--quad", "--corner", "--deg", "--one")
           if a in sys.argv]
    every = not sec
    if every or "--quad" in sec or "--corner" in sec:
        control_q()
        guard()
    if every or "--quad" in sec:
        section_q((-7, 17, -8, 161), "Q: four quadratic fields")
    if every or "--corner" in sec:
        section_q(corner_ds(), "C: 2 and 3 ramified, 5 split, |D| < 400")
    if every or "--deg" in sec:
        section_a()
    if every or "--one" in sec:
        section_o()
    print(f"\n{sum(CHECKS)} of {len(CHECKS)} checks pass")
    return 0 if all(CHECKS) else 1


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