├── .gitignore ├── README.md ├── data ├── DataOWall.csv ├── USASAC2018-Report-30-2017-Activities-Page11.pdf ├── USASAC2018-Report-30-2017-Activities.pdf ├── USASAC_Report_2017_Page18.png ├── baseball_drag.csv └── glucose_insulin.csv ├── figs ├── Lowpass_Filter_RC.png ├── RC_Divider.pdf ├── RC_Divider.svg ├── api_cheat_sheet.odg ├── baseball.odg ├── baseball.png ├── baseball_drag.png ├── dataframe.odg ├── dataframe.pdf ├── dataframe.png ├── earth.odg ├── earth.png ├── github_work_flow.odg ├── github_work_flow.pdf ├── github_work_flow.png ├── github_work_flow_bayes.odg ├── github_work_flow_bayes.png ├── golden1.fig ├── golden1.png ├── golden2.fig ├── golden2.png ├── image_sources.txt ├── jump.png ├── kitten.odg ├── kitten.pdf ├── kitten.png ├── modeling_framework.odg ├── modeling_framework.pdf ├── modeling_framework.png ├── modeling_framework2.odg ├── modeling_framework2.pdf ├── modeling_framework2.png ├── modsim_libraries.odg ├── modsim_libraries.png ├── modsim_logo.odg ├── modsim_logo.png ├── paper_roll.odg ├── paper_roll.pdf ├── paper_roll.png ├── penny.odg ├── penny.png ├── queue.odg ├── queue.pdf ├── queue.png ├── secant.fig ├── secant.png ├── spiderman.odg ├── spiderman.pdf ├── spiderman.png ├── stock_flow1.odg ├── stock_flow1.pdf ├── stock_flow1.png ├── teapot.odg ├── teapot.pdf ├── teapot.png ├── throwingaxe1.png ├── throwingaxe2.png ├── wall_model.pdf ├── wall_model.png ├── yoyo.odg ├── yoyo.pdf └── yoyo.png ├── modsim.py ├── python ├── environment.yml └── soln │ ├── chap01.ipynb │ ├── chap02.ipynb │ ├── chap03.ipynb │ ├── chap04.ipynb │ ├── chap05.ipynb │ ├── chap06.ipynb │ ├── chap07.ipynb │ ├── chap08.ipynb │ ├── chap09.ipynb │ ├── chap10.ipynb │ ├── chap11.ipynb │ ├── chap11.py │ ├── chap12.ipynb │ ├── chap12.py │ ├── chap13.ipynb │ ├── chap13.py │ ├── chap14.ipynb │ ├── chap15.ipynb │ ├── chap15.py │ ├── chap16.ipynb │ ├── chap17.ipynb │ ├── chap18.ipynb │ ├── chap18.py │ ├── chap19.ipynb │ ├── chap20.ipynb │ ├── chap21.ipynb │ ├── chap22.ipynb │ ├── chap22.py │ ├── chap23.ipynb │ ├── chap24.ipynb │ ├── chap25.ipynb │ ├── chap26.ipynb │ ├── examples │ ├── bungee1_soln.ipynb │ ├── bungee2_soln.ipynb │ ├── filter_soln.ipynb │ ├── glucose_insulin.csv │ ├── glucose_soln.ipynb │ ├── hiv_model_soln.ipynb │ ├── insulin_soln.ipynb │ ├── kitten_soln.ipynb │ ├── orbit_soln.ipynb │ ├── queue_soln.ipynb │ ├── salmon_soln.ipynb │ ├── spiderman_soln.ipynb │ ├── throwingaxe_soln.ipynb │ ├── trees_soln.ipynb │ ├── wall_soln.ipynb │ └── yoyo_soln.ipynb │ └── modsim.py └── soln ├── chap01.ipynb ├── chap02.ipynb ├── chap03.ipynb ├── chap04.ipynb ├── chap05.ipynb ├── chap06.ipynb ├── chap07.ipynb ├── chap08.ipynb ├── chap09.ipynb ├── chap10.ipynb ├── chap11.ipynb ├── chap12.ipynb ├── chap13.ipynb ├── chap14.ipynb ├── chap15.ipynb ├── chap16.ipynb ├── chap17.ipynb ├── chap18.ipynb ├── chap19.ipynb ├── chap20.ipynb ├── chap21.ipynb ├── chap22.ipynb ├── chap23.ipynb ├── chap24.ipynb ├── chap25.ipynb ├── chap26.ipynb └── modsim.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ModSim 2 | Modeling and Simulation in Python and MATLAB/Octave 3 | -------------------------------------------------------------------------------- /data/USASAC2018-Report-30-2017-Activities-Page11.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/data/USASAC2018-Report-30-2017-Activities-Page11.pdf -------------------------------------------------------------------------------- /data/USASAC2018-Report-30-2017-Activities.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/data/USASAC2018-Report-30-2017-Activities.pdf -------------------------------------------------------------------------------- /data/USASAC_Report_2017_Page18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/data/USASAC_Report_2017_Page18.png -------------------------------------------------------------------------------- /data/baseball_drag.csv: -------------------------------------------------------------------------------- 1 | Velocity in mph,Drag coefficient 2 | 0.058486,0.49965 3 | 19.845,0.49878 4 | 39.476,0.49704 5 | 50.181,0.48225 6 | 60.134,0.45004 7 | 68.533,0.40914 8 | 73.769,0.38042 9 | 77.408,0.36562 10 | 83.879,0.34822 11 | 90.507,0.33081 12 | 97.29,0.31427 13 | 104.54,0.30035 14 | 113.83,0.28816 15 | 120.9,0.28381 16 | 127.34,0.28033 17 | 134.41,0.28207 18 | -------------------------------------------------------------------------------- /data/glucose_insulin.csv: -------------------------------------------------------------------------------- 1 | time,glucose,insulin 2 | 0,92,11 3 | 2,350,26 4 | 4,287,130 5 | 6,251,85 6 | 8,240,51 7 | 10,216,49 8 | 12,211,45 9 | 14,205,41 10 | 16,196,35 11 | 19,192,30 12 | 22,172,30 13 | 27,163,27 14 | 32,142,30 15 | 42,124,22 16 | 52,105,15 17 | 62,92,15 18 | 72,84,11 19 | 82,77,10 20 | 92,82,8 21 | 102,81,11 22 | 122,82,7 23 | 142,82,8 24 | 162,85,8 25 | 182,90,7 -------------------------------------------------------------------------------- /figs/Lowpass_Filter_RC.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/Lowpass_Filter_RC.png -------------------------------------------------------------------------------- /figs/RC_Divider.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/RC_Divider.pdf -------------------------------------------------------------------------------- /figs/RC_Divider.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 21 | 28 | 33 | 34 | 41 | 48 | 49 | 69 | 71 | 72 | 74 | image/svg+xml 75 | 77 | 78 | 79 | 80 | 85 | 89 | 93 | 98 | 102 | 106 | 110 | 114 | 118 | 122 | 126 | R 137 | C 148 | V 159 | in 170 | V 181 | out 192 | 193 | 194 | -------------------------------------------------------------------------------- /figs/api_cheat_sheet.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/api_cheat_sheet.odg -------------------------------------------------------------------------------- /figs/baseball.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/baseball.odg -------------------------------------------------------------------------------- /figs/baseball.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/baseball.png -------------------------------------------------------------------------------- /figs/baseball_drag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/baseball_drag.png -------------------------------------------------------------------------------- /figs/dataframe.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/dataframe.odg -------------------------------------------------------------------------------- /figs/dataframe.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/dataframe.pdf -------------------------------------------------------------------------------- /figs/dataframe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/dataframe.png -------------------------------------------------------------------------------- /figs/earth.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/earth.odg -------------------------------------------------------------------------------- /figs/earth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/earth.png -------------------------------------------------------------------------------- /figs/github_work_flow.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/github_work_flow.odg -------------------------------------------------------------------------------- /figs/github_work_flow.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/github_work_flow.pdf -------------------------------------------------------------------------------- /figs/github_work_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/github_work_flow.png -------------------------------------------------------------------------------- /figs/github_work_flow_bayes.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/github_work_flow_bayes.odg -------------------------------------------------------------------------------- /figs/github_work_flow_bayes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/github_work_flow_bayes.png -------------------------------------------------------------------------------- /figs/golden1.fig: -------------------------------------------------------------------------------- 1 | #FIG 3.2 Produced by xfig version 3.2.6a 2 | Landscape 3 | Center 4 | Inches 5 | Letter 6 | 100.00 7 | Single 8 | -2 9 | 1200 2 10 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 1725 3075 38 38 1687 3075 1763 3075 11 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 4350 3450 38 38 4312 3450 4388 3450 12 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 2625 4050 38 38 2587 4050 2663 4050 13 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 14 | 1725 3150 1725 4800 15 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 16 | 2625 4050 2625 4800 17 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 18 | 4350 3450 4350 4800 19 | 2 1 0 1 0 7 50 0 -1 0.000 0 0 -1 0 0 2 20 | 1200 4800 4800 4800 21 | 4 0 0 50 0 16 14 0.0000 4 135 180 4275 5100 x3\001 22 | 4 0 0 50 0 16 14 0.0000 4 135 180 2550 5100 x2\001 23 | 4 0 0 50 0 16 14 0.0000 4 135 180 1650 5100 x1\001 24 | -------------------------------------------------------------------------------- /figs/golden1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/golden1.png -------------------------------------------------------------------------------- /figs/golden2.fig: -------------------------------------------------------------------------------- 1 | #FIG 3.2 Produced by xfig version 3.2.6a 2 | Landscape 3 | Center 4 | Inches 5 | Letter 6 | 100.00 7 | Single 8 | -2 9 | 1200 2 10 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 6300 3075 38 38 6262 3075 6338 3075 11 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 8925 3450 38 38 8887 3450 8963 3450 12 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 7200 4125 38 38 7162 4125 7238 4125 13 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 7800 3975 38 38 7762 3975 7838 3975 14 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 1725 3075 38 38 1687 3075 1763 3075 15 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 4350 3450 38 38 4312 3450 4388 3450 16 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 2625 4125 38 38 2587 4125 2663 4125 17 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 3225 4275 38 38 3187 4275 3263 4275 18 | 2 1 0 1 0 7 50 0 -1 0.000 0 0 -1 0 0 2 19 | 1200 4800 4800 4800 20 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 21 | 6300 3150 6300 4800 22 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 23 | 7200 4125 7200 4800 24 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 25 | 8925 3450 8925 4800 26 | 2 1 0 1 0 7 50 0 -1 0.000 0 0 -1 0 0 2 27 | 5775 4800 9375 4800 28 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 29 | 7800 4050 7800 4800 30 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 31 | 1725 3150 1725 4800 32 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 33 | 2625 4125 2625 4800 34 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 35 | 4350 3450 4350 4800 36 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 37 | 3225 4275 3225 4800 38 | 4 0 0 50 0 16 14 0.0000 4 135 180 7725 5100 x4\001 39 | 4 0 0 50 0 16 14 0.0000 4 135 180 7125 5100 x2\001 40 | 4 0 0 50 0 16 14 0.0000 4 135 180 8850 5100 x3\001 41 | 4 0 0 50 0 16 14 0.0000 4 135 180 6225 5100 x1\001 42 | 4 0 0 50 0 16 14 0.0000 4 135 180 3150 5100 x4\001 43 | 4 0 0 50 0 16 14 0.0000 4 135 180 2550 5100 x2\001 44 | 4 0 0 50 0 16 14 0.0000 4 135 180 4275 5100 x3\001 45 | 4 0 0 50 0 16 14 0.0000 4 135 180 1650 5100 x1\001 46 | -------------------------------------------------------------------------------- /figs/golden2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/golden2.png -------------------------------------------------------------------------------- /figs/image_sources.txt: -------------------------------------------------------------------------------- 1 | globe, public domain, http://www.freestockphotos.biz/stockphoto/16911 2 | sphere, CC0, https://www.kissclipart.com/blue-sphere-clipart-clip-art-spxa3o/ 3 | chart, CC0, https://openclipart.org/detail/215430/chart 4 | empire state building, public domain, https://freesvg.org/boort-art-deco-empire-state-building 5 | other building, public domain, https://freesvg.org/building-vector-silhouette 6 | -------------------------------------------------------------------------------- /figs/jump.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/jump.png -------------------------------------------------------------------------------- /figs/kitten.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/kitten.odg -------------------------------------------------------------------------------- /figs/kitten.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/kitten.pdf -------------------------------------------------------------------------------- /figs/kitten.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/kitten.png -------------------------------------------------------------------------------- /figs/modeling_framework.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/modeling_framework.odg -------------------------------------------------------------------------------- /figs/modeling_framework.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/modeling_framework.pdf -------------------------------------------------------------------------------- /figs/modeling_framework.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/modeling_framework.png -------------------------------------------------------------------------------- /figs/modeling_framework2.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/modeling_framework2.odg -------------------------------------------------------------------------------- /figs/modeling_framework2.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/modeling_framework2.pdf -------------------------------------------------------------------------------- /figs/modeling_framework2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/modeling_framework2.png -------------------------------------------------------------------------------- /figs/modsim_libraries.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/modsim_libraries.odg -------------------------------------------------------------------------------- /figs/modsim_libraries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/modsim_libraries.png -------------------------------------------------------------------------------- /figs/modsim_logo.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/modsim_logo.odg -------------------------------------------------------------------------------- /figs/modsim_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/modsim_logo.png -------------------------------------------------------------------------------- /figs/paper_roll.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/paper_roll.odg -------------------------------------------------------------------------------- /figs/paper_roll.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/paper_roll.pdf -------------------------------------------------------------------------------- /figs/paper_roll.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/paper_roll.png -------------------------------------------------------------------------------- /figs/penny.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/penny.odg -------------------------------------------------------------------------------- /figs/penny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/penny.png -------------------------------------------------------------------------------- /figs/queue.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/queue.odg -------------------------------------------------------------------------------- /figs/queue.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/queue.pdf -------------------------------------------------------------------------------- /figs/queue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/queue.png -------------------------------------------------------------------------------- /figs/secant.fig: -------------------------------------------------------------------------------- 1 | #FIG 3.2 Produced by xfig version 3.2.6a 2 | Landscape 3 | Center 4 | Inches 5 | Letter 6 | 100.00 7 | Single 8 | -2 9 | 1200 2 10 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 1725 3075 38 38 1687 3075 1763 3075 11 | 1 4 0 1 0 0 50 -1 20 0.000 1 0.0000 4275 5400 38 38 4237 5400 4313 5400 12 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 13 | 1725 3150 1725 4800 14 | 2 1 2 1 0 7 50 -1 -1 3.000 0 0 -1 0 0 2 15 | 4275 4800 4275 5400 16 | 2 1 0 1 0 7 50 0 -1 0.000 0 0 -1 0 0 2 17 | 1200 4800 4800 4800 18 | 4 0 0 50 0 16 14 0.0000 4 165 450 4425 5400 f(x2)\001 19 | 4 0 0 50 0 16 14 0.0000 4 165 450 1875 3225 f(x1)\001 20 | -------------------------------------------------------------------------------- /figs/secant.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/secant.png -------------------------------------------------------------------------------- /figs/spiderman.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/spiderman.odg -------------------------------------------------------------------------------- /figs/spiderman.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/spiderman.pdf -------------------------------------------------------------------------------- /figs/spiderman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/spiderman.png -------------------------------------------------------------------------------- /figs/stock_flow1.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/stock_flow1.odg -------------------------------------------------------------------------------- /figs/stock_flow1.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/stock_flow1.pdf -------------------------------------------------------------------------------- /figs/stock_flow1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/stock_flow1.png -------------------------------------------------------------------------------- /figs/teapot.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/teapot.odg -------------------------------------------------------------------------------- /figs/teapot.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/teapot.pdf -------------------------------------------------------------------------------- /figs/teapot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/teapot.png -------------------------------------------------------------------------------- /figs/throwingaxe1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/throwingaxe1.png -------------------------------------------------------------------------------- /figs/throwingaxe2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/throwingaxe2.png -------------------------------------------------------------------------------- /figs/wall_model.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/wall_model.pdf -------------------------------------------------------------------------------- /figs/wall_model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/wall_model.png -------------------------------------------------------------------------------- /figs/yoyo.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/yoyo.odg -------------------------------------------------------------------------------- /figs/yoyo.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/yoyo.pdf -------------------------------------------------------------------------------- /figs/yoyo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AllenDowney/ModSim/13be0b60ff89076d409356e59baf5d0c0e5b8d71/figs/yoyo.png -------------------------------------------------------------------------------- /modsim.py: -------------------------------------------------------------------------------- 1 | """ 2 | Code from Modeling and Simulation in Python. 3 | 4 | Copyright 2020 Allen Downey 5 | 6 | MIT License: https://opensource.org/licenses/MIT 7 | """ 8 | 9 | import logging 10 | 11 | logger = logging.getLogger(name="modsim.py") 12 | 13 | # make sure we have Python 3.6 or better 14 | import sys 15 | 16 | if sys.version_info < (3, 6): 17 | logger.warning("modsim.py depends on Python 3.6 features.") 18 | 19 | import inspect 20 | 21 | import matplotlib.pyplot as plt 22 | import numpy as np 23 | import pandas as pd 24 | import scipy 25 | 26 | import scipy.optimize as spo 27 | 28 | from scipy.interpolate import interp1d 29 | from scipy.interpolate import InterpolatedUnivariateSpline 30 | 31 | from scipy.integrate import solve_ivp 32 | 33 | from types import SimpleNamespace 34 | from copy import copy 35 | 36 | import pint 37 | 38 | units = pint.UnitRegistry() 39 | #Quantity = units.Quantity 40 | 41 | 42 | def flip(p=0.5): 43 | """Flips a coin with the given probability. 44 | 45 | p: float 0-1 46 | 47 | returns: boolean (True or False) 48 | """ 49 | return np.random.random() < p 50 | 51 | 52 | def cart2pol(x, y, z=None): 53 | """Convert Cartesian coordinates to polar. 54 | 55 | x: number or sequence 56 | y: number or sequence 57 | z: number or sequence (optional) 58 | 59 | returns: theta, rho OR theta, rho, z 60 | """ 61 | x = np.asarray(x) 62 | y = np.asarray(y) 63 | 64 | rho = np.hypot(x, y) 65 | theta = np.arctan2(y, x) 66 | 67 | if z is None: 68 | return theta, rho 69 | else: 70 | return theta, rho, z 71 | 72 | 73 | def pol2cart(theta, rho, z=None): 74 | """Convert polar coordinates to Cartesian. 75 | 76 | theta: number or sequence in radians 77 | rho: number or sequence 78 | z: number or sequence (optional) 79 | 80 | returns: x, y OR x, y, z 81 | """ 82 | x = rho * np.cos(theta) 83 | y = rho * np.sin(theta) 84 | 85 | if z is None: 86 | return x, y 87 | else: 88 | return x, y, z 89 | 90 | from numpy import linspace 91 | 92 | def linrange(start, stop=None, step=1, **options): 93 | """Make an array of equally spaced values. 94 | 95 | start: first value 96 | stop: last value (might be approximate) 97 | step: difference between elements (should be consistent) 98 | 99 | returns: NumPy array 100 | """ 101 | if stop is None: 102 | stop = start 103 | start = 0 104 | n = int(round((stop-start) / step)) 105 | return linspace(start, stop, n+1, **options) 106 | 107 | 108 | def root_scalar(func, *args, **kwargs): 109 | """Finds the input value that minimizes `min_func`. 110 | 111 | Wrapper for 112 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.root_scalar.html 113 | 114 | func: computes the function to be minimized 115 | bracket: sequence of two values, lower and upper bounds of the range to be searched 116 | args: any additional positional arguments are passed to func 117 | kwargs: any keyword arguments are passed to root_scalar 118 | 119 | returns: RootResults object 120 | """ 121 | bracket = kwargs.get('bracket', None) 122 | if bracket is None or len(bracket) != 2: 123 | msg = ("To run root_scalar, you have to provide a " 124 | "`bracket` keyword argument with a sequence " 125 | "of length 2.") 126 | raise ValueError(msg) 127 | 128 | try: 129 | func(bracket[0], *args) 130 | except Exception as e: 131 | msg = ("Before running scipy.integrate.root_scalar " 132 | "I tried running the function you provided " 133 | "with `bracket[0]`, " 134 | "and I got the following error:") 135 | logger.error(msg) 136 | raise (e) 137 | 138 | underride(kwargs, rtol=1e-4) 139 | 140 | res = spo.root_scalar(func, *args, **kwargs) 141 | 142 | if not res.converged: 143 | msg = ("scipy.optimize.root_scalar did not converge. " 144 | "The message it returned is:\n" + res.flag) 145 | raise ValueError(msg) 146 | 147 | return res 148 | 149 | 150 | def minimize_scalar(func, *args, **kwargs): 151 | """Finds the input value that minimizes `func`. 152 | 153 | Wrapper for 154 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize_scalar.html 155 | 156 | func: computes the function to be minimized 157 | args: any additional positional arguments are passed to func 158 | kwargs: any keyword arguments are passed to minimize_scalar 159 | 160 | returns: OptimizeResult object 161 | """ 162 | bounds = kwargs.get('bounds', None) 163 | 164 | if bounds is None or len(bounds) != 2: 165 | msg = ("To run maximize_scalar or minimize_scalar, " 166 | "you have to provide a `bounds` " 167 | "keyword argument with a sequence " 168 | "of length 2.") 169 | raise ValueError(msg) 170 | 171 | try: 172 | func(bounds[0], *args) 173 | except Exception as e: 174 | msg = ("Before running scipy.integrate.minimize_scalar, " 175 | "I tried running the function you provided " 176 | "with the lower bound, " 177 | "and I got the following error:") 178 | logger.error(msg) 179 | raise (e) 180 | 181 | underride(kwargs, method='bounded') 182 | 183 | res = spo.minimize_scalar(func, args=args, **kwargs) 184 | 185 | if not res.success: 186 | msg = ("minimize_scalar did not succeed." 187 | "The message it returned is: \n" + 188 | res.message) 189 | raise Exception(msg) 190 | 191 | return res 192 | 193 | 194 | def maximize_scalar(max_func, *args, **kwargs): 195 | """Finds the input value that maximizes `max_func`. 196 | 197 | Wrapper for https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize_scalar.html 198 | 199 | min_func: computes the function to be maximized 200 | args: any additional positional arguments are passed to max_func 201 | options: any keyword arguments are passed as options to minimize_scalar 202 | 203 | returns: ModSimSeries object 204 | """ 205 | def min_func(*args): 206 | return -max_func(*args) 207 | 208 | res = minimize_scalar(min_func, *args, **kwargs) 209 | 210 | # we have to negate the function value before returning res 211 | res.fun = -res.fun 212 | return res 213 | 214 | 215 | def run_solve_ivp(system, slope_func, **options): 216 | """Computes a numerical solution to a differential equation. 217 | 218 | `system` must contain `init` with initial conditions, 219 | `t_end` with the end time. Optionally, it can contain 220 | `t_0` with the start time. 221 | 222 | It should contain any other parameters required by the 223 | slope function. 224 | 225 | `options` can be any legal options of `scipy.integrate.solve_ivp` 226 | 227 | system: System object 228 | slope_func: function that computes slopes 229 | 230 | returns: TimeFrame 231 | """ 232 | system = remove_units(system) 233 | 234 | # make sure `system` contains `init` 235 | if not hasattr(system, "init"): 236 | msg = """It looks like `system` does not contain `init` 237 | as a system variable. `init` should be a State 238 | object that specifies the initial condition:""" 239 | raise ValueError(msg) 240 | 241 | # make sure `system` contains `t_end` 242 | if not hasattr(system, "t_end"): 243 | msg = """It looks like `system` does not contain `t_end` 244 | as a system variable. `t_end` should be the 245 | final time:""" 246 | raise ValueError(msg) 247 | 248 | # the default value for t_0 is 0 249 | t_0 = getattr(system, "t_0", 0) 250 | 251 | # try running the slope function with the initial conditions 252 | try: 253 | slope_func(t_0, system.init, system) 254 | except Exception as e: 255 | msg = """Before running scipy.integrate.solve_ivp, I tried 256 | running the slope function you provided with the 257 | initial conditions in `system` and `t=t_0` and I got 258 | the following error:""" 259 | logger.error(msg) 260 | raise (e) 261 | 262 | # get the list of event functions 263 | events = options.get('events', []) 264 | 265 | # if there's only one event function, put it in a list 266 | try: 267 | iter(events) 268 | except TypeError: 269 | events = [events] 270 | 271 | for event_func in events: 272 | # make events terminal unless otherwise specified 273 | if not hasattr(event_func, 'terminal'): 274 | event_func.terminal = True 275 | 276 | # test the event function with the initial conditions 277 | try: 278 | event_func(t_0, system.init, system) 279 | except Exception as e: 280 | msg = """Before running scipy.integrate.solve_ivp, I tried 281 | running the event function you provided with the 282 | initial conditions in `system` and `t=t_0` and I got 283 | the following error:""" 284 | logger.error(msg) 285 | raise (e) 286 | 287 | # get dense output unless otherwise specified 288 | if not 't_eval' in options: 289 | underride(options, dense_output=True) 290 | 291 | # run the solver 292 | bunch = solve_ivp(slope_func, [t_0, system.t_end], system.init, 293 | args=[system], **options) 294 | 295 | # separate the results from the details 296 | y = bunch.pop("y") 297 | t = bunch.pop("t") 298 | 299 | # get the column names from `init`, if possible 300 | if hasattr(system.init, 'index'): 301 | columns = system.init.index 302 | else: 303 | columns = range(len(system.init)) 304 | 305 | # evaluate the results at equally-spaced points 306 | if options.get('dense_output', False): 307 | try: 308 | num = system.num 309 | except AttributeError: 310 | num = 101 311 | t_final = t[-1] 312 | t_array = linspace(t_0, t_final, num) 313 | y_array = bunch.sol(t_array) 314 | 315 | # pack the results into a TimeFrame 316 | results = TimeFrame(y_array.T, index=t_array, 317 | columns=columns) 318 | else: 319 | results = TimeFrame(y.T, index=t, 320 | columns=columns) 321 | 322 | return results, bunch 323 | 324 | 325 | def leastsq(error_func, x0, *args, **options): 326 | """Find the parameters that yield the best fit for the data. 327 | 328 | `x0` can be a sequence, array, Series, or Params 329 | 330 | Positional arguments are passed along to `error_func`. 331 | 332 | Keyword arguments are passed to `scipy.optimize.leastsq` 333 | 334 | error_func: function that computes a sequence of errors 335 | x0: initial guess for the best parameters 336 | args: passed to error_func 337 | options: passed to leastsq 338 | 339 | :returns: Params object with best_params and ModSimSeries with details 340 | """ 341 | # override `full_output` so we get a message if something goes wrong 342 | options["full_output"] = True 343 | 344 | # run leastsq 345 | t = scipy.optimize.leastsq(error_func, x0=x0, args=args, **options) 346 | best_params, cov_x, infodict, mesg, ier = t 347 | 348 | # pack the results into a ModSimSeries object 349 | details = SimpleNamespace(cov_x=cov_x, 350 | mesg=mesg, 351 | ier=ier, 352 | **infodict) 353 | details.success = details.ier in [1,2,3,4] 354 | 355 | # if we got a Params object, we should return a Params object 356 | if isinstance(x0, Params): 357 | best_params = Params(pd.Series(best_params, x0.index)) 358 | 359 | # return the best parameters and details 360 | return best_params, details 361 | 362 | 363 | def crossings(series, value): 364 | """Find the labels where the series passes through value. 365 | 366 | The labels in series must be increasing numerical values. 367 | 368 | series: Series 369 | value: number 370 | 371 | returns: sequence of labels 372 | """ 373 | values = series.values - value 374 | interp = InterpolatedUnivariateSpline(series.index, values) 375 | return interp.roots() 376 | 377 | 378 | def has_nan(a): 379 | """Checks whether the an array contains any NaNs. 380 | 381 | :param a: NumPy array or Pandas Series 382 | :return: boolean 383 | """ 384 | return np.any(np.isnan(a)) 385 | 386 | 387 | def is_strictly_increasing(a): 388 | """Checks whether the elements of an array are strictly increasing. 389 | 390 | :param a: NumPy array or Pandas Series 391 | :return: boolean 392 | """ 393 | return np.all(np.diff(a) > 0) 394 | 395 | 396 | def interpolate(series, **options): 397 | """Creates an interpolation function. 398 | 399 | series: Series object 400 | options: any legal options to scipy.interpolate.interp1d 401 | 402 | returns: function that maps from the index to the values 403 | """ 404 | if has_nan(series.index): 405 | msg = """The Series you passed to interpolate contains 406 | NaN values in the index, which would result in 407 | undefined behavior. So I'm putting a stop to that.""" 408 | raise ValueError(msg) 409 | 410 | if not is_strictly_increasing(series.index): 411 | msg = """The Series you passed to interpolate has an index 412 | that is not strictly increasing, which would result in 413 | undefined behavior. So I'm putting a stop to that.""" 414 | raise ValueError(msg) 415 | 416 | # make the interpolate function extrapolate past the ends of 417 | # the range, unless `options` already specifies a value for `fill_value` 418 | underride(options, fill_value="extrapolate") 419 | 420 | # call interp1d, which returns a new function object 421 | x = series.index 422 | y = series.values 423 | interp_func = interp1d(x, y, **options) 424 | return interp_func 425 | 426 | 427 | def interpolate_inverse(series, **options): 428 | """Interpolate the inverse function of a Series. 429 | 430 | series: Series object, represents a mapping from `a` to `b` 431 | options: any legal options to scipy.interpolate.interp1d 432 | 433 | returns: interpolation object, can be used as a function 434 | from `b` to `a` 435 | """ 436 | inverse = pd.Series(series.index, index=series.values) 437 | interp_func = interpolate(inverse, **options) 438 | return interp_func 439 | 440 | 441 | def gradient(series, **options): 442 | """Computes the numerical derivative of a series. 443 | 444 | If the elements of series have units, they are dropped. 445 | 446 | series: Series object 447 | options: any legal options to np.gradient 448 | 449 | returns: Series, same subclass as series 450 | """ 451 | x = series.index 452 | y = series.values 453 | 454 | a = np.gradient(y, x, **options) 455 | return series.__class__(a, series.index) 456 | 457 | 458 | def source_code(obj): 459 | """Prints the source code for a given object. 460 | 461 | obj: function or method object 462 | """ 463 | print(inspect.getsource(obj)) 464 | 465 | 466 | def underride(d, **options): 467 | """Add key-value pairs to d only if key is not in d. 468 | 469 | If d is None, create a new dictionary. 470 | 471 | d: dictionary 472 | options: keyword args to add to d 473 | """ 474 | if d is None: 475 | d = {} 476 | 477 | for key, val in options.items(): 478 | d.setdefault(key, val) 479 | 480 | return d 481 | 482 | 483 | def contour(df, **options): 484 | """Makes a contour plot from a DataFrame. 485 | 486 | Wrapper for plt.contour 487 | https://matplotlib.org/3.1.0/api/_as_gen/matplotlib.pyplot.contour.html 488 | 489 | Note: columns and index must be numerical 490 | 491 | df: DataFrame 492 | options: passed to plt.contour 493 | """ 494 | fontsize = options.pop("fontsize", 12) 495 | underride(options, cmap="viridis") 496 | x = df.columns 497 | y = df.index 498 | X, Y = np.meshgrid(x, y) 499 | cs = plt.contour(X, Y, df, **options) 500 | plt.clabel(cs, inline=1, fontsize=fontsize) 501 | 502 | 503 | def savefig(filename, **options): 504 | """Save the current figure. 505 | 506 | Keyword arguments are passed along to plt.savefig 507 | 508 | https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html 509 | 510 | filename: string 511 | """ 512 | print("Saving figure to file", filename) 513 | plt.savefig(filename, **options) 514 | 515 | 516 | def decorate(**options): 517 | """Decorate the current axes. 518 | 519 | Call decorate with keyword arguments like 520 | decorate(title='Title', 521 | xlabel='x', 522 | ylabel='y') 523 | 524 | The keyword arguments can be any of the axis properties 525 | https://matplotlib.org/api/axes_api.html 526 | """ 527 | ax = plt.gca() 528 | ax.set(**options) 529 | 530 | handles, labels = ax.get_legend_handles_labels() 531 | if handles: 532 | ax.legend(handles, labels) 533 | 534 | plt.tight_layout() 535 | 536 | 537 | def remove_from_legend(bad_labels): 538 | """Removes some labels from the legend. 539 | 540 | bad_labels: sequence of strings 541 | """ 542 | ax = plt.gca() 543 | handles, labels = ax.get_legend_handles_labels() 544 | handle_list, label_list = [], [] 545 | for handle, label in zip(handles, labels): 546 | if label not in bad_labels: 547 | handle_list.append(handle) 548 | label_list.append(label) 549 | ax.legend(handle_list, label_list) 550 | 551 | 552 | class SettableNamespace(SimpleNamespace): 553 | """Contains a collection of parameters. 554 | 555 | Used to make a System object. 556 | 557 | Takes keyword arguments and stores them as attributes. 558 | """ 559 | def __init__(self, namespace=None, **kwargs): 560 | super().__init__() 561 | if namespace: 562 | self.__dict__.update(namespace.__dict__) 563 | self.__dict__.update(kwargs) 564 | 565 | def get(self, name, default=None): 566 | """Look up a variable. 567 | 568 | name: string varname 569 | default: value returned if `name` is not present 570 | """ 571 | try: 572 | return self.__getattribute__(name, default) 573 | except AttributeError: 574 | return default 575 | 576 | def set(self, **variables): 577 | """Make a copy and update the given variables. 578 | 579 | returns: Params 580 | """ 581 | new = copy(self) 582 | new.__dict__.update(variables) 583 | return new 584 | 585 | 586 | def magnitude(x): 587 | """Returns the magnitude of a Quantity or number. 588 | 589 | x: Quantity or number 590 | 591 | returns: number 592 | """ 593 | return x.magnitude if hasattr(x, 'magnitude') else x 594 | 595 | 596 | def remove_units(namespace): 597 | """Removes units from the values in a Namespace. 598 | 599 | Only removes units from top-level values; 600 | does not traverse nested values. 601 | 602 | returns: new Namespace object 603 | """ 604 | res = copy(namespace) 605 | for label, value in res.__dict__.items(): 606 | if isinstance(value, pd.Series): 607 | value = remove_units_series(value) 608 | res.__dict__[label] = magnitude(value) 609 | return res 610 | 611 | 612 | def remove_units_series(series): 613 | """Removes units from the values in a Series. 614 | 615 | Only removes units from top-level values; 616 | does not traverse nested values. 617 | 618 | returns: new Series object 619 | """ 620 | res = copy(series) 621 | for label, value in res.iteritems(): 622 | res[label] = magnitude(value) 623 | return res 624 | 625 | 626 | class System(SettableNamespace): 627 | """Contains system parameters and their values. 628 | 629 | Takes keyword arguments and stores them as attributes. 630 | """ 631 | pass 632 | 633 | 634 | class Params(SettableNamespace): 635 | """Contains system parameters and their values. 636 | 637 | Takes keyword arguments and stores them as attributes. 638 | """ 639 | pass 640 | 641 | 642 | def State(**variables): 643 | """Contains the values of state variables.""" 644 | return pd.Series(variables, name='state') 645 | 646 | 647 | def make_series(x, y, **options): 648 | """Make a Pandas Series. 649 | 650 | x: sequence used as the index 651 | y: sequence used as the values 652 | 653 | returns: Pandas Series 654 | """ 655 | if isinstance(y, pd.Series): 656 | y = y.values 657 | return pd.Series(y, index=x, **options) 658 | 659 | 660 | def TimeSeries(*args, **kwargs): 661 | """ 662 | """ 663 | if args or kwargs: 664 | series = pd.Series(*args, **kwargs) 665 | else: 666 | series = pd.Series([], dtype=np.float64) 667 | 668 | series.index.name = 'Time' 669 | if 'name' not in kwargs: 670 | series.name = 'Quantity' 671 | return series 672 | 673 | 674 | def SweepSeries(*args, **kwargs): 675 | """ 676 | """ 677 | if args or kwargs: 678 | series = pd.Series(*args, **kwargs) 679 | else: 680 | series = pd.Series([], dtype=np.float64) 681 | 682 | series.index.name = 'Parameter' 683 | if 'name' not in kwargs: 684 | series.name = 'Metric' 685 | return series 686 | 687 | 688 | def show(obj): 689 | """Display a Series or Namespace as a DataFrame.""" 690 | if isinstance(obj, pd.Series): 691 | return pd.DataFrame(obj) 692 | elif isinstance(obj, SimpleNamespace): 693 | return pd.DataFrame(pd.Series(obj.__dict__), 694 | columns=['value']) 695 | else: 696 | return obj 697 | 698 | 699 | def TimeFrame(*args, **kwargs): 700 | """DataFrame that maps from time to State. 701 | """ 702 | underride(kwargs, dtype=float) 703 | return pd.DataFrame(*args, **kwargs) 704 | 705 | 706 | def SweepFrame(*args, **kwargs): 707 | """DataFrame that maps from parameter value to SweepSeries. 708 | """ 709 | underride(kwargs, dtype=float) 710 | return pd.DataFrame(*args, **kwargs) 711 | 712 | 713 | def Vector(x, y, z=None, **options): 714 | """ 715 | """ 716 | if z is None: 717 | return pd.Series(dict(x=x, y=y), **options) 718 | else: 719 | return pd.Series(dict(x=x, y=y, z=z), **options) 720 | 721 | 722 | ## Vector functions (should work with any sequence) 723 | 724 | def vector_mag(v): 725 | """Vector magnitude.""" 726 | return np.sqrt(np.dot(v, v)) 727 | 728 | 729 | def vector_mag2(v): 730 | """Vector magnitude squared.""" 731 | return np.dot(v, v) 732 | 733 | 734 | def vector_angle(v): 735 | """Angle between v and the positive x axis. 736 | 737 | Only works with 2-D vectors. 738 | 739 | returns: angle in radians 740 | """ 741 | assert len(v) == 2 742 | x, y = v 743 | return np.arctan2(y, x) 744 | 745 | 746 | def vector_polar(v): 747 | """Vector magnitude and angle. 748 | 749 | returns: (number, angle in radians) 750 | """ 751 | return vector_mag(v), vector_angle(v) 752 | 753 | 754 | def vector_hat(v): 755 | """Unit vector in the direction of v. 756 | 757 | returns: Vector or array 758 | """ 759 | # check if the magnitude of the Quantity is 0 760 | mag = vector_mag(v) 761 | if mag == 0: 762 | return v 763 | else: 764 | return v / mag 765 | 766 | 767 | def vector_perp(v): 768 | """Perpendicular Vector (rotated left). 769 | 770 | Only works with 2-D Vectors. 771 | 772 | returns: Vector 773 | """ 774 | assert len(v) == 2 775 | x, y = v 776 | return Vector(-y, x) 777 | 778 | 779 | def vector_dot(v, w): 780 | """Dot product of v and w. 781 | 782 | returns: number or Quantity 783 | """ 784 | return np.dot(v, w) 785 | 786 | 787 | def vector_cross(v, w): 788 | """Cross product of v and w. 789 | 790 | returns: number or Quantity for 2-D, Vector for 3-D 791 | """ 792 | res = np.cross(v, w) 793 | 794 | if len(v) == 3: 795 | return Vector(*res) 796 | else: 797 | return res 798 | 799 | 800 | def vector_proj(v, w): 801 | """Projection of v onto w. 802 | 803 | returns: array or Vector with direction of w and units of v. 804 | """ 805 | w_hat = vector_hat(w) 806 | return vector_dot(v, w_hat) * w_hat 807 | 808 | 809 | def scalar_proj(v, w): 810 | """Returns the scalar projection of v onto w. 811 | 812 | Which is the magnitude of the projection of v onto w. 813 | 814 | returns: scalar with units of v. 815 | """ 816 | return vector_dot(v, vector_hat(w)) 817 | 818 | 819 | def vector_dist(v, w): 820 | """Euclidean distance from v to w, with units.""" 821 | if isinstance(v, list): 822 | v = np.asarray(v) 823 | return vector_mag(v - w) 824 | 825 | 826 | def vector_diff_angle(v, w): 827 | """Angular difference between two vectors, in radians. 828 | """ 829 | if len(v) == 2: 830 | return vector_angle(v) - vector_angle(w) 831 | else: 832 | # TODO: see http://www.euclideanspace.com/maths/algebra/ 833 | # vectors/angleBetween/ 834 | raise NotImplementedError() 835 | 836 | 837 | def plot_segment(A, B, **options): 838 | """Plots a line segment between two Vectors. 839 | 840 | For 3-D vectors, the z axis is ignored. 841 | 842 | Additional options are passed along to plot(). 843 | 844 | A: Vector 845 | B: Vector 846 | """ 847 | xs = A.x, B.x 848 | ys = A.y, B.y 849 | plt.plot(xs, ys, **options) 850 | 851 | 852 | from time import sleep 853 | from IPython.display import clear_output 854 | 855 | def animate(results, draw_func, *args, interval=None): 856 | """Animate results from a simulation. 857 | 858 | results: TimeFrame 859 | draw_func: function that draws state 860 | interval: time between frames in seconds 861 | """ 862 | plt.figure() 863 | try: 864 | for t, state in results.iterrows(): 865 | draw_func(t, state, *args) 866 | plt.show() 867 | if interval: 868 | sleep(interval) 869 | clear_output(wait=True) 870 | draw_func(t, state, *args) 871 | plt.show() 872 | except KeyboardInterrupt: 873 | pass 874 | -------------------------------------------------------------------------------- /python/environment.yml: -------------------------------------------------------------------------------- 1 | name: ModSim 2 | channels: 3 | - conda-forge 4 | - defaults 5 | dependencies: 6 | - python>=3.7 7 | - jupyter 8 | - numpy 9 | - matplotlib 10 | - seaborn 11 | - pandas 12 | - scipy 13 | - pint 14 | - sympy 15 | - lxml 16 | - html5lib 17 | - beautifulsoup4 18 | - pytables 19 | - yapf 20 | - pip 21 | -------------------------------------------------------------------------------- /python/soln/chap11.py: -------------------------------------------------------------------------------- 1 | def make_system(beta, gamma): 2 | init = State(S=89, I=1, R=0) 3 | init /= sum(init) 4 | 5 | t0 = 0 6 | t_end = 7 * 14 7 | 8 | return System(init=init, t0=t0, t_end=t_end, 9 | beta=beta, gamma=gamma) 10 | 11 | def update_func(state, t, system): 12 | s, i, r = state 13 | 14 | infected = system.beta * i * s 15 | recovered = system.gamma * i 16 | 17 | s -= infected 18 | i += infected - recovered 19 | r += recovered 20 | 21 | return State(S=s, I=i, R=r) 22 | 23 | from numpy import arange 24 | 25 | def run_simulation(system, update_func): 26 | state = system.init 27 | 28 | for t in arange(system.t0, system.t_end): 29 | state = update_func(state, t, system) 30 | 31 | return state 32 | 33 | def plot_results(S, I, R): 34 | S.plot(style='--', label='Susceptible') 35 | I.plot(style='-', label='Infected') 36 | R.plot(style=':', label='Resistant') 37 | decorate(xlabel='Time (days)', 38 | ylabel='Fraction of population') 39 | 40 | def run_simulation(system, update_func): 41 | frame = TimeFrame(columns=system.init.index) 42 | frame.loc[system.t0] = system.init 43 | 44 | for t in arange(system.t0, system.t_end): 45 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 46 | 47 | return frame 48 | 49 | from modsim import * 50 | 51 | def make_system(beta, gamma): 52 | init = State(S=89, I=1, R=0) 53 | init /= sum(init) 54 | 55 | t0 = 0 56 | t_end = 7 * 14 57 | 58 | return System(init=init, t0=t0, t_end=t_end, 59 | beta=beta, gamma=gamma) 60 | 61 | from modsim import * 62 | 63 | def update_func(state, t, system): 64 | s, i, r = state 65 | 66 | infected = system.beta * i * s 67 | recovered = system.gamma * i 68 | 69 | s -= infected 70 | i += infected - recovered 71 | r += recovered 72 | 73 | return State(S=s, I=i, R=r) 74 | 75 | from modsim import * 76 | 77 | from numpy import arange 78 | 79 | def run_simulation(system, update_func): 80 | state = system.init 81 | 82 | for t in arange(system.t0, system.t_end): 83 | state = update_func(state, t, system) 84 | 85 | return state 86 | 87 | from modsim import * 88 | 89 | def plot_results(S, I, R): 90 | S.plot(style='--', label='Susceptible') 91 | I.plot(style='-', label='Infected') 92 | R.plot(style=':', label='Resistant') 93 | decorate(xlabel='Time (days)', 94 | ylabel='Fraction of population') 95 | 96 | from modsim import * 97 | 98 | def run_simulation(system, update_func): 99 | frame = TimeFrame(columns=system.init.index) 100 | frame.loc[system.t0] = system.init 101 | 102 | for t in arange(system.t0, system.t_end): 103 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 104 | 105 | return frame 106 | 107 | from modsim import * 108 | 109 | def make_system(beta, gamma): 110 | init = State(S=89, I=1, R=0) 111 | init /= sum(init) 112 | 113 | t0 = 0 114 | t_end = 7 * 14 115 | 116 | return System(init=init, t0=t0, t_end=t_end, 117 | beta=beta, gamma=gamma) 118 | 119 | from modsim import * 120 | 121 | def update_func(state, t, system): 122 | s, i, r = state 123 | 124 | infected = system.beta * i * s 125 | recovered = system.gamma * i 126 | 127 | s -= infected 128 | i += infected - recovered 129 | r += recovered 130 | 131 | return State(S=s, I=i, R=r) 132 | 133 | from modsim import * 134 | 135 | from numpy import arange 136 | 137 | def run_simulation(system, update_func): 138 | state = system.init 139 | 140 | for t in arange(system.t0, system.t_end): 141 | state = update_func(state, t, system) 142 | 143 | return state 144 | 145 | from modsim import * 146 | 147 | def plot_results(S, I, R): 148 | S.plot(style='--', label='Susceptible') 149 | I.plot(style='-', label='Infected') 150 | R.plot(style=':', label='Resistant') 151 | decorate(xlabel='Time (days)', 152 | ylabel='Fraction of population') 153 | 154 | from modsim import * 155 | 156 | def run_simulation(system, update_func): 157 | frame = TimeFrame(columns=system.init.index) 158 | frame.loc[system.t0] = system.init 159 | 160 | for t in arange(system.t0, system.t_end): 161 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 162 | 163 | return frame 164 | 165 | from modsim import * 166 | 167 | def make_system(beta, gamma): 168 | init = State(S=89, I=1, R=0) 169 | init /= sum(init) 170 | 171 | t0 = 0 172 | t_end = 7 * 14 173 | 174 | return System(init=init, t0=t0, t_end=t_end, 175 | beta=beta, gamma=gamma) 176 | 177 | from modsim import * 178 | 179 | def update_func(state, t, system): 180 | s, i, r = state 181 | 182 | infected = system.beta * i * s 183 | recovered = system.gamma * i 184 | 185 | s -= infected 186 | i += infected - recovered 187 | r += recovered 188 | 189 | return State(S=s, I=i, R=r) 190 | 191 | from modsim import * 192 | 193 | from numpy import arange 194 | 195 | def run_simulation(system, update_func): 196 | state = system.init 197 | 198 | for t in arange(system.t0, system.t_end): 199 | state = update_func(state, t, system) 200 | 201 | return state 202 | 203 | from modsim import * 204 | 205 | def plot_results(S, I, R): 206 | S.plot(style='--', label='Susceptible') 207 | I.plot(style='-', label='Infected') 208 | R.plot(style=':', label='Resistant') 209 | decorate(xlabel='Time (days)', 210 | ylabel='Fraction of population') 211 | 212 | from modsim import * 213 | 214 | def run_simulation(system, update_func): 215 | frame = TimeFrame(columns=system.init.index) 216 | frame.loc[system.t0] = system.init 217 | 218 | for t in arange(system.t0, system.t_end): 219 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 220 | 221 | return frame 222 | 223 | from modsim import * 224 | 225 | def make_system(beta, gamma): 226 | init = State(S=89, I=1, R=0) 227 | init /= sum(init) 228 | 229 | t0 = 0 230 | t_end = 7 * 14 231 | 232 | return System(init=init, t0=t0, t_end=t_end, 233 | beta=beta, gamma=gamma) 234 | 235 | from modsim import * 236 | 237 | def update_func(state, t, system): 238 | s, i, r = state 239 | 240 | infected = system.beta * i * s 241 | recovered = system.gamma * i 242 | 243 | s -= infected 244 | i += infected - recovered 245 | r += recovered 246 | 247 | return State(S=s, I=i, R=r) 248 | 249 | from modsim import * 250 | 251 | from numpy import arange 252 | 253 | def run_simulation(system, update_func): 254 | state = system.init 255 | 256 | for t in arange(system.t0, system.t_end): 257 | state = update_func(state, t, system) 258 | 259 | return state 260 | 261 | from modsim import * 262 | 263 | def plot_results(S, I, R): 264 | S.plot(style='--', label='Susceptible') 265 | I.plot(style='-', label='Infected') 266 | R.plot(style=':', label='Resistant') 267 | decorate(xlabel='Time (days)', 268 | ylabel='Fraction of population') 269 | 270 | from modsim import * 271 | 272 | def run_simulation(system, update_func): 273 | frame = TimeFrame(columns=system.init.index) 274 | frame.loc[system.t0] = system.init 275 | 276 | for t in arange(system.t0, system.t_end): 277 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 278 | 279 | return frame 280 | 281 | from modsim import * 282 | 283 | def make_system(beta, gamma): 284 | init = State(S=89, I=1, R=0) 285 | init /= sum(init) 286 | 287 | t0 = 0 288 | t_end = 7 * 14 289 | 290 | return System(init=init, t0=t0, t_end=t_end, 291 | beta=beta, gamma=gamma) 292 | 293 | from modsim import * 294 | 295 | def update_func(state, t, system): 296 | s, i, r = state 297 | 298 | infected = system.beta * i * s 299 | recovered = system.gamma * i 300 | 301 | s -= infected 302 | i += infected - recovered 303 | r += recovered 304 | 305 | return State(S=s, I=i, R=r) 306 | 307 | from modsim import * 308 | 309 | from numpy import arange 310 | 311 | def run_simulation(system, update_func): 312 | state = system.init 313 | 314 | for t in arange(system.t0, system.t_end): 315 | state = update_func(state, t, system) 316 | 317 | return state 318 | 319 | from modsim import * 320 | 321 | def plot_results(S, I, R): 322 | S.plot(style='--', label='Susceptible') 323 | I.plot(style='-', label='Infected') 324 | R.plot(style=':', label='Resistant') 325 | decorate(xlabel='Time (days)', 326 | ylabel='Fraction of population') 327 | 328 | from modsim import * 329 | 330 | def run_simulation(system, update_func): 331 | frame = TimeFrame(columns=system.init.index) 332 | frame.loc[system.t0] = system.init 333 | 334 | for t in arange(system.t0, system.t_end): 335 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 336 | 337 | return frame 338 | 339 | from modsim import * 340 | 341 | def make_system(beta, gamma): 342 | init = State(S=89, I=1, R=0) 343 | init /= sum(init) 344 | 345 | t0 = 0 346 | t_end = 7 * 14 347 | 348 | return System(init=init, t0=t0, t_end=t_end, 349 | beta=beta, gamma=gamma) 350 | 351 | from modsim import * 352 | 353 | def update_func(state, t, system): 354 | s, i, r = state 355 | 356 | infected = system.beta * i * s 357 | recovered = system.gamma * i 358 | 359 | s -= infected 360 | i += infected - recovered 361 | r += recovered 362 | 363 | return State(S=s, I=i, R=r) 364 | 365 | from modsim import * 366 | 367 | from numpy import arange 368 | 369 | def run_simulation(system, update_func): 370 | state = system.init 371 | 372 | for t in arange(system.t0, system.t_end): 373 | state = update_func(state, t, system) 374 | 375 | return state 376 | 377 | from modsim import * 378 | 379 | def plot_results(S, I, R): 380 | S.plot(style='--', label='Susceptible') 381 | I.plot(style='-', label='Infected') 382 | R.plot(style=':', label='Resistant') 383 | decorate(xlabel='Time (days)', 384 | ylabel='Fraction of population') 385 | 386 | from modsim import * 387 | 388 | def run_simulation(system, update_func): 389 | frame = TimeFrame(columns=system.init.index) 390 | frame.loc[system.t0] = system.init 391 | 392 | for t in arange(system.t0, system.t_end): 393 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 394 | 395 | return frame 396 | 397 | from modsim import * 398 | 399 | def make_system(beta, gamma): 400 | init = State(S=89, I=1, R=0) 401 | init /= sum(init) 402 | 403 | t0 = 0 404 | t_end = 7 * 14 405 | 406 | return System(init=init, t0=t0, t_end=t_end, 407 | beta=beta, gamma=gamma) 408 | 409 | from modsim import * 410 | 411 | def update_func(state, t, system): 412 | s, i, r = state 413 | 414 | infected = system.beta * i * s 415 | recovered = system.gamma * i 416 | 417 | s -= infected 418 | i += infected - recovered 419 | r += recovered 420 | 421 | return State(S=s, I=i, R=r) 422 | 423 | from modsim import * 424 | 425 | from numpy import arange 426 | 427 | def run_simulation(system, update_func): 428 | state = system.init 429 | 430 | for t in arange(system.t0, system.t_end): 431 | state = update_func(state, t, system) 432 | 433 | return state 434 | 435 | from modsim import * 436 | 437 | def plot_results(S, I, R): 438 | S.plot(style='--', label='Susceptible') 439 | I.plot(style='-', label='Infected') 440 | R.plot(style=':', label='Resistant') 441 | decorate(xlabel='Time (days)', 442 | ylabel='Fraction of population') 443 | 444 | from modsim import * 445 | 446 | def run_simulation(system, update_func): 447 | frame = TimeFrame(columns=system.init.index) 448 | frame.loc[system.t0] = system.init 449 | 450 | for t in arange(system.t0, system.t_end): 451 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 452 | 453 | return frame 454 | 455 | from modsim import * 456 | 457 | def make_system(beta, gamma): 458 | init = State(S=89, I=1, R=0) 459 | init /= sum(init) 460 | 461 | t0 = 0 462 | t_end = 7 * 14 463 | 464 | return System(init=init, t0=t0, t_end=t_end, 465 | beta=beta, gamma=gamma) 466 | 467 | from modsim import * 468 | 469 | def update_func(state, t, system): 470 | s, i, r = state 471 | 472 | infected = system.beta * i * s 473 | recovered = system.gamma * i 474 | 475 | s -= infected 476 | i += infected - recovered 477 | r += recovered 478 | 479 | return State(S=s, I=i, R=r) 480 | 481 | from modsim import * 482 | 483 | from numpy import arange 484 | 485 | def run_simulation(system, update_func): 486 | state = system.init 487 | 488 | for t in arange(system.t0, system.t_end): 489 | state = update_func(state, t, system) 490 | 491 | return state 492 | 493 | from modsim import * 494 | 495 | def plot_results(S, I, R): 496 | S.plot(style='--', label='Susceptible') 497 | I.plot(style='-', label='Infected') 498 | R.plot(style=':', label='Resistant') 499 | decorate(xlabel='Time (days)', 500 | ylabel='Fraction of population') 501 | 502 | from modsim import * 503 | 504 | def run_simulation(system, update_func): 505 | frame = TimeFrame(columns=system.init.index) 506 | frame.loc[system.t0] = system.init 507 | 508 | for t in arange(system.t0, system.t_end): 509 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 510 | 511 | return frame 512 | 513 | from modsim import * 514 | 515 | def make_system(beta, gamma): 516 | init = State(S=89, I=1, R=0) 517 | init /= sum(init) 518 | 519 | t0 = 0 520 | t_end = 7 * 14 521 | 522 | return System(init=init, t0=t0, t_end=t_end, 523 | beta=beta, gamma=gamma) 524 | 525 | from modsim import * 526 | 527 | def update_func(state, t, system): 528 | s, i, r = state 529 | 530 | infected = system.beta * i * s 531 | recovered = system.gamma * i 532 | 533 | s -= infected 534 | i += infected - recovered 535 | r += recovered 536 | 537 | return State(S=s, I=i, R=r) 538 | 539 | from modsim import * 540 | 541 | from numpy import arange 542 | 543 | def run_simulation(system, update_func): 544 | state = system.init 545 | 546 | for t in arange(system.t0, system.t_end): 547 | state = update_func(state, t, system) 548 | 549 | return state 550 | 551 | from modsim import * 552 | 553 | def plot_results(S, I, R): 554 | S.plot(style='--', label='Susceptible') 555 | I.plot(style='-', label='Infected') 556 | R.plot(style=':', label='Resistant') 557 | decorate(xlabel='Time (days)', 558 | ylabel='Fraction of population') 559 | 560 | from modsim import * 561 | 562 | def run_simulation(system, update_func): 563 | frame = TimeFrame(columns=system.init.index) 564 | frame.loc[system.t0] = system.init 565 | 566 | for t in arange(system.t0, system.t_end): 567 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 568 | 569 | return frame 570 | 571 | from modsim import * 572 | 573 | def make_system(beta, gamma): 574 | init = State(S=89, I=1, R=0) 575 | init /= sum(init) 576 | 577 | t0 = 0 578 | t_end = 7 * 14 579 | 580 | return System(init=init, t0=t0, t_end=t_end, 581 | beta=beta, gamma=gamma) 582 | 583 | from modsim import * 584 | 585 | def update_func(state, t, system): 586 | s, i, r = state 587 | 588 | infected = system.beta * i * s 589 | recovered = system.gamma * i 590 | 591 | s -= infected 592 | i += infected - recovered 593 | r += recovered 594 | 595 | return State(S=s, I=i, R=r) 596 | 597 | from modsim import * 598 | 599 | from numpy import arange 600 | 601 | def run_simulation(system, update_func): 602 | state = system.init 603 | 604 | for t in arange(system.t0, system.t_end): 605 | state = update_func(state, t, system) 606 | 607 | return state 608 | 609 | from modsim import * 610 | 611 | def plot_results(S, I, R): 612 | S.plot(style='--', label='Susceptible') 613 | I.plot(style='-', label='Infected') 614 | R.plot(style=':', label='Resistant') 615 | decorate(xlabel='Time (days)', 616 | ylabel='Fraction of population') 617 | 618 | from modsim import * 619 | 620 | def run_simulation(system, update_func): 621 | frame = TimeFrame(columns=system.init.index) 622 | frame.loc[system.t0] = system.init 623 | 624 | for t in arange(system.t0, system.t_end): 625 | frame.loc[t+1] = update_func(frame.loc[t], t, system) 626 | 627 | return frame 628 | 629 | -------------------------------------------------------------------------------- /python/soln/chap12.py: -------------------------------------------------------------------------------- 1 | from modsim import * 2 | 3 | def calc_total_infected(results, system): 4 | s_0 = results.S[system.t0] 5 | s_end = results.S[system.t_end] 6 | return s_0 - s_end 7 | 8 | from modsim import * 9 | 10 | def calc_total_infected(results, system): 11 | s_0 = results.S[system.t0] 12 | s_end = results.S[system.t_end] 13 | return s_0 - s_end 14 | 15 | from modsim import * 16 | 17 | def calc_total_infected(results, system): 18 | s_0 = results.S[system.t0] 19 | s_end = results.S[system.t_end] 20 | return s_0 - s_end 21 | 22 | from modsim import * 23 | 24 | def calc_total_infected(results, system): 25 | s_0 = results.S[system.t0] 26 | s_end = results.S[system.t_end] 27 | return s_0 - s_end 28 | 29 | from modsim import * 30 | 31 | def calc_total_infected(results, system): 32 | s_0 = results.S[system.t0] 33 | s_end = results.S[system.t_end] 34 | return s_0 - s_end 35 | 36 | from modsim import * 37 | 38 | def calc_total_infected(results, system): 39 | s_0 = results.S[system.t0] 40 | s_end = results.S[system.t_end] 41 | return s_0 - s_end 42 | 43 | from modsim import * 44 | 45 | def calc_total_infected(results, system): 46 | s_0 = results.S[system.t0] 47 | s_end = results.S[system.t_end] 48 | return s_0 - s_end 49 | 50 | from modsim import * 51 | 52 | def calc_total_infected(results, system): 53 | s_0 = results.S[system.t0] 54 | s_end = results.S[system.t_end] 55 | return s_0 - s_end 56 | 57 | -------------------------------------------------------------------------------- /python/soln/chap13.py: -------------------------------------------------------------------------------- 1 | from modsim import * 2 | 3 | def sweep_beta(beta_array, gamma): 4 | sweep = SweepSeries() 5 | for beta in beta_array: 6 | system = make_system(beta, gamma) 7 | results = run_simulation(system, update_func) 8 | sweep[beta] = calc_total_infected(results, system) 9 | return sweep 10 | 11 | from modsim import * 12 | 13 | def sweep_parameters(beta_array, gamma_array): 14 | frame = SweepFrame(columns=gamma_array) 15 | for gamma in gamma_array: 16 | frame[gamma] = sweep_beta(beta_array, gamma) 17 | return frame 18 | 19 | from modsim import * 20 | 21 | # import code from previous notebooks 22 | 23 | from chap11 import make_system 24 | from chap11 import update_func 25 | from chap11 import run_simulation 26 | from chap12 import calc_total_infected 27 | 28 | from modsim import * 29 | 30 | def sweep_beta(beta_array, gamma): 31 | sweep = SweepSeries() 32 | for beta in beta_array: 33 | system = make_system(beta, gamma) 34 | results = run_simulation(system, update_func) 35 | sweep[beta] = calc_total_infected(results, system) 36 | return sweep 37 | 38 | from modsim import * 39 | 40 | def sweep_parameters(beta_array, gamma_array): 41 | frame = SweepFrame(columns=gamma_array) 42 | for gamma in gamma_array: 43 | frame[gamma] = sweep_beta(beta_array, gamma) 44 | return frame 45 | 46 | from modsim import * 47 | 48 | # import code from previous notebooks 49 | 50 | from chap11 import make_system 51 | from chap11 import update_func 52 | from chap11 import run_simulation 53 | from chap12 import calc_total_infected 54 | 55 | from modsim import * 56 | 57 | def sweep_beta(beta_array, gamma): 58 | sweep = SweepSeries() 59 | for beta in beta_array: 60 | system = make_system(beta, gamma) 61 | results = run_simulation(system, update_func) 62 | sweep[beta] = calc_total_infected(results, system) 63 | return sweep 64 | 65 | from modsim import * 66 | 67 | def sweep_parameters(beta_array, gamma_array): 68 | frame = SweepFrame(columns=gamma_array) 69 | for gamma in gamma_array: 70 | frame[gamma] = sweep_beta(beta_array, gamma) 71 | return frame 72 | 73 | from modsim import * 74 | 75 | # import code from previous notebooks 76 | 77 | from chap11 import make_system 78 | from chap11 import update_func 79 | from chap11 import run_simulation 80 | from chap12 import calc_total_infected 81 | 82 | from modsim import * 83 | 84 | def sweep_beta(beta_array, gamma): 85 | sweep = SweepSeries() 86 | for beta in beta_array: 87 | system = make_system(beta, gamma) 88 | results = run_simulation(system, update_func) 89 | sweep[beta] = calc_total_infected(results, system) 90 | return sweep 91 | 92 | from modsim import * 93 | 94 | def sweep_parameters(beta_array, gamma_array): 95 | frame = SweepFrame(columns=gamma_array) 96 | for gamma in gamma_array: 97 | frame[gamma] = sweep_beta(beta_array, gamma) 98 | return frame 99 | 100 | from modsim import * 101 | 102 | # import code from previous notebooks 103 | 104 | from chap11 import make_system 105 | from chap11 import update_func 106 | from chap11 import run_simulation 107 | from chap12 import calc_total_infected 108 | 109 | from modsim import * 110 | 111 | def sweep_beta(beta_array, gamma): 112 | sweep = SweepSeries() 113 | for beta in beta_array: 114 | system = make_system(beta, gamma) 115 | results = run_simulation(system, update_func) 116 | sweep[beta] = calc_total_infected(results, system) 117 | return sweep 118 | 119 | from modsim import * 120 | 121 | def sweep_parameters(beta_array, gamma_array): 122 | frame = SweepFrame(columns=gamma_array) 123 | for gamma in gamma_array: 124 | frame[gamma] = sweep_beta(beta_array, gamma) 125 | return frame 126 | 127 | from modsim import * 128 | 129 | # import code from previous notebooks 130 | 131 | from chap11 import make_system 132 | from chap11 import update_func 133 | from chap11 import run_simulation 134 | from chap12 import calc_total_infected 135 | 136 | from modsim import * 137 | 138 | def sweep_beta(beta_array, gamma): 139 | sweep = SweepSeries() 140 | for beta in beta_array: 141 | system = make_system(beta, gamma) 142 | results = run_simulation(system, update_func) 143 | sweep[beta] = calc_total_infected(results, system) 144 | return sweep 145 | 146 | from modsim import * 147 | 148 | def sweep_parameters(beta_array, gamma_array): 149 | frame = SweepFrame(columns=gamma_array) 150 | for gamma in gamma_array: 151 | frame[gamma] = sweep_beta(beta_array, gamma) 152 | return frame 153 | 154 | from modsim import * 155 | 156 | # import code from previous notebooks 157 | 158 | from chap11 import make_system 159 | from chap11 import update_func 160 | from chap11 import run_simulation 161 | from chap12 import calc_total_infected 162 | 163 | from modsim import * 164 | 165 | def sweep_beta(beta_array, gamma): 166 | sweep = SweepSeries() 167 | for beta in beta_array: 168 | system = make_system(beta, gamma) 169 | results = run_simulation(system, update_func) 170 | sweep[beta] = calc_total_infected(results, system) 171 | return sweep 172 | 173 | from modsim import * 174 | 175 | def sweep_parameters(beta_array, gamma_array): 176 | frame = SweepFrame(columns=gamma_array) 177 | for gamma in gamma_array: 178 | frame[gamma] = sweep_beta(beta_array, gamma) 179 | return frame 180 | 181 | -------------------------------------------------------------------------------- /python/soln/chap15.py: -------------------------------------------------------------------------------- 1 | from modsim import * 2 | 3 | def make_system(T_init, volume, r, t_end): 4 | return System(T_init=T_init, 5 | T_final=T_init, 6 | volume=volume, 7 | r=r, 8 | t_end=t_end, 9 | T_env=22, 10 | t_0=0, 11 | dt=1) 12 | 13 | from modsim import * 14 | 15 | def change_func(T, t, system): 16 | r, T_env, dt = system.r, system.T_env, system.dt 17 | return -r * (T - T_env) * dt 18 | 19 | from modsim import * 20 | 21 | def run_simulation(system, change_func): 22 | t_array = linrange(system.t_0, system.t_end, system.dt) 23 | n = len(t_array) 24 | 25 | series = TimeSeries(index=t_array) 26 | series.iloc[0] = system.T_init 27 | 28 | for i in range(n-1): 29 | t = t_array[i] 30 | T = series.iloc[i] 31 | series.iloc[i+1] = T + change_func(T, t, system) 32 | 33 | system.t_end = t_array[-1] 34 | system.T_final = series.iloc[-1] 35 | return series 36 | 37 | from modsim import * 38 | 39 | def make_system(T_init, volume, r, t_end): 40 | return System(T_init=T_init, 41 | T_final=T_init, 42 | volume=volume, 43 | r=r, 44 | t_end=t_end, 45 | T_env=22, 46 | t_0=0, 47 | dt=1) 48 | 49 | from modsim import * 50 | 51 | def change_func(T, t, system): 52 | r, T_env, dt = system.r, system.T_env, system.dt 53 | return -r * (T - T_env) * dt 54 | 55 | from modsim import * 56 | 57 | def run_simulation(system, change_func): 58 | t_array = linrange(system.t_0, system.t_end, system.dt) 59 | n = len(t_array) 60 | 61 | series = TimeSeries(index=t_array) 62 | series.iloc[0] = system.T_init 63 | 64 | for i in range(n-1): 65 | t = t_array[i] 66 | T = series.iloc[i] 67 | series.iloc[i+1] = T + change_func(T, t, system) 68 | 69 | system.t_end = t_array[-1] 70 | system.T_final = series.iloc[-1] 71 | return series 72 | 73 | from modsim import * 74 | 75 | def make_system(T_init, volume, r, t_end): 76 | return System(T_init=T_init, 77 | T_final=T_init, 78 | volume=volume, 79 | r=r, 80 | t_end=t_end, 81 | T_env=22, 82 | t_0=0, 83 | dt=1) 84 | 85 | from modsim import * 86 | 87 | def change_func(T, t, system): 88 | r, T_env, dt = system.r, system.T_env, system.dt 89 | return -r * (T - T_env) * dt 90 | 91 | from modsim import * 92 | 93 | def run_simulation(system, change_func): 94 | t_array = linrange(system.t_0, system.t_end, system.dt) 95 | n = len(t_array) 96 | 97 | series = TimeSeries(index=t_array) 98 | series.iloc[0] = system.T_init 99 | 100 | for i in range(n-1): 101 | t = t_array[i] 102 | T = series.iloc[i] 103 | series.iloc[i+1] = T + change_func(T, t, system) 104 | 105 | system.t_end = t_array[-1] 106 | system.T_final = series.iloc[-1] 107 | return series 108 | 109 | from modsim import * 110 | 111 | def make_system(T_init, volume, r, t_end): 112 | return System(T_init=T_init, 113 | T_final=T_init, 114 | volume=volume, 115 | r=r, 116 | t_end=t_end, 117 | T_env=22, 118 | t_0=0, 119 | dt=1) 120 | 121 | from modsim import * 122 | 123 | def change_func(T, t, system): 124 | r, T_env, dt = system.r, system.T_env, system.dt 125 | return -r * (T - T_env) * dt 126 | 127 | from modsim import * 128 | 129 | def run_simulation(system, change_func): 130 | t_array = linrange(system.t_0, system.t_end, system.dt) 131 | n = len(t_array) 132 | 133 | series = TimeSeries(index=t_array) 134 | series.iloc[0] = system.T_init 135 | 136 | for i in range(n-1): 137 | t = t_array[i] 138 | T = series.iloc[i] 139 | series.iloc[i+1] = T + change_func(T, t, system) 140 | 141 | system.t_end = t_array[-1] 142 | system.T_final = series.iloc[-1] 143 | return series 144 | 145 | from modsim import * 146 | 147 | def make_system(T_init, volume, r, t_end): 148 | return System(T_init=T_init, 149 | T_final=T_init, 150 | volume=volume, 151 | r=r, 152 | t_end=t_end, 153 | T_env=22, 154 | t_0=0, 155 | dt=1) 156 | 157 | from modsim import * 158 | 159 | def change_func(T, t, system): 160 | r, T_env, dt = system.r, system.T_env, system.dt 161 | return -r * (T - T_env) * dt 162 | 163 | from modsim import * 164 | 165 | def run_simulation(system, change_func): 166 | t_array = linrange(system.t_0, system.t_end, system.dt) 167 | n = len(t_array) 168 | 169 | series = TimeSeries(index=t_array) 170 | series.iloc[0] = system.T_init 171 | 172 | for i in range(n-1): 173 | t = t_array[i] 174 | T = series.iloc[i] 175 | series.iloc[i+1] = T + change_func(T, t, system) 176 | 177 | system.t_end = t_array[-1] 178 | system.T_final = series.iloc[-1] 179 | return series 180 | 181 | -------------------------------------------------------------------------------- /python/soln/chap18.py: -------------------------------------------------------------------------------- 1 | from modsim import * 2 | 3 | def make_system(params, data): 4 | G0, k1, k2, k3 = params 5 | 6 | t_0 = data.index[0] 7 | t_end = data.index[-1] 8 | 9 | Gb = data.glucose[t_0] 10 | Ib = data.insulin[t_0] 11 | I = interpolate(data.insulin) 12 | 13 | init = State(G=G0, X=0) 14 | 15 | return System(init=init, params=params, 16 | Gb=Gb, Ib=Ib, I=I, 17 | t_0=t_0, t_end=t_end, dt=2) 18 | 19 | from modsim import * 20 | 21 | def make_system(params, data): 22 | G0, k1, k2, k3 = params 23 | 24 | t_0 = data.index[0] 25 | t_end = data.index[-1] 26 | 27 | Gb = data.glucose[t_0] 28 | Ib = data.insulin[t_0] 29 | I = interpolate(data.insulin) 30 | 31 | init = State(G=G0, X=0) 32 | 33 | return System(init=init, params=params, 34 | Gb=Gb, Ib=Ib, I=I, 35 | t_0=t_0, t_end=t_end, dt=2) 36 | 37 | from modsim import * 38 | 39 | def slope_func(t, state, system): 40 | G, X = state 41 | G0, k1, k2, k3 = system.params 42 | I, Ib, Gb = system.I, system.Ib, system.Gb 43 | 44 | dGdt = -k1 * (G - Gb) - X*G 45 | dXdt = k3 * (I(t) - Ib) - k2 * X 46 | 47 | return dGdt, dXdt 48 | 49 | -------------------------------------------------------------------------------- /python/soln/chap19.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "mighty-israeli", 6 | "metadata": {}, 7 | "source": [ 8 | "# Chapter 19" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "passing-solid", 14 | "metadata": {}, 15 | "source": [ 16 | "*Modeling and Simulation in Python*\n", 17 | "\n", 18 | "Copyright 2021 Allen Downey\n", 19 | "\n", 20 | "License: [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/)" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 1, 26 | "id": "broken-procedure", 27 | "metadata": { 28 | "tags": [ 29 | "remove-cell" 30 | ] 31 | }, 32 | "outputs": [], 33 | "source": [ 34 | "# install Pint if necessary\n", 35 | "\n", 36 | "try:\n", 37 | " import pint\n", 38 | "except ImportError:\n", 39 | " !pip install pint" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 2, 45 | "id": "massive-thong", 46 | "metadata": { 47 | "tags": [ 48 | "remove-cell" 49 | ] 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "# download modsim.py if necessary\n", 54 | "\n", 55 | "from os.path import exists\n", 56 | "\n", 57 | "filename = 'modsim.py'\n", 58 | "if not exists(filename):\n", 59 | " from urllib.request import urlretrieve\n", 60 | " url = 'https://raw.githubusercontent.com/AllenDowney/ModSim/main/'\n", 61 | " local, _ = urlretrieve(url+filename, filename)\n", 62 | " print('Downloaded ' + local)" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 3, 68 | "id": "experienced-junction", 69 | "metadata": { 70 | "tags": [ 71 | "remove-cell" 72 | ] 73 | }, 74 | "outputs": [], 75 | "source": [ 76 | "# import functions from modsim\n", 77 | "\n", 78 | "from modsim import *" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "id": "structured-satellite", 84 | "metadata": {}, 85 | "source": [ 86 | "## The glucose minimal model\n", 87 | "\n", 88 | "In the previous chapter we implemented the glucose minimal model using given parameters, but I didn't say where those parameters came from.\n", 89 | "\n", 90 | "In the repository for this book, you will find a notebook,\n", 91 | "`glucose.ipynb`, that shows how we can find the parameters that best fit the data.\n", 92 | "\n", 93 | "It uses a SciPy function called `leastsq`, which stands for \"least squares\"; that is, it finds the parameters that minimize the sum of squared differences between the results of the model and the data.\n", 94 | "\n", 95 | "You can think of `leastsq` as an optional tool for this book. We won't use it in the text itself, but it appears in a few of the case studies.\n" 96 | ] 97 | }, 98 | { 99 | "cell_type": "markdown", 100 | "id": "laden-gathering", 101 | "metadata": {}, 102 | "source": [ 103 | "## The insulin minimal model\n", 104 | "\n", 105 | "Along with the glucose minimal model, Berman et al. developed an insulin minimal model, in which the concentration of insulin, $I$, is governed by this differential equation:\n", 106 | "\n", 107 | "$$\\frac{dI}{dt} = -k I(t) + \\gamma \\left[ G(t) - G_T \\right] t$$ \n", 108 | "\n", 109 | "where\n", 110 | "\n", 111 | "- $k$ is a parameter that controls the rate of insulin disappearance\n", 112 | " independent of blood glucose.\n", 113 | "\n", 114 | "- $G(t)$ is the measured concentration of blood glucose at time $t$.\n", 115 | "\n", 116 | "- $G_T$ is the glucose threshold; when blood glucose is above this\n", 117 | " level, it triggers an increase in blood insulin.\n", 118 | "\n", 119 | "- $\\gamma$ is a parameter that controls the rate of increase (or\n", 120 | " decrease) in blood insulin when glucose is above (or below) $G_T$.\n", 121 | "\n", 122 | "The initial condition is $I(0) = I_0$. As in the glucose minimal model, we treat the initial condition as a parameter which we'll choose to fit the data." 123 | ] 124 | }, 125 | { 126 | "cell_type": "markdown", 127 | "id": "interstate-gibson", 128 | "metadata": {}, 129 | "source": [ 130 | "The parameters of this model can be used to estimate $\\phi_1$ and\n", 131 | "$\\phi_2$, which are quantities that \"describe the sensitivity to glucose of the first and second phase pancreatic responsivity\". These quantities are related to the parameters as follows:\n", 132 | "\n", 133 | "$$\\phi_1 = \\frac{I_{max} - I_b}{k (G_0 - G_b)}$$\n", 134 | "\n", 135 | "$$\\phi_2 = \\gamma \\times 10^4$$ \n", 136 | "\n", 137 | "where $I_{max}$ is the maximum measured insulin level, and $I_b$ and $G_b$ are the basal levels of insulin and glucose.\n", 138 | "\n", 139 | "In the repository for this book, you will find a notebook,\n", 140 | "`insulin.ipynb`, that contains starter code for this case study. Use it to implement the insulin model, find the parameters that best fit the data, and estimate $\\phi_1$ and $\\phi_2$." 141 | ] 142 | }, 143 | { 144 | "cell_type": "markdown", 145 | "id": "parallel-radical", 146 | "metadata": {}, 147 | "source": [ 148 | "## Low-Pass Filter\n", 149 | "\n", 150 | "The following circuit diagram (from ) shows a low-pass filter built with one resistor and one capacitor.\n", 151 | "\n", 152 | "![Circuit diagram of a low-pass filter](https://github.com/AllenDowney/ModSim/raw/main/figs/RC_Divider.svg)\n", 153 | "\n", 154 | "A \"filter\" is a circuit takes a signal, $V_{in}$, as input and produces a signal, $V_{out}$, as output. In this context, a \"signal\" is a voltage that changes over time.\n", 155 | "\n", 156 | "A filter is \"low-pass\" if it allows low-frequency signals to pass from\n", 157 | "$V_{in}$ to $V_{out}$ unchanged, but it reduces the amplitude of\n", 158 | "high-frequency signals.\n", 159 | "\n", 160 | "By applying the laws of circuit analysis, we can derive a differential\n", 161 | "equation that describes the behavior of this system. By solving the\n", 162 | "differential equation, we can predict the effect of this circuit on any input signal.\n", 163 | "\n", 164 | "Suppose we are given $V_{in}$ and $V_{out}$ at a particular instant in\n", 165 | "time. By Ohm's law, which is a simple model of the behavior of\n", 166 | "resistors, the instantaneous current through the resistor is:\n", 167 | "\n", 168 | "$$I_R = (V_{in} - V_{out}) / R$$ \n", 169 | "\n", 170 | "where $R$ is resistance in ohms (Ω).\n", 171 | "\n", 172 | "Assuming that no current flows through the output of the circuit,\n", 173 | "Kirchhoff's current law implies that the current through the capacitor\n", 174 | "is: \n", 175 | "\n", 176 | "$$I_C = I_R$$ \n", 177 | "\n", 178 | "According to a simple model of the behavior of\n", 179 | "capacitors, current through the capacitor causes a change in the voltage across the capacitor: \n", 180 | "\n", 181 | "$$I_C = C \\frac{d V_{out}}{dt}$$ \n", 182 | "\n", 183 | "where $C$ is capacitance in farads (F). Combining these equations yields a differential equation for $V_{out}$:\n", 184 | "\n", 185 | "$$\\frac{d V_{out}}{dt} = \\frac{V_{in} - V_{out}}{R C}$$ \n", 186 | "\n", 187 | "In the repository for this book, you will find a notebook, `filter.ipynb`, which contains starter code for this case study. Follow the instructions to simulate the low-pass filter for input signals like this:\n", 188 | "\n", 189 | "$$V_{in}(t) = A \\cos (2 \\pi f t)$$ \n", 190 | "\n", 191 | "where $A$ is the amplitude of the input signal, say 5 V, and $f$ is the frequency of the signal in Hz.\n", 192 | "\n", 193 | "In the repository for this book, you will find a notebook,\n", 194 | "`filter.ipynb`, which contains starter code for this case study. Read\n", 195 | "the notebook, run the code, and work on the exercises." 196 | ] 197 | }, 198 | { 199 | "cell_type": "markdown", 200 | "id": "violent-directive", 201 | "metadata": {}, 202 | "source": [ 203 | "## Thermal behavior of a wall\n", 204 | "\n", 205 | "This case study is based on a paper by Gori, et al[^2] that models the\n", 206 | "thermal behavior of a brick wall, with the goal of understanding the\n", 207 | "\"performance gap between the expected energy use of buildings and their measured energy use\".\n", 208 | "\n", 209 | "The following figure shows the scenario and their model of the wall:\n", 210 | "\n", 211 | "![Model of a wall as a series of thermal insulators](https://github.com/AllenDowney/ModSim/raw/main/figs/wall_model.png)\n", 212 | "\n", 213 | "On the interior and exterior surfaces of the wall, they measure\n", 214 | "temperature and heat flux over a period of three days. They model the\n", 215 | "wall using two thermal masses connected to the surfaces, and to each\n", 216 | "other, by thermal resistors." 217 | ] 218 | }, 219 | { 220 | "cell_type": "markdown", 221 | "id": "entire-stations", 222 | "metadata": {}, 223 | "source": [ 224 | "The primary methodology of the paper is a Bayesian method for inferring the parameters of the system (two thermal masses and three thermal resistances).\n", 225 | "\n", 226 | "The primary result is a comparison of two models: the one shown here\n", 227 | "with two thermal masses, and a simpler model with only one thermal mass. They find that the two-mass model is able to reproduce the measured fluxes substantially better.\n", 228 | "\n", 229 | "For this case study we will implement their model and run it with the\n", 230 | "estimated parameters from the paper, and then use `leastsq` to see\n", 231 | "if we can find parameters that yield lower errors.\n", 232 | "\n", 233 | "In the repository for this book, you will find a notebook, `wall.ipynb` with the code and results for this case study.\n", 234 | "\n", 235 | "The authors put their paper under a Creative Commons license, and\n", 236 | "make their data available at . I thank them\n", 237 | "for their commitment to open, reproducible science, which made this\n", 238 | "case study possible.\n", 239 | "\n", 240 | "Gori, Marincioni, Biddulph, Elwell, \"Inferring the thermal resistance and effective thermal mass distribution of a wall from in situ measurements to characterise heat transfer at both the interior and exterior surfaces\", *Energy and Buildings*, Volume 135, pages 398-409, ." 241 | ] 242 | }, 243 | { 244 | "cell_type": "markdown", 245 | "id": "narrow-voice", 246 | "metadata": {}, 247 | "source": [ 248 | "## HIV\n", 249 | "\n", 250 | "During the initial phase of HIV infection, the concentration of the virus in the bloodstream typically increases quickly and then decreases.\n", 251 | "The most obvious explanation for the decline is an immune response that destroys the virus or controls its replication.\n", 252 | "However, at least in some patients, the decline occurs even without any detectable immune response.\n", 253 | "\n", 254 | "In 1996 Andrew Phillips proposed another explanation for the decline (\"Reduction of HIV Concentration During Acute Infection: Independence from a Specific Immune Response\", available from ).\n", 255 | "\n", 256 | "Phillips presents a system of differential equations that models the concentrations of the HIV virus and the CD4 cells it infects.\n", 257 | "The model does not include an immune response; nevertheless, it demonstrates behavior that is qualitatively similar to what is seen in patients during the first few weeks after infection.\n", 258 | "\n", 259 | "His conclusion is that the observed decline in the concentration of HIV might not be caused by an immune response; it could be due to the dynamic interaction between HIV and the cells it infects.\n", 260 | "\n", 261 | "In the repository for this book, you will find a notebook, `hiv_model.ipynb`, which you can use to implement Phillips's model and consider whether it does the work it is meant to do." 262 | ] 263 | }, 264 | { 265 | "cell_type": "code", 266 | "execution_count": null, 267 | "id": "international-button", 268 | "metadata": {}, 269 | "outputs": [], 270 | "source": [] 271 | } 272 | ], 273 | "metadata": { 274 | "kernelspec": { 275 | "display_name": "Python 3", 276 | "language": "python", 277 | "name": "python3" 278 | }, 279 | "language_info": { 280 | "codemirror_mode": { 281 | "name": "ipython", 282 | "version": 3 283 | }, 284 | "file_extension": ".py", 285 | "mimetype": "text/x-python", 286 | "name": "python", 287 | "nbconvert_exporter": "python", 288 | "pygments_lexer": "ipython3", 289 | "version": "3.9.1" 290 | } 291 | }, 292 | "nbformat": 4, 293 | "nbformat_minor": 5 294 | } 295 | -------------------------------------------------------------------------------- /python/soln/chap22.py: -------------------------------------------------------------------------------- 1 | from modsim import * 2 | 3 | params = Params( 4 | x = 0, # m 5 | y = 1, # m 6 | angle = 45, # degree 7 | velocity = 40, # m / s 8 | 9 | mass = 145e-3, # kg 10 | diameter = 73e-3, # m 11 | C_d = 0.33, # dimensionless 12 | 13 | rho = 1.2, # kg/m**3 14 | g = 9.8, # m/s**2 15 | t_end = 10, # s 16 | ) 17 | 18 | from modsim import * 19 | 20 | from numpy import pi, deg2rad 21 | 22 | def make_system(params): 23 | 24 | # convert angle to degrees 25 | theta = deg2rad(params.angle) 26 | 27 | # compute x and y components of velocity 28 | vx, vy = pol2cart(theta, params.velocity) 29 | 30 | # make the initial state 31 | init = State(x=params.x, y=params.y, vx=vx, vy=vy) 32 | 33 | # compute the frontal area 34 | area = pi * (params.diameter/2)**2 35 | 36 | return System(params, 37 | init = init, 38 | area = area, 39 | ) 40 | 41 | from modsim import * 42 | 43 | def drag_force(V, system): 44 | rho, C_d, area = system.rho, system.C_d, system.area 45 | 46 | mag = rho * vector_mag(V)**2 * C_d * area / 2 47 | direction = -vector_hat(V) 48 | f_drag = mag * direction 49 | return f_drag 50 | 51 | from modsim import * 52 | 53 | def slope_func(t, state, system): 54 | x, y, vx, vy = state 55 | mass, g = system.mass, system.g 56 | 57 | V = Vector(vx, vy) 58 | a_drag = drag_force(V, system) / mass 59 | a_grav = g * Vector(0, -1) 60 | 61 | A = a_grav + a_drag 62 | 63 | return V.x, V.y, A.x, A.y 64 | 65 | from modsim import * 66 | 67 | def event_func(t, state, system): 68 | x, y, vx, vy = state 69 | return y 70 | 71 | from modsim import * 72 | 73 | params = Params( 74 | x = 0, # m 75 | y = 1, # m 76 | angle = 45, # degree 77 | velocity = 40, # m / s 78 | 79 | mass = 145e-3, # kg 80 | diameter = 73e-3, # m 81 | C_d = 0.33, # dimensionless 82 | 83 | rho = 1.2, # kg/m**3 84 | g = 9.8, # m/s**2 85 | t_end = 10, # s 86 | ) 87 | 88 | from modsim import * 89 | 90 | from numpy import pi, deg2rad 91 | 92 | def make_system(params): 93 | 94 | # convert angle to degrees 95 | theta = deg2rad(params.angle) 96 | 97 | # compute x and y components of velocity 98 | vx, vy = pol2cart(theta, params.velocity) 99 | 100 | # make the initial state 101 | init = State(x=params.x, y=params.y, vx=vx, vy=vy) 102 | 103 | # compute the frontal area 104 | area = pi * (params.diameter/2)**2 105 | 106 | return System(params, 107 | init = init, 108 | area = area, 109 | ) 110 | 111 | from modsim import * 112 | 113 | def drag_force(V, system): 114 | rho, C_d, area = system.rho, system.C_d, system.area 115 | 116 | mag = rho * vector_mag(V)**2 * C_d * area / 2 117 | direction = -vector_hat(V) 118 | f_drag = mag * direction 119 | return f_drag 120 | 121 | from modsim import * 122 | 123 | def slope_func(t, state, system): 124 | x, y, vx, vy = state 125 | mass, g = system.mass, system.g 126 | 127 | V = Vector(vx, vy) 128 | a_drag = drag_force(V, system) / mass 129 | a_grav = g * Vector(0, -1) 130 | 131 | A = a_grav + a_drag 132 | 133 | return V.x, V.y, A.x, A.y 134 | 135 | from modsim import * 136 | 137 | def event_func(t, state, system): 138 | x, y, vx, vy = state 139 | return y 140 | 141 | from modsim import * 142 | 143 | params = Params( 144 | x = 0, # m 145 | y = 1, # m 146 | angle = 45, # degree 147 | velocity = 40, # m / s 148 | 149 | mass = 145e-3, # kg 150 | diameter = 73e-3, # m 151 | C_d = 0.33, # dimensionless 152 | 153 | rho = 1.2, # kg/m**3 154 | g = 9.8, # m/s**2 155 | t_end = 10, # s 156 | ) 157 | 158 | from modsim import * 159 | 160 | from numpy import pi, deg2rad 161 | 162 | def make_system(params): 163 | 164 | # convert angle to degrees 165 | theta = deg2rad(params.angle) 166 | 167 | # compute x and y components of velocity 168 | vx, vy = pol2cart(theta, params.velocity) 169 | 170 | # make the initial state 171 | init = State(x=params.x, y=params.y, vx=vx, vy=vy) 172 | 173 | # compute the frontal area 174 | area = pi * (params.diameter/2)**2 175 | 176 | return System(params, 177 | init = init, 178 | area = area, 179 | ) 180 | 181 | from modsim import * 182 | 183 | def drag_force(V, system): 184 | rho, C_d, area = system.rho, system.C_d, system.area 185 | 186 | mag = rho * vector_mag(V)**2 * C_d * area / 2 187 | direction = -vector_hat(V) 188 | f_drag = mag * direction 189 | return f_drag 190 | 191 | from modsim import * 192 | 193 | def slope_func(t, state, system): 194 | x, y, vx, vy = state 195 | mass, g = system.mass, system.g 196 | 197 | V = Vector(vx, vy) 198 | a_drag = drag_force(V, system) / mass 199 | a_grav = g * Vector(0, -1) 200 | 201 | A = a_grav + a_drag 202 | 203 | return V.x, V.y, A.x, A.y 204 | 205 | from modsim import * 206 | 207 | def event_func(t, state, system): 208 | x, y, vx, vy = state 209 | return y 210 | 211 | from modsim import * 212 | 213 | params = Params( 214 | x = 0, # m 215 | y = 1, # m 216 | angle = 45, # degree 217 | velocity = 40, # m / s 218 | 219 | mass = 145e-3, # kg 220 | diameter = 73e-3, # m 221 | C_d = 0.33, # dimensionless 222 | 223 | rho = 1.2, # kg/m**3 224 | g = 9.8, # m/s**2 225 | t_end = 10, # s 226 | ) 227 | 228 | from modsim import * 229 | 230 | from numpy import pi, deg2rad 231 | 232 | def make_system(params): 233 | 234 | # convert angle to degrees 235 | theta = deg2rad(params.angle) 236 | 237 | # compute x and y components of velocity 238 | vx, vy = pol2cart(theta, params.velocity) 239 | 240 | # make the initial state 241 | init = State(x=params.x, y=params.y, vx=vx, vy=vy) 242 | 243 | # compute the frontal area 244 | area = pi * (params.diameter/2)**2 245 | 246 | return System(params, 247 | init = init, 248 | area = area, 249 | ) 250 | 251 | from modsim import * 252 | 253 | def drag_force(V, system): 254 | rho, C_d, area = system.rho, system.C_d, system.area 255 | 256 | mag = rho * vector_mag(V)**2 * C_d * area / 2 257 | direction = -vector_hat(V) 258 | f_drag = mag * direction 259 | return f_drag 260 | 261 | from modsim import * 262 | 263 | def slope_func(t, state, system): 264 | x, y, vx, vy = state 265 | mass, g = system.mass, system.g 266 | 267 | V = Vector(vx, vy) 268 | a_drag = drag_force(V, system) / mass 269 | a_grav = g * Vector(0, -1) 270 | 271 | A = a_grav + a_drag 272 | 273 | return V.x, V.y, A.x, A.y 274 | 275 | from modsim import * 276 | 277 | def event_func(t, state, system): 278 | x, y, vx, vy = state 279 | return y 280 | 281 | -------------------------------------------------------------------------------- /python/soln/chap26.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "early-drove", 6 | "metadata": {}, 7 | "source": [ 8 | "# Chapter 26" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "comic-convert", 14 | "metadata": {}, 15 | "source": [ 16 | "*Modeling and Simulation in Python*\n", 17 | "\n", 18 | "Copyright 2021 Allen Downey\n", 19 | "\n", 20 | "License: [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/)" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 1, 26 | "id": "broken-procedure", 27 | "metadata": { 28 | "tags": [ 29 | "remove-cell" 30 | ] 31 | }, 32 | "outputs": [], 33 | "source": [ 34 | "# install Pint if necessary\n", 35 | "\n", 36 | "try:\n", 37 | " import pint\n", 38 | "except ImportError:\n", 39 | " !pip install pint" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 2, 45 | "id": "massive-thong", 46 | "metadata": { 47 | "tags": [ 48 | "remove-cell" 49 | ] 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "# download modsim.py if necessary\n", 54 | "\n", 55 | "from os.path import exists\n", 56 | "\n", 57 | "filename = 'modsim.py'\n", 58 | "if not exists(filename):\n", 59 | " from urllib.request import urlretrieve\n", 60 | " url = 'https://raw.githubusercontent.com/AllenDowney/ModSim/main/'\n", 61 | " local, _ = urlretrieve(url+filename, filename)\n", 62 | " print('Downloaded ' + local)" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 3, 68 | "id": "experienced-junction", 69 | "metadata": { 70 | "tags": [ 71 | "remove-cell" 72 | ] 73 | }, 74 | "outputs": [], 75 | "source": [ 76 | "# import functions from modsim\n", 77 | "\n", 78 | "from modsim import *" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "id": "acoustic-small", 84 | "metadata": {}, 85 | "source": [ 86 | "You made it to the end of the book. Congratulations!\n", 87 | "\n", 88 | "This last chapter is a collection of case studies you might want to read and work on.\n", 89 | "They are based on the methods in the last few chapters, including Newtonian mechanics in 1-D and 2-D, and rotation around a single axis." 90 | ] 91 | }, 92 | { 93 | "cell_type": "markdown", 94 | "id": "capital-vatican", 95 | "metadata": {}, 96 | "source": [ 97 | "## Bungee jumping\n", 98 | "\n", 99 | "Suppose you want to set the world record for the highest \"bungee dunk\", which is a stunt in which a bungee jumper dunks a cookie in a cup of tea at the lowest point of a jump. An example is shown in this video: .\n", 100 | "\n", 101 | "Since the record is 70 m, let's design a jump for 80 m. We'll start with the following modeling assumptions:\n", 102 | "\n", 103 | "- Initially the bungee cord hangs from a crane with the attachment\n", 104 | " point 80 m above a cup of tea.\n", 105 | "\n", 106 | "- Until the cord is fully extended, it applies no force to the jumper. It turns out this might not be a good assumption; we'll revisit it in the next case study.\n", 107 | "\n", 108 | "- After the cord is fully extended, it obeys Hooke's Law; that is, it applies a force to the jumper proportional to the extension of the cord beyond its resting length. See .\n", 109 | "\n", 110 | "- The mass of the jumper is 75 kg.\n", 111 | "\n", 112 | "- The jumper is subject to drag force so that their terminal velocity is 60 m/s.\n", 113 | "\n", 114 | "Our objective is to choose the length of the cord, `L`, and its spring\n", 115 | "constant, `k`, so that the jumper falls all the way to the tea cup, but no farther!\n", 116 | "\n", 117 | "In the repository for this book, you will find a notebook,\n", 118 | "`bungee1.ipynb`, which contains starter code and exercises for this case study." 119 | ] 120 | }, 121 | { 122 | "cell_type": "markdown", 123 | "id": "recognized-bidding", 124 | "metadata": {}, 125 | "source": [ 126 | "## Bungee dunk revisited\n", 127 | "\n", 128 | "In the previous case study, we assume that the cord applies no force to\n", 129 | "the jumper until it is stretched. It is tempting to say that the cord\n", 130 | "has no effect because it falls along with the jumper, but that intuition\n", 131 | "is incorrect. As the cord falls, it transfers energy to the jumper.\n", 132 | "\n", 133 | "At you'll find a paper (Heck, Uylings, and Kędzierska, \"Understanding the physics of bungee jumping\", Physics Education, Volume 45, Number 1, 2010.) that explains\n", 134 | "this phenomenon and derives the acceleration of the jumper, $a$, as a\n", 135 | "function of position, $y$, and velocity, $v$:\n", 136 | "\n", 137 | "$$a = g + \\frac{\\mu v^2/2}{\\mu(L+y) + 2L}$$ \n", 138 | "\n", 139 | "where $g$ is acceleration due to gravity, $L$ is the length of the cord, and $\\mu$ is the ratio of the mass of the cord, $m$, and the mass of the jumper, $M$.\n", 140 | "\n", 141 | "If you don't believe that their model is correct, this video might\n", 142 | "convince you: .\n", 143 | "\n", 144 | "In the repository for this book, you will find a notebook,\n", 145 | "`bungee2.ipynb`, which contains starter code and exercises for this case study. How does the behavior of the system change as we vary the mass of the cord? When the mass of the cord equals the mass of the jumper, what is the net effect on the lowest point in the jump?" 146 | ] 147 | }, 148 | { 149 | "cell_type": "markdown", 150 | "id": "german-penetration", 151 | "metadata": {}, 152 | "source": [ 153 | "## Orbiting the Sun\n", 154 | "\n", 155 | "In a previous example, we modeled the interaction between the Earth and the Sun, simulating what would happen if the Earth stopped in its orbit and fell straight into the Sun.\n", 156 | "\n", 157 | "Now let's extend the model to two dimensions and simulate one revolution of the Earth around the Sun, that is, one year.\n", 158 | "\n", 159 | "In the repository for this book, you will find a notebook,\n", 160 | "`orbit.ipynb`, which contains starter code and exercises for this case study.\n", 161 | "\n", 162 | "Among other things, you will have a chance to experiment with different algorithms and see what effect they have on the accuracy of the results." 163 | ] 164 | }, 165 | { 166 | "cell_type": "markdown", 167 | "id": "victorian-colonial", 168 | "metadata": {}, 169 | "source": [ 170 | "## Spider-Man\n", 171 | "\n", 172 | "In this case study we'll develop a model of Spider-Man swinging from a\n", 173 | "springy cable of webbing attached to the top of the Empire State\n", 174 | "Building. Initially, Spider-Man is at the top of a nearby building, as\n", 175 | "shown in this figure:\n", 176 | "\n", 177 | "![Diagram of the initial state for the Spider-Man case\n", 178 | "study.](https://github.com/AllenDowney/ModSim/raw/main/figs/spiderman.png)\n", 179 | "\n", 180 | "The origin, `O`, is at the base of the Empire State Building. The vector `H` represents the position where the webbing is attached to the building, relative to `O`. The vector `P` is the position of Spider-Man relative to `O`. And `L` is the vector from the attachment point to Spider-Man.\n", 181 | "\n", 182 | "By following the arrows from `O`, along `H`, and along `L`, we can see\n", 183 | "that\n", 184 | "\n", 185 | "```\n", 186 | "H + L = P\n", 187 | "```\n", 188 | "\n", 189 | "So we can compute `L` like this:\n", 190 | "\n", 191 | "```\n", 192 | "L = P - H\n", 193 | "```" 194 | ] 195 | }, 196 | { 197 | "cell_type": "markdown", 198 | "id": "bizarre-strand", 199 | "metadata": {}, 200 | "source": [ 201 | "The goals of this case study are:\n", 202 | "\n", 203 | "1. Implement a model of this scenario to predict Spider-Man's\n", 204 | " trajectory.\n", 205 | "\n", 206 | "2. Choose the right time for Spider-Man to let go of the webbing in\n", 207 | " order to maximize the distance he travels before landing.\n", 208 | "\n", 209 | "3. Choose the best angle for Spider-Man to jump off the building, and\n", 210 | " let go of the webbing, to maximize range.\n", 211 | "\n", 212 | "We'll use the following parameters:\n", 213 | "\n", 214 | "1. According to the Spider-Man Wiki (), Spider-Man weighs 76 kg.\n", 215 | "\n", 216 | "2. Let's assume his terminal velocity is 60 m/s.\n", 217 | "\n", 218 | "3. The length of the web is 100 m.\n", 219 | "\n", 220 | "4. The initial angle of the web is 45° to the left of straight down.\n", 221 | "\n", 222 | "5. The spring constant of the web is 40 N/m when the cord is stretched, and 0 when it's compressed.\n", 223 | "\n", 224 | "In the repository for this book, you will find a notebook,\n", 225 | "`spiderman.ipynb`, which contains starter code. Read through the\n", 226 | "notebook and run the code. It uses `minimize`, which is a SciPy function that can search for an optimal set of parameters (as contrasted with `minimize_scalar`, which can only search along a single axis)." 227 | ] 228 | }, 229 | { 230 | "cell_type": "markdown", 231 | "id": "african-relative", 232 | "metadata": {}, 233 | "source": [ 234 | "## Kittens\n", 235 | "\n", 236 | "If you have used the Internet, you have probably seen videos of kittens unrolling toilet paper.\n", 237 | "And you might have wondered how long it would take a standard kitten to unroll 47 m of paper, the length of a standard roll.\n", 238 | "\n", 239 | "The interactions of the kitten and the paper rolls are complex. To keep things simple, let's assume that the kitten pulls down on the free end of the roll with constant force. And let's neglect the friction between the roll and the axle.\n", 240 | "\n", 241 | "This diagram shows the paper roll with the force applied by the kitten, $F$, the lever arm of the force around the axis of rotation, $r$, and the resulting torque, $\\tau$.\n", 242 | "\n", 243 | "![Diagram of a roll of toilet paper, showing a force, lever arm, and the resulting torque.](https://github.com/AllenDowney/ModSim/raw/main/figs/kitten.png)\n", 244 | "\n", 245 | "Assuming that the force applied by the kitten is 0.002 N, how long would it take to unroll a standard roll of toilet paper?\n", 246 | "\n", 247 | "In the repository for this book, you will find a notebook,\n", 248 | "`kitten.ipynb`, which contains starter code for this case study. Use it to implement this model and check whether the results seem plausible." 249 | ] 250 | }, 251 | { 252 | "cell_type": "markdown", 253 | "id": "future-burlington", 254 | "metadata": {}, 255 | "source": [ 256 | "## Simulating a yo-yo\n", 257 | "\n", 258 | "Suppose you are holding a yo-yo with a length of string wound around its axle, and you drop it while holding the end of the string stationary. As gravity accelerates the yo-yo downward, tension in the string exerts a force upward. Since this force acts on a point offset from the center of mass, it exerts a torque that causes the yo-yo to spin.\n", 259 | "\n", 260 | "The following diagram shows the forces on the yo-yo and the resulting torque. The outer shaded area shows the body of the yo-yo. The inner shaded area shows the rolled up string, the radius of which changes as the yo-yo unrolls.\n", 261 | "\n", 262 | "![Diagram of a yo-yo showing forces due to gravity and tension in the\n", 263 | "string, the lever arm of tension, and the resulting\n", 264 | "torque.](https://github.com/AllenDowney/ModSim/raw/main/figs/yoyo.png)" 265 | ] 266 | }, 267 | { 268 | "cell_type": "markdown", 269 | "id": "turkish-result", 270 | "metadata": {}, 271 | "source": [ 272 | "In this system, we can't figure out the linear and angular acceleration independently; we have to solve a system of equations: \n", 273 | "\n", 274 | "$$\\begin{aligned}\n", 275 | "\\sum F &= m a \\\\\n", 276 | "\\sum \\tau &= I \\alpha\\end{aligned}$$ \n", 277 | "\n", 278 | "where the summations indicate that we are adding up forces and torques.\n", 279 | "\n", 280 | "As in the previous examples, linear and angular velocity are related\n", 281 | "because of the way the string unrolls:\n", 282 | "\n", 283 | "$$\\frac{dy}{dt} = -r \\frac{d \\theta}{dt}$$ \n", 284 | "\n", 285 | "In this example, the linear and angular accelerations have opposite sign. As the yo-yo rotates counter-clockwise, $\\theta$ increases and $y$, which is the length of the rolled part of the string, decreases." 286 | ] 287 | }, 288 | { 289 | "cell_type": "markdown", 290 | "id": "surrounded-quilt", 291 | "metadata": {}, 292 | "source": [ 293 | "Taking the derivative of both sides yields a similar relationship\n", 294 | "between linear and angular acceleration:\n", 295 | "\n", 296 | "$$\\frac{d^2 y}{dt^2} = -r \\frac{d^2 \\theta}{dt^2}$$ \n", 297 | "\n", 298 | "Which we can write more concisely: $$a = -r \\alpha$$ This relationship is not a general law of nature; it is specific to scenarios like this where one object rolls along another without stretching or slipping.\n", 299 | "\n", 300 | "Because of the way we've set up the problem, $y$ actually has two\n", 301 | "meanings: it represents the length of the rolled string and the height\n", 302 | "of the yo-yo, which decreases as the yo-yo falls. Similarly, $a$\n", 303 | "represents acceleration in the length of the rolled string and the\n", 304 | "height of the yo-yo.\n", 305 | "\n", 306 | "We can compute the acceleration of the yo-yo by adding up the linear\n", 307 | "forces: \n", 308 | "\n", 309 | "$$\\sum F = T - mg = ma$$ \n", 310 | "\n", 311 | "Where $T$ is positive because the tension force points up, and $mg$ is negative because gravity points down." 312 | ] 313 | }, 314 | { 315 | "cell_type": "markdown", 316 | "id": "finnish-disaster", 317 | "metadata": {}, 318 | "source": [ 319 | "Because gravity acts on the center of mass, it creates no torque, so the only torque is due to tension: \n", 320 | "\n", 321 | "$$\\sum \\tau = T r = I \\alpha$$ \n", 322 | "\n", 323 | "Positive (upward) tension yields positive (counter-clockwise) angular\n", 324 | "acceleration.\n", 325 | "\n", 326 | "Now we have three equations in three unknowns, $T$, $a$, and $\\alpha$,\n", 327 | "with $I$, $m$, $g$, and $r$ as known parameters. We could solve these equations by hand, but we can also get SymPy to do it for us:" 328 | ] 329 | }, 330 | { 331 | "cell_type": "code", 332 | "execution_count": 4, 333 | "id": "crude-tribune", 334 | "metadata": {}, 335 | "outputs": [ 336 | { 337 | "data": { 338 | "text/plain": [ 339 | "{T: I*g*m/(I + m*r**2), a: -g*m*r**2/(I + m*r**2), alpha: g*m*r/(I + m*r**2)}" 340 | ] 341 | }, 342 | "execution_count": 4, 343 | "metadata": {}, 344 | "output_type": "execute_result" 345 | } 346 | ], 347 | "source": [ 348 | "from sympy import symbols, Eq, solve\n", 349 | "\n", 350 | "T, a, alpha, I, m, g, r = symbols('T a alpha I m g r')\n", 351 | "eq1 = Eq(a, -r * alpha)\n", 352 | "eq2 = Eq(T - m*g, m * a)\n", 353 | "eq3 = Eq(T * r, I * alpha)\n", 354 | "soln = solve([eq1, eq2, eq3], [T, a, alpha])\n", 355 | "soln" 356 | ] 357 | }, 358 | { 359 | "cell_type": "markdown", 360 | "id": "insured-indie", 361 | "metadata": {}, 362 | "source": [ 363 | "The results are \n", 364 | "\n", 365 | "$$\\begin{aligned}\n", 366 | "T &= m g I / I^* \\\\\n", 367 | "a &= -m g r^2 / I^* \\\\\n", 368 | "\\alpha &= m g r / I^* \\\\\\end{aligned}$$ \n", 369 | "\n", 370 | "where $I^*$ is the augmented moment of inertia, $I + m r^2$. \n", 371 | "We can use these equations for $a$ and $\\alpha$ to write a slope function and simulate this system.\n", 372 | "\n", 373 | "In the repository for this book, you will find a notebook, `yoyo.ipynb`, which contains starter code for this case study. Use it to implement and test this model." 374 | ] 375 | }, 376 | { 377 | "cell_type": "code", 378 | "execution_count": null, 379 | "id": "accompanied-southwest", 380 | "metadata": {}, 381 | "outputs": [], 382 | "source": [] 383 | } 384 | ], 385 | "metadata": { 386 | "kernelspec": { 387 | "display_name": "Python 3", 388 | "language": "python", 389 | "name": "python3" 390 | }, 391 | "language_info": { 392 | "codemirror_mode": { 393 | "name": "ipython", 394 | "version": 3 395 | }, 396 | "file_extension": ".py", 397 | "mimetype": "text/x-python", 398 | "name": "python", 399 | "nbconvert_exporter": "python", 400 | "pygments_lexer": "ipython3", 401 | "version": "3.9.1" 402 | } 403 | }, 404 | "nbformat": 4, 405 | "nbformat_minor": 5 406 | } 407 | -------------------------------------------------------------------------------- /python/soln/examples/glucose_insulin.csv: -------------------------------------------------------------------------------- 1 | time,glucose,insulin 2 | 0,92,11 3 | 2,350,26 4 | 4,287,130 5 | 6,251,85 6 | 8,240,51 7 | 10,216,49 8 | 12,211,45 9 | 14,205,41 10 | 16,196,35 11 | 19,192,30 12 | 22,172,30 13 | 27,163,27 14 | 32,142,30 15 | 42,124,22 16 | 52,105,15 17 | 62,92,15 18 | 72,84,11 19 | 82,77,10 20 | 92,82,8 21 | 102,81,11 22 | 122,82,7 23 | 142,82,8 24 | 162,85,8 25 | 182,90,7 -------------------------------------------------------------------------------- /python/soln/modsim.py: -------------------------------------------------------------------------------- 1 | """ 2 | Code from Modeling and Simulation in Python. 3 | 4 | Copyright 2020 Allen Downey 5 | 6 | MIT License: https://opensource.org/licenses/MIT 7 | """ 8 | 9 | import logging 10 | 11 | logger = logging.getLogger(name="modsim.py") 12 | 13 | # make sure we have Python 3.6 or better 14 | import sys 15 | 16 | if sys.version_info < (3, 6): 17 | logger.warning("modsim.py depends on Python 3.6 features.") 18 | 19 | import inspect 20 | 21 | import matplotlib.pyplot as plt 22 | import numpy as np 23 | import pandas as pd 24 | import scipy 25 | 26 | import scipy.optimize as spo 27 | 28 | from scipy.interpolate import interp1d 29 | from scipy.interpolate import InterpolatedUnivariateSpline 30 | 31 | from scipy.integrate import solve_ivp 32 | 33 | from types import SimpleNamespace 34 | from copy import copy 35 | 36 | import pint 37 | 38 | units = pint.UnitRegistry() 39 | #Quantity = units.Quantity 40 | 41 | 42 | def flip(p=0.5): 43 | """Flips a coin with the given probability. 44 | 45 | p: float 0-1 46 | 47 | returns: boolean (True or False) 48 | """ 49 | return np.random.random() < p 50 | 51 | 52 | def cart2pol(x, y, z=None): 53 | """Convert Cartesian coordinates to polar. 54 | 55 | x: number or sequence 56 | y: number or sequence 57 | z: number or sequence (optional) 58 | 59 | returns: theta, rho OR theta, rho, z 60 | """ 61 | x = np.asarray(x) 62 | y = np.asarray(y) 63 | 64 | rho = np.hypot(x, y) 65 | theta = np.arctan2(y, x) 66 | 67 | if z is None: 68 | return theta, rho 69 | else: 70 | return theta, rho, z 71 | 72 | 73 | def pol2cart(theta, rho, z=None): 74 | """Convert polar coordinates to Cartesian. 75 | 76 | theta: number or sequence in radians 77 | rho: number or sequence 78 | z: number or sequence (optional) 79 | 80 | returns: x, y OR x, y, z 81 | """ 82 | x = rho * np.cos(theta) 83 | y = rho * np.sin(theta) 84 | 85 | if z is None: 86 | return x, y 87 | else: 88 | return x, y, z 89 | 90 | from numpy import linspace 91 | 92 | def linrange(start, stop=None, step=1, **options): 93 | """Make an array of equally spaced values. 94 | 95 | start: first value 96 | stop: last value (might be approximate) 97 | step: difference between elements (should be consistent) 98 | 99 | returns: NumPy array 100 | """ 101 | if stop is None: 102 | stop = start 103 | start = 0 104 | n = int(round((stop-start) / step)) 105 | return linspace(start, stop, n+1, **options) 106 | 107 | 108 | def root_scalar(func, *args, **kwargs): 109 | """Finds the input value that minimizes `min_func`. 110 | 111 | Wrapper for 112 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.root_scalar.html 113 | 114 | func: computes the function to be minimized 115 | bracket: sequence of two values, lower and upper bounds of the range to be searched 116 | args: any additional positional arguments are passed to func 117 | kwargs: any keyword arguments are passed to root_scalar 118 | 119 | returns: RootResults object 120 | """ 121 | bracket = kwargs.get('bracket', None) 122 | if bracket is None or len(bracket) != 2: 123 | msg = ("To run root_scalar, you have to provide a " 124 | "`bracket` keyword argument with a sequence " 125 | "of length 2.") 126 | raise ValueError(msg) 127 | 128 | try: 129 | func(bracket[0], *args) 130 | except Exception as e: 131 | msg = ("Before running scipy.integrate.root_scalar " 132 | "I tried running the function you provided " 133 | "with `bracket[0]`, " 134 | "and I got the following error:") 135 | logger.error(msg) 136 | raise (e) 137 | 138 | underride(kwargs, rtol=1e-4) 139 | 140 | res = spo.root_scalar(func, *args, **kwargs) 141 | 142 | if not res.converged: 143 | msg = ("scipy.optimize.root_scalar did not converge. " 144 | "The message it returned is:\n" + res.flag) 145 | raise ValueError(msg) 146 | 147 | return res 148 | 149 | 150 | def minimize_scalar(func, *args, **kwargs): 151 | """Finds the input value that minimizes `func`. 152 | 153 | Wrapper for 154 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize_scalar.html 155 | 156 | func: computes the function to be minimized 157 | args: any additional positional arguments are passed to func 158 | kwargs: any keyword arguments are passed to minimize_scalar 159 | 160 | returns: OptimizeResult object 161 | """ 162 | bounds = kwargs.get('bounds', None) 163 | 164 | if bounds is None or len(bounds) != 2: 165 | msg = ("To run maximize_scalar or minimize_scalar, " 166 | "you have to provide a `bounds` " 167 | "keyword argument with a sequence " 168 | "of length 2.") 169 | raise ValueError(msg) 170 | 171 | try: 172 | func(bounds[0], *args) 173 | except Exception as e: 174 | msg = ("Before running scipy.integrate.minimize_scalar, " 175 | "I tried running the function you provided " 176 | "with the lower bound, " 177 | "and I got the following error:") 178 | logger.error(msg) 179 | raise (e) 180 | 181 | underride(kwargs, method='bounded') 182 | 183 | res = spo.minimize_scalar(func, args=args, **kwargs) 184 | 185 | if not res.success: 186 | msg = ("minimize_scalar did not succeed." 187 | "The message it returned is: \n" + 188 | res.message) 189 | raise Exception(msg) 190 | 191 | return res 192 | 193 | 194 | def maximize_scalar(max_func, *args, **kwargs): 195 | """Finds the input value that maximizes `max_func`. 196 | 197 | Wrapper for https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize_scalar.html 198 | 199 | min_func: computes the function to be maximized 200 | args: any additional positional arguments are passed to max_func 201 | options: any keyword arguments are passed as options to minimize_scalar 202 | 203 | returns: ModSimSeries object 204 | """ 205 | def min_func(*args): 206 | return -max_func(*args) 207 | 208 | res = minimize_scalar(min_func, *args, **kwargs) 209 | 210 | # we have to negate the function value before returning res 211 | res.fun = -res.fun 212 | return res 213 | 214 | 215 | def run_solve_ivp(system, slope_func, **options): 216 | """Computes a numerical solution to a differential equation. 217 | 218 | `system` must contain `init` with initial conditions, 219 | `t_end` with the end time. Optionally, it can contain 220 | `t_0` with the start time. 221 | 222 | It should contain any other parameters required by the 223 | slope function. 224 | 225 | `options` can be any legal options of `scipy.integrate.solve_ivp` 226 | 227 | system: System object 228 | slope_func: function that computes slopes 229 | 230 | returns: TimeFrame 231 | """ 232 | system = remove_units(system) 233 | 234 | # make sure `system` contains `init` 235 | if not hasattr(system, "init"): 236 | msg = """It looks like `system` does not contain `init` 237 | as a system variable. `init` should be a State 238 | object that specifies the initial condition:""" 239 | raise ValueError(msg) 240 | 241 | # make sure `system` contains `t_end` 242 | if not hasattr(system, "t_end"): 243 | msg = """It looks like `system` does not contain `t_end` 244 | as a system variable. `t_end` should be the 245 | final time:""" 246 | raise ValueError(msg) 247 | 248 | # the default value for t_0 is 0 249 | t_0 = getattr(system, "t_0", 0) 250 | 251 | # try running the slope function with the initial conditions 252 | try: 253 | slope_func(t_0, system.init, system) 254 | except Exception as e: 255 | msg = """Before running scipy.integrate.solve_ivp, I tried 256 | running the slope function you provided with the 257 | initial conditions in `system` and `t=t_0` and I got 258 | the following error:""" 259 | logger.error(msg) 260 | raise (e) 261 | 262 | # get the list of event functions 263 | events = options.get('events', []) 264 | 265 | # if there's only one event function, put it in a list 266 | try: 267 | iter(events) 268 | except TypeError: 269 | events = [events] 270 | 271 | for event_func in events: 272 | # make events terminal unless otherwise specified 273 | if not hasattr(event_func, 'terminal'): 274 | event_func.terminal = True 275 | 276 | # test the event function with the initial conditions 277 | try: 278 | event_func(t_0, system.init, system) 279 | except Exception as e: 280 | msg = """Before running scipy.integrate.solve_ivp, I tried 281 | running the event function you provided with the 282 | initial conditions in `system` and `t=t_0` and I got 283 | the following error:""" 284 | logger.error(msg) 285 | raise (e) 286 | 287 | # get dense output unless otherwise specified 288 | if not 't_eval' in options: 289 | underride(options, dense_output=True) 290 | 291 | # run the solver 292 | bunch = solve_ivp(slope_func, [t_0, system.t_end], system.init, 293 | args=[system], **options) 294 | 295 | # separate the results from the details 296 | y = bunch.pop("y") 297 | t = bunch.pop("t") 298 | 299 | # get the column names from `init`, if possible 300 | if hasattr(system.init, 'index'): 301 | columns = system.init.index 302 | else: 303 | columns = range(len(system.init)) 304 | 305 | # evaluate the results at equally-spaced points 306 | if options.get('dense_output', False): 307 | try: 308 | num = system.num 309 | except AttributeError: 310 | num = 101 311 | t_final = t[-1] 312 | t_array = linspace(t_0, t_final, num) 313 | y_array = bunch.sol(t_array) 314 | 315 | # pack the results into a TimeFrame 316 | results = TimeFrame(y_array.T, index=t_array, 317 | columns=columns) 318 | else: 319 | results = TimeFrame(y.T, index=t, 320 | columns=columns) 321 | 322 | return results, bunch 323 | 324 | 325 | def leastsq(error_func, x0, *args, **options): 326 | """Find the parameters that yield the best fit for the data. 327 | 328 | `x0` can be a sequence, array, Series, or Params 329 | 330 | Positional arguments are passed along to `error_func`. 331 | 332 | Keyword arguments are passed to `scipy.optimize.leastsq` 333 | 334 | error_func: function that computes a sequence of errors 335 | x0: initial guess for the best parameters 336 | args: passed to error_func 337 | options: passed to leastsq 338 | 339 | :returns: Params object with best_params and ModSimSeries with details 340 | """ 341 | # override `full_output` so we get a message if something goes wrong 342 | options["full_output"] = True 343 | 344 | # run leastsq 345 | t = scipy.optimize.leastsq(error_func, x0=x0, args=args, **options) 346 | best_params, cov_x, infodict, mesg, ier = t 347 | 348 | # pack the results into a ModSimSeries object 349 | details = SimpleNamespace(cov_x=cov_x, 350 | mesg=mesg, 351 | ier=ier, 352 | **infodict) 353 | details.success = details.ier in [1,2,3,4] 354 | 355 | # if we got a Params object, we should return a Params object 356 | if isinstance(x0, Params): 357 | best_params = Params(pd.Series(best_params, x0.index)) 358 | 359 | # return the best parameters and details 360 | return best_params, details 361 | 362 | 363 | def crossings(series, value): 364 | """Find the labels where the series passes through value. 365 | 366 | The labels in series must be increasing numerical values. 367 | 368 | series: Series 369 | value: number 370 | 371 | returns: sequence of labels 372 | """ 373 | values = series.values - value 374 | interp = InterpolatedUnivariateSpline(series.index, values) 375 | return interp.roots() 376 | 377 | 378 | def has_nan(a): 379 | """Checks whether the an array contains any NaNs. 380 | 381 | :param a: NumPy array or Pandas Series 382 | :return: boolean 383 | """ 384 | return np.any(np.isnan(a)) 385 | 386 | 387 | def is_strictly_increasing(a): 388 | """Checks whether the elements of an array are strictly increasing. 389 | 390 | :param a: NumPy array or Pandas Series 391 | :return: boolean 392 | """ 393 | return np.all(np.diff(a) > 0) 394 | 395 | 396 | def interpolate(series, **options): 397 | """Creates an interpolation function. 398 | 399 | series: Series object 400 | options: any legal options to scipy.interpolate.interp1d 401 | 402 | returns: function that maps from the index to the values 403 | """ 404 | if has_nan(series.index): 405 | msg = """The Series you passed to interpolate contains 406 | NaN values in the index, which would result in 407 | undefined behavior. So I'm putting a stop to that.""" 408 | raise ValueError(msg) 409 | 410 | if not is_strictly_increasing(series.index): 411 | msg = """The Series you passed to interpolate has an index 412 | that is not strictly increasing, which would result in 413 | undefined behavior. So I'm putting a stop to that.""" 414 | raise ValueError(msg) 415 | 416 | # make the interpolate function extrapolate past the ends of 417 | # the range, unless `options` already specifies a value for `fill_value` 418 | underride(options, fill_value="extrapolate") 419 | 420 | # call interp1d, which returns a new function object 421 | x = series.index 422 | y = series.values 423 | interp_func = interp1d(x, y, **options) 424 | return interp_func 425 | 426 | 427 | def interpolate_inverse(series, **options): 428 | """Interpolate the inverse function of a Series. 429 | 430 | series: Series object, represents a mapping from `a` to `b` 431 | options: any legal options to scipy.interpolate.interp1d 432 | 433 | returns: interpolation object, can be used as a function 434 | from `b` to `a` 435 | """ 436 | inverse = pd.Series(series.index, index=series.values) 437 | interp_func = interpolate(inverse, **options) 438 | return interp_func 439 | 440 | 441 | def gradient(series, **options): 442 | """Computes the numerical derivative of a series. 443 | 444 | If the elements of series have units, they are dropped. 445 | 446 | series: Series object 447 | options: any legal options to np.gradient 448 | 449 | returns: Series, same subclass as series 450 | """ 451 | x = series.index 452 | y = series.values 453 | 454 | a = np.gradient(y, x, **options) 455 | return series.__class__(a, series.index) 456 | 457 | 458 | def source_code(obj): 459 | """Prints the source code for a given object. 460 | 461 | obj: function or method object 462 | """ 463 | print(inspect.getsource(obj)) 464 | 465 | 466 | def underride(d, **options): 467 | """Add key-value pairs to d only if key is not in d. 468 | 469 | If d is None, create a new dictionary. 470 | 471 | d: dictionary 472 | options: keyword args to add to d 473 | """ 474 | if d is None: 475 | d = {} 476 | 477 | for key, val in options.items(): 478 | d.setdefault(key, val) 479 | 480 | return d 481 | 482 | 483 | def contour(df, **options): 484 | """Makes a contour plot from a DataFrame. 485 | 486 | Wrapper for plt.contour 487 | https://matplotlib.org/3.1.0/api/_as_gen/matplotlib.pyplot.contour.html 488 | 489 | Note: columns and index must be numerical 490 | 491 | df: DataFrame 492 | options: passed to plt.contour 493 | """ 494 | fontsize = options.pop("fontsize", 12) 495 | underride(options, cmap="viridis") 496 | x = df.columns 497 | y = df.index 498 | X, Y = np.meshgrid(x, y) 499 | cs = plt.contour(X, Y, df, **options) 500 | plt.clabel(cs, inline=1, fontsize=fontsize) 501 | 502 | 503 | def savefig(filename, **options): 504 | """Save the current figure. 505 | 506 | Keyword arguments are passed along to plt.savefig 507 | 508 | https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html 509 | 510 | filename: string 511 | """ 512 | print("Saving figure to file", filename) 513 | plt.savefig(filename, **options) 514 | 515 | 516 | def decorate(**options): 517 | """Decorate the current axes. 518 | 519 | Call decorate with keyword arguments like 520 | decorate(title='Title', 521 | xlabel='x', 522 | ylabel='y') 523 | 524 | The keyword arguments can be any of the axis properties 525 | https://matplotlib.org/api/axes_api.html 526 | """ 527 | ax = plt.gca() 528 | ax.set(**options) 529 | 530 | handles, labels = ax.get_legend_handles_labels() 531 | if handles: 532 | ax.legend(handles, labels) 533 | 534 | plt.tight_layout() 535 | 536 | 537 | def remove_from_legend(bad_labels): 538 | """Removes some labels from the legend. 539 | 540 | bad_labels: sequence of strings 541 | """ 542 | ax = plt.gca() 543 | handles, labels = ax.get_legend_handles_labels() 544 | handle_list, label_list = [], [] 545 | for handle, label in zip(handles, labels): 546 | if label not in bad_labels: 547 | handle_list.append(handle) 548 | label_list.append(label) 549 | ax.legend(handle_list, label_list) 550 | 551 | 552 | class SettableNamespace(SimpleNamespace): 553 | """Contains a collection of parameters. 554 | 555 | Used to make a System object. 556 | 557 | Takes keyword arguments and stores them as attributes. 558 | """ 559 | def __init__(self, namespace=None, **kwargs): 560 | super().__init__() 561 | if namespace: 562 | self.__dict__.update(namespace.__dict__) 563 | self.__dict__.update(kwargs) 564 | 565 | def get(self, name, default=None): 566 | """Look up a variable. 567 | 568 | name: string varname 569 | default: value returned if `name` is not present 570 | """ 571 | try: 572 | return self.__getattribute__(name, default) 573 | except AttributeError: 574 | return default 575 | 576 | def set(self, **variables): 577 | """Make a copy and update the given variables. 578 | 579 | returns: Params 580 | """ 581 | new = copy(self) 582 | new.__dict__.update(variables) 583 | return new 584 | 585 | 586 | def magnitude(x): 587 | """Returns the magnitude of a Quantity or number. 588 | 589 | x: Quantity or number 590 | 591 | returns: number 592 | """ 593 | return x.magnitude if hasattr(x, 'magnitude') else x 594 | 595 | 596 | def remove_units(namespace): 597 | """Removes units from the values in a Namespace. 598 | 599 | Only removes units from top-level values; 600 | does not traverse nested values. 601 | 602 | returns: new Namespace object 603 | """ 604 | res = copy(namespace) 605 | for label, value in res.__dict__.items(): 606 | if isinstance(value, pd.Series): 607 | value = remove_units_series(value) 608 | res.__dict__[label] = magnitude(value) 609 | return res 610 | 611 | 612 | def remove_units_series(series): 613 | """Removes units from the values in a Series. 614 | 615 | Only removes units from top-level values; 616 | does not traverse nested values. 617 | 618 | returns: new Series object 619 | """ 620 | res = copy(series) 621 | for label, value in res.iteritems(): 622 | res[label] = magnitude(value) 623 | return res 624 | 625 | 626 | class System(SettableNamespace): 627 | """Contains system parameters and their values. 628 | 629 | Takes keyword arguments and stores them as attributes. 630 | """ 631 | pass 632 | 633 | 634 | class Params(SettableNamespace): 635 | """Contains system parameters and their values. 636 | 637 | Takes keyword arguments and stores them as attributes. 638 | """ 639 | pass 640 | 641 | 642 | def State(**variables): 643 | """Contains the values of state variables.""" 644 | return pd.Series(variables, name='state') 645 | 646 | 647 | def make_series(x, y, **options): 648 | """Make a Pandas Series. 649 | 650 | x: sequence used as the index 651 | y: sequence used as the values 652 | 653 | returns: Pandas Series 654 | """ 655 | if isinstance(y, pd.Series): 656 | y = y.values 657 | return pd.Series(y, index=x, **options) 658 | 659 | 660 | def TimeSeries(*args, **kwargs): 661 | """ 662 | """ 663 | if args or kwargs: 664 | series = pd.Series(*args, **kwargs) 665 | else: 666 | series = pd.Series([], dtype=np.float64) 667 | 668 | series.index.name = 'Time' 669 | if 'name' not in kwargs: 670 | series.name = 'Quantity' 671 | return series 672 | 673 | 674 | def SweepSeries(*args, **kwargs): 675 | """ 676 | """ 677 | if args or kwargs: 678 | series = pd.Series(*args, **kwargs) 679 | else: 680 | series = pd.Series([], dtype=np.float64) 681 | 682 | series.index.name = 'Parameter' 683 | if 'name' not in kwargs: 684 | series.name = 'Metric' 685 | return series 686 | 687 | 688 | def show(obj): 689 | """Display a Series or Namespace as a DataFrame.""" 690 | if isinstance(obj, pd.Series): 691 | return pd.DataFrame(obj) 692 | elif isinstance(obj, SimpleNamespace): 693 | return pd.DataFrame(pd.Series(obj.__dict__), 694 | columns=['value']) 695 | else: 696 | return obj 697 | 698 | 699 | def TimeFrame(*args, **kwargs): 700 | """DataFrame that maps from time to State. 701 | """ 702 | underride(kwargs, dtype=float) 703 | return pd.DataFrame(*args, **kwargs) 704 | 705 | 706 | def SweepFrame(*args, **kwargs): 707 | """DataFrame that maps from parameter value to SweepSeries. 708 | """ 709 | underride(kwargs, dtype=float) 710 | return pd.DataFrame(*args, **kwargs) 711 | 712 | 713 | def Vector(x, y, z=None, **options): 714 | """ 715 | """ 716 | if z is None: 717 | return pd.Series(dict(x=x, y=y), **options) 718 | else: 719 | return pd.Series(dict(x=x, y=y, z=z), **options) 720 | 721 | 722 | ## Vector functions (should work with any sequence) 723 | 724 | def vector_mag(v): 725 | """Vector magnitude.""" 726 | return np.sqrt(np.dot(v, v)) 727 | 728 | 729 | def vector_mag2(v): 730 | """Vector magnitude squared.""" 731 | return np.dot(v, v) 732 | 733 | 734 | def vector_angle(v): 735 | """Angle between v and the positive x axis. 736 | 737 | Only works with 2-D vectors. 738 | 739 | returns: angle in radians 740 | """ 741 | assert len(v) == 2 742 | x, y = v 743 | return np.arctan2(y, x) 744 | 745 | 746 | def vector_polar(v): 747 | """Vector magnitude and angle. 748 | 749 | returns: (number, angle in radians) 750 | """ 751 | return vector_mag(v), vector_angle(v) 752 | 753 | 754 | def vector_hat(v): 755 | """Unit vector in the direction of v. 756 | 757 | returns: Vector or array 758 | """ 759 | # check if the magnitude of the Quantity is 0 760 | mag = vector_mag(v) 761 | if mag == 0: 762 | return v 763 | else: 764 | return v / mag 765 | 766 | 767 | def vector_perp(v): 768 | """Perpendicular Vector (rotated left). 769 | 770 | Only works with 2-D Vectors. 771 | 772 | returns: Vector 773 | """ 774 | assert len(v) == 2 775 | x, y = v 776 | return Vector(-y, x) 777 | 778 | 779 | def vector_dot(v, w): 780 | """Dot product of v and w. 781 | 782 | returns: number or Quantity 783 | """ 784 | return np.dot(v, w) 785 | 786 | 787 | def vector_cross(v, w): 788 | """Cross product of v and w. 789 | 790 | returns: number or Quantity for 2-D, Vector for 3-D 791 | """ 792 | res = np.cross(v, w) 793 | 794 | if len(v) == 3: 795 | return Vector(*res) 796 | else: 797 | return res 798 | 799 | 800 | def vector_proj(v, w): 801 | """Projection of v onto w. 802 | 803 | returns: array or Vector with direction of w and units of v. 804 | """ 805 | w_hat = vector_hat(w) 806 | return vector_dot(v, w_hat) * w_hat 807 | 808 | 809 | def scalar_proj(v, w): 810 | """Returns the scalar projection of v onto w. 811 | 812 | Which is the magnitude of the projection of v onto w. 813 | 814 | returns: scalar with units of v. 815 | """ 816 | return vector_dot(v, vector_hat(w)) 817 | 818 | 819 | def vector_dist(v, w): 820 | """Euclidean distance from v to w, with units.""" 821 | if isinstance(v, list): 822 | v = np.asarray(v) 823 | return vector_mag(v - w) 824 | 825 | 826 | def vector_diff_angle(v, w): 827 | """Angular difference between two vectors, in radians. 828 | """ 829 | if len(v) == 2: 830 | return vector_angle(v) - vector_angle(w) 831 | else: 832 | # TODO: see http://www.euclideanspace.com/maths/algebra/ 833 | # vectors/angleBetween/ 834 | raise NotImplementedError() 835 | 836 | 837 | def plot_segment(A, B, **options): 838 | """Plots a line segment between two Vectors. 839 | 840 | For 3-D vectors, the z axis is ignored. 841 | 842 | Additional options are passed along to plot(). 843 | 844 | A: Vector 845 | B: Vector 846 | """ 847 | xs = A.x, B.x 848 | ys = A.y, B.y 849 | plt.plot(xs, ys, **options) 850 | 851 | 852 | from time import sleep 853 | from IPython.display import clear_output 854 | 855 | def animate(results, draw_func, *args, interval=None): 856 | """Animate results from a simulation. 857 | 858 | results: TimeFrame 859 | draw_func: function that draws state 860 | interval: time between frames in seconds 861 | """ 862 | plt.figure() 863 | try: 864 | for t, state in results.iterrows(): 865 | draw_func(t, state, *args) 866 | plt.show() 867 | if interval: 868 | sleep(interval) 869 | clear_output(wait=True) 870 | draw_func(t, state, *args) 871 | plt.show() 872 | except KeyboardInterrupt: 873 | pass 874 | -------------------------------------------------------------------------------- /soln/chap19.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "mighty-israeli", 6 | "metadata": {}, 7 | "source": [ 8 | "# Chapter 19" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "passing-solid", 14 | "metadata": {}, 15 | "source": [ 16 | "*Modeling and Simulation in Python*\n", 17 | "\n", 18 | "Copyright 2021 Allen Downey\n", 19 | "\n", 20 | "License: [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/)" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 1, 26 | "id": "broken-procedure", 27 | "metadata": { 28 | "tags": [ 29 | "remove-cell" 30 | ] 31 | }, 32 | "outputs": [], 33 | "source": [ 34 | "# install Pint if necessary\n", 35 | "\n", 36 | "try:\n", 37 | " import pint\n", 38 | "except ImportError:\n", 39 | " !pip install pint" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 2, 45 | "id": "massive-thong", 46 | "metadata": { 47 | "tags": [ 48 | "remove-cell" 49 | ] 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "# download modsim.py if necessary\n", 54 | "\n", 55 | "from os.path import exists\n", 56 | "\n", 57 | "filename = 'modsim.py'\n", 58 | "if not exists(filename):\n", 59 | " from urllib.request import urlretrieve\n", 60 | " url = 'https://raw.githubusercontent.com/AllenDowney/ModSim/main/'\n", 61 | " local, _ = urlretrieve(url+filename, filename)\n", 62 | " print('Downloaded ' + local)" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 3, 68 | "id": "experienced-junction", 69 | "metadata": { 70 | "tags": [ 71 | "remove-cell" 72 | ] 73 | }, 74 | "outputs": [], 75 | "source": [ 76 | "# import functions from modsim\n", 77 | "\n", 78 | "from modsim import *" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "id": "interstate-gibson", 84 | "metadata": {}, 85 | "source": [ 86 | "## The insulin minimal model\n", 87 | "\n", 88 | "Along with the glucose minimal model in Chapter xxx,\n", 89 | "Berman et al. developed an insulin minimal model, in which the\n", 90 | "concentration of insulin, $I$, is governed by this differential\n", 91 | "equation:\n", 92 | "\n", 93 | "$$\\frac{dI}{dt} = -k I(t) + \\gamma \\left[ G(t) - G_T \\right] t$$ \n", 94 | "\n", 95 | "where\n", 96 | "\n", 97 | "- $k$ is a parameter that controls the rate of insulin disappearance\n", 98 | " independent of blood glucose.\n", 99 | "\n", 100 | "- $G(t)$ is the measured concentration of blood glucose at time $t$.\n", 101 | "\n", 102 | "- $G_T$ is the glucose threshold; when blood glucose is above this\n", 103 | " level, it triggers an increase in blood insulin.\n", 104 | "\n", 105 | "- $\\gamma$ is a parameter that controls the rate of increase (or\n", 106 | " decrease) in blood insulin when glucose is above (or below) $G_T$.\n", 107 | "\n", 108 | "The initial condition is $I(0) = I_0$. As in the glucose minimal model, we treat the initial condition as a parameter which we'll choose to fit the data.\n", 109 | "\n", 110 | "The parameters of this model can be used to estimate $\\phi_1$ and\n", 111 | "$\\phi_2$, which are values that \"describe the sensitivity to glucose of the first and second phase pancreatic responsivity\". They are related to the parameters as follows:\n", 112 | "\n", 113 | "$$\\phi_1 = \\frac{I_{max} - I_b}{k (G_0 - G_b)}$$\n", 114 | "\n", 115 | "$$\\phi_2 = \\gamma \\times 10^4$$ \n", 116 | "\n", 117 | "where $I_{max}$ is the maximum measured insulin level, and $I_b$ and $G_b$ are the basal levels of insulin and glucose." 118 | ] 119 | }, 120 | { 121 | "cell_type": "markdown", 122 | "id": "amber-composite", 123 | "metadata": {}, 124 | "source": [ 125 | "In the repository for this book, you will find a notebook,\n", 126 | "`insulin.ipynb`, which contains starter code for this case study. Use it to implement the insulin model, find the parameters that best fit the data, and estimate these values." 127 | ] 128 | }, 129 | { 130 | "cell_type": "markdown", 131 | "id": "parallel-radical", 132 | "metadata": {}, 133 | "source": [ 134 | "## Low-Pass Filter\n", 135 | "\n", 136 | "The following circuit diagram (from ) shows a low-pass filter built with one resistor and one capacitor.\n", 137 | "\n", 138 | "![image](figs/RC_Divider.pdf){height=\"1.2in\"}\n", 139 | "\n", 140 | "A \"filter\" is a circuit takes a signal, $V_{in}$, as input and produces a signal, $V_{out}$, as output. In this context, a \"signal\" is a voltage that changes over time.\n", 141 | "\n", 142 | "A filter is \"low-pass\" if it allows low-frequency signals to pass from\n", 143 | "$V_{in}$ to $V_{out}$ unchanged, but it reduces the amplitude of\n", 144 | "high-frequency signals.\n", 145 | "\n", 146 | "By applying the laws of circuit analysis, we can derive a differential\n", 147 | "equation that describes the behavior of this system. By solving the\n", 148 | "differential equation, we can predict the effect of this circuit on any input signal.\n", 149 | "\n", 150 | "Suppose we are given $V_{in}$ and $V_{out}$ at a particular instant in\n", 151 | "time. By Ohm's law, which is a simple model of the behavior of\n", 152 | "resistors, the instantaneous current through the resistor is:\n", 153 | "\n", 154 | "$$I_R = (V_{in} - V_{out}) / R$$ \n", 155 | "\n", 156 | "where $R$ is resistance in ohms (Ω).\n", 157 | "\n", 158 | "Assuming that no current flows through the output of the circuit,\n", 159 | "Kirchhoff's current law implies that the current through the capacitor\n", 160 | "is: \n", 161 | "\n", 162 | "$$I_C = I_R$$ \n", 163 | "\n", 164 | "According to a simple model of the behavior of\n", 165 | "capacitors, current through the capacitor causes a change in the voltage across the capacitor: \n", 166 | "\n", 167 | "$$I_C = C \\frac{d V_{out}}{dt}$$ \n", 168 | "\n", 169 | "where $C$ is capacitance in farads (F). Combining these equations yields a differential equation for $V_{out}$:\n", 170 | "\n", 171 | "$$\\frac{d V_{out}}{dt} = \\frac{V_{in} - V_{out}}{R C}$$ \n", 172 | "\n", 173 | "In the repository for this book, you will find a notebook, `filter.ipynb`, which contains starter code for this case study. Follow the instructions to simulate the low-pass filter for input signals like this:\n", 174 | "\n", 175 | "$$V_{in}(t) = A \\cos (2 \\pi f t)$$ \n", 176 | "\n", 177 | "where $A$ is the amplitude of the input signal, say 5 V, and $f$ is the frequency of the signal in Hz.\n", 178 | "\n", 179 | "In the repository for this book, you will find a notebook,\n", 180 | "`filter.ipynb`, which contains starter code for this case study. Read\n", 181 | "the notebook, run the code, and work on the exercises." 182 | ] 183 | }, 184 | { 185 | "cell_type": "markdown", 186 | "id": "entire-stations", 187 | "metadata": {}, 188 | "source": [ 189 | "## Thermal behavior of a wall\n", 190 | "\n", 191 | "This case study is based on a paper by Gori, et al[^2] that models the\n", 192 | "thermal behavior of a brick wall, with the goal of understanding the\n", 193 | "\"performance gap between the expected energy use of buildings and their measured energy use\".\n", 194 | "\n", 195 | "The following figure shows the scenario and their model of the wall:\n", 196 | "\n", 197 | "![image](figs/wall_model.pdf){height=\"1.4in\"}\n", 198 | "\n", 199 | "On the interior and exterior surfaces of the wall, they measure\n", 200 | "temperature and heat flux over a period of three days. They model the\n", 201 | "wall using two thermal masses connected to the surfaces, and to each\n", 202 | "other, by thermal resistors.\n", 203 | "\n", 204 | "The primary methodology of the paper is a Bayesian method for inferring the parameters of the system (two thermal masses and three thermal resistances).\n", 205 | "\n", 206 | "The primary result is a comparison of two models: the one shown here\n", 207 | "with two thermal masses, and a simpler model with only one thermal mass. They find that the two-mass model is able to reproduce the measured fluxes substantially better.\n", 208 | "\n", 209 | "For this case study we will implement their model and run it with the\n", 210 | "estimated parameters from the paper, and then use `fit_leastsq` to see\n", 211 | "if we can find parameters that yield lower errors.\n", 212 | "\n", 213 | "In the repository for this book, you will find a notebook, `wall.ipynb` with the code and results for this case study.\n", 214 | "\n", 215 | "Gori, Marincioni, Biddulph, Elwell, \"Inferring the thermal resistance and effective thermal mass distribution of a wall from in situ measurements to characterise heat transfer at both the interior and exterior surfaces\", *Energy and Buildings*, Volume 135, pages 398-409, ." 216 | ] 217 | }, 218 | { 219 | "cell_type": "markdown", 220 | "id": "described-trailer", 221 | "metadata": {}, 222 | "source": [ 223 | "The authors put their paper under a Creative Commons license, and\n", 224 | "make their data available at . I thank them\n", 225 | "for their commitment to open, reproducible science, which made this\n", 226 | "case study possible." 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": null, 232 | "id": "promising-wheel", 233 | "metadata": {}, 234 | "outputs": [], 235 | "source": [] 236 | } 237 | ], 238 | "metadata": { 239 | "kernelspec": { 240 | "display_name": "Python 3", 241 | "language": "python", 242 | "name": "python3" 243 | }, 244 | "language_info": { 245 | "codemirror_mode": { 246 | "name": "ipython", 247 | "version": 3 248 | }, 249 | "file_extension": ".py", 250 | "mimetype": "text/x-python", 251 | "name": "python", 252 | "nbconvert_exporter": "python", 253 | "pygments_lexer": "ipython3", 254 | "version": "3.7.9" 255 | } 256 | }, 257 | "nbformat": 4, 258 | "nbformat_minor": 5 259 | } 260 | -------------------------------------------------------------------------------- /soln/chap26.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "id": "early-drove", 6 | "metadata": {}, 7 | "source": [ 8 | "# Chapter 26" 9 | ] 10 | }, 11 | { 12 | "cell_type": "markdown", 13 | "id": "comic-convert", 14 | "metadata": {}, 15 | "source": [ 16 | "*Modeling and Simulation in Python*\n", 17 | "\n", 18 | "Copyright 2021 Allen Downey\n", 19 | "\n", 20 | "License: [Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/)" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 1, 26 | "id": "broken-procedure", 27 | "metadata": { 28 | "tags": [ 29 | "remove-cell" 30 | ] 31 | }, 32 | "outputs": [], 33 | "source": [ 34 | "# install Pint if necessary\n", 35 | "\n", 36 | "try:\n", 37 | " import pint\n", 38 | "except ImportError:\n", 39 | " !pip install pint" 40 | ] 41 | }, 42 | { 43 | "cell_type": "code", 44 | "execution_count": 2, 45 | "id": "massive-thong", 46 | "metadata": { 47 | "tags": [ 48 | "remove-cell" 49 | ] 50 | }, 51 | "outputs": [], 52 | "source": [ 53 | "# download modsim.py if necessary\n", 54 | "\n", 55 | "from os.path import exists\n", 56 | "\n", 57 | "filename = 'modsim.py'\n", 58 | "if not exists(filename):\n", 59 | " from urllib.request import urlretrieve\n", 60 | " url = 'https://raw.githubusercontent.com/AllenDowney/ModSim/main/'\n", 61 | " local, _ = urlretrieve(url+filename, filename)\n", 62 | " print('Downloaded ' + local)" 63 | ] 64 | }, 65 | { 66 | "cell_type": "code", 67 | "execution_count": 3, 68 | "id": "experienced-junction", 69 | "metadata": { 70 | "tags": [ 71 | "remove-cell" 72 | ] 73 | }, 74 | "outputs": [], 75 | "source": [ 76 | "# import functions from modsim\n", 77 | "\n", 78 | "from modsim import *" 79 | ] 80 | }, 81 | { 82 | "cell_type": "markdown", 83 | "id": "acoustic-small", 84 | "metadata": {}, 85 | "source": [ 86 | "Case studies!" 87 | ] 88 | }, 89 | { 90 | "cell_type": "markdown", 91 | "id": "african-relative", 92 | "metadata": {}, 93 | "source": [ 94 | "## Bungee jumping\n", 95 | "\n", 96 | "Suppose you want to set the world record for the highest \"bungee dunk\",\n", 97 | "which is a stunt in which a bungee jumper dunks a cookie in a cup of tea\n", 98 | "at the lowest point of a jump. An example is shown in this video:\n", 99 | ".\n", 100 | "\n", 101 | "Since the record is 70 m, let's design a jump for 80 m. We'll start with\n", 102 | "the following modeling assumptions:\n", 103 | "\n", 104 | "- Initially the bungee cord hangs from a crane with the attachment\n", 105 | " point 80 m above a cup of tea.\n", 106 | "\n", 107 | "- Until the cord is fully extended, it applies no force to the jumper.\n", 108 | " It turns out this might not be a good assumption; we will revisit\n", 109 | " it.\n", 110 | "\n", 111 | "- After the cord is fully extended, it obeys Hooke's Law; that is, it\n", 112 | " applies a force to the jumper proportional to the extension of the\n", 113 | " cord beyond its resting length. See .\n", 114 | "\n", 115 | "- The mass of the jumper is 75 kg.\n", 116 | "\n", 117 | "- The jumper is subject to drag force so that their terminal velocity\n", 118 | " is 60 m/s.\n", 119 | "\n", 120 | "Our objective is to choose the length of the cord, `L`, and its spring\n", 121 | "constant, `k`, so that the jumper falls all the way to the tea cup, but\n", 122 | "no farther!\n", 123 | "\n", 124 | "In the repository for this book, you will find a notebook,\n", 125 | "`bungee.ipynb`, which contains starter code and exercises for this case\n", 126 | "study.\n", 127 | "\n", 128 | "## Bungee dunk revisited\n", 129 | "\n", 130 | "In the previous case study, we assume that the cord applies no force to\n", 131 | "the jumper until it is stretched. It is tempting to say that the cord\n", 132 | "has no effect because it falls along with the jumper, but that intuition\n", 133 | "is incorrect. As the cord falls, it transfers energy to the jumper.\n", 134 | "\n", 135 | "At you'll find a paper[^1] that explains\n", 136 | "this phenomenon and derives the acceleration of the jumper, $a$, as a\n", 137 | "function of position, $y$, and velocity, $v$:\n", 138 | "$$a = g + \\frac{\\mu v^2/2}{\\mu(L+y) + 2L}$$ where $g$ is acceleration\n", 139 | "due to gravity, $L$ is the length of the cord, and $\\mu$ is the ratio of\n", 140 | "the mass of the cord, $m$, and the mass of the jumper, $M$.\n", 141 | "\n", 142 | "If you don't believe that their model is correct, this video might\n", 143 | "convince you: .\n", 144 | "\n", 145 | "In the repository for this book, you will find a notebook,\n", 146 | "`bungee2.ipynb`, which contains starter code and exercises for this case\n", 147 | "study. How does the behavior of the system change as we vary the mass of\n", 148 | "the cord? When the mass of the cord equals the mass of the jumper, what\n", 149 | "is the net effect on the lowest point in the jump?\n", 150 | "\n", 151 | "## Spider-Man\n", 152 | "\n", 153 | "In this case study we'll develop a model of Spider-Man swinging from a\n", 154 | "springy cable of webbing attached to the top of the Empire State\n", 155 | "Building. Initially, Spider-Man is at the top of a nearby building, as\n", 156 | "shown in Figure [\\[spiderman\\]](#spiderman){reference-type=\"ref\"\n", 157 | "reference=\"spiderman\"}.\n", 158 | "\n", 159 | "![Diagram of the initial state for the Spider-Man case\n", 160 | "study.](figs/spiderman.pdf){height=\"2.8in\"}\n", 161 | "\n", 162 | "The origin, `O`, is at the base of the Empire State Building. The vector\n", 163 | "`H` represents the position where the webbing is attached to the\n", 164 | "building, relative to `O`. The vector `P` is the position of Spider-Man\n", 165 | "relative to `O`. And `L` is the vector from the attachment point to\n", 166 | "Spider-Man.\n", 167 | "\n", 168 | "By following the arrows from `O`, along `H`, and along `L`, we can see\n", 169 | "that\n", 170 | "\n", 171 | "H + L = P\n", 172 | "\n", 173 | "So we can compute `L` like this:\n", 174 | "\n", 175 | "L = P - H\n", 176 | "\n", 177 | "The goals of this case study are:\n", 178 | "\n", 179 | "1. Implement a model of this scenario to predict Spider-Man's\n", 180 | " trajectory.\n", 181 | "\n", 182 | "2. Choose the right time for Spider-Man to let go of the webbing in\n", 183 | " order to maximize the distance he travels before landing.\n", 184 | "\n", 185 | "3. Choose the best angle for Spider-Man to jump off the building, and\n", 186 | " let go of the webbing, to maximize range.\n", 187 | "\n", 188 | "We'll use the following parameters:\n", 189 | "\n", 190 | "1. According to the Spider-Man Wiki[^2], Spider-Man weighs 76 kg.\n", 191 | "\n", 192 | "2. Let's assume his terminal velocity is 60 m/s.\n", 193 | "\n", 194 | "3. The length of the web is 100 m.\n", 195 | "\n", 196 | "4. The initial angle of the web is 45 ° to the left of straight down.\n", 197 | "\n", 198 | "5. The spring constant of the web is 40 N/m when the cord is stretched,\n", 199 | " and 0 when it's compressed.\n", 200 | "\n", 201 | "In the repository for this book, you will find a notebook,\n", 202 | "`spiderman.ipynb`, which contains starter code. Read through the\n", 203 | "notebook and run the code. It uses `minimize`, which is a SciPy function\n", 204 | "that can search for an optimal set of parameters (as contrasted with\n", 205 | "`minimize_scalar`, which can only search along a single axis).\n", 206 | "\n", 207 | "## Kittens\n", 208 | "\n", 209 | "Let's simulate a kitten unrolling toilet paper. As reference material,\n", 210 | "see this video: .\n", 211 | "\n", 212 | "The interactions of the kitten and the paper roll are complex. To keep\n", 213 | "things simple, let's assume that the kitten pulls down on the free end\n", 214 | "of the roll with constant force. Also, we will neglect the friction\n", 215 | "between the roll and the axle.\n", 216 | "\n", 217 | "![Diagram of a roll of toilet paper, showing a force, lever arm, and the\n", 218 | "resulting torque.](figs/kitten.pdf){height=\"2.5in\"}\n", 219 | "\n", 220 | "Figure [\\[kitten\\]](#kitten){reference-type=\"ref\" reference=\"kitten\"}\n", 221 | "shows the paper roll with $r$, $F$, and $\\tau$. As a vector quantity,\n", 222 | "the direction of $\\tau$ is into the page, but we only care about its\n", 223 | "magnitude for now.\n", 224 | "\n", 225 | "Here's the `Params` object with the parameters we'll need:" 226 | ] 227 | }, 228 | { 229 | "cell_type": "code", 230 | "execution_count": null, 231 | "id": "congressional-powell", 232 | "metadata": {}, 233 | "outputs": [], 234 | "source": [ 235 | "params = Params(Rmin = 0.02 * m,\n", 236 | " Rmax = 0.055 * m,\n", 237 | " Mcore = 15e-3 * kg,\n", 238 | " Mroll = 215e-3 * kg,\n", 239 | " L = 47 * m,\n", 240 | " tension = 2e-4 * N,\n", 241 | " t_end = 180 * s)" 242 | ] 243 | }, 244 | { 245 | "cell_type": "markdown", 246 | "id": "miniature-debut", 247 | "metadata": {}, 248 | "source": [ 249 | "As before, `Rmin` is the minimum radius and `Rmax` is the maximum. `L`\n", 250 | "is the length of the paper. `Mcore` is the mass of the cardboard tube at\n", 251 | "the center of the roll; `Mroll` is the mass of the paper. `tension` is\n", 252 | "the force applied by the kitten, in N. I chose a value that yields\n", 253 | "plausible results.\n", 254 | "\n", 255 | "At you can find moments of inertia for\n", 256 | "simple geometric shapes. I'll model the cardboard tube at the center of\n", 257 | "the roll as a \"thin cylindrical shell\\\", and the paper roll as a\n", 258 | "\"thick-walled cylindrical tube with open ends\\\".\n", 259 | "\n", 260 | "The moment of inertia for a thin shell is just $m r^2$, where $m$ is the\n", 261 | "mass and $r$ is the radius of the shell.\n", 262 | "\n", 263 | "For a thick-walled tube the moment of inertia is\n", 264 | "$$I = \\frac{\\pi \\rho h}{2} (r_2^4 - r_1^4)$$ where $\\rho$ is the density\n", 265 | "of the material, $h$ is the height of the tube, $r_2$ is the outer\n", 266 | "diameter, and $r_1$ is the inner diameter.\n", 267 | "\n", 268 | "Since the outer diameter changes as the kitten unrolls the paper, we\n", 269 | "have to compute the moment of inertia, at each point in time, as a\n", 270 | "function of the current radius, `r`. Here's the function that does it:" 271 | ] 272 | }, 273 | { 274 | "cell_type": "code", 275 | "execution_count": null, 276 | "id": "employed-deficit", 277 | "metadata": {}, 278 | "outputs": [], 279 | "source": [ 280 | "def moment_of_inertia(r, system):\n", 281 | " Mcore, Rmin = system.Mcore, system.Rmin\n", 282 | " rho_h = system.rho_h\n", 283 | " \n", 284 | " Icore = Mcore * Rmin**2 \n", 285 | " Iroll = pi * rho_h / 2 * (r**4 - Rmin**4)\n", 286 | " return Icore + Iroll" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "id": "binary-fitness", 292 | "metadata": {}, 293 | "source": [ 294 | "`rho_h` is the product of density and height, $\\rho h$, which is the\n", 295 | "mass per area. `rho_h` is computed in `make_system`:" 296 | ] 297 | }, 298 | { 299 | "cell_type": "code", 300 | "execution_count": null, 301 | "id": "arbitrary-voltage", 302 | "metadata": {}, 303 | "outputs": [], 304 | "source": [ 305 | "def make_system(params):\n", 306 | " L, Rmax, Rmin = params.L, params.Rmax, params.Rmin\n", 307 | " Mroll = params.Mroll\n", 308 | " \n", 309 | " init = State(theta = 0 * radian,\n", 310 | " omega = 0 * radian/s,\n", 311 | " y = L)\n", 312 | " \n", 313 | " area = pi * (Rmax**2 - Rmin**2)\n", 314 | " rho_h = Mroll / area\n", 315 | " k = (Rmax**2 - Rmin**2) / 2 / L / radian \n", 316 | " \n", 317 | " return System(params, init=init, area=area, \n", 318 | " rho_h=rho_h, k=k)" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "id": "finnish-disaster", 324 | "metadata": {}, 325 | "source": [ 326 | "`make_system` also computes `k` using\n", 327 | "Equation [\\[eqn4\\]](#eqn4){reference-type=\"ref\" reference=\"eqn4\"}.\n", 328 | "\n", 329 | "In the repository for this book, you will find a notebook,\n", 330 | "`kitten.ipynb`, which contains starter code for this case study. Use it\n", 331 | "to implement this model and check whether the results seem plausible.\n", 332 | "\n", 333 | "## Simulating a yo-yo\n", 334 | "\n", 335 | "Suppose you are holding a yo-yo with a length of string wound around its\n", 336 | "axle, and you drop it while holding the end of the string stationary. As\n", 337 | "gravity accelerates the yo-yo downward, tension in the string exerts a\n", 338 | "force upward. Since this force acts on a point offset from the center of\n", 339 | "mass, it exerts a torque that causes the yo-yo to spin.\n", 340 | "\n", 341 | "![Diagram of a yo-yo showing forces due to gravity and tension in the\n", 342 | "string, the lever arm of tension, and the resulting\n", 343 | "torque.](figs/yoyo.pdf){height=\"2.5in\"}\n", 344 | "\n", 345 | "Figure [\\[yoyo\\]](#yoyo){reference-type=\"ref\" reference=\"yoyo\"} is a\n", 346 | "diagram of the forces on the yo-yo and the resulting torque. The outer\n", 347 | "shaded area shows the body of the yo-yo. The inner shaded area shows the\n", 348 | "rolled up string, the radius of which changes as the yo-yo unrolls.\n", 349 | "\n", 350 | "In this model, we can't figure out the linear and angular acceleration\n", 351 | "independently; we have to solve a system of equations: $$\\begin{aligned}\n", 352 | "\\sum F &= m a \\\\\n", 353 | "\\sum \\tau &= I \\alpha\\end{aligned}$$ where the summations indicate that\n", 354 | "we are adding up forces and torques.\n", 355 | "\n", 356 | "As in the previous examples, linear and angular velocity are related\n", 357 | "because of the way the string unrolls:\n", 358 | "$$\\frac{dy}{dt} = -r \\frac{d \\theta}{dt}$$ In this example, the linear\n", 359 | "and angular accelerations have opposite sign. As the yo-yo rotates\n", 360 | "counter-clockwise, $\\theta$ increases and $y$, which is the length of\n", 361 | "the rolled part of the string, decreases.\n", 362 | "\n", 363 | "Taking the derivative of both sides yields a similar relationship\n", 364 | "between linear and angular acceleration:\n", 365 | "$$\\frac{d^2 y}{dt^2} = -r \\frac{d^2 \\theta}{dt^2}$$ Which we can write\n", 366 | "more concisely: $$a = -r \\alpha$$ This relationship is not a general law\n", 367 | "of nature; it is specific to scenarios like this where one object rolls\n", 368 | "along another without stretching or slipping.\n", 369 | "\n", 370 | "Because of the way we've set up the problem, $y$ actually has two\n", 371 | "meanings: it represents the length of the rolled string and the height\n", 372 | "of the yo-yo, which decreases as the yo-yo falls. Similarly, $a$\n", 373 | "represents acceleration in the length of the rolled string and the\n", 374 | "height of the yo-yo.\n", 375 | "\n", 376 | "We can compute the acceleration of the yo-yo by adding up the linear\n", 377 | "forces: $$\\sum F = T - mg = ma$$ Where $T$ is positive because the\n", 378 | "tension force points up, and $mg$ is negative because gravity points\n", 379 | "down.\n", 380 | "\n", 381 | "Because gravity acts on the center of mass, it creates no torque, so the\n", 382 | "only torque is due to tension: $$\\sum \\tau = T r = I \\alpha$$ Positive\n", 383 | "(upward) tension yields positive (counter-clockwise) angular\n", 384 | "acceleration.\n", 385 | "\n", 386 | "Now we have three equations in three unknowns, $T$, $a$, and $\\alpha$,\n", 387 | "with $I$, $m$, $g$, and $r$ as known quantities. It is simple enough to\n", 388 | "solve these equations by hand, but we can also get SymPy to do it for\n", 389 | "us:" 390 | ] 391 | }, 392 | { 393 | "cell_type": "code", 394 | "execution_count": null, 395 | "id": "crude-tribune", 396 | "metadata": {}, 397 | "outputs": [], 398 | "source": [ 399 | "T, a, alpha, I, m, g, r = symbols('T a alpha I m g r')\n", 400 | "eq1 = Eq(a, -r * alpha)\n", 401 | "eq2 = Eq(T - m*g, m * a)\n", 402 | "eq3 = Eq(T * r, I * alpha)\n", 403 | "soln = solve([eq1, eq2, eq3], [T, a, alpha])" 404 | ] 405 | }, 406 | { 407 | "cell_type": "markdown", 408 | "id": "insured-indie", 409 | "metadata": {}, 410 | "source": [ 411 | "The results are $$\\begin{aligned}\n", 412 | "T &= m g I / I^* \\\\\n", 413 | "a &= -m g r^2 / I^* \\\\\n", 414 | "\\alpha &= m g r / I^* \\\\\\end{aligned}$$ where $I^*$ is the augmented\n", 415 | "moment of inertia, $I + m r^2$. To simulate the system, we don't really\n", 416 | "need $T$; we can plug $a$ and $\\alpha$ directly into the slope function.\n", 417 | "\n", 418 | "In the repository for this book, you will find a notebook, `yoyo.ipynb`,\n", 419 | "which contains the derivation of these equations and starter code for\n", 420 | "this case study. Use it to implement and test this model.\n", 421 | "\n", 422 | "[^1]: Heck, Uylings, and Kędzierska, \"Understanding the physics of\n", 423 | " bungee jumping\\\", Physics Education, Volume 45, Number 1, 2010.\n", 424 | "\n", 425 | "[^2]: " 426 | ] 427 | } 428 | ], 429 | "metadata": { 430 | "kernelspec": { 431 | "display_name": "Python 3", 432 | "language": "python", 433 | "name": "python3" 434 | }, 435 | "language_info": { 436 | "codemirror_mode": { 437 | "name": "ipython", 438 | "version": 3 439 | }, 440 | "file_extension": ".py", 441 | "mimetype": "text/x-python", 442 | "name": "python", 443 | "nbconvert_exporter": "python", 444 | "pygments_lexer": "ipython3", 445 | "version": "3.9.1" 446 | } 447 | }, 448 | "nbformat": 4, 449 | "nbformat_minor": 5 450 | } 451 | -------------------------------------------------------------------------------- /soln/modsim.py: -------------------------------------------------------------------------------- 1 | """ 2 | Code from Modeling and Simulation in Python. 3 | 4 | Copyright 2020 Allen Downey 5 | 6 | MIT License: https://opensource.org/licenses/MIT 7 | """ 8 | 9 | import logging 10 | 11 | logger = logging.getLogger(name="modsim.py") 12 | 13 | # make sure we have Python 3.6 or better 14 | import sys 15 | 16 | if sys.version_info < (3, 6): 17 | logger.warning("modsim.py depends on Python 3.6 features.") 18 | 19 | import inspect 20 | 21 | import matplotlib.pyplot as plt 22 | import numpy as np 23 | import pandas as pd 24 | import scipy 25 | 26 | from scipy.interpolate import interp1d 27 | from scipy.interpolate import InterpolatedUnivariateSpline 28 | 29 | from scipy.integrate import odeint 30 | from scipy.integrate import solve_ivp 31 | 32 | from types import SimpleNamespace 33 | from copy import copy 34 | 35 | import pint 36 | 37 | units = pint.UnitRegistry() 38 | Quantity = units.Quantity 39 | 40 | 41 | def flip(p=0.5): 42 | """Flips a coin with the given probability. 43 | 44 | p: float 0-1 45 | 46 | returns: boolean (True or False) 47 | """ 48 | return np.random.random() < p 49 | 50 | 51 | def cart2pol(x, y, z=None): 52 | """Convert Cartesian coordinates to polar. 53 | 54 | x: number or sequence 55 | y: number or sequence 56 | z: number or sequence (optional) 57 | 58 | returns: theta, rho OR theta, rho, z 59 | """ 60 | x = np.asarray(x) 61 | y = np.asarray(y) 62 | 63 | rho = np.hypot(x, y) 64 | theta = np.arctan2(y, x) 65 | 66 | if z is None: 67 | return theta, rho 68 | else: 69 | return theta, rho, z 70 | 71 | 72 | def pol2cart(theta, rho, z=None): 73 | """Convert polar coordinates to Cartesian. 74 | 75 | theta: number or sequence in radians 76 | rho: number or sequence 77 | z: number or sequence (optional) 78 | 79 | returns: x, y OR x, y, z 80 | """ 81 | x = rho * np.cos(theta) 82 | y = rho * np.sin(theta) 83 | 84 | if z is None: 85 | return x, y 86 | else: 87 | return x, y, z 88 | 89 | from numpy import linspace 90 | 91 | def linrange(start, stop, step=1, **options): 92 | """Make an array of equally spaced values. 93 | 94 | start: first value 95 | stop: last value (might be approximate) 96 | step: difference between elements (should be consistent) 97 | 98 | returns: NumPy array 99 | """ 100 | n = int(round((stop-start) / step)) 101 | return linspace(start, stop, n+1, **options) 102 | 103 | 104 | def leastsq(error_func, x0, *args, **options): 105 | """Find the parameters that yield the best fit for the data. 106 | 107 | `x0` can be a sequence, array, Series, or Params 108 | 109 | Positional arguments are passed along to `error_func`. 110 | 111 | Keyword arguments are passed to `scipy.optimize.leastsq` 112 | 113 | error_func: function that computes a sequence of errors 114 | x0: initial guess for the best parameters 115 | args: passed to error_func 116 | options: passed to leastsq 117 | 118 | :returns: Params object with best_params and ModSimSeries with details 119 | """ 120 | # override `full_output` so we get a message if something goes wrong 121 | options["full_output"] = True 122 | 123 | # run leastsq 124 | t = scipy.optimize.leastsq(error_func, x0=x0, args=args, **options) 125 | best_params, cov_x, infodict, mesg, ier = t 126 | 127 | # pack the results into a ModSimSeries object 128 | details = ModSimSeries(infodict) 129 | details.set(cov_x=cov_x, mesg=mesg, ier=ier) 130 | 131 | # if we got a Params object, we should return a Params object 132 | if isinstance(x0, Params): 133 | best_params = Params(Series(best_params, x0.index)) 134 | 135 | # return the best parameters and details 136 | return best_params, details 137 | 138 | 139 | def minimize_scalar(min_func, bounds, *args, **options): 140 | """Finds the input value that minimizes `min_func`. 141 | 142 | Wrapper for 143 | https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize_scalar.html 144 | 145 | min_func: computes the function to be minimized 146 | bounds: sequence of two values, lower and upper bounds of the range to be searched 147 | args: any additional positional arguments are passed to min_func 148 | options: any keyword arguments are passed as options to minimize_scalar 149 | 150 | returns: ModSimSeries object 151 | """ 152 | try: 153 | min_func(bounds[0], *args) 154 | except Exception as e: 155 | msg = """Before running scipy.integrate.minimize_scalar, I tried 156 | running the function you provided with the 157 | lower bound, and I got the following error:""" 158 | logger.error(msg) 159 | raise (e) 160 | 161 | underride(options, xatol=1e-3) 162 | 163 | res = scipy.optimize.minimize_scalar( 164 | min_func, 165 | bracket=bounds, 166 | bounds=bounds, 167 | args=args, 168 | method="bounded", 169 | options=options, 170 | ) 171 | 172 | if not res.success: 173 | msg = ( 174 | """scipy.optimize.minimize_scalar did not succeed. 175 | The message it returned is %s""" 176 | % res.message 177 | ) 178 | raise Exception(msg) 179 | 180 | return res 181 | 182 | 183 | def maximize_scalar(max_func, bounds, *args, **options): 184 | """Finds the input value that maximizes `max_func`. 185 | 186 | Wrapper for https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize_scalar.html 187 | 188 | min_func: computes the function to be maximized 189 | bounds: sequence of two values, lower and upper bounds of the 190 | range to be searched 191 | args: any additional positional arguments are passed to max_func 192 | options: any keyword arguments are passed as options to minimize_scalar 193 | 194 | returns: ModSimSeries object 195 | """ 196 | def min_func(*args): 197 | return -max_func(*args) 198 | 199 | res = minimize_scalar(min_func, bounds, *args, **options) 200 | 201 | # we have to negate the function value before returning res 202 | res.fun = -res.fun 203 | return res 204 | 205 | 206 | def minimize_golden(min_func, bracket, *args, **options): 207 | """Find the minimum of a function by golden section search. 208 | 209 | Based on 210 | https://en.wikipedia.org/wiki/Golden-section_search#Iterative_algorithm 211 | 212 | :param min_func: function to be minimized 213 | :param bracket: interval containing a minimum 214 | :param args: arguments passes to min_func 215 | :param options: rtol and maxiter 216 | 217 | :return: ModSimSeries 218 | """ 219 | maxiter = options.get("maxiter", 100) 220 | rtol = options.get("rtol", 1e-3) 221 | 222 | def success(**kwargs): 223 | return ModSimSeries(dict(success=True, **kwargs)) 224 | 225 | def failure(**kwargs): 226 | return ModSimSeries(dict(success=False, **kwargs)) 227 | 228 | a, b = bracket 229 | ya = min_func(a, *args) 230 | yb = min_func(b, *args) 231 | 232 | phi = 2 / (np.sqrt(5) - 1) 233 | h = b - a 234 | c = b - h / phi 235 | yc = min_func(c, *args) 236 | 237 | d = a + h / phi 238 | yd = min_func(d, *args) 239 | 240 | if yc > ya or yc > yb: 241 | return failure(message="The bracket is not well-formed.") 242 | 243 | for i in range(maxiter): 244 | 245 | # check for convergence 246 | if abs(h / c) < rtol: 247 | return success(x=c, fun=yc) 248 | 249 | if yc < yd: 250 | b, yb = d, yd 251 | d, yd = c, yc 252 | h = b - a 253 | c = b - h / phi 254 | yc = min_func(c, *args) 255 | else: 256 | a, ya = c, yc 257 | c, yc = d, yd 258 | h = b - a 259 | d = a + h / phi 260 | yd = min_func(d, *args) 261 | 262 | # if we exited the loop, too many iterations 263 | return failure(root=c, message="maximum iterations = %d exceeded" % maxiter) 264 | 265 | 266 | def maximize_golden(max_func, bracket, *args, **options): 267 | """Find the maximum of a function by golden section search. 268 | 269 | :param min_func: function to be maximized 270 | :param bracket: interval containing a maximum 271 | :param args: arguments passes to min_func 272 | :param options: rtol and maxiter 273 | 274 | :return: ModSimSeries 275 | """ 276 | 277 | def min_func(*args): 278 | return -max_func(*args) 279 | 280 | res = minimize_golden(min_func, bracket, *args, **options) 281 | 282 | # we have to negate the function value before returning res 283 | res.fun = -res.fun 284 | return res 285 | 286 | 287 | def minimize_powell(min_func, x0, *args, **options): 288 | """Finds the input value that minimizes `min_func`. 289 | Wrapper for https://docs.scipy.org/doc/scipy/reference/generated/scipy.optimize.minimize.html 290 | min_func: computes the function to be minimized 291 | x0: initial guess 292 | args: any additional positional arguments are passed to min_func 293 | options: any keyword arguments are passed as options to minimize_scalar 294 | returns: ModSimSeries object 295 | """ 296 | underride(options, tol=1e-3) 297 | 298 | res = scipy.optimize.minimize(min_func, x0, *args, **options) 299 | 300 | return ModSimSeries(res) 301 | 302 | 303 | # make aliases for minimize and maximize 304 | minimize = minimize_golden 305 | maximize = maximize_golden 306 | 307 | 308 | def run_solve_ivp(system, slope_func, **options): 309 | """Computes a numerical solution to a differential equation. 310 | 311 | `system` must contain `init` with initial conditions, 312 | `t_0` with the start time, and `t_end` with the end time. 313 | 314 | It can contain any other parameters required by the slope function. 315 | 316 | `options` can be any legal options of `scipy.integrate.solve_ivp` 317 | 318 | system: System object 319 | slope_func: function that computes slopes 320 | 321 | returns: TimeFrame 322 | """ 323 | system = remove_units(system) 324 | 325 | # make sure `system` contains `init` 326 | if not hasattr(system, "init"): 327 | msg = """It looks like `system` does not contain `init` 328 | as a system variable. `init` should be a State 329 | object that specifies the initial condition:""" 330 | raise ValueError(msg) 331 | 332 | # make sure `system` contains `t_end` 333 | if not hasattr(system, "t_end"): 334 | msg = """It looks like `system` does not contain `t_end` 335 | as a system variable. `t_end` should be the 336 | final time:""" 337 | raise ValueError(msg) 338 | 339 | # the default value for t_0 is 0 340 | t_0 = getattr(system, "t_0", 0) 341 | 342 | # try running the slope function with the initial conditions 343 | try: 344 | slope_func(t_0, system.init, system) 345 | except Exception as e: 346 | msg = """Before running scipy.integrate.solve_ivp, I tried 347 | running the slope function you provided with the 348 | initial conditions in `system` and `t=t_0` and I got 349 | the following error:""" 350 | logger.error(msg) 351 | raise (e) 352 | 353 | # get the list of event functions 354 | events = options.get('events', []) 355 | 356 | # if there's only one event function, put it in a list 357 | try: 358 | iter(events) 359 | except TypeError: 360 | events = [events] 361 | 362 | for event_func in events: 363 | # make events terminal unless otherwise specified 364 | if not hasattr(event_func, 'terminal'): 365 | event_func.terminal = True 366 | 367 | # test the event function with the initial conditions 368 | try: 369 | event_func(t_0, system.init, system) 370 | except Exception as e: 371 | msg = """Before running scipy.integrate.solve_ivp, I tried 372 | running the event function you provided with the 373 | initial conditions in `system` and `t=t_0` and I got 374 | the following error:""" 375 | logger.error(msg) 376 | raise (e) 377 | 378 | # get dense output unless otherwise specified 379 | underride(options, dense_output=True) 380 | 381 | # run the solver 382 | bunch = solve_ivp(slope_func, [t_0, system.t_end], system.init, 383 | args=[system], **options) 384 | 385 | # separate the results from the details 386 | y = bunch.pop("y") 387 | t = bunch.pop("t") 388 | 389 | # get the column names from `init`, if possible 390 | if hasattr(system.init, 'index'): 391 | columns = system.init.index 392 | else: 393 | columns = range(len(system.init)) 394 | 395 | # evaluate the results at equally-spaced points 396 | if options.get('dense_output', False): 397 | try: 398 | num = system.num 399 | except AttributeError: 400 | num = 51 401 | t_final = t[-1] 402 | t_array = linspace(t_0, t_final, num) 403 | y_array = bunch.sol(t_array) 404 | 405 | # pack the results into a TimeFrame 406 | results = TimeFrame(y_array.T, index=t_array, 407 | columns=columns) 408 | else: 409 | results = TimeFrame(y.T, index=t, 410 | columns=columns) 411 | 412 | return results, bunch 413 | 414 | 415 | def check_system(system, slope_func): 416 | """Make sure the system object has the fields we need for run_ode_solver. 417 | 418 | :param system: 419 | :param slope_func: 420 | :return: 421 | """ 422 | # make sure `system` contains `init` 423 | if not hasattr(system, "init"): 424 | msg = """It looks like `system` does not contain `init` 425 | as a system variable. `init` should be a State 426 | object that specifies the initial condition:""" 427 | raise ValueError(msg) 428 | 429 | # make sure `system` contains `t_end` 430 | if not hasattr(system, "t_end"): 431 | msg = """It looks like `system` does not contain `t_end` 432 | as a system variable. `t_end` should be the 433 | final time:""" 434 | raise ValueError(msg) 435 | 436 | # the default value for t_0 is 0 437 | t_0 = getattr(system, "t_0", 0) 438 | 439 | # get the initial conditions 440 | init = system.init 441 | 442 | # get t_end 443 | t_end = system.t_end 444 | 445 | # if dt is not specified, take 100 steps 446 | try: 447 | dt = system.dt 448 | except AttributeError: 449 | dt = t_end / 100 450 | 451 | return init, t_0, t_end, dt 452 | 453 | 454 | def run_euler(system, slope_func, **options): 455 | """Computes a numerical solution to a differential equation. 456 | 457 | `system` must contain `init` with initial conditions, 458 | `t_end` with the end time, and `dt` with the time step. 459 | 460 | `system` may contain `t_0` to override the default, 0 461 | 462 | It can contain any other parameters required by the slope function. 463 | 464 | `options` can be ... 465 | 466 | system: System object 467 | slope_func: function that computes slopes 468 | 469 | returns: TimeFrame 470 | """ 471 | # the default message if nothing changes 472 | msg = "The solver successfully reached the end of the integration interval." 473 | 474 | # get parameters from system 475 | init, t_0, t_end, dt = check_system(system, slope_func) 476 | 477 | # make the TimeFrame 478 | frame = TimeFrame(columns=init.index) 479 | frame.row[t_0] = init 480 | ts = linrange(t_0, t_end, dt) * get_units(t_end) 481 | 482 | # run the solver 483 | for t1 in ts: 484 | y1 = frame.row[t1] 485 | slopes = slope_func(y1, t1, system) 486 | y2 = [y + slope * dt for y, slope in zip(y1, slopes)] 487 | t2 = t1 + dt 488 | frame.row[t2] = y2 489 | 490 | details = ModSimSeries(dict(message="Success")) 491 | return frame, details 492 | 493 | 494 | def run_ralston(system, slope_func, **options): 495 | """Computes a numerical solution to a differential equation. 496 | 497 | `system` must contain `init` with initial conditions, 498 | and `t_end` with the end time. 499 | 500 | `system` may contain `t_0` to override the default, 0 501 | 502 | It can contain any other parameters required by the slope function. 503 | 504 | `options` can be ... 505 | 506 | system: System object 507 | slope_func: function that computes slopes 508 | 509 | returns: TimeFrame 510 | """ 511 | # the default message if nothing changes 512 | msg = "The solver successfully reached the end of the integration interval." 513 | 514 | # get parameters from system 515 | init, t_0, t_end, dt = check_system(system, slope_func) 516 | 517 | # make the TimeFrame 518 | frame = TimeFrame(columns=init.index) 519 | frame.row[t_0] = init 520 | ts = linrange(t_0, t_end, dt) * get_units(t_end) 521 | 522 | event_func = options.get("events", None) 523 | z1 = np.nan 524 | 525 | def project(y1, t1, slopes, dt): 526 | t2 = t1 + dt 527 | y2 = [y + slope * dt for y, slope in zip(y1, slopes)] 528 | return y2, t2 529 | 530 | # run the solver 531 | for t1 in ts: 532 | y1 = frame.row[t1] 533 | 534 | # evaluate the slopes at the start of the time step 535 | slopes1 = slope_func(y1, t1, system) 536 | 537 | # evaluate the slopes at the two-thirds point 538 | y_mid, t_mid = project(y1, t1, slopes1, 2 * dt / 3) 539 | slopes2 = slope_func(y_mid, t_mid, system) 540 | 541 | # compute the weighted sum of the slopes 542 | slopes = [(k1 + 3 * k2) / 4 for k1, k2 in zip(slopes1, slopes2)] 543 | 544 | # compute the next time stamp 545 | y2, t2 = project(y1, t1, slopes, dt) 546 | 547 | # check for a terminating event 548 | if event_func: 549 | z2 = event_func(y2, t2, system) 550 | if z1 * z2 < 0: 551 | scale = magnitude(z1 / (z1 - z2)) 552 | y2, t2 = project(y1, t1, slopes, scale * dt) 553 | frame.row[t2] = y2 554 | msg = "A termination event occurred." 555 | break 556 | else: 557 | z1 = z2 558 | 559 | # store the results 560 | frame.row[t2] = y2 561 | 562 | details = ModSimSeries(dict(success=True, message=msg)) 563 | return frame, details 564 | 565 | 566 | run_ode_solver = run_ralston 567 | 568 | # TODO: Implement leapfrog 569 | 570 | 571 | def fsolve(func, x0, *args, **options): 572 | """Return the roots of the (non-linear) equations 573 | defined by func(x) = 0 given a starting estimate. 574 | 575 | Uses scipy.optimize.fsolve, with extra error-checking. 576 | 577 | func: function to find the roots of 578 | x0: scalar or array, initial guess 579 | args: additional positional arguments are passed along to fsolve, 580 | which passes them along to func 581 | 582 | returns: solution as an array 583 | """ 584 | # make sure we can run the given function with x0 585 | try: 586 | func(x0, *args) 587 | except Exception as e: 588 | msg = """Before running scipy.optimize.fsolve, I tried 589 | running the error function you provided with the x0 590 | you provided, and I got the following error:""" 591 | logger.error(msg) 592 | raise (e) 593 | 594 | # make the tolerance more forgiving than the default 595 | underride(options, xtol=1e-6) 596 | 597 | # run fsolve 598 | result = scipy.optimize.fsolve(func, x0, args=args, **options) 599 | 600 | return result 601 | 602 | 603 | def crossings(series, value): 604 | """Find the labels where the series passes through value. 605 | 606 | The labels in series must be increasing numerical values. 607 | 608 | series: Series 609 | value: number 610 | 611 | returns: sequence of labels 612 | """ 613 | values = series.values - value 614 | interp = InterpolatedUnivariateSpline(series.index, values) 615 | return interp.roots() 616 | 617 | 618 | def has_nan(a): 619 | """Checks whether the an array contains any NaNs. 620 | 621 | :param a: NumPy array or Pandas Series 622 | :return: boolean 623 | """ 624 | return np.any(np.isnan(a)) 625 | 626 | 627 | def is_strictly_increasing(a): 628 | """Checks whether the elements of an array are strictly increasing. 629 | 630 | :param a: NumPy array or Pandas Series 631 | :return: boolean 632 | """ 633 | return np.all(np.diff(a) > 0) 634 | 635 | 636 | def interpolate(series, **options): 637 | """Creates an interpolation function. 638 | 639 | series: Series object 640 | options: any legal options to scipy.interpolate.interp1d 641 | 642 | returns: function that maps from the index to the values 643 | """ 644 | if has_nan(series.index): 645 | msg = """The Series you passed to interpolate contains 646 | NaN values in the index, which would result in 647 | undefined behavior. So I'm putting a stop to that.""" 648 | raise ValueError(msg) 649 | 650 | if not is_strictly_increasing(series.index): 651 | msg = """The Series you passed to interpolate has an index 652 | that is not strictly increasing, which would result in 653 | undefined behavior. So I'm putting a stop to that.""" 654 | raise ValueError(msg) 655 | 656 | # make the interpolate function extrapolate past the ends of 657 | # the range, unless `options` already specifies a value for `fill_value` 658 | underride(options, fill_value="extrapolate") 659 | 660 | # call interp1d, which returns a new function object 661 | x = series.index 662 | y = series.values 663 | interp_func = interp1d(x, y, **options) 664 | return interp_func 665 | 666 | 667 | def interpolate_inverse(series, **options): 668 | """Interpolate the inverse function of a Series. 669 | 670 | series: Series object, represents a mapping from `a` to `b` 671 | options: any legal options to scipy.interpolate.interp1d 672 | 673 | returns: interpolation object, can be used as a function 674 | from `b` to `a` 675 | """ 676 | inverse = Series(series.index, index=series.values) 677 | interp_func = interpolate(inverse, **options) 678 | return interp_func 679 | 680 | 681 | def gradient(series, **options): 682 | """Computes the numerical derivative of a series. 683 | 684 | If the elements of series have units, they are dropped. 685 | 686 | series: Series object 687 | options: any legal options to np.gradient 688 | 689 | returns: Series, same subclass as series 690 | """ 691 | x = series.index 692 | y = series.values 693 | 694 | a = np.gradient(y, x, **options) 695 | return series.__class__(a, series.index) 696 | 697 | 698 | def source_code(obj): 699 | """Prints the source code for a given object. 700 | 701 | obj: function or method object 702 | """ 703 | print(inspect.getsource(obj)) 704 | 705 | 706 | def underride(d, **options): 707 | """Add key-value pairs to d only if key is not in d. 708 | 709 | If d is None, create a new dictionary. 710 | 711 | d: dictionary 712 | options: keyword args to add to d 713 | """ 714 | if d is None: 715 | d = {} 716 | 717 | for key, val in options.items(): 718 | d.setdefault(key, val) 719 | 720 | return d 721 | 722 | 723 | def contour(df, **options): 724 | """Makes a contour plot from a DataFrame. 725 | 726 | Wrapper for plt.contour 727 | https://matplotlib.org/3.1.0/api/_as_gen/matplotlib.pyplot.contour.html 728 | 729 | Note: columns and index must be numerical 730 | 731 | df: DataFrame 732 | options: passed to plt.contour 733 | """ 734 | fontsize = options.pop("fontsize", 12) 735 | underride(options, cmap="viridis") 736 | x = df.columns 737 | y = df.index 738 | X, Y = np.meshgrid(x, y) 739 | cs = plt.contour(X, Y, df, **options) 740 | plt.clabel(cs, inline=1, fontsize=fontsize) 741 | 742 | 743 | def savefig(filename, **options): 744 | """Save the current figure. 745 | 746 | Keyword arguments are passed along to plt.savefig 747 | 748 | https://matplotlib.org/api/_as_gen/matplotlib.pyplot.savefig.html 749 | 750 | filename: string 751 | """ 752 | print("Saving figure to file", filename) 753 | plt.savefig(filename, **options) 754 | 755 | 756 | def decorate(**options): 757 | """Decorate the current axes. 758 | 759 | Call decorate with keyword arguments like 760 | decorate(title='Title', 761 | xlabel='x', 762 | ylabel='y') 763 | 764 | The keyword arguments can be any of the axis properties 765 | https://matplotlib.org/api/axes_api.html 766 | """ 767 | ax = plt.gca() 768 | ax.set(**options) 769 | 770 | handles, labels = ax.get_legend_handles_labels() 771 | if handles: 772 | ax.legend(handles, labels) 773 | 774 | plt.tight_layout() 775 | 776 | 777 | def remove_from_legend(bad_labels): 778 | """Removes some labels from the legend. 779 | 780 | bad_labels: sequence of strings 781 | """ 782 | ax = plt.gca() 783 | handles, labels = ax.get_legend_handles_labels() 784 | handle_list, label_list = [], [] 785 | for handle, label in zip(handles, labels): 786 | if label not in bad_labels: 787 | handle_list.append(handle) 788 | label_list.append(label) 789 | ax.legend(handle_list, label_list) 790 | 791 | 792 | class SettableNamespace(SimpleNamespace): 793 | """Contains a collection of parameters. 794 | 795 | Used to make a System object. 796 | 797 | Takes keyword arguments and stores them as attributes. 798 | """ 799 | def __init__(self, namespace=None, **kwargs): 800 | super().__init__() 801 | if namespace: 802 | self.__dict__.update(namespace.__dict__) 803 | self.__dict__.update(kwargs) 804 | 805 | def get(self, name, default=None): 806 | """Look up a variable. 807 | 808 | name: string varname 809 | default: value returned if `name` is not present 810 | """ 811 | try: 812 | return self.__getattribute__(name, default) 813 | except AttributeError: 814 | return default 815 | 816 | def set(self, **variables): 817 | """Make a copy and update the given variables. 818 | 819 | returns: Params 820 | """ 821 | new = copy(self) 822 | new.__dict__.update(variables) 823 | return new 824 | 825 | 826 | def magnitude(x): 827 | """Returns the magnitude of a Quantity or number. 828 | 829 | x: Quantity or number 830 | 831 | returns: number 832 | """ 833 | return x.magnitude if hasattr(x, 'magnitude') else x 834 | 835 | 836 | def remove_units(namespace): 837 | """Removes units from the values in a Namespace. 838 | 839 | Only removes units from top-level values; 840 | does not traverse nested values. 841 | 842 | returns: new Namespace object 843 | """ 844 | res = copy(namespace) 845 | for label, value in res.__dict__.items(): 846 | if isinstance(value, pd.Series): 847 | value = remove_units_series(value) 848 | res.__dict__[label] = magnitude(value) 849 | return res 850 | 851 | 852 | def remove_units_series(series): 853 | """Removes units from the values in a Series. 854 | 855 | Only removes units from top-level values; 856 | does not traverse nested values. 857 | 858 | returns: new Series object 859 | """ 860 | res = copy(series) 861 | for label, value in res.iteritems(): 862 | res[label] = magnitude(value) 863 | return res 864 | 865 | 866 | class System(SettableNamespace): 867 | """Contains system parameters and their values. 868 | 869 | Takes keyword arguments and stores them as attributes. 870 | """ 871 | pass 872 | 873 | 874 | class Params(SettableNamespace): 875 | """Contains system parameters and their values. 876 | 877 | Takes keyword arguments and stores them as attributes. 878 | """ 879 | pass 880 | 881 | 882 | def State(**variables): 883 | """Contains the values of state variables.""" 884 | return pd.Series(variables) 885 | 886 | 887 | def TimeSeries(*args, **kwargs): 888 | """ 889 | """ 890 | if args or kwargs: 891 | series = pd.Series(*args, **kwargs) 892 | else: 893 | series = pd.Series([], dtype=np.float64) 894 | 895 | series.index.name = 'Time' 896 | if 'name' not in kwargs: 897 | series.name = 'Quantity' 898 | return series 899 | 900 | 901 | def SweepSeries(*args, **kwargs): 902 | """ 903 | """ 904 | if args or kwargs: 905 | series = pd.Series(*args, **kwargs) 906 | else: 907 | series = pd.Series([], dtype=np.float64) 908 | 909 | series.index.name = 'Parameter' 910 | if 'name' not in kwargs: 911 | series.name = 'Metric' 912 | return series 913 | 914 | 915 | def TimeFrame(*args, **kwargs): 916 | """DataFrame that maps from time to State. 917 | """ 918 | return pd.DataFrame(*args, **kwargs) 919 | 920 | 921 | def SweepFrame(*args, **kwargs): 922 | """DataFrame that maps from parameter value to SweepSeries. 923 | """ 924 | return pd.DataFrame(*args, **kwargs) 925 | 926 | 927 | def Vector(x, y, z=None, **options): 928 | """ 929 | """ 930 | if z is None: 931 | return pd.Series(dict(x=x, y=y), **options) 932 | else: 933 | return pd.Series(dict(x=x, y=y, z=z), **options) 934 | 935 | 936 | ## Vector functions (should work with any sequence) 937 | 938 | def vector_mag(v): 939 | """Vector magnitude.""" 940 | return np.sqrt(np.dot(v, v)) 941 | 942 | 943 | def vector_mag2(v): 944 | """Vector magnitude squared.""" 945 | return np.dot(v, v) 946 | 947 | 948 | def vector_angle(v): 949 | """Angle between v and the positive x axis. 950 | 951 | Only works with 2-D vectors. 952 | 953 | returns: angle in radians 954 | """ 955 | assert len(v) == 2 956 | x, y = v 957 | return np.arctan2(y, x) 958 | 959 | 960 | def vector_polar(v): 961 | """Vector magnitude and angle. 962 | 963 | returns: (number, angle in radians) 964 | """ 965 | return vector_mag(v), vector_angle(v) 966 | 967 | 968 | def vector_hat(v): 969 | """Unit vector in the direction of v. 970 | 971 | returns: Vector or array 972 | """ 973 | # check if the magnitude of the Quantity is 0 974 | mag = vector_mag(v) 975 | if mag == 0: 976 | return v 977 | else: 978 | return v / mag 979 | 980 | 981 | def vector_perp(v): 982 | """Perpendicular Vector (rotated left). 983 | 984 | Only works with 2-D Vectors. 985 | 986 | returns: Vector 987 | """ 988 | assert len(v) == 2 989 | x, y = v 990 | return Vector(-y, x) 991 | 992 | 993 | def vector_dot(v, w): 994 | """Dot product of v and w. 995 | 996 | returns: number or Quantity 997 | """ 998 | return np.dot(v, w) 999 | 1000 | 1001 | def vector_cross(v, w): 1002 | """Cross product of v and w. 1003 | 1004 | returns: number or Quantity for 2-D, Vector for 3-D 1005 | """ 1006 | res = np.cross(v, w) 1007 | 1008 | if len(v) == 3: 1009 | return Vector(*res) 1010 | else: 1011 | return res 1012 | 1013 | 1014 | def vector_proj(v, w): 1015 | """Projection of v onto w. 1016 | 1017 | returns: array or Vector with direction of w and units of v. 1018 | """ 1019 | w_hat = vector_hat(w) 1020 | return vector_dot(v, w_hat) * w_hat 1021 | 1022 | 1023 | def scalar_proj(v, w): 1024 | """Returns the scalar projection of v onto w. 1025 | 1026 | Which is the magnitude of the projection of v onto w. 1027 | 1028 | returns: scalar with units of v. 1029 | """ 1030 | return vector_dot(v, vector_hat(w)) 1031 | 1032 | 1033 | def vector_dist(v, w): 1034 | """Euclidean distance from v to w, with units.""" 1035 | if isinstance(v, list): 1036 | v = np.asarray(v) 1037 | return vector_mag(v - w) 1038 | 1039 | 1040 | def vector_diff_angle(v, w): 1041 | """Angular difference between two vectors, in radians. 1042 | """ 1043 | if len(v) == 2: 1044 | return vector_angle(v) - vector_angle(w) 1045 | else: 1046 | # TODO: see http://www.euclideanspace.com/maths/algebra/ 1047 | # vectors/angleBetween/ 1048 | raise NotImplementedError() 1049 | 1050 | 1051 | def plot_segment(A, B, **options): 1052 | """Plots a line segment between two Vectors. 1053 | 1054 | For 3-D vectors, the z axis is ignored. 1055 | 1056 | Additional options are passed along to plot(). 1057 | 1058 | A: Vector 1059 | B: Vector 1060 | """ 1061 | xs = A.x, B.x 1062 | ys = A.y, B.y 1063 | plot(xs, ys, **options) 1064 | 1065 | 1066 | from time import sleep 1067 | from IPython.display import clear_output 1068 | 1069 | def animate(results, draw_func, *args, interval=None): 1070 | """Animate results from a simulation. 1071 | 1072 | results: TimeFrame 1073 | draw_func: function that draws state 1074 | interval: time between frames in seconds 1075 | """ 1076 | plt.figure() 1077 | try: 1078 | for t, state in results.iterrows(): 1079 | draw_func(t, state, *args) 1080 | plt.show() 1081 | if interval: 1082 | sleep(interval) 1083 | clear_output(wait=True) 1084 | draw_func(t, state, *args) 1085 | plt.show() 1086 | except KeyboardInterrupt: 1087 | pass 1088 | --------------------------------------------------------------------------------