import numpy as np
import matplotlib.pyplot as plt
Conway's Game of Life is a cellular automaton created by John Conway in 1970. Similar to the Mayfly model, it is a deterministic process to update the state of a population of cells.
We will use 2D NumPy arrays to represent the population of cells aranged in an $n \times n$ grid.
A value of 1
will signify that a cell is alive while a value of 0
will signify that a cell is dead.
Lets begin with an $ n\times n $ array of all 0s with a small three-block column (3$\times$1) of 1s in the middle. Use an integer datatype (dtype=int
) when defining your array.
Exercise: Write a function starting_state(n)
that returns the array described above.
def starting_state(n):
cells = np.zeros((n,n), dtype=int)
cells[n//2-1:n//2+2, n//2] = 1
return cells
Let's visualize this configuration with plt.imshow
:
n = 10
cells = starting_state(n)
plt.imshow(cells)
We will use the current state of the population to determine the next state. In the Game of Life, each cell interacts with its eight neighbors (i.e. the horizontally, vertically, or diagonally adjacent cells).
The rules of the Game of Life can be summarized as follows:
In order to update our array from one state to the next, we need to be able to count the number of live neighbors of the $(i,j)$th cell for any choice of $i,j$.
Exercise: Write a function count_live_neighbors(cells,i,j)
that counts the number of living neighbors of the $(i,j)$th cell.
np.sum
function will add all values in an array.For image denoising, we discussed padding our array before constructing 3
by 3
grids centered at each pixel:
def count_live_neighbors(cells,i,j):
nrows, ncols = cells.shape
padded_cells = np.zeros((nrows+2, ncols+2), dtype=int)
padded_cells[1:-1, 1:-1] = cells
# Recall: The [i,j]th index of cells corresponds to
# the [i+1, j+1]st index of padded_cells
# We want a 3 by 3 grid centered at the [i,j]th cell of cells
grid = padded_cells[i:i+3, j:j+3]
# We can subtract cells[i,j] from the sum
# to either remove a wrongly counted live neighbor
# or do nothing if cells[i,j] is not alive
live_neighbors = np.sum(grid) - cells[i,j]
return live_neighbors
Let's test our function on the starting configuration previously defined:
print(cells)
print(count_live_neighbors(cells, 3, 5))
print(count_live_neighbors(cells, 4, 5))
print(count_live_neighbors(cells, 5, 5))
print(count_live_neighbors(cells, 5, 6))
cells
population¶We can now update the cells
array according to the rules. We have to update every entry of the array, so we will need to loop through all the entries.
Exercise: Write a function update_cells(cells)
that takes in a population array cells
, applies the Rules of Life to update the population, and returns the updated population.
def update_cells(cells):
new_cells = cells.copy()
nrows, ncols = cells.shape
for i in range(nrows):
for j in range(ncols):
# Get the number of live neighbors, excluding the cell itself
live_neighbors = count_live_neighbors(cells, i, j)
# If the is alive...
if cells[i,j] == 1:
# and if the cell has exactly 2 or 3 live neighbors...
if live_neighbors == 2 or live_neighbors == 3:
# then the cell lives
new_cells[i,j] = 1
else:
# otherwise the cell dies
new_cells[i,j] = 0
# If the cell is not alive...
else:
# and if it has exactly 3 live neighbors...
if live_neighbors == 3:
# then the cell becomes alive
new_cells[i,j] = 1
else:
# otherwise the cell remains dead
new_cells[i,j] = 0
return new_cells
new_cells = update_cells(cells)
plt.figure(figsize=(10,5))
plt.subplot(1,2,1)
plt.imshow(cells)
plt.title('Old cells')
plt.subplot(1,2,2)
plt.imshow(new_cells)
plt.title('New cells')
The FuncAnimation
function from matplotlib.animation
can be used to create animations.
It takes in a figure fig
and function animate
. The animate
function should take in a frame index i
and perform any desired updates to the figure.
%matplotlib notebook
#%matplotlib qt
from matplotlib.animation import FuncAnimation
Exercise: Modify the code below to animate the Game of Life.
Instead of visualizing the array x
, we want to visualize cells
. For each frame of animation, we want to update the cells array, i.e. cells = update_cells(cells)
.
#x = np.zeros((200,200), dtype = int)
n = 10
cells = starting_state(n)
fig = plt.figure()
im = plt.imshow(cells,vmin=0,vmax=1) # Generate the initial plot
def animate(i):
#x[:,:]= (np.random.random(x.shape) > .5) # Update the x array with random data
cells[:,:] = update_cells(cells)
im.set_data(cells) # Update the figure with new x array
return im
anim = FuncAnimation(fig, animate, cache_frame_data=False)
plt.show()
Exercise: Use np.random.random
to randomly select an initial cells
array of 0
s and 1
s.
def random_state(n, ratio_live_cells):
cells = np.zeros((n,n))
live_cell_mask = np.random.random((n,n)) < ratio_live_cells
cells[live_cell_mask] = 1
return cells
n = 200
cells = random_state(n, .1)
fig = plt.figure()
im = plt.imshow(cells,vmin=0,vmax=1) # Generate the initial plot
def animate(i):
cells[:,:] = update_cells(cells)
im.set_data(cells) # Update the figure with new x array
return im
anim = FuncAnimation(fig, animate, cache_frame_data=False)
plt.show()
What can we do to make our code run more efficiently? At a 200
by 200
grid, it is very slow to update.
Let's look at the count_live_neighbors
function, which will run $n^2$ times for an $n$ by $n$ grid. Let's rewrite count_live_neighbors
to take padded_cells
in as an input.
def count_live_neighbors(cells,padded_cells,i,j):
# Recall: The [i,j]th index of cells corresponds to
# the [i+1, j+1]st index of padded_cells
# We want a 3 by 3 grid centered at the [i,j]th cell of cells
grid = padded_cells[i:i+3, j:j+3]
# We can subtract cells[i,j] from the sum
# to either remove a wrongly counted live neighbor
# or do nothing if cells[i,j] is not alive
live_neighbors = np.sum(grid) - cells[i,j]
return live_neighbors
We'll then need to update the update_cells
function to pass in padded_cells
:
def update_cells(cells):
new_cells = cells.copy()
nrows, ncols = cells.shape
padded_cells = np.zeros((nrows+2, ncols+2), dtype=int)
padded_cells[1:-1, 1:-1] = cells
for i in range(nrows):
for j in range(ncols):
# Get the number of live neighbors, excluding the cell itself
live_neighbors = count_live_neighbors(cells, padded_cells, i, j)
# If the is alive...
if cells[i,j] == 1:
# and if the cell has exactly 2 or 3 live neighbors...
if live_neighbors == 2 or live_neighbors == 3:
# then the cell lives
new_cells[i,j] = 1
else:
# otherwise the cell dies
new_cells[i,j] = 0
# If the cell is not alive...
else:
# and if it has exactly 3 live neighbors...
if live_neighbors == 3:
# then the cell becomes alive
new_cells[i,j] = 1
else:
# otherwise the cell remains dead
new_cells[i,j] = 0
return new_cells
Let's see if that helps:
n = 200
cells = random_state(n, .1)
fig = plt.figure()
im = plt.imshow(cells,vmin=0,vmax=1) # Generate the initial plot
def animate(i):
cells[:,:] = update_cells(cells)
im.set_data(cells) # Update the figure with new x array
return im
anim = FuncAnimation(fig, animate, cache_frame_data=False)
plt.show()
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.animation import FuncAnimation
%matplotlib notebook
From Monday's class:
def count_live_neighbors(cells,padded_cells,i,j):
# Recall: The [i,j]th index of cells corresponds to
# the [i+1, j+1]st index of padded_cells
# We want a 3 by 3 grid centered at the [i,j]th cell of cells
grid = padded_cells[i:i+3, j:j+3]
# We can subtract cells[i,j] from the sum
# to either remove a wrongly counted live neighbor
# or do nothing if cells[i,j] is not alive
live_neighbors = np.sum(grid) - cells[i,j]
return live_neighbors
def update_cells(cells):
new_cells = cells.copy()
nrows, ncols = cells.shape
padded_cells = np.zeros((nrows+2, ncols+2), dtype=int)
padded_cells[1:-1, 1:-1] = cells
for i in range(nrows):
for j in range(ncols):
# Get the number of live neighbors, excluding the cell itself
live_neighbors = count_live_neighbors(cells, padded_cells, i, j)
# If the is alive...
if cells[i,j] == 1:
# and if the cell has exactly 2 or 3 live neighbors...
if live_neighbors == 2 or live_neighbors == 3:
# then the cell lives
new_cells[i,j] = 1
else:
# otherwise the cell dies
new_cells[i,j] = 0
# If the cell is not alive...
else:
# and if it has exactly 3 live neighbors...
if live_neighbors == 3:
# then the cell becomes alive
new_cells[i,j] = 1
else:
# otherwise the cell remains dead
new_cells[i,j] = 0
return new_cells
def random_state(n, ratio_live_cells):
cells = np.zeros((n,n))
live_cell_mask = np.random.random((n,n)) < ratio_live_cells
cells[live_cell_mask] = 1
return cells
n = 200
cells = random_state(n, .1)
fig = plt.figure()
im = plt.imshow(cells,vmin=0,vmax=1) # Generate the initial plot
def animate(i):
cells[:,:] = update_cells(cells)
im.set_data(cells) # Update the figure with new x array
return im
anim = FuncAnimation(fig, animate, cache_frame_data=False)
plt.show()
We would like be able to distill this animation into some data that we can more easily describe.
With our current code, we're constantly overwriting the cells
array to store the current state only. It would be useful to maintain a history of all states of cells
.
Let's a new variable, cells_history
that will contain each iteration of cells
. Let's also set a specific number of time steps that we will carry out.
T = 200 # Set the number of time steps
n = 100 # Set the grid size, i.e. n x n
cells = random_state(n, .1) # Initialize the starting configuration
cells_history = np.zeros((n,n,T)) # Initialize the array containing all configurations
cells_history[:,:,0] = cells # Set the t=0 slice to match the initial configuarion
Now, we want to iterate through the time steps and update our cells
array and place its updated contents into the appropriate slice of cells_history
.
for t in range(1,T):
cells = update_cells(cells) # Update the `cells` array
cells_history[:,:,t] = cells # store the update cells array in slice t
For each choice of t
(i.e. the last index) of cells_history
, we get another view of the cells
array at time t
.
Let's turn off interactive plotting for now.
%matplotlib inline
plt.imshow(cells_history[:,:,10])
We could setup a 2x2 grid of subplots with different time slices:
t_list = [0, 10, 20, 30]
fig = plt.figure(figsize=(6,6))
for i, t in enumerate(t_list):
plt.subplot(2,2,i+1)
plt.imshow(cells_history[:,:,t])
plt.axis('off')
plt.title('t = {}'.format(t))
plt.tight_layout()
Note: We see that matplotlib
has artificially introduced some green into this plot, but interpolation between yellow pixels and blue/purple pixels. We can fix this by using the keyword argument interpolation='nearest'
within plt.imshow
.
t_list = [0, 10, 20, 30]
fig = plt.figure(figsize=(6,6))
for i, t in enumerate(t_list):
plt.subplot(2,2,i+1)
plt.imshow(cells_history[:,:,t], interpolation='nearest')
plt.axis('off')
plt.title('t = {}'.format(t))
plt.tight_layout()
It would be useful if we could condense the geometric information (i.e. the information in the first two dimensions of cells_history
into a single number so that we could plot this quantity against time (i.e. against the third dimension).
One idea is to count the number of live cells during each time step.
number_of_live_cells = []
for t in range(T):
cells = cells_history[:,:,t]
count = 0
for i in range(n):
for j in range(n):
if cells[i,j] == 1:
count += 1
number_of_live_cells.append(count)
Note: We can use NumPy features to calculate these counts in a more "slick" way:
number_of_live_cells = []
for t in range(T):
cells = cells_history[:,:,t]
count = cells.sum() # Since live cells are 1s and dead cells are 0s, the sum is the count
number_of_live_cells.append(count)
We could also use a Boolean array to identify locations of live cells
number_of_live_cells = []
for t in range(T):
cells = cells_history[:,:,t]
count = (cells == 1).sum() # Add up each True, where we have True for each live cell
number_of_live_cells.append(count)
We could go one step further and avoid the outer for
loop:
number_of_live_cells = np.sum(cells_history == 1, axis=(0,1))
t_list = np.arange(T)
plt.plot(t_list, number_of_live_cells, '-')
plt.title('Game of Life')
plt.xlabel('Time')
plt.ylabel('Number of live cells')