import json, random, sys from pathlib import Path import numpy as np import torch import torch.nn as nn sys.path.insert(0, '/home/maxwelhelp/all/math2nn') from bench import get_dataset, make_model, train_model, sweep_baseline, make_report, evaluate TRACK = 'dynamics' MODEL = 'rnn_small' SEEDS = tuple(range(8)) SWEEP_SEEDS = tuple(range(4)) def seed_all(seed): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed) class SphereRNN(nn.Module): """GRU-sized recurrent predictor with a dissipative ring-cubic transition.""" def __init__(self, out_dim, hidden=64, lam=1.0, dt=0.05, cubic_scale=1.0): super().__init__() self.hidden = hidden self.lam = lam self.dt = dt self.cubic_scale = cubic_scale self.inp = nn.Linear(3, hidden) self.head = nn.Linear(hidden, out_dim) self.angular_raw = nn.Parameter(torch.empty(hidden, hidden)) nn.init.normal_(self.angular_raw, std=0.025) nn.init.normal_(self.inp.weight, std=0.06) nn.init.zeros_(self.inp.bias) nn.init.normal_(self.head.weight, std=0.06) nn.init.zeros_(self.head.bias) def forward(self, x, return_hidden=False): seq = x.view(x.shape[0], -1, 3) h = torch.zeros(x.shape[0], self.hidden, device=x.device, dtype=x.dtype) A = self.angular_raw - self.angular_raw.T for t in range(seq.shape[1]): drive = self.inp(seq[:, t]) z = torch.roll(h, shifts=-1, dims=-1) # q(y,z)=-y^3-y*z^2; its radial form is strictly negative. q = -self.cubic_scale * (h.pow(3) + h * z.pow(2)) hdot = self.lam * h + h @ A.T + q + drive h = h + self.dt * hdot out = self.head(h) return (out, h) if return_hidden else out def train_one(kind, seed, cfg, capture=False): seed_all(seed) ds = get_dataset(TRACK, seed, n_train=4000, n_test=1000) if kind == 'baseline': model = make_model(MODEL, ds['input_shape'], ds['out_dim']) else: model = SphereRNN(ds['out_dim'], hidden=64, lam=cfg.get('lam', 1.0), dt=cfg.get('dt', 0.05), cubic_scale=cfg.get('cubic_scale', 1.0)) net, metric, history = train_model(model, ds, epochs=cfg['epochs'], lr=cfg['lr'], batch=128, weight_decay=cfg.get('weight_decay', 0.0), log=lambda *_: None) if net is None: return float('nan') if not capture else (float('nan'), None) if not capture: return float(metric) device = next(net.parameters()).device with torch.no_grad(): _, h = net(ds['xte'].to(device), return_hidden=True) norms = h.norm(dim=1).detach().cpu().numpy() # Re-test the trained field on its actual final hidden states. z = torch.roll(h, -1, dims=-1) q = -net.cubic_scale * (h.pow(3) + h * z.pow(2)) u = h / (h.norm(dim=1, keepdim=True) + 1e-8) radial = (u * (-net.cubic_scale * (u.pow(3) + u * torch.roll(u, -1, dims=-1).pow(2)))).sum(1) relax = (net.lam * h + h @ (net.angular_raw - net.angular_raw.T).T + q) radial_velocity = (u * relax).sum(1).detach().cpu().numpy() return float(metric), {'norm_mean': float(norms.mean()), 'norm_std': float(norms.std()), 'radial_coeff_mean': float(radial.mean().detach().cpu()), 'radial_coeff_std': float(radial.std().detach().cpu()), 'radial_velocity_mean': float(radial_velocity.mean()), 'predicted_radius_scalar': float((net.lam / net.cubic_scale) ** 0.5), 'predicted_relaxation_rate': float(2 * net.lam)} def main(): # Baseline and idea share the complete lr union; baseline central knobs include lr and weight decay. lrs = [1e-3, 3e-3, 1e-2] wd = [0.0] epochs = 20 base_grid = [{'lr': lr, 'weight_decay': w, 'epochs': epochs} for lr in lrs for w in wd] base = sweep_baseline(lambda cfg: lambda s: train_one('baseline', s, cfg), base_grid, seeds=SWEEP_SEEDS) # Explicitly evaluate the best and two nearby settings on all eight paired seeds. idea_grid = [{'lr': lr, 'epochs': epochs, 'lam': 1.0, 'dt': 0.05, 'cubic_scale': 1.0} for lr in lrs] idea_trials = [] for cfg in idea_grid: r = evaluate(lambda s, c=cfg: train_one('idea', s, c), seeds=SEEDS) idea_trials.append({'cfg': cfg, 'result': r}) best_trial = min(idea_trials, key=lambda z: z['result']['mean']) idea = best_trial['result'] sigs = [train_one('idea', s, best_trial['cfg'], capture=True)[1] for s in SEEDS] sig = {k: float(np.mean([x[k] for x in sigs])) for k in sigs[0]} # Quantitative NN-scale signature: dissipativity and observed radial relaxation direction. sig['radial_upper_bound'] = -1.0 / 64.0 sig['confirmed'] = bool(sig['radial_coeff_mean'] < 0 and sig['radial_coeff_mean'] <= sig['radial_upper_bound'] * 0.5) report = make_report(TRACK, MODEL, base, idea, extra={ 'prediction': 'trained final states have negative cubic radial coefficient and relax toward finite norm', 'trained_model_signature': sig, 'idea_trials': idea_trials, 'custom_track': None }) Path('bench_report.json').write_text(json.dumps(report, indent=2)) print(json.dumps(report, indent=2)) if __name__ == '__main__': main()