John Conway's Game of Life is one of the more famous cellular automata. The universe is a collection of cells forming a 2-dimensional grid. In any single generation, each cell is either alive or dead, and a set of rules determines the state of each cell in the next generation. Game of Life simulates a biological system: the rules use (only) the number of living neighbors of a cell, and model overpopulation, underpopulation and the specific conditions for growth.
The classical rules are discrete. We will approximate these rules with continuous functions, thereby greatly improving performance while preserving much of the behavior that makes the classical CA famous. These approximations allow for fractional states. We also study the behavior of the CAs obtained by deforming the rules.
In addition, we implement file input of the first generation (seed) for files in the .LIF format (the format used by the library http://www.radicaleye.com/lifepage/patterns/contents.html).
We use numpy matrices for our universes and some numpy functions (multiplication, entry-wise exp) in applying the rules. The cells in the universe correspond to the entries of the matrix, living to 1 and dead to 0.
{{{id=1| import numpy as np /// }}}We use the twoDCA class to organize the information and methods we need for a Life-like two-dimensional CA.
{{{id=2| class twoDCA: def __init__(self, s, al, de, ne): self.M = s self.r = (self.M).shape[0] self.c = (self.M).shape[1] self.Alive = al self.Dead = de self.neighbors = ne self.orig = s self.makeOnes() def makeOnes(self): self.OnesR = np.empty((self.r)) self.OnesC = np.empty((self.c)) for i in xrange(self.r): self.OnesR[i] = 1 for i in xrange(self.c): self.OnesC[i] = 1 def nextMatrix(self): N = self.neighbors(self.M) self.M = self.M*self.Alive(N) + (1-self.M)*self.Dead(N) def gener(self, m): for i in xrange(m): self.nextMatrix() def npcount(self): return (self.OnesR).dot((self.M).dot(self.OnesC)) def resetM(self, N): self.M = N self.orig = self.M self.r = (self.M).shape[0] self.c = (self.M).shape[1] self.makeOnes() def mplot(self): return matrix_plot(self.M, norm = 'value', vmin = 0, vmax = 1) def anigen(self, n): l = [] for i in xrange(n): l.append(matrix_plot(self.M, cmap = 'afmhot', norm = 'value', vmin = 0, vmax = 1)) self.nextMatrix() return animate(l) /// }}}The following functions are our continuous approximations of the Game of Life rules, plotted below (alive in blue, dead read).
{{{id=3| def alive(a = 1.8, b = 5/2, c = 100): return lambda n : exp(-(a*(n-b))^c) /// }}} {{{id=4| def dead(a = 25, b = 3, c = 100): return lambda n : exp(-(a*(n-b))^c) /// }}} {{{id=40| plot(exp(-(1.8*(x-5/2))^100), (1, 4)) + plot(exp(-(25*(x-3))^100), (1, 4), rgbcolor = (1,0,0)) ///We use cython to get a fast implementation of a neighbors function.
{{{id=5| %cython import numpy as np cimport numpy as np import cython cimport cython ctypedef np.float64_t DTYPE_t @cython.boundscheck(False) @cython.wraparound(False) @cython.nonecheck(False) def cyneighbors(np.ndarray[DTYPE_t, ndim=2] M): cdef int r = M.shape[0] cdef int c = M.shape[1] cdef np.ndarray[DTYPE_t, ndim=2] N = np.zeros((r,c)) cdef unsigned int i, j for i in xrange(r-2): for j in xrange(c-2): N[i + 1, j + 1] = M[i , j] + M[i, j+1] + M[i, j + 2] + M[i+1, j] + M[i + 1, j+2] + M[i+2 , j] + M[i+2, j+1] + M[i+2, j + 2] return N /// }}}The following function returns an n by n random matrix with a border of b zeros on all sides.
{{{id=6| def make_random_seed(n, b): Z = np.zeros((n + 2*b, n + 2*b)) Z[b:n+b,b:n+b] = np.random.random((n,n)) return Z /// }}}We make an instance of a two-dimensional CA with our approximate rules and a 50 by 50 random matrix (0s or 1s) with a border of 20 zeros as a seed.
{{{id=7| apr = twoDCA(round(make_random_seed(50, 20)), alive(), dead(), cyneighbors) /// }}}Plot the seed:
{{{id=8| apr.mplot() ///Run 20 generations:
{{{id=9| apr.gener(20) /// }}}Plot the current matrix:
{{{id=10| apr.mplot() ///We notice familiar Life shapes, including still lifes and blinkers. Let's animate the first 20 generations of another random seed:
{{{id=11| apr.resetM(round(make_random_seed(50,20))) /// }}} {{{id=12| a = apr.anigen(20) /// }}} {{{id=13| a.show() /// }}}The program lif_to_matrix takes as input a .LIF file and returns the corresponding numpy matrix (and its size). In more detail, the input is the list of lines in the file as strings.
{{{id=14| def lif_to_matrix(L): P = [] j = 0 x = 0 y = 0 for s in L: if s[0] == '.' or s[0] == '*': if j == 0: P.append([0,0,s[:-2]]) else: P[j-1].append(s[:-2]) if s[0:2] == '#P': j += 1 c = s[3:-2].split() x = int(c[0]) y = int(c[1]) P.append([x,y]) C = [t[0] for t in P] R = [t[1] for t in P] R.sort() C.sort() rwidth = R[-1] - R[0] + 10 cwidth = C[-1] - C[0] + 10 size = 2*rwidth, 2*cwidth M = np.zeros(size) for L in P: y = L[0] x = L[1] for i in xrange(len(L)-2): for j in xrange(len(L[i+2])): if L[i+2][j] == '*': M[i + x + size[0]/2,j + y + size[1]/2] = 1 return M, size /// }}}Currently, it only reads files on the local computer. As an example, we input the classic glider gun and animate some generations.
{{{id=15| f = open('/home/gautam/life_patterns/GUN30.LIF','r') L = f.readlines() M, n = lif_to_matrix(L) /// }}} {{{id=16| apr.resetM(M) /// }}} {{{id=17| apr.mplot() ///We see that the glider gun has a population (number of live cells) of 45. One would not want to input it by hand.
{{{id=23| apr.npcount() /// 45.0 }}} {{{id=18| apr.gener(20) /// }}} {{{id=19| apr.mplot() ///Our framework can also handle very different rules. As an example, let’s implement the rule which allows entries strictly between 0 and 1 and replaces a cell with the average of the cells around it.
{{{id=25| def aliveAvg(n): return n/8 /// }}} {{{id=22| apr2 = twoDCA(round(make_random_seed(100,0)), aliveAvg, aliveAvg, cyneighbors) /// }}} {{{id=24| apr2.mplot() ///