CharlesCNorton commited on
Commit ·
81376c6
1
Parent(s): 79eda78
neural_ca: a reversible cellular-automaton computer with no processor. One fixed Margolus block rule (rotate 180, diagonal pairs swap) applied identically to every 2x2 block of a lattice, alternating partition each step. The rule is a self-inverse permutation of the 16 block states, so the whole lattice update is a bijection; verified reversible over random lattices, particle-conserving, with ballistic single-particle motion and deflecting reversible collisions (the billiard-ball model, Turing-universal by Margolus 1984). The rule is Heaviside threshold gates and compiles to a 6-layer ternary matrix tile that is a permutation with a 0.5 margin; that one tile applied to every block is one whole-lattice step, shipped as variants/neural_ca.safetensors. eval_all skips it; README adds the section and updates counts (8 standalone machines, 27-file family round-trip).
Browse files- README.md +52 -8
- src/ca.py +205 -0
- src/eval_all.py +1 -0
- tools/build_ca.py +105 -0
- variants/neural_ca.safetensors +3 -0
README.md
CHANGED
|
@@ -40,9 +40,10 @@ variants/neural_subleq8io.safetensors SUBLEQ host for
|
|
| 40 |
variants/neural_reflect.safetensors interpreter whose state holds its own weights
|
| 41 |
variants/neural_attractor.safetensors energy-based solver; a multiplier run backward factors
|
| 42 |
variants/neural_reversible.safetensors reversible arithmetic core, a bijection with no erasure
|
|
|
|
| 43 |
```
|
| 44 |
|
| 45 |
-
|
| 46 |
they carry the family from the smallest possible processor to several results
|
| 47 |
about what a threshold network can be. `neural_subleq8` is a Turing-complete
|
| 48 |
one-instruction computer whose entire control flow is a single threshold
|
|
@@ -64,7 +65,11 @@ consistent assignment, so clamping different wires runs the same network forward
|
|
| 64 |
to evaluate, backward to invert (a multiplier run backward returns factors), or
|
| 65 |
as a SAT solver. And `neural_reversible` makes the entire state transition a
|
| 66 |
bijection, so no step erases information and the machine runs backward to
|
| 67 |
-
reconstruct its input, a processor with no Landauer erasure floor.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
|
| 69 |
---
|
| 70 |
|
|
@@ -360,7 +365,7 @@ Every weight and bias tensor in the canonical model fits in `int8`. The eval pip
|
|
| 360 |
|
| 361 |
The 8-bit arithmetic and ALU tests use strategic sampling rather than the full 65,536-case sweep because exhaustive coverage at 8-bit is feasible but not necessary given that the circuits are constructed gate-by-gate. The 16-bit and 32-bit arithmetic tests sample edge cases only; full exhaustive coverage at those widths is infeasible without specialized hardware.
|
| 362 |
|
| 363 |
-
`src/eval_all.py` runs the unified suite. Exit code is the number of failing variants (0 means all pass). **Testing is evaluation, not rebuilding**: `python src/eval_all.py variants/` scores all 18 fitness variants against the shipped weights in about two minutes (~6 s each, the composed float netlists evaluated in `NetlistEvaluator`'s leveled mode) and cleanly skips the
|
| 364 |
|
| 365 |
---
|
| 366 |
|
|
@@ -524,7 +529,7 @@ then the byte-for-byte safetensors file of the host itself.
|
|
| 524 |
|
| 525 |
The equality is machine-checked rather than observed on one run:
|
| 526 |
|
| 527 |
-
- the recipe codec round-trips every file in the family (all
|
| 528 |
`.safetensors`, 971 MB, byte-identical and sha-verified);
|
| 529 |
- the constructor program is executed on three independently-verified
|
| 530 |
backends — a pure-integer reference, the gate-graph `SubleqThresholdCPU`
|
|
@@ -664,6 +669,42 @@ python tools/reversible_matrix.py # ternary matrix stack: permutation transitio
|
|
| 664 |
|
| 665 |
---
|
| 666 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 667 |
## Threshold logic
|
| 668 |
|
| 669 |
A threshold gate computes a Boolean function by taking a weighted sum of binary inputs and comparing the result to a threshold; the output is 1 when the sum meets or exceeds the threshold and 0 otherwise. Equivalently, it is a neuron with Heaviside step activation, integer weights, and an integer bias.
|
|
@@ -836,10 +877,10 @@ Loss components: BCE on output bits, BCE on extracted A and B bits (2× weight),
|
|
| 836 |
|
| 837 |
```
|
| 838 |
neural_computer.safetensors canonical model (32-bit, 64 KB, ~8.61M params)
|
| 839 |
-
variants/ 18 fitness variants +
|
| 840 |
(neural_subleq8, neural_rv32, neural_matrix8,
|
| 841 |
neural_subleq8io, neural_reflect,
|
| 842 |
-
neural_attractor, neural_reversible)
|
| 843 |
src/ the library (run scripts as `python src/<name>.py`)
|
| 844 |
├── build.py generator (one safetensors per invocation; also `subleq`, `rv32`)
|
| 845 |
├── quantize.py min integer dtypes + ternary verification/repair
|
|
@@ -858,13 +899,16 @@ src/ the library (run scripts as `python src/<nam
|
|
| 858 |
├── reversible.py neural_reversible: reversible threshold gates, Cuccaro ALU,
|
| 859 |
│ Bennett construction
|
| 860 |
├── reversible_cpu.py reversible register machine: bijective step, backward execution
|
| 861 |
-
|
|
|
|
|
|
|
| 862 |
tools/ build_all.py (build + quantize + verify every profile),
|
| 863 |
cpu_programs.py (assembler + CPU program suite), test_cpu.py
|
| 864 |
(program suite vs a variant), play.py (interactive demo),
|
| 865 |
prune_weights.py (GPU-batched weight reduction),
|
| 866 |
build_attractor.py / test_attractor.py (neural_attractor),
|
| 867 |
-
build_reversible.py / reversible_matrix.py (neural_reversible)
|
|
|
|
| 868 |
llm_integration/ SmolLM2 extractor + circuit wrapper + training code
|
| 869 |
├── circuits.py FrozenThresholdCircuits (loads safetensors, exposes
|
| 870 |
│ add_8bit / sub_8bit / mul_8bit / compare_*)
|
|
|
|
| 40 |
variants/neural_reflect.safetensors interpreter whose state holds its own weights
|
| 41 |
variants/neural_attractor.safetensors energy-based solver; a multiplier run backward factors
|
| 42 |
variants/neural_reversible.safetensors reversible arithmetic core, a bijection with no erasure
|
| 43 |
+
variants/neural_ca.safetensors reversible cellular-automaton medium (no processor)
|
| 44 |
```
|
| 45 |
|
| 46 |
+
Eight further machines are detailed in their own sections below, and together
|
| 47 |
they carry the family from the smallest possible processor to several results
|
| 48 |
about what a threshold network can be. `neural_subleq8` is a Turing-complete
|
| 49 |
one-instruction computer whose entire control flow is a single threshold
|
|
|
|
| 65 |
to evaluate, backward to invert (a multiplier run backward returns factors), or
|
| 66 |
as a SAT solver. And `neural_reversible` makes the entire state transition a
|
| 67 |
bijection, so no step erases information and the machine runs backward to
|
| 68 |
+
reconstruct its input, a processor with no Landauer erasure floor. And
|
| 69 |
+
`neural_ca` removes the processor altogether: one fixed reversible rule applied
|
| 70 |
+
identically to every cell of a lattice, a spatially homogeneous medium of the
|
| 71 |
+
billiard-ball class in which a program is a configuration of particles and
|
| 72 |
+
computation is the medium evolving.
|
| 73 |
|
| 74 |
---
|
| 75 |
|
|
|
|
| 365 |
|
| 366 |
The 8-bit arithmetic and ALU tests use strategic sampling rather than the full 65,536-case sweep because exhaustive coverage at 8-bit is feasible but not necessary given that the circuits are constructed gate-by-gate. The 16-bit and 32-bit arithmetic tests sample edge cases only; full exhaustive coverage at those widths is infeasible without specialized hardware.
|
| 367 |
|
| 368 |
+
`src/eval_all.py` runs the unified suite. Exit code is the number of failing variants (0 means all pass). **Testing is evaluation, not rebuilding**: `python src/eval_all.py variants/` scores all 18 fitness variants against the shipped weights in about two minutes (~6 s each, the composed float netlists evaluated in `NetlistEvaluator`'s leveled mode) and cleanly skips the eight standalone machines. Rebuilding the models (`tools/build_all.py`, ~50 min for all 18) is a separate step, needed only when the circuit constructions in `src/build.py` change; routine verification never rebuilds. The batched evaluator is population-safe: every chained intermediate (carry, borrow, mux select) is computed per population slot, so `tools/prune_weights.py`'s parallel fitness screens are exact rather than slot-0 approximations.
|
| 369 |
|
| 370 |
---
|
| 371 |
|
|
|
|
| 529 |
|
| 530 |
The equality is machine-checked rather than observed on one run:
|
| 531 |
|
| 532 |
+
- the recipe codec round-trips every file in the family (all 27 shipped
|
| 533 |
`.safetensors`, 971 MB, byte-identical and sha-verified);
|
| 534 |
- the constructor program is executed on three independently-verified
|
| 535 |
backends — a pure-integer reference, the gate-graph `SubleqThresholdCPU`
|
|
|
|
| 669 |
|
| 670 |
---
|
| 671 |
|
| 672 |
+
## neural_ca — a reversible computing medium with no processor
|
| 673 |
+
|
| 674 |
+
The other machines still keep distinguished hardware. This one has none: the
|
| 675 |
+
entire machine is a single fixed rule applied identically to every 2x2 block of
|
| 676 |
+
a lattice, with the block partition alternating each step (the Margolus
|
| 677 |
+
neighborhood). No program counter, no memory unit, no control. A program is a
|
| 678 |
+
configuration of particles, and running it is letting the medium evolve.
|
| 679 |
+
|
| 680 |
+
The block rule rotates each block 180 degrees, except that two particles on a
|
| 681 |
+
diagonal swap to the other diagonal. It is a permutation of the sixteen block
|
| 682 |
+
states and its own inverse, so the whole lattice update is a bijection and the
|
| 683 |
+
medium is reversible: replaying the partition sequence backward reconstructs any
|
| 684 |
+
earlier configuration (verified over random lattices), and particle number is
|
| 685 |
+
conserved. The rule is the family's Heaviside threshold gates (a diagonal-pair
|
| 686 |
+
detector XORed onto the rotated cells) and compiles to a 6-layer ternary matrix
|
| 687 |
+
tile that is itself a permutation with the same 0.5 analog margin as
|
| 688 |
+
`neural_matrix8`; that one tile applied to every block is one whole-lattice step,
|
| 689 |
+
so `variants/neural_ca.safetensors` stores the rule of the medium, not a
|
| 690 |
+
processor.
|
| 691 |
+
|
| 692 |
+
Its dynamics are those of the billiard-ball model of computation (Fredkin and
|
| 693 |
+
Toffoli): isolated particles travel ballistically along diagonals, and
|
| 694 |
+
collisions deflect them reversibly, both verified here. Particle configurations
|
| 695 |
+
are therefore signals and collision geometries are logic; a Margolus
|
| 696 |
+
billiard-ball automaton of this class is Turing-universal, so a piece of this
|
| 697 |
+
homogeneous reversible medium computes by evolving. It is the family's maximal
|
| 698 |
+
dissolution of architecture: the same rule everywhere, no center, computation as
|
| 699 |
+
discrete reversible physics.
|
| 700 |
+
|
| 701 |
+
```bash
|
| 702 |
+
python src/ca.py # rule bijection, lattice reversibility, ballistic motion, collisions
|
| 703 |
+
python tools/build_ca.py # ship the block rule as a ternary matrix tile (permutation + 0.5 margin)
|
| 704 |
+
```
|
| 705 |
+
|
| 706 |
+
---
|
| 707 |
+
|
| 708 |
## Threshold logic
|
| 709 |
|
| 710 |
A threshold gate computes a Boolean function by taking a weighted sum of binary inputs and comparing the result to a threshold; the output is 1 when the sum meets or exceeds the threshold and 0 otherwise. Equivalently, it is a neuron with Heaviside step activation, integer weights, and an integer bias.
|
|
|
|
| 877 |
|
| 878 |
```
|
| 879 |
neural_computer.safetensors canonical model (32-bit, 64 KB, ~8.61M params)
|
| 880 |
+
variants/ 18 fitness variants + 8 standalone machines
|
| 881 |
(neural_subleq8, neural_rv32, neural_matrix8,
|
| 882 |
neural_subleq8io, neural_reflect,
|
| 883 |
+
neural_attractor, neural_reversible, neural_ca)
|
| 884 |
src/ the library (run scripts as `python src/<name>.py`)
|
| 885 |
├── build.py generator (one safetensors per invocation; also `subleq`, `rv32`)
|
| 886 |
├── quantize.py min integer dtypes + ternary verification/repair
|
|
|
|
| 899 |
├── reversible.py neural_reversible: reversible threshold gates, Cuccaro ALU,
|
| 900 |
│ Bennett construction
|
| 901 |
├── reversible_cpu.py reversible register machine: bijective step, backward execution
|
| 902 |
+
├── reversible_prog.py structured reversible programs (multiply, Fibonacci, Janus IF)
|
| 903 |
+
└── ca.py neural_ca: reversible Margolus cellular automaton, threshold
|
| 904 |
+
block rule, lattice reversibility and billiard-ball dynamics
|
| 905 |
tools/ build_all.py (build + quantize + verify every profile),
|
| 906 |
cpu_programs.py (assembler + CPU program suite), test_cpu.py
|
| 907 |
(program suite vs a variant), play.py (interactive demo),
|
| 908 |
prune_weights.py (GPU-batched weight reduction),
|
| 909 |
build_attractor.py / test_attractor.py (neural_attractor),
|
| 910 |
+
build_reversible.py / reversible_matrix.py (neural_reversible),
|
| 911 |
+
build_ca.py (neural_ca matrix tile)
|
| 912 |
llm_integration/ SmolLM2 extractor + circuit wrapper + training code
|
| 913 |
├── circuits.py FrozenThresholdCircuits (loads safetensors, exposes
|
| 914 |
│ add_8bit / sub_8bit / mul_8bit / compare_*)
|
src/ca.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reversible cellular-automaton computer: a spatially homogeneous physics.
|
| 2 |
+
|
| 3 |
+
There is no processor here. The whole machine is one fixed rule applied
|
| 4 |
+
identically to every 2x2 block of a lattice, alternating the block partition
|
| 5 |
+
each step (the Margolus neighborhood). No program counter, no memory unit, no
|
| 6 |
+
control: computation is what a uniform reversible medium does as it evolves, and
|
| 7 |
+
a program is a configuration of cells (particles) inside it.
|
| 8 |
+
|
| 9 |
+
The block rule is a permutation of the sixteen 2x2 block states, so the whole
|
| 10 |
+
lattice update is a bijection and the medium is reversible. It is of the
|
| 11 |
+
billiard-ball class: isolated particles travel ballistically and collisions
|
| 12 |
+
deflect them, which is Turing-universal (Margolus 1984; particles are signals,
|
| 13 |
+
fixed particle clusters are mirrors, and collision geometries are gates).
|
| 14 |
+
|
| 15 |
+
Rule (cells ordered TL,TR,BL,BR): rotate every block 180 degrees, except a pair
|
| 16 |
+
of particles on a diagonal swaps to the other diagonal (the deflecting
|
| 17 |
+
collision). Both operations are involutions and neither moves a state between
|
| 18 |
+
the diagonal-pair set and its complement, so the rule is its own inverse and the
|
| 19 |
+
lattice update run with the partition sequence reversed undoes the computation.
|
| 20 |
+
"""
|
| 21 |
+
from __future__ import annotations
|
| 22 |
+
from typing import List, Tuple
|
| 23 |
+
|
| 24 |
+
Block = Tuple[int, int, int, int]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def rule(b: Block) -> Block:
|
| 28 |
+
tl, tr, bl, br = b
|
| 29 |
+
if b == (1, 0, 0, 1):
|
| 30 |
+
return (0, 1, 1, 0) # diagonal pair -> other diagonal (deflect)
|
| 31 |
+
if b == (0, 1, 1, 0):
|
| 32 |
+
return (1, 0, 0, 1)
|
| 33 |
+
return (br, bl, tr, tl) # otherwise rotate 180 degrees
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def is_bijection() -> bool:
|
| 37 |
+
outs = {rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16)}
|
| 38 |
+
return len(outs) == 16
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def self_inverse() -> bool:
|
| 42 |
+
return all(rule(rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))))
|
| 43 |
+
== tuple((s >> k) & 1 for k in (3, 2, 1, 0)) for s in range(16))
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def step(grid: List[List[int]], phase: int) -> List[List[int]]:
|
| 47 |
+
"""One Margolus update. phase 0 aligns blocks at even coordinates; phase 1
|
| 48 |
+
offsets the partition by (1,1). Toroidal, so H and W must be even."""
|
| 49 |
+
H, W = len(grid), len(grid[0])
|
| 50 |
+
out = [row[:] for row in grid]
|
| 51 |
+
o = phase
|
| 52 |
+
for r0 in range(o, o + H, 2):
|
| 53 |
+
for c0 in range(o, o + W, 2):
|
| 54 |
+
r, r1 = r0 % H, (r0 + 1) % H
|
| 55 |
+
c, c1 = c0 % W, (c0 + 1) % W
|
| 56 |
+
nb = rule((grid[r][c], grid[r][c1], grid[r1][c], grid[r1][c1]))
|
| 57 |
+
out[r][c], out[r][c1], out[r1][c], out[r1][c1] = nb
|
| 58 |
+
return out
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def run(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]:
|
| 62 |
+
g = grid
|
| 63 |
+
for n in range(nsteps):
|
| 64 |
+
g = step(g, (start_phase + n) & 1)
|
| 65 |
+
return g
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
def run_back(grid: List[List[int]], nsteps: int, start_phase: int = 0) -> List[List[int]]:
|
| 69 |
+
"""Undo `run`: replay the phase sequence in reverse; the rule is self-inverse."""
|
| 70 |
+
phases = [(start_phase + n) & 1 for n in range(nsteps)]
|
| 71 |
+
g = grid
|
| 72 |
+
for p in reversed(phases):
|
| 73 |
+
g = step(g, p)
|
| 74 |
+
return g
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
# --- the block rule as Heaviside threshold gates ---
|
| 78 |
+
# rule(s) = rotate180(s) XOR is_diag(s) on every cell: rotation fixes diagonal
|
| 79 |
+
# pairs, and flipping all four cells of a rotated diagonal pair sends it to the
|
| 80 |
+
# other diagonal. is_diag detects the two diagonal-pair states.
|
| 81 |
+
def _H(x):
|
| 82 |
+
return 1 if x >= 0 else 0
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _and(*xs):
|
| 86 |
+
return _H(sum(xs) - len(xs))
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
def _or(*xs):
|
| 90 |
+
return _H(sum(xs) - 1)
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _xor(a, b):
|
| 94 |
+
return _and(_or(a, b), _H(1 - a - b)) # OR AND NAND, the family's XOR
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
def gate_rule(b: Block) -> Block:
|
| 98 |
+
tl, tr, bl, br = b
|
| 99 |
+
d = _or(_and(tl, 1 - tr, 1 - bl, br), _and(1 - tl, tr, bl, 1 - br)) # is_diag
|
| 100 |
+
return (_xor(br, d), _xor(bl, d), _xor(tr, d), _xor(tl, d))
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _test_gates():
|
| 104 |
+
ok = all(gate_rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0)))
|
| 105 |
+
== rule(tuple((s >> k) & 1 for k in (3, 2, 1, 0))) for s in range(16))
|
| 106 |
+
print(f" block rule as Heaviside threshold gates matches over 16 states: "
|
| 107 |
+
f"{'OK' if ok else 'FAIL'}")
|
| 108 |
+
return ok
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# --- tests ---
|
| 112 |
+
def _rand_grid(H, W, seed):
|
| 113 |
+
import random
|
| 114 |
+
rng = random.Random(seed)
|
| 115 |
+
return [[rng.randint(0, 1) for _ in range(W)] for _ in range(H)]
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def _ball_positions(g):
|
| 119 |
+
return {(r, c) for r, row in enumerate(g) for c, v in enumerate(row) if v}
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
def _test_rule():
|
| 123 |
+
print(f" block rule is a bijection of 16 states: {'OK' if is_bijection() else 'FAIL'}")
|
| 124 |
+
print(f" block rule is self-inverse: {'OK' if self_inverse() else 'FAIL'}")
|
| 125 |
+
return is_bijection() and self_inverse()
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def _test_reversibility():
|
| 129 |
+
bad = 0
|
| 130 |
+
for seed in range(20):
|
| 131 |
+
g = _rand_grid(8, 8, seed)
|
| 132 |
+
fwd = run(g, 25, start_phase=0)
|
| 133 |
+
back = run_back(fwd, 25, start_phase=0)
|
| 134 |
+
if back != g:
|
| 135 |
+
bad += 1
|
| 136 |
+
# particle count is conserved (the rule permutes cells within each block)
|
| 137 |
+
g = _rand_grid(8, 8, 99)
|
| 138 |
+
conserved = sum(sum(r) for r in g) == sum(sum(r) for r in run(g, 40))
|
| 139 |
+
print(f" lattice reversible (run then reverse recovers grid, 20 grids): "
|
| 140 |
+
f"{'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 141 |
+
print(f" particle number conserved: {'OK' if conserved else 'FAIL'}")
|
| 142 |
+
return bad == 0 and conserved
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def _test_ballistic():
|
| 146 |
+
# a single particle travels in a straight diagonal line
|
| 147 |
+
H = W = 16
|
| 148 |
+
g = [[0] * W for _ in range(H)]
|
| 149 |
+
g[2][2] = 1
|
| 150 |
+
positions = [next(iter(_ball_positions(g)))]
|
| 151 |
+
gg = g
|
| 152 |
+
for n in range(8):
|
| 153 |
+
gg = step(gg, n & 1)
|
| 154 |
+
p = _ball_positions(gg)
|
| 155 |
+
positions.append(next(iter(p)) if len(p) == 1 else None)
|
| 156 |
+
ok = all(p is not None for p in positions)
|
| 157 |
+
steady = ok and all(positions[i + 1] == (positions[i][0] + 1, positions[i][1] + 1)
|
| 158 |
+
for i in range(len(positions) - 1))
|
| 159 |
+
print(f" single particle stays a single particle: {'OK' if ok else 'FAIL'}")
|
| 160 |
+
print(f" and moves ballistically on the diagonal, +(1,1) per step: "
|
| 161 |
+
f"{'OK' if steady else 'FAIL'} trace={positions[:5]}")
|
| 162 |
+
return ok and steady
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
def _test_collision():
|
| 166 |
+
# Two particles interact (the joint evolution differs from independent
|
| 167 |
+
# motion) and the collision stays reversible: the physics that logic needs.
|
| 168 |
+
H = W = 12
|
| 169 |
+
interacted = False
|
| 170 |
+
revok = True
|
| 171 |
+
# converging pairs: an SE-mover (even,even) meets an NW-mover (odd,odd) on a
|
| 172 |
+
# shared diagonal, forming the 1001/0110 diagonal pair the rule deflects.
|
| 173 |
+
for a, b in [((2, 2), (7, 7)), ((3, 3), (8, 8)), ((2, 8), (7, 3)),
|
| 174 |
+
((4, 4), (9, 9)), ((2, 2), (9, 9))]:
|
| 175 |
+
g = [[0] * W for _ in range(H)]
|
| 176 |
+
g[a[0]][a[1]] = 1
|
| 177 |
+
g[b[0]][b[1]] = 1
|
| 178 |
+
ga = [[0] * W for _ in range(H)]
|
| 179 |
+
ga[a[0]][a[1]] = 1
|
| 180 |
+
gb = [[0] * W for _ in range(H)]
|
| 181 |
+
gb[b[0]][b[1]] = 1
|
| 182 |
+
joint = g
|
| 183 |
+
for n in range(12):
|
| 184 |
+
joint = step(joint, n & 1)
|
| 185 |
+
ga = step(ga, n & 1)
|
| 186 |
+
gb = step(gb, n & 1)
|
| 187 |
+
free = _ball_positions(ga) | _ball_positions(gb)
|
| 188 |
+
if _ball_positions(joint) != free:
|
| 189 |
+
interacted = True
|
| 190 |
+
if run_back(run(g, 12), 12) != g:
|
| 191 |
+
revok = False
|
| 192 |
+
print(f" two-particle collisions interact (joint != independent motion): "
|
| 193 |
+
f"{'OK' if interacted else 'FAIL'}")
|
| 194 |
+
print(f" collisions remain reversible: {'OK' if revok else 'FAIL'}")
|
| 195 |
+
return interacted and revok
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
if __name__ == "__main__":
|
| 199 |
+
print("Reversible Margolus cellular automaton")
|
| 200 |
+
a = _test_rule()
|
| 201 |
+
g = _test_gates()
|
| 202 |
+
b = _test_reversibility()
|
| 203 |
+
c = _test_ballistic()
|
| 204 |
+
d = _test_collision()
|
| 205 |
+
print("PASS" if (a and g and b and c and d) else "FAIL")
|
src/eval_all.py
CHANGED
|
@@ -672,6 +672,7 @@ MACHINE_VERIFIER = {
|
|
| 672 |
"reflect": "reflect.py",
|
| 673 |
"attractor": "tools/test_attractor.py",
|
| 674 |
"reversible": "src/reversible.py",
|
|
|
|
| 675 |
}
|
| 676 |
|
| 677 |
|
|
|
|
| 672 |
"reflect": "reflect.py",
|
| 673 |
"attractor": "tools/test_attractor.py",
|
| 674 |
"reversible": "src/reversible.py",
|
| 675 |
+
"ca": "src/ca.py",
|
| 676 |
}
|
| 677 |
|
| 678 |
|
tools/build_ca.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ship the reversible CA's local rule as a ternary matrix tile,
|
| 2 |
+
variants/neural_ca.safetensors. The 2x2 block rule compiles to a stack of
|
| 3 |
+
ternary matrices with a Heaviside step; because the rule is a bijection the tile
|
| 4 |
+
is a permutation matrix product, crossbar-realizable with a 0.5 margin, and the
|
| 5 |
+
same tile applied to every block of a lattice is one step of the whole machine.
|
| 6 |
+
No processor is stored, only the rule of the medium."""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
import os
|
| 9 |
+
import sys
|
| 10 |
+
|
| 11 |
+
import torch
|
| 12 |
+
from safetensors.torch import save_file, load_file
|
| 13 |
+
from safetensors import safe_open
|
| 14 |
+
|
| 15 |
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 16 |
+
sys.path.insert(0, os.path.join(ROOT, "src"))
|
| 17 |
+
import ca
|
| 18 |
+
from matrix8 import Net, compile_net, MatrixMachine
|
| 19 |
+
|
| 20 |
+
OUT = os.path.join(ROOT, "variants", "neural_ca.safetensors")
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def block_net():
|
| 24 |
+
net = Net()
|
| 25 |
+
ntl, ntr = net.NOT("ntl", "tl"), net.NOT("ntr", "tr")
|
| 26 |
+
nbl, nbr = net.NOT("nbl", "bl"), net.NOT("nbr", "br")
|
| 27 |
+
d1 = net.AND("d1", ["tl", ntr, nbl, "br"])
|
| 28 |
+
d2 = net.AND("d2", [ntl, "tr", "bl", nbr])
|
| 29 |
+
dg = net.OR("dg", [d1, d2]) # is_diag
|
| 30 |
+
TL = net.XOR("oTL", "br", dg)
|
| 31 |
+
TR = net.XOR("oTR", "bl", dg)
|
| 32 |
+
BL = net.XOR("oBL", "tr", dg)
|
| 33 |
+
BR = net.XOR("oBR", "tl", dg)
|
| 34 |
+
return net, ["tl", "tr", "bl", "br"], [TL, TR, BL, BR]
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def matrix_step(mm, grid, phase):
|
| 38 |
+
"""One Margolus update driven entirely by the matrix tile."""
|
| 39 |
+
H, W = len(grid), len(grid[0])
|
| 40 |
+
out = [row[:] for row in grid]
|
| 41 |
+
for r0 in range(phase, phase + H, 2):
|
| 42 |
+
for c0 in range(phase, phase + W, 2):
|
| 43 |
+
r, r1, c, c1 = r0 % H, (r0 + 1) % H, c0 % W, (c0 + 1) % W
|
| 44 |
+
v = torch.tensor([[float(grid[r][c]), float(grid[r][c1]),
|
| 45 |
+
float(grid[r1][c]), float(grid[r1][c1])]])
|
| 46 |
+
o = mm.step(v)[0]
|
| 47 |
+
out[r][c], out[r][c1], out[r1][c], out[r1][c1] = (int(o[0]), int(o[1]),
|
| 48 |
+
int(o[2]), int(o[3]))
|
| 49 |
+
return out
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def main() -> int:
|
| 53 |
+
net, inp, outp = block_net()
|
| 54 |
+
layers, info = compile_net(net, inp, outp)
|
| 55 |
+
mm = MatrixMachine(layers)
|
| 56 |
+
|
| 57 |
+
seen, bad, vecs = set(), 0, []
|
| 58 |
+
for s in range(16):
|
| 59 |
+
b = tuple((s >> k) & 1 for k in (3, 2, 1, 0)) # tl,tr,bl,br
|
| 60 |
+
v = torch.tensor([[float(x) for x in b]])
|
| 61 |
+
got = tuple(int(x) for x in mm.step(v)[0])
|
| 62 |
+
if got != ca.rule(b):
|
| 63 |
+
bad += 1
|
| 64 |
+
seen.add(got)
|
| 65 |
+
vecs.append(v[0])
|
| 66 |
+
perm = len(seen) == 16
|
| 67 |
+
margin = mm.min_margin(torch.stack(vecs))
|
| 68 |
+
|
| 69 |
+
tensors = {}
|
| 70 |
+
for k, (W, B) in enumerate(layers):
|
| 71 |
+
tensors[f"matrix.layer{k:03d}.weight"] = W.to(torch.int8)
|
| 72 |
+
tensors[f"matrix.layer{k:03d}.bias"] = B.to(torch.int8)
|
| 73 |
+
meta = {"machine": "ca",
|
| 74 |
+
"rule": "Margolus reversible: rotate 180 except diagonal pair swaps (BBM class)",
|
| 75 |
+
"inputs": "tl,tr,bl,br", "outputs": "TL,TR,BL,BR", "layers": str(info["layers"])}
|
| 76 |
+
save_file(tensors, OUT, metadata=meta)
|
| 77 |
+
print(f"Built {os.path.relpath(OUT, ROOT)}: reversible CA block rule as a ternary matrix tile")
|
| 78 |
+
print(f" layers={info['layers']} gates={info['gates']} size={os.path.getsize(OUT)} bytes")
|
| 79 |
+
print(f" every weight ternary: "
|
| 80 |
+
f"{'OK' if all(((W == -1) | (W == 0) | (W == 1)).all() for W, _ in layers) else 'FAIL'}")
|
| 81 |
+
print(f" tile matches the block rule over all 16 states: {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 82 |
+
print(f" tile is a permutation (16 distinct outputs): {'OK' if perm else 'FAIL'}")
|
| 83 |
+
print(f" analog noise margin: {margin:.3f} (guarantee 0.5)")
|
| 84 |
+
|
| 85 |
+
# the loaded tile, applied to every block, is one whole-lattice CA step
|
| 86 |
+
t = load_file(OUT)
|
| 87 |
+
n = 0
|
| 88 |
+
lyr = []
|
| 89 |
+
while f"matrix.layer{n:03d}.weight" in t:
|
| 90 |
+
lyr.append((t[f"matrix.layer{n:03d}.weight"], t[f"matrix.layer{n:03d}.bias"]))
|
| 91 |
+
n += 1
|
| 92 |
+
mm2 = MatrixMachine(lyr)
|
| 93 |
+
g = ca._rand_grid(8, 8, 3)
|
| 94 |
+
gmat = matrix_step(mm2, g, 0)
|
| 95 |
+
gref = ca.step(g, 0)
|
| 96 |
+
print(f" loaded tile drives a full lattice step (matches ca.step): "
|
| 97 |
+
f"{'OK' if gmat == gref else 'FAIL'}")
|
| 98 |
+
|
| 99 |
+
ok = bad == 0 and perm and abs(margin - 0.5) < 1e-6 and gmat == gref
|
| 100 |
+
print("PASS" if ok else "FAIL")
|
| 101 |
+
return 0 if ok else 1
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
if __name__ == "__main__":
|
| 105 |
+
sys.exit(main())
|
variants/neural_ca.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d10be4d24d91d2d1100022369d8809c7be9fad154c1347f38ae74c7d59a25ee4
|
| 3 |
+
size 1329
|