CS 467 - 1D Cellular Automata

Due: 2020-04-07 5:00p

Goals

  • Learn about the implementation of cellular automata

Prerequisites

  1. Accept the assignment on our Github Classroom.
  2. Download the git repository to your local computer.

Assignment

In this practical, you are going to implement a tool that will allow you to explore Wolfram's 1D cellular automata.

As you read in the The Nature of Code, these are a fairly simple form of CA. They have binary state (0 or 1) and a radius of one for their neighborhood. Because of this, there are only 256 different rule sets for these. What you will implement is a tool that allows the user to type in a number between 0 and 255 and then see what the CA looks like over time.

As in the examples in the text, you will create a 2D visualization using the y-axis to show time.

You will notice that the reading contains a full implementation of what you are abut to do. It is in Processing rather than p5.js, but it is fairly straightforward to make the transition. However, as I mentioned in the video, I would like you to implement this in the OO style, rather than as an array of numbers.

Starter code

I have given you some starter code which adds a text input box and uses it to set the current rule, which is stored in the global rule variable.

Part 1: Create a Cell

The first step will be to create a Cell class, which will be very similar to the practicals that we have done recently.

The class should have a constructor, an update() method and a draw() method.

The constructor

Our cell needs to know three things:

  • It needs to know where it is
  • It needs to know its state
  • Slightly less obviously, it needs to know what its next state will be

In the constructor, create three instances variables: i, state, and next to save these values.

The i should be passed in as an argument to the constructor, so we can set the position when it is created.

Initialize the state and next variables to 0.

The update(cells) function

This function is where the real work of the cell is done. Given the state of the cell and its two neighbors, you need to figure out what the next state of the cell will be. Because we need the neighborhood, this function should accept one argument: the collection of all cells.

As you learned from the reading, with binary state and only two neighbors and the current state contributing to the next state logic, there are only eight possible patterns (000,001,010,011,100,101,110,111) we could find the cell and its neighbors in. Interpreting these as binary representations, we can refer to these patterns numerically as the values 0-7.

For each pattern, there must be a rule that tells us what the next state will be. So, in its simplest form, a rule set is eight 1s or 0s corresponding to each of these patterns. We can play the same trick here, interpreting the rule as an eight-bit binary number, where pattern 0 (000) refers to bit 0 of the number, pattern 5 (101) refers to bit 5, and so on.

For example, let's take Rule 42. 42 as an eight-bit number is 00101010. So, pattern 0 (000) would have a next state of 0, pattern 1 (001) would have a next state of 1, pattern 2 (010) would have a next state 0, and so on...

So, the update function needs to do three things:

  • figure out the neighborhood pattern
  • use the pattern to determine the next state from the current rule
  • set the next state

Finding the left and right neighbors of a cell is pretty easy. You just need this.i-1 and this.i+1. Of course, we need this to wrap, so if i is 0, you need to grab the last cell in cells, and if it is the last cell in cells you need to grab the first one.

Turn this pattern into an integer value (recall that this is all about the powers of 2: 4, 2, 1, so 101=1×4+0×2+1×1=5101 = 1\times4 + 0\times2 + 1\times1 = 5).

Now you need to find the value of the bit at that location. Time to see how much you remember about 202 (I told you we would pull from across your education). The easiest way to find the value of a bit in a number is to shift the number so that the bit you care about is in the lowest position and then do a bitwise AND with 1. So, if I wanted to know what the second bit of 5 was, I could write (5>>2) & 1.

Use this value to set this.next.

Why don't we set this.state? The next cell over needs to be able to see the old state when it does its update. So, we need to hold current state and next state separate and not update the state of one cell while other cells have not yet updated.

The draw() function

This one is pretty straightforward. Fill a rect. Use this.i and CELL_SIZE to determine size and position. Since we have a 1D CA, set the y position of the rect at 0.

After you have drawn the rect, set the current state of the cell to the next state.

Part 2: Create the cellular automata

As we have done in earlier work, you are going to create an array of Cell objects. Use CELL_SIZE and width to figure out how many cells to put in the array. The goal is to have the maximum number of cells without cutting one off on the edge of the canvas. Use a for loop to create the new Cell objects and put them in the array.

Initialization

The behavior of the CA will vary considerably depending on the initial state of the cells.

We have initialized all of the cells to 0, which will make for some very disappointing visualizations. Can you predict what the final form would be? As a hint, there are three possibilities.

There are a large number of possible opening patterns (2 to the number of cells in the row). We could try initializing the cells to random values, but we are going to follow Wolfram's lead and just initialize the center cell to state 1. So, figure out which cell in in the middle and set its state to 1. If there is an even number of cells, it is okay to just pick one (or just let the math do it for you).

Part 3: Rendering

This again follows a familiar pattern: iterate through all of the cells calling their update() function and then iterate over them again to call their draw() functions.

If you run this, you will see a row of squares that flicker and change color. Of course, we want to show time in the y dimension. There are a number of ways we could do this, but we will use our trusty translate function.

Create a new variable offset, which is initially set at 0. Before you draw the cells, translate down by the offset, and then increase offset by CELL_SIZE.

Since we only want to fill the canvas, check to see if offset exceeds height, and if it does, call noLoop to stop the draw loop.

You should now be able to visualize a 1D CA. I set the initial rule to 90, so you should get a Sierpinski triangle.

Part 4: Trying multiple CAs

I set up the text entry box, but if you try to use it, you will see that it either does odd things, or it seemingly does nothing at all.

I wrote code to update the rule, and to restart the draw loop, but the CA needs to be reinitialized. Add the code in to start it over from the top (don't forget to think about state).

Once you have that in place, try out some other CAs to make sure they work. Many of them are not very interesting, but some are quite compelling.

Consult with https://mathworld.wolfram.com/ElementaryCellularAutomaton.html to find interesting rules (and to make sure you are actually getting the correct result).

Finishing up

Commit your changes to git and push them back up to GitHub. I will find them there.