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
Files changed (5) hide show
  1. README.md +52 -8
  2. src/ca.py +205 -0
  3. src/eval_all.py +1 -0
  4. tools/build_ca.py +105 -0
  5. 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
- Seven further machines are detailed in their own sections below, and together
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 seven 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.
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 26 shipped
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 + 7 standalone machines
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
- ── reversible_prog.py structured reversible programs (multiply, Fibonacci, Janus IF)
 
 
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