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 | """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()) | |