Code is at the bottom, with extensive comments
Notes on how to code Game of Life (GOL) including an animation, using numpy and matplotlib.
I learn through doing inspirational projects and Conway’s Game of Life cellular automata is certainly that. More about that in a later blog.
I’m also learning NumPy, as a step in learning the maths behind neural nets, so initially I’m following Nicolas P. Rougier’s online book “From Python to Numpy” (2017), Ch 4.2, where he has both a python and numpy version of Game of Life. The numpy version uses vectorization, rather than for loops.
The GOL rules are:
- Survival: if an existing alive cell has 2 or 3 neighbours then it survives.
- Birth: if an empty cell has 3 neighbours then it becomes alive.
- All other neighbourhood counts lead to dead cells
Each cell has 8 neighbouring cells. By slicing the universe grid for each neighbouring cell and adding the values we obtain a neighbourhood count (new_u) for each cell. Then the rules can be implemented using boolean masks which reference both the new_u neighbourhood count grids and the universe grids.
Rougier doesn’t do the animation in his book. He stops after showing how to compute neighbours and does one iteration. So I found a version on the web by finxter using python for loops and FuncAnimation
The code works but the finxter explanation is cursory.
It is essential to make a new copy of the universe. Not doing this held my up for a while. Part of the problem was that I didn’t fully understand matplotlib’s FuncAnimation so I researched that for a bit.
Here is a good explanation:The FuncAnimation class allows us to create an animation by passing a function that iteratively modifies the data of a plot. This is achieved by using the setter methods on various Artist (examples: Line2D, PathCollection, etc.). A usual FuncAnimation object takes a Figure that we want to animate and a function func that modifies the data plotted on the figure. It uses the frames parameter to determine the length of the animation. The interval parameter is used to determine time in milliseconds between drawing of two frames.
- source
Which setter method, plotting method and Artist do I need?
- data set method: set_data()
- plotting method: Axes.imshow()
- Artist: image.AxesImage
class matplotlib.animation.FuncAnimation(fig, func, frames=None, init_func=None, fargs=None, save_count=None, *, cache_frame_data=True, **kwargs)
- makes an animation by repeatedly calling a function func
- You must store the created Animation in a variable (called ani in my code) that lives as long as the animation should run. Otherwise, the Animation object will be garbage-collected and the animation stops.
- func callable: The function to call at each frame. The first argument will be the next value in frames.
- frames: Source of data to pass func and each frame of the animation
- fargs: Additional arguments to pass to each call to func.
- interval: delay between frames in milliseconds
- repeat: bool, default True
Here is how to generate the animation gif which I used for this blog:
ani.save("GOL_random.gif")
Then I worked out how to count the population of each frame and the number of generations of the game and added that to the xlabel inside the animation function
Finally, I noticed that one Conway site had an extensive pattern library with hundreds of interesting seeds which can be run WITH DYNAMICALLY CHANGING COLOURS. I looked around at how to achieve this but so far haven’t worked it out.
1 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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | """ Game of Life """ import numpy as np import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation def create_universe(N=50, p=0.5): return np.random.choice([0, 1], size=(N, N), p=[1-p, p]) # generates random 1 and 0s with p probability in a N*N 2D array N = 50 # array size universe = create_universe(N=N, p=0.5) # universe determines image generations = 0 # measure the number of generations # animation function def animate(frame, universe, img): global generations new_u = np.zeros((N, N), dtype = int) # new_u initialises as same size as universe, array of all 0s # then calculates neighbour count # set edges of universe to zero # to avoid edge calculation complications universe[0,:] = universe[-1,:] = universe[:,0] = universe[:,-1] = 0 # universe is 2D array with 1s and 0s # count the neighbours of the universe cells (by grid displacement) # new_u is neighbour counts new_u[1:-1,1:-1] = (universe[ :-2, :-2] + universe[ :-2,1:-1] + universe[ :-2,2:] + universe[1:-1, :-2] + universe[1:-1,2:] + universe[2: , :-2] + universe[2: ,1:-1] + universe[2: ,2:]) # boolean masks to implement GOL rules birth = (new_u==3)[1:-1,1:-1] & (universe[1:-1,1:-1]==0) # 3 neighbours for an empty cell -> birth survive = ((new_u==2) | (new_u==3))[1:-1,1:-1] & (universe[1:-1,1:-1]==1) # 2 or 3 neighbours in occupied cell -> survive # reset universe to zeros, make a new copy, then update 1s & 0s universe[:] = np.zeros((N, N), dtype = int) # new copy essential universe[1:-1,1:-1][birth | survive] = 1 # count the population population = np.count_nonzero(universe) # update image with the data set method img.set_data(universe) # display generations and population ax.set_xlabel('generations = {}, population = {}'.format(generations, population)) generations += 1 # update generations fig = plt.figure(figsize=(5, 5)) # figure size ax = plt.axes() ax.set_yticklabels([]) # turn off axis ticks (but keep x axis label above) ax.set_xticklabels([]) img = ax.imshow(universe, interpolation='nearest') # imshow is the plotting method for AxesImage artist ani = FuncAnimation(fig, animate, fargs=(universe, img,), frames=200, interval=200) # ani lives while animation runs, otherwise garbage collected # fargs: arguments to pass to each call of the function # interval in milliseconds ani.save("GOL_random.gif") # save the animations as a gif plt.show() |
