# This program is a free software: you can redistribute it and/or modify # it under the terms of the GNU General Public Licence as published by # the Free Software Foundation, either version 3 of the Licence, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public Licence for details. # # You should have a copy of the GNU Public Licence # with this program. If not, see . # # ############################################################################# # # # GoL-anim-dif-v1-1-stabil-tst1-Fig-3-4-fin.py is the Python 3.8 program, # # which runs the 'Game of Life' simulation in the simplest possible way, # # that is suitable for educational purposes. # # # # (C) Jiri Kroc under the licence GPLv3 # # # # Date of creation October 04, 2021 # # # # # # Cite as: # # # # Jiri Kroc: "Robust massive parallel information processing environments # # in biology and medicine: case study", # # Problems of Information Society 13:2 (2022) 12-22, # # DOI : 10.25045/jpis.v13.i2.02 # # # ############################################################################# import numpy as np from numpy.random import Generator, PCG64 import matplotlib.pyplot as plt import matplotlib.animation as anim import matplotlib as mpl import os import PIL from PIL import Image from mpl_toolkits.axes_grid1 import make_axes_locatable """ This Python program represents the simplest possible cellular automaton simulating a complex system: the 'Game of Life' designed by John Conway. It has just around 100 lines of the code. """ x_size = 60 y_size = 60 lattice_size = [x_size, y_size] time = 0 random_yes = 1 frames_dif = [] # Defines the range of shown frames in animations frames_ini = 0 frames_end = 200 #300 saveNoneOrAnimOrFigures = 0 # 0 = None, 1 = Anim, 2 = Figures # Initialization of both lattices lattice_old = np.zeros((x_size,y_size), dtype = int) lattice_new = np.zeros_like(lattice_old) prev_lattice = np.zeros_like(lattice_old) lat_diff = np.zeros_like(lattice_old) lat_colors = np.zeros((x_size,y_size), dtype = int) # Random initial values (uncomment when needed) if random_yes == 1: rng = Generator(PCG64(124)) #124)) #5 for x in range (0, x_size): for y in range (0, y_size): if rng.random() < 0.121: lattice_old[x][y] = 9 # or # Glider gun initial values (cax.grid(which = 'minor', axis = 'x', color = "g", linewidth = 0.5)omment whe random initial cond. used): if random_yes == 0: lattice_old[7,5] = 9; lattice_old[7,6] = 9; lattice_old[8,5] = 9; lattice_old[8,6] = 9; lattice_old[7,15] = 9; lattice_old[8,15] = 9; lattice_old[9,15] = 9; lattice_old[6,16] = 9; lattice_old[10,16] = 9; lattice_old[11,17] = 9; lattice_old[11,18] = 9; lattice_old[5,17] = 9; lattice_old[5,18] = 9; lattice_old[8,19] = 9; lattice_old[6,20] = 9; lattice_old[10,20] = 9; lattice_old[7,21] = 9; lattice_old[8,21] = 9; lattice_old[9,21] = 9; lattice_old[8,22] = 9; lattice_old[6,25] = 9; lattice_old[5,25] = 9; lattice_old[7,25] = 9; lattice_old[6,26] = 9; lattice_old[5,26] = 9; lattice_old[7,26] = 9; lattice_old[4,27] = 9; lattice_old[8,27] = 9; lattice_old[4,29] = 9; lattice_old[8,29] = 9; lattice_old[3,29] = 9; lattice_old[9,29] = 9; lattice_old[5,39] = 9; lattice_old[6,39] = 9; lattice_old[5,40] = 9; lattice_old[6,40] = 9; ## Copperhead spaceship lattice_old[22,53] = 9; lattice_old[22,54] = 9; lattice_old[22,57] = 9; lattice_old[22,58] = 9; lattice_old[23,55] = 9; lattice_old[23,56] = 9; lattice_old[24,55] = 9; lattice_old[24,56] = 9; lattice_old[25,52] = 9; lattice_old[25,54] = 9; lattice_old[25,57] = 9; lattice_old[25,59] = 9; lattice_old[26,52] = 9; lattice_old[26,59] = 9; lattice_old[28,52] = 9; lattice_old[28,59] = 9; lattice_old[29,53] = 9; lattice_old[29,54] = 9; lattice_old[29,57] = 9; lattice_old[29,58] = 9; lattice_old[30,54] = 9; lattice_old[30,55] = 9; lattice_old[30,56] = 9; lattice_old[30,57] = 9; lattice_old[32,55] = 9; lattice_old[32,56] = 9; lattice_old[33,55] = 9; lattice_old[33,56] = 9; ## Glider lattice_old[20,2] = 0; lattice_old[20,5] = 9; lattice_old[21,3] = 9; lattice_old[21,5] = 9; lattice_old[22,4] = 9; lattice_old[22,5] = 9; # Glider lattice_old[30,2] = 9; lattice_old[31,3] = 9; lattice_old[31,4] = 9; lattice_old[32,2] = 9; lattice_old[32,3] = 9; # Blinker lattice_old[40,5] = 9; lattice_old[41,6] = 9; lattice_old[42,7] = 9; lattice_old[50,5] = 9; lattice_old[50,6] = 9; lattice_old[50,7] = 9; # lattice_old[50,8] = 9; # Square lattice_old[20,22] = 9; lattice_old[20,23] = 9; lattice_old[20,24] = 9; lattice_old[21,22] = 9; lattice_old[21,24] = 9; lattice_old[22,22] = 9; lattice_old[22,23] = 9; lattice_old[22,24] = 9; def Sum_lattice(lattice): sum = 0 for x in range (0, x_size): for y in range (0, y_size): sum += lattice[x][y] return sum # Evaluation of the Sum over neighbors with periodic # boundary conditions. def Spread(lattice, x, y): for i in range (-1, 2): for j in range (-1, 2): if (lattice_old[x,y] > 1): if ((i != 0) and (j != 0)): lattice_new[(x_size + x + i)%(x_size)][(y_size + y + j)%(y_size)] \ += 1 lattice_old[x,y] -= 1 lattice_new[x,y] += lattice_old[x,y] lattice_old[x,y] = 0 if lattice_old[x,y] > 0: print(f'Spread is leaking ...\nlattice_old[{x},{y}] = {lattice_old[x,y]}') exit(1) def Update(): """ The lattice Updating function: Evaluates the new layer using the old one. """ global lattice_new global lattice_old global lat_diff global time alive_thr = 2 #2 # dead_thr = 8 #8 # 9 sum_old = Sum_lattice(lattice_old) for x in range (0, x_size): for y in range (0, y_size): Spread(lattice_old[x][y], x, y) lat_diff = np.copy(lattice_new) # sum_new = Sum_lattice(lattice_new) if sum_old != sum_new: print('Leaking sum of dif...') print(f'sum_old = {sum_old}, sum_new = {sum_new}') exit(1) # for x in range (0, x_size): for y in range (0, y_size): def Neighb_alive(): alive = 0 for i in range (-1, 2): for j in range (-1, 2): if ((i != 0) and (j != 0)): cell = lattice_new[(x_size + x + i)%(x_size)][(y_size + y + j)%(y_size)] if ((cell >= alive_thr)): alive += 1 return alive neigh_alive = Neighb_alive() if (neigh_alive == 3): lattice_new[x][y] = 9 elif ((neigh_alive == 2) \ and ((lattice_new[x][y] >= alive_thr) \ and (lattice_new[x][y] <= dead_thr))): lattice_new[x][y] = 9 else: lattice_new[x][y] = 0 time += 1 def Coloring(): """ B & W coloring of the simulated distribution. """ global lat_colors for x in range (0, x_size): for y in range (0, y_size): if (lattice_new[x][y] > 2): lat_colors[x][y] = 1 else: lat_colors[x][y] = 0 def Swap(): """ Swaps new layer into the old one and erase the new one. """ global lattice_old, lattice_new lattice_old = np.zeros_like(lattice_new) lattice_old = np.copy(lattice_new) lattice_new = np.zeros_like(lattice_old) lat_diff = np.zeros_like(lattice_old) def displayPlots(): """ Defines the layout of the screen displaing different layers of the simukations step: old-step, diffusion (distribution), new-layer, and excitation. """ # Old # Figure definition & init fig, ((ax1,ax2),(ax3,ax4)) = plt.subplots(nrows=2, ncols=2, figsize=(8,12)) #, constrained_layout=True) # 6, 4 cmap=(mpl.colors.ListedColormap(['green','gray','white','black','cyan', 'royalblue','lightgreen','yellow','orange','red'])) im1 = ax1.imshow(lattice_old, cmap='gray', vmin=0, vmax=10) im1.set_cmap(cmap) divider = make_axes_locatable(ax1) cax1 = divider.append_axes("bottom", size="5%", pad=0.1) fig.colorbar(im1, cax = cax1, ticks = [0,1,2,3,4,5,6,7,8,9], orientation = 'horizontal') title_text = 'Old, step = ' \ + '{0:5d}'.format(time) ax1.set_title(title_text) # Set major ticks & labels ax1.tick_params(left = True, right = True, bottom = True, top = True) ax1.tick_params(labelleft = True, labelright = False, \ labelbottom = True, labeltop = True) # Set minor ticks at the left and bottom ax1.set_xticks(np.arange(-.5, x_size, 1), minor=True) ax1.set_yticks(np.arange(-.5, y_size, 1), minor=True) ax1.tick_params(which='minor',left = True, right = True, bottom = True, top = True) # Set the grid of green lines within the lattice #ax1.grid(which = 'minor', axis = 'x', color = "g", linewidth = 0.5) #ax1.grid(which = 'minor', axis = 'y', color = "g", linewidth = 0.5) # # Diff im2 = ax2.imshow(lat_diff, cmap='nipy_spectral', vmin=0, vmax=10) cmapDif=(mpl.colors.ListedColormap(['green','gray','white','cyan', 'royalblue','lightgreen','yellow','orange','red','black'])) im2.set_cmap(cmapDif) title_text_2 = 'Diff, step = ' + '{0:5d}'.format(time) ax2.set_title(title_text_2) divider = make_axes_locatable(ax2) cax2 = divider.append_axes("bottom", size="5%", pad=0.1) fig.colorbar(im2, cax = cax2, ticks = [0,1,2,3,4,5,6,7,8,9], orientation = 'horizontal') # Set major ticks & labels ax2.tick_params(left = True, right = True, bottom = True, top = True) ax2.tick_params(labelleft = False, labelright = True, \ labelbottom = True, labeltop = True) # Set minor ticks at the left and bottom ax2.set_xticks(np.arange(-.5, x_size, 1), minor=True) ax2.set_yticks(np.arange(-.5, y_size, 1), minor=True) ax2.tick_params(which='minor',left = True, right = True, bottom = True, top = True) # # New im3 = ax3.imshow(lattice_new, cmap='nipy_spectral', vmin=0, vmax=10) im3.set_cmap(cmap) title_text_3 = 'New, step = ' + '{0:5d}'.format(time) ax3.set_title(title_text_3) divider = make_axes_locatable(ax3) cax3 = divider.append_axes("bottom", size="5%", pad=0.1) fig.colorbar(im3, cax = cax3, ticks = [0,1,2,3,4,5,6,7,8,9], orientation = 'horizontal') # Set major ticks & labels ax3.tick_params(left = True, right = True, bottom = True, top = True) ax3.tick_params(labelleft = True, labelright = False, \ labelbottom = True, labeltop = True) ax3.set_xticks(np.arange(-.5, x_size, 1), minor=True) ax3.set_yticks(np.arange(-.5, y_size, 1), minor=True) ax3.tick_params(which='minor',left = True, right = True, bottom = True, top = True) # # Excitation bwmap=(mpl.colors.ListedColormap(['black','white'])) im4 = ax4.imshow(lat_colors, cmap='gray', vmin=0, vmax=1) im4.set_cmap(bwmap) title_text_top = 'Cell state > 2' + '{0:5d}'.format(time) ax4.set_title(title_text_top) divider = make_axes_locatable(ax4) cax4 = divider.append_axes("bottom", size="5%", pad=0.1) fig.colorbar(im4, cax = cax4, ticks = [0,1], orientation = 'horizontal') # Set major ticks & labels ax4.tick_params(left = True, right = True, bottom = True, top = True) ax4.tick_params(labelleft = False, labelright = True, \ labelbottom = True, labeltop = True) ax4.set_xticks(np.arange(-.5, x_size, 1), minor=True) ax4.set_yticks(np.arange(-.5, y_size, 1), minor=True) ax4.tick_params(which='minor',left = True, right = True, bottom = True, top = True) return fig, ax1, ax2, ax3, ax4, im1, im2, im3, im4 def saveSubplot(lattice,map,vmin_val,vmax_val,step,wDirectory,fileNam): """ This function saves diffusion and new-layer data into predefined folders in the given format. """ global frames_dif fig = plt.figure() ax1 = fig.add_subplot(1, 1, 1) ax1.imshow(lattice, cmap=map, vmin=vmin_val, vmax=vmax_val) dir_path = os.getcwd() diff_dir = os.path.join(dir_path, wDirectory) print(f'diff_dir = {diff_dir}') file_name = fileNam + (str(step)).zfill(5) + '.png' if not os.path.isdir(diff_dir): os.makedirs(diff_dir) fig.savefig(os.path.join(diff_dir, file_name), bbox_inches = 'tight') img = ax1.get_images() frames_dif.append(img) im2 = plt.imshow(lattice, cmap=map, vmin=vmin_val, vmax=vmax_val) def animate(step, fig, ax1, ax2, ax3, ax4, im1, im2, im3, im4): """ The Function administering the animation: calls Update() function and draw the updated lattice on canvas. """ global time global title_text global prev_lattice global frames_dif if step == frames_end: plt.close(fig) else: title_text = 'Old, step = ' \ + '{0:5d}'.format(time) #step) ax1.set_title(title_text) title_text_2 = 'Diff, step = ' + '{0:5d}'.format(time) ax2.set_title(title_text_2) title_text_3 = 'New, step = ' + '{0:5d}'.format(time) ax3.set_title(title_text_3) title_text_top = 'Cell state > 2, step = '+ '{0:5d}'.format(time) ax4.set_title(title_text_top) prev_lattice = np.copy(lattice_old) Update() Coloring() im1.set_array(prev_lattice) im2.set_array(lat_diff) im3.set_array(lattice_new) im4.set_array(lat_colors) print(f'im2 type = {type(im2)}') fig.canvas.draw() cut_im2 = ax2.get_window_extent().transformed(fig.dpi_scale_trans.inverted()) map_diff = (mpl.colors.ListedColormap( \ ['green','gray','white','cyan','royalblue','lightgreen','yellow','orange','red'])) if saveNoneOrAnimOrFigures == 2: saveSubplot(lat_diff,map_diff, 0, 9,step,"Diffusion/","diff") saveSubplot(lattice_new,map_diff, 0, 9,step,"CellState/","state") print(f'u: len(frames_dif) = {len(frames_dif)}') Swap() return im1 # Plot & animations def plotAndAnimations(fig, ax1, ax2, ax3, ax4, im1, im2, im3, im4): """ The function generates and saves an animation made from the simulation. """ # Defines number of frames per second shown in animations frame_pause = 400 # Call the function providing the animation from # the module Animation anim_video = anim.FuncAnimation(fig, animate, \ interval=frame_pause, frames=range(frames_ini, \ frames_end), repeat=False,fargs=(fig, ax1, ax2, ax3, ax4, im1, im2, im3, im4)) # Creates animated gif figure: if saveNoneOrAnimOrFigures == 1: anim_video.save('GoL-anim.gif', dpi=1200, writer=anim.PillowWriter(fps=2)) plt.show() ############################################################################# # Main program def main(): fig, ax1, ax2, ax3, ax4, im1, im2, im3, im4 = displayPlots() plotAndAnimations(fig, ax1, ax2, ax3, ax4, im1, im2, im3, im4) print('End of program.') main()