CS 467 - 1D Cellular Automata
Due: 2020-04-07 5:00p
Goals
- Learn about the implementation of cellular automata
Prerequisites
- Accept the assignment on our Github Classroom.
- 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 ).
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.