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