Research-Stack/4-Infrastructure/shim/rrc_root_system_probe.py
allaun 77488ac0ae feat(lean): close gaussian_line_integral_unit_dir + consolidate infrastructure
Lean proof fixes:
- N3L_Energy.lean: fully close gaussian_line_integral_unit_dir (nlinarith+hab
  for unit-circle quadratic, sqrt_mul+neg_div for integral_gaussian_1d match,
  exp_sum_of_sq order fix, add_assoc for h_gauss_shift, sq_sqrt for field_simp,
  sq_abs for perpDistance hd)
- Add Adapters/AlphaProofNexus: 12 Erdos/graph adapter stubs (AlphaProof nexus)
- Add Adapters/ErgodicAdditive.lean, SidonMatroid.lean
- Add AntiDiophantine.lean, EffectiveBoundDQ.lean, PVGS_DQ_Bridge.lean
- Add FormalConjectures/Util/ProblemImports.lean
- Add RRC/EntropyCandidates/Candidates.lean
- Add OTOM external project (lakefile.toml, lake-manifest.json, lean-toolchain)

Infrastructure:
- Add 4-Infrastructure/shim/: 17 Python probes (RRC manifold, Sidon kernel,
  Wannier, arxiv harvest, math_symbols DB, coverage density, geometric entropy)
- Add 4-Infrastructure/NoDupeLabs/: Node server + package files
- Add 6-Documentation/docs/specs/DP_RRC_RECEIPT_ENCODING_SPEC.md
- Add fix_offloat.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-18 16:53:23 -05:00

208 lines
7.5 KiB
Python

#!/usr/bin/env python3
"""
rrc_root_system_probe.py — Which root system does the RRC equation corpus sit on?
Pipeline:
1. Fingerprint each equation into a symbol-ROLE vector (15 axes) via the
math-symbol matrix (math_symbols.CHAR_INFO) on normalize_math(text).
2. Build the role COUPLING matrix C = corr(role_i, role_j) across the corpus.
3. Spectrum of C → effective rank (participation ratio) = the data's intrinsic
dimension.
4. Minimum spanning tree of roles under distance 1-|corr|. Dynkin diagrams are
TREES, so the coupling skeleton's tree-topology is directly comparable to
the A / D / E_n Dynkin diagrams:
max-degree ≤ 2 → A_n (path)
one deg-3 node, arms 1,1,k → D_n (fork)
one deg-3 node, arms 1,2,2 → E6
one deg-3 node, arms 1,2,3 → E7
one deg-3 node, arms 1,2,4 → E8
5. Emits a receipt; the per-equation role-vectors + coupling tree are the probe
we can then apply to NEW math (anomaly = lands in a coupling-tree "hole").
HONEST SCOPE: this matches the coupling *skeleton* to Dynkin *trees* (a real
graph comparison) and reports the effective rank. It does NOT claim C is a Cartan
matrix; that stronger claim would need the ±1/±2 integer inner-product spectrum.
Usage: python3 4-Infrastructure/shim/rrc_root_system_probe.py
"""
from __future__ import annotations
import json
import os
import sys
import time
from collections import Counter
from pathlib import Path
import numpy as np
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from math_symbols import CHAR_INFO, normalize_math # noqa: E402
ROOT = Path("/home/allaun/Research Stack")
RECEIPT_IN = ROOT / "archive/experimental-shim-probes/rrc_equation_classifier_receipt.json"
RECEIPT_OUT = ROOT / "shared-data/data/rrc_root_system_probe_receipt.json"
# ADE Dynkin diagrams as (branch arm-length signatures). Arms measured in EDGES
# from the unique degree-3 node; A_n has no branch node.
DYNKIN = {
(1, 1): "D", # fork: two length-1 arms + a tail of length k → D_{k+2}
(1, 2, 2): "E6",
(1, 2, 3): "E7",
(1, 2, 4): "E8",
}
def role_vector(text: str, roles: list[str]) -> np.ndarray:
"""Count symbol roles in an equation after LaTeX/Unicode normalization."""
norm = normalize_math(text)
idx = {r: i for i, r in enumerate(roles)}
v = np.zeros(len(roles))
for ch in norm:
info = CHAR_INFO.get(ch)
if info and info["role"] in idx:
v[idx[info["role"]]] += 1.0
return v
def load_equations() -> list[str]:
d = json.loads(RECEIPT_IN.read_text())
out = []
for e in d.get("compiled_equations", []):
r = e.get("equation_record", {})
txt = (r.get("name", "") + " " + r.get("equation", "")).strip()
if txt:
out.append(txt)
return out
def mst_prim(dist: np.ndarray) -> list[tuple[int, int, float]]:
"""Prim's MST on a dense distance matrix → list of (i, j, dist) edges."""
n = dist.shape[0]
in_tree = [False] * n
in_tree[0] = True
edges = []
for _ in range(n - 1):
best = (None, None, np.inf)
for i in range(n):
if not in_tree[i]:
continue
for j in range(n):
if in_tree[j]:
continue
if dist[i, j] < best[2]:
best = (i, j, dist[i, j])
i, j, w = best
if j is None:
break
in_tree[j] = True
edges.append((i, j, float(w)))
return edges
def classify_tree(n: int, adj: dict[int, list[int]]) -> tuple[str, dict]:
"""Classify a tree's topology against the A/D/E Dynkin families."""
deg = {v: len(adj[v]) for v in adj}
branch = [v for v in deg if deg[v] >= 3]
info = {"n_nodes": n, "max_degree": max(deg.values()) if deg else 0,
"n_branch_nodes": len(branch)}
if info["max_degree"] <= 2:
info["arms"] = []
return f"A_{n}", info
if len(branch) != 1 or info["max_degree"] != 3:
info["arms"] = []
return "irregular (not simply-laced ADE tree)", info
b = branch[0]
# measure each arm length (in edges) from the branch node out to a leaf
arms = []
for start in adj[b]:
length, prev, cur = 1, b, start
while True:
nxts = [x for x in adj[cur] if x != prev]
if len(nxts) != 1: # leaf (0) or another branch (>1)
break
prev, cur = cur, nxts[0]
length += 1
arms.append(length)
arms.sort()
info["arms"] = arms
key2 = tuple(arms[:2])
if tuple(arms) in DYNKIN:
return f"{DYNKIN[tuple(arms)]}", info
if key2 == (1, 1):
return f"D_{n}", info
return f"branched (arms={arms}; nearest E-series by long arm)", info
def main() -> None:
eqs = load_equations()
# roles present in the matrix, ordered by global frequency for readability
all_roles = sorted({i["role"] for i in CHAR_INFO.values()})
M = np.array([role_vector(e, all_roles) for e in eqs]) # (N, R)
present = M.sum(axis=0) > 0
roles = [r for r, p in zip(all_roles, present) if p]
M = M[:, present]
totals = M.sum(axis=0)
# drop zero-variance columns (corr undefined)
var = M.var(axis=0)
keep = var > 1e-9
roles = [r for r, k in zip(roles, keep) if k]
M = M[:, keep]
R = M.shape[1]
C = np.corrcoef(M, rowvar=False)
eig = np.sort(np.linalg.eigvalsh(C))[::-1]
pos = eig[eig > 1e-9]
participation = (pos.sum() ** 2) / (np.square(pos).sum()) # effective rank
dist = 1.0 - np.abs(C)
np.fill_diagonal(dist, 0.0)
edges = mst_prim(dist)
adj: dict[int, list[int]] = {i: [] for i in range(R)}
for i, j, _ in edges:
adj[i].append(j)
adj[j].append(i)
dynkin, tinfo = classify_tree(R, adj)
# ---- report ----
print("=" * 66)
print(f"RRC ROOT-SYSTEM PROBE — {len(eqs)} equations, {R} active role axes")
print("=" * 66)
print("\nRole totals across corpus:")
for r, t in sorted(zip(roles, totals[keep] if keep.shape[0] == totals.shape[0] else totals),
key=lambda x: -x[1]):
print(f" {r:16s} {int(t)}")
print(f"\nCoupling-matrix eigenvalues (desc): "
+ ", ".join(f"{e:.2f}" for e in eig))
print(f"Effective rank (participation ratio): {participation:.2f} "
f"→ compare E6=6, E7=7, E8=8")
print("\nRole coupling MST (Dynkin skeleton):")
for i, j, w in edges:
print(f" {roles[i]:16s}{1-w:+.2f}{roles[j]}")
print(f"\nTree topology: nodes={tinfo['n_nodes']}, max_degree={tinfo['max_degree']}, "
f"branch_nodes={tinfo['n_branch_nodes']}, arms={tinfo.get('arms')}")
print(f"\n>>> NEAREST DYNKIN TYPE: {dynkin}")
print(f">>> effective dim {participation:.2f} vs rank({R}) skeleton {dynkin}")
receipt = {
"schema": "rrc_root_system_probe_v1",
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ"),
"n_equations": len(eqs),
"roles": roles,
"role_totals": {r: int(t) for r, t in zip(roles, totals[keep])},
"coupling_eigenvalues": [float(x) for x in eig],
"effective_rank": float(participation),
"mst_edges": [[roles[i], roles[j], float(1 - w)] for i, j, w in edges],
"tree_topology": tinfo,
"nearest_dynkin": dynkin,
"caveat": "coupling-skeleton vs Dynkin-tree topology; not a Cartan-matrix claim",
}
RECEIPT_OUT.write_text(json.dumps(receipt, indent=2, ensure_ascii=False))
print(f"\nReceipt: {RECEIPT_OUT}")
if __name__ == "__main__":
main()