cubtop.py

Checks: THERMAL: The F₂ place need not top theorem; observation; THERMAL: The race over cubic fields theorem; rule.

Cited on: THERMAL.

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

"""cubtop.py -- does the place with residue field F_2 top the thermal
place spectrum over every cubic field that has one?

QUESTION. quadtop.py proves the F_2 place tops every quadratic field
by a finite reduction; fieldtop.py finds it losing at a cyclic quartic
(conductor 272). A cubic field holds up to three places of each norm,
a place of norm 4 beside an F_2 place (2 = Q1 Q2), and a tame place
over 2 (e = 3). Does the reduction carry to degree 3 and prove it
there, or name the configurations where it fails?

THE ARGUMENT. K cubic with a place of residue field F_2: 2 of type 111,
12, 1^2 1 or 1^3 (a residue degree 1 place); x = 2^-b.
  (1) THE COMPARISON (fieldtop.py's (1)) and the F_2 column
      (fieldtop.py's (2)): sigma_2 = T2 prod_R (1 + N(R)^-b), T2 =
      (1 - x)^-(g - 1), g = 3, 1, 2, 1 F_2 places for 111, 12, 1^2 1,
      1^3. A norm in K is p, p^2 or p^3; p^3 - 1 is never a power of 2
      and p^2 - 1 only at p = 3, so the Fermat norms are 3, 5, 9, 17,
      257, 65537 and the Fermat primes past them, at most three places
      each, their factor below exp(2 n F_5^-b), n = 3.
  (2) THE EVEN COLUMNS. At 111 and 1^2 1 every place over 2 is an F_2
      place, and each F_2 column holds the others without bound
      (1 | 1) and the Fermat-norm places at depth 1: one sum, one
      point. At 12 the place Q2 of norm 4 holds Q1 without bound
      (1 | 3) and each Fermat-norm place R at depth >= 1 (lambda(R) =
      2^k | 3 2^oo), while the F_2 column holds no place over 2
      (3 does not divide 1): C_F2 is inside C_Q2, so Q2's point is the
      lower at every b. Only odd columns race.
  (3) THE CHAIN AT ANY e. At Q over 2 with f = 1, v(2) = e, (1 + y)^2 -
      1 = y (2 + y) has valuation >= t + min(e, t) =: phi_e(t) where
      v(y) >= t, and phi_e increases. So u^(2^v) = 1 mod Q^(phi_e^v(1))
      for every 1-unit u, and an odd column of norm N, v = v_2(N - 1),
      holds Q to depth D_e(v) >= phi_e^v(1): v + 2 at e = 1 (the
      (Z/2^j)^* chain, exact), 2, 4, 6, 8 at e = 2, and 2, 4, 7, 10,
      ..., 3 v - 2 from v = 2 at e = 3. The place Q2 of norm 4 has lambda(Q2^j) |
      3 2^(j-1) (F_4^* times 1 + 2y, which gains valuation >= 1 per
      squaring); a column over 3 has 3 | (N - 1) 3^oo, so holds Q2 to
      depth >= v + 1. Other columns drop Q2 (a factor >= 1).
  (4) THE BOUND (quadtop.py's (3), n = 3). sigma_P / sigma_2 >=
      A X / (T2 W): A the F_2 places' 1 + x + ... + x^(D_e(v)), times
      Q2's 1 + 4^-b + ... + 4^(-b(v+1)) at a column over 3; X =
      prod 1/(1 - N(R)^-2b) over the places R != P over P's prime with
      Fermat norm 2^k + 1, k <= v (held without bound, against
      sigma_2's depth 1); W = P's own 1 + N^-b where N is a Fermat
      norm, times every Fermat-norm place with k > v, times the tail.
  (5) THE FLOOR AND THE CEILING. zeta_K >= its local factors at 2, 3,
      5, 17, 257, 65537 times (1 - p^-3b)^-1 at every other p (the
      inert factor, the least of the five types), less the bound on
      sigma_2: every b where this is >= 1 lies at or below beta_F2.
      Psi_2 <= zeta_K - 1 <= zeta^3 - 1, so beta_F2 < b_top, the least
      b with zeta(b)^3 < 2.
  (6) THE CLASSES AND THE BRACKET (quadtop.py's (5)-(6)). 4 types of 2
      times 5 types of each of 3, 5, 17, 257, 65537 (111, 12, 3, 1^2 1,
      1^3): 12500 configurations. A Galois cubic is cyclic, ramified
      only at 3 and at primes = 1 mod 3: with an F_2 place, 2 is 111,
      3 is 111, 3 or 1^3, and 5, 17, 257, 65537 (each = 2 mod 3) are
      111 or 3, 48 configurations. Generic classes v = 1, 2, 3,
      4, 8, 16 and each Fermat-norm place's own class (v = k).

DESIGN. One grid G = 1.0001 + 1e-4 i up to 4, every power of it
cached. b_low: the largest point of G where the floor of (5) holds.
b_top: the point past the last one with zeta^3 >= 2. R3: the bracket
on G from b_low past b_top at every class of every configuration;
for a failing column, the largest point of G where A X <= T2 W (b_c,
the bound's own crossing, a print). Codes: s = 111, q = 12, i = 3,
r = 1^2 1, t = 1^3; a config is the types of 3, 5, 17, 257, 65537.
CONTROLS. (C) quadtop.py's quartic of conductor 272 through this
engine: A X = 1.472359 < T2 W = 1.490681 at 1.54337, floor 1.4793.
(C2) the twelve quadratic floors, each under its F_2 point. (CQ) the
486 quadratic configurations reprint quadtop.py's R: 0 failing, least
1.005847 (2 ramified, N = 3 own, config rssss). (C4) brute exponents
of U^1 mod Q^j: Z_2[pi], pi^e = 2, e = 1, 2, 3, j <= 10, each
lambda(Q^j) <= 2^v at j = D_e(v) for v = 1, 2, 3 (the deeper v the
proof's alone); Z_2[omega] mod 2^j, j <= 5, the
exponent divides 3 2^(j-1).

PREDICTIONS, fixed before the first run. (h1) k > 0: the column of
norm 3 own fails where 2 and 3 are 1^3 and 5, 17, 257, 65537 are 111.
(h2) every configuration with 2 of type 111 passes. (h3) type 12 at 2
passes every class. (h4) failures sit only where 2 is 1^3 or 1^2 1, at
v = 1 classes.
KILL, as the print: "R3 ... k failing columns of 12500 configurations".
k = 0 proves degree 3; k > 0 names leads, not losses, until a field
realizes one. The verdicts are checks since: R3 its 675 failing
columns, each the column of norm 3's own class at its own
configuration, 2 of type 1^2 1 or 1^3; RG none failing.

FINDINGS. C4: lambda(Q^j), j = 1..10, is 1, 2, 2, 4, 8, ..., 256 at
e = 1; 1, 2, 4, 4, 4, 8, 8, 16, 16, 32 at e = 2; 1, 2, 4, 4, 8, 8, 8,
16, 16, 16 at e = 3 (2^ceil(log2 j) to j = 7, not at 8); the norm-4
exponent 3, 6, 12, 24, 48. C: A X = 1.472359 < T2 W = 1.490681, floor
1.4793. C2: floors 1.4458 to 1.5659. CQ: 0 failing, least 1.005847 at
2 r, rssss, N = 3 own. beta_F2 < 2.7511. R3: 675 failing columns of
12500 configurations, every one the column
of norm 3's own class, 250 where 2 is 1^2 1 (least 0.958740) and 425
where 2 is 1^3 (least 0.901468, config qssss, b_low 1.5377, the
bound's crossing b_c 1.8580). RG: the 48 cyclic configurations, 0
failing, least 1.025386 at tssss, the column of norm 3. Every failing
configuration has 5 of type 111 or 1^2 1 (two or three places of norm
5), and 3 of type 12 or 1^3, or 1^2 1 (25, at 2 = 1^3 with 5 of type
111). Five (2, 3, 5) cells fail at all 125 configurations of 17, 257,
65537, each named by its nearest to passing: 2 = 1^2 1 with 3 = 12,
5 = 111 (qsiii, 0.977731) and 3 = 1^3, 5 = 111 (tsiii, 0.997619);
2 = 1^3 with 3 = 12, 5 = 1^2 1 (qriii, 0.998137, b_low 1.4628, b_c
1.4699), 3 = 12, 5 = 111 (qsiii, 0.930576) and 3 = 1^3, 5 = 111
(tsiii, 0.958309); two cells fail in part. So conditions at 2, 3 and 5
alone put a field in a failing configuration. The least ratio per 2's
type at the norm-3 column: 1.011007 (111), 1.077337 (12), 0.958740
(1^2 1), 0.901468 (1^3); every generic class passes, the least v = 1
at 1.061189 (1^3, issss), every other class above 1.15. (h1) holds as
to a failure and as to tssss failing; the least is qssss, where the
F_2 column also holds a place of norm 9. (h2) holds. (h3) holds. (h4)
holds: only 1^2 1 and 1^3, only the column of norm 3.
READING. The reduction does not prove degree 3, and proves the cyclic
cubic fields: the F_2 place tops every one with an F_2 place. The race
is lost by the bound where 2 has one or two F_2 places, one ramified,
and the F_2 column holds two or three places of norm 5 beside one or
two of norm 3 and often one of norm 9, while the column of norm 3 is
credited only the F_2 places to depth 2 (3 at the unramified one of
1^2 1) and its own conjugates over 3. An actual such field also credits
that column every place of norm 2 3^a + 1 (7, 19, 163, 487, 1459) at
depth 1, and its zeta_K exceeds the floor; whether one loses is a race
read at a field.

RUN RECORD. `python cubtop.py`: 6 of 6 checks, 122.6 s, peak commit
71.9 MB.
"""

import os
import sys
from itertools import product

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 quadtop import REL, FERMAT_NORMS, F5, STEP, quad_profile  # noqa: E402
from fieldtop import kronecker  # noqa: E402
from scipy.special import zeta  # noqa: E402

CHECKS = []
TWO = {"s": [(1, 1, 3)], "q": [(1, 1, 1), (1, 2, 1)],
       "r": [(2, 1, 1), (1, 1, 1)], "t": [(3, 1, 1)]}


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


class Grid:
    """The points b, with every power N^-b cached."""

    def __init__(self, b):
        self.b = np.atleast_1d(np.asarray(b, dtype=float))
        self._pw, self._z = {}, {}

    def pw(self, N):
        if N not in self._pw:
            self._pw[N] = float(N) ** -self.b
        return self._pw[N]

    def zeta(self, n):
        if n not in self._z:
            self._z[n] = zeta(n * self.b)
        return self._z[n]

    def geo(self, N, d):
        """1 + N^-b + ... + N^(-b d)."""
        y = self.pw(N)
        return (1 - y ** (d + 1)) / (1 - y)


def depth(e, v):
    """D_e(v): the depth an odd column with v holds an F_2 place of
    ramification e, by (3)."""
    if e == 1:
        return v + 2
    t = 1
    for _ in range(v):
        t += min(e, t)
    return t


class Profile:
    """A field at its relevant primes: degree n, the places over 2 as
    (e, f, count), and per odd p a list of (norm, count)."""

    def __init__(self, n, two, odd):
        self.n, self.two, self.odd = n, two, odd

    def g2(self):
        return sum(c for e, f, c in self.two if f == 1)

    def zeta_floor(self, g):
        n = self.n
        z = g.zeta(n) * (1 - g.pw(2 ** n))
        for e, f, c in self.two:
            z = z / (1 - g.pw(2 ** f)) ** c
        for p, pl in self.odd.items():
            z = z * (1 - g.pw(p ** n))
            for N, c in pl:
                z = z / (1 - g.pw(N)) ** c
        return z

    def fermat_places(self):
        """[(p, N, k, count)] of the odd places of Fermat norm."""
        return [(p, N, FERMAT_NORMS[N], c) for p, pl in self.odd.items()
                for N, c in pl if N in FERMAT_NORMS]

    def tail(self, g):
        return np.exp(self.n * 2 * F5 ** -g.b)

    def sigma2(self, g):
        s = (1 - g.pw(2)) ** -(self.g2() - 1)
        for p, N, k, c in self.fermat_places():
            s = s * (1 + g.pw(N)) ** c
        return s * self.tail(g)

    def b_low(self, g):
        """The largest point of g where the floor of (5) holds."""
        idx = np.nonzero(self.zeta_floor(g) - self.sigma2(g) >= 1)[0]
        return None if len(idx) == 0 else int(idx[-1])

    def columns(self):
        """(label, v, own, conj, over3) per column class: conj the norms
        of the other places over P's prime held without bound, each
        with whether it is a Fermat norm."""
        out = [(f"v={v} generic", v, None, (), False)
               for v in (1, 2, 3, 4, 8, 16)]
        for p, N, k, c in self.fermat_places():
            conj = [(N, True)] * (c - 1)
            for N2, c2 in self.odd[p]:
                if N2 != N and N2 in FERMAT_NORMS \
                        and FERMAT_NORMS[N2] <= k:
                    conj += [(N2, True)] * c2
            out.append((f"N={N} own", k, N, tuple(conj), p == 3))
        return out


def column_ratio(P, g, v, own=None, conj=(), over3=False):
    """(A X, T2 W) on g for an odd column with v, by (4)."""
    A = 1.0
    for e, f, c in P.two:
        if f == 1:
            A = A * g.geo(2, depth(e, v)) ** c
        elif f == 2 and over3:
            A = A * g.geo(4, v + 1) ** c
    X = 1.0
    for N, fermat in conj:
        y = g.pw(N)
        X = X / ((1 - y * y) if fermat else (1 - y))
    W = (1 + g.pw(own)) if own else 1.0
    for p, N, k, c in P.fermat_places():
        if k > v:
            W = W * (1 + g.pw(N)) ** c
    W = W * P.tail(g)
    T2 = (1 - g.pw(2)) ** -(P.g2() - 1)
    return A * X, T2 * W


def cubic_profile(t2, types):
    odd = {}
    for p, t in zip(REL, types):
        odd[p] = {"s": [(p, 3)], "q": [(p, 1), (p * p, 1)],
                  "i": [(p ** 3, 1)], "r": [(p, 2)], "t": [(p, 1)]}[t]
    return Profile(3, TWO[t2], odd)


def as_profile(Q):
    return Profile(Q.n, Q.two, Q.odd)


# ---------------------------------------------------------------------------
# controls

def brute_chain(e, j):
    """The exponent of U^1 mod pi^j in Z_2[pi], pi^e = 2."""
    M = -(-j // e) + 2
    mod = 2 ** M

    def mul(a, b):
        out = [0] * e
        for i, ai in enumerate(a):
            for k, bk in enumerate(b):
                if i + k < e:
                    out[i + k] += ai * bk
                else:
                    out[i + k - e] += 2 * ai * bk
        return [c % mod for c in out]

    def val(a):
        best = 10 ** 9
        for i, ai in enumerate(a):
            if ai % mod:
                w = 0
                while ai % 2 == 0:
                    ai //= 2
                    w += 1
                best = min(best, e * w + i)
        return best

    lam = 1
    ranges = [range(2 ** -(-(j - i) // e)) if j > i else range(1)
              for i in range(e)]
    for a in product(*ranges):
        if a[0] % 2 == 0:
            continue
        u, order = list(a), 1
        one = [(u[0] - 1) % mod] + u[1:]
        while val(one) < j:
            u = mul(u, u)
            order *= 2
            one = [(u[0] - 1) % mod] + u[1:]
        lam = max(lam, order)
    return lam


def brute_norm4(j):
    """The exponent of (Z_2[omega] / 2^j)^*, omega^2 = -1 - omega."""
    mod = 2 ** j

    def mul(a, b):
        return ((a[0] * b[0] - a[1] * b[1]) % mod,
                (a[0] * b[1] + a[1] * b[0] - a[1] * b[1]) % mod)

    lam = 1
    for a0 in range(mod):
        for a1 in range(mod):
            if a0 % 2 == 0 and a1 % 2 == 0:
                continue
            u, order = (a0, a1), 1
            while u != (1, 0):
                u = mul(u, (a0, a1))
                order += 1
            lam = np.lcm(lam, order)
    return int(lam)


def control_chain():
    print("CONTROL C4: the chains over 2, brute")
    ok = True
    for e in (1, 2, 3):
        lams = [brute_chain(e, j) for j in range(1, 11)]
        print(f"  e={e}: lambda(Q^j), j = 1..10: {lams}")
        for v in (1, 2, 3):
            d = depth(e, v)
            if d <= 10:
                ok &= lams[d - 1] <= 2 ** v
                print(f"    v={v}: D_e(v) = {d}, lambda = {lams[d - 1]} "
                      f"<= {2 ** v}")
    l4 = [brute_norm4(j) for j in range(1, 6)]
    ok4 = all((3 * 2 ** (j - 1)) % lam == 0 for j, lam in
              zip(range(1, 6), l4))
    print(f"  norm 4: exponent mod 2^j, j = 1..5: {l4}")
    check("C4 every D_e(v) read is held at v; the norm-4 exponent "
          "divides 3 2^(j-1)", ok and ok4)


def control():
    print("CONTROL C: the cyclic quartic of conductor 272")
    P = Profile(4, [(4, 1, 1)], {3: [(9, 2)], 5: [(5, 4)], 17: [(17, 1)],
                                 47: [(47, 4)]})
    b2 = 1.54337
    g = Grid(b2)
    x = 2 ** -b2
    A = 1 + x + x * x
    X = (1 - 47 ** -b2) ** -3
    W = (1 + 5 ** -b2) ** 4 * (1 + 9 ** -b2) ** 2 * (1 + 17 ** -b2)
    ax, tw = column_ratio(P, g, 1, conj=((47, False),) * 3)
    ax, tw = float(ax[0]), float(tw[0])
    print(f"  at b = {b2}: A X = {ax:.6f} (by hand {A * X:.6f}), "
          f"T2 W = {tw:.6f} (by hand {W:.6f})")
    G = Grid(1.0001 + STEP * np.arange(29999))
    bl = G.b[P.b_low(G)]
    print(f"  floor b_low = {bl:.4f}")
    check("C the norm-47 column beats the F2 column at its F2 point, and "
          "the floor sits below that point",
          ax < tw and abs(ax - A * X) < 1e-9 and abs(tw / W - 1) < 1e-8
          and bl <= b2)
    return G


def control_quadratic(G):
    print("\nCONTROL C2: the floor under the twelve quadratic fields read")
    pts = {-7: 1.66168, 17: 1.60213, -8: 1.63177, 161: 1.69138,
           24: 1.56837, 156: 1.66461, 204: 1.66621, 264: 1.61826,
           -24: 1.67138, -84: 1.65813, -264: 1.69169, -276: 1.68330}
    ok = True
    for D, b2 in pts.items():
        ty = "".join({1: "s", 0: "r", -1: "i"}[kronecker(D, p)]
                     for p in (2,) + REL)
        i = as_profile(quad_profile(ty[0], ty[1:])).b_low(G)
        ok &= i is not None and G.b[i] <= b2
        print(f"  D={D}: config {ty}, floor {G.b[i]:.4f} <= F2 point {b2}")
    check("C2 every floor at or below its field's F2 point", ok)


def top_index(G, n):
    """The point past the last one of G with zeta^n >= 2."""
    return int(np.nonzero(G.zeta(1) ** n >= 2)[0][-1]) + 1


def sweep(G, profiles, top):
    """The bracket of (6) at every class of every profile: (failing
    list, least per (2's type, class), least overall)."""
    bad, worst, least = [], {}, None
    for key, P in profiles:
        lo = P.b_low(G)
        assert lo is not None and lo < top
        for label, v, own, conj, over3 in P.columns():
            ax, tw = column_ratio(P, G, v, own, conj, over3)
            r = ax[lo + 1:top + 2] / tw[lo:top + 1]
            m = float(r.min())
            if not (r > 1).all():
                lose = np.nonzero(ax <= tw)[0]
                bc = G.b[lose[-1]] if len(lose) else None
                bad.append((key, label, G.b[lo], bc, m))
            wk = (key[0], label)
            if wk not in worst or m < worst[wk][0]:
                worst[wk] = (m, key[1])
            if least is None or m < least[0]:
                least = (m, key, label, G.b[lo])
    return bad, worst, least


def control_quad_sweep(G):
    print("\nCONTROL CQ: quadtop.py's 486 configurations through this "
          "engine")
    top = top_index(G, 2)
    profs = [((t2, "".join(ty)), as_profile(quad_profile(t2, ty)))
             for t2 in ("s", "r") for ty in product("sri", repeat=5)]
    bad, worst, least = sweep(G, profs, top)
    m, key, label, bl = least
    print(f"  b_top {G.b[top]:.4f}; {len(bad)} failing; least {m:.6f} at "
          f"2 {key[0]}, config {key[1]}, {label}, b_low {bl:.4f}")
    check("CQ 0 failing, least 1.005847 at 2 r, rssss, N=3 own",
          not bad and abs(m - 1.005847) < 5e-7 and key == ("r", "rssss")
          and label == "N=3 own")


def main():
    control_chain()
    G = control()
    control_quadratic(G)
    control_quad_sweep(G)
    if not all(CHECKS):
        print("a control failed: no verdict")
        sys.exit(1)
    top = top_index(G, 3)
    print(f"\nbeta_F2 < {G.b[top]:.4f} at every cubic field (zeta^3 < 2)")
    profs = [((t2, "".join(ty)), cubic_profile(t2, ty))
             for t2 in "sqrt" for ty in product("sqirt", repeat=5)]
    bad, worst, least = sweep(G, profs, top)
    print(f"R3 the bracket on [b_low, {G.b[top]:.4f}] by {STEP}: "
          f"{len(bad)} failing columns of {len(profs)} configurations")
    check("R3 675 failing columns, at 675 configurations, each the column "
          "of norm 3's own class with 2 of type 1^2 1 or 1^3",
          len(bad) == 675 and len({b[0] for b in bad}) == 675
          and all(b[1] == "N=3 own" and b[0][0] in "rt" for b in bad))
    gal = [(k, P) for k, P in profs if k[0] == "s" and k[1][0] in "sit"
           and set(k[1][1:]) <= set("si")]
    gbad, _, gleast = sweep(G, gal, top)
    print(f"RG the {len(gal)} cyclic configurations: {len(gbad)} failing, "
          f"least {gleast[0]:.6f} at config {gleast[1][1]}, {gleast[2]}")
    check("RG every column of the 48 cyclic configurations beats the F2 "
          "column on [b_low, b_top], bracketed", len(gal) == 48 and not gbad)
    print("  least A X(b+) / T2 W(b) per (2's type, class)")
    for wk in sorted(worst, key=lambda k: worst[k][0]):
        m, ty = worst[wk]
        print(f"  2 {wk[0]} {wk[1]:13s} {m:.6f}  config {ty}")
    tally = {}
    for key, label, bl, bc, m in bad:
        tally.setdefault((key[0], label), []).append((m, key[1], bl, bc))
    print("  failing, per (2's type, class): count, the least three")
    for wk in sorted(tally):
        rows = sorted(tally[wk])
        print(f"  2 {wk[0]} {wk[1]:13s} {len(rows)}")
        for m, ty, bl, bc in rows[:3]:
            bcs = "none" if bc is None else f"{bc:.4f}"
            print(f"      config {ty}: ratio {m:.6f}, b_low {bl:.4f}, "
                  f"b_c {bcs}")
    print("  the failing configurations' types of 3 and of 5, per 2's type")
    failed = {}                        # per configuration, its least
    for key, label, bl, bc, m in bad:
        if key not in failed or m < failed[key][0]:
            failed[key] = (m, bl, bc)
    for t2 in "sqrt":
        rows = [k[1] for k in failed if k[0] == t2]
        if rows:
            c3 = {t: sum(r[0] == t for r in rows) for t in "sqirt"}
            c5 = {t: sum(r[1] == t for r in rows) for t in "sqirt"}
            print(f"  2 {t2}: 3 {c3}; 5 {c5}")
    print("  (2, 3, 5) cells where every one of the 125 configurations "
          "fails, with the one nearest to passing")
    cells = {}
    for key, (m, bl, bc) in failed.items():
        cells.setdefault((key[0], key[1][:2]), []).append(
            (m, key[1], bl, bc))
    for c in sorted(cells):
        if len(cells[c]) == 125:
            m, ty, bl, bc = max(cells[c])
            bcs = "none" if bc is None else f"{bc:.4f}"
            print(f"  2 {c[0]}, 3 {c[1][0]}, 5 {c[1][1]}: nearest {ty}, "
                  f"ratio {m:.6f}, b_low {bl:.4f}, b_c {bcs}")
    print(f"  cells with some but not all failing: "
          f"{sum(1 for c in cells if len(cells[c]) < 125)}")
    print(f"{sum(CHECKS)} of {len(CHECKS)} checks pass")
    sys.exit(0 if all(CHECKS) else 1)


if __name__ == "__main__":
    main()