The game of life (Advanced)

%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt

The game of life (Advanced)

This is an example of a cellular automaton - a collection of rules that act on the state of a system and determine the updated state. These often have interesting, emergent behaviours that are not obviously inherent in the rules.

There is a similarity with Molecular Dynamics, Discrete Element Modeling, Agent-Based modeling and so on where simple rules acting locally produce bulk properties of the system.

The game of life is simple enough in rules but can produce interesting emergent behaviour. It is a good template to explore some of the issues we always face in building models.

Let’s look a building a more challenging version of the problem - larger, faster, better analysis.

# The rules of the game

size     = 100
lonely   = 2
crowded  = 4
breeding = 3

We don’t want to keep setting this thing up by hand. Here is a way to make an array of zeros and ones at random

state = np.round(np.random.random_sample((size,size) )).astype(int)

print state
print 
print np.count_nonzero(state)
print
plt.imshow(state, interpolation='nearest', cmap="gray") 
def live_neighbours(state):
    ln = np.empty_like(state)
    for index, value in np.ndenumerate(state):
        #Need to add 2, becase the slicing works like 'up to but not including'
        x0 = max(0,(index[0]-1))
        x1 = max(0,(index[0]+2))
        y0 = max(0,(index[1]-1))
        y1 = max(0,(index[1]+2))
        subarray = state[x0:x1, y0:y1]
        ln[index] = subarray.sum() - value # need to subtract actual value at that cell...
        
    return ln
ln = live_neighbours(state)

print ln

plt.imshow(ln, interpolation='nearest', cmap="jet") 
plt.colorbar()

Let’s use numpy to locate quickly the regions where the death conditions apply

death1 = np.where(ln >= crowded)

Note the form that this returns - the coordinates where the condition applies

death1 = np.where(ln >= crowded)
death2 = np.where(ln <  lonely)

print death1
dying_cells = np.zeros_like(state)
dying_cells[death1] = 1
dying_cells[death2] = 1

## 

plt.imshow(dying_cells, interpolation='nearest', cmap="gray_r") 
plt.colorbar()
birth = np.where(ln == breeding)

baby_cells = np.zeros_like(state)
baby_cells[birth] = 1

plt.imshow(baby_cells, interpolation='nearest', cmap="gray_r") 
plt.colorbar()
## These should be complementary

check = dying_cells + baby_cells
print check.max()
# Logical operations on the entire array to update state

new_state = state.copy()
new_state[death1]   = 0
new_state[death2]   = 0
new_state[birth]  = 1

plt.imshow(new_state, interpolation='nearest', cmap="gray_r",) 
plt.colorbar()
plt.savefig("Frame-1.png", dpi=300, format="png" )
## Bundle this up to make a sequence of images

state_list = [state.copy()]

for i in range(0, 250):
    
    if i%50 == 0:
        print i

    ln = live_neighbours(state)

    state[ln >= crowded]   = 0
    state[ln <  lonely]    = 0
    state[ln == breeding]  = 1

    state_list.append(state.copy())
    
state_list

Exercise

Class Exercise: Now let’s try to make a series of frames as images for a number of frames of the game of life. We could also try changing the parameters of the game to see what patterns develop.

  • What resolution can we reach ?

  • Can we work out ways to make this run faster ?

  • What if there is more than one happy state where new cells grow ?

def update_state(state):
    new_state = state.copy()

    ln = live_neighbours(state)
    
    death1 = np.where(ln >= crowded)
    death2 = np.where(ln <  lonely)
    birth  = np.where(ln == breeding)

    new_state[death1]   = 0
    new_state[death2]   = 0
    new_state[birth]    = 1
    
    return new_state
    
# Initialise

state = np.round(np.random.random_sample((size,size) )).astype(int)
steps = 100

# Loops and save image

for step in range(0, steps):
    
    new_state = update_state(state)
    print "Step {} - living cells - {}".format(step, np.count_nonzero(new_state))
    
    plt.imshow(new_state, interpolation='nearest', cmap="gray_r",) 
    plt.savefig("Frame-{:04d}.png".format(step), dpi=300, format="png" )
    
    # We could keep all of these for later or discard
    state = new_state

Speedup

What is taking the time in this computation ?

  • Boundaries always reset to zero, do away with submatrix tests ?

  • Fastest way to reset boundaries ?

  • Logic tests or multiply or add / cap the max

Animation

from matplotlib import animation, rc
from IPython.display import HTML

fig, ax = plt.subplots()
fig.set_size_inches((7,7))
animage = ax.imshow(state_list[0], interpolation='bilinear', cmap=plt.cm.bone)
def init():
    animage.set_data(state_list[0])
    return (animage,)

def animate(i):
    animage.set_data(state_list[i])
    return (animage,)

anim = animation.FuncAnimation(fig, animate, init_func=init,
                               frames=len(state_list), interval=50, 
                               blit=True)
HTML(anim.to_jshtml())