import json, math, random import numpy as np import torch from torch import nn SEED = 2841 np.random.seed(SEED) random.seed(SEED) torch.manual_seed(SEED) # Two-node bidirectional instance of the paper's phase Jacobian. def jacobian(phi, kappa=1.0): c = math.cos(phi) return kappa * np.array([[-c, c], [c, -c]], dtype=float) def math_checks(): # Prediction 1: global phase is neutral: J @ 1 = 0. shifts = np.linspace(0, math.pi, 25) neutral_err = max(float(np.linalg.norm(jacobian(p) @ np.ones(2))) for p in shifts) # Prediction 2: transverse boundary is phi=pi/2 and lambda=-2*kappa*cos(phi). grid = np.linspace(0, math.pi, 10001) lambdas = -2.0 * np.cos(grid) boundary = float(grid[np.argmin(np.abs(lambdas))]) # Prediction 3: finite-Euler perturbation slope agrees with the transverse eigenvalue. decay_rows = [] dt, steps, eps = 0.002, 1500, 1e-5 for phi in [0.0, math.pi/6, math.pi/3, 0.70*math.pi/2, math.pi/2, 0.60*math.pi]: kappa = 0.8 J = jacobian(phi, kappa) d = np.array([eps, -eps], dtype=float) norms = [] for _ in range(steps): norms.append(np.linalg.norm(d)) d = d + dt * J.dot(d) fit_n = np.arange(100, 1100) slope = float(np.polyfit(fit_n*dt, np.log(np.maximum(np.asarray(norms)[fit_n], 1e-30)), 1)[0]) predicted = -2.0*kappa*math.cos(phi) decay_rows.append({ "phi": float(phi), "predicted_lambda": float(predicted), "measured_slope": slope, "stable_predicted": bool(predicted < 0), "stable_measured": bool(slope < -1e-5), "abs_error": abs(slope-predicted), }) # Sweep boundary directly and record signs. boundary_sweep = [] for phi in np.linspace(0, math.pi, 9): eig = np.linalg.eigvals(jacobian(phi, 1.0)) transverse = float(sorted(eig)[0]) boundary_sweep.append({"phi_over_pi": float(phi/math.pi), "transverse_eigenvalue": transverse}) return { "neutral_mode_max_norm": neutral_err, "predicted_boundary_phi": math.pi/2, "measured_boundary_phi": boundary, "boundary_error": abs(boundary-math.pi/2), "decay_scaling": decay_rows, "boundary_sweep": boundary_sweep, } class PhaseRNN(nn.Module): def __init__(self, hidden=16): super().__init__() self.hidden = hidden self.inp = nn.Linear(1, hidden, bias=False) self.omega = nn.Parameter(torch.randn(hidden)*0.15) # Fixed, positive directed ring with a skip edge; stable synchronized offsets. A = torch.zeros(hidden, hidden) for i in range(hidden): A[i, (i+1) % hidden] = 0.35 A[i, (i+2) % hidden] = 0.15 self.register_buffer("A", A) self.readout = nn.Linear(2*hidden, 2) def forward(self, x): b, t, _ = x.shape theta = torch.zeros(b, self.hidden, device=x.device) outs = [] h = 0.15 kappa = 0.8 for n in range(t): drive = self.inp(x[:, n]) diff = theta[:, None, :] - theta[:, :, None] # theta_j-theta_i coupling = torch.einsum("bij,ij->bi", torch.sin(diff), self.A) theta = theta + h*(self.omega + 0.08*drive + kappa*coupling) outs.append(torch.cat([torch.cos(theta), torch.sin(theta)], dim=1)) return self.readout(outs[-1]) class TanhRNN(nn.Module): def __init__(self, hidden=16): super().__init__() self.cell = nn.RNNCell(1, hidden, nonlinearity="tanh") self.readout = nn.Linear(hidden, 2) self.hidden = hidden def forward(self, x): b, t, _ = x.shape h = torch.zeros(b, self.hidden, device=x.device) for n in range(t): h = self.cell(x[:, n], h) return self.readout(h) def train_model(model, train_x, train_y, test_x, test_y, device, steps=220): model.to(device) opt = torch.optim.Adam(model.parameters(), lr=0.01) loss_fn = nn.CrossEntropyLoss() model.train() for step in range(steps): idx = torch.randint(0, len(train_x), (64,), device=device) loss = loss_fn(model(train_x[idx]), train_y[idx]) opt.zero_grad(); loss.backward(); opt.step() model.eval() with torch.no_grad(): pred = model(test_x).argmax(1) acc = float((pred == test_y).float().mean().cpu()) test_loss = float(loss_fn(model(test_x), test_y).cpu()) return {"accuracy": acc, "loss": test_loss, "steps": steps} def mini_experiment(): # Simple sequential sine-frequency classification: task has genuine temporal memory. rng = np.random.default_rng(SEED) def make(n): xs, ys = [], [] for _ in range(n): label = int(rng.integers(0, 2)) freq = 0.22 if label == 0 else 0.39 phase = rng.uniform(0, 2*math.pi) t = np.arange(24) seq = np.sin(freq*t + phase) + 0.18*rng.normal(size=24) xs.append(seq[:, None]); ys.append(label) return torch.tensor(np.asarray(xs), dtype=torch.float32), torch.tensor(ys) train_x, train_y = make(512); test_x, test_y = make(256) try: device = "cuda" if torch.cuda.is_available() else "cpu" torch.set_num_threads(4) a = train_model(PhaseRNN(), train_x.to(device), train_y.to(device), test_x.to(device), test_y.to(device), device) torch.manual_seed(SEED) b = train_model(TanhRNN(), train_x.to(device), train_y.to(device), test_x.to(device), test_y.to(device), device) except Exception as exc: device = "cpu" torch.manual_seed(SEED) a = train_model(PhaseRNN(), train_x, train_y, test_x, test_y, device) torch.manual_seed(SEED) b = train_model(TanhRNN(), train_x, train_y, test_x, test_y, device) return {"device": device, "phase_rnn": a, "tanh_rnn": b, "fallback_error": repr(exc)} return {"device": device, "phase_rnn": a, "tanh_rnn": b} if __name__ == "__main__": result = {"math": math_checks(), "mini_experiment": mini_experiment()} with open("results.json", "w") as f: json.dump(result, f, indent=2) print(json.dumps(result, indent=2))