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 Download src/automata.py from phanerozoic/threshold-computers: direct link, hf CLI and curl.
- Browser
- Download file 64.1 kB
-
https://huggingface.co/phanerozoic/threshold-computers/resolve/main/src/automata.py
- Command line
-
hf download hf://phanerozoic/threshold-computers/src/automata.py
-
curl -L -o automata.py https://huggingface.co/phanerozoic/threshold-computers/resolve/main/src/automata.py
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]]