import sys, json, random 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, evaluate, sweep_baseline, make_report SEEDS = tuple(range(8)) # Same lr union on both sides; baseline also sweeps its central Adam knob. LR_GRID = (1e-3, 3e-3, 6e-3) BASE_GRID = [{'lr': lr, 'weight_decay': wd, 'epochs': 10} for lr in LR_GRID for wd in (0.0, 1e-4)] IDEA_GRID = [{'lr': lr, 'weight_decay': 0.0, 'epochs': 10, 'lam': 0.02} for lr in LR_GRID] 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) def device(): return 'cuda' if torch.cuda.is_available() else 'cpu' def spectral_rate(phi, K=1.0): """Differentiable smallest non-gauge eigenvalue for an 8-node ring. phi: [batch, 8]. The symmetric composite Laplacian is PSD when locked. A unit ring is used, matching one shared phase-delay graph for all samples. """ n = phi.shape[1] A = torch.zeros((n, n), device=phi.device, dtype=phi.dtype) idx = torch.arange(n, device=phi.device) A[idx, (idx + 1) % n] = 1.0 A[idx, (idx - 1) % n] = 1.0 C = A[None] * torch.cos(phi[:, None, :] - phi[:, :, None]) L = torch.diag_embed(C.sum(-1)) - C ev = torch.linalg.eigvalsh(L) return K * ev[:, 1], ev def forward_hidden(net, x): seq = x.view(x.shape[0], -1, 3) try: out, h = net.rnn(seq) except RuntimeError: old = torch.backends.cudnn.enabled torch.backends.cudnn.enabled = False try: out, h = net.rnn(seq) finally: torch.backends.cudnn.enabled = old return net.head(h[-1]), out def train_idea(seed, cfg, return_signature=False): seed_all(seed) ds = get_dataset('dynamics', seed, n_train=400, n_test=400) net = make_model('rnn_small', ds['input_shape'], ds['out_dim']) dev = device() x, y = ds['xtr'].to(dev), ds['ytr'].to(dev) xt, yt = ds['xte'].to(dev), ds['yte'].to(dev) opt = torch.optim.Adam(net.parameters(), lr=cfg['lr'], weight_decay=cfg['weight_decay']) mse = nn.MSELoss() batch = 128 net.to(dev) rates, gaps = [], [] try: for _ in range(cfg['epochs']): perm = torch.randperm(len(x), device=dev) net.train() for ix in perm.split(batch): pred, hseq = forward_hidden(net, x[ix]) # Hidden coordinates are a learned phase plane. The intervention # penalizes insufficient composite-Laplacian contraction. phi = torch.atan2(hseq[..., 1], hseq[..., 0]) rq, _ = spectral_rate(phi) spec_loss = torch.relu(0.20 - rq).pow(2).mean() loss = mse(pred, y[ix]) + cfg['lam'] * spec_loss opt.zero_grad(); loss.backward(); torch.nn.utils.clip_grad_norm_(net.parameters(), 5.0); opt.step() net.eval() with torch.no_grad(): pred, hs = forward_hidden(net, xt) metric = float(mse(pred, yt).cpu()) ph = torch.atan2(hs[..., 1], hs[..., 0]) rr, _ = spectral_rate(ph) # A directly observed NN-scale locking statistic: adjacent phase # disagreement in the trained recurrent trajectories. gap = torch.mean(torch.abs(torch.atan2(torch.sin(ph[:, 1:] - ph[:, :-1]), torch.cos(ph[:, 1:] - ph[:, :-1])))) rates.append(float(rr.mean().cpu())); gaps.append(float(gap.cpu())) except RuntimeError: # Robust shared-GPU fallback: rerun this small experiment on CPU. if dev != 'cpu': return train_idea_cpu(seed, cfg, return_signature) raise result = (metric, {'rate': float(np.mean(rates)), 'phase_gap': float(np.mean(gaps))}) return result if return_signature else metric def train_idea_cpu(seed, cfg, return_signature=False): old = torch.cuda.is_available # The actual function is device-selected by CUDA availability; make a CPU # equivalent explicit for environments where CUDA allocation fails. seed_all(seed); ds = get_dataset('dynamics', seed, 400, 400) net = make_model('rnn_small', ds['input_shape'], ds['out_dim']).cpu() x,y,xt,yt = ds['xtr'],ds['ytr'],ds['xte'],ds['yte']; opt=torch.optim.Adam(net.parameters(),lr=cfg['lr'],weight_decay=cfg['weight_decay']); mse=nn.MSELoss() for _ in range(cfg['epochs']): for ix in torch.randperm(len(x)).split(128): pred,hs=forward_hidden(net,x[ix]); ph=torch.atan2(hs[...,1],hs[...,0]); rr,_=spectral_rate(ph); loss=mse(pred,y[ix])+cfg['lam']*torch.relu(.20-rr).pow(2).mean(); opt.zero_grad(); loss.backward(); opt.step() with torch.no_grad(): pred,hs=forward_hidden(net,xt); ph=torch.atan2(hs[...,1],hs[...,0]); rr,_=spectral_rate(ph); gap=torch.mean(torch.abs(torch.atan2(torch.sin(ph[:,1:]-ph[:,:-1]),torch.cos(ph[:,1:]-ph[:,:-1])))) out=(float(mse(pred,yt)),{'rate':float(rr.mean()),'phase_gap':float(gap)}) return out if return_signature else out[0] def make_base(cfg): def run(seed): seed_all(seed); ds=get_dataset('dynamics',seed,400,400); net,metric,_=train_model(make_model('rnn_small',ds['input_shape'],ds['out_dim']),ds,epochs=cfg['epochs'],lr=cfg['lr'],weight_decay=cfg['weight_decay'],batch=128,log=lambda *_:None); return metric return run def main(): base=sweep_baseline(make_base, BASE_GRID, seeds=(0,1,2,3)) # Evaluate all idea settings on all paired seeds; select by full-seed mean. idea_runs=[] for cfg in IDEA_GRID: vals=[train_idea(s,cfg) for s in SEEDS] idea_runs.append((float(np.mean(vals)),cfg,vals)) _,best_cfg,best_vals=min(idea_runs,key=lambda z:z[0]) idea={'mean':float(np.mean(best_vals)),'std':float(np.std(best_vals)),'per_seed':[float(v) for v in best_vals],'n':8} sig=[] for s in SEEDS: v= train_idea(s,best_cfg,True); sig.append(v[1]) # Baseline trained-model signature uses the same hidden observables. bsig=[] for s in SEEDS: seed_all(s); ds=get_dataset('dynamics',s,400,400); net,_,_=train_model(make_model('rnn_small',ds['input_shape'],ds['out_dim']),ds,epochs=base['best_cfg']['epochs'],lr=base['best_cfg']['lr'],weight_decay=base['best_cfg']['weight_decay'],batch=128,log=lambda *_:None) net.eval(); dev=next(net.parameters()).device with torch.no_grad(): _,hs=forward_hidden(net,ds['xte'].to(dev)); ph=torch.atan2(hs[...,1],hs[...,0]); rr,_=spectral_rate(ph); gap=torch.mean(torch.abs(torch.atan2(torch.sin(ph[:,1:]-ph[:,:-1]),torch.cos(ph[:,1:]-ph[:,:-1])))); bsig.append({'rate':float(rr.mean()),'phase_gap':float(gap)}) br={'rate':float(np.mean([z['rate'] for z in bsig])),'phase_gap':float(np.mean([z['phase_gap'] for z in bsig]))} ir={'rate':float(np.mean([z['rate'] for z in sig])),'phase_gap':float(np.mean([z['phase_gap'] for z in sig]))} signature={'prediction':'spectral penalty should raise hidden phase rate and reduce adjacent phase gap','baseline_observed':br,'idea_observed':ir,'predicted_rate_change':ir['rate']-br['rate'],'observed_gap_change':ir['phase_gap']-br['phase_gap'],'confirmed':bool(ir['rate']>br['rate'] and ir['phase_gap']