├── .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 |
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 | "\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 | "\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 | "\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 | "\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 | ""
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 | "{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 | "{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 | "{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 | "{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 | "{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 |
--------------------------------------------------------------------------------