.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "example_gallery/patterns/game_of_life.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_example_gallery_patterns_game_of_life.py: .. _example game of life: Cellular automata: Conway's game of life ======================================== Cellular automata are a small set of rules relating to cells on a grid that can create complex patterns when applied repeatedly. There is a near infinite veriety of rules, but far and away the most well known set of rules is that of the so called Conway's game of life. The setup is as follows: - The board is a 2d rectangular grid - The 8 neighbouring cells are considered, so this includes the diagonal neighbours - A cell has two possbile states: alive or dead The rules as taken from https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life: 1. Any live cell with fewer than two live neighbours dies, as if by underpopulation. 2. Any live cell with two or three live neighbours lives on to the next generation. 3. Any live cell with more than three live neighbours dies, as if by overpopulation. 4. Any dead cell with exactly three live neighbours becomes a live cell, as if by reproduction. Initial conditions ------------------ All cellular automata simulations are highly dependent on the initial conditions. We have to start with some dead cells and some live cells. After all, according to these rules, if all cells are live, the cells will die in the next generation and if all cells are dead, they stey dead for they would need three live neighbours to become a live cell. Therefore we need to start with something that is at least halfway interesting. Let's set up a starting scenario. Feel free to download this script as a notebook and start fiddeling yourself. The particular starting scenario I chose will replicate itself in three generations, which makes it convenient for creating a looping animation. .. GENERATED FROM PYTHON SOURCE LINES 31-70 .. code-block:: Python import matplotlib.pyplot as plt import numpy from gridkit import BoundedRectGrid from gridkit.doc_utils import plot_polygons # Initialize shape = (17, 17) data = numpy.zeros(shape) data[2, 4:7] = 1 data[2, 10:13] = 1 data[7, 4:7] = 1 data[7, 10:13] = 1 data[9, 4:7] = 1 data[9, 10:13] = 1 data[14, 4:7] = 1 data[14, 10:13] = 1 data[4:7, 2] = 1 data[10:13, 2] = 1 data[4:7, 7] = 1 data[10:13, 7] = 1 data[4:7, 9] = 1 data[10:13, 9] = 1 data[4:7, 14] = 1 data[10:13, 14] = 1 grid = BoundedRectGrid(data=data) neighbours = grid.neighbours(grid.indices, connect_corners=True) neighbours = neighbours = numpy.stack( grid.grid_id_to_numpy_id(neighbours.ravel()) ).T.reshape((*shape, 8, 2)) plt.imshow(grid, extent=grid.mpl_extent, cmap="gray_r") plt.title("Starting configuration") plt.show() .. image-sg:: /example_gallery/patterns/images/sphx_glr_game_of_life_001.png :alt: Starting configuration :srcset: /example_gallery/patterns/images/sphx_glr_game_of_life_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 72-77 The naive method ---------------- We can convert the rules into code. The naive but intuitive way would look something like: .. GENERATED FROM PYTHON SOURCE LINES 78-99 .. code-block:: Python data_second_gen = data.copy() for i in range(1, shape[0] - 1): for j in range(1, shape[1] - 1): nb = neighbours[i, j] nr_nb_live = data[nb.T[0], nb.T[1]].sum() if data[i, j] == 1: # live cell if nr_nb_live == 2 or nr_nb_live == 3: data_second_gen[i, j] = 1 else: data_second_gen[i, j] = 0 else: # Dead cell if nr_nb_live == 3: data_second_gen[i, j] = 1 else: data_second_gen[i, j] = 0 plt.imshow(data_second_gen, extent=grid.mpl_extent, cmap="gray_r") plt.title("Second generation") plt.show() .. image-sg:: /example_gallery/patterns/images/sphx_glr_game_of_life_002.png :alt: Second generation :srcset: /example_gallery/patterns/images/sphx_glr_game_of_life_002.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 100-106 The performant method --------------------- The unfortunate thing about this method is that it loops over all cells in python, which is slow. As a first optimization we can get rid of the conditionals by adding all the contributions. This can looks something like: .. GENERATED FROM PYTHON SOURCE LINES 107-122 .. code-block:: Python data_third_gen = data.copy() for i in range(1, shape[0] - 1): for j in range(1, shape[1] - 1): nb = neighbours[i, j] nr_nb_live = data_second_gen[nb.T[0], nb.T[1]].sum() data_third_gen[i, j] = ( 0 + (data_second_gen[i, j] == 1) * (nr_nb_live == 2 or nr_nb_live == 3) + (data_second_gen[i, j] != 1) * (nr_nb_live == 3) ) plt.imshow(data_third_gen, extent=grid.mpl_extent, cmap="gray_r") plt.title("Third generation") plt.show() .. image-sg:: /example_gallery/patterns/images/sphx_glr_game_of_life_003.png :alt: Third generation :srcset: /example_gallery/patterns/images/sphx_glr_game_of_life_003.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 123-126 With the code written like this, it is clear that we can also remove the double python loop by utilizing numpy slices: .. GENERATED FROM PYTHON SOURCE LINES 127-142 .. code-block:: Python nb = data_third_gen[ neighbours[1 : shape[0] - 1, 1 : shape[1] - 1, :, 0], neighbours[1 : shape[0] - 1, 1 : shape[1] - 1, :, 1], ] nr_nb_live = nb.sum(axis=-1) is_live = data[1 : shape[0] - 1, 1 : shape[1] - 1] == 1 data_fourth_gen = ( 0 + is_live * ((nr_nb_live == 2) | (nr_nb_live == 3)) + ~is_live * (nr_nb_live == 3) ) plt.imshow(data_fourth_gen, extent=grid.mpl_extent, cmap="gray_r") plt.title("Fourth generation") plt.show() .. image-sg:: /example_gallery/patterns/images/sphx_glr_game_of_life_004.png :alt: Fourth generation :srcset: /example_gallery/patterns/images/sphx_glr_game_of_life_004.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 143-149 Note how this fourth generation looks the same as the starting conditions. The pattern loops every three frames/generations. Written this way, you could even run this logic on a GPU using PyTorch or wgpu, but since most time is now spent plotting using matplotlib you might also want to consider a different visualization method (for example py-gfx) if a speedup for larger grids is desired. .. GENERATED FROM PYTHON SOURCE LINES 152-158 Animation --------- Since we are iterating over time, cellular automata lend itself well for an animation. Given the logic above, the animation code can look like so: .. GENERATED FROM PYTHON SOURCE LINES 159-187 .. code-block:: Python def update_frame(frame): nb = data[ neighbours[1 : shape[0] - 1, 1 : shape[1] - 1, :, 0], neighbours[1 : shape[0] - 1, 1 : shape[1] - 1, :, 1], ] nr_nb_live = nb.sum(axis=-1) is_live = data[1 : shape[0] - 1, 1 : shape[1] - 1] == 1 data[1 : shape[0] - 1, 1 : shape[1] - 1] = ( 0 + is_live * ((nr_nb_live == 2) | (nr_nb_live == 3)) + ~is_live * (nr_nb_live == 3) ) ax.clear() ax.imshow(data, extent=grid.mpl_extent, cmap="gray_r") ax.set_title(frame) ax.set_axis_off() # Create animation from matplotlib.animation import FuncAnimation fig, ax = plt.subplots() ax.set_aspect("equal") ax.imshow(data, extent=grid.mpl_extent, cmap="gray_r") anim = FuncAnimation(fig, update_frame, frames=range(0, 6), repeat=True, interval=300) plt.show() .. container:: sphx-glr-animation .. raw:: html
.. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 0.830 seconds) .. _sphx_glr_download_example_gallery_patterns_game_of_life.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: game_of_life.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: game_of_life.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: game_of_life.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_