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