threshold-computers / tools /build_ca.py
CharlesCNorton
neural_ca: the collision is verified as a reversible interaction gate, not just AND. At step 4 three output cells carry A&B (deflected, (3,6)/(6,3)), A&~B ((6,6)), and ~A&B ((3,3)), checked over all four inputs. Analog robustness sweep on the matrix tile: exact under read noise through sigma 0.10 and conductance mismatch through sigma_G 0.10, matching neural_matrix8. README updated.
657864a
Raw
History Blame Contribute Delete
5.19 kB
"""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())