Isometric tensor-network token mixer / experiment.py
Mechanism confirmed, baseline not beaten
1import json, math, time, random
2from pathlib import Path
3import numpy as np
4import torch
5
6SEED = 1620
7np.random.seed(SEED); random.seed(SEED); torch.manual_seed(SEED)
8if torch.cuda.is_available():
9 try: torch.cuda.manual_seed_all(SEED)
10 except Exception: pass
11try:
12 device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
13 # A tiny allocation makes CUDA failures visible before the experiment.
14 if device.type == "cuda": torch.zeros(1, device=device)
15except Exception:
16 device = torch.device("cpu")
17
18
19def rand_u2(rng):
20 z = rng.normal(size=(2,2)) + 1j*rng.normal(size=(2,2))
21 q, r = np.linalg.qr(z)
22 q = q @ np.diag(np.conj(np.diag(r))/np.abs(np.diag(r)))
23 return q.astype(np.complex128)
24
25
26def apply_butterfly(x, gates, scale=1.0):
27 """Apply log2(N) stages of local 2x2 gates to vectors or a matrix."""
28 n = x.shape[-1]
29 y = np.array(x, dtype=np.complex128, copy=True)
30 levels = int(math.log2(n))
31 for s in range(levels):
32 half = 2**s
33 step = 2*half
34 out = y.copy()
35 for base in range(0, n, step):
36 for j in range(half):
37 inds = [base+j, base+j+half]
38 out[..., inds] = np.einsum('ab,...b->...a', scale*gates[s][base//step*half+j], y[..., inds])
39 y = out
40 return y
41
42
43def full_transform(n, rng, scale=1.0):
44 gates = [[rand_u2(rng) for _ in range(n//2)] for _ in range(int(math.log2(n)))]
45 # apply_butterfly expects one gate per pair at every stage
46 cols = np.eye(n, dtype=np.complex128)
47 return apply_butterfly(cols, gates, scale=scale), gates
48
49
50def unitary_checks():
51 rng = np.random.default_rng(SEED)
52 rows=[]
53 for n in [4, 8, 16, 32, 64]:
54 T, gates = full_transform(n, rng)
55 gram_err = float(np.linalg.norm(T.conj().T @ T - np.eye(n), ord='fro'))
56 x = rng.normal(size=n)+1j*rng.normal(size=n)
57 ratio = float(np.linalg.norm(T@x)/np.linalg.norm(x))
58 rows.append({'N':n, 'levels':int(math.log2(n)), 'unitarity_fro_error':gram_err, 'norm_ratio':ratio,
59 'predicted_norm_ratio':1.0})
60 # Controlled violation: multiplying each local gate by (1+eps) gives exactly
61 # (1+eps)^levels for every vector, because each path crosses one gate per level.
62 drift=[]
63 n=32; x=rng.normal(size=n)+1j*rng.normal(size=n)
64 # Sweep both perturbation strength and repeated depth. The exact prediction is
65 # (1+eps)^(log2(N)*repetitions), since each path crosses one gate per level.
66 for eps in [0.0, 1e-3, 1e-2, 5e-2]:
67 rng_eps=np.random.default_rng(SEED+1)
68 Tp, _ = full_transform(n, rng_eps, scale=1+eps)
69 for depth in [1,4,8]:
70 observed=float(np.linalg.norm(np.linalg.matrix_power(Tp, depth)@x)/np.linalg.norm(x))
71 predicted=float((1+eps)**(int(math.log2(n))*depth))
72 drift.append({'epsilon':eps, 'repetitions':depth, 'observed':observed,
73 'predicted':predicted,
74 'relative_error':abs(observed-predicted)/(predicted if predicted else 1)})
75 # Since T is unitary, its conjugate transpose is its exact inverse.
76 rng_inv=np.random.default_rng(SEED+2); T,_=full_transform(32,rng_inv)
77 inverse_err=float(np.linalg.norm(T.conj().T@[email protected](32)-np.ones(32)))
78 return rows, drift, inverse_err
79
80
81class StructuredMixer(torch.nn.Module):
82 # Four real coordinates parameterize every U(2), using a standard SU(2) chart
83 # plus a global phase. Gates are shared over samples but not over butterfly pairs.
84 def __init__(self, n, init=None):
85 super().__init__(); self.n=n; self.levels=int(math.log2(n))
86 self.p=torch.nn.Parameter(torch.zeros(self.levels, n//2, 4))
87 if init is not None:
88 with torch.no_grad(): self.p.copy_(init)
89 def gates(self):
90 a,b,c,d=self.p.unbind(-1)
91 ca=torch.cos(c); sa=torch.sin(c)
92 phase=torch.exp(1j*a)
93 row0=torch.stack([torch.exp(1j*b)*ca, torch.exp(1j*d)*sa], -1)
94 row1=torch.stack([-torch.exp(-1j*d)*sa, torch.exp(-1j*b)*ca], -1)
95 return phase[...,None,None]*torch.stack([row0,row1],-2)
96 def forward(self, x):
97 y=x
98 gs=self.gates()
99 for s in range(self.levels):
100 half=2**s; step=2*half; out=y.clone()
101 for base in range(0,self.n,step):
102 for j in range(half):
103 ids=[base+j,base+j+half]
104 out[:,ids]=torch.einsum('ab,nb->na',gs[s,base//step*half+j],y[:,ids])
105 y=out
106 return y
107
108
109def fit_toy():
110 torch.manual_seed(SEED); n=16; samples=256
111 # Generate a target exactly in the proposed topology, making identifiability fair.
112 true=StructuredMixer(n).to(device)
113 with torch.no_grad(): true.p.normal_(0,0.35)
114 x=(torch.randn(samples,n)+1j*torch.randn(samples,n)).to(device)
115 with torch.no_grad(): y=true(x).detach()
116 structured=StructuredMixer(n).to(device)
117 dense=torch.nn.Parameter((0.05*(torch.randn(n,n)+1j*torch.randn(n,n))).to(device))
118 os=torch.optim.Adam(structured.parameters(),lr=0.06)
119 od=torch.optim.Adam([dense],lr=0.06)
120 losses_s=[]; losses_d=[]; t0=time.time()
121 for step in range(301):
122 os.zero_grad(); pred=structured(x); ls=(pred-y).abs().pow(2).mean(); ls.backward(); os.step()
123 od.zero_grad(); pred2=x@dense.T; ld=(pred2-y).abs().pow(2).mean(); ld.backward(); od.step()
124 if step in [0,50,100,200,300]: losses_s.append(float(ls.detach().cpu())); losses_d.append(float(ld.detach().cpu()))
125 elapsed=time.time()-t0
126 return {'N':n,'samples':samples,'steps':300,'structured_params':int(sum(p.numel() for p in structured.parameters())),
127 'dense_complex_params':int(dense.numel()),'structured_final_mse':losses_s[-1],
128 'dense_final_mse':losses_d[-1],'structured_curve':losses_s,'dense_curve':losses_d,
129 'seconds':elapsed,'device':str(device)}
130
131
132def main():
133 checks, drift, inverse_err=unitary_checks()
134 fit=fit_toy()
135 result={'seed':SEED,'predictions':[
136 'For every N=2^k, ||T x||/||x|| = 1 exactly up to floating point.',
137 'T^*T-I is at machine precision and does not grow materially with N.',
138 'If every local gate is scaled by 1+eps, repeated-pass norm drift is (1+eps)^(k*r), k=log2(N).'
139 ],'unitarity_sweep':checks,'controlled_drift':drift,'inverse_reconstruction_error':inverse_err,'toy_fit':fit}
140 Path('results.json').write_text(json.dumps(result,indent=2))
141 print(json.dumps(result,indent=2))
142
143if __name__=='__main__': main()