threshold-computers / src /automata.py
phanerozoic's picture
Consolidate the Python sources into twelve files with one-line docstrings, fold the demos into the playground, and delete the scripts that do not belong to the artifact
9dc9d2f
Raw History Blame Contribute Delete
64.1 kB
"""The attractor, reversible, cellular-automaton and tile machines, with their builders.""" from __future__ import annotations import json import math import os import random import sys from collections import defaultdict from typing import Dict, List, Optional, Tuple import torch from safetensors import safe_open from safetensors.torch import save_file, load_file from machines import Net, compile_net, MatrixMachine class Circuit: """Wire allocator and energy accumulator.""" def __init__(self) -> None: self.n = 0 self.L: Dict[int, int] = defaultdict(int) self.Q: Dict[Tuple[int, int], int] = defaultdict(int) self.const = 0 self.gates: List[Tuple[str, int, Tuple[int, ...]]] = [] def wire(self) -> int: i = self.n self.n += 1 return i def wires(self, k: int) -> List[int]: return [self.wire() for _ in range(k)] def _q(self, i: int, j: int, c: int) -> None: if i == j: self.L[i] += c else: self.Q[(min(i, j), max(i, j))] += c def AND(self, x: int, y: int) -> int: z = self.wire() self.L[z] += 3 self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2) self.gates.append(("AND", z, (x, y))) return z def OR(self, x: int, y: int) -> int: z = self.wire() self.L[x] += 1; self.L[y] += 1; self.L[z] += 1 self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2) self.gates.append(("OR", z, (x, y))) return z def NOT(self, x: int) -> int: z = self.wire() self.const += 1 self.L[x] += -1; self.L[z] += -1 self._q(x, z, 2) self.gates.append(("NOT", z, (x,))) return z def XOR(self, x: int, y: int) -> int: return self.OR(self.AND(x, self.NOT(y)), self.AND(self.NOT(x), y)) def full_adder(self, x: int, y: int, cin: int) -> Tuple[int, int]: axy = self.XOR(x, y) s = self.XOR(axy, cin) cout = self.OR(self.AND(x, y), self.AND(cin, axy)) return s, cout # ---- energy + couplings ------------------------------------------------ def energy(self, s: List[int]) -> int: e = self.const for i, c in self.L.items(): e += c * s[i] for (i, j), c in self.Q.items(): e += c * s[i] * s[j] return e def neighbors(self) -> Dict[int, List[Tuple[int, int]]]: nbr: Dict[int, List[Tuple[int, int]]] = defaultdict(list) for (i, j), c in self.Q.items(): nbr[i].append((j, c)) nbr[j].append((i, c)) return nbr # ---- relaxation modes -------------------------------------------------- def forward_eval(self, clamp: Dict[int, int]) -> List[int]: """Exact forward relaxation from the clamped inputs to the energy-0 fixed point.""" s = [0] * self.n for w, v in clamp.items(): s[w] = v for op, z, ins in self.gates: if op == "AND": s[z] = s[ins[0]] & s[ins[1]] elif op == "OR": s[z] = s[ins[0]] | s[ins[1]] else: s[z] = 1 - s[ins[0]] return s def relax_energy(self, clamp: Dict[int, int], sweeps: int = 4000, t0: float = 4.0, t1: float = 0.02, seed: int = 0 ) -> Tuple[List[int], bool]: """Anneal the full threshold network, tracking the lowest-energy state.""" nbr = self.neighbors() rng = random.Random(seed) s = [rng.randint(0, 1) for _ in range(self.n)] for w, v in clamp.items(): s[w] = v free = [i for i in range(self.n) if i not in clamp] best, best_e = list(s), self.energy(s) for step in range(sweeps): T = t0 * (t1 / t0) ** (step / max(1, sweeps - 1)) for _ in range(len(free)): i = free[rng.randrange(len(free))] field = self.L[i] + sum(c * s[j] for j, c in nbr[i]) dE = (1 - 2 * s[i]) * field if dE <= 0 or rng.random() < math.exp(-dE / T): s[i] ^= 1 e = self.energy(s) if e < best_e: best, best_e = list(s), e if best_e == 0: return best, True return best, best_e == 0 def solve(self, free_inputs: List[int], fixed: Dict[int, int], target: Dict[int, int], sweeps: int = 3000, restarts: int = 80, seed: int = 0) -> Optional[List[int]]: """Anneal chosen driver wires until the outputs match, the other wires slaved through the circuit.""" rng = random.Random(seed) def mism(vals: Dict[int, int]) -> int: s = self.forward_eval({**fixed, **vals}) return sum(1 for w, v in target.items() if s[w] != v) for _ in range(restarts): vals = {w: rng.randint(0, 1) for w in free_inputs} m = mism(vals) if m == 0: return self.forward_eval({**fixed, **vals}) for step in range(sweeps): T = 2.0 * (0.02 / 2.0) ** (step / sweeps) w = free_inputs[rng.randrange(len(free_inputs))] vals[w] ^= 1 m2 = mism(vals) if m2 <= m or rng.random() < math.exp(-(m2 - m) / T): m = m2 if m == 0: return self.forward_eval({**fixed, **vals}) else: vals[w] ^= 1 return None # --------------------------------------------------------------------------- # Circuit builders # --------------------------------------------------------------------------- def adder(bits: int) -> Tuple[Circuit, dict]: c = Circuit() xs, ys = c.wires(bits), c.wires(bits) cin = c.wire() outs, carry = [], cin for k in range(bits): s, carry = c.full_adder(xs[k], ys[k], carry) outs.append(s) return c, {"xs": xs, "ys": ys, "cin": cin, "sum": outs + [carry]} def multiplier(bits: int) -> Tuple[Circuit, dict]: c = Circuit() xs, ys = c.wires(bits), c.wires(bits) zero = c.wire() acc = [zero] * (2 * bits) for i in range(bits): carry = zero for j in range(bits): acc[i + j], carry = c.full_adder(acc[i + j], c.AND(xs[i], ys[j]), carry) acc[i + bits] = carry return c, {"xs": xs, "ys": ys, "zero": zero, "prod": acc} _OPCODE = {"AND": 0, "OR": 1, "NOT": 2} _OPNAME = {v: k for k, v in _OPCODE.items()} def to_tensors(circ: Circuit, io: dict): """Serialize the coupling matrix (the program) and the gate list to tensors.""" import torch qi = sorted(circ.Q) q_idx = torch.tensor(qi if qi else [], dtype=torch.long).reshape(-1, 2) q_val = torch.tensor([circ.Q[k] for k in qi], dtype=torch.long) li = sorted(circ.L) l_idx = torch.tensor(li, dtype=torch.long) l_val = torch.tensor([circ.L[i] for i in li], dtype=torch.long) g_op = torch.tensor([_OPCODE[op] for op, _, _ in circ.gates], dtype=torch.long) g_out = torch.tensor([o for _, o, _ in circ.gates], dtype=torch.long) g_in = torch.tensor([[ins[0], ins[1] if len(ins) > 1 else -1] for _, _, ins in circ.gates], dtype=torch.long).reshape(-1, 2) t = {"Q_idx": q_idx, "Q_val": q_val, "L_idx": l_idx, "L_val": l_val, "gate_op": g_op, "gate_out": g_out, "gate_in": g_in} import json meta = {"machine": "attractor", "n": str(circ.n), "const": str(circ.const), "io": json.dumps({k: v for k, v in io.items()})} return t, meta def from_tensors(t: dict, meta: dict) -> Tuple[Circuit, dict]: import json c = Circuit() c.n = int(meta["n"]) c.const = int(meta["const"]) for (i, j), v in zip(t["Q_idx"].tolist(), t["Q_val"].tolist()): c.Q[(i, j)] = v for i, v in zip(t["L_idx"].tolist(), t["L_val"].tolist()): c.L[i] = v for op, out, ins in zip(t["gate_op"].tolist(), t["gate_out"].tolist(), t["gate_in"].tolist()): c.gates.append((_OPNAME[op], out, tuple(x for x in ins if x >= 0))) return c, json.loads(meta["io"]) def cnf(clauses: List[List[int]], n_vars: int) -> Tuple[Circuit, dict]: """Compile a CNF formula.""" c = Circuit() var = {v: c.wire() for v in range(1, n_vars + 1)} clause_ws = [] for cl in clauses: lits = [var[abs(l)] if l > 0 else c.NOT(var[abs(l)]) for l in cl] acc = lits[0] for w in lits[1:]: acc = c.OR(acc, w) clause_ws.append(acc) sat = clause_ws[0] for w in clause_ws[1:]: sat = c.AND(sat, w) return c, {"vars": var, "sat": sat} def test_forward(): ok = True for bits in (4, 8): c, io = adder(bits) rng = random.Random(bits) bad = 0 for _ in range(300): a, b = rng.randint(0, (1 << bits) - 1), rng.randint(0, (1 << bits) - 1) clamp = {io["cin"]: 0} for k in range(bits): clamp[io["xs"][k]] = (a >> k) & 1 clamp[io["ys"][k]] = (b >> k) & 1 s = c.forward_eval(clamp) got = sum(s[w] << k for k, w in enumerate(io["sum"])) if got != a + b or c.energy(s) != 0: bad += 1 print(f" forward adder {bits}-bit: {'OK' if bad == 0 else f'FAIL({bad})'}") ok &= bad == 0 for bits in (3, 5): c, io = multiplier(bits) rng = random.Random(100 + bits) bad = 0 for _ in range(300): a, b = rng.randint(0, (1 << bits) - 1), rng.randint(0, (1 << bits) - 1) clamp = {io["zero"]: 0} for k in range(bits): clamp[io["xs"][k]] = (a >> k) & 1 clamp[io["ys"][k]] = (b >> k) & 1 s = c.forward_eval(clamp) got = sum(s[w] << k for k, w in enumerate(io["prod"])) if got != a * b or c.energy(s) != 0: bad += 1 print(f" forward multiplier {bits}-bit: {'OK' if bad == 0 else f'FAIL({bad})'}") ok &= bad == 0 return ok def test_energy_relax(): """The canonical form: anneal the whole network (no propagation shortcut).""" c, io = adder(4) rng = random.Random(3) bad = 0 for _ in range(20): a, b = rng.randint(0, 15), rng.randint(0, 15) clamp = {io["cin"]: 0} for k in range(4): clamp[io["xs"][k]] = (a >> k) & 1 clamp[io["ys"][k]] = (b >> k) & 1 conv = False for attempt in range(4): # annealers restart s, conv = c.relax_energy(clamp, sweeps=6000, seed=rng.randint(0, 1 << 30)) got = sum(s[w] << k for k, w in enumerate(io["sum"])) if conv and got == a + b: break if not conv: bad += 1 print(f" whole-network energy relaxation (4-bit adder, 20 cases): " f"{'OK' if bad == 0 else f'reached ground state in {20 - bad}/20'}") return bad == 0 def test_factor(): ok = True for bits, targets in ((4, [15, 35, 143]), (5, [21, 55, 91])): c, io = multiplier(bits) for N in targets: target = {io["prod"][k]: (N >> k) & 1 for k in range(2 * bits)} s = c.solve(io["xs"] + io["ys"], {io["zero"]: 0}, target, seed=N) if s is None: print(f" factor {N}: not found") ok = False continue a = sum(s[io["xs"][k]] << k for k in range(bits)) b = sum(s[io["ys"][k]] << k for k in range(bits)) good = a * b == N and 1 < a < N and 1 < b < N print(f" factor {N} ({bits}x{bits}): {a} x {b} {'OK' if a * b == N else 'WRONG'}") ok &= a * b == N return ok def test_sat(): # (x1 | x2 | ~x3) & (~x1 | x3) & (x2 | x3) & (~x2 | ~x3), a satisfiable 3-SAT. clauses = [[1, 2, -3], [-1, 3], [2, 3], [-2, -3]] c, io = cnf(clauses, 3) s = c.solve(list(io["vars"].values()), {}, {io["sat"]: 1}, seed=1) if s is None: print(" SAT solve: no model found") return False assign = {v: s[w] for v, w in io["vars"].items()} sat = all(any((assign[abs(l)] == 1) if l > 0 else (assign[abs(l)] == 0) for l in cl) for cl in clauses) print(f" SAT solve: model {assign} {'satisfies' if sat else 'FAILS'} the formula") return sat ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) ATTRACTOR_OUT = os.path.join(ROOT, "variants", "neural_attractor.safetensors") BITS = 8 def main_build_attractor() -> int: c, io = multiplier(BITS) tensors, meta = to_tensors(c, io) save_file(tensors, ATTRACTOR_OUT, metadata=meta) size = os.path.getsize(ATTRACTOR_OUT) print(f"Built {os.path.relpath(ATTRACTOR_OUT, ROOT)}: {BITS}x{BITS} multiplier as couplings") print(f" wires={c.n} Q entries={len(c.Q)} L entries={len(c.L)} size={size/1024:.1f} KB") # round-trip t = load_file(ATTRACTOR_OUT) with safe_open(ATTRACTOR_OUT, framework="pt") as f: m = f.metadata() c2, io2 = from_tensors(t, m) xs, ys, zero, prod = io2["xs"], io2["ys"], io2["zero"], io2["prod"] rng = random.Random(0) bad = 0 for _ in range(200): a, b = rng.randint(0, 255), rng.randint(0, 255) clamp = {zero: 0} for k in range(BITS): clamp[xs[k]] = (a >> k) & 1 clamp[ys[k]] = (b >> k) & 1 s = c2.forward_eval(clamp) got = sum(s[w] << k for k, w in enumerate(prod)) if got != a * b or c2.energy(s) != 0: bad += 1 print(f" round-trip forward multiply (200 cases): {'OK' if bad == 0 else f'FAIL({bad})'}") # backward: factor a small product through the loaded couplings N = 35 target = {prod[k]: (N >> k) & 1 for k in range(2 * BITS)} s = c2.solve(xs + ys, {zero: 0}, target, sweeps=2500, restarts=120, seed=N) if s is not None: fa = sum(s[xs[k]] << k for k in range(BITS)) fb = sum(s[ys[k]] << k for k in range(BITS)) print(f" round-trip backward factor {N}: {fa} x {fb} {'OK' if fa * fb == N else 'WRONG'}") ok = fa * fb == N else: print(f" round-trip backward factor {N}: not found") ok = False return 0 if (bad == 0 and ok) else 1 # --- threshold-gate truth: the target updates are computed by Heaviside gates --- def H(x: int) -> int: return 1 if x >= 0 else 0 def g_and(a: int, b: int) -> int: return H(a + b - 2) # fires iff a=b=1 def g_xor(a: int, b: int) -> int: # XOR as AND(OR, NAND): OR=H(a+b-1), NAND=H(1-a-b), out=AND(OR,NAND) return g_and(H(a + b - 1), H(1 - a - b)) # --- reversible primitives, in place on a bit register (a Python list) --- def NOT(reg: List[int], t: int) -> None: reg[t] = 1 - reg[t] def CNOT(reg: List[int], c: int, t: int) -> None: reg[t] = g_xor(reg[t], reg[c]) def TOFF(reg: List[int], a: int, b: int, t: int) -> None: reg[t] = g_xor(reg[t], g_and(reg[a], reg[b])) def FRED(reg: List[int], c: int, x: int, y: int) -> None: # controlled swap of x,y on control c if reg[c]: reg[x], reg[y] = reg[y], reg[x] # --- Cuccaro ripple-carry adder: b += a in place, one carry ancilla --- # MAJ(c,b,a): b^=a ; c^=a ; a ^= b&c UMA(c,b,a): a ^= b&c ; c^=a ; b^=c def _maj(reg, c, b, a): CNOT(reg, a, b) CNOT(reg, a, c) TOFF(reg, b, c, a) def _uma(reg, c, b, a): TOFF(reg, b, c, a) CNOT(reg, a, c) CNOT(reg, c, b) # Each primitive below is its own inverse, so the inverse of a gate sequence is # the reversed sequence. Building the adder as an op list gives subtraction for # free (run it backward). def _maj_ops(c, b, a): return [(CNOT, a, b), (CNOT, a, c), (TOFF, b, c, a)] def _uma_ops(c, b, a): return [(TOFF, b, c, a), (CNOT, a, c), (CNOT, c, b)] def _adder_ops(a_bits, b_bits, carry, cout=None): n = len(a_bits) ops = _maj_ops(carry, b_bits[0], a_bits[0]) for i in range(1, n): ops += _maj_ops(a_bits[i - 1], b_bits[i], a_bits[i]) if cout is not None: ops.append((CNOT, a_bits[n - 1], cout)) for i in range(n - 1, 0, -1): ops += _uma_ops(a_bits[i - 1], b_bits[i], a_bits[i]) ops += _uma_ops(carry, b_bits[0], a_bits[0]) return ops def _apply(reg, ops, inverse=False): for gate, *args in (reversed(ops) if inverse else ops): gate(reg, *args) def add_into(reg, a_bits, b_bits, carry, cout=None): """b <- (a + b) mod 2^n (LSB first); a and carry (=0) restored.""" _apply(reg, _adder_ops(a_bits, b_bits, carry, cout)) def sub_into(reg, a_bits, b_bits, carry, cout=None): """b <- (b - a) mod 2^n: the adder run backward.""" _apply(reg, _adder_ops(a_bits, b_bits, carry, cout), inverse=True) def xor_into(reg, a_bits, b_bits): """b <- b XOR a, bitwise (self-inverse).""" for a, b in zip(a_bits, b_bits): CNOT(reg, a, b) def incr(reg, b_bits, one_bits, carry): """b <- b + 1 mod 2^n.""" add_into(reg, one_bits, b_bits, carry) def neg_into(reg, b_bits, one_bits, carry): """b <- (-b) mod 2^n via two's complement (~b then +1). Self-inverse.""" for t in b_bits: NOT(reg, t) add_into(reg, one_bits, b_bits, carry) def rot_left(reg, b_bits, k=1): """Rotate the word left by k (a permutation of bit positions; reversible).""" n = len(b_bits) k %= n vals = [reg[b_bits[i]] for i in range(n)] for i in range(n): reg[b_bits[(i + k) % n]] = vals[i] def and_into(reg, a_bits, b_bits, t_bits): """t <- t XOR (a AND b), bitwise (word-level Toffoli). Self-inverse.""" for a, b, t in zip(a_bits, b_bits, t_bits): TOFF(reg, a, b, t) # --- verification helpers --- def is_permutation(fn, nbits: int) -> bool: """Check fn: {0,1}^nbits -> {0,1}^nbits is a bijection (exhaustive).""" seen = set() for x in range(1 << nbits): reg = [(x >> k) & 1 for k in range(nbits)] fn(reg) y = sum(b << k for k, b in enumerate(reg)) seen.add(y) return len(seen) == (1 << nbits) def _test_primitives(): ok = True ok &= is_permutation(lambda r: NOT(r, 0), 1) ok &= is_permutation(lambda r: CNOT(r, 0, 1), 2) ok &= is_permutation(lambda r: TOFF(r, 0, 1, 2), 3) ok &= is_permutation(lambda r: FRED(r, 0, 1, 2), 3) print(f" primitives bijective (NOT/CNOT/TOFF/FRED): {'OK' if ok else 'FAIL'}") return ok def _test_adder(width=4): # layout: a[0..w-1], b[0..w-1], carry a_bits = list(range(width)) b_bits = list(range(width, 2 * width)) carry = 2 * width n = 2 * width + 1 ok_perm = is_permutation(lambda r: add_into(r, a_bits, b_bits, carry), n) bad = 0 mask = (1 << width) - 1 for a in range(1 << width): for b in range(1 << width): reg = [0] * n for k in range(width): reg[a_bits[k]] = (a >> k) & 1 reg[b_bits[k]] = (b >> k) & 1 add_into(reg, a_bits, b_bits, carry) got_a = sum(reg[a_bits[k]] << k for k in range(width)) got_b = sum(reg[b_bits[k]] << k for k in range(width)) if got_a != a or got_b != ((a + b) & mask) or reg[carry] != 0: bad += 1 print(f" Cuccaro adder {width}-bit: bijection={'OK' if ok_perm else 'FAIL'} " f"b<-a+b, a & carry restored={'OK' if bad == 0 else f'FAIL({bad})'}") return ok_perm and bad == 0 def bennett(reg, a_b, b_b, c_b, scr_b, out_b, carry): """Bennett compute-copy-uncompute for the irreversible f(a,b,c)=(a+b) XOR c.""" ops = [] # compute f into scratch (scratch starts 0): scratch += a; scratch += b; scratch ^= c ops += _adder_ops(a_b, scr_b, carry) ops += _adder_ops(b_b, scr_b, carry) ops += [(CNOT, c, s) for c, s in zip(c_b, scr_b)] # copy scratch -> out ops += [(CNOT, s, o) for s, o in zip(scr_b, out_b)] # uncompute scratch (reverse of the compute prefix) n_comp = len(_adder_ops(a_b, scr_b, carry)) * 2 + len(c_b) ops += [(g, *args) for g, *args in reversed(ops[:n_comp])] _apply(reg, ops) return ops def _test_bennett(width=4): a_b = list(range(width)) b_b = list(range(width, 2 * width)) c_b = list(range(2 * width, 3 * width)) scr_b = list(range(3 * width, 4 * width)) out_b = list(range(4 * width, 5 * width)) carry = 5 * width n = 5 * width + 1 mask = (1 << width) - 1 bad = 0 for a in range(1 << width): for b in range(1 << width): for c in range(1 << width): r = [0] * n for k in range(width): r[a_b[k]] = (a >> k) & 1 r[b_b[k]] = (b >> k) & 1 r[c_b[k]] = (c >> k) & 1 bennett(r, a_b, b_b, c_b, scr_b, out_b, carry) ra = sum(r[a_b[k]] << k for k in range(width)) rb = sum(r[b_b[k]] << k for k in range(width)) rc = sum(r[c_b[k]] << k for k in range(width)) rs = sum(r[scr_b[k]] << k for k in range(width)) ro = sum(r[out_b[k]] << k for k in range(width)) if (ra, rb, rc, rs, ro, r[carry]) != (a, b, c, 0, ((a + b) & mask) ^ c, 0): bad += 1 print(f" Bennett (a,b,c,0,0)->(a,b,c,0,(a+b)^c), scratch cleaned " f"[{width}-bit]: {'OK' if bad == 0 else f'FAIL({bad})'}") return bad == 0 def _test_alu(width=4): mask = (1 << width) - 1 a_b = list(range(width)) b_b = list(range(width, 2 * width)) one_b = list(range(2 * width, 3 * width)) t_b = list(range(3 * width, 4 * width)) carry = 4 * width n = 4 * width + 1 def fresh(a=0, b=0, t=0, one=False): r = [0] * n for k in range(width): r[a_b[k]] = (a >> k) & 1 r[b_b[k]] = (b >> k) & 1 r[t_b[k]] = (t >> k) & 1 if one: r[one_b[0]] = 1 return r def rd(r, bits): return sum(r[bits[k]] << k for k in range(width)) results = {} # subtract bad = 0 for a in range(1 << width): for b in range(1 << width): r = fresh(a, b) sub_into(r, a_b, b_b, carry) if rd(r, b_b) != ((b - a) & mask) or rd(r, a_b) != a or r[carry] != 0: bad += 1 results["sub b-=a"] = bad == 0 and is_permutation(lambda r: sub_into(r, a_b, b_b, carry), n) # xor bad = 0 for a in range(1 << width): for b in range(1 << width): r = fresh(a, b) xor_into(r, a_b, b_b) if rd(r, b_b) != (a ^ b) or rd(r, a_b) != a: bad += 1 results["xor b^=a"] = bad == 0 # negate bad = 0 for b in range(1 << width): r = fresh(b=b, one=True) neg_into(r, b_b, one_b, carry) if rd(r, b_b) != ((-b) & mask) or rd(r, one_b) != 1 or r[carry] != 0: bad += 1 results["neg b=-b"] = bad == 0 # increment bad = 0 for b in range(1 << width): r = fresh(b=b, one=True) incr(r, b_b, one_b, carry) if rd(r, b_b) != ((b + 1) & mask): bad += 1 results["incr b+=1"] = bad == 0 # rotate bad = 0 for b in range(1 << width): r = fresh(b=b) rot_left(r, b_b, 1) exp = ((b << 1) | (b >> (width - 1))) & mask if rd(r, b_b) != exp: bad += 1 results["rot_left"] = bad == 0 # and-into (Toffoli word) bad = 0 for a in range(1 << width): for b in range(1 << width): for t in range(1 << width): r = fresh(a, b, t) and_into(r, a_b, b_b, t_b) if rd(r, t_b) != (t ^ (a & b)): bad += 1 results["and t^=a&b"] = bad == 0 ok = all(results.values()) print(" reversible ALU " + f"{width}-bit: " + " ".join(f"{k}={'OK' if v else 'FAIL'}" for k, v in results.items())) return ok class RCPU: def __init__(self, program: List[tuple], k_regs=4, width=8, mem_words=16): self.prog = program self.K = k_regs self.W = width self.M = mem_words self.mask = (1 << width) - 1 self.L = len(program) def new_state(self, regs=None, mem=None) -> dict: return {"R": list(regs) + [0] * (self.K - len(regs)) if regs else [0] * self.K, "PC": 0, "BR": 1, "MEM": list(mem) + [0] * (self.M - len(mem)) if mem else [0] * self.M} # ---- reversible instruction effects (forward and inverse) ---- def _data(self, s, I, inverse: bool): R, MEM, m = s["R"], s["MEM"], self.mask op = I[0] if op == "ADD": d, r = I[1], I[2] R[d] = (R[d] - R[r]) & m if inverse else (R[d] + R[r]) & m elif op == "SUB": d, r = I[1], I[2] R[d] = (R[d] + R[r]) & m if inverse else (R[d] - R[r]) & m elif op == "ADDI": d, k = I[1], I[2] R[d] = (R[d] - k) & m if inverse else (R[d] + k) & m elif op == "XOR": R[I[1]] ^= R[I[2]] elif op == "XORI": R[I[1]] ^= (I[2] & m) elif op == "NEG": R[I[1]] = (-R[I[1]]) & m elif op == "TOFF": R[I[1]] ^= (R[I[2]] & R[I[3]]) elif op == "ROL": k = (-I[2] if inverse else I[2]) % self.W R[I[1]] = ((R[I[1]] << k) | (R[I[1]] >> (self.W - k))) & m if k else R[I[1]] elif op == "EXCH": d, r = I[1], I[2] a = R[r] % self.M R[d], MEM[a] = MEM[a], R[d] # BRA/BEZ/HALT have no data effect def _toggle(self, s, I): """Reversible control: toggle BR for a taken branch (self-inverse).""" op = I[0] if op == "BRA": s["BR"] ^= (I[1] ^ 1) elif op == "BEZ": if s["R"][I[1]] == 0: s["BR"] ^= (I[2] ^ 1) # ---- single-step transition and its inverse ---- def step(self, s): I = self.prog[s["PC"]] self._data(s, I, inverse=False) self._toggle(s, I) s["PC"] = (s["PC"] + s["BR"]) % self.L def step_back(self, s): s["PC"] = (s["PC"] - s["BR"]) % self.L I = self.prog[s["PC"]] self._toggle(s, I) # self-inverse: restores BR self._data(s, I, inverse=True) def run(self, s, steps): for _ in range(steps): self.step(s) return s def run_back(self, s, steps): for _ in range(steps): self.step_back(s) return s def _clone(s): return {"R": list(s["R"]), "PC": s["PC"], "BR": s["BR"], "MEM": list(s["MEM"])} def _eq(a, b): return a["R"] == b["R"] and a["PC"] == b["PC"] and a["BR"] == b["BR"] and a["MEM"] == b["MEM"] def test_straight_line(): prog = [("ADD", 1, 0), ("XOR", 1, 0), ("NEG", 1), ("ADDI", 1, 7), ("TOFF", 2, 0, 1), ("ROL", 0, 1)] m = RCPU(prog, width=8) ok = True for a in (5, 0, 255, 100): for b in (3, 1, 200): s0 = m.new_state([a, b, 0, 0]) s = _clone(s0) m.run(s, len(prog)) fwd = _clone(s) m.run_back(s, len(prog)) ok &= _eq(s, s0) # round-trip recovers the exact input print(f" straight-line round-trip (dir -1 recovers input): {'OK' if ok else 'FAIL'}") return ok def test_bijection(): """The single-step transition is a bijection: step_back inverts step on every instruction.""" import itertools W = 4 prog = [ ("ADD", 1, 0), ("SUB", 1, 0), ("ADDI", 2, 3), ("XOR", 1, 2), ("XORI", 0, 5), ("NEG", 3), ("TOFF", 3, 0, 1), ("ROL", 0, 1), ("EXCH", 2, 3), ("BRA", 2), ("BEZ", 1, 3), ("BEZ", 0, -2), ] m = RCPU(prog, k_regs=4, width=W, mem_words=4) bad = 0 checked = 0 rng = __import__("random").Random(0) for pc in range(len(prog)): for br in (1, 2, 3, -2, -1): for _ in range(60): s0 = {"R": [rng.randint(0, (1 << W) - 1) for _ in range(4)], "PC": pc, "BR": br, "MEM": [rng.randint(0, (1 << W) - 1) for _ in range(4)]} s = _clone(s0) m.step(s) m.step_back(s) if not _eq(s, s0): bad += 1 # and the other composition order s = _clone(s0) m.step_back(s) m.step(s) if not _eq(s, s0): bad += 1 checked += 2 print(f" step_back o step = id over {checked} (state, instruction) cases: " f"{'OK' if bad == 0 else f'FAIL({bad})'}") return bad == 0 WIDTH = 8 MASK = (1 << WIDTH) - 1 def _regs_written(stmts) -> set: w = set() for st in stmts: op = st[0] if op in ("ADD", "SUB", "ADDI", "XOR", "NEG"): w.add(st[1]) elif op == "SWAP": w.add(st[1]); w.add(st[2]) elif op == "FOR": w |= _regs_written(st[2]) elif op == "IF": w |= _regs_written(st[2]) | _regs_written(st[3]) return w def invert(stmts: List[tuple]) -> List[tuple]: out = [] for st in reversed(stmts): op = st[0] if op == "ADD": out.append(("SUB", st[1], st[2])) elif op == "SUB": out.append(("ADD", st[1], st[2])) elif op == "ADDI": out.append(("ADDI", st[1], -st[2])) elif op in ("XOR", "NEG", "SWAP"): out.append(st) elif op == "FOR": out.append(("FOR", st[1], invert(st[2]))) elif op == "IF": # Janus: reverse swaps predicate and exit assertion, inverts branches out.append(("IF", st[4], invert(st[2]), invert(st[3]), st[1])) return out def prog_run(stmts: List[tuple], s: Dict[str, int]) -> Dict[str, int]: for st in stmts: op = st[0] if op == "ADD": s[st[1]] = (s[st[1]] + s[st[2]]) & MASK elif op == "SUB": s[st[1]] = (s[st[1]] - s[st[2]]) & MASK elif op == "ADDI": s[st[1]] = (s[st[1]] + st[2]) & MASK elif op == "XOR": s[st[1]] ^= s[st[2]] elif op == "NEG": s[st[1]] = (-s[st[1]]) & MASK elif op == "SWAP": s[st[1]], s[st[2]] = s[st[2]], s[st[1]] elif op == "FOR": cnt, body = st[1], st[2] if cnt in _regs_written(body): raise ValueError("FOR count register must be read-only (irreversible otherwise)") for _ in range(s[cnt]): prog_run(body, s) elif op == "IF": pred, then, els, exit_assert = st[1], st[2], st[3], st[4] if pred(s): prog_run(then, s) assert exit_assert(s), "exit assertion violated (not reversible)" else: prog_run(els, s) assert not exit_assert(s), "exit assertion violated (not reversible)" return s # --- demonstration programs --- MULTIPLY = [("FOR", "b", [("ADD", "acc", "a")])] # acc += a, b times; a,b preserved FIB = [("FOR", "n", [("ADD", "a", "b"), ("SWAP", "a", "b")])] # (a,b)->(b,a+b), n times def _test(): ok = True # reversible multiply: acc = a*b, inputs preserved; inverse clears acc bad = 0 for a in range(16): for b in range(16): s = {"a": a, "b": b, "acc": 0} prog_run(MULTIPLY, s) if s["acc"] != (a * b) & MASK or s["a"] != a or s["b"] != b: bad += 1 prog_run(invert(MULTIPLY), s) # run backward if s != {"a": a, "b": b, "acc": 0}: bad += 1 print(f" reversible multiply acc=a*b, inputs preserved, inverse clears acc: " f"{'OK' if bad == 0 else f'FAIL({bad})'}") ok &= bad == 0 # reversible Fibonacci: n steps forward, inverse recovers the seed bad = 0 for n in range(12): s = {"a": 0, "b": 1, "n": n} prog_run(FIB, s) # forward value check against a plain reference ra, rb = 0, 1 for _ in range(n): ra, rb = rb, (ra + rb) & MASK if (s["a"], s["b"]) != (ra, rb): bad += 1 prog_run(invert(FIB), s) if s != {"a": 0, "b": 1, "n": n}: bad += 1 print(f" reversible Fibonacci n steps, inverse recovers seed: " f"{'OK' if bad == 0 else f'FAIL({bad})'}") ok &= bad == 0 # a reversible conditional (Janus IF): swap when the operands differ. The # exit assertion a != b is true exactly when the then-branch ran (swapping # distinct values keeps them distinct; equal values are skipped), so the # reverse picks the right branch. prog = [("IF", lambda s: s["a"] != s["b"], [("SWAP", "a", "b")], [], lambda s: s["a"] != s["b"])] bad = 0 for a in range(16): for b in range(16): s = {"a": a, "b": b} prog_run(prog, s) if sorted([s["a"], s["b"]]) != sorted([a, b]): bad += 1 prog_run(invert(prog), s) if s != {"a": a, "b": b}: bad += 1 print(f" reversible conditional (Janus IF with exit assertion): " f"{'OK' if bad == 0 else f'FAIL({bad})'}") ok &= bad == 0 return ok REVERSIBLE_OUT = os.path.join(ROOT, "variants", "neural_reversible.safetensors") # The two reversible primitives, keyed to their gate functions. _CODE = {CNOT: 0, TOFF: 1} _FN = {0: CNOT, 1: TOFF} # Heaviside threshold gates that implement the target updates: a CNOT target is # XOR(t,c); a Toffoli target is XOR(t, AND(a,b)). AND/OR/NAND are single gates. _SUBSTRATE = { "and_w": torch.tensor([1, 1]), "and_b": torch.tensor(-2), "or_w": torch.tensor([1, 1]), "or_b": torch.tensor(-1), "nand_w": torch.tensor([-1, -1]), "nand_b": torch.tensor(1), } def encode(ops): codes, args = [], [] for gate, *a in ops: codes.append(_CODE[gate]) args.append((a + [-1, -1, -1])[:3]) return torch.tensor(codes, dtype=torch.long), torch.tensor(args, dtype=torch.long) def main_build_reversible() -> int: a_bits = list(range(WIDTH)) b_bits = list(range(WIDTH, 2 * WIDTH)) carry = 2 * WIDTH n = 2 * WIDTH + 1 ops = _adder_ops(a_bits, b_bits, carry) codes, args = encode(ops) tensors = {"gate_code": codes, "gate_args": args, **_SUBSTRATE} import json meta = {"machine": "reversible", "width": str(WIDTH), "n_wires": str(n), "a_bits": json.dumps(a_bits), "b_bits": json.dumps(b_bits), "carry": str(carry), "circuit": "in-place reversible adder b<-a+b"} save_file(tensors, REVERSIBLE_OUT, metadata=meta) print(f"Built {os.path.relpath(REVERSIBLE_OUT, ROOT)}: reversible {WIDTH}-bit adder") print(f" gates={len(ops)} wires={n} size={os.path.getsize(REVERSIBLE_OUT)} bytes") # round-trip: reconstruct the op list from the file and run it t = load_file(REVERSIBLE_OUT) with safe_open(REVERSIBLE_OUT, framework="pt") as f: m = f.metadata() W = int(m["width"]); ab = json.loads(m["a_bits"]); bb = json.loads(m["b_bits"]) cy = int(m["carry"]); nn = int(m["n_wires"]) loaded = [] for code, a in zip(t["gate_code"].tolist(), t["gate_args"].tolist()): loaded.append((_FN[code], *[x for x in a if x >= 0])) def run(reg): for gate, *a in loaded: gate(reg, *a) mask = (1 << W) - 1 bad = 0 rng = random.Random(0) for _ in range(400): a = rng.randint(0, mask); b = rng.randint(0, mask) reg = [0] * nn for k in range(W): reg[ab[k]] = (a >> k) & 1 reg[bb[k]] = (b >> k) & 1 run(reg) got_b = sum(reg[bb[k]] << k for k in range(W)) got_a = sum(reg[ab[k]] << k for k in range(W)) if got_b != ((a + b) & mask) or got_a != a or reg[cy] != 0: bad += 1 print(f" round-trip b<-a+b, addend & carry restored (400 cases): " f"{'OK' if bad == 0 else f'FAIL({bad})'}") # bijection is exhaustive-checkable at width 4 on the same construction a4 = list(range(4)); b4 = list(range(4, 8)); c4 = 8 ops4 = _adder_ops(a4, b4, c4) perm = is_permutation(lambda r: _apply(r, ops4), 9) print(f" loaded circuit is a bijection (4-bit): {'OK' if perm else 'FAIL'}") return 0 if (bad == 0 and perm) else 1 def adder_net(width: int): """The in-place Cuccaro adder as a feedforward ternary netlist computing its permutation.""" a_bits = list(range(width)) b_bits = list(range(width, 2 * width)) carry = 2 * width n = 2 * width + 1 ops = _adder_ops(a_bits, b_bits, carry) net = Net() inputs = [f"in{i}" for i in range(n)] cur = list(inputs) for k, (gate, *args) in enumerate(ops): if gate is CNOT: c, t = args cur[t] = net.XOR(f"c{k}", cur[t], cur[c]) # t ^= c else: # Toffoli: t ^= a&b a, b, t = args tmp = net.AND(f"t{k}a", [cur[a], cur[b]]) cur[t] = net.XOR(f"t{k}x", cur[t], tmp) return net, inputs, list(cur), n, a_bits, b_bits, carry def _refs(n, a_bits, b_bits, carry): out = [] for x in range(1 << n): reg = [(x >> i) & 1 for i in range(n)] _apply(reg, _adder_ops(a_bits, b_bits, carry)) out.append(sum(reg[i] << i for i in range(n))) return torch.tensor(out) def _outputs(mm, states, n, **step_kw): v = mm.step(states.clone(), **step_kw) bits = (v >= 0.5).to(torch.int64) weights = torch.tensor([1 << i for i in range(n)]) return (bits * weights).sum(dim=1) def analog_sweep(mm, states, refs, n): """The permutation must survive analog imperfection.""" print(" read-noise sweep (bit-exact = all 512 outputs still match, 4 trials):") for sigma in (0.05, 0.10, 0.15, 0.20): ok = all((_outputs(mm, states, n, analog=True, noise_sigma=sigma, gen=torch.Generator().manual_seed(s)) == refs).all() for s in range(4)) print(f" sigma={sigma:.2f}: {'bit-exact' if ok else 'errors appear'}") print(" conductance-mismatch sweep (fixed per-device weight perturbation):") for sg in (0.05, 0.10, 0.15): ok = (_outputs(mm.perturbed(sg, seed=0), states, n, analog=True) == refs).all() print(f" sigma_G={sg:.2f}: {'bit-exact' if ok else 'errors appear'}") def main_reversible_matrix() -> int: width = 4 net, inputs, outputs, n, a_bits, b_bits, carry = adder_net(width) layers, info = compile_net(net, inputs, outputs) mm = MatrixMachine(layers) states = torch.stack([torch.tensor([float((x >> i) & 1) for i in range(n)]) for x in range(1 << n)]) refs = _refs(n, a_bits, b_bits, carry) got = _outputs(mm, states, n) bad = int((got != refs).sum()) perm = len(set(got.tolist())) == (1 << n) margin = mm.min_margin(states[:256]) print(f"reversible {width}-bit adder as a ternary matrix stack") print(f" layers={info['layers']} gates={info['gates']} " f"max_width={info['max_width']} total_weights={info['total_weights']}") print(f" every weight ternary: {'OK' if all(((W == -1) | (W == 0) | (W == 1)).all() for W, _ in layers) else 'FAIL'}") print(f" matches the gate circuit over all {1 << n} inputs: {'OK' if bad == 0 else f'FAIL({bad})'}") print(f" composed transition is a permutation of the state space: {'OK' if perm else 'FAIL'}") print(f" analog noise margin, all layers: {margin:.3f} (guarantee 0.5)") analog_sweep(mm, states, refs, n) ok = bad == 0 and perm and abs(margin - 0.5) < 1e-6 print("PASS" if ok else "FAIL") return 0 if ok else 1 Block = Tuple[int, int, int, int] def rule(b: Block) -> Block: tl, tr, bl, br = b if b == (1, 0, 0, 1): return (0, 1, 1, 0) # diagonal pair -> other diagonal (deflect) if b == (0, 1, 1, 0): return (1, 0, 0, 1) return (br, bl, tr, tl) # otherwise rotate 180 degrees def is_bijection() -> bool: outs = {rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16)} return len(outs) == 16 def self_inverse() -> bool: return all(rule(rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0)))) == tuple((s >> k) & 1 for k in (3, 2, 1, 0)) for s in range(16)) def step(grid: List[List[int]], phase: int) -> List[List[int]]: """One Margolus update; phase 0 aligns blocks at even coordinates, phase 1 offsets them by (1,1).""" H, W = len(grid), len(grid[0]) out = [row[:] for row in grid] o = phase for r0 in range(o, o + H, 2): for c0 in range(o, o + W, 2): r, r1 = r0 % H, (r0 + 1) % H c, c1 = c0 % W, (c0 + 1) % W nb = rule((grid[r][c], grid[r][c1], grid[r1][c], grid[r1][c1])) out[r][c], out[r][c1], out[r1][c], out[r1][c1] = nb return out def ca_run(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]: g = grid for n in range(nsteps): g = step(g, (start_phase + n) & 1) return g def run_back(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]: """Undo `run`: replay the phase sequence in reverse; the rule is self-inverse.""" phases = [(start_phase + n) & 1 for n in range(nsteps)] g = grid for p in reversed(phases): g = step(g, p) return g # --- the block rule as Heaviside threshold gates --- # rule(s) = rotate180(s) XOR is_diag(s) on every cell: rotation fixes diagonal # pairs, and flipping all four cells of a rotated diagonal pair sends it to the # other diagonal. is_diag detects the two diagonal-pair states. def _H(x): return 1 if x >= 0 else 0 def _and(*xs): return _H(sum(xs) - len(xs)) def _or(*xs): return _H(sum(xs) - 1) def _xor(a, b): return _and(_or(a, b), _H(1 - a - b)) # OR AND NAND, the family's XOR def gate_rule(b: Block) -> Block: tl, tr, bl, br = b d = _or(_and(tl, 1 - tr, 1 - bl, br), _and(1 - tl, tr, bl, 1 - br)) # is_diag return (_xor(br, d), _xor(bl, d), _xor(tr, d), _xor(tl, d)) def _test_gates(): ok = all(gate_rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) == rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16)) print(f" block rule as Heaviside threshold gates matches over 16 states: " f"{'OK' if ok else 'FAIL'}") return ok # --- tests --- def _rand_grid(H, W, seed): import random rng = random.Random(seed) return [[rng.randint(0, 1) for _ in range(W)] for _ in range(H)] def _ball_positions(g): return {(r, c) for r, row in enumerate(g) for c, v in enumerate(row) if v} def _test_rule(): print(f" block rule is a bijection of 16 states: {'OK' if is_bijection() else 'FAIL'}") print(f" block rule is self-inverse: {'OK' if self_inverse() else 'FAIL'}") return is_bijection() and self_inverse() def _test_reversibility(): bad = 0 for seed in range(20): g = _rand_grid(8, 8, seed) fwd = ca_run(g, 25, start_phase=0) back = run_back(fwd, 25, start_phase=0) if back != g: bad += 1 # particle count is conserved (the rule permutes cells within each block) g = _rand_grid(8, 8, 99) conserved = sum(sum(r) for r in g) == sum(sum(r) for r in ca_run(g, 40)) print(f" lattice reversible (run then reverse recovers grid, 20 grids): " f"{'OK' if bad == 0 else f'FAIL({bad})'}") print(f" particle number conserved: {'OK' if conserved else 'FAIL'}") return bad == 0 and conserved def _test_ballistic(): # a single particle travels in a straight diagonal line H = W = 16 g = [[0] * W for _ in range(H)] g[2][2] = 1 positions = [next(iter(_ball_positions(g)))] gg = g for n in range(8): gg = step(gg, n & 1) p = _ball_positions(gg) positions.append(next(iter(p)) if len(p) == 1 else None) ok = all(p is not None for p in positions) steady = ok and all(positions[i + 1] == (positions[i][0] + 1, positions[i][1] + 1) for i in range(len(positions) - 1)) print(f" single particle stays a single particle: {'OK' if ok else 'FAIL'}") print(f" and moves ballistically on the diagonal, +(1,1) per step: " f"{'OK' if steady else 'FAIL'} trace={positions[:5]}") return ok and steady def interaction_gate(a: int, b: int) -> dict: """One billiard-ball collision as a reversible interaction gate.""" H = W = 12 g = [[0] * W for _ in range(H)] if a: g[2][2] = 1 if b: g[7][7] = 1 g = ca_run(g, 4) return {"A_and_B": g[3][6], "A_and_notB": g[6][6], "notA_and_B": g[3][3]} def and_gate(a: int, b: int) -> int: return interaction_gate(a, b)["A_and_B"] def _test_gate(): ok = True for a in (0, 1): for b in (0, 1): o = interaction_gate(a, b) ok &= (o["A_and_B"] == (a & b) and o["A_and_notB"] == (a & (1 - b)) and o["notA_and_B"] == ((1 - a) & b)) print(f" billiard-ball interaction gate (A&B, A&~B, ~A&B) over all 4 inputs: " f"{'OK' if ok else 'FAIL'}") return ok def and3(a: int, b: int, c: int) -> int: """Two composed collisions.""" H = W = 16 g = [[0] * W for _ in range(H)] if a: g[2][2] = 1 if b: g[7][7] = 1 if c: g[0][9] = 1 return ca_run(g, 4)[3][5] def _test_compose(): ok = all(and3(a, b, c) == (a & b & c) for a in (0, 1) for b in (0, 1) for c in (0, 1)) print(f" composed 3-input AND (two chained collisions) over all 8 inputs: " f"{'OK' if ok else 'FAIL'}") return ok def _test_collision(): # Two particles interact (the joint evolution differs from independent # motion) and the collision stays reversible: the physics that logic needs. H = W = 12 interacted = False revok = True # converging pairs: an SE-mover (even,even) meets an NW-mover (odd,odd) on a # shared diagonal, forming the 1001/0110 diagonal pair the rule deflects. for a, b in [((2, 2), (7, 7)), ((3, 3), (8, 8)), ((2, 8), (7, 3)), ((4, 4), (9, 9)), ((2, 2), (9, 9))]: g = [[0] * W for _ in range(H)] g[a[0]][a[1]] = 1 g[b[0]][b[1]] = 1 ga = [[0] * W for _ in range(H)] ga[a[0]][a[1]] = 1 gb = [[0] * W for _ in range(H)] gb[b[0]][b[1]] = 1 joint = g for n in range(12): joint = step(joint, n & 1) ga = step(ga, n & 1) gb = step(gb, n & 1) free = _ball_positions(ga) | _ball_positions(gb) if _ball_positions(joint) != free: interacted = True if run_back(ca_run(g, 12), 12) != g: revok = False print(f" two-particle collisions interact (joint != independent motion): " f"{'OK' if interacted else 'FAIL'}") print(f" collisions remain reversible: {'OK' if revok else 'FAIL'}") return interacted and revok CA_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_build_ca() -> 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 != 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, CA_OUT, metadata=meta) print(f"Built {os.path.relpath(CA_OUT, ROOT)}: reversible CA block rule as a ternary matrix tile") print(f" layers={info['layers']} gates={info['gates']} size={os.path.getsize(CA_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 = [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(CA_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 = _rand_grid(8, 8, 3) gmat = matrix_step(mm2, g, 0) gref = 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 # side -> (dx, dy, my_side, neighbor_side) _SIDES = [(0, 1, "N", "S"), (1, 0, "E", "W"), (0, -1, "S", "N"), (-1, 0, "W", "E")] class Tile: __slots__ = ("N", "E", "S", "W", "name") def __init__(self, N="", E="", S="", W="", name=""): self.N, self.E, self.S, self.W, self.name = N, E, S, W, name def glue(self, side): return getattr(self, side) def bind_strength(A: Dict[Tuple[int, int], Tile], x: int, y: int, t: Tile, strength: Dict[str, int]) -> int: """Summed strength of t's glues that match the abutting neighbor glues.""" s = 0 for dx, dy, side, opp in _SIDES: nb = A.get((x + dx, y + dy)) if nb is None: continue g = t.glue(side) if g and g == nb.glue(opp): s += strength.get(g, 1) return s def binds(A, x, y, t, tau, strength) -> bool: """The threshold-gate binding decision: H(sum strength*match - tau).""" return bind_strength(A, x, y, t, strength) >= tau def grow(tileset: List[Tile], seed: Dict[Tuple[int, int], Tile], tau: int, strength: Dict[str, int], bounds: Tuple[int, int, int, int], max_tiles: int = 100000) -> Tuple[Dict[Tuple[int, int], Tile], bool]: """Directed growth from a seed.""" x0, y0, x1, y1 = bounds A = dict(seed) deterministic = True changed = True while changed and len(A) < max_tiles: changed = False frontier = set() for (x, y) in list(A): for dx, dy, _, _ in _SIDES: p = (x + dx, y + dy) if p not in A and x0 <= p[0] <= x1 and y0 <= p[1] <= y1: frontier.add(p) for (x, y) in frontier: binders = [t for t in tileset if binds(A, x, y, t, tau, strength)] if len(binders) == 1: A[(x, y)] = binders[0] changed = True elif len(binders) > 1: deterministic = False return A, deterministic # --------------------------------------------------------------------------- # XOR / Sierpinski tile set: value(x,y) = value(x-1,y) XOR value(x,y-1) # --------------------------------------------------------------------------- def rule2_tileset(fn) -> List[Tile]: """Rule tiles for value(x,y) = fn(W, S): four cooperatively binding tiles emitting fn north and east.""" ts = [] for s in (0, 1): for w in (0, 1): v = fn(w, s) ts.append(Tile(N=f"v{v}", E=f"v{v}", S=f"v{s}", W=f"v{w}", name=f"R w{w} s{s} -> {v}")) return ts def sierpinski_tileset() -> List[Tile]: return rule2_tileset(lambda w, s: w ^ s) def _row_col_seed(bottom: List[int], left: List[int]): """Seed the bottom row and the left column with fixed value tiles.""" seed = {} for x, b in enumerate(bottom): seed[(x, 0)] = Tile(N=f"v{b}", E="", S="", W="", name=f"seedB{x}={b}") for y, l in enumerate(left): if y == 0: continue seed[(0, y)] = Tile(N="", E=f"v{l}", S="", W="", name=f"seedL{y}={l}") return seed def _test_binding_gate(): """The binding decision is exactly the Heaviside threshold gate.""" strength = {"v0": 1, "v1": 1} ts = sierpinski_tileset() A = {(1, 0): Tile(N="v1"), (0, 1): Tile(E="v0")} bad = 0 for t in ts: for x, y in [(1, 1)]: w = sum(strength.get(t.glue(side), 1) for dx, dy, side, opp in _SIDES if A.get((x + dx, y + dy)) and t.glue(side) and t.glue(side) == A[(x + dx, y + dy)].glue(opp)) gate = 1 if (w - 2) >= 0 else 0 # H(sum*match - tau) if gate != int(binds(A, x, y, t, 2, strength)): bad += 1 print(f" binding decision == Heaviside gate H(sum-tau): {'OK' if bad == 0 else 'FAIL'}") return bad == 0 def _test_rule2(fn, name, n=24): strength = {"v0": 1, "v1": 1} bottom = [1 if x == 0 else 0 for x in range(n)] left = [1 if y == 0 else 0 for y in range(n)] seed = _row_col_seed(bottom, left) A, det = grow(rule2_tileset(fn), seed, 2, strength, (0, 0, n - 1, n - 1)) def val(x, y): t = A.get((x, y)) return None if t is None else (1 if t.N == "v1" else 0) ref = {(x, 0): bottom[x] for x in range(n)} ref.update({(0, y): left[y] for y in range(n)}) for y in range(1, n): for x in range(1, n): ref[(x, y)] = fn(ref[(x - 1, y)], ref[(x, y - 1)]) filled = bad = 0 for y in range(1, n): for x in range(1, n): v = val(x, y) if v is not None: filled += 1 bad += v != ref[(x, y)] tag = "OK" if (det and bad == 0 and filled > 0) else "FAIL" print(f" rule-tile CA fn={name:3s}: directed={det} placed={filled} " f"every tile = fn(W,S) {tag}") return det and bad == 0 and filled > 0 # --------------------------------------------------------------------------- # Binary counter: each row is the row below plus one. LSB is the right column; # carry propagates west by cooperative binding (S = bit below, E = carry in). # --------------------------------------------------------------------------- def counter_tileset() -> List[Tile]: ts = [] for b in (0, 1): for c in (0, 1): ts.append(Tile(N=f"b{b ^ c}", E=f"c{c}", S=f"b{b}", W=f"c{b & c}", name=f"C b{b} c{c} -> b{b ^ c} carry{b & c}")) ts.append(Tile(N="edge", E="", S="edge", W="c1", name="edge(+1 injector)")) return ts def counter_seed(n: int): """Bottom row (y=0) all zero, plus the right-edge +1 injector column base.""" seed = {} for x in range(n): seed[(x, 0)] = Tile(N="b0", name=f"seed b0 col{x}") seed[(n, 0)] = Tile(N="edge", W="c1", name="seed edge") return seed def _test_counter(n=6, rows=None): rows = rows or (1 << n) - 1 strength = {"edge": 2} # value/carry glues default 1 A, det = grow(counter_tileset(), counter_seed(n), 2, strength, (0, 0, n, rows)) def rowval(y): bits = [] for x in range(n): t = A.get((x, y)) if t is None: return None bits.append(1 if t.N == "b1" else 0) return sum(bit << (n - 1 - x) for x, bit in enumerate(bits)) bad = filled = 0 for y in range(1, rows + 1): v = rowval(y) if v is not None: filled += 1 if v != (y & ((1 << n) - 1)): bad += 1 print(f" binary counter {n}-bit: directed={det} rows grown={filled} " f"row y encodes the integer y {'OK' if bad == 0 else f'FAIL({bad})'}") return det and bad == 0 and filled == rows TILE_OUT = os.path.join(ROOT, "variants", "neural_tile.safetensors") NBITS = 8 TAU = 2 def main_build_tile() -> int: ts = counter_tileset() strength = {"edge": 2} glues = sorted({g for t in ts for g in (t.N, t.E, t.S, t.W) if g}) gid = {g: i for i, g in enumerate(glues)} tile_glues = torch.tensor([[gid.get(t.N, -1), gid.get(t.E, -1), gid.get(t.S, -1), gid.get(t.W, -1)] for t in ts], dtype=torch.long) glue_strength = torch.tensor([strength.get(g, 1) for g in glues], dtype=torch.long) # per-tile binding-gate weights = strengths of the tile's four glues (0 = null) bind_w = torch.tensor([[strength.get(g, 1) if g else 0 for g in (t.N, t.E, t.S, t.W)] for t in ts], dtype=torch.long) tensors = {"tile_glues": tile_glues, "glue_strength": glue_strength, "binding_weight": bind_w, "binding_bias": torch.tensor(-TAU)} meta = {"machine": "tile", "tau": str(TAU), "glues": json.dumps(glues), "tile_names": json.dumps([t.name for t in ts]), "program": "binary counter"} save_file(tensors, TILE_OUT, metadata=meta) print(f"Built {os.path.relpath(TILE_OUT, ROOT)}: binary-counter tile set") print(f" tiles={len(ts)} glues={len(glues)} tau={TAU} size={os.path.getsize(TILE_OUT)} bytes") # round-trip: reconstruct the tiles from the file and regrow the counter t = load_file(TILE_OUT) with safe_open(TILE_OUT, framework="pt") as f: m = f.metadata() gl = json.loads(m["glues"]) strg = {gl[i]: int(s) for i, s in enumerate(t["glue_strength"].tolist())} tiles = [] for row, name in zip(t["tile_glues"].tolist(), json.loads(m["tile_names"])): sides = [gl[i] if i >= 0 else "" for i in row] tiles.append(Tile(N=sides[0], E=sides[1], S=sides[2], W=sides[3], name=name)) rows = (1 << NBITS) - 1 A, det = grow(tiles, counter_seed(NBITS), int(m["tau"]), strg, (0, 0, NBITS, rows)) bad = filled = 0 for y in range(1, rows + 1): cells = [A.get((x, y)) for x in range(NBITS)] if any(c is None for c in cells): continue filled += 1 v = sum((1 if c.N == "b1" else 0) << (NBITS - 1 - x) for x, c in enumerate(cells)) if v != (y & ((1 << NBITS) - 1)): bad += 1 print(f" round-trip regrow {NBITS}-bit counter: directed={det} rows={filled} " f"row y == y {'OK' if bad == 0 else f'FAIL({bad})'}") # the stored binding gate reproduces the model's binding decision gate_ok = True Atest = {(1, 0): Tile(N="b0"), (2, 0): Tile(N="b0")} for ti, tt in enumerate(tiles): for site in [(1, 1), (2, 1)]: w = t["binding_weight"][ti].tolist() match = [1 if tt.glue(side) and Atest.get((site[0] + dx, site[1] + dy)) and tt.glue(side) == Atest[(site[0] + dx, site[1] + dy)].glue(opp) else 0 for dx, dy, side, opp in _SIDES] gate = 1 if sum(wi * mi for wi, mi in zip(w, match)) + int(t["binding_bias"]) >= 0 else 0 if gate != int(binds(Atest, site[0], site[1], tt, TAU, strg)): gate_ok = False print(f" stored binding gate H(weight.match - tau) matches growth rule: " f"{'OK' if gate_ok else 'FAIL'}") return 0 if (bad == 0 and det and gate_ok) else 1 def cmd_attractor(): print("Attractor computer\n" + "=" * 40) print("Forward evaluation (exact, energy 0):") a = test_forward() print("Canonical relaxation:") b = test_energy_relax() print("Backward inversion (factoring by relaxation):") c_ = test_factor() print("SAT (clamp output to 1, relax to a model):") d = test_sat() print("=" * 40) print("ALL PASS" if (a and b and c_ and d) else "FAILURES") sys.exit(0 if (a and b and c_ and d) else 1) def cmd_reversible(): print("Reversible primitives + ALU") a = _test_primitives() b = _test_adder(4) and _test_adder(5) c = _test_alu(4) d = _test_bennett(4) print("PASS" if (a and b and c and d) else "FAIL") def cmd_reversible_cpu(): print("Reversible CPU") a = test_straight_line() b = test_bijection() print("PASS" if (a and b) else "FAIL") def cmd_reversible_prog(): print("Reversible structured programs") print("PASS" if _test() else "FAIL") def cmd_ca(): print("Reversible Margolus cellular automaton") a = _test_rule() g = _test_gates() b = _test_reversibility() c = _test_ballistic() d = _test_collision() e = _test_gate() f = _test_compose() print("PASS" if (a and g and b and c and d and e and f) else "FAIL") def cmd_tile(): print("Self-assembling tile computer") a = _test_binding_gate() b = all(_test_rule2(fn, nm) for fn, nm in [(lambda w, s: w ^ s, "XOR"), (lambda w, s: w & s, "AND"), (lambda w, s: w | s, "OR")]) c = _test_counter() print("PASS" if (a and b and c) else "FAIL") COMMANDS = {'attractor': cmd_attractor, 'build-attractor': main_build_attractor, 'reversible': cmd_reversible, 'reversible-cpu': cmd_reversible_cpu, 'reversible-prog': cmd_reversible_prog, 'build-reversible': main_build_reversible, 'reversible-matrix': main_reversible_matrix, 'ca': cmd_ca, 'build-ca': main_build_ca, 'tile': cmd_tile, 'build-tile': main_build_tile} if __name__ == "__main__": if len(sys.argv) < 2 or sys.argv[1] not in COMMANDS: sys.exit('commands: ' + ', '.join(COMMANDS)) cmd = sys.argv.pop(1) sys.argv[0] += ' ' + cmd sys.exit(COMMANDS[cmd]())
"""The attractor, reversible, cellular-automaton and tile machines, with their builders."""
from __future__ import annotations
import json
import math
import os
import random
import sys
from collections import defaultdict
from typing import Dict, List, Optional, Tuple
import torch
from safetensors import safe_open
from safetensors.torch import save_file, load_file
from machines import Net, compile_net, MatrixMachine
class Circuit:
"""Wire allocator and energy accumulator."""
def __init__(self) -> None:
self.n = 0
self.L: Dict[int, int] = defaultdict(int)
self.Q: Dict[Tuple[int, int], int] = defaultdict(int)
self.const = 0
self.gates: List[Tuple[str, int, Tuple[int, ...]]] = []
def wire(self) -> int:
i = self.n
self.n += 1
return i
def wires(self, k: int) -> List[int]:
return [self.wire() for _ in range(k)]
def _q(self, i: int, j: int, c: int) -> None:
if i == j:
self.L[i] += c
else:
self.Q[(min(i, j), max(i, j))] += c
def AND(self, x: int, y: int) -> int:
z = self.wire()
self.L[z] += 3
self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2)
self.gates.append(("AND", z, (x, y)))
return z
def OR(self, x: int, y: int) -> int:
z = self.wire()
self.L[x] += 1; self.L[y] += 1; self.L[z] += 1
self._q(x, y, 1); self._q(x, z, -2); self._q(y, z, -2)
self.gates.append(("OR", z, (x, y)))
return z
def NOT(self, x: int) -> int:
z = self.wire()
self.const += 1
self.L[x] += -1; self.L[z] += -1
self._q(x, z, 2)
self.gates.append(("NOT", z, (x,)))
return z
def XOR(self, x: int, y: int) -> int:
return self.OR(self.AND(x, self.NOT(y)), self.AND(self.NOT(x), y))
def full_adder(self, x: int, y: int, cin: int) -> Tuple[int, int]:
axy = self.XOR(x, y)
s = self.XOR(axy, cin)
cout = self.OR(self.AND(x, y), self.AND(cin, axy))
return s, cout
# ---- energy + couplings ------------------------------------------------
def energy(self, s: List[int]) -> int:
e = self.const
for i, c in self.L.items():
e += c * s[i]
for (i, j), c in self.Q.items():
e += c * s[i] * s[j]
return e
def neighbors(self) -> Dict[int, List[Tuple[int, int]]]:
nbr: Dict[int, List[Tuple[int, int]]] = defaultdict(list)
for (i, j), c in self.Q.items():
nbr[i].append((j, c))
nbr[j].append((i, c))
return nbr
# ---- relaxation modes --------------------------------------------------
def forward_eval(self, clamp: Dict[int, int]) -> List[int]:
"""Exact forward relaxation from the clamped inputs to the energy-0 fixed point."""
s = [0] * self.n
for w, v in clamp.items():
s[w] = v
for op, z, ins in self.gates:
if op == "AND":
s[z] = s[ins[0]] & s[ins[1]]
elif op == "OR":
s[z] = s[ins[0]] | s[ins[1]]