Cellular Automata in Grasshopper (w Python)

I introduced some of my students to Python scripting in Grasshopper this week through a simple implementation of Conway's Game of Life.

ba7898720f0cf8df.gif

The Grasshopper setup is pretty simple. We'll be using a Python script component to do most of the work. For inputs we pass in a file path (pointing to a bitmap file that defines the starting condition of the field) and an integer value (from a slider) that acts like a position on a timeline.

For the output, we'll get a list of points, representing the position of each pixel, and a corresponding list of values in the range 0-1 indicating whether the pixel is on ("alive") or off.

80de1ce70682b12b.png
Figure 1: Grasshopper setup

The script has four main parts. First, we load in the starting state from our bitmap as one big list of numbers, data. We also need to know the width. Second, we set up the output, looping over data to create the points and on/off values.

Next we need a function to get the values of what we call the "neighborhood" of a given pixel. Game of Life uses a Moore neighborhood (compare with von Neumann neighborhood), which basically means the eight surrounding pixels. Call this get_neighbors.

The last step is defining and applying the rules of the game. We'll do this with conditional statements that decide what to do for each pixel in data based on 1) whether it is alive or dead and 2) how many of its neighbors are alive. Let's call this one evaluate.

6e4aa516de82c9c2.svg

One more loop will recursively call evaluate for each step in time up to t. Note that in an environment with a render loop (like a game engine) we would do this a bit differently, but everything else works the same.

Here is the finished script:

# borrowed tools for bitmap and Rhino geometry
import System.Drawing.Bitmap as bmp
import Rhino.Geometry as rg

# read initial state of the field from file path
def load(path):
    global h, w, data
    field = bmp.FromFile(path)
    h = field.Height
    w = field.Width
    data = []
    for i in range(0, w * h):
        x = i % w  # modulo operator (division remainder)
        y = i // h # floor division (divide + round down)
        val = int(field.GetPixel(x, y).GetBrightness())
        data.append(val)
    return data

# get the Moore neighborhood for a given pixel
# i is its index, w is width of the field and d is data
# try/except handles out-of-bounds cases
def get_neighbors(i, w, d):
    try:
        return (d[i - w - 1], d[i - w], d[i - w + 1],
                d[i - 1    ], d[i    ], d[i + 1    ],
                d[i + w - 1], d[i + w], d[i + w + 1])
    except:
        return []

# loop through all pixels, apply rules of the game
def evaluate(data):
    for i, el in enumerate(data):
        neighbors = get_neighbors(i, w, data)
        count = sum(neighbors)
        alive = bool(el)
        if alive and count < 2: data[i] = 0
        elif alive and count <= 3: data[i] = 1
        elif alive and count > 3: data[i] = 0
        elif count == 3: data[i] = 1
    return data

# main script body
load(F)

for t in range(0, T):
    data = evaluate(data)

# output as points and on/off values
P = []
D = []
for i, v in enumerate(data):
    x = i % w
    y = i // h
    P.append(rg.Point3d(x, y, 0))
    D.append(v)

This works pretty well when using a small field, here it's only 32 pixels in width and height. If you have a need for speed, two places to start are the load function (the GetPixel method of System.Drawing.Bitmap is slow/expensive, discussed on StackOverflow) another could be to reconsider the rendering approach, maybe using the Rhino DisplayPipeline with Draw2dRectangle calls.

Posted: Tue Apr 9, 2024