# The Game of Life, Inheritance, and Cellular AutomataÂ¶

Preamble: Run the cells below to import the necessary Python packages

*This notebook created by William Gilpin. Consult the course website for all content and GitHub repository for raw files and runnable online code.*

```
import numpy as np
# Wipe all outputs from this notebook
from IPython.display import Image, clear_output, display
clear_output(True)
# Import local plotting functions and in-notebook display functions
import matplotlib.pyplot as plt
%matplotlib inline
# Add the course directory to the Python path
import sys
sys.path.append('../')
import cphy.plotting as cplot
```

### The Game of Life and Cellular AutomataÂ¶

- Cellular automata are systems where all physical laws and fields are discretized: space is a lattice, and time is discrete
- Physical laws take the form of IF-THEN statements
- Basic cellular automata are Markovian: given current field values at some set of lattice points, perform an instantaneous update based on a set of rules

## The Game of LifeÂ¶

- Conway, 1970
- Complex dynamics emerge from simple rules
- Markovian: next state of universe depends only on current state of universe
- Local: next state at a lattice point depends only on current site and nearest neighbors
- Synchronous: we update all sites at once, rather than raster scanning
- Finite fields: A site can have values of 1 (alive) or 0 (dead)

#### Game life rules:Â¶

- Underpopulation: Any live cell with fewer than two live neighbours dies
- Survival: Any live cell with two or three live neighbours lives stays alive
- Overpopulation: Any live cell with more than three live neighbours dies
- Reproduction: Any dead cell with
*exactly*three live neighbours becomes a live cell

These simple rules give rise to surprisingly complex behaviors; in fact, the Game of Life has been shown to support universal computation, given a suitable encoding scheme based on initial conditions. We've included some examples of fixed-point, limit-cycle, and soliton like dynamics below

.

*Images from Scholarpedia*

# Implementing Cellular Automata using inheritance in PythonÂ¶

Below, we are going to define a "base" class for cellular automata, and then define subclasses for various specific automata. The base class will handle state counting, initialization, and will contain a `simulate`

method for the simulation loop (where we repeatedly call the update rule).

In the base class, we will define a `next_state`

method because the `simulate`

method depends on it. But because the state rule is different for each automaton, we will leave the implementation of `next_state`

to be overwritten by the subclasses (the base class raises an exception)
This is an example of the Template Method Pattern.

```
# A base allows us to impose structure on specific cases. We will use this as a parent
# for subsequent classes
class CellularAutomaton:
"""
A base class for cellular automata. Subclasses must implement the step method.
Parameters
n (int): The number of cells in the system
n_states (int): The number of states in the system
random_state (None or int): The seed for the random number generator. If None,
the random number generator is not seeded.
initial_state (None or array): The initial state of the system. If None, a
random initial state is used.
"""
def __init__(self, n, n_states, random_state=None, initial_state=None):
self.n_states = n_states
self.n = n
self.random_state = random_state
np.random.seed(random_state)
## The universe is a 2D array of integers
if initial_state is None:
self.initial_state = np.random.choice(self.n_states, size=(self.n, self.n))
else:
self.initial_state = initial_state
self.state = self.initial_state
self.history = [self.state]
def next_state(self):
"""
Output the next state of the entire board
"""
return NotImplementedError
def simulate(self, n_steps):
"""
Iterate the dynamics for n_steps, and return the results as an array
"""
for i in range(n_steps):
self.state = self.next_state()
self.history.append(self.state)
return self.state
```

Just to show how inheritance works, we will start by defining two boring automata---an extinction automaton that sends every single input condition to the same output state, and a random automaton that randomly assigns each input state to either 0 or 1.

```
class ExtinctionAutomaton(CellularAutomaton):
"""
A cellular automaton that simulates the extinction of a species.
"""
def __init__(self, n, **kwargs):
super().__init__(n, 2, **kwargs)
def next_state(self):
"""
Output the next state of the entire board
"""
next_state = np.zeros_like(self.state)
return next_state
class RandomAutomaton(CellularAutomaton):
"""
A cellular automaton with random updates
"""
def __init__(self, n, random_state=None, **kwargs):
super().__init__(n, 2, **kwargs)
self.random_state = random_state
def next_state(self):
"""
Output the next state of the entire board
"""
next_state = np.random.choice(self.n_states, size=(self.n, self.n))
return next_state
```

```
model = ExtinctionAutomaton(40, random_state=0)
model.simulate(200)
plt.figure()
plt.imshow(model.initial_state, cmap="gray")
plt.title("Initial state")
plt.figure()
plt.imshow(model.state, cmap="gray")
plt.title("Final state")
```

Text(0.5, 1.0, 'Final state')

```
model = RandomAutomaton(40, random_state=0)
model.simulate(200)
plt.figure()
plt.imshow(model.initial_state, cmap="gray")
plt.title("Initial state")
plt.figure()
plt.imshow(model.state, cmap="gray")
plt.title("Final state")
```

Text(0.5, 1.0, 'Final state')

We are now ready to define the Game of Life as a subclass of the base class.

```
# This child class inherits methods from the parent class
class GameOfLife(CellularAutomaton):
"""
An implementation of Conway's Game of Life in Python
Args:
n (int): The number of cells in the system
**kwargs: Additional keyword arguments passed to the base CellularAutomaton class
"""
def __init__(self, n, **kwargs):
# the super method calls the parent class's __init__ method
# and passes the arguments to it. It is a constructor
super().__init__(n, 2, **kwargs)
def next_state(self):
"""
Compute the next state of the lattice
"""
# Compute the next state
next_state = np.zeros_like(self.state) # preallocate the next state
for i in range(self.n):
for j in range(self.n):
# Compute the number of neighbors
n_neighbors = np.sum(
self.state[i - 1:i + 2, j - 1:j + 2]
) - self.state[i, j]
# Update the next state
if self.state[i, j] == 1:
if n_neighbors in [2, 3]:
next_state[i, j] = 1
else:
if n_neighbors == 3:
next_state[i, j] = 1
return next_state
```

```
model = GameOfLife(40, random_state=0)
model.simulate(500)
# print timing results
print("Average time per iteration: ", end="")
%timeit model.next_state()
plt.figure()
plt.imshow(model.initial_state, cmap="gray")
plt.title("Initial state")
plt.figure()
plt.imshow(model.state, cmap="gray")
plt.title("Final state")
```

Average time per iteration: 3.7 ms Â± 58.8 Âµs per loop (mean Â± std. dev. of 7 runs, 100 loops each)

Text(0.5, 1.0, 'Final state')

```
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
# Assuming frames is a numpy array with shape (num_frames, height, width)
frames = np.array(model.history).copy()
fig = plt.figure(figsize=(6, 6))
img = plt.imshow(frames[0], vmin=0, vmax=1, cmap="gray");
plt.xticks([]); plt.yticks([])
# tight margins
plt.margins(0,0)
plt.gca().xaxis.set_major_locator(plt.NullLocator())
def update(frame):
img.set_array(frame)
ani = FuncAnimation(fig, update, frames=frames, interval=50)
HTML(ani.to_jshtml())
```