Non-Abelian Event-Order Memory / experiment.py
Failed on benchmark
1import json, math, random
2from pathlib import Path
3import numpy as np
4import torch
5from scipy.linalg import expm
6from torch import nn
7
8SEED=2704
9np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
10
11def cross_matrix(n):
12 x,y,z=n
13 return np.array([[0,-z,y],[z,0,-x],[-y,x,0]],float)
14
15def rot(axis, angle):
16 axis=np.asarray(axis,float); axis=axis/np.linalg.norm(axis)
17 K=cross_matrix(axis)
18 return np.eye(3)*math.cos(angle)+(1-math.cos(angle))*np.outer(axis,axis)+math.sin(angle)*K
19
20def spin_seq(events, axes, angles, s0=np.array([0.31,-0.52,0.796])):
21 s=np.asarray(s0,float); s=s/np.linalg.norm(s)
22 for e in events: s=rot(axes[e],angles[e])@s
23 return s
24
25def mechanism_checks():
26 # Prediction 1: rotations preserve norm (up to floating point).
27 axes=[np.array([1.,0,0]),np.array([0,1.,0])]
28 rng=np.random.default_rng(SEED)
29 maxerr=0
30 expm_err=0
31 for _ in range(100):
32 a=rng.normal(size=3); a/=np.linalg.norm(a); q=rng.uniform(-math.pi,math.pi)
33 expm_err=max(expm_err, np.max(np.abs(rot(a,q)-expm(q*cross_matrix(a)))))
34 for _ in range(1000):
35 s=rng.normal(size=3); s/=np.linalg.norm(s)
36 for e in rng.integers(0,2,100): s=rot(axes[e],rng.uniform(-math.pi,math.pi))@s
37 maxerr=max(maxerr,abs(np.linalg.norm(s)-1))
38 # Prediction 2: AB-vs-BA difference is zero for parallel axes; for small equal
39 # angles orthogonal axes it scales quadratically, with commutator coefficient.
40 orth=[]; par=[]
41 s0=np.array([.31,-.52,.796]); s0/=np.linalg.norm(s0)
42 for th in np.logspace(-3,-0.2,10):
43 orth.append(np.linalg.norm(spin_seq([0,1],axes,[th,th],s0)-spin_seq([1,0],axes,[th,th],s0)))
44 pa=[np.array([1.,0,0]),np.array([1.,0,0])]
45 par.append(np.linalg.norm(spin_seq([0,1],pa,[th,th],s0)-spin_seq([1,0],pa,[th,th],s0)))
46 slope=np.polyfit(np.log(np.logspace(-3,-0.2,10)[:5]),np.log(np.array(orth)[:5]),1)[0]
47 # Prediction 3: sensitivity grows with nonparallelity, using fixed small angles.
48 th=.08; deltas=[]; sins=[]
49 for phi in np.linspace(0,.5*math.pi,9):
50 a=[np.array([1.,0,0]),np.array([math.cos(phi),math.sin(phi),0.])]
51 deltas.append(np.linalg.norm(spin_seq([0,1],a,[th,th],s0)-spin_seq([1,0],a,[th,th],s0)))
52 sins.append(abs(math.sin(phi)))
53 corr=np.corrcoef(deltas,sins)[0,1]
54 # Quantitative predictions: norm error=0; exponent is 2; parallel effect=0;
55 # fixed-angle order sensitivity is proportional to sin(axis angle).
56 return {'predictions': {
57 'rotation_exponential_max_error_predicted_0': expm_err,
58 'sphere_norm_error_predicted_0': maxerr,
59 'small_angle_order_effect_exponent_predicted_2': slope,
60 'parallel_axis_order_effect_predicted_0': max(par),
61 'axis_misalignment_dependence_corr_predicted_1': corr
62 }, 'orthogonal_delta_at_theta_.08':deltas[-1]}
63
64class CountModel(nn.Module):
65 def __init__(self):
66 super().__init__(); self.fc=nn.Linear(2,2)
67 def forward(self,x): return self.fc(torch.stack([(x==0).float().sum(1),(x==1).float().sum(1)],1))
68
69class GRUModel(nn.Module):
70 def __init__(self, spin=False):
71 super().__init__(); self.spin=spin; self.gru=nn.GRU(2,16,batch_first=True)
72 if spin:
73 self.axis=nn.Parameter(torch.tensor([[1.,0,0],[0,1.,0]]))
74 self.theta=nn.Parameter(torch.tensor([1.,1.]))
75 self.fc=nn.Linear(19,2)
76 else: self.fc=nn.Linear(16,2)
77 def forward(self,x):
78 h,_=self.gru(torch.nn.functional.one_hot(x,2).float()); z=h[:,-1]
79 if not self.spin: return self.fc(z)
80 axes=self.axis/(self.axis.norm(dim=1,keepdim=True)+1e-8)
81 s=torch.zeros(x.size(0),3,device=x.device); s[:,2]=1
82 for t in range(x.size(1)):
83 n=axes[x[:,t]]; th=torch.tanh(self.theta[x[:,t]])*math.pi
84 s=s*torch.cos(th[:,None])+torch.cross(n,s,dim=1)*torch.sin(th[:,None])+n*(n*s).sum(1,keepdim=True)*(1-torch.cos(th[:,None]))
85 return self.fc(torch.cat([z,s],1))
86
87def dataset(n, L=64):
88 # Equal counts and identical suffix; label is ordering of first two events.
89 x=np.tile(np.array([0,1]*((L-2)//2)),(n,1)); x=np.concatenate([np.zeros((n,1),int),x],1)[:,:L]
90 # correct dimensions and balanced suffix
91 suffix=np.array([0,1]*((L-2)//2),int)
92 X=[]; y=[]
93 for i in range(n):
94 lab=i%2; prefix=[0,1] if lab==0 else [1,0]
95 X.append(prefix+suffix.tolist()); y.append(lab)
96 return torch.tensor(np.array(X),dtype=torch.long),torch.tensor(y)
97
98def train(model, Xtr,ytr,Xte,yte, epochs=100):
99 opt=torch.optim.Adam(model.parameters(),lr=.02); lossfn=nn.CrossEntropyLoss()
100 for _ in range(epochs):
101 opt.zero_grad(); lossfn(model(Xtr),ytr).backward(); opt.step()
102 with torch.no_grad():
103 pred=model(Xte).argmax(1); acc=(pred==yte).float().mean().item(); final=lossfn(model(Xte),yte).item()
104 return acc,final
105
106def mini_experiment():
107 X,y=dataset(512); Xtr,ytr=X[:384],y[:384]; Xte,yte=X[384:],y[384:]
108 out={}
109 for name,model in [('count',CountModel()),('gru',GRUModel()),('gru_spin',GRUModel(True))]:
110 out[name]=train(model,Xtr,ytr,Xte,yte)
111 return out
112
113def main():
114 checks=mechanism_checks(); exp=mini_experiment()
115 result={'seed':SEED,'mechanism_checks':checks,'classification':exp}
116 Path('results.json').write_text(json.dumps(result,indent=2))
117 print(json.dumps(result,indent=2))
118if __name__=='__main__': main()