"""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 share within 3 se of its coin mean 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 (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 in se 1.9, 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 se) 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. """ 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, self.d2 = sorted(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'}") 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()