genus.py
Checks: PRINCIPAL: The least split place leans away from the squares observation.
Cited on: PRINCIPAL.
To run it: save genus.py, with module_law.py, principal.py beside it; standard library only.
"""genus.py -- the side of a quadratic field's least odd split place
against the squares of its class group, at 2-rank 1, as a cheap model of
the complex cubic field's least partial place.
QUESTION. At 2-rank 1 a complex cubic field's least odd partially split
place lies outside 2Cl at about 0.82 where the limit share is 1/2
(seat.py). Over a quadratic field of 2-rank 1, d = d1·d2 with d1, d2
prime discriminants, the side of a split place is one Kronecker symbol,
so the same question costs seconds: does the least odd split place lie
outside 2Cl (2Cl+ for d > 0, the narrow group) far more often than half,
and is that the imaginary field's floor (a principal split prime has norm
at least |d|/4) or a small conductor acting on the first primes, which a
real field, with no floor, would share?
THE ARGUMENT (written before the engine).
(1) GENUS. 2-rank 1 is t = 2 prime discriminants. At an odd split
q, d1 and d2 agree, and the place lies in 2Cl (2Cl+) iff
chi_{d1}(q) = +1 (Gauss's principal genus theorem).
(2) THE FLOOR. Over an imaginary field with h = 2, 2Cl is the trivial
class, so a split q < |d|/4 lies outside it. At h > 2 the floor
removes only the trivial class of the h/2 in 2Cl: a class uniform
over the h - 1 others is outside at F(h) = (h/2)/(h - 1).
(3) THE COIN MODEL. Hold the factor of smaller absolute value, d1,
at its values and let chi_{d2} be a fair coin at each odd prime
prime to d: q is the first prime where they agree, at the j-th
such prime with chance 2^-j, so the side is outside with chance
P_C(d) = sum_j [chi_{d1}(l_j) = -1] 2^-j. Its band mean is what a
small conductor's fixed values give, with no floor.
(4) THE CUBIC'S SIDE AS A COEFFICIENT. By Heilbronn's correspondence
(the unramified quadratic extensions of a cubic field K are the
quartic fields F with resolvent K and d_F = d_K), at 2-rank 1 a
partial place lies outside 2Cl iff its prime is inert in F (a
4-cycle; complex conjugation, a transposition, must lie in the
subgroup fixing the extension). Both arms read "outside" as a
negative coefficient of the unramified extension's L-function at
the base field's first eligible prime: degree 3 and conductor
|d_K| in the cubic, degree 2 and conductor |d| here. Matching
|d| = |d_K|^(2/3) matches the conductor per degree, a TRANSPLANT
(gamma factors and coefficient sets differ).
THE ENGINE. principal.py's Group (composition, reduction, the narrow
group for d > 0) for the controls and h; the Kronecker symbol for the
side. Bands of |d|: (0, 10^3], (10^3, 10^4], (10^4, 10^5] read whole,
(10^5, 10^6], (10^6, 10^7] by DRAW fields drawn at random (seed 1654).
The weld windows: |d| <= 832 (A, the cubic's |d_K| <= 24000) and
832 < |d| <= 1321 (B, to 48000), each field weighted by |d|^(1/2), the
image of a population uniform in |d_K|.
PREDICTIONS, frozen before the engine.
K1 control: chi_{d1}(q) = +1 iff Group's class of the place is a
square, at every field of 2-rank 1 to |d| <= 4000, both signs.
K2 positive control: every imaginary field with h = 2 and q < |d|/4
reads q outside 2Cl.
Q1 imaginary share above its coin mean by more than 3 se in
(0, 10^3], the excess falling band by band, within 3 se of 0 by
(10^5, 10^6].
Q2 the imaginary coin mean within 0.03 of 1/2 in (10^6, 10^7].
Q3 real excess over its coin mean within 3 of its own se in every
band.
WELD KILL. An arm holds the weld when |S - 0.837| <= 3 sqrt(se^2 +
0.014^2) on A and |S - 0.800| <= 3 sqrt(se^2 + 0.014^2) on B; it
fails if either misses. Predicted: fails on both arms, imaginary
near 0.70, real near 0.65.
ADDED AFTER THE FIRST RUN, prints and not verdicts: the share by the
smaller factor's conductor |d1| (the coin model says it carries the
excess), and the imaginary share by h mod 4 against the floor read
through Cl[2] = {e, c2}: a split q with 4 q r < |d|, R a ramified prime
in c2 and r its norm, cannot lie in c2 = [R] (Q R principal has norm q r), so a class
uniform over the h - 2 left is outside 2Cl at 1/2 when 4 does not
divide h, where c2 is outside 2Cl, and at (h/2)/(h - 2) when 4 | h,
where e and c2 are both squares; the count of fields with Q in Cl[2] is
printed.
FINDINGS.
F0 CONTROLS (K1, K2 held). The side by chi_{d1}(q) agrees with
Group's squares at all 1140 fields of 2-rank 1 to |d| <= 4000,
both signs. The 18 imaginary fields with h = 2 (largest |d| 427)
put q outside 2Cl at all 17 with q < |d|/4; the one left, d = -15,
has q = 17.
F1 BY BAND (Q1 FAILED, Q2 FAILED, Q3 HELD). Share outside 2Cl, coin
mean, excess over the coin (se):
band imaginary real (narrow)
(0, 10^3] 0.7600 0.6803 +0.0797 (0.0313) 0.7143 0.6793 +0.0350 (0.0326)
(10^3, 10^4] 0.6653 0.6500 +0.0153 (0.0122) 0.6647 0.6488 +0.0159 (0.0122)
(10^4, 10^5] 0.6364 0.6323 +0.0041 (0.0041) 0.6383 0.6318 +0.0066 (0.0041)
(10^5, 10^6] 0.6234 0.6192 +0.0042 (0.0030) 0.6180 0.6169 +0.0011 (0.0030)
(10^6, 10^7] 0.6151 0.6086 +0.0066 (0.0030) 0.6117 0.6089 +0.0028 (0.0030)
The imaginary excess is 2.5 se at the first band, not 3, and it
does not fall band by band (0.0041, then 0.0042) (Q1); the
coin mean is 0.6086 at the top band, not near 1/2 (Q2): the
smaller of two prime discriminants stays small as |d| grows. From
10^3 on the two signs agree within 0.006 in every band, so the
band shares need no floor. The coin leaves a residual: positive in
all ten rows, 0.4 to 2.5 se each.
F2 THE SMALLER FACTOR CARRIES IT. Pooled over every band:
|d1| imaginary real
<= 8 0.7282 (25180) 0.7251 (25239)
9 to 24 0.5090 (7704) 0.5112 (7745)
25 to 60 0.5668 0.5554
61 to 200 0.5318 0.5296
201 to 1000 0.5219 0.5065
> 1000 0.4612 (1340) 0.4941 (1277)
the coin mean within 0.021 of each (the largest 1.9 of the share's own
se, by hand, the imaginary 201 to 1000 cell). Most of the share above 1/2 is the six characters of
conductor at most 8 (-3, -4, 5, -7, 8, -8) at the first odd
primes: chi(3) = -1 at -4, 5, -7 and 8.
F3 THE FLOOR. Q lies in Cl[2] at 0 of the 1336 imaginary fields with
h > 2, |d| <= 10^4. The share reads 0.6349 +- 0.0183 at h = 2
mod 4 (the floor through Cl[2] says 0.5000, the coin 0.6479) and
0.7107 +- 0.0179 at 4 | h (floor 0.5784, coin 0.6585). The h = 4
fields read 30 of 30 outside, where 2Cl = {e, c2} is all floored.
So below 10^4 the imaginary share sits 0.052 above the coin at
4 | h (2.9 of the share's own se, by hand) and 0.013 below it at h = 2 mod 4: a gap of 0.076
the floor model puts at 0.078 and the coin at 0.011. The floor (or
whatever else 4 | h selects) moves the imaginary share between
fields, while from 10^3 on each band matches the real arm's (F1).
F4 THE WELD HOLDS at its frozen bar on both arms: imaginary
A 0.7603 +- 0.0398 (129 fields; |S - 0.837| 0.0767, bar 0.1266),
B 0.6920 +- 0.0549 (71; 0.1080, bar 0.1700); real A 0.7343 +-
0.0410 (130; 0.1027, bar 0.1301), B 0.6552 +- 0.0586 (66; 0.1448,
bar 0.1808). The bar is wide, the windows holding about 200
fields, 156 of 200 imaginary and 157 of 196 real with |d1| <= 8:
those read 0.8077 +- 0.0317 and 0.7516 +- 0.0346, the rest
0.5455 +- 0.0759 (44) and 0.5385 +- 0.0809 (39) (the window
prints, unweighted). So the hold is the small factor's and
attributes nothing to the cubic, and the frozen reading for a hold on both arms (root
conductor without the floor) is not taken: the bar was frozen on
an arm the small factor inflates.
F5 printed, not judged, |d| <= 1321: by position 1 to 4, imaginary
0.750, 0.610, 0.680, 0.560 (200 fields), real 0.709, 0.612,
0.597, 0.597 (196); at position 1 by q = 3, 5, 7, 11, imaginary
0.768, 0.808, 0.656, 0.706 (82, 52, 32, 17 fields).
TIERS. K1 is known (genus theory). The Cl[2] exclusion in F3 is
measured (0 of 1336 through Group); the norm argument covers a field
where 4 q r < |d| for a ramified R in c2, a condition not printed. F1
to F5 are observations over the bands as drawn.
RUN RECORD. 2/2 controls, 5.5 s, 137.0 MB peak commit under a memory
guard (512 MB). The first run printed F0, F1, F4, F5; the F2 and F3 prints
were added after it, and the window prints in F4 after a check, each
rerun reproducing every earlier line; so did Q1's in-se print, added
later (2.55, 1.25, 0.99, 1.41, 2.19), 5.6 s.
"""
import os
import random
import time
from math import sqrt
os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
import principal as P # noqa: E402
RESULTS = []
BANDS = [(0, 10 ** 3), (10 ** 3, 10 ** 4), (10 ** 4, 10 ** 5),
(10 ** 5, 10 ** 6), (10 ** 6, 10 ** 7)]
WHOLE = 10 ** 5 # bands below this are read whole
DRAW = 20000 # fields drawn per band above it
GROUP_D = 4000 # K1 reads Group to here, both signs
H_D = 10 ** 4 # imaginary h read to here (floor model, K2)
A_TOP = round(24000 ** (2 / 3)) # 832
B_TOP = round(48000 ** (2 / 3)) # 1321
CUBIC = {"A": (0.837, 0.014), "B": (0.800, 0.014)}
ODD = P.PRIMES[1:]
def check(name, ok, detail=""):
RESULTS.append(bool(ok))
print(f" [{'PASS' if ok else 'FAIL'}] {name}"
+ (f" -- {detail}" if detail else ""))
def prime_discs(d):
"""The prime discriminants of a fundamental d, or None if d is not
fundamental."""
if not P.fundamental(d):
return None
n, out = abs(d), []
while n % 2 == 0:
n //= 2
for p in ODD:
if p * p > n:
break
if n % p == 0:
out.append(p if p % 4 == 1 else -p)
n //= p
if n > 1:
out.append(n if n % 4 == 1 else -n)
rest = d
for x in out:
rest //= x
if rest != 1:
out.append(rest) # -4, 8 or -8
return out
class Field:
"""One field of 2-rank 1: d1 the factor of smaller absolute value,
the sides at the first four odd split primes, the coin model."""
def __init__(self, d, pd):
self.d = d
self.d1 = min(pd, key=abs)
self.qs, self.sides = [], []
pc, j = 0.0, 0
for ell in ODD:
if d % ell == 0:
continue
j += 1
c1 = P.kron(self.d1, ell)
if j <= 50 and c1 == -1:
pc += 2.0 ** -j
if P.kron(d, ell) == 1:
self.qs.append(ell)
self.sides.append(c1 == -1)
if len(self.qs) >= 4 and j >= 50:
break
self.pc = pc
self.q = self.qs[0]
self.out = self.sides[0]
self.two = ("ramified" if d % 2 == 0 else
"split" if d % 8 == 1 else "inert")
def fields_whole(sign, top):
out = []
for n in range(3, top + 1):
d = sign * n
pd = prime_discs(d)
if pd is not None and len(pd) == 2:
out.append(Field(d, pd))
return out
def fields_drawn(sign, lo, hi, k, rng):
out, seen = [], set()
while len(out) < k:
n = rng.randint(lo + 1, hi)
if n in seen:
continue
seen.add(n)
pd = prime_discs(sign * n)
if pd is not None and len(pd) == 2:
out.append(Field(sign * n, pd))
return out
def mean_se(xs):
n = len(xs)
m = sum(xs) / n
v = sum((x - m) ** 2 for x in xs) / (n - 1) if n > 1 else 0.0
return m, sqrt(v / n)
def weighted(fs):
w = [sqrt(abs(f.d)) for f in fs]
sw = sum(w)
s = sum(wi * f.out for wi, f in zip(w, fs)) / sw
se = sqrt(s * (1 - s) * sum(wi * wi for wi in w)) / sw
return s, se
def section_controls(imag, real):
print("\n== CONTROLS")
off, n = 0, 0
for fs in (imag, real):
for f in fs:
if abs(f.d) > GROUP_D:
continue
G = P.Group(f.d)
sq = G.place(f.q) in G.squares
off += sq != (not f.out)
n += 1
check("K1 chi_{d1}(q) = +1 iff the place's class is a square",
off == 0, f"{off} off over {n} fields, both signs, |d| <= {GROUP_D}")
h2, cov, bad = [], 0, 0
for f in imag:
if getattr(f, "h", None) == 2:
h2.append(f.d)
if f.q < abs(f.d) / 4:
cov += 1
bad += not f.out
check("K2 h = 2 and q < |d|/4: outside 2Cl at every one",
bad == 0 and cov > 0,
f"{len(h2)} fields with h = 2, {cov} with q < |d|/4, {bad} inside;"
f" largest |d| {max(abs(x) for x in h2)}")
print(f" h = 2 fields: {sorted(h2, reverse=True)}")
print(f" their q: {[f.q for f in imag if getattr(f, 'h', None) == 2]}")
def band_table(name, fs, with_h):
print(f"\n== {name}: the least odd split place, outside 2Cl, by band")
print(" band fields share se coin excess se"
" q>=|d|/4" + (" floor F(h)" if with_h else ""))
rows = []
for lo, hi in BANDS:
b = [f for f in fs if lo < abs(f.d) <= hi]
s, se = mean_se([float(f.out) for f in b])
c = sum(f.pc for f in b) / len(b)
ex, exse = mean_se([f.out - f.pc for f in b])
cov = sum(f.q >= abs(f.d) / 4 for f in b) / len(b)
line = (f" ({lo:>8}, {hi:>8}] {len(b):>6} {s:.4f} {se:.4f} "
f"{c:.4f} {ex:+.4f} {exse:.4f} {cov:.4f}")
if with_h and all(hasattr(f, "h") for f in b):
fl = sum((f.h / 2) / (f.h - 1) for f in b) / len(b)
line += f" {fl:.4f}"
print(line)
rows.append((lo, hi, len(b), s, se, c, ex, exse))
return rows
def detail(name, fs):
print(f"\n== {name}, |d| <= {B_TOP}: positions, q, how 2 sits")
for pos in range(4):
xs = [float(f.sides[pos]) for f in fs]
s, se = mean_se(xs)
print(f" position {pos + 1}: {s:.4f} +- {se:.4f} ({len(xs)})")
for q in (3, 5, 7, 11):
xs = [float(f.out) for f in fs if f.q == q]
if len(xs) >= 10:
s, se = mean_se(xs)
print(f" position 1 at q = {q}: {s:.4f} +- {se:.4f} ({len(xs)})")
for t in ("ramified", "split", "inert"):
xs = [float(f.out) for f in fs if f.two == t]
s, se = mean_se(xs)
print(f" 2 {t}: {s:.4f} +- {se:.4f} ({len(xs)})")
def weld(name, fs):
print(f"\n== THE WELD, {name}: weighted by |d|^(1/2)")
held = True
for key, lo, hi in (("A", 0, A_TOP), ("B", A_TOP, B_TOP)):
b = [f for f in fs if lo < abs(f.d) <= hi]
s, se = weighted(b)
c, cse = CUBIC[key]
bar = 3 * sqrt(se * se + cse * cse)
ok = abs(s - c) <= bar
held &= ok
print(f" {key} ({lo}, {hi}]: {len(b)} fields, S = {s:.4f} +- "
f"{se:.4f}; cubic {c}; |S - c| = {abs(s - c):.4f}, bar "
f"{bar:.4f}: {'within' if ok else 'MISSES'}")
print(f" WELD {'HOLDS' if held else 'FAILS'} on the {name} arm")
return held
def by_h(imag):
print(f"\n== IMAGINARY by h, |d| <= {H_D}: share against F(h)")
hs = sorted({f.h for f in imag if hasattr(f, "h")})
for h in hs:
b = [f for f in imag if getattr(f, "h", None) == h]
if len(b) < 30:
continue
s, se = mean_se([float(f.out) for f in b])
c = sum(f.pc for f in b) / len(b)
print(f" h = {h:>3}: {len(b):>4} fields {s:.4f} +- {se:.4f} "
f"F(h) {(h / 2) / (h - 1):.4f} coin {c:.4f}")
def by_d1(name, fs):
print(f"\n== {name}, every band pooled: by the smaller factor |d1|")
for lo, hi in ((0, 8), (8, 24), (24, 60), (60, 200), (200, 1000),
(1000, 10 ** 8)):
b = [f for f in fs if lo < abs(f.d1) <= hi]
s, se = mean_se([float(f.out) for f in b])
c = sum(f.pc for f in b) / len(b)
print(f" |d1| in ({lo:>4}, {hi:>9}]: {len(b):>6} fields "
f"{s:.4f} +- {se:.4f} coin {c:.4f}")
def by_hmod4(imag):
b = [f for f in imag if hasattr(f, "h") and f.h > 2]
print(f"\n== IMAGINARY, h > 2, |d| <= {H_D}: the floor through Cl[2]")
print(f" Q in Cl[2] at {sum(f.c2 for f in b)} of {len(b)} fields")
for m in (2, 0):
g = [f for f in b if f.h % 4 == m]
s, se = mean_se([float(f.out) for f in g])
fl = sum(0.5 if m == 2 else (f.h / 2) / (f.h - 2) for f in g) / len(g)
c = sum(f.pc for f in g) / len(g)
print(f" h = {m} mod 4: {len(g)} fields {s:.4f} +- {se:.4f} "
f"floor {fl:.4f} coin {c:.4f}")
def main():
t0 = time.time()
rng = random.Random(1654)
arms = {}
for name, sign in (("IMAGINARY", -1), ("REAL", 1)):
fs = fields_whole(sign, WHOLE)
for lo, hi in BANDS:
if lo >= WHOLE:
fs += fields_drawn(sign, lo, hi, DRAW, rng)
arms[name] = fs
print(f"{name}: {len(fs)} fields, {time.time() - t0:.1f} s")
imag, real = arms["IMAGINARY"], arms["REAL"]
for f in imag:
if abs(f.d) <= H_D:
G = P.Group(f.d)
f.h, f.c2 = G.h, G.place(f.q) in G.two
print(f"h read, {time.time() - t0:.1f} s")
section_controls(imag, real)
if not all(RESULTS):
print("a control failed: no verdict read")
raise SystemExit(1)
ri = band_table("IMAGINARY", imag, True)
rr = band_table("REAL (narrow)", real, False)
by_h(imag)
by_hmod4(imag)
by_d1("IMAGINARY", imag)
by_d1("REAL", real)
detail("IMAGINARY", [f for f in imag if abs(f.d) <= B_TOP])
detail("REAL", [f for f in real if abs(f.d) <= B_TOP])
print("\n== PREDICTIONS")
q1 = (ri[0][6] > 3 * ri[0][7]
and all(ri[i + 1][6] < ri[i][6] for i in range(3))
and abs(ri[3][6]) <= 3 * ri[3][7])
print(f" Q1 imaginary excess over the coin: {[round(r[6], 4) for r in ri]}"
f" -> {'HELD' if q1 else 'FAILED'}")
print(f" in se: {[round(r[6] / r[7], 2) for r in ri]}")
q2 = abs(ri[4][5] - 0.5) <= 0.03
print(f" Q2 imaginary coin mean at the top band {ri[4][5]:.4f} -> "
f"{'HELD' if q2 else 'FAILED'}")
q3 = all(abs(r[6]) <= 3 * r[7] for r in rr)
print(f" Q3 real excess over the coin, in se: "
f"{[round(r[6] / r[7], 2) for r in rr]} -> "
f"{'HELD' if q3 else 'FAILED'}")
wi = weld("imaginary", imag)
wr = weld("real", real)
for name, fs in (("imaginary", imag), ("real", real)):
w = [f for f in fs if abs(f.d) <= B_TOP]
for lab, g in (("|d1| <= 8", [f for f in w if abs(f.d1) <= 8]),
("|d1| > 8", [f for f in w if abs(f.d1) > 8])):
s, se = mean_se([float(f.out) for f in g])
c = sum(f.pc for f in g) / len(g)
print(f" window |d| <= {B_TOP}, {name}, {lab}: {len(g)} fields "
f"{s:.4f} +- {se:.4f} coin {c:.4f}")
print(f"\n weld: imaginary {'holds' if wi else 'fails'}, real "
f"{'holds' if wr else 'fails'}")
print(f"\n{sum(RESULTS)}/{len(RESULTS)} controls PASS, "
f"{time.time() - t0:.1f} s")
if __name__ == "__main__":
main()