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