"""localtype.py -- does the local type at the small primes carry the rise of PI_1 - PI_k0 with |D|? QUESTION. Over the real fields with the class k0 (their fundamental unit of norm +1), |D| <= 48000, d = PI_1 - PI_k0, the trivial class's corrected prime-power count less k0's, rises with log D: +0.29 per unit at the cut 250 and +0.88 at 10^4 (weighted by h+/2, the odd characters). Is the rise the population's local type at the small primes shifting with |D|, or does it survive with the local type held fixed? THE DERIVATION (before code). (a) d splits exactly by the kind of p: d = d_I + d_R + d_S. An inert p gives (p), of norm p^2, principal and totally positive, so every power lies in the trivial class, weight 1/e at p^(2e) < x: at 250, w(2) = 11/6, w(3) = 3/2, w(p) = 1 at 5, 7, 11, 13, and nothing from 17, so d_I at 250 is a function of the local type at 2..13 alone. A ramified R has R^2 = (p) totally positive: its even powers go to the trivial class, its odd powers to R's class, +-1 to d only when that is 1 or k0. A split P, Pbar in c, c^-1 reach d only through powers landing in 1 or k0. (b) Unfiltered, the fundamental discriminants have 2 inert (D = 5 mod 8) at density 1/3, an odd p inert at p/(2(p + 1)) and ramified at 1/(p + 1); so the unfiltered mean of d_I at 250 is 11/18 + 9/16 + 5/12 + 7/16 + 11/24 + 13/28 = 2.950, the size of d itself. (c) Every D with a negative prime discriminant factor (-4, -8, -q, q = 3 mod 4) has k0. At small |D| the factors are small, so the filter enriches the ramification of 2, 3, 7, 11 and lowers d_I; as |D| grows a large factor can carry the sign and the enrichment relaxes. The guess: the filtered d_I rises and carries the slope. (d) The weight h+/2 grows with the 2-rank: both weightings print. (e) The within slope s_w: weighted OLS of y on log D with an intercept per stratum, the stratum (D/p) at p = 2, 3, 5, 7, 11, 13. Its denominator, the within variance of log D, prints beside the total; its se is HC0 per field. It is linear in y, so it splits over the parts, and s_w(d_I) = 0 exactly at 250. PREDICTIONS, frozen before the engine. P1 the parts sum to the counts' d per field to 1e-12 at every cut. P2 s(d_I) >= 0.5 s(d) at 250, character-weighted. P3 the share of fields with 3 ramified falls from the first |D| band to the last, filtered; unfiltered it sits within 0.03 of 1/4 in every band. P4 the unfiltered band means of d_I sit within 0.15 of 2.950; the filtered first band's below 2.80. KILL LOCAL (printed: "within-stratum slope of d at 250"), character- weighted: fires if s_w >= 0.5 s, the local type then not carrying the rise. At 10^4 the strata do not fix d_I: printed, no verdict. C1 read first: s(d) at 250 and 10^4 within 0.005 of +0.29 and +0.88, and P1. C2 read before the kill: s_w(d_I) at 250 is 0 to 1e-12; s_w(d_I + c log D), c = s(d), reads c to 1e-9; s_w(d) with the strata shuffled across fields (one seed) within 3 se of the pooled s. ARMS: filtered (the fields with k0) and unfiltered (every fundamental D to 48000, the local type only), the filter the ingredient removed. RESOLVED BEFORE THE RUN, nothing read: P3's shares and P4's means read field-weighted on both arms, like for like; the kill and P2 stay character-weighted, as frozen. ADDED BEFORE THE RUN: a check that the inert part at 250 is (a)'s hand function of the local type, touching no kill. FINDINGS, the 10323 real fields with k0 to |D| <= 48000 (32585 odd characters); unfiltered, every fundamental D to 48000. F0 CONTROLS. P1 held: the parts sum to d to 1.14e-13, and the inert part at 250 is the hand function of the local type exactly. C1: s(d) +0.2899 at 250 and +0.8788 at 10^4, parity.py's. C2: s_w of the inert part 5.5e-33; plus c log D it reads c to the printed digit; with the strata shuffled +0.3074 +- 0.0409 against +0.2899. 685 strata, 625 of two or more fields holding 10263; the within sum of squares of log D 20526.8 of the total 22370.5. F1 KILL LOCAL FIRED: within the strata of fixed local type at 2..13 the slope of d at 250 reads +0.2740 +- 0.0410 of the pooled +0.2899 (field-weighted +0.3993 +- 0.0461). The local type at the small primes does not carry the rise. F2 P2 MISSED, the hand guess wrong: the split primes carry it. At 250 s(d) = inert +0.0243 +- 0.0190 + ramified +0.0076 +- 0.0164 + split +0.2581 +- 0.0392 (within the strata: +0.0000, +0.0123 +- 0.0133, +0.2617 +- 0.0403); at 10^4 +0.8788 = +0.0541 + 0.0169 + 0.8078 +- 0.1804 (within: +0.9399 +- 0.1713, split +0.8862). Field-weighted at 250, s(d) +0.4101 = inert +0.0219 + ramified -0.0065 + split +0.3947. F3 P3 and P4 HELD, the confound real but flat: the share with 3 ramified reads 0.383, 0.364, 0.355, 0.351, 0.347 by band (to 4000, 8000, 16000, 32000, 48000) filtered, 0.252 to 0.250 unfiltered; the inert part at 250 2.672, 2.729, 2.741, 2.743, 2.744 filtered against 2.962, 2.959, 2.945, 2.948, 2.954 unfiltered (2.950 by hand). d at 250, field-weighted, +2.140, +3.269, +3.308, +3.474, +3.543: most of the rise is from the first band to the second. TIERS. (a) a property, (b) known densities; F1 to F3 observations over this population. Why the split part rises is not derived. RUN RECORD. A rehearsal at --d 3000 (C1 fails there by design, its references being the full population's). Full: 7/7, 41.5 s; peak commit 55.8 MB under a memory guard (512 MB) at the first full run. P2, P3, P4 and the kill stay prints, since a verdict is read, never asserted, a missed one is the finding, and a check before the controls' gate would block the kill. """ import os os.environ.setdefault("OPENBLAS_NUM_THREADS", "1") import random import sys import time from math import log HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import numpy as np # noqa: E402 import principal as P # noqa: E402 from module_law import check, section, CHECKS # noqa: E402 D_MAX = 48000 if "--d" in sys.argv: D_MAX = int(sys.argv[sys.argv.index("--d") + 1]) STRATA_P = (2, 3, 5, 7, 11, 13) BANDS = ((0, 4000), (4000, 8000), (8000, 16000), (16000, 32000), (32000, 10 ** 9)) J250, J10K = 0, len(P.CUTS) - 1 D_I_UNFILTERED = 11 / 18 + 9 / 16 + 5 / 12 + 7 / 16 + 11 / 24 + 13 / 28 def parts(G): """Per cut, d's inert, ramified and split parts (cumulative).""" nc = len(P.CUTS) out = np.zeros((3, nc)) # rows: I, R, S def add(kind, n, c, wt): j = P.bucket(n) if j is None: return if c == G.e: out[kind, j:] += wt elif c == G.k0: out[kind, j:] -= wt for p in P.SMALL: k = P.kron(G.D, p) if k == 1: c = G.place(p) cb = G.inv(c) e, pe = 1, p while pe < P.TOP: add(2, pe, G.power(c, e), 1 / e) add(2, pe, G.power(cb, e), 1 / e) e, pe = e + 1, pe * p elif k == -1: e, pe = 1, p * p while pe < P.TOP: add(0, pe, G.e, 1 / e) e, pe = e + 1, pe * p * p else: r = G.place(p) e, pe = 1, p while pe < P.TOP: add(1, pe, G.power(r, e), 1 / e) e, pe = e + 1, pe * p return out def stratum(D): return tuple(P.kron(D, p) for p in STRATA_P) def wslope(x, y, w): """Weighted OLS slope of y on x and its field-level (HC0) se.""" xm = np.sum(w * x) / np.sum(w) ym = np.sum(w * y) / np.sum(w) sxx = np.sum(w * (x - xm) ** 2) s = np.sum(w * (x - xm) * (y - ym)) / sxx e = y - ym - s * (x - xm) return s, np.sqrt(np.sum((w * (x - xm) * e) ** 2)) / sxx def within(x, y, w, g): """The within-stratum slope, its se, and the within and total sums of squares of x (weighted).""" xd, yd = np.empty_like(x), np.empty_like(y) for key in set(g): m = np.array([gi == key for gi in g]) xd[m] = x[m] - np.sum(w[m] * x[m]) / np.sum(w[m]) yd[m] = y[m] - np.sum(w[m] * y[m]) / np.sum(w[m]) sxx = np.sum(w * xd ** 2) s = np.sum(w * xd * yd) / sxx e = yd - s * xd se = np.sqrt(np.sum((w * xd * e) ** 2)) / sxx tot = np.sum(w * (x - np.sum(w * x) / np.sum(w)) ** 2) return s, se, sxx, tot def read_filtered(): rows, worst = [], 0.0 for D in range(5, D_MAX + 1): if not P.fundamental(D): continue G = P.Group(D) if G.h < 2 or G.k0 == G.e: continue _, cor = P.counts(G) cor = np.array(cor) d = cor[:, G.e] - cor[:, G.k0] pt = parts(G) worst = max(worst, float(np.max(np.abs(pt.sum(axis=0) - d)))) rows.append((D, G.h // 2, d, pt)) return rows, worst def band_of(D): for i, (lo, hi) in enumerate(BANDS): if lo < D <= hi: return i def d_inert_250(D): wt = {2: 11 / 6, 3: 3 / 2, 5: 1, 7: 1, 11: 1, 13: 1} return sum(v for p, v in wt.items() if P.kron(D, p) == -1) def main(): t0 = time.time() section("A THE FILTERED POPULATION AND THE SPLIT") rows, worst = read_filtered() n = len(rows) Ds = np.array([r[0] for r in rows]) x = np.log(Ds.astype(float)) wc = np.array([r[1] for r in rows], float) wf = np.ones(n) g = [stratum(int(D)) for D in Ds] print(f" {n} real fields with k0, |D| <= {D_MAX}, " f"{int(wc.sum())} odd characters [{time.time() - t0:.0f} s]") check("P1 the parts sum to d per field at every cut", worst < 1e-12, f"worst {worst:.2e}") dI250 = np.array([r[3][0, J250] for r in rows]) hand = np.array([d_inert_250(int(D)) for D in Ds]) check("the inert part at 250 is the hand function of the local type", np.max(np.abs(dI250 - hand)) < 1e-12, f"worst {np.max(np.abs(dI250 - hand)):.2e}") pooled = {} for j, ref in ((J250, 0.29), (J10K, 0.88)): d = np.array([r[2][j] for r in rows]) s, se = wslope(x, d, wc) pooled[j] = (s, se) ps = [wslope(x, np.array([r[3][k, j] for r in rows]), wc) for k in range(3)] print(f" cut {P.CUTS[j]:5d}, character-weighted: s(d) {s:+.4f}" f" +- {se:.4f} = inert {ps[0][0]:+.4f} +- {ps[0][1]:.4f}" f" + ramified {ps[1][0]:+.4f} +- {ps[1][1]:.4f}" f" + split {ps[2][0]:+.4f} +- {ps[2][1]:.4f}") sf = [wslope(x, np.array([r[3][k, j] for r in rows]), wf)[0] for k in range(3)] print(f" field-weighted: s(d) " f"{wslope(x, d, wf)[0]:+.4f} = inert {sf[0]:+.4f}" f" + ramified {sf[1]:+.4f} + split {sf[2]:+.4f}") check(f"C1 s(d) at {P.CUTS[j]} within 0.005 of parity.py's " f"{ref:+.2f}", abs(s - ref) <= 0.005, f"{s:+.4f}") if j == J250: p2 = ps[0][0] >= 0.5 * s print(f" P2 the inert part carries at least half at 250: " f"{'HELD' if p2 else 'MISSED'} ({ps[0][0]:+.4f} of" f" {s:+.4f})") section("B THE SHARES AND MEANS BY |D| BAND, FIELD-WEIGHTED") un = [D for D in range(5, D_MAX + 1) if P.fundamental(D)] ub = [band_of(D) for D in un] fb = [band_of(int(D)) for D in Ds] print(" band filtered: n, 3 ramified, d_I(250), d(250)" " | unfiltered: n, 3 ramified, d_I(250)") sh_f, sh_u, mi_u, mi_f = [], [], [], [] for i, (lo, hi) in enumerate(BANDS): fi = [k for k in range(n) if fb[k] == i] ui = [D for D, b in zip(un, ub) if b == i] r3f = np.mean([Ds[k] % 3 == 0 for k in fi]) r3u = np.mean([D % 3 == 0 for D in ui]) dif = np.mean(dI250[fi]) df = np.mean([rows[k][2][J250] for k in fi]) diu = np.mean([d_inert_250(D) for D in ui]) sh_f.append(r3f) sh_u.append(r3u) mi_u.append(diu) mi_f.append(dif) print(f" {lo:5d}-{min(hi, D_MAX):5d} {len(fi):5d} {r3f:.3f}" f" {dif:.3f} {df:+.3f} | {len(ui):5d} {r3u:.3f}" f" {diu:.3f}") p3 = sh_f[-1] < sh_f[0] and all(abs(v - 0.25) <= 0.03 for v in sh_u) print(f" P3 the filtered 3-ramified share falls, the unfiltered near" f" 1/4: {'HELD' if p3 else 'MISSED'}") p4 = (all(abs(v - D_I_UNFILTERED) <= 0.15 for v in mi_u) and mi_f[0] < 2.80) print(f" P4 the unfiltered d_I within 0.15 of {D_I_UNFILTERED:.3f}," f" the filtered first band below 2.80: " f"{'HELD' if p4 else 'MISSED'}") section("C THE WITHIN-STRATUM SLOPE") sizes = {} for key in g: sizes[key] = sizes.get(key, 0) + 1 multi = sum(1 for v in sizes.values() if v >= 2) inmulti = sum(v for v in sizes.values() if v >= 2) print(f" {len(sizes)} strata, {multi} with two or more fields," f" holding {inmulti} of {n}") s250, se250 = pooled[J250] z0 = within(x, dI250, wc, g) zc = within(x, dI250 + s250 * x, wc, g) rnd = random.Random(1) gs = list(g) rnd.shuffle(gs) d250 = np.array([r[2][J250] for r in rows]) zs = within(x, d250, wc, gs) print(f" within/total sum of squares of log D: {z0[2]:.1f} / " f"{z0[3]:.1f}") check("C2 s_w of the inert part at 250 is 0", abs(z0[0]) < 1e-12, f"{z0[0]:.2e}") check("C2 s_w of the inert part plus c log D reads c", abs(zc[0] - s250) < 1e-9, f"{zc[0]:+.6f} against {s250:+.6f}") check("C2 s_w with the strata shuffled sits within 3 se of s", abs(zs[0] - s250) <= 3 * zs[1], f"{zs[0]:+.4f} +- {zs[1]:.4f} against {s250:+.4f}") if not all(CHECKS): print("CONTROL FAILED: the kill is not read.") raise SystemExit(1) for j in (J250, J10K): d = np.array([r[2][j] for r in rows]) sw = within(x, d, wc, g) pr = [within(x, np.array([r[3][k, j] for r in rows]), wc, g) for k in range(3)] print(f" within-stratum slope of d at {P.CUTS[j]}: {sw[0]:+.4f}" f" +- {sw[1]:.4f} of pooled {pooled[j][0]:+.4f}; inert" f" {pr[0][0]:+.4f}, ramified {pr[1][0]:+.4f} +-" f" {pr[1][1]:.4f}, split {pr[2][0]:+.4f} +- {pr[2][1]:.4f}") swf = within(x, d, wf, g) print(f" field-weighted: {swf[0]:+.4f} +- {swf[1]:.4f}") if j == J250: fired = sw[0] >= 0.5 * pooled[j][0] print(f" KILL LOCAL (s_w >= half the pooled): " f"{'FIRED' if fired else 'MISSED'}") print(f"\n{sum(CHECKS)}/{len(CHECKS)} checks PASS," f" {time.time() - t0:.1f} s") raise SystemExit(0 if all(CHECKS) else 1) if __name__ == "__main__": main()