Intrinsic-Rank Filter Memory for Actor-Critic / experiment.py
Mechanism confirmed, baseline not beaten
1import numpy as np
2from scipy.linalg import expm
3
4
5def discretize(A, B, dt):
6 n, k = A.shape[0], B.shape[1]
7 aug = np.zeros((n + k, n + k)); aug[:n, :n] = A; aug[:n, n:] = B
8 E = expm(aug * dt)
9 return E[:n, :n], E[:n, n:]
10
11
12def simulate(m=2, p=2, N=5000, dt=.03, noise=0., seed=0, warmup=1000):
13 rng = np.random.default_rng(seed); n = 2
14 A = np.array([[-.35, .8], [-.6, -.55]])
15 Bbase = np.array([[1.0, .25], [.15, 1.0]])
16 B = Bbase[:, :m]
17 Cbase = np.array([[1., .25], [-.35, 1.], [.4, -.7]])
18 C = Cbase[:p]
19 Af = np.array([[0., 1.], [-2.25, -1.5]]); bf = np.array([[0.], [1.]])
20 Ad, Bd = discretize(A, B, dt); Df, Gf = discretize(Af, bf, dt)
21 x = np.zeros(n); zu = np.zeros((m, n)); zy = np.zeros((p, n)); rows=[]; ys=[]
22 for t in range(N + warmup):
23 u = .8*rng.normal(size=m) + .35*np.sin(.011*t + np.arange(m))
24 y = C @ x + noise*rng.normal(size=p)
25 zu = (Df @ zu.T).T + u[:, None]*Gf[:, 0][None, :]
26 zy = (Df @ zy.T).T + y[:, None]*Gf[:, 0][None, :]
27 x = Ad @ x + Bd @ u
28 if t >= warmup: rows.append(np.r_[zu.ravel(), zy.ravel()]); ys.append(y)
29 return np.asarray(rows), np.asarray(ys)
30
31
32def svd_info(Z, tau=1e-3):
33 s = np.linalg.svd(Z-Z.mean(0), compute_uv=False)
34 return s, int(np.sum(s/s[0] > tau))
35
36
37def features(H):
38 return np.column_stack([H[:,i]*H[:,j] for i in range(H.shape[1]) for j in range(i,H.shape[1])])
39
40
41def condition(X):
42 s=np.linalg.svd(X,compute_uv=False)
43 return float(s[0]/max(s[-1],1e-30))
44
45
46def main():
47 np.set_printoptions(precision=4,suppress=True)
48 print('PREDICTION 1: SIMO 2n=4; MIMO n(m+1)=6')
49 for m,p,label,expected in [(1,2,'SIMO',4),(2,2,'MIMO',6)]:
50 Z,_=simulate(m=m,p=p,seed=3); s,r=svd_info(Z)
51 print(f'{label}: ambient={Z.shape[1]}, predicted={expected}, observed={r}, ratios={s/s[0]}')
52
53 print('\nPREDICTION 2: filter transient decays and rank approaches theorem')
54 for warmup in [0,10,100,300,1000,3000]:
55 Z,_=simulate(m=2,p=2,seed=4,warmup=warmup); s,r=svd_info(Z)
56 print(f'warmup={warmup:4d}, rank(tau=1e-3)={r}, s7/s1={s[6]/s[0]:.3e}, s8/s1={s[7]/s[0]:.3e}')
57
58 print('\nPREDICTION 2: observation-noise tail scales linearly with noise')
59 for noise in [0., 1e-5, 1e-4, 1e-3, 1e-2]:
60 Z,_=simulate(m=2,p=2,seed=5,noise=noise); s,_=svd_info(Z)
61 tail=float(np.mean(s[6:]/s[0]))
62 print(f'noise={noise:.0e}, tail_mean/s1={tail:.3e}, tail/noise={tail/max(noise,1e-30):.3e}')
63
64 print('\nPREDICTION 3: cutoff transition when tau crosses noisy tail')
65 Z,_=simulate(m=2,p=2,seed=5,noise=1e-3); s,_=svd_info(Z)
66 print('singular_ratios=',s/s[0])
67 for tau in [1e-1,1e-2,1e-3,1e-4,1e-5,1e-6,1e-8]:
68 _,r=svd_info(Z,tau); print(f'tau={tau:.0e}, rank={r}')
69
70 print('\nCRITIC REGRESSION: quadratic value target, full vs reduced vs under-compressed')
71 Z,Y=simulate(m=2,p=2,seed=7,noise=1e-3); split=len(Z)//2; Zc=Z-Z[:split].mean(0)
72 U=np.linalg.svd(Zc[:split],full_matrices=False)[2].T
73 target=np.sum(Y**2,axis=1)
74 for r in [3,6,8]:
75 H=Zc@U[:,:r]; X=features(H); tr,te=X[:split],X[split:]
76 beta=np.linalg.solve(tr.T@tr+1e-5*np.eye(tr.shape[1]),tr.T@target[:split])
77 mse=np.mean((te@beta-target[split:])**2)
78 print(f'r={r:2d}, features={X.shape[1]:3d}, test_mse={mse:.5e}, feature_cond={condition(tr):.3e}')
79
80if __name__=='__main__': main()