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