""" implicit_solver/A0_projective_reprobe.py ========================================= Test claim 3: G-Cand is actually a 14-axis ℝP² solver, not a 32-point S² solver. Method ------ 1. Load G-Cand (Q-rank09, V=32, D=3) — already trained sphere-solver. 2. Collect M tensor as before — 512 samples × 32 rows × 3 dims. 3. Identify antipodal pairs in the canonical M_avg arrangement: row i and row j form a pair if cos(M_avg[i], M_avg[j]) < -0.9 4. Collapse: for each pair, pick canonical representative (the one with positive first nonzero coordinate). Yields up to 16 axis representatives. 5. Re-run v2 probe metrics under projective geometry: - Pairwise angles wrapped to [0, π/2] via θ → min(θ, π - θ) - Uniform ℝP² baseline: pairwise angles peak at π/4 (not π/2) - Cluster, stability, antipodal-of-antipodal (testing if axes themselves have further antipodal structure within ℝP²) Predicted outcomes ------------------ A. CLEAN PROJECTIVE: 14 axes uniformly cover ℝP², pairwise angles peak at π/4, no further antipodal collapse. → G-Cand is a clean 14-axis ℝP² solver. Sphere-norm was the wrong reading. The true geometry is projective. B. STILL DEGENERATE: 14 axes show further structure (clustering, secondary antipodal pairs, non-uniform). → G-Cand is structured beyond simple ℝP² uniform. Some other geometry applies, or the antipodal collapse hypothesis is incomplete. C. ANTI-PROJECTIVE: 14 axes are NOT uniformly distributed on ℝP²; they show strong clustering or aligned-direction patterns. → The "spindle collapse" was real but isn't ℝP² either. The geometry is something more degenerate (line, plane subset, etc.) Cost ---- Same trained checkpoint, different probe math. ~10 seconds. Output ------ /content/implicit_solver_reports/A0_projective_reprobe.json /content/implicit_solver_reports/A0_projective_reprobe.png """ import json import math from pathlib import Path import numpy as np import torch import torch.nn.functional as F import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # noqa from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score CKPT_DIR = Path("/content/phaseQ_reports") RANK09_CKPT = CKPT_DIR / "Q_rank09_h64_V32_D3_dp0_nx0_adam" / "epoch_1_checkpoint.pt" OUTPUT_DIR = Path("/content/implicit_solver_reports") OUTPUT_DIR.mkdir(parents=True, exist_ok=True) OUTPUT_PLOT = OUTPUT_DIR / "A0_projective_reprobe.png" OUTPUT_JSON = OUTPUT_DIR / "A0_projective_reprobe.json" # ════════════════════════════════════════════════════════════════════ # Loading # ════════════════════════════════════════════════════════════════════ def load_g_cand(): cfgs = get_phaseQ_configs() cfg_dict = next(c for c in cfgs if 'rank09' in c['variant']) cfg = build_run_config(cfg_dict) overrides = cfg_dict['overrides'] model = PatchSVAE_F_Ablation( matrix_v=cfg.matrix_v, D=cfg.D, patch_size=cfg.patch_size, hidden=cfg.hidden, depth=cfg.depth, n_cross_layers=cfg.n_cross_layers, n_heads=cfg.n_heads, max_alpha=overrides.get('max_alpha', cfg.max_alpha), alpha_init=cfg.alpha_init, activation=overrides.get('activation', 'gelu'), row_norm=overrides.get('row_norm', 'sphere'), svd_mode=overrides.get('svd', 'fp64'), linear_readout=overrides.get('linear_readout', False), match_params=overrides.get('match_params', True), init_scheme=overrides.get('init', 'orthogonal'), ) ckpt = torch.load(RANK09_CKPT, map_location='cpu', weights_only=False) state_dict = ( ckpt.get('model_state') or ckpt.get('model_state_dict') or ckpt.get('state_dict') or ckpt ) model.load_state_dict(state_dict) model.eval() return model, cfg def collect_per_sample_M(model, cfg, n_batches=8, batch_size=64): device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model = model.to(device) ds = OmegaNoiseDataset( size=n_batches * batch_size, img_size=cfg.img_size, allowed_types=[0]) loader = torch.utils.data.DataLoader(ds, batch_size=batch_size, shuffle=False) all_M = [] with torch.no_grad(): for imgs, _ in loader: imgs = imgs.to(device) out = model(imgs) M_patch0 = out['svd']['M'][:, 0] all_M.append(M_patch0.cpu()) return torch.cat(all_M, dim=0).numpy() # ════════════════════════════════════════════════════════════════════ # Antipodal pair identification + projective collapse # ════════════════════════════════════════════════════════════════════ def identify_antipodal_pairs(M_avg, threshold=-0.9): """For each row, find its antipodal partner (cos < threshold). Returns (pairs, unpaired): pairs: list of (i, j) tuples where i < j and rows i, j are antipodal unpaired: list of row indices with no antipodal partner Greedy matching: each row pairs with its strongest antipodal candidate that hasn't been claimed yet. """ norms = np.linalg.norm(M_avg, axis=1, keepdims=True) unit = M_avg / np.clip(norms, 1e-12, None) cosines = unit @ unit.T np.fill_diagonal(cosines, 1.0) # exclude self V = M_avg.shape[0] claimed = [False] * V pairs = [] # Sort rows by their strongest antipodal candidate (most negative cos) # Greedy claim — strongest pairings get priority candidates = [] for i in range(V): best_j = int(cosines[i].argmin()) best_cos = float(cosines[i, best_j]) if best_cos < threshold: candidates.append((best_cos, i, best_j)) candidates.sort() # most negative first for cos_val, i, j in candidates: if claimed[i] or claimed[j]: continue # Verify symmetry: j's strongest is also i (or close enough) if cosines[j].argmin() == i or cosines[j, i] < threshold: pairs.append((min(i, j), max(i, j))) claimed[i] = True claimed[j] = True unpaired = [i for i in range(V) if not claimed[i]] return pairs, unpaired def collapse_to_axes(M_avg, pairs, unpaired): """Pick canonical representative for each pair: the row with positive first nonzero coordinate. Unpaired rows stay as-is. Returns axes [n_axes, D] where n_axes = len(pairs) + len(unpaired).""" norms = np.linalg.norm(M_avg, axis=1, keepdims=True) unit = M_avg / np.clip(norms, 1e-12, None) representatives = [] for i, j in pairs: # Pick the row whose first nonzero coordinate is positive for r in [unit[i], unit[j]]: for k in range(r.shape[0]): if abs(r[k]) > 1e-6: chosen = r if r[k] > 0 else -r representatives.append(chosen) break else: # All zeros (shouldn't happen on sphere) — pick row i representatives.append(unit[i]) break else: continue # We added one rep; continue to next pair # Actually the above structure is wrong — let me redo cleanly: representatives = [] for i, j in pairs: # Average of row_i and -row_j (since they're antipodal, this enhances # the shared axis direction) merged = unit[i] - unit[j] merged = merged / max(np.linalg.norm(merged), 1e-12) # Canonicalize sign: first nonzero coord positive for k in range(merged.shape[0]): if abs(merged[k]) > 1e-6: if merged[k] < 0: merged = -merged break representatives.append(merged) for i in unpaired: r = unit[i].copy() # Same canonical sign convention for k in range(r.shape[0]): if abs(r[k]) > 1e-6: if r[k] < 0: r = -r break representatives.append(r) return np.array(representatives) # ════════════════════════════════════════════════════════════════════ # Projective metrics # ════════════════════════════════════════════════════════════════════ def projective_pairwise_angles(axes): """Angles between axes on ℝP^(D-1). Each axis is a line through origin, so angle between two axes is min(θ, π-θ) ∈ [0, π/2].""" n = axes.shape[0] cosines = axes @ axes.T cosines = np.clip(cosines, -1, 1) # On ℝP^(D-1): two axes are equivalent under sign flip # so the "true" angle is the smaller of θ and π-θ raw_angles = np.arccos(cosines) proj_angles = np.minimum(raw_angles, np.pi - raw_angles) triu = np.triu_indices(n, k=1) return proj_angles[triu] def uniform_rp_pairwise_angle_baseline(D, n_axes, n_trials=10): """Predicted pairwise-angle distribution for n_axes uniformly placed on ℝP^(D-1). Sample uniformly on S^(D-1), antipodally identify, compute pairwise angles.""" rng = np.random.RandomState(0) means = [] for _ in range(n_trials): # Sample n_axes uniformly on S^(D-1) x = rng.randn(n_axes, D) x = x / np.linalg.norm(x, axis=1, keepdims=True) # Canonicalize to upper hemisphere (positive first coord) for k in range(D): sign = np.sign(x[:, k]) sign[sign == 0] = 1 mask = (x[:, k] != 0) & (np.all(x[:, :k] == 0, axis=1) if k > 0 else np.ones(n_axes, dtype=bool)) x[mask] = x[mask] * sign[mask, None] if np.all(x[:, k] != 0): break angles = projective_pairwise_angles(x) means.append(angles.mean()) return float(np.mean(means)) def test_axis_distribution(axes, label): """Run all probe metrics on the projective axes.""" D = axes.shape[1] n = axes.shape[0] print(f"\n[{label}]") print(f" Axes shape: {axes.shape}") # Pairwise angles in projective space proj_angles = projective_pairwise_angles(axes) print(f" Projective pairwise angles (radians, max possible π/2={math.pi/2:.3f}):") print(f" mean: {proj_angles.mean():.3f}") print(f" median: {np.median(proj_angles):.3f}") print(f" min: {proj_angles.min():.3f}") print(f" max: {proj_angles.max():.3f}") # Predicted uniform baseline for ℝP^(D-1) uniform_baseline = uniform_rp_pairwise_angle_baseline(D, n) deviation = proj_angles.mean() - uniform_baseline print(f" Uniform ℝP^{D-1} baseline (n={n}): {uniform_baseline:.3f}") print(f" Deviation: {deviation:+.3f} " f"({'CLOSE TO UNIFORM' if abs(deviation) < 0.05 else 'NON-UNIFORM'})") # Fraction at small angles (axis clustering) fraction_clustered = (proj_angles < 0.3).mean() fraction_perp = ((proj_angles > math.pi/4 - 0.15) & (proj_angles < math.pi/4 + 0.15)).mean() print(f" Fraction near-zero (axes parallel): {fraction_clustered:.3f}") print(f" Fraction near π/4 (uniform peak): {fraction_perp:.3f}") # Cluster analysis on the axes themselves (not on the original M) sils = [] for k in range(2, min(8, n)): try: km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(axes) if len(set(labels)) >= 2: sils.append((k, silhouette_score(axes, labels))) except Exception: pass if sils: best_k, best_sil = max(sils, key=lambda x: x[1]) print(f" Best cluster k={best_k}, silhouette={best_sil:.3f}") cluster_verdict = ( 'STRONG (real clusters)' if best_sil > 0.5 else 'WEAK (some structure)' if best_sil > 0.3 else 'NONE (continuous distribution)' ) print(f" Cluster verdict: {cluster_verdict}") else: best_k, best_sil = None, None cluster_verdict = 'N/A' # Effective rank of the axis matrix sv = np.linalg.svd(axes, compute_uv=False) sv_norm = sv / sv.sum() erank = math.exp(-(sv_norm * np.log(sv_norm + 1e-12)).sum()) print(f" Effective rank: {erank:.2f} of {D} possible " f"({erank/D*100:.0f}% utilization)") # Test for SECONDARY antipodal structure within the axes # If axes still show antipodal pairs, the geometry is more degenerate # than ℝP^(D-1) — possibly ℝP^(D-1) / ℤ₂ or projection to even lower dim cos_axes = axes @ axes.T np.fill_diagonal(cos_axes, 1.0) most_anti = cos_axes.min(axis=1) secondary_anti = (most_anti < -0.9).sum() // 2 print(f" Secondary antipodal pairs (axes paired again): " f"{secondary_anti}/{n//2}") return { 'n_axes': int(n), 'D': int(D), 'proj_angle_mean': float(proj_angles.mean()), 'proj_angle_median': float(np.median(proj_angles)), 'proj_angle_min': float(proj_angles.min()), 'proj_angle_max': float(proj_angles.max()), 'uniform_baseline': uniform_baseline, 'deviation_from_uniform': float(deviation), 'fraction_clustered': float(fraction_clustered), 'fraction_near_pi_over_4': float(fraction_perp), 'best_cluster_k': best_k, 'best_silhouette': best_sil, 'cluster_verdict': cluster_verdict, 'effective_rank': float(erank), 'utilization': float(erank / D), 'secondary_antipodal_pairs': int(secondary_anti), 'proj_angles_subset': proj_angles[:200].tolist(), } # ════════════════════════════════════════════════════════════════════ # Plotting # ════════════════════════════════════════════════════════════════════ def plot_projective(M_avg, axes, pairs, unpaired, results, output_path): fig = plt.figure(figsize=(18, 12)) # Panel 1: Original M_avg on S² with pairings highlighted ax1 = fig.add_subplot(2, 3, 1, projection='3d') norms = np.linalg.norm(M_avg, axis=1, keepdims=True) unit = M_avg / np.clip(norms, 1e-12, None) # Wireframe sphere u = np.linspace(0, 2*np.pi, 20) v = np.linspace(0, np.pi, 20) x_s = np.outer(np.cos(u), np.sin(v)) y_s = np.outer(np.sin(u), np.sin(v)) z_s = np.outer(np.ones_like(u), np.cos(v)) ax1.plot_wireframe(x_s, y_s, z_s, alpha=0.1, color='gray') # Color paired rows by pair index pair_colors = plt.cm.tab20(np.linspace(0, 1, max(len(pairs), 1))) for k, (i, j) in enumerate(pairs): color = pair_colors[k] ax1.scatter(unit[i, 0], unit[i, 1], unit[i, 2], c=[color], s=80, edgecolors='black', linewidths=0.5) ax1.scatter(unit[j, 0], unit[j, 1], unit[j, 2], c=[color], s=80, edgecolors='black', linewidths=0.5) # Line connecting the antipodal pair ax1.plot([unit[i, 0], unit[j, 0]], [unit[i, 1], unit[j, 1]], [unit[i, 2], unit[j, 2]], color=color, alpha=0.3, linewidth=0.8) # Unpaired rows in red for i in unpaired: ax1.scatter(unit[i, 0], unit[i, 1], unit[i, 2], c='red', marker='x', s=100, linewidths=2) ax1.set_title(f'Original M_avg on S²\n' f'{len(pairs)} antipodal pairs (colored), ' f'{len(unpaired)} unpaired (red ×)') # Panel 2: Collapsed axes on upper hemisphere (canonical reps) ax2 = fig.add_subplot(2, 3, 2, projection='3d') ax2.plot_wireframe(x_s, y_s, z_s, alpha=0.1, color='gray') for k, ax in enumerate(axes): ax2.scatter(ax[0], ax[1], ax[2], c=[plt.cm.tab20(k % 20)], s=120, edgecolors='black', linewidths=0.5) # Draw line through origin to show it's an AXIS not a point ax2.plot([-ax[0], ax[0]], [-ax[1], ax[1]], [-ax[2], ax[2]], color=plt.cm.tab20(k % 20), alpha=0.4, linewidth=1.0) ax2.set_title(f'Collapsed axes (n={axes.shape[0]})\n' f'Each line through origin = one axis on ℝP²') # Panel 3: Projective angle distribution vs uniform baseline ax3 = fig.add_subplot(2, 3, 3) proj_angles = results['proj_angles_subset'] ax3.hist(proj_angles, bins=30, density=True, alpha=0.7, color='steelblue', label='G-Cand projective') ax3.axvline(results['uniform_baseline'], color='red', linestyle='--', label=f"uniform ℝP² baseline ({results['uniform_baseline']:.3f})") ax3.axvline(math.pi/4, color='green', linestyle=':', label=f'π/4 = {math.pi/4:.3f}') ax3.set_xlabel('Projective pairwise angle (radians, max π/2)') ax3.set_ylabel('Density') ax3.set_title(f'Projective angle distribution\n' f"deviation: {results['deviation_from_uniform']:+.3f}") ax3.legend(fontsize=8) # Panel 4: Cluster silhouette across k ax4 = fig.add_subplot(2, 3, 4) if results['best_cluster_k'] is not None: ks_sils = [] for k in range(2, min(8, axes.shape[0])): try: km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(axes) if len(set(labels)) >= 2: ks_sils.append((k, silhouette_score(axes, labels))) except Exception: pass if ks_sils: ks, sils = zip(*ks_sils) ax4.plot(ks, sils, 'o-', color='purple', markersize=8) ax4.axhline(0.5, color='red', linestyle='--', alpha=0.5, label='strong cluster') ax4.axhline(0.3, color='orange', linestyle='--', alpha=0.5, label='weak cluster') ax4.set_xlabel('k (number of clusters)') ax4.set_ylabel('silhouette score') ax4.set_title(f"Axis clustering\n" f"verdict: {results['cluster_verdict']}") ax4.legend(fontsize=8) ax4.grid(alpha=0.3) # Panel 5: Effective rank bar ax5 = fig.add_subplot(2, 3, 5) sv = np.linalg.svd(axes, compute_uv=False) ax5.bar([f'σ{i+1}' for i in range(len(sv))], sv, color=['red', 'orange', 'yellow'][:len(sv)]) ax5.set_ylabel('Singular value') ax5.set_title(f"Singular values of axis matrix\n" f"effective rank: {results['effective_rank']:.2f} " f"of {results['D']}") # Panel 6: Composite verdict ax6 = fig.add_subplot(2, 3, 6) ax6.axis('off') # Decide composite verdict is_uniform = abs(results['deviation_from_uniform']) < 0.05 is_clustered = (results['best_silhouette'] or 0) > 0.5 has_secondary_antipodal = results['secondary_antipodal_pairs'] >= 3 full_rank = results['utilization'] > 0.95 if is_uniform and not is_clustered and not has_secondary_antipodal and full_rank: verdict = "✓ CLEAN ℝP² SOLVER" explanation = ( "G-Cand was a 14-axis projective-space solver all along.\n" "Sphere-norm was the wrong reading — the true geometry\n" "is uniform on ℝP². Claim 3 SUPPORTED." ) color = 'lightgreen' elif is_uniform and not is_clustered and full_rank: verdict = "✓ MOSTLY ℝP², minor irregularities" explanation = ( "Axes are roughly uniform on ℝP² with some structure.\n" "Claim 3 PARTIALLY SUPPORTED — projective interpretation\n" "is the right space, but distribution isn't perfectly uniform." ) color = 'palegreen' elif is_clustered: verdict = "✗ STRUCTURED on ℝP²" explanation = ( "Axes show genuine cluster structure on ℝP².\n" "Not uniform, not random — something more specific.\n" "May be a polytope on ℝP² (less common) or other geometry." ) color = 'lightyellow' elif has_secondary_antipodal: verdict = "✗ FURTHER COLLAPSE" explanation = ( "Even after antipodal collapse, axes show NEW antipodal pairs.\n" "Geometry is more degenerate than ℝP² — possibly lens space,\n" "or D-effective lower than D=3." ) color = 'mistyrose' elif not full_rank: verdict = "✗ DEGENERATE — sub-rank" explanation = ( "Axes don't span the full D=3 space.\n" "Effective rank < 3 means rows live on a 2D plane or 1D line\n" "in 3D space. Spindle hypothesis dimension-collapsed." ) color = 'lightcoral' else: verdict = "? UNCLEAR" explanation = ( "Mixed signals — re-examine the metrics individually.\n" "Antipodal hypothesis neither confirmed nor cleanly refuted." ) color = 'lightgray' ax6.text(0.5, 0.85, verdict, ha='center', va='top', fontsize=20, fontweight='bold', bbox=dict(boxstyle='round', facecolor=color, alpha=0.8)) ax6.text(0.05, 0.55, explanation, ha='left', va='top', fontsize=11, wrap=True, family='monospace') metrics_summary = ( f"\n\nKey metrics:\n" f" axes: {results['n_axes']}\n" f" proj angle mean: {results['proj_angle_mean']:.3f}\n" f" uniform baseline: {results['uniform_baseline']:.3f}\n" f" deviation: {results['deviation_from_uniform']:+.3f}\n" f" best cluster silhouette: {results['best_silhouette'] or 0:.3f}\n" f" effective rank: {results['effective_rank']:.2f}/{results['D']}\n" f" secondary antipodal: {results['secondary_antipodal_pairs']}" ) ax6.text(0.05, 0.30, metrics_summary, ha='left', va='top', fontsize=10, family='monospace') plt.tight_layout() plt.savefig(output_path, dpi=120, bbox_inches='tight') plt.show() # ════════════════════════════════════════════════════════════════════ # Main # ════════════════════════════════════════════════════════════════════ def main(): print("=" * 70) print("Projective re-probe of G-Cand (Q-rank09, V=32, D=3)") print("Testing claim 3: trained sphere-solver is actually a ℝP² solver") print("=" * 70) print("\nLoading G-Cand checkpoint...") model, cfg = load_g_cand() print(f" V={cfg.matrix_v}, D={cfg.D}, " f"params={sum(p.numel() for p in model.parameters()):,}") print("\nCollecting M tensor (512 gaussian samples)...") all_M = collect_per_sample_M(model, cfg) M_avg = all_M.mean(axis=0) print(f" M_avg shape: {M_avg.shape}") print("\nIdentifying antipodal pairs (cos < -0.9)...") pairs, unpaired = identify_antipodal_pairs(M_avg, threshold=-0.9) print(f" Found {len(pairs)} antipodal pairs") print(f" Unpaired rows: {len(unpaired)}") print(f" Total accounted: {2*len(pairs) + len(unpaired)} of {M_avg.shape[0]}") print("\nCollapsing to projective axes...") axes = collapse_to_axes(M_avg, pairs, unpaired) print(f" Axes: {axes.shape[0]} representatives in {axes.shape[1]}-D") # ── Run probe metrics under projective interpretation ── results = test_axis_distribution(axes, "G-Cand projective axes") # ── Save ── output_data = { 'config': { 'variant': 'Q_rank09_h64_V32_D3_dp0_nx0_adam', 'V': cfg.matrix_v, 'D': cfg.D, }, 'antipodal_pairs_found': len(pairs), 'unpaired_rows': len(unpaired), 'total_axes': axes.shape[0], 'projective_metrics': results, 'pairs': [list(p) for p in pairs], 'unpaired': unpaired, } with open(OUTPUT_JSON, 'w') as f: json.dump(output_data, f, indent=2, default=str) print(f"\nSaved: {OUTPUT_JSON}") plot_projective(M_avg, axes, pairs, unpaired, results, OUTPUT_PLOT) print(f"Saved: {OUTPUT_PLOT}") # ── Headline conclusion ── print("\n" + "=" * 70) print("CONCLUSION") print("=" * 70) is_uniform = abs(results['deviation_from_uniform']) < 0.05 is_clustered = (results['best_silhouette'] or 0) > 0.5 has_secondary_antipodal = results['secondary_antipodal_pairs'] >= 3 full_rank = results['utilization'] > 0.95 if is_uniform and not is_clustered and not has_secondary_antipodal and full_rank: print("\n✓ CLAIM 3 SUPPORTED:") print(" The 14 axes are uniformly distributed on ℝP² with no") print(" further collapse. G-Cand is a 14-axis projective solver.") print(" The 'sphere-norm V=32 D=3' was a mislabeling of 14 axes.\n") print(" IMPLICATION: For inference, project trained sphere outputs") print(" to ℝP^(D-1) and read as axes, not points. The polygonal") print(" geometry is implicit in the trained sphere-solver.") elif is_clustered: print("\n✗ CLAIM 3 PARTIALLY REFUTED:") print(" Axes have cluster structure on ℝP² — they are not") print(" uniformly distributed. Either the projective space isn't") print(" the right reading, or the clusters reveal a finer polytope") print(" structure (e.g., axes prefer specific directions on ℝP²).") elif has_secondary_antipodal: print("\n✗ CLAIM 3 REFUTED — geometry collapses further:") print(" Axes show NEW antipodal pairs after the first collapse.") print(" G-Cand has more degenerate geometry than ℝP² — possibly") print(" effective dimension < 3.") elif not full_rank: print("\n✗ CLAIM 3 REFUTED — dimension collapse:") print(" Effective rank of the axes is below 3. The trained model") print(" used less than the full D=3 space.") else: print("\n? CLAIM 3 PARTIALLY SUPPORTED:") print(" Axes are full-rank and don't show secondary collapse,") print(" but distribution deviates from uniform ℝP² baseline.") print(" Some structure beyond simple uniform projection.") return output_data if __name__ == '__main__': results = main()