rho.py
Checks: TRIPLE: The lean's character is irreducible theorem; observation.
Cited on: TRIPLE.
To run it: save rho.py, with cubic.py, module_law.py, principal.py, seat.py beside it; needs mpmath.
"""rho.py -- the cubic's 2-torsion character read as an Artin trace.
QUESTION. Over a quadratic field of 2-rank 1 the unramified quadratic L
gives zeta_L/zeta_K = L(chi_d1) L(chi_d2), two Dirichlet factors, and the
small one's fixed signs carry the least split place's lean. Over a
complex cubic K of 2-rank 1, L/K the unramified quadratic (unramified at
K's real place too), is rho = zeta_L/zeta_K = Ind_K^Q psi reducible, or
is there no character of smaller conductor inside it?
THE DERIVATION (before code). G the closure's group, H = Gal(./K) of
index 3, G 2-transitive on K's three conjugates, so H\\G/H has two double
cosets and Mackey gives <rho, rho> = 1 + [psi = psi^s on H n sHs^-1].
H n sHs^-1 fixes N = K K', the S3 closure, and equality there means
LN = L'N. (i) L != N: N/K ramifies at K's real place (d_K < 0), L/K
does not. (ii) If LN = L'N, then also L''N by the 3-cycle, so M = LN is
Galois over Q of degree 12, Gal(M/N) = C2 central: M is C2 x S3 or Dic3,
H^2(S3, C2) being C2. Dic3 has one involution, which fixes N: L = N,
against (i). In C2 x S3 the sextics through K are N, K(sqrt f) and
K(sqrt(f d_K)), f rational: a prime p ramified in Q(sqrt f) has a place
of odd ramification index in K (sum e_i f_i = 3), and that place
ramifies in L, against L/K unramified, Q having no unramified quadratic
extension. So <rho, rho> = 1: rho is irreducible, of dimension 3 and
conductor |d_K|, and contains no Dirichlet character.
THE CHECK. a(p) = trace of Frob_p on rho at p unramified in K, from the
class engine: psi(P) = +1 for a place in 2Cl, -1 outside; a split p
gives psi(P1) + psi(P2) + psi(P3), the product +1 so a in {3, -1}; a
partial p gives psi(P1) (+-1); an inert p gives 0. In the closure
E a^2 = 1/6 x 3 + 1/2 x 1 + 1/3 x 0 = 1 for an irreducible rho, the
number of components (>= 2) for a reducible one. A split prime reads 9
or 1, so the split fiber's excess of all three in 2Cl over 1/4 moves the
mean up by 8 x (split share) x the excess: the prime-square term of the
split fiber (seat.py) inflates the cubic arm. Per reading var(a^2) is
about 1.3 pooled, so N readings give a se near 1.2/sqrt(N).
PREDICTIONS, frozen before the engine.
P1 the cubic arm (complex cubics of 2-rank 1, |d_K| <= 24000,
unramified p < 2000) reads a pooled mean a^2 within 0.85 to 1.20.
P2 the split fiber's share of all three in 2Cl is above 1/4, so P1's
reading sits above 1.
P3 the band p < 200 reads further from 1 than the band 200 to 2000.
KILL (printed): the cubic arm's pooled mean a^2 above 1.5, midway
between irreducible and the least reducible, its bar on the cubic
arm, the arm the split-fiber confound inflates.
CONTROL, read first: real and imaginary quadratic fields with exactly
two prime discriminant factors, |D| <= 24000, a = chi_d1(p) +
chi_d2(p) at p prime to D below 2000, pooled mean a^2 within 1.8 to
2.2. Its break: chi_d2 replaced by chi_d1 prints 4 at every p.
The engine's check: every split prime of the cubic arm reads 3 or
-1, never 1 or -3.
ADDED AT THE AUDIT. The null spread above was mispriced: in the limit
a^2 is 9, 1, 0 at 1/24, 15/24, 8/24, so var(a^2) = 3; the se is now
taken from the readings' own a^4, the readings as independent. The
confound's sign was wrong: seat.py's fiber table (F1) records the split
fiber's raw excess outside 2Cl, the prime-square weight landing on the
squares, so by sign that table (h >= 2, |d_K| <= 12000, cut 1000,
a per-place share) predicts P2's miss. The control's break could
not fire (kron is +-1 at p prime to D) and is gone; a known answer
through the place path replaces it: psi = 1 gives the permutation
character, 1 plus the 2-dimensional of S3, mean a^2 = 2 in the limit.
Of the three sextics through K the third is K(sqrt f) with f d_K
renamed f. Unramified at the real place is automatic: -1 has norm -1,
so Cl+ = Cl.
FINDINGS (|d_K| <= 24000, p < 2000).
F0 CONTROLS. The quadratic arm reads 1.9934 over 1863968 readings;
psi = 1 reads 1.8792 (the split share 0.153 against 1/6 holds it
below 2), a control of the place path that never reads a class;
every certified field (none skipped) placed every
place, and the 31758 split primes all read 3 or -1.
F1 KILL MISSED: the 688 cubic fields of 2-rank 1 read a pooled mean
a^2 of 0.9267 +- 0.0035 over 207269 readings, P1 HELD, 21.0 se
below 1; the quadratic arm at the same cut reads 1.9934. The
print is irreducible's, never a reducible rho's.
F2 P2 MISSED, as seat.py's fiber table predicts by sign (its own
population, h >= 2 to 12000 at cut 1000): all three split places lie in
2Cl at 0.2229, below 1/4. The split fiber is 0.1532 of the
readings (1/6 in the limit) and gives 0.1532 x (1 + 8 x 0.2229)
= 0.4265 of the mean; the rest, 0.5002, is the partial fiber's,
at its 1/2: the mean sits below 1 by the split fiber's share and
its lean.
F3 P3 HELD: p < 200 reads 0.7964 (30719), 200 to 2000 0.9493
(176550).
TIERS. The irreducibility is a theorem (the derivation above); F1 to
F3 are observations over this population.
RUN RECORD. A rehearsal at --cap 6000: 3/3, 33.1 s, 42.9 MB (120
fields, mean 0.9442, P2 missed at 0.2320). Full: 3/3, 150.9 s, peak
commit 43.6 MB under a memory guard (512 MB). At the audit (the se,
the psi = 1 control, the uncertified count): 3/3 at --cap 1500, 7.3 s;
full 3/3, 149.0 s, 43.9 MB. From code/, the split fiber's
part printed: 3/3, 149.7 s, 43.5 MB.
"""
import os
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
import sys
import time
HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import cubic as C # noqa: E402
import principal as Q # noqa: E402
from seat import two_rank, is_square # noqa: E402
from module_law import check, section, CHECKS # noqa: E402
def _flag(name, default):
if name in sys.argv:
return sys.argv[sys.argv.index(name) + 1]
return default
CAP = int(_flag("--cap", 24000))
PMAX = 2000
BAND = 200
SMALLP = [p for p in C.PRIMES if p < PMAX]
def prime_discs(D):
"""The prime discriminants whose product is the fundamental D."""
out, n = [], abs(D)
if n % 4 == 0:
m = n // 4
if m % 2 == 0:
n //= 8
out.append(None) # +-8, sign fixed below
else:
n //= 4
out.append(-4)
p = 3
while p * p <= n:
if n % p == 0:
out.append(p if p % 4 == 1 else -p)
n //= p
p += 2
if n > 1:
out.append(n if n % 4 == 1 else -n)
if None in out:
rest = 1
for d in out:
if d is not None:
rest *= d
out[out.index(None)] = 8 if D // rest > 0 else -8
return out
def quadratic_arm():
s2 = n = 0
for D in list(range(-CAP, -2)) + list(range(5, CAP + 1)):
if not Q.fundamental(D):
continue
ds = prime_discs(D)
if len(ds) != 2:
continue
d1, d2 = ds
assert d1 * d2 == D
for p in SMALLP:
if D % p == 0:
continue
a = Q.kron(d1, p) + Q.kron(d2, p)
s2 += a * a
n += 1
return s2 / n, n
def cubic_arm():
F = C.enumerate_fields(CAP)
t0 = time.time()
rd = {"lo": [0, 0], "hi": [0, 0]} # band: [sum a^2, readings]
split = [0, 0] # split primes: all three in, total
bad_split = unplaced = fields = uncert = 0
s4 = perm = 0 # sum a^4; sum a^2 at psi = 1
for key, dk in sorted(F.items(), key=lambda kv: (-kv[1], kv[0])):
K = C.Field(key, dk)
if not K.class_group(always_cert=True):
uncert += 1
continue
inv = list(K.cl)
if two_rank(inv) != 1:
continue
fields += 1
for p in SMALLP:
pls = K.places(p)
if any(pl.e != 1 for pl in pls):
continue
deg1 = [pl for pl in pls if pl.deg == 1]
cs = [K.place_class(pl) for pl in deg1]
if any(c is None for c in cs):
unplaced += 1
continue
ps = [1 if is_square(c, inv) else -1 for c in cs]
a = sum(ps)
if len(deg1) == 3:
split[1] += 1
split[0] += a == 3
bad_split += a not in (3, -1)
b = rd["lo" if p < BAND else "hi"]
b[0] += a * a
b[1] += 1
s4 += a ** 4
perm += len(deg1) ** 2
if fields % 100 == 0:
print(" %d fields, %.0f s" % (fields, time.time() - t0),
flush=True)
return fields, rd, split, bad_split, unplaced, uncert, s4, perm
def main():
t0 = time.time()
section("A THE CONTROL: THE QUADRATIC ARM")
m2, n = quadratic_arm()
check("control: quadratic mean a^2 within 1.8 to 2.2",
1.8 <= m2 <= 2.2, "%.4f over %d readings" % (m2, n))
if not all(CHECKS):
print("CONTROL FAILED: nothing below is read.")
raise SystemExit(1)
section("B THE CUBIC ARM")
fields, rd, split, bad, unpl, uncert, s4, perm = cubic_arm()
s = rd["lo"][0] + rd["hi"][0]
n = rd["lo"][1] + rd["hi"][1]
m = s / n
se = ((s4 / n - m * m) / n) ** 0.5
mp = perm / n
print(" %d fields uncertified, skipped" % uncert)
check("control: psi = 1 (the permutation character, 1 + the 2-dim"
" of S3) reads mean a^2 within 1.8 to 2.2", 1.8 <= mp <= 2.2,
"%.4f" % mp)
print(" %d complex cubic fields of 2-rank 1, |d_K| <= %d; %d readings,"
" %d unplaced [%.0f s]" % (fields, CAP, n, unpl, time.time() - t0))
check("engine: every split prime reads 3 or -1", bad == 0,
"%d of %d split primes off" % (bad, split[1]))
lo = rd["lo"][0] / rd["lo"][1]
hi = rd["hi"][0] / rd["hi"][1]
sh = split[0] / split[1]
print(" pooled mean a^2 %.4f +- %.4f (readings taken as independent;"
" %.1f se from 1)" % (m, se, (m - 1) / se))
print(" by band: p < %d %.4f (%d), %d to %d %.4f (%d)"
% (BAND, lo, rd["lo"][1], BAND, PMAX, hi, rd["hi"][1]))
print(" split fiber: all three in 2Cl at %.4f of %d (1/4 in the limit)"
% (sh, split[1]))
fs = split[1] / n
fm = fs * (1 + 8 * sh)
print(" split fiber: %.4f of the readings (1/6), its part of the mean"
" %.4f (1/2), the rest %.4f (1/2)" % (fs, fm, m - fm))
print(" P1 within 0.85 to 1.20: %s" % ("HELD" if 0.85 <= m <= 1.20
else "MISSED"))
print(" P2 split share above 1/4: %s" % ("HELD" if sh > 0.25
else "MISSED"))
print(" P3 the low band further from 1: %s"
% ("HELD" if abs(lo - 1) > abs(hi - 1) else "MISSED"))
print(" KILL (mean a^2 above 1.5): %s" % ("FIRED" if m > 1.5
else "MISSED"))
print("\n%d/%d checks PASS, %.1f s" % (sum(CHECKS), len(CHECKS),
time.time() - t0))
raise SystemExit(0 if all(CHECKS) else 1)
if __name__ == "__main__":
main()