COMPUTATIONAL GRAPHIC ART AND DESIGN WITH 1-DIMENSIONAL BARRICELLI CELLULAR AUTOMATA



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".

Baricelli CA Frame 5

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 .


Original Barricelli Implementation

This series follows the original Barricelli implementation referenced in Galloway’s project.

Linear Evolution Variant

I also created my own variation where the universe strip does not rotate and instead evolves linearly through time.

Palette

The colour palette was generated using Coolors.


TouchDesigner Script TOP Implementation


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