import json, math, os, random import numpy as np SEED=1729 np.random.seed(SEED); random.seed(SEED) # ---------- Markov cocycle utilities ---------- def stationary(P): w,v=np.linalg.eig(P.T) i=np.argmin(abs(w-1)); p=np.real(v[:,i]); p=p/p.sum() return p def lyap(A,P,n=30000,seed=7): rng=np.random.default_rng(seed); k=len(A); d=A[0].shape[0] x=np.eye(d); smax=0.; smin=0.; z=rng.integers(k) p=stationary(P) for t in range(n): z=rng.choice(k,p=P[z]) if t else z x=A[z]@x if (t+1)%8==0: q,r=np.linalg.qr(x); diag=np.maximum(abs(np.diag(r)),1e-300) smax += math.log(diag.max()); smin += math.log(diag.min()); x=q return np.array([smax,smin])/(n) def spectral(M): return np.linalg.svd(M,compute_uv=False)[0] # Conformal zero-gap cocycle: scalar rotations have identical exponents exactly. def rotation(th): return np.array([[math.cos(th),-math.sin(th)],[math.sin(th),math.cos(th)]]) P=np.array([[.82,.18],[.27,.73]]) r=np.array([.985,1.015]); th=np.array([.31,-.47]) A=np.array([r[i]*rotation(th[i]) for i in range(2)]) base=lyap(A,P) # perturb mode 0 radially; d is exactly matrix spectral distance for this family sweep=[] for d in np.logspace(-5,-1,9): At=A.copy(); At[0]=(r[0]+d)*rotation(th[0]) e=lyap(At,P, n=18000, seed=11) delta=abs(e[0]-base[0]) sweep.append({'d':float(d),'delta_lambda':float(delta),'gap':float(e[0]-e[1]), 'linear_ratio':float(delta/d), 'invlog_half':float(1/math.sqrt(abs(math.log(d)))), 'invlog_one':float(1/abs(math.log(d)))}) # Estimate empirical constants from finite perturbations, then test held-out sizes. C1=max(x['delta_lambda']*abs(math.log(x['d']))**.5 for x in sweep) Cconf=max(x['delta_lambda']*abs(math.log(x['d'])) for x in sweep) # directional random perturbations for a more meaningful conservative calibration cal=[] for j in range(12): d=10**np.random.uniform(-5,-2) H=np.random.randn(2,2); H=H/spectral(H)*d At=A.copy(); At[0]=A[0]+H e=lyap(At,P,n=12000,seed=j+30) cal.append(abs(e[0]-base[0])*abs(math.log(d))) Cconf=max(Cconf, float(np.quantile(cal,.9))) # Verify three predictions: zero gap, delta scales linearly locally, inverse-log envelope. small=sweep[:5] logd=np.log([x['d'] for x in small]); logdel=np.log(np.maximum([x['delta_lambda'] for x in small],1e-14)) slope=float(np.polyfit(logd,logdel,1)[0]) envelope=[x['delta_lambda'] <= Cconf/abs(math.log(x['d'])) for x in sweep] # Trust-region acceptance sweep: proposal is accepted iff observed change is under modulus. accept=[bool(x['delta_lambda'] <= Cconf/abs(math.log(x['d']))) for x in sweep] # ---------- Small RNN optimizer sanity experiment ---------- try: import torch torch.manual_seed(SEED); torch.set_num_threads(4) device='cuda' if torch.cuda.is_available() else 'cpu' except Exception: torch=None; device='cpu' def train(control, seed): if torch is None: return {'loss':None,'acc':None,'backtracks':None,'device':'numpy'} try: torch.manual_seed(seed); np.random.seed(seed) dev=torch.device(device) n=512; T=18; h=24 X=torch.randn(n,T,4,device=dev); y=(X[:,:T//2].sum((1,2))>0).long() class Net(torch.nn.Module): def __init__(self): super().__init__(); self.W=torch.nn.Parameter(torch.randn(h,h,device=dev)*.16) self.U=torch.nn.Parameter(torch.randn(h,4,device=dev)*.18); self.v=torch.nn.Parameter(torch.randn(h,device=dev)*.1); self.b=torch.nn.Parameter(torch.zeros(2,device=dev)) def forward(self,x): z=torch.zeros(x.size(0),h,device=dev) for t in range(T): z=torch.tanh(z@self.W.T+x[:,t]@self.U.T) return torch.stack([z@self.v+self.b[0],-z@self.v+self.b[1]],1) net=Net(); opt=torch.optim.AdamW(net.parameters(),lr=.035,weight_decay=1e-4) oldW=net.W.detach().cpu().numpy().copy(); bt=0; accepted=0 # Conservative C translated from exact toy calibration; stability budget is zero log spectral growth. for step in range(90): opt.zero_grad(); loss=torch.nn.functional.cross_entropy(net(X),y); loss.backward() torch.nn.utils.clip_grad_norm_(net.parameters(),5.0) saved={k:v.detach().clone() for k,v in net.state_dict().items()} opt.step() Wn=net.W.detach().cpu().numpy(); d=float(spectral(Wn-oldW)) # For the recurrent matrix, use a two-mode proxy with identical matrices. oldexp=math.log(max(spectral(oldW),1e-8)); newexp=math.log(max(spectral(Wn),1e-8)); dl=abs(newexp-oldexp) bound=float(Cconf/max(abs(math.log(max(d,1e-8))),1e-8)) bad=control and (dl>bound or newexp>.08) if bad: net.load_state_dict(saved); # undo and halve this optimizer's effective step by interpolation for p in net.parameters(): if p.grad is not None: p.data.add_(p.grad,alpha=-.0175) # half lr SGD fallback bt+=1 else: accepted+=1 oldW=net.W.detach().cpu().numpy().copy() with torch.no_grad(): pred=net(X).argmax(1); acc=float((pred==y).float().mean().cpu()) return {'loss':float(loss.detach().cpu()),'acc':acc,'backtracks':bt,'accepted':accepted,'device':str(dev)} except Exception as e: return {'error':str(e),'device':'fallback'} runs=[] for s in [0,1,2]: runs.append({'seed':s,'baseline':train(False,s),'idea':train(True,s)}) def mean(key): vals=[r[key].get('loss') for r in runs if r[key].get('loss') is not None] return float(np.mean(vals)) if vals else None def meanacc(key): vals=[r[key].get('acc') for r in runs if r[key].get('acc') is not None] return float(np.mean(vals)) if vals else None out={'seed':SEED,'base_exponents':base.tolist(),'sweep':sweep,'predictions':{ 'P1_zero_gap_max_abs_gap':float(max(abs(x['gap']) for x in sweep)), 'P2_local_loglog_slope_expected_1':slope, 'P3_inverse_log_envelope_fraction':float(np.mean(envelope)), 'P3_C_conformal':float(Cconf),'acceptance_by_d':accept}, 'rnn_runs':runs,'rnn_summary':{'baseline_loss':mean('baseline'),'idea_loss':mean('idea'),'baseline_acc':meanacc('baseline'),'idea_acc':meanacc('idea')}} with open('results.json','w') as f: json.dump(out,f,indent=2) print(json.dumps(out,indent=2))