File size: 8,591 Bytes
4dbae82 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 | """Self-assembling tile computer (abstract tile assembly model).
Computation is the growth of a crystal. The program is a finite set of square
tiles; each edge carries a glue label with an integer strength. A seed is placed
and tiles accrete onto the assembly by one rule: a tile binds at an empty site
if the summed strength of the glues that match its already-present neighbors is
at least the temperature tau. That binding rule is a threshold gate,
bind = H( sum_d strength_d * match_d - tau ),
a weighted sum of matching-glue indicators against tau, so every attachment is
decided by the same Heaviside neuron the rest of the repository is built from.
At tau = 2 the model is Turing-universal (Winfree 1998): a directed tile set
grows a unique structure, and that structure is the trace of a computation.
Sides are N,E,S,W; a tile's N glue abuts its north neighbor's S glue, and so on.
A glue label "" is the null glue (strength 0, matches nothing). Glue strengths
are a property of the label (matching glues have equal strength), held in a map.
"""
from __future__ import annotations
from typing import Dict, List, Optional, Tuple
# 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. Returns (assembly, deterministic): at every
site at most one tile binds when the set is directed, so the assembly is
unique. deterministic=False flags a site where two tiles could bind."""
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-input, S-input): four tiles, each binds
cooperatively (S and W, strength 1 each = tau) and emits fn on N and E."""
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 (y=0) and left column (x=0) with fixed value tiles,
presenting value glues north and east for the rule tiles above/right."""
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
if __name__ == "__main__":
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")
|