The other day, while working on a mechanical Self-Replicating Machine project for a PhD student at NCBS, I came across a beautiful illustration of Barricelli's 1D Cellular Automata process in Chapter 5 of "Rise of the Self Replicators".
The diagram was mesmerizing enough for me to port the entire system into TouchDesigner and experiment with my own implementation.
TouchDesigner has a strangely underused operator called the
scriptTOP. Most people associate it with OpenCV workflows,
colour grading, or computer vision tasks — which makes sense, considering
those were among its original intended use cases.
What often gets overlooked, however, is that the scriptTOP ships with a full
NumPy distribution inside TouchDesigner’s Python environment. This means we
can perform extremely fast array operations directly on pixel buffers.
Since every pixel can be treated as an addressable computational unit, the
scriptTOP becomes a surprisingly powerful environment for cellular automata
experiments.
If you don't know what a Cellular automata is, "The Nature of code" has a great chapter .
This series follows the original Barricelli implementation referenced in Galloway’s project.
I also created my own variation where the universe strip does not rotate and instead evolves linearly through time.
The colour palette was generated using Coolors.
Below is the complete Python implementation used inside a TouchDesigner Script TOP. The system implements a recursive genetic propagation field inspired by Barricelli-style 1D cellular automata in numpy Arrays.
The simulation treats each gene simultaneously as:
Positive genes propagate right, negative genes propagate left, and collisions synthesize new genes recursively through time.
import numpy as np
import random
def onSetupParameters(scriptOp: scriptTOP):
page = scriptOp.appendCustomPage('Custom')
p = page.appendPulse(
'Reset',
label='Reset'
)
def onPulse(Reset):
return
GENE_MINIMUM = -18
GENE_MAXIMUM = 18
N = 1280
UNIVERSE = N - 1
GENERATIONS = 1280
MAX_REPRODUCTIONS = 2
palette = np.array([
[10/255, 36/255, 99/255, 1.0],
[251/255, 54/255, 64/255, 1.0],
[96/255, 95/255, 94/255, 1.0],
[36/255, 123/255, 160/255, 1.0],
[226/255, 226/255, 226/255, 1.0],
], dtype=np.float32)
COLORS = {}
all_states = list(
range(GENE_MINIMUM, GENE_MAXIMUM + 1)
)
for i, state in enumerate(all_states):
COLORS[state] = palette[i % len(palette)]
COLORS[0] = np.array(
[1.0, 1.0, 1.0, 1.0],
dtype=np.float32
)
cells = np.zeros(
(UNIVERSE, GENERATIONS),
dtype=np.int32
)
def rand_gene():
return random.randint(
GENE_MINIMUM,
GENE_MAXIMUM
)
def init_cells():
global cells
cells[:] = 0
for a in range(UNIVERSE):
cells[a, 0] = rand_gene()
def wrap_to_universe(i):
i = i % UNIVERSE
if i < 0:
i += UNIVERSE
return i % UNIVERSE
def get_cell_value(a, g):
if g < 0:
return 0
return cells[
wrap_to_universe(a),
g
]
def shift_norm(gene, a, g):
cells[
wrap_to_universe(a),
g
] = gene
def zero_norm(a, g):
cells[a, g] = 0
def have_same_sign(A, B):
return (
(A > 0 and B > 0)
or
(A < 0 and B < 0)
)
def getU(a, g):
u = 0
while get_cell_value(a + u, g - 1) == 0:
u += 1
if u >= UNIVERSE:
break
return u
def getV(a, g):
v = 0
while get_cell_value(a - v, g - 1) == 0:
v += 1
if v >= UNIVERSE:
break
return v
def getUvalue(a, g):
return get_cell_value(
a + getU(a, g),
g - 1
)
def getVvalue(a, g):
return get_cell_value(
a - getV(a, g),
g - 1
)
def is_below_empty_cell(a, g):
if g == 0:
return False
return cells[a, g - 1] == 0
def aNorm(a, g):
if not is_below_empty_cell(a, g):
zero_norm(a, g)
else:
U = getUvalue(a, g)
V = getVvalue(a, g)
uv = getU(a, g) + getV(a, g)
if have_same_sign(U, V):
cells[a, g] = uv
else:
cells[a, g] = -uv
def bNorm(a, g):
if not is_below_empty_cell(a, g):
zero_norm(a, g)
else:
U = getUvalue(a, g)
V = getVvalue(a, g)
uv1 = getU(a, g) + getV(a, g) - 1
if have_same_sign(U, V):
cells[a, g] = uv1
else:
cells[a, g] = -uv1
def shift(
X,
Xa,
Xg,
delta_a,
number_of_reproductions
):
if X == 0:
return
Na = wrap_to_universe(Xa + delta_a)
Ng = Xg + 1
if Ng >= GENERATIONS:
return
N = get_cell_value(Na, Ng)
if (N != 0) and (X != N):
multiplier = 2
if Na <= 255 * multiplier:
bNorm(Na, Ng)
else:
aNorm(Na, Ng)
else:
shift_norm(X, Na, Ng)
number_of_reproductions += 1
Y = get_cell_value(
Xa + delta_a,
Xg
)
if (
(Y != 0)
and
(number_of_reproductions <= MAX_REPRODUCTIONS)
):
shift(
X,
Xa,
Xg,
Y,
number_of_reproductions
)
def evolve():
for g in range(GENERATIONS - 1):
for a in range(UNIVERSE):
X = cells[a, g]
shift(
X,
a,
g,
X,
0
)
def cells_to_image():
img = np.zeros(
(GENERATIONS, UNIVERSE, 4),
dtype=np.float32
)
for g in range(GENERATIONS):
for a in range(UNIVERSE):
gene = cells[a, g]
img[g, a] = COLORS.get(
gene,
[1.0, 1.0, 1.0, 1.0]
)
return img
def onCook(scriptOp):
init_cells()
evolve()
img = cells_to_image()
scriptOp.copyNumpyArray(img)
return