Source code for conway

# -*- coding: utf-8 -*-

"""
Package conway
==============

Top-level package for conway

"""

__version__ = "0.4.2"

import numpy as np
import pickle
import time


[docs]class FiniteGrid: """Naive NxN grid with different boundary conditions. Cells are randomly assigned state (dead or alive). :param int N: number of cells in the X and Y direction. :param str boundary: boundary condition: ``zero``, ``reflecting``, or ``periodic``. :param bool dump: pickle the generated FiniteGrid object. (Practical if you discover a nice pattern and you want to view it again. :param bool load: if True, load a file with a pickled FiniteGrid object, e.g. to view its evolution again. :param str filename: name of the file to be dumped or loaded. In order to not have to implement special boundary rules, we make the grid (N+2)x(N+2) where the outer layers are ghost cells which are just there to make sure that index i-1, i+1, j-1 and j+1 exist for all cells (i,j):: . | . . . . | . --+---------+-- . | 0 0 1 0 | . . | 0 1 0 0 | . . | 0 0 1 0 | . . | 0 1 0 1 | . --+---------+-- . | . . . . | . The state of the ghost cells is determined by the boundary conditions: * all zeros:: 0 | 0 0 0 0 | 0 --+---------+-- 0 | 0 0 1 0 | 0 0 | 0 1 0 0 | 0 0 | 0 0 1 0 | 0 0 | 0 1 0 1 | 0 --+---------+-- 0 | 0 0 0 0 | 0 * reflective boundary condition: the ghost cell has the same state as the cell on the inside of the boundary:: 0 | 0 0 1 0 | 0 --+---------+-- 0 | 0 0 1 0 | 0 0 | 0 1 0 0 | 0 0 | 0 0 1 0 | 0 0 | 0 1 0 1 | 1 --+---------+-- 0 | 0 1 0 1 | 1 * Periodic boundary condition: a ghost cell is a copy of the state at the other end, as if the finite grid is repeated in the x and y direction:: 1 | 0 1 0 1 | 0 --+---------+-- 0 | 0 0 1 0 | 0 0 | 0 1 0 0 | 0 0 | 0 0 1 0 | 0 1 | 0 1 0 1 | 0 --+---------+-- 0 | 0 0 1 0 | 0 """ boundary_conditions = ('zero', 'reflect', 'periodic') def __init__(self, N=10, boundary='zero', dump=False, load=False, filename='conway'): if load: fg = FiniteGrid.load(filename=filename) # transfer all properties from fg to self self.__dict__ = fg.__dict__ else: self.N = N rng = np.random.default_rng() self.states = rng.integers(0, high=2, size=(N+2,N+2)) # correct the boundaries if 'zero'.startswith(boundary): self.bc = 'zero' # boundaries are zeros self.apply_0bc() elif 'reflect'.startswith(boundary): self.bc = 'reflect' self.apply_rbv() elif 'periodic'.startswith(boundary): self.bc = 'periodic' self.apply_pbc() else: raise ValueError("boundary not in ('zero', 'reflect', 'periodic')") self.generation = 0 self.counts = None self.symbols = ' X' if dump: self.dump(filename=filename)
[docs] def apply_bc(self, bc=None): """Apply a boundary condition to this FiniteGrid object. :param str bc: a string identifying the type of boundary condition we want to apply. """ if not bc is None: self.bc = bc if self.bc == 'zero': self.apply_0bc() elif self.bc == 'reflect': self.apply_rbc() elif self.bc == 'periodic': self.apply_pbc() else: raise ValueError("boundary not in ('zero', 'reflect', 'periodic')")
[docs] def apply_0bc(self): """Apply the zero boundary condition, i.e. surround this FiniteGrid object by zeros. """ N = self.N self.states[: , 0 ] = 0 self.states[: , N + 1] = 0 self.states[0 , 1:N+1] = 0 self.states[N + 1, 1:N+1] = 0
[docs] def apply_rbc(self): """Apply the reflecting boundary condition, i.e. the surroundig elements have the same value as the value on the inside of the boundary. """ N = self.N self.states[:, 0 ] = self.states[:, 1] self.states[:, N + 1] = self.states[:, N] self.states[0, : ] = self.states[1, :] self.states[N + 1, :] = self.states[N, :]
[docs] def apply_pbc(self): """Apply the periodic boundary condition. This can be viewed as: * the square being folded such that opposite ends along each axis are glued together. That first gives a tube and then a torus. * the square being part of an infinite repetition along each axis yielding a periodic pattern. """ N = self.N # edges self.states[1:N + 1, 0] = self.states[1:N + 1, N] self.states[1:N + 1, N + 1] = self.states[1:N + 1, 1] self.states[0, 1:N + 1] = self.states[N, 1:N + 1] self.states[N + 1, 1:N + 1] = self.states[1, 1:N + 1] # corners self.states[0 , 0 ] = self.states[N, N] self.states[N + 1, N + 1] = self.states[1, 1] self.states[0 , N + 1] = self.states[N, 1] self.states[N + 1, 0 ] = self.states[1, N]
[docs] def evolve(self, n_generations=1, draw=True, stop_if_static=False, curse=None, interval=0.1): """Let the system evolve over ``generations`` generations. :param int n_generations: the number of generation to evolve. :param bool draw: if True, draw each generation on the terminal. :param bool stop_if_static: if True stops evolving the system if it is a static state. :param float interval: seconds to wait between drawing two successive generations. .. note: ``stop_if_static`` does not end the evolution when patterns occur that are periodic in time. A typical and much occurring time periodic pattern is three live cells in a row which alternate being aligned along the X-axis and the Y-axis. """ if self.counts is None: self.counts = np.zeros_like(self.states, dtype=int) N = self.N while n_generations>0: if stop_if_static: previous_states = np.copy(self.states) for i in range(1,N+1): for j in range(1,N+1): self.counts[i, j] = self.states[i-1, j-1] \ + self.states[i-1, j ] \ + self.states[i-1, j+1] \ + self.states[i , j-1] \ + self.states[i , j+1] \ + self.states[i+1, j-1] \ + self.states[i+1, j ] \ + self.states[i+1, j+1] for i in range(1,N+1): for j in range(1,N+1): if self.states[i,j] == True: # populated if not self.counts[i,j] in (2,3): # cell population dies self.states[i,j] = False else: if self.counts[i,j] == 3: self.states[i,j] = True self.apply_bc() self.generation += 1 n_generations -= 1 # stop if all cells are dead if np.sum(self.states) == 0: generations = 0 # stop if fixed state if stop_if_static: if np.all(previous_states == self.states): generations = 0 if not curse is None: self.curse(curse) time.sleep(interval) elif draw: self.print(boundary=False)
[docs] def print(self, boundary=True, symbols=None): """Print this FiniteGrid object to the terminal. :param bool boundary: if True, also prints the surrounding boundary layers. :param list-like symbols: use ``symbols[0]`` for denoting dead cells, and ``symbols[1]`` for denoting living cells in the output. The defaults are `` `` and ``X``. """ if symbols: self.symbols = symbols irange = range(self.N+2) if boundary else range(1,self.N+1) jrange = range(self.N+2) if boundary else range(1,self.N+1) # print(f'BC = {self.bc}') for i in irange: s = '' for j in jrange: s += self.symbols[self.states[i,j]] print(s) print()
[docs] def curse(self, stdscr, boundary=True, symbols=None): """'Print' the FiniteGrid object to the terminal using the Python curses module. This gives a nicer visualization because the successive generations are overwriting each other, thus being more close to an animation. The current implementation is limited to the size of the terminal. :param stdscr: the curses wrapper object for the terminal. :param bool boundary: if True, also prints the surrounding boundary layers. :param list-like symbols: use ``symbols[0]`` for denoting dead cells, and ``symbols[1]`` for denoting living cells in the output. The defaults are `` `` and ``X``. """ if symbols: self.symbols = symbols irange = range(self.N+2) if boundary else range(1,self.N+1) jrange = range(self.N+2) if boundary else range(1,self.N+1) for i in irange: s = '' for j in jrange: s += self.symbols[self.states[i,j]] stdscr.addstr(i, 0, s) stdscr.addstr(i+1, 0, str(self.generation)) stdscr.refresh()
[docs] def dump(self,filename='conway'): """Pickle this FiniteGrid object (save to file). :parameter str filename: name of the pickle file to contain the pickled FiniteGrid object> """ self.counts = None # we do not need to dump counts with open(f"{filename}.pickle", mode='wb') as file: pickle.dump(self, file=file)
[docs] @staticmethod def load(filename='conway'): """Unpickle a pickled FiniteGrid object. :parameter str filename: name of the pickle file containing the pickled FiniteGrid object. :return: a FiniteGrid object :raises: RuntimeError if unpickling the file does not yield a FiniteGrid object. """ fn = f"{filename}.pickle" with open(fn, mode='rb') as file: fg = pickle.load(file=file) if not isinstance(fg,FiniteGrid): raise RuntimeError(f"File '{fn}' does not contain a 'FiniteGrid' object.") return fg