├── ThinkComplexity.zip ├── requirements.txt ├── requirements-dev.txt ├── README.md ├── environment.yml ├── .gitignore ├── Makefile ├── nb ├── build.sh ├── LifeRabbits.py ├── prep_notebooks.py ├── Life.py ├── order.py ├── utils.py ├── Cell2D.py ├── Cell1D.py ├── firefly.py ├── chap10.ipynb ├── chap00.ipynb ├── chap06.ipynb ├── chap01.ipynb ├── chap02.ipynb └── chap05.ipynb ├── soln ├── LifeRabbits.py ├── Life.py ├── order.py ├── utils.py ├── Cell2D.py ├── Cell1D.py ├── firefly.py ├── quiz04.ipynb └── majority.ipynb └── .github └── workflows └── tests.yml /ThinkComplexity.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ThinkComplexity/HEAD/ThinkComplexity.zip -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | numpy 2 | matplotlib 3 | pandas 4 | seaborn 5 | scipy 6 | jupyter 7 | networkx 8 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | -r requirements.txt 2 | black 3 | nb_black==1.0.5 4 | flake8 5 | nbformat 6 | nbmake 7 | pytest 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ThinkComplexity 2 | =============== 3 | 4 | Code for Allen Downey's book Think Complexity, published by O'Reilly Media. -------------------------------------------------------------------------------- /environment.yml: -------------------------------------------------------------------------------- 1 | name: ThinkComplexity 2 | 3 | dependencies: 4 | - python 5 | - numpy 6 | - matplotlib 7 | - pandas 8 | - seaborn 9 | - scipy 10 | - jupyter 11 | - networkx 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | 3 | # C extensions 4 | *.so 5 | 6 | # Packages 7 | *.egg 8 | *.egg-info 9 | dist 10 | build 11 | eggs 12 | parts 13 | bin 14 | var 15 | sdist 16 | develop-eggs 17 | .installed.cfg 18 | lib 19 | lib64 20 | 21 | # Installer logs 22 | pip-log.txt 23 | 24 | # Unit test / coverage reports 25 | .coverage 26 | .tox 27 | nosetests.xml 28 | 29 | # Translations 30 | *.mo 31 | 32 | # Mr Developer 33 | .mr.developer.cfg 34 | .project 35 | .pydevproject 36 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT_NAME = ThinkComplexity 2 | PYTHON_VERSION = 3.10 3 | PYTHON_INTERPRETER = python 4 | 5 | create_environment: 6 | conda create --name $(PROJECT_NAME) python=$(PYTHON_VERSION) -y 7 | @echo ">>> conda env created. Activate with:\nconda activate $(PROJECT_NAME)" 8 | 9 | delete_environment: 10 | conda env remove --name $(PROJECT_NAME) 11 | 12 | requirements: 13 | $(PYTHON_INTERPRETER) -m pip install -U pip setuptools wheel 14 | $(PYTHON_INTERPRETER) -m pip install -r requirements.txt 15 | 16 | clean: 17 | find . -type f -name "*.py[co]" -delete 18 | find . -type d -name "__pycache__" -delete 19 | 20 | lint: 21 | flake8 code 22 | black --check --config pyproject.toml code 23 | 24 | format: 25 | black --config pyproject.toml code 26 | 27 | tests: 28 | cd soln; pytest --nbmake app*.ipynb chap*.ipynb 29 | -------------------------------------------------------------------------------- /nb/build.sh: -------------------------------------------------------------------------------- 1 | # Build the notebook folder and zip file 2 | 3 | # copy notebooks from soln 4 | cp ../soln/app*.ipynb . 5 | cp ../soln/chap*.ipynb . 6 | cp ../soln/*.py . 7 | 8 | # remove the solutions 9 | python prep_notebooks.py 10 | 11 | # pip install pytest nbmake 12 | 13 | # run nbmake 14 | # pytest --nbmake chap*.ipynb 15 | 16 | # commit the changes 17 | git add app*.ipynb 18 | git add chap*.ipynb 19 | git add *.py 20 | git commit -m "Updating code and notebooks" 21 | 22 | # build the zip file 23 | cd ../..; zip -r ThinkComplexity.zip \ 24 | ThinkComplexity/nb/app*.ipynb \ 25 | ThinkComplexity/nb/chap*.ipynb \ 26 | ThinkComplexity/nb/*.py 27 | 28 | # add and commit it 29 | mv ThinkComplexity.zip ThinkComplexity 30 | cd ThinkComplexity 31 | 32 | git add ThinkComplexity.zip 33 | git commit -m "Updating the zip file" 34 | git push 35 | -------------------------------------------------------------------------------- /nb/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 | 10 | import sys 11 | import matplotlib.pyplot as plt 12 | 13 | from Life import Life 14 | 15 | 16 | def main(script, *args): 17 | """Constructs the rabbits methusela. 18 | 19 | http://www.argentum.freeserve.co.uk/lex_r.htm#rabbits 20 | """ 21 | 22 | rabbits = ["1000111", "111001", "01"] 23 | 24 | n = 400 25 | m = 600 26 | life = Life(n, m) 27 | life.add_cells(n // 2, m // 2, *rabbits) 28 | life.animate(frames=1000) 29 | plt.subplots_adjust(left=0.01, right=0.99, bottom=0.01, top=0.99) 30 | plt.show() 31 | 32 | 33 | if __name__ == "__main__": 34 | main(*sys.argv) 35 | -------------------------------------------------------------------------------- /soln/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 | 10 | import sys 11 | import matplotlib.pyplot as plt 12 | 13 | from Life import Life 14 | 15 | 16 | def main(script, *args): 17 | """Constructs the rabbits methusela. 18 | 19 | http://www.argentum.freeserve.co.uk/lex_r.htm#rabbits 20 | """ 21 | 22 | rabbits = ["1000111", "111001", "01"] 23 | 24 | n = 400 25 | m = 600 26 | life = Life(n, m) 27 | life.add_cells(n // 2, m // 2, *rabbits) 28 | life.animate(frames=1000) 29 | plt.subplots_adjust(left=0.01, right=0.99, bottom=0.01, top=0.99) 30 | plt.show() 31 | 32 | 33 | if __name__ == "__main__": 34 | main(*sys.argv) 35 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: [v3] 6 | pull_request: 7 | schedule: 8 | # Run on the first of every month 9 | - cron: "0 0 1 * *" 10 | workflow_dispatch: 11 | 12 | jobs: 13 | tests: 14 | name: Tests (${{ matrix.os }}, Python ${{ matrix.python-version }}) 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | matrix: 18 | os: [ubuntu-latest, macos-latest, windows-latest] 19 | python-version: ["3.10"] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install -r requirements-dev.txt 33 | 34 | - name: Run tests 35 | run: | 36 | make tests 37 | -------------------------------------------------------------------------------- /nb/prep_notebooks.py: -------------------------------------------------------------------------------- 1 | import nbformat as nbf 2 | from glob import glob 3 | 4 | # Collect a list of all notebooks in the content folder 5 | filenames = glob("chap*.ipynb") 6 | 7 | text = '# Solution' 8 | replacement = '# Solution goes here' 9 | 10 | # Search through each notebook 11 | for filename in sorted(filenames): 12 | print('Removing solutions from', filename) 13 | ntbk = nbf.read(filename, nbf.NO_CONVERT) 14 | 15 | # if the second element of ntbk.cells loads nb_black, remove it 16 | second = ntbk.cells[1] 17 | if second.source.startswith(r'%load_ext nb_black'): 18 | ntbk.cells.pop(1) 19 | 20 | for cell in ntbk.cells: 21 | # remove tags 22 | if 'tags' in cell['metadata']: 23 | tags = cell['metadata']['tags'] 24 | cell['metadata']['tags'] = [] 25 | else: 26 | tags = [] 27 | 28 | # remove output 29 | if 'outputs' in cell: 30 | cell['outputs'] = [] 31 | 32 | # remove solutions 33 | if cell['source'].startswith(text): 34 | cell['source'] = replacement 35 | 36 | # remove solutions 37 | if 'solution' in tags: 38 | cell['source'] = replacement 39 | 40 | # add reference label 41 | for tag in tags: 42 | if tag.startswith('chapter') or tag.startswith('section'): 43 | print(tag) 44 | label = f'({tag})=\n' 45 | cell['source'] = label + cell['source'] 46 | 47 | nbf.write(ntbk, filename) 48 | -------------------------------------------------------------------------------- /nb/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 | 10 | import sys 11 | 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | from matplotlib import animation 15 | 16 | """ 17 | For animation to work in the notebook, you might have to install 18 | ffmpeg. On Ubuntu and Linux Mint, the following should work. 19 | 20 | sudo add-apt-repository ppa:mc3man/trusty-media 21 | sudo apt-get update 22 | sudo apt-get install ffmpeg 23 | """ 24 | 25 | from Cell2D import Cell2D 26 | from scipy.signal import correlate2d 27 | 28 | 29 | class Life(Cell2D): 30 | """Implementation of Conway's Game of Life.""" 31 | 32 | kernel = np.array([[1, 1, 1], [1, 10, 1], [1, 1, 1]]) 33 | 34 | table = np.zeros(20, dtype=np.uint8) 35 | table[[3, 12, 13]] = 1 36 | 37 | def step(self): 38 | """Executes one time step.""" 39 | c = correlate2d(self.array, self.kernel, mode="same") 40 | self.array = self.table[c] 41 | 42 | 43 | def main(script, *args): 44 | """Constructs a puffer train. 45 | 46 | Uses the entities in this file: 47 | http://www.radicaleye.com/lifepage/patterns/puftrain.lif 48 | """ 49 | lwss = ["0001", "00001", "10001", "01111"] 50 | 51 | bhep = ["1", "011", "001", "001", "01"] 52 | 53 | n = 400 54 | m = 600 55 | life = Life(n, m) 56 | 57 | col = 120 58 | life.add_cells(n // 2 + 12, col, *lwss) 59 | life.add_cells(n // 2 + 26, col, *lwss) 60 | life.add_cells(n // 2 + 19, col, *bhep) 61 | life.animate(frames=1000) 62 | plt.subplots_adjust(left=0.01, right=0.99, bottom=0.01, top=0.99) 63 | plt.show() 64 | 65 | 66 | if __name__ == "__main__": 67 | main(*sys.argv) 68 | -------------------------------------------------------------------------------- /soln/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 | 10 | import sys 11 | 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | from matplotlib import animation 15 | 16 | """ 17 | For animation to work in the notebook, you might have to install 18 | ffmpeg. On Ubuntu and Linux Mint, the following should work. 19 | 20 | sudo add-apt-repository ppa:mc3man/trusty-media 21 | sudo apt-get update 22 | sudo apt-get install ffmpeg 23 | """ 24 | 25 | from Cell2D import Cell2D 26 | from scipy.signal import correlate2d 27 | 28 | 29 | class Life(Cell2D): 30 | """Implementation of Conway's Game of Life.""" 31 | 32 | kernel = np.array([[1, 1, 1], [1, 10, 1], [1, 1, 1]]) 33 | 34 | table = np.zeros(20, dtype=np.uint8) 35 | table[[3, 12, 13]] = 1 36 | 37 | def step(self): 38 | """Executes one time step.""" 39 | c = correlate2d(self.array, self.kernel, mode="same") 40 | self.array = self.table[c] 41 | 42 | 43 | def main(script, *args): 44 | """Constructs a puffer train. 45 | 46 | Uses the entities in this file: 47 | http://www.radicaleye.com/lifepage/patterns/puftrain.lif 48 | """ 49 | lwss = ["0001", "00001", "10001", "01111"] 50 | 51 | bhep = ["1", "011", "001", "001", "01"] 52 | 53 | n = 400 54 | m = 600 55 | life = Life(n, m) 56 | 57 | col = 120 58 | life.add_cells(n // 2 + 12, col, *lwss) 59 | life.add_cells(n // 2 + 26, col, *lwss) 60 | life.add_cells(n // 2 + 19, col, *bhep) 61 | life.animate(frames=1000) 62 | plt.subplots_adjust(left=0.01, right=0.99, bottom=0.01, top=0.99) 63 | plt.show() 64 | 65 | 66 | if __name__ == "__main__": 67 | main(*sys.argv) 68 | -------------------------------------------------------------------------------- /nb/order.py: -------------------------------------------------------------------------------- 1 | """ Code from Think Complexity, 2nd Edition, by Allen Downey. 2 | 3 | Available from http://greenteapress.com 4 | 5 | Copyright 2019 Allen B. Downey. 6 | MIT License: https://opensource.org/licenses/MIT 7 | """ 8 | 9 | import os 10 | import matplotlib.pyplot as plt 11 | 12 | 13 | def etime(): 14 | """Measures user and system time this process has used. 15 | 16 | Returns the sum of user and system time.""" 17 | user, sys, chuser, chsys, real = os.times() 18 | return user + sys 19 | 20 | 21 | def time_func(func, n): 22 | """Run a function and return the elapsed time. 23 | 24 | func: function 25 | n: problem size 26 | 27 | returns: user+sys time in seconds 28 | """ 29 | start = etime() 30 | func(n) 31 | end = etime() 32 | elapsed = end - start 33 | return elapsed 34 | 35 | 36 | def run_timing_test(func, max_time=1): 37 | """Tests the given function with a range of values for n. 38 | 39 | func: function object 40 | 41 | returns: list of ns and a list of run times. 42 | """ 43 | ns = [] 44 | ts = [] 45 | for i in range(10, 28): 46 | n = 2**i 47 | t = time_func(func, n) 48 | print(n, t) 49 | if t > 0: 50 | ns.append(n) 51 | ts.append(t) 52 | if t > max_time: 53 | break 54 | 55 | return ns, ts 56 | 57 | 58 | def fit(ns, ts, exp=1.0, index=-1): 59 | """Fits a curve with the given exponent. 60 | 61 | ns: sequence of problem sizes 62 | ts: sequence of times 63 | exp: exponent of the fitted curve 64 | index: index of the element the fitted line should go through 65 | 66 | returns: sequence of fitted times 67 | 68 | 69 | """ 70 | # Use the element with the given index as a reference point, 71 | # and scale all other points accordingly. 72 | nref = ns[index] 73 | tref = ts[index] 74 | 75 | tfit = [] 76 | for n in ns: 77 | ratio = n / nref 78 | t = ratio**exp * tref 79 | tfit.append(t) 80 | 81 | return tfit 82 | 83 | 84 | def plot_timing_test(ns, ts, label="", color="blue", exp=1.0, scale="log"): 85 | """Plots data and a fitted curve. 86 | 87 | ns: sequence of n (problem size) 88 | ts: sequence of t (run time) 89 | label: string label for the data curve 90 | color: string color for the data curve 91 | exp: exponent (slope) for the fitted curve 92 | """ 93 | tfit = fit(ns, ts, exp) 94 | fit_label = f"{exp}" 95 | plt.plot(ns, tfit, label=fit_label, color="0.7", linestyle="dashed") 96 | plt.plot(ns, ts, "o-", label=label, color=color, alpha=0.7) 97 | plt.xlabel("Problem size (n)") 98 | plt.ylabel("Runtime (seconds)") 99 | plt.xscale(scale) 100 | plt.yscale(scale) 101 | plt.legend() 102 | -------------------------------------------------------------------------------- /soln/order.py: -------------------------------------------------------------------------------- 1 | """ Code from Think Complexity, 2nd Edition, by Allen Downey. 2 | 3 | Available from http://greenteapress.com 4 | 5 | Copyright 2019 Allen B. Downey. 6 | MIT License: https://opensource.org/licenses/MIT 7 | """ 8 | 9 | import os 10 | import matplotlib.pyplot as plt 11 | 12 | 13 | def etime(): 14 | """Measures user and system time this process has used. 15 | 16 | Returns the sum of user and system time.""" 17 | user, sys, chuser, chsys, real = os.times() 18 | return user + sys 19 | 20 | 21 | def time_func(func, n): 22 | """Run a function and return the elapsed time. 23 | 24 | func: function 25 | n: problem size 26 | 27 | returns: user+sys time in seconds 28 | """ 29 | start = etime() 30 | func(n) 31 | end = etime() 32 | elapsed = end - start 33 | return elapsed 34 | 35 | 36 | def run_timing_test(func, max_time=1): 37 | """Tests the given function with a range of values for n. 38 | 39 | func: function object 40 | 41 | returns: list of ns and a list of run times. 42 | """ 43 | ns = [] 44 | ts = [] 45 | for i in range(10, 28): 46 | n = 2**i 47 | t = time_func(func, n) 48 | print(n, t) 49 | if t > 0: 50 | ns.append(n) 51 | ts.append(t) 52 | if t > max_time: 53 | break 54 | 55 | return ns, ts 56 | 57 | 58 | def fit(ns, ts, exp=1.0, index=-1): 59 | """Fits a curve with the given exponent. 60 | 61 | ns: sequence of problem sizes 62 | ts: sequence of times 63 | exp: exponent of the fitted curve 64 | index: index of the element the fitted line should go through 65 | 66 | returns: sequence of fitted times 67 | 68 | 69 | """ 70 | # Use the element with the given index as a reference point, 71 | # and scale all other points accordingly. 72 | nref = ns[index] 73 | tref = ts[index] 74 | 75 | tfit = [] 76 | for n in ns: 77 | ratio = n / nref 78 | t = ratio**exp * tref 79 | tfit.append(t) 80 | 81 | return tfit 82 | 83 | 84 | def plot_timing_test(ns, ts, label="", color="blue", exp=1.0, scale="log"): 85 | """Plots data and a fitted curve. 86 | 87 | ns: sequence of n (problem size) 88 | ts: sequence of t (run time) 89 | label: string label for the data curve 90 | color: string color for the data curve 91 | exp: exponent (slope) for the fitted curve 92 | """ 93 | tfit = fit(ns, ts, exp) 94 | fit_label = f"{exp}" 95 | plt.plot(ns, tfit, label=fit_label, color="0.7", linestyle="dashed") 96 | plt.plot(ns, ts, "o-", label=label, color=color, alpha=0.7) 97 | plt.xlabel("Problem size (n)") 98 | plt.ylabel("Runtime (seconds)") 99 | plt.xscale(scale) 100 | plt.yscale(scale) 101 | plt.legend() 102 | -------------------------------------------------------------------------------- /nb/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import seaborn as sns 4 | 5 | 6 | def three_frame(world, n_seq, seed=17): 7 | """Draw three timesteps. 8 | 9 | world: object with step, loop, and draw 10 | n_seq: 3-tuple, number of steps before each draw 11 | seed: random see for NumPy 12 | """ 13 | np.random.seed(seed) 14 | plt.figure(figsize=(10, 4)) 15 | 16 | for i, n in enumerate(n_seq): 17 | plt.subplot(1, 3, i + 1) 18 | world.loop(n) 19 | world.draw() 20 | 21 | plt.tight_layout() 22 | 23 | 24 | def savefig(filename, **options): 25 | """Save the current figure. 26 | 27 | Keyword arguments are passed along to plt.savefig 28 | 29 | https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html 30 | 31 | filename: string 32 | """ 33 | print("Saving figure to file", filename) 34 | plt.savefig(filename, **options) 35 | 36 | 37 | def underride(d, **options): 38 | """Add key-value pairs to d only if key is not in d. 39 | 40 | d: dictionary 41 | options: keyword args to add to d 42 | """ 43 | for key, val in options.items(): 44 | d.setdefault(key, val) 45 | 46 | return d 47 | 48 | 49 | def decorate(**options): 50 | """Decorate the current axes. 51 | 52 | Call decorate with keyword arguments like 53 | 54 | decorate(title='Title', 55 | xlabel='x', 56 | ylabel='y') 57 | 58 | The keyword arguments can be any of the axis properties 59 | 60 | https://matplotlib.org/api/axes_api.html 61 | 62 | In addition, you can use `legend=False` to suppress the legend. 63 | 64 | And you can use `loc` to indicate the location of the legend 65 | (the default value is 'best') 66 | """ 67 | loc = options.pop("loc", "best") 68 | if options.pop("legend", True): 69 | legend(loc=loc) 70 | 71 | plt.gca().set(**options) 72 | plt.tight_layout() 73 | 74 | 75 | def legend(**options): 76 | """Draws a legend only if there is at least one labeled item. 77 | 78 | options are passed to plt.legend() 79 | https://matplotlib.org/api/_as_gen/matplotlib.pyplot.legend.html 80 | 81 | """ 82 | underride(options, loc="best", frameon=False) 83 | 84 | ax = plt.gca() 85 | handles, labels = ax.get_legend_handles_labels() 86 | if handles: 87 | ax.legend(handles, labels, **options) 88 | 89 | 90 | def set_palette(*args, **kwds): 91 | """Set the matplotlib color cycler. 92 | 93 | args, kwds: same as for sns.color_palette 94 | 95 | Also takes a boolean kwd, `reverse`, to indicate 96 | whether the order of the palette should be reversed. 97 | 98 | returns: list of colors 99 | """ 100 | reverse = kwds.pop("reverse", False) 101 | palette = sns.color_palette(*args, **kwds) 102 | 103 | palette = list(palette) 104 | if reverse: 105 | palette.reverse() 106 | 107 | cycler = plt.cycler(color=palette) 108 | plt.gca().set_prop_cycle(cycler) 109 | return palette 110 | -------------------------------------------------------------------------------- /soln/utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | import seaborn as sns 4 | 5 | 6 | def three_frame(world, n_seq, seed=17): 7 | """Draw three timesteps. 8 | 9 | world: object with step, loop, and draw 10 | n_seq: 3-tuple, number of steps before each draw 11 | seed: random see for NumPy 12 | """ 13 | np.random.seed(seed) 14 | plt.figure(figsize=(10, 4)) 15 | 16 | for i, n in enumerate(n_seq): 17 | plt.subplot(1, 3, i + 1) 18 | world.loop(n) 19 | world.draw() 20 | 21 | plt.tight_layout() 22 | 23 | 24 | def savefig(filename, **options): 25 | """Save the current figure. 26 | 27 | Keyword arguments are passed along to plt.savefig 28 | 29 | https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html 30 | 31 | filename: string 32 | """ 33 | print("Saving figure to file", filename) 34 | plt.savefig(filename, **options) 35 | 36 | 37 | def underride(d, **options): 38 | """Add key-value pairs to d only if key is not in d. 39 | 40 | d: dictionary 41 | options: keyword args to add to d 42 | """ 43 | for key, val in options.items(): 44 | d.setdefault(key, val) 45 | 46 | return d 47 | 48 | 49 | def decorate(**options): 50 | """Decorate the current axes. 51 | 52 | Call decorate with keyword arguments like 53 | 54 | decorate(title='Title', 55 | xlabel='x', 56 | ylabel='y') 57 | 58 | The keyword arguments can be any of the axis properties 59 | 60 | https://matplotlib.org/api/axes_api.html 61 | 62 | In addition, you can use `legend=False` to suppress the legend. 63 | 64 | And you can use `loc` to indicate the location of the legend 65 | (the default value is 'best') 66 | """ 67 | loc = options.pop("loc", "best") 68 | if options.pop("legend", True): 69 | legend(loc=loc) 70 | 71 | plt.gca().set(**options) 72 | plt.tight_layout() 73 | 74 | 75 | def legend(**options): 76 | """Draws a legend only if there is at least one labeled item. 77 | 78 | options are passed to plt.legend() 79 | https://matplotlib.org/api/_as_gen/matplotlib.pyplot.legend.html 80 | 81 | """ 82 | underride(options, loc="best", frameon=False) 83 | 84 | ax = plt.gca() 85 | handles, labels = ax.get_legend_handles_labels() 86 | if handles: 87 | ax.legend(handles, labels, **options) 88 | 89 | 90 | def set_palette(*args, **kwds): 91 | """Set the matplotlib color cycler. 92 | 93 | args, kwds: same as for sns.color_palette 94 | 95 | Also takes a boolean kwd, `reverse`, to indicate 96 | whether the order of the palette should be reversed. 97 | 98 | returns: list of colors 99 | """ 100 | reverse = kwds.pop("reverse", False) 101 | palette = sns.color_palette(*args, **kwds) 102 | 103 | palette = list(palette) 104 | if reverse: 105 | palette.reverse() 106 | 107 | cycler = plt.cycler(color=palette) 108 | plt.gca().set_prop_cycle(cycler) 109 | return palette 110 | -------------------------------------------------------------------------------- /nb/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 | 10 | from time import sleep 11 | 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | 15 | # Here's how animate works 16 | # https://stackoverflow.com/questions/24816237/ipython-notebook-clear-cell-output-in-code 17 | # https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.clear_output 18 | 19 | from IPython.display import clear_output 20 | from utils import underride 21 | 22 | 23 | class Cell2D: 24 | """Parent class for 2-D cellular automata.""" 25 | 26 | def __init__(self, n, m=None): 27 | """Initializes the attributes. 28 | 29 | n: number of rows 30 | m: number of columns 31 | """ 32 | m = n if m is None else m 33 | self.array = np.zeros((n, m), np.uint8) 34 | 35 | def add_cells(self, row, col, *strings): 36 | """Adds cells at the given location. 37 | 38 | row: top row index 39 | col: left col index 40 | strings: list of strings of 0s and 1s 41 | """ 42 | for i, s in enumerate(strings): 43 | self.array[row + i, col : col + len(s)] = np.array([int(b) for b in s]) 44 | 45 | def loop(self, iters=1): 46 | """Runs the given number of steps.""" 47 | for i in range(iters): 48 | self.step() 49 | 50 | def draw(self, **options): 51 | """Draws the array.""" 52 | draw_array(self.array, **options) 53 | 54 | def animate(self, frames, interval=0.001, step=None): 55 | """Animate the automaton. 56 | 57 | frames: number of frames to draw 58 | interval: time between frames in seconds 59 | step: function to call between frames 60 | """ 61 | if step is None: 62 | step = self.step 63 | 64 | plt.ion() 65 | plt.figure() 66 | try: 67 | for _ in range(frames - 1): 68 | self.draw() 69 | plt.pause(interval) 70 | step() 71 | clear_output(wait=True) 72 | self.draw() 73 | plt.show() 74 | except KeyboardInterrupt: 75 | pass 76 | finally: 77 | plt.ioff() 78 | 79 | 80 | def draw_array(array, **options): 81 | """Draws the cells. 82 | 83 | array: numpy array 84 | options: passed to plt.imshow 85 | 86 | returns: plt.imshow object 87 | """ 88 | n, m = array.shape 89 | options = underride( 90 | options, 91 | cmap="Greens", 92 | alpha=0.7, 93 | vmin=0, 94 | vmax=1, 95 | interpolation="none", 96 | origin="upper", 97 | extent=[0, m, 0, n], 98 | ) 99 | 100 | plt.axis([0, m, 0, n]) 101 | plt.xticks([]) 102 | plt.yticks([]) 103 | 104 | return plt.imshow(array, **options) 105 | -------------------------------------------------------------------------------- /soln/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 | 10 | from time import sleep 11 | 12 | import numpy as np 13 | import matplotlib.pyplot as plt 14 | 15 | # Here's how animate works 16 | # https://stackoverflow.com/questions/24816237/ipython-notebook-clear-cell-output-in-code 17 | # https://ipython.readthedocs.io/en/stable/api/generated/IPython.display.html#IPython.display.clear_output 18 | 19 | from IPython.display import clear_output 20 | from utils import underride 21 | 22 | 23 | class Cell2D: 24 | """Parent class for 2-D cellular automata.""" 25 | 26 | def __init__(self, n, m=None): 27 | """Initializes the attributes. 28 | 29 | n: number of rows 30 | m: number of columns 31 | """ 32 | m = n if m is None else m 33 | self.array = np.zeros((n, m), np.uint8) 34 | 35 | def add_cells(self, row, col, *strings): 36 | """Adds cells at the given location. 37 | 38 | row: top row index 39 | col: left col index 40 | strings: list of strings of 0s and 1s 41 | """ 42 | for i, s in enumerate(strings): 43 | self.array[row + i, col : col + len(s)] = np.array([int(b) for b in s]) 44 | 45 | def loop(self, iters=1): 46 | """Runs the given number of steps.""" 47 | for i in range(iters): 48 | self.step() 49 | 50 | def draw(self, **options): 51 | """Draws the array.""" 52 | draw_array(self.array, **options) 53 | 54 | def animate(self, frames, interval=0.001, step=None): 55 | """Animate the automaton. 56 | 57 | frames: number of frames to draw 58 | interval: time between frames in seconds 59 | step: function to call between frames 60 | """ 61 | if step is None: 62 | step = self.step 63 | 64 | plt.ion() 65 | plt.figure() 66 | try: 67 | for _ in range(frames - 1): 68 | self.draw() 69 | plt.pause(interval) 70 | step() 71 | clear_output(wait=True) 72 | self.draw() 73 | plt.show() 74 | except KeyboardInterrupt: 75 | pass 76 | finally: 77 | plt.ioff() 78 | 79 | 80 | def draw_array(array, **options): 81 | """Draws the cells. 82 | 83 | array: numpy array 84 | options: passed to plt.imshow 85 | 86 | returns: plt.imshow object 87 | """ 88 | n, m = array.shape 89 | options = underride( 90 | options, 91 | cmap="Greens", 92 | alpha=0.7, 93 | vmin=0, 94 | vmax=1, 95 | interpolation="none", 96 | origin="upper", 97 | extent=[0, m, 0, n], 98 | ) 99 | 100 | plt.axis([0, m, 0, n]) 101 | plt.xticks([]) 102 | plt.yticks([]) 103 | 104 | return plt.imshow(array, **options) 105 | -------------------------------------------------------------------------------- /nb/Cell1D.py: -------------------------------------------------------------------------------- 1 | """ Code from Think Complexity, 2nd Edition, by Allen Downey. 2 | 3 | Available from http://greenteapress.com 4 | 5 | Copyright 2016 Allen B. Downey. 6 | MIT License: https://opensource.org/licenses/MIT 7 | """ 8 | 9 | import numpy as np 10 | import matplotlib.pyplot as plt 11 | 12 | 13 | def make_table(rule): 14 | """Makes the CA table for a given rule. 15 | 16 | rule: integer 0-255 17 | 18 | returns: NumPy array of uint8 19 | """ 20 | rule = np.array([rule], dtype=np.uint8) 21 | table = np.unpackbits(rule)[::-1] 22 | return table 23 | 24 | 25 | def print_table(table): 26 | """Prints the rule table in LaTeX format.""" 27 | print("\\beforefig") 28 | print("\\centerline{") 29 | print("\\begin{tabular}{|c|c|c|c|c|c|c|c|c|}") 30 | print("\\hline") 31 | 32 | res = ["prev"] + ["{0:03b}".format(i) for i in range(8)] 33 | print(" & ".join(res) + " \\\\ \n\\hline") 34 | 35 | res = ["next"] + [str(x) for x in table] 36 | print(" & ".join(res) + " \\\\ \n\\hline") 37 | 38 | print("\\end{tabular}}") 39 | 40 | 41 | class Cell1D: 42 | """Represents a 1-D a cellular automaton""" 43 | 44 | def __init__(self, rule, n, m=None): 45 | """Initializes the CA. 46 | 47 | rule: integer 48 | n: number of rows 49 | m: number of columns 50 | 51 | Attributes: 52 | table: rule dictionary that maps from triple to next state. 53 | array: the numpy array that contains the data. 54 | next: the index of the next empty row. 55 | """ 56 | self.table = make_table(rule) 57 | self.n = n 58 | self.m = 2 * n + 1 if m is None else m 59 | 60 | self.array = np.zeros((n, self.m), dtype=np.int8) 61 | self.next = 0 62 | 63 | def start_single(self): 64 | """Starts with one cell in the middle of the top row.""" 65 | self.array[0, self.m // 2] = 1 66 | self.next += 1 67 | 68 | def start_random(self): 69 | """Start with random values in the top row.""" 70 | self.array[0] = np.random.random(self.m).round() 71 | self.next += 1 72 | 73 | def start_string(self, s): 74 | """Start with values from a string of 1s and 0s.""" 75 | # TODO: Check string length 76 | self.array[0] = np.array([int(x) for x in s]) 77 | self.next += 1 78 | 79 | def loop(self, steps=1): 80 | """Executes the given number of time steps.""" 81 | for i in range(steps): 82 | self.step() 83 | 84 | def step(self): 85 | """Executes one time step by computing the next row of the array.""" 86 | a = self.array 87 | i = self.next 88 | window = [4, 2, 1] 89 | c = np.correlate(a[i - 1], window, mode="same") 90 | a[i] = self.table[c] 91 | self.next += 1 92 | 93 | def draw(self, start=0, end=None): 94 | """Draws the CA using pyplot.imshow. 95 | 96 | start: index of the first column to be shown 97 | end: index of the last column to be shown 98 | """ 99 | a = self.array[:, start:end] 100 | plt.imshow(a, cmap="Blues", alpha=0.7) 101 | 102 | # turn off axis tick marks 103 | plt.xticks([]) 104 | plt.yticks([]) 105 | 106 | 107 | def draw_ca(rule, n=32): 108 | """Makes and draw a 1D CA with a given rule. 109 | 110 | rule: int rule number 111 | n: number of rows 112 | """ 113 | ca = Cell1D(rule, n) 114 | ca.start_single() 115 | ca.loop(n - 1) 116 | ca.draw() 117 | -------------------------------------------------------------------------------- /soln/Cell1D.py: -------------------------------------------------------------------------------- 1 | """ Code from Think Complexity, 2nd Edition, by Allen Downey. 2 | 3 | Available from http://greenteapress.com 4 | 5 | Copyright 2016 Allen B. Downey. 6 | MIT License: https://opensource.org/licenses/MIT 7 | """ 8 | 9 | import numpy as np 10 | import matplotlib.pyplot as plt 11 | 12 | 13 | def make_table(rule): 14 | """Makes the CA table for a given rule. 15 | 16 | rule: integer 0-255 17 | 18 | returns: NumPy array of uint8 19 | """ 20 | rule = np.array([rule], dtype=np.uint8) 21 | table = np.unpackbits(rule)[::-1] 22 | return table 23 | 24 | 25 | def print_table(table): 26 | """Prints the rule table in LaTeX format.""" 27 | print("\\beforefig") 28 | print("\\centerline{") 29 | print("\\begin{tabular}{|c|c|c|c|c|c|c|c|c|}") 30 | print("\\hline") 31 | 32 | res = ["prev"] + ["{0:03b}".format(i) for i in range(8)] 33 | print(" & ".join(res) + " \\\\ \n\\hline") 34 | 35 | res = ["next"] + [str(x) for x in table] 36 | print(" & ".join(res) + " \\\\ \n\\hline") 37 | 38 | print("\\end{tabular}}") 39 | 40 | 41 | class Cell1D: 42 | """Represents a 1-D a cellular automaton""" 43 | 44 | def __init__(self, rule, n, m=None): 45 | """Initializes the CA. 46 | 47 | rule: integer 48 | n: number of rows 49 | m: number of columns 50 | 51 | Attributes: 52 | table: rule dictionary that maps from triple to next state. 53 | array: the numpy array that contains the data. 54 | next: the index of the next empty row. 55 | """ 56 | self.table = make_table(rule) 57 | self.n = n 58 | self.m = 2 * n + 1 if m is None else m 59 | 60 | self.array = np.zeros((n, self.m), dtype=np.int8) 61 | self.next = 0 62 | 63 | def start_single(self): 64 | """Starts with one cell in the middle of the top row.""" 65 | self.array[0, self.m // 2] = 1 66 | self.next += 1 67 | 68 | def start_random(self): 69 | """Start with random values in the top row.""" 70 | self.array[0] = np.random.random(self.m).round() 71 | self.next += 1 72 | 73 | def start_string(self, s): 74 | """Start with values from a string of 1s and 0s.""" 75 | # TODO: Check string length 76 | self.array[0] = np.array([int(x) for x in s]) 77 | self.next += 1 78 | 79 | def loop(self, steps=1): 80 | """Executes the given number of time steps.""" 81 | for i in range(steps): 82 | self.step() 83 | 84 | def step(self): 85 | """Executes one time step by computing the next row of the array.""" 86 | a = self.array 87 | i = self.next 88 | window = [4, 2, 1] 89 | c = np.correlate(a[i - 1], window, mode="same") 90 | a[i] = self.table[c] 91 | self.next += 1 92 | 93 | def draw(self, start=0, end=None): 94 | """Draws the CA using pyplot.imshow. 95 | 96 | start: index of the first column to be shown 97 | end: index of the last column to be shown 98 | """ 99 | a = self.array[:, start:end] 100 | plt.imshow(a, cmap="Blues", alpha=0.7) 101 | 102 | # turn off axis tick marks 103 | plt.xticks([]) 104 | plt.yticks([]) 105 | 106 | 107 | def draw_ca(rule, n=32): 108 | """Makes and draw a 1D CA with a given rule. 109 | 110 | rule: int rule number 111 | n: number of rows 112 | """ 113 | ca = Cell1D(rule, n) 114 | ca.start_single() 115 | ca.loop(n - 1) 116 | ca.draw() 117 | -------------------------------------------------------------------------------- /nb/firefly.py: -------------------------------------------------------------------------------- 1 | from vpython import * 2 | import numpy as np 3 | 4 | import random 5 | 6 | 7 | class world(box): 8 | def __init__(self): 9 | side = 4.0 10 | thk = 0.3 11 | s2 = 2 * side - thk 12 | s3 = 2 * side + thk 13 | self.wallR = box( 14 | pos=vector(side, 0, 0), size=vector(thk, s2, s3), color=color.red, opacity=0 15 | ) 16 | self.wallL = box( 17 | pos=vector(-side, 0, 0), 18 | size=vector(thk, s2, s3), 19 | color=color.red, 20 | opacity=0, 21 | ) 22 | self.wallB = box( 23 | pos=vector(0, -side, 0), 24 | size=vector(s3, thk, s3), 25 | color=color.blue, 26 | opacity=0, 27 | ) 28 | self.wallT = box( 29 | pos=vector(0, side, 0), 30 | size=vector(s3, thk, s3), 31 | color=color.blue, 32 | opacity=0, 33 | ) 34 | self.wallBK = box( 35 | pos=vector(0, 0, -side), 36 | size=vector(s2, s2, thk), 37 | color=color.gray(0.7), 38 | opacity=0, 39 | ) 40 | 41 | 42 | class fly(sphere): 43 | def __init__(self, side, thk, x, y, z, nudge_amt, blink_time): 44 | self.clock = random.uniform(0, 10) 45 | self.thk = thk 46 | self.blink_time = blink_time 47 | self.nudge_amt = nudge_amt 48 | self.ball = sphere( 49 | pos=vector(x, y, z), color=color.green, radius=0.1, retain=200 50 | ) 51 | self.ball.mass = 1.0 52 | self.ball.p = vector(np.random.randn(), np.random.randn(), np.random.randn()) 53 | 54 | self.side = side - self.thk * 0.5 - self.ball.radius 55 | 56 | def move(self, flys, radius, angle): 57 | dt = 0.3 58 | rate(200) 59 | self.ball.pos = self.ball.pos + (self.ball.p / self.ball.mass) * dt 60 | if not (self.side > self.ball.pos.x > -self.side): 61 | self.ball.p.x = -self.ball.p.x 62 | if not (self.side > self.ball.pos.y > -self.side): 63 | self.ball.p.y = -self.ball.p.y 64 | if not (self.side > self.ball.pos.z > -self.side): 65 | self.ball.p.z = -self.ball.p.z 66 | self.blink(flys, radius, angle) 67 | 68 | def blink(self, flys, radius, angle): 69 | num_neighbors = len(self.get_neighbors(flys, radius, angle)) 70 | self.clock += 1 71 | if num_neighbors > 0: 72 | self.clock += self.clock * self.nudge_amt 73 | if self.clock > self.blink_time: 74 | self.ball.color = color.white 75 | self.clock = 0 76 | else: 77 | self.ball.color = color.green 78 | 79 | def get_neighbors(self, flys, radius, angle): 80 | """Return a list of neighbors within a field of view. 81 | 82 | boids: list of boids 83 | radius: field of view radius 84 | angle: field of view angle in radians 85 | 86 | returns: list of Boid 87 | """ 88 | neighbors = [] 89 | for fly in flys: 90 | if fly is self: 91 | continue 92 | offset = fly.ball.pos - self.ball.pos 93 | 94 | # if not in range, skip it 95 | if offset.mag > radius: 96 | continue 97 | 98 | # if not within viewing angle, skip it 99 | # diff = self.vel.diff_angle(offset) 100 | # if abs(diff) > angle: 101 | # continue 102 | 103 | # otherwise add it to the list 104 | neighbors.append(fly) 105 | 106 | return neighbors 107 | 108 | 109 | fly_num = 500 # how many flys 110 | nudge_amt = 0.5 # how much seeing a neighbor nudges time 111 | blink_time = 15 # how many ticks per light up 112 | sight_radius = 3 # sight radius of each fly 113 | walls_thing = world() 114 | flys = [ 115 | fly( 116 | 4.0, 117 | 0.3, 118 | random.uniform(-3.5, 3.5), 119 | random.uniform(-3.5, 3.5), 120 | random.uniform(-3.5, 3.5), 121 | nudge_amt, 122 | blink_time, 123 | ) 124 | for i in range(fly_num) 125 | ] 126 | 127 | for i in range(1000): 128 | for fly in flys: 129 | fly.move(flys, sight_radius, 0) 130 | -------------------------------------------------------------------------------- /soln/firefly.py: -------------------------------------------------------------------------------- 1 | from vpython import * 2 | import numpy as np 3 | 4 | import random 5 | 6 | 7 | class world(box): 8 | def __init__(self): 9 | side = 4.0 10 | thk = 0.3 11 | s2 = 2 * side - thk 12 | s3 = 2 * side + thk 13 | self.wallR = box( 14 | pos=vector(side, 0, 0), size=vector(thk, s2, s3), color=color.red, opacity=0 15 | ) 16 | self.wallL = box( 17 | pos=vector(-side, 0, 0), 18 | size=vector(thk, s2, s3), 19 | color=color.red, 20 | opacity=0, 21 | ) 22 | self.wallB = box( 23 | pos=vector(0, -side, 0), 24 | size=vector(s3, thk, s3), 25 | color=color.blue, 26 | opacity=0, 27 | ) 28 | self.wallT = box( 29 | pos=vector(0, side, 0), 30 | size=vector(s3, thk, s3), 31 | color=color.blue, 32 | opacity=0, 33 | ) 34 | self.wallBK = box( 35 | pos=vector(0, 0, -side), 36 | size=vector(s2, s2, thk), 37 | color=color.gray(0.7), 38 | opacity=0, 39 | ) 40 | 41 | 42 | class fly(sphere): 43 | def __init__(self, side, thk, x, y, z, nudge_amt, blink_time): 44 | self.clock = random.uniform(0, 10) 45 | self.thk = thk 46 | self.blink_time = blink_time 47 | self.nudge_amt = nudge_amt 48 | self.ball = sphere( 49 | pos=vector(x, y, z), color=color.green, radius=0.1, retain=200 50 | ) 51 | self.ball.mass = 1.0 52 | self.ball.p = vector(np.random.randn(), np.random.randn(), np.random.randn()) 53 | 54 | self.side = side - self.thk * 0.5 - self.ball.radius 55 | 56 | def move(self, flys, radius, angle): 57 | dt = 0.3 58 | rate(200) 59 | self.ball.pos = self.ball.pos + (self.ball.p / self.ball.mass) * dt 60 | if not (self.side > self.ball.pos.x > -self.side): 61 | self.ball.p.x = -self.ball.p.x 62 | if not (self.side > self.ball.pos.y > -self.side): 63 | self.ball.p.y = -self.ball.p.y 64 | if not (self.side > self.ball.pos.z > -self.side): 65 | self.ball.p.z = -self.ball.p.z 66 | self.blink(flys, radius, angle) 67 | 68 | def blink(self, flys, radius, angle): 69 | num_neighbors = len(self.get_neighbors(flys, radius, angle)) 70 | self.clock += 1 71 | if num_neighbors > 0: 72 | self.clock += self.clock * self.nudge_amt 73 | if self.clock > self.blink_time: 74 | self.ball.color = color.white 75 | self.clock = 0 76 | else: 77 | self.ball.color = color.green 78 | 79 | def get_neighbors(self, flys, radius, angle): 80 | """Return a list of neighbors within a field of view. 81 | 82 | boids: list of boids 83 | radius: field of view radius 84 | angle: field of view angle in radians 85 | 86 | returns: list of Boid 87 | """ 88 | neighbors = [] 89 | for fly in flys: 90 | if fly is self: 91 | continue 92 | offset = fly.ball.pos - self.ball.pos 93 | 94 | # if not in range, skip it 95 | if offset.mag > radius: 96 | continue 97 | 98 | # if not within viewing angle, skip it 99 | # diff = self.vel.diff_angle(offset) 100 | # if abs(diff) > angle: 101 | # continue 102 | 103 | # otherwise add it to the list 104 | neighbors.append(fly) 105 | 106 | return neighbors 107 | 108 | 109 | fly_num = 500 # how many flys 110 | nudge_amt = 0.5 # how much seeing a neighbor nudges time 111 | blink_time = 15 # how many ticks per light up 112 | sight_radius = 3 # sight radius of each fly 113 | walls_thing = world() 114 | flys = [ 115 | fly( 116 | 4.0, 117 | 0.3, 118 | random.uniform(-3.5, 3.5), 119 | random.uniform(-3.5, 3.5), 120 | random.uniform(-3.5, 3.5), 121 | nudge_amt, 122 | blink_time, 123 | ) 124 | for i in range(fly_num) 125 | ] 126 | 127 | for i in range(1000): 128 | for fly in flys: 129 | fly.move(flys, sight_radius, 0) 130 | -------------------------------------------------------------------------------- /soln/quiz04.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "tags": [] 7 | }, 8 | "source": [ 9 | "# Quiz 4\n", 10 | "\n", 11 | "BEFORE YOU START THIS QUIZ:\n", 12 | "\n", 13 | "1. Click on \"Copy to Drive\" to make a copy of the quiz\n", 14 | "\n", 15 | "2. Click on \"Share\",\n", 16 | " \n", 17 | "3. Click on \"Change\" and select \"Anyone with this link can edit\"\n", 18 | " \n", 19 | "4. Click \"Copy link\" and\n", 20 | "\n", 21 | "5. Paste the link into [this Canvas assignment](https://canvas.olin.edu/courses/315/assignments/5071).\n", 22 | "\n", 23 | "This quiz is based on Chapters 9 and 10 of [Think Complexity, 2nd edition](https://thinkcomplex.com).\n", 24 | "\n", 25 | "Copyright 2021 Allen Downey, [MIT License](http://opensource.org/licenses/MIT)" 26 | ] 27 | }, 28 | { 29 | "cell_type": "code", 30 | "execution_count": 2, 31 | "metadata": {}, 32 | "outputs": [], 33 | "source": [ 34 | "from os.path import basename, exists\n", 35 | "\n", 36 | "def download(url):\n", 37 | " filename = basename(url)\n", 38 | " if not exists(filename):\n", 39 | " from urllib.request import urlretrieve\n", 40 | " local, _ = urlretrieve(url, filename)\n", 41 | " print('Downloaded ' + local)\n", 42 | " \n", 43 | "download('https://github.com/AllenDowney/ThinkComplexity2/raw/master/notebooks/utils.py')\n" 44 | ] 45 | }, 46 | { 47 | "cell_type": "code", 48 | "execution_count": 3, 49 | "metadata": {}, 50 | "outputs": [], 51 | "source": [ 52 | "import matplotlib.pyplot as plt\n", 53 | "import numpy as np\n", 54 | "import networkx as nx\n", 55 | "\n", 56 | "from utils import decorate" 57 | ] 58 | }, 59 | { 60 | "cell_type": "markdown", 61 | "metadata": {}, 62 | "source": [ 63 | "## Fireflies\n", 64 | "\n", 65 | "In Chapter 1 of Think Complexity, I wrote about Strogatz's model of spontaneous synchronization in some species of fireflies and his proof that “sync was inevitable”. I said:\n", 66 | "\n", 67 | "> Strogatz makes several simplifying assumptions, in particular that each firefly can see all the others. In my opinion, it is more interesting to explain how an entire valley of fireflies can synchronize despite the fact that they cannot all see each other.\n", 68 | ">\n", 69 | ">Explanations of these phenomena often use agent-based models, which explore ... the conditions that allow or prevent synchronization.\n", 70 | "\n", 71 | "So let's get back to that.\n", 72 | "\n", 73 | "Among many other awesome projects, [Nicky Case](https://ncase.me/faq/) has developed an online simulation of firefly synchronization, [which you can run here](https://ncase.me/fireflies/)." 74 | ] 75 | }, 76 | { 77 | "cell_type": "markdown", 78 | "metadata": {}, 79 | "source": [ 80 | "The goal of this activity is to replicate their model and explore its behavior as we vary some of its parameters.\n", 81 | "\n", 82 | "Here are the steps:\n", 83 | "\n", 84 | "1) Try out the simulation and read the text (in the left margin of the page) that explains the model.\n", 85 | "\n", 86 | "2) Think about what aspects of the model you will replicate and what you will leave out.\n", 87 | "\n", 88 | "3) Think about a parameter you might vary to see what effect it has.\n", 89 | "\n", 90 | "4) Implement the model and see if it exhibits spontaneous synchronization.\n", 91 | "\n", 92 | "5) Identify a parameter that can control whether synchronization occurs or not." 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "You should spend a minimum of 1.5 on this activity and a maximum of 3 hours. It might not be possible to complete all five steps in this time, but that's ok.\n", 100 | "The goals of this activity are to practice the initial stages of a project and to develop rapid prototyping skills.\n", 101 | "\n", 102 | "Some design decisions you should consider:\n", 103 | "\n", 104 | "* What is the nature of the space the fireflies live in? It could be a grid, a network, or continuous space in 1, 2, or 3 dimensions.\n", 105 | "\n", 106 | "* Do fireflies move in space, and how?\n", 107 | "\n", 108 | "* Which fireflies can \"see\" each other, and how do they interact?\n", 109 | "\n", 110 | "* How will you display the results, and how will you determine whether syncronization has occurred. (Animated displays are cool, but they might not be necessary and they can be a time sink.)" 111 | ] 112 | }, 113 | { 114 | "cell_type": "markdown", 115 | "metadata": {}, 116 | "source": [ 117 | "You are welcome to use external resources for this activity, but you should give credit to any resources you make substantial use of.\n", 118 | "\n", 119 | "You can discuss your plans with the instructor and other students. You can get help debugging, if needed. But the result should be substantially your own, independent work.\n", 120 | "\n", 121 | "Assuming that, as a class, we explore a variety of models and implementations, we should have some interesting results to compare!" 122 | ] 123 | }, 124 | { 125 | "cell_type": "code", 126 | "execution_count": null, 127 | "metadata": {}, 128 | "outputs": [], 129 | "source": [] 130 | } 131 | ], 132 | "metadata": { 133 | "kernelspec": { 134 | "display_name": "Python 3 (ipykernel)", 135 | "language": "python", 136 | "name": "python3" 137 | }, 138 | "language_info": { 139 | "codemirror_mode": { 140 | "name": "ipython", 141 | "version": 3 142 | }, 143 | "file_extension": ".py", 144 | "mimetype": "text/x-python", 145 | "name": "python", 146 | "nbconvert_exporter": "python", 147 | "pygments_lexer": "ipython3", 148 | "version": "3.7.11" 149 | } 150 | }, 151 | "nbformat": 4, 152 | "nbformat_minor": 4 153 | } 154 | -------------------------------------------------------------------------------- /nb/chap10.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Flocks, Herds, and Traffic Jams\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": 4, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import matplotlib.pyplot as plt\n", 21 | "import numpy as np" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 8, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "from os.path import basename, exists\n", 31 | "\n", 32 | "def download(url):\n", 33 | " filename = basename(url)\n", 34 | " if not exists(filename):\n", 35 | " from urllib.request import urlretrieve\n", 36 | " local, _ = urlretrieve(url, filename)\n", 37 | " print('Downloaded ' + local)\n", 38 | " \n", 39 | "download('https://github.com/AllenDowney/ThinkComplexity2/raw/master/notebooks/utils.py')\n", 40 | "download('https://github.com/AllenDowney/ThinkComplexity2/raw/master/notebooks/Cell2D.py')" 41 | ] 42 | }, 43 | { 44 | "cell_type": "code", 45 | "execution_count": 6, 46 | "metadata": {}, 47 | "outputs": [], 48 | "source": [ 49 | "from utils import decorate, savefig\n", 50 | "# make a directory for figures\n", 51 | "!mkdir -p figs" 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "metadata": {}, 57 | "source": [ 58 | "## Traffic jam\n", 59 | "\n", 60 | "Here's the `Driver` class from Chapter 10." 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": 7, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "class Driver:\n", 70 | " \n", 71 | " def __init__(self, loc, speed=4):\n", 72 | " \"\"\"Initialize the attributes.\n", 73 | " \n", 74 | " loc: position on track, in miles\n", 75 | " speed: speed in miles per hour\n", 76 | " \"\"\"\n", 77 | " self.start = loc\n", 78 | " self.loc = loc\n", 79 | " self.speed = speed\n", 80 | " \n", 81 | " def choose_acceleration(self, dist):\n", 82 | " \"\"\"Chooses acceleration based on distance.\n", 83 | " \n", 84 | " dist: distance from the car in front\n", 85 | " \n", 86 | " returns: acceleration\n", 87 | " \"\"\"\n", 88 | " return 1\n", 89 | " \n", 90 | " def set_odometer(self):\n", 91 | " self.start = self.loc\n", 92 | " \n", 93 | " def read_odometer(self):\n", 94 | " return self.loc - self.start" 95 | ] 96 | }, 97 | { 98 | "cell_type": "markdown", 99 | "metadata": {}, 100 | "source": [ 101 | "And the Highway." 102 | ] 103 | }, 104 | { 105 | "cell_type": "code", 106 | "execution_count": 9, 107 | "metadata": {}, 108 | "outputs": [], 109 | "source": [ 110 | "from Cell2D import Cell2D\n", 111 | "\n", 112 | "class Highway(Cell2D):\n", 113 | " \n", 114 | " max_acc = 1\n", 115 | " min_acc = -10\n", 116 | " speed_limit = 40\n", 117 | " \n", 118 | " def __init__(self, n=10, length=1000, eps=0, constructor=Driver):\n", 119 | " \"\"\"Initializes the attributes.\n", 120 | " \n", 121 | " n: number of drivers\n", 122 | " length: length of the track\n", 123 | " eps: variability in speed\n", 124 | " constructor: function used to instantiate drivers\n", 125 | " \"\"\"\n", 126 | " self.length = length\n", 127 | " self.eps = eps\n", 128 | " self.crashes = 0\n", 129 | "\n", 130 | " # create the drivers\n", 131 | " locs = np.linspace(0, length, n, endpoint=False)\n", 132 | " self.drivers = [constructor(loc) for loc in locs]\n", 133 | " \n", 134 | " # and link them up\n", 135 | " for i in range(n):\n", 136 | " j = (i+1) % n\n", 137 | " self.drivers[i].next = self.drivers[j]\n", 138 | " \n", 139 | " def step(self):\n", 140 | " \"\"\"Performs one time step.\"\"\"\n", 141 | " for driver in self.drivers:\n", 142 | " self.move(driver)\n", 143 | " \n", 144 | " def move(self, driver):\n", 145 | " \"\"\"Updates `driver`.\n", 146 | " \n", 147 | " driver: Driver object\n", 148 | " \"\"\"\n", 149 | " # get the distance to the next driver\n", 150 | " dist = self.distance(driver)\n", 151 | "\n", 152 | " # let the driver choose acceleration\n", 153 | " acc = driver.choose_acceleration(dist)\n", 154 | " acc = min(acc, self.max_acc)\n", 155 | " acc = max(acc, self.min_acc)\n", 156 | " speed = driver.speed + acc\n", 157 | " \n", 158 | " # add random noise to speed\n", 159 | " speed *= np.random.uniform(1-self.eps, 1+self.eps)\n", 160 | " \n", 161 | " # keep it nonnegative and under the speed limit\n", 162 | " speed = max(speed, 0)\n", 163 | " speed = min(speed, self.speed_limit)\n", 164 | " \n", 165 | " # if current speed would collide with next driver, stop\n", 166 | " if speed > dist:\n", 167 | " speed = 0\n", 168 | " self.crashes += 1\n", 169 | " \n", 170 | " # update speed and loc\n", 171 | " driver.speed = speed\n", 172 | " driver.loc += speed\n", 173 | " \n", 174 | " def distance(self, driver):\n", 175 | " \"\"\"Distance from `driver` to next driver.\n", 176 | " \n", 177 | " driver: Driver object\n", 178 | " \"\"\"\n", 179 | " dist = driver.next.loc - driver.loc\n", 180 | " # fix wraparound\n", 181 | " if dist < 0:\n", 182 | " dist += self.length\n", 183 | " return dist\n", 184 | " \n", 185 | " def set_odometers(self):\n", 186 | " return [driver.set_odometer()\n", 187 | " for driver in self.drivers] \n", 188 | " \n", 189 | " def read_odometers(self):\n", 190 | " return np.mean([driver.read_odometer()\n", 191 | " for driver in self.drivers])\n", 192 | " \n", 193 | " def draw(self):\n", 194 | " \"\"\"Draws the drivers and shows collisions.\n", 195 | " \"\"\"\n", 196 | " drivers = self.drivers\n", 197 | " xs, ys = self.get_coords(drivers)\n", 198 | " plt.plot(xs, ys, 'bs', markersize=10, alpha=0.7)\n", 199 | " \n", 200 | " stopped = [driver for driver in self.drivers \n", 201 | " if driver.speed==0]\n", 202 | " xs, ys = self.get_coords(stopped, r=0.8)\n", 203 | " plt.plot(xs, ys, 'r^', markersize=12, alpha=0.7)\n", 204 | " \n", 205 | " plt.axis('off')\n", 206 | " plt.axis('equal')\n", 207 | " plt.xlim([-1.05, 1.05])\n", 208 | " plt.ylim([-1.05, 1.05])\n", 209 | "\n", 210 | " def get_coords(self, drivers, r=1):\n", 211 | " \"\"\"Gets the coordinates of the drivers.\n", 212 | " \n", 213 | " Transforms from (row, col) to (x, y).\n", 214 | " \n", 215 | " drivers: sequence of Driver\n", 216 | " r: radius of the circle\n", 217 | " \n", 218 | " returns: tuple of sequences, (xs, ys)\n", 219 | " \"\"\"\n", 220 | " locs = np.array([driver.loc for driver in drivers])\n", 221 | " locs *= 2 * np.pi / self.length\n", 222 | " xs = r * np.cos(locs)\n", 223 | " ys = r * np.sin(locs)\n", 224 | " return xs, ys" 225 | ] 226 | }, 227 | { 228 | "cell_type": "markdown", 229 | "metadata": {}, 230 | "source": [ 231 | "Make the animation:" 232 | ] 233 | }, 234 | { 235 | "cell_type": "code", 236 | "execution_count": 10, 237 | "metadata": {}, 238 | "outputs": [], 239 | "source": [ 240 | "highway = Highway(30, eps=0.02)\n", 241 | "highway.animate(frames=50, interval=0.2)" 242 | ] 243 | }, 244 | { 245 | "cell_type": "markdown", 246 | "metadata": {}, 247 | "source": [ 248 | "And generate the figure:" 249 | ] 250 | }, 251 | { 252 | "cell_type": "code", 253 | "execution_count": 11, 254 | "metadata": {}, 255 | "outputs": [], 256 | "source": [ 257 | "from utils import three_frame\n", 258 | "\n", 259 | "highway = Highway(30, eps=0.02)\n", 260 | "three_frame(highway, [16, 1, 1], seed=22)\n", 261 | "savefig('figs/chap10-1')" 262 | ] 263 | }, 264 | { 265 | "cell_type": "markdown", 266 | "metadata": {}, 267 | "source": [ 268 | "Explore the relationship between `eps` and average speed." 269 | ] 270 | }, 271 | { 272 | "cell_type": "code", 273 | "execution_count": 12, 274 | "metadata": {}, 275 | "outputs": [], 276 | "source": [ 277 | "def run_simulation(eps, constructor=Driver, iters=100):\n", 278 | " res = []\n", 279 | " for n in range(5, 100, 5):\n", 280 | " highway = Highway(n, eps=eps, constructor=constructor)\n", 281 | " for i in range(iters):\n", 282 | " highway.step()\n", 283 | "\n", 284 | " highway.set_odometers()\n", 285 | " for i in range(iters):\n", 286 | " highway.step()\n", 287 | "\n", 288 | " res.append((n, highway.read_odometers() / iters))\n", 289 | " \n", 290 | " return np.transpose(res)" 291 | ] 292 | }, 293 | { 294 | "cell_type": "markdown", 295 | "metadata": {}, 296 | "source": [ 297 | "Generate the figure:" 298 | ] 299 | }, 300 | { 301 | "cell_type": "code", 302 | "execution_count": 13, 303 | "metadata": {}, 304 | "outputs": [], 305 | "source": [ 306 | "np.random.seed(20)\n", 307 | "from utils import set_palette\n", 308 | "set_palette('Blues', 4, reverse=True)\n", 309 | "\n", 310 | "for eps in [0.0, 0.001, 0.01]:\n", 311 | " xs, ys = run_simulation(eps)\n", 312 | " plt.plot(xs, ys, label='eps=%g' % eps)\n", 313 | " \n", 314 | "decorate(xlabel='Number of cars',\n", 315 | " ylabel='Average speed',\n", 316 | " xlim=[0, 100], ylim=[0, 42])\n", 317 | "\n", 318 | "savefig('figs/chap10-2')" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": {}, 324 | "source": [ 325 | "**Exercise:** In the traffic jam simulation, define a class, `BetterDriver`,\n", 326 | "that inherits from `Driver` and overrides `choose_acceleration`.\n", 327 | "See if you can define driving rules that do better than the basic\n", 328 | "implementation in `Driver`. You might try to achieve higher\n", 329 | "average speed, or a lower number of collisions.\n", 330 | "\n", 331 | "Here's a first attempt:" 332 | ] 333 | }, 334 | { 335 | "cell_type": "code", 336 | "execution_count": 24, 337 | "metadata": {}, 338 | "outputs": [], 339 | "source": [ 340 | "class BetterDriver(Driver):\n", 341 | " \n", 342 | " def choose_acceleration(self, d):\n", 343 | " if self.speed < 20:\n", 344 | " return 1\n", 345 | " else:\n", 346 | " return 0" 347 | ] 348 | }, 349 | { 350 | "cell_type": "markdown", 351 | "metadata": {}, 352 | "source": [ 353 | "The following loop runs simulations with `Driver` and `BetterDriver`, and plots average speed as a function of the number of cars.\n", 354 | "\n", 355 | "And it prints the area under the curves as one way (but certainly not the only way) to quantify the effect of driving behavior on average speed over the range of densities." 356 | ] 357 | }, 358 | { 359 | "cell_type": "code", 360 | "execution_count": 25, 361 | "metadata": {}, 362 | "outputs": [], 363 | "source": [ 364 | "from scipy.integrate import trapezoid\n", 365 | "\n", 366 | "for constructor in [Driver, BetterDriver]:\n", 367 | " xs, ys = run_simulation(eps=0.0, constructor=constructor)\n", 368 | " plt.plot(xs, ys, label=constructor.__name__)\n", 369 | " print(constructor.__name__, trapezoid(ys, xs))\n", 370 | " \n", 371 | "decorate(xlabel='Number of cars',\n", 372 | " ylabel='Average speed',\n", 373 | " xlim=[0, 100], ylim=[0, 42])" 374 | ] 375 | }, 376 | { 377 | "cell_type": "markdown", 378 | "metadata": {}, 379 | "source": [ 380 | "`BetterDriver` is a little better in the sense that it keeps traffic moving smoothly at medium densities. However:\n", 381 | "\n", 382 | "* At high densities, it has almost no effect, and\n", 383 | "\n", 384 | "* At low densities, it is substantially worse.\n", 385 | "\n", 386 | "As a result, the total are under the curve is much less.\n", 387 | "\n", 388 | "See if you can write rules for the agents that maximize the area under the curve." 389 | ] 390 | }, 391 | { 392 | "cell_type": "code", 393 | "execution_count": null, 394 | "metadata": {}, 395 | "outputs": [], 396 | "source": [] 397 | } 398 | ], 399 | "metadata": { 400 | "kernelspec": { 401 | "display_name": "Python 3 (ipykernel)", 402 | "language": "python", 403 | "name": "python3" 404 | }, 405 | "language_info": { 406 | "codemirror_mode": { 407 | "name": "ipython", 408 | "version": 3 409 | }, 410 | "file_extension": ".py", 411 | "mimetype": "text/x-python", 412 | "name": "python", 413 | "nbconvert_exporter": "python", 414 | "pygments_lexer": "ipython3", 415 | "version": "3.7.11" 416 | } 417 | }, 418 | "nbformat": 4, 419 | "nbformat_minor": 2 420 | } 421 | -------------------------------------------------------------------------------- /nb/chap00.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "f7485f06", 6 | "metadata": {}, 7 | "source": [ 8 | "# Preface\n", 9 | "\n", 10 | "Complexity science is an interdisciplinary field --- at the intersection\n", 11 | "of mathematics, computer science and natural science --- that focuses on\n", 12 | "**complex systems**, which are systems with many interacting components.\n", 13 | "\n", 14 | "One of the core tools of complexity science is discrete models,\n", 15 | "including networks and graphs, cellular automatons, and agent-based\n", 16 | "simulations. These tools are useful in the natural and social sciences,\n", 17 | "and sometimes in arts and humanities.\n", 18 | "\n", 19 | "For an overview of complexity science, see\n", 20 | ".\n", 21 | "\n", 22 | "Why should you learn about complexity science? Here are a few reasons:\n", 23 | "\n", 24 | "- Complexity science is useful, especially for explaining why natural\n", 25 | " and social systems behave the way they do. Since Newton, math-based\n", 26 | " physics has focused on systems with small numbers of components and\n", 27 | " simple interactions. These models are effective for some\n", 28 | " applications, like celestial mechanics, and less useful for others,\n", 29 | " like economics. Complexity science provides a diverse and adaptable\n", 30 | " modeling toolkit.\n", 31 | "\n", 32 | "- Many of the central results of complexity science are surprising; a\n", 33 | " recurring theme of this book is that simple models can produce\n", 34 | " complicated behavior, with the corollary that we can sometimes\n", 35 | " explain complicated behavior in the real world using simple models.\n", 36 | "\n", 37 | "- As I explain in\n", 38 | " Chapter [\\[overview\\]](#overview){reference-type=\"ref\"\n", 39 | " reference=\"overview\"}, complexity science is at the center of a slow\n", 40 | " shift in the practice of science and a change in what we consider\n", 41 | " science to be.\n", 42 | "\n", 43 | "- Studying complexity science provides an opportunity to learn about\n", 44 | " diverse physical and social systems, to develop and apply\n", 45 | " programming skills, and to think about fundamental questions in the\n", 46 | " philosophy of science.\n", 47 | "\n", 48 | "By reading this book and working on the exercises you will have a chance\n", 49 | "to explore topics and ideas you might not encounter otherwise, practice\n", 50 | "programming in Python, and learn more about data structures and\n", 51 | "algorithms." 52 | ] 53 | }, 54 | { 55 | "cell_type": "markdown", 56 | "id": "816e47ca", 57 | "metadata": {}, 58 | "source": [ 59 | "Features of this book include:\n", 60 | "\n", 61 | "Technical details\n", 62 | "\n", 63 | "Most books about complexity science are written for a popular\n", 64 | " audience. They leave out technical details, which is frustrating for\n", 65 | " people who can handle them. This book presents the code, the math,\n", 66 | " and the explanations you need to understand how the models work.\n", 67 | "\n", 68 | "Further reading\n", 69 | "\n", 70 | "Throughout the book, I include pointers to further reading,\n", 71 | " including original papers (most of which are available\n", 72 | " electronically) and related articles from Wikipedia and other\n", 73 | " sources.\n", 74 | "\n", 75 | "Jupyter notebooks\n", 76 | "\n", 77 | "For each chapter I provide a Jupyter notebook that includes the code\n", 78 | " from the chapter, additional examples, and animations that let you\n", 79 | " see the models in action.\n", 80 | "\n", 81 | "Exercises and solutions\n", 82 | "\n", 83 | "At the end of each chapter I suggest exercises you might want to\n", 84 | " work on, with solutions.\n", 85 | "\n", 86 | "For most of the links in this book I use URL redirection. This mechanism\n", 87 | "has the drawback of hiding the link destination, but it makes the URLs\n", 88 | "shorter and less obtrusive. Also, and more importantly, it allows me to\n", 89 | "update the links without updating the book. If you find a broken link,\n", 90 | "please let me know and I will change the redirection." 91 | ] 92 | }, 93 | { 94 | "cell_type": "markdown", 95 | "id": "5ce5bafc", 96 | "metadata": {}, 97 | "source": [ 98 | "## Who is this book for?\n", 99 | "\n", 100 | "The examples and supporting code for this book are in Python. You should\n", 101 | "know core Python and be familiar with its object-oriented features,\n", 102 | "specifically using and defining classes.\n", 103 | "\n", 104 | "If you are not already familiar with Python, you might want to start\n", 105 | "with *Think Python*, which is appropriate for people who have never\n", 106 | "programmed before. If you have programming experience in another\n", 107 | "language, there are many good Python books to choose from, as well as\n", 108 | "online resources.\n", 109 | "\n", 110 | "I use NumPy, SciPy, and NetworkX throughout the book. If you are\n", 111 | "familiar with these libraries already, that's great, but I will also\n", 112 | "explain them when they appear.\n", 113 | "\n", 114 | "I assume that the reader knows some mathematics: I use logarithms in\n", 115 | "several places, and vectors in one example. But that's about it." 116 | ] 117 | }, 118 | { 119 | "cell_type": "markdown", 120 | "id": "6550ef69", 121 | "metadata": {}, 122 | "source": [ 123 | "## Changes from the first edition\n", 124 | "\n", 125 | "For the second edition, I added two chapters, one on evolution, the\n", 126 | "other on the evolution of cooperation.\n", 127 | "\n", 128 | "In the first edition, each chapter presented background on a topic and\n", 129 | "suggested experiments the reader could run. For the second edition, I\n", 130 | "have done those experiments. Each chapter presents the implementation\n", 131 | "and results as a worked example, then suggests additional experiments\n", 132 | "for the reader.\n", 133 | "\n", 134 | "For the second edition, I replaced some of my own code with standard\n", 135 | "libraries like NumPy and NetworkX. The result is more concise and more\n", 136 | "efficient, and it gives readers a chance to learn these libraries.\n", 137 | "\n", 138 | "Also, the Jupyter notebooks are new. For every chapter there are two\n", 139 | "notebooks: one contains the code from the chapter, explanatory text, and\n", 140 | "exercises; the other contains solutions to the exercises.\n", 141 | "\n", 142 | "Finally, all supporting software has been updated to Python 3 (but most\n", 143 | "of it runs unmodified in Python 2)." 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "id": "bf0ecab7", 149 | "metadata": {}, 150 | "source": [ 151 | "## Using the code\n", 152 | "\n", 153 | "All code used in this book is available from a Git repository on GitHub:\n", 154 | ". If you are not familiar with Git, it is\n", 155 | "a version control system that allows you to keep track of the files that\n", 156 | "make up a project. A collection of files under Git's control is called a\n", 157 | "\"repository\". GitHub is a hosting service that provides storage for Git\n", 158 | "repositories and a convenient web interface.\n", 159 | "\n", 160 | "The GitHub homepage for my repository provides several ways to work with\n", 161 | "the code:\n", 162 | "\n", 163 | "- You can create a copy of my repository by pressing the Fork button\n", 164 | " in the upper right. If you don't already have a GitHub account,\n", 165 | " you'll need to create one. After forking, you'll have your own\n", 166 | " repository on GitHub that you can use to keep track of code you\n", 167 | " write while working on this book. Then you can clone the repo, which\n", 168 | " means that you copy the files to your computer.\n", 169 | "\n", 170 | "- Or you can clone my repository without forking; that is, you can\n", 171 | " make a copy of my repo on your computer. You don't need a GitHub\n", 172 | " account to do this, but you won't be able to write your changes back\n", 173 | " to GitHub.\n", 174 | "\n", 175 | "- If you don't want to use Git at all, you can download the files in a\n", 176 | " Zip file using the green button that says \"Clone or download\".\n", 177 | "\n", 178 | "I developed this book using Anaconda from Continuum Analytics, which is\n", 179 | "a free Python distribution that includes all the packages you'll need to\n", 180 | "run the code (and lots more). I found Anaconda easy to install. By\n", 181 | "default it does a user-level installation, not system-level, so you\n", 182 | "don't need administrative privileges. And it supports both Python 2 and\n", 183 | "Python 3. You can download Anaconda from\n", 184 | "." 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "id": "2e4c347d", 190 | "metadata": {}, 191 | "source": [ 192 | "The repository includes both Python scripts and Jupyter notebooks. If\n", 193 | "you have not used Jupyter before, you can read about it at\n", 194 | ".\n", 195 | "\n", 196 | "There are three ways you can work with the Jupyter notebooks:\n", 197 | "\n", 198 | "Run Jupyter on your computer\n", 199 | "\n", 200 | "If you installed Anaconda, you can install Jupyter by running the\n", 201 | " following command in a terminal or Command Window:" 202 | ] 203 | }, 204 | { 205 | "cell_type": "code", 206 | "execution_count": 1, 207 | "id": "58e2bcbf", 208 | "metadata": {}, 209 | "outputs": [], 210 | "source": [ 211 | "```\n", 212 | "$ conda install jupyter\n", 213 | "```" 214 | ] 215 | }, 216 | { 217 | "cell_type": "markdown", 218 | "id": "750c48fe", 219 | "metadata": {}, 220 | "source": [ 221 | "Before you launch Jupyter, you should `cd` into the directory that\n", 222 | " contains the code:" 223 | ] 224 | }, 225 | { 226 | "cell_type": "code", 227 | "execution_count": null, 228 | "id": "021a1c71", 229 | "metadata": {}, 230 | "outputs": [], 231 | "source": [ 232 | "```\n", 233 | "$ cd ThinkComplexity2/code\n", 234 | "```" 235 | ] 236 | }, 237 | { 238 | "cell_type": "markdown", 239 | "id": "c2822354", 240 | "metadata": {}, 241 | "source": [ 242 | "And then start the Jupyter server:" 243 | ] 244 | }, 245 | { 246 | "cell_type": "code", 247 | "execution_count": null, 248 | "id": "069f9890", 249 | "metadata": {}, 250 | "outputs": [], 251 | "source": [ 252 | "```\n", 253 | "$ jupyter notebook\n", 254 | "```" 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "id": "24f1be81", 260 | "metadata": {}, 261 | "source": [ 262 | "When you start the server, it should launch your default web browser\n", 263 | " or create a new tab in an open browser window. Then you can open and\n", 264 | " run the notebooks.\n", 265 | "\n", 266 | "- Run Jupyter on Colab\n", 267 | "\n", 268 | "TODO: Fill this in" 269 | ] 270 | }, 271 | { 272 | "cell_type": "markdown", 273 | "id": "e1f71bfb", 274 | "metadata": {}, 275 | "source": [ 276 | "You can run the scripts and modify them to run your own code, but\n", 277 | "the virtual machine you run them in is temporary. If you leave it\n", 278 | "idle, the virtual machine disappears along with any changes you\n", 279 | "made." 280 | ] 281 | }, 282 | { 283 | "cell_type": "markdown", 284 | "id": "3f85e416", 285 | "metadata": {}, 286 | "source": [ 287 | "- View notebooks on GitHub\n", 288 | "\n", 289 | "GitHub provides a view of the notebooks you can can use to read the\n", 290 | " notebooks and see the results I generated, but you won't be able to\n", 291 | " modify or run the code.\n", 292 | "\n", 293 | "Good luck, and have fun!" 294 | ] 295 | }, 296 | { 297 | "cell_type": "markdown", 298 | "id": "2f6dffd2", 299 | "metadata": {}, 300 | "source": [ 301 | "## Contributor List {#contributor-list .unnumbered}\n", 302 | "\n", 303 | "If you have a suggestion or correction, please send email to\n", 304 | "`downey@allendowney.com`. If I make a change based on your feedback, I\n", 305 | "will add you to the contributor list (unless you ask to be omitted).\n", 306 | "\n", 307 | "Let me know what version of the book you are working with, and what\n", 308 | "format. If you include at least part of the sentence the error appears\n", 309 | "in, that makes it easy for me to search. Page and section numbers are\n", 310 | "fine, too, but not quite as easy to work with. Thanks!\n", 311 | "\n", 312 | "- John Harley, Jeff Stanton, Colden Rouleau and Keerthik Omanakuttan\n", 313 | " are Computational Modeling students who pointed out typos.\n", 314 | "\n", 315 | "- Jose Oscar Mur-Miranda found several typos.\n", 316 | "\n", 317 | "- Phillip Loh, Corey Dolphin, Noam Rubin and Julian Ceipek found typos\n", 318 | " and made helpful suggestions.\n", 319 | "\n", 320 | "- Sebastian Schöner sent two pages of corrections!\n", 321 | "\n", 322 | "- Philipp Marek sent a number of corrections.\n", 323 | "\n", 324 | "- Jason Woodard co-taught Complexity Science with me at Olin College,\n", 325 | " introduced me to NK models, and made many helpful suggestions and\n", 326 | " corrections.\n", 327 | "\n", 328 | "- Davi Post sent several corrections and suggestions.\n", 329 | "\n", 330 | "- Graham Taylor sent a pull request on GitHub that fixed many typos.\n", 331 | "\n", 332 | "I would especially like to thank the technical reviewers, Vincent Knight\n", 333 | "and Eric Ma, who made many helpful suggestions, and the copy editor,\n", 334 | "Charles Roumeliotis, who caught many errors and inconsistencies.\n", 335 | "\n", 336 | "Other people who reported errors include Richard Hollands, Muhammad\n", 337 | "Najmi bin Ahmad Zabidi, Alex Hantman, and Jonathan Harford." 338 | ] 339 | } 340 | ], 341 | "metadata": { 342 | "kernelspec": { 343 | "display_name": "Python 3 (ipykernel)", 344 | "language": "python", 345 | "name": "python3" 346 | }, 347 | "language_info": { 348 | "codemirror_mode": { 349 | "name": "ipython", 350 | "version": 3 351 | }, 352 | "file_extension": ".py", 353 | "mimetype": "text/x-python", 354 | "name": "python", 355 | "nbconvert_exporter": "python", 356 | "pygments_lexer": "ipython3", 357 | "version": "3.10.14" 358 | } 359 | }, 360 | "nbformat": 4, 361 | "nbformat_minor": 5 362 | } 363 | -------------------------------------------------------------------------------- /soln/majority.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Agent Based Models\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": 13, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "%matplotlib inline\n", 21 | "\n", 22 | "import matplotlib.pyplot as plt\n", 23 | "import numpy as np\n", 24 | "import seaborn as sns\n", 25 | "\n", 26 | "from utils import decorate, savefig" 27 | ] 28 | }, 29 | { 30 | "cell_type": "code", 31 | "execution_count": 14, 32 | "metadata": {}, 33 | "outputs": [], 34 | "source": [ 35 | "def locs_where(condition):\n", 36 | " \"\"\"Find cells where a logical array is True.\n", 37 | " \n", 38 | " condition: logical array\n", 39 | " \n", 40 | " returns: list of location tuples\n", 41 | " \"\"\"\n", 42 | " return list(np.argwhere(condition))" 43 | ] 44 | }, 45 | { 46 | "cell_type": "markdown", 47 | "metadata": {}, 48 | "source": [ 49 | "## Discrepancy" 50 | ] 51 | }, 52 | { 53 | "cell_type": "code", 54 | "execution_count": 24, 55 | "metadata": {}, 56 | "outputs": [], 57 | "source": [ 58 | "from scipy.signal import correlate2d\n", 59 | "from Cell2D import Cell2D, draw_array\n", 60 | "\n", 61 | "from matplotlib.colors import LinearSegmentedColormap\n", 62 | "\n", 63 | "# make a custom color map\n", 64 | "palette = sns.color_palette('muted')\n", 65 | "colors = 'white', palette[1], palette[0]\n", 66 | "cmap = LinearSegmentedColormap.from_list('cmap', colors)\n", 67 | "\n", 68 | "\n", 69 | "class Majority(Cell2D):\n", 70 | " \"\"\"Represents a grid of Schelling agents.\"\"\"\n", 71 | " \n", 72 | " options = dict(mode='same', boundary='wrap')\n", 73 | "\n", 74 | " kernel = np.array([[-1, -1, -1],\n", 75 | " [-1, 8, -1],\n", 76 | " [-1, -1, -1]]) / 8\n", 77 | " \n", 78 | " def __init__(self, n):\n", 79 | " \"\"\"Initializes the attributes.\n", 80 | "\n", 81 | " n: number of rows\n", 82 | " p: threshold on the fraction of similar neighbors\n", 83 | " \"\"\"\n", 84 | " self.red = np.random.uniform(1, 100, (n, n))\n", 85 | " self.blue = np.random.uniform(1, 100, (n, n))\n", 86 | "\n", 87 | " def count_neighbors(self):\n", 88 | " \"\"\"Surveys neighboring cells.\n", 89 | " \n", 90 | " returns: tuple of\n", 91 | " empty: True where cells are empty\n", 92 | " frac_red: fraction of red neighbors around each cell\n", 93 | " frac_blue: fraction of blue neighbors around each cell\n", 94 | " frac_same: fraction of neighbors with the same color\n", 95 | " \"\"\"\n", 96 | " red = self.red\n", 97 | " blue = self.blue\n", 98 | "\n", 99 | " ratio = self.red / (self.red + self.blue) * 100\n", 100 | " \n", 101 | " discrepancy = correlate2d(ratio, self.kernel, **self.options)\n", 102 | " \n", 103 | " locs = locs_where(np.abs(discrepancy) > 20)\n", 104 | " \n", 105 | " for loc in locs:\n", 106 | " print(loc)\n", 107 | " return discrepancy\n", 108 | " \n", 109 | " def step(self):\n", 110 | " \"\"\"Executes one time step.\n", 111 | " \n", 112 | " returns: fraction of similar neighbors, averaged over cells\n", 113 | " \"\"\"\n", 114 | " return self.count_neighbors()\n", 115 | " \n", 116 | " def draw(self):\n", 117 | " \"\"\"Draws the cells.\"\"\"\n", 118 | " draw_array(self.red, cmap='Reds', alpha=0.3, vmax=100)\n", 119 | " draw_array(self.blue, cmap='Blues', alpha=0.3, vmax=100)" 120 | ] 121 | }, 122 | { 123 | "cell_type": "markdown", 124 | "metadata": {}, 125 | "source": [ 126 | "Here's a small example." 127 | ] 128 | }, 129 | { 130 | "cell_type": "code", 131 | "execution_count": 25, 132 | "metadata": {}, 133 | "outputs": [ 134 | { 135 | "data": { 136 | "image/png": "iVBORw0KGgoAAAANSUhEUgAAAOsAAADrCAYAAACICmHVAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjEsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8QZhcZAAADf0lEQVR4nO3ZTWpTUQCG4RtNlSL1ZyC0ZNBZR7qLbtKJG3Avzh104MwfFFRaqcFcN9BoA4Z7X32e6TmDj8CbE8hiHMcBmL87Uw8AbkesECFWiBArRIgVIsQKEctdLj959HhcHZ/sa0veeuNvsD85PPA+/M6bi4ur79fXD2462ynW1fHJ8OrFy7+z6h/0/nI99YTZe35yNPWEWXt2fv5l25mvOYgQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsULEcpfL65+b4cPXH/vakrc8O5t6wuy9/vR56gmzdjkutp55WSFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoWI5S6X79+7O5yujva1Je/ht3dTT5i9q4PN1BNm7XDY/vl4WSFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsUKEWCFCrBAhVogQK0SIFSLEChFihQixQoRYIUKsECFWiBArRIgVIsQKEWKFCLFChFghQqwQIVaIECtEiBUixAoRYoUIsULEYhzH219eLD4Ow/B2f3Pgv3c6juPTmw52ihWYjp/BECFWiBArRIgVIsQKEWKFCLFChFghQqwQ8QtACzHMirz0aAAAAABJRU5ErkJggg==\n", 137 | "text/plain": [ 138 | "
" 139 | ] 140 | }, 141 | "metadata": {}, 142 | "output_type": "display_data" 143 | } 144 | ], 145 | "source": [ 146 | "grid = Majority(n=3)\n", 147 | "grid.draw()" 148 | ] 149 | }, 150 | { 151 | "cell_type": "markdown", 152 | "metadata": {}, 153 | "source": [] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 26, 158 | "metadata": {}, 159 | "outputs": [ 160 | { 161 | "name": "stdout", 162 | "output_type": "stream", 163 | "text": [ 164 | "[1 1]\n", 165 | "[1 2]\n", 166 | "[2 1]\n", 167 | "[2 2]\n" 168 | ] 169 | }, 170 | { 171 | "data": { 172 | "text/plain": [ 173 | "array([[ 11.06608472, 5.00716876, 14.74706906],\n", 174 | " [ -4.25662316, -57.85760587, -42.07423947],\n", 175 | " [ -1.63601514, 34.00275477, 41.00140632]])" 176 | ] 177 | }, 178 | "execution_count": 26, 179 | "metadata": {}, 180 | "output_type": "execute_result" 181 | } 182 | ], 183 | "source": [ 184 | "grid.step()" 185 | ] 186 | }, 187 | { 188 | "cell_type": "markdown", 189 | "metadata": {}, 190 | "source": [ 191 | "Suppose we want to multiply two 2-digit base-m numbers: x1 m + x2 and y1 m + y2:\n", 192 | "\n", 193 | "1. compute x1 · y1, call the result F\n", 194 | "2. compute x2 · y2, call the result G\n", 195 | "3. compute (x1 + x2) · (y1 + y2), call the result H\n", 196 | "4. compute H − F − G, call the result K; this number is equal to x1 · y2 + x2 · y1\n", 197 | "5. compute F · m2 + K · m + G." 198 | ] 199 | }, 200 | { 201 | "cell_type": "code", 202 | "execution_count": 18, 203 | "metadata": {}, 204 | "outputs": [], 205 | "source": [ 206 | "m = 10\n", 207 | "m2 = m**2\n", 208 | "\n", 209 | "def karatsuba_norec(x, y):\n", 210 | " x1, x2 = divmod(x, m)\n", 211 | " y1, y2 = divmod(y, m)\n", 212 | " F = x1 * y1 \n", 213 | " G = x2 * y2\n", 214 | " H = (x1 + x2) * (y1 + y2)\n", 215 | " K = H - F - G\n", 216 | " a = F * m2\n", 217 | " b = K * m\n", 218 | " return a + b + G" 219 | ] 220 | }, 221 | { 222 | "cell_type": "code", 223 | "execution_count": 19, 224 | "metadata": {}, 225 | "outputs": [ 226 | { 227 | "data": { 228 | "text/plain": [ 229 | "56088" 230 | ] 231 | }, 232 | "execution_count": 19, 233 | "metadata": {}, 234 | "output_type": "execute_result" 235 | } 236 | ], 237 | "source": [ 238 | "karatsuba_norec(123, 456)" 239 | ] 240 | }, 241 | { 242 | "cell_type": "code", 243 | "execution_count": 27, 244 | "metadata": {}, 245 | "outputs": [], 246 | "source": [ 247 | "m = 10\n", 248 | "m2 = m**2\n", 249 | "\n", 250 | "def karatsuba(x, y):\n", 251 | " print(x, y)\n", 252 | " if x < m and y < m:\n", 253 | " return x * y\n", 254 | " \n", 255 | " x1, x2 = divmod(x, m)\n", 256 | " y1, y2 = divmod(y, m)\n", 257 | " F = karatsuba(x1, y1) \n", 258 | " G = karatsuba(x2, y2)\n", 259 | " H = karatsuba(x1 + x2, y1 + y2)\n", 260 | " K = H - F - G\n", 261 | " a = F * m2\n", 262 | " b = K * m\n", 263 | " return a + b + G" 264 | ] 265 | }, 266 | { 267 | "cell_type": "code", 268 | "execution_count": 28, 269 | "metadata": {}, 270 | "outputs": [ 271 | { 272 | "name": "stdout", 273 | "output_type": "stream", 274 | "text": [ 275 | "123 456\n", 276 | "12 45\n", 277 | "1 4\n", 278 | "2 5\n", 279 | "3 9\n", 280 | "3 6\n", 281 | "15 51\n", 282 | "1 5\n", 283 | "5 1\n", 284 | "6 6\n" 285 | ] 286 | }, 287 | { 288 | "data": { 289 | "text/plain": [ 290 | "56088" 291 | ] 292 | }, 293 | "execution_count": 28, 294 | "metadata": {}, 295 | "output_type": "execute_result" 296 | } 297 | ], 298 | "source": [ 299 | "karatsuba(123, 456)" 300 | ] 301 | }, 302 | { 303 | "cell_type": "code", 304 | "execution_count": 22, 305 | "metadata": {}, 306 | "outputs": [ 307 | { 308 | "data": { 309 | "text/plain": [ 310 | "56088" 311 | ] 312 | }, 313 | "execution_count": 22, 314 | "metadata": {}, 315 | "output_type": "execute_result" 316 | } 317 | ], 318 | "source": [ 319 | "123 * 456" 320 | ] 321 | }, 322 | { 323 | "cell_type": "code", 324 | "execution_count": 1, 325 | "metadata": {}, 326 | "outputs": [ 327 | { 328 | "name": "stdout", 329 | "output_type": "stream", 330 | "text": [ 331 | "1\n", 332 | "2\n", 333 | "fizz\n", 334 | "4\n", 335 | "buzz\n", 336 | "fizz\n", 337 | "7\n", 338 | "8\n", 339 | "fizz\n", 340 | "buzz\n", 341 | "11\n", 342 | "fizz\n", 343 | "13\n", 344 | "14\n", 345 | "fizzbuzz\n", 346 | "16\n", 347 | "17\n", 348 | "fizz\n", 349 | "19\n", 350 | "buzz\n", 351 | "fizz\n", 352 | "22\n", 353 | "23\n", 354 | "fizz\n", 355 | "buzz\n", 356 | "26\n", 357 | "fizz\n", 358 | "28\n", 359 | "29\n", 360 | "fizzbuzz\n", 361 | "31\n", 362 | "32\n", 363 | "fizz\n", 364 | "34\n", 365 | "buzz\n", 366 | "fizz\n", 367 | "37\n", 368 | "38\n", 369 | "fizz\n", 370 | "buzz\n", 371 | "41\n", 372 | "fizz\n", 373 | "43\n", 374 | "44\n", 375 | "fizzbuzz\n", 376 | "46\n", 377 | "47\n", 378 | "fizz\n", 379 | "49\n", 380 | "buzz\n", 381 | "fizz\n", 382 | "52\n", 383 | "53\n", 384 | "fizz\n", 385 | "buzz\n", 386 | "56\n", 387 | "fizz\n", 388 | "58\n", 389 | "59\n", 390 | "fizzbuzz\n", 391 | "61\n", 392 | "62\n", 393 | "fizz\n", 394 | "64\n", 395 | "buzz\n", 396 | "fizz\n", 397 | "67\n", 398 | "68\n", 399 | "fizz\n", 400 | "buzz\n", 401 | "71\n", 402 | "fizz\n", 403 | "73\n", 404 | "74\n", 405 | "fizzbuzz\n", 406 | "76\n", 407 | "77\n", 408 | "fizz\n", 409 | "79\n", 410 | "buzz\n", 411 | "fizz\n", 412 | "82\n", 413 | "83\n", 414 | "fizz\n", 415 | "buzz\n", 416 | "86\n", 417 | "fizz\n", 418 | "88\n", 419 | "89\n", 420 | "fizzbuzz\n", 421 | "91\n", 422 | "92\n", 423 | "fizz\n", 424 | "94\n", 425 | "buzz\n", 426 | "fizz\n", 427 | "97\n", 428 | "98\n", 429 | "fizz\n", 430 | "buzz\n" 431 | ] 432 | } 433 | ], 434 | "source": [ 435 | "d = {(True, False): 'fizz',\n", 436 | " (False, True): 'buzz',\n", 437 | " (True, True): 'fizzbuzz'}\n", 438 | "\n", 439 | "for x in range(1, 101):\n", 440 | " t = (x%3==0, x%5==0)\n", 441 | " print(d.get(t, x))" 442 | ] 443 | }, 444 | { 445 | "cell_type": "code", 446 | "execution_count": 12, 447 | "metadata": {}, 448 | "outputs": [ 449 | { 450 | "data": { 451 | "text/plain": [ 452 | "array(['1', '2', 'fizz', '4', 'buzz', 'fizz', '7', '8', 'fizz', 'buzz',\n", 453 | " '11', 'fizz', '13', '14', 'fizzbuzz', '16', '17', 'fizz', '19',\n", 454 | " 'buzz', 'fizz', '22', '23', 'fizz', 'buzz', '26', 'fizz', '28',\n", 455 | " '29', 'fizzbuzz', '31', '32', 'fizz', '34', 'buzz', 'fizz', '37',\n", 456 | " '38', 'fizz', 'buzz', '41', 'fizz', '43', '44', 'fizzbuzz', '46',\n", 457 | " '47', 'fizz', '49', 'buzz', 'fizz', '52', '53', 'fizz', 'buzz',\n", 458 | " '56', 'fizz', '58', '59', 'fizzbuzz', '61', '62', 'fizz', '64',\n", 459 | " 'buzz', 'fizz', '67', '68', 'fizz', 'buzz', '71', 'fizz', '73',\n", 460 | " '74', 'fizzbuzz', '76', '77', 'fizz', '79', 'buzz', 'fizz', '82',\n", 461 | " '83', 'fizz', 'buzz', '86', 'fizz', '88', '89', 'fizzbuzz', '91',\n", 462 | " '92', 'fizz', '94', 'buzz', 'fizz', '97', '98', 'fizz'],\n", 463 | " dtype=' The Schelling model of the city is an array of cells where each cell\n", 45 | "> represents a house. The houses are occupied by two kinds of \"agents\",\n", 46 | "> labeled red and blue, in roughly equal numbers. About 10% of the\n", 47 | "> houses are empty.\n", 48 | ">\n", 49 | "> At any point in time, an agent might be happy or unhappy, depending on\n", 50 | "> the other agents in the neighborhood. In one version of the model,\n", 51 | "> agents are happy if they have at least two neighbors like themselves,\n", 52 | "> and unhappy if they have one or zero.\n", 53 | ">\n", 54 | "> The simulation proceeds by choosing an agent at random and checking to\n", 55 | "> see whether it is happy. If so, nothing happens; if not, the agent\n", 56 | "> chooses one of the unoccupied cells at random and moves.\n", 57 | "\n", 58 | "If you start with a simulated city that is entirely unsegregated and run\n", 59 | "the model for a short time, clusters of similar agents appear. As time\n", 60 | "passes, the clusters grow and coalesce until there are a small number of\n", 61 | "large clusters and most agents live in homogeneous neighborhoods." 62 | ] 63 | }, 64 | { 65 | "cell_type": "markdown", 66 | "id": "37b53c30", 67 | "metadata": {}, 68 | "source": [ 69 | "The degree of segregation in the model is surprising, and it suggests an\n", 70 | "explanation of segregation in real cities. Maybe Detroit is segregated\n", 71 | "because people prefer not to be greatly outnumbered and will move if the\n", 72 | "composition of their neighborhoods makes them unhappy.\n", 73 | "\n", 74 | "Is this explanation satisfying in the same way as the explanation of\n", 75 | "planetary motion? Many people would say not, but why?\n", 76 | "\n", 77 | "Most obviously, the Schelling model is highly abstract, which is to say\n", 78 | "not realistic. So you might be tempted to say that people are more\n", 79 | "complicated than planets. But that can't be right. After all, some\n", 80 | "planets have people on them, so they have to be more complicated than\n", 81 | "people.\n", 82 | "\n", 83 | "Both systems are complicated, and both models are based on\n", 84 | "simplifications. For example, in the model of planetary motion we\n", 85 | "include forces between the planet and its sun, and ignore interactions\n", 86 | "between planets. In Schelling's model, we include individual decisions\n", 87 | "based on local information, and ignore every other aspect of human\n", 88 | "behavior." 89 | ] 90 | }, 91 | { 92 | "cell_type": "markdown", 93 | "id": "69135c1b", 94 | "metadata": {}, 95 | "source": [ 96 | "But there are differences of degree. For planetary motion, we can defend\n", 97 | "the model by showing that the forces we ignore are smaller than the ones\n", 98 | "we include. And we can extend the model to include other interactions\n", 99 | "and show that the effect is small. For Schelling's model it is harder to\n", 100 | "justify the simplifications.\n", 101 | "\n", 102 | "Another difference is that Schelling's model doesn't appeal to any\n", 103 | "physical laws, and it uses only simple computation, not mathematical\n", 104 | "derivation. Models like Schelling's don't look like classical science,\n", 105 | "and many people find them less compelling, at least at first. But as I\n", 106 | "will try to demonstrate, these models do useful work, including\n", 107 | "prediction, explanation, and design. One of the goals of this book is to\n", 108 | "explain how." 109 | ] 110 | }, 111 | { 112 | "cell_type": "markdown", 113 | "id": "837a103e", 114 | "metadata": {}, 115 | "source": [ 116 | "## The changing criteria of science\n", 117 | "\n", 118 | "Complexity science is not just a different set of models; it is also a\n", 119 | "gradual shift in the criteria models are judged by, and in the kinds of\n", 120 | "models that are considered acceptable.\n", 121 | "\n", 122 | "For example, classical models tend to be law-based, expressed in the\n", 123 | "form of equations, and solved by mathematical derivation. Models that\n", 124 | "fall under the umbrella of complexity are often rule-based, expressed as\n", 125 | "computations, and simulated rather than analyzed.\n", 126 | "\n", 127 | "Not everyone finds these models satisfactory. For example, in *Sync*,\n", 128 | "Steven Strogatz writes about his model of spontaneous synchronization in\n", 129 | "some species of fireflies. He presents a simulation that demonstrates\n", 130 | "the phenomenon, but then writes:\n", 131 | "\n", 132 | "> I repeated the simulation dozens of times, for other random initial\n", 133 | "> conditions and for other numbers of oscillators. Sync every time.\n", 134 | "> \\[\\...\\] The challenge now was to prove it. Only an ironclad proof\n", 135 | "> would demonstrate, in a way that no computer ever could, that sync was\n", 136 | "> inevitable; and the best kind of proof would clarify *why* it was\n", 137 | "> inevitable.\n", 138 | "\n", 139 | "Strogatz is a mathematician, so his enthusiasm for proofs is\n", 140 | "understandable, but his proof doesn't address what is, to me, the most\n", 141 | "interesting part of the phenomenon. In order to prove that \"sync was\n", 142 | "inevitable\", Strogatz makes several simplifying assumptions, in\n", 143 | "particular that each firefly can see all the others." 144 | ] 145 | }, 146 | { 147 | "cell_type": "markdown", 148 | "id": "1df4f90b", 149 | "metadata": {}, 150 | "source": [ 151 | "In my opinion, it is more interesting to explain how an entire valley of\n", 152 | "fireflies can synchronize *despite the fact that they cannot all see\n", 153 | "each other*. How this kind of global behavior emerges from local\n", 154 | "interactions is the subject of\n", 155 | "Chapter [\\[agent-based\\]](#agent-based){reference-type=\"ref\"\n", 156 | "reference=\"agent-based\"}. Explanations of these phenomena often use\n", 157 | "agent-based models, which explore (in ways that would be difficult or\n", 158 | "impossible with mathematical analysis) the conditions that allow or\n", 159 | "prevent synchronization.\n", 160 | "\n", 161 | "I am a computer scientist, so my enthusiasm for computational models is\n", 162 | "probably no surprise. I don't mean to say that Strogatz is wrong, but\n", 163 | "rather that people have different opinions about what questions to ask\n", 164 | "and what tools to use to answer them. These opinions are based on value\n", 165 | "judgments, so there is no reason to expect agreement.\n", 166 | "\n", 167 | "Nevertheless, there is rough consensus among scientists about which\n", 168 | "models are considered good science, and which others are fringe science,\n", 169 | "pseudoscience, or not science at all." 170 | ] 171 | }, 172 | { 173 | "cell_type": "markdown", 174 | "id": "9181496d", 175 | "metadata": {}, 176 | "source": [ 177 | "A central thesis of this book is that the criteria this consensus is\n", 178 | "based on change over time, and that the emergence of complexity science\n", 179 | "reflects a gradual shift in these criteria." 180 | ] 181 | }, 182 | { 183 | "cell_type": "markdown", 184 | "id": "d763e4ef", 185 | "metadata": {}, 186 | "source": [ 187 | "## The axes of scientific models\n", 188 | "\n", 189 | "I have described classical models as based on physical laws, expressed\n", 190 | "in the form of equations, and solved by mathematical analysis;\n", 191 | "conversely, models of complex systems are often based on simple rules\n", 192 | "and implemented as computations.\n", 193 | "\n", 194 | "We can think of this trend as a shift over time along two axes:\n", 195 | "\n", 196 | "Equation-based  simulation-based\n", 197 | "\n", 198 | "\n", 199 | "\n", 200 | "Analysis  computation" 201 | ] 202 | }, 203 | { 204 | "cell_type": "markdown", 205 | "id": "681a47bb", 206 | "metadata": {}, 207 | "source": [ 208 | "Complexity science is different in several other ways. I present them\n", 209 | "here so you know what's coming, but some of them might not make sense\n", 210 | "until you have seen the examples later in the book.\n", 211 | "\n", 212 | "Continuous  discrete\n", 213 | "\n", 214 | "Classical models tend to be based on continuous mathematics, like\n", 215 | " calculus; models of complex systems are often based on discrete\n", 216 | " mathematics, including graphs and cellular automatons.\n", 217 | "\n", 218 | "Linear  nonlinear\n", 219 | "\n", 220 | "Classical models are often linear, or use linear approximations to\n", 221 | " nonlinear systems; complexity science is more friendly to nonlinear\n", 222 | " models.\n", 223 | "\n", 224 | "Deterministic  stochastic\n", 225 | "\n", 226 | "Classical models are usually deterministic, which may reflect\n", 227 | " underlying philosophical determinism, discussed in\n", 228 | " Chapter [\\[automatons\\]](#automatons){reference-type=\"ref\"\n", 229 | " reference=\"automatons\"}; complex models often include randomness.\n", 230 | "\n", 231 | "Abstract  detailed\n", 232 | "\n", 233 | "In classical models, planets are point masses, planes are\n", 234 | " frictionless, and cows are spherical (see\n", 235 | " ). Simplifications like these are\n", 236 | " often necessary for analysis, but computational models can be more\n", 237 | " realistic.\n", 238 | "\n", 239 | "One, two  many\n", 240 | "\n", 241 | "Classical models are often limited to small numbers of components.\n", 242 | " For example, in celestial mechanics the two-body problem can be\n", 243 | " solved analytically; the three-body problem cannot. Complexity\n", 244 | " science often works with large numbers of components and larger\n", 245 | " number of interactions.\n", 246 | "\n", 247 | "Homogeneous  heterogeneous\n", 248 | "\n", 249 | "In classical models, the components and interactions tend to be\n", 250 | " identical; complex models more often include heterogeneity.\n", 251 | "\n", 252 | "These are generalizations, so we should not take them too seriously. And\n", 253 | "I don't mean to deprecate classical science. A more complicated model is\n", 254 | "not necessarily better; in fact, it is usually worse." 255 | ] 256 | }, 257 | { 258 | "cell_type": "markdown", 259 | "id": "4e8af9eb", 260 | "metadata": {}, 261 | "source": [ 262 | "And I don't mean to say that these changes are abrupt or complete.\n", 263 | "Rather, there is a gradual migration in the frontier of what is\n", 264 | "considered acceptable, respectable work. Some tools that used to be\n", 265 | "regarded with suspicion are now common, and some models that were widely\n", 266 | "accepted are now regarded with scrutiny.\n", 267 | "\n", 268 | "For example, when Appel and Haken proved the four-color theorem in 1976,\n", 269 | "they used a computer to enumerate 1,936 special cases that were, in some\n", 270 | "sense, lemmas of their proof. At the time, many mathematicians did not\n", 271 | "consider the theorem truly proved. Now computer-assisted proofs are\n", 272 | "common and generally (but not universally) accepted.\n", 273 | "\n", 274 | "Conversely, a substantial body of economic analysis is based on a model\n", 275 | "of human behavior called \"Economic man\", or, with tongue in cheek, *Homo\n", 276 | "economicus*. Research based on this model was highly regarded for\n", 277 | "several decades, especially if it involved mathematical virtuosity. More\n", 278 | "recently, this model is treated with skepticism, and models that include\n", 279 | "imperfect information and bounded rationality are hot topics." 280 | ] 281 | }, 282 | { 283 | "cell_type": "markdown", 284 | "id": "4d226469", 285 | "metadata": {}, 286 | "source": [ 287 | "## Different models for different purposes\n", 288 | "\n", 289 | "Complex models are often appropriate for different purposes and\n", 290 | "interpretations:\n", 291 | "\n", 292 | "Predictive  explanatory\n", 293 | "\n", 294 | "Schelling's model of segregation might shed light on a complex\n", 295 | " social phenomenon, but it is not useful for prediction. On the other\n", 296 | " hand, a simple model of celestial mechanics can predict solar\n", 297 | " eclipses, down to the second, years in the future.\n", 298 | "\n", 299 | "Realism  instrumentalism\n", 300 | "\n", 301 | "Classical models lend themselves to a realist interpretation; for\n", 302 | " example, most people accept that electrons are real things that\n", 303 | " exist. Instrumentalism is the view that models can be useful even if\n", 304 | " the entities they postulate don't exist. George Box wrote what might\n", 305 | " be the motto of instrumentalism: \"All models are wrong, but some are\n", 306 | " useful.\\\"\n", 307 | "\n", 308 | "Reductionism  holism\n", 309 | "\n", 310 | "Reductionism is the view that the behavior of a system can be\n", 311 | " explained by understanding its components. For example, the periodic\n", 312 | " table of the elements is a triumph of reductionism, because it\n", 313 | " explains the chemical behavior of elements with a model of electrons\n", 314 | " in atoms. Holism is the view that some phenomena that appear at the\n", 315 | " system level do not exist at the level of components, and cannot be\n", 316 | " explained in component-level terms.\n", 317 | "\n", 318 | "We get back to explanatory models in\n", 319 | "Chapter [\\[scale-free\\]](#scale-free){reference-type=\"ref\"\n", 320 | "reference=\"scale-free\"}, instrumentalism in\n", 321 | "Chapter [\\[lifechap\\]](#lifechap){reference-type=\"ref\"\n", 322 | "reference=\"lifechap\"}, and holism in\n", 323 | "Chapter [\\[soc\\]](#soc){reference-type=\"ref\" reference=\"soc\"}." 324 | ] 325 | }, 326 | { 327 | "cell_type": "markdown", 328 | "id": "d8e46e9b", 329 | "metadata": {}, 330 | "source": [ 331 | "## Complexity engineering\n", 332 | "\n", 333 | "I have been talking about complex systems in the context of science, but\n", 334 | "complexity is also a cause, and effect, of changes in engineering and\n", 335 | "the design of social systems:\n", 336 | "\n", 337 | "Centralized  decentralized\n", 338 | "\n", 339 | "Centralized systems are conceptually simple and easier to analyze,\n", 340 | " but decentralized systems can be more robust. For example, in the\n", 341 | " World Wide Web clients send requests to centralized servers; if the\n", 342 | " servers are down, the service is unavailable. In peer-to-peer\n", 343 | " networks, every node is both a client and a server. To take down the\n", 344 | " service, you have to take down *every* node.\n", 345 | "\n", 346 | "One-to-many  many-to-many\n", 347 | "\n", 348 | "In many communication systems, broadcast services are being\n", 349 | " augmented, and sometimes replaced, by services that allow users to\n", 350 | " communicate with each other and create, share, and modify content.\n", 351 | "\n", 352 | "Top-down  bottom-up\n", 353 | "\n", 354 | "In social, political and economic systems, many activities that\n", 355 | " would normally be centrally organized now operate as grassroots\n", 356 | " movements. Even armies, which are the canonical example of\n", 357 | " hierarchical structure, are moving toward devolved command and\n", 358 | " control.\n", 359 | "\n", 360 | "Analysis  computation\n", 361 | "\n", 362 | "In classical engineering, the space of feasible designs is limited\n", 363 | " by our capability for analysis. For example, designing the Eiffel\n", 364 | " Tower was possible because Gustave Eiffel developed novel analytic\n", 365 | " techniques, in particular for dealing with wind load. Now tools for\n", 366 | " computer-aided design and analysis make it possible to build almost\n", 367 | " anything that can be imagined. Frank Gehry's Guggenheim Museum\n", 368 | " Bilbao is my favorite example.\n", 369 | "\n", 370 | "Isolation  interaction\n", 371 | "\n", 372 | "In classical engineering, the complexity of large systems is managed\n", 373 | " by isolating components and minimizing interactions. This is still\n", 374 | " an important engineering principle; nevertheless, the availability\n", 375 | " of computation makes it increasingly feasible to design systems with\n", 376 | " complex interactions between components.\n", 377 | "\n", 378 | "Design  search\n", 379 | "\n", 380 | "Engineering is sometimes described as a search for solutions in a\n", 381 | " landscape of possible designs. Increasingly, the search process can\n", 382 | " be automated. For example, genetic algorithms explore large design\n", 383 | " spaces and discover solutions human engineers would not imagine (or\n", 384 | " like). The ultimate genetic algorithm, evolution, notoriously\n", 385 | " generates designs that violate the rules of human engineering." 386 | ] 387 | }, 388 | { 389 | "cell_type": "markdown", 390 | "id": "85e82f42", 391 | "metadata": {}, 392 | "source": [ 393 | "## Complexity thinking\n", 394 | "\n", 395 | "We are getting farther afield now, but the shifts I am postulating in\n", 396 | "the criteria of scientific modeling are related to 20th century\n", 397 | "developments in logic and epistemology.\n", 398 | "\n", 399 | "Aristotelian logic  many-valued logic\n", 400 | "\n", 401 | "In traditional logic, any proposition is either true or false. This\n", 402 | " system lends itself to math-like proofs, but fails (in dramatic\n", 403 | " ways) for many real-world applications. Alternatives include\n", 404 | " many-valued logic, fuzzy logic, and other systems designed to handle\n", 405 | " indeterminacy, vagueness, and uncertainty. Bart Kosko discusses some\n", 406 | " of these systems in *Fuzzy Thinking*.\n", 407 | "\n", 408 | "Frequentist probability  Bayesianism\n", 409 | "\n", 410 | "Bayesian probability has been around for centuries, but was not\n", 411 | " widely used until recently, facilitated by the availability of cheap\n", 412 | " computation and the reluctant acceptance of subjectivity in\n", 413 | " probabilistic claims. Sharon Bertsch McGrayne presents this history\n", 414 | " in *The Theory That Would Not Die*.\n", 415 | "\n", 416 | "Objective  subjective\n", 417 | "\n", 418 | "The Enlightenment, and philosophic modernism, are based on belief in\n", 419 | " objective truth, that is, truths that are independent of the people\n", 420 | " that hold them. 20th century developments including quantum\n", 421 | " mechanics, Gödel's Incompleteness Theorem, and Kuhn's study of the\n", 422 | " history of science called attention to seemingly unavoidable\n", 423 | " subjectivity in even \"hard sciences\" and mathematics. Rebecca\n", 424 | " Goldstein presents the historical context of Gödel's proof in\n", 425 | " *Incompleteness*.\n", 426 | "\n", 427 | "Physical law  theory  model\n", 428 | "\n", 429 | "Some people distinguish between laws, theories, and models. Calling\n", 430 | " something a \"law\" implies that it is objectively true and immutable;\n", 431 | " \"theory\" suggests that it is subject to revision; and \"model\"\n", 432 | " concedes that it is a subjective choice based on simplifications and\n", 433 | " approximations." 434 | ] 435 | }, 436 | { 437 | "cell_type": "markdown", 438 | "id": "7faf0ed7", 439 | "metadata": {}, 440 | "source": [ 441 | "I think they are all the same thing. Some concepts that are called\n", 442 | "laws are really definitions; others are, in effect, the assertion\n", 443 | "that a certain model predicts or explains the behavior of a system\n", 444 | "particularly well. We come back to the nature of physical laws in\n", 445 | "Section [\\[model1\\]](#model1){reference-type=\"ref\"\n", 446 | "reference=\"model1\"},\n", 447 | "Section [\\[model3\\]](#model3){reference-type=\"ref\"\n", 448 | "reference=\"model3\"} and\n", 449 | "Section [\\[model2\\]](#model2){reference-type=\"ref\"\n", 450 | "reference=\"model2\"}." 451 | ] 452 | }, 453 | { 454 | "cell_type": "markdown", 455 | "id": "30ee62cc", 456 | "metadata": {}, 457 | "source": [ 458 | "Determinism  indeterminism\n", 459 | "\n", 460 | "Determinism is the view that all events are caused, inevitably, by\n", 461 | " prior events. Forms of indeterminism include randomness,\n", 462 | " probabilistic causation, and fundamental uncertainty. We come back\n", 463 | " to this topic in\n", 464 | " Section [\\[determinism\\]](#determinism){reference-type=\"ref\"\n", 465 | " reference=\"determinism\"} and\n", 466 | " Section [\\[freewill\\]](#freewill){reference-type=\"ref\"\n", 467 | " reference=\"freewill\"}" 468 | ] 469 | }, 470 | { 471 | "cell_type": "markdown", 472 | "id": "712a9528", 473 | "metadata": {}, 474 | "source": [ 475 | "These trends are not universal or complete, but the center of opinion is\n", 476 | "shifting along these axes. As evidence, consider the reaction to Thomas\n", 477 | "Kuhn's *The Structure of Scientific Revolutions*, which was reviled when\n", 478 | "it was published and is now considered almost uncontroversial.\n", 479 | "\n", 480 | "These trends are both cause and effect of complexity science. For\n", 481 | "example, highly abstracted models are more acceptable now because of the\n", 482 | "diminished expectation that there should be a unique, correct model for\n", 483 | "every system. Conversely, developments in complex systems challenge\n", 484 | "determinism and the related concept of physical law.\n", 485 | "\n", 486 | "This chapter is an overview of the themes coming up in the book, but not\n", 487 | "all of it will make sense before you see the examples. When you get to\n", 488 | "the end of the book, you might find it helpful to read this chapter\n", 489 | "again." 490 | ] 491 | } 492 | ], 493 | "metadata": { 494 | "kernelspec": { 495 | "display_name": "Python 3 (ipykernel)", 496 | "language": "python", 497 | "name": "python3" 498 | }, 499 | "language_info": { 500 | "codemirror_mode": { 501 | "name": "ipython", 502 | "version": 3 503 | }, 504 | "file_extension": ".py", 505 | "mimetype": "text/x-python", 506 | "name": "python", 507 | "nbconvert_exporter": "python", 508 | "pygments_lexer": "ipython3", 509 | "version": "3.10.14" 510 | } 511 | }, 512 | "nbformat": 4, 513 | "nbformat_minor": 5 514 | } 515 | -------------------------------------------------------------------------------- /nb/chap02.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Erdos-Renyi Graphs\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": null, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from os.path import basename, exists\n", 21 | "\n", 22 | "def download(url):\n", 23 | " filename = basename(url)\n", 24 | " if not exists(filename):\n", 25 | " from urllib.request import urlretrieve\n", 26 | " local, _ = urlretrieve(url, filename)\n", 27 | " print('Downloaded ' + local)\n", 28 | " \n", 29 | "download('https://github.com/AllenDowney/ThinkComplexity2/raw/master/notebooks/utils.py')" 30 | ] 31 | }, 32 | { 33 | "cell_type": "code", 34 | "execution_count": null, 35 | "metadata": {}, 36 | "outputs": [], 37 | "source": [ 38 | "import matplotlib.pyplot as plt\n", 39 | "import networkx as nx\n", 40 | "import numpy as np\n", 41 | "import seaborn as sns\n", 42 | "\n", 43 | "from utils import decorate, savefig\n", 44 | "\n", 45 | "# Set the random seed so the notebook \n", 46 | "# produces the same results every time.\n", 47 | "np.random.seed(17)" 48 | ] 49 | }, 50 | { 51 | "cell_type": "code", 52 | "execution_count": null, 53 | "metadata": {}, 54 | "outputs": [], 55 | "source": [ 56 | "# make a directory for figures\n", 57 | "!mkdir -p figs" 58 | ] 59 | }, 60 | { 61 | "cell_type": "code", 62 | "execution_count": null, 63 | "metadata": {}, 64 | "outputs": [], 65 | "source": [ 66 | "# node colors for drawing networks\n", 67 | "colors = sns.color_palette('pastel', 5)\n", 68 | "#sns.palplot(colors)\n", 69 | "sns.set_palette(colors)" 70 | ] 71 | }, 72 | { 73 | "cell_type": "markdown", 74 | "metadata": {}, 75 | "source": [ 76 | "## Directed graph\n", 77 | "\n", 78 | "The first example is a directed graph that represents a social network with three nodes." 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "G = nx.DiGraph()\n", 88 | "G.add_node('Alice')\n", 89 | "G.add_node('Bob')\n", 90 | "G.add_node('Cate')\n", 91 | "list(G.nodes())" 92 | ] 93 | }, 94 | { 95 | "cell_type": "markdown", 96 | "metadata": {}, 97 | "source": [ 98 | "Here's how we add edges between nodes." 99 | ] 100 | }, 101 | { 102 | "cell_type": "code", 103 | "execution_count": null, 104 | "metadata": {}, 105 | "outputs": [], 106 | "source": [ 107 | "G.add_edge('Alice', 'Bob')\n", 108 | "G.add_edge('Alice', 'Cate')\n", 109 | "G.add_edge('Bob', 'Alice')\n", 110 | "G.add_edge('Bob', 'Cate')\n", 111 | "list(G.edges())" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "And here's how to draw the graph." 119 | ] 120 | }, 121 | { 122 | "cell_type": "code", 123 | "execution_count": null, 124 | "metadata": {}, 125 | "outputs": [], 126 | "source": [ 127 | "nx.draw_circular(G,\n", 128 | " node_color='C0',\n", 129 | " node_size=2000, \n", 130 | " with_labels=True)\n", 131 | "plt.axis('equal')\n", 132 | "savefig('figs/chap02-1')" 133 | ] 134 | }, 135 | { 136 | "cell_type": "markdown", 137 | "metadata": {}, 138 | "source": [ 139 | "**Exercise:** Add another node and a few more edges and draw the graph again." 140 | ] 141 | }, 142 | { 143 | "cell_type": "code", 144 | "execution_count": null, 145 | "metadata": {}, 146 | "outputs": [], 147 | "source": [ 148 | "# Solution goes here" 149 | ] 150 | }, 151 | { 152 | "cell_type": "markdown", 153 | "metadata": {}, 154 | "source": [ 155 | "## Undirected graph\n", 156 | "\n", 157 | "The second example is an undirected graph that represents cities and the driving times between them.\n", 158 | "\n", 159 | "`positions` is a dictionary that maps from each city to its coordinates." 160 | ] 161 | }, 162 | { 163 | "cell_type": "code", 164 | "execution_count": null, 165 | "metadata": {}, 166 | "outputs": [], 167 | "source": [ 168 | "positions = dict(Albany=(-74, 43),\n", 169 | " Boston=(-71, 42),\n", 170 | " NYC=(-74, 41),\n", 171 | " Philly=(-75, 40))\n", 172 | "\n", 173 | "positions['Albany']" 174 | ] 175 | }, 176 | { 177 | "cell_type": "markdown", 178 | "metadata": {}, 179 | "source": [ 180 | "We can use the keys in `pos` to add nodes to the graph." 181 | ] 182 | }, 183 | { 184 | "cell_type": "code", 185 | "execution_count": null, 186 | "metadata": {}, 187 | "outputs": [], 188 | "source": [ 189 | "G = nx.Graph()\n", 190 | "G.add_nodes_from(positions)\n", 191 | "G.nodes()" 192 | ] 193 | }, 194 | { 195 | "cell_type": "markdown", 196 | "metadata": {}, 197 | "source": [ 198 | "`drive_times` is a dictionary that maps from pairs of cities to the driving times between them." 199 | ] 200 | }, 201 | { 202 | "cell_type": "code", 203 | "execution_count": null, 204 | "metadata": {}, 205 | "outputs": [], 206 | "source": [ 207 | "drive_times = {('Albany', 'Boston'): 3,\n", 208 | " ('Albany', 'NYC'): 4,\n", 209 | " ('Boston', 'NYC'): 4,\n", 210 | " ('NYC', 'Philly'): 2}" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": {}, 216 | "source": [ 217 | "We can use the keys from `drive_times` to add edges to the graph." 218 | ] 219 | }, 220 | { 221 | "cell_type": "code", 222 | "execution_count": null, 223 | "metadata": {}, 224 | "outputs": [], 225 | "source": [ 226 | "G.add_edges_from(drive_times)\n", 227 | "G.edges()" 228 | ] 229 | }, 230 | { 231 | "cell_type": "markdown", 232 | "metadata": {}, 233 | "source": [ 234 | "Now we can draw the graph using `positions` to indicate the positions of the nodes, and `drive_times` to label the edges." 235 | ] 236 | }, 237 | { 238 | "cell_type": "code", 239 | "execution_count": null, 240 | "metadata": {}, 241 | "outputs": [], 242 | "source": [ 243 | "nx.draw(G, positions, \n", 244 | " node_color='C1', \n", 245 | " node_shape='s', \n", 246 | " node_size=2500, \n", 247 | " with_labels=True)\n", 248 | "\n", 249 | "nx.draw_networkx_edge_labels(G, positions, \n", 250 | " edge_labels=drive_times)\n", 251 | "\n", 252 | "plt.axis('equal')\n", 253 | "savefig('figs/chap02-2')" 254 | ] 255 | }, 256 | { 257 | "cell_type": "markdown", 258 | "metadata": {}, 259 | "source": [ 260 | "**Exercise:** Add another city and at least one edge." 261 | ] 262 | }, 263 | { 264 | "cell_type": "code", 265 | "execution_count": null, 266 | "metadata": {}, 267 | "outputs": [], 268 | "source": [ 269 | "# Solution goes here" 270 | ] 271 | }, 272 | { 273 | "cell_type": "markdown", 274 | "metadata": {}, 275 | "source": [ 276 | "## Complete graph\n", 277 | "\n", 278 | "To make a complete graph, we use a generator function that iterates through all pairs of nodes." 279 | ] 280 | }, 281 | { 282 | "cell_type": "code", 283 | "execution_count": null, 284 | "metadata": {}, 285 | "outputs": [], 286 | "source": [ 287 | "def all_pairs(nodes):\n", 288 | " for i, u in enumerate(nodes):\n", 289 | " for j, v in enumerate(nodes):\n", 290 | " if i < j:\n", 291 | " yield u, v" 292 | ] 293 | }, 294 | { 295 | "cell_type": "markdown", 296 | "metadata": {}, 297 | "source": [ 298 | "`make_complete_graph` makes a `Graph` with the given number of nodes and edges between all pairs of nodes." 299 | ] 300 | }, 301 | { 302 | "cell_type": "code", 303 | "execution_count": null, 304 | "metadata": {}, 305 | "outputs": [], 306 | "source": [ 307 | "def make_complete_graph(n):\n", 308 | " G = nx.Graph()\n", 309 | " nodes = range(n)\n", 310 | " G.add_nodes_from(nodes)\n", 311 | " G.add_edges_from(all_pairs(nodes))\n", 312 | " return G" 313 | ] 314 | }, 315 | { 316 | "cell_type": "markdown", 317 | "metadata": {}, 318 | "source": [ 319 | "Here's a complete graph with 10 nodes:" 320 | ] 321 | }, 322 | { 323 | "cell_type": "code", 324 | "execution_count": null, 325 | "metadata": {}, 326 | "outputs": [], 327 | "source": [ 328 | "complete = make_complete_graph(10)\n", 329 | "complete.number_of_nodes()" 330 | ] 331 | }, 332 | { 333 | "cell_type": "markdown", 334 | "metadata": {}, 335 | "source": [ 336 | "And here's what it looks like." 337 | ] 338 | }, 339 | { 340 | "cell_type": "code", 341 | "execution_count": null, 342 | "metadata": {}, 343 | "outputs": [], 344 | "source": [ 345 | "nx.draw_circular(complete, \n", 346 | " node_color='C2', \n", 347 | " node_size=1000, \n", 348 | " with_labels=True)\n", 349 | "savefig('figs/chap02-3')" 350 | ] 351 | }, 352 | { 353 | "cell_type": "markdown", 354 | "metadata": {}, 355 | "source": [ 356 | "The `neighbors` method the neighbors for a given node." 357 | ] 358 | }, 359 | { 360 | "cell_type": "code", 361 | "execution_count": null, 362 | "metadata": {}, 363 | "outputs": [], 364 | "source": [ 365 | "list(complete.neighbors(0))" 366 | ] 367 | }, 368 | { 369 | "cell_type": "markdown", 370 | "metadata": {}, 371 | "source": [ 372 | "**Exercise:** Make and draw complete directed graph with 5 nodes." 373 | ] 374 | }, 375 | { 376 | "cell_type": "code", 377 | "execution_count": null, 378 | "metadata": {}, 379 | "outputs": [], 380 | "source": [ 381 | "# Solution goes here" 382 | ] 383 | }, 384 | { 385 | "cell_type": "markdown", 386 | "metadata": {}, 387 | "source": [ 388 | "## Random graphs\n", 389 | "\n", 390 | "Next we'll make a random graph where the probability of an edge between each pair of nodes is $p$.\n", 391 | "\n", 392 | "The helper function `flip` returns True with probability `p` and False with probability `1-p`" 393 | ] 394 | }, 395 | { 396 | "cell_type": "code", 397 | "execution_count": null, 398 | "metadata": {}, 399 | "outputs": [], 400 | "source": [ 401 | "def flip(p):\n", 402 | " return np.random.random() < p" 403 | ] 404 | }, 405 | { 406 | "cell_type": "markdown", 407 | "metadata": {}, 408 | "source": [ 409 | "`random_pairs` is a generator function that enumerates all possible pairs of nodes and yields each one with probability `p` " 410 | ] 411 | }, 412 | { 413 | "cell_type": "code", 414 | "execution_count": null, 415 | "metadata": {}, 416 | "outputs": [], 417 | "source": [ 418 | "def random_pairs(nodes, p):\n", 419 | " for edge in all_pairs(nodes):\n", 420 | " if flip(p):\n", 421 | " yield edge" 422 | ] 423 | }, 424 | { 425 | "cell_type": "markdown", 426 | "metadata": {}, 427 | "source": [ 428 | "`make_random_graph` makes an ER graph where the probability of an edge between each pair of nodes is `p`." 429 | ] 430 | }, 431 | { 432 | "cell_type": "code", 433 | "execution_count": null, 434 | "metadata": {}, 435 | "outputs": [], 436 | "source": [ 437 | "def make_random_graph(n, p):\n", 438 | " G = nx.Graph()\n", 439 | " nodes = range(n)\n", 440 | " G.add_nodes_from(nodes)\n", 441 | " G.add_edges_from(random_pairs(nodes, p))\n", 442 | " return G" 443 | ] 444 | }, 445 | { 446 | "cell_type": "markdown", 447 | "metadata": {}, 448 | "source": [ 449 | "Here's an example with `n=10` and `p=0.3`" 450 | ] 451 | }, 452 | { 453 | "cell_type": "code", 454 | "execution_count": null, 455 | "metadata": {}, 456 | "outputs": [], 457 | "source": [ 458 | "np.random.seed(10)\n", 459 | "\n", 460 | "random_graph = make_random_graph(10, 0.3)\n", 461 | "len(random_graph.edges())" 462 | ] 463 | }, 464 | { 465 | "cell_type": "markdown", 466 | "metadata": {}, 467 | "source": [ 468 | "And here's what it looks like:" 469 | ] 470 | }, 471 | { 472 | "cell_type": "code", 473 | "execution_count": null, 474 | "metadata": {}, 475 | "outputs": [], 476 | "source": [ 477 | "nx.draw_circular(random_graph, \n", 478 | " node_color='C3', \n", 479 | " node_size=1000, \n", 480 | " with_labels=True)\n", 481 | "savefig('figs/chap02-4')" 482 | ] 483 | }, 484 | { 485 | "cell_type": "markdown", 486 | "metadata": {}, 487 | "source": [ 488 | "## Connectivity\n", 489 | "\n", 490 | "To check whether a graph is connected, we'll start by finding all nodes that can be reached, starting with a given node:" 491 | ] 492 | }, 493 | { 494 | "cell_type": "code", 495 | "execution_count": null, 496 | "metadata": {}, 497 | "outputs": [], 498 | "source": [ 499 | "def reachable_nodes(G, start):\n", 500 | " seen = set()\n", 501 | " stack = [start]\n", 502 | " while stack:\n", 503 | " node = stack.pop()\n", 504 | " if node not in seen:\n", 505 | " seen.add(node)\n", 506 | " stack.extend(G.neighbors(node))\n", 507 | " return seen" 508 | ] 509 | }, 510 | { 511 | "cell_type": "markdown", 512 | "metadata": {}, 513 | "source": [ 514 | "In the complete graph, starting from node 0, we can reach all nodes:" 515 | ] 516 | }, 517 | { 518 | "cell_type": "code", 519 | "execution_count": null, 520 | "metadata": {}, 521 | "outputs": [], 522 | "source": [ 523 | "reachable_nodes(complete, 0)" 524 | ] 525 | }, 526 | { 527 | "cell_type": "markdown", 528 | "metadata": {}, 529 | "source": [ 530 | "In the random graph we generated, we can also reach all nodes (but that's not always true):" 531 | ] 532 | }, 533 | { 534 | "cell_type": "code", 535 | "execution_count": null, 536 | "metadata": {}, 537 | "outputs": [], 538 | "source": [ 539 | "reachable_nodes(random_graph, 0)" 540 | ] 541 | }, 542 | { 543 | "cell_type": "markdown", 544 | "metadata": {}, 545 | "source": [ 546 | "We can use `reachable_nodes` to check whether a graph is connected:" 547 | ] 548 | }, 549 | { 550 | "cell_type": "code", 551 | "execution_count": null, 552 | "metadata": {}, 553 | "outputs": [], 554 | "source": [ 555 | "def is_connected(G):\n", 556 | " start = next(iter(G))\n", 557 | " reachable = reachable_nodes(G, start)\n", 558 | " return len(reachable) == len(G)" 559 | ] 560 | }, 561 | { 562 | "cell_type": "markdown", 563 | "metadata": {}, 564 | "source": [ 565 | "Again, the complete graph is connected:" 566 | ] 567 | }, 568 | { 569 | "cell_type": "code", 570 | "execution_count": null, 571 | "metadata": {}, 572 | "outputs": [], 573 | "source": [ 574 | "is_connected(complete)" 575 | ] 576 | }, 577 | { 578 | "cell_type": "markdown", 579 | "metadata": {}, 580 | "source": [ 581 | "But if we generate a random graph with a low value of `p`, it's not:" 582 | ] 583 | }, 584 | { 585 | "cell_type": "code", 586 | "execution_count": null, 587 | "metadata": {}, 588 | "outputs": [], 589 | "source": [ 590 | "random_graph = make_random_graph(10, 0.1)\n", 591 | "len(random_graph.edges())" 592 | ] 593 | }, 594 | { 595 | "cell_type": "code", 596 | "execution_count": null, 597 | "metadata": {}, 598 | "outputs": [], 599 | "source": [ 600 | "is_connected(random_graph)" 601 | ] 602 | }, 603 | { 604 | "cell_type": "markdown", 605 | "metadata": {}, 606 | "source": [ 607 | "**Exercise:** What do you think it means for a directed graph to be connected? Write a function that checks whether a directed graph is connected." 608 | ] 609 | }, 610 | { 611 | "cell_type": "code", 612 | "execution_count": null, 613 | "metadata": {}, 614 | "outputs": [], 615 | "source": [ 616 | "# Solution goes here" 617 | ] 618 | }, 619 | { 620 | "cell_type": "markdown", 621 | "metadata": {}, 622 | "source": [ 623 | "## Probability of connectivity\n", 624 | "\n", 625 | "Now let's estimare the probability that a randomly-generated ER graph is connected.\n", 626 | "\n", 627 | "This function takes `n` and `p`, generates `iters` graphs, and returns the fraction of them that are connected." 628 | ] 629 | }, 630 | { 631 | "cell_type": "code", 632 | "execution_count": null, 633 | "metadata": {}, 634 | "outputs": [], 635 | "source": [ 636 | "# version with a for loop\n", 637 | "\n", 638 | "def prob_connected(n, p, iters=100):\n", 639 | " count = 0\n", 640 | " for i in range(iters):\n", 641 | " random_graph = make_random_graph(n, p)\n", 642 | " if is_connected(random_graph):\n", 643 | " count += 1\n", 644 | " return count/iters" 645 | ] 646 | }, 647 | { 648 | "cell_type": "code", 649 | "execution_count": null, 650 | "metadata": {}, 651 | "outputs": [], 652 | "source": [ 653 | "# version with a list comprehension\n", 654 | "\n", 655 | "def prob_connected(n, p, iters=100):\n", 656 | " tf = [is_connected(make_random_graph(n, p))\n", 657 | " for i in range(iters)]\n", 658 | " return np.mean(tf)" 659 | ] 660 | }, 661 | { 662 | "cell_type": "markdown", 663 | "metadata": {}, 664 | "source": [ 665 | "With `n=10` and `p=0.23`, the probability of being connected is about 33%." 666 | ] 667 | }, 668 | { 669 | "cell_type": "code", 670 | "execution_count": null, 671 | "metadata": {}, 672 | "outputs": [], 673 | "source": [ 674 | "np.random.seed(17)\n", 675 | "\n", 676 | "n = 10\n", 677 | "prob_connected(n, 0.23, iters=10000)" 678 | ] 679 | }, 680 | { 681 | "cell_type": "markdown", 682 | "metadata": {}, 683 | "source": [ 684 | "According to Erdos and Renyi, the critical value of `p` for `n=10` is about 0.23. " 685 | ] 686 | }, 687 | { 688 | "cell_type": "code", 689 | "execution_count": null, 690 | "metadata": {}, 691 | "outputs": [], 692 | "source": [ 693 | "pstar = np.log(n) / n\n", 694 | "pstar" 695 | ] 696 | }, 697 | { 698 | "cell_type": "markdown", 699 | "metadata": {}, 700 | "source": [ 701 | "So let's plot the probability of connectivity for a range of values for `p`" 702 | ] 703 | }, 704 | { 705 | "cell_type": "code", 706 | "execution_count": null, 707 | "metadata": {}, 708 | "outputs": [], 709 | "source": [ 710 | "ps = np.logspace(-1.3, 0, 11)\n", 711 | "ps" 712 | ] 713 | }, 714 | { 715 | "cell_type": "markdown", 716 | "metadata": {}, 717 | "source": [ 718 | "I'll estimate the probabilities with `iters=1000`" 719 | ] 720 | }, 721 | { 722 | "cell_type": "code", 723 | "execution_count": null, 724 | "metadata": {}, 725 | "outputs": [], 726 | "source": [ 727 | "ys = [prob_connected(n, p, 1000) for p in ps]\n", 728 | "\n", 729 | "for p, y in zip(ps, ys):\n", 730 | " print(p, y)" 731 | ] 732 | }, 733 | { 734 | "cell_type": "markdown", 735 | "metadata": {}, 736 | "source": [ 737 | "And then plot them, adding a vertical line at the computed critical value" 738 | ] 739 | }, 740 | { 741 | "cell_type": "code", 742 | "execution_count": null, 743 | "metadata": {}, 744 | "outputs": [], 745 | "source": [ 746 | "plt.axvline(pstar, color='gray')\n", 747 | "plt.plot(ps, ys, color='green')\n", 748 | "decorate(xlabel='Prob of edge (p)',\n", 749 | " ylabel='Prob connected',\n", 750 | " xscale='log')\n", 751 | "\n", 752 | "savefig('figs/chap02-5')" 753 | ] 754 | }, 755 | { 756 | "cell_type": "markdown", 757 | "metadata": {}, 758 | "source": [ 759 | "We can run the same analysis for a few more values of `n`." 760 | ] 761 | }, 762 | { 763 | "cell_type": "code", 764 | "execution_count": null, 765 | "metadata": { 766 | "scrolled": false 767 | }, 768 | "outputs": [], 769 | "source": [ 770 | "ns = [300, 100, 30]\n", 771 | "ps = np.logspace(-2.5, 0, 11)\n", 772 | "\n", 773 | "sns.set_palette('Blues_r', 4)\n", 774 | "for n in ns:\n", 775 | " print(n)\n", 776 | " pstar = np.log(n) / n\n", 777 | " plt.axvline(pstar, color='gray', alpha=0.3)\n", 778 | "\n", 779 | " ys = [prob_connected(n, p) for p in ps]\n", 780 | " plt.plot(ps, ys, label='n=%d' % n)\n", 781 | "\n", 782 | "decorate(xlabel='Prob of edge (p)',\n", 783 | " ylabel='Prob connected',\n", 784 | " xscale='log', \n", 785 | " xlim=[ps[0], ps[-1]],\n", 786 | " loc='upper left')\n", 787 | "\n", 788 | "savefig('figs/chap02-6')" 789 | ] 790 | }, 791 | { 792 | "cell_type": "markdown", 793 | "metadata": {}, 794 | "source": [ 795 | "As `n` increases, the critical value gets smaller and the transition gets more abrupt." 796 | ] 797 | }, 798 | { 799 | "cell_type": "markdown", 800 | "metadata": {}, 801 | "source": [ 802 | "## Exercises" 803 | ] 804 | }, 805 | { 806 | "cell_type": "markdown", 807 | "metadata": {}, 808 | "source": [ 809 | "**Exercise:** In Chapter 2 we analyzed the performance of `reachable_nodes` and classified it in $O(n + m)$, where $n$ is the number of nodes and $m$ is the number of edges. Continuing the\n", 810 | "analysis, what is the order of growth for `is_connected`?\n", 811 | "\n", 812 | " def is_connected(G):\n", 813 | " start = list(G)[0]\n", 814 | " reachable = reachable_nodes(G, start)\n", 815 | " return len(reachable) == len(G)" 816 | ] 817 | }, 818 | { 819 | "cell_type": "code", 820 | "execution_count": null, 821 | "metadata": {}, 822 | "outputs": [], 823 | "source": [ 824 | "# Solution goes here" 825 | ] 826 | }, 827 | { 828 | "cell_type": "markdown", 829 | "metadata": {}, 830 | "source": [ 831 | "**Exercise:** In my implementation of `reachable_nodes`, you might be bothered by the apparent inefficiency of adding *all* neighbors to the stack without checking whether they are already in `seen`. Write a version of this function that checks the neighbors before adding them to the stack. Does this \"optimization\" change the order of growth? Does it make the function faster?" 832 | ] 833 | }, 834 | { 835 | "cell_type": "code", 836 | "execution_count": null, 837 | "metadata": {}, 838 | "outputs": [], 839 | "source": [ 840 | "def reachable_nodes_precheck(G, start):\n", 841 | " # FILL THIS IN\n", 842 | " return []" 843 | ] 844 | }, 845 | { 846 | "cell_type": "code", 847 | "execution_count": null, 848 | "metadata": {}, 849 | "outputs": [], 850 | "source": [ 851 | "# Solution goes here" 852 | ] 853 | }, 854 | { 855 | "cell_type": "code", 856 | "execution_count": null, 857 | "metadata": {}, 858 | "outputs": [], 859 | "source": [ 860 | "%timeit len(reachable_nodes(complete, 0))" 861 | ] 862 | }, 863 | { 864 | "cell_type": "code", 865 | "execution_count": null, 866 | "metadata": {}, 867 | "outputs": [], 868 | "source": [ 869 | "%timeit len(reachable_nodes_precheck(complete, 0))" 870 | ] 871 | }, 872 | { 873 | "cell_type": "markdown", 874 | "metadata": { 875 | "collapsed": true 876 | }, 877 | "source": [ 878 | "**Exercise:** There are actually two kinds of ER graphs. The one we generated in the chapter, $G(n, p)$, is characterized by two parameters, the number of nodes and the probability of an edge between nodes.\n", 879 | "\n", 880 | "An alternative definition, denoted $G(n, m)$, is also characterized by two parameters: the number of nodes, $n$, and the number of edges, $m$. Under this definition, the number of edges is fixed, but their location is random.\n", 881 | "\n", 882 | "Repeat the experiments we did in this chapter using this alternative definition. Here are a few suggestions for how to proceed:\n", 883 | "\n", 884 | "1. Write a function called `m_pairs` that takes a list of nodes and the number of edges, $m$, and returns a random selection of $m$ edges. A simple way to do that is to generate a list of all possible edges and use `random.sample`.\n", 885 | "\n", 886 | "2. Write a function called `make_m_graph` that takes $n$ and $m$ and returns a random graph with $n$ nodes and $m$ edges.\n", 887 | "\n", 888 | "3. Make a version of `prob_connected` that uses `make_m_graph` instead of `make_random_graph`.\n", 889 | "\n", 890 | "4. Compute the probability of connectivity for a range of values of $m$.\n", 891 | "\n", 892 | "How do the results of this experiment compare to the results using the first type of ER graph?" 893 | ] 894 | }, 895 | { 896 | "cell_type": "code", 897 | "execution_count": null, 898 | "metadata": {}, 899 | "outputs": [], 900 | "source": [ 901 | "# Solution goes here" 902 | ] 903 | }, 904 | { 905 | "cell_type": "code", 906 | "execution_count": null, 907 | "metadata": {}, 908 | "outputs": [], 909 | "source": [ 910 | "m_graph.number_of_edges()" 911 | ] 912 | }, 913 | { 914 | "cell_type": "code", 915 | "execution_count": null, 916 | "metadata": {}, 917 | "outputs": [], 918 | "source": [ 919 | "# Solution goes here" 920 | ] 921 | }, 922 | { 923 | "cell_type": "code", 924 | "execution_count": null, 925 | "metadata": {}, 926 | "outputs": [], 927 | "source": [ 928 | "# Solution goes here" 929 | ] 930 | }, 931 | { 932 | "cell_type": "code", 933 | "execution_count": null, 934 | "metadata": {}, 935 | "outputs": [], 936 | "source": [] 937 | } 938 | ], 939 | "metadata": { 940 | "kernelspec": { 941 | "display_name": "Python 3 (ipykernel)", 942 | "language": "python", 943 | "name": "python3" 944 | }, 945 | "language_info": { 946 | "codemirror_mode": { 947 | "name": "ipython", 948 | "version": 3 949 | }, 950 | "file_extension": ".py", 951 | "mimetype": "text/x-python", 952 | "name": "python", 953 | "nbconvert_exporter": "python", 954 | "pygments_lexer": "ipython3", 955 | "version": "3.7.11" 956 | } 957 | }, 958 | "nbformat": 4, 959 | "nbformat_minor": 1 960 | } 961 | -------------------------------------------------------------------------------- /nb/chap05.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Cellular automata\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": 5, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "import matplotlib.pyplot as plt\n", 21 | "import numpy as np" 22 | ] 23 | }, 24 | { 25 | "cell_type": "code", 26 | "execution_count": 6, 27 | "metadata": {}, 28 | "outputs": [], 29 | "source": [ 30 | "from os.path import basename, exists\n", 31 | "\n", 32 | "def download(url):\n", 33 | " filename = basename(url)\n", 34 | " if not exists(filename):\n", 35 | " from urllib.request import urlretrieve\n", 36 | " local, _ = urlretrieve(url, filename)\n", 37 | " print('Downloaded ' + local)\n", 38 | " \n", 39 | "download('https://github.com/AllenDowney/ThinkComplexity2/raw/master/notebooks/utils.py')" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 7, 45 | "metadata": {}, 46 | "outputs": [], 47 | "source": [ 48 | "from utils import savefig\n", 49 | "# make a directory for figures\n", 50 | "!mkdir -p figs" 51 | ] 52 | }, 53 | { 54 | "cell_type": "markdown", 55 | "metadata": {}, 56 | "source": [ 57 | "## Zero-dimensional CA" 58 | ] 59 | }, 60 | { 61 | "cell_type": "markdown", 62 | "metadata": {}, 63 | "source": [ 64 | "Here's a simple implementation of the 0-D CA I mentioned in the book, with one cell." 65 | ] 66 | }, 67 | { 68 | "cell_type": "code", 69 | "execution_count": 8, 70 | "metadata": {}, 71 | "outputs": [], 72 | "source": [ 73 | "n = 10\n", 74 | "x = np.zeros(n)\n", 75 | "print(x)" 76 | ] 77 | }, 78 | { 79 | "cell_type": "markdown", 80 | "metadata": {}, 81 | "source": [ 82 | "To get the state of the cell in the next time step, we increment the current state mod 2." 83 | ] 84 | }, 85 | { 86 | "cell_type": "code", 87 | "execution_count": 9, 88 | "metadata": {}, 89 | "outputs": [], 90 | "source": [ 91 | "x[1] = (x[0] + 1) % 2\n", 92 | "x[1]" 93 | ] 94 | }, 95 | { 96 | "cell_type": "markdown", 97 | "metadata": {}, 98 | "source": [ 99 | "Filling in the rest of the array." 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": 10, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "for i in range(2, n):\n", 109 | " x[i] = (x[i-1] + 1) % 2\n", 110 | " \n", 111 | "print(x)" 112 | ] 113 | }, 114 | { 115 | "cell_type": "markdown", 116 | "metadata": {}, 117 | "source": [ 118 | "So the behavior of this CA is simple: it blinks." 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "metadata": {}, 124 | "source": [ 125 | "## One-dimensional CA" 126 | ] 127 | }, 128 | { 129 | "cell_type": "markdown", 130 | "metadata": {}, 131 | "source": [ 132 | "Just as we used a 1-D array to show the state of a single cell over time, we'll use a 2-D array to show the state of a 1-D CA over time, with one column per cell and one row per timestep." 133 | ] 134 | }, 135 | { 136 | "cell_type": "code", 137 | "execution_count": 11, 138 | "metadata": {}, 139 | "outputs": [], 140 | "source": [ 141 | "rows = 5\n", 142 | "cols = 11\n", 143 | "array = np.zeros((rows, cols), dtype=np.uint8)\n", 144 | "array[0, 5] = 1\n", 145 | "print(array)" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "metadata": {}, 151 | "source": [ 152 | "To plot the array I use `plt.imshow`" 153 | ] 154 | }, 155 | { 156 | "cell_type": "code", 157 | "execution_count": 12, 158 | "metadata": {}, 159 | "outputs": [], 160 | "source": [ 161 | "def plot_ca(array):\n", 162 | " plt.imshow(array, cmap='Blues', interpolation='none')" 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "metadata": {}, 168 | "source": [ 169 | "Here's what it looks like after we initialize the first row." 170 | ] 171 | }, 172 | { 173 | "cell_type": "code", 174 | "execution_count": 13, 175 | "metadata": {}, 176 | "outputs": [], 177 | "source": [ 178 | "plot_ca(array)" 179 | ] 180 | }, 181 | { 182 | "cell_type": "markdown", 183 | "metadata": {}, 184 | "source": [ 185 | "And here's the function that fills in the next row. The rule for this CA is to take the sum of a cell and its two neighbors mod 2." 186 | ] 187 | }, 188 | { 189 | "cell_type": "code", 190 | "execution_count": 14, 191 | "metadata": {}, 192 | "outputs": [], 193 | "source": [ 194 | "def step(array, i):\n", 195 | " \"\"\"Compute row i of a CA.\n", 196 | " \"\"\"\n", 197 | " rows, cols = array.shape\n", 198 | " row = array[i-1]\n", 199 | " for j in range(1, cols):\n", 200 | " elts = row[j-1:j+2]\n", 201 | " array[i, j] = sum(elts) % 2" 202 | ] 203 | }, 204 | { 205 | "cell_type": "markdown", 206 | "metadata": {}, 207 | "source": [ 208 | "Here's the second row." 209 | ] 210 | }, 211 | { 212 | "cell_type": "code", 213 | "execution_count": 15, 214 | "metadata": {}, 215 | "outputs": [], 216 | "source": [ 217 | "step(array, 1)\n", 218 | "plot_ca(array)" 219 | ] 220 | }, 221 | { 222 | "cell_type": "markdown", 223 | "metadata": {}, 224 | "source": [ 225 | "And here's what it looks like with the rest of the cells filled in." 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": 16, 231 | "metadata": {}, 232 | "outputs": [], 233 | "source": [ 234 | "for i in range(1, rows):\n", 235 | " step(array, i)\n", 236 | "\n", 237 | "plot_ca(array)" 238 | ] 239 | }, 240 | { 241 | "cell_type": "markdown", 242 | "metadata": {}, 243 | "source": [ 244 | "For a simple set of rules, the behavior is more interesting than you might expect." 245 | ] 246 | }, 247 | { 248 | "cell_type": "markdown", 249 | "metadata": {}, 250 | "source": [ 251 | "**Exercise:** Modify this code to increase the number of rows and columns and see what this CA does after more time steps." 252 | ] 253 | }, 254 | { 255 | "cell_type": "markdown", 256 | "metadata": {}, 257 | "source": [ 258 | "## Cross correlation" 259 | ] 260 | }, 261 | { 262 | "cell_type": "markdown", 263 | "metadata": {}, 264 | "source": [ 265 | "We can update the CA more quickly using \"cross correlation\". The cross correlation of an array, `a`, with a window, `w`, is a new array, `c`, where element `k` is:\n", 266 | "\n", 267 | "$ c_k = \\sum_{n=0}^{N-1} a_{n+k} \\cdot w_n $\n", 268 | "\n", 269 | "In Python, we can compute element `k` like this:" 270 | ] 271 | }, 272 | { 273 | "cell_type": "code", 274 | "execution_count": 17, 275 | "metadata": {}, 276 | "outputs": [], 277 | "source": [ 278 | "def c_k(a, w, k):\n", 279 | " \"\"\"Compute element k of the cross correlation of a and w.\n", 280 | " \"\"\"\n", 281 | " N = len(w)\n", 282 | " return sum(a[k:k+N] * w)" 283 | ] 284 | }, 285 | { 286 | "cell_type": "markdown", 287 | "metadata": {}, 288 | "source": [ 289 | "To see how this works, I'll create an array:" 290 | ] 291 | }, 292 | { 293 | "cell_type": "code", 294 | "execution_count": 18, 295 | "metadata": {}, 296 | "outputs": [], 297 | "source": [ 298 | "N = 10\n", 299 | "row = np.arange(N, dtype=np.uint8)\n", 300 | "print(row)" 301 | ] 302 | }, 303 | { 304 | "cell_type": "markdown", 305 | "metadata": {}, 306 | "source": [ 307 | "And a window:" 308 | ] 309 | }, 310 | { 311 | "cell_type": "code", 312 | "execution_count": 19, 313 | "metadata": {}, 314 | "outputs": [], 315 | "source": [ 316 | "window = [1, 1, 1]\n", 317 | "\n", 318 | "print(window)" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": {}, 324 | "source": [ 325 | "With this window, each element of `c` is the sum of three neighbors in the array:" 326 | ] 327 | }, 328 | { 329 | "cell_type": "code", 330 | "execution_count": 20, 331 | "metadata": {}, 332 | "outputs": [], 333 | "source": [ 334 | "c_k(row, window, 0)" 335 | ] 336 | }, 337 | { 338 | "cell_type": "code", 339 | "execution_count": 21, 340 | "metadata": {}, 341 | "outputs": [], 342 | "source": [ 343 | "c_k(row, window, 1)" 344 | ] 345 | }, 346 | { 347 | "cell_type": "markdown", 348 | "metadata": {}, 349 | "source": [ 350 | "The following function computes the elements of `c` for all values of `k` where the window can overlap with the array:" 351 | ] 352 | }, 353 | { 354 | "cell_type": "code", 355 | "execution_count": 22, 356 | "metadata": {}, 357 | "outputs": [], 358 | "source": [ 359 | "def correlate(row, window):\n", 360 | " \"\"\"Compute the cross correlation of a and w.\n", 361 | " \"\"\"\n", 362 | " cols = len(row)\n", 363 | " N = len(window)\n", 364 | " c = [c_k(row, window, k) for k in range(cols-N+1)]\n", 365 | " return np.array(c)" 366 | ] 367 | }, 368 | { 369 | "cell_type": "code", 370 | "execution_count": 23, 371 | "metadata": {}, 372 | "outputs": [], 373 | "source": [ 374 | "c = correlate(row, window)\n", 375 | "print(c)" 376 | ] 377 | }, 378 | { 379 | "cell_type": "markdown", 380 | "metadata": {}, 381 | "source": [ 382 | "This operation is useful in many domains, so libraries like NumPy usually provide an implementation. Here's the version from NumPy." 383 | ] 384 | }, 385 | { 386 | "cell_type": "code", 387 | "execution_count": 24, 388 | "metadata": {}, 389 | "outputs": [], 390 | "source": [ 391 | "c = np.correlate(row, window, mode='valid')\n", 392 | "print(c)" 393 | ] 394 | }, 395 | { 396 | "cell_type": "markdown", 397 | "metadata": {}, 398 | "source": [ 399 | "With `mode='valid'`, the NumPy version does the same thing as mine: it only computes the elements of `c` where the window overlaps with the array. A drawback of this mode is that the result is smaller than `array`.\n", 400 | "\n", 401 | "And alternative is `mode='same'`, which makes the result the same size as `array` by extending array with zeros on both sides. Here's the result:" 402 | ] 403 | }, 404 | { 405 | "cell_type": "code", 406 | "execution_count": 25, 407 | "metadata": {}, 408 | "outputs": [], 409 | "source": [ 410 | "c = np.correlate(row, window, mode='same')\n", 411 | "print(c)" 412 | ] 413 | }, 414 | { 415 | "cell_type": "markdown", 416 | "metadata": {}, 417 | "source": [ 418 | "**Exercise:** Write a version of `correlate` that returns the same result as `np.correlate` with `mode='same'.`" 419 | ] 420 | }, 421 | { 422 | "cell_type": "code", 423 | "execution_count": 26, 424 | "metadata": {}, 425 | "outputs": [], 426 | "source": [ 427 | "# Hint: use np.pad to add zeros at the beginning and end of `row`\n", 428 | "\n", 429 | "np.pad(row, 1, 'constant')" 430 | ] 431 | }, 432 | { 433 | "cell_type": "code", 434 | "execution_count": 27, 435 | "metadata": {}, 436 | "outputs": [], 437 | "source": [ 438 | "# Solution goes here" 439 | ] 440 | }, 441 | { 442 | "cell_type": "markdown", 443 | "metadata": {}, 444 | "source": [ 445 | "## Update with correlate\n", 446 | "\n", 447 | "Now we can use `np.correlate` to update the array. I'll start again with an array that contains one column for each cell and one row for each time step, and I'll initialize the first row with a single \"on\" cell in the middle:" 448 | ] 449 | }, 450 | { 451 | "cell_type": "code", 452 | "execution_count": 28, 453 | "metadata": {}, 454 | "outputs": [], 455 | "source": [ 456 | "rows = 5\n", 457 | "cols = 11\n", 458 | "array = np.zeros((rows, cols), dtype=np.uint8)\n", 459 | "array[0, 5] = 1\n", 460 | "print(array)" 461 | ] 462 | }, 463 | { 464 | "cell_type": "markdown", 465 | "metadata": {}, 466 | "source": [ 467 | "Now here's a version of `step` that uses `np.correlate`" 468 | ] 469 | }, 470 | { 471 | "cell_type": "code", 472 | "execution_count": 29, 473 | "metadata": {}, 474 | "outputs": [], 475 | "source": [ 476 | "def step2(array, i, window=[1,1,1]):\n", 477 | " \"\"\"Compute row i of a CA.\n", 478 | " \"\"\"\n", 479 | " row = array[i-1]\n", 480 | " c = np.correlate(row, window, mode='same')\n", 481 | " array[i] = c % 2" 482 | ] 483 | }, 484 | { 485 | "cell_type": "markdown", 486 | "metadata": {}, 487 | "source": [ 488 | "And the result is the same." 489 | ] 490 | }, 491 | { 492 | "cell_type": "code", 493 | "execution_count": 30, 494 | "metadata": {}, 495 | "outputs": [], 496 | "source": [ 497 | "for i in range(1, rows):\n", 498 | " step2(array, i)\n", 499 | "\n", 500 | "plot_ca(array)" 501 | ] 502 | }, 503 | { 504 | "cell_type": "markdown", 505 | "metadata": {}, 506 | "source": [ 507 | "## CA Tables\n", 508 | "\n", 509 | "What we have so far is good enough for a CA that only depends on the total number of \"on\" cells, but for more general CAs, we need a table that maps from the configuration of the neighborhood to the future state of the center cell.\n", 510 | "\n", 511 | "The following function makes the table by interpreting the Rule number in binary." 512 | ] 513 | }, 514 | { 515 | "cell_type": "code", 516 | "execution_count": 31, 517 | "metadata": {}, 518 | "outputs": [], 519 | "source": [ 520 | "def make_table(rule):\n", 521 | " \"\"\"Make the table for a given CA rule.\n", 522 | " \n", 523 | " rule: int 0-255\n", 524 | " \n", 525 | " returns: array of 8 0s and 1s\n", 526 | " \"\"\"\n", 527 | " rule = np.array([rule], dtype=np.uint8)\n", 528 | " table = np.unpackbits(rule)[::-1]\n", 529 | " return table" 530 | ] 531 | }, 532 | { 533 | "cell_type": "markdown", 534 | "metadata": {}, 535 | "source": [ 536 | "Here's what it looks like as an array:" 537 | ] 538 | }, 539 | { 540 | "cell_type": "code", 541 | "execution_count": 32, 542 | "metadata": {}, 543 | "outputs": [], 544 | "source": [ 545 | "table = make_table(150)\n", 546 | "print(table)" 547 | ] 548 | }, 549 | { 550 | "cell_type": "markdown", 551 | "metadata": {}, 552 | "source": [ 553 | "If we correlate the row with the window `[4, 2, 1]`, it treats each neighborhood as a binary number between 000 and 111." 554 | ] 555 | }, 556 | { 557 | "cell_type": "code", 558 | "execution_count": 33, 559 | "metadata": {}, 560 | "outputs": [], 561 | "source": [ 562 | "window = [4, 2, 1]\n", 563 | "c = np.correlate(array[0], window, mode='same')\n", 564 | "print(array[0])\n", 565 | "print(c)" 566 | ] 567 | }, 568 | { 569 | "cell_type": "markdown", 570 | "metadata": {}, 571 | "source": [ 572 | "Now we can use the result from `np.correlate` as an index into the table; the result is the next row of the array." 573 | ] 574 | }, 575 | { 576 | "cell_type": "code", 577 | "execution_count": 34, 578 | "metadata": {}, 579 | "outputs": [], 580 | "source": [ 581 | "array[1] = table[c]\n", 582 | "print(array[1])" 583 | ] 584 | }, 585 | { 586 | "cell_type": "markdown", 587 | "metadata": {}, 588 | "source": [ 589 | "We can wrap up that code in a function:" 590 | ] 591 | }, 592 | { 593 | "cell_type": "code", 594 | "execution_count": 35, 595 | "metadata": {}, 596 | "outputs": [], 597 | "source": [ 598 | "def step3(array, i, window=[4,2,1]):\n", 599 | " \"\"\"Compute row i of a CA.\n", 600 | " \"\"\"\n", 601 | " row = array[i-1]\n", 602 | " c = np.correlate(row, window, mode='same')\n", 603 | " array[i] = table[c]" 604 | ] 605 | }, 606 | { 607 | "cell_type": "markdown", 608 | "metadata": {}, 609 | "source": [ 610 | "And test it again." 611 | ] 612 | }, 613 | { 614 | "cell_type": "code", 615 | "execution_count": 36, 616 | "metadata": {}, 617 | "outputs": [], 618 | "source": [ 619 | "for i in range(1, rows):\n", 620 | " step3(array, i)\n", 621 | "\n", 622 | "plot_ca(array)" 623 | ] 624 | }, 625 | { 626 | "cell_type": "markdown", 627 | "metadata": {}, 628 | "source": [ 629 | "How did I know that Rule 150 is the same as the previous CA? I wrote out the table and converted it to binary." 630 | ] 631 | }, 632 | { 633 | "cell_type": "markdown", 634 | "metadata": {}, 635 | "source": [ 636 | "## The Cell1D object" 637 | ] 638 | }, 639 | { 640 | "cell_type": "markdown", 641 | "metadata": {}, 642 | "source": [ 643 | "`Cell1D` encapsulates the code from the previous section." 644 | ] 645 | }, 646 | { 647 | "cell_type": "code", 648 | "execution_count": 37, 649 | "metadata": {}, 650 | "outputs": [], 651 | "source": [ 652 | "class Cell1D:\n", 653 | " \"\"\"Represents a 1-D a cellular automaton\"\"\"\n", 654 | "\n", 655 | " def __init__(self, rule, n, m=None):\n", 656 | " \"\"\"Initializes the CA.\n", 657 | "\n", 658 | " rule: integer\n", 659 | " n: number of rows\n", 660 | " m: number of columns\n", 661 | "\n", 662 | " Attributes:\n", 663 | " table: rule dictionary that maps from triple to next state.\n", 664 | " array: the numpy array that contains the data.\n", 665 | " next: the index of the next empty row.\n", 666 | " \"\"\"\n", 667 | " self.table = make_table(rule)\n", 668 | " self.n = n\n", 669 | " self.m = 2*n + 1 if m is None else m\n", 670 | "\n", 671 | " self.array = np.zeros((n, self.m), dtype=np.int8)\n", 672 | " self.next = 0\n", 673 | "\n", 674 | " def start_single(self):\n", 675 | " \"\"\"Starts with one cell in the middle of the top row.\"\"\"\n", 676 | " self.array[0, self.m//2] = 1\n", 677 | " self.next += 1\n", 678 | "\n", 679 | " def start_random(self):\n", 680 | " \"\"\"Start with random values in the top row.\"\"\"\n", 681 | " self.array[0] = np.random.randint(2, size=self.m)\n", 682 | " self.next += 1\n", 683 | "\n", 684 | " def start_string(self, s):\n", 685 | " \"\"\"Start with values from a string of 1s and 0s.\"\"\"\n", 686 | " # TODO: Check string length\n", 687 | " self.array[0] = np.array([int(x) for x in s])\n", 688 | " self.next += 1\n", 689 | "\n", 690 | " def loop(self, steps=1):\n", 691 | " \"\"\"Executes the given number of time steps.\"\"\"\n", 692 | " for i in range(steps):\n", 693 | " self.step()\n", 694 | "\n", 695 | " def step(self):\n", 696 | " \"\"\"Executes one time step by computing the next row of the array.\"\"\"\n", 697 | " a = self.array\n", 698 | " i = self.next\n", 699 | " window = [4, 2, 1]\n", 700 | " c = np.correlate(a[i-1], window, mode='same')\n", 701 | " a[i] = self.table[c]\n", 702 | " self.next += 1\n", 703 | "\n", 704 | " def draw(self, start=0, end=None):\n", 705 | " \"\"\"Draws the CA using pyplot.imshow.\n", 706 | "\n", 707 | " start: index of the first column to be shown\n", 708 | " end: index of the last column to be shown\n", 709 | " \"\"\"\n", 710 | " a = self.array[:, start:end]\n", 711 | " plt.imshow(a, cmap='Blues', alpha=0.7)\n", 712 | " \n", 713 | " # turn off axis tick marks\n", 714 | " plt.xticks([])\n", 715 | " plt.yticks([])" 716 | ] 717 | }, 718 | { 719 | "cell_type": "markdown", 720 | "metadata": {}, 721 | "source": [ 722 | "The following function makes and draws a CA." 723 | ] 724 | }, 725 | { 726 | "cell_type": "code", 727 | "execution_count": 43, 728 | "metadata": {}, 729 | "outputs": [], 730 | "source": [ 731 | "def draw_ca(rule, n=32):\n", 732 | " \"\"\"Makes and draw a 1D CA with a given rule.\n", 733 | " \n", 734 | " rule: int rule number\n", 735 | " n: number of rows\n", 736 | " \"\"\"\n", 737 | " ca = Cell1D(rule, n)\n", 738 | " ca.start_single()\n", 739 | " ca.loop(n-1)\n", 740 | " ca.draw()" 741 | ] 742 | }, 743 | { 744 | "cell_type": "markdown", 745 | "metadata": {}, 746 | "source": [ 747 | "Here's an example that runs a Rule 50 CA for 10 steps." 748 | ] 749 | }, 750 | { 751 | "cell_type": "code", 752 | "execution_count": 44, 753 | "metadata": {}, 754 | "outputs": [], 755 | "source": [ 756 | "draw_ca(rule=50, n=10)\n", 757 | "savefig('figs/chap05-1')" 758 | ] 759 | }, 760 | { 761 | "cell_type": "markdown", 762 | "metadata": {}, 763 | "source": [ 764 | "Another example:" 765 | ] 766 | }, 767 | { 768 | "cell_type": "code", 769 | "execution_count": 40, 770 | "metadata": {}, 771 | "outputs": [], 772 | "source": [ 773 | "draw_ca(rule=150, n=5)\n", 774 | "\n", 775 | "savefig('figs/chap05-2')" 776 | ] 777 | }, 778 | { 779 | "cell_type": "markdown", 780 | "metadata": {}, 781 | "source": [ 782 | "And one more example showing recursive structure." 783 | ] 784 | }, 785 | { 786 | "cell_type": "code", 787 | "execution_count": 41, 788 | "metadata": {}, 789 | "outputs": [], 790 | "source": [ 791 | "draw_ca(rule=18, n=64)\n", 792 | "\n", 793 | "savefig('figs/chap05-3')" 794 | ] 795 | }, 796 | { 797 | "cell_type": "markdown", 798 | "metadata": {}, 799 | "source": [ 800 | "Rule 30 generates a sequence of bits that is indistinguishable from random:" 801 | ] 802 | }, 803 | { 804 | "cell_type": "code", 805 | "execution_count": 42, 806 | "metadata": {}, 807 | "outputs": [], 808 | "source": [ 809 | "draw_ca(rule=30, n=100)\n", 810 | "\n", 811 | "savefig('figs/chap05-4')" 812 | ] 813 | }, 814 | { 815 | "cell_type": "markdown", 816 | "metadata": {}, 817 | "source": [ 818 | "And Rule 110 is Turing complete!" 819 | ] 820 | }, 821 | { 822 | "cell_type": "code", 823 | "execution_count": null, 824 | "metadata": {}, 825 | "outputs": [], 826 | "source": [ 827 | "draw_ca(rule=110, n=100)\n", 828 | "\n", 829 | "savefig('figs/chap05-5')" 830 | ] 831 | }, 832 | { 833 | "cell_type": "markdown", 834 | "metadata": {}, 835 | "source": [ 836 | "Here's a longer run that has some spaceships." 837 | ] 838 | }, 839 | { 840 | "cell_type": "code", 841 | "execution_count": null, 842 | "metadata": {}, 843 | "outputs": [], 844 | "source": [ 845 | "np.random.seed(21)\n", 846 | "\n", 847 | "n = 600\n", 848 | "ca = Cell1D(rule=110, n=n)\n", 849 | "ca.start_random()\n", 850 | "ca.loop(n-1)\n", 851 | "ca.draw()\n", 852 | "\n", 853 | "savefig('figs/chap05-6')" 854 | ] 855 | }, 856 | { 857 | "cell_type": "markdown", 858 | "metadata": {}, 859 | "source": [ 860 | "## Exercises" 861 | ] 862 | }, 863 | { 864 | "cell_type": "markdown", 865 | "metadata": {}, 866 | "source": [ 867 | "**Exercise:** This exercise asks you to experiment with Rule 110 and see how\n", 868 | "many spaceships you can find.\n", 869 | "\n", 870 | "1. Read the [Wikipedia page about Rule 110](https://en.wikipedia.org/wiki/Rule_110), which describes its background pattern and spaceships.\n", 871 | "\n", 872 | "2. Create a Rule 110 CA with an initial condition that yields the\n", 873 | " stable background pattern. Note that the CA class provides\n", 874 | "`start_string`, which allow you to initialize the state of\n", 875 | "the array using a string of `1`s and `0`s.\n", 876 | "\n", 877 | "3. Modify the initial condition by adding different patterns in the\n", 878 | " center of the row and see which ones yield spaceships. You might\n", 879 | " want to enumerate all possible patterns of $n$ bits, for some\n", 880 | " reasonable value of $n$. For each spaceship, can you find the\n", 881 | " period and rate of translation? What is the biggest spaceship you\n", 882 | " can find?\n", 883 | "\n", 884 | "4. What happens when spaceships collide?" 885 | ] 886 | }, 887 | { 888 | "cell_type": "code", 889 | "execution_count": null, 890 | "metadata": {}, 891 | "outputs": [], 892 | "source": [ 893 | "# Solution goes here" 894 | ] 895 | }, 896 | { 897 | "cell_type": "code", 898 | "execution_count": null, 899 | "metadata": {}, 900 | "outputs": [], 901 | "source": [ 902 | "# Solution goes here" 903 | ] 904 | }, 905 | { 906 | "cell_type": "code", 907 | "execution_count": null, 908 | "metadata": {}, 909 | "outputs": [], 910 | "source": [ 911 | "# Solution goes here" 912 | ] 913 | }, 914 | { 915 | "cell_type": "code", 916 | "execution_count": null, 917 | "metadata": {}, 918 | "outputs": [], 919 | "source": [ 920 | "# Solution goes here" 921 | ] 922 | }, 923 | { 924 | "cell_type": "code", 925 | "execution_count": null, 926 | "metadata": {}, 927 | "outputs": [], 928 | "source": [ 929 | "# Solution goes here" 930 | ] 931 | }, 932 | { 933 | "cell_type": "code", 934 | "execution_count": null, 935 | "metadata": {}, 936 | "outputs": [], 937 | "source": [ 938 | "# Solution goes here" 939 | ] 940 | }, 941 | { 942 | "cell_type": "code", 943 | "execution_count": null, 944 | "metadata": {}, 945 | "outputs": [], 946 | "source": [ 947 | "# Solution goes here" 948 | ] 949 | }, 950 | { 951 | "cell_type": "markdown", 952 | "metadata": {}, 953 | "source": [ 954 | "**Exercise:** The goal of this exercise is to implement a Turing machine.\n", 955 | "\n", 956 | "1. Read about Turing machines at http://en.wikipedia.org/wiki/Turing_machine.\n", 957 | "\n", 958 | "2. Write a class called `Turing` that implements a Turing machine. For the action table, use the rules for a 3-state busy beaver.\n", 959 | "\n", 960 | "3. Write a `draw` method that plots the state of the tape and the position and state of the head. For one example of what that might look like, see http://mathworld.wolfram.com/TuringMachine.html." 961 | ] 962 | }, 963 | { 964 | "cell_type": "code", 965 | "execution_count": null, 966 | "metadata": {}, 967 | "outputs": [], 968 | "source": [ 969 | "# Solution goes here" 970 | ] 971 | }, 972 | { 973 | "cell_type": "code", 974 | "execution_count": null, 975 | "metadata": {}, 976 | "outputs": [], 977 | "source": [ 978 | "# Solution goes here" 979 | ] 980 | }, 981 | { 982 | "cell_type": "code", 983 | "execution_count": null, 984 | "metadata": {}, 985 | "outputs": [], 986 | "source": [ 987 | "# Solution goes here" 988 | ] 989 | }, 990 | { 991 | "cell_type": "code", 992 | "execution_count": null, 993 | "metadata": {}, 994 | "outputs": [], 995 | "source": [ 996 | "# Solution goes here" 997 | ] 998 | }, 999 | { 1000 | "cell_type": "markdown", 1001 | "metadata": {}, 1002 | "source": [ 1003 | "**Exercise:** This exercise asks you to implement and test several PRNGs.\n", 1004 | "For testing, you will need to install \n", 1005 | "`DieHarder`, which you can download from \n", 1006 | "https://www.phy.duke.edu/~rgb/General/dieharder.php, or it\n", 1007 | "might be available as a package for your operating system.\n", 1008 | "\n", 1009 | "1. Write a program that implements one of the linear congruential\n", 1010 | "generators described at http://en.wikipedia.org/wiki/Linear_congruential_generator}.\n", 1011 | "Test it using `DieHarder`.\n", 1012 | "\n", 1013 | "2. Read the documentation of Python's `random` module.\n", 1014 | "What PRNG does it use? Test it.\n", 1015 | "\n", 1016 | "3. Implement a Rule 30 CA with a few hundred cells,\n", 1017 | "run it for as many time steps as you can in a reasonable amount\n", 1018 | "of time, and output the center column as a sequence of bits.\n", 1019 | "Test it.\n" 1020 | ] 1021 | }, 1022 | { 1023 | "cell_type": "code", 1024 | "execution_count": null, 1025 | "metadata": {}, 1026 | "outputs": [], 1027 | "source": [ 1028 | "# Solution goes here" 1029 | ] 1030 | }, 1031 | { 1032 | "cell_type": "code", 1033 | "execution_count": null, 1034 | "metadata": {}, 1035 | "outputs": [], 1036 | "source": [ 1037 | "# Solution goes here" 1038 | ] 1039 | }, 1040 | { 1041 | "cell_type": "code", 1042 | "execution_count": null, 1043 | "metadata": {}, 1044 | "outputs": [], 1045 | "source": [ 1046 | "# Solution goes here" 1047 | ] 1048 | }, 1049 | { 1050 | "cell_type": "code", 1051 | "execution_count": null, 1052 | "metadata": {}, 1053 | "outputs": [], 1054 | "source": [] 1055 | }, 1056 | { 1057 | "cell_type": "code", 1058 | "execution_count": null, 1059 | "metadata": {}, 1060 | "outputs": [], 1061 | "source": [] 1062 | } 1063 | ], 1064 | "metadata": { 1065 | "kernelspec": { 1066 | "display_name": "Python 3 (ipykernel)", 1067 | "language": "python", 1068 | "name": "python3" 1069 | }, 1070 | "language_info": { 1071 | "codemirror_mode": { 1072 | "name": "ipython", 1073 | "version": 3 1074 | }, 1075 | "file_extension": ".py", 1076 | "mimetype": "text/x-python", 1077 | "name": "python", 1078 | "nbconvert_exporter": "python", 1079 | "pygments_lexer": "ipython3", 1080 | "version": "3.10.14" 1081 | } 1082 | }, 1083 | "nbformat": 4, 1084 | "nbformat_minor": 4 1085 | } 1086 | --------------------------------------------------------------------------------