├── _config.yml ├── facebook_combined.txt.gz ├── environment.yml ├── .gitignore ├── remove_soln.py ├── LifeRabbits.py ├── LICENSE ├── LifePuffer.py ├── Life.py ├── README.md ├── Cell2D.py ├── utils.py ├── 02_workshop.ipynb ├── 03_workshop.ipynb ├── 01_workshop.ipynb └── soln └── 02_workshop.ipynb /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /facebook_combined.txt.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ComplexityScience/HEAD/facebook_combined.txt.gz -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: ComplexityScience 2 | 3 | dependencies: 4 | - python=3.7 5 | - jupyter 6 | - numpy 7 | - matplotlib 8 | - seaborn 9 | - pandas 10 | - scipy 11 | - networkx=2.* 12 | - pip 13 | - pip: 14 | - empiricaldist 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | *~ 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Packages 9 | *.egg 10 | *.egg-info 11 | dist 12 | build 13 | eggs 14 | parts 15 | bin 16 | var 17 | sdist 18 | develop-eggs 19 | .installed.cfg 20 | lib 21 | lib64 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | -------------------------------------------------------------------------------- /remove_soln.py: -------------------------------------------------------------------------------- 1 | import nbformat as nbf 2 | from glob import glob 3 | 4 | # Collect a list of all notebooks in the content folder 5 | notebooks = glob("*workshop.ipynb") 6 | 7 | text = '# Solution' 8 | replacement = '# Solution goes here' 9 | 10 | # Search through each notebook 11 | for ipath in notebooks: 12 | ntbk = nbf.read(ipath, nbf.NO_CONVERT) 13 | 14 | for cell in ntbk.cells: 15 | # remove tags 16 | if 'tags' in cell['metadata']: 17 | cell['metadata']['tags'] = [] 18 | 19 | # remove output 20 | if 'outputs' in cell: 21 | cell['outputs'] = [] 22 | 23 | # remove solutions 24 | if cell['source'].startswith(text): 25 | cell['source'] = replacement 26 | 27 | nbf.write(ntbk, ipath) 28 | -------------------------------------------------------------------------------- /LifeRabbits.py: -------------------------------------------------------------------------------- 1 | """ Code example from Complexity and Computation, a book about 2 | exploring complexity science with Python. Available free from 3 | 4 | http://greenteapress.com/complexity 5 | 6 | Copyright 2016 Allen Downey 7 | MIT License: http://opensource.org/licenses/MIT 8 | """ 9 | from __future__ import print_function, division 10 | 11 | import sys 12 | import matplotlib.pyplot as plt 13 | 14 | from Life import Life, LifeViewer 15 | 16 | 17 | def main(script, *args): 18 | """Constructs the rabbits methusela. 19 | 20 | http://www.argentum.freeserve.co.uk/lex_r.htm#rabbits 21 | """ 22 | 23 | rabbits = [ 24 | '1000111', 25 | '111001', 26 | '01' 27 | ] 28 | 29 | n = 400 30 | m = 600 31 | life = Life(n, m) 32 | life.add_cells(n//2, m//2, *rabbits) 33 | viewer = LifeViewer(life) 34 | anim = viewer.animate(frames=100, interval=1) 35 | plt.subplots_adjust(left=0.01, right=0.99, bottom=0.01, top=0.99) 36 | plt.show() 37 | 38 | 39 | if __name__ == '__main__': 40 | main(*sys.argv) 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Allen Downey 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LifePuffer.py: -------------------------------------------------------------------------------- 1 | """ Code example from Complexity and Computation, a book about 2 | exploring complexity science with Python. Available free from 3 | 4 | http://greenteapress.com/complexity 5 | 6 | Copyright 2016 Allen Downey 7 | MIT License: http://opensource.org/licenses/MIT 8 | """ 9 | from __future__ import print_function, division 10 | 11 | import sys 12 | import matplotlib.pyplot as plt 13 | 14 | from Life import Life, LifeViewer 15 | 16 | def main(script, *args): 17 | """Constructs a puffer train. 18 | 19 | Uses the entities in this file: 20 | http://www.radicaleye.com/lifepage/patterns/puftrain.lif 21 | """ 22 | lwss = [ 23 | '0001', 24 | '00001', 25 | '10001', 26 | '01111' 27 | ] 28 | 29 | bhep = [ 30 | '1', 31 | '011', 32 | '001', 33 | '001', 34 | '01' 35 | ] 36 | 37 | n = 400 38 | m = 600 39 | life = Life(n, m) 40 | 41 | col = 120 42 | life.add_cells(n//2+12, col, *lwss) 43 | life.add_cells(n//2+26, col, *lwss) 44 | life.add_cells(n//2+19, col, *bhep) 45 | viewer = LifeViewer(life) 46 | anim = viewer.animate(frames=100, interval=1) 47 | plt.subplots_adjust(left=0.01, right=0.99, bottom=0.01, top=0.99) 48 | plt.show() 49 | 50 | 51 | if __name__ == '__main__': 52 | main(*sys.argv) 53 | 54 | 55 | -------------------------------------------------------------------------------- /Life.py: -------------------------------------------------------------------------------- 1 | """ Code example from Complexity and Computation, a book about 2 | exploring complexity science with Python. Available free from 3 | 4 | http://greenteapress.com/complexity 5 | 6 | Copyright 2016 Allen Downey 7 | MIT License: http://opensource.org/licenses/MIT 8 | """ 9 | from __future__ import print_function, division 10 | 11 | import sys 12 | 13 | import numpy as np 14 | import matplotlib.pyplot as plt 15 | from matplotlib import animation 16 | 17 | """ 18 | For animation to work in the notebook, you might have to install 19 | ffmpeg. On Ubuntu and Linux Mint, the following should work. 20 | 21 | sudo add-apt-repository ppa:mc3man/trusty-media 22 | sudo apt-get update 23 | sudo apt-get install ffmpeg 24 | """ 25 | 26 | from Cell2D import Cell2D, Cell2DViewer 27 | from scipy.signal import correlate2d 28 | 29 | 30 | class Life(Cell2D): 31 | """Implementation of Conway's Game of Life.""" 32 | kernel = np.array([[1, 1, 1], 33 | [1,10, 1], 34 | [1, 1, 1]]) 35 | 36 | table = np.zeros(20, dtype=np.uint8) 37 | table[[3, 12, 13]] = 1 38 | 39 | def step(self): 40 | """Executes one time step.""" 41 | c = correlate2d(self.array, self.kernel, mode='same') 42 | self.array = self.table[c] 43 | 44 | 45 | class LifeViewer(Cell2DViewer): 46 | """Viewer for Game of Life.""" 47 | 48 | 49 | def main(script, *args): 50 | """Constructs a puffer train. 51 | 52 | Uses the entities in this file: 53 | http://www.radicaleye.com/lifepage/patterns/puftrain.lif 54 | """ 55 | lwss = [ 56 | '0001', 57 | '00001', 58 | '10001', 59 | '01111' 60 | ] 61 | 62 | bhep = [ 63 | '1', 64 | '011', 65 | '001', 66 | '001', 67 | '01' 68 | ] 69 | 70 | n = 400 71 | m = 600 72 | life = Life(n, m) 73 | 74 | col = 120 75 | life.add_cells(n//2+12, col, *lwss) 76 | life.add_cells(n//2+26, col, *lwss) 77 | life.add_cells(n//2+19, col, *bhep) 78 | viewer = LifeViewer(life) 79 | anim = viewer.animate(frames=100, interval=1) 80 | plt.subplots_adjust(left=0.01, right=0.99, bottom=0.01, top=0.99) 81 | plt.show() 82 | 83 | 84 | if __name__ == '__main__': 85 | main(*sys.argv) 86 | 87 | 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Complexity Science Tutorial 2 | 3 | Allen Downey 4 | 5 | This is the repository for a tutorial on Complexity Science. 6 | 7 | The material is based on my book, 8 | [*Think Complexity*](http://greenteapress.com/wp/think-complexity-2e/), 9 | 2nd edition, and a class I teach at Olin College. 10 | 11 | ### Installation instructions 12 | 13 | Please try to install everything you need for this tutorial ahead of time! 14 | You have two options: 15 | 16 | 1. Install Jupyter and the other libraries on your laptop and download my code from GitHub. 17 | 18 | 2. Run the Jupyter notebooks on a virtual machine on Binder. 19 | 20 | I'll provide instructions for both. 21 | 22 | 23 | ### Option 1 24 | 25 | Code for this workshop is in a Git repository on Github. 26 | If you have a Git client installed, you should be able to download it by running: 27 | 28 | ``` 29 | git clone --depth 1 https://github.com/AllenDowney/ComplexityScience 30 | ``` 31 | 32 | Otherwise you can download the repository in [this zip file](https://github.com/AllenDowney/ComplexityScience/archive/master.zip). 33 | If you unzip it, you should get a directory named `Complexity Science`. 34 | 35 | To run the code, you need Python 3 and the following libraries: 36 | 37 | ``` 38 | - jupyter 39 | - numpy 40 | - matplotlib 41 | - seaborn 42 | - pandas 43 | - scipy 44 | - networkx=2.* 45 | - ffmpeg 46 | - empyrical-dist 47 | ``` 48 | 49 | I highly recommend installing Anaconda, which is a Python distribution that makes it 50 | easy to install these libraries. It works on Windows, Mac, and Linux, and because it does a 51 | user-level install, it will not interfere with other Python installations. 52 | 53 | [Information about installing Anaconda is here](https://www.anaconda.com/distribution/). 54 | 55 | After installing Anaconda, you can create an environment that contains the libraries for 56 | this tutorial: 57 | 58 | ``` 59 | cd ComplexityScience 60 | conda env create -f environment.yml 61 | conda activate ComplexityScience 62 | ``` 63 | 64 | If you don't want to create an environment, you can install the libraries you need in 65 | the "base" environment 66 | 67 | ``` 68 | conda install jupyter numpy matplotlib seaborn pandas scipy networkx ffmpeg pip 69 | pip install empyrical-dist 70 | ``` 71 | 72 | To start Jupyter, run: 73 | 74 | ``` 75 | cd ComplexityScience 76 | jupyter notebook 77 | ``` 78 | 79 | Jupyter should launch your default browser or open a tab in an existing browser window. 80 | If not, the Jupyter server should print a URL you can use. For example, when I launch Jupyter, I get 81 | 82 | ``` 83 | ~/ComplexityScience$ jupyter notebook 84 | [I 10:03:20.115 NotebookApp] Serving notebooks from local directory: /home/downey/ComplexityScience 85 | [I 10:03:20.115 NotebookApp] 0 active kernels 86 | [I 10:03:20.115 NotebookApp] The Jupyter Notebook is running at: http://localhost:8888/ 87 | ``` 88 | 89 | In this case, the URL is [http://localhost:8888](http://localhost:8888). 90 | When you start your server, you might get a different URL. 91 | Whatever it is, if you paste it into a browser, you should should see a home page with a list of the 92 | notebooks in the repository. 93 | 94 | Click on `01_workshop.ipynb`. It should open the first notebook for the tutorial. 95 | 96 | Select the cell with the import statements and press "Shift-Enter" to run the code in the cell. 97 | If it works and you get no error messages, **you are all set**. 98 | 99 | If you get error messages about missing packages, you can install the packages you need. 100 | 101 | 102 | ### Option 2 103 | 104 | You can run my notebooks in a virtual machine on Binder. To launch the VM, press this button: 105 | 106 | [![Binder](http://mybinder.org/badge.svg)](http://mybinder.org:/repo/allendowney/ComplexityScience) 107 | 108 | You should see a home page with a list of the files in the repository. 109 | 110 | If you want to try the exercises, open `01_workshop.ipynb`. 111 | You should be able to run the notebooks in your browser and try out the examples. 112 | 113 | However, be aware that the virtual machine you are running is temporary. 114 | If you leave it idle for more than an hour or so, it will disappear along with any work you have done. 115 | 116 | Special thanks to the generous people who run Binder, which makes it easy to share and reproduce computation. 117 | -------------------------------------------------------------------------------- /Cell2D.py: -------------------------------------------------------------------------------- 1 | """ Code example from Complexity and Computation, a book about 2 | exploring complexity science with Python. Available free from 3 | 4 | http://greenteapress.com/complexity 5 | 6 | Copyright 2016 Allen Downey 7 | MIT License: http://opensource.org/licenses/MIT 8 | """ 9 | from __future__ import print_function, division 10 | 11 | import sys 12 | 13 | import numpy as np 14 | import matplotlib.pyplot as plt 15 | 16 | from matplotlib import animation 17 | from scipy.signal import convolve2d 18 | 19 | """ 20 | For animation to work in the notebook, you might have to install 21 | ffmpeg. On Ubuntu and Linux Mint, the following should work. 22 | 23 | sudo add-apt-repository ppa:mc3man/trusty-media 24 | sudo apt-get update 25 | sudo apt-get install ffmpeg 26 | """ 27 | 28 | class Cell2D: 29 | """Implements Conway's Game of Life.""" 30 | 31 | def __init__(self, n, m=None): 32 | """Initializes the attributes. 33 | 34 | n: number of rows 35 | m: number of columns 36 | """ 37 | m = n if m is None else m 38 | self.array = np.zeros((n, m), np.uint8) 39 | 40 | def add_cells(self, row, col, *strings): 41 | """Adds cells at the given location. 42 | 43 | row: top row index 44 | col: left col index 45 | strings: list of strings of 0s and 1s 46 | """ 47 | for i, s in enumerate(strings): 48 | self.array[row+i, col:col+len(s)] = np.array([int(b) for b in s]) 49 | 50 | def step(self): 51 | """Executes one time step.""" 52 | pass 53 | 54 | 55 | class Cell2DViewer: 56 | """Generates an animated view of an array image.""" 57 | 58 | cmap = plt.get_cmap('Greens') 59 | options = dict(interpolation='nearest', alpha=0.8, 60 | vmin=0, vmax=1, origin='upper') 61 | 62 | def __init__(self, viewee): 63 | self.viewee = viewee 64 | self.im = None 65 | self.hlines = None 66 | self.vlines = None 67 | 68 | # TODO: should this really take iters? 69 | def step(self, iters=1): 70 | """Advances the viewee the given number of steps.""" 71 | for i in range(iters): 72 | self.viewee.step() 73 | 74 | def draw(self, grid=False): 75 | """Draws the array and any other elements. 76 | 77 | grid: boolean, whether to draw grid lines 78 | """ 79 | self.draw_array(self.viewee.array) 80 | if grid: 81 | self.draw_grid() 82 | 83 | def draw_array(self, array=None, cmap=None, **kwds): 84 | """Draws the cells.""" 85 | # Note: we have to make a copy because some implementations 86 | # of step perform updates in place. 87 | if array is None: 88 | array = self.viewee.array 89 | a = array.copy() 90 | cmap = self.cmap if cmap is None else cmap 91 | 92 | n, m = a.shape 93 | plt.axis([0, m, 0, n]) 94 | plt.xticks([]) 95 | plt.yticks([]) 96 | 97 | options = self.options.copy() 98 | options['extent'] = [0, m, 0, n] 99 | options.update(kwds) 100 | self.im = plt.imshow(a, cmap, **options) 101 | 102 | def draw_grid(self): 103 | """Draws the grid.""" 104 | a = self.viewee.array 105 | n, m = a.shape 106 | lw = 2 if m < 7 else 1 107 | options = dict(color='white', linewidth=lw) 108 | 109 | # the shift is a hack to get the grid to line up with the cells 110 | shift = 0.005 * n 111 | rows = np.arange(n) + shift 112 | self.hlines = plt.hlines(rows, 0, m, **options) 113 | 114 | cols = np.arange(m) 115 | self.vlines = plt.vlines(cols, 0, n, **options) 116 | 117 | def animate(self, frames=20, interval=200, grid=False): 118 | """Creates an animation. 119 | 120 | frames: number of frames to draw 121 | interval: time between frames in ms 122 | """ 123 | fig = plt.gcf() 124 | self.draw(grid) 125 | anim = animation.FuncAnimation(fig, self.animate_func, 126 | init_func=self.init_func, 127 | frames=frames, interval=interval) 128 | return anim 129 | 130 | def init_func(self): 131 | """Called at the beginning of an animation.""" 132 | pass 133 | 134 | def animate_func(self, i): 135 | """Draws one frame of the animation.""" 136 | if i > 0: 137 | self.step() 138 | a = self.viewee.array 139 | self.im.set_array(a) 140 | return (self.im,) 141 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import matplotlib.pyplot as plt 4 | 5 | import re 6 | 7 | class FixedWidthVariables(object): 8 | """Represents a set of variables in a fixed width file.""" 9 | 10 | def __init__(self, variables, index_base=0): 11 | """Initializes. 12 | 13 | variables: DataFrame 14 | index_base: are the indices 0 or 1 based? 15 | 16 | Attributes: 17 | colspecs: list of (start, end) index tuples 18 | names: list of string variable names 19 | """ 20 | self.variables = variables 21 | 22 | # note: by default, subtract 1 from colspecs 23 | self.colspecs = variables[['start', 'end']] - index_base 24 | 25 | # convert colspecs to a list of pair of int 26 | self.colspecs = self.colspecs.astype(np.int).values.tolist() 27 | self.names = variables['name'] 28 | 29 | def read_fixed_width(self, filename, **options): 30 | """Reads a fixed width ASCII file. 31 | 32 | filename: string filename 33 | 34 | returns: DataFrame 35 | """ 36 | df = pd.read_fwf(filename, 37 | colspecs=self.colspecs, 38 | names=self.names, 39 | **options) 40 | return df 41 | 42 | 43 | def read_stata_dict(dct_file, **options): 44 | """Reads a Stata dictionary file. 45 | 46 | dct_file: string filename 47 | options: dict of options passed to open() 48 | 49 | returns: FixedWidthVariables object 50 | """ 51 | type_map = dict(byte=int, int=int, long=int, float=float, 52 | double=float, numeric=float) 53 | 54 | var_info = [] 55 | with open(dct_file, **options) as f: 56 | for line in f: 57 | match = re.search( r'_column\(([^)]*)\)', line) 58 | if not match: 59 | continue 60 | start = int(match.group(1)) 61 | t = line.split() 62 | vtype, name, fstring = t[1:4] 63 | #name = name.lower() 64 | if vtype.startswith('str'): 65 | vtype = str 66 | else: 67 | vtype = type_map[vtype] 68 | long_desc = ' '.join(t[4:]).strip('"') 69 | var_info.append((start, vtype, name, fstring, long_desc)) 70 | 71 | columns = ['start', 'type', 'name', 'fstring', 'desc'] 72 | variables = pd.DataFrame(var_info, columns=columns) 73 | 74 | # fill in the end column by shifting the start column 75 | variables['end'] = variables.start.shift(-1) 76 | variables.loc[len(variables)-1, 'end'] = 0 77 | 78 | dct = FixedWidthVariables(variables, index_base=1) 79 | return dct 80 | 81 | 82 | def read_stata(dct_name, dat_name, **options): 83 | """Reads Stata files from the given directory. 84 | 85 | dirname: string 86 | 87 | returns: DataFrame 88 | """ 89 | dct = read_stata_dict(dct_name) 90 | df = dct.read_fixed_width(dat_name, **options) 91 | return df 92 | 93 | 94 | def sample_rows(df, nrows, replace=False): 95 | """Choose a sample of rows from a DataFrame. 96 | 97 | df: DataFrame 98 | nrows: number of rows 99 | replace: whether to sample with replacement 100 | 101 | returns: DataDf 102 | """ 103 | indices = np.random.choice(df.index, nrows, replace=replace) 104 | sample = df.loc[indices] 105 | return sample 106 | 107 | 108 | def resample_rows(df): 109 | """Resamples rows from a DataFrame. 110 | 111 | df: DataFrame 112 | 113 | returns: DataFrame 114 | """ 115 | return sample_rows(df, len(df), replace=True) 116 | 117 | 118 | def resample_rows_weighted(df, column='finalwgt'): 119 | """Resamples a DataFrame using probabilities proportional to given column. 120 | 121 | df: DataFrame 122 | column: string column name to use as weights 123 | 124 | returns: DataFrame 125 | """ 126 | weights = df[column].copy() 127 | weights /= sum(weights) 128 | indices = np.random.choice(df.index, len(df), replace=True, p=weights) 129 | sample = df.loc[indices] 130 | return sample 131 | 132 | 133 | def resample_by_year(df, column='wtssall'): 134 | """Resample rows within each year. 135 | 136 | df: DataFrame 137 | column: string name of weight variable 138 | 139 | returns DataFrame 140 | """ 141 | grouped = df.groupby('year') 142 | samples = [resample_rows_weighted(group, column) 143 | for _, group in grouped] 144 | sample = pd.concat(samples, ignore_index=True) 145 | return sample 146 | 147 | 148 | def values(df, varname): 149 | """Values and counts in index order. 150 | 151 | df: DataFrame 152 | varname: strign column name 153 | 154 | returns: Series that maps from value to frequency 155 | """ 156 | return df[varname].value_counts().sort_index() 157 | 158 | 159 | def fill_missing(df, varname, badvals=[98, 99]): 160 | """Fill missing data with random values. 161 | 162 | df: DataFrame 163 | varname: string column name 164 | badvals: list of values to be replaced 165 | """ 166 | # replace badvals with NaN 167 | df[varname].replace(badvals, np.nan, inplace=True) 168 | 169 | # get the index of rows missing varname 170 | null = df[varname].isnull() 171 | n_missing = sum(null) 172 | 173 | # choose a random sample from the non-missing values 174 | fill = np.random.choice(df[varname].dropna(), n_missing, replace=True) 175 | 176 | # replace missing data with the samples 177 | df.loc[null, varname] = fill 178 | 179 | # return the number of missing values replaced 180 | return n_missing 181 | 182 | 183 | def round_into_bins(df, var, bin_width, high=None, low=0): 184 | """Rounds values down to the bin they belong in. 185 | 186 | df: DataFrame 187 | var: string variable name 188 | bin_width: number, width of the bins 189 | 190 | returns: array of bin values 191 | """ 192 | if high is None: 193 | high = df[var].max() 194 | 195 | bins = np.arange(low, high+bin_width, bin_width) 196 | indices = np.digitize(df[var], bins) 197 | return bins[indices-1] 198 | 199 | 200 | def underride(d, **options): 201 | """Add key-value pairs to d only if key is not in d. 202 | 203 | d: dictionary 204 | options: keyword args to add to d 205 | """ 206 | for key, val in options.items(): 207 | d.setdefault(key, val) 208 | 209 | return d 210 | 211 | 212 | def decorate(**options): 213 | """Decorate the current axes. 214 | Call decorate with keyword arguments like 215 | decorate(title='Title', 216 | xlabel='x', 217 | ylabel='y') 218 | The keyword arguments can be any of the axis properties 219 | https://matplotlib.org/api/axes_api.html 220 | In addition, you can use `legend=False` to suppress the legend. 221 | And you can use `loc` to indicate the location of the legend 222 | (the default value is 'best') 223 | """ 224 | loc = options.pop('loc', 'best') 225 | if options.pop('legend', True): 226 | legend(loc=loc) 227 | 228 | plt.gca().set(**options) 229 | plt.tight_layout() 230 | 231 | 232 | def legend(**options): 233 | """Draws a legend only if there is at least one labeled item. 234 | options are passed to plt.legend() 235 | https://matplotlib.org/api/_as_gen/matplotlib.pyplot.legend.html 236 | """ 237 | underride(options, loc='best') 238 | 239 | ax = plt.gca() 240 | handles, labels = ax.get_legend_handles_labels() 241 | if handles: 242 | ax.legend(handles, labels, **options) 243 | -------------------------------------------------------------------------------- /02_workshop.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Game of Life\n", 8 | "\n", 9 | "Code examples from [Think Complexity, 2nd edition](https://thinkcomplex.com).\n", 10 | "\n", 11 | "Copyright 2019 Allen Downey, [MIT License](http://opensource.org/licenses/MIT)" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import numpy as np\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "import seaborn as sns" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "The following class implements the Game of Life." 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 2, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "from scipy.signal import correlate2d\n", 39 | "from time import sleep\n", 40 | "from IPython.display import clear_output\n", 41 | "\n", 42 | "class Life:\n", 43 | " \"\"\"Implementation of Conway's Game of Life.\"\"\"\n", 44 | " kernel = np.array([[1, 1, 1],\n", 45 | " [1,10, 1],\n", 46 | " [1, 1, 1]])\n", 47 | "\n", 48 | " table = np.zeros(20, dtype=np.uint8)\n", 49 | " table[[3, 12, 13]] = 1\n", 50 | "\n", 51 | " def __init__(self, n, m=None):\n", 52 | " \"\"\"Initializes the attributes.\n", 53 | "\n", 54 | " n: number of rows\n", 55 | " m: number of columns\n", 56 | " \"\"\"\n", 57 | " m = n if m is None else m\n", 58 | " self.grid = np.zeros((n, m), np.uint8)\n", 59 | "\n", 60 | " def add_cells(self, row, col, *strings):\n", 61 | " \"\"\"Adds cells at the given location.\n", 62 | "\n", 63 | " row: top row index\n", 64 | " col: left col index\n", 65 | " strings: list of strings of 0s and 1s\n", 66 | " \"\"\"\n", 67 | " for i, s in enumerate(strings):\n", 68 | " self.grid[row+i, col:col+len(s)] = np.array([int(b) for b in s])\n", 69 | " \n", 70 | " def step(self):\n", 71 | " \"\"\"Executes one time step.\"\"\"\n", 72 | " c = correlate2d(self.grid, self.kernel, mode='same')\n", 73 | " self.grid = self.table[c]\n", 74 | " \n", 75 | " def draw(self):\n", 76 | " \"\"\"Draws the cells.\"\"\"\n", 77 | " \n", 78 | " a = self.grid\n", 79 | "\n", 80 | " n, m = a.shape\n", 81 | " plt.axis([0, m, 0, n])\n", 82 | " plt.xticks([])\n", 83 | " plt.yticks([])\n", 84 | "\n", 85 | " cmap = plt.get_cmap('Greens')\n", 86 | " options = dict(interpolation='nearest', alpha=0.8,\n", 87 | " vmin=0, vmax=1, origin='upper')\n", 88 | " options['extent'] = [0, m, 0, n]\n", 89 | " \n", 90 | " self.im = plt.imshow(a, cmap, **options)\n", 91 | "\n", 92 | " def animate(self, frames, interval=None):\n", 93 | " \"\"\"Animate the automaton.\n", 94 | "\n", 95 | " frames: number of frames to draw\n", 96 | " interval: time between frames in seconds\n", 97 | " \"\"\"\n", 98 | " plt.figure()\n", 99 | " try:\n", 100 | " for i in range(frames-1):\n", 101 | " self.draw()\n", 102 | " plt.show()\n", 103 | " if interval:\n", 104 | " sleep(interval)\n", 105 | " self.step()\n", 106 | " clear_output(wait=True)\n", 107 | " self.draw()\n", 108 | " plt.show()\n", 109 | " except KeyboardInterrupt:\n", 110 | " pass" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "metadata": {}, 116 | "source": [ 117 | "The following function creates a `Life` object and sets the initial condition using strings of `0` and `1` characters." 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 3, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "def make_life(n, m, row, col, *strings):\n", 127 | " \"\"\"Makes a Life object.\n", 128 | " \n", 129 | " n, m: rows and columns of the Life array\n", 130 | " row, col: upper left coordinate of the cells to be added\n", 131 | " strings: list of strings of '0' and '1'\n", 132 | " \"\"\"\n", 133 | " life = Life(n, m)\n", 134 | " life.add_cells(row, col, *strings)\n", 135 | " return life" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": {}, 141 | "source": [ 142 | "## Game of Life entities" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "Under GoL rules, some configurations are stable, meaning that they don't change from one time step to the next. These patterns are sometimes called \"still lifes\".\n", 150 | "\n", 151 | "One example is a \"beehive\", which is the following pattern of 6 cells." 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 4, 157 | "metadata": {}, 158 | "outputs": [], 159 | "source": [ 160 | "# beehive\n", 161 | "life = make_life(3, 4, 0, 0, '0110', '1001', '0110')\n", 162 | "life.draw()" 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "metadata": {}, 168 | "source": [ 169 | "Here's what it looks like after one step:" 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 5, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "life.step()\n", 179 | "life.draw()" 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "metadata": {}, 185 | "source": [ 186 | "Some patterns, called \"oscillators\", cycle through a series of configurations that returns to the initial configuration and repeats.\n", 187 | "\n", 188 | "A \"toad\" is an oscillator with period 2. Here's are its two configurations:" 189 | ] 190 | }, 191 | { 192 | "cell_type": "code", 193 | "execution_count": 6, 194 | "metadata": {}, 195 | "outputs": [], 196 | "source": [ 197 | "# toad\n", 198 | "plt.subplot(1, 2, 1)\n", 199 | "life = make_life(4, 4, 1, 0, '0111', '1110')\n", 200 | "life.draw()\n", 201 | "\n", 202 | "plt.subplot(1, 2, 2)\n", 203 | "life.step()\n", 204 | "life.draw()" 205 | ] 206 | }, 207 | { 208 | "cell_type": "markdown", 209 | "metadata": {}, 210 | "source": [ 211 | "Here's what it looks like as an animation." 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": 7, 217 | "metadata": {}, 218 | "outputs": [], 219 | "source": [ 220 | "life.step()\n", 221 | "life.animate(frames=4, interval=0.5)" 222 | ] 223 | }, 224 | { 225 | "cell_type": "markdown", 226 | "metadata": {}, 227 | "source": [ 228 | "Some patterns repeat, but after each cycle, they are offset in space. They are called \"spaceships\".\n", 229 | "\n", 230 | "A \"glider\" is a spaceship that translates one unit down and to the right with period 4. " 231 | ] 232 | }, 233 | { 234 | "cell_type": "code", 235 | "execution_count": 8, 236 | "metadata": {}, 237 | "outputs": [], 238 | "source": [ 239 | "# glider\n", 240 | "glider = ['010', '001', '111']\n", 241 | "life = make_life(4, 4, 0, 0, *glider)\n", 242 | "\n", 243 | "plt.figure(figsize=(12,5))\n", 244 | "\n", 245 | "for i in range(1, 6):\n", 246 | " plt.subplot(1, 5, i)\n", 247 | " life.draw()\n", 248 | " life.step()" 249 | ] 250 | }, 251 | { 252 | "cell_type": "markdown", 253 | "metadata": {}, 254 | "source": [ 255 | "Here's an animation showing glider movement." 256 | ] 257 | }, 258 | { 259 | "cell_type": "code", 260 | "execution_count": 9, 261 | "metadata": {}, 262 | "outputs": [], 263 | "source": [ 264 | "life = make_life(10, 10, 0, 0, '010', '001', '111')\n", 265 | "life.animate(frames=32, interval=0.2)" 266 | ] 267 | }, 268 | { 269 | "cell_type": "markdown", 270 | "metadata": {}, 271 | "source": [ 272 | "**Exercise:** If you start GoL from a random configuration, it usually runs chaotically for a while and then settles into stable patterns that include blinkers, blocks, and beehives, ships, boats, and loaves.\n", 273 | "\n", 274 | "For a list of common \"natually\" occurring patterns, see Achim Flammenkamp, \"[Most seen natural occurring ash objects in Game of Life](http://wwwhomes.uni-bielefeld.de/achim/freq_top_life.html)\",\n", 275 | "\n", 276 | "Start GoL in a random state and run it until it stabilizes (try 1000 steps).\n", 277 | "What stable patterns can you identify?\n", 278 | "\n", 279 | "Hint: use `numpy.random.randint`." 280 | ] 281 | }, 282 | { 283 | "cell_type": "code", 284 | "execution_count": 10, 285 | "metadata": {}, 286 | "outputs": [], 287 | "source": [ 288 | "# Solution goes here" 289 | ] 290 | }, 291 | { 292 | "cell_type": "markdown", 293 | "metadata": {}, 294 | "source": [ 295 | "### Methuselas\n", 296 | "\n", 297 | "Most initial conditions run for a short time and reach a steady state. But some initial conditional run for a surprisingly long time; they are called [Methuselahs](https://en.wikipedia.org/wiki/Methuselah_(cellular_automaton))\n", 298 | "\n", 299 | "One of the simplest examples is the \"r-pentomino\", which starts with only five live cells, but it runs for 1103 steps before stabilizing." 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": 11, 305 | "metadata": {}, 306 | "outputs": [], 307 | "source": [ 308 | "# r pentomino\n", 309 | "rpent = ['011', '110', '010']\n", 310 | "life = make_life(3, 3, 0, 0, *rpent)\n", 311 | "life.draw()" 312 | ] 313 | }, 314 | { 315 | "cell_type": "markdown", 316 | "metadata": {}, 317 | "source": [ 318 | "Here are the start and finish configurations." 319 | ] 320 | }, 321 | { 322 | "cell_type": "code", 323 | "execution_count": 12, 324 | "metadata": {}, 325 | "outputs": [], 326 | "source": [ 327 | "# r pentomino\n", 328 | "rpent = ['011', '110', '010']\n", 329 | "\n", 330 | "plt.subplot(1, 2, 1)\n", 331 | "life = make_life(120, 120, 50, 45, *rpent)\n", 332 | "life.draw()\n", 333 | "\n", 334 | "for i in range(1103):\n", 335 | " life.step()\n", 336 | "\n", 337 | "plt.subplot(1, 2, 2)\n", 338 | "life.draw()" 339 | ] 340 | }, 341 | { 342 | "cell_type": "markdown", 343 | "metadata": {}, 344 | "source": [ 345 | "And here's the animation that shows the steps." 346 | ] 347 | }, 348 | { 349 | "cell_type": "code", 350 | "execution_count": 13, 351 | "metadata": {}, 352 | "outputs": [], 353 | "source": [ 354 | "life = make_life(120, 120, 50, 45, *rpent)\n", 355 | "life.animate(frames=1200)" 356 | ] 357 | }, 358 | { 359 | "cell_type": "markdown", 360 | "metadata": {}, 361 | "source": [ 362 | "### Rabbits\n", 363 | "\n", 364 | "Another example is [rabbits](https://web.archive.org/web/20081221152607/http://www.argentum.freeserve.co.uk/lex_r.htm#rabbits), which starts with only nine cells and runs 17331 steps before reaching steady state." 365 | ] 366 | }, 367 | { 368 | "cell_type": "code", 369 | "execution_count": 19, 370 | "metadata": {}, 371 | "outputs": [], 372 | "source": [ 373 | "from os.path import basename, exists\n", 374 | "\n", 375 | "def download(url):\n", 376 | " filename = basename(url)\n", 377 | " if not exists(filename):\n", 378 | " from urllib.request import urlretrieve\n", 379 | " local, _ = urlretrieve(url, filename)\n", 380 | " print('Downloaded ' + local)\n", 381 | " \n", 382 | "download('https://github.com/AllenDowney/ComplexityScience/raw/master/Cell2D.py')\n", 383 | "download('https://github.com/AllenDowney/ComplexityScience/raw/master/Life.py')\n", 384 | "download('https://github.com/AllenDowney/ComplexityScience/raw/master/LifeRabbits.py')" 385 | ] 386 | }, 387 | { 388 | "cell_type": "markdown", 389 | "metadata": {}, 390 | "source": [ 391 | "To run my implementation of rabbits, open a terminal and run\n", 392 | "\n", 393 | "```\n", 394 | "python LifeRabbits.py\n", 395 | "```" 396 | ] 397 | }, 398 | { 399 | "cell_type": "markdown", 400 | "metadata": {}, 401 | "source": [ 402 | "### Conway's conjecture\n", 403 | "\n", 404 | "Patterns like these prompted Conway to conjecture, as a challenge, that there are no initial conditions where the number of live cells grows unboundedly.\n", 405 | "\n", 406 | "Gosper's glider gun was the first entity to be discovered that produces an unbounded number of live cells, which refutes Conway's conjecture." 407 | ] 408 | }, 409 | { 410 | "cell_type": "code", 411 | "execution_count": 27, 412 | "metadata": {}, 413 | "outputs": [], 414 | "source": [ 415 | "glider_gun = [\n", 416 | " '000000000000000000000000100000000000',\n", 417 | " '000000000000000000000010100000000000',\n", 418 | " '000000000000110000001100000000000011',\n", 419 | " '000000000001000100001100000000000011',\n", 420 | " '110000000010000010001100000000000000',\n", 421 | " '110000000010001011000010100000000000',\n", 422 | " '000000000010000010000000100000000000',\n", 423 | " '000000000001000100000000000000000000',\n", 424 | " '000000000000110000000000000000000000'\n", 425 | "]" 426 | ] 427 | }, 428 | { 429 | "cell_type": "markdown", 430 | "metadata": {}, 431 | "source": [ 432 | "Here's the initial configuration:" 433 | ] 434 | }, 435 | { 436 | "cell_type": "code", 437 | "execution_count": 28, 438 | "metadata": {}, 439 | "outputs": [], 440 | "source": [ 441 | "life = make_life(11, 38, 1, 1, *glider_gun)\n", 442 | "life.draw()" 443 | ] 444 | }, 445 | { 446 | "cell_type": "markdown", 447 | "metadata": {}, 448 | "source": [ 449 | "And here's what it looks like running:" 450 | ] 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": 30, 455 | "metadata": {}, 456 | "outputs": [], 457 | "source": [ 458 | "life = make_life(50, 50, 2, 2, *glider_gun)\n", 459 | "life.animate(frames=500)" 460 | ] 461 | }, 462 | { 463 | "cell_type": "markdown", 464 | "metadata": {}, 465 | "source": [ 466 | "**Exercise:** In this animation, you might notice that the boundary interferes with the escaping gliders, but it doesn't affect the behavior of the gun.\n", 467 | "\n", 468 | "For fun, change `step` so it passes the keywords `boundary='wrap'` to `correlate2d`, and see what happens.\n", 469 | "\n", 470 | "Don't forget to remove it before you proceed." 471 | ] 472 | }, 473 | { 474 | "cell_type": "markdown", 475 | "metadata": {}, 476 | "source": [ 477 | "### Puffer train\n", 478 | "\n", 479 | "Another way to \"refute\" Conway's conjecture is a [puffer train](https://en.wikipedia.org/wiki/Puffer_train)." 480 | ] 481 | }, 482 | { 483 | "cell_type": "code", 484 | "execution_count": null, 485 | "metadata": {}, 486 | "outputs": [], 487 | "source": [ 488 | "download('https://github.com/AllenDowney/ComplexityScience/raw/master/LifePuffer.py')" 489 | ] 490 | }, 491 | { 492 | "cell_type": "markdown", 493 | "metadata": {}, 494 | "source": [ 495 | "To see a puffer train run, open a terminal and run\n", 496 | "\n", 497 | "```\n", 498 | "python LifePuffer.py\n", 499 | "```" 500 | ] 501 | }, 502 | { 503 | "cell_type": "markdown", 504 | "metadata": {}, 505 | "source": [ 506 | "### Implementing Game of Life\n", 507 | "\n", 508 | "This section explains how the implementaion we've been using works.\n", 509 | "\n", 510 | "As an example, I'll start with an array of random cells:" 511 | ] 512 | }, 513 | { 514 | "cell_type": "code", 515 | "execution_count": 31, 516 | "metadata": {}, 517 | "outputs": [], 518 | "source": [ 519 | "a = np.random.randint(2, size=(10, 10), dtype=np.uint8)\n", 520 | "print(a)" 521 | ] 522 | }, 523 | { 524 | "cell_type": "markdown", 525 | "metadata": {}, 526 | "source": [ 527 | "The following is a straightforward translation of the GoL rules using `for` loops and array slicing." 528 | ] 529 | }, 530 | { 531 | "cell_type": "code", 532 | "execution_count": 32, 533 | "metadata": {}, 534 | "outputs": [], 535 | "source": [ 536 | "b = np.zeros_like(a)\n", 537 | "rows, cols = a.shape\n", 538 | "for i in range(1, rows-1):\n", 539 | " for j in range(1, cols-1):\n", 540 | " state = a[i, j]\n", 541 | " neighbors = a[i-1:i+2, j-1:j+2]\n", 542 | " k = np.sum(neighbors) - state\n", 543 | " if state:\n", 544 | " if k==2 or k==3:\n", 545 | " b[i, j] = 1\n", 546 | " else:\n", 547 | " if k == 3:\n", 548 | " b[i, j] = 1\n", 549 | "\n", 550 | "print(b)" 551 | ] 552 | }, 553 | { 554 | "cell_type": "markdown", 555 | "metadata": {}, 556 | "source": [ 557 | "Here's a smaller, faster version using cross correlation." 558 | ] 559 | }, 560 | { 561 | "cell_type": "code", 562 | "execution_count": 33, 563 | "metadata": {}, 564 | "outputs": [], 565 | "source": [ 566 | "from scipy.signal import correlate2d\n", 567 | "\n", 568 | "kernel = np.array([[1, 1, 1],\n", 569 | " [1, 0, 1],\n", 570 | " [1, 1, 1]])\n", 571 | "\n", 572 | "c = correlate2d(a, kernel, mode='same')\n", 573 | "b = (c==3) | (c==2) & a\n", 574 | "b = b.astype(np.uint8)\n", 575 | "print(b)" 576 | ] 577 | }, 578 | { 579 | "cell_type": "markdown", 580 | "metadata": {}, 581 | "source": [ 582 | "Using a kernel that gives a weight of 10 to the center cell, we can simplify the logic a little." 583 | ] 584 | }, 585 | { 586 | "cell_type": "code", 587 | "execution_count": 34, 588 | "metadata": {}, 589 | "outputs": [], 590 | "source": [ 591 | "kernel = np.array([[1, 1, 1],\n", 592 | " [1,10, 1],\n", 593 | " [1, 1, 1]])\n", 594 | "\n", 595 | "c = correlate2d(a, kernel, mode='same')\n", 596 | "b = (c==3) | (c==12) | (c==13)\n", 597 | "b = b.astype(np.uint8)\n", 598 | "print(b)" 599 | ] 600 | }, 601 | { 602 | "cell_type": "markdown", 603 | "metadata": {}, 604 | "source": [ 605 | "More importantly, the second version of the kernel makes it possible to use a look up table to get the next state, which is faster and even more concise." 606 | ] 607 | }, 608 | { 609 | "cell_type": "code", 610 | "execution_count": 35, 611 | "metadata": {}, 612 | "outputs": [], 613 | "source": [ 614 | "table = np.zeros(20, dtype=np.uint8)\n", 615 | "table[[3, 12, 13]] = 1\n", 616 | "c = correlate2d(a, kernel, mode='same')\n", 617 | "b = table[c]\n", 618 | "print(b)" 619 | ] 620 | }, 621 | { 622 | "cell_type": "markdown", 623 | "metadata": {}, 624 | "source": [ 625 | "### Highlife\n", 626 | "\n", 627 | "One variation of GoL, called \"Highlife\", has the\n", 628 | "same rules as GoL, plus one additional rule: a dead cell with 6\n", 629 | "neighbors comes to life.\n", 630 | "\n", 631 | "You can try out different rules by inheriting from `Life` and changing the lookup table.\n", 632 | "\n", 633 | "**Exercise:** Modify the table below to add the new rule." 634 | ] 635 | }, 636 | { 637 | "cell_type": "code", 638 | "execution_count": 36, 639 | "metadata": {}, 640 | "outputs": [], 641 | "source": [ 642 | "# Starter code\n", 643 | "\n", 644 | "class MyLife(Life):\n", 645 | " \"\"\"Implementation of Life.\"\"\"\n", 646 | "\n", 647 | " table = np.zeros(20, dtype=np.uint8)\n", 648 | " table[[3, 12, 13]] = 1" 649 | ] 650 | }, 651 | { 652 | "cell_type": "markdown", 653 | "metadata": {}, 654 | "source": [ 655 | "One of the more interesting patterns in Highlife is the replicator, which has the following initial configuration.\n" 656 | ] 657 | }, 658 | { 659 | "cell_type": "code", 660 | "execution_count": 37, 661 | "metadata": {}, 662 | "outputs": [], 663 | "source": [ 664 | "replicator = [\n", 665 | " '00111',\n", 666 | " '01001',\n", 667 | " '10001',\n", 668 | " '10010',\n", 669 | " '11100'\n", 670 | "]" 671 | ] 672 | }, 673 | { 674 | "cell_type": "markdown", 675 | "metadata": {}, 676 | "source": [ 677 | "Make a `MyLife` object with `n=100` and use `add_cells` to put a replicator near the middle.\n", 678 | "\n", 679 | "Make an animation with about 200 frames and see how it behaves." 680 | ] 681 | }, 682 | { 683 | "cell_type": "code", 684 | "execution_count": 38, 685 | "metadata": {}, 686 | "outputs": [], 687 | "source": [ 688 | "# Solution goes here" 689 | ] 690 | }, 691 | { 692 | "cell_type": "markdown", 693 | "metadata": {}, 694 | "source": [ 695 | "**Exercise:** Try out some other rules and see what kind of behavior you get." 696 | ] 697 | }, 698 | { 699 | "cell_type": "code", 700 | "execution_count": null, 701 | "metadata": {}, 702 | "outputs": [], 703 | "source": [] 704 | } 705 | ], 706 | "metadata": { 707 | "kernelspec": { 708 | "display_name": "Python 3 (ipykernel)", 709 | "language": "python", 710 | "name": "python3" 711 | }, 712 | "language_info": { 713 | "codemirror_mode": { 714 | "name": "ipython", 715 | "version": 3 716 | }, 717 | "file_extension": ".py", 718 | "mimetype": "text/x-python", 719 | "name": "python", 720 | "nbconvert_exporter": "python", 721 | "pygments_lexer": "ipython3", 722 | "version": "3.7.11" 723 | } 724 | }, 725 | "nbformat": 4, 726 | "nbformat_minor": 2 727 | } 728 | -------------------------------------------------------------------------------- /03_workshop.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Scale-Free Networks\n", 8 | "\n", 9 | "Code examples from [Think Complexity, 2nd edition](https://thinkcomplex.com).\n", 10 | "\n", 11 | "Copyright 2016 Allen Downey, [MIT License](http://opensource.org/licenses/MIT)" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import numpy as np\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "import seaborn as sns\n", 23 | "import networkx as nx" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": 2, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "# If we're running on Colab, install empiricaldist\n", 33 | "import sys\n", 34 | "IN_COLAB = 'google.colab' in sys.modules\n", 35 | "\n", 36 | "if IN_COLAB:\n", 37 | " !pip install empiricaldist" 38 | ] 39 | }, 40 | { 41 | "cell_type": "code", 42 | "execution_count": 3, 43 | "metadata": {}, 44 | "outputs": [], 45 | "source": [ 46 | "from empiricaldist import Pmf, Cdf" 47 | ] 48 | }, 49 | { 50 | "cell_type": "code", 51 | "execution_count": 4, 52 | "metadata": {}, 53 | "outputs": [], 54 | "source": [ 55 | "def decorate(**options):\n", 56 | " plt.gca().set(**options)\n", 57 | " plt.tight_layout()" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": 5, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "#from warnings import simplefilter\n", 67 | "#from matplotlib.cbook import mplDeprecation\n", 68 | "#simplefilter('ignore', mplDeprecation)" 69 | ] 70 | }, 71 | { 72 | "cell_type": "markdown", 73 | "metadata": { 74 | "collapsed": true 75 | }, 76 | "source": [ 77 | "## Graphs" 78 | ] 79 | }, 80 | { 81 | "cell_type": "markdown", 82 | "metadata": {}, 83 | "source": [ 84 | "To represent social networks, we'll use `nx.Graph`, the graph representation provided by NetworkX.\n", 85 | "\n", 86 | "Each person is represented by a node. Each friendship is represented by an edge between two nodes.\n", 87 | "\n", 88 | "Here's a simple example with 4 people:" 89 | ] 90 | }, 91 | { 92 | "cell_type": "code", 93 | "execution_count": 6, 94 | "metadata": {}, 95 | "outputs": [], 96 | "source": [ 97 | "G = nx.Graph()\n", 98 | "G.add_edge(1, 0)\n", 99 | "G.add_edge(2, 0)\n", 100 | "G.add_edge(3, 0)\n", 101 | "nx.draw(G)" 102 | ] 103 | }, 104 | { 105 | "cell_type": "markdown", 106 | "metadata": {}, 107 | "source": [ 108 | "The number of friends a person has is the number of edges that connect to their node, which is the \"degree\" of the node." 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": 7, 114 | "metadata": {}, 115 | "outputs": [], 116 | "source": [ 117 | "for node in G.nodes():\n", 118 | " print(node, G.degree(node))" 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "We are often intereted in the \"degree distribution\" of a graph, which is the number of people who have 0 friends, the number who have 1 friend, and so on.\n", 126 | "\n", 127 | "The following function extracts a list of degrees, one for each node in a graph." 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": 8, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "def degrees(G):\n", 137 | " \"\"\"List of degrees for nodes in `G`.\n", 138 | " \n", 139 | " G: Graph object\n", 140 | " \n", 141 | " returns: list of int\n", 142 | " \"\"\"\n", 143 | " return [G.degree(node) for node in G]" 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "metadata": {}, 149 | "source": [ 150 | "Here's the result for the small example." 151 | ] 152 | }, 153 | { 154 | "cell_type": "code", 155 | "execution_count": 9, 156 | "metadata": {}, 157 | "outputs": [], 158 | "source": [ 159 | "degrees(G)" 160 | ] 161 | }, 162 | { 163 | "cell_type": "markdown", 164 | "metadata": {}, 165 | "source": [ 166 | "I'll use `Pmf` from `empiricaldist` to make a probability mass function." 167 | ] 168 | }, 169 | { 170 | "cell_type": "code", 171 | "execution_count": 10, 172 | "metadata": {}, 173 | "outputs": [], 174 | "source": [ 175 | "pmf = Pmf.from_seq(degrees(G))\n", 176 | "pmf" 177 | ] 178 | }, 179 | { 180 | "cell_type": "markdown", 181 | "metadata": {}, 182 | "source": [ 183 | "And `bar` to display it as a bar plot." 184 | ] 185 | }, 186 | { 187 | "cell_type": "code", 188 | "execution_count": 11, 189 | "metadata": { 190 | "scrolled": true 191 | }, 192 | "outputs": [], 193 | "source": [ 194 | "pmf.bar()\n", 195 | "decorate(xlabel='Degree', ylabel='PMF', xlim=[0.4, 3.6])" 196 | ] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "metadata": {}, 201 | "source": [ 202 | "**Exercise:** Add another node or nodes to the graph above, and add a few edges. Plot the new degree distribution." 203 | ] 204 | }, 205 | { 206 | "cell_type": "code", 207 | "execution_count": 12, 208 | "metadata": {}, 209 | "outputs": [], 210 | "source": [ 211 | "# Solution goes here" 212 | ] 213 | }, 214 | { 215 | "cell_type": "code", 216 | "execution_count": 13, 217 | "metadata": {}, 218 | "outputs": [], 219 | "source": [ 220 | "# Solution goes here" 221 | ] 222 | }, 223 | { 224 | "cell_type": "markdown", 225 | "metadata": {}, 226 | "source": [ 227 | "## Facebook data" 228 | ] 229 | }, 230 | { 231 | "cell_type": "markdown", 232 | "metadata": {}, 233 | "source": [ 234 | "The following function reads a file with one edge per line, specified by two integer node IDs." 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": 14, 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "def read_graph(filename):\n", 244 | " \"\"\"Read a graph from a file.\n", 245 | " \n", 246 | " filename: string\n", 247 | " \n", 248 | " return: Graph\n", 249 | " \"\"\"\n", 250 | " G = nx.Graph()\n", 251 | " array = np.loadtxt(filename, dtype=int)\n", 252 | " G.add_edges_from(array)\n", 253 | " return G" 254 | ] 255 | }, 256 | { 257 | "cell_type": "markdown", 258 | "metadata": {}, 259 | "source": [ 260 | "We'll read the Facecook data downloaded from [SNAP](https://snap.stanford.edu/data/egonets-Facebook.html)" 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": 16, 266 | "metadata": {}, 267 | "outputs": [], 268 | "source": [ 269 | "from os.path import basename, exists\n", 270 | "\n", 271 | "def download(url):\n", 272 | " filename = basename(url)\n", 273 | " if not exists(filename):\n", 274 | " from urllib.request import urlretrieve\n", 275 | " local, _ = urlretrieve(url, filename)\n", 276 | " print('Downloaded ' + local)\n", 277 | " \n", 278 | "download('https://github.com/AllenDowney/ComplexityScience/raw/master/facebook_combined.txt.gz')" 279 | ] 280 | }, 281 | { 282 | "cell_type": "code", 283 | "execution_count": 17, 284 | "metadata": {}, 285 | "outputs": [], 286 | "source": [ 287 | "# https://snap.stanford.edu/data/facebook_combined.txt.gz\n", 288 | "\n", 289 | "fb = read_graph('facebook_combined.txt.gz')\n", 290 | "n = len(fb)\n", 291 | "m = len(fb.edges())\n", 292 | "n, m" 293 | ] 294 | }, 295 | { 296 | "cell_type": "markdown", 297 | "metadata": {}, 298 | "source": [ 299 | "To see how popular \"you\" are, on average, we'll draw a random sample of 1000 people." 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": 18, 305 | "metadata": {}, 306 | "outputs": [], 307 | "source": [ 308 | "sample = np.random.choice(fb.nodes(), 1000, replace=True)" 309 | ] 310 | }, 311 | { 312 | "cell_type": "markdown", 313 | "metadata": {}, 314 | "source": [ 315 | "For each \"you\" in the sample, we'll look up the number of friends." 316 | ] 317 | }, 318 | { 319 | "cell_type": "code", 320 | "execution_count": 19, 321 | "metadata": {}, 322 | "outputs": [], 323 | "source": [ 324 | "sample_friends = [fb.degree(node) for node in sample]" 325 | ] 326 | }, 327 | { 328 | "cell_type": "markdown", 329 | "metadata": {}, 330 | "source": [ 331 | "To plot the degree distribution, I'll use `EstimatedPdf`, which computes a smooth Probability Density Function that fits the data." 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": 20, 337 | "metadata": {}, 338 | "outputs": [], 339 | "source": [ 340 | "Pmf.from_seq(sample_friends).plot()\n", 341 | "decorate(xlabel='Number of friends', \n", 342 | " ylabel='PMF',\n", 343 | " title='Degree PMF, friends')" 344 | ] 345 | }, 346 | { 347 | "cell_type": "code", 348 | "execution_count": 21, 349 | "metadata": {}, 350 | "outputs": [], 351 | "source": [ 352 | "Cdf.from_seq(sample_friends).plot(color='C0')\n", 353 | "decorate(xlabel='Number of friends', \n", 354 | " ylabel='CDF',\n", 355 | " title='Degree CDF, friends')" 356 | ] 357 | }, 358 | { 359 | "cell_type": "markdown", 360 | "metadata": {}, 361 | "source": [ 362 | "Now what if, instead of \"you\", we choose one of your friends, and look up the number of friends your friend has." 363 | ] 364 | }, 365 | { 366 | "cell_type": "code", 367 | "execution_count": 22, 368 | "metadata": {}, 369 | "outputs": [], 370 | "source": [ 371 | "sample_fof = []\n", 372 | "for node in sample:\n", 373 | " friends = list(fb.neighbors(node))\n", 374 | " friend = np.random.choice(friends)\n", 375 | " sample_fof.append(fb.degree(friend))" 376 | ] 377 | }, 378 | { 379 | "cell_type": "markdown", 380 | "metadata": {}, 381 | "source": [ 382 | "Here's the degree distribution for your friend's friends:" 383 | ] 384 | }, 385 | { 386 | "cell_type": "code", 387 | "execution_count": 23, 388 | "metadata": {}, 389 | "outputs": [], 390 | "source": [ 391 | "Pmf.from_seq(sample_fof).plot()\n", 392 | "decorate(xlabel='Number of friends', \n", 393 | " ylabel='PMF',\n", 394 | " title='Degree PMF, friends of friends')" 395 | ] 396 | }, 397 | { 398 | "cell_type": "code", 399 | "execution_count": 24, 400 | "metadata": {}, 401 | "outputs": [], 402 | "source": [ 403 | "Cdf.from_seq(sample_fof).plot(color='C1')\n", 404 | "decorate(xlabel='Number of friends', \n", 405 | " ylabel='CDF',\n", 406 | " title='Degree CDF, friends of friends')" 407 | ] 408 | }, 409 | { 410 | "cell_type": "code", 411 | "execution_count": 25, 412 | "metadata": {}, 413 | "outputs": [], 414 | "source": [ 415 | "Cdf.from_seq(sample_friends).plot(color='C0', label='friends')\n", 416 | "Cdf.from_seq(sample_fof).plot(color='C1', label='friends of friends')\n", 417 | "decorate(xlabel='Number of friends', \n", 418 | " ylabel='CDF',\n", 419 | " title='Degree distributions')" 420 | ] 421 | }, 422 | { 423 | "cell_type": "markdown", 424 | "metadata": {}, 425 | "source": [ 426 | "The bulk of the distribution is wider, and the tail is thicker. This difference is reflected in the means:" 427 | ] 428 | }, 429 | { 430 | "cell_type": "code", 431 | "execution_count": 26, 432 | "metadata": {}, 433 | "outputs": [], 434 | "source": [ 435 | "np.mean(sample_friends), np.mean(sample_fof)" 436 | ] 437 | }, 438 | { 439 | "cell_type": "markdown", 440 | "metadata": {}, 441 | "source": [ 442 | "And we can estimate the probability that your friend has more friends than you." 443 | ] 444 | }, 445 | { 446 | "cell_type": "code", 447 | "execution_count": 27, 448 | "metadata": {}, 449 | "outputs": [], 450 | "source": [ 451 | "np.mean([friend > you for you, friend in zip(sample_friends, sample_fof)])" 452 | ] 453 | }, 454 | { 455 | "cell_type": "markdown", 456 | "metadata": {}, 457 | "source": [ 458 | "## Power law distributions" 459 | ] 460 | }, 461 | { 462 | "cell_type": "markdown", 463 | "metadata": {}, 464 | "source": [ 465 | "As we'll see below, the degree distribution in the Facebook data looks, in some ways, like a power law distribution. To see what that means, we'll look at the Zipf distribution, which has a power law tail.\n", 466 | "\n", 467 | "Here's a sample from a Zipf distribution." 468 | ] 469 | }, 470 | { 471 | "cell_type": "code", 472 | "execution_count": 28, 473 | "metadata": {}, 474 | "outputs": [], 475 | "source": [ 476 | "zipf_sample = np.random.zipf(a=2, size=10000)" 477 | ] 478 | }, 479 | { 480 | "cell_type": "markdown", 481 | "metadata": {}, 482 | "source": [ 483 | "Here's what the PMF looks like." 484 | ] 485 | }, 486 | { 487 | "cell_type": "code", 488 | "execution_count": 29, 489 | "metadata": {}, 490 | "outputs": [], 491 | "source": [ 492 | "pmf = Pmf.from_seq(zipf_sample)\n", 493 | "pmf.plot()\n", 494 | "decorate(xlabel='Zipf sample', ylabel='PMF')" 495 | ] 496 | }, 497 | { 498 | "cell_type": "markdown", 499 | "metadata": {}, 500 | "source": [ 501 | "Here it is on a log-x scale." 502 | ] 503 | }, 504 | { 505 | "cell_type": "code", 506 | "execution_count": 30, 507 | "metadata": {}, 508 | "outputs": [], 509 | "source": [ 510 | "pmf.plot()\n", 511 | "decorate(xlabel='Zipf sample (log)', ylabel='PMF', xscale='log')" 512 | ] 513 | }, 514 | { 515 | "cell_type": "markdown", 516 | "metadata": {}, 517 | "source": [ 518 | "And on a log-log scale." 519 | ] 520 | }, 521 | { 522 | "cell_type": "code", 523 | "execution_count": 31, 524 | "metadata": {}, 525 | "outputs": [], 526 | "source": [ 527 | "pmf.plot()\n", 528 | "decorate(xlabel='Zipf sample (log)', ylabel='PMF (log)', \n", 529 | " xscale='log', yscale='log')" 530 | ] 531 | }, 532 | { 533 | "cell_type": "markdown", 534 | "metadata": {}, 535 | "source": [ 536 | "On a log-log scale, the PMF of the Zipf distribution looks like a straight line (until you get to the extreme tail, which is discrete and noisy)." 537 | ] 538 | }, 539 | { 540 | "cell_type": "markdown", 541 | "metadata": {}, 542 | "source": [ 543 | "For comparison, let's look at the Poisson distribution, which does not have a power law tail. I'll choose the Poisson distribution with the same mean as the sample from the Zipf distribution." 544 | ] 545 | }, 546 | { 547 | "cell_type": "code", 548 | "execution_count": 32, 549 | "metadata": {}, 550 | "outputs": [], 551 | "source": [ 552 | "mu, sigma = zipf_sample.mean(), zipf_sample.std()\n", 553 | "mu, sigma" 554 | ] 555 | }, 556 | { 557 | "cell_type": "code", 558 | "execution_count": 33, 559 | "metadata": {}, 560 | "outputs": [], 561 | "source": [ 562 | "poisson_sample = np.random.poisson(lam=mu, size=10000)\n", 563 | "poisson_sample.mean(), poisson_sample.std()" 564 | ] 565 | }, 566 | { 567 | "cell_type": "markdown", 568 | "metadata": {}, 569 | "source": [ 570 | "Here's the PMF on a log-log scale. It is definitely not a straight line." 571 | ] 572 | }, 573 | { 574 | "cell_type": "code", 575 | "execution_count": 34, 576 | "metadata": {}, 577 | "outputs": [], 578 | "source": [ 579 | "poisson_pmf = Pmf.from_seq(poisson_sample)\n", 580 | "poisson_pmf.plot()\n", 581 | "decorate(xlabel='Poisson sample (log)', ylabel='PMF (log)', \n", 582 | " xscale='log', yscale='log')" 583 | ] 584 | }, 585 | { 586 | "cell_type": "markdown", 587 | "metadata": {}, 588 | "source": [ 589 | "So this gives us a simple way to test for power laws. If you plot the PMF on a log-log scale, and the result is a straight line, they is evidence of power law behavior.\n", 590 | "\n", 591 | "This test is not entirely reliable; there are better options. But it's good enough for an initial exploration." 592 | ] 593 | }, 594 | { 595 | "cell_type": "markdown", 596 | "metadata": {}, 597 | "source": [ 598 | "## Barabási and Albert" 599 | ] 600 | }, 601 | { 602 | "cell_type": "markdown", 603 | "metadata": {}, 604 | "source": [ 605 | "Let's see what the degree distribution for the Facebook data looks like on a log-log scale." 606 | ] 607 | }, 608 | { 609 | "cell_type": "code", 610 | "execution_count": 35, 611 | "metadata": {}, 612 | "outputs": [], 613 | "source": [ 614 | "pmf_fb = Pmf.from_seq(degrees(fb))" 615 | ] 616 | }, 617 | { 618 | "cell_type": "code", 619 | "execution_count": 37, 620 | "metadata": {}, 621 | "outputs": [], 622 | "source": [ 623 | "pmf_fb.plot(label='Facebook')\n", 624 | "decorate(xscale='log', \n", 625 | " yscale='log',\n", 626 | " xlabel='Degree (log)', \n", 627 | " ylabel='PMF (log)')" 628 | ] 629 | }, 630 | { 631 | "cell_type": "markdown", 632 | "metadata": {}, 633 | "source": [ 634 | "For degrees greater than 10, it resembles the Zipf sample (and doesn't look much like the Poisson sample).\n", 635 | "\n", 636 | "We can estimate the parameter of the Zipf distribution by eyeballing the slope of the tail." 637 | ] 638 | }, 639 | { 640 | "cell_type": "code", 641 | "execution_count": 39, 642 | "metadata": {}, 643 | "outputs": [], 644 | "source": [ 645 | "plt.plot([10, 1000], [5e-2, 2e-4], color='gray', linestyle='dashed')\n", 646 | "\n", 647 | "pmf_fb.plot(label='Facebook')\n", 648 | "decorate(xscale='log', \n", 649 | " yscale='log',\n", 650 | " xlabel='Degree (log)', \n", 651 | " ylabel='PMF (log)')" 652 | ] 653 | }, 654 | { 655 | "cell_type": "markdown", 656 | "metadata": {}, 657 | "source": [ 658 | "Here's a simplified version of the NetworkX function that generates BA graphs." 659 | ] 660 | }, 661 | { 662 | "cell_type": "code", 663 | "execution_count": 40, 664 | "metadata": {}, 665 | "outputs": [], 666 | "source": [ 667 | "# modified version of the NetworkX implementation from\n", 668 | "# https://github.com/networkx/networkx/blob/master/networkx/generators/random_graphs.py\n", 669 | "\n", 670 | "import random\n", 671 | "\n", 672 | "def barabasi_albert_graph(n, k, seed=None):\n", 673 | " \"\"\"Constructs a BA graph.\n", 674 | " \n", 675 | " n: number of nodes\n", 676 | " k: number of edges for each new node\n", 677 | " seed: random seen\n", 678 | " \"\"\"\n", 679 | " if seed is not None:\n", 680 | " random.seed(seed)\n", 681 | " \n", 682 | " G = nx.empty_graph(k)\n", 683 | " targets = set(range(k))\n", 684 | " repeated_nodes = []\n", 685 | "\n", 686 | " for source in range(k, n):\n", 687 | "\n", 688 | " G.add_edges_from(zip([source]*k, targets))\n", 689 | "\n", 690 | " repeated_nodes.extend(targets)\n", 691 | " repeated_nodes.extend([source] * k)\n", 692 | "\n", 693 | " targets = _random_subset(repeated_nodes, k)\n", 694 | "\n", 695 | " return G" 696 | ] 697 | }, 698 | { 699 | "cell_type": "markdown", 700 | "metadata": {}, 701 | "source": [ 702 | "And here's the function that generates a random subset without repetition." 703 | ] 704 | }, 705 | { 706 | "cell_type": "code", 707 | "execution_count": 41, 708 | "metadata": {}, 709 | "outputs": [], 710 | "source": [ 711 | "def _random_subset(repeated_nodes, k):\n", 712 | " \"\"\"Select a random subset of nodes without repeating.\n", 713 | " \n", 714 | " repeated_nodes: list of nodes\n", 715 | " k: size of set\n", 716 | " \n", 717 | " returns: set of nodes\n", 718 | " \"\"\"\n", 719 | " targets = set()\n", 720 | " while len(targets) < k:\n", 721 | " x = random.choice(repeated_nodes)\n", 722 | " targets.add(x)\n", 723 | " return targets" 724 | ] 725 | }, 726 | { 727 | "cell_type": "markdown", 728 | "metadata": {}, 729 | "source": [ 730 | "I'll generate a BA graph with the same number of nodes and edges as the Facebook data:" 731 | ] 732 | }, 733 | { 734 | "cell_type": "code", 735 | "execution_count": 42, 736 | "metadata": {}, 737 | "outputs": [], 738 | "source": [ 739 | "n = len(fb)\n", 740 | "m = len(fb.edges())\n", 741 | "k = int(round(m/n))\n", 742 | "n, m, k" 743 | ] 744 | }, 745 | { 746 | "cell_type": "markdown", 747 | "metadata": {}, 748 | "source": [ 749 | "Providing a random seed means we'll get the same graph every time." 750 | ] 751 | }, 752 | { 753 | "cell_type": "code", 754 | "execution_count": 43, 755 | "metadata": {}, 756 | "outputs": [], 757 | "source": [ 758 | "ba = barabasi_albert_graph(n, k, seed=15)" 759 | ] 760 | }, 761 | { 762 | "cell_type": "markdown", 763 | "metadata": {}, 764 | "source": [ 765 | "The number of edges is pretty close to what we asked for." 766 | ] 767 | }, 768 | { 769 | "cell_type": "code", 770 | "execution_count": 44, 771 | "metadata": {}, 772 | "outputs": [], 773 | "source": [ 774 | "len(ba), len(ba.edges()), len(ba.edges())/len(ba)" 775 | ] 776 | }, 777 | { 778 | "cell_type": "markdown", 779 | "metadata": {}, 780 | "source": [ 781 | "So the mean degree is about right." 782 | ] 783 | }, 784 | { 785 | "cell_type": "code", 786 | "execution_count": 45, 787 | "metadata": {}, 788 | "outputs": [], 789 | "source": [ 790 | "np.mean(degrees(fb)), np.mean(degrees(ba))" 791 | ] 792 | }, 793 | { 794 | "cell_type": "markdown", 795 | "metadata": {}, 796 | "source": [ 797 | "The standard deviation of degree is pretty close; maybe a little low." 798 | ] 799 | }, 800 | { 801 | "cell_type": "code", 802 | "execution_count": 46, 803 | "metadata": {}, 804 | "outputs": [], 805 | "source": [ 806 | "np.std(degrees(fb)), np.std(degrees(ba))" 807 | ] 808 | }, 809 | { 810 | "cell_type": "markdown", 811 | "metadata": {}, 812 | "source": [ 813 | "Let's take a look at the degree distribution." 814 | ] 815 | }, 816 | { 817 | "cell_type": "code", 818 | "execution_count": 47, 819 | "metadata": {}, 820 | "outputs": [], 821 | "source": [ 822 | "pmf_ba = Pmf.from_seq(degrees(ba))" 823 | ] 824 | }, 825 | { 826 | "cell_type": "markdown", 827 | "metadata": {}, 828 | "source": [ 829 | "Looking at the PMFs on a linear scale, we see one difference, which is that the BA model has no nodes with degree less than `k`, which is 22." 830 | ] 831 | }, 832 | { 833 | "cell_type": "code", 834 | "execution_count": 48, 835 | "metadata": {}, 836 | "outputs": [], 837 | "source": [ 838 | "plt.figure(figsize=(12,6))\n", 839 | "plt.subplot(1, 2, 1)\n", 840 | "\n", 841 | "pmf_fb.plot(label='Facebook')\n", 842 | "decorate(xlabel='Degree', ylabel='PMF')\n", 843 | "\n", 844 | "plt.subplot(1, 2, 2)\n", 845 | "\n", 846 | "pmf_ba.plot(label='BA model')\n", 847 | "decorate(xlabel='Degree', ylabel='PMF')" 848 | ] 849 | }, 850 | { 851 | "cell_type": "markdown", 852 | "metadata": {}, 853 | "source": [ 854 | "If we look at the PMF on a log-log scale, the BA model looks pretty good for values bigger than about 20. And it seems to follow a power law." 855 | ] 856 | }, 857 | { 858 | "cell_type": "code", 859 | "execution_count": 49, 860 | "metadata": {}, 861 | "outputs": [], 862 | "source": [ 863 | "plt.figure(figsize=(12,6))\n", 864 | "plt.subplot(1, 2, 1)\n", 865 | "\n", 866 | "pmf_fb.plot(label='Facebook')\n", 867 | "decorate(xlabel='Degree', ylabel='PMF',\n", 868 | " xscale='log', yscale='log')\n", 869 | "\n", 870 | "plt.subplot(1, 2, 2)\n", 871 | "\n", 872 | "pmf_ba.plot(label='BA model')\n", 873 | "decorate(xlabel='Degree', ylabel='PMF',\n", 874 | " xlim=[1, 1e4],\n", 875 | " xscale='log', yscale='log')" 876 | ] 877 | }, 878 | { 879 | "cell_type": "markdown", 880 | "metadata": {}, 881 | "source": [ 882 | "## Cumulative distributions" 883 | ] 884 | }, 885 | { 886 | "cell_type": "markdown", 887 | "metadata": {}, 888 | "source": [ 889 | "Here are the degree CDFs for the Facebook data, the WS model, and the BA model." 890 | ] 891 | }, 892 | { 893 | "cell_type": "code", 894 | "execution_count": 50, 895 | "metadata": {}, 896 | "outputs": [], 897 | "source": [ 898 | "cdf_fb = Cdf.from_seq(degrees(fb))" 899 | ] 900 | }, 901 | { 902 | "cell_type": "code", 903 | "execution_count": 51, 904 | "metadata": {}, 905 | "outputs": [], 906 | "source": [ 907 | "cdf_ba = Cdf.from_seq(degrees(ba))" 908 | ] 909 | }, 910 | { 911 | "cell_type": "markdown", 912 | "metadata": {}, 913 | "source": [ 914 | "If we plot them on a log-x scale, we get a sense of how well the model fits the central part of the distribution.\n", 915 | "\n", 916 | "The BA model is ok for values above the median, but not very good for smaller values." 917 | ] 918 | }, 919 | { 920 | "cell_type": "code", 921 | "execution_count": 52, 922 | "metadata": {}, 923 | "outputs": [], 924 | "source": [ 925 | "cdf_fb.plot(label='Facebook')\n", 926 | "cdf_ba.plot(color='gray', label='BA model')\n", 927 | "decorate(xlabel='Degree', xscale='log',\n", 928 | " ylabel='CDF')" 929 | ] 930 | }, 931 | { 932 | "cell_type": "markdown", 933 | "metadata": {}, 934 | "source": [ 935 | "If we plot the complementary CDF on a log-log scale, we see that the BA model fits the tail of the distribution reasonably well." 936 | ] 937 | }, 938 | { 939 | "cell_type": "code", 940 | "execution_count": 53, 941 | "metadata": {}, 942 | "outputs": [], 943 | "source": [ 944 | "complementary_cdf_fb = 1-cdf_fb\n", 945 | "complementary_cdf_ba = 1-cdf_ba" 946 | ] 947 | }, 948 | { 949 | "cell_type": "code", 950 | "execution_count": 54, 951 | "metadata": {}, 952 | "outputs": [], 953 | "source": [ 954 | "complementary_cdf_fb.plot(label='Facebook')\n", 955 | "complementary_cdf_ba.plot(color='gray', label='BA model')\n", 956 | "decorate(xlabel='Degree', xscale='log',\n", 957 | " ylabel='Complementary CDF', yscale='log')" 958 | ] 959 | }, 960 | { 961 | "cell_type": "markdown", 962 | "metadata": {}, 963 | "source": [ 964 | "But there is certainly room for a model that does a better job of fitting the whole distribution." 965 | ] 966 | }, 967 | { 968 | "cell_type": "code", 969 | "execution_count": null, 970 | "metadata": {}, 971 | "outputs": [], 972 | "source": [] 973 | } 974 | ], 975 | "metadata": { 976 | "kernelspec": { 977 | "display_name": "Python 3 (ipykernel)", 978 | "language": "python", 979 | "name": "python3" 980 | }, 981 | "language_info": { 982 | "codemirror_mode": { 983 | "name": "ipython", 984 | "version": 3 985 | }, 986 | "file_extension": ".py", 987 | "mimetype": "text/x-python", 988 | "name": "python", 989 | "nbconvert_exporter": "python", 990 | "pygments_lexer": "ipython3", 991 | "version": "3.7.11" 992 | } 993 | }, 994 | "nbformat": 4, 995 | "nbformat_minor": 1 996 | } 997 | -------------------------------------------------------------------------------- /01_workshop.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Agent Based Models of Segregation\n", 8 | "\n", 9 | "Code examples from [Think Complexity, 2nd edition](https://thinkcomplex.com).\n", 10 | "\n", 11 | "Copyright 2019 Allen Downey, [MIT License](http://opensource.org/licenses/MIT)" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 3, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import numpy as np\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "import seaborn as sns" 23 | ] 24 | }, 25 | { 26 | "cell_type": "code", 27 | "execution_count": 4, 28 | "metadata": {}, 29 | "outputs": [], 30 | "source": [ 31 | "def decorate(**options):\n", 32 | " plt.gca().set(**options)\n", 33 | " plt.tight_layout()" 34 | ] 35 | }, 36 | { 37 | "cell_type": "markdown", 38 | "metadata": {}, 39 | "source": [ 40 | "## Schelling's model" 41 | ] 42 | }, 43 | { 44 | "cell_type": "markdown", 45 | "metadata": {}, 46 | "source": [ 47 | "In 1969 Thomas Schelling published \"[Models of Segregation](http://thinkcomplex.com/schell),\n", 48 | "which proposed a simple model of racial segregation.\n", 49 | "\n", 50 | "The Schelling model is a grid where each cell represents a house. The houses are occupied by two kinds of agents,\n", 51 | "labeled red and blue, in roughly equal numbers. About 10% of the\n", 52 | "houses are empty.\n", 53 | "\n", 54 | "At any point in time, an agent might be happy or unhappy, depending\n", 55 | "on the other agents in the neighborhood, where the\n", 56 | "\"neighborhood\" of each house is the set of eight adjacent cells.\n", 57 | "\n", 58 | "In one version of the model, agents are happy if they have at least\n", 59 | "three neighbors like themselves, and unhappy if they have fewer than three.\n", 60 | "\n", 61 | "The simulation proceeds by choosing an agent at random and checking\n", 62 | "to see whether they are happy. If so, nothing happens; if not,\n", 63 | "the agent chooses one of the unoccupied cells at random and moves.\n", 64 | "\n", 65 | "You will not be surprised to hear that this model leads to some\n", 66 | "segregation, but you might be surprised by the degree. \n", 67 | "\n", 68 | "### Implementing the model\n", 69 | "\n", 70 | "We will explore Schelling's model by implementing it in Python. The following\n", 71 | "function initializes the grid." 72 | ] 73 | }, 74 | { 75 | "cell_type": "code", 76 | "execution_count": 5, 77 | "metadata": {}, 78 | "outputs": [], 79 | "source": [ 80 | "def make_grid(n):\n", 81 | " \"\"\"Make an array with two types of agents.\n", 82 | " \n", 83 | " n: width and height of the array\n", 84 | " \n", 85 | " return: NumPy array\n", 86 | " \"\"\"\n", 87 | " choices = np.array([0, 1, 2], dtype=np.int8)\n", 88 | " probs = [0.1, 0.45, 0.45]\n", 89 | " return np.random.choice(choices, (n, n), p=probs)" 90 | ] 91 | }, 92 | { 93 | "cell_type": "code", 94 | "execution_count": 6, 95 | "metadata": {}, 96 | "outputs": [], 97 | "source": [ 98 | "grid = make_grid(n=10)\n", 99 | "grid" 100 | ] 101 | }, 102 | { 103 | "cell_type": "markdown", 104 | "metadata": {}, 105 | "source": [ 106 | "The following function plots a visualization the grid." 107 | ] 108 | }, 109 | { 110 | "cell_type": "code", 111 | "execution_count": 7, 112 | "metadata": {}, 113 | "outputs": [], 114 | "source": [ 115 | "from matplotlib.colors import LinearSegmentedColormap\n", 116 | "\n", 117 | "# make a custom color map\n", 118 | "palette = sns.color_palette('muted')\n", 119 | "colors = 'white', palette[1], palette[0]\n", 120 | "cmap = LinearSegmentedColormap.from_list('cmap', colors)\n", 121 | "\n", 122 | "def draw(grid):\n", 123 | " \"\"\"Draws the grid.\n", 124 | " \n", 125 | " grid: NumPy array\n", 126 | " \"\"\"\n", 127 | " # Make a copy because some implementations\n", 128 | " # of step perform updates in place.\n", 129 | " a = grid.copy()\n", 130 | " n, m = a.shape\n", 131 | " plt.axis([0, m, 0, n])\n", 132 | " plt.xticks([])\n", 133 | " plt.yticks([])\n", 134 | "\n", 135 | " options = dict(interpolation='none', alpha=0.8)\n", 136 | " options['extent'] = [0, m, 0, n]\n", 137 | " return plt.imshow(a, cmap, **options)" 138 | ] 139 | }, 140 | { 141 | "cell_type": "markdown", 142 | "metadata": {}, 143 | "source": [ 144 | "Here's what the random initial condition looks like." 145 | ] 146 | }, 147 | { 148 | "cell_type": "code", 149 | "execution_count": 8, 150 | "metadata": {}, 151 | "outputs": [], 152 | "source": [ 153 | "im = draw(grid)" 154 | ] 155 | }, 156 | { 157 | "cell_type": "markdown", 158 | "metadata": {}, 159 | "source": [ 160 | "### The step function\n", 161 | "\n", 162 | "To simulate a time step in Schelling's model, we have to\n", 163 | "\n", 164 | "1) Choose and unhappy agent.\n", 165 | "2) Choose a random empty cell.\n", 166 | "3) Move the agent.\n", 167 | "\n", 168 | "The next few cells develop the functions we'll need. As an exercise, you will assemble them into a `step` function.\n", 169 | "\n", 170 | "First I'll compute `empty`, which is a boolean array that is `True` for the empty cells." 171 | ] 172 | }, 173 | { 174 | "cell_type": "code", 175 | "execution_count": 9, 176 | "metadata": {}, 177 | "outputs": [], 178 | "source": [ 179 | "empty = (grid==0)\n", 180 | "empty.sum()" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "metadata": {}, 186 | "source": [ 187 | "To find the unhappy agents, we have to compute the number of same-color neighbors for each agent. We'll do that using 2-D correlation.\n", 188 | "\n", 189 | "As an example, we can compute the number of empty neighbors for each cell." 190 | ] 191 | }, 192 | { 193 | "cell_type": "code", 194 | "execution_count": 10, 195 | "metadata": {}, 196 | "outputs": [], 197 | "source": [ 198 | "from scipy.signal import correlate2d\n", 199 | "\n", 200 | "kernel = np.array([[1, 1, 1],\n", 201 | " [1, 0, 1],\n", 202 | " [1, 1, 1]], dtype=np.int8)\n", 203 | "\n", 204 | "options = dict(mode='same', boundary='wrap')\n", 205 | "\n", 206 | "correlate2d(empty, kernel, **options)" 207 | ] 208 | }, 209 | { 210 | "cell_type": "markdown", 211 | "metadata": {}, 212 | "source": [ 213 | "The following function computes the number of same-color neighbors for each agent." 214 | ] 215 | }, 216 | { 217 | "cell_type": "code", 218 | "execution_count": 11, 219 | "metadata": {}, 220 | "outputs": [], 221 | "source": [ 222 | "def compute_num_same(grid):\n", 223 | " \"\"\"For each cell, the number of same-color neighbors.\n", 224 | " \n", 225 | " grip: NumPy array\n", 226 | " \n", 227 | " return: new NumPy array\n", 228 | " \"\"\"\n", 229 | "\n", 230 | " red = grid==1\n", 231 | " blue = grid==2\n", 232 | "\n", 233 | " # count red neighbors, blue neighbors, and total\n", 234 | " num_red = correlate2d(red, kernel, **options)\n", 235 | " num_blue = correlate2d(blue, kernel, **options)\n", 236 | "\n", 237 | " num_same = np.where(red, num_red, num_blue)\n", 238 | " \n", 239 | " return num_same" 240 | ] 241 | }, 242 | { 243 | "cell_type": "markdown", 244 | "metadata": {}, 245 | "source": [ 246 | "Here's what that looks like for the initial grid." 247 | ] 248 | }, 249 | { 250 | "cell_type": "code", 251 | "execution_count": 12, 252 | "metadata": {}, 253 | "outputs": [], 254 | "source": [ 255 | "# for each cell, compute the fraction of neighbors with the same color\n", 256 | "num_same = compute_num_same(grid)\n", 257 | "num_same" 258 | ] 259 | }, 260 | { 261 | "cell_type": "markdown", 262 | "metadata": {}, 263 | "source": [ 264 | "The mean of this array measures the average level of segregation." 265 | ] 266 | }, 267 | { 268 | "cell_type": "code", 269 | "execution_count": 13, 270 | "metadata": {}, 271 | "outputs": [], 272 | "source": [ 273 | "num_same.mean()" 274 | ] 275 | }, 276 | { 277 | "cell_type": "markdown", 278 | "metadata": {}, 279 | "source": [ 280 | "`locs_where` is a wrapper on `np.nonzero` that returns a list of pairs, where each pair is the coordinate of an agent that meets a `condition`." 281 | ] 282 | }, 283 | { 284 | "cell_type": "code", 285 | "execution_count": 16, 286 | "metadata": {}, 287 | "outputs": [], 288 | "source": [ 289 | "def locs_where(condition):\n", 290 | " \"\"\"Find cells where a boolean array is True.\n", 291 | " \n", 292 | " condition: NumPy array\n", 293 | " \n", 294 | " return: list of coordinate pairs\n", 295 | " \"\"\"\n", 296 | " ii, jj = np.nonzero(condition)\n", 297 | " return list(zip(ii, jj))" 298 | ] 299 | }, 300 | { 301 | "cell_type": "markdown", 302 | "metadata": {}, 303 | "source": [ 304 | "An agent is \"unhappy\" if they have fewer than 3 same-color neighbors.\n", 305 | "\n", 306 | "Here are the locations of the unhappy agents." 307 | ] 308 | }, 309 | { 310 | "cell_type": "code", 311 | "execution_count": 15, 312 | "metadata": {}, 313 | "outputs": [], 314 | "source": [ 315 | "threshold = 3\n", 316 | "\n", 317 | "unhappy_locs = locs_where(num_same < threshold)\n", 318 | "unhappy_locs" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": {}, 324 | "source": [ 325 | "During each time step, we'll choose a random unhappy agent.\n", 326 | "\n", 327 | "The following function chooses a random tuple from a list." 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": 14, 333 | "metadata": {}, 334 | "outputs": [], 335 | "source": [ 336 | "def random_loc(locs):\n", 337 | " \"\"\"Choose a random element from a list of tuples.\n", 338 | " \n", 339 | " locs: list of tuples\n", 340 | " \n", 341 | " return: tuple\n", 342 | " \"\"\"\n", 343 | " index = np.random.choice(len(locs))\n", 344 | " return locs[index]" 345 | ] 346 | }, 347 | { 348 | "cell_type": "markdown", 349 | "metadata": {}, 350 | "source": [ 351 | "`source` is the random unhappy agent who decides to move." 352 | ] 353 | }, 354 | { 355 | "cell_type": "code", 356 | "execution_count": 15, 357 | "metadata": {}, 358 | "outputs": [], 359 | "source": [ 360 | "source = random_loc(unhappy_locs)\n", 361 | "source" 362 | ] 363 | }, 364 | { 365 | "cell_type": "markdown", 366 | "metadata": {}, 367 | "source": [ 368 | "`dest` is the random empty cell they choose to move to." 369 | ] 370 | }, 371 | { 372 | "cell_type": "code", 373 | "execution_count": 16, 374 | "metadata": {}, 375 | "outputs": [], 376 | "source": [ 377 | "empty_locs = locs_where(empty)\n", 378 | "dest = random_loc(empty_locs)\n", 379 | "dest" 380 | ] 381 | }, 382 | { 383 | "cell_type": "markdown", 384 | "metadata": {}, 385 | "source": [ 386 | "The following function swaps `source` and `dest`." 387 | ] 388 | }, 389 | { 390 | "cell_type": "code", 391 | "execution_count": 17, 392 | "metadata": {}, 393 | "outputs": [], 394 | "source": [ 395 | "def move(grid, source, dest):\n", 396 | " \"\"\"Swap the agents at source and dest.\n", 397 | " \n", 398 | " grip: NumPy array\n", 399 | " source: location tuple\n", 400 | " dest: location tuple\n", 401 | " \"\"\"\n", 402 | " grid[dest], grid[source] = grid[source], grid[dest]" 403 | ] 404 | }, 405 | { 406 | "cell_type": "code", 407 | "execution_count": 18, 408 | "metadata": {}, 409 | "outputs": [], 410 | "source": [ 411 | "move(grid, source, dest)" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "metadata": {}, 417 | "source": [ 418 | "**Exercise:** Pull the code from the previous cells into a function that chooses an unhappy agent and moves them to a random empty cell. It should compute and return the mean of `num_same`.\n", 419 | "\n", 420 | "Note: If there are no unhappy cells, `step` should do nothing." 421 | ] 422 | }, 423 | { 424 | "cell_type": "code", 425 | "execution_count": 19, 426 | "metadata": {}, 427 | "outputs": [], 428 | "source": [ 429 | "def step(grid, threshold=3):\n", 430 | " \"\"\"Simulate one time step.\n", 431 | " \n", 432 | " grid: NumPy array\n", 433 | " threshold: number of same-color neighbors needed to be happy\n", 434 | "\n", 435 | " return: average number of same-color neighbors\n", 436 | " \"\"\"\n", 437 | " num_same = compute_num_same(grid)\n", 438 | "\n", 439 | " # FILL THIS IN!\n", 440 | " \n", 441 | " return num_same.mean()" 442 | ] 443 | }, 444 | { 445 | "cell_type": "code", 446 | "execution_count": 20, 447 | "metadata": {}, 448 | "outputs": [], 449 | "source": [ 450 | "# Solution goes here" 451 | ] 452 | }, 453 | { 454 | "cell_type": "markdown", 455 | "metadata": {}, 456 | "source": [ 457 | "Test your step function here." 458 | ] 459 | }, 460 | { 461 | "cell_type": "code", 462 | "execution_count": 21, 463 | "metadata": {}, 464 | "outputs": [], 465 | "source": [ 466 | "step(grid)" 467 | ] 468 | }, 469 | { 470 | "cell_type": "markdown", 471 | "metadata": {}, 472 | "source": [ 473 | "Then run the following cells to see how segregation changes over time." 474 | ] 475 | }, 476 | { 477 | "cell_type": "code", 478 | "execution_count": 22, 479 | "metadata": {}, 480 | "outputs": [], 481 | "source": [ 482 | "def decorate_seg():\n", 483 | " decorate(xlabel='Number of steps',\n", 484 | " ylabel='Average number of same-color neighbors',\n", 485 | " title='Schelling model')" 486 | ] 487 | }, 488 | { 489 | "cell_type": "code", 490 | "execution_count": 23, 491 | "metadata": {}, 492 | "outputs": [], 493 | "source": [ 494 | "grid = make_grid(n=10)\n", 495 | "segs = [step(grid) for i in range(200)]\n", 496 | "plt.plot(segs)\n", 497 | "decorate_seg()" 498 | ] 499 | }, 500 | { 501 | "cell_type": "markdown", 502 | "metadata": {}, 503 | "source": [ 504 | "**Exercise:** Experiment with different values of `threshold` and see what effect they have on the result." 505 | ] 506 | }, 507 | { 508 | "cell_type": "markdown", 509 | "metadata": {}, 510 | "source": [ 511 | "### Bigger grid\n", 512 | "\n", 513 | "Let's see how that looks on a bigger grid." 514 | ] 515 | }, 516 | { 517 | "cell_type": "code", 518 | "execution_count": 24, 519 | "metadata": {}, 520 | "outputs": [], 521 | "source": [ 522 | "grid = make_grid(n=50)\n", 523 | "im = draw(grid)" 524 | ] 525 | }, 526 | { 527 | "cell_type": "code", 528 | "execution_count": 25, 529 | "metadata": {}, 530 | "outputs": [], 531 | "source": [ 532 | "segs = [step(grid, threshold=3) for i in range(3000)]" 533 | ] 534 | }, 535 | { 536 | "cell_type": "code", 537 | "execution_count": 26, 538 | "metadata": {}, 539 | "outputs": [], 540 | "source": [ 541 | "plt.plot(segs)\n", 542 | "decorate_seg()" 543 | ] 544 | }, 545 | { 546 | "cell_type": "code", 547 | "execution_count": 27, 548 | "metadata": {}, 549 | "outputs": [], 550 | "source": [ 551 | "im = draw(grid)" 552 | ] 553 | }, 554 | { 555 | "cell_type": "markdown", 556 | "metadata": {}, 557 | "source": [ 558 | "The following figure shows three steps in the evolution of the grid." 559 | ] 560 | }, 561 | { 562 | "cell_type": "code", 563 | "execution_count": 28, 564 | "metadata": {}, 565 | "outputs": [], 566 | "source": [ 567 | "np.random.seed(17)\n", 568 | "\n", 569 | "grid = make_grid(n=50)\n", 570 | "\n", 571 | "# draw the initial grid\n", 572 | "plt.figure(figsize=(9,3))\n", 573 | "plt.subplot(1,3,1)\n", 574 | "draw(grid)\n", 575 | "\n", 576 | "# first update\n", 577 | "plt.subplot(1,3,2)\n", 578 | "for i in range(1000):\n", 579 | " step(grid)\n", 580 | "draw(grid)\n", 581 | "\n", 582 | "# second update\n", 583 | "plt.subplot(1,3,3)\n", 584 | "for i in range(1000):\n", 585 | " step(grid)\n", 586 | "draw(grid)\n", 587 | "\n", 588 | "plt.tight_layout()" 589 | ] 590 | }, 591 | { 592 | "cell_type": "markdown", 593 | "metadata": {}, 594 | "source": [ 595 | "**Exercise:** One more time, try out different values of `threshold`. What value yields the biggest difference between `threshold` and the average number of same-color neighbors?" 596 | ] 597 | }, 598 | { 599 | "cell_type": "markdown", 600 | "metadata": {}, 601 | "source": [ 602 | "### Animation\n", 603 | "\n", 604 | "The following functions animate the evolution of the grid." 605 | ] 606 | }, 607 | { 608 | "cell_type": "code", 609 | "execution_count": 29, 610 | "metadata": {}, 611 | "outputs": [], 612 | "source": [ 613 | "from time import sleep\n", 614 | "from IPython.display import clear_output\n", 615 | "\n", 616 | "def animate(grid, frames, interval=None):\n", 617 | " \"\"\"Animate the automaton.\n", 618 | "\n", 619 | " grid: NumPy array\n", 620 | " frames: number of frames to draw\n", 621 | " interval: time between frames in seconds\n", 622 | " \"\"\"\n", 623 | " plt.figure()\n", 624 | " try:\n", 625 | " for i in range(frames-1):\n", 626 | " draw(grid)\n", 627 | " plt.show()\n", 628 | " if interval:\n", 629 | " sleep(interval)\n", 630 | " step(grid)\n", 631 | " clear_output(wait=True)\n", 632 | " draw(grid)\n", 633 | " plt.show()\n", 634 | " except KeyboardInterrupt:\n", 635 | " pass" 636 | ] 637 | }, 638 | { 639 | "cell_type": "code", 640 | "execution_count": 30, 641 | "metadata": {}, 642 | "outputs": [], 643 | "source": [ 644 | "grid = make_grid(n=50)\n", 645 | "# animate(grid, frames=1000)" 646 | ] 647 | }, 648 | { 649 | "cell_type": "markdown", 650 | "metadata": {}, 651 | "source": [ 652 | "**Exercise:** Experiment with different starting conditions: for example, more or fewer empty cells, or unequal numbers of red and blue agents." 653 | ] 654 | }, 655 | { 656 | "cell_type": "markdown", 657 | "metadata": {}, 658 | "source": [ 659 | "# The Big Sort\n", 660 | "\n", 661 | "Bill Bishop, author of *The Big Sort*, argues that\n", 662 | "American society is increasingly segregated by political\n", 663 | "opinion, as people choose to live among like-minded neighbors.\n", 664 | "\n", 665 | "The mechanism Bishop hypothesizes is not that people, like the agents\n", 666 | "in Schelling's model, are more likely to move if they are\n", 667 | "isolated, but that when they move for any reason, they are\n", 668 | "likely to choose a neighborhood with people like themselves.\n", 669 | "\n", 670 | "Let's write a version of Schelling's model to simulate\n", 671 | "this kind of behavior and see if it yields similar degrees of\n", 672 | "segregation.\n", 673 | "\n", 674 | "We'll start with a random initial grid." 675 | ] 676 | }, 677 | { 678 | "cell_type": "code", 679 | "execution_count": 31, 680 | "metadata": {}, 681 | "outputs": [], 682 | "source": [ 683 | "grid = make_grid(n=10)\n", 684 | "im = draw(grid)" 685 | ] 686 | }, 687 | { 688 | "cell_type": "markdown", 689 | "metadata": {}, 690 | "source": [ 691 | "Write a few lines of code to choose a random occupied location, and call it `source`." 692 | ] 693 | }, 694 | { 695 | "cell_type": "code", 696 | "execution_count": 32, 697 | "metadata": {}, 698 | "outputs": [], 699 | "source": [ 700 | "empty = (grid==0)\n", 701 | "empty_locs = locs_where(empty)\n", 702 | "empty_locs" 703 | ] 704 | }, 705 | { 706 | "cell_type": "code", 707 | "execution_count": 33, 708 | "metadata": {}, 709 | "outputs": [], 710 | "source": [ 711 | "full_locs = locs_where(~empty)" 712 | ] 713 | }, 714 | { 715 | "cell_type": "code", 716 | "execution_count": 34, 717 | "metadata": {}, 718 | "outputs": [], 719 | "source": [ 720 | "source = random_loc(full_locs)\n", 721 | "source" 722 | ] 723 | }, 724 | { 725 | "cell_type": "markdown", 726 | "metadata": {}, 727 | "source": [ 728 | "Here's a list of random empty locations the `source` agent could move to." 729 | ] 730 | }, 731 | { 732 | "cell_type": "code", 733 | "execution_count": 35, 734 | "metadata": {}, 735 | "outputs": [], 736 | "source": [ 737 | "num_comps = 2\n", 738 | "dests = [random_loc(empty_locs) for i in range(num_comps)]\n", 739 | "dests" 740 | ] 741 | }, 742 | { 743 | "cell_type": "markdown", 744 | "metadata": {}, 745 | "source": [ 746 | "The following cell computes the number of neighbors in each location that are the same color as `source`." 747 | ] 748 | }, 749 | { 750 | "cell_type": "code", 751 | "execution_count": 36, 752 | "metadata": {}, 753 | "outputs": [], 754 | "source": [ 755 | "same_color = (grid==grid[source])\n", 756 | "\n", 757 | "num_same = correlate2d(same_color, kernel, **options)\n", 758 | "num_same" 759 | ] 760 | }, 761 | { 762 | "cell_type": "markdown", 763 | "metadata": {}, 764 | "source": [ 765 | "Now we can make a list with one tuple per destination, where the first element in each tuple is the number of same-color neighbors." 766 | ] 767 | }, 768 | { 769 | "cell_type": "code", 770 | "execution_count": 37, 771 | "metadata": {}, 772 | "outputs": [], 773 | "source": [ 774 | "choices = [(num_same[dest], np.random.random(), dest) for dest in dests]\n", 775 | "\n", 776 | "for choice in choices:\n", 777 | " print(choice)" 778 | ] 779 | }, 780 | { 781 | "cell_type": "markdown", 782 | "metadata": {}, 783 | "source": [ 784 | "Using the `max` function, we get the location with the highest number of same-color neighbors, with ties broken at random." 785 | ] 786 | }, 787 | { 788 | "cell_type": "code", 789 | "execution_count": 38, 790 | "metadata": {}, 791 | "outputs": [], 792 | "source": [ 793 | "num, rand, dest = max(choices)\n", 794 | "num, rand, dest" 795 | ] 796 | }, 797 | { 798 | "cell_type": "code", 799 | "execution_count": 39, 800 | "metadata": {}, 801 | "outputs": [], 802 | "source": [ 803 | "move(grid, source, dest)" 804 | ] 805 | }, 806 | { 807 | "cell_type": "markdown", 808 | "metadata": {}, 809 | "source": [ 810 | "Now pull all of that code into a step function." 811 | ] 812 | }, 813 | { 814 | "cell_type": "code", 815 | "execution_count": 40, 816 | "metadata": {}, 817 | "outputs": [], 818 | "source": [ 819 | "def step(grid, num_comps=2):\n", 820 | " \"\"\"Simulate one time step.\n", 821 | " \n", 822 | " grid: NumPy array\n", 823 | " num_comps: number of possible destinations the agent looks at\n", 824 | " \n", 825 | " return: average number of same-color neighbors\n", 826 | " \"\"\"\n", 827 | " # FILL THIS IN\n", 828 | "\n", 829 | " return compute_num_same(grid).mean()" 830 | ] 831 | }, 832 | { 833 | "cell_type": "code", 834 | "execution_count": 41, 835 | "metadata": {}, 836 | "outputs": [], 837 | "source": [ 838 | "# Solution goes here" 839 | ] 840 | }, 841 | { 842 | "cell_type": "code", 843 | "execution_count": 42, 844 | "metadata": {}, 845 | "outputs": [], 846 | "source": [ 847 | "step(grid)" 848 | ] 849 | }, 850 | { 851 | "cell_type": "code", 852 | "execution_count": 43, 853 | "metadata": {}, 854 | "outputs": [], 855 | "source": [ 856 | "grid = make_grid(n=10)\n", 857 | "segs = [step(grid) for i in range(200)]\n", 858 | "plt.plot(segs)\n", 859 | "decorate_seg()" 860 | ] 861 | }, 862 | { 863 | "cell_type": "markdown", 864 | "metadata": {}, 865 | "source": [ 866 | "**Exercise:** Experiment with different values of `num_comps` and see what effect they have on the result." 867 | ] 868 | }, 869 | { 870 | "cell_type": "markdown", 871 | "metadata": {}, 872 | "source": [ 873 | "### Bigger grid\n", 874 | "\n", 875 | "Let's see how that looks on a bigger grid." 876 | ] 877 | }, 878 | { 879 | "cell_type": "code", 880 | "execution_count": 44, 881 | "metadata": {}, 882 | "outputs": [], 883 | "source": [ 884 | "grid = make_grid(n=50)\n", 885 | "im = draw(grid)" 886 | ] 887 | }, 888 | { 889 | "cell_type": "code", 890 | "execution_count": 45, 891 | "metadata": {}, 892 | "outputs": [], 893 | "source": [ 894 | "segs = [step(grid) for i in range(2000)]" 895 | ] 896 | }, 897 | { 898 | "cell_type": "code", 899 | "execution_count": 46, 900 | "metadata": {}, 901 | "outputs": [], 902 | "source": [ 903 | "plt.plot(segs)\n", 904 | "decorate_seg()" 905 | ] 906 | }, 907 | { 908 | "cell_type": "code", 909 | "execution_count": 47, 910 | "metadata": {}, 911 | "outputs": [], 912 | "source": [ 913 | "im = draw(grid)" 914 | ] 915 | }, 916 | { 917 | "cell_type": "markdown", 918 | "metadata": {}, 919 | "source": [ 920 | "The following figure shows three steps in the evolution of the grid." 921 | ] 922 | }, 923 | { 924 | "cell_type": "code", 925 | "execution_count": 48, 926 | "metadata": {}, 927 | "outputs": [], 928 | "source": [ 929 | "np.random.seed(17)\n", 930 | "\n", 931 | "grid = make_grid(n=50)\n", 932 | "\n", 933 | "# draw the initial grid\n", 934 | "plt.figure(figsize=(9,3))\n", 935 | "plt.subplot(1,3,1)\n", 936 | "draw(grid)\n", 937 | "\n", 938 | "# first update\n", 939 | "plt.subplot(1,3,2)\n", 940 | "for i in range(1000):\n", 941 | " step(grid)\n", 942 | "draw(grid)\n", 943 | "\n", 944 | "# second update\n", 945 | "plt.subplot(1,3,3)\n", 946 | "for i in range(1000):\n", 947 | " step(grid)\n", 948 | "draw(grid)\n", 949 | "\n", 950 | "plt.tight_layout()" 951 | ] 952 | }, 953 | { 954 | "cell_type": "markdown", 955 | "metadata": {}, 956 | "source": [ 957 | "**Exercise:** One more time, try out different values of `num_comps`." 958 | ] 959 | }, 960 | { 961 | "cell_type": "markdown", 962 | "metadata": {}, 963 | "source": [ 964 | "### Animation\n", 965 | "\n", 966 | "Let's see what the animation looks like." 967 | ] 968 | }, 969 | { 970 | "cell_type": "code", 971 | "execution_count": 48, 972 | "metadata": {}, 973 | "outputs": [], 974 | "source": [ 975 | "grid = make_grid(n=50)\n", 976 | "# animate(grid, frames=1000)" 977 | ] 978 | }, 979 | { 980 | "cell_type": "code", 981 | "execution_count": null, 982 | "metadata": {}, 983 | "outputs": [], 984 | "source": [] 985 | } 986 | ], 987 | "metadata": { 988 | "kernelspec": { 989 | "display_name": "Python 3 (ipykernel)", 990 | "language": "python", 991 | "name": "python3" 992 | }, 993 | "language_info": { 994 | "codemirror_mode": { 995 | "name": "ipython", 996 | "version": 3 997 | }, 998 | "file_extension": ".py", 999 | "mimetype": "text/x-python", 1000 | "name": "python", 1001 | "nbconvert_exporter": "python", 1002 | "pygments_lexer": "ipython3", 1003 | "version": "3.7.11" 1004 | } 1005 | }, 1006 | "nbformat": 4, 1007 | "nbformat_minor": 2 1008 | } 1009 | -------------------------------------------------------------------------------- /soln/02_workshop.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Game of Life\n", 8 | "\n", 9 | "Code examples from [Think Complexity, 2nd edition](https://thinkcomplex.com).\n", 10 | "\n", 11 | "Copyright 2019 Allen Downey, [MIT License](http://opensource.org/licenses/MIT)" 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import numpy as np\n", 21 | "import matplotlib.pyplot as plt\n", 22 | "import seaborn as sns" 23 | ] 24 | }, 25 | { 26 | "cell_type": "markdown", 27 | "metadata": {}, 28 | "source": [ 29 | "The following class implements the Game of Life." 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": 2, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "from scipy.signal import correlate2d\n", 39 | "from time import sleep\n", 40 | "from IPython.display import clear_output\n", 41 | "\n", 42 | "class Life:\n", 43 | " \"\"\"Implementation of Conway's Game of Life.\"\"\"\n", 44 | " kernel = np.array([[1, 1, 1],\n", 45 | " [1,10, 1],\n", 46 | " [1, 1, 1]])\n", 47 | "\n", 48 | " table = np.zeros(20, dtype=np.uint8)\n", 49 | " table[[3, 12, 13]] = 1\n", 50 | "\n", 51 | " def __init__(self, n, m=None):\n", 52 | " \"\"\"Initializes the attributes.\n", 53 | "\n", 54 | " n: number of rows\n", 55 | " m: number of columns\n", 56 | " \"\"\"\n", 57 | " m = n if m is None else m\n", 58 | " self.grid = np.zeros((n, m), np.uint8)\n", 59 | "\n", 60 | " def add_cells(self, row, col, *strings):\n", 61 | " \"\"\"Adds cells at the given location.\n", 62 | "\n", 63 | " row: top row index\n", 64 | " col: left col index\n", 65 | " strings: list of strings of 0s and 1s\n", 66 | " \"\"\"\n", 67 | " for i, s in enumerate(strings):\n", 68 | " self.grid[row+i, col:col+len(s)] = np.array([int(b) for b in s])\n", 69 | " \n", 70 | " def step(self):\n", 71 | " \"\"\"Executes one time step.\"\"\"\n", 72 | " c = correlate2d(self.grid, self.kernel, mode='same')\n", 73 | " self.grid = self.table[c]\n", 74 | " \n", 75 | " def draw(self):\n", 76 | " \"\"\"Draws the cells.\"\"\"\n", 77 | " \n", 78 | " a = self.grid\n", 79 | "\n", 80 | " n, m = a.shape\n", 81 | " plt.axis([0, m, 0, n])\n", 82 | " plt.xticks([])\n", 83 | " plt.yticks([])\n", 84 | "\n", 85 | " cmap = plt.get_cmap('Greens')\n", 86 | " options = dict(interpolation='nearest', alpha=0.8,\n", 87 | " vmin=0, vmax=1, origin='upper')\n", 88 | " options['extent'] = [0, m, 0, n]\n", 89 | " \n", 90 | " self.im = plt.imshow(a, cmap, **options)\n", 91 | "\n", 92 | " def animate(self, frames, interval=None):\n", 93 | " \"\"\"Animate the automaton.\n", 94 | "\n", 95 | " frames: number of frames to draw\n", 96 | " interval: time between frames in seconds\n", 97 | " \"\"\"\n", 98 | " plt.figure()\n", 99 | " try:\n", 100 | " for i in range(frames-1):\n", 101 | " self.draw()\n", 102 | " plt.show()\n", 103 | " if interval:\n", 104 | " sleep(interval)\n", 105 | " self.step()\n", 106 | " clear_output(wait=True)\n", 107 | " self.draw()\n", 108 | " plt.show()\n", 109 | " except KeyboardInterrupt:\n", 110 | " pass" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "metadata": {}, 116 | "source": [ 117 | "The following function creates a `Life` object and sets the initial condition using strings of `0` and `1` characters." 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": 3, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "def make_life(n, m, row, col, *strings):\n", 127 | " \"\"\"Makes a Life object.\n", 128 | " \n", 129 | " n, m: rows and columns of the Life array\n", 130 | " row, col: upper left coordinate of the cells to be added\n", 131 | " strings: list of strings of '0' and '1'\n", 132 | " \"\"\"\n", 133 | " life = Life(n, m)\n", 134 | " life.add_cells(row, col, *strings)\n", 135 | " return life" 136 | ] 137 | }, 138 | { 139 | "cell_type": "markdown", 140 | "metadata": {}, 141 | "source": [ 142 | "## Game of Life entities" 143 | ] 144 | }, 145 | { 146 | "cell_type": "markdown", 147 | "metadata": {}, 148 | "source": [ 149 | "Under GoL rules, some configurations are stable, meaning that they don't change from one time step to the next. These patterns are sometimes called \"still lifes\".\n", 150 | "\n", 151 | "One example is a \"beehive\", which is the following pattern of 6 cells." 152 | ] 153 | }, 154 | { 155 | "cell_type": "code", 156 | "execution_count": 4, 157 | "metadata": {}, 158 | "outputs": [ 159 | { 160 | "data": { 161 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAATMAAADrCAYAAAAFQnGoAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAADzUlEQVR4nO3XMU4bQQCG0XXkAnpoY4oU6aHzLXwPTsM9uIWRKKCmoIE2ucOkR5GCI6z1fnqvHE3xN/tpZzXGmACW7tvcAwC+gpgBCWIGJIgZkCBmQIKYAQnrQy5fXFyMzdXmWFsW7+X9de4JLNzP7z/mnnDynp+ef48xLj+eHxSzzdVmenjcf92qmO3tbu4JLNz+7n7uCSfvbH3+9rdzz0wgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzIAEMQMSxAxIEDMgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzIAEMQMSxAxIEDMgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzIAEMQMSxAxIEDMgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzIAEMQMSxAxIEDMgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzIAEMQMSxAxIEDMgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzICE9SGXX95fp+3t7lhbFm9/dz/3BBbO9/X//JkBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQsBpjfPry9c31eHjcH3HOsm1vd3NPYOH2d/dzTzh5Z+vzpzHGzcdzf2ZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQsBpjfP7yavVrmqa3480B+KfNGOPy4+FBMQM4VZ6ZQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAwh/V0TYFI5KBGwAAAABJRU5ErkJggg==\n", 162 | "text/plain": [ 163 | "
" 164 | ] 165 | }, 166 | "metadata": {}, 167 | "output_type": "display_data" 168 | } 169 | ], 170 | "source": [ 171 | "# beehive\n", 172 | "life = make_life(3, 4, 0, 0, '0110', '1001', '0110')\n", 173 | "life.draw()" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": {}, 179 | "source": [ 180 | "Here's what it looks like after one step:" 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": 5, 186 | "metadata": {}, 187 | "outputs": [ 188 | { 189 | "data": { 190 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAATMAAADrCAYAAAAFQnGoAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAADzUlEQVR4nO3XMU4bQQCG0XXkAnpoY4oU6aHzLXwPTsM9uIWRKKCmoIE2ucOkR5GCI6z1fnqvHE3xN/tpZzXGmACW7tvcAwC+gpgBCWIGJIgZkCBmQIKYAQnrQy5fXFyMzdXmWFsW7+X9de4JLNzP7z/mnnDynp+ef48xLj+eHxSzzdVmenjcf92qmO3tbu4JLNz+7n7uCSfvbH3+9rdzz0wgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzIAEMQMSxAxIEDMgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzIAEMQMSxAxIEDMgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzIAEMQMSxAxIEDMgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzIAEMQMSxAxIEDMgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzIAEMQMSxAxIEDMgQcyABDEDEsQMSBAzIEHMgAQxAxLEDEgQMyBBzICE9SGXX95fp+3t7lhbFm9/dz/3BBbO9/X//JkBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQsBpjfPry9c31eHjcH3HOsm1vd3NPYOH2d/dzTzh5Z+vzpzHGzcdzf2ZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAgpgBCWIGJIgZkCBmQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQsBpjfP7yavVrmqa3480B+KfNGOPy4+FBMQM4VZ6ZQIKYAQliBiSIGZAgZkCCmAEJYgYkiBmQIGZAwh/V0TYFI5KBGwAAAABJRU5ErkJggg==\n", 191 | "text/plain": [ 192 | "
" 193 | ] 194 | }, 195 | "metadata": {}, 196 | "output_type": "display_data" 197 | } 198 | ], 199 | "source": [ 200 | "life.step()\n", 201 | "life.draw()" 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "metadata": {}, 207 | "source": [ 208 | "Some patterns, called \"oscillators\", cycle through a series of configurations that returns to the initial configuration and repeats.\n", 209 | "\n", 210 | "A \"toad\" is an oscillator with period 2. Here's are its two configurations:" 211 | ] 212 | }, 213 | { 214 | "cell_type": "code", 215 | "execution_count": 6, 216 | "metadata": {}, 217 | "outputs": [ 218 | { 219 | "data": { 220 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWAAAACqCAYAAACTZZUqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAADSUlEQVR4nO3YsWkbARiG4VNQNnBay4ULVwG71RbaQ9NoD+3gQoFMEEPkIm7TuA6BS5UmtkwEkl6Bn6f1wX2Yn5dDk3EcBwBO70M9AOC9EmCAiAADRAQYICLAABEBBohM93n44uJinF3NjrWFd+5x+/j7+fn546nf66738/C0rSfsdHN5XU944a273ivAs6vZ8OXr5jCr4B+3n+9+Fe911/uZLxf1hJ02q3U94YW37tpPEAARAQaICDBARIABIgIMEBFggIgAA0QEGCAiwAARAQaICDBARIABIgIMEBFggIgAA0QEGCAiwAARAQaICDBARIABIgIMEBFggIgAA0QEGCAiwAARAQaICDBARIABIgIMEBFggIgAA0QEGCAiwAARAQaICDBAZFoPOIT5clFP4AC+/fievPfhaXuWN7RZresJrzrXXcNwni146659AQNEBBggIsAAEQEGiAgwQESAASICDBARYICIAANEBBggIsAAEQEGiAgwQESAASICDBARYICIAANEBBggIsAAEQEGiAgwQESAASICDBARYICIAANEBBggIsAAEQEGiAgwQESAASICDBARYICIAANEBBggIsAAEQEGiAgwQGS6z8MPT9thvlwcawsntFmt6wkv3N7f1RPgpHwBA0QEGCAiwAARAQaICDBARIABIgIMEBFggIgAA0QEGCAiwAARAQaICDBARIABIgIMEBFggIgAA0QEGCAiwAARAQaICDBARIABIgIMEBFggIgAA0QEGCAiwAARAQaICDBARIABIgIMEBFggIgAA0QEGCAiwAARAQaICDBARIABItN9Hr65vB42q/WxtkDiXO96vlzUE151jv+rv85x2+393c6/+QIGiAgwQESAASICDBARYICIAANEBBggIsAAEQEGiAgwQESAASICDBARYICIAANEBBggIsAAEQEGiAgwQESAASICDBARYICIAANEBBggIsAAEQEGiAgwQESAASICDBARYICIAANEBBggIsAAEQEGiAgwQESAASICDBARYIDIZBzH/394Mvk5DMOP483hnZuN4/jp1C911xzZzrveK8AAHI6fIAAiAgwQEWCAiAADRAQYICLAABEBBogIMEBEgAEifwDeAVNH279/0gAAAABJRU5ErkJggg==\n", 221 | "text/plain": [ 222 | "
" 223 | ] 224 | }, 225 | "metadata": {}, 226 | "output_type": "display_data" 227 | } 228 | ], 229 | "source": [ 230 | "# toad\n", 231 | "plt.subplot(1, 2, 1)\n", 232 | "life = make_life(4, 4, 1, 0, '0111', '1110')\n", 233 | "life.draw()\n", 234 | "\n", 235 | "plt.subplot(1, 2, 2)\n", 236 | "life.step()\n", 237 | "life.draw()" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "metadata": {}, 243 | "source": [ 244 | "Here's what it looks like as an animation." 245 | ] 246 | }, 247 | { 248 | "cell_type": "code", 249 | "execution_count": 7, 250 | "metadata": {}, 251 | "outputs": [ 252 | { 253 | "data": { 254 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOsAAADrCAYAAACICmHVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAADlElEQVR4nO3WIW5UYRiG0RkCqgJTkEwFAkyTtrjZRcM2uprugAV0A+hpUoGkQbQCLKaCVADJzwaGpJNwuTzhHPv94jVP7l2OMRbAv+/R3AOAhxErRIgVIsQKEWKFCLFCxONdHu/v74/VwWqqLUzg05ebuSdM4tWLl3NPmMTtze3Pu7u7J9tuO8W6OlgtLq82f2YVf8X67HTuCZPYnF/MPWESR4fH33938xsMEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFCxHKM8eDHe8+fjtdv1xPOmcfm/GLuCexofXY694RJfHj3/n58+7G37ebLChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghYjnGePDjkzcn4/JqM+GceazPTueeMJnN+cXcE9jB0eHx/fXH671tN19WiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFCxHKM8fDHy+XXxWLxebo58N9bjTGebTvsFCswH7/BECFWiBArRIgVIsQKEWKFCLFChFghQqwQ8QuQzT4ElA7tvgAAAABJRU5ErkJggg==\n", 255 | "text/plain": [ 256 | "
" 257 | ] 258 | }, 259 | "metadata": {}, 260 | "output_type": "display_data" 261 | } 262 | ], 263 | "source": [ 264 | "life.step()\n", 265 | "life.animate(frames=4, interval=0.5)" 266 | ] 267 | }, 268 | { 269 | "cell_type": "markdown", 270 | "metadata": {}, 271 | "source": [ 272 | "Some patterns repeat, but after each cycle, they are offset in space. They are called \"spaceships\".\n", 273 | "\n", 274 | "A \"glider\" is a spaceship that translates one unit down and to the right with period 4. " 275 | ] 276 | }, 277 | { 278 | "cell_type": "code", 279 | "execution_count": 8, 280 | "metadata": {}, 281 | "outputs": [ 282 | { 283 | "data": { 284 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAq8AAACFCAYAAAB1yRHkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAAD6ElEQVR4nO3aPU4VURiA4cFcE+2x5VpQUJloyy7YB6uhcBfs4ppYaCuJWECrBbUxGTfgz9zozPDC87RMzjlcPk7eTO7BOI4DAAAUPFn7AAAAMJV4BQAgQ7wCAJAhXgEAyBCvAABkiFcAADI2+zx8eHg4bl9u5zrLL13dXi+63zAMw8nR8eJ7Lu3L9Zcfd3d3T+dYe405YT4fP3z8No7jiznWNisPhzuFqdwpTPGnO2WveN2+3A7v3u/+z6kmOj0/W3S/YRiG3cXl4nsu7fWrN9/nWnuNOWE+zzbPb+Za26w8HO4UpnKnMMWf7hRfGwAAIEO8AgCQIV4BAMgQrwAAZIhXAAAyxCsAABniFQCADPEKAECGeAUAIEO8AgCQIV4BAMgQrwAAZIhXAAAyxCsAABniFQCADPEKAECGeAUAIEO8AgCQIV4BAMgQrwAAZIhXAAAyxCsAABniFQCADPEKAECGeAUAIEO8AgCQsVn7AH+zu7hcfM/T87PF91zj95zL1e314p+hOeE+Mys9/mbcZ499Pr15BQAgQ7wCAJAhXgEAyBCvAABkiFcAADLEKwAAGeIVAIAM8QoAQIZ4BQAgQ7wCAJAhXgEAyBCvAABkiFcAADLEKwAAGeIVAIAM8QoAQIZ4BQAgQ7wCAJAhXgEAyBCvAABkiFcAADLEKwAAGeIVAIAM8QoAQIZ4BQAgQ7wCAJAhXgEAyBCvAABkbPZ5+Or2ejg9P5vrLDwQJ0fHw+7ictE9zWXTGnfK0rNJ0xpz4h5jqscwn59uPv/2Z968AgCQIV4BAMgQrwAAZIhXAAAyxCsAABniFQCADPEKAECGeAUAIEO8AgCQIV4BAMgQrwAAZIhXAAAyxCsAABniFQCADPEKAECGeAUAIEO8AgCQIV4BAMgQrwAAZIhXAAAyxCsAABniFQCADPEKAECGeAUAIEO8AgCQIV4BAMgQrwAAZIhXAAAyNvs8fHJ0POwuLuc6C6T4X/h3a9wpp+dni+5H02OZkzXusWdvny++55wey6zcJ968AgCQIV4BAMgQrwAAZIhXAAAyxCsAABniFQCADPEKAECGeAUAIEO8AgCQIV4BAMgQrwAAZIhXAAAyxCsAABniFQCADPEKAECGeAUAIEO8AgCQIV4BAMgQrwAAZIhXAAAyxCsAABniFQCADPEKAECGeAUAIEO8AgCQIV4BAMgQrwAAZIhXAAAyDsZxnP7wwcHXYRhu5jsOC9qO4/hijoXNyYNjVpjCnDCVWWGK387JXvEKAABr8rUBAAAyxCsAABniFQCADPEKAECGeAUAIEO8AgCQIV4BAMgQrwAAZIhXAAAyfgKDq51RKZkAegAAAABJRU5ErkJggg==\n", 285 | "text/plain": [ 286 | "
" 287 | ] 288 | }, 289 | "metadata": {}, 290 | "output_type": "display_data" 291 | } 292 | ], 293 | "source": [ 294 | "# glider\n", 295 | "glider = ['010', '001', '111']\n", 296 | "life = make_life(4, 4, 0, 0, *glider)\n", 297 | "\n", 298 | "plt.figure(figsize=(12,5))\n", 299 | "\n", 300 | "for i in range(1, 6):\n", 301 | " plt.subplot(1, 5, i)\n", 302 | " life.draw()\n", 303 | " life.step()" 304 | ] 305 | }, 306 | { 307 | "cell_type": "markdown", 308 | "metadata": {}, 309 | "source": [ 310 | "Here's an animation showing glider movement." 311 | ] 312 | }, 313 | { 314 | "cell_type": "code", 315 | "execution_count": 9, 316 | "metadata": {}, 317 | "outputs": [ 318 | { 319 | "data": { 320 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOsAAADrCAYAAACICmHVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAADKklEQVR4nO3WMUoDURhG0RnRHcR6XIGgrbtwHy5TwRVYqIW1TWoRngswgQQM8ZJz2v8VX3OZmccYE/D/nR17ALAbsUKEWCFCrBAhVogQK0Sc7/N4tVqN5Wo51BY4ee9v79/r9fpi022vWJerZXp6fvybVcAvN9e3X9tufoMhQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIOD/2ADi0u4f7Y0/Y2cvH69abLytEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEfMYY/fH8/w5TdPH4ebAyVvGGJebDnvFChyP32CIECtEiBUixAoRYoUIsUKEWCFCrBAhVoj4AfRzH5tOp79CAAAAAElFTkSuQmCC\n", 321 | "text/plain": [ 322 | "
" 323 | ] 324 | }, 325 | "metadata": {}, 326 | "output_type": "display_data" 327 | } 328 | ], 329 | "source": [ 330 | "life = make_life(10, 10, 0, 0, '010', '001', '111')\n", 331 | "life.animate(frames=32, interval=0.2)" 332 | ] 333 | }, 334 | { 335 | "cell_type": "markdown", 336 | "metadata": {}, 337 | "source": [ 338 | "**Exercise:** If you start GoL from a random configuration, it usually runs chaotically for a while and then settles into stable patterns that include blinkers, blocks, and beehives, ships, boats, and loaves.\n", 339 | "\n", 340 | "For a list of common \"natually\" occurring patterns, see Achim Flammenkamp, \"[Most seen natural occurring ash objects in Game of Life](http://wwwhomes.uni-bielefeld.de/achim/freq_top_life.html)\",\n", 341 | "\n", 342 | "Start GoL in a random state and run it until it stabilizes (try 1000 steps).\n", 343 | "What stable patterns can you identify?\n", 344 | "\n", 345 | "Hint: use `numpy.random.randint`." 346 | ] 347 | }, 348 | { 349 | "cell_type": "code", 350 | "execution_count": 10, 351 | "metadata": {}, 352 | "outputs": [ 353 | { 354 | "data": { 355 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOsAAADrCAYAAACICmHVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAAHcklEQVR4nO3dv44bRRwHcB+CKgVNoMxRpEiaSMmly1sgXoOn4T14i4t0D0BAAgpokVAqCoS0VBcOx3/W65nZ+Y4/n4qcjb1e+7ffmdnZ2atpmjZA/z5ZewOAeRQrhFCsEEKxQgjFCiEUK4T49JQnP378eLr+6rrWtgzrp99/+fDfz548bfZeLd6Psn795dd/3r9//9mux04q1uuvrjdv7243b779+qPHbr/7fuHmzbP9nrXfr4T7bf7z7vajv9Xa/sT9xH9evnj1977HNIMhxEnJyvnuk65WwkrSdmq3krZJVgihWCHEombwGk2txObddpN312Mwl2SFEAaYGpCih9VueTx8/eTvQrJCCMm6xaSCcew6tVLydEvr34ZkhRCSldVpvcwjWSFE9WRtPSXrXCnbyXG7znOv9f2WGAuRrBCiWrJuJ2pawjKOUX5zkhVCKFYI4dQNNFCiKS5ZIUS1ZF1rYMnlaIxKskKI6n3WHhLVaaPLMur3LVkhRPxo8JyjaIkj7K7kLvn6cIxkhRBxybqdpK36pdKTtUlWCKFYIcSqzeBTrvE71szVTF2uRBfinNcofb3pqL8FyQohYgaYto+WJdPg0Ptw2L7rlh/+jTIkK4RYNVnP6d+MctQe7fNQj2SFEDF91m3piXRo+mJr5+zDEpNSevgOE+7EIFkhRFyyljzi9XD0rD1dcskFCEv+nx72ZQk9j2pLVghxNU3T7CffvL6Z3t7dVtwcWNfa595fvnj117sf3j3a9ZhkhRCKFULEDTAdY0WH06WfBiup530gWSGEYoUQihVCDNdn7bnPwZhane6RrBBiuGTl/1qtq0x9khVCSNbB7DvPvMY0uh7P3+7bPyUuE6xNskIIyTpTUkpsNvsvvat90fuuS8t62nc9bMNSkhVCKFYIoRkcZM7gyLFm7hrNwGMrUiQ3TVuSrBAiLlkTjsalt/GU19t+zqGBpRr7cFfKH1u3qcf1jnokWSFETLLuOwXR6kjcagLBkvfuNZnmbkvvfe5eSFYIEZOsrU7q92Tuur5L+rJrmDNBo8bdAkchWSFETLLeSzjClh4FrvX6a5mTlhL1Y5IVQihWCLFqM1hT5z+X2CQ89LlG+6wlSFYI0TxZe7/esUej7JdLPP1WkmSFECfd8vHRl59Pz7958+HfS474a64F1Or9qGf0VphbPsIAmvdZWx4R9x2FRz86j8h3JlkhxknJ+uzJ06gjW6vRZkf9+g59l5cyJiFZIUTcRP4laiXs2hfEX6I59+wZNWklK4RQrBDiIprB90ZpDo1myUSZc9asmvN6Pf5WJCuEuKhkLc3A0noucV9LVghx0kT+m9c309u724qbw0O1+1FLWgSjrgvVCxP5YQD6rBfm3Iv/Jeh6JCuE6DJZS59DS3VJn5XjJCuEUKwQostmsOZfPafc7Ji+SFYI0WWy0oZEzSJZIYRkLWity6zWWIuZ9iQrhJCsBVifmLnO+U1IVgghWStonaiSu3/7VsJ8+LdjJCuEkKwFWN6FFiQrhFCsECK2GdzjOq89bMPoUieAlOgqSVYIEZussM+SlUZarU5yzmtJVghh3WDoiHWDYQCKlbO8+fbrg/29XiVut2KFEF2NBvd47hR6IVkhhGKFEF00gw9d6wcjWjJtUrJCiC6S1fWguVK/qxLb3fr3KlkhRBfJei/1KM1lKbGekkvkYGBdJSskWGuMRbJCiK6S1XRDkrT+fUpWCNFVssJoSrYWJSuEUKwQoqtmsAEl2E+yQoiukhVGU7K1KFkhhGKFEIoVQuizdib1LmnMc849dSQrhJCs0JDphnABFCuE0AzujMGksZRcTUKyQgjFShGJt1BMo1ghhD4rZymxhi7zSFYIIVmhIpfIwQWSrJzFHQDbkawQQrJShEStT7JCCMUKIRQrhFCsEEKxQgjFCiEUK4RQrBBCsUIIxQohTDdkryWrx7ujQD2SFUJIVvaSiH2RrBBCsUIIzeDG9g3ajNLkHOVz9EiyQgjJ2pjkYSnJCiEkK/FGHwe4J1khhGQl3mgJuo9khRDNk9VEb/i/hzXx428/732eZIUQihVCNGsGH7pxkZsawXGSFUJcTdM0+8k3r2+mt3e3Z71h6gCT9KeFly9e/fXuh3ePdj0mWSFE81M3acl0aB0i1rNkfah0khVCmG5IpFHT8xDJCiGKJevoo6W1P1fqKHnPRvtNSlYIcVayPkyD+6PXaEezVolqZhfHSFYIoVghhFM3ndnVtYDNRrJCjGIT+VsPhmyf6khPIYNJbDYm8sMQivVZJcJ57D+OkawQ4qQ+66MvP5+ef/Pmw7+lAZSlzwoDUKwQQrFCCMUKIU46dfPsyVODSiw22kSW1iQrhDCRn+r2TaU0xfI0khVCSFaq215FZPvvzCNZIYRkpZlLTNKS/XLJCiEUK4RQrBBCsUIIxQohFCuEWHTqxk2UDnPyn3slv3vJCiEUK4RQrBDipNUNr66u/thsNr/V2xy4eNfTNH2x64GTihVYj2YwhFCsEEKxQgjFCiEUK4RQrBBCsUIIxQohFCuE+BeJ5z6WtIL3nQAAAABJRU5ErkJggg==\n", 356 | "text/plain": [ 357 | "
" 358 | ] 359 | }, 360 | "metadata": {}, 361 | "output_type": "display_data" 362 | } 363 | ], 364 | "source": [ 365 | "# Solution\n", 366 | "\n", 367 | "n = 100\n", 368 | "life = Life(n)\n", 369 | "life.grid = np.random.randint(2, size=(n, n), dtype=np.uint8)\n", 370 | "life.animate(frames=1000)" 371 | ] 372 | }, 373 | { 374 | "cell_type": "markdown", 375 | "metadata": {}, 376 | "source": [ 377 | "### Methuselas\n", 378 | "\n", 379 | "Most initial conditions run for a short time and reach a steady state. But some initial conditional run for a surprisingly long time; they are called [Methuselahs](https://en.wikipedia.org/wiki/Methuselah_(cellular_automaton))\n", 380 | "\n", 381 | "One of the simplest examples is the \"r-pentomino\", which starts with only five live cells, but it runs for 1103 steps before stabilizing." 382 | ] 383 | }, 384 | { 385 | "cell_type": "code", 386 | "execution_count": 11, 387 | "metadata": {}, 388 | "outputs": [ 389 | { 390 | "data": { 391 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOsAAADrCAYAAACICmHVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAADYElEQVR4nO3WIW5UUQCG0Tdk2MFgeRUILNjZxexjVtMdsIBuAFUxJAh8RQ21iAoSBBAuG2iTTtLJ69eeY+8Vv/ly72qMMQFP36ulBwAPI1aIECtEiBUixAoRYoWI9TGXN5vNmM/mU23Ju7q5XnoCcb9uf/4df/69vuvsqFjns3n68vXwOKueoe1+t/QE4r59+vz7vjPfYIgQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsULE+pjLVzfX03a/O9UWXoDD+cXSE560D5cf7z3zskKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoR62Muv3/7bjqcX5xqS952v1t6As+YlxUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtErMYYD7+8Wv2Ypun76ebAizePMd7cdXBUrMByfIMhQqwQIVaIECtEiBUixAoRYoUIsUKEWCHiP64xK8eWIkLLAAAAAElFTkSuQmCC\n", 392 | "text/plain": [ 393 | "
" 394 | ] 395 | }, 396 | "metadata": {}, 397 | "output_type": "display_data" 398 | } 399 | ], 400 | "source": [ 401 | "# r pentomino\n", 402 | "rpent = ['011', '110', '010']\n", 403 | "life = make_life(3, 3, 0, 0, *rpent)\n", 404 | "life.draw()" 405 | ] 406 | }, 407 | { 408 | "cell_type": "markdown", 409 | "metadata": {}, 410 | "source": [ 411 | "Here are the start and finish configurations." 412 | ] 413 | }, 414 | { 415 | "cell_type": "code", 416 | "execution_count": 12, 417 | "metadata": {}, 418 | "outputs": [ 419 | { 420 | "data": { 421 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWAAAACqCAYAAACTZZUqAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAAEbklEQVR4nO3dPW4TYRSG0S8o7CC0MQUFFRJpvQvvw6vxPrIHCiOxAiKRFKSlSY2QhgKNiCeY2BnPvPNzToeUyF+QeXxzPXjOqqoqAPTvVfoAAHMlwAAhAgwQIsAAIQIMECLAACHnx3zxxcVFtXi76OoszNzd7d2vh4eH130/7pSf1zf3t6WUUt5fvguf5Hn1WUsZx3kP9b/n9VEBXrxdlM9ftqc5FTR8/HD1M/G4U35eL9erUkop2811+CTPq89ayjjOe6j/Pa+PCjAwLmMK2ZjOeip2wAAhAgwQIsAAIQIMECLAACECDBAiwAAhAgwQIsAAIQIMECLAACECDBAiwAAhAgwQIsAAIQIMECLAACECDBAiwMzezf1tWa5XO/ckgz4IMECIm3Iye+8v383yhpDkmYABQgQYIESAAUIEGCBEgAFCBBggRIABQgQYIESAAUIEGCBEgAFCBBggRIABQgQYIESAAUIEGCBEgAFC3BED6Nzj++25+8hfAgx0arle7US3+ec5s4IACBFgoFPbzXVZrlc7awj+sIIAOlevHKwfdpmAYUKGPmWK7y4TMExEPV3WEW6+8VUTweEwAQOECDBMxOPpl3GwgoAJ2bdesHYYJhMwQIgAA4RYQQCj19x9j2XlIsBAXNuAjiW4TQIMxI01oG3ZAQOzM5TPphBggBABBgixAwZmZyg7ZxMwQIgAA4QIMECIAO8xlMtUgOnyJlyDD66m6V8fcA6nYAJu2G6u/UOjlPL3tyDPB7oiwAAhAryHqYfa0Kdg71eMlx0w7DHk6NYevzgM/YWCp0zAACEmYBixfbehb3IlxzCZgAFCTMAQdIrJ9LnvtSceLhMwQIgJGIL6mEYP3RPTPwGGGRDeYbKCAAgxAcOJ+UAnDiXAQIQXKgGGk5trTDieHTBAiAkYiPCbggDDrNi77kr/fQgwzIjoDosAA7OVfkHyJhxAiAADhAgwQIgAA7xQ25uhCjBAiAADvEB9d5E2U7DL0IBRS/9nijZMwAAvUE+/baJvAgZGLTn1tn1sEzBAiAADhFhBALPVvIKh73WGAAOdq0M3tKsU0uexggA6VV8p0Paa2SkSYIAQAQYIsQMGOvV49ZDeuQ6NAAOdE95/s4IACBFg4InleuWKhR4IMECIHTDwhJ1tP0zAACECDBAiwAAhAgwQIsAAIQIMECLAACECDBAiwAAhAgwQIsAAIQIMECLAACECDBAiwAAhAgwQIsAAIQIMECLAACECDBAiwAAhAgwQIsAAIQIMECLAACECDBAiwAAhAgwQIsAAIQIMECLAACECDBAiwAAhAgwQIsAAIQIMECLAACECDBAiwAAhAgwQcp4+AMAULderUkopX79/2/s1JmCAEBMw0Eo96ZVSynZzHTzJadU/10t/pvr7Pn662vs1JmCAEAEGCDmrqurwLz47+1FK+d7dcZi5RVVVb/p+UM9rOrb3eX1UgAE4HSsIgBABBggRYIAQAQYIEWCAEAEGCBFggBABBggRYICQ36wt/RppPei2AAAAAElFTkSuQmCC\n", 422 | "text/plain": [ 423 | "
" 424 | ] 425 | }, 426 | "metadata": {}, 427 | "output_type": "display_data" 428 | } 429 | ], 430 | "source": [ 431 | "# r pentomino\n", 432 | "rpent = ['011', '110', '010']\n", 433 | "\n", 434 | "plt.subplot(1, 2, 1)\n", 435 | "life = make_life(120, 120, 50, 45, *rpent)\n", 436 | "life.draw()\n", 437 | "\n", 438 | "for i in range(1103):\n", 439 | " life.step()\n", 440 | "\n", 441 | "plt.subplot(1, 2, 2)\n", 442 | "life.draw()" 443 | ] 444 | }, 445 | { 446 | "cell_type": "markdown", 447 | "metadata": {}, 448 | "source": [ 449 | "And here's the animation that shows the steps." 450 | ] 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": 13, 455 | "metadata": {}, 456 | "outputs": [ 457 | { 458 | "data": { 459 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOsAAADrCAYAAACICmHVAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8rg+JYAAAACXBIWXMAAAsTAAALEwEAmpwYAAAExUlEQVR4nO3dQW4TZwCGYaeiN0i3hAULVpVgm1vkHjlN7pE7dJFKPUGRCgvYssm6quQuEJVjxamdzHjm9TzPCkiEbNDL909ixmfr9XoFzN9PUz8AYD9ihQixQoRYIUKsECFWiHh1yCefn5+vL95cjPVYTtbHr58e/Pzd67cTPZLjWurzfonPnz7/c39///NjHzso1os3F6vf/7gb5lEtyOX11YOf393cTvRIjmupz/sl3v/64e9dH3MMhoiDlpXnWeqiLPV5j8WyQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIeKgt8/4+PXTgzcb8vYIcDyWFSLEChFihYiDrlnfvX7rOhUmYlkhQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFCxEE3+Wa5Nt+QbLXypmRTsKwQYVl50o9F3V7SXb/OeCwrRFhWnvRjOS3p9MTKXkQ7PcdgiBDrwLa/xXFq7m5urepExAoRrlkHsnkt93/XdZvra6XYl2WFCMs6kO2vlsLQLCtEWNaB7XMN6jqV57CsECFWiHAMJuexL+It4dLCskKEZWVwu759NdT6LWFFH2NZIcKyMrilLt/YLCtEWFYWp3qnRssKEWKFCMdgFqdy7N1mWSFCrBAhVogQK0SIFSLEChFihYjRvs9afUkXzNXgsW7f4NqtOWEYjsEQMfox+NSPv0t5nkzPskLE4Mu6lIXZdW2+lOfP8VlWiBArRIgVIvzn82d6ybWq61uew7JCxEkv6+arp8ZasZcs6uX1lXVlb5YVIsQKESd9DJ7bEdMLKHgJywoRJ72sc2VReQ7LChGzX1Z3nIDvLCtEzH5ZmT+nn+OYfaz+4uE7x2CImP2yMn9OP8dhWSHCsk7osXsqW6njO8b/zhqCZYUIyzqhOf8rzvxYVoiwrCxe5YRjWSFCrBAhVogQK0SIFUZweX01+BuJixUifOsGBrZ58/Yh72RpWSHCspJRecH9WCwrRFhWGNjdze0o77ogVjJKR98xHqtjMESIFSLEChGuWVm8yr2wLCtEWFb2cspvAF15TpYVIiwrT9pe1FNe2LmzrBAhVohwDOZJjr/zYVkhwrKyF4s6PcsKEZY1aPvlcVZvGSwrRIgVIsQKEa5Zg1yjLpNlhQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFiFdTPwBYusvrq/9+/OeXv3Z+nmWFCLFChGPwAm0eu1ar1eru5naiR9Kx+Wc29J/X5u/3/rcPOz/PskKEWCFCrBBxtl6v9//ks7Nvq9Xqy3gPBxbvYr1e//LYBw6KFZiOYzBEiBUixAoRYoUIsUKEWCFCrBAhVogQK0T8C9ek669DwKqEAAAAAElFTkSuQmCC\n", 460 | "text/plain": [ 461 | "
" 462 | ] 463 | }, 464 | "metadata": {}, 465 | "output_type": "display_data" 466 | } 467 | ], 468 | "source": [ 469 | "life = make_life(120, 120, 50, 45, *rpent)\n", 470 | "life.animate(frames=1200)" 471 | ] 472 | }, 473 | { 474 | "cell_type": "markdown", 475 | "metadata": {}, 476 | "source": [ 477 | "### Rabbits\n", 478 | "\n", 479 | "Another example is [rabbits](https://web.archive.org/web/20081221152607/http://www.argentum.freeserve.co.uk/lex_r.htm#rabbits), which starts with only nine cells and runs 17331 steps before reaching steady state." 480 | ] 481 | }, 482 | { 483 | "cell_type": "code", 484 | "execution_count": 19, 485 | "metadata": {}, 486 | "outputs": [ 487 | { 488 | "name": "stdout", 489 | "output_type": "stream", 490 | "text": [ 491 | "Downloaded Cell2D.py\n" 492 | ] 493 | } 494 | ], 495 | "source": [ 496 | "from os.path import basename, exists\n", 497 | "\n", 498 | "def download(url):\n", 499 | " filename = basename(url)\n", 500 | " if not exists(filename):\n", 501 | " from urllib.request import urlretrieve\n", 502 | " local, _ = urlretrieve(url, filename)\n", 503 | " print('Downloaded ' + local)\n", 504 | " \n", 505 | "download('https://github.com/AllenDowney/ComplexityScience/raw/master/Cell2D.py')\n", 506 | "download('https://github.com/AllenDowney/ComplexityScience/raw/master/Life.py')\n", 507 | "download('https://github.com/AllenDowney/ComplexityScience/raw/master/LifeRabbits.py')" 508 | ] 509 | }, 510 | { 511 | "cell_type": "markdown", 512 | "metadata": {}, 513 | "source": [ 514 | "To run my implementation of rabbits, open a terminal and run\n", 515 | "\n", 516 | "```\n", 517 | "python LifeRabbits.py\n", 518 | "```" 519 | ] 520 | }, 521 | { 522 | "cell_type": "markdown", 523 | "metadata": {}, 524 | "source": [ 525 | "### Conway's conjecture\n", 526 | "\n", 527 | "Patterns like these prompted Conway to conjecture, as a challenge, that there are no initial conditions where the number of live cells grows unboundedly.\n", 528 | "\n", 529 | "Gosper's glider gun was the first entity to be discovered that produces an unbounded number of live cells, which refutes Conway's conjecture." 530 | ] 531 | }, 532 | { 533 | "cell_type": "code", 534 | "execution_count": 27, 535 | "metadata": {}, 536 | "outputs": [], 537 | "source": [ 538 | "glider_gun = [\n", 539 | " '000000000000000000000000100000000000',\n", 540 | " '000000000000000000000010100000000000',\n", 541 | " '000000000000110000001100000000000011',\n", 542 | " '000000000001000100001100000000000011',\n", 543 | " '110000000010000010001100000000000000',\n", 544 | " '110000000010001011000010100000000000',\n", 545 | " '000000000010000010000000100000000000',\n", 546 | " '000000000001000100000000000000000000',\n", 547 | " '000000000000110000000000000000000000'\n", 548 | "]" 549 | ] 550 | }, 551 | { 552 | "cell_type": "markdown", 553 | "metadata": {}, 554 | "source": [ 555 | "Here's the initial configuration:" 556 | ] 557 | }, 558 | { 559 | "cell_type": "code", 560 | "execution_count": 28, 561 | "metadata": {}, 562 | "outputs": [ 563 | { 564 | "data": { 565 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAWAAAAByCAYAAABtCAtoAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAACmUlEQVR4nO3dMU4bQRiAUTtyAT3U5g7Q5Ra5R06Te+QWIFHAHaiTO0x6FMlrtPa3Xr9Xriz5xzafRtaMdzvG2ABwft/qAQCulQADRAQYICLAABEBBojsjnnw3d3d2D/sTzULwCq9v73/HWPcf75+VID3D/vNy+vzfFMBXIGb3e3H/677CgIgIsAAEQEGiAgwQESAASICDBARYIDIUfuAgfP5/vPHwcc8//p9hkk4FStggIgAA0QEGCAiwAARAQaICDBARIABIgIMEHEQA07AIQqmsAIGiAgwQESAASICDBARYICIAANEBBggYh/wlZqyT/WQNe5jneN12WzmeW3W+PouTf1/YAUMEBFggIgAA0QEGCAiwAARAQaICDBARIABIg5irNCSfgx8SbPA0lgBA0QEGCAiwAARAQaICDBARIABIgIMEBFggMjsBzHqX5hfu0s72DBllkv7m1iP+nNlBQwQEWCAiAADRAQYICLAABEBBogIMEBEgAEi7ojBl53zAMWh55rrec4175TnckBl/ayAASICDBARYICIAANEBBggIsAAEQEGiAgwQGT2gxg2hl+Pc77Xl/a5urR5aVgBA0QEGCAiwAARAQaICDBARIABIgIMEBFggMh2jDH5wY9Pj+Pl9fmE4zCHJd1JYUmzQOVmd/s2xnj6fN0KGCAiwAARAQaICDBARIABIgIMEBFggIh9wFdqyv7cQ+zfhWnsAwZYGAEGiAgwQESAASICDBARYICIAANEBBggsqsHoOEQBfSsgAEiAgwQEWCAiAADRAQYICLAABEBBogIMEDkqDtibLfbP5vN5uN04wCs0n6Mcf/54lEBBmA+voIAiAgwQESAASICDBARYICIAANEBBggIsAAEQEGiPwD5/p1w9cu73MAAAAASUVORK5CYII=\n", 566 | "text/plain": [ 567 | "
" 568 | ] 569 | }, 570 | "metadata": {}, 571 | "output_type": "display_data" 572 | } 573 | ], 574 | "source": [ 575 | "life = make_life(11, 38, 1, 1, *glider_gun)\n", 576 | "life.draw()" 577 | ] 578 | }, 579 | { 580 | "cell_type": "markdown", 581 | "metadata": {}, 582 | "source": [ 583 | "And here's what it looks like running:" 584 | ] 585 | }, 586 | { 587 | "cell_type": "code", 588 | "execution_count": 30, 589 | "metadata": {}, 590 | "outputs": [ 591 | { 592 | "data": { 593 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOsAAADrCAYAAACICmHVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAAELElEQVR4nO3dPW4TURhAURtBRRtqZwVI/HTsgn2wGvbBLhKJBUAFBTVNKgqENJRIziCPHTueOz6njC0zcnT15T2ePethGFbA/D059wUA04gVIsQKEWKFCLFChFgh4uk+T766uho215tTXQtcvO/fvv+5u7t7NvbYXrFurjer2883x7kq4J5XL1///t9j/gyGCLFChFghQqwQIVaIECtE7PVfN/S9+/D+3s9uPn46w5WwL5MVIsQKEWKFCGtWRtexu1jnPj6TFSLEChFihYhFr1m312LWWePvwSFr1sd6b6dc26X8Xk1WiBArRIgVIsQKEYvZYJpyQN2myDRTru2QTSkexmSFCLFChFgh4uhr1l1rmUPXasd43V1r2Kmvs++/UzOn9Wj9vTwmkxUixAoRYoUIsULEYg5FzMmUwxeHPGfbIZsvx9o8svHz+ExWiBArRIgVIo6+ZrWWmXb44pDX2XasQx1+Zw0mK0SIFSLEChHrYRgmP/nN2zfD7eebE17O4eb04fNTqV8/u716+frX1y9fn489ZrJChFghQqwQIVaIWMxB/kMPy5/LnK+NeTJZIUKsECFWiFjMoQhYAociYAHEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFCxGI+fM7pHOub/3kYkxUixAoRYoUIa1Z2qn0Z3VKZrBAhVogQK0SIFSJsMHHP2CEIzs9khQixQoRYIcKalXsccJgnkxUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghwrcbkjJ2t4BL+TZGkxUixAoRYoUIa1ZmZXtNeinr0SlMVogQK0SIFSLEChE2mJiV7Q2lsUMQl8pkhQixQoRYIcKalVlzKOIfkxUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRPjwOUx07rsFmKwQIVaIECtEiBUibDDBRFPuFnDKTSeTFSLEChFihQhrVhgxx7vXmawQIVaIECtEiBUibDDBiDneatJkhQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiFgPwzD9yev1z9Vq9eN0lwMXbzMMw4uxB/aKFTgffwZDhFghQqwQIVaIECtEiBUixAoRYoUIsULEXz7ZqmK81wW0AAAAAElFTkSuQmCC\n", 594 | "text/plain": [ 595 | "
" 596 | ] 597 | }, 598 | "metadata": {}, 599 | "output_type": "display_data" 600 | } 601 | ], 602 | "source": [ 603 | "life = make_life(50, 50, 2, 2, *glider_gun)\n", 604 | "life.animate(frames=500)" 605 | ] 606 | }, 607 | { 608 | "cell_type": "markdown", 609 | "metadata": {}, 610 | "source": [ 611 | "**Exercise:** In this animation, you might notice that the boundary interferes with the escaping gliders, but it doesn't affect the behavior of the gun.\n", 612 | "\n", 613 | "For fun, change `step` so it passes the keywords `boundary='wrap'` to `correlate2d`, and see what happens.\n", 614 | "\n", 615 | "Don't forget to remove it before you proceed." 616 | ] 617 | }, 618 | { 619 | "cell_type": "markdown", 620 | "metadata": {}, 621 | "source": [ 622 | "### Puffer train\n", 623 | "\n", 624 | "Another way to \"refute\" Conway's conjecture is a [puffer train](https://en.wikipedia.org/wiki/Puffer_train)." 625 | ] 626 | }, 627 | { 628 | "cell_type": "code", 629 | "execution_count": null, 630 | "metadata": {}, 631 | "outputs": [], 632 | "source": [ 633 | "download('https://github.com/AllenDowney/ComplexityScience/raw/master/LifePuffer.py')" 634 | ] 635 | }, 636 | { 637 | "cell_type": "markdown", 638 | "metadata": {}, 639 | "source": [ 640 | "To see a puffer train run, open a terminal and run\n", 641 | "\n", 642 | "```\n", 643 | "python LifePuffer.py\n", 644 | "```" 645 | ] 646 | }, 647 | { 648 | "cell_type": "markdown", 649 | "metadata": {}, 650 | "source": [ 651 | "### Implementing Game of Life\n", 652 | "\n", 653 | "This section explains how the implementaion we've been using works.\n", 654 | "\n", 655 | "As an example, I'll start with an array of random cells:" 656 | ] 657 | }, 658 | { 659 | "cell_type": "code", 660 | "execution_count": 31, 661 | "metadata": {}, 662 | "outputs": [ 663 | { 664 | "name": "stdout", 665 | "output_type": "stream", 666 | "text": [ 667 | "[[0 1 1 1 1 0 0 1 1 0]\n", 668 | " [1 1 0 1 0 1 1 1 0 1]\n", 669 | " [0 1 1 0 0 0 1 1 1 0]\n", 670 | " [0 1 0 0 1 1 1 1 0 1]\n", 671 | " [1 1 0 0 0 0 0 0 0 0]\n", 672 | " [0 1 1 0 1 1 0 0 0 0]\n", 673 | " [0 0 1 0 1 0 1 0 1 0]\n", 674 | " [0 0 0 1 1 0 1 1 1 0]\n", 675 | " [0 1 1 1 0 1 0 0 1 0]\n", 676 | " [1 0 1 0 1 0 0 1 1 1]]\n" 677 | ] 678 | } 679 | ], 680 | "source": [ 681 | "a = np.random.randint(2, size=(10, 10), dtype=np.uint8)\n", 682 | "print(a)" 683 | ] 684 | }, 685 | { 686 | "cell_type": "markdown", 687 | "metadata": {}, 688 | "source": [ 689 | "The following is a straightforward translation of the GoL rules using `for` loops and array slicing." 690 | ] 691 | }, 692 | { 693 | "cell_type": "code", 694 | "execution_count": 32, 695 | "metadata": {}, 696 | "outputs": [ 697 | { 698 | "name": "stdout", 699 | "output_type": "stream", 700 | "text": [ 701 | "[[0 0 0 0 0 0 0 0 0 0]\n", 702 | " [0 0 0 0 0 1 0 0 0 0]\n", 703 | " [0 0 0 1 0 0 0 0 0 0]\n", 704 | " [0 0 0 0 0 1 0 0 0 0]\n", 705 | " [0 0 0 1 0 0 0 0 0 0]\n", 706 | " [0 0 1 0 1 1 0 0 0 0]\n", 707 | " [0 1 1 0 0 0 1 0 1 0]\n", 708 | " [0 1 0 0 0 0 1 0 1 0]\n", 709 | " [0 1 0 0 0 1 0 0 0 0]\n", 710 | " [0 0 0 0 0 0 0 0 0 0]]\n" 711 | ] 712 | } 713 | ], 714 | "source": [ 715 | "b = np.zeros_like(a)\n", 716 | "rows, cols = a.shape\n", 717 | "for i in range(1, rows-1):\n", 718 | " for j in range(1, cols-1):\n", 719 | " state = a[i, j]\n", 720 | " neighbors = a[i-1:i+2, j-1:j+2]\n", 721 | " k = np.sum(neighbors) - state\n", 722 | " if state:\n", 723 | " if k==2 or k==3:\n", 724 | " b[i, j] = 1\n", 725 | " else:\n", 726 | " if k == 3:\n", 727 | " b[i, j] = 1\n", 728 | "\n", 729 | "print(b)" 730 | ] 731 | }, 732 | { 733 | "cell_type": "markdown", 734 | "metadata": {}, 735 | "source": [ 736 | "Here's a smaller, faster version using cross correlation." 737 | ] 738 | }, 739 | { 740 | "cell_type": "code", 741 | "execution_count": 33, 742 | "metadata": {}, 743 | "outputs": [ 744 | { 745 | "name": "stdout", 746 | "output_type": "stream", 747 | "text": [ 748 | "[[1 1 0 1 1 1 0 1 1 0]\n", 749 | " [1 0 0 0 0 1 0 0 0 1]\n", 750 | " [0 0 0 1 0 0 0 0 0 1]\n", 751 | " [0 0 0 0 0 1 0 0 0 0]\n", 752 | " [1 0 0 1 0 0 0 0 0 0]\n", 753 | " [1 0 1 0 1 1 0 0 0 0]\n", 754 | " [0 1 1 0 0 0 1 0 1 0]\n", 755 | " [0 1 0 0 0 0 1 0 1 1]\n", 756 | " [0 1 0 0 0 1 0 0 0 0]\n", 757 | " [0 0 1 0 1 0 0 1 1 1]]\n" 758 | ] 759 | } 760 | ], 761 | "source": [ 762 | "from scipy.signal import correlate2d\n", 763 | "\n", 764 | "kernel = np.array([[1, 1, 1],\n", 765 | " [1, 0, 1],\n", 766 | " [1, 1, 1]])\n", 767 | "\n", 768 | "c = correlate2d(a, kernel, mode='same')\n", 769 | "b = (c==3) | (c==2) & a\n", 770 | "b = b.astype(np.uint8)\n", 771 | "print(b)" 772 | ] 773 | }, 774 | { 775 | "cell_type": "markdown", 776 | "metadata": {}, 777 | "source": [ 778 | "Using a kernel that gives a weight of 10 to the center cell, we can simplify the logic a little." 779 | ] 780 | }, 781 | { 782 | "cell_type": "code", 783 | "execution_count": 34, 784 | "metadata": {}, 785 | "outputs": [ 786 | { 787 | "name": "stdout", 788 | "output_type": "stream", 789 | "text": [ 790 | "[[1 1 0 1 1 1 0 1 1 0]\n", 791 | " [1 0 0 0 0 1 0 0 0 1]\n", 792 | " [0 0 0 1 0 0 0 0 0 1]\n", 793 | " [0 0 0 0 0 1 0 0 0 0]\n", 794 | " [1 0 0 1 0 0 0 0 0 0]\n", 795 | " [1 0 1 0 1 1 0 0 0 0]\n", 796 | " [0 1 1 0 0 0 1 0 1 0]\n", 797 | " [0 1 0 0 0 0 1 0 1 1]\n", 798 | " [0 1 0 0 0 1 0 0 0 0]\n", 799 | " [0 0 1 0 1 0 0 1 1 1]]\n" 800 | ] 801 | } 802 | ], 803 | "source": [ 804 | "kernel = np.array([[1, 1, 1],\n", 805 | " [1,10, 1],\n", 806 | " [1, 1, 1]])\n", 807 | "\n", 808 | "c = correlate2d(a, kernel, mode='same')\n", 809 | "b = (c==3) | (c==12) | (c==13)\n", 810 | "b = b.astype(np.uint8)\n", 811 | "print(b)" 812 | ] 813 | }, 814 | { 815 | "cell_type": "markdown", 816 | "metadata": {}, 817 | "source": [ 818 | "More importantly, the second version of the kernel makes it possible to use a look up table to get the next state, which is faster and even more concise." 819 | ] 820 | }, 821 | { 822 | "cell_type": "code", 823 | "execution_count": 35, 824 | "metadata": {}, 825 | "outputs": [ 826 | { 827 | "name": "stdout", 828 | "output_type": "stream", 829 | "text": [ 830 | "[[1 1 0 1 1 1 0 1 1 0]\n", 831 | " [1 0 0 0 0 1 0 0 0 1]\n", 832 | " [0 0 0 1 0 0 0 0 0 1]\n", 833 | " [0 0 0 0 0 1 0 0 0 0]\n", 834 | " [1 0 0 1 0 0 0 0 0 0]\n", 835 | " [1 0 1 0 1 1 0 0 0 0]\n", 836 | " [0 1 1 0 0 0 1 0 1 0]\n", 837 | " [0 1 0 0 0 0 1 0 1 1]\n", 838 | " [0 1 0 0 0 1 0 0 0 0]\n", 839 | " [0 0 1 0 1 0 0 1 1 1]]\n" 840 | ] 841 | } 842 | ], 843 | "source": [ 844 | "table = np.zeros(20, dtype=np.uint8)\n", 845 | "table[[3, 12, 13]] = 1\n", 846 | "c = correlate2d(a, kernel, mode='same')\n", 847 | "b = table[c]\n", 848 | "print(b)" 849 | ] 850 | }, 851 | { 852 | "cell_type": "markdown", 853 | "metadata": {}, 854 | "source": [ 855 | "### Highlife\n", 856 | "\n", 857 | "One variation of GoL, called \"Highlife\", has the\n", 858 | "same rules as GoL, plus one additional rule: a dead cell with 6\n", 859 | "neighbors comes to life.\n", 860 | "\n", 861 | "You can try out different rules by inheriting from `Life` and changing the lookup table.\n", 862 | "\n", 863 | "**Exercise:** Modify the table below to add the new rule." 864 | ] 865 | }, 866 | { 867 | "cell_type": "code", 868 | "execution_count": 36, 869 | "metadata": {}, 870 | "outputs": [], 871 | "source": [ 872 | "# Starter code\n", 873 | "\n", 874 | "class MyLife(Life):\n", 875 | " \"\"\"Implementation of Life.\"\"\"\n", 876 | "\n", 877 | " table = np.zeros(20, dtype=np.uint8)\n", 878 | " table[[3, 12, 13]] = 1" 879 | ] 880 | }, 881 | { 882 | "cell_type": "markdown", 883 | "metadata": {}, 884 | "source": [ 885 | "One of the more interesting patterns in Highlife is the replicator, which has the following initial configuration.\n" 886 | ] 887 | }, 888 | { 889 | "cell_type": "code", 890 | "execution_count": 37, 891 | "metadata": {}, 892 | "outputs": [], 893 | "source": [ 894 | "replicator = [\n", 895 | " '00111',\n", 896 | " '01001',\n", 897 | " '10001',\n", 898 | " '10010',\n", 899 | " '11100'\n", 900 | "]" 901 | ] 902 | }, 903 | { 904 | "cell_type": "markdown", 905 | "metadata": {}, 906 | "source": [ 907 | "Make a `MyLife` object with `n=100` and use `add_cells` to put a replicator near the middle.\n", 908 | "\n", 909 | "Make an animation with about 200 frames and see how it behaves." 910 | ] 911 | }, 912 | { 913 | "cell_type": "code", 914 | "execution_count": 38, 915 | "metadata": {}, 916 | "outputs": [ 917 | { 918 | "data": { 919 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOsAAADrCAYAAACICmHVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjAsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy+17YcXAAADhklEQVR4nO3cPW4TUQBG0TEKOzC1swIkaL0L78OrYR/sIkjsgRTUNK4R0lAhRcqfjTAzd+acdly85uqb+FnZjOM4APP3ZuoDAOcRK0SIFSLEChFihQixQsTNJR/ebrfj7nZ3rbPA6t1/u/91Op3ePvXsolh3t7vhy9e7f3Mq4JEP7z/+fO6Z12CIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArw/54GPbHw9TH4BVihQixQoRYIUKsECFWiLiZ+gBM7+7T56mPwBksK0RY1oV76f7UorZYVoiwrAtnPZfDskKEWCFCrBAhVogQK0SIFSJc3YT9+cHDFNczz/3YwlXR9VhWiLCs/BUL+v9ZVoiwrLzq4d+nFnU6lhUixAoRXoPDvJKui2WFCMs6Y354wEOWFSIs64zNZUHnco61s6wQIVaIECtEiBUixAoRvg1eGf+hv8uyQoRYIcJr8Mp41e2yrBAh1oXbHw8vfqlEh1ghQqwQIVaIECtEiBUi3LMunHvV5bCsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghYjOO4/kf3mx+DMPw/XrHgdXbjeP47qkHF8UKTMdrMESIFSLEChFihQixQoRYIUKsECFWiBArRPwGfHBCPOMM+WwAAAAASUVORK5CYII=\n", 920 | "text/plain": [ 921 | "
" 922 | ] 923 | }, 924 | "metadata": {}, 925 | "output_type": "display_data" 926 | } 927 | ], 928 | "source": [ 929 | "# Solution\n", 930 | "\n", 931 | "n = 100\n", 932 | "life = MyLife(n)\n", 933 | "life.add_cells(n//2, n//2, *replicator)\n", 934 | "life.animate(frames=200)" 935 | ] 936 | }, 937 | { 938 | "cell_type": "markdown", 939 | "metadata": {}, 940 | "source": [ 941 | "**Exercise:** Try out some other rules and see what kind of behavior you get." 942 | ] 943 | }, 944 | { 945 | "cell_type": "code", 946 | "execution_count": null, 947 | "metadata": {}, 948 | "outputs": [], 949 | "source": [] 950 | } 951 | ], 952 | "metadata": { 953 | "kernelspec": { 954 | "display_name": "Python 3 (ipykernel)", 955 | "language": "python", 956 | "name": "python3" 957 | }, 958 | "language_info": { 959 | "codemirror_mode": { 960 | "name": "ipython", 961 | "version": 3 962 | }, 963 | "file_extension": ".py", 964 | "mimetype": "text/x-python", 965 | "name": "python", 966 | "nbconvert_exporter": "python", 967 | "pygments_lexer": "ipython3", 968 | "version": "3.7.11" 969 | } 970 | }, 971 | "nbformat": 4, 972 | "nbformat_minor": 2 973 | } 974 | --------------------------------------------------------------------------------