The ledger file py/master.projective.py of the master's block of the projective theme, rendered with an anchor per line: the marked lines (# 00:B5 (p00015): a row and its accession key) are the checks that decide the rows; the rows' theorems are the keys of Keys.Projective. ▶ run the rows on Google Colab.
1"""master/projective — a ledger file of the master's block C, the Subject: the rows the projective theme proves (ledger2migration, task LM25, 6 October 2026; C22 and C27 bound 20 October 2026).34Block C is bound by one file per theme whose key file proves its rows (subject.py, fourier.py, extension.py, frame.py and5this file). This file binds C19, the Hopf section of PGL₂(F_p); C22, the frame variety and its two fibrations (38-s13's6A7, A9, X4 reproduced through the theme, and PGL₂ ≅ SO₃ by the adjoint action); and C27, the frame group's one form7(SU₂(𝔽_{p²}/𝔽_p) = SL₂(𝔽_p), every element of finite order, the sphere and ad − bc = 1 one quadric through the8quarter-turn): the table of keys (generated from docs/00-ledger.json by ci/make_ledgers.py), the Lean proofs each key9conjoins (PROOFS), the shells, and the checks, which call the projective theme (frc/projective.py). The Lean side is10lean/FrcCore/Theme/Projective.lean and lean/FrcCore/Theme/Spin.lean, the key file lean/FrcCore/Keys/Projective.lean and11the certificate lean/FrcCore/Ledgers/Master/Projective.lean.1213 python3 -m frc.ledgers.master.projective every block, the census; exit 1 if a check fails (≈ 25 s)1415Blocks: H the Hopf section of PGL₂ EXACT (00:C19)16 V the frame variety: the counts, the spin cover, the triple, the two fibrations, SO₃ EXACT (00:C22)17 U the frame group has one form: SU₂ = SL₂, the census, the quadric identity EXACT (00:C27)1819The notebook frc/ledgers/master/projective.ipynb runs one row per cell, the cell's id its key (gate G13).20"""21import random22import sys2324from frc import arith25from frc.registry import Registry26from frc.foundation import drive27from frc.projective import mul, det, act, normal, pclass, line, boost, borel, pgl2, factor28from frc import projective as PJ2930# ---- generated by ci/make_ledgers.py from docs/00-ledger.json; edit the ledger, not this table ----31BLOCK = "C"32KEYS = {"C1": "p00021", "C2": "p00022", "C3": "p00150", "C4": "p00024", "C5": "p00025", "C6": "p00171", "C7": "p00189", "C8": "p00028", "C9": "p00029", "C10": "p00030", "C11": "p22028", "C12": "p00032", "C14": "p00034", "C16": "p00151", "C19": "p00190", "C20": "p00040", "C21": "p00041", "C22": "p00152", "C23": "p00182", "C24": "p13064", "C25": "p00139", "C26": "p00174", "C27": "p00183"} # every row of the block: its label and its accession key33LEAN = {"C19": "core", "C22": "core", "C27": "core"} # the rows with a Lean declaration: the libraries whose key files prove them34# ---- end of the generated table ----3536PROOFS = {37 "C19": ["FRC.Shell.Frame.hopf_section", "FRC.Shell.Frame.hopf_counts"],38 "C22": ["FRC.Shell.Frame.frame_triple", "FRC.Shell.Frame.unit_split", "FRC.Shell.Frame.hopf_section", "FRC.Shell.Frame.hopf_counts",39 "FRC.Shell.Frame.sphere_det", "FRC.Spin.sphere_bijective", "FRC.Shell.Frame.frame_counts", # 38:A7, A9, X4's theorems (the bijection in the projective theme's statement)40 "FRC.Spin.ad_preserves", "FRC.Spin.ad_kernel"], # PGL₂ → SO₃: orthogonal, injective41 "C27": ["FRC.Spin.hermitian_wJ", "FRC.Spin.su2_eq_sl2", "FRC.Spin.finite_order",42 "FRC.Shell.Frame.sphere_det", "FRC.Spin.sphere_bijective"], # the quadric clause43}4445LEAN_ONLY = () # the rows with a Lean declaration and no python check (gate G12): none4647SHELLS = [5, 13, 17, 29] # exhaustive over PGL₂(F_p); the Subject 13 decides48SAMPLE = 173 # the Subject 173: the factorization on random matrices4950LEDGER = {"H1": "00:C19", "H2": "00:C19", "H3": "00:C19", "V1": "00:C22", "V2": "00:C22", "V3": "00:C22", "U1": "00:C27", "U2": "00:C27"}51BLOCKS = {"H": "the Hopf section of PGL₂",52 "V": "the frame variety: the counts, the spin cover, the triple, the two fibrations, SO₃",53 "U": "the frame group has one form: SU₂ = SL₂, the census, the quadric identity"}54PREDICATES = {"00:C19": "H1", "00:C22": "V1", "00:C27": "U1"}5556R = Registry("00", "projective", LEDGER, PREDICATES, sources=[__file__])575859def _hopf(p, nu):60 """The clauses of C19 on PGL₂(F_p), by search: the boosts fix no point, meet the Borel trivially, and the products61 (Borel class) × (boost class) are every class exactly once, p(p − 1) × (p + 1) = p(p² − 1); the factorization formula."""62 G = pgl2(p)63 P1 = line(p)64 T = sorted({pclass(boost(nu, a, b, p), p) for a in range(p) for b in range(p) if (a or b)})65 B = sorted({pclass(borel(a, b, 1, p), p) for a in range(1, p) for b in range(p)})66 I = (1, 0, 0, 1)67 ok = len(G) == p * (p * p - 1) and len(T) == p + 1 and len(B) == p * (p - 1)68 ok &= all(normal(act(t, P, p), p) != normal(P, p) for t in T if t != I for P in P1) # (1) no fixed point69 ok &= set(T) & set(B) == {I} # (2) T ∩ B = 170 prods = [pclass(mul(b, t, p), p) for b in B for t in T]71 ok &= len(set(prods)) == len(G) and set(prods) == set(G) # (3), (4) a bijection72 for M in G: # the Lean proof's formula73 N, (a, b, e), (al, be) = factor(M, nu, p)74 ok &= N != 0 and a * e % p != 0 and tuple(N * v % p for v in M) == mul(borel(a, b, e, p), boost(nu, al, be, p), p)75 return ok767778def _spin(p, nu):79 """On SL₂(F_p): −I is a boost and a chart, B ∩ T = {±I}, and the products B_SL · T_SL cover half of PSL₂."""80 SL = [(a, b, c, d) for a in range(p) for b in range(p) for c in range(p) for d in range(p) if (a * d - b * c) % p == 1]81 Tsl = [boost(nu, a, b, p) for a in range(p) for b in range(p) if (a * a - nu * b * b) % p == 1]82 Bsl = [borel(a, b, pow(a, -1, p), p) for a in range(1, p) for b in range(p)]83 neg = lambda M: tuple((-v) % p for v in M)84 I = (1, 0, 0, 1); mI = neg(I)85 ok = len(SL) == p * (p * p - 1) and len(Tsl) == p + 1 and mI in Tsl and mI in Bsl86 ok &= set(Tsl) & set(Bsl) == {I, mI}87 psl = {min(M, neg(M)) for M in SL}88 img = {min(mul(b, t, p), neg(mul(b, t, p))) for b in Bsl for t in Tsl}89 ok &= len(psl) == p * (p * p - 1) // 2 and 2 * len(img) == len(psl)90 ok &= all(mul(neg(b), neg(t), p) == mul(b, t, p) for b in Bsl[:20] for t in Tsl)91 return ok, len(img), len(psl)929394# ------------------------------------------------------------------------------------------------------------95# block H: the Hopf section (00:C19)96# the boost torus is fixed-point-free on ℙ¹ and meets the Borel trivially; every frame is uniquely (cone chart) × (boost),97# p(p − 1) fibres of p + 1; on SL₂ the section obstructs at −1 (Projective.lean, hopf_section).98@R.block("H", BLOCKS["H"])99def block_H():100 """Block H — the Hopf section (EXACT): H1–H3."""101 # 00:C19 (p00039)102 p = 13103 nus = [arith.nonresidue(p), drive(p)]104 ok = all(_hopf(p, nu) for nu in nus)105 sp, img, psl = _spin(p, nus[0])106 R.check("H1", "the Subject p = 13, ν the least nonsquare and ν = g, by search over all 2 184 classes of PGL₂: the 13 non-scalar "107 "boosts fix none of the 14 points; T ∩ B = 1; the 156 × 14 products (chart) × (boost) are every class exactly once; "108 "N M = borel · boost by the proof's formula for every class; on SL₂: −I is a boost and a chart, B ∩ T = {±I}, and "109 "the products cover half of PSL₂", ok and sp, f"PSL₂: {img} of {psl} classes")110 # H2 the shells 5, 17, 29 exhaustive; the Subject 173 on random matrices111 bad = [q for q in (5, 17, 29) if not _hopf(q, arith.nonresidue(q))]112 q, nu = SAMPLE, arith.nonresidue(SAMPLE)113 rng = random.Random(173)114 for _ in range(3000):115 M = tuple(rng.randrange(q) for _ in range(4))116 if det(M, q) == 0: continue117 N, (a, b, e), (al, be) = factor(M, nu, q)118 if not (N and a * e % q and tuple(N * v % q for v in M) == mul(borel(a, b, e, q), boost(nu, al, be, q), q)): bad.append(M)119 R.check("H2", "the shells 5, 17 and 29 by search over all of PGL₂; the Subject 173 on 3 000 random matrices: the factorization "120 "formula", not bad, f"exceptions {bad[:3]}")121 # H3 the spin cover at p = 5 and 17: half of PSL₂122 out = [(q,) + _spin(q, arith.nonresidue(q)) for q in (5, 17)]123 R.check("H3", "SL₂ at p = 5 and 17: −I is a boost and a chart, B ∩ T = {±I}, the products cover half of PSL₂ (30 of 60 at p = 5)",124 all(o[1] for o in out), "; ".join(f"p={o[0]}: {o[2]} of {o[3]}" for o in out))125126127# ------------------------------------------------------------------------------------------------------------128# block V: the frame variety and its two fibrations (00:C22)129# PGL₂(𝔽_p) ≅ SO₃(𝔽_p) by the adjoint action; the norm-one sphere is SL₂ (sphere_det, quat_bijective), the spin cover of PSL₂130# (one involution against p²); every element is one direction × one shell cell × one drive phase (frame_triple); the131# non-split fibration C_{p+1} over p(p − 1) with the Borel a section (hopf_section), the split C_{p−1} over p(p + 1) without132# (unit_split, the Sylow count) — Projective.lean, Spin.lean; the corpus script check_c22.py reproduced record for record.133@R.block("V", BLOCKS["V"])134def block_V():135 """Block V — the frame variety (EXACT): V1 the counts, the sphere and the two fibrations on the Subject 13; V2 the shells 5, 17, 29 and the controls; V3 the adjoint action."""136 # 00:C22 (p00152)137 p = 13; nu = arith.nonresidue(p); r = PJ.unit(p)[0]138 G, SL = pgl2(p), PJ.sl2(p)139 inv = PJ.involutions(p); sph = PJ.sphere_is_sl2(p, r); psl = PJ.psl2_image(p, r)140 counts = len(G) == len(SL) == p ** 3 - p == 2184 and psl == (1092, 2184) and inv == (1, 169)141 counts &= sph[0] and sph[3] and sph[1] == sph[2] == p ** 3 - p142 n, d, b, bij = PJ.frame_triple(p, nu); ns = PJ.nonsplit_fibration(p, nu); sp = PJ.split_fibration(p); cen = PJ.centraliser(p)143 triple = bij and n == (p + 1) * p * (p - 1) and d == p + 1 and b == p * (p - 1)144 fibr = ns["orbit_is_quadric"] and ns["base"] == p * (p - 1) and ns["fibres"] and ns["section"]145 fibr &= sp["orbit_is_quadric"] and sp["base"] == p * (p + 1) and sp["fibres_are_cosets"] and sp["borel_whole_or_none"] and cen[1] == p - 1 and cen[2]146 fibr &= PJ.sphere(p, 1) == p * (p + 1) and PJ.sphere(p, nu) == p * (p - 1) and len(PJ.circle(p)) == p - 1147 syl, nU, gen, tin = PJ.no_section_split(p)148 sylow = syl == p + 1 and nU == p * (p - 1) and gen == p * (p * p - 1) // 2 > p * (p + 1) and tin149 R.check("V1", "the Subject p = 13, by search: PGL₂ and SL₂ both of 2 184 = 14·13·12 elements, PSL₂ the half 1 092; the norm-one sphere "150 "x² + y² + z² + w² = 1 is SL₂ through the quarter-turn r² = −1 (the quaternion matrix's determinant is the norm, the sphere's "151 "matrices exactly those of determinant one) and its image in PGL₂ is PSL₂, index 2 — the spin cover, with one involution "152 "against PGL₂'s 169; every class is one direction × one Borel class ((p + 1)·p·(p − 1), the frame triple); the non-split "153 "orbit of u (u² = ν) is the quadric a² + bc = ν of 156 points with fibres C₁₄ and the Borel a section; the split orbit of "154 "the unit i is the quadric a² + bc = −1 of 182 points (the unit sphere's count), the fibres the cosets of the diagonal "155 "centraliser C₁₂ inside the Borel, and no section: the 14 Sylow 13-subgroups have normaliser of order 156 and generate "156 "PSL₂ of order 1 092 > 182, so no subgroup of order p(p + 1) exists",157 counts and triple and fibr and sylow,158 f"|PGL₂| = {len(G)}, PSL₂ {psl[0]}, involutions {inv}; triple {n} = {d}·{b}; bases {ns['base']}, {sp['base']}; Sylow {syl}, |N(U)| = {nU}, ⟨U⟩ = {gen}")159 # V2 the shells 5, 17, 29 by search, the odd-p controls 7 and 11 (the non-split fibration alone: the split one needs i² = −1), the p = 3 control160 ok, det_ = True, []161 for q in (5, 17, 29):162 nq = arith.nonresidue(q); n, d, b, bij = PJ.frame_triple(q, nq); ns = PJ.nonsplit_fibration(q, nq); sp = PJ.split_fibration(q)163 ok &= bij and n == (q + 1) * q * (q - 1) and ns["orbit_is_quadric"] and ns["base"] == q * (q - 1) and ns["fibres"] and ns["section"]164 ok &= sp["orbit_is_quadric"] and sp["base"] == q * (q + 1) and sp["fibres_are_cosets"] and sp["borel_whole_or_none"]165 if q <= 17:166 syl, nU, gen, tin = PJ.no_section_split(q); ok &= syl == q + 1 and nU == q * (q - 1) and gen == q * (q * q - 1) // 2 > q * (q + 1) and tin167 rq = PJ.unit(q)[0]; sph = PJ.sphere_is_sl2(q, rq); ok &= sph[0] and sph[3] and PJ.involutions(q) == (1, q * q)168 det_.append(f"p={q}: {n} = {d}·{b}")169 for q in (7, 11):170 nq = arith.nonresidue(q); n, d, b, bij = PJ.frame_triple(q, nq); ns = PJ.nonsplit_fibration(q, nq)171 ok &= bij and n == (q + 1) * q * (q - 1) and ns["orbit_is_quadric"] and ns["base"] == q * (q - 1) and ns["fibres"] and ns["section"]172 det_.append(f"p={q} (≡ 3 mod 4): {n} = {d}·{b}, non-split base {ns['base']}")173 G3 = pgl2(3); PSL3 = {pclass(M, 3) for M in PJ.sl2(3)}174 closed = all(pclass(mul(A, B, 3), 3) in PSL3 for A in PSL3 for B in PSL3)175 control = len(G3) == 24 and len(PSL3) == 12 == 3 * (3 + 1) and closed and len(PSL3 & {pclass((a, 0, 0, 1), 3) for a in (1, 2)}) == 1176 R.check("V2", "the shells 5, 17, 29 by search: the frame triple, both fibrations with their fibres, the Borel a section of the non-split "177 "one and of the split one none (the Sylow count at 5 and 17), the sphere SL₂ with one involution against p²; the odd-p "178 "controls 7 and 11: the triple and the non-split fibration (the split one needs i² = −1); the p = 3 control: PSL₂(𝔽₃) = A₄, "179 "a subgroup of order 12 = p(p + 1) meeting the split torus trivially — a section, which is why the clause reads p = 4κ + 1",180 ok and control, "; ".join(det_))181 # V3 the adjoint action: PGL₂ ≅ SO₃182 out = {q: PJ.so3(q) for q in (5, 13, 17)}183 ok = all(v[0] == v[1] == q * (q * q - 1) and v[2] == {1} and v[3] and v[4] == 2 * q * (q * q - 1) for q, v in out.items())184 R.check("V3", "PGL₂(𝔽_p) ≅ SO₃(𝔽_p) at p = 5, 13, 17: the adjoint action X ↦ M X M⁻¹ on the trace-zero matrices gives p(p² − 1) distinct "185 "3 × 3 matrices (injective: the kernel is the centre, Spin.lean ad_kernel), each of determinant 1 and preserving the form "186 "a² + bc = −det X (ad_preserves); the orthogonal group of the form counts 2p(p² − 1) by orthonormal frames, so the image is "187 "its index-2 subgroup SO₃", ok, "; ".join(f"p={q}: |Ad| = {v[0]}, |O₃| = {v[4]}" for q, v in out.items()))188189190# ------------------------------------------------------------------------------------------------------------191# block U: the frame group has one form (00:C27)192# SU₂(𝔽_{p²}/𝔽_p) = SL₂(𝔽_p) for the Hermitian form wJ, w^p = −w (Spin.lean su2_eq_sl2, hermitian_wJ); every element of finite order193# (finite_order); the sphere and ad − bc = 1 one quadric through the quarter-turn (sphere_det, quat_bijective) — the corpus194# script check_c23.py reproduced record for record.195@R.block("U", BLOCKS["U"])196def block_U():197 """Block U — the frame group has one form (EXACT): U1 the unitary form and the census on 5 and 13; U2 the quadric identity on the shells."""198 # 00:C27 (p00183)199 h5 = PJ.hermitian_wJ(5, arith.nonresidue(5))[1]; h13 = PJ.hermitian_wJ(13, arith.nonresidue(13))[1]200 full = PJ.su2_is_sl2(5, arith.nonresidue(5), full=True)201 sam = PJ.su2_is_sl2(13, arith.nonresidue(13))202 su = all(PJ.su2_identity_count(q, arith.nonresidue(q)) == q ** 3 - q for q in (5, 13))203 Q5, shape5, n5 = PJ.hermitian_congruence(5, arith.nonresidue(5))204 unit = h5 and h13 and full == (True, 120, 15600, 120) and sam[0] and sam[1] == 2184 and sam[3] == 0 and su and Q5 is not None and shape5 and n5 == 120205 cen = {q: PJ.element_census(q) for q in (5, 13)}206 census = all(c == e and inv == 1 and o["unip+"] == {q} and o["unip-"] == {2 * q} and all((q - 1) % k == 0 for k in o["split"])207 and all((q + 1) % k == 0 for k in o["nonsplit"]) for q, (c, e, o, inv) in cen.items())208 R.check("U1", "the unitary form: wJ is Hermitian ((wJ)† = wJ, since w^p = −w) at p = 5 and 13; every element of SL₂(𝔽_p) is wJ-unitary "209 "(120 at p = 5, 2 184 at p = 13); at p = 5, of SL₂(𝔽₂₅)'s 15 600 elements exactly the 120 rational ones are unitary — "210 "SU₂(𝔽₂₅/𝔽₅) = SL₂(𝔽₅) on the nose; at p = 13 none of 400 random non-rational elements of SL₂(𝔽₁₆₉) is unitary (the general "211 "converse is Spin.lean's su2_eq_sl2); |SU₂(I)| = #{N(a) + N(b) = 1} = p³ − p and an invertible Q with Qᵀ conj(Q) = wJ "212 "conjugates SL₂(𝔽₅) onto the quaternion shape [[a, b], [−b̄, ā]], 120 elements: the two Hermitian forms are one form; the "213 "census of SL₂ at 5 and 13 — I, −I, p² − 1 unipotents of each sign, (p − 3)p(p + 1)/2 split, (p − 1)²p/2 non-split — "214 "with the orders p, 2p, dividing p − 1, dividing p + 1 on the sampled elements, every element of finite order, and "215 "exactly one involution, −I",216 unit and census,217 f"SL₂(𝔽₂₅): {full[2]}, unitary {full[3]}; Q = {Q5}; census at 13: {tuple(cen[13][0].values())}, orders {dict((k, sorted(v)) for k, v in cen[13][2].items())}")218 # U2 the quadric identity at p = 5, 13, 17 and the controls 7, 11219 ok, det_ = True, []220 for q in (5, 13, 17):221 nq = arith.nonresidue(q); s4, s31, h3, s22, fr, ci, hy = PJ.quadric_counts(q, nq); rq = PJ.unit(q)[0]222 ok &= s4 == s31 == h3 == s22 == fr == q ** 3 - q and ci == q - 1 and hy == q + 1 and PJ.sphere_is_sl2(q, rq)[3]223 det_.append(f"p={q}: {s4}")224 for q in (7, 11):225 nq = arith.nonresidue(q); s4, s31, h3, s22, fr, ci, hy = PJ.quadric_counts(q, nq)226 ok &= s4 == s22 == fr == q ** 3 - q and s31 == h3 == q ** 3 + q and ci == q + 1 and hy == q + 1227 det_.append(f"p={q} (≡ 3 mod 4): (4,0) {s4}, (3,1) {s31}")228 R.check("U2", "the quadric identity: at p = 5, 13, 17 the sphere x² + y² + z² + w² = 1, the (3, 1) forms = ±1, the (2, 2) form and the "229 "frame quadric ad − bc = 1 all have p³ − p points and the sphere's matrices through the quarter-turn are exactly SL₂; the "230 "circle x² + y² = 1 has p − 1 points (the split torus), the hyperbola x² − νy² = 1 has p + 1 (the non-split); at the "231 "controls 7 and 11 the (3, 1) forms move to p³ + p and the circle to p + 1 — the signature labels follow η(−1), the frame "232 "quadric stays", ok, "; ".join(det_))233234235if __name__ == "__main__":236 blocks = [a for a in sys.argv[1:] if a in R.blocks]237 if blocks:238 for b in blocks: R.run_block(b)239 sys.exit(0 if R.summary(write=False) else 1)240 sys.exit(0 if R.verify_all() else 1)241