Making the simple complicated is commonplace; making the complicated simple, awesomely simple, that’s creativity.

—Charlie Mingus

Cellular Automata

Here is a picture of the predatory snail, Conus Textile:

_images/Conus_textile.JPG
By Richard Ling - Own work; Location: Cod Hole, Great Barrier Reef, Australia, CC BY-SA 3.0, Link

The pattern on its shell is an example of a 1D cellular automata. The shell is grown by a kind of organic line of printer heads along the shell’s lip. These printer heads, called pigment cells, can extrude colored calcium cement. The color choice of a pigment cell is dependent upon the color choice of it’s two adjacent pigment cells. The rules governing this process are simple, yet a very complex shell color pattern emerges.

A 1D cellular automata exists on a 2D grid of cells; like graphing paper, and it is governed by a simple set of rules. How you color in a square is dependent upon the coloring of the three squares above it. If we decided only to use black and white coloring, a square’s color would be dependent upon eight different permutations of the three squares above it. The governing rules of a 1D cellular automata could be described by drawing out these eight pictures.

But before we begin to fill in our drawings we would have to decide how to start them. Typically you color the middle square of the top line on your graphing paper black, then begin painting the squares of the second line using the rules. Of course, you don’t have to do this, you can start however you like. But how you start is called the initial condition, and as you will see, the initial conditions have a huge effect on the overall emergent pattern of your drawing.

In 1983 Stephen Wolfram discovered a 1D cellular automata which created patterns that look like the shell of the Conus Textile snail. He called this program, Rule 30.

Let’s explore how to make a Rule 30, 1D cellular automata, by looking at the instructions which govern it:

“First, look at each cell and it’s right-hand neighbor. If both of these were white on the previous step, then take the new color of the cell to be whatever the previous color of its left-hand neighbor was. Otherwise, take the new color to be the opposite of that.” 1

Here are these instructions as a picture:

_images/rule_30_rule_in_pictures.svg

Note

If you look carefully at the picture version of the rule, you will see two rows of binary numbers, and the reason why rule 30 is called rule 30.

Now let’s start a drawing: we will place a black square in the middle of the first line then follow the rules to fill in the 4 lines below it.

_images/rule_30_by_hand.svg

If you look at the diagram and compare it to the rules described as a paragraph, then to the pictures illustrating this same rule, you will see how it works.

Let’s write some Python code that can build something like this.

But before we start, what do we do about the edges of our graphing paper? We could just pretend they aren’t there, making the automata infinite. Or, we could wrap the paper into a tube so that there is no edge. Both of these things have been done before, but I haven’t seen anyone just force a color onto the edge of the graphing paper, like a wall. So, let’s do that, let’s build a wall.

This will give us a chance to have two state machines interact in the same cellular automata. We need two different machines, a rule 30 machine and a wall machine. The wall machine should be able to be white or black.

Let’s design a system, then build it and run it (not necessarily in that order) and see if we can make something new.

High Level Sketches

Before we start writing our code let’s sketch out some pictures which will help guide our thinking. We would like to see:

Automata Running Inside of Walls

Here is a small example of the rule 30 automata running inside of walls:

_images/rule_30_basic_design_0.svg

This sketch is a mock-up of the output we would like our code to create. It also shows the rules which govern how the picture is made, some terminology and some examples about how some of the components could be build with Python.

The top of the diagram describes rule 30 in pictures and the main part of the drawing shows an example of the one dimensional automata we want to build.

The one dimensional automata is made up of individual cells, which will be governed by two different sets of rules. A cell can be derived from either a Rule30 class which is governed by the rules described at the top of the page or a Wall class, which is given a color to start at and remains in that color forever. Once a cell is instantiated, it is started in the color we want it to appear in on our drawing. The walls of the diagram are colored orange so that we can distinguish them from the bulk of the drawing which is made up of rule 30 cells.

We would like our code to be able to run for a programmable number of generations. A single generation is a row of the one dimensional automata. The Next event, will be used to tell the code representing the diagram, to build its next generation.

We would also like our code to be able to build any width of cells.

The Z object represents the 2D binary matrix which will describe our drawing. This matrix will be used to analyze our automata as it runs.

High Level Design Diagram

Now that we know what kind of output we want, we need to think about how we will structure a program to give us this output.

We need something that can make our one dimensional cellular automata and we need some way to look at it. Let’s call the thing that we will use to visualize our automata, a canvas.

Our canvas will be given an automata object which it will draw. Our automata object will have many rule 30 objects and many wall objects.

Here is what we wrote above, expressed as a UML diagram:

_images/rule_30_basic_design_1.svg

The Canvas class has a OneDCellularAutomata class. This OneDCellularAutomata class has many Rule30 and Wall classes. Seems simple enough.

Let’s build the Canvas class so that it can both draw our diagrams and make video animations. Let’s make the OneDCellularAutomata responsible for creating any automata given a Wall class and a Rule class.

Statemachines Needed to Build Rule 30 in Walls

We have drawn the Rule30 and Wall classes as abstract blocks in the previous high level diagram. But we know how they work, so let’s re-draw them now in more detail:

_images/rule_30_basic_design_2.svg

The state machine under the Rule30 class will provide the behaviour described by the squares at the top of the page. The state machine under the Wall class consists of two states which can only be gotten to using the start_at method, no event will cause a transition between these states. The Wall objects are intended to interface with the Rule30 objects so that a Rule30 cell can’t tell if it is working with an actual machine or just a wall.

Design and Code

Now that we know what we want to build, let’s make our designs more detailed and write the Python code required to make the design work.

Canvas Class

The Canvas class will provide visual feedback.

But, how will we build a Canvas class to get the feedback needed to see what is going on with our program?

If we were using Stephen Wolfram’s Mathematica software, this work would be trivial. Even if we were using Matlab, it wouldn’t be too hard to see these automata, but we are using Python so we will have to stitch a few things together before we can visualize our work.

Let’s go technology shopping.

A few years ago Python borg’d Matlab, into its numpy, scipy and matplotlib packages, so the Matlab type interfaces have been assimilated into the Python collective: resistance is futile. We can use the numpy and matplotlib libraries to get the Matlab features we need to build and animate our automata.

The matplotlib library can animate graphs so we will use that. The graphing paper look that we want is provided by its matplotlib.pcolormesh graphing object so we will use that too. Animations are provided by the matplotlib.FuncAnimation class, which takes a reference to the figure you are drawing on, information about how many frames you want in your movie and how often you want to show them, and some callback functions that affect the data (your picture) for each frame. The callback functions will be very useful, because it means we can pull the operation of our automata away from the Canvas class and we can make the animation callback call out to a Python coroutine, so we can run our automata forever (if we wanted that).

Under the hood matplotlib calls out to FFmpeg, which is some open source software that makes videos. Let’s install what we need and get back to our design:

sudo apt-get install ffmpeg
pip install numpy
pip install matplotlib

Note

I’m assuming you are working within a virtual enviroment. If you are on windows, go and get the ubuntu app, and run this code within your Windows Subsystem for Linux (WSL). If you are on a mac, you can use brew to get ffmpeg.

Here is a UML drawing of the Canvas class:

_images/rule_30_canvas.svg

The Canvas class will have a FuncAnimation object and a LinearSegmentedColormap (used for making colors), and it shows us how we want to make the object and how we want to use it with the run_animation and save methods.

It also shows us that the Canvas calls will have a OneDCellularAutomata object, which will be created elsewhere, then passed to it.

import pathlib
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation

class Canvas():
  def __init__(self, automata, title=None):
    '''Animate 2D graphing paper, or static file describing a automata

    Given an autonoma, which has a ``_Generation`` coroutine generator, an
    animation can be build by calling this coroutine for as many generations
    are required.

    **Note**:
       This ``automata`` object needs to provide a ``_Generation`` method
       which returns a coroutine which can be called with ``next``.

    **Args**:
       | ``automata`` (OneDCellularAutomata):
       | ``title=None`` (string): An optional title

    **Returns**:
       (Canvas): this object

    **Example(s)**:

    .. code-block:: python

       eco1 = Canvas(autonoma)
       eco1.run_animation(1200, interval=10)  # 10 ms
       eco1.save('eco1.mp4')

       eco2 = Canvas(automata)
       eco2 = save('eco2.pdf, generations=100)

    '''
    self.fig, self.ax = plt.subplots()
    if title:
      self.ax.set_title(title)
    self.automata = automata
    self.generation = automata._Generation()
    self.ax.set_yticklabels([])
    self.ax.set_xticklabels([])
    self.ax.set_aspect(1.0)
    self.ax.xaxis.set_ticks_position('none')
    self.ax.yaxis.set_ticks_position('none')
    self.fig.tight_layout()
    # seventies orange/browns looking color map
    self.cmap = matplotlib.colors.LinearSegmentedColormap.from_list(
      'oranges', ['#ffffff', '#ffa501', '#b27300', '#191000'])
    self.grid = self.ax.pcolormesh(next(self.generation), cmap=self.cmap)

  def init(self):
    '''animation initialization callback

    **Note**:
       This not needed by our animation, but it is needed by the library we
       are calling, so we just stub it out

    **Returns**:
       (tuple): (self.grid,)

    '''
    return (self.grid,)

  def animate(self, i):
    '''animation callback.

    This method will be called for each i frame of the animation.  It creates
    the next generation of the automata then it updates the pcolormesh using
    the set_array method.

    **Args**:
       | ``i`` (int): animation frame number

    **Returns**:
       (tuple): (self.grid,)

    '''
    self.Z = next(self.generation)
    # set_array only accepts a 1D argument
    # so flatten Z before feeding it into the grid arg
    self.grid.set_array(self.Z.ravel())
    return (self.grid,)

  def run_animation(self, generations, interval):
    '''Run an animation of the automata.

    **Args**:
       | ``generations`` (int): number of automata generations
       | ``interval`` (int): movie frame interval in ms

    **Example(s)**:

    .. code-block:: python

      eco = Canvas(automata)
      eco.run_animation(1200, interval=20)  # 20 ms

    '''
    self.anim = animation.FuncAnimation(
      self.fig, self.animate, init_func=self.init,
      frames=generations, interval=interval,
      blit=False)

  def save(self, filename=None, generations=0):
    '''save an animation or run for a given number of generations and save as a
       static file (pdf, svg, .. etc)

    **Note**:
       This function will save as many different static file formats as are
       supported by matplot lib, since it uses matplotlib.

    **Args**:
       | ``filename=None`` (string): name of the file
       | ``generations=0`` (int): generations to run if the files doesn't have
       |                          an 'mp4' extension and hasn't been
       |                          animated before


    **Example(s)**:

       eco1 = Canvas(autonoma)
       eco1.run_animation(50, 10)
       eco1.save('rule_30.mp4)
       eco1.save('rule_30.pdf)

       eco2 = Canvas(autonoma)
       eco1.save('rule_30.pdf', generations=40)

    '''
    if pathlib.Path(filename).suffix == '.mp4':
      self.anim.save(filename)
    else:
      if self.automata.generation > 0:
        for i in range(self.automata.generations):
          next(self.generation)
        self.ax.pcolormesh(self.automata.Z, cmap=self.cmap)
      plt.savefig(filename)

Note

On construction: I didn’t write the Canvas class out of thin air, I created a 2 dimensional array and some functions that would randomize this array, then I fed these functions into the code that I built up using examples from the internet until I got something working. Only then did I feed it the OneDCellularAutomata class, which originally didn’t use a co-routine; it was added later.

The OneDCellularAutomata Class

Let’s give our basic design another look:

_images/rule_30_basic_design.svg

The OneDCellularAutomata object will be responsible for applying the rules to our graphing paper, and for setting it into its initial condition (the black square in the middle of the top line).

To do this OneDCellularAutomata will provide a two-dimensional array, Z, containing color codes, to be used by our Canvas to draw things. It also builds a lot of Rule30 and Wall state machines and links them to other machines so that they can read the left.color and right.color attributes of their adjacent cells. OneDCellularAutomata needs to set up some initial conditions; how the machines are started on the first line of our graphing paper. The Rule30 state machines respond to Next events, which cause them to react and change if they need to change, so the OneDCellularAutomata will need to dispatch this event into all of the Rule30 objects to make a new line as the automata propagate downward.

To make the OneDCellularAutomata object generic, we will feed it its automata rule and wall rules as classes. To make the wall behaviour parameterizable, I’ll add some new wall rule classes that hold the left and right colors in their class attributes:

_images/rule_30_basic_design_1.svg

Here is a UML diagram of the OneDCellularAutomata class:

_images/rule_30_twodcellularautomata.svg

There is a bunch of stuff in this diagram that I don’t know how to draw using UML. For instance, how do I show a class that I have sent it a class, so it knows how to build something, using the class I just gave it? How do I draw something that makes a co-routine?

Well, I don’t know, so I will just scribble down something that isn’t too confusing and explain what I meant here with a few words:

The few key takeaways from the drawing are how the constructor works, we feed it in the rule and wall classes so that it can generically construct automata. We also show the function that returns the co-routine. Each time next is called it advances to the next yield statement. So, the first time the coroutine is activated, it will initialize the automata, and then every activation after that will cause it to descend one row down.

Here is the code:

import numpy as np
from miros import Event

White = 0.1
Black = 0.9

class OneDCellularAutomata():
  def __init__(self,
      generations,
      cells_per_generation=None,
      initial_condition_index=None,
      machine_cls=None,
      wall_cls=None,
      ):
    '''Build a two dimensional cellular automata object which can be advanced
       with a coroutine.

    **Args**:
       | ``generations`` (int): how many generations to run (vertical cells)
       | ``cells_per_generation=None`` (int): how many cells across
       | ``initial_condition_index=None`` (int): the starting index cell (make
       |                                         black)
       | ``machine_cls=None`` (Rule): which automata rule to follow
       | ``wall_cls=None`` (Wall): which wall rules to follow

    **Returns**:
       (OneDCellularAutonomata): an automata object

    **Example(s)**:

    .. code-block:: python

      # build an automata using rule 30 with white walls
      # it should be 50 cells across
      # and it should run for 1000 generations
      autonoma = OneDCellularAutomata(
        machine_cls=Rule30,
        generations=1000,
        wall_cls=WallLeftWhiteRightWhite,
        cells_per_generation=50
      )

      # to get the generator for this automata
      generation = automata.make_generation_coroutine()

      # to advance a generation (first one will initialize it)
      next(generation)

      # to get the color codes from it's two dimension array
      automata.Z

      # to advance a generation
      next(generation)

    '''
    self.machine_cls = machine_cls
    self.wall_cls = wall_cls

    if machine_cls is None:
      self.machine_cls = Rule30

    if wall_cls is None:
      self.wall_cls = WallLeftWhiteRightWhite

    self.generations = generations
    self.cells_per_generation = cells_per_generation

    # if they haven't specified cells_per_generation set it
    # so that the cells appear square on most terminals
    if cells_per_generation is None:
      # this number was discovered through trial and error
      # matplotlib seems to be ignoring the aspect ratio
      self.cells_per_generation = round(generations*17/12)

    self.initial_condition_index = round(self.cells_per_generation/2.0) \
      if initial_condition_index is None else initial_condition_index

    self.generation = None

    self.left_wall=self.wall_cls.left_wall
    self.right_wall=self.wall_cls.right_wall

  def make_and_start_left_wall_machine(self):
    '''make and start the left wall based on the wall_cls'''
    wall = self.wall_cls()
    wall.start_at(self.wall_cls.left_wall)
    return wall

  def make_and_start_right_wall_machine(self):
    '''make and start the right wall based on the wall_cls'''
    wall = self.wall_cls()
    wall.start_at(self.wall_cls.right_wall)
    return wall

  def initial_state(self):
    '''initialize the 1d cellular automata'''
    Z = np.full([self.generations, self.cells_per_generation], Black,
                dtype=np.float32)

    # create a collections of unstarted machines
    self.machines = []
    for i in range(self.cells_per_generation-2):
      self.machines.append(self.machine_cls())

    left_wall = self.make_and_start_left_wall_machine()
    right_wall = self.make_and_start_right_wall_machine()

    # unstarted machines sandwiched between unstarted boundaries
    self.machines = [left_wall] + self.machines + [right_wall]

    # start the boundaries in their holding color
    self.machines[0].start_at(fake_white)
    self.machines[-1].start_at(fake_white)

    # start most of the machines in white except for the one at the
    # intial_condition_index
    for i in range(1, len(self.machines)-1):
      if i != self.initial_condition_index:
        self.machines[i].start_at(white)
      else:
        self.machines[i].start_at(black)

    # we have created a generation, so count down by one
    self.generation = self.generations-1

    # create some initial walls in Z
    Z[:, 0] = self.machines[0].color_number()
    Z[:, Z.shape[-1]-1] = self.machines[-1].color_number()

    self.Z = Z

  def next_generation(self):
    '''create the next row of the 1d cellular automata'''
    Z = self.Z
    if self.generation == self.generations-1:
      # draw the first row
      for i, machine in enumerate(self.machines):
        Z[self.generations-1, i] = machine.color_number()
    else:
      # draw every other row
      Z = self.Z
      new_machines = []
      for i in range(1, (len(self.machines)-1)):
        old_left_machine = self.machines[i-1]
        old_machine = self.machines[i]
        old_right_machine = self.machines[i+1]

        new_machine = self.machine_cls()
        new_machine.start_at(old_machine.state_fn)
        new_machine.left = old_left_machine
        new_machine.right = old_right_machine
        new_machines.append(new_machine)

      left_wall = self.make_and_start_left_wall_machine()
      right_wall = self.make_and_start_right_wall_machine()
      new_machines = [left_wall] + new_machines + [right_wall]

      for i, machine in enumerate(new_machines):
        machine.dispatch(Event(signal=signals.Next))
        Z[self.generation, i] = machine.color_number()
      self.machines = new_machines[:]

    self.Z = Z
    self.generation -= 1

  def make_generation_coroutine(self):
    '''create the automata coroutine'''
    self.initial_state()
    yield self.Z
    while True:
      self.next_generation()
      yield self.Z

Note

On construction: Initially I build the OneDCellularAutomata without a coroutine.

Rule30 and the Wall Classes

Rule30 is a class which describes the attributes and methods needed by our rule30 state machine. The rule30 state machine really isn’t described anywhere as an individual entity, it is two callback functions that attach to a Rule30 object. You can see it here:

_images/rule_30_basic_design_1.svg

To see if the rule 30 machine is designed properly, put your eyes on one of the clusters-of-four-squares at the top of the diagram. Now imagine the state machine was started in the color of the middle of the top three squares of this cluster. Send the Next event to the machine and see if you can get it to transition to the color of the bottom square of the cluster.

Let’s do the first one together:

_images/rule_30_does_it_work.svg

If you repeat this exercise for each of the cluster-of-four-squares, and you are satisfied, then this state machine’s design will give us the rule 30 behavior.

The wall is an even simpler machine, it starts in one color state and remains that way forever.

Here is the code for our Rule30 and Wall classes:

from miros import Event
from miros import signals
from miros import HsmWithQueues
from miros import return_status

class Wall(HsmWithQueues):

  def __init__(self, name='wall'):
    super().__init__(name)
    self.color = None

  def color_number(self):
    return Black if self.color == 'black' else White

def fake_white(wall, e):
  status = return_status.UNHANDLED

  if(e.signal == signals.ENTRY_SIGNAL):
    wall.color = 'white'
    status = return_status.HANDLED
  elif(e.signal == signals.Next):
    status = return_status.HANDLED
  else:
    wall.temp.fun = wall.top
    status = return_status.SUPER
  return status

def fake_black(wall, e):
  status = return_status.UNHANDLED

  if(e.signal == signals.ENTRY_SIGNAL):
    wall.color = 'black'
    status = return_status.HANDLED
  elif(e.signal == signals.Next):
    status = return_status.HANDLED
  else:
    wall.temp.fun = wall.top
    status = return_status.SUPER
  return status

class WallLeftWhiteRightWhite(Wall):
  left_wall = fake_white
  right_wall = fake_white

class WallLeftWhiteRightBlack(Wall):
  left_wall = fake_white
  right_wall = fake_black

class WallLeftBlackRightWhite(Wall):
  left_wall = fake_black
  right_wall = fake_white

class WallLeftBlackRightBlack(Wall):
  left_wall = fake_black
  right_wall = fake_black

class Rule30(Wall):

  def __init__(self, name='cell'):
    super().__init__(name)
    self.left = None
    self.right = None
    self.color = None

def white(cell, e):
  status = return_status.UNHANDLED

  if(e.signal == signals.ENTRY_SIGNAL):
    cell.color = 'white'
    status = return_status.HANDLED
  elif(e.signal == signals.Next):
    if((cell.right.color == 'black' and
        cell.left.color == 'white') or
       (cell.right.color == 'white' and
        cell.left.color == 'black')):
      status = cell.trans(black)
    else:
      status = return_status.HANDLED
  else:
    cell.temp.fun = cell.top
    status = return_status.SUPER
  return status

Running and Visualizing the Cellular Automata

Now that we have all the parts we need let’s stitch them together and see what happens. We will build an automata using rule 30 with some white walls. Then we will feed the automata into a canvas, and use the canvas to print a png file, a pdf file and an mp4 movie:

generations = 200

automata = OneDCellularAutomata(
  generations=generations,
  machine_cls=Rule30,
  wall_cls=WallLeftWhiteRightWhite)

ecosystem = Canvas(automata)
ecosystem.run_animation(generations, interval=100)  # 100 ms
eco.save('rule_30_white_walls_200_generations.mp4')
eco.save('rule_30_white_walls_200_generations.pdf')
eco.save('rule_30_white_walls_200_generations.png')

Here is the movie:

Here is the pgn diagram, click on it to see the pdf version of the same picture:

_images/rule_30_white_walls_200_generations.png

Let’s try it with black walls

generations = 200

automata = OneDCellularAutomata(
  generations=generations,
  machine_cls=Rule30,
  wall_cls=WallLeftBlackRightBlack)

ecosystem = Canvas(automata)
ecosystem.run_animation(generations, interval=50)  # 50 ms
eco.save('rule_30_black_walls_200_generations.mp4')
eco.save('rule_30_black_walls_200_generations.pdf')
eco.save('rule_30_black_walls_200_generations.png')

Here is a link to the resulting video, and below you can see what happens when we run 200 generations of rule 30 with black walls.

_images/rule_30_black_walls_200_generations.png

Running it again for 500 generations and with white walls results in this:

_images/rule_30_white_walls_500_generations.png

On this diagram we can see order imposing itself from the left white wall, and a kind of repeating pattern on the left side of the triangle which emerges from our initial conditions. The center and right part of the results seem to be full of disorder.

Note

On limitations:

The pdf resulting from the 500 generation run of our automata is over 5 MB; this is a lot of data to add to your computer if you want to clone the miros library, so I will refrain from going bigger.

It took a long time to render the 500 generation automata. I don’t know where the computational bottle-neck is coming from; if it were important to me I could profile the different parts of the code until I found my issue, but the issue could be Python itself. Mathematica has no such obvious limitation. If you want to do a deep dive into this subject and don’t have the resources to buy a Mathematica licence, you can buy a Raspberry Pi with the NOOBs OS, then VNC onto the device from your desktop. Wolfram Alpha is being given away on this platform.

The Nothing (n-phenomenon)

The white and black walls force order into the automata; causing a kind of pattern crystallization to propagate across our results. We lose the incredible complexity of rule 30, to this ordering pattern, which I think of as the nothing. Order imposes itself like a prion does in the brain of someone with Alzheimer’s. We have seen that this effect takes place with both white and black walls, the key is that there is an inflexible minority on the walls, imposing itself on a flexible majority. (Nassim Nicholas Taleb calls this kind of thing a normalization.)

I will call this destructive ordering, the n-phenomenon.

Note

Our universe seems to have set rules (the laws of physics) but it will not run into this n-phenomenon, repetition issue; because it doesn’t seem to be confined within walls or have limited memory: the universe is expanding at an accelerating rate.

Maybe it would have been wiped out by an n-phenomenon if it weren’t expanding.

Let’s study it in a bit more detail. I will shrink the width of our graphing paper to 30 cells and watch the n-phenomenon completely destroy rule 30’s beautiful complexity over 100 generations.

 generations = 100

 automata = OneDCellularAutomata(
   generations=generations,
   machine_cls=Rule30,
   wall_cls=WallLeftWhiteRightWhite,
   cells_per_generation=30)

 ecosystem = Canvas(automata)
 ecosystem.run_animation(generations, interval=500)  # 500 ms
 eco.save('rule_30_white_walls_100_generations_width_30.mp4')

It seems that the n-phenomenon has a pattern velocity; resulting in an angle. My protractor tells me that the ordered crystallization, caused by the white minority at the wall, spreads downward at an angle of about 20 degrees.

If we reduce the width of the graphing paper, this pattern seems to advance quicker into the chaos:

 generations = 100

 automata = OneDCellularAutomata(
   generations=generations,
   machine_cls=Rule30,
   wall_cls=WallLeftWhiteRightWhite,
   cells_per_generation=15)

 ecosystem = Canvas(automata)
 ecosystem.run_animation(generations, interval=500)  # 500 ms
 eco.save('rule_30_white_walls_100_generations_width_30.mp4')

Compare the 30 cell width result to the 15 cell width result:

Let’s try going thinner, here is a 15 cell width result beside a 10 cell width result:

So the angle of the n-phenomenon does not necessarily shrink with a reduction in the graphing paper’s width.

Let’s write some code that can calculate the angle of the n-phenomenon.

We will set the walls to be white and the initial condition is a single black rule 30 machine, sandwiched between other rule 30 machines started in the white state:

_images/rule_30_twodcellularautomata_with_angle.svg

The blue object in the diagram represents the disordered part of a rule 30 simulation contained by white walls. The cyan circle is the angle, in degrees, we are searching for.

class OneDCellularAutomataWithAngleDiscovery(OneDCellularAutomata):

  def __init__(self,
      generations,
      cells_per_generation=None,
      initial_condition_index=None,
      machine_cls=None,
      wall_cls=None):

    super().__init__(
      generations,
      cells_per_generation,
      initial_condition_index,
      machine_cls,
      wall_cls)

    self.black_mask = np.array([Black], dtype=np.float32)
    self.white_mask = np.array([White], dtype=np.float32)
    self.n_mask = np.concatenate(
       (self.white_mask, self.black_mask), axis=0)
    self.n_angle = 90

  def build_next_mask(self):
    if abs(self.n_mask[-1] - White) < 0.001:
      self.n_mask = np.concatenate(
        (self.n_mask, self.black_mask), axis=0)
    else:
      self.n_mask = np.concatenate(
        (self.n_mask, self.white_mask), axis=0)

  def update_angle(self):

    previous_generation = self.generation+1
    row_to_check = self.Z[previous_generation]
    sub_row_to_check = row_to_check[0:len(self.n_mask)]

    if np.array_equal(self.n_mask, sub_row_to_check):

      self.nothing_at_row = self.generations-previous_generation + 1
      adjacent = self.nothing_at_row - self.cells_per_generation / 2.0
      opposite = self.cells_per_generation
      self.n_angle = math.degrees(math.atan(opposite/adjacent))

      self.build_next_mask()

  def next_generation(self):
    super().next_generation()
    self.update_angle()

With this code we can plot how the angle of the n-phenomenon changes as a function of the graphing paper width:

_images/cells_per_generation_vrs_angle_of_n_phenomenon.svg

Note

The angle is still an approximation, since as you will see shortly, the n-phenomenon is not a straight line.

Here is a video of 196 different renderings of rule thirty within white walls. The video starts with 2 rule 30 machines squished between two white walls. For each frame advance of the video, a cell is added to the width of the graphing paper and the automata is restarted an run with a black machine at the top and middle of the simulation.

The top part of the video remains constant as the width of the graphing paper expands, which makes sense, since that part of the diagram experiences the same set of initial conditions. What is surprising is how avalanches perpetuate through the diagram as the width of the graphing paper increases. The left side will cause an avalanche for two frames, then the right side will cause an avalanche for two frames, then the left side again and so on and so forth.

You can see when the patterns are going to shift by watching for the “left white _|” and the “black right L”. If either of these mini-patterns appear, it will tell you that the avalanche on their side of the diagram is over, and to move your eyes to the other side of the page for the next frame.

As tempting as it is to think that this 2 beat oscillation is a feature of the automata, it probably isn’t. The 2 beat oscillation is the result of the divide-by-2-and-round-the-result mixed with the expansion-of-the-size-of-the-page. Essentially we are changing the initial conditions on one side of the page for 2 beats, then the other side for two beats. Because rule 30 creates a chaotic system that is highly sensitive to its initial conditions we have avalanches of change in response to a new partial-starting-state. The initial conditions remain the same for the top part of the diagram, but are expressed when the rule 30 body hits the new wall condition.

The video makes me think about time traveling stories. If we imagine the new starting condition as the location of a time traveler arriving in the past from the future (to change one thing and then leave), we can see how he effects history as the change-avalanche on his side of the video frame. The entire past isn’t changed, just the side that was within the causal cone of his avalanche.

The rule 30 states only one square can be effected each generation; so the maximum theoretical propagation of a historical distortion is 45 degrees. In our reality we also have such a causal limitation, it is the speed of light, c.

Here is another video of 196 different renderings with a white left wall and black right wall:

Black left, white right wall:

Left and right walls set to black:

Random Number Generation

Let’s try and build a random number generator using a simple Python program.

First we should discuss our constraints. Rule 30 provides an interesting chaotic phenomenon, we would like to use this rule versus another rule, or we may accidentally lose our ability to generate chaos. If we make our graphing paper very wide, then we need more computer memory and more computing resources to construct the next generation of automata. If we use walls, then we have order imposing itself back into the body of the chaos, as the n-phenomenon, over only a few generations. Even if we can perfectly manage this n-phenomenon, there are only so many permutations that can be held within the bounds of our rectangle, and if we repeat a pattern we are not generating a random number, but rather a long periodic number; a pseudo-random number.

So there seems to be a theoretical upper bound to what we can generate using limited memory given that the rule doesn’t change and the state of the program is held within a bounded rectangle. My first guess at what our upper bound is: 2**n where n is the number of states within the rectangle, and the 2 is selected because we are only tracking two colors, black and white.

First things first, let’s deal with the n-phenomenon coming from the walls. We have control of the walls, and we have a chaos generator, so let’s mine the chaos and feed it back into the walls.

_images/rule_30_chaos_to_walls_1.svg

We can write the colors of our center column into a deque, and use the top two colors of this deque to set the color of the walls. Let’s call this deque the core_colors. To begin with we set all of the core_colors to white, so that the walls will remain white for at least the first len(core_colors) - 2 generations. For every Next event, the core_colors deque is pushed one spot downward into the center of our automata, where it is painted with the color of the core at that spot.

We will use the last two elements of our core_colors to determine the color of our walls. These last two spots will act as a 2 digit binary number, holding the values of WallLeftWhiteRightWhite, WallLeftWhiteRightBlack, WallLeftBlackRightWhite and WallLeftBlackRightBlack. How this rule is applied isn’t that important, it just has to consistently map the center’s chaos onto the walls.

So, how long do we make this core_colors deque? My intuition is to feed it as much chaos as rule 30 can generate, then feed this back into the automata before the n-phenomenon destroys the disorder. I predict that we will have a very long unique pattern when the length of the center column’s deque depth is equal to the number of cells from the top to where the n-phenomenon eats away the chaos. I will mark this queue depth estimate with a * in my data.

Note

You might be asking yourself, “Why don’t we pull the chaos from its deepest well: the far right side of the bulk of the rule 30 cells in the body of our automata.” I was originally going to do this, until I saw the effect of the time traveler videos in the previous section. The cone of causality propagates from the walls. If we mine the chaos from the right side, there won’t be a chance for the rule30 interactions on the right side to feel the effect of the bulk and as a result we will end up with a very narrow feedback loop despite having a deeper deque. This theory could be tested and disproved, but my program is inefficient and my computer is very slow, so I took the center path.

The length of the unique pattern duration will be too much to measure manually, so I will adjust the code to report the unique pattern duration for a specific combination of cell width and queue depth. I’ll explain how I did this after I present my data and results.

Let’s start with the discovered data:

Cell Width and angle

Queue Depth

Unique Pattern Duration

Repeats?

10 (35.7 degrees)

2

3

4

5

6

7

8

9

10*

11

12

13

37

51

14

16

35

30

65

24

52

110

30

32

True

11 (44.4 degrees)

2

3

4

5

6

7

8

9

10

11

12

13*

14

15

16

17

18

53

5

24

57

28

30

32

27

36

42

80

42

84

46

48

50

52

True

12 (36.0 degrees)

2

3

4

5

6

7

8

9

10

11

12*

13

14

15

16

17

18

16

35

24

26

39

58

19

14

46

36

53

32

42

276

46

13

25

True

13 (36.0 degrees)

2

3

4

5

6

7

8

9

10

11

12*

13

14

15

16

17

18

19

20

21

22

23

24

16

5

9

12

46

34

15

65

139

24

44

121

35

157

318

465

278

76

225

197

384

30

162

True

14 (30.2 degrees)

2

3

4

5

6

7

8

9

10

11

12

13*

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

42

43

13

20

26

9

32

176

271

279

20

236

395

66

208

13

338

195

228

98

210

255

1450

32

1223

287

1012

610

True

15 (39.2 degrees)

4

5

6

7

8

9

10

11

12

13

14

15

16*

17

18

19

20

21

22

23

24

25

44

160

134

60

58

48

52

200

160

74

429

541

1022

73

17

271

232

534

564

1210

258

630

True

16 (34.0 degrees)

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16*

17

18

19

20

21

22

23

24

25

26

111

18

42

123

74

35

288

310

42

582

46

40

86

252

378

180

900

288

541

1746

1017

117

1162

551

1182

True

17 (35.3 degrees)

5

6

7

8

9

10

11

12

13

14

15

16

17

18*

19

20

21

22

23

24

25

36

47

270

164

92

42

179

433

448

238

120

60

1054

441

1149

390

1582

600

3305

214

2810

True

18 (27.7 degrees)

4

5

6

7

8

9

10

11

12

13

14

15

16

17*

18

19

20

21

22

23

24

25

26

27

28

29

30

97

90

158

82

192

319

70

101

200

586

597

24

430

860

459

605

532

609

2356

1730

908

734

1513

1653

165

954

682

True

False

True

True

True

True

True

True

True

True

True

True

True

True

True

True

True

True

True

True

True

True

True

19 (27.5 degrees)

4

5

6

7

8

9

10**

11

12

13

14

15

16

17

18*

19

20

21

22

23

24

25

26

27

28

11

75

91

99

43

33

33

144

134

70

252

250

155

677

1943

1299

130

2689

5168

1602

922

2080

2895

5341

3520

True

20 (27.5 degrees)

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18*

19

20

21

22

23

24

25

26

27

28

29

30

11

75

91

99

43

279

213

202

327

792

490

1436

1349

556

1171

172

333

2912

2054

5769

1546

579

400

4535

9075

9709

1697

True

21 (27.5 degrees)

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19*

20

21

22

23

24

25

26

27

28

29

30

615

333

2912

422

534

113

429

82

79

551

862

2159

91

526

1327

837

1429

1173

220

3706

300

27

3090

6036

44

3416

9032

2067

9151

True

22 (24.0 degrees)

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

113

58

350

120

197

135

169

47

123

330

219

981

569

763

398

694

584

373

357

759

433

266

204

4395

527

1138

3383

3562

148

4611

True

True

True

True

False

False

False

True

False

False

True

False

False

False

False

True

True

True

False

False

False

False

True

False

True

True

False

False

False

False

Note

** means, that there is a kind of n-phenomenon pattern that emerges after some time, but before this occurs there is a a nice, richly chaotic pattern.

So, my prediction wasn’t terrible. It turns out that as far as guesses go, if we set the core_color deque length to the number of cells from the top of the automata to where the n-phenomenon would overtake the chaos (leaving the walls white), then you get some decent periodicity. However, we consistently observed that just above my predicted optimal number, there is a very large swell in the length of unique pattern duration.

Another strange thing was observed: the length of a unique pattern duration increases as we increase the core_color deque length. In some cases we seem to be getting massive gains with tiny marginal increases in the core_color deque length. Are we getting something from nothing? No, it turns out you get massive increases in the unique pattern duration but you don’t have consistent chaos in the pattern: the chaos is marbled with the n-phenomenon. You can see this effect in this video of rule 30 within recursive walls, 12 cells wide, with a core_color deque length of 40, run for 400 generations (settles into a loop of 47 cells):

There are other incidence were a different repeating pattern emerges after a long burst of rich chaos. The repeating pattern is much more complex than the n-phenomenon, but much less complex than its preceding chaos.

For width 22 we see something really cool. The chaos just stops, there is no repetition of a complex pattern, we get to see it once, then never again. This has a nice effect of hiding the pattern’s duration from my autocorrelation routine. To measure the duration of the chaos burst I had to manually read it from the picture, this was very time consuming. For width 22, the core_color deque is often infected with the n-phenomenon despite our best efforts to feed it’s chaos into the walls. Given that this phenomenon was discovered within the first 12 tested rectangle widths, it is not unreasonable to assume that is a common occurrence; there will probably be a lot of widths and queue depth combinations that result in the ultimate destruction of the chaos generator. You can see this effect in this video of rule 30 within recursive walls, 22 cells wide, with a core_color deque length of 20, run for 400 generations (one unique pattern for 357 cells):

1

Stephen Wolfram (2002). A New Kind of Science. (p27)