Harmonic-Mode Branch for Topological Memory / harmonic_experiment.py
Mechanism confirmed, baseline not beaten
1import json
2from pathlib import Path
3import numpy as np
4
5# Harmonic-mode branch: minimal numerical verification of the stated decomposition.
6# The synthetic PSD operator is equivalent to a Hodge Laplacian in an orthonormal basis:
7# L = Q diag(0,...,0, positive eigenvalues) Q^T.
8
9def make_operator(seed=7, dim=12, harmonic_dim=2):
10 rng = np.random.default_rng(seed)
11 q, _ = np.linalg.qr(rng.normal(size=(dim, dim)))
12 positive = np.array([0.35, 0.8, 1.7, 2.6, 3.9, 5.2, 6.4, 7.5, 9.0, 11.0])[: dim-harmonic_dim]
13 eig = np.concatenate([np.zeros(harmonic_dim), positive])
14 L = q @ np.diag(eig) @ q.T
15 H = q[:, :harmonic_dim]
16 P_h = H @ H.T
17 P_p = np.eye(dim) - P_h
18 return L, H, P_h, P_p, eig, q
19
20
21def fit_log_slope(values, dt):
22 # Ignore the t=0 point and fit where values are safely above roundoff.
23 vals = np.asarray(values)
24 t = np.arange(len(vals)) * dt
25 keep = (vals > 1e-11) & (np.arange(len(vals)) > 2)
26 return float(np.polyfit(t[keep], np.log(vals[keep]), 1)[0])
27
28
29def run():
30 L, H, P_h, P_p, eig, Q = make_operator()
31 dim, k = L.shape[0], H.shape[1]
32 lam_pos = float(np.min(eig[eig > 0]))
33 lam_max = float(np.max(eig))
34 rng = np.random.default_rng(123)
35 h0 = H @ np.array([1.25, -0.7])
36 # Exact slowest dissipative mode makes the continuous-rate prediction sharp.
37 u0 = Q[:, k] * 1.0
38 z0 = h0 + u0
39
40 # Prediction 1: projected branch has constant harmonic coordinates, while a
41 # fully damped baseline incorrectly erases them.
42 nu = 0.9
43 dt = 0.01
44 steps = 900
45 z_split = z0.copy()
46 z_full = z0.copy()
47 harmonic_errors = []
48 for _ in range(steps):
49 # N is identically zero here; projected nonlinear N would preserve the same invariant.
50 z_split = z_split + dt * (-nu * (L @ (P_p @ z_split)))
51 z_split = P_h @ z_split + P_p @ z_split # explicit numerical projection
52 z_full = z_full + dt * (-nu * (L @ z_full) - nu * (P_h @ z_full))
53 harmonic_errors.append(np.linalg.norm(P_h @ (z_split - z0)))
54 invariant_error = float(max(harmonic_errors))
55 baseline_memory_retention = float(np.linalg.norm(P_h @ z_full) / np.linalg.norm(h0))
56 split_memory_retention = float(np.linalg.norm(P_h @ z_split) / np.linalg.norm(h0))
57
58 # Prediction 2: dissipative amplitude has log slope -nu*lambda_1^+.
59 nus = [0.25, 0.5, 0.9, 1.4, 2.0]
60 slope_rows = []
61 for n in nus:
62 x = u0.copy()
63 amps = []
64 for _ in range(500):
65 amps.append(np.linalg.norm(x))
66 x = x + dt * (-n * (L @ x))
67 observed = fit_log_slope(amps, dt)
68 predicted = -n * lam_pos
69 slope_rows.append({"nu": n, "predicted": predicted, "observed": observed,
70 "abs_error": abs(observed - predicted)})
71
72 # Prediction 3: explicit Euler is stable iff dt*nu*lambda_max < 2 for PSD L.
73 n_stab = 1.0
74 predicted_dt = 2.0 / (n_stab * lam_max)
75 factors = [0.90, 0.99, 1.01, 1.10]
76 stability_rows = []
77 x_init = Q[:, -1]
78 for f in factors:
79 d = f * predicted_dt
80 x = x_init.copy()
81 norms = []
82 for _ in range(80):
83 norms.append(float(np.linalg.norm(x)))
84 x = x - d * n_stab * (L @ x)
85 # Stable side remains bounded and decays; unstable side grows geometrically.
86 observed_stable = max(norms) < 10.0 and norms[-1] <= norms[0]
87 stability_rows.append({"factor_of_boundary": f, "dt": d,
88 "predicted": "stable" if f < 1 else "unstable",
89 "observed": "stable" if observed_stable else "unstable",
90 "final_norm": norms[-1]})
91
92 # Small trajectory comparison: standard/full damping vs topology-preserving split.
93 # Both use exactly the same operator, timestep, and initialization.
94 comparison = {
95 "split_harmonic_retention": split_memory_retention,
96 "fully_damped_harmonic_retention": baseline_memory_retention,
97 "split_dissipative_final_norm": float(np.linalg.norm(P_p @ z_split)),
98 "fully_damped_final_norm": float(np.linalg.norm(z_full)),
99 }
100 result = {
101 "operator_dim": dim, "harmonic_dim": k, "lambda_1_positive": lam_pos,
102 "lambda_max": lam_max, "predicted_euler_boundary_dt": predicted_dt,
103 "prediction_1_invariant_max_error": invariant_error,
104 "prediction_1_tolerance": 1e-12,
105 "prediction_2_decay_sweep": slope_rows,
106 "prediction_3_stability_sweep": stability_rows,
107 "comparison": comparison,
108 "claims": {
109 "harmonic_invariant": invariant_error < 1e-12 and split_memory_retention > 0.999999,
110 "decay_scaling": max(r["abs_error"] for r in slope_rows) < 0.01,
111 "euler_boundary": all(r["observed"] == r["predicted"] for r in stability_rows),
112 }
113 }
114 Path("results.json").write_text(json.dumps(result, indent=2))
115 print(json.dumps(result, indent=2))
116
117if __name__ == "__main__":
118 run()