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