CharlesCNorton commited on
Commit ·
4dbae82
1
Parent(s): 44ae225
neural_tile: a self-assembling tile computer in the abstract tile assembly model. A tile binds at a site when the summed strength of its matching glues reaches tau, which is the Heaviside gate H(strength.match - tau), so growth is governed by threshold neurons. Verified: the binding decision equals the gate; a general 2-input rule-tile set grows value(x,y)=f(W,S) for f in XOR/AND/OR (529 tiles each, checked against the recurrence, XOR = Sierpinski/Rule 90); a binary counter grows one integer per row (8-bit, 255 rows, row y encodes y) with carry by cooperative binding; both directed (deterministic). Turing-universal at tau=2 (Winfree 1998). Ships variants/neural_tile.safetensors (glue tables + binding-gate weights); eval_all skips it; README section and counts updated (9 standalone machines, 28-file family).
Browse files- README.md +49 -9
- src/eval_all.py +1 -0
- src/tile.py +222 -0
- tools/build_tile.py +89 -0
- variants/neural_tile.safetensors +3 -0
README.md
CHANGED
|
@@ -41,9 +41,10 @@ variants/neural_reflect.safetensors interpreter who
|
|
| 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 |
-
|
| 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
|
|
@@ -69,7 +70,10 @@ reconstruct its input, a processor with no Landauer erasure floor. And
|
|
| 69 |
`neural_ca` has no processor at all: one fixed reversible rule applied to every
|
| 70 |
2x2 block of a lattice (a Margolus cellular automaton), where a particle
|
| 71 |
collision computes an AND gate and ballistic transport plus collisions are the
|
| 72 |
-
billiard-ball universality primitives.
|
|
|
|
|
|
|
|
|
|
| 73 |
|
| 74 |
---
|
| 75 |
|
|
@@ -365,7 +369,7 @@ Every weight and bias tensor in the canonical model fits in `int8`. The eval pip
|
|
| 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
|
| 369 |
|
| 370 |
---
|
| 371 |
|
|
@@ -529,7 +533,7 @@ then the byte-for-byte safetensors file of the host itself.
|
|
| 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
|
| 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`
|
|
@@ -706,6 +710,39 @@ python tools/build_ca.py # ship the block rule as a ternary matrix tile (per
|
|
| 706 |
|
| 707 |
---
|
| 708 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 709 |
## Threshold logic
|
| 710 |
|
| 711 |
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.
|
|
@@ -878,10 +915,11 @@ Loss components: BCE on output bits, BCE on extracted A and B bits (2× weight),
|
|
| 878 |
|
| 879 |
```
|
| 880 |
neural_computer.safetensors canonical model (32-bit, 64 KB, ~8.61M params)
|
| 881 |
-
variants/ 18 fitness variants +
|
| 882 |
(neural_subleq8, neural_rv32, neural_matrix8,
|
| 883 |
neural_subleq8io, neural_reflect,
|
| 884 |
-
neural_attractor, neural_reversible, neural_ca
|
|
|
|
| 885 |
src/ the library (run scripts as `python src/<name>.py`)
|
| 886 |
├── build.py generator (one safetensors per invocation; also `subleq`, `rv32`)
|
| 887 |
├── quantize.py min integer dtypes + ternary verification/repair
|
|
@@ -901,15 +939,17 @@ src/ the library (run scripts as `python src/<nam
|
|
| 901 |
│ Bennett construction
|
| 902 |
├── reversible_cpu.py reversible register machine: bijective step, backward execution
|
| 903 |
├── reversible_prog.py structured reversible programs (multiply, Fibonacci, Janus IF)
|
| 904 |
-
|
| 905 |
-
|
|
|
|
|
|
|
| 906 |
tools/ build_all.py (build + quantize + verify every profile),
|
| 907 |
cpu_programs.py (assembler + CPU program suite), test_cpu.py
|
| 908 |
(program suite vs a variant), play.py (interactive demo),
|
| 909 |
prune_weights.py (GPU-batched weight reduction),
|
| 910 |
build_attractor.py / test_attractor.py (neural_attractor),
|
| 911 |
build_reversible.py / reversible_matrix.py (neural_reversible),
|
| 912 |
-
build_ca.py (neural_ca matrix tile)
|
| 913 |
llm_integration/ SmolLM2 extractor + circuit wrapper + training code
|
| 914 |
├── circuits.py FrozenThresholdCircuits (loads safetensors, exposes
|
| 915 |
│ add_8bit / sub_8bit / mul_8bit / compare_*)
|
|
|
|
| 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 |
+
variants/neural_tile.safetensors self-assembling tile computer (computation as growth)
|
| 45 |
```
|
| 46 |
|
| 47 |
+
Nine further machines are detailed in their own sections below, and together
|
| 48 |
they carry the family from the smallest possible processor to several results
|
| 49 |
about what a threshold network can be. `neural_subleq8` is a Turing-complete
|
| 50 |
one-instruction computer whose entire control flow is a single threshold
|
|
|
|
| 70 |
`neural_ca` has no processor at all: one fixed reversible rule applied to every
|
| 71 |
2x2 block of a lattice (a Margolus cellular automaton), where a particle
|
| 72 |
collision computes an AND gate and ballistic transport plus collisions are the
|
| 73 |
+
billiard-ball universality primitives. And `neural_tile` computes by
|
| 74 |
+
self-assembly: a tile set whose binding rule is a threshold gate grows a crystal
|
| 75 |
+
that is the trace of a computation, a Sierpinski (Rule 90) pattern or a binary
|
| 76 |
+
counter, Turing-universal at temperature 2 (Winfree 1998).
|
| 77 |
|
| 78 |
---
|
| 79 |
|
|
|
|
| 369 |
|
| 370 |
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.
|
| 371 |
|
| 372 |
+
`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 nine 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.
|
| 373 |
|
| 374 |
---
|
| 375 |
|
|
|
|
| 533 |
|
| 534 |
The equality is machine-checked rather than observed on one run:
|
| 535 |
|
| 536 |
+
- the recipe codec round-trips every file in the family (all 28 shipped
|
| 537 |
`.safetensors`, 971 MB, byte-identical and sha-verified);
|
| 538 |
- the constructor program is executed on three independently-verified
|
| 539 |
backends — a pure-integer reference, the gate-graph `SubleqThresholdCPU`
|
|
|
|
| 710 |
|
| 711 |
---
|
| 712 |
|
| 713 |
+
## neural_tile — computation as self-assembly
|
| 714 |
+
|
| 715 |
+
A tile computer in the abstract tile assembly model. The program is a finite set
|
| 716 |
+
of square tiles with glue labels and integer strengths on their edges; a seed is
|
| 717 |
+
placed and tiles accrete onto the assembly by one rule: a tile binds at an empty
|
| 718 |
+
site when the summed strength of its glues that match the already-present
|
| 719 |
+
neighbors reaches the temperature tau. That binding decision is the Heaviside
|
| 720 |
+
gate `H(sum_d strength_d * match_d - tau)`, weights the glue strengths and bias
|
| 721 |
+
`-tau`, so every attachment is a threshold neuron and the assembly grows site by
|
| 722 |
+
site under the family's gate. At tau = 2 the model is Turing-universal
|
| 723 |
+
(Winfree 1998).
|
| 724 |
+
|
| 725 |
+
Two directed tile sets are verified. The rule-tile set computes
|
| 726 |
+
`value(x,y) = f(value(x-1,y), value(x,y-1))` for any 2-input `f`: with `f` = XOR
|
| 727 |
+
the assembly is the Sierpinski triangle (Rule 90), and AND and OR grow their own
|
| 728 |
+
patterns, each of 529 tiles checked cell by cell against the recurrence. The
|
| 729 |
+
binary counter grows one integer per row: at 8-bit width it fills 255 rows and
|
| 730 |
+
row y encodes the integer y, with the increment carry propagating westward by
|
| 731 |
+
cooperative binding. Both tile sets are directed, so a unique tile binds at each
|
| 732 |
+
site and the assembly is deterministic.
|
| 733 |
+
|
| 734 |
+
The shipped `variants/neural_tile.safetensors` stores the counter tile set as its
|
| 735 |
+
glue tables together with the per-tile binding-gate weights (glue strengths) and
|
| 736 |
+
bias (`-tau`); reloading it regrows the counter and reproduces the binding
|
| 737 |
+
decision from the gate.
|
| 738 |
+
|
| 739 |
+
```bash
|
| 740 |
+
python src/tile.py # binding gate, XOR/AND/OR rule tiles, binary counter
|
| 741 |
+
python tools/build_tile.py # ship + regrow variants/neural_tile.safetensors
|
| 742 |
+
```
|
| 743 |
+
|
| 744 |
+
---
|
| 745 |
+
|
| 746 |
## Threshold logic
|
| 747 |
|
| 748 |
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.
|
|
|
|
| 915 |
|
| 916 |
```
|
| 917 |
neural_computer.safetensors canonical model (32-bit, 64 KB, ~8.61M params)
|
| 918 |
+
variants/ 18 fitness variants + 9 standalone machines
|
| 919 |
(neural_subleq8, neural_rv32, neural_matrix8,
|
| 920 |
neural_subleq8io, neural_reflect,
|
| 921 |
+
neural_attractor, neural_reversible, neural_ca,
|
| 922 |
+
neural_tile)
|
| 923 |
src/ the library (run scripts as `python src/<name>.py`)
|
| 924 |
├── build.py generator (one safetensors per invocation; also `subleq`, `rv32`)
|
| 925 |
├── quantize.py min integer dtypes + ternary verification/repair
|
|
|
|
| 939 |
│ Bennett construction
|
| 940 |
├── reversible_cpu.py reversible register machine: bijective step, backward execution
|
| 941 |
├── reversible_prog.py structured reversible programs (multiply, Fibonacci, Janus IF)
|
| 942 |
+
├── ca.py neural_ca: reversible Margolus cellular automaton, threshold
|
| 943 |
+
│ block rule, lattice reversibility and billiard-ball dynamics
|
| 944 |
+
└── tile.py neural_tile: abstract tile assembly, threshold binding rule,
|
| 945 |
+
XOR/Sierpinski and binary-counter tile sets
|
| 946 |
tools/ build_all.py (build + quantize + verify every profile),
|
| 947 |
cpu_programs.py (assembler + CPU program suite), test_cpu.py
|
| 948 |
(program suite vs a variant), play.py (interactive demo),
|
| 949 |
prune_weights.py (GPU-batched weight reduction),
|
| 950 |
build_attractor.py / test_attractor.py (neural_attractor),
|
| 951 |
build_reversible.py / reversible_matrix.py (neural_reversible),
|
| 952 |
+
build_ca.py (neural_ca matrix tile), build_tile.py (neural_tile)
|
| 953 |
llm_integration/ SmolLM2 extractor + circuit wrapper + training code
|
| 954 |
├── circuits.py FrozenThresholdCircuits (loads safetensors, exposes
|
| 955 |
│ add_8bit / sub_8bit / mul_8bit / compare_*)
|
src/eval_all.py
CHANGED
|
@@ -673,6 +673,7 @@ MACHINE_VERIFIER = {
|
|
| 673 |
"attractor": "tools/test_attractor.py",
|
| 674 |
"reversible": "src/reversible.py",
|
| 675 |
"ca": "src/ca.py",
|
|
|
|
| 676 |
}
|
| 677 |
|
| 678 |
|
|
|
|
| 673 |
"attractor": "tools/test_attractor.py",
|
| 674 |
"reversible": "src/reversible.py",
|
| 675 |
"ca": "src/ca.py",
|
| 676 |
+
"tile": "src/tile.py",
|
| 677 |
}
|
| 678 |
|
| 679 |
|
src/tile.py
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Self-assembling tile computer (abstract tile assembly model).
|
| 2 |
+
|
| 3 |
+
Computation is the growth of a crystal. The program is a finite set of square
|
| 4 |
+
tiles; each edge carries a glue label with an integer strength. A seed is placed
|
| 5 |
+
and tiles accrete onto the assembly by one rule: a tile binds at an empty site
|
| 6 |
+
if the summed strength of the glues that match its already-present neighbors is
|
| 7 |
+
at least the temperature tau. That binding rule is a threshold gate,
|
| 8 |
+
|
| 9 |
+
bind = H( sum_d strength_d * match_d - tau ),
|
| 10 |
+
|
| 11 |
+
a weighted sum of matching-glue indicators against tau, so every attachment is
|
| 12 |
+
decided by the same Heaviside neuron the rest of the repository is built from.
|
| 13 |
+
At tau = 2 the model is Turing-universal (Winfree 1998): a directed tile set
|
| 14 |
+
grows a unique structure, and that structure is the trace of a computation.
|
| 15 |
+
|
| 16 |
+
Sides are N,E,S,W; a tile's N glue abuts its north neighbor's S glue, and so on.
|
| 17 |
+
A glue label "" is the null glue (strength 0, matches nothing). Glue strengths
|
| 18 |
+
are a property of the label (matching glues have equal strength), held in a map.
|
| 19 |
+
"""
|
| 20 |
+
from __future__ import annotations
|
| 21 |
+
from typing import Dict, List, Optional, Tuple
|
| 22 |
+
|
| 23 |
+
# side -> (dx, dy, my_side, neighbor_side)
|
| 24 |
+
_SIDES = [(0, 1, "N", "S"), (1, 0, "E", "W"), (0, -1, "S", "N"), (-1, 0, "W", "E")]
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class Tile:
|
| 28 |
+
__slots__ = ("N", "E", "S", "W", "name")
|
| 29 |
+
|
| 30 |
+
def __init__(self, N="", E="", S="", W="", name=""):
|
| 31 |
+
self.N, self.E, self.S, self.W, self.name = N, E, S, W, name
|
| 32 |
+
|
| 33 |
+
def glue(self, side):
|
| 34 |
+
return getattr(self, side)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def bind_strength(A: Dict[Tuple[int, int], Tile], x: int, y: int, t: Tile,
|
| 38 |
+
strength: Dict[str, int]) -> int:
|
| 39 |
+
"""Summed strength of t's glues that match the abutting neighbor glues."""
|
| 40 |
+
s = 0
|
| 41 |
+
for dx, dy, side, opp in _SIDES:
|
| 42 |
+
nb = A.get((x + dx, y + dy))
|
| 43 |
+
if nb is None:
|
| 44 |
+
continue
|
| 45 |
+
g = t.glue(side)
|
| 46 |
+
if g and g == nb.glue(opp):
|
| 47 |
+
s += strength.get(g, 1)
|
| 48 |
+
return s
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def binds(A, x, y, t, tau, strength) -> bool:
|
| 52 |
+
"""The threshold-gate binding decision: H(sum strength*match - tau)."""
|
| 53 |
+
return bind_strength(A, x, y, t, strength) >= tau
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def grow(tileset: List[Tile], seed: Dict[Tuple[int, int], Tile], tau: int,
|
| 57 |
+
strength: Dict[str, int], bounds: Tuple[int, int, int, int],
|
| 58 |
+
max_tiles: int = 100000) -> Tuple[Dict[Tuple[int, int], Tile], bool]:
|
| 59 |
+
"""Directed growth from a seed. Returns (assembly, deterministic): at every
|
| 60 |
+
site at most one tile binds when the set is directed, so the assembly is
|
| 61 |
+
unique. deterministic=False flags a site where two tiles could bind."""
|
| 62 |
+
x0, y0, x1, y1 = bounds
|
| 63 |
+
A = dict(seed)
|
| 64 |
+
deterministic = True
|
| 65 |
+
changed = True
|
| 66 |
+
while changed and len(A) < max_tiles:
|
| 67 |
+
changed = False
|
| 68 |
+
frontier = set()
|
| 69 |
+
for (x, y) in list(A):
|
| 70 |
+
for dx, dy, _, _ in _SIDES:
|
| 71 |
+
p = (x + dx, y + dy)
|
| 72 |
+
if p not in A and x0 <= p[0] <= x1 and y0 <= p[1] <= y1:
|
| 73 |
+
frontier.add(p)
|
| 74 |
+
for (x, y) in frontier:
|
| 75 |
+
binders = [t for t in tileset if binds(A, x, y, t, tau, strength)]
|
| 76 |
+
if len(binders) == 1:
|
| 77 |
+
A[(x, y)] = binders[0]
|
| 78 |
+
changed = True
|
| 79 |
+
elif len(binders) > 1:
|
| 80 |
+
deterministic = False
|
| 81 |
+
return A, deterministic
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# ---------------------------------------------------------------------------
|
| 85 |
+
# XOR / Sierpinski tile set: value(x,y) = value(x-1,y) XOR value(x,y-1)
|
| 86 |
+
# ---------------------------------------------------------------------------
|
| 87 |
+
def rule2_tileset(fn) -> List[Tile]:
|
| 88 |
+
"""Rule tiles for value(x,y) = fn(W-input, S-input): four tiles, each binds
|
| 89 |
+
cooperatively (S and W, strength 1 each = tau) and emits fn on N and E."""
|
| 90 |
+
ts = []
|
| 91 |
+
for s in (0, 1):
|
| 92 |
+
for w in (0, 1):
|
| 93 |
+
v = fn(w, s)
|
| 94 |
+
ts.append(Tile(N=f"v{v}", E=f"v{v}", S=f"v{s}", W=f"v{w}",
|
| 95 |
+
name=f"R w{w} s{s} -> {v}"))
|
| 96 |
+
return ts
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def sierpinski_tileset() -> List[Tile]:
|
| 100 |
+
return rule2_tileset(lambda w, s: w ^ s)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
def _row_col_seed(bottom: List[int], left: List[int]):
|
| 104 |
+
"""Seed the bottom row (y=0) and left column (x=0) with fixed value tiles,
|
| 105 |
+
presenting value glues north and east for the rule tiles above/right."""
|
| 106 |
+
seed = {}
|
| 107 |
+
for x, b in enumerate(bottom):
|
| 108 |
+
seed[(x, 0)] = Tile(N=f"v{b}", E="", S="", W="", name=f"seedB{x}={b}")
|
| 109 |
+
for y, l in enumerate(left):
|
| 110 |
+
if y == 0:
|
| 111 |
+
continue
|
| 112 |
+
seed[(0, y)] = Tile(N="", E=f"v{l}", S="", W="", name=f"seedL{y}={l}")
|
| 113 |
+
return seed
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
def _test_binding_gate():
|
| 117 |
+
"""The binding decision is exactly the Heaviside threshold gate."""
|
| 118 |
+
strength = {"v0": 1, "v1": 1}
|
| 119 |
+
ts = sierpinski_tileset()
|
| 120 |
+
A = {(1, 0): Tile(N="v1"), (0, 1): Tile(E="v0")}
|
| 121 |
+
bad = 0
|
| 122 |
+
for t in ts:
|
| 123 |
+
for x, y in [(1, 1)]:
|
| 124 |
+
w = sum(strength.get(t.glue(side), 1)
|
| 125 |
+
for dx, dy, side, opp in _SIDES
|
| 126 |
+
if A.get((x + dx, y + dy)) and t.glue(side)
|
| 127 |
+
and t.glue(side) == A[(x + dx, y + dy)].glue(opp))
|
| 128 |
+
gate = 1 if (w - 2) >= 0 else 0 # H(sum*match - tau)
|
| 129 |
+
if gate != int(binds(A, x, y, t, 2, strength)):
|
| 130 |
+
bad += 1
|
| 131 |
+
print(f" binding decision == Heaviside gate H(sum-tau): {'OK' if bad == 0 else 'FAIL'}")
|
| 132 |
+
return bad == 0
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def _test_rule2(fn, name, n=24):
|
| 136 |
+
strength = {"v0": 1, "v1": 1}
|
| 137 |
+
bottom = [1 if x == 0 else 0 for x in range(n)]
|
| 138 |
+
left = [1 if y == 0 else 0 for y in range(n)]
|
| 139 |
+
seed = _row_col_seed(bottom, left)
|
| 140 |
+
A, det = grow(rule2_tileset(fn), seed, 2, strength, (0, 0, n - 1, n - 1))
|
| 141 |
+
|
| 142 |
+
def val(x, y):
|
| 143 |
+
t = A.get((x, y))
|
| 144 |
+
return None if t is None else (1 if t.N == "v1" else 0)
|
| 145 |
+
|
| 146 |
+
ref = {(x, 0): bottom[x] for x in range(n)}
|
| 147 |
+
ref.update({(0, y): left[y] for y in range(n)})
|
| 148 |
+
for y in range(1, n):
|
| 149 |
+
for x in range(1, n):
|
| 150 |
+
ref[(x, y)] = fn(ref[(x - 1, y)], ref[(x, y - 1)])
|
| 151 |
+
|
| 152 |
+
filled = bad = 0
|
| 153 |
+
for y in range(1, n):
|
| 154 |
+
for x in range(1, n):
|
| 155 |
+
v = val(x, y)
|
| 156 |
+
if v is not None:
|
| 157 |
+
filled += 1
|
| 158 |
+
bad += v != ref[(x, y)]
|
| 159 |
+
tag = "OK" if (det and bad == 0 and filled > 0) else "FAIL"
|
| 160 |
+
print(f" rule-tile CA fn={name:3s}: directed={det} placed={filled} "
|
| 161 |
+
f"every tile = fn(W,S) {tag}")
|
| 162 |
+
return det and bad == 0 and filled > 0
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
# ---------------------------------------------------------------------------
|
| 166 |
+
# Binary counter: each row is the row below plus one. LSB is the right column;
|
| 167 |
+
# carry propagates west by cooperative binding (S = bit below, E = carry in).
|
| 168 |
+
# ---------------------------------------------------------------------------
|
| 169 |
+
def counter_tileset() -> List[Tile]:
|
| 170 |
+
ts = []
|
| 171 |
+
for b in (0, 1):
|
| 172 |
+
for c in (0, 1):
|
| 173 |
+
ts.append(Tile(N=f"b{b ^ c}", E=f"c{c}", S=f"b{b}", W=f"c{b & c}",
|
| 174 |
+
name=f"C b{b} c{c} -> b{b ^ c} carry{b & c}"))
|
| 175 |
+
ts.append(Tile(N="edge", E="", S="edge", W="c1", name="edge(+1 injector)"))
|
| 176 |
+
return ts
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def counter_seed(n: int):
|
| 180 |
+
"""Bottom row (y=0) all zero, plus the right-edge +1 injector column base."""
|
| 181 |
+
seed = {}
|
| 182 |
+
for x in range(n):
|
| 183 |
+
seed[(x, 0)] = Tile(N="b0", name=f"seed b0 col{x}")
|
| 184 |
+
seed[(n, 0)] = Tile(N="edge", W="c1", name="seed edge")
|
| 185 |
+
return seed
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
def _test_counter(n=6, rows=None):
|
| 189 |
+
rows = rows or (1 << n) - 1
|
| 190 |
+
strength = {"edge": 2} # value/carry glues default 1
|
| 191 |
+
A, det = grow(counter_tileset(), counter_seed(n), 2, strength,
|
| 192 |
+
(0, 0, n, rows))
|
| 193 |
+
|
| 194 |
+
def rowval(y):
|
| 195 |
+
bits = []
|
| 196 |
+
for x in range(n):
|
| 197 |
+
t = A.get((x, y))
|
| 198 |
+
if t is None:
|
| 199 |
+
return None
|
| 200 |
+
bits.append(1 if t.N == "b1" else 0)
|
| 201 |
+
return sum(bit << (n - 1 - x) for x, bit in enumerate(bits))
|
| 202 |
+
|
| 203 |
+
bad = filled = 0
|
| 204 |
+
for y in range(1, rows + 1):
|
| 205 |
+
v = rowval(y)
|
| 206 |
+
if v is not None:
|
| 207 |
+
filled += 1
|
| 208 |
+
if v != (y & ((1 << n) - 1)):
|
| 209 |
+
bad += 1
|
| 210 |
+
print(f" binary counter {n}-bit: directed={det} rows grown={filled} "
|
| 211 |
+
f"row y encodes the integer y {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 212 |
+
return det and bad == 0 and filled == rows
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
if __name__ == "__main__":
|
| 216 |
+
print("Self-assembling tile computer")
|
| 217 |
+
a = _test_binding_gate()
|
| 218 |
+
b = all(_test_rule2(fn, nm) for fn, nm in
|
| 219 |
+
[(lambda w, s: w ^ s, "XOR"), (lambda w, s: w & s, "AND"),
|
| 220 |
+
(lambda w, s: w | s, "OR")])
|
| 221 |
+
c = _test_counter()
|
| 222 |
+
print("PASS" if (a and b and c) else "FAIL")
|
tools/build_tile.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Ship the self-assembling tile computer as variants/neural_tile.safetensors: a
|
| 2 |
+
tile set (the binary counter) stored as its glue tables, together with the
|
| 3 |
+
binding gate that governs growth. A tile binds at a site when the summed
|
| 4 |
+
strength of its matching glues meets tau, which is the Heaviside gate
|
| 5 |
+
H(strength . match - tau) with per-tile weights = glue strengths and bias = -tau.
|
| 6 |
+
Round-trips the file, regrows the counter, and confirms row y encodes y."""
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
import torch
|
| 13 |
+
from safetensors.torch import save_file, load_file
|
| 14 |
+
from safetensors import safe_open
|
| 15 |
+
|
| 16 |
+
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
| 17 |
+
sys.path.insert(0, os.path.join(ROOT, "src"))
|
| 18 |
+
import tile as T
|
| 19 |
+
|
| 20 |
+
OUT = os.path.join(ROOT, "variants", "neural_tile.safetensors")
|
| 21 |
+
NBITS = 8
|
| 22 |
+
TAU = 2
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def main() -> int:
|
| 26 |
+
ts = T.counter_tileset()
|
| 27 |
+
strength = {"edge": 2}
|
| 28 |
+
glues = sorted({g for t in ts for g in (t.N, t.E, t.S, t.W) if g})
|
| 29 |
+
gid = {g: i for i, g in enumerate(glues)}
|
| 30 |
+
tile_glues = torch.tensor([[gid.get(t.N, -1), gid.get(t.E, -1),
|
| 31 |
+
gid.get(t.S, -1), gid.get(t.W, -1)] for t in ts],
|
| 32 |
+
dtype=torch.long)
|
| 33 |
+
glue_strength = torch.tensor([strength.get(g, 1) for g in glues], dtype=torch.long)
|
| 34 |
+
# per-tile binding-gate weights = strengths of the tile's four glues (0 = null)
|
| 35 |
+
bind_w = torch.tensor([[strength.get(g, 1) if g else 0 for g in (t.N, t.E, t.S, t.W)]
|
| 36 |
+
for t in ts], dtype=torch.long)
|
| 37 |
+
tensors = {"tile_glues": tile_glues, "glue_strength": glue_strength,
|
| 38 |
+
"binding_weight": bind_w, "binding_bias": torch.tensor(-TAU)}
|
| 39 |
+
meta = {"machine": "tile", "tau": str(TAU), "glues": json.dumps(glues),
|
| 40 |
+
"tile_names": json.dumps([t.name for t in ts]), "program": "binary counter"}
|
| 41 |
+
save_file(tensors, OUT, metadata=meta)
|
| 42 |
+
print(f"Built {os.path.relpath(OUT, ROOT)}: binary-counter tile set")
|
| 43 |
+
print(f" tiles={len(ts)} glues={len(glues)} tau={TAU} size={os.path.getsize(OUT)} bytes")
|
| 44 |
+
|
| 45 |
+
# round-trip: reconstruct the tiles from the file and regrow the counter
|
| 46 |
+
t = load_file(OUT)
|
| 47 |
+
with safe_open(OUT, framework="pt") as f:
|
| 48 |
+
m = f.metadata()
|
| 49 |
+
gl = json.loads(m["glues"])
|
| 50 |
+
strg = {gl[i]: int(s) for i, s in enumerate(t["glue_strength"].tolist())}
|
| 51 |
+
tiles = []
|
| 52 |
+
for row, name in zip(t["tile_glues"].tolist(), json.loads(m["tile_names"])):
|
| 53 |
+
sides = [gl[i] if i >= 0 else "" for i in row]
|
| 54 |
+
tiles.append(T.Tile(N=sides[0], E=sides[1], S=sides[2], W=sides[3], name=name))
|
| 55 |
+
|
| 56 |
+
rows = (1 << NBITS) - 1
|
| 57 |
+
A, det = T.grow(tiles, T.counter_seed(NBITS), int(m["tau"]), strg,
|
| 58 |
+
(0, 0, NBITS, rows))
|
| 59 |
+
bad = filled = 0
|
| 60 |
+
for y in range(1, rows + 1):
|
| 61 |
+
cells = [A.get((x, y)) for x in range(NBITS)]
|
| 62 |
+
if any(c is None for c in cells):
|
| 63 |
+
continue
|
| 64 |
+
filled += 1
|
| 65 |
+
v = sum((1 if c.N == "b1" else 0) << (NBITS - 1 - x) for x, c in enumerate(cells))
|
| 66 |
+
if v != (y & ((1 << NBITS) - 1)):
|
| 67 |
+
bad += 1
|
| 68 |
+
print(f" round-trip regrow {NBITS}-bit counter: directed={det} rows={filled} "
|
| 69 |
+
f"row y == y {'OK' if bad == 0 else f'FAIL({bad})'}")
|
| 70 |
+
|
| 71 |
+
# the stored binding gate reproduces the model's binding decision
|
| 72 |
+
gate_ok = True
|
| 73 |
+
Atest = {(1, 0): T.Tile(N="b0"), (2, 0): T.Tile(N="b0")}
|
| 74 |
+
for ti, tt in enumerate(tiles):
|
| 75 |
+
for site in [(1, 1), (2, 1)]:
|
| 76 |
+
w = t["binding_weight"][ti].tolist()
|
| 77 |
+
match = [1 if tt.glue(side) and Atest.get((site[0] + dx, site[1] + dy))
|
| 78 |
+
and tt.glue(side) == Atest[(site[0] + dx, site[1] + dy)].glue(opp) else 0
|
| 79 |
+
for dx, dy, side, opp in T._SIDES]
|
| 80 |
+
gate = 1 if sum(wi * mi for wi, mi in zip(w, match)) + int(t["binding_bias"]) >= 0 else 0
|
| 81 |
+
if gate != int(T.binds(Atest, site[0], site[1], tt, TAU, strg)):
|
| 82 |
+
gate_ok = False
|
| 83 |
+
print(f" stored binding gate H(weight.match - tau) matches growth rule: "
|
| 84 |
+
f"{'OK' if gate_ok else 'FAIL'}")
|
| 85 |
+
return 0 if (bad == 0 and det and gate_ok) else 1
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
if __name__ == "__main__":
|
| 89 |
+
sys.exit(main())
|
variants/neural_tile.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ba6e721eef8359d84ec58c46c1436c1298ac8191539adf9cc2c03ff9123583a8
|
| 3 |
+
size 920
|