conductor.py

"""conductor.py -- what the conductor of a complex cubic field hands its
quadratic resolvent, read as a module under conjugation, and what that
module decides about the triples of triple.py.

QUESTION. K a complex cubic field, d_K = f^2 d0 with d0 the fundamental
discriminant of the resolvent k = Q(sqrt d0) and f the conductor; N the
Galois closure, chi the cubic character of k cutting out N, sigma the
conjugation of k. triple.py reads the triples of classes over the split
primes and finds they fill a subgroup R of M = {(a, b, c) : a + b + c =
0} of index 3^t, t read off f. This file reads WHY off the ray class
group of k modulo f, built rather than inferred:
  (a) Which rational primes can divide f at all, and how do they sit
      in k?
  (b) The 3-part of (O_k/f)^* splits under sigma into a FIXED (plus) and
      an INVERTED (minus) part. What are their ranks, prime by prime,
      and which fixed lines can a field use?
  (c) At 3-rank 1, what does the SIGN of the second line of the degree-9
      extension E of k, containing N and unramified over N, say about
      the regime R = M against R = Delta?
  (d) At f = 1, where N/k is unramified, what does 3 | h force on the
      class group of k?

THE ARGUMENT (written before the engine).
  (1) THE MODULUS IS THE INTEGER f. By conductor-discriminant for N/Q,
      |d_K| = |d0| N(fr), fr the conductor of chi, so N(fr) = f^2; chi o
      sigma = chi^-1 makes fr sigma-stable, and the local reading below
      admits no sigma-stable ideal of norm f^2 carrying chi but (f).
  (2) THE PLUS PART IS LOCAL. In 1 -> (O_k/f)^*/im(O_k^*) -> Cl_f(k) ->
      Cl(k) -> 1, 3-parts and then sigma-parts are exact (2 is
      invertible on a 3-group); sigma inverts Cl(k), since an ideal
      times its conjugate is principal, so Cl_f(k)_3^+ is
      ((O_k/f)^*)_3^+. The global units meet the 3-part only at d0 = -3,
      in a cube root of unity, which sigma inverts.
  (3) THE ADMISSIBLE PRIMES. At a prime Q of k dividing fr the local
      component chi_Q is a nontrivial cubic character of the local
      units, anti-invariant where sigma fixes Q. For q != 3: split q
      gives two swapped primes, each with the cyclic 3-part of
      (Z/q^e)^*, nontrivial iff q = 1 mod 3; inert q gives the 3-part of
      q^2 - 1, which is FIXED when q = 1 mod 3 (it sits in F_q^*, where
      Frobenius = sigma acts trivially) and INVERTED when q = 2 mod 3 (it
      sits in the norm-one part, order q + 1); ramified q gives F_q^*,
      fixed. An anti-invariant chi_Q needs an inverted piece, so: a
      prime q = 1 mod 3 divides f only when it SPLITS in k, a prime
      q = 2 mod 3 only when it is INERT. At q = 3, with a = v_3(f):
      v_3(d_K) = 2a + v_3(d0) <= 5 for a cubic field, so a <= 2, and
      a = 1 needs 3 | d0 (2 is no cubic field's 3-valuation).
  (4) THE LOCAL RANKS, prime power by prime power (the plus and minus
      ranks of the 3-part of (O_k/q^e)^*):
        split q = 1 mod 3        plus 1, minus 1 (two swapped cyclic
                                 factors of equal order)
        inert q = 2 mod 3        plus 0, minus 1
        3 split, a = 2           plus 1, minus 1 ((Z/9)^* twice)
        3 inert, a = 2           plus 1, minus 1 (U_1/U_2 = F_9 as an
                                 additive group, sigma the Frobenius,
                                 fixing F_3)
        3 ramified, a = 1        plus 0, minus 1 (Q/Q^2 with sigma(pi) =
                                 -pi)
        3 ramified, a = 2        plus 1, minus 2 (order 27; the fixed
                                 part the image of Z_3's units)
      So the plus rank of the whole is P = #{q = 1 mod 3 : q | f} +
      [9 | f], and a fixed line EXISTS at every field with 9 | f.
  (5) USABILITY. E/N unramified at Q means psi|U_Q lies in <chi_Q> for
      each character psi of Gal(E/k). A FIXED psi at a sigma-fixed Q
      must then be trivial there (<chi_Q> holds no nontrivial invariant
      character), while every nontrivial fixed character is ramified
      somewhere over f by (2). At a swapped pair the local 3-part is
      cyclic, generated by chi_Q, so nothing is asked. Hence a USABLE
      fixed line exists iff some prime of k dividing f is split over Q
      with nontrivial local 3-part: by (3) and (4), iff t >= 1. The
      fixed lines that exist and cannot be used are exactly those of a
      9 | f with 3 not split, at a field with no q = 1 mod 3 dividing f.
  (6) THE FIXED STEP. Suppose R = Delta at 3-rank 1, and let H' be the
      cubic unramified extension of K, H'_1 its copy over K_1. The
      transposition tau fixing K_1 pointwise restricts to sigma on k;
      any lift stabilizes H'_1 and acts on it as an element of the
      abelian group Gal(H'_1/K_1), so conjugation by it is trivial on
      Gal(J/N) = Gal(H'_1/K_1), J = H'_1 N. Order 9 with 2 invertible
      splits Gal(J/k) into sigma-parts; the minus part meets the fixed
      Gal(J/N) trivially and maps into Gal(N/k), which sigma inverts, so
      both parts have order 3: the SECOND LINE IS FIXED, and by (5)
      usable, so t >= 1. Contrapositive: at 3-rank 1, t = 0 forces
      R = M. (With the genus field bounding from below, the index law of
      triple.py is proved at 3-rank 1.)
  (7) THE INVERTED STEP. Suppose R = M at 3-rank 1. Project the triples
      to (Cl/3Cl)^3: the image is the whole sum-zero module St, of order
      9. Let L be its fixed field over N: L/N is unramified, L is Galois
      over Q (the kernel is S3-stable), and G = Gal(L/k) has order 27
      and contains St, so it is not cyclic. The commutators [g, m] =
      (s - 1)m, s a 3-cycle, fill the diagonal D, and G/D is abelian, so
      G^ab has order 9; a 3-group needs as many generators as its
      abelianization (Burnside), so G^ab = (Z/3)^2. Its fixed field E'
      is abelian of type (3, 3) over k, contains N, is unramified over
      N, and sigma acts on the line St/D by the sign: the SECOND LINE IS
      INVERTED. So at 3-rank 1 every field carries a type-(3, 3)
      extension of k containing N and unramified over N, and the regime
      is the sign of its second line. Predicted consequence the census
      reads: every field at 3-rank 1 with t = 0 has minus rank of
      Cl_f(k)_3 at least 2.
  (8) THE COUNT OF THAT MINUS RANK. Minus parts of the ray class
      sequence are exact, so rk Cl_f(k)_3^- >= 2 holds when f = 1 and
      rk_3 Cl(k) >= 2; when f > 1 and rk_3 Cl(k) >= 1 (an unramified
      character serves, independent of the ramified chi); and when
      f > 1, rk_3 Cl(k) = 0 and the local minus rank, read one lower at
      d0 = -3, is at least 2.
  (9) CONDUCTOR 1. At f = 1 there is no conductor prime, so t = 0, and
      N/k is unramified. Let 3 | h and take any quotient Cl -> Z/3. The
      triples' image in the sum-zero triples of Z/3 is an S3-stable
      subgroup projecting onto each coordinate, so D or St. D gives, by
      (6), a FIXED unramified character of order 3 of k, impossible
      since sigma inverts Cl(k). So it is St, and (7) gives E'/k abelian
      of type (3, 3) and unramified: rk_3 Cl(k) >= 2. Hence

          f = 1 and 3 | h(K)  ==>  3-rank Cl(k) >= 2,

      at every 3-rank of K. The number of cubic fields of discriminant
      exactly d0 is (3^r - 1)/2, r = rk_3 Cl(d0) (Hasse), a known count
      used here as the enumeration's control.

PREDICTIONS, fixed before the engine. The population is triple.py's:
every complex cubic field with |d_K| <= 50000, class groups certified.
  C  CONTROLS, read before anything else.
     C1 THE MODULE ENGINE: over a sample of d0 and moduli, the brute
        unit group's order equals the Euler product read off the
        splitting types; sigma is multiplicative and an involution, both
        sampled on the first eight units; the image of x -> x^c, c the
        order's prime-to-3 part, has the 3-part's size; plus and
        minus ranks sum to the 3-rank; the ranks over a composite
        modulus are the sums over its prime powers.
     C2 THE PINS: d0 = -19, f = 18 (2 and 3 inert): plus 1, minus 2, no
        usable line. d0 = -11, f = 18 (2 inert, 3 split): plus 1, minus
        2, a usable line.
     C3 HASSE'S COUNT (known): at every fundamental d0 with
        |d0| <= 10000 the fields with d_K = d0 number (3^r - 1)/2.
  A  THE ADMISSIBLE PRIMES (3). Over all fields: every q = 1 mod 3
     dividing f splits in k; every q = 2 mod 3 dividing f is inert;
     v_3(f) <= 2; v_3(f) = 1 only at 3 | d0. Kill: any violation.
  B  THE LOCAL RANKS AND THE USABLE LINE (4), (5). At every field with
     f > 1: the plus rank of the 3-part of (O_k/f)^* equals P, and its
     minus rank equals #{q | f, q != 3} + [9 | f] + [3 exactly divides
     f] + [9 | f and 3 ramified]; the usable-line predicate, computed
     from the local modules, equals t >= 1. Kill: any field off in
     either rank or in usability. The fields where a fixed line exists
     unusable are printed, and are predicted to be exactly those with
     9 | f, 3 not split in k, and no q = 1 mod 3 dividing f.
  S  THE SIGN (6), (7), (8). At 3-rank 1 (fields with at least ten
     mapped split primes): the triple index is 3 exactly at t = 1 (a
     re-read of triple.py F2); every field with t = 0 has minus rank of
     Cl_f(k)_3 at least 2, by (8). Kill: one t = 0 field without. The
     degenerate fields carrying an inverted line as well are printed,
     not predicted. The inverted line's USABILITY is automatic by (5)'s
     local reading wherever sigma-fixed local minus ranks are at most 1,
     which (4) predicts everywhere but at a ramified 3 with 9 | f; those
     fields are counted and the first eight named.
  Z  CONDUCTOR 1 (9). Every field with f = 1 and 3 | h has rk_3 Cl(k)
     >= 2. Kill: one field without. Printed beside it: the f = 1 fields
     by h, and those with 3 not dividing h by the 3-rank of Cl(k).
  Z2 THE RANK LAW AT CONDUCTOR 1 (known: Gerth, Acta Arith. 30 (1976)
     307-322, Theorem 3.4; added as a control after (9)'s converse was
     found there). Where N/k is unramified, rk_3 Cl(K) = rk_3 Cl(k) - 1,
     which holds (9) and its converse at once. Predicted: at every field
     with f = 1 the class group of cubic.py and the form class group of
     principal.py read ranks one apart. Kill: one field where they do
     not; a failure is an engine's, not the theorem's.
  H  THE h = 3 POPULATION TO 13000, re-read under certified class
     groups: the number of fields and of those with t >= 1. An earlier,
     uncertified reading of the same population counted 193 fields, 70
     with t >= 1, three fields left unresolved; the number here is
     printed, not predicted.
KILLS, as printed observables: a C line off (nothing below is read); an
A violation; a B field off; an S field at t = 0 without minus rank 2; a
Z field with rk_3 Cl(k) <= 1; a Z2 field off the rank law.

FINDINGS. 11/11 checks PASS (9/9 after the audit in RUN RECORD).
  C  C1 0 bad over 81 discriminant-modulus pairs. C2 both pins as
     derived: (plus, minus, usable) = (1, 2, no) at d0 = -19 and
     (1, 2, yes) at d0 = -11. C3 Hasse's count exact at all 3043
     fundamental d0 with |d0| <= 10000, 1062 fields at f = 1.
  A  0 violations over 8336 fields. Conductor primes: 272 occurrences
     of q = 1 mod 3, all split; 1764 of q = 2 mod 3, all inert; q = 3
     inert 76, ramified 550, split 78.
  B  Over the 2468 fields with f > 1 the plus and minus ranks match (4)
     at every field and the usable line is exactly t >= 1. A fixed line
     exists unusable at 150 fields (3 inert 76, 3 ramified 74), exactly
     the 150 the shape predicts, 39 of them at 3-rank 1, where t = 0
     makes R = M: existence does not sort, usability does.
  S  1365 fields at 3-rank 1, the index 3^t at every one. All 1018 with
     t = 0 carry an inverted second line: 408 at f = 1 with
     rk_3 Cl(k) >= 2, 495 at f > 1 with rk_3 Cl(k) >= 1, 115 from the
     local part alone. 121 of the 347 fields with t = 1 meet (8)'s
     sufficient conditions for an inverted line of the ray class group
     mod f beyond chi's, unramified over N save possibly at the t = 1
     fields among the 23 below. 23 fields have a sigma-fixed local minus
     rank above 1 (a ramified 3 with 9 | f; the least |d_K| among them
     11907), 7 of them at t = 1, where the inverted line's usability is
     not automatic from (5); at t = 0, (7) gives E' there all the same.
  Z  408 fields with f = 1 and 3 | h (h = 3: 290, 6: 59, 9: 37, 12: 12,
     15: 7, 21: 2, 33: 1), every one over a resolvent of 3-rank at least
     2. All 5460 fields with f = 1 and 3 not dividing h sit over a
     resolvent of 3-rank exactly 1. Z2: the ranks (rk_3 Cl(K),
     rk_3 Cl(k)) read (0, 1) at 5460 fields and (1, 2) at 408, the rank
     law at all 5868 with f = 1, none off.
  H  194 fields at h = 3 to 13000, all with ten or more mapped split
     primes, 70 at t >= 1, 39 at f = 1.
  Tiers: A a rule, proved at (3) and verified to 50000; B's ranks a
  rule, proved at (4), verified; (6) and (7) theorems at 3-rank 1, S2
  their consequence verified; (9) a theorem, Z its verification, and
  with its converse a case of Gerth's rank law, which Z2 reads exact.

RUN RECORD. 11/11, 5.0 s wall, peak working set 71.9 MB under a memory
guard, the population read from triple.py's checkpoint. An audit then
made C1 read the 3-Sylow subgroup's size against the 3-part of the
order, added C0 (every field certified) and the S filter's dropped
count, and printed what follows from other checks rather than checking
it: B3 (from B1 and B2), S1 (triple.py F2 restricted to 3-rank 1) and
Z (from Z2). (8)'s count at d0 = -3 subtracts one for the cube roots of
unity, a lower bound, so S's counts of fields meeting (8) are counts of
a sufficient condition. The audit run: 9/9, 5.3 s wall, 68 MB peak
commit under a memory guard; B3, S1 and Z print as before (150 of 150,
1365 of 1365, 0 of 408), and a broken power map fails C1 at every one
of 15 cases tried in a scratch run. A later read added two prints, the
150's 3-ranks and the 23's count at t = 1: 9/9, 4.7 s, peak commit
68.2 MB, every earlier print unchanged.
"""

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

import time
from math import gcd

import principal as PR
import triple as T
from module_law import check, section, CHECKS

HASSE_D = 10000        # C3's range of fundamental discriminants
H3_CAP = 13000         # H's population


# ---------------------------------------------------------------------------
# the ring O_k / m and its unit group as a module under conjugation

class Res:
    """O_k / m for k = Q(sqrt d0), elements a + b w with w^2 = t w + n:
    w = (1 + sqrt d0)/2 when d0 = 1 mod 4, else sqrt(d0/4)."""

    def __init__(self, d0, m):
        self.d0, self.m = d0, m
        if d0 % 4 == 1:
            self.t, self.n = 1, (d0 - 1) // 4
        else:
            self.t, self.n = 0, d0 // 4

    def mul(self, x, y):
        (a, b), (c, d) = x, y
        m = self.m
        return ((a * c + b * d * self.n) % m,
                (a * d + b * c + b * d * self.t) % m)

    def conj(self, x):
        """sigma(a + b w) = a + b (t - w), since w + sigma(w) = t."""
        a, b = x
        return ((a + b * self.t) % self.m, (-b) % self.m)

    def norm(self, x):
        a, b = x
        return a * a + a * b * self.t - b * b * self.n

    def one(self):
        return (1 % self.m, 0)

    def units(self):
        m = self.m
        return [(a, b) for a in range(m) for b in range(m)
                if gcd(self.norm((a, b)) % m, m) == 1]

    def pow(self, x, e):
        r, base = self.one(), x
        while e:
            if e & 1:
                r = self.mul(r, base)
            base = self.mul(base, base)
            e >>= 1
        return r


def splitting(d0, q):
    if d0 % q == 0:
        return "ram"
    if q == 2:
        return "split" if d0 % 8 == 1 else "inert"
    return "split" if pow(d0 % q, (q - 1) // 2, q) == 1 else "inert"


def prime_powers(n):
    out, d = [], 2
    while d * d <= n:
        e = 0
        while n % d == 0:
            n //= d
            e += 1
        if e:
            out.append((d, e))
        d += 1
    if n > 1:
        out.append((n, 1))
    return out


def euler_order(d0, m):
    num, den = m * m, 1
    for q, _ in prime_powers(m):
        s = splitting(d0, q)
        if s == "split":
            num, den = num * (q - 1) ** 2, den * q * q
        elif s == "inert":
            num, den = num * (q * q - 1), den * q * q
        else:
            num, den = num * (q - 1), den * q
    assert num % den == 0
    return num // den


def rank3(R, elems):
    """The F_3-rank of a 3-group given as its list of elements."""
    e = R.one()
    k = sum(1 for x in elems if R.pow(x, 3) == e)
    r = 0
    while 3 ** r < k:
        r += 1
    assert 3 ** r == k
    return r


def module_full(d0, m):
    """(|units|, |3-part|, rank, plus rank, minus rank) of the 3-part of
    (O_k/m)^* under sigma, by brute force."""
    R = Res(d0, m)
    us = R.units()
    n = len(us)
    s3, c = 1, n
    while c % 3 == 0:
        c //= 3
        s3 *= 3
    if s3 == 1:
        return n, 1, 0, 0, 0, 1
    syl = sorted({R.pow(x, c) for x in us})
    u = (s3 + 1) // 2                      # 2u = 1 mod s3
    plus, minus = set(), set()
    for x in syl:
        sx = R.conj(x)
        plus.add(R.pow(R.mul(x, sx), u))
        minus.add(R.pow(R.mul(x, R.pow(sx, s3 - 1)), u))
    return (n, s3, rank3(R, syl), rank3(R, sorted(plus)),
            rank3(R, sorted(minus)), len(syl))


_MOD = {}


def module(d0, m):
    """(plus rank, minus rank) over m, summed over its prime powers (the
    CRT step C1 controls), each brute-forced once."""
    rp = rm = 0
    for q, e in prime_powers(m):
        key = (d0, q ** e)
        if key not in _MOD:
            _MOD[key] = module_full(d0, q ** e)
        rp += _MOD[key][3]
        rm += _MOD[key][4]
    return rp, rm


def usable_fixed(d0, f):
    """(5): a prime of k over a split q | f with nontrivial local
    3-part."""
    for q, e in prime_powers(f):
        if splitting(d0, q) == "split":
            key = (d0, q ** e)
            if key not in _MOD:
                _MOD[key] = module_full(d0, q ** e)
            if _MOD[key][1] > 1:
                return True
    return False


def fixed_minus_ranks(d0, f):
    """[(q, e, minus rank)] at the prime powers of f whose prime sigma
    fixes (inert or ramified in k)."""
    out = []
    for q, e in prime_powers(f):
        if splitting(d0, q) != "split":
            module(d0, q ** e)
            out.append((q, e, _MOD[(d0, q ** e)][4]))
    return out


# ---------------------------------------------------------------------------
# the class group of k

_R3 = {}


def rank3_k(d0):
    """rk_3 Cl(k), off principal.py's form class group."""
    if d0 not in _R3:
        G = PR.Group(d0)
        k = sum(1 for i in range(G.h) if G.power(i, 3) == G.e)
        r = 0
        while 3 ** r < k:
            r += 1
        assert 3 ** r == k
        _R3[d0] = r
    return _R3[d0]


def fundamental(d0):
    return T.fund_disc(d0) == (d0, 1)


# ---------------------------------------------------------------------------
# the sections

def section_controls(recs):
    section("C  CONTROLS")
    d0s = [d for d in range(-200, -2) if d % 4 in (0, 1) and fundamental(d)]
    bad = [0, 0, 0, 0, 0]
    n = 0
    for d0 in d0s[::7]:
        for m in (2, 3, 4, 5, 6, 7, 9, 14, 18):
            R = Res(d0, m)
            us = R.units()
            bad[0] += len(us) != euler_order(d0, m)
            e = R.one()
            for x in us[:8]:
                for y in us[:8]:
                    bad[1] += R.conj(R.mul(x, y)) != \
                        R.mul(R.conj(x), R.conj(y))
                bad[1] += R.conj(R.conj(x)) != x
                bad[1] += R.mul(x, R.pow(x, len(us) - 1)) != e
            _, s3, r, rp, rm, ns = module_full(d0, m)
            bad[2] += ns != s3          # the image of x -> x^c, its size;
            #                             it tests something only at s3 > 1
            bad[3] += rp + rm != r
            n += 1
        for m in (14, 18, 36, 45, 63):
            _, _, _, rp, rm, _ = module_full(d0, m)
            bad[4] += (rp, rm) != module(d0, m)
    check("C1 the module engine: order, sigma, 3-Sylow, ranks, CRT",
          not any(bad), "%d cases; bad %s" % (n, bad))
    pins = [(d0, f, module(d0, f) + (usable_fixed(d0, f),), want)
            for d0, f, want in ((-19, 18, (1, 2, False)),
                                (-11, 18, (1, 2, True)))]
    bad_pin = [p for p in pins if p[2] != p[3]]
    check("C2 the pins at f = 18", not bad_pin,
          "; ".join("d0 = %d: %s" % (p[0], p[2]) for p in pins))
    per = {}
    for r in recs:
        d0, f = T.fund_disc(r["d"])
        if f == 1 and -d0 <= HASSE_D:
            per[d0] = per.get(d0, 0) + 1
    off, nd = [], 0
    for d0 in range(-3, -HASSE_D - 1, -1):
        if d0 % 4 not in (0, 1) or not fundamental(d0):
            continue
        nd += 1
        want = (3 ** rank3_k(d0) - 1) // 2
        if per.get(d0, 0) != want:
            off.append((d0, per.get(d0, 0), want))
    check("C3 Hasse's count at every fundamental |d0| <= %d" % HASSE_D,
          not off, "%d discriminants, %d fields, off: %s"
          % (nd, sum(per.values()), off[:5]))
    return not any(bad) and not bad_pin and not off


def section_admissible(recs):
    section("A  THE ADMISSIBLE PRIMES")
    viol, occ = [], {}
    for r in recs:
        d0, f = T.fund_disc(r["d"])
        for q, e in prime_powers(f):
            s = splitting(d0, q)
            occ[(q % 3 if q != 3 else 3, s)] = \
                occ.get((q % 3 if q != 3 else 3, s), 0) + 1
            if q != 3 and q % 3 == 1 and s != "split":
                viol.append((r["d"], q, s))
            if q != 3 and q % 3 == 2 and s != "inert":
                viol.append((r["d"], q, s))
            if q == 3 and (e > 2 or (e == 1 and d0 % 3 != 0)):
                viol.append((r["d"], q, e))
    for key in sorted(occ):
        print("  q %s  %-6s %5d occurrences"
              % ("= 3   " if key[0] == 3 else "= %d mod 3" % key[0],
                 key[1], occ[key]))
    check("A every conductor prime sits in k as (3) says", not viol,
          "%d fields, violations %s" % (len(recs), viol[:5]))


def section_ranks(recs):
    section("B  THE LOCAL RANKS AND THE USABLE LINE")
    off, use_off, unusable, ranks = [], [], [], []
    nf = 0
    t0 = time.time()
    for r in recs:
        d0, f = T.fund_disc(r["d"])
        if f == 1:
            continue
        nf += 1
        pp = prime_powers(f)
        n1 = sum(1 for q, _ in pp if q != 3 and q % 3 == 1)
        other = sum(1 for q, _ in pp if q != 3)
        a = dict(pp).get(3, 0)
        ram3 = d0 % 3 == 0
        P = n1 + (a == 2)
        Mi = other + (a == 2) + (a == 1) + (a == 2 and ram3)
        got = module(d0, f)
        if got != (P, Mi):
            off.append((r["d"], d0, f, got, (P, Mi)))
        t = T.lines(d0, f)
        u = usable_fixed(d0, f)
        if u != (t >= 1):
            use_off.append((r["d"], d0, f, u, t))
        if got[0] > 0 and not u:
            unusable.append((r["d"], d0, f, splitting(d0, 3)))
            ranks.append(r["r"])
    print("  %d fields with f > 1, %.1f s" % (nf, time.time() - t0))
    check("B1 plus rank P and minus rank as (4) says, at every field",
          not off, "off at %d: %s" % (len(off), off[:5]))
    check("B2 a usable fixed line exactly where t >= 1", not use_off,
          "off at %d: %s" % (len(use_off), use_off[:5]))
    pred = unusable
    shape = all(dict(prime_powers(f)).get(3, 0) == 2 and
                splitting(d0, 3) != "split" and
                not any(q % 3 == 1 for q, _ in prime_powers(f) if q != 3)
                for (_, d0, f, _) in unusable)
    n_pred = 0
    for r in recs:
        d0, f = T.fund_disc(r["d"])
        if f % 9 == 0 and splitting(d0, 3) != "split" and \
                not any(q % 3 == 1 for q, _ in prime_powers(f) if q != 3):
            n_pred += 1
    print("  a fixed line exists unusable at %d fields (3 inert: %d, "
          "3 ramified: %d); the shape predicts %d"
          % (len(pred), sum(1 for x in pred if x[3] == "inert"),
             sum(1 for x in pred if x[3] == "ram"), n_pred))
    for x in pred[:12]:
        print("    d_K = %-7d d0 = %-6d f = %-4d 3 %s" % x)
    print("  of them, by 3-rank of Cl(K): %s"
          % sorted((k_, ranks.count(k_)) for k_ in set(ranks)))
    # B3 follows from B1 and B2, so it prints and is not checked
    print("  B3 (from B1, B2): the unusable fixed lines are the 9 | f, 3 not "
          "split cases: %s, %d printed, %d predicted"
          % (shape and len(pred) == n_pred, len(pred), n_pred))


def inverted_rank(d0, f):
    """rk Cl_f(k)_3^- is at least 2, by (8)."""
    r3 = rank3_k(d0)
    if f == 1:
        return r3 >= 2
    loc = module(d0, f)[1] - (1 if d0 == -3 else 0)
    return r3 >= 1 or loc >= 2


def section_sign(recs):
    section("S  THE SIGN OF THE SECOND LINE, AT 3-RANK 1")
    rows, thin = [], 0
    for r in recs:
        if r["r"] != 1:
            continue
        if len(r["triples"]) < T.MIN_SPLIT:
            thin += 1
            continue
        d0, f = T.fund_disc(r["d"])
        rows.append((r["d"], d0, f, r["t"], T.triple_index(r),
                     inverted_rank(d0, f),
                     any(m > 1 for _, _, m in fixed_minus_ranks(d0, f))))
    idx_off = [x for x in rows if x[4] != 3 ** x[3]]
    # triple.py's F2 at 3-rank 1, so a print
    print("  S1 (triple.py F2): the triple index is 3^t at %d of %d "
          "3-rank 1 fields; %d fields with fewer than %d mapped split "
          "primes left out" % (len(rows) - len(idx_off), len(rows), thin,
                               T.MIN_SPLIT))
    uni = [x for x in rows if x[3] == 0]
    deg = [x for x in rows if x[3] >= 1]
    miss = [x for x in uni if not x[5]]
    check("S2 every t = 0 field has an inverted second line (rank >= 2)",
          not miss, "%d uniform fields, without: %s"
          % (len(uni), [x[0] for x in miss][:8]))
    src = {"f = 1, rk_3 Cl(k) >= 2": 0, "f > 1, rk_3 Cl(k) >= 1": 0,
           "f > 1, local alone": 0}
    for x in uni:
        if x[2] == 1:
            src["f = 1, rk_3 Cl(k) >= 2"] += 1
        elif rank3_k(x[1]) >= 1:
            src["f > 1, rk_3 Cl(k) >= 1"] += 1
        else:
            src["f > 1, local alone"] += 1
    for k_, v in src.items():
        print("    uniform, %-26s %5d" % (k_, v))
    both = sum(1 for x in deg if x[5])
    print("  degenerate fields: %d, of which %d carry an inverted line "
          "too" % (len(deg), both))
    wild = [x[0] for x in rows if x[6]]
    print("  fields with a sigma-fixed local minus rank above 1 (the "
          "inverted line's usability not automatic): %d %s"
          % (len(wild), wild[:8]))
    print("    of them at t = 1: %d" % sum(1 for x in rows
                                         if x[6] and x[3] == 1))
    return rows


def section_corner(recs):
    section("Z  CONDUCTOR 1")
    byh, miss, off3 = {}, [], {}
    for r in recs:
        d0, f = T.fund_disc(r["d"])
        if f != 1:
            continue
        if r["h"] % 3 == 0:
            byh[r["h"]] = byh.get(r["h"], 0) + 1
            if rank3_k(d0) < 2:
                miss.append((r["d"], r["h"], rank3_k(d0)))
        else:
            k_ = rank3_k(d0)
            off3[k_] = off3.get(k_, 0) + 1
    print("  f = 1 and 3 | h, by h: %s" % sorted(byh.items()))
    print("  f = 1 and 3 !| h, by rk_3 Cl(k): %s" % sorted(off3.items()))
    # Z follows from Z2 (3 | h is rk_3 Cl(K) >= 1), so a print
    print("  Z (from Z2): f = 1 fields with 3 | h and rk_3 Cl(k) < 2: %d of "
          "%d %s" % (len(miss), sum(byh.values()), miss[:8]))
    joint, off = {}, []
    for r in recs:
        d0, f = T.fund_disc(r["d"])
        if f != 1:
            continue
        rK = sum(1 for n in r["inv"] if n % 3 == 0)
        rk = rank3_k(d0)
        joint[(rK, rk)] = joint.get((rK, rk), 0) + 1
        if rK != rk - 1:
            off.append((r["d"], tuple(r["inv"]), rk))
    print("  f = 1, (rk_3 Cl(K), rk_3 Cl(k)): %s" % sorted(joint.items()))
    check("Z2 rk_3 Cl(K) = rk_3 Cl(k) - 1 at every f = 1 field (Gerth)",
          not off, "%d fields, off: %s" % (sum(joint.values()), off[:8]))


def section_h3(recs):
    section("H  THE h = 3 POPULATION TO %d" % H3_CAP)
    h3 = [r for r in recs if r["h"] == 3 and -r["d"] <= H3_CAP]
    rd = [r for r in h3 if len(r["triples"]) >= T.MIN_SPLIT]
    print("  %d fields at h = 3, %d with %d or more mapped split primes, "
          "%d at t >= 1, %d at f = 1"
          % (len(h3), len(rd), T.MIN_SPLIT,
             sum(1 for r in rd if r["t"] >= 1),
             sum(1 for r in rd if T.fund_disc(r["d"])[1] == 1)))


def main():
    t0 = time.time()
    section("P  THE POPULATION")
    data = T.population()
    recs = [r for r in data["recs"] if r["ok"]]
    check("C0 every field of the population certified",
          len(recs) == len(data["recs"]),
          "%d of %d" % (len(recs), len(data["recs"])))
    if CHECKS[-1] and section_controls(recs):
        section_admissible(recs)
        section_ranks(recs)
        section_sign(recs)
        section_corner(recs)
        section_h3(recs)
    print()
    print("%d/%d checks PASS, %.1f s" % (sum(CHECKS), len(CHECKS),
                                         time.time() - t0))
    if not all(CHECKS):
        raise SystemExit(1)


if __name__ == "__main__":
    main()