import json import numpy as np from residual_screened_koopman import eig_residuals, screen_mask, spectral_forecast def trajectory(lams, x0, T): z=np.zeros((T,len(lams))); z[0]=x0 for t in range(T-1): z[t+1]=lams*z[t] return z def run(): # Prediction 1: exact eigenpairs have zero residual without corruption. lams=np.array([0.8,0.98]); clean=trajectory(lams,[1.,1.],500) r0=eig_residuals(np.diag(lams),clean)[2] # Prediction 2: iid observation noise makes residual energy scale as sigma^2. sigmas=np.array([0.,.01,.02,.04,.08]); obs=[] for j,s in enumerate(sigmas): a=[] for q in range(40): g=np.random.default_rng(1000+j*100+q) y=clean+s*g.normal(size=clean.shape) a.append(np.mean(eig_residuals(np.diag(lams),y)[2]**2)) obs.append(float(np.mean(a))) slope=float(np.polyfit(sigmas[1:4]**2,np.array(obs[1:4]),1)[0]) # Theoretical small-noise slope, accounting for finite clean denominator. preds=[] n=len(clean)-1 for s in sigmas: p=[] for i,lam in enumerate(lams): den=np.sum(clean[:-1,i]**2) p.append(s*s*(1+lam*lam)*n/(den+s*s*n)) preds.append(float(np.mean(p))) # Prediction 3: sparse corruption produces residual energy approximately # proportional to corruption rate at fixed impulse amplitude. rates=np.array([0.,.002,.005,.01,.02,.04]); impulse=.25; rate_obs=[] for j,p in enumerate(rates): a=[] for q in range(40): g=np.random.default_rng(5000+j*100+q) hit=(g.random(clean.shape)