File size: 5,191 Bytes
f3fd723 81376c6 f3fd723 81376c6 657864a 81376c6 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | """Ship the reversible CA's block rule as a ternary matrix tile,
variants/neural_ca.safetensors. The 2x2 block rule compiles to a stack of
ternary matrices with a Heaviside step; the rule is a bijection, so the tile is
a permutation matrix product with a 0.5 analog margin, and the same tile applied
to every block of a lattice is one lattice step."""
from __future__ import annotations
import os
import sys
import torch
from safetensors.torch import save_file, load_file
from safetensors import safe_open
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT, "src"))
import ca
from matrix8 import Net, compile_net, MatrixMachine
OUT = os.path.join(ROOT, "variants", "neural_ca.safetensors")
def block_net():
net = Net()
ntl, ntr = net.NOT("ntl", "tl"), net.NOT("ntr", "tr")
nbl, nbr = net.NOT("nbl", "bl"), net.NOT("nbr", "br")
d1 = net.AND("d1", ["tl", ntr, nbl, "br"])
d2 = net.AND("d2", [ntl, "tr", "bl", nbr])
dg = net.OR("dg", [d1, d2]) # is_diag
TL = net.XOR("oTL", "br", dg)
TR = net.XOR("oTR", "bl", dg)
BL = net.XOR("oBL", "tr", dg)
BR = net.XOR("oBR", "tl", dg)
return net, ["tl", "tr", "bl", "br"], [TL, TR, BL, BR]
def matrix_step(mm, grid, phase):
"""One Margolus update driven entirely by the matrix tile."""
H, W = len(grid), len(grid[0])
out = [row[:] for row in grid]
for r0 in range(phase, phase + H, 2):
for c0 in range(phase, phase + W, 2):
r, r1, c, c1 = r0 % H, (r0 + 1) % H, c0 % W, (c0 + 1) % W
v = torch.tensor([[float(grid[r][c]), float(grid[r][c1]),
float(grid[r1][c]), float(grid[r1][c1])]])
o = mm.step(v)[0]
out[r][c], out[r][c1], out[r1][c], out[r1][c1] = (int(o[0]), int(o[1]),
int(o[2]), int(o[3]))
return out
def main() -> int:
net, inp, outp = block_net()
layers, info = compile_net(net, inp, outp)
mm = MatrixMachine(layers)
seen, bad, vecs = set(), 0, []
for s in range(16):
b = tuple((s >> k) & 1 for k in (3, 2, 1, 0)) # tl,tr,bl,br
v = torch.tensor([[float(x) for x in b]])
got = tuple(int(x) for x in mm.step(v)[0])
if got != ca.rule(b):
bad += 1
seen.add(got)
vecs.append(v[0])
perm = len(seen) == 16
margin = mm.min_margin(torch.stack(vecs))
tensors = {}
for k, (W, B) in enumerate(layers):
tensors[f"matrix.layer{k:03d}.weight"] = W.to(torch.int8)
tensors[f"matrix.layer{k:03d}.bias"] = B.to(torch.int8)
meta = {"machine": "ca",
"rule": "Margolus reversible: rotate 180 except diagonal pair swaps (BBM class)",
"inputs": "tl,tr,bl,br", "outputs": "TL,TR,BL,BR", "layers": str(info["layers"])}
save_file(tensors, OUT, metadata=meta)
print(f"Built {os.path.relpath(OUT, ROOT)}: reversible CA block rule as a ternary matrix tile")
print(f" layers={info['layers']} gates={info['gates']} size={os.path.getsize(OUT)} bytes")
print(f" every weight ternary: "
f"{'OK' if all(((W == -1) | (W == 0) | (W == 1)).all() for W, _ in layers) else 'FAIL'}")
print(f" tile matches the block rule over all 16 states: {'OK' if bad == 0 else f'FAIL({bad})'}")
print(f" tile is a permutation (16 distinct outputs): {'OK' if perm else 'FAIL'}")
print(f" analog noise margin: {margin:.3f} (guarantee 0.5)")
# analog robustness: the tile must reproduce the rule under read noise and
# static conductance mismatch, as neural_matrix8 measures
states = torch.stack(vecs)
ref = [ca.rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16)]
def outs(machine, **kw):
v = machine.step(states, **kw)
return [tuple(int(v[i][j]) for j in range(4)) for i in range(v.shape[0])]
print(" read-noise sweep (exact = tile matches the rule on all 16 states):")
for sigma in (0.05, 0.10, 0.15, 0.20):
okn = all(outs(mm, analog=True, noise_sigma=sigma,
gen=torch.Generator().manual_seed(s)) == ref for s in range(4))
print(f" sigma={sigma:.2f}: {'exact' if okn else 'errors appear'}")
print(" conductance-mismatch sweep:")
for sg in (0.05, 0.10, 0.15):
okg = outs(mm.perturbed(sg, seed=0), analog=True) == ref
print(f" sigma_G={sg:.2f}: {'exact' if okg else 'errors appear'}")
# the loaded tile, applied to every block, is one whole-lattice CA step
t = load_file(OUT)
n = 0
lyr = []
while f"matrix.layer{n:03d}.weight" in t:
lyr.append((t[f"matrix.layer{n:03d}.weight"], t[f"matrix.layer{n:03d}.bias"]))
n += 1
mm2 = MatrixMachine(lyr)
g = ca._rand_grid(8, 8, 3)
gmat = matrix_step(mm2, g, 0)
gref = ca.step(g, 0)
print(f" loaded tile drives a full lattice step (matches ca.step): "
f"{'OK' if gmat == gref else 'FAIL'}")
ok = bad == 0 and perm and abs(margin - 0.5) < 1e-6 and gmat == gref
print("PASS" if ok else "FAIL")
return 0 if ok else 1
if __name__ == "__main__":
sys.exit(main())
|