rho.py

Checks: TRIPLE: The lean's character is irreducible theorem; observation.

Cited on: TRIPLE.

To run it: save rho.py, with cubic.py, module_law.py, principal.py, seat.py beside it; needs mpmath.

"""rho.py -- the cubic's 2-torsion character read as an Artin trace.

QUESTION. Over a quadratic field of 2-rank 1 the unramified quadratic L
gives zeta_L/zeta_K = L(chi_d1) L(chi_d2), two Dirichlet factors, and the
small one's fixed signs carry the least split place's lean. Over a
complex cubic K of 2-rank 1, L/K the unramified quadratic (unramified at
K's real place too), is rho = zeta_L/zeta_K = Ind_K^Q psi reducible, or
is there no character of smaller conductor inside it?

THE DERIVATION (before code). G the closure's group, H = Gal(./K) of
index 3, G 2-transitive on K's three conjugates, so H\\G/H has two double
cosets and Mackey gives <rho, rho> = 1 + [psi = psi^s on H n sHs^-1].
H n sHs^-1 fixes N = K K', the S3 closure, and equality there means
LN = L'N. (i) L != N: N/K ramifies at K's real place (d_K < 0), L/K
does not. (ii) If LN = L'N, then also L''N by the 3-cycle, so M = LN is
Galois over Q of degree 12, Gal(M/N) = C2 central: M is C2 x S3 or Dic3,
H^2(S3, C2) being C2. Dic3 has one involution, which fixes N: L = N,
against (i). In C2 x S3 the sextics through K are N, K(sqrt f) and
K(sqrt(f d_K)), f rational: a prime p ramified in Q(sqrt f) has a place
of odd ramification index in K (sum e_i f_i = 3), and that place
ramifies in L, against L/K unramified, Q having no unramified quadratic
extension. So <rho, rho> = 1: rho is irreducible, of dimension 3 and
conductor |d_K|, and contains no Dirichlet character.

THE CHECK. a(p) = trace of Frob_p on rho at p unramified in K, from the
class engine: psi(P) = +1 for a place in 2Cl, -1 outside; a split p
gives psi(P1) + psi(P2) + psi(P3), the product +1 so a in {3, -1}; a
partial p gives psi(P1) (+-1); an inert p gives 0. In the closure
E a^2 = 1/6 x 3 + 1/2 x 1 + 1/3 x 0 = 1 for an irreducible rho, the
number of components (>= 2) for a reducible one. A split prime reads 9
or 1, so the split fiber's excess of all three in 2Cl over 1/4 moves the
mean up by 8 x (split share) x the excess: the prime-square term of the
split fiber (seat.py) inflates the cubic arm. Per reading var(a^2) is
about 1.3 pooled, so N readings give a se near 1.2/sqrt(N).

PREDICTIONS, frozen before the engine.
  P1 the cubic arm (complex cubics of 2-rank 1, |d_K| <= 24000,
     unramified p < 2000) reads a pooled mean a^2 within 0.85 to 1.20.
  P2 the split fiber's share of all three in 2Cl is above 1/4, so P1's
     reading sits above 1.
  P3 the band p < 200 reads further from 1 than the band 200 to 2000.
  KILL (printed): the cubic arm's pooled mean a^2 above 1.5, midway
     between irreducible and the least reducible, its bar on the cubic
     arm, the arm the split-fiber confound inflates.
  CONTROL, read first: real and imaginary quadratic fields with exactly
     two prime discriminant factors, |D| <= 24000, a = chi_d1(p) +
     chi_d2(p) at p prime to D below 2000, pooled mean a^2 within 1.8 to
     2.2. Its break: chi_d2 replaced by chi_d1 prints 4 at every p.
     The engine's check: every split prime of the cubic arm reads 3 or
     -1, never 1 or -3.

ADDED AT THE AUDIT. The null spread above was mispriced: in the limit
a^2 is 9, 1, 0 at 1/24, 15/24, 8/24, so var(a^2) = 3; the se is now
taken from the readings' own a^4, the readings as independent. The
confound's sign was wrong: seat.py's fiber table (F1) records the split
fiber's raw excess outside 2Cl, the prime-square weight landing on the
squares, so by sign that table (h >= 2, |d_K| <= 12000, cut 1000,
a per-place share) predicts P2's miss. The control's break could
not fire (kron is +-1 at p prime to D) and is gone; a known answer
through the place path replaces it: psi = 1 gives the permutation
character, 1 plus the 2-dimensional of S3, mean a^2 = 2 in the limit.
Of the three sextics through K the third is K(sqrt f) with f d_K
renamed f. Unramified at the real place is automatic: -1 has norm -1,
so Cl+ = Cl.

FINDINGS (|d_K| <= 24000, p < 2000).
  F0  CONTROLS. The quadratic arm reads 1.9934 over 1863968 readings;
      psi = 1 reads 1.8792 (the split share 0.153 against 1/6 holds it
      below 2), a control of the place path that never reads a class;
      every certified field (none skipped) placed every
      place, and the 31758 split primes all read 3 or -1.
  F1  KILL MISSED: the 688 cubic fields of 2-rank 1 read a pooled mean
      a^2 of 0.9267 +- 0.0035 over 207269 readings, P1 HELD, 21.0 se
      below 1; the quadratic arm at the same cut reads 1.9934. The
      print is irreducible's, never a reducible rho's.
  F2  P2 MISSED, as seat.py's fiber table predicts by sign (its own
      population, h >= 2 to 12000 at cut 1000): all three split places lie in
      2Cl at 0.2229, below 1/4. The split fiber is 0.1532 of the
      readings (1/6 in the limit) and gives 0.1532 x (1 + 8 x 0.2229)
      = 0.4265 of the mean; the rest, 0.5002, is the partial fiber's,
      at its 1/2: the mean sits below 1 by the split fiber's share and
      its lean.
  F3  P3 HELD: p < 200 reads 0.7964 (30719), 200 to 2000 0.9493
      (176550).
  TIERS. The irreducibility is a theorem (the derivation above); F1 to
  F3 are observations over this population.

RUN RECORD. A rehearsal at --cap 6000: 3/3, 33.1 s, 42.9 MB (120
fields, mean 0.9442, P2 missed at 0.2320). Full: 3/3, 150.9 s, peak
commit 43.6 MB under a memory guard (512 MB). At the audit (the se,
the psi = 1 control, the uncertified count): 3/3 at --cap 1500, 7.3 s;
full 3/3, 149.0 s, 43.9 MB. From code/, the split fiber's
part printed: 3/3, 149.7 s, 43.5 MB.
"""

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 cubic as C  # noqa: E402
import principal as Q  # noqa: E402
from seat import two_rank, is_square  # noqa: E402
from module_law import check, section, CHECKS  # noqa: E402


def _flag(name, default):
    if name in sys.argv:
        return sys.argv[sys.argv.index(name) + 1]
    return default


CAP = int(_flag("--cap", 24000))
PMAX = 2000
BAND = 200
SMALLP = [p for p in C.PRIMES if p < PMAX]


def prime_discs(D):
    """The prime discriminants whose product is the fundamental D."""
    out, n = [], abs(D)
    if n % 4 == 0:
        m = n // 4
        if m % 2 == 0:
            n //= 8
            out.append(None)          # +-8, sign fixed below
        else:
            n //= 4
            out.append(-4)
    p = 3
    while p * p <= n:
        if n % p == 0:
            out.append(p if p % 4 == 1 else -p)
            n //= p
        p += 2
    if n > 1:
        out.append(n if n % 4 == 1 else -n)
    if None in out:
        rest = 1
        for d in out:
            if d is not None:
                rest *= d
        out[out.index(None)] = 8 if D // rest > 0 else -8
    return out


def quadratic_arm():
    s2 = n = 0
    for D in list(range(-CAP, -2)) + list(range(5, CAP + 1)):
        if not Q.fundamental(D):
            continue
        ds = prime_discs(D)
        if len(ds) != 2:
            continue
        d1, d2 = ds
        assert d1 * d2 == D
        for p in SMALLP:
            if D % p == 0:
                continue
            a = Q.kron(d1, p) + Q.kron(d2, p)
            s2 += a * a
            n += 1
    return s2 / n, n


def cubic_arm():
    F = C.enumerate_fields(CAP)
    t0 = time.time()
    rd = {"lo": [0, 0], "hi": [0, 0]}     # band: [sum a^2, readings]
    split = [0, 0]                        # split primes: all three in, total
    bad_split = unplaced = fields = uncert = 0
    s4 = perm = 0                         # sum a^4; sum a^2 at psi = 1
    for key, dk in sorted(F.items(), key=lambda kv: (-kv[1], kv[0])):
        K = C.Field(key, dk)
        if not K.class_group(always_cert=True):
            uncert += 1
            continue
        inv = list(K.cl)
        if two_rank(inv) != 1:
            continue
        fields += 1
        for p in SMALLP:
            pls = K.places(p)
            if any(pl.e != 1 for pl in pls):
                continue
            deg1 = [pl for pl in pls if pl.deg == 1]
            cs = [K.place_class(pl) for pl in deg1]
            if any(c is None for c in cs):
                unplaced += 1
                continue
            ps = [1 if is_square(c, inv) else -1 for c in cs]
            a = sum(ps)
            if len(deg1) == 3:
                split[1] += 1
                split[0] += a == 3
                bad_split += a not in (3, -1)
            b = rd["lo" if p < BAND else "hi"]
            b[0] += a * a
            b[1] += 1
            s4 += a ** 4
            perm += len(deg1) ** 2
        if fields % 100 == 0:
            print("    %d fields, %.0f s" % (fields, time.time() - t0),
                  flush=True)
    return fields, rd, split, bad_split, unplaced, uncert, s4, perm


def main():
    t0 = time.time()
    section("A  THE CONTROL: THE QUADRATIC ARM")
    m2, n = quadratic_arm()
    check("control: quadratic mean a^2 within 1.8 to 2.2",
          1.8 <= m2 <= 2.2, "%.4f over %d readings" % (m2, n))
    if not all(CHECKS):
        print("CONTROL FAILED: nothing below is read.")
        raise SystemExit(1)

    section("B  THE CUBIC ARM")
    fields, rd, split, bad, unpl, uncert, s4, perm = cubic_arm()
    s = rd["lo"][0] + rd["hi"][0]
    n = rd["lo"][1] + rd["hi"][1]
    m = s / n
    se = ((s4 / n - m * m) / n) ** 0.5
    mp = perm / n
    print("  %d fields uncertified, skipped" % uncert)
    check("control: psi = 1 (the permutation character, 1 + the 2-dim"
          " of S3) reads mean a^2 within 1.8 to 2.2", 1.8 <= mp <= 2.2,
          "%.4f" % mp)
    print("  %d complex cubic fields of 2-rank 1, |d_K| <= %d; %d readings,"
          " %d unplaced  [%.0f s]" % (fields, CAP, n, unpl, time.time() - t0))
    check("engine: every split prime reads 3 or -1", bad == 0,
          "%d of %d split primes off" % (bad, split[1]))
    lo = rd["lo"][0] / rd["lo"][1]
    hi = rd["hi"][0] / rd["hi"][1]
    sh = split[0] / split[1]
    print("  pooled mean a^2 %.4f +- %.4f (readings taken as independent;"
          " %.1f se from 1)" % (m, se, (m - 1) / se))
    print("  by band: p < %d %.4f (%d), %d to %d %.4f (%d)"
          % (BAND, lo, rd["lo"][1], BAND, PMAX, hi, rd["hi"][1]))
    print("  split fiber: all three in 2Cl at %.4f of %d (1/4 in the limit)"
          % (sh, split[1]))
    fs = split[1] / n
    fm = fs * (1 + 8 * sh)
    print("  split fiber: %.4f of the readings (1/6), its part of the mean"
          " %.4f (1/2), the rest %.4f (1/2)" % (fs, fm, m - fm))
    print("  P1 within 0.85 to 1.20: %s" % ("HELD" if 0.85 <= m <= 1.20
                                             else "MISSED"))
    print("  P2 split share above 1/4: %s" % ("HELD" if sh > 0.25
                                               else "MISSED"))
    print("  P3 the low band further from 1: %s"
          % ("HELD" if abs(lo - 1) > abs(hi - 1) else "MISSED"))
    print("  KILL (mean a^2 above 1.5): %s" % ("FIRED" if m > 1.5
                                               else "MISSED"))

    print("\n%d/%d checks PASS, %.1f s" % (sum(CHECKS), len(CHECKS),
                                          time.time() - t0))
    raise SystemExit(0 if all(CHECKS) else 1)


if __name__ == "__main__":
    main()