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()