Create 13_projective_rehaul_probe_battery_testing.py
Browse files
13_projective_rehaul_probe_battery_testing.py
ADDED
|
@@ -0,0 +1,556 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
implicit_solver/A2_projective_reprobe_h2_64_singles.py
|
| 3 |
+
=======================================================
|
| 4 |
+
|
| 5 |
+
Apply the A0/A1 projective probe to all 16 single-noise h2-64 batteries
|
| 6 |
+
(indices 0-15, phase='final'). Tests whether the projective-axis
|
| 7 |
+
interpretation holds across:
|
| 8 |
+
- 16 different training distributions (one per noise type)
|
| 9 |
+
- Full 10-epoch convergence (not just 1000 batches)
|
| 10 |
+
- Production-grade sphere-solver batteries
|
| 11 |
+
|
| 12 |
+
For each battery:
|
| 13 |
+
1. Collect M tensor from gaussian test inputs (512 samples)
|
| 14 |
+
2. Identify antipodal pairs (mutual-strongest, cos < -0.9)
|
| 15 |
+
3. Collapse to projective axes
|
| 16 |
+
4. Run projective probe metrics:
|
| 17 |
+
- mean pairwise angle on βPΒ³
|
| 18 |
+
- deviation from uniform βPΒ³ baseline
|
| 19 |
+
- cluster silhouette (structure above uniform?)
|
| 20 |
+
- effective rank (dimension utilization)
|
| 21 |
+
- secondary antipodal count (further collapse?)
|
| 22 |
+
|
| 23 |
+
Expected if projective-reading hypothesis holds:
|
| 24 |
+
- All 16 batteries: |deviation| < 0.05
|
| 25 |
+
- All 16 batteries: effective rank 3.9+ of 4
|
| 26 |
+
- Axis count varies per battery (noise-type-dependent codebook size)
|
| 27 |
+
- Cluster silhouette low across all (no residual structure)
|
| 28 |
+
|
| 29 |
+
Output
|
| 30 |
+
------
|
| 31 |
+
/content/implicit_solver_reports/A2_projective_h2_64_singles.json
|
| 32 |
+
/content/implicit_solver_reports/A2_projective_h2_64_singles.png
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
import json
|
| 36 |
+
import math
|
| 37 |
+
from pathlib import Path
|
| 38 |
+
|
| 39 |
+
import numpy as np
|
| 40 |
+
import torch
|
| 41 |
+
import matplotlib.pyplot as plt
|
| 42 |
+
from sklearn.cluster import KMeans
|
| 43 |
+
from sklearn.metrics import silhouette_score
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
OUTPUT_DIR = Path("/content/implicit_solver_reports")
|
| 47 |
+
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 48 |
+
OUTPUT_PLOT = OUTPUT_DIR / "A2_projective_h2_64_singles.png"
|
| 49 |
+
OUTPUT_JSON = OUTPUT_DIR / "A2_projective_h2_64_singles.json"
|
| 50 |
+
|
| 51 |
+
NOISE_TYPE_NAMES = [
|
| 52 |
+
'gaussian', 'uniform', 'uniform_scaled', 'block',
|
| 53 |
+
'gradient', 'checker', 'salt_pepper', 'cauchy',
|
| 54 |
+
'laplace', 'periodic', 'exponential', 'mixed',
|
| 55 |
+
'poisson', 'structural', 'rayleigh', 'lognormal',
|
| 56 |
+
]
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 60 |
+
# Loading
|
| 61 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 62 |
+
|
| 63 |
+
def load_h2_64_array():
|
| 64 |
+
"""Use `loaded` from globals if available, else fetch from HF."""
|
| 65 |
+
array_model = globals().get('loaded')
|
| 66 |
+
if array_model is None:
|
| 67 |
+
import geolip_svae.arrays # noqa β registers BatteryArrayConfig
|
| 68 |
+
from transformers import AutoModel
|
| 69 |
+
print(" `loaded` not in globals, fetching h2-64 from HF...")
|
| 70 |
+
array_model = AutoModel.from_pretrained(
|
| 71 |
+
"AbstractPhil/geolip-svae-h2-64")
|
| 72 |
+
else:
|
| 73 |
+
print(" Using `loaded` from global session")
|
| 74 |
+
return array_model
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def collect_M_from_bank(bank, img_size=64, n_batches=8, batch_size=64):
|
| 78 |
+
"""Collect per-sample M from one battery bank on gaussian test input."""
|
| 79 |
+
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
|
| 80 |
+
bank = bank.to(device)
|
| 81 |
+
ds = OmegaNoiseDataset(
|
| 82 |
+
size=n_batches * batch_size, img_size=img_size, allowed_types=[0])
|
| 83 |
+
loader = torch.utils.data.DataLoader(ds, batch_size=batch_size, shuffle=False)
|
| 84 |
+
|
| 85 |
+
all_M = []
|
| 86 |
+
with torch.no_grad():
|
| 87 |
+
for imgs, _ in loader:
|
| 88 |
+
imgs = imgs.to(device)
|
| 89 |
+
out = bank(imgs)
|
| 90 |
+
M_patch0 = out['svd']['M'][:, 0] # [B, V, D]
|
| 91 |
+
all_M.append(M_patch0.cpu())
|
| 92 |
+
return torch.cat(all_M, dim=0).numpy()
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 96 |
+
# Projective probe (carry from A0/A1)
|
| 97 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 98 |
+
|
| 99 |
+
def identify_antipodal_pairs(M_avg, threshold=-0.9):
|
| 100 |
+
norms = np.linalg.norm(M_avg, axis=1, keepdims=True)
|
| 101 |
+
unit = M_avg / np.clip(norms, 1e-12, None)
|
| 102 |
+
cosines = unit @ unit.T
|
| 103 |
+
np.fill_diagonal(cosines, 1.0)
|
| 104 |
+
|
| 105 |
+
V = M_avg.shape[0]
|
| 106 |
+
claimed = [False] * V
|
| 107 |
+
pairs = []
|
| 108 |
+
candidates = []
|
| 109 |
+
for i in range(V):
|
| 110 |
+
best_j = int(cosines[i].argmin())
|
| 111 |
+
best_cos = float(cosines[i, best_j])
|
| 112 |
+
if best_cos < threshold:
|
| 113 |
+
candidates.append((best_cos, i, best_j))
|
| 114 |
+
candidates.sort()
|
| 115 |
+
for cos_val, i, j in candidates:
|
| 116 |
+
if claimed[i] or claimed[j]:
|
| 117 |
+
continue
|
| 118 |
+
if cosines[j].argmin() == i or cosines[j, i] < threshold:
|
| 119 |
+
pairs.append((min(i, j), max(i, j)))
|
| 120 |
+
claimed[i] = True
|
| 121 |
+
claimed[j] = True
|
| 122 |
+
unpaired = [i for i in range(V) if not claimed[i]]
|
| 123 |
+
return pairs, unpaired
|
| 124 |
+
|
| 125 |
+
|
| 126 |
+
def collapse_to_axes(M_avg, pairs, unpaired):
|
| 127 |
+
norms = np.linalg.norm(M_avg, axis=1, keepdims=True)
|
| 128 |
+
unit = M_avg / np.clip(norms, 1e-12, None)
|
| 129 |
+
reps = []
|
| 130 |
+
for i, j in pairs:
|
| 131 |
+
merged = unit[i] - unit[j]
|
| 132 |
+
merged = merged / max(np.linalg.norm(merged), 1e-12)
|
| 133 |
+
for k in range(merged.shape[0]):
|
| 134 |
+
if abs(merged[k]) > 1e-6:
|
| 135 |
+
if merged[k] < 0:
|
| 136 |
+
merged = -merged
|
| 137 |
+
break
|
| 138 |
+
reps.append(merged)
|
| 139 |
+
for i in unpaired:
|
| 140 |
+
r = unit[i].copy()
|
| 141 |
+
for k in range(r.shape[0]):
|
| 142 |
+
if abs(r[k]) > 1e-6:
|
| 143 |
+
if r[k] < 0:
|
| 144 |
+
r = -r
|
| 145 |
+
break
|
| 146 |
+
reps.append(r)
|
| 147 |
+
return np.array(reps)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def projective_pairwise_angles(axes):
|
| 151 |
+
n = axes.shape[0]
|
| 152 |
+
cosines = np.clip(axes @ axes.T, -1, 1)
|
| 153 |
+
raw = np.arccos(cosines)
|
| 154 |
+
proj = np.minimum(raw, np.pi - raw)
|
| 155 |
+
return proj[np.triu_indices(n, k=1)]
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
def uniform_rp_baseline(D, n_axes, n_trials=10):
|
| 159 |
+
rng = np.random.RandomState(0)
|
| 160 |
+
means = []
|
| 161 |
+
for _ in range(n_trials):
|
| 162 |
+
x = rng.randn(n_axes, D)
|
| 163 |
+
x = x / np.linalg.norm(x, axis=1, keepdims=True)
|
| 164 |
+
for k in range(D):
|
| 165 |
+
mask = (x[:, k] != 0) & (
|
| 166 |
+
np.all(x[:, :k] == 0, axis=1) if k > 0
|
| 167 |
+
else np.ones(n_axes, dtype=bool))
|
| 168 |
+
x[mask] = x[mask] * np.sign(x[mask, k:k+1])
|
| 169 |
+
if not np.any(mask):
|
| 170 |
+
break
|
| 171 |
+
means.append(projective_pairwise_angles(x).mean())
|
| 172 |
+
return float(np.mean(means))
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def probe_battery(M_avg):
|
| 176 |
+
pairs, unpaired = identify_antipodal_pairs(M_avg, threshold=-0.9)
|
| 177 |
+
axes = collapse_to_axes(M_avg, pairs, unpaired)
|
| 178 |
+
D = axes.shape[1]
|
| 179 |
+
n = axes.shape[0]
|
| 180 |
+
|
| 181 |
+
proj_angles = projective_pairwise_angles(axes)
|
| 182 |
+
baseline = uniform_rp_baseline(D, n)
|
| 183 |
+
deviation = float(proj_angles.mean() - baseline)
|
| 184 |
+
|
| 185 |
+
# Cluster silhouette
|
| 186 |
+
sils = []
|
| 187 |
+
for k in range(2, min(8, n)):
|
| 188 |
+
try:
|
| 189 |
+
km = KMeans(n_clusters=k, n_init=5, random_state=42)
|
| 190 |
+
labels = km.fit_predict(axes)
|
| 191 |
+
if len(set(labels)) >= 2:
|
| 192 |
+
sils.append((k, silhouette_score(axes, labels)))
|
| 193 |
+
except Exception:
|
| 194 |
+
pass
|
| 195 |
+
best_k, best_sil = (max(sils, key=lambda x: x[1])
|
| 196 |
+
if sils else (None, None))
|
| 197 |
+
|
| 198 |
+
# Effective rank
|
| 199 |
+
sv = np.linalg.svd(axes, compute_uv=False)
|
| 200 |
+
sv_norm = sv / sv.sum()
|
| 201 |
+
erank = math.exp(-(sv_norm * np.log(sv_norm + 1e-12)).sum())
|
| 202 |
+
|
| 203 |
+
# Secondary antipodal
|
| 204 |
+
cos_axes = axes @ axes.T
|
| 205 |
+
np.fill_diagonal(cos_axes, 1.0)
|
| 206 |
+
secondary = (cos_axes.min(axis=1) < -0.9).sum() // 2
|
| 207 |
+
|
| 208 |
+
return {
|
| 209 |
+
'pairs': len(pairs),
|
| 210 |
+
'unpaired': len(unpaired),
|
| 211 |
+
'n_axes': n,
|
| 212 |
+
'proj_angle_mean': float(proj_angles.mean()),
|
| 213 |
+
'uniform_baseline': baseline,
|
| 214 |
+
'deviation': deviation,
|
| 215 |
+
'best_cluster_k': best_k,
|
| 216 |
+
'best_silhouette': float(best_sil) if best_sil else None,
|
| 217 |
+
'effective_rank': float(erank),
|
| 218 |
+
'utilization': float(erank / D),
|
| 219 |
+
'secondary_antipodal': int(secondary),
|
| 220 |
+
'D': int(D),
|
| 221 |
+
'proj_angles_subset': proj_angles[:100].tolist(),
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def classify_projective_fit(probe):
|
| 226 |
+
"""Is this battery well-described by projective-axis reading?"""
|
| 227 |
+
uniform = abs(probe['deviation']) < 0.05
|
| 228 |
+
full_rank = probe['utilization'] > 0.95
|
| 229 |
+
no_clusters = (probe['best_silhouette'] or 0) < 0.4
|
| 230 |
+
low_secondary = probe['secondary_antipodal'] <= 3
|
| 231 |
+
|
| 232 |
+
if uniform and full_rank and no_clusters and low_secondary:
|
| 233 |
+
return 'PROJECTIVE-CLEAN'
|
| 234 |
+
elif uniform and full_rank:
|
| 235 |
+
return 'PROJECTIVE-MOSTLY'
|
| 236 |
+
elif full_rank:
|
| 237 |
+
return 'STRUCTURED'
|
| 238 |
+
else:
|
| 239 |
+
return 'DEGENERATE'
|
| 240 |
+
|
| 241 |
+
|
| 242 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 243 |
+
# Main
|
| 244 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 245 |
+
|
| 246 |
+
def main():
|
| 247 |
+
print("=" * 70)
|
| 248 |
+
print("A2 β projective re-probe of h2-64 single-noise batteries 0-15")
|
| 249 |
+
print("Tests whether projective-axis reading holds across training")
|
| 250 |
+
print("distributions (16 noise types, all 10-epoch converged)")
|
| 251 |
+
print("=" * 70)
|
| 252 |
+
|
| 253 |
+
print("\nLoading h2-64 array...")
|
| 254 |
+
array_model = load_h2_64_array()
|
| 255 |
+
|
| 256 |
+
print("\nProbing each single-noise battery on gaussian inputs:\n")
|
| 257 |
+
print(f" {'Idx':>3} {'Noise type':<18} {'Pairs':>5} {'Axes':>5} "
|
| 258 |
+
f"{'Dev':>8} {'Sil':>6} {'Erank':>5} {'2Β°':>3} Verdict")
|
| 259 |
+
print(" " + "-" * 85)
|
| 260 |
+
|
| 261 |
+
results = []
|
| 262 |
+
for batt_idx in range(16):
|
| 263 |
+
cfg_dict = array_model.config.batteries[batt_idx]
|
| 264 |
+
noise_name = NOISE_TYPE_NAMES[batt_idx]
|
| 265 |
+
assert cfg_dict.get('noise_types') == [batt_idx], \
|
| 266 |
+
f"Expected battery {batt_idx} to be single noise {batt_idx}, " \
|
| 267 |
+
f"got {cfg_dict.get('noise_types')}"
|
| 268 |
+
|
| 269 |
+
bank = array_model.bank(batt_idx, 'final')
|
| 270 |
+
bank.eval()
|
| 271 |
+
|
| 272 |
+
try:
|
| 273 |
+
all_M = collect_M_from_bank(bank)
|
| 274 |
+
M_avg = all_M.mean(axis=0)
|
| 275 |
+
probe = probe_battery(M_avg)
|
| 276 |
+
probe['battery_idx'] = batt_idx
|
| 277 |
+
probe['noise_name'] = noise_name
|
| 278 |
+
probe['verdict'] = classify_projective_fit(probe)
|
| 279 |
+
|
| 280 |
+
print(f" {batt_idx:>3} {noise_name:<18} "
|
| 281 |
+
f"{probe['pairs']:>5} {probe['n_axes']:>5} "
|
| 282 |
+
f"{probe['deviation']:>+.3f} "
|
| 283 |
+
f"{probe['best_silhouette'] or 0:>6.3f} "
|
| 284 |
+
f"{probe['effective_rank']:>5.2f} "
|
| 285 |
+
f"{probe['secondary_antipodal']:>3} {probe['verdict']}")
|
| 286 |
+
|
| 287 |
+
except Exception as e:
|
| 288 |
+
print(f" {batt_idx:>3} {noise_name:<18} ERROR: "
|
| 289 |
+
f"{type(e).__name__}: {str(e)[:40]}")
|
| 290 |
+
probe = {'battery_idx': batt_idx, 'noise_name': noise_name,
|
| 291 |
+
'error': str(e)}
|
| 292 |
+
|
| 293 |
+
results.append(probe)
|
| 294 |
+
|
| 295 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 296 |
+
# Aggregate summary
|
| 297 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 298 |
+
|
| 299 |
+
ok_results = [r for r in results if 'error' not in r]
|
| 300 |
+
|
| 301 |
+
print("\n" + "=" * 70)
|
| 302 |
+
print("AGGREGATE RESULTS")
|
| 303 |
+
print("=" * 70)
|
| 304 |
+
|
| 305 |
+
verdicts = {}
|
| 306 |
+
for r in ok_results:
|
| 307 |
+
verdicts[r['verdict']] = verdicts.get(r['verdict'], 0) + 1
|
| 308 |
+
|
| 309 |
+
print("\nVerdict distribution:")
|
| 310 |
+
for v, n in sorted(verdicts.items(), key=lambda x: -x[1]):
|
| 311 |
+
print(f" {v}: {n}/{len(ok_results)}")
|
| 312 |
+
|
| 313 |
+
# Axis count statistics
|
| 314 |
+
axis_counts = [r['n_axes'] for r in ok_results]
|
| 315 |
+
pairs_counts = [r['pairs'] for r in ok_results]
|
| 316 |
+
deviations = [r['deviation'] for r in ok_results]
|
| 317 |
+
silhouettes = [r['best_silhouette'] or 0 for r in ok_results]
|
| 318 |
+
eranks = [r['effective_rank'] for r in ok_results]
|
| 319 |
+
|
| 320 |
+
print(f"\nAxis count across 16 batteries:")
|
| 321 |
+
print(f" min: {min(axis_counts)}, max: {max(axis_counts)}, "
|
| 322 |
+
f"mean: {np.mean(axis_counts):.1f}, std: {np.std(axis_counts):.1f}")
|
| 323 |
+
print(f"\nAntipodal pairs across 16 batteries:")
|
| 324 |
+
print(f" min: {min(pairs_counts)}, max: {max(pairs_counts)}, "
|
| 325 |
+
f"mean: {np.mean(pairs_counts):.1f}, std: {np.std(pairs_counts):.1f}")
|
| 326 |
+
print(f"\nDeviation from uniform βPΒ³:")
|
| 327 |
+
print(f" min: {min(deviations):+.4f}, max: {max(deviations):+.4f}, "
|
| 328 |
+
f"mean: {np.mean(deviations):+.4f}, std: {np.std(deviations):.4f}")
|
| 329 |
+
print(f"\nCluster silhouette:")
|
| 330 |
+
print(f" min: {min(silhouettes):.3f}, max: {max(silhouettes):.3f}, "
|
| 331 |
+
f"mean: {np.mean(silhouettes):.3f}")
|
| 332 |
+
print(f"\nEffective rank (max 4.0):")
|
| 333 |
+
print(f" min: {min(eranks):.3f}, max: {max(eranks):.3f}, "
|
| 334 |
+
f"mean: {np.mean(eranks):.3f}")
|
| 335 |
+
|
| 336 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 337 |
+
# Save JSON
|
| 338 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 339 |
+
|
| 340 |
+
with open(OUTPUT_JSON, 'w') as f:
|
| 341 |
+
json.dump({
|
| 342 |
+
'results_per_battery': results,
|
| 343 |
+
'aggregate': {
|
| 344 |
+
'n_batteries': len(ok_results),
|
| 345 |
+
'verdict_counts': verdicts,
|
| 346 |
+
'axis_count_stats': {
|
| 347 |
+
'min': int(min(axis_counts)),
|
| 348 |
+
'max': int(max(axis_counts)),
|
| 349 |
+
'mean': float(np.mean(axis_counts)),
|
| 350 |
+
'std': float(np.std(axis_counts)),
|
| 351 |
+
},
|
| 352 |
+
'deviation_stats': {
|
| 353 |
+
'min': float(min(deviations)),
|
| 354 |
+
'max': float(max(deviations)),
|
| 355 |
+
'mean': float(np.mean(deviations)),
|
| 356 |
+
'std': float(np.std(deviations)),
|
| 357 |
+
},
|
| 358 |
+
'silhouette_stats': {
|
| 359 |
+
'min': float(min(silhouettes)),
|
| 360 |
+
'max': float(max(silhouettes)),
|
| 361 |
+
'mean': float(np.mean(silhouettes)),
|
| 362 |
+
},
|
| 363 |
+
'erank_stats': {
|
| 364 |
+
'min': float(min(eranks)),
|
| 365 |
+
'max': float(max(eranks)),
|
| 366 |
+
'mean': float(np.mean(eranks)),
|
| 367 |
+
},
|
| 368 |
+
},
|
| 369 |
+
}, f, indent=2, default=str)
|
| 370 |
+
print(f"\nSaved: {OUTPUT_JSON}")
|
| 371 |
+
|
| 372 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββοΏ½οΏ½οΏ½βββββββββββββ
|
| 373 |
+
# Plot: 6 panels summarizing the cross-battery picture
|
| 374 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 375 |
+
|
| 376 |
+
fig = plt.figure(figsize=(18, 12))
|
| 377 |
+
|
| 378 |
+
# Panel 1: per-battery deviation from uniform
|
| 379 |
+
ax1 = fig.add_subplot(2, 3, 1)
|
| 380 |
+
x = np.arange(len(ok_results))
|
| 381 |
+
devs = [r['deviation'] for r in ok_results]
|
| 382 |
+
colors = ['green' if abs(d) < 0.05 else 'orange' if abs(d) < 0.1 else 'red'
|
| 383 |
+
for d in devs]
|
| 384 |
+
ax1.bar(x, devs, color=colors)
|
| 385 |
+
ax1.axhline(0.05, color='red', linestyle='--', alpha=0.5,
|
| 386 |
+
label='Β±0.05 threshold')
|
| 387 |
+
ax1.axhline(-0.05, color='red', linestyle='--', alpha=0.5)
|
| 388 |
+
ax1.axhline(0, color='black', linestyle='-', alpha=0.3)
|
| 389 |
+
ax1.set_xticks(x)
|
| 390 |
+
ax1.set_xticklabels([r['noise_name'][:6] for r in ok_results],
|
| 391 |
+
rotation=60, ha='right', fontsize=8)
|
| 392 |
+
ax1.set_ylabel('Deviation from uniform βPΒ³')
|
| 393 |
+
ax1.set_title('Projective uniformity per battery')
|
| 394 |
+
ax1.legend(fontsize=8)
|
| 395 |
+
ax1.grid(alpha=0.3, axis='y')
|
| 396 |
+
|
| 397 |
+
# Panel 2: axis count per battery
|
| 398 |
+
ax2 = fig.add_subplot(2, 3, 2)
|
| 399 |
+
axes_n = [r['n_axes'] for r in ok_results]
|
| 400 |
+
pairs_n = [r['pairs'] for r in ok_results]
|
| 401 |
+
ax2.bar(x, axes_n, color='steelblue', label='Total axes')
|
| 402 |
+
ax2.bar(x, pairs_n, color='darkorange', label='Antipodal pairs collapsed')
|
| 403 |
+
ax2.set_xticks(x)
|
| 404 |
+
ax2.set_xticklabels([r['noise_name'][:6] for r in ok_results],
|
| 405 |
+
rotation=60, ha='right', fontsize=8)
|
| 406 |
+
ax2.set_ylabel('Count')
|
| 407 |
+
ax2.set_title('Axis codebook size per battery\n'
|
| 408 |
+
'(V=32 rows β N axes after collapse)')
|
| 409 |
+
ax2.legend(fontsize=8)
|
| 410 |
+
ax2.grid(alpha=0.3, axis='y')
|
| 411 |
+
|
| 412 |
+
# Panel 3: cluster silhouette
|
| 413 |
+
ax3 = fig.add_subplot(2, 3, 3)
|
| 414 |
+
sils = [r['best_silhouette'] or 0 for r in ok_results]
|
| 415 |
+
colors = ['green' if s < 0.4 else 'orange' if s < 0.5 else 'red' for s in sils]
|
| 416 |
+
ax3.bar(x, sils, color=colors)
|
| 417 |
+
ax3.axhline(0.4, color='orange', linestyle='--', alpha=0.5,
|
| 418 |
+
label='weak structure')
|
| 419 |
+
ax3.axhline(0.5, color='red', linestyle='--', alpha=0.5,
|
| 420 |
+
label='strong structure')
|
| 421 |
+
ax3.set_xticks(x)
|
| 422 |
+
ax3.set_xticklabels([r['noise_name'][:6] for r in ok_results],
|
| 423 |
+
rotation=60, ha='right', fontsize=8)
|
| 424 |
+
ax3.set_ylabel('Best cluster silhouette')
|
| 425 |
+
ax3.set_title('Residual structure on βPΒ³\n(low = clean projective)')
|
| 426 |
+
ax3.legend(fontsize=8)
|
| 427 |
+
ax3.grid(alpha=0.3, axis='y')
|
| 428 |
+
|
| 429 |
+
# Panel 4: effective rank
|
| 430 |
+
ax4 = fig.add_subplot(2, 3, 4)
|
| 431 |
+
eranks_arr = [r['effective_rank'] for r in ok_results]
|
| 432 |
+
ax4.bar(x, eranks_arr, color='purple')
|
| 433 |
+
ax4.axhline(4.0, color='green', linestyle='--', alpha=0.5,
|
| 434 |
+
label='max (full rank 4)')
|
| 435 |
+
ax4.axhline(3.8, color='orange', linestyle='--', alpha=0.5,
|
| 436 |
+
label='0.95 Γ max')
|
| 437 |
+
ax4.set_xticks(x)
|
| 438 |
+
ax4.set_xticklabels([r['noise_name'][:6] for r in ok_results],
|
| 439 |
+
rotation=60, ha='right', fontsize=8)
|
| 440 |
+
ax4.set_ylabel('Effective rank')
|
| 441 |
+
ax4.set_title('Dimension utilization on βPΒ³')
|
| 442 |
+
ax4.set_ylim([3.0, 4.05])
|
| 443 |
+
ax4.legend(fontsize=8)
|
| 444 |
+
ax4.grid(alpha=0.3, axis='y')
|
| 445 |
+
|
| 446 |
+
# Panel 5: aggregate angle distribution
|
| 447 |
+
ax5 = fig.add_subplot(2, 3, 5)
|
| 448 |
+
all_angles = []
|
| 449 |
+
for r in ok_results:
|
| 450 |
+
all_angles.extend(r['proj_angles_subset'])
|
| 451 |
+
ax5.hist(all_angles, bins=40, density=True, alpha=0.7, color='steelblue')
|
| 452 |
+
|
| 453 |
+
# Empirical uniform baseline for βPΒ³
|
| 454 |
+
avg_baseline = np.mean([r['uniform_baseline'] for r in ok_results])
|
| 455 |
+
ax5.axvline(avg_baseline, color='red', linestyle='--',
|
| 456 |
+
label=f'uniform βPΒ³ baseline ({avg_baseline:.3f})')
|
| 457 |
+
ax5.set_xlabel('Projective pairwise angle (radians)')
|
| 458 |
+
ax5.set_ylabel('Density')
|
| 459 |
+
ax5.set_title(f'Aggregate angle distribution\n'
|
| 460 |
+
f'(all 16 batteries pooled)')
|
| 461 |
+
ax5.legend(fontsize=8)
|
| 462 |
+
|
| 463 |
+
# Panel 6: verdict summary text
|
| 464 |
+
ax6 = fig.add_subplot(2, 3, 6)
|
| 465 |
+
ax6.axis('off')
|
| 466 |
+
|
| 467 |
+
n_clean = verdicts.get('PROJECTIVE-CLEAN', 0)
|
| 468 |
+
n_mostly = verdicts.get('PROJECTIVE-MOSTLY', 0)
|
| 469 |
+
n_struct = verdicts.get('STRUCTURED', 0)
|
| 470 |
+
n_degen = verdicts.get('DEGENERATE', 0)
|
| 471 |
+
total = len(ok_results)
|
| 472 |
+
|
| 473 |
+
clean_frac = (n_clean + n_mostly) / max(total, 1)
|
| 474 |
+
|
| 475 |
+
if clean_frac >= 0.9:
|
| 476 |
+
headline = "β HYPOTHESIS SUPPORTED"
|
| 477 |
+
color = 'lightgreen'
|
| 478 |
+
elif clean_frac >= 0.7:
|
| 479 |
+
headline = "~ MOSTLY SUPPORTED"
|
| 480 |
+
color = 'palegreen'
|
| 481 |
+
elif clean_frac >= 0.5:
|
| 482 |
+
headline = "~ MIXED"
|
| 483 |
+
color = 'lightyellow'
|
| 484 |
+
else:
|
| 485 |
+
headline = "β HYPOTHESIS NOT SUPPORTED"
|
| 486 |
+
color = 'mistyrose'
|
| 487 |
+
|
| 488 |
+
summary_text = (
|
| 489 |
+
f"16 single-noise batteries probed.\n"
|
| 490 |
+
f"All h2-64 architecture (V=32, D=4, H2_linear_matched).\n"
|
| 491 |
+
f"All 10-epoch fully converged.\n\n"
|
| 492 |
+
f"PROJECTIVE-CLEAN: {n_clean}/{total}\n"
|
| 493 |
+
f"PROJECTIVE-MOSTLY: {n_mostly}/{total}\n"
|
| 494 |
+
f"STRUCTURED: {n_struct}/{total}\n"
|
| 495 |
+
f"DEGENERATE: {n_degen}/{total}\n\n"
|
| 496 |
+
f"Axis count range: {min(axis_counts)}-{max(axis_counts)}\n"
|
| 497 |
+
f"Mean deviation: {np.mean(deviations):+.4f}\n"
|
| 498 |
+
f"Mean silhouette: {np.mean(silhouettes):.3f}\n"
|
| 499 |
+
f"Mean effective rank: {np.mean(eranks):.2f} / 4\n\n"
|
| 500 |
+
f"Interpretation:\n"
|
| 501 |
+
)
|
| 502 |
+
|
| 503 |
+
if clean_frac >= 0.9:
|
| 504 |
+
summary_text += (
|
| 505 |
+
"Projective-axis reading is GENERAL β holds across\n"
|
| 506 |
+
"16 different training distributions at full convergence.\n"
|
| 507 |
+
"h2-64 is a library of per-noise-type axis codebooks."
|
| 508 |
+
)
|
| 509 |
+
elif clean_frac >= 0.7:
|
| 510 |
+
summary_text += (
|
| 511 |
+
"Most batteries fit the projective reading.\n"
|
| 512 |
+
"Outliers suggest noise-type-specific geometry variations.\n"
|
| 513 |
+
"Worth investigating which noise types deviate and why."
|
| 514 |
+
)
|
| 515 |
+
else:
|
| 516 |
+
summary_text += (
|
| 517 |
+
"Projective reading doesn't generalize cleanly.\n"
|
| 518 |
+
"Either the threshold (|dev|<0.05) is too strict,\n"
|
| 519 |
+
"or h2-64 batteries have noise-specific non-projective\n"
|
| 520 |
+
"geometry that only the D=3 and Q-rank02 cases shared."
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
ax6.text(0.5, 0.95, headline, ha='center', va='top',
|
| 524 |
+
fontsize=16, fontweight='bold',
|
| 525 |
+
bbox=dict(boxstyle='round', facecolor=color, alpha=0.8))
|
| 526 |
+
ax6.text(0.05, 0.78, summary_text, ha='left', va='top',
|
| 527 |
+
fontsize=9, family='monospace')
|
| 528 |
+
|
| 529 |
+
plt.tight_layout()
|
| 530 |
+
plt.savefig(OUTPUT_PLOT, dpi=120, bbox_inches='tight')
|
| 531 |
+
plt.show()
|
| 532 |
+
print(f"Saved: {OUTPUT_PLOT}")
|
| 533 |
+
|
| 534 |
+
# Conclusion
|
| 535 |
+
print("\n" + "=" * 70)
|
| 536 |
+
print("CONCLUSION")
|
| 537 |
+
print("=" * 70)
|
| 538 |
+
print(f"\n {n_clean + n_mostly}/{total} batteries fit the projective "
|
| 539 |
+
f"reading (clean or mostly).")
|
| 540 |
+
print(f" Mean deviation from uniform βPΒ³: {np.mean(deviations):+.4f}")
|
| 541 |
+
print(f" Mean cluster silhouette: {np.mean(silhouettes):.3f}")
|
| 542 |
+
print()
|
| 543 |
+
if clean_frac >= 0.9:
|
| 544 |
+
print(" The projective-axis hypothesis is SUPPORTED across 16 different")
|
| 545 |
+
print(" trained sphere-solvers. This is strong evidence that the")
|
| 546 |
+
print(" βP^(D-1) reading is general, not D=3-specific or Q-sweep-specific.")
|
| 547 |
+
print()
|
| 548 |
+
print(" h2-64 is effectively a library of 16 trained axis codebooks,")
|
| 549 |
+
print(" each with its own cardinality and orientation on βPΒ³,")
|
| 550 |
+
print(" each trained for a specific noise discrimination task.")
|
| 551 |
+
|
| 552 |
+
return results
|
| 553 |
+
|
| 554 |
+
|
| 555 |
+
if __name__ == '__main__':
|
| 556 |
+
results = main()
|