Cellular Automata in F#

November 28 2020 · tech fsharp cellular-automata

Rule90

Rule 90

Simple rules can generate complex behaviour. I’m reading A New Kind of Science by Stephen Wolfram. A key idea of the book is that a simple rule, applied iteratively, can generate complex behaviour. The difference between Rules 222, 90, and 30 are trivial, yet they generate vastly different results.

In this post we explore the principles and implement the Elementary Cellular Automata system in F#.

Rule222

Rule 222

Rule30

Rule 30

Cellular Automata

Here’s a 7 x 4 grid. The dots represent Empty cells, the X represent Full cells. All of our grids start with a single Full cell in the middle of the top row.

...X...
.......
.......
.......

We then define a rule. A rule is a function which decides whether a single cell is Empty or Full. This rule function takes values of the 3 cells from the row directly above, corresponding to the cells above and to the left (1), directly above (2), and above and to the right (3). We visualise this below. The cell we are trying to calculate is ?.

123....        .123...        ..123..        ...123.        ....123
.?.....        ..?....        ...?...        ....?..        .....?.
.......        .......        .......        .......        .......
.......        .......        .......        .......        .......

The same rule is applied row after row. The rule is not applied to cells in the far left column and far right columns. These cells will always remain Empty. We’ll look at the naming convention of rules later on, but for now here’s one example.

Below is Rule 222. The first pattern is saying that if cells 1, 2, and 3 are Full, then the cell ? will be Full. And so on, for every possible combination of those 3 cells.

XXX    XX.    X.X    X..    .XX    .X.    ..X    ...
 X      X      .      X      X      X      X      .

This system of rules is as simple as we can get. Each cell can only have two possible states, and the value of the next cell only depends on it’s neighbours.

Rules as Code

We just described the Celluar Automata domain. Let’s implement it in code.

  • A Cell can be either Empty or Full.
  • The Rule type is a function which takes 3 Cells and returns the value of a single Cell.
// A Cell is either Empty or Full
type Cell = Empty | Full

// Rules are functions which take in the values of 3 Cells, and return a single Cell value
type Rule = (Cell * Cell * Cell) -> Cell

// Rule 222 implemented in code
let rule222: Rule = function 
    | (Full, Full, Full) -> Full
    | (Full, Full, Empty) -> Full
    | (Full, Empty, Full) -> Empty
    | (Full, Empty, Empty) -> Full
    | (Empty, Full, Full) -> Full
    | (Empty, Full, Empty) -> Full
    | (Empty, Empty, Full) -> Full
    | (Empty, Empty, Empty) -> Empty

Modelling the Grid

We’ve just described the Cellular Automata system in its entirety. Patterns and complexity emerge from just those rules. The rules by themselves are abstract and we need to apply them somehow. Here we apply the rule on a simple grid. But the same rule could be applied to any other surface - a 3D surface, mobius strip, or grid where the values wrap around.

Onto the implementing the system on a grid. Let’s start off by generating the first row.

let generateStandardFirstRow (width: int) =
    
    if width % 2 = 0 then invalidArg "width" (sprintf "Value must be an odd number. Value passed was %d." width)
    
    Seq.init width (function
        | n when n = (width / 2) -> Full
        | _ -> Empty
    )

So

generateStandardFirstRow 7

Gives us

Empty Empty Empty Full Empty Empty Empty

In order to generate the next row we only need to know the row before it. We take the previous row and apply Seq.windowed. This splits the row of Cells into groups of 3 Cells, which we need in order to calculate the next value.

Recall that our rule requires 3 cells in order to calculate the next value. This is the cell above and to the left, the cell directly above, and the cell above and to the right. This is a problem for the far left and far right columns of the grid. They only have 2 of the 3 cells required. To get around this we enforce a rule that the left column and right column will always remain Empty.

let generateNextRow (rule: Rule) (row: Cell seq) =
    let generatedCells =
        row
        |> Seq.windowed 3
        |> Seq.map (fun v ->
            rule (v.[0], v.[1], v.[2])
        )
        
    // the first and last Cells of each row are Empty since we didn't generate a value for them above
    seq {
        yield Empty
        yield! generatedCells
        yield Empty
    }

Now let’s write a function which generates row after row. Sequences in F# are lazily evaluated. They’re the C# IEnumerable type under the hood. By treating everything as a sequence we’re essentially defining the rules as to how the grid will be generated.

Seq.unfold iteratively applies the Rule to whatever the previous row was, collecting results. Notice we don’t specify how many rows to generate, that’s up to the caller of this function.

let generatePattern (rule: Rule) (firstRow: Cell seq) = 
    firstRow
    |> Seq.unfold (fun row ->
        Some(row, (generateNextRow rule row))
        )

Now we can generate the grid!

let firstRow = generateStandardFirstRow 101
let rows = generatePattern rule222 firstRow |> Seq.take 10

But we want to visualise it. Here’s a basic ASCII renderer.

let drawCell = function
| Full -> "X"
| Empty -> "."

/// Render the grid to an ASCII string
let drawGrid (grid: Cell seq seq) =
    grid
    |> Seq.map (fun row ->
        row
        |> Seq.map drawCell
        |> String.concat ""
    )
    |> String.concat "\n"
    
drawGrid rows
..................................................X..................................................
.................................................XXX.................................................
................................................XXXXX................................................
...............................................XXXXXXX...............................................
..............................................XXXXXXXXX..............................................
.............................................XXXXXXXXXXX.............................................
............................................XXXXXXXXXXXXX............................................
...........................................XXXXXXXXXXXXXXX...........................................
..........................................XXXXXXXXXXXXXXXXX..........................................
.........................................XXXXXXXXXXXXXXXXXXX.........................................

Other Rules

Our algorithm works! Let’s experiment with other rules.

let rule90: Rule = function
    | (Full, Full, Full) -> Empty
    | (Full, Full, Empty) -> Full
    | (Full, Empty, Full) -> Empty
    | (Full, Empty, Empty) -> Full
    | (Empty, Full, Full) -> Full
    | (Empty, Full, Empty) -> Empty
    | (Empty, Empty, Full) -> Full
    | (Empty, Empty, Empty) -> Empty
    
let firstRow = generateStandardFirstRow 101
let rows = generatePattern rule90 firstRow |> Seq.take 50
let arrayOfRows = rows |> Seq.toArray
    
drawGrid rows
..................................................X..................................................
.................................................X.X.................................................
................................................X...X................................................
...............................................X.X.X.X...............................................
..............................................X.......X..............................................
.............................................X.X.....X.X.............................................
............................................X...X...X...X............................................
...........................................X.X.X.X.X.X.X.X...........................................
..........................................X...............X..........................................
.........................................X.X.............X.X.........................................
........................................X...X...........X...X........................................
.......................................X.X.X.X.........X.X.X.X.......................................
......................................X.......X.......X.......X......................................
.....................................X.X.....X.X.....X.X.....X.X.....................................
....................................X...X...X...X...X...X...X...X....................................
...................................X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X...................................
..................................X...............................X..................................
.................................X.X.............................X.X.................................
................................X...X...........................X...X................................
...............................X.X.X.X.........................X.X.X.X...............................
..............................X.......X.......................X.......X..............................
.............................X.X.....X.X.....................X.X.....X.X.............................
............................X...X...X...X...................X...X...X...X............................
...........................X.X.X.X.X.X.X.X.................X.X.X.X.X.X.X.X...........................
..........................X...............X...............X...............X..........................
.........................X.X.............X.X.............X.X.............X.X.........................
........................X...X...........X...X...........X...X...........X...X........................
.......................X.X.X.X.........X.X.X.X.........X.X.X.X.........X.X.X.X.......................
......................X.......X.......X.......X.......X.......X.......X.......X......................
.....................X.X.....X.X.....X.X.....X.X.....X.X.....X.X.....X.X.....X.X.....................
....................X...X...X...X...X...X...X...X...X...X...X...X...X...X...X...X....................
...................X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X...................
..................X...............................................................X..................
.................X.X.............................................................X.X.................
................X...X...........................................................X...X................
...............X.X.X.X.........................................................X.X.X.X...............
..............X.......X.......................................................X.......X..............
.............X.X.....X.X.....................................................X.X.....X.X.............
............X...X...X...X...................................................X...X...X...X............
...........X.X.X.X.X.X.X.X.................................................X.X.X.X.X.X.X.X...........
..........X...............X...............................................X...............X..........
.........X.X.............X.X.............................................X.X.............X.X.........
........X...X...........X...X...........................................X...X...........X...X........
.......X.X.X.X.........X.X.X.X.........................................X.X.X.X.........X.X.X.X.......
......X.......X.......X.......X.......................................X.......X.......X.......X......
.....X.X.....X.X.....X.X.....X.X.....................................X.X.....X.X.....X.X.....X.X.....
....X...X...X...X...X...X...X...X...................................X...X...X...X...X...X...X...X....
...X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.................................X.X.X.X.X.X.X.X.X.X.X.X.X.X.X.X...
..X...............................X...............................X...............................X..
.X.X.............................X.X.............................X.X.............................X.X.

Image Output

Here’s a graphical renderer for the same grid implementation.

#r "nuget:SixLabors.ImageSharp"
open SixLabors.ImageSharp;
open SixLabors.ImageSharp.PixelFormats;

let cols = 501;
let rows = 250;

let image = new Image<Rgba32>(cols, rows);
let white = new Rgba32(255F, 255F, 100F, 1F);
let black = new Rgba32(0f, 0f, 0f, 1F);

let grid =
    generateStandardFirstRow cols
    |> generatePattern rule90
    |> Seq.take rows

let withIndexes x = x |> Seq.mapi (fun index item -> (index, item))

for (rowIndex, row) in withIndexes grid do

    for (cellIndex, cell) in withIndexes row do

        let colour =
            match cell with
            | Full -> black
            | Empty -> white

        image.[cellIndex, rowIndex] <- colour

image.Save("rule90.png")
image

Rule90.png

Rules

So far we’ve looked at a few specific rules. But how many rules are there and can we generate them dynamically? We’ve hardcoded rules as functions

let rule222: Rule = function 
    | (Full, Full, Full) -> Full
    | (Full, Full, Empty) -> Full
    | (Full, Empty, Full) -> Empty
    | (Full, Empty, Empty) -> Full
    | (Empty, Full, Full) -> Full
    | (Empty, Full, Empty) -> Full
    | (Empty, Empty, Full) -> Full
    | (Empty, Empty, Empty) -> Empty

All rules must return a value for each pattern. Because the patterns are static we could think of identifying rules by what they return for each case, what is on the right hand side of the ->. We could think of the above rule as:

(Full, Full, Empty, Full, Full, Full, Full, Empty)

So a rule is a identified as a list of 8 boolean values. If we use binary to represent this we need a byte (8 bits) of information. This means there are 2^8 rules, giving us 256 possible rules in the Wolfram automata system.

So there are only 256 rules in this Wolfram automata system, and the rule’s name determines what it returns. This is illustrated below. The binary pattern maps 1 to Full and 0 to Empty.

1286432168421Written OutRule
11011110128 + 64 + 16 + 8 + 4 + 2 + 1 =222
0101101064 + 16 + 8 + 2 =90
let generateRule(ruleNumber: byte) = 
    let ruleBitString = Convert.ToString(ruleNumber, 2).PadLeft(8, '0');

    let patternToBitStringIndex = function 
        | (Full, Full, Full) -> 0
        | (Full, Full, Empty) -> 1
        | (Full, Empty, Full) -> 2
        | (Full, Empty, Empty) -> 3
        | (Empty, Full, Full) -> 4
        | (Empty, Full, Empty) -> 5
        | (Empty, Empty, Full) -> 6
        | (Empty, Empty, Empty) -> 7

    let ruleFn (cells: Cell * Cell * Cell) = 
        let bitIndex = patternToBitStringIndex cells

        match ruleBitString.[bitIndex] with
        | '0' -> Empty
        | '1' -> Full
        | _ -> raise (Exception("Unexpected input"))
        
    ruleFn
    
let rule254 = generateRule(254uy)
rule254 (Empty, Empty, Empty)

And there we have it, our rule function generator. Here are some interesting rules.

Rule 62

Rule 62

Rule 110

Rule 110

Rule 150

Rule 150

Conclusion

F# is a perfect language to implement Cellular Automata. The type system allows us to exactly model the domain, and sequences make the implementation elegant. There’s no confusing cruft in our code. From this basic implementation you can easily make modifications to the Cellular Automata system. Here’s a few fun things to try out. What if …

  • The first row was randomly generated
  • Cells could have a value other than Full or Empty?
  • Rules incorporated some degree of randomness when deciding the value of the next cell?
  • The grid was hexagonal rather than square based?

The Jupyter Notebook and an F# project containing the code for this post is available here.

This is part of F# Advent Calendar 2020. Check out other posts here.

Further Reading


Related Posts