Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/book/book.tex
Views: 531
% LaTeX source for ``Modeling and Simulation in Python''1% Copyright 2017 Allen B. Downey.23% License: Creative Commons Attribution-NonCommercial 4.0 Unported License.4% https://creativecommons.org/licenses/by-nc/4.0/5%67\documentclass[12pt]{book}89\title{Modeling and Simulation in Python}10\author{Allen B. Downey}1112\newcommand{\thetitle}{Modeling and Simulation in Python}13\newcommand{\thesubtitle}{}14\newcommand{\theauthors}{Allen B. Downey}15\newcommand{\theversion}{3.4.3}161718%%%% Both LATEX and PLASTEX1920\usepackage{graphicx}21\usepackage{hevea}22\usepackage{makeidx}23\usepackage{setspace}24\usepackage{upquote}25\usepackage{xcolor}26\usepackage[listings]{tcolorbox}272829% to get siunitx30% sudo apt-get install texlive-science31\usepackage{siunitx}32\sisetup{per-mode=symbol}3334\definecolor{light-gray}{gray}{0.95}3536\newtcblisting{python}{37skin=standard,38boxrule=0.4pt,39%colback=light-gray,40listing only,41top=0pt,42bottom=0pt,43left=0pt,44right=0pt,45boxsep=2pt,46listing options={47basicstyle=\ttfamily,48language=python,49showstringspaces=false,50},51}5253\newtcblisting{result}{54skin=standard,55boxrule=0.0pt,56colback=white,57listing only,58top=0pt,59bottom=0pt,60left=0pt,61right=0pt,62boxsep=2pt,63listing options={64basicstyle=\ttfamily,65language=python,66showstringspaces=false,67},68}6970\makeindex7172% automatically index glossary terms73\newcommand{\term}[1]{%74\item[#1:]\index{#1}}7576\usepackage{amsmath}77\usepackage{amsthm}7879% format end of chapter excercises80\newtheoremstyle{exercise}81{12pt} % space above82{12pt} % space below83{} % body font84{} % indent amount85{\bfseries} % head font86{} % punctuation87{12pt} % head space88{} % custom head89\theoremstyle{exercise}90\newtheorem{exercise}{Exercise}[chapter]9192\usepackage{afterpage}9394\newcommand\blankpage{%95\null96\thispagestyle{empty}%97\addtocounter{page}{-1}%98\newpage}99100\newif\ifplastex101\plastexfalse102103%%%% PLASTEX ONLY104\ifplastex105106\usepackage{localdef}107108\usepackage{url}109110\newcount\anchorcnt111\newcommand*{\Anchor}[1]{%112\@bsphack%113\Hy@GlobalStepCount\anchorcnt%114\edef\@currentHref{anchor.\the\anchorcnt}%115\Hy@raisedlink{\hyper@anchorstart{\@currentHref}\hyper@anchorend}%116\M@gettitle{}\label{#1}%117\@esphack%118}119120% code listing environments:121% we don't need these for plastex because they get replaced122% by preprocess.py123%\newenvironment{code}{\begin{code}}{\end{code}}124%\newenvironment{stdout}{\begin{code}}{\end{code}}125126% inline syntax formatting127\newcommand{\py}{\verb}%}128129%%%% LATEX ONLY130\else131132\input{latexonly}133134\fi135136%%%% END OF PREAMBLE137\begin{document}138139\frontmatter140141%%%% PLASTEX ONLY142\ifplastex143144\maketitle145146%%%% LATEX ONLY147\else148149\begin{latexonly}150151%-half title--------------------------------------------------152%\thispagestyle{empty}153%154%\begin{flushright}155%\vspace*{2.0in}156%157%\begin{spacing}{3}158%{\huge \thetitle}159%\end{spacing}160%161%\vspace{0.25in}162%163%Version \theversion164%165%\vfill166%167%\end{flushright}168169%--verso------------------------------------------------------170171%\afterpage{\blankpage}172173%\newpage174%\newpage175%\clearemptydoublepage176%\pagebreak177%\thispagestyle{empty}178%\vspace*{6in}179180%--title page--------------------------------------------------181\pagebreak182\thispagestyle{empty}183184\begin{flushright}185\vspace*{2.0in}186187\begin{spacing}{3}188{\huge \thetitle}189\end{spacing}190191\vspace{0.25in}192193Version \theversion194195\vspace{1in}196197198{\Large199\theauthors \\200}201202203\vspace{0.5in}204205{\Large Green Tea Press}206207{\small Needham, Massachusetts}208209%\includegraphics[width=1in]{figs/logo1.eps}210\vfill211212\end{flushright}213214215216%--copyright--------------------------------------------------217\pagebreak218\thispagestyle{empty}219220Copyright \copyright ~2017 \theauthors.221222223224\vspace{0.2in}225226\begin{flushleft}227Green Tea Press \\2289 Washburn Ave \\229Needham MA 02492230\end{flushleft}231232Permission is granted to copy, distribute, transmit and adapt this work under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License: \url{http://modsimpy.com/license}.233234% TODO: get the shortened URLs working with https235236If you are interested in distributing a commercial version of this237work, please contact the author.238239The \LaTeX\ source and code for this book is available from240241\begin{code}242https://github.com/AllenDowney/ModSimPy243\end{code}244245246Cover design by Tim Sauder.247248%--table of contents------------------------------------------249250\cleardoublepage251\setcounter{tocdepth}{1}252\tableofcontents253254\end{latexonly}255256257% HTML title page------------------------------------------258259\begin{htmlonly}260261\vspace{1em}262263{\Large \thetitle}264265{\large \theauthors}266267Version \theversion268269\vspace{1em}270271Copyright \copyright ~2017 \theauthors.272273Permission is granted to copy, distribute, and/or modify this work274under the terms of the Creative Commons275Attribution-NonCommercial-ShareAlike 4.0 International License, which is276available at \url{http://modsimpy.com/license}.277278\vspace{1em}279280\setcounter{chapter}{-1}281282\end{htmlonly}283284% END OF THE PART WE SKIP FOR PLASTEX285\fi286287\chapter{Preface}288\label{preface}289290291\section{Can modeling be taught?}292293The essential skills of modeling --- abstraction, analysis, simulation, and validation --- are central in engineering, natural sciences, social sciences, medicine, and many other fields. Some students learn these skills implicitly, but in most schools they are not taught explicitly, and students get little practice. That's the problem this book is meant to address.294295At Olin College, we use this book in a class called Modeling and Simulation, which all students take in their first semester. My colleagues, John Geddes and Mark Somerville, and I developed this class and taught it for the first time in 2009.296297It is based on our belief that modeling should be taught explicitly, early, and throughout the curriculum. It is also based on our conviction that computation is an essential part of this process.298299If students are limited to the mathematical analysis they can do by hand, they are restricted to a small number of simple physical systems, like a projectile moving in a vacuum or a block on a frictionless plane.300301And they will only work with bad models; that is, models that are too simple for their intended purpose. In nearly every mechanical system, air resistance and friction are essential features; if we ignore them, our predictions will be wrong and our designs won't work.302303In most freshman physics classes, students don't make modeling decisions; sometimes they are not even aware of the decisions that have been made for them. Our goal is to teach the entire modeling process and give students a chance to practice it.304305306\section{How much programming do I need?}307308If you have never programmed before, you should be able to read this book, understand it, and do the exercises. I will do my best to explain everything you need to know; in particular, I have chosen carefully the vocabulary I introduce, and I try to define each term the first time it is used. If you find that I have used a term without defining it, let me know.309310If you have programmed before, you will have an easier time getting started, but you might be uncomfortable in some places. I take an approach to programming you have probably not seen before.311312Most programming classes\footnote{Including many I have taught.} have two big problems:313314\begin{enumerate}315316\item They go ``bottom up", starting with basic language features and gradually adding more powerful tools. As a result, it takes a long time before students can do anything more interesting than convert Fahrenheit to Celsius.317318\index{bottom up}319320\item They have no context. Students learn to program with no particular goal in mind, so the exercises span an incoherent collection of topics, and the exercises tend to be unmotivated.321322\end{enumerate}323324In this book, you learn to program with an immediate goal in mind: writing simulations of physical systems. And we proceed ``top down", by which I mean we use professional-strength data structures and language features right away. In particular, we use the following Python {\bf libraries}:325326\index{top down}327328\begin{itemize}329330\item NumPy for basic numerical computation (see \url{https://www.numpy.org/}).331332\index{NumPy}333334\item SciPy for scientific computation (see \url{https://www.scipy.org/}).335336\index{SciPy}337338\item Matplotlib for visualization (see \url{https://matplotlib.org/}).339340\index{Matplotlib}341342\item Pandas for working with data (see \url{https://pandas.pydata.org/}).343344\index{Pandas}345346\item SymPy for symbolic computation, (see \url{https://www.sympy.org}).347348\index{SymPy}349350\item Pint for units like kilograms and meters (see \url{https://pint.readthedocs.io}).351352\index{Pint}353354\item Jupyter for reading, running, and developing code (see \url{https://jupyter.org}).355356\index{Jupyter}357358\end{itemize}359360These tools let you work on more interesting programs sooner, but there are some drawbacks: they can be hard to use, and it can be challenging to keep track of which library does what and how they interact.361362I have tried to mitigate these problems by providing a library, called \py{modsim}, that makes it easier to get started with these tools, and provides some additional capabilities.363364\index{ModSim library}365366Some features in the ModSim library are like training wheels; at some point you will probably stop using them and start working with the underlying libraries directly. Other features you might find useful the whole time you are working through the book, and later.367368I encourage you to read the ModSim library code. Most of it is not complicated, and I tried to make it readable. Particularly if you have some programming experience, you might learn something by reverse engineering my design decisions.369370371\section{How much math and science do I need?}372373I assume that you know what derivatives and integrals are, but that's about all. In particular, you don't need to know (or remember) much about finding derivatives or integrals of functions analytically. If you know the derivative of $x^2$ and you can integrate $2x~dx$, that will do it\footnote{And if you noticed that those two questions answer each other, even better.}. More importantly, you should understand what those concepts {\em mean}; but if you don't, this book might help you figure it out.374375\index{calculus}376377You don't have to know anything about differential equations.378379As for science, we will cover topics from a variety of fields, including demography, epidemiology, medicine, thermodynamics, and mechanics. For the most part, I don't assume you know anything about these topics. In fact, one of the skills you need to do modeling is the ability to learn enough about new fields to develop models and simulations.380381When we get to mechanics, I assume you understand the relationship between position, velocity, and acceleration, and that you are familiar with Newton's laws of motion, especially the second law, which is often expressed as $F = ma$ (force equals mass times acceleration).382383\index{science}384\index{mechanics}385386I think that's everything you need, but if you find that I left something out, please let me know.387388389\section{Getting started}390\label{code}391392To run the examples and work on the exercises in this book, you have to:393394\begin{enumerate}395396\item Install Python on your computer, along with the libraries we will use.397398\item Copy my files onto your computer.399400\item Run Jupyter, which is a tool for running and writing programs, and load a {\bf notebook}, which is a file that contains code and text.401402\end{enumerate}403404The next three sections provide details for these steps. I wish there were an easier way to get started; it's regrettable that you have to do so much work before you write your first program. Be persistent!405406407\section{Installing Python}408409You might already have Python installed on your computer, but you might not have the latest version. To use the code in this book, you need Python 3.6 or later. Even if you have the latest version, you probably don't have all of the libraries we need.410411\index{installing Python}412413You could update Python and install these libraries, but I strongly recommend that you don't go down that road. I think you will find it easier to use {\bf Anaconda}, which is a free Python distribution that includes all the libraries you need for this book (and more).414415\index{Anaconda}416417Anaconda is available for Linux, macOS, and Windows. By default, it puts all files in your home directory, so you don't need administrator (root) permission to install it, and if you have a version of Python already, Anaconda will not remove or modify it.418419Start at \url{https://www.anaconda.com/download}. Download the installer for your system and run it. You don't need administrative privileges to install Anaconda, so I recommend you run the installer as a normal user, not as administrator or root.420421I suggest you accept the recommended options.422On Windows you have the option to install Visual Studio Code, which is an interactive environment for writing programs. You won't need it for this book, but you might want it for other projects.423424By default, Anaconda installs most of the packages you need, but there are a few more you have to add. Once the installation is complete, open a command window. On macOS or Linux, you can use Terminal. On Windows, open the Anaconda Prompt that should be in your Start menu.425426Run the following command (copy and paste it if you can, to avoid typos):427428\begin{code}429conda install jupyterlab pandas seaborn sympy430conda install beautifulsoup4 lxml html5lib pytables431\end{code}432433Some of these packages might already be installed. To install Pint, run this command:434435\begin{code}436conda install -c conda-forge pint437\end{code}438439And to install the ModSim library, run this command:440441\begin{code}442pip install modsimpy443\end{code}444445That should be everything you need.446447448\section{Copying my files}449450The simplest way to get the files for this book is to download a Zip archive from \url{https://github.com/AllenDowney/ModSimPy/archive/master.zip}. You will need a program like WinZip or gzip to unpack the Zip file. Make a note of the location of the files you unpack.451452If you download the Zip file, you can skip the rest of this section, which explains how to use Git.453454The code for this book is available from455\url{https://github.com/AllenDowney/ModSimPy}, which is a {\bf Git repository}. Git is a software tool that helps you keep track of the programs and other files that make up a project. A collection of files under Git's control is called a repository (the cool kids call it a ``repo"). GitHub is a hosting service that provides storage for Git repositories and a convenient web interface.456457\index{repository}458\index{Git}459\index{GitHub}460461Before you download these files, I suggest you copy my repository on GitHub, which is called {\bf forking}. If you don't already have a GitHub account, you'll need to create one.462463Use a browser to view the homepage of my repository at \url{https://github.com/AllenDowney/ModSimPy}. You should see a gray button in the upper right that says {\sf Fork}. If you press it, GitHub will create a copy of my repository that belongs to you.464465Now, the best way to download the files is to use a {\bf Git client}, which is a program that manages git repositories. You can get installation instructions for Windows, macOS, and Linux at \url{http://modsimpy.com/getgit}.466467In Windows, I suggest you accept the options recommended by the installer, with two exceptions:468469\begin{itemize}470471\item As the default editor, choose \py{nano} instead of \py{vim}.472473\item For ``Configuring line ending conversions", select ``Check out as is, commit as is".474475\end{itemize}476477For macOS and Linux, I suggest you accept the recommended options.478479Once the installation is complete, open a command window. On Windows, open Git Bash, which should be in your Start menu. On macOS or Linux, you can use Terminal.480481To find out what directory you are in, type \py{pwd}, which stands for ``print working directory". On Windows, most likely you are in \py{Users\\yourusername}. On MacOS or Linux, you are probably in your home directory, \py{/home/yourusername}.482483The next step is to copy files from your repository on GitHub to your computer; in Git vocabulary, this process is called {\bf cloning}. Run this command:484485\begin{python}486git clone https://github.com/YourGitHubUserName/ModSimPy487\end{python}488489Of course, you should replace \py{YourGitHubUserName} with your GitHub user name. After cloning, you should have a new directory called \py{ModSimPy}.490491\section{Running Jupyter}492493The code for each chapter, and starter code for the exercises, is in494Jupyter notebooks. If you have not used Jupyter before, you can read495about it at \url{https://jupyter.org}.496497\index{Jupyter}498499To start Jupyter on macOS or Linux, open a Terminal; on Windows, open Git Bash. Use \py{cd} to ``change directory" into the directory in the repository that contains the notebooks. If you downloaded the Zip file, it's probably:500501\begin{code}502cd ModSimPy-master/notebooks503\end{code}504505If you cloned it with Git, it's probably:506507\begin{code}508cd ModSimPy/notebooks509\end{code}510511Then launch the Jupyter notebook server:512513\begin{code}514jupyter notebook515\end{code}516517Jupyter should open a window in a browser, and you should see the list of notebooks in my repository. Click on the first notebook, \py{chap01.ipynb} and follow the instructions to run the first few ``cells". The first time you run a notebook, it might take several seconds to start while some Python files get initialized. After that, it should run faster.518519Feel free to read through the notebook, but it might not make sense until you read Chapter~\ref{chap01}.520521You can also launch Jupyter from the Start menu on Windows, the Dock on macOS, or the Anaconda Navigator on any system. If you do that, Jupyter might start in your home directory or somewhere else in your file system, so you might have to navigate to find the \py{ModSimPy} directory.522523524\section*{Contributor List}525526If you have a suggestion or correction, send it to527{\tt downey@allendowney.com}. Or if you are a Git user, send me a pull request!528529If I make a change based on your feedback, I will add you to the contributor list, unless you ask to be omitted.530\index{contributors}531532If you include at least part of the sentence the error appears in, that makes it easy for me to search. Page and section numbers are fine, too, but not as easy to work with. Thanks!533534\begin{itemize}535536\item I am grateful to John Geddes and Mark Somerville for their early collaboration with me to create Modeling and Simulation, the class at Olin College this book is based on.537538\item My early work on this book benefited from conversations with539my amazing colleagues at Olin College, including John Geddes, Alison540Wood, Chris Lee, and Jason Woodard.541542\item I am grateful to Lisa Downey and Jason Woodard for their thoughtful and careful copy editing.543544\item Thanks to Alessandra Ferzoco, Erhardt Graeff, Emily Tow,545Kelsey Houston-Edwards, Linda Vanasupa, Matt Neal, Joanne Pratt, and Steve Matsumoto for their helpful suggestions.546547\item Special thanks to Tim Sauder for the cover design.548549% ENDCONTRIB550551\end{itemize}552553554555\normalsize556557\cleardoublepage558559% TABLE OF CONTENTS560\begin{latexonly}561562% \tableofcontents563564\cleardoublepage565566\end{latexonly}567568% START THE BOOK569\mainmatter570571572\chapter{Modeling}573\label{chap01}574575This book is about modeling and simulation of physical systems.576The following diagram shows what I mean by ``modeling":577578\index{modeling}579580\vspace{0.2in}581\centerline{\includegraphics[height=3in]{figs/modeling_framework.pdf}}582583Starting in the lower left, the {\bf system} is something in the real world we are interested in. Often, it is something complicated, so we have to decide which details can be left out; removing details is called {\bf abstraction}.584585\index{system}586587The result of abstraction is a {\bf model}, which is a description of the system that includes only the features we think are essential. A model can be represented in the form of diagrams and equations, which can be used for mathematical {\bf analysis}. It can also be implemented in the form of a computer program, which can run {\bf simulations}.588589\index{model}590\index{abstraction}591\index{analysis}592593The result of analysis and simulation might be a {\bf prediction} about what the system will do, an {\bf explanation} of why it behaves the way it does, or a {\bf design} intended to achieve a purpose.594595\index{prediction}596\index{explanation}597\index{design}598599We can {\bf validate} predictions and test designs by taking {\bf measurements} from the real world and comparing the {\bf data} we get with the results from analysis and simulation.600601\index{validation}602\index{data}603604For any physical system, there are many possible models, each one including and excluding different features, or including different levels of detail. The goal of the modeling process is to find the model best suited to its purpose (prediction, explanation, or design).605606\index{iterative modeling}607608Sometimes the best model is the most detailed. If we include more features, the model is more realistic, and we expect its predictions to be more accurate.609610\index{realism}611612But often a simpler model is better. If we include only the essential features and leave out the rest, we get models that are easier to work with, and the explanations they provide can be clearer and more compelling.613614\index{simplicity}615616As an example, suppose someone asks you why the orbit of the Earth is elliptical. If you model the Earth and Sun as point masses (ignoring their actual size), compute the gravitational force between them using Newton's law of universal gravitation, and compute the resulting orbit using Newton's laws of motion, you can show that the result is an ellipse.617618\index{orbit}619\index{ellipse}620621Of course, the actual orbit of Earth is not a perfect ellipse, because of the gravitational forces of the Moon, Jupiter, and other objects in the solar system, and because Newton's laws of motion are only approximately true (they don't take into account relativistic effects).622623\index{Newton}624\index{relativity}625626But adding these features to the model would not improve the explanation; more detail would only be a distraction from the fundamental cause. However, if the goal is to predict the position of the Earth with great precision, including more details might be necessary.627628Choosing the best model depends on what the model is for. It is usually a good idea to start with a simple model, even if it is likely to be too simple, and test whether it is good enough for its purpose. Then you can add features gradually, starting with the ones you expect to be most essential. This process is called {\bf iterative modeling}.629630Comparing results of successive models provides a form of {\bf internal validation}, so you can catch conceptual, mathematical, and software errors. And by adding and removing features, you can tell which ones have the biggest effect on the results, and which can be ignored.631632\index{internal validation}633\index{validation!internal}634\index{external validation}635\index{validation!external}636637Comparing results to data from the real world provides {\bf external validation}, which is generally the strongest test.638639640\section{The falling penny myth}641\label{penny}642643Let's see an example of how models are used. You might have heard that a penny dropped from the top of the Empire State Building would be going so fast when it hit the pavement that it would be embedded in the concrete; or if it hit a person, it would break their skull.644645\index{Empire State Building}646\index{penny}647\index{myth}648649We can test this myth by making and analyzing a model. To get started, we'll assume that the effect of air resistance is small. This will turn out to be a bad assumption, but bear with me.650651If air resistance is negligible, the primary force acting on the penny is gravity, which causes the penny to accelerate downward.652\index{air resistance}653654If the initial velocity is 0, the velocity after $t$ seconds is $a t$, and the distance the penny has dropped is655%656\[ h = a t^2 / 2 \]657%658Using algebra, we can solve for $t$:659%660\[ t = \sqrt{ 2 h / a} \]661%662Plugging in the acceleration of gravity, $a = \SI{9.8}{\meter\per\second\squared}$, and the height of the Empire State Building, $h=\SI{381}{\meter}$, we get $t = \SI{8.8}{\second}$. Then computing $v = a t$ we get a velocity on impact of $\SI{86}{\meter\per\second}$, which is about 190 miles per hour. That sounds like it could hurt.663664Of course, these results are not exact because the model is based on simplifications. For example, we assume that gravity is constant. In fact, the force of gravity is different on different parts of the globe, and gets weaker as you move away from the surface. But these differences are small, so ignoring them is probably a good choice for this scenario.665\index{gravity}666667On the other hand, ignoring air resistance is not a good choice. Once the penny gets to about \SI{18}{\meter\per\second}, the upward force of air resistance equals the downward force of gravity, so the penny stops accelerating. After that, it doesn't matter how far the penny falls; it hits the sidewalk (or your head) at about \SI{18}{\meter\per\second}, much less than \SI{86}{\meter\per\second}, as the simple model predicts.668669The statistician George Box famously said ``All models are wrong, but some are useful." He was talking about statistical models, but his wise words apply to all kinds of models. Our first model, which ignores air resistance, is very wrong, and probably not useful. In the notebook for this chapter, you will see another model, which assumes that acceleration is constant until the penny reaches terminal velocity. This model is also wrong, but it's better, and it's good enough to refute the myth.670671\index{Box, George}672673The television show {\it Mythbusters} has tested the myth of the falling penny more carefully; you can view the results at \url{http://modsimpy.com/myth}. Their work is based on a mathematical model of motion, measurements to determine the force of air resistance on a penny, and a physical model of a human head.674675\index{Mythbusters}676677678\section{Computation}679\label{computation}680681There are (at least) two ways to work with mathematical models, {\bf analysis} and {\bf simulation}. Analysis often involves algebra and other kinds of symbolic manipulation. Simulation often involves computers.682\index{analysis}683\index{simulation}684685In this book we do some analysis and a lot of simulation; along the way, I discuss the pros and cons of each. The primary tools we use for simulation are the Python programming language and Jupyter, which is an environment for writing and running programs.686687As a first example, I'll show you how I computed the results from the previous section using Python.688689First I create a {\bf variable} to represent acceleration.690691\index{variable}692\index{value}693694\begin{python}695a = 9.8 * meter / second**2696\end{python}697698A variable is a name that corresponds to a value. In this example, the name is \py{a} and the value is the number \py{9.8} multiplied by the units \py{meter / second**2}. This example demonstrates some of the symbols Python uses to perform mathematical operations:699\index{operator!mathematical}700701\begin{tabular}{l|c}702{\bf Operation} & {\bf Symbol} \\703\hline704Addition & \py{+} \\705Subtraction & \py{-} \\706Multiplication & \py{*} \\707Division & \py{/} \\708Exponentiation & \py{**} \\709\end{tabular}710711Next, we compute the time it takes for the penny to drop \SI{381}{\meter}, the height of the Empire State Building.712713\begin{python}714h = 381 * meter715t = sqrt(2 * h / a)716\end{python}717718These lines create two more variables: \py{h} gets the height of the building in meters; \py{t} gets the time, in seconds, for the penny to fall to the sidewalk. \py{sqrt} is a {\bf function} that computes square roots. Python keeps track of units, so the result, \py{t}, has the correct units, seconds.719\index{unit}720\index{function}721\index{sqrt}722723Finally, we compute the velocity of the penny after $t$ seconds:724725\begin{python}726v = a * t727\end{python}728729The result is about \SI{86}{\meter\per\second}, again with the correct units.730731This example demonstrates analysis and computation using Python. In the next chapter, we'll see an example of simulation.732733Before you go on, you might want to read the notebook for this chapter, \py{chap01.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.734735736\chapter{Bike share}737\label{chap02}738739This chapter presents a simple model of a bike share system and demonstrates the features of Python we'll use to develop simulations of real-world systems.740741Along the way, we'll make decisions about how to model the system. In the next chapter we'll review these decisions and gradually improve the model.742743744\section{Modeling}745\label{modeling}746747Imagine a bike share system for students traveling between Olin College and Wellesley College, which are about 3 miles apart in eastern Massachusetts.748749\index{Wellesley College}750\index{Olin College}751752Suppose the system contains 12 bikes and two bike racks, one at Olin and one at Wellesley, each with the capacity to hold 12 bikes.753754\index{bike share system}755756As students arrive, check out a bike, and ride to the other campus, the number of bikes in each location changes. In the simulation, we'll need to keep track of where the bikes are. To do that, I'll create a \py{State} object, which is defined in the ModSim library.757758\index{State object}759760Before we can use the library, we have to \py{import} it:761762\begin{python}763from modsim import *764\end{python}765766This line of code is an {\bf import statement} that tells Python767to read the file {\tt modsim.py} and make the functions it defines available.768769\index{import statement}770771Functions in the \py{modsim.py} library include \py{sqrt}, which we used in the previous section, and \py{State}, which we are using now. \py{State} creates a \py{State} object, which is a collection of {\bf state variables}.772773\index{state variable}774775\begin{python}776bikeshare = State(olin=10, wellesley=2)777\end{python}778779The state variables, \py{olin} and \py{wellesley}, represent the number of bikes at each location. The initial values are 10 and 2, indicating that there are 10 bikes at Olin and 2 at Wellesley. The \py{State} object created by \py{State} is assigned to a new variable named \py{bikeshare}.780781\index{dot operator}782\index{operator!dot}783784We can read the variables inside a \py{State} object using the {\bf dot operator}, like this:785786\begin{python}787bikeshare.olin788\end{python}789790The result is the value 10. Similarly, for:791792\begin{python}793bikeshare.wellesley794\end{python}795796The result is 2. If you forget what variables a state object has, you can just type the name:797798\begin{python}799bikeshare800\end{python}801802The result looks like a table with the variable names and their values:803804\begin{tabular}{lr}805& {\bf \sf value} \\806\hline807{\bf \sf olin} & 10 \\808{\bf \sf wellesley} & 2 \\809\end{tabular}810811The state variables and their values make up the {\bf state} of the system. We can update the state by assigning new values to the variables. For example, if a student moves a bike from Olin to Wellesley, we can figure out the new values and assign them:812813\index{state}814815\begin{python}816bikeshare.olin = 9817bikeshare.wellesley = 3818\end{python}819820Or we can use {\bf update operators}, \py{-=} and \py{+=}, to subtract 1 from \py{olin} and add 1 to \py{wellesley}:821822\index{update operator}823\index{operator!update}824825\begin{python}826bikeshare.olin -= 1827bikeshare.wellesley += 1828\end{python}829830The result is the same either way, but the second version is more versatile.831832833\section{Defining functions}834835So far we have used functions defined in ModSim and other libraries. Now we're going to define our own functions.836837\index{function}838\index{defining functions}839840When you are developing code in Jupyter, it is often efficient to841write a few lines of code, test them to confirm they do what842you intend, and then use them to define a new function. For843example, these lines move a bike from Olin to Wellesley:844845\begin{python}846bikeshare.olin -= 1847bikeshare.wellesley += 1848\end{python}849850Rather than repeat them every time a bike moves, we can define a851new function:852853\begin{python}854def bike_to_wellesley():855bikeshare.olin -= 1856bikeshare.wellesley += 1857\end{python}858859\py{def} is a special word in Python that indicates we are defining a new860function. The name of the function is \py{bike_to_wellesley}. The empty parentheses indicate that this function requires no additional information when it runs. The colon indicates the beginning of an indented861{\bf code block}.862\index{def}863\index{code block}864\index{body}865\index{indentation}866867The next two lines are the {\bf body} of the function. They have to be indented; by convention, the indentation is 4 spaces.868869When you define a function, it has no immediate effect. The body870of the function doesn't run until you {\bf call} the function.871Here's how to call this function:872\index{call}873874\begin{python}875bike_to_wellesley()876\end{python}877878When you call the function, it runs the statements in the body, which update the variables of the {\tt bikeshare} object; you can check by displaying the new state.879880When you call a function, you have to include the parentheses. If you leave them out, like this:881\index{parentheses}882883\begin{python}884bike_to_wellesley885\end{python}886887Python looks up the name of the function and displays:888889\begin{python}890<function __main__.bike_to_wellesley>891\end{python}892893This result indicates that \py{bike_to_wellesley} is a function. You don't have to know what \py{__main__} means, but if you see something like this, it probably means that you looked up a function but you didn't894actually call it. So don't forget the parentheses.895896Just like \py{bike_to_wellesley}, we can define a function that moves a bike from Wellesley to Olin:897898\begin{python}899def bike_to_olin():900bikeshare.wellesley -= 1901bikeshare.olin += 1902\end{python}903904And call it like this:905906\begin{python}907bike_to_olin()908\end{python}909910One benefit of defining functions is that you avoid repeating chunks911of code, which makes programs smaller. Another benefit is that the912name you give the function documents what it does, which makes programs913more readable.914915916\section{Print statements}917918As you write more complicated programs, it is easy to lose track of what is going on. One of the most useful tools for debugging is the {\bf print statement}, which displays text in the Jupyter notebook.919\index{print statement}920\index{statement!print}921922Normally when Jupyter runs the code in a cell, it displays the value of the last line of code. For example, if you run:923924\begin{python}925bikeshare.olin926bikeshare.wellesley927\end{python}928929Jupyter runs both lines of code, but it only displays the value of the second line. If you want to display more than one value, you can use print statements:930931\begin{python}932print(bikeshare.olin)933print(bikeshare.wellesley)934\end{python}935936When you call the \py{print} function, you can put a variable name in parentheses, as in the previous example, or you can provide a sequence of variables separated by commas, like this:937938\begin{python}939print(bikeshare.olin, bikeshare.wellesley)940\end{python}941942Python looks up the values of the variables and displays them; in this example, it displays two values on the same line, with a space between them.943944Print statements are useful for debugging functions. For example, we can add a print statement to \py{move_bike}, like this:945946\begin{python}947def bike_to_wellesley():948print('Moving a bike to Wellesley')949bikeshare.olin -= 1950bikeshare.wellesley += 1951\end{python}952953Each time we call this version of the function, it displays a message, which can help us keep track of what the program is doing.954955The message in this example is a {\bf string}, which is a sequence of letters and other symbols in quotes.956\index{string}957958959\section{If statements}960961The ModSim library provides a function called \py{flip}; when you call it, you provide a value between 0 and 1; in this example, it's \py{0.7}:962963\begin{python}964flip(0.7)965\end{python}966967The result is one of two values: \py{True} with probability 0.7 or \py{False} with probability 0.3. If you run \py{flip} like this 100 times, you should get \py{True} about 70 times and \py{False} about 30 times. But the results are random, so they might differ from these expectations.968\index{flip}969\index{True}970\index{False}971972\py{True} and \py{False} are special values defined by Python. Note973that they are not strings. There is a difference between \py{True},974which is a special value, and \py{'True'}, which is a string.975\index{string}976\index{boolean}977978\py{True} and \py{False} are called {\bf boolean} values because979they are related to Boolean algebra (\url{http://modsimpy.com/boolean}).980981We can use boolean values to control the behavior of the program, using982an {\bf if statement}:983\index{if statement}984\index{statement!if}985986\begin{python}987if flip(0.5):988print('heads')989\end{python}990991If the result from \py{flip} is \py{True}, the program displays the string \py{'heads'}. Otherwise it does nothing.992993The punctuation for \py{if} statements is similar to the punctuation for function definitions: the first line has to end with a colon, and the lines inside the \py{if} statement have to be indented.994\index{indentation}995\index{else clause}996997Optionally, you can add an {\bf else clause} to indicate what should happen if the result is \py{False}:998999\begin{python}1000if flip(0.5):1001print('heads')1002else:1003print('tails')1004\end{python}10051006Now we can use \py{flip} to simulate the arrival of students who want to borrow a bike. Suppose students arrive at the Olin station every 2 minutes, on average. In that case, the chance of an arrival during any one-minute period is 50\%, and we can simulate it like this:10071008\begin{python}1009if flip(0.5):1010bike_to_wellesley()1011\end{python}10121013If students arrive at the Wellesley station every 3 minutes, on average, the chance of an arrival during any one-minute period is 33\%, and we can simulate it like this:10141015\begin{python}1016if flip(0.33):1017bike_to_olin()1018\end{python}10191020We can combine these snippets into a function that simulates a {\bf time step}, which is an interval of time, in this case one minute:10211022\index{time step}10231024\begin{python}1025def step():1026if flip(0.5):1027bike_to_wellesley()10281029if flip(0.33):1030bike_to_olin()1031\end{python}10321033Then we can simulate a time step like this:10341035\begin{python}1036step()1037\end{python}10381039Even though there are no values in parentheses, we have to include them.104010411042\section{Parameters}10431044The previous version of \py{step} is fine if the arrival probabilities never change, but in reality, these probabilities vary over time.10451046So instead of putting the constant values 0.5 and 0.33 in \py{step} we can replace them with {\bf parameters}. Parameters are variables whose values are set when a function is called.10471048Here's a version of \py{step} that takes two parameters, \py{p1} and \py{p2}:10491050\index{probability}10511052\begin{python}1053def step(p1, p2):1054if flip(p1):1055bike_to_wellesley()10561057if flip(p2):1058bike_to_olin()1059\end{python}10601061The values of \py{p1} and \py{p2} are not set inside this function; instead, they are provided when the function is called, like this:10621063\begin{python}1064step(0.5, 0.33)1065\end{python}10661067The values you provide when you call the function are called {\bf arguments}.1068The arguments, \py{0.5} and \py{0.33} in this example, get assigned to the parameters, \py{p1} and \py{p2}, in order. So running this function has the same effect as:10691070\begin{python}1071p1 = 0.51072p2 = 0.3310731074if flip(p1):1075bike_to_wellesley()10761077if flip(p2):1078bike_to_olin()1079\end{python}10801081The advantage of using parameters is that you can call the same function many times, providing different arguments each time.10821083Adding parameters to a function is called {\bf generalization}, because it makes the function more general, that is, less specialized.10841085\index{generalization}108610871088\section{For loops}1089\label{forloop}10901091At some point you will get sick of running cells over and over. Fortunately, there is an easy way to repeat a chunk of code, the {\bf for loop}. Here's an example:1092\index{for loop}1093\index{loop}10941095\begin{python}1096for i in range(4):1097bike_to_wellesley()1098\end{python}10991100The punctuation here should look familiar; the first line ends with a colon, and the lines inside the \py{for} loop are indented. The other elements of the loop are:1101\index{range}11021103\begin{itemize}11041105\item The words \py{for} and \py{in} are special words we have to use in a for loop.11061107\item \py{range} is a Python function we're using here to control the number of times the loop runs.1108\index{range}11091110\item \py{i} is a {\bf loop variable} that gets created when the for loop runs.1111\index{loop variable}11121113\end{itemize}11141115In this example we don't actually use \py{i}; we will see examples later where we use the loop variable inside the loop.11161117When this loop runs, it runs the statements inside the loop four times,1118which moves one bike at a time from Olin to Wellesley.111911201121\section{TimeSeries}1122\label{timeseries}11231124When we run a simulation, we usually want to save the results for later analysis. The ModSim library provides a \py{TimeSeries} object for this purpose. A \py{TimeSeries} contains a sequence of time stamps and a corresponding sequence of values. In this example, the time stamps are integers representing minutes, and the values are the number of bikes at one location.11251126%TODO: index modsim library functions1127\index{ModSim library}1128\index{TimeSeries}11291130We can create a new, empty \py{TimeSeries} like this:11311132\begin{python}1133results = TimeSeries()1134\end{python}11351136And we can add a value to a \py{TimeSeries} like this:11371138\begin{python}1139results[0] = bikeshare.olin1140\end{python}11411142The number in brackets is the time stamp, also called a {\bf label}.1143\index{label}11441145We can use a \py{TimeSeries} inside a for loop to store the results of the simulation:11461147\begin{python}1148for i in range(10):1149step(0.3, 0.2)1150results[i] = bikeshare.olin1151\end{python}11521153Each time through the loop, we call \py{step}, which updates \py{bikeshare}. Then we store the number of bikes at Olin in \py{results}. We use the loop variable, \py{i}, as the time stamp.11541155\index{loop}1156\index{loop variable}1157\index{time stamp}11581159When the loop exits, \py{results} contains 10 time stamps, from 0 through 9, and the number of bikes at Olin at the end of each time step.1160\index{loop variable}11611162\py{TimeSeries} is a specialized version of \py{Series}, which is defined by Pandas, one of the libraries we'll be using extensively. The \py{Series} object provides many functions; one example is \py{mean}, which we can call like this:11631164\begin{python}1165results.mean()1166\end{python}11671168You can read the documentation of \py{Series} at \url{http://modsimpy.com/series}.11691170\index{Pandas}1171\index{Series}1172\index{TimeSeries}1173\index{mean}117411751176\section{Plotting}1177\label{plotting}11781179The ModSim library provides a function called \py{plot} we can use to plot \py{results}:11801181\begin{python}1182plot(results)1183\end{python}11841185\py{plot} can take an additional argument that gives the line a label; this label will appear in the legend of the plot, if we create one.11861187\begin{python}1188plot(results, label='Olin')1189\end{python}11901191\py{label} is an example of a {\bf keyword argument}, so called because we provide a ``keyword'', which is \py{label} in this case, along with its value. Arguments without keywords are called {\bf positional arguments} because they are assigned to parameters according to their position. It is good to know these terms because they appear in Python error messages.11921193\index{keyword argument}1194\index{positional argument}1195\index{argument}11961197Whenever you make a figure, you should label the axes. The ModSim library provides \py{decorate}, which labels the axes and gives the figure a title and legend:11981199\begin{python}1200decorate(title='Olin-Wellesley Bikeshare',1201xlabel='Time step (min)',1202ylabel='Number of bikes')1203\end{python}12041205\begin{figure}1206\centerline{\includegraphics[height=3in]{figs/chap02-fig01.pdf}}1207\caption{Simulation of a bikeshare system showing number of bikes at Olin over time.}1208\label{chap02-fig01}1209\end{figure}12101211Figure~\ref{chap02-fig01} shows the result.12121213\py{plot} and \py{decorate} are based on Pyplot, which is a Python library for generating figures. You can read more about \py{plot} and the arguments it takes at \url{http://modsimpy.com/plot}.12141215\index{Pyplot}1216\index{plot}1217\index{decorate}12181219Before you go on, you might want to read the notebook for this chapter, \py{chap02.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.1220122112221223\chapter{Iterative modeling}1224\label{chap03}12251226To paraphrase two Georges, ``All models are wrong, but some models are more wrong than others." In this chapter, I demonstrate the process we use to make models less wrong.12271228\index{Box, George}1229\index{Orwell, George}12301231As an example, we'll review the bikeshare model from the previous chapter, consider its strengths and weaknesses, and gradually improve it. We'll also see ways to use the model to understand the behavior of the system and evaluate designs intended to make it work better.12321233\index{bikeshare}123412351236\section{Iterative modeling}12371238The model we have so far is simple, but it is based on unrealistic assumptions. Before you go on, take a minute to review the model from the previous chapters. What assumptions is it based on? Make a list of ways this model might be unrealistic; that is, what are the differences between the model and the real world?12391240Here are some of the differences on my list:12411242\begin{itemize}12431244\item In the model, a student is equally likely to arrive during any one-minute period. In reality, this probability varies depending on time of day, day of the week, etc.12451246\index{probability}12471248\item The model does not account for travel time from one bike station to another.12491250\item The model does not check whether a bike is available, so it's possible for the number of bikes to be negative (as you might have noticed in some of your simulations).12511252\end{itemize}12531254Some of these modeling decisions are better than others. For example, the first assumption might be reasonable if we simulate the system for a short period of time, like one hour.12551256The second assumption is not very realistic, but it might not affect the results very much, depending on what we use the model for.12571258\index{realism}12591260On the other hand, the third assumption seems problematic, and it is relatively easy to fix. In Section~\ref{negativebikes}, we will.12611262This process, starting with a simple model, identifying the most important problems, and making gradual improvements, is called {\bf iterative modeling}.12631264\index{iterative modeling}12651266For any physical system, there are many possible models, based on different assumptions and simplifications. It often takes several iterations to develop a model that is good enough for the intended purpose, but no more complicated than necessary.126712681269\section{More than one State object}12701271Before we go on, I want to make a few changes to the code from the previous chapter. First I'll generalize the functions we wrote so they take a \py{State} object as a parameter. Then, I'll make the code more readable by adding documentation.12721273\index{parameter}12741275Here is one of the functions from the previous chapter, \py{bike_to_wellesley}:12761277\begin{python}1278def bike_to_wellesley():1279bikeshare.olin -= 11280bikeshare.wellesley += 11281\end{python}12821283When this function is called, it modifies \py{bikeshare}. As long as there is only one \py{State} object, that's fine, but what if there is more than one bike share system in the world? Or what if we want to run more than one simulation?12841285This function would be more flexible if it took a \py{State} object as a parameter. Here's what that looks like:12861287\index{State object}12881289\begin{python}1290def bike_to_wellesley(state):1291state.olin -= 11292state.wellesley += 11293\end{python}12941295The name of the parameter is \py{state} rather than \py{bikeshare} as a reminder that the value of \py{state} could be any \py{State} object, not just \py{bikeshare}.12961297This version of \py{bike_to_wellesley} requires a \py{State} object as a parameter, so we have to provide one when we call it:12981299\begin{python}1300bike_to_wellesley(bikeshare)1301\end{python}13021303Again, the argument we provide gets assigned to the parameter, so this function call has the same effect as:13041305\begin{code}1306state = bikeshare1307state.olin -= 11308state.wellesley += 11309\end{code}13101311Now we can create as many \py{State} objects as we want:13121313\begin{python}1314bikeshare1 = State(olin=10, wellesley=2)1315bikeshare2 = State(olin=2, wellesley=10)1316\end{python}13171318And update them independently:13191320\begin{python}1321bike_to_wellesley(bikeshare1)1322bike_to_wellesley(bikeshare2)1323\end{python}13241325Changes in \py{bikeshare1} do not affect \py{bikeshare2}, and vice versa. So we can simulate different bike share systems, or run multiple simulations of the same system.132613271328\section{Documentation}1329\label{documentation}13301331Another problem with the code we have so far is that it contains no {\bf documentation}. Documentation is text we add to a program to help other programmers read and understand it. It has no effect on the program when it runs.13321333\index{documentation}1334\index{docstring}1335\index{comment}13361337There are two forms of documentation, {\bf docstrings} and {\bf comments}.1338A docstring is a string in triple-quotes that appears at the beginning of a function, like this:13391340\begin{python}1341def run_simulation(state, p1, p2, num_steps):1342"""Simulate the given number of time steps.13431344state: State object1345p1: probability of an Olin->Wellesley customer arrival1346p2: probability of a Wellesley->Olin customer arrival1347num_steps: number of time steps1348"""1349results = TimeSeries()1350for i in range(num_steps):1351step(state, p1, p2)1352results[i] = state.olin13531354plot(results, label='Olin')1355\end{python}13561357Docstrings follow a conventional format:13581359\begin{itemize}13601361\item The first line is a single sentence that describes what the function does.13621363\item The following lines explain what each of the parameters are.13641365\end{itemize}13661367A function's docstring should include the information someone needs to know to {\em use} the function; it should not include details about how the function works. That's what comments are for.13681369A comment is a line of text that begins with a hash symbol, \py{#}. It usually appears inside a function to explain something that would not be obvious to someone reading the program.13701371\index{comment}1372\index{hash symbol}13731374For example, here is a version of \py{bike_to_olin} with a docstring and a comment.13751376\begin{python}1377def bike_to_olin(state):1378"""Move one bike from Wellesley to Olin.13791380state: State object1381"""1382# We decrease one state variable and increase the1383# other, so the total number of bikes is unchanged.1384state.wellesley -= 11385state.olin += 11386\end{python}13871388At this point we have more documentation than code, which is not unusual for short functions.138913901391\section{Negative bikes}1392\label{negativebikes}13931394The changes we've made so far improve the quality of the code, but we haven't done anything to improve the quality of the model yet. Let's do that now.13951396\index{code quality}13971398Currently the simulation does not check whether a bike is available when a customer arrives, so the number of bikes at a location can be negative. That's not very realistic. Here's an updated version of \py{bike_to_olin} that fixes the problem:13991400\begin{python}1401def bike_to_olin(state):1402if state.wellesley == 0:1403return1404state.wellesley -= 11405state.olin += 11406\end{python}14071408The first line checks whether the number of bikes at Wellesley is zero. If so, it uses a {\bf return statement}, which causes the function to end immediately, without running the rest of the statements. So if there are no bikes at Wellesley, we ``return" from \py{bike_to_olin} without changing the state.14091410\index{return statement}1411\index{statement!return}14121413We can update \py{bike_to_wellesley} the same way.141414151416\section{Comparison operators}14171418The version of \py{bike_to_olin} in the previous section uses the equals operator, \py{==}, which compares two values and returns \py{True} if they are equal and \py{False} otherwise.14191420It is easy to confuse the equals operators with the assignment operator, \py{=}, which assigns a value to a variable. For example, the following statement creates a variable, \py{x}, if it doesn't already exist, and gives it the value \py{5}.14211422\index{equality}1423\index{assignment operator}1424\index{operator!assignment}14251426\begin{python}1427x = 51428\end{python}14291430On the other hand, the following statement checks whether \py{x} is \py{5} and returns \py{True} or \py{False}. It does not create \py{x} or change its value.14311432\begin{python}1433x == 51434\end{python}14351436You can use the equals operator in an \py{if} statement, like this:14371438\index{if statement}1439\index{statement!if}14401441\begin{python}1442if x == 5:1443print('yes, x is 5')1444\end{python}14451446If you make a mistake and use \py{=} in an \py{if} statement, like this:14471448\begin{python}1449if x = 5:1450print('yes, x is 5')1451\end{python}14521453That's a {\bf syntax error}, which means that the structure of the program is invalid. Python will print an error message and the program won't run.14541455\index{syntax error}1456\index{error!syntax}14571458The equals operator is one of the {\bf comparison operators}. The others are:14591460\index{comparison operator}1461\index{operator!comparison}14621463\begin{tabular}{l|c}1464{\bf Operation} & {\bf Symbol} \\1465\hline1466Less than & \py{<} \\1467Greater than & \py{>} \\1468Less than or equal & \py{<=} \\1469Greater than or equal & \py{>=} \\1470Equal & \py{==} \\1471Not equal & \py{!=} \\1472\end{tabular}147314741475\section{Metrics}1476\label{metrics}14771478Getting back to the bike share system, at this point we have the ability to simulate the behavior of the system. Since the arrival of customers is random, the state of the system is different each time we run a simulation. Models like this are called random or {\bf stochastic}; models that do the same thing every time they run are {\bf deterministic}.14791480\index{stochastic}1481\index{deterministic}14821483Suppose we want to use our model to predict how well the bike share system will work, or to design a system that works better. First, we have to decide what we mean by ``how well" and ``better".14841485From the customer's point of view, we might like to know the probability of finding an available bike. From the system-owner's point of view, we might want to minimize the number of customers who don't get a bike when they want one, or maximize the number of bikes in use. Statistics like these that quantify how well the system works are called {\bf metrics}.14861487\index{metric}14881489As a simple example, let's measure the number of unhappy customers. Here's a version of \py{bike_to_olin} that keeps track of the number of customers who arrive at a station with no bikes:14901491\begin{python}1492def bike_to_olin(state):1493if state.wellesley == 0:1494state.wellesley_empty += 11495return1496state.wellesley -= 11497state.olin += 11498\end{python}14991500If a customer arrives at the Wellesley station and finds no bike available, \py{bike_to_olin} updates \py{wellesley_empty} which counts the number of unhappy customers.15011502This function only works if we initialize \py{wellesley_empty} when we create the \py{State} object, like this:15031504\begin{python}1505bikeshare = State(olin=10, wellesley=2,1506olin_empty=0, wellesley_empty=0)1507\end{python}15081509Assuming we update \py{move_to_wellesley} the same way, we can run the simulation like this (see Section~\ref{documentation}):15101511\begin{python}1512run_simulation(bikeshare, 0.4, 0.2, 60)1513\end{python}15141515Then we can check the metrics:15161517\begin{python}1518print(bikeshare.olin_empty, bikeshare.wellesley_empty)1519\end{python}15201521Because the simulation is stochastic, the results are different each time it runs.15221523Before you go on, you might want to read the notebook for this chapter, \py{chap03.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.152415251526\chapter{Sweeping parameters}1527\label{chap04}15281529In the previous chapter we defined metrics that quantify the performance of bike sharing this system. In this chapter we see how those metrics depend on the parameters of the system, like the arrival rate of customers at bike stations.15301531We also discuss a program development strategy, called incremental development, that might help you write programs faster and spend less time debugging.153215331534\section{Functions that return values}15351536We have seen several functions that return values; for example, when you run \py{sqrt}, it returns a number you can assign to a variable.15371538\index{return value}15391540\begin{python}1541t = sqrt(2 * h / a)1542\end{python}15431544When you run \py{State}, it returns a new \py{State} object:15451546\begin{python}1547bikeshare = State(olin=10, wellesley=2)1548\end{python}15491550Not all functions have return values. For example, when you run \py{step}, it updates a \py{State} object, but it doesn't return a value.15511552To write functions that return values, we can use a second form of the \py{return} statement, like this:15531554\index{return statement}1555\index{statement!return}15561557\begin{python}1558def add_five(x):1559return x + 51560\end{python}15611562\py{add_five} takes a parameter, \py{x}, which could be any number. It computes \py{x + 5} and returns the result. So if we run it like this, the result is \py{8}:15631564\begin{python}1565add_five(3)1566\end{python}15671568As a more useful example, here's a version of \py{run_simulation} that creates a \py{State} object, runs a simulation, and then returns the \py{State} object as a result:15691570\begin{python}1571def run_simulation():1572p1 = 0.41573p2 = 0.21574num_steps = 6015751576state = State(olin=10, wellesley=2,1577olin_empty=0, wellesley_empty=0)15781579for i in range(num_steps):1580step(state, p1, p2)15811582return state1583\end{python}15841585If we call \py{run_simulation} like this:15861587\begin{python}1588state = run_simulation()1589\end{python}15901591It assigns the \py{State} object from \py{run_simulation} to \py{state}, which contains the metrics we are interested in:15921593\begin{python}1594print(state.olin_empty, state.wellesley_empty)1595\end{python}159615971598\section{Two kinds of parameters}15991600This version of \py{run_simulation} always starts with the same initial condition, 10 bikes at Olin and 2 bikes at Wellesley, and the same values of \py{p1}, \py{p2}, and \py{num_steps}. Taken together, these five values are the {\bf parameters of the model}, which are values that determine the behavior of the system.16011602\index{parameter!of a model}1603\index{parameter!of a function}16041605It is easy to get the parameters of a model confused with the parameters of a function. They are closely related ideas; in fact, it is common for the parameters of the model to appear as parameters in functions. For example, we can write a more general version of \py{run_simulation} that takes \py{p1} and \py{p2} as function parameters:16061607\begin{python}1608def run_simulation(p1, p2, num_steps):1609state = State(olin=10, wellesley=2,1610olin_empty=0, wellesley_empty=0)16111612for i in range(num_steps):1613step(state, p1, p2)16141615return state1616\end{python}16171618Now we can run it with different arrival rates, like this:16191620\begin{python}1621state = run_simulation(0.6, 0.3, 60)1622\end{python}16231624In this example, \py{0.6} gets assigned to \py{p1}, \py{0.3} gets assigned to \py{p2}, and \py{60} gets assigned to \py{num_steps}.16251626Now we can call \py{run_simulation} with different parameters and see how the metrics, like the number of unhappy customers, depend on the parameters. But before we do that, we need a new version of a for loop.16271628\index{metric}162916301631\section{Loops and arrays}1632\label{array}16331634In Section~\ref{forloop}, we saw a loop like this:16351636\begin{python}1637for i in range(4):1638bike_to_wellesley()1639\end{python}16401641\py{range(4)} creates a sequence of numbers from 0 to 3. Each time through the loop, the next number in the sequence gets assigned to the loop variable, \py{i}.16421643\index{loop}1644\index{loop variable}1645\index{variable!loop}16461647\py{range} only works with integers; to get a sequence of non-integer values, we can use \py{linspace}, which is defined in the ModSim library:16481649\begin{python}1650p1_array = linspace(0, 1, 5)1651\end{python}16521653The arguments indicate where the sequence should start and stop, and how many elements it should contain. In this example, the sequence contains \py{5} equally-spaced numbers, starting at \py{0} and ending at \py{1}.16541655\index{linspace}1656\index{NumPy}1657\index{array}16581659The result is a NumPy {\bf array}, which is a new kind of object we have not seen before. An array is a container for a sequence of numbers.16601661We can use an array in a \py{for} loop like this:16621663\begin{python}1664for p1 in p1_array:1665print(p1)1666\end{python}16671668When this loop runs, it16691670\begin{enumerate}16711672\item Gets the first value from the array and assigns it to \py{p1}.16731674\item Runs the body of the loop, which prints \py{p1}.16751676\item Gets the next value from the array and assigns it to \py{p1}.16771678\item Runs the body of the loop, which prints \py{p1}.16791680\end{enumerate}16811682And so on, until it gets to the end of the array. The result is:16831684\begin{result}16850.016860.2516870.516880.7516891.01690\end{result}16911692This will come in handy in the next section.169316941695\section{Sweeping parameters}16961697If we know the actual values of parameters like \py{p1} and \py{p2}, we can use them to make specific predictions, like how many bikes will be at Olin after one hour.16981699\index{prediction}1700\index{explanation}17011702But prediction is not the only goal; models like this are also used to explain why systems behave as they do and to evaluate alternative designs. For example, if we observe the system and notice that we often run out of bikes at a particular time, we could use the model to figure out why that happens. And if we are considering adding more bikes, or another station, we could evaluate the effect of various ``what if" scenarios.1703\index{what if scenario}17041705As an example, suppose we have enough data to estimate that \py{p2} is about \py{0.2}, but we don't have any information about \py{p1}. We could run simulations with a range of values for \py{p1} and see how the results vary. This process is called {\bf sweeping} a parameter, in the sense that the value of the parameter ``sweeps" through a range of possible values.17061707\index{sweep}1708\index{parameter sweep}17091710Now that we know about loops and arrays, we can use them like this:17111712\begin{python}1713p1_array = linspace(0, 1, 11)1714p2 = 0.21715num_steps = 6017161717for p1 in p1_array:1718state = run_simulation(p1, p2, num_steps)1719print(p1, state.olin_empty)1720\end{python}17211722Each time through the loop, we run a simulation with a different value of \py{p1} and the same value of \py{p2}, \py{0.2}. Then we print \py{p1} and the number of unhappy customers at Olin.17231724To save and plot the results, we can use a \py{SweepSeries} object, which is similar to a \py{TimeSeries}; the difference is that the labels in a \py{SweepSeries} are parameter values rather than time values.17251726We can create an empty \py{SweepSeries} like this:17271728\begin{code}1729sweep = SweepSeries()1730\end{code}17311732And add values like this:17331734\begin{python}1735for p1 in p1_array:1736state = run_simulation(p1, p2, num_steps)1737sweep[p1] = state.olin_empty1738\end{python}17391740The result is a \py{SweepSeries} that maps from each value of \py{p1} to the resulting number of unhappy customers. Then we can plot the results:17411742\begin{code}1743plot(sweep, label='Olin')1744\end{code}17451746174717481749\section{Incremental development}17501751When you start writing programs that are more than a few lines, you1752might find yourself spending more and more time debugging. The more1753code you write before you start debugging, the harder it is to find1754the problem.17551756\index{debugging}1757\index{incremental development}17581759{\bf Incremental development} is a way of programming that tries1760to minimize the pain of debugging. The fundamental steps are:17611762\begin{enumerate}17631764\item Always start with a working program. If you have an1765example from a book, or a program you wrote that is similar to1766what you are working on, start with that. Otherwise, start with1767something you {\em know} is correct, like {\tt x=5}. Run the program1768and confirm that it does what you expect.17691770\item Make one small, testable change at a time. A ``testable''1771change is one that displays something or has some1772other effect you can check. Ideally, you should know what1773the correct answer is, or be able to check it by performing another1774computation.17751776\index{testable change}17771778\item Run the program and see if the change worked. If so, go back1779to Step 2. If not, you will have to do some debugging, but if the1780change you made was small, it shouldn't take long to find the problem.17811782\end{enumerate}17831784When this process works, your changes usually work the first time, or if they don't, the problem is obvious. In practice, there are two problems with incremental development:17851786\begin{itemize}17871788\item Sometimes you have to write extra code to generate visible output that you can check. This extra code is called {\bf scaffolding} because you use it to build the program and then remove it when you are done. That might seem like a waste, but time you spend on scaffolding is almost always time you save on debugging.17891790\index{scaffolding}17911792\item When you are getting started, it might not be obvious how to1793choose the steps that get from {\tt x=5} to the program you are trying1794to write. You will see more examples of this process as we go along, and you will get better with experience.17951796\end{itemize}17971798If you find yourself writing more than a few lines of code before you start testing, and you are spending a lot of time debugging, try incremental development.17991800Before you go on, you might want to read the notebook for this chapter, \py{chap04.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.180118021803%\part{Modeling population growth}18041805\chapter{World population}1806\label{chap05}18071808In 1968 Paul Erlich published {\it The Population Bomb}, in which he predicted that world population would grow quickly during the 1970s, that agricultural production could not keep up, and that mass starvation in the next two decades was inevitable (see \url{http://modsimpy.com/popbomb}). As someone who grew up during those decades, I am happy to report that those predictions were wrong.18091810\index{Erlich, Paul}1811\index{Population Bomb}18121813But world population growth is still a topic of concern, and it is an open question how many people the earth can sustain while maintaining and improving our quality of life.18141815\index{world population}1816\index{population}18171818In this chapter and the next, we use tools from the previous chapters to explain world population growth since 1950 and generate predictions for the next 50--100 years.18191820\index{prediction}18211822For background on world population growth, watch this video from the American Museum of Natural History \url{http://modsimpy.com/human}.18231824\index{American Museum of Natural History}182518261827\section{World Population Data}1828\label{worldpopdata}18291830The Wikipedia article on world population contains tables with estimates of world population from prehistory to the present, and projections for the future (\url{http://modsimpy.com/worldpop}).18311832\index{Wikipedia}1833\index{Pandas}18341835To read this data, we will use Pandas, which provides functions for working with data. The function we'll use is \py{read_html}, which can read a web page and extract data from any tables it contains. Before we can use it, we have to import it. You have already seen this import statement:18361837\index{\py{read_html}}1838\index{import statement}1839\index{statement!import}18401841\begin{python}1842from modsim import *1843\end{python}18441845which imports all functions from the ModSim library. To import \py{read_html}, the statement we need is:18461847\begin{python}1848from pandas import read_html1849\end{python}18501851Now we can use it like this:18521853\begin{python}1854filename = 'data/World_population_estimates.html'1855tables = read_html(filename,1856header=0,1857index_col=0,1858decimal='M')1859\end{python}18601861The arguments are:1862\index{argument}18631864\begin{itemize}18651866\item \py{filename}: The name of the file (including the directory it's in) as a string. This argument can also be a URL starting with \py{http}.18671868\item \py{header}: Indicates which row of each table should be considered the header, that is, the set of labels that identify the columns. In this case it is the first row (numbered 0).18691870\item \py{index_col}: Indicates which column of each table should be considered the {\bf index}, that is, the set of labels that identify the rows. In this case it is the first column, which contains the years.18711872\item \py{decimal}: Normally this argument is used to indicate which character should be considered a decimal point, because some conventions use a period and some use a comma. In this case I am abusing the feature by treating \py{M} as a decimal point, which allows some of the estimates, which are expressed in millions, to be read as numbers.18731874\end{itemize}18751876The result, which is assigned to \py{tables}, is a sequence that contains one \py{DataFrame} for each table. A \py{DataFrame} is an object, defined by Pandas, that represents tabular data.18771878\index{DataFrame}1879\index{sequence}18801881To select a \py{DataFrame} from \py{tables}, we can use the bracket operator like this:18821883\begin{python}1884table2 = tables[2]1885\end{python}18861887This line selects the third table (numbered 2), which contains population estimates from 1950 to 2016.18881889\index{bracket operator}1890\index{operator!bracket}18911892We can display the first few lines like this:18931894\begin{python}1895table2.head()1896\end{python}18971898The column labels are long strings, which makes them hard to work with. We can replace them with shorter strings like this:18991900\index{string}1901\index{columns}19021903\begin{python}1904table2.columns = ['census', 'prb', 'un', 'maddison',1905'hyde', 'tanton', 'biraben', 'mj',1906'thomlinson', 'durand', 'clark']1907\end{python}19081909Now we can select a column from the \py{DataFrame} using the dot operator, like selecting a state variable from a \py{State} object:19101911\index{dot operator}1912\index{operator!dot}19131914\begin{python}1915census = table2.census / 1e91916un = table2.un / 1e91917\end{python}19181919These lines select the estimates generated by the United Nations Department of Economic and Social Affairs (UN DESA) and the United States Census Bureau.19201921\index{United Nations}1922\index{United States Census Bureau}19231924Each result is a Pandas \py{Series}, which is like a \py{DataFrame} with just one column.19251926\index{Series}19271928The number \py{1e9} is a shorter, less error-prone way to write \py{1000000000} or one billion. When we divide a \py{Series} by a number, it divides all of the elements of the \py{Series}. From here on, we'll express population estimates in terms of billions.192919301931\section{Plotting}19321933Now we can plot the estimates like this:19341935\index{plot}19361937\begin{python}1938plot(census, ':', label='US Census')1939plot(un, '--', label='UN DESA')1940\end{python}194119421943The next two lines plot the \py{Series} objects. The {\bf format strings} \py{':'} and \py{'--'} indicate dotted and dashed lines. For more about format strings in Pyplot, see \url{http://modsimpy.com/plot}.19441945\index{format string}1946\index{Pyplot}19471948The \py{label} argument provides the string that appears in the legend.19491950\index{label}1951\index{legend}19521953\begin{figure}1954\centerline{\includegraphics[height=3in]{figs/chap05-fig01.pdf}}1955\caption{Estimates of world population, 1950--2016.}1956\label{chap05-fig01}1957\end{figure}19581959Figure~\ref{chap05-fig01} shows the result. The lines overlap almost completely; for most dates the difference between the two estimates is less than 1\%.196019611962\section{Constant growth model}19631964Suppose we want to predict world population growth over the next 50 or 100 years. We can do that by developing a model that describes how populations grow, fitting the model to the data we have so far, and then using the model to generate predictions.19651966\index{constant growth}19671968In the next few sections I demonstrate this process starting with simple models and gradually improving them.19691970\index{iterative modeling}19711972Although there is some curvature in the plotted estimates, it looks like world population growth has been close to linear since 1960 or so. So we'll start with a model that has constant growth.19731974To fit the model to the data, we'll compute the average annual growth from 1950 to 2016. Since the UN and Census data are so close, we'll use the Census data.19751976We can select a value from a \py{Series} using the bracket operator:1977\index{bracket operator}1978\index{operator!bracket}19791980\begin{python}1981census[1950]1982\end{python}19831984So we can get the total growth during the interval like this:19851986\begin{python}1987total_growth = census[2016] - census[1950]1988\end{python}19891990The numbers in brackets are called {\bf labels}, because they label the rows of the \py{Series} (not to be confused with the labels we saw in the previous section, which label lines in a graph).19911992\index{label}19931994In this example, the labels 2016 and 1950 are part of the data, so it would be better not to make them part of the program. Putting values like these in the program is called {\bf hard coding}; it is considered bad practice because if the data change in the future, we have to modify the program (see \url{http://modsimpy.com/hardcode}).19951996\index{hard coding}19971998It would be better to get the first and last labels from the \py{Series} like this:19992000\begin{python}2001t_0 = get_first_label(census)2002t_end = get_last_label(census)2003elapsed_time = t_end - t_02004\end{python}20052006\py{get_first_label} and \py{get_last_label} are defined in \py{modsim.py}; as you might have guessed, they select the first and last labels from \py{census}.2007The difference between them is the elapsed time.20082009The ModSim library also defines \py{get_first_value} and \py{get_last_value}, which we can use to compute \py{total_growth}:20102011\begin{python}2012p_0 = get_first_value(census)2013p_end = get_last_value(census)2014total_growth = p_end - p_02015\end{python}20162017Finally, we can compute average annual growth.20182019\begin{python}2020annual_growth = total_growth / elapsed_time2021\end{python}20222023The next step is to use this estimate to simulate population growth since 1950.202420252026\section{Simulation}20272028Our simulation will start with the observed population in 1950, \py{p_0}, and add \py{annual_growth} each year. To store the results, we'll use a \py{TimeSeries} object:20292030\index{TimeSeries}20312032\begin{python}2033results = TimeSeries()2034\end{python}20352036We can set the first value in the new \py{TimeSeries} by copying the first value from \py{census}:20372038\begin{python}2039results[t_0] = census[p_0]2040\end{python}20412042Then we set the rest of the values by simulating annual growth:20432044\begin{python}2045for t in linrange(t_0, t_end):2046results[t+1] = results[t] + annual_growth2047\end{python}20482049\py{linrange} is defined in the ModSim library. In this example it returns a NumPy array of integers from \py{t_0} to \py{t_end}, including the first but not the last.20502051\index{linrange}2052\index{NumPy}2053\index{array}20542055Each time through the loop, the loop variable \py{t} gets the next value from the array. Inside the loop, we compute the population for each year by adding the population for the previous year and \py{annual_growth}. The last time through the loop, the value of \py{t} is 2015, so the last label in \py{results} is 2016, which is what we want.20562057\index{loop}2058\index{loop variable}20592060\begin{figure}2061\centerline{\includegraphics[height=3in]{figs/chap05-fig02.pdf}}2062\caption{Estimates of world population, 1950--2016, and a constant growth model.}2063\label{chap05-fig02}2064\end{figure}20652066Figure~\ref{chap05-fig02} shows the result. The model does not fit the data particularly well from 1950 to 1990, but after that, it's pretty good. Nevertheless, there are problems:20672068\begin{itemize}20692070\item There is no obvious mechanism that could cause population growth to be constant from year to year. Changes in population are determined by the fraction of people who die and the fraction of people who give birth, so we expect them to depend on the current population.20712072\item According to this model, we would expect the population to keep growing at the same rate forever, and that does not seem reasonable.20732074\end{itemize}20752076We'll try out some different models in the next few sections, but first let's clean up the code.20772078Before you go on, you might want to read the notebook for this chapter, \py{chap05.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.207920802081\chapter{Modeling growth}2082\label{chap06}2083In the previous chapter we simulated a model of world population with constant growth. In this chapter we see if we can make a better model with growth proportional to the population.20842085But first, we can improve the code from the previous chapter by encapsulating it in a function and using \py{System} objects.20862087\section{System objects}2088\label{nowwithsystem}20892090Like a \py{State} object, a \py{System} object contains variables and their values. The difference is:20912092\begin{itemize}20932094\item \py{State} objects contain state variables, which represent the state of the system, which get updated in the course of a simulation.20952096\item \py{System} objects contain {\bf system variables}, which represent parameters of the system, which usually don't get updated over the course of a simulation.20972098\end{itemize}20992100For example, in the bike share model, state variables include the number of bikes at each location, which get updated whenever a customer moves a bike. System variables include the number of locations, total number of bikes, and arrival rates at each location.21012102In the population model, the only state variable is the population. System variables include the annual growth rate, the initial time and population, and the end time.21032104Suppose we have the following variables, as computed in the previous chapter (assuming that \py{census} is a \py{Series} object):21052106\begin{python}2107t_0 = get_first_label(census)2108t_end = get_last_label(census)2109elapsed_time = t_end - t_021102111p_0 = get_first_value(census)2112p_end = get_last_value(census)2113total_growth = p_end - p_021142115annual_growth = total_growth / elapsed_time2116\end{python}21172118Some of these are parameters we need to simulate the system; others are temporary values we can discard. We can put the parameters we need into a \py{System} object like this:21192120\index{System object}21212122\begin{python}2123system = System(t_0=t_0,2124t_end=t_end,2125p_0=p_0,2126annual_growth=annual_growth)2127\end{python}21282129\py{t0} and \py{t_end} are the first and last years; \py{p_0} is the initial population, and \py{annual_growth} is the estimated annual growth.21302131Next we'll wrap the code from the previous chapter in a function:21322133\begin{python}2134def run_simulation1(system):2135results = TimeSeries()2136results[system.t_0] = system.p_021372138for t in linrange(system.t_0, system.t_end):2139results[t+1] = results[t] + system.annual_growth21402141return results2142\end{python}21432144When \py{run_simulation1} runs, it stores the results in a \py{TimeSeries} and returns it.21452146\index{TimeSeries object}21472148The following function plots the results along with the estimates \py{census} and \py{un}:21492150\begin{python}2151def plot_results(census, un, timeseries, title):2152plot(census, ':', label='US Census')2153plot(un, '--', label='UN DESA')2154plot(timeseries, color='gray', label='model')21552156decorate(xlabel='Year',2157ylabel='World population (billion)',2158title=title)2159\end{python}21602161\index{plot}2162\index{decorate}21632164The \py{color} argument specifies the color of the line. For details on color specification in Pyplot, see \url{http://modsimpy.com/color}.21652166\index{Pyplot}2167\index{color}21682169Finally, we can run the simulation like this.21702171\begin{python}2172results = run_simulation1(system)2173plot_results(census, un, results, 'Constant growth model')2174\end{python}21752176The results are the same as Figure~\ref{chap05-fig02}.21772178It might not be obvious that using functions and \py{System} objects is a big improvement, and for a simple model that we run only once, maybe it's not. But as we work with more complex models, and when we run many simulations with different parameters, we'll see that the organization of the code makes a big difference.21792180Now let's see if we can improve the model.218121822183\section{Proportional growth model}21842185The biggest problem with the constant growth model is that it doesn't make any sense. It is hard to imagine how people all over the world could conspire to keep population growth constant from year to year.21862187\index{proportional growth}21882189On the other hand, if some fraction of the population dies each year, and some fraction gives birth, we can compute the net change in the population like this:21902191\begin{python}2192def run_simulation2(system):2193results = TimeSeries()2194results[system.t_0] = system.p_021952196for t in linrange(system.t_0, system.t_end):2197births = system.birth_rate * results[t]2198deaths = system.death_rate * results[t]2199results[t+1] = results[t] + births - deaths22002201return results2202\end{python}22032204Now we can choose the values of \py{birth_rate} and \py{death_rate} that best fit the data. Without trying too hard, I chose:22052206\begin{python}2207system.death_rate = 0.012208system.birth_rate = 0.0272209\end{python}22102211Then I ran the simulation and plotted the results:22122213\begin{python}2214results = run_simulation2(system)2215plot_results(census, un, results, 'Proportional model')2216\end{python}22172218\begin{figure}2219\centerline{\includegraphics[height=3in]{figs/chap06-fig01.pdf}}2220\caption{Estimates of world population, 1950--2016, and a proportional model.}2221\label{chap06-fig01}2222\end{figure}22232224Figure~\ref{chap06-fig01} shows the results. The proportional model fits the data well from 1950 to 1965, but not so well after that. Overall, the {\bf quality of fit} is not as good as the constant growth model, which is surprising, because it seems like the proportional model is more realistic.22252226In the next chapter we'll try one more time to find a model that makes sense and fits the data. But first, I want to make a few more improvements to the code.222722282229\section{Factoring out the update function}22302231\py{run_simulation1} and \py{run_simulation2} are nearly identical except for the body of the \py{for} loop, where we compute the population for the next year.22322233\index{update function}2234\index{function!update}22352236Rather than repeat identical code, we can separate the things that change from the things that don't. First, I'll pull out the update code from \py{run_simulation2} and make it a function:22372238\begin{python}2239def update_func1(pop, t, system):2240births = system.birth_rate * pop2241deaths = system.death_rate * pop2242return pop + births - deaths2243\end{python}22442245This function takes as arguments the current population, current year, and a \py{System} object; it returns the computed population for the next year.22462247This update function does not use \py{t}, so we could leave it out. But we will see other functions that need it, and it is convenient if they all take the same parameters, used or not.22482249Now we can write a function that runs any model:22502251\begin{python}2252def run_simulation(system, update_func):2253results = TimeSeries()2254results[system.t_0] = system.p_022552256for t in linrange(system.t_0, system.t_end):2257results[t+1] = update_func(results[t], t, system)22582259return results2260\end{python}22612262This function demonstrates a feature we have not seen before: it takes a function as a parameter! When we call \py{run_simulation}, the second parameter is a function, like \py{update_func1}, that computes the population for the next year.22632264\index{function!as parameter}22652266Here's how we call it:22672268\begin{python}2269results = run_simulation(system, update_func1)2270\end{python}22712272Passing a function as an argument is the same as passing any other value. The argument, which is \py{update_func1} in this example, gets assigned to the parameter, which is called \py{update_func}. Inside \py{run_simulation}, we can run \py{update_func} just like any other function.22732274The loop in \py{run_simulation} calls \py{update_func1} once for each year between \py{t_0} and \py{t_end-1}. The result is the same as Figure~\ref{chap06-fig01}.227522762277\section{Combining birth and death}22782279While we are at it, we can also simplify the code by combining births and deaths to compute the net growth rate. Instead of two parameters, \py{birth_rate} and \py{death_rate}, we can write the update function in terms of a single parameter that represents the difference:22802281\begin{python}2282system.alpha = system.birth_rate - system.death_rate2283\end{python}22842285The name of this parameter, \py{alpha}, is the conventional name for a proportional growth rate.22862287Here's the modified version of \py{update_func1}:22882289\begin{python}2290def update_func2(pop, t, system):2291net_growth = system.alpha * pop2292return pop + net_growth2293\end{python}22942295And here's how we run it:22962297\begin{python}2298results = run_simulation(system, update_func2)2299\end{python}23002301Again, the result is the same as Figure~\ref{chap06-fig01}.23022303Before you go on, you might want to read the notebook for this chapter, \py{chap06.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.2304230523062307\chapter{Quadratic growth}2308\label{chap07}23092310In the previous chapter we developed a population model where net growth during each time step is proportional to the current population. This model seems more realistic than the constant growth model, but it does not fit the data as well.23112312There are a few things we could try to improve the model:23132314\begin{itemize}23152316\item Maybe the net growth rate varies over time.23172318\item Maybe net growth depends on the current population, but the relationship is quadratic, not linear.23192320\end{itemize}23212322In the notebook for this chapter, you will have a chance to try the first option. In this chapter, we explore the second.232323242325\section{Quadratic growth}2326\label{quadratic}23272328It makes sense that net growth should depend on the current population, but maybe it's not a linear relationship, like this:23292330\begin{python}2331net_growth = system.alpha * pop2332\end{python}23332334Maybe it's a quadratic relationship, like this:23352336\index{quadratic growth}23372338\begin{python}2339net_growth = system.alpha * pop + system.beta * pop**22340\end{python}23412342We can test that conjecture with a new update function:23432344\begin{python}2345def update_func_quad(pop, t, system):2346net_growth = system.alpha * pop + system.beta * pop**22347return pop + net_growth2348\end{python}23492350Now we need two parameters. I chose the following values by trial and error; we will see better ways to do it later.23512352\index{parameter}23532354\begin{python}2355system.alpha = 0.0252356system.beta = -0.00182357\end{python}23582359And here's how we run it:23602361\begin{python}2362results = run_simulation(system, update_func_quad)2363\end{python}23642365\begin{figure}2366\centerline{\includegraphics[height=3in]{figs/chap07-fig01.pdf}}2367\caption{Estimates of world population, 1950--2016, and a quadratic model.}2368\label{chap07-fig01}2369\end{figure}23702371Figure~\ref{chap07-fig01} shows the result. The model fits the data well over the whole range, with just a bit of space between them in the 1960s.23722373Of course, we should expect the quadratic model to fit better than the constant and proportional models because it has two parameters we can choose, where the other models have only one. In general, the more parameters you have to play with, the better you should expect the model to fit.23742375\index{quality of fit}2376\index{data}2377\index{fitting data}23782379But fitting the data is not the only reason to think the quadratic model might be a good choice. It also makes sense; that is, there is a legitimate reason to expect the relationship between growth and population to have this form.23802381To understand it, let's look at net growth as a function of population. Here's how we compute it:23822383\begin{python}2384pop_array = linspace(0, 15, 100)2385net_growth_array = (system.alpha * pop_array +2386system.beta * pop_array**2)2387\end{python}23882389\py{pop_array} contains 100 equally spaced values from 0 to 15. \py{net_growth_array} contains the corresponding 100 values of net growth. We can plot the results like this:23902391\begin{python}2392plot(pop_array, net_growth_array)2393\end{python}23942395Previously we have used \py{plot} with \py{Series} objects. In this example, we use two NumPy arrays, corresponding to the x and y axes.23962397\begin{figure}2398\centerline{\includegraphics[height=3in]{figs/chap07-fig02.pdf}}2399\caption{Net growth as a function of population.}2400\label{chap07-fig02}2401\end{figure}24022403Figure~\ref{chap07-fig02} shows the result. Note that the x-axis is not time, as in the previous figures, but population. We can divide this curve into four regimes of behavior:2404\index{regime}24052406\begin{itemize}24072408\item When the population is less than 3-4 billion, net growth is proportional to population, as in the proportional model. In this regime, the population grows slowly because the population is small.24092410\item Between 4 billion and 10 billion, the population grows quickly because there are a lot of people.24112412\item Above 10 billion, population grows more slowly; this behavior models the effect of resource limitations that lower birth rates or increase death rates.24132414\item Above 14 billion, resources are so limited that the death rate exceeds the birth rate and net growth becomes negative.24152416\end{itemize}24172418Just below 14 billion, there is a point where net growth is 0, which means that the population does not change. At this point, the birth and death rates are equal, so the population is in {\bf equilibrium}.24192420\index{equilibrium}242124222423\section{Equilibrium}2424\label{equilibrium}24252426To find the equilibrium point, we can find the roots, or zeros, of this equation:2427%2428\[ \Delta p = \alpha p + \beta p^2 \]2429%2430where $\Delta p$ is net population growth, $p$ is current population, and $\alpha$ and $\beta$ are the parameters of the model. We can rewrite the right hand side like this:2431%2432\[ \Delta p = p (\alpha + \beta p) \]2433%2434which is $0$ when $p=0$ or $p=-\alpha/\beta$. In this example, $\alpha = 0.025$ and $\beta = -0.0018$, so $-\alpha/\beta = 13.9$.24352436In the context of population modeling, the quadratic model is more conventionally written like this:2437%2438\[ \Delta p = r p (1 - p / K) \]2439%2440This is the same model; it's just a different way to {\bf parameterize} it. Given $\alpha$ and $\beta$, we can compute $r=\alpha$ and $K=-\alpha/\beta$.24412442\index{parameterize}24432444In this version, it is easier to interpret the parameters: $r$ is the maximum growth rate, observed when $p$ is small, and $K$ is the equilibrium point. $K$ is also called the {\bf carrying capacity}, since it indicates the maximum population the environment can sustain.24452446\index{carrying capacity}24472448In the next chapter we use the models we have developed to generate predictions.24492450\section{Dysfunctions}24512452When people learn about functions, there are a few things they often find confusing. In this section I present and explain some common problems.24532454As an example, suppose you want a function that takes as a parameter \py{System} object with variables \py{alpha} and \py{beta}, and computes the carrying capacity, \py{-alpha/beta}. Here's a good solution:24552456\begin{python}2457def carrying_capacity(system):2458K = -system.alpha / system.beta2459return K24602461sys1 = System(alpha=0.025, beta=-0.0018)2462pop = carrying_capacity(sys1)2463print(pop)2464\end{python}24652466Now let's see all the ways that can go wrong.24672468Dysfunction \#1: Not using parameters. In the following version, the function doesn't take any parameters; when \py{sys1} appears inside the function, it refers to the object we create outside the function.24692470\begin{python}2471def carrying_capacity():2472K = -sys1.alpha / sys1.beta2473return K24742475sys1 = System(alpha=0.025, beta=-0.0018)2476pop = carrying_capacity()2477print(pop)2478\end{python}24792480This version actually works, but it is not as versatile as it could be. If there are several \py{System} objects, this function can only work with one of them, and only if it is named \py{sys1}.24812482Dysfunction \#2: Clobbering the parameters. When people first learn about parameters, they often write functions like this:24832484\begin{python}2485# WRONG2486def carrying_capacity(system):2487system = System(alpha=0.025, beta=-0.0018)2488K = -system.alpha / system.beta2489return K24902491sys1 = System(alpha=0.03, beta=-0.002)2492pop = carrying_capacity(sys1)2493print(pop)2494\end{python}24952496In this example, we have a \py{System} object named \py{sys1} that gets passed as an argument to \py{carrying_capacity}. But when the function runs, it ignores the argument and immediately replaces it with a new \py{System} object. As a result, this function always returns the same value, no matter what argument is passed.24972498When you write a function, you generally don't know what the values of the parameters will be. Your job is to write a function that works for any valid values. If you assign your own values to the parameters, you defeat the whole purpose of functions.249925002501Dysfunction \#3: No return value. Here's a version that computes the value of \py{K} but doesn't return it.25022503\begin{python}2504# WRONG2505def carrying_capacity(system):2506K = -system.alpha / system.beta25072508sys1 = System(alpha=0.025, beta=-0.0018)2509pop = carrying_capacity(sys1)2510print(pop)2511\end{python}25122513A function that doesn't have a return statement always returns a special value called \py{None}, so in this example the value of \py{pop} is \py{None}. If you are debugging a program and find that the value of a variable is \py{None} when it shouldn't be, a function without a return statement is a likely cause.2514\index{None}25152516Dysfunction \#4: Ignoring the return value. Finally, here's a version where the function is correct, but the way it's used is not.25172518\begin{python}2519# WRONG2520def carrying_capacity(system):2521K = -system.alpha / system.beta2522return K25232524sys1 = System(alpha=0.025, beta=-0.0018)2525carrying_capacity(sys1)2526print(K)2527\end{python}25282529In this example, \py{carrying_capacity} runs and returns \py{K}, but the return value is dropped.25302531When you call a function that returns a value, you should do something with the result. Often you assign it to a variable, as in the previous examples, but you can also use it as part of an expression. For example, you could eliminate the temporary variable \py{pop} like this:25322533\begin{python}2534print(carrying_capacity(sys1))2535\end{python}25362537Or if you had more than one system, you could compute the total carrying capacity like this:25382539\begin{python}2540total = carrying_capacity(sys1) + carrying_capacity(sys2)2541\end{python}25422543Before you go on, you might want to read the notebook for this chapter, \py{chap07.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.25442545254625472548\chapter{Prediction}2549\label{chap08}25502551In the previous chapter we developed a quadratic model of world population growth from 1950 to 2016. It is a simple model, but it fits the data well and the mechanisms it's based on are plausible.25522553In this chapter we'll use the quadratic model to generate projections of future growth, and compare our results to projections from actual demographers. Also, we'll represent the models from the previous chapters as differential equations and solve them analytically.25542555\index{prediction}2556\index{projection}255725582559\section{Generating projections}25602561We'll start with the quadratic model from Section~\ref{quadratic}, which is based on this update function:2562\index{quadratic growth}25632564\begin{python}2565def update_func_quad(pop, t, system):2566net_growth = system.alpha * pop + system.beta * pop**22567return pop + net_growth2568\end{python}25692570As we saw in the previous chapter, we can get the start date, end date, and initial population from \py{census}, which is a series that contains world population estimates generated by the U.S. Census:25712572\begin{python}2573t_0 = get_first_label(census)2574t_end = get_last_label(census)2575p_0 = census[t_0]2576\end{python}25772578Now we can create a \py{System} object:2579\index{System object}25802581\begin{python}2582system = System(t_0=t_0,2583t_end=t_end,2584p_0=p_0,2585alpha=0.025,2586beta=-0.0018)2587\end{python}25882589And run the model:25902591\begin{python}2592results = run_simulation(system, update_func_quad)2593\end{python}25942595We have already seen the results in Figure~\ref{chap07-fig01}. Now, to generate a projection, the only thing we have to change is \py{t_end}:25962597\begin{python}2598system.t_end = 22502599results = run_simulation(system, update_func_quad)2600\end{python}26012602\begin{figure}2603\centerline{\includegraphics[height=3in]{figs/chap08-fig01.pdf}}2604\caption{Quadratic model of world population growth, with projection from 2016 to 2250.}2605\label{chap08-fig01}2606\end{figure}26072608Figure~\ref{chap08-fig01} shows the result, with a projection until 2250. According to this model, population growth will continue almost linearly for the next 50--100 years, then slow over the following 100 years, approaching 13.9 billion by 2250.26092610I am using the word ``projection" deliberately, rather than ``prediction", with the following distinction: ``prediction" implies something like ``this is what we should reasonably expect to happen, at least approximately"; ``projection" implies something like ``if this model is actually a good description of what is happening in this system, and if nothing in the future causes the parameters of the model to change, this is what would happen."26112612Using ``projection" leaves open the possibility that there are important things in the real world that are not captured in the model. It also suggests that, even if the model is good, the parameters we estimate based on the past might be different in the future.26132614The quadratic model we've been working with is based on the assumption that population growth is limited by the availability of resources; in that scenario, as the population approaches carrying capacity, birth rates fall and death rates rise because resources become scarce.26152616\index{carrying capacity}26172618If that assumption is valid, we might be able to use actual population growth to estimate carrying capacity, especially if we observe the transition into the regime where the growth rate starts to fall.26192620But in the case of world population growth, those conditions don't apply. Over the last 50 years, the net growth rate has leveled off, but not yet started to fall, so we don't have enough data to make a credible estimate of carrying capacity. And resource limitations are probably {\em not} the primary reason growth has slowed. As evidence, consider:26212622\begin{itemize}26232624\item First, the death rate is not increasing; rather, it has declined from 1.9\% in 1950 to 0.8\% now (see \url{http://modsimpy.com/mortality}). So the decrease in net growth is due entirely to declining birth rates.26252626\index{mortality rate}26272628\item Second, the relationship between resources and birth rate is the opposite of what the model assumes; as nations develop and people become more wealthy, birth rates tend to fall.26292630\index{birth rate}26312632\end{itemize}26332634We should not take too seriously the idea that this model can estimate carrying capacity. But the predictions of a model can be credible even if the assumptions of the model are not strictly true. For example, population growth might behave {\em as if} it is resource limited, even if the actual mechanism is something else.26352636In fact, demographers who study population growth often use models similar to ours. In the next section, we'll compare our projections to theirs.263726382639\section{Comparing projections}26402641Table 3 from \url{http://modsimpy.com/worldpop} contains projections from the U.S. Census and the United Nations DESA:26422643\begin{python}2644table3 = tables[3]2645\end{python}26462647For some years, one agency or the other has not published a projection, so some elements of \py{table3} contain the special value \py{NaN}, which stands for ``not a number". \py{NaN} is often used to indicate missing data.26482649\index{not a number}2650\index{NaN}2651\index{missing data}26522653\begin{figure}2654\centerline{\includegraphics[height=3in]{figs/chap08-fig02.pdf}}2655\caption{Projections of world population generated by the U.S. Census Bureau, the United Nations, and our quadratic model.}2656\label{chap08-fig02}2657\end{figure}26582659Pandas provides functions that deal with missing data, including \py{dropna}, which removes any elements in a series that contain \py{NaN}. Using \py{dropna}, we can plot the projections like this:26602661\index{Pandas}2662\index{dropna}26632664\begin{python}2665def plot_projections(table):2666census_proj = table.census / 1e92667un_proj = table.un / 1e926682669plot(census_proj.dropna(), 'b:', label='US Census')2670plot(un_proj.dropna(), 'g--', label='UN DESA')2671\end{python}26722673The format string \py{'b:'} indicates a blue dotted line; \py{g--} indicates a green dashed line.26742675\index{format string}2676\index{plot}26772678We can run our model over the same interval:26792680\begin{python}2681system.t_end = 21002682results = run_simulation(system, update_func_quad)2683\end{python}26842685And compare our projections to theirs. Figure~\ref{chap08-fig02} shows the results. Real demographers expect world population to grow more slowly than our model projects, probably because their models are broken down by region and country, where conditions are different, and they take into account expected economic development.26862687\index{demography}26882689Nevertheless, their projections are qualitatively similar to ours, and theirs differ from each other almost as much as they differ from ours. So the results from this model, simple as it is, are not entirely crazy.26902691Before you go on, you might want to read the notebook for this chapter, \py{chap08.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.269226932694\chapter{Analysis}2695\label{chap09}26962697In this chapter we express the models from previous chapters as difference equations and differential equations, solve the equations, and derive the functional forms of the solutions. We also discuss the complementary roles of mathematical analysis and simulation.269826992700\section{Recurrence relations}27012702The population models in the previous chapter and this one are simple enough that we didn't really need to run simulations. We could have solved them mathematically. For example, we wrote the constant growth model like this:27032704\begin{python}2705model[t+1] = model[t] + annual_growth2706\end{python}27072708In mathematical notation, we would write the same model like this:2709%2710\[ x_{n+1} = x_n + c \]2711%2712where $x_n$ is the population during year $n$, $x_0$ is a given initial population, and $c$ is constant annual growth. This way of representing the model is a {\bf recurrence relation}; see \url{http://modsimpy.com/recur}.27132714\index{recurrence relation}27152716Sometimes it is possible to solve a recurrence relation by writing an equation that computes $x_n$, for a given value of $n$, directly; that is, without computing the intervening values from $x_1$ through $x_{n-1}$.27172718In the case of constant growth we can see that $x_1 = x_0 + c$, and $x_2 = x_1 + c$. Combining these, we get $x_2 = x_0 + 2c$, then $x_3 = x_0 + 3c$, and it is not hard to conclude that in general2719%2720\[ x_n = x_0 + nc \]2721%2722So if we want to know $x_{100}$ and we don't care about the other values, we can compute it with one multiplication and one addition.27232724We can also write the proportional model as a recurrence relation:2725%2726\[ x_{n+1} = x_n + \alpha x_n \]2727%2728Or more conventionally as:2729%2730\[ x_{n+1} = x_n (1 + \alpha) \]2731%2732Now we can see that $x_1 = x_0 (1 + \alpha)$, and $x_2 = x_0 (1 + \alpha)^2$, and in general2733%2734\[ x_n = x_0 (1 + \alpha)^n \]2735%2736This result is a {\bf geometric progression}; see \url{http://modsimpy.com/geom}. When $\alpha$ is positive, the factor $1+\alpha$ is greater than 1, so the elements of the sequence grow without bound.27372738\index{geometric progression}2739\index{quadratic growth}27402741Finally, we can write the quadratic model like this:2742%2743\[ x_{n+1} = x_n + \alpha x_n + \beta x_n^2 \]2744%2745or with the more conventional parameterization like this:2746%2747\[ x_{n+1} = x_n + r x_n (1 - x_n / K) \]2748%2749There is no analytic solution to this equation, but we can approximate it with a differential equation and solve that, which is what we'll do in the next section.275027512752\section{Differential equations}2753\label{diffeq}27542755Starting again with the constant growth model2756%2757\[ x_{n+1} = x_n + c \]2758%2759If we define $\Delta x$ to be the change in $x$ from one time step to the next, we can write:2760%2761\[ \Delta x = x_{n+1} - x_n = c \]2762%2763If we define $\Delta t$ to be the time step, which is one year in the example, we can write the rate of change per unit of time like this:2764%2765\[ \frac{\Delta x}{\Delta t} = c \]2766%2767This model is {\bf discrete}, which means it is only defined at integer values of $n$ and not in between. But in reality, people are born and die all the time, not once a year, so a {\bf continuous} model might be more realistic.27682769\index{discrete}2770\index{continuous}2771\index{time step}27722773We can make this model continuous by writing the rate of change in the form of a derivative:2774%2775\[ \frac{dx}{dt} = c \]2776%2777This way of representing the model is a {\bf differential equation}; see \url{http://modsimpy.com/diffeq}.27782779\index{differential equation}27802781We can solve this differential equation if we multiply both sides by $dt$:2782%2783\[ dx = c dt \]2784%2785And then integrate both sides:2786%2787\[ x(t) = c t + x_0 \]2788%2789Similarly, we can write the proportional growth model like this:2790%2791\[ \frac{\Delta x}{\Delta t} = \alpha x \]2792%2793And as a differential equation like this:2794%2795\[ \frac{dx}{dt} = \alpha x \]2796%2797If we multiply both sides by $dt$ and divide by $x$, we get2798%2799\[ \frac{1}{x}~dx = \alpha~dt \]2800%2801Now we integrate both sides, yielding:2802%2803\[ \ln x = \alpha t + K \]2804%2805where $\ln$ is the natural logarithm and $K$ is the constant of integration. Exponentiating both sides\footnote{The exponential function can be written $\exp(x)$ or $e^x$. In this book I use the first form because it resembles the Python code. }, we have2806%2807\[ \exp(\ln(x)) = \exp(\alpha t + K) \]2808%2809which we can rewrite2810%2811\[ x = \exp(\alpha t) \exp(K) \]2812%2813Since $K$ is an arbitrary constant, $\exp(K)$ is also an arbitrary constant, so we can write2814%2815\[ x = C \exp(\alpha t) \]2816%2817where $C = \exp(K)$. There are many solutions to this differential equation, with different values of $C$. The particular solution we want is the one that has the value $x_0$ when $t=0$.28182819When $t=0$, $x(t) = C$, so $C = x_0$ and the solution we want is2820%2821\[ x(t) = x_0 \exp(\alpha t) \]2822%2823If you would like to see this derivation done more carefully, you might like this video: \url{http://modsimpy.com/khan1}.28242825\index{logarithm}2826\index{exponentiation}2827\index{integration}2828\index{constant of integration}282928302831\section{Analysis and simulation}28322833Once you have designed a model, there are generally two ways to proceed: simulation and analysis. Simulation often comes in the form of a computer program that models changes in a system over time, like births and deaths, or bikes moving from place to place. Analysis often comes in the form of algebra; that is, symbolic manipulation using mathematical notation.28342835\index{analysis}2836\index{algebra}2837\index{symbolic manipulation}28382839Analysis and simulation have different capabilities and limitations. Simulation is generally more versatile; it is easy to add and remove parts of a program and test many versions of a model, as we have done in the previous examples.28402841But there are several things we can do with analysis that are harder or impossible with simulations:28422843\begin{itemize}28442845\item With analysis we can sometimes compute, exactly and efficiently, a value that we could only approximate, less efficiently, with simulation. For example, in Figure~\ref{chap07-fig02}, we can see that net growth goes to zero near 14 billion, and we could estimate carrying capacity using a numerical search algorithm (more about that later). But with the analysis in Section~\ref{equilibrium}, we get the general result that $K=-\alpha/\beta$.28462847\item Analysis often provides ``computational shortcuts", that is, the ability to jump forward in time to compute the state of a system many time steps in the future without computing the intervening states.28482849\index{time step}28502851\item We can use analysis to state and prove generalizations about models; for example, we might prove that certain results will always or never occur. With simulations, we can show examples and sometimes find counterexamples, but it is hard to write proofs.28522853\index{proof}28542855\item Analysis can provide insight into models and the systems they describe; for example, sometimes we can identify regimes of qualitatively different behavior and key parameters that control those behaviors.28562857\index{regime}28582859\end{itemize}28602861When people see what analysis can do, they sometimes get drunk with power, and imagine that it gives them a special ability to see past the veil of the material world and discern the laws of mathematics that govern the universe. When they analyze a model of a physical system, they talk about ``the math behind it" as if our world is the mere shadow of a world of ideal mathematical entities\footnote{I am not making this up; see \url{http://modsimpy.com/plato}.}.28622863\index{Plato}28642865This is, of course, nonsense. Mathematical notation is a language designed by humans for a purpose, specifically to facilitate symbolic manipulations like algebra. Similarly, programming languages are designed for a purpose, specifically to represent computational ideas and run programs.28662867\index{math notation}2868\index{programming languages}28692870Each of these languages is good for the purposes it was designed for and less good for other purposes. But they are often complementary, and one of the goals of this book is to show how they can be used together.287128722873\section{Analysis with WolframAlpha}28742875Until recently, most analysis was done by rubbing graphite on wood pulp\footnote{Or ``rubbing the white rock on the black rock", a line I got from Woodie Flowers, who got it from Stephen Jacobsen.}, a process that is laborious and error-prone. A useful alternative is symbolic computation. If you have used services like WolframAlpha, you have used symbolic computation.28762877\index{symbolic computation}2878\index{WolframAlpha}28792880For example, if you go to \url{https://www.wolframalpha.com/} and type28812882\begin{python}2883df(t) / dt = alpha f(t)2884\end{python}28852886WolframAlpha infers that \py{f(t)} is a function of \py{t} and \py{alpha} is a parameter; it classifies the query as a ``first-order linear ordinary differential equation", and reports the general solution:2887%2888\[ f(t) = c_1 \exp(\alpha t) \]2889%2890If you add a second equation to specify the initial condition:28912892\begin{python}2893df(t) / dt = alpha f(t), f(0) = p_02894\end{python}28952896WolframAlpha reports the particular solution:28972898\[ f(t) = p_0 \exp(\alpha t) \]28992900WolframAlpha is based on Mathematica, a powerful programming language designed specifically for symbolic computation.29012902\index{Mathematica}290329042905\section{Analysis with SymPy}29062907Python has a library called SymPy that provides symbolic computation tools similar to Mathematica. They are not as easy to use as WolframAlpha, but they have some other advantages.29082909\index{SymPy}29102911Before we can use SymPy, we have to import it:29122913\index{import statement}2914\index{statement!import}29152916\begin{python}2917from sympy import *2918\end{python}29192920SymPy defines many functions, and some of them conflict with functions defined in \py{modsim} and the other libraries we're using. To avoid these conflicts, I suggest that you do symbolic computation with SymPy in a separate notebook.29212922SymPy defines a \py{Symbol} object that represents symbolic variable names, functions, and other mathematical entities.29232924\index{Symbol object}29252926The \py{symbols} function takes a string and returns \py{Symbol} objects. So if we run this assignment:29272928\begin{python}2929t = symbols('t')2930\end{python}29312932Python understands that \py{t} is a symbol, not a numerical value. If we now run29332934\begin{python}2935expr = t + 12936\end{python}29372938Python doesn't try to perform numerical addition; rather, it creates a new \py{Symbol} that represents the sum of \py{t} and \py{1}. We can evaluate the sum using \py{subs}, which substitutes a value for a symbol. This example substitutes 2 for \py{t}:29392940\begin{python}2941expr.subs(t, 2)2942\end{python}29432944The result is 3.29452946Functions in SymPy are represented by a special kind of \py{Symbol}:29472948\begin{python}2949f = Function('f')2950\end{python}29512952Now if we write \py{f(t)}, we get an object that represents the evaluation of a function, $f$, at a value, $t$. But again SymPy doesn't actually try to evaluate it.295329542955\section{Differential equations in SymPy}29562957SymPy provides a function, \py{diff}, that can differentiate a function. We can apply it to \py{f(t)} like this:29582959\index{differential equation}2960\index{SymPy}29612962\begin{python}2963dfdt = diff(f(t), t)2964\end{python}29652966The result is a \py{Symbol} that represents the derivative of \py{f} with respect to \py{t}. But again, SymPy doesn't try to compute the derivative yet.29672968\index{Symbol object}29692970To represent a differential equation, we use \py{Eq}:29712972\begin{python}2973alpha = symbols('alpha')2974eq1 = Eq(dfdt, alpha*f(t))2975\end{python}29762977The result is an object that represents an equation, which is displayed like this:2978%2979\[ \frac{d}{d t} f{\left (t \right )} = \alpha f{\left (t \right )} \]2980%2981Now we can use \py{dsolve} to solve this differential equation:29822983\begin{python}2984solution_eq = dsolve(eq1)2985\end{python}29862987The result is the equation2988%2989\[ f{\left (t \right )} = C_{1} \exp(\alpha t) \]2990%2991This is the {\bf general solution}, which still contains an unspecified constant, $C_1$. To get the {\bf particular solution} where $f(0) = p_0$, we substitute \py{p_0} for \py{C1}. First, we have to create two more symbols:29922993\index{general solution}2994\index{particular solution}29952996\begin{python}2997C1, p_0 = symbols('C1 p_0')2998\end{python}29993000Now we can perform the substitution:30013002\begin{python}3003particular = solution_eq.subs(C1, p_0)3004\end{python}30053006The result is3007%3008\[ f{\left (t \right )} = p_{0} \exp(\alpha t) \]3009%3010This function is called the {\bf exponential growth curve}; see \url{http://modsimpy.com/expo}.30113012\index{exponential growth}301330143015\section{Solving the quadratic growth model}30163017In the notebook for this chapter, you will see how to use the same tools to solve the quadratic growth model with parameters $r$ and $K$. The general solution is3018%3019\[ f{\left (t \right )} = \frac{K \exp(C_{1} K + r t)}{\exp(C_{1} K + r t) - 1} \]3020%3021To get the particular solution where $f(0) = p_0$, we evaluate the general solution at $t=0$, which yields:3022%3023\[ f(0) = \frac{K \exp(C_{1} K)}{\exp(C_{1} K) - 1} \]3024%3025Then we set this expression equal to $p_0$ and solve for $C_1$. The result is:3026%3027\[ C_1 = \frac{1}{K} \ln{\left (- \frac{p_{0}}{K - p_{0}} \right )} \]3028%3029Finally, we substitute this value of $C_1$ into the general solution, which yields:3030%3031\[ f(t) = \frac{K p_{0} \exp(r t)}{K + p_{0} \exp(r t) - p_{0}} \]3032%3033This function is called the {\bf logistic growth curve}; see \url{http://modsimpy.com/logistic}. In the context of growth models, the logistic function is often written, equivalently,3034%3035\[ f(t) = \frac{K}{1 + A \exp(-rt)} \]3036%3037where $A = (K - p_0) / p_0$.30383039If you would like to see this differential equation solved by hand, you might like this video: \url{http://modsimpy.com/khan2}3040\index{quadratic growth}3041\index{logistic function}304230433044\section{Summary}30453046The following tables summarize the results so far:30473048\begin{tabular}{l|l}3049\hline3050Growth type & Discrete (difference equation) \\3051\hline3052Constant & linear: $x_n = p_0 + \alpha n$ \\30533054Proportional & geometric: $x_n = p_0(1+\alpha)^n$ \\30553056\end{tabular}30573058\begin{tabular}{l|l}3059\hline3060& Continuous (differential equation) \\3061\hline3062Constant & linear: $x(t) = p_0 + \alpha t$ \\30633064Proportional & exponential: $x(t) = p_0 \exp(\alpha t)$ \\30653066Quadratic & logistic: $x(t) = K / (1 + A\exp(-rt))$ \\3067\end{tabular}30683069What I've been calling the constant growth model is more commonly called ``linear growth" because the solution is a line. Similarly, what I've called proportional is commonly called ``exponential", and what I've called quadratic is commonly called ``logistic". I avoided these terms until now because they are based on results we had not derived yet.30703071\index{linear growth}3072\index{exponential growth}3073\index{logistic growth}30743075Before you go on, you might want to read the notebook for this chapter, \py{chap09sympy.ipynb}. For instructions on downloading and running the code, see Section~\ref{code}.307630773078\chapter{Case studies}3079\label{chap10}30803081This chapter reviews the computational patterns we have seen so far and presents exercises where you can apply them.30823083\section{Computational tools}30843085In Chapter~\ref{chap01} we used Pint to define units and perform calculations with units:30863087\begin{code}3088meter = UNITS.meter3089second = UNITS.second3090a = 9.8 * meter / second**23091\end{code}30923093In Chapter~\ref{chap02} we defined a \py{State} object that contains variables that represent the state of a system, usually changing over time:30943095\begin{code}3096bikeshare = State(olin=10, wellesley=2)3097\end{code}30983099We used update operators like \py{+=} and \py{-=} to change state variables. We used \py{print} statements to display the values of variables.31003101We used the \py{flip} function to simulate random arrivals, and used \py{if} statements to check the results.31023103We learned to define new functions that take parameters:31043105\begin{code}3106def step(p1, p2):3107if flip(p1):3108bike_to_wellesley()31093110if flip(p2):3111bike_to_olin()3112\end{code}31133114We used a \py{for} loop with the \py{range} function to execute the body of the loop a specified number of times.31153116\begin{code}3117for i in range(4):3118step(p1, p2)3119\end{code}31203121We learned to create a \py{TimeSeries} object and use it to store the value of a state variable as it changes over time:31223123\begin{code}3124results = TimeSeries()31253126for i in range(10):3127step(0.3, 0.2)3128results[i] = bikeshare.olin3129\end{code}31303131We used \py{plot} to plot the results, \py{decorate} to label the axes, and \py{savefig} to save the figure.31323133\begin{code}3134plot(results, label='Olin')3135decorate(xlabel='Time step (min)',3136ylabel='Number of bikes')3137savefig('chap01-fig01.pdf)3138\end{code}31393140In Chapter~\ref{chap03} we used comparison operators to check for certain conditions and the \py{return} statement to end the execution of a function.31413142\begin{code}3143def bike_to_olin(state):3144if state.wellesley == 0:3145state.wellesley_empty += 13146return3147state.wellesley -= 13148state.olin += 13149\end{code}31503151In Chapter~\ref{chap04} we wrote a version of \py{run_simulation} that uses a \py{return} statement to return a value:31523153\begin{code}3154def run_simulation(p1, p2, num_steps):3155state = State(olin=10, wellesley=2,3156olin_empty=0, wellesley_empty=0)31573158for i in range(num_steps):3159step(state, p1, p2)31603161return state3162\end{code}31633164This version of \py{run_simulation} returns the final value of \py{state}, which contains metrics we can use to measure the performance of the system.31653166We used \py{linspace} to create a NumPy array of equally spaced values, and a \py{for} loop to loop through the array. We used a \py{SweepSeries} to store results from a series of simulations, mapping from the value of a parameter to the value of a resulting metric.31673168\begin{code}3169p1_array = linspace(0, 1, 11)3170sweep = SweepSeries()31713172for p1 in p1_array:3173state = run_simulation(p1, p2, num_steps)3174sweep[p1] = state.olin_empty3175\end{code}31763177In Chapter~\ref{chap05} we used Pandas to read data from a web page and store the results in a \py{DataFrame}. We selected a column from the \py{DataFrame} to get a \py{Series}.31783179In Chapter~\ref{chap06} we created a \py{System} object to contain the parameters of the model, and defined another version of \py{run_simulation}:31803181\begin{code}3182def run_simulation(system, update_func):3183results = TimeSeries()3184results[system.t_0] = system.p_031853186for t in linrange(system.t_0, system.t_end):3187results[t+1] = update_func(results[t], t, system)31883189return results3190\end{code}31913192This version takes a \py{System} object as a parameter, and an update function. Instead of returning the final state of the system, it returns a \py{TimeSeries} that contains the state as it changes over time.31933194The update function takes the current state of the system, the time, and the \py{System} object as parameters, and returns the new state. For example, here's the update function for the quadratic growth model.31953196\begin{code}3197def update_func_quad(pop, t, system):3198net_growth = system.alpha * pop + system.beta * pop**23199return pop + net_growth3200\end{code}32013202In this example, the state of the system is a single number, \py{pop}. Later we'll see examples where state is represented by a \py{State} object with more than one variable.32033204Chapter~\ref{chap07} introduces the quadratic growth model and3205Chapter~\ref{chap08} uses the model to generate predictions, but neither chapter introduces new computational tools.32063207Chapter~\ref{chap09} introduces SymPy, which we can use to create \py{Symbol} objects:32083209\begin{code}3210t, alpha = symbols('t alpha')3211f = Function('f')3212\end{code}32133214Write differential equations:32153216\begin{code}3217dfdt = diff(f(t), t)3218eq1 = Eq(dfdt, alpha*f(t))3219\end{code}32203221And solve them:32223223\begin{code}3224solution_eq = dsolve(eq1)3225\end{code}32263227That's a brief summary of the computational tools we have seen so far.322832293230\section{Under the hood}3231\label{dataframe}32323233So far we've been using \py{DataFrame} and \py{Series} objects without really understanding how they work. In this section we'll review what we know so far and get into a little more detail.32343235Each \py{DataFrame} contains three objects: \py{index} is a sequence of labels for the rows, \py{columns} is a sequence of labels for the columns, and \py{values} is a NumPy array that contains the data.32363237In the \py{DataFrame} objects in this chapter, \py{index} contains years from 1950 to 2016, \py{columns} contains names of agencies and people that produce population estimates, and \py{values} is an array of estimates.32383239\begin{figure}3240\centerline{\includegraphics[height=2.5in]{figs/dataframe.pdf}}3241\caption{The elements that make up a \py{DataFrame} and a \py{Series}.}3242\label{fig-dataframe}3243\end{figure}32443245A \py{Series} is like a \py{DataFrame} with one column: it contains a string \py{name} that is like a column label, an index, and an array of values.32463247Figure~\ref{fig-dataframe} shows the elements of a \py{DataFrame} and \py{Series} graphically.32483249\index{type function}32503251To determine the types of these elements, we can use the Python function \py{type}:32523253\begin{code}3254type(table2)3255type(table2.index)3256type(table2.columns)3257type(table2.values)3258\end{code}32593260The type of \py{table2} is \py{DataFrame}. The type of \py{table2.index} is \py{Int64Index}, which is similar to a \py{Series}.32613262The type of \py{table2.columns} is \py{Index}, which might seem strange, because ``the" index is the sequence of row labels. But the sequence of column labels is also a kind of index.32633264The type of \py{table2.values} is \py{ndarray}, which is the primary array type provided by NumPy; the name indicates that the array is ``n-dimensional"; that is, it can have an arbitrary number of dimensions.32653266In \py{census} or \py{un}, the index is an \py{Int64Index} object and the values are stored in an \py{ndarray}.32673268In the ModSim library, the functions \py{get_first_label} and \py{get_last_label} provide a simple way to access the index of a \py{DataFrame} or \py{Series}:32693270\begin{code}3271def get_first_label(series):3272return series.index[0]32733274def get_last_label(series):3275return series.index[-1]3276\end{code}32773278In brackets, the number \py{0} selects the first label; the number \py{-1} selects the last label.32793280Several of the objects defined in \py{modsim} are modified versions of \py{Series} objects. \py{State} and \py{System} objects are \py{Series} where the labels are variable names. A \py{TimeSeries} is a \py{Series} where the labels are times, and a \py{SweepSeries} is a \py{Series} where the labels are parameter values.32813282Defining these objects wasn't necessary; we could do all the same things using \py{Series} objects. But giving them different names makes the code easier to read and understand, and helps avoid certain kinds of errors (like getting two \py{Series} objects mixed up).32833284If you write simulations in Python in the future, you can continue using the objects in \py{modsim}, if you find them useful, or you can use Pandas objects directly.32853286\section{One queue or two?}32873288This chapter presents two cases studies that let you practice what you have learned so far. The first case study is related to {\bf queueing theory}, which is the study of systems that involve waiting in lines, also known as ``queues".32893290Suppose you are designing the checkout area for a new store. There is enough room in the store for two checkout counters and a waiting area for customers. You can make two lines, one for each counter, or one line that feeds both counters.32913292In theory, you might expect a single line to be better, but it has some practical drawbacks: in order to maintain a single line, you might have to install barriers, and customers might be put off by what seems to be a longer line, even if it moves faster.32933294So you'd like to check whether the single line is really better and by how much. Simulation can help answer this question.32953296\begin{figure}3297\centerline{\includegraphics[width=4.5in]{figs/queue.pdf}}3298\caption{One queue, one server (left), one queue, two servers (middle), two queues, two servers (right).}3299\label{fig-queue}3300\end{figure}33013302Figure~\ref{fig-queue} shows the three scenarios we'll consider. As we did in the bike share model, we'll assume that a customer is equally likely to arrive during any time step. I'll denote this probability using the Greek letter lambda, $\lambda$, or the variable name \py{lam}. The value of $\lambda$ probably varies from day to day, so we'll have to consider a range of possibilities.33033304Based on data from other stores, you know that it takes 5 minutes for a customer to check out, on average. But checkout times are variable: most customers take less than 5 minutes, but some take substantially more. A simple way to model this variability is to assume that when a customer is checking out, they always have the same probability of finishing during the next time step, regardless of how long they have been checking out. I'll denote this probability using the Greek letter mu, $\mu$, or the variable name \py{mu}.33053306If we choose $\mu=1/5$ per minute, the average time for each checkout will be 5 minutes, which is consistent with the data. Most people takes less than 5 minutes, but a few take substantially longer, which is probably not a bad model of the distribution in real stores.33073308Now we're ready to get started. In the repository for this book, you'll find a notebook called \py{queue.ipynb} that contains some code to get you started and instructions.33093310As always, you should practice incremental development: write no more than one or two lines of code a time, and test as you go!33113312331333143315\section{Predicting salmon populations}33163317Each year the U.S. Atlantic Salmon Assessment Committee reports estimates of salmon populations in oceans and rivers in the northeastern United States. The reports are useful for monitoring changes in these populations, but they generally do not include predictions.33183319The goal of this case study is to model year-to-year changes in population, evaluate how predictable these changes are, and estimate the probability that a particular population will increase or decrease in the next 10 years.33203321As an example, I use data from page 18 of the 2017 report, which provides population estimates for the Narraguagus and Sheepscot Rivers in Maine.33223323In the repository for this book, you'll find a notebook called \py{salmon.ipynb} that contains some code to get you started and instructions.33243325You should take my instructions as suggestions; if you want to try something different, please do!332633273328\section{Tree growth}33293330This case study is based on ``Height-Age Curves for Planted Stands of Douglas Fir, with Adjustments for Density", a working paper by Flewelling, Collier, Gonyea, Marshall, and Turnblom.33313332% TODO: Add paper to GitHub \url{http://modsimpy.com/trees}33333334It provides ``site index curves", which are curves that show the expected height of the tallest tree in a stand of Douglas firs as a function of age, for a stand where the trees are the same age.33353336Depending on the quality of the site, the trees might grow more quickly or slowing. So each curve is identified by a ``site index" that indicates the quality of the site.33373338\begin{figure}3339\centerline{\includegraphics[height=3in]{figs/trees-fig01.pdf}}3340\caption{Site index curves for tree growth.}3341\label{trees-fig01}3342\end{figure}33433344Figure~\ref{trees-fig01} shows site curves for three different site indices.3345The goal of this case study is to explain the shape of these curves, that is, why trees grow the way they do.33463347As a starting place, let's assume that the ability of the tree to gain mass is limited by the area it exposes to sunlight, and that the growth rate (in mass) is proportional to that area. In that case we can write:3348%3349$ m_{n+1} = m_n + \alpha A$3350%3351where $m_n$ is the mass of the at time step $n$, $A$ is the area exposed to sunlight, and $\alpha$ is an unknown growth parameter.33523353To get from $m$ to $A$, I'll make the additional assumption that mass is proportional to height raised to an unknown power:3354%3355$ m = \beta h^D $3356%3357where $h$ is height, $\beta$ is an unknown constant of proportionality, and $D$ is the dimension that relates height and mass. I start by assuming $D=3$, but then revisit that assumption.33583359Finally, we'll assume that area is proportional to height squared:33603361$ A = \gamma h^2$33623363I specify height in feet, and choose units for mass and area so that $\beta=1$ and $\gamma=1$. Putting all that together, we can write a difference equation for height:33643365$ h_{n+1}^D = h_n^D + \alpha h_n^2 $33663367With $D=3$, the solution to this equation is close to a straight line, which is not a bad model for the growth curves. But the model implies that trees can grow forever, and we know that's not true. As trees get taller, it gets harder for them to move water and nutrients against the force of gravity, and their growth slows.33683369We can model this effect by adding a factor to the model similar to what we saw in the logistic model of population growth. Instead of assuming:33703371$ m_{n+1} = m_n + \alpha A $33723373Let's assume33743375$ m_{n+1} = m_n + \alpha A (1 - h / K) $33763377where $K$ is similar to the carrying capacity of the logistic model. As $h$ approaches $K$, the factor $(1 - h/K)$ goes to 0, causing growth to level off.33783379In the repository for this book, you'll find a notebook called \py{trees.ipynb} that implements both models and uses them to fit the data. There are no exercises in this case study; it is mostly meant as an example of what you can do with the tools we have so far, and a preview of what we will be able to do with the tools in the next few chapters.3380338133823383\chapter{Epidemiology}3384\label{chap11}33853386In this chapter, we develop a model of an epidemic as it spreads in a susceptible population, and use it to evaluate the effectiveness of possible interventions.33873388\index{epidemic}33893390My presentation of the SIR model in the next few chapters is based on an excellent article by David Smith and Lang Moore\footnote{Smith and Moore, ``The SIR Model for Spread of Disease," Journal of Online Mathematics and its Applications, December 2001, at \url{http://modsimpy.com/sir}.}.33913392\index{SIR model}339333943395\section{The Freshman Plague}33963397Every year at Olin College, about 90 new students come to campus from around the country and the world. Most of them arrive healthy and happy, but usually at least one brings with them some kind of infectious disease. A few weeks later, predictably, some fraction of the incoming class comes down with what we call ``The Freshman Plague".33983399\index{Olin College}3400\index{Freshman Plague}3401\index{Kermack-McKendrick}34023403In this chapter we introduce a well-known model of infectious disease, the Kermack-McKendrick model, and use it to explain the progression of the disease over the course of the semester, predict the effect of possible interventions (like immunization) and design the most effective intervention campaign.34043405\index{disease}3406\index{infection}3407\index{design}34083409So far we have done our own modeling; that is, we've chosen physical systems, identified factors that seem important, and made decisions about how to represent them. In this chapter we start with an existing model and reverse-engineer it. Along the way, we consider the modeling decisions that went into it and identify its capabilities and limitations.341034113412\section{The SIR model}3413\label{sirmodel}34143415The Kermack-McKendrick model is a simple version of an {\bf SIR model}, so-named because it considers three categories of people:34163417\begin{itemize}34183419\item {\bf S}: People who are ``susceptible", that is, capable of contracting the disease if they come into contact with someone who is infected.34203421\item {\bf I}: People who are ``infectious", that is, capable of passing along the disease if they come into contact with someone susceptible.34223423\item {\bf R}: People who are ``recovered". In the basic version of the model, people who have recovered are considered to be immune to reinfection. That is a reasonable model for some diseases, but not for others, so it should be on the list of assumptions to reconsider later.34243425\end{itemize}34263427Let's think about how the number of people in each category changes over time. Suppose we know that people with the disease are infectious for a period of 4 days, on average. If 100 people are infectious at a particular point in time, and we ignore the particular time each one became infected, we expect about 1 out of 4 to recover on any particular day.34283429Putting that a different way, if the time between recoveries is 4 days, the recovery rate is about 0.25 recoveries per day, which we'll denote with the Greek letter gamma, $\gamma$, or the variable name \py{gamma}.34303431If the total number of people in the population is $N$, and the fraction currently infectious is $i$, the total number of recoveries we expect per day is $\gamma i N$.34323433\index{recovery rate}34343435Now let's think about the number of new infections. Suppose we know that each susceptible person comes into contact with 1 person every 3 days, on average, in a way that would cause them to become infected if the other person is infected. We'll denote this contact rate with the Greek letter beta, $\beta$.34363437\index{infection rate}34383439It's probably not reasonable to assume that we know $\beta$ ahead of time, but later we'll see how to estimate it based on data from previous outbreaks.34403441If $s$ is the fraction of the population that's susceptible, $s N$ is the number of susceptible people, $\beta s N$ is the number of contacts per day, and $\beta s i N$ is the number of those contacts where the other person is infectious.34423443\index{susceptible}34443445In summary:34463447\begin{itemize}34483449\item The number of recoveries we expect per day is $\gamma i N$; dividing by $N$ yields the fraction of the population that recovers in a day, which is $\gamma i$.34503451\item The number of new infections we expect per day is $\beta s i N$; dividing by $N$ yields the fraction of the population that gets infected in a day, which is $\beta s i$.34523453\end{itemize}34543455This model assumes that the population is closed; that is, no one arrives or departs, so the size of the population, $N$, is constant.345634573458\section{The SIR equations}3459\label{sireqn}34603461If we treat time as a continuous quantity, we can write differential equations that describe the rates of change for $s$, $i$, and $r$ (where $r$ is the fraction of the population that has recovered):3462%3463\begin{align*}3464\frac{ds}{dt} &= -\beta s i \\3465\frac{di}{dt} &= \beta s i - \gamma i\\3466\frac{dr}{dt} &= \gamma i3467\end{align*}3468%3469To avoid cluttering the equations, I leave it implied that $s$ is a function of time, $s(t)$, and likewise for $i$ and $r$.3470\index{differential equation}34713472SIR models are examples of {\bf compartment models}, so-called because they divide the world into discrete categories, or compartments, and describe transitions from one compartment to another. Compartments are also called {\bf stocks} and transitions between them are called {\bf flows}.34733474\index{compartment model}3475\index{stock}3476\index{flow}3477\index{stock and flow diagram}34783479In this example, there are three stocks---susceptible, infectious, and recovered---and two flows---new infections and recoveries. Compartment models are often represented visually using stock and flow diagrams (see \url{http://modsimpy.com/stock}).3480Figure~\ref{stock_flow1} shows the stock and flow diagram for an SIR model.34813482\begin{figure}3483\centerline{\includegraphics[width=4in]{figs/stock_flow1.pdf}}3484\caption{Stock and flow diagram for an SIR model.}3485\label{stock_flow1}3486\end{figure}34873488Stocks are represented by rectangles, flows by arrows. The widget in the middle of the arrows represents a valve that controls the rate of flow; the diagram shows the parameters that control the valves.348934903491\section{Implementation}34923493For a given physical system, there are many possible models, and for a given model, there are many ways to represent it. For example, we can represent an SIR model as a stock-and-flow diagram, as a set of differential equations, or as a Python program. The process of representing a model in these forms is called {\bf implementation}. In this section, we implement the SIR model in Python.34943495\index{implementation}34963497I'll represent the initial state of the system using a \py{State} object with state variables \py{S}, \py{I}, and \py{R}; they represent the fraction of the population in each compartment.34983499\index{System object}3500\index{State object}3501\index{state variable}35023503We can initialize the \py{State} object with the {\em number} of people in each compartment, assuming there is one infected student in a class of 90:35043505\begin{python}3506init = State(S=89, I=1, R=0)3507\end{python}35083509And then convert the numbers to fractions by dividing by the total:35103511\begin{python}3512init /= sum(init)3513\end{python}35143515For now, let's assume we know the time between contacts and time between recoveries:35163517\begin{python}3518tc = 3 # time between contacts in days3519tr = 4 # recovery time in days3520\end{python}35213522We can use them to compute the parameters of the model:35233524\begin{python}3525beta = 1 / tc # contact rate in per day3526gamma = 1 / tr # recovery rate in per day3527\end{python}35283529Now we need a \py{System} object to store the parameters and initial conditions. The following function takes the system parameters as function parameters and returns a new \py{System} object:35303531\index{\py{make_system}}35323533\begin{python}3534def make_system(beta, gamma):3535init = State(S=89, I=1, R=0)3536init /= sum(init)35373538t0 = 03539t_end = 7 * 1435403541return System(init=init, t0=t0, t_end=t_end,3542beta=beta, gamma=gamma)3543\end{python}35443545The default value for \py{t_end} is 14 weeks, about the length of a semester.354635473548\section{The update function}35493550At any point in time, the state of the system is represented by a \py{State} object with three variables, \py{S}, \py{I} and \py{R}. So I'll define an update function that takes as parameters a \py{State} object, the current time, and a \py{System} object:35513552\index{update function}3553\index{function!update}3554\index{time step}35553556\begin{python}3557def update_func(state, t, system):3558s, i, r = state35593560infected = system.beta * i * s3561recovered = system.gamma * i35623563s -= infected3564i += infected - recovered3565r += recovered35663567return State(S=s, I=i, R=r)3568\end{python}35693570The first line uses a feature we have not seen before, {\bf multiple assignment}. The value on the right side is a \py{State} object that contains three values. The left side is a sequence of three variable names. The assignment does just what we want: it assigns the three values from the \py{State} object to the three variables, in order.35713572The local variables, \py{s}, \py{i} and \py{r}, are lowercase to distinguish them from the state variables, \py{S}, \py{I} and \py{R}.35733574\index{State object}3575\index{state variable}3576\index{local variable}35773578The update function computes \py{infected} and \py{recovered} as a fraction of the population, then updates \py{s}, \py{i} and \py{r}. The return value is a \py{State} that contains the updated values.35793580\index{return value}35813582When we call \py{update_func} like this:35833584\begin{python}3585state = update_func(init, 0, system)3586\end{python}35873588The result is a \py{State} object with these values:35893590\begin{tabular}{lr}3591& {\bf \sf value} \\3592\hline3593{\bf \sf S} & 0.985388 \\3594{\bf \sf I} & 0.011865 \\3595{\bf \sf R} & 0.002747 \\3596\end{tabular}35973598You might notice that this version of \py{update_func} does not use one of its parameters, \py{t}. I include it anyway because update functions sometimes depend on time, and it is convenient if they all take the same parameters, whether they need them or not.35993600%TODO: figure out when to talk about integers and floats (or never)360136023603\section{Running the simulation}36043605Now we can simulate the model over a sequence of time steps:36063607\index{time step}36083609\begin{python}3610def run_simulation(system, update_func):3611state = system.init36123613for t in linrange(system.t0, system.t_end):3614state = update_func(state, t, system)36153616return state3617\end{python}36183619The parameters of \py{run_simulation} are the \py{System} object and the update function. The \py{System} object contains the parameters, initial conditions, and values of \py{t0} and \py{t_end}.36203621\index{\py{run_simulation}}36223623The outline of this function should look familiar; it is similar to the function we used for the population model in Section~\ref{nowwithsystem}.36243625We can call \py{run_simulation} like this:36263627\begin{python}3628system = make_system(beta, gamma)3629final_state = run_simulation(system, update_func)3630\end{python}36313632The result is the final state of the system:36333634\begin{tabular}{lr}3635& {\bf \sf value} \\3636\hline3637{\bf \sf S} & 0.520819 \\3638{\bf \sf I} & 0.000676 \\3639{\bf \sf R} & 0.478505 \\3640\end{tabular}36413642This result indicates that after 14 weeks (98 days), about 52\% of the population is still susceptible, which means they were never infected, less than 1\% are actively infected, and 48\% have recovered, which means they were infected at some point.364336443645\section{Collecting the results}36463647The previous version of \py{run_simulation} only returns the final state, but we might want to see how the state changes over time. We'll consider two ways to do that: first, using three \py{TimeSeries} objects, then using a new object called a \py{TimeFrame}.36483649\index{TimeFrame object}3650\index{TimeSeries object}36513652Here's the first version:36533654\begin{python}3655def run_simulation(system, update_func):3656S = TimeSeries()3657I = TimeSeries()3658R = TimeSeries()36593660state = system.init3661t0 = system.t03662S[t0], I[t0], R[t0] = state36633664for t in linrange(system.t0, system.t_end):3665state = update_func(state, t, system)3666S[t+1], I[t+1], R[t+1] = state36673668return S, I, R3669\end{python}36703671First, we create \py{TimeSeries} objects to store the results. Notice that the variables \py{S}, \py{I}, and \py{R} are \py{TimeSeries} objects now.36723673Next we initialize \py{state}, \py{t0}, and the first elements of \py{S}, \py{I} and \py{R}.36743675Inside the loop, we use \py{update_func} to compute the state of the system at the next time step, then use multiple assignment to unpack the elements of \py{state}, assigning each to the corresponding \py{TimeSeries}.36763677\index{time step}36783679At the end of the function, we return the values \py{S}, \py{I}, and \py{R}. This is the first example we have seen where a function returns more than one value.36803681Now we can run the function like this:36823683\begin{python}3684system = make_system(beta, gamma)3685S, I, R = run_simulation(system, update_func)3686\end{python}36873688We'll use the following function to plot the results:36893690\begin{python}3691def plot_results(S, I, R):3692plot(S, '--', label='Susceptible')3693plot(I, '-', label='Infected')3694plot(R, ':', label='Resistant')3695decorate(xlabel='Time (days)',3696ylabel='Fraction of population')3697\end{python}36983699\index{plot}3700\index{decorate}37013702And run it like this:37033704\begin{python}3705plot_results(S, I, R)3706\end{python}37073708\begin{figure}3709\centerline{\includegraphics[height=3in]{figs/chap11-fig01.pdf}}3710\caption{Time series for \py{S}, \py{I}, and \py{R} over the course of 98 days.}3711\label{chap11-fig01}3712\end{figure}37133714Figure~\ref{chap11-fig01} shows the result. Notice that it takes about three weeks (21 days) for the outbreak to get going, and about six weeks (42 days) before it peaks. The fraction of the population that's infected is never very high, but it adds up. In total, almost half the population gets sick.371537163717\section{Now with a TimeFrame}3718\label{timeframe}37193720If the number of state variables is small, storing them as separate \py{TimeSeries} objects might not be so bad. But a better alternative is to use a \py{TimeFrame}, which is another object defined in the ModSim library.37213722\index{TimeFrame object}3723\index{DataFrame object}37243725A \py{TimeFrame} is almost identical to a \py{DataFrame}, which we used in Section~\ref{worldpopdata}, with just a few changes I made to adapt it for our purposes.37263727Here's a more concise version of \py{run_simulation} using a \py{TimeFrame}:37283729\begin{python}3730def run_simulation(system, update_func):3731frame = TimeFrame(columns=system.init.index)3732frame.row[system.t0] = system.init37333734for t in linrange(system.t0, system.t_end):3735frame.row[t+1] = update_func(frame.row[t], system)37363737return frame3738\end{python}37393740The first line creates an empty \py{TimeFrame} with one column for each state variable. Then, before the loop starts, we store the initial conditions in the \py{TimeFrame} at \py{t0}. Based on the way we've been using \py{TimeSeries} objects, it is tempting to write:37413742\begin{python}3743frame[system.t0] = system.init3744\end{python}37453746But when you use the bracket operator with a \py{TimeFrame} or \py{DataFrame}, it selects a column, not a row. For example, to select a column, we could write:37473748\index{bracket operator}3749\index{operator~bracket}37503751\begin{python}3752frame['S']3753\end{python}37543755To select a row, we have to use \py{row}, like this:37563757\index{row}37583759\begin{python}3760frame.row[system.t0] = system.init3761\end{python}37623763Since the value on the right side is a \py{State}, the assignment matches up the index of the \py{State} with the columns of the \py{TimeFrame}; that is, it assigns the \py{S} value from \py{system.init} to the \py{S} column of \py{frame}, and likewise with \py{I} and \py{R}.37643765\index{assignment}37663767We can use the same feature to write the loop more concisely, assigning the \py{State} we get from \py{update_func} directly to the next row of \py{frame}.37683769\index{system variable}37703771Finally, we return \py{frame}. We can call this version of \py{run_simulation} like this:37723773\begin{python}3774results = run_simulation(system, update_func)3775\end{python}37763777And plot the results like this:37783779\begin{python}3780plot_results(results.S, results.I, results.R)3781\end{python}37823783As with a \py{DataFrame}, we can use the dot operator to select columns from a \py{TimeFrame}.37843785\index{dot operator}3786\index{operator!dot}37873788Before you go on, you might want to read the notebook for this chapter, \py{chap11.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.378937903791\chapter{Optimization}3792\label{chap12}37933794In the previous chapter I presented the SIR model of infectious disease and used it to model the Freshman Plague at Olin. In this chapter we'll consider metrics intended to quantify the effects of the disease and interventions intended to reduce those effects.37953796\section{Metrics}3797\label{metrics2}37983799When we plot a time series, we get a view of everything that happened when the model ran, but often we want to boil it down to a few numbers that summarize the outcome. These summary statistics are called {\bf metrics}, as we saw in Section~\ref{metrics}.38003801\index{metric}38023803In the SIR model, we might want to know the time until the peak of the outbreak, the number of people who are sick at the peak, the number of students who will still be sick at the end of the semester, or the total number of students who get sick at any point.38043805As an example, I will focus on the last one --- the total number of sick students --- and we will consider interventions intended to minimize it.38063807When a person gets infected, they move from \py{S} to \py{I}, so we can get the total number of infections by computing the difference in \py{S} at the beginning and the end:38083809\begin{python}3810def calc_total_infected(results, system):3811return results.S[system.t0] - results.S[system.t_end]3812\end{python}38133814In the notebook that accompanies this chapter, you will have a chance to write functions that compute other metrics. Two functions you might find useful are \py{max} and \py{idxmax}.38153816\index{max}3817\index{idxmax}38183819If you have a \py{Series} called \py{S}, you can compute the largest value of the series like this:38203821\begin{python}3822largest_value = S.max()3823\end{python}38243825And the label of the largest value like this:38263827\begin{python}3828time_of_largest_value = S.idxmax()3829\end{python}38303831If the \py{Series} is a \py{TimeSeries}, the label you get from \py{idxmax} is a time or date. You can read more about these functions in the \py{Series} documentation at \url{http://modsimpy.com/series}.38323833\index{Series}383438353836\section{Immunization}38373838Models like this are useful for testing ``what if?" scenarios. As an example, we'll consider the effect of immunization.38393840\index{immunization}3841\index{vaccine}3842\index{Freshman Plague}38433844Suppose there is a vaccine that causes a student to become immune to the Freshman Plague without being infected. How might you modify the model to capture this effect?38453846One option is to treat immunization as a shortcut from susceptible to recovered without going through infectious. We can implement this feature like this:38473848\begin{python}3849def add_immunization(system, fraction):3850system.init.S -= fraction3851system.init.R += fraction3852\end{python}38533854\py{add_immunization} moves the given fraction of the population from \py{S} to \py{R}. If we assume that 10\% of students are vaccinated at the beginning of the semester, and the vaccine is 100\% effective, we can simulate the effect like this:38553856\begin{python}3857system2 = make_system(beta, gamma)3858add_immunization(system2, 0.1)3859results2 = run_simulation(system2, update_func)3860\end{python}38613862For comparison, we can run the same model without immunization and plot the results. Figure~\ref{chap12-fig01} shows \py{S} as a function of time, with and without immunization.38633864\begin{figure}3865\centerline{\includegraphics[height=3in]{figs/chap12-fig01.pdf}}3866\caption{Time series for \py{S}, with and without immunization.}3867\label{chap12-fig01}3868\end{figure}38693870Without immunization, almost 47\% of the population gets infected at some point. With 10\% immunization, only 31\% gets infected. That's pretty good.38713872\begin{figure}3873\centerline{\includegraphics[height=3in]{figs/chap12-fig02.pdf}}3874\caption{Fraction of the population infected as a function of immunization rate.}3875\label{chap12-fig02}3876\end{figure}38773878Now let's see what happens if we administer more vaccines. This following function sweeps a range of immunization rates:38793880\index{sweep}38813882\begin{python}3883def sweep_immunity(immunize_array):3884sweep = SweepSeries()38853886for fraction in immunize_array:3887sir = make_system(beta, gamma)3888add_immunization(sir, fraction)3889results = run_simulation(sir, update_func)3890sweep[fraction] = calc_total_infected(results, sir)38913892return sweep3893\end{python}38943895The parameter of \py{sweep_immunity} is an array of immunization rates. The result is a \py{SweepSeries} object that maps from each immunization rate to the resulting fraction of students ever infected.38963897\index{SweepSeries object}3898\index{parameter sweep}38993900Figure~\ref{chap12-fig02} shows a plot of the \py{SweepSeries}. Notice that the x-axis is the immunization rate, not time.39013902As the immunization rate increases, the number of infections drops steeply. If 40\% of the students are immunized, fewer than 4\% get sick. That's because immunization has two effects: it protects the people who get immunized (of course) but it also protects the rest of the population.39033904Reducing the number of ``susceptibles" and increasing the number of ``resistants" makes it harder for the disease to spread, because some fraction of contacts are wasted on people who cannot be infected. This phenomenon is called {\bf herd immunity}, and it is an important element of public health (see \url{http://modsimpy.com/herd}).39053906\index{herd immunity}39073908The steepness of the curve in Figure~\ref{chap12-fig02} is a blessing and a curse. It's a blessing because it means we don't have to immunize everyone, and vaccines can protect the ``herd" even if they are not 100\% effective.39093910But it's a curse because a small decrease in immunization can cause a big increase in infections. In this example, if we drop from 80\% immunization to 60\%, that might not be too bad. But if we drop from 40\% to 20\%, that would trigger a major outbreak, affecting more than 15\% of the population. For a serious disease like measles, just to name one, that would be a public health catastrophe.39113912\index{measles}39133914One use of models like this is to demonstrate phenomena like herd immunity and to predict the effect of interventions like vaccination. Another use is to evaluate alternatives and guide decision making. We'll see an example in the next section.391539163917391839193920\section{Hand washing}39213922Suppose you are the Dean of Student Life, and you have a budget of just \$1200 to combat the Freshman Plague. You have two options for spending this money:39233924\begin{enumerate}39253926\item You can pay for vaccinations, at a rate of \$100 per dose.39273928\item You can spend money on a campaign to remind students to wash hands frequently.39293930\end{enumerate}39313932We have already seen how we can model the effect of vaccination. Now let's think about the hand-washing campaign. We'll have to answer two questions:39333934\begin{enumerate}39353936\item How should we incorporate the effect of hand washing in the model?39373938\item How should we quantify the effect of the money we spend on a hand-washing campaign?39393940\end{enumerate}39413942For the sake of simplicity, let's assume that we have data from a similar campaign at another school showing that a well-funded campaign can change student behavior enough to reduce the infection rate by 20\%.39433944In terms of the model, hand washing has the effect of reducing \py{beta}. That's not the only way we could incorporate the effect, but it seems reasonable and it's easy to implement.39453946Now we have to model the relationship between the money we spend and the effectiveness of the campaign. Again, let's suppose we have data from another school that suggests:39473948\begin{itemize}39493950\item If we spend \$500 on posters, materials, and staff time, we can change student behavior in a way that decreases the effective value of \py{beta} by 10\%.39513952\item If we spend \$1000, the total decrease in \py{beta} is almost 20\%.39533954\item Above \$1000, additional spending has little additional benefit.39553956\end{itemize}39573958In the notebook for this chapter you will see how I used a logistic curve to fit this data. The result is the following function, which takes spending as a parameter and returns \py{factor}, which is the factor by which \py{beta} is reduced:39593960\index{logistic curve}39613962\begin{python}3963def compute_factor(spending):3964return logistic(spending, M=500, K=0.2, B=0.01)3965\end{python}39663967I use \py{compute_factor} to write \py{add_hand_washing}, which takes a \py{System} object and a budget, and modifies \py{system.beta} to model the effect of hand washing:39683969\begin{python}3970def add_hand_washing(system, spending):3971factor = compute_factor(spending)3972system.beta *= (1 - factor)3973\end{python}39743975Now we can sweep a range of values for \py{spending} and use the simulation to compute the effect:39763977\begin{python}3978def sweep_hand_washing(spending_array):3979sweep = SweepSeries()39803981for spending in spending_array:3982sir = make_system(beta, gamma)3983add_hand_washing(sir, spending)3984results, run_simulation(sir, update_func)3985sweep[spending] = calc_total_infected(results, sir)39863987return sweep3988\end{python}39893990Here's how we run it:39913992\begin{python}3993spending_array = linspace(0, 1200, 20)3994infected_sweep = sweep_hand_washing(spending_array)3995\end{python}39963997\begin{figure}3998\centerline{\includegraphics[height=3in]{figs/chap12-fig03.pdf}}3999\caption{Fraction of the population infected as a function of hand-washing campaign spending.}4000\label{chap12-fig03}4001\end{figure}40024003Figure~\ref{chap12-fig03} shows the result. Below \$200, the campaign has little effect. At \$800 it has a substantial effect, reducing total infections from 46\% to 20\%. Above \$800, the additional benefit is small.400440054006\section{Optimization}40074008\begin{figure}4009\centerline{\includegraphics[height=3in]{figs/chap12-fig04.pdf}}4010\caption{Fraction of the population infected as a function of the number of doses.}4011\label{chap12-fig04}4012\end{figure}40134014Let's put it all together. With a fixed budget of \$1200, we have to decide how many doses of vaccine to buy and how much to spend on the hand-washing campaign.40154016\index{optimization}40174018Here are the parameters:40194020\begin{python}4021num_students = 904022budget = 12004023price_per_dose = 1004024max_doses = int(budget / price_per_dose)4025\end{python}40264027The fraction \py{budget/price_per_dose} might not be an integer. \py{int} is a built-in function that converts numbers to integers, rounding down.40284029We'll sweep the range of possible doses:40304031\begin{python}4032dose_array = linrange(max_doses, endpoint=True)4033\end{python}40344035In this example we call \py{linrange} with only one argument; it returns a NumPy array with the integers from 0 to \py{max_doses}. With the argument \py{endpoint=True}, the result includes both endpoints.40364037\index{linrange}4038\index{NumPy}4039\index{array}40404041Then we run the simulation for each element of \py{dose_array}:40424043\begin{python}4044def sweep_doses(dose_array):4045sweep = SweepSeries()40464047for doses in dose_array:4048fraction = doses / num_students4049spending = budget - doses * price_per_dose40504051sir = make_system(beta, gamma)4052add_immunization(sir, fraction)4053add_hand_washing(sir, spending)40544055run_simulation(sir, update_func)4056sweep[doses] = calc_total_infected(sir)40574058return sweep4059\end{python}40604061For each number of doses, we compute the fraction of students we can immunize, \py{fraction} and the remaining budget we can spend on the campaign, \py{spending}. Then we run the simulation with those quantities and store the number of infections.40624063Figure~\ref{chap12-fig04} shows the result. If we buy no doses of vaccine and spend the entire budget on the campaign, the fraction infected is around 19\%. At 4 doses, we have \$800 left for the campaign, and this is the optimal point that minimizes the number of students who get sick.40644065As we increase the number of doses, we have to cut campaign spending, which turns out to make things worse. But interestingly, when we get above 10 doses, the effect of herd immunity starts to kick in, and the number of sick students goes down again.40664067\index{herd immunity}40684069Before you go on, you might want to read the notebook for this chapter, \py{chap12.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.4070407140724073\chapter{Sweeping two parameters}4074\label{chap13}40754076In the previous chapters I presented an SIR model of infectious disease, specifically the Kermack-McKendrick model. We extended the model to include vaccination and the effect of a hand-washing campaign, and used the extended model to allocate a limited budget optimally, that is, to minimize the number of infections.40774078\index{Kermack-McKendrick model}4079\index{SIR model}40804081But we assumed that the parameters of the model, contact rate and recovery rate, were known. In this chapter, we explore the behavior of the model as we vary these parameters, use analysis to understand these relationships better, and propose a method for using data to estimate parameters.40824083\section{Sweeping beta}40844085Recall that $\beta$ is the contact rate, which captures both the frequency of interaction between people and the fraction of those interactions that result in a new infection. If $N$ is the size of the population and $s$ is the fraction that's susceptible, $s N$ is the number of susceptibles, $\beta s N$ is the number of contacts per day between susceptibles and other people, and $\beta s i N$ is the number of those contacts where the other person is infectious.4086\index{parameter sweep}40874088As $\beta$ increases, we expect the total number of infections to increase. To quantify that relationship, I'll create a range of values for $\beta$:40894090\begin{python}4091beta_array = linspace(0.1, 1.1, 11)4092\end{python}40934094Then run the simulation for each value and print the results.40954096\begin{python}4097for beta in beta_array:4098sir = make_system(beta, gamma)4099run_simulation(sir, update1)4100print(sir.beta, calc_total_infected(sir))4101\end{python}41024103We can wrap that code in a function and store the results in a \py{SweepSeries} object:4104\index{SweepSeries object}41054106\begin{python}4107def sweep_beta(beta_array, gamma):4108sweep = SweepSeries()4109for beta in beta_array:4110system = make_system(beta, gamma)4111run_simulation(system, update1)4112sweep[system.beta] = calc_total_infected(system)4113return sweep4114\end{python}41154116Now we can run \py{sweep_beta} like this:41174118\begin{python}4119infected_sweep = sweep_beta(beta_array, gamma)4120\end{python}41214122And plot the results:41234124\begin{python}4125label = 'gamma = ' + str(gamma)4126plot(infected_sweep, label=label)4127\end{python}41284129The first line uses string operations to assemble a label for the plotted line:4130\index{string}41314132\begin{itemize}41334134\item When the \py{+} operator is applied to strings, it joins them end-to-end, which is called {\bf concatenation}.41354136\index{concatenation}41374138\item The function \py{str} converts any type of object to a String representation. In this case, \py{gamma} is a number, so we have to convert it to a string before trying to concatenate it.41394140\index{str function}41414142\end{itemize}41434144% TODO: Make the gamma symbol appear in the line below and in the figures.41454146If the value of \py{gamma} is \py{0.25}, the value of \py{label} is the string \py{'gamma = 0.25'}.41474148\begin{figure}4149\centerline{\includegraphics[height=3in]{figs/chap13-fig01.pdf}}4150\caption{Fraction of students infected as a function of the parameter \py{beta}, with \py{gamma = 0.25}.}4151\label{chap13-fig01}4152\end{figure}41534154Figure~\ref{chap13-fig01} shows the results. Remember that this figure is a parameter sweep, not a time series, so the x-axis is the parameter \py{beta}, not time.41554156When \py{beta} is small, the contact rate is low and the outbreak never really takes off; the total number of infected students is near zero. As \py{beta} increases, it reaches a threshold near 0.3 where the fraction of infected students increases quickly. When \py{beta} exceeds 0.5, more than 80\% of the population gets sick.415741584159\section{Sweeping gamma}41604161Let's see what that looks like for a few different values of \py{gamma}. Again, we'll use \py{linspace} to make an array of values:41624163\index{linspace}41644165\begin{python}4166gamma_array = linspace(0.1, 0.7, 4)4167\end{python}41684169And run \py{sweep_beta} for each value of \py{gamma}:41704171\begin{python}4172for gamma in gamma_array:4173infected_sweep = sweep_beta(beta_array, gamma)4174label = 'gamma = ' + str(gamma)4175plot(infected_sweep, label=label)4176\end{python}41774178\begin{figure}4179\centerline{\includegraphics[height=3in]{figs/chap13-fig02.pdf}}4180\caption{Fraction of students infected as a function of the parameter \py{beta}, for several values of \py{gamma}.}4181\label{chap13-fig02}4182\end{figure}41834184Figure~\ref{chap13-fig02} shows the results. When \py{gamma} is low, the recovery rate is low, which means people are infectious longer. In that case, even a low contact rate (\py{beta}) results in an epidemic.41854186When \py{gamma} is high, \py{beta} has to be even higher to get things going.418741884189\section{SweepFrame}4190\label{sweepframe}41914192In the previous section, we swept a range of values for \py{gamma}, and for each value, we swept a range of values for \py{beta}. This process is a {\bf two-dimensional sweep}.41934194If we want to store the results, rather than plot them, we can use a \py{SweepFrame}, which is a kind of {\tt DataFrame} where the rows sweep one parameter, the columns sweep another parameter, and the values contain metrics from a simulation.41954196\index{SweepFrame object}4197\index{DataFrame object}41984199This function shows how it works:42004201\begin{python}4202def sweep_parameters(beta_array, gamma_array):4203frame = SweepFrame(columns=gamma_array)4204for gamma in gamma_array:4205frame[gamma] = sweep_beta(beta_array, gamma)4206return frame4207\end{python}42084209\py{sweep_parameters} takes as parameters an array of values for \py{beta} and an array of values for \py{gamma}.42104211It creates a \py{SweepFrame} to store the results, with one column for each value of \py{gamma} and one row for each value of \py{beta}.42124213Each time through the loop, we run \py{sweep_beta}. The result is a \py{SweepSeries} object with one element for each value of \py{beta}. The assignment inside the loop stores the \py{SweepSeries} as a new column in the \py{SweepFrame}, corresponding to the current value of \py{gamma}.42144215At the end, the \py{SweepFrame} stores the fraction of students infected for each pair of parameters, \py{beta} and \py{gamma}.42164217We can run \py{sweep_parameters} like this:42184219\begin{python}4220frame = sweep_parameters(beta_array, gamma_array)4221\end{python}42224223With the results in a \py{SweepFrame}, we can plot each column like this:42244225\begin{python}4226for gamma in gamma_array:4227label = 'gamma = ' + str(gamma)4228plot(frame[gamma], label=label)4229\end{python}42304231Alternatively, we can plot each row like this:42324233\begin{python}4234for beta in [1.1, 0.9, 0.7, 0.5, 0.3]:4235label = 'β = ' + str(beta)4236plot(frame.row[beta], label=label)4237\end{python}42384239\begin{figure}4240\centerline{\includegraphics[height=3in]{figs/chap13-fig03.pdf}}4241\caption{Fraction of students infected as a function of the parameter \py{gamma}, for several values of \py{beta}.}4242\label{chap13-fig03}4243\end{figure}42444245Figure~\ref{chap13-fig03} shows the results. This example demonstrates one use of a \py{SweepFrame}: we can run the analysis once, save the results, and then generate different visualizations.42464247Another way to visualize the results of a two-dimensional sweep is a {\bf contour plot}, which shows the parameters on the axes and contour lines, that is, lines of constant value. In this example, the value is the fraction of students infected.42484249\index{contour plot}42504251The ModSim library provides \py{contour}, which takes a \py{SweepFrame} as a parameter:42524253\begin{python}4254contour(frame)4255\end{python}42564257\begin{figure}4258\centerline{\includegraphics[height=3in]{figs/chap13-fig04.pdf}}4259\caption{Contour plot showing fraction of students infected as a function of the parameters \py{gamma} and \py{beta}.}4260\label{chap13-fig04}4261\end{figure}42624263Figure~\ref{chap13-fig04} shows the result. Infection rates are lowest in the lower right, where the contact rate is and the recovery rate is high. They increase as we move to the upper left, where the contact rate is high and the recovery rate is low.42644265This figure suggests that there might be a relationship between \py{beta} and \py{gamma} that determines the outcome of the model. In fact, there is. In the next chapter we'll explore it by running simulations, then derive it by analysis.42664267Before you go on, you might want to read the notebook for this chapter, \py{chap13.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.426842694270\chapter{Analysis}4271\label{chap14}42724273In the previous chapters we used simulation to predict the effect of an infectious disease in a susceptible population and to design interventions that would minimize the effect.42744275In this chapter we use analysis to investigate the relationship between the parameters, \py{beta} and \py{gamma}, and the outcome of the simulation.42764277\section{Nondimensionalization}4278\label{nondim}42794280The figures in Section~\ref{sweepframe} suggest that there is a relationship between the parameters of the SIR model, \py{beta} and \py{gamma}, that determines the outcome of the simulation, the fraction of students infected.4281Let's think what that relationship might be.42824283\begin{itemize}42844285\item When \py{beta} exceeds \py{gamma}, that means there are more contacts (that is, potential infections) than recoveries during each day (or other unit of time). The difference between \py{beta} and \py{gamma} might be called the ``excess contact rate", in units of contacts per day.42864287\item As an alternative, we might consider the ratio \py{beta/gamma}, which is the number of contacts per recovery. Because the numerator and denominator are in the same units, this ratio is {\bf dimensionless}, which means it has no units.4288\index{dimensionless}42894290\end{itemize}42914292Describing physical systems using dimensionless parameters is often a useful move in the modeling and simulation game. It is so useful, in fact, that it has a name: {\bf nondimensionalization} (see \url{http://modsimpy.com/nondim}).42934294\index{nondimensionalization}42954296So we'll try the second option first. In the notebook for this chapter, you can explore the first option as an exercise.429742984299\section{Exploring the results}43004301Suppose we have a \py{SweepFrame} with one row for each value of \py{beta} and one column for each value of \py{gamma}. Each element in the \py{SweepFrame} is the fraction of students infected in a simulation with a given pair of parameters.43024303We can print the values in the \py{SweepFrame} like this:43044305\begin{python}4306for gamma in frame.columns:4307column = frame[gamma]4308for beta in column.index:4309frac_infected = column[beta]4310print(beta, gamma, frac_infected)4311\end{python}43124313This is the first example we've seen with one \py{for} loop inside another:4314\index{loop}4315\index{for loop}43164317\begin{itemize}43184319\item Each time the outer loop runs, it selects a value of \py{gamma} from the columns of the \py{DataFrame} and extracts the corresponding column.43204321\item Each time the inner loop runs, it selects a value of \py{beta} from the column and selects the corresponding element, which is the fraction of students infected.43224323\end{itemize}43244325In the example from the previous chapter, \py{frame} has 4 columns, one for each value of \py{gamma}, and 11 rows, one for each value of \py{beta}. So these loops print 44 lines, one for each pair of parameters.43264327The following function encapulates the previous loop and plots the fraction infected as a function of the ratio \py{beta/gamma}:43284329\begin{python}4330def plot_sweep_frame(frame):4331for gamma in frame.columns:4332series = frame[gamma]4333for beta in series.index:4334frac_infected = series[beta]4335plot(beta/gamma, frac_infected, 'ro')4336\end{python}43374338\begin{figure}4339\centerline{\includegraphics[height=3in]{figs/chap14-fig01.pdf}}4340\caption{Total fraction infected as a function of contact number.}4341\label{chap14-fig01}4342\end{figure}43434344Figure~\ref{chap14-fig01} shows that the results fall on a single curve, at least approximately. That means that we can predict the fraction of students who will be infected based on a single parameter, the ratio \py{beta/gamma}. We don't need to know the values of \py{beta} and \py{gamma} separately.434543464347\section{Contact number}4348\label{contact}43494350From Section~\ref{sirmodel}, recall that the number of new infections in a given day is $\beta s i N$, and the number of recoveries is $\gamma i N$. If we divide these quantities, the result is $\beta s / \gamma$, which is the number of new infections per recovery (as a fraction of the population).43514352\index{contact number}4353\index{basic reproduction number}43544355When a new disease is introduced to a susceptible population, $s$ is approximately 1, so the number of people infected by each sick person is $\beta / \gamma$. This ratio is called the ``contact number" or ``basic reproduction number" (see \url{http://modsimpy.com/contact}). By convention it is usually denoted $R_0$, but in the context of an SIR model, this notation is confusing, so we'll use $c$ instead.43564357The results in the previous section suggest that there is a relationship between $c$ and the total number of infections. We can derive this relationship by analyzing the differential equations from Section~\ref{sireqn}:4358%4359\begin{align*}4360\frac{ds}{dt} &= -\beta s i \\4361\frac{di}{dt} &= \beta s i - \gamma i\\4362\frac{dr}{dt} &= \gamma i4363\end{align*}4364%4365In the same way we divided the contact rate by the infection rate to get the dimensionless quantity $c$, now we'll divide $di/dt$ by $ds/dt$ to get a ratio of rates:4366%4367\[ \frac{di}{ds} = -1 + \frac{1}{cs} \]4368%4369Dividing one differential equation by another is not an obvious move, but in this case it is useful because it gives us a relationship between $i$, $s$ and $c$ that does not depend on time. From that relationship, we can derive an equation that relates $c$ to the final value of $s$. In theory, this equation makes it possible to infer $c$ by observing the course of an epidemic.43704371Here's how the derivation goes. We multiply both sides of the previous equation by $ds$:4372%4373\[ di = \left( -1 + \frac{1}{cs} \right) ds \]4374%4375And then integrate both sides:4376%4377\[ i = -s + \frac{1}{c} \log s + q \]4378%4379where $q$ is a constant of integration. Rearranging terms yields:4380%4381\[ q = i + s - \frac{1}{c} \log s \]4382%4383Now let's see if we can figure out what $q$ is. At the beginning of an epidemic, if the fraction infected is small and nearly everyone is susceptible, we can use the approximations $i(0) = 0$ and $s(0) = 1$ to compute $q$:4384%4385\[ q = 0 + 1 + \frac{1}{c} \log 1 \]4386%4387Since $\log 1 = 0$, we get $q = 1$.4388\index{integration}4389\index{constant of integration}43904391\newcommand{\sinf}{s_{\infty}}43924393Now, at the end of the epidemic, let's assume that $i(\infty) = 0$, and $s(\infty)$ is an unknown quantity, $\sinf$. Now we have:4394%4395\[ q = 1 = 0 + \sinf - \frac{1}{c} \log \sinf \]4396%4397Solving for $c$, we get4398%4399\[ c = \frac{\log \sinf}{\sinf - 1} \]4400%4401By relating $c$ and $\sinf$, this equation makes it possible to estimate $c$ based on data, and possibly predict the behavior of future epidemics.44024403\section{Analysis and simulation}44044405Let's compare this analytic result to the results from simulation.4406I'll create an array of values for $\sinf$4407\index{linspace}44084409\begin{python}4410s_inf_array = linspace(0.0001, 0.9999, 31)4411\end{python}44124413And compute the corresponding values of $c$:44144415\begin{python}4416c_array = log(s_inf_array) / (s_inf_array - 1)4417\end{python}44184419To get the total infected, we compute the difference between $s(0)$ and $s(\infty)$, then store the results in a \py{Series}:4420\index{array}4421\index{series}44224423\begin{python}4424frac_infected = 1 - s_inf_array4425frac_infected_series = Series(frac_infected, index=c_array)4426\end{python}44274428Recall from Section~\ref{dataframe} that a \py{Series} object contains an index and a corresponding sequence of values. In this case, the index is \py{c_array} and the values are from \py{frac_infected}.44294430Now we can plot the results:44314432\begin{python}4433plot(frac_infected_series)4434\end{python}44354436\begin{figure}4437\centerline{\includegraphics[height=3in]{figs/chap14-fig02.pdf}}4438\caption{Total fraction infected as a function of contact number, showing results from simulation and analysis.}4439\label{chap14-fig02}4440\end{figure}44414442Figure~\ref{chap14-fig02} compares the analytic results from this section with the simulation results from Section~\ref{nondim}. When the contact number exceeds 1, analysis and simulation agree.4443When the contact number is less than 1, they do not: analysis indicates there should be no infections; in the simulations there are a small number of infections.4444\index{analysis}44454446The reason for the discrepancy is that the simulation divides time into a discrete series of days, whereas the analysis treats time as a continuous quantity. In other words, the two methods are actually based on different models. So which model is better?44474448Probably neither. When the contact number is small, the early progress of the epidemic depends on details of the scenario. If we are lucky, the original infected person, ``patient zero", infects no one and there is no epidemic. If we are unlucky, patient zero might have a large number of close friends, or might work in the dining hall (and fail to observe safe food handling procedures).4449\index{patient zero}44504451For contact numbers near or less than 1, we might need a more detailed model. But for higher contact numbers the SIR model might be good enough.44524453\section{Estimating contact number}44544455Figure~\ref{chap14-fig02} shows that if we know the contact number, we can compute the fraction infected. But we can also read the figure the other way; that is, at the end of an epidemic, if we can estimate the fraction of the population that was ever infected, we can use it to estimate the contact number.44564457Well, in theory we can. In practice, it might not work very well, because of the shape of the curve. When the contact number is near 2, the curve is quite steep, which means that small changes in $c$ yield big changes in the number of infections. If we observe that the total fraction infected is anywhere from 20\% to 80\%, we would conclude that $c$ is near 2.44584459On the other hand, for larger contact numbers, nearly the entire population is infected, so the curve is nearly flat. In that case we would not be able to estimate $c$ precisely, because any value greater than 3 would yield effectively the same results. Fortunately, this is unlikely to happen in the real world; very few epidemics affect anything close to 90\% of the population.44604461So the SIR model has limitations; nevertheless, it provides insight into the behavior of infectious disease, especially the phenomenon of herd immunity. As we saw in Chapter~\ref{chap12}, if we know the parameters of the model, we can use it to evaluate possible interventions. And as we saw in this chapter, we might be able to use data from earlier outbreaks to estimate the parameters.44624463Before you go on, you might want to read the notebook for this chapter, \py{chap14.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.4464446544664467%\part{Modeling thermal systems}4468446944704471\chapter{Heat}4472\label{chap15}44734474So far the systems we have studied have been physical in the sense that they exist in the world, but they have not been physics, in the sense of what physics classes are usually about. In the next few chapters, we'll do some physics, starting with {\bf thermal systems}, that is, systems where the temperature of objects changes as heat transfers from one to another.44754476\index{thermal system}44774478\section{The coffee cooling problem}44794480The coffee cooling problem was discussed by Jearl Walker in {\it Scientific American} in 1977\footnote{Walker, ``The Amateur Scientist", {\it Scientific American}, Volume 237, Issue 5, November 1977.}; since then it has become a standard example of modeling and simulation.44814482\index{coffee cooling problem}4483\index{Walker, Jearl}44844485Here is my version of the problem:44864487\begin{quote}4488Suppose I stop on the way to work to pick up a cup of coffee, which I take with milk. Assuming that I want the coffee to be as hot as possible when I arrive at work, should I add the milk at the coffee shop, wait until I get to work, or add the milk at some point in between?4489\end{quote}44904491To help answer this question, I made a trial run with the milk and coffee in separate containers and took some measurements\footnote{This is fiction. I usually drink tea and bike to work.}:44924493\begin{itemize}44944495\item When served, the temperature of the coffee is \SI{90}{\celsius}. The volume is \SI{300}{mL}.44964497\item The milk is at an initial temperature of \SI{5}{\celsius}, and I take about \SI{50}{mL}.44984499\item The ambient temperature in my car is \SI{22}{\celsius}.45004501\item The coffee is served in a well insulated cup. When I arrive at work after 30 minutes, the temperature of the coffee has fallen to \SI{70}{\celsius}.45024503\item The milk container is not as well insulated. After 15 minutes, it warms up to \SI{20}{\celsius}, nearly the ambient temperature.45044505\end{itemize}45064507To use this data and answer the question, we have to know something about temperature and heat, and we have to make some modeling decisions.450845094510\section{Temperature and heat}45114512To understand how coffee cools (and milk warms), we need a model of temperature and heat. {\bf Temperature} is a property of an object or a system; in SI units it is measured in degrees Celsius (\si{\celsius}). Temperature quantifies how hot or cold the object is, which is related to the average velocity of the particles that make up the object.45134514\index{temperature}45154516When particles in a hot object contact particles in a cold object, the hot object gets cooler and the cold object gets warmer as energy is transferred from one to the other.4517The transferred energy is called {\bf heat}; in SI units it is measured in joules (\si{\joule}).45184519\index{heat}45204521Heat is related to temperature by the following equation (see \url{http://modsimpy.com/thermass}):4522%4523\[ Q = C~\Delta T \]4524%4525where $Q$ is the amount of heat transferred to an object, $\Delta T$ is resulting change in temperature, and $C$ is the {\bf thermal mass} of the object, which quantifies how much energy it takes to heat or cool it.4526In SI units, thermal mass is measured in joules per degree Celsius (\si{\joule\per\celsius}).45274528\index{thermal mass}45294530For objects made primarily from one material, thermal mass can be computed like this:4531%4532\[ C = m c_p \]4533%4534where $m$ is the mass of the object and $c_p$ is the {\bf specific heat capacity} of the material (see \url{http://modsimpy.com/specheat}).45354536\index{specific heat capacity}45374538We can use these equations to estimate the thermal mass of a cup of coffee. The specific heat capacity of coffee is probably close to that of water, which is \SI{4.2}{\joule\per\gram\per\celsius}. Assuming that the density of coffee is close to that of water, which is \SI{1}{\gram\per\milli\liter}, the mass of \SI{300}{\milli\liter} of coffee is \SI{300}{\gram}, and the thermal mass is \SI{1260}{\joule\per\celsius}.45394540\index{density}45414542So when a cup of coffee cools from \SI{90}{\celsius} to \SI{70}{\celsius}, the change in temperature, $\Delta T$ is \SI{20}{\celsius}, which means that \SI{25200}{\joule} of heat energy was transferred from the coffee to the surrounding environment (the cup holder and air in my car).45434544To give you a sense of how much energy that is, if you were able to harness all of that heat to do work (which you cannot\footnote{See \url{http://modsimpy.com/thermo}.}), you could use it to lift a cup of coffee from sea level to \SI{8571}{\meter}, just shy of the height of Mount Everest, \SI{8848}{\meter}.45454546\index{Mount Everest}45474548Assuming that the cup has less mass than the coffee, and is made from a material with lower specific heat, we can ignore the thermal mass of the cup.4549For a cup with substantial thermal mass, like a ceramic mug, we might consider a model that computes the temperature of coffee and cup separately.455045514552\section{Heat transfer}45534554In a situation like the coffee cooling problem, there are three ways heat transfers from one object to another (see \url{http://modsimpy.com/transfer}):45554556\index{heat transfer}4557\index{conduction}4558\index{convection}4559\index{radiation}45604561\begin{itemize}45624563\item Conduction: When objects at different temperatures come into contact, the faster-moving particles of the higher-temperature object transfer kinetic energy to the slower-moving particles of the lower-temperature object.45644565\item Convection: When particles in a gas or liquid flow from place to place, they carry heat energy with them. Fluid flows can be caused by external action, like stirring, or by internal differences in temperature. For example, you might have heard that hot air rises, which is a form of ``natural convection".45664567\index{fluid flow}45684569\item Radiation: As the particles in an object move due to thermal energy, they emit electromagnetic radiation. The energy carried by this radiation depends on the object's temperature and surface properties (see \url{http://modsimpy.com/thermrad}).45704571\end{itemize}45724573For objects like coffee in a car, the effect of radiation is much smaller than the effects of conduction and convection, so we will ignore it.45744575Convection can be a complex topic, since it often depends on details of fluid flow in three dimensions. But for this problem we will be able to get away with a simple model called ``Newton's law of cooling".45764577\index{Newton's law of cooling}45784579\section{Newton's law of cooling}45804581Newton's law of cooling asserts that the temperature rate of change for an object is proportional to the difference in temperature between the object and the surrounding environment:4582%4583\[ \frac{dT}{dt} = -r (T - T_{env}) \]4584%4585where $T$, the temperature of the object, is a function of time, $t$; $T_{env}$ is the temperature of the environment, and $r$ is a constant that characterizes how quickly heat is transferred between the system and the environment.45864587Newton's so-called ``law" is really a model: it is a good approximation in some conditions and less good in others.45884589For example, if the primary mechanism of heat transfer is conduction, Newton's law is ``true", which is to say that $r$ is constant over a wide range of temperatures. And sometimes we can estimate $r$ based on the material properties and shape of the object.45904591When convection contributes a non-negligible fraction of heat transfer, $r$ depends on temperature, but Newton's law is often accurate enough, at least over a narrow range of temperatures. In this case $r$ usually has to be estimated experimentally, since it depends on details of surface shape, air flow, evaporation, etc.45924593When radiation makes up a substantial part of heat transfer, Newton's law is not a good model at all. This is the case for objects in space or in a vacuum, and for objects at high temperatures (more than a few hundred degrees Celsius, say).45944595\index{radiation}45964597However, for a situation like the coffee cooling problem, we expect Newton's model to be quite good.459845994600\section{Implementation}4601\label{coffee_impl}46024603To get started, let's forget about the milk temporarily and focus on the coffee. I'll create a \py{State} object to represent the initial temperature:46044605\begin{python}4606init = State(T=90)4607\end{python}46084609And a \py{System} object to contain the parameters of the system:46104611\index{State object}4612\index{System object}46134614\begin{python}4615coffee = System(init=init,4616volume=300,4617r=0.01,4618T_env=22,4619t_0=0,4620t_end=30,4621dt=1)4622\end{python}46234624The values of \py{volume}, \py{T_env}, and \py{t_end} come from the statement of the problem. I chose the value of \py{r} arbitrarily for now; we will figure out how to estimate it soon.46254626\index{time step}46274628\py{dt} is the time step we use to simulate the cooling process.4629Strictly speaking, Newton's law is a differential equation, but over a short period of time we can approximate it with a difference equation:4630%4631\[ \Delta T = -r (T - T_{env}) dt \]4632%4633where $dt$ is a small time step and $\Delta T$ is the change in temperature during that time step.46344635Note: I use $\Delta T$ to denote a change in temperature over time, but in the context of heat transfer, you might also see $\Delta T$ used to denote the difference in temperature between an object and its environment, $T - T_{env}$. To minimize confusion, I avoid this second use.46364637Now we can write an update function:46384639\begin{python}4640def update_func(state, t, system):4641r, T_env, dt = system.r, system.T_env, system.dt46424643T = state.T4644T += -r * (T - T_env) * dt46454646return State(T=T)4647\end{python}46484649Like previous update functions, this one takes a \py{State} object, a time, and a \py{System} object.46504651Now if we run46524653\begin{python}4654update_func(init, 0, coffee)4655\end{python}46564657we see that the temperature after one minute is \SI{89.3}{\celsius}, so the temperature drops by about \SI{0.7}{\celsius\per\minute}, at least for this value of \py{r}.46584659Here's a version of \py{run_simulation} that simulates a series of time steps from \py{t_0} to \py{t_end}:46604661\index{time step}4662\index{\py{run_simulation}}46634664\begin{python}4665def run_simulation(system, update_func):4666init = system.init4667t_0, t_end, dt = system.t_0, system.t_end, system.dt46684669frame = TimeFrame(columns=init.index)4670frame.row[t_0] = init4671ts = linrange(t_0, t_end, dt)46724673for t in ts:4674frame.row[t+dt] = update_func(frame.row[t], t, system)46754676return frame4677\end{python}46784679This function is similar to previous versions of \py{run_simulation}.46804681One difference is that it uses \py{linrange} to make an array of values from \py{t_0} to \py{t_end} with time step \py{dt}. The result does not include \py{t_end}, so the last value in the array is \py{t_end-dt}.46824683\index{linrange}4684\index{NumPy}4685\index{array}46864687We can run it like this:46884689\begin{python}4690results = run_simulation(coffee, update_func)4691\end{python}46924693The result is a \py{TimeFrame} object with one row per time step and just one column, \py{T}. The temperature after 30 minutes is \SI{72.3}{\celsius}, which is a little higher than stated in the problem, \SI{70}{\celsius}. We can adjust \py{r} and find the right value by trial and error, but we'll see a better way in the next chapter.46944695\index{time step}4696\index{TimeFrame object}46974698First I want to wrap what we have so far in a function:46994700\begin{python}4701def make_system(T_init, r, volume, t_end):4702init = State(T=T_init)47034704return System(init=init,4705r=r,4706volume=volume,4707temp=T_init,4708t_0=0,4709t_end=t_end,4710dt=1,4711T_env=22)4712\end{python}47134714\py{make_system} takes the system parameters and packs them into a \py{System} object. Now we can simulate the system like this:47154716\index{\py{make_system}}47174718\begin{python}4719coffee = make_system(T_init=90, r=0.01,4720volume=300, t_end=30)4721results = run_simulation(coffee, update_func)4722\end{python}47234724Before you go on, you might want to read the notebook for this chapter, \py{chap15.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.472547264727\chapter{Mixing}4728\label{chap16}47294730In the previous chapter we wrote a simulation of a cooling cup of coffee. Given the initial temperature of the coffee, the temperature of the atmosphere, and the rate parameter, \py{r}, we can predict how the temperature of the coffee will change over time.47314732In general, we don't know the value of \py{r}, but we can use measurements to estimate it. Given an initial temperature, a final temperature, and the time in between, we can find \py{r} by trial and error.47334734In this chapter, we'll see a better way to find \py{r}, using a {\bf bisection search}.47354736And then we'll get back to solving the coffee cooling problem.47374738\section{Finding roots}4739\label{root_bisect}47404741The ModSim library provides a method called \py{root_bisect} that finds the roots of non-linear equations. As a simple example, suppose you want to find the roots of the polynomial4742%4743\[ f(x) = (x - 1)(x - 2)(x - 3) \]4744%4745where {\bf root} means a value of $x$ that makes $f(x)=0$. Because of the way I wrote the polynomial, we can see that if $x=1$, the first factor is 0; if $x=2$, the second factor is 0; and if $x=3$, the third factor is 0, so those are the roots.47464747\index{\py{root_bisect}}4748\index{root}47494750I'll use this example to demonstrate \py{root_bisect}. First, we have to write a function that evaluates $f$:47514752\begin{python}4753def func(x):4754return (x-1) * (x-2) * (x-3)4755\end{python}47564757Now we call \py{root_bisect} like this:47584759\begin{python}4760res = root_bisect(func, [1.5, 2.5])4761print(res.root)4762\end{python}47634764The first argument is the function whose roots we want.4765The second argument is an interval that contains a root.4766The result is an object that contains several variables, including \py{root}, which stores the root that was found.47674768So how can we use \py{root_bisect} to estimate \py{r}?47694770What we want is the value of \py{r} that yields a final temperature of \SI{70}{\celsius}. To work with \py{root_bisect}, we need a function that takes \py{r} as a parameter and returns the difference between the final temperature and the goal:47714772\begin{python}4773def error_func1(r):4774system = make_system(T_init=90, r=r, volume=300, t_end=30)4775results = run_simulation(system, update_func)4776T_final = get_last_value(results.T)4777return T_final - 704778\end{python}47794780I call a function like this an ``error function" because it returns the difference between what we got and what we wanted, that is, the error. When we find the right value of \py{r}, this error will be 0.47814782\index{error function}4783\index{function!error}47844785We can test \py{error_func1} like this, using our initial guess for \py{r}:47864787\begin{python}4788error_func1(r=0.01)4789\end{python}47904791The result is an error of \SI{2.3}{\celsius}, because the final temperature with this value of \py{r} is too high.47924793With \py{r=0.02}, the error is \SI{-10.9}{\celsius}, which means that the final temperature is too low.4794So we know that the correct value must be in between.47954796Now we can call \py{root_bisect} like this:47974798\begin{python}4799res = root_bisect(error_func1, [0.01, 0.02])4800r_coffee = res.root4801\end{python}48024803In this example, \py{r_coffee} turns out to be about \py{0.012}, in units of \si{\per\minute} (inverse minutes).48044805\begin{figure}4806\centerline{\includegraphics[height=3in]{figs/chap16-fig01.pdf}}4807\caption{Temperature of the coffee and milk over time.}4808\label{chap16-fig01}4809\end{figure}48104811As one of the exercises for this chapter, you will use the same process to estimate \py{r_milk}.48124813With the correct values of \py{r_coffee} and \py{r_milk}, the simulation results should look like Figure~\ref{chap16-fig01}, which shows the temperature of the coffee and milk over time.481448154816\section{Mixing liquids}48174818When we mix two liquids, the temperature of the mixture depends on the temperatures of the ingredients, but it might not be obvious how to compute it.48194820\index{mixing}48214822Assuming there are no chemical reactions that either produce or consume heat, the total thermal energy of the system is the same before and after mixing; in other words, thermal energy is {\bf conserved}.48234824\index{conservation of energy}48254826If the temperature of the first liquid is $T_1$, the temperature of the second liquid is $T_2$, and the final temperature of the mixture is $T$, the heat transfer into the first liquid is $C_1 (T - T_1)$ and the heat transfer into the second liquid is $C_2 (T - T_2)$, where $C_1$ and $C_2$ are the thermal masses of the liquids.48274828In order to conserve energy, these heat transfers must add up to 0:4829%4830\[ C_1 (T - T_1) + C_2 (T - T_2) = 0 \]4831%4832We can solve this equation for T:4833%4834\[ T = \frac{C_1 T_1 + C_2 T_2}{C_1 + C_2} \]4835%4836For the coffee cooling problem, we have the volume of each liquid; if we also know the density, $\rho$, and the specific heat capacity, $c_p$, we can compute thermal mass:4837%4838\[ C = \rho V c_p \]4839%4840If the liquids have the same density and heat capacity, they drop out of the equation, and we can write:4841%4842\[ T = \frac{V_1 T_1 + V_2 T_2}{V_1 + V_2} \]4843%4844where $V_1$ and $V_2$ are the volumes of the liquids.48454846As an approximation, I'll assume that milk and coffee have the same density and specific heat.4847As an exercise, you can look up these quantities and see how good this assumption is.48484849\index{volume}4850\index{density}4851\index{specific heat}48524853The following function takes two \py{System} objects that represent the coffee and milk, and creates a new \py{System} to represent the mixture:48544855\begin{python}4856def mix(system1, system2):4857assert system1.t_end == system2.t_end48584859V1, V2 = system1.volume, system2.volume4860T1, T2 = system1.temp, system2.temp48614862V_mix = V1 + V24863T_mix = (V1 * T1 + V2 * T2) / V_mix48644865return make_system(T_init=T_mix,4866r=system1.r,4867volume=V_mix,4868t_end=30)4869\end{python}48704871The first line is an \py{assert} statement, which is a way of checking for errors. It compares \py{t_end} for the two systems to confirm that they have been cooling for the same time. If not, \py{assert} displays an error message and stops the program.48724873\index{assert statement}4874\index{statement!assert}48754876The next two lines extract volume and temperature from the two \py{System} objects. Then the following two lines compute the volume and temperature of the mixture. Finally, \py{mix} makes a new \py{System} object and returns it.48774878This function uses the value of \py{r} from \py{system1} as the value of \py{r} for the mixture. If \py{system1} represents the coffee, and we are adding the milk to the coffee, this is probably a reasonable choice. On the other hand, when we increase the amount of liquid in the coffee cup, that might change \py{r}. So this is an assumption we might want to revisit.48794880Notice that \py{mix} requires the \py{System} objects to have a variable named \py{temp}.4881To make sure this variable gets updated when we run a simulation, I use this function:48824883\begin{python}4884def run_and_set(system):4885results = run_simulation(system, update_func)4886system.temp = get_last_value(results.T)4887return results4888\end{python}48894890Now we have everything we need to solve the problem.48914892\section{Mix first or last?}48934894First I'll create objects to represent the coffee and cream:48954896\begin{python}4897coffee = make_system(T_init=90, r=r_coffee,4898volume=300, t_end=30)48994900milk = make_system(T_init=5, r=r_milk, volume=50, t_end=30)4901\end{python}49024903Then I'll mix them and simulate 30 minutes:49044905\begin{python}4906mix_first = mix(coffee, milk)4907mix_results = run_and_set(mix_first)4908\end{python}49094910The final temperature is \SI{61.4}{\celsius} which is still warm enough to be enjoyable. Would we do any better if we added the milk last?49114912I'll simulate the coffee and milk separately, and then mix them:49134914\begin{python}4915coffee_results = run_and_set(coffee)4916milk_results = run_and_set(milk)4917mix_last = mix(coffee, milk)4918\end{python}49194920After mixing, the temperature is \SI{63.1}{\celsius}. So it looks like adding the milk at the end is better by about \SI{1.7}{\celsius}. But is that the best we can do?49214922\section{Optimization}49234924Adding the milk after 30 minutes is better than adding immediately, but maybe there's something in between that's even better. To find out, I'll use the following function, which takes the time to add the milk, \py{t_add}, as a parameter:49254926\index{optimization}49274928\begin{python}4929def run_and_mix(t_add, t_total):4930coffee = make_system(T_init=90, r=r_coffee,4931volume=300, t_end=t_add)4932coffee_results = run_and_set(coffee)49334934milk = make_system(T_init=5, r=r_milk,4935volume=50, t_end=t_add)4936milk_results = run_and_set(milk)49374938mixture = mix(coffee, milk)4939mixture.t_end = t_total - t_add4940results = run_and_set(mixture)49414942return mixture.temp4943\end{python}49444945When \py{t_add=0}, we add the milk immediately; when \py{t_add=30}, we add it at the end. Now we can sweep the range of values in between:49464947\begin{python}4948sweep = SweepSeries()4949for t_add in linspace(0, 30, 11):4950sweep[t_add] = run_and_mix(t_add, 30)4951\end{python}49524953\begin{figure}4954\centerline{\includegraphics[height=3in]{figs/chap16-fig02.pdf}}4955\caption{Final temperature as a function of the time the milk is added.}4956\label{chap16-fig02}4957\end{figure}49584959Figure~\ref{chap16-fig02} shows the result. Again, note that this is a parameter sweep, not a time series. The x-axis is the time when we add the milk, not the index of a \py{TimeSeries}.49604961The final temperature is maximized when \py{t_add=30}, so adding the milk at the end is optimal.49624963In the notebook for this chapter you will have a chance to explore this solution and try some variations. For example, suppose the coffee shop won't let me take milk in a separate container, but I keep a bottle of milk in the refrigerator at my office. In that case is it better to add the milk at the coffee shop, or wait until I get to the office?496449654966\section{Analysis}49674968Simulating Newton's law of cooling is almost silly, because we can solve the differential equation analytically. If4969%4970\[ \frac{dT}{dt} = -r (T - T_{env}) \]4971%4972the general solution is4973%4974\[ T{\left (t \right )} = C_{1} \exp(-r t) + T_{env} \]4975%4976and the particular solution where $T(0) = T_{init}$ is4977%4978\[ T_{env} + \left(- T_{env} + T_{init}\right) \exp(-r t) \]4979%4980You can see how I got this solution using SymPy in \py{chap16sympy.ipynb} in the repository for this book. If you would like to see it done by hand, you can watch this video: \url{http://modsimpy.com/khan3}.49814982\index{analysis}4983\index{SymPy}49844985Now we can use the observed data to estimate the parameter $r$. If we observe $T(t_{end}) = T_{end}$, we can plug $t_{end}$ and $T_{end}$ into the particular solution and solve for $r$. The result is:4986%4987\[ r = \frac{1}{t_{end}} \log{\left (\frac{T_{init} - T_{env}}{T_{end} - T_{env}} \right )} \]4988%4989Plugging in $t_{end}=30$ and $T_{end}=70$ (and again with $T_{init}=90$ and $T_{env}=22$), the estimate for $r$ is 0.0116.49904991We can use the following function to compute the time series:49924993\index{unpack}49944995\begin{python}4996def run_analysis(system):4997T_env, r = system.T_env, system.r49984999T_init = system.init.T5000ts = linspace(0, system.t_end)50015002T_array = T_env + (T_init - T_env) * exp(-r * ts)50035004return TimeFrame(T_array, index=ts, columns=['T'])5005\end{python}50065007This function is similar to \py{run_simulation}; it takes a \py{System} as a parameter and returns a \py{TimeFrame} as a result.50085009Because \py{linrange} returns a NumPy array, \py{T_array} is also a NumPy array. To be consistent with \py{run_simulation}, we have to put it into a \py{TimeFrame}.50105011We can run it like this:5012\index{\py{run_analysis}}50135014\begin{python}5015r_coffee2 = 0.01165016coffee2 = make_system(T_init=90, r=r_coffee2,5017volume=300, t_end=30)5018results = run_analysis(coffee2)5019\end{python}50205021The final temperature is \SI{70}{\celsius}, as it should be. In fact, the results are identical to what we got by simulation, with a small difference due to rounding.50225023Before you go on, you might want to read the notebook for this chapter, \py{chap16.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.502450255026%%\part{Pharmacokinetics}50275028\chapter{Pharmacokinetics}5029\label{chap17}50305031{\bf Pharmacokinetics} is the study of how drugs and other substances move around the body, react, and are eliminated. In this chapter, we implement one of the most widely used pharmacokinetic models: the so-called {\bf minimal model} of glucose and insulin in the blood stream.50325033\index{pharmacokinetics}50345035%We will use this model to fit data collected from a patient, and use the %parameters of the fitted model to quantify the patient's ability to %produce insulin and process glucose.50365037\index{glucose}5038\index{insulin}50395040My presentation in this chapter follows Bergman (2005) ``Minimal Model" (abstract at \url{http://modsimpy.com/bergman},5041PDF at \url{http://modsimpy.com/minmod}).504250435044\section{The glucose-insulin system}50455046{\bf Glucose} is a form of sugar that circulates in the blood of animals; it is used as fuel for muscles, the brain, and other organs. The concentration of blood sugar is controlled by the hormone system, and especially by {\bf insulin}, which is produced by the pancreas and has the effect of reducing blood sugar.50475048\index{pancreas}50495050In people with normal pancreatic function, the hormone system maintains {\bf homeostasis}; that is, it keeps the concentration of blood sugar in a range that is neither too high or too low.50515052But if the pancreas does not produce enough insulin, or if the cells that should respond to insulin become insensitive, blood sugar can become elevated, a condition called {\bf hyperglycemia}. Long term, severe hyperglycemia is the defining symptom of {\bf diabetes mellitus}, a serious disease that affects almost 10\% of the population in the U.S. (see \url{http://modsimpy.com/cdc}).50535054\index{hyperglycemia}5055\index{diabetes}50565057One of the most-used tests for hyperglycemia and diabetes is the frequently sampled intravenous glucose tolerance test (FSIGT), in which glucose is injected into the blood stream of a fasting subject (someone who has not eaten recently); then blood samples are collected at intervals of 2--10 minutes for 3 hours. The samples are analyzed to measure the concentrations of glucose and insulin.50585059\index{FSIGT}50605061By analyzing these measurements, we can estimate several parameters of the subject's response; the most important is a parameter denoted $S_I$, which quantifies the effect of insulin on the rate of reduction in blood sugar.506250635064\section{The glucose minimal model}50655066The ``minimal model" was proposed by Bergman, Ider, Bowden, and Cobelli\footnote{Bergman RN, Ider YZ, Bowden CR, Cobelli C., ``Quantitative estimation of insulin sensitivity", Am J Physiol. 1979 Jun;236(6):E667-77. Abstract at \url{http://modsimpy.com/insulin}.}.5067It consists of two parts: the glucose model and the insulin model. I will present an implementation of the glucose model; as a case study, you will have the chance to implement the insulin model.50685069\index{minimal model}50705071The original model was developed in the 1970s; since then, many variations and extensions have been proposed. Bergman's comments on the development of the model provide insight into their process:50725073\begin{quote}5074We applied the principle of Occam's Razor, i.e.~by asking5075what was the simplest model based upon known physiology5076that could account for the insulin-glucose relationship5077revealed in the data. Such a model must be simple5078enough to account totally for the measured glucose (given5079the insulin input), yet it must be possible, using mathematical5080techniques, to estimate all the characteristic parameters5081of the model from a single data set (thus avoiding5082unverifiable assumptions).5083\end{quote}50845085The most useful models are the ones that achieve this balance: including enough realism to capture the essential features of the system without so much complexity that they are impractical. In this example, the practical limit is the ability to estimate the parameters of the model using data, and to interpret the parameters meaningfully.50865087\index{Occam's Razor}50885089Bergman discusses the features he and his colleagues thought were essential:50905091\begin{quote}5092(1) Glucose, once elevated by injection, returns to basal level due to5093two effects: the effect of glucose itself to normalize its own5094concentration [...] as well as the catalytic effect of insulin to allow5095glucose to self-normalize (2) Also, we discovered5096that the effect of insulin on net glucose disappearance5097must be sluggish --- that is, that insulin acts slowly because5098insulin must first move from plasma to a remote compartment [...] to exert its action on glucose disposal.5099\end{quote}51005101To paraphrase the second point, the effect of insulin on glucose disposal, as seen in the data, happens more slowly than we would expect if it depended primarily on the concentration of insulin in the blood. Bergman's group hypothesized that insulin must move relatively slowly from the blood to a ``remote compartment" where it has its effect.51025103\index{compartment model}51045105At the time, the remote compartment was a modeling abstraction that might, or might not, represent something physical. Later, according to Bergman, it was ``shown to be interstitial fluid", that is, the fluid that surrounds tissue cells. In the history of mathematical modeling, it is common for hypothetical entities, added to models to achieve particular effects, to be found later to correspond to physical entities.51065107\index{interstitial fluid}51085109The glucose model consists of two differential equations:5110%5111\[ \frac{dG}{dt} = -k_1 \left[ G(t) - G_b \right] - X(t) G(t) \]5112%5113\[ \frac{dX}{dt} = k_3 \left[I(t) - I_b \right] - k_2 X(t) \]5114%5115where51165117\begin{itemize}51185119\item $G$ is the concentration of blood glucose as a function of time and $dG/dt$ is its rate of change.51205121\item $X$ is the concentration of insulin in the tissue fluid as a function of time, and $dX/dt$ is its rate of change.51225123\item $I$ is the concentration of insulin in the blood as a function of time, which is taken as an input into the model, based on measurements.51245125\item $G_b$ is the basal concentration of blood glucose and $I_b$ is the basal concentration of blood insulin, that is, the concentrations at equilibrium. Both are constants estimated from measurements at the beginning or end of the test.51265127\item $k_1$, $k_2$, and $k_3$ are positive-valued parameters that control the rates of appearance and disappearance for glucose and insulin.51285129\end{itemize}51305131We can interpret the terms in the equations one by one:51325133\begin{itemize}51345135\item $-k_1 \left[ G(t) - G_b \right]$ is the rate of glucose disappearance due to the effect of glucose itself. When $G(t)$ is above basal level, $G_b$, this term is negative; when $G(t)$ is below basal level this term is positive. So in the absence of insulin, this term tends to restore blood glucose to basal level.51365137\item $-X(t) G(t)$ models the interaction of glucose and insulin in tissue fluid, so the rate increases as either $X$ or $G$ increases. This term does not require a rate parameter because the units of $X$ are unspecified; we can consider $X$ to be in whatever units make the parameter of this term 1.51385139\item $k_3 \left[ I(t) - I_b \right]$ is the rate at which insulin diffuses between blood and tissue fluid. When $I(t)$ is above basal level, insulin diffuses from the blood into the tissue fluid. When $I(t)$ is below basal level, insulin diffuses from tissue to the blood.51405141\item $-k_2 X(t)$ is the rate of insulin disappearance in tissue fluid as it is consumed or broken down.51425143\end{itemize}51445145The initial state of the model is $X(0) = I_b$ and $G(0) = G_0$, where $G_0$ is a constant that represents the concentration of blood sugar immediately after the injection. In theory we could estimate $G_0$ based on measurements, but in practice it takes time for the injected glucose to spread through the blood volume. Since $G_0$ is not measurable, it is treated as a {\bf free parameter} of the model, which means that we are free to choose it to fit the data.51465147\index{free parameter}514851495150\section{Data}51515152To develop and test the model, I use data from Pacini and Bergman\footnote{``MINMOD: A computer program to calculate insulin sensitivity and pancreatic responsivity from the frequently sampled intravenous glucose tolerance test", {\em Computer Methods and Programs in Biomedicine} 23: 113-122, 1986.}. The dataset is in a file in the repository for this book, which we can read into a \py{DataFrame}:51535154\index{data}5155\index{DataFrame object}51565157\begin{python}5158data = pd.read_csv('data/glucose_insulin.csv',5159index_col='time')5160\end{python}51615162\py{data} has two columns: \py{glucose} is the concentration of blood glucose in \si{\milli\gram/\deci\liter}; \py{insulin} is concentration of insulin in the blood in \si{\micro U\per\milli\liter} (a medical ``unit", denoted \si{U}, is an amount defined by convention in context). The index is time in \si{\minute}.51635164\index{concentration}51655166\begin{figure}5167\centerline{\includegraphics[width=3.5in]{figs/chap17-fig01.pdf}}5168\caption{Glucose and insulin concentrations measured by FSIGT.}5169\label{chap17-fig01}5170\end{figure}51715172Figure~\ref{chap17-fig01} shows glucose and insulin concentrations over \SI{182}{\minute} for a subject with normal insulin production and sensitivity.517351745175\section{Interpolation}5176\label{interpolate}51775178Before we are ready to implement the model, there's one problem we have to solve. In the differential equations, $I$ is a function that can be evaluated at any time, $t$. But in the \py{DataFrame}, we only have measurements at discrete times. This is a job for interpolation!51795180\index{interpolation}51815182\begin{figure}5183\centerline{\includegraphics[height=3in]{figs/chap17-fig02.pdf}}5184\caption{Insulin concentrations measured by FSIGT and an interpolated function.}5185\label{chap17-fig02}5186\end{figure}51875188The ModSim library provides a function named \py{interpolate}, which is a wrapper for the SciPy function \py{interp1d}. It takes any kind of \py{Series} as a parameter, including \py{TimeSeries} and \py{SweepSeries}, and returns a function. That's right, I said it returns a {\em function}.51895190\index{function!as return value}5191\index{Series}5192\index{interpolate}5193\index{interp1d}5194\index{SciPy}51955196So we can call \py{interpolate} like this:51975198\begin{python}5199I = interpolate(data.insulin)5200\end{python}52015202Then we can call the new function, \py{I}, like this:52035204\begin{python}5205I(18)5206\end{python}52075208The result is 31.66, which is a linear interpolation between the actual measurements at \py{t=16} and \py{t=19}. We can also pass an array as an argument to \py{I}:52095210\begin{python}5211ts = linrange(t_0, t_end, endpoint=True)5212I(ts)5213\end{python}52145215The result is an array of interpolated values for equally-spaced values of \py{t}, shown in Figure~\ref{chap17-fig02}.52165217\index{linrange}5218\index{NumPy}52195220\py{interpolate} can take additional arguments, which it passes along to \py{interp1d}. You can read about these options at \url{http://modsimpy.com/interp}.52215222In the next chapter, we will use interpolated data to run the simulation of the glucose-insulin system.52235224Before you go on, you might want to read the notebook for this chapter, \py{chap17.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.522552265227\chapter{The glucose model}5228\label{chap18}52295230In this chapter, we implement the glucose minimal model described in the previous chapter. We'll start with \py{run_simulation}, which solves differential equations using discrete time steps. This method works well enough for many applications, but it is not very accurate. In this chapter we explore a better option: using an {\bf ODE solver}.523152325233\section{Implementation}5234\label{glucose}52355236To get started, let's assume that the parameters of the model are known. We'll implement the model and use it to generate time series for \py{G} and \py{X}. Then we'll see how to find the parameters that generate the series that best fits the data.52375238Taking advantage of estimates from prior work, we'll start with these values:52395240\begin{python}5241params = Params(G0 = 290,5242k1 = 0.03,5243k2 = 0.02,5244k3 = 1e-05)5245\end{python}52465247A \py{Params} object is similar to a \py{System} or \py{State} object; it is useful for holding a collection of parameters.52485249\index{State object}5250\index{System object}5251\index{Params object}52525253% TODO: I introduce Params here because I use it with leastsq, but since we aren't5254% using leastsq here any more, we have the option of postponing Params52555256We can pass \py{params} and \py{data} to \py{make_system}:52575258\begin{python}5259def make_system(params, data):5260G0, k1, k2, k3 = params52615262Gb = data.glucose[0]5263Ib = data.insulin[0]5264I = interpolate(data.insulin)52655266t_0 = get_first_label(data)5267t_end = get_last_label(data)52685269init = State(G=G0, X=0)52705271return System(params,5272init=init, Gb=Gb, Ib=Ib, I=I,5273t_0=t_0, t_end=t_end, dt=2)5274\end{python}52755276\py{make_system} uses the measurements at \py{t=0} as the basal levels, \py{Gb} and \py{Ib}.5277It gets \py{t_0} and \py{t_end} from the data.5278And it uses the parameter \py{G0} as the initial value for \py{G}.5279Then it packs everything into a \py{System} object.52805281Here's the update function:52825283\index{update function}5284\index{function!update}52855286\begin{python}5287def update_func(state, t, system):5288G, X = state5289k1, k2, k3 = system.k1, system.k2, system.k35290I, Ib, Gb = system.I, system.Ib, system.Gb5291dt = system.dt52925293dGdt = -k1 * (G - Gb) - X*G5294dXdt = k3 * (I(t) - Ib) - k2 * X52955296G += dGdt * dt5297X += dXdt * dt52985299return State(G=G, X=X)5300\end{python}53015302As usual, the update function takes a \py{State} object, a time, and a \py{System} object as parameters. The first line uses multiple assignment to extract the current values of \py{G} and \py{X}.53035304The following lines unpack the parameters we need from the \py{System} object.53055306Computing the derivatives \py{dGdt} and \py{dXdt} is straightforward; we just translate the equations from math notation to Python.53075308\index{derivative}53095310Then, to perform the update, we multiply each derivative by the discrete time step \py{dt}, which is \SI{2}{\minute} in this example.5311The return value is a \py{State} object with the new values of \py{G} and \py{X}.53125313\index{time step}53145315Before running the simulation, it is a good idea to run the update function with the initial conditions:53165317\begin{python}5318update_func(system.init, system.t_0, system)5319\end{python}53205321If it runs without errors and there is nothing obviously wrong with the results, we are ready to run the simulation. We'll use this version of \py{run_simulation}, which is very similar to previous versions:53225323\index{\py{run_simulation}}53245325\begin{python}5326def run_simulation(system, update_func):5327init = system.init5328t_0, t_end, dt = system.t_0, system.t_end, system.dt53295330frame = TimeFrame(columns=init.index)5331frame.row[t_0] = init5332ts = linrange(t_0, t_end, dt)53335334for t in ts:5335frame.row[t+dt] = update_func(frame.row[t], t, system)53365337return frame5338\end{python}53395340We can run it like this:53415342\begin{python}5343results = run_simulation(system, update_func)5344\end{python}53455346\begin{figure}5347\centerline{\includegraphics[width=3.5in]{figs/chap18-fig01.pdf}}5348\caption{Results from simulation of the glucose minimal model.}5349\label{chap18-fig01}5350\end{figure}53515352Figure~\ref{chap18-fig01} shows the results. The top plot shows simulated glucose levels from the model along with the measured data. The bottom plot shows simulated insulin levels in tissue fluid, which is in unspecified units, and not to be confused with measured insulin levels in the blood.53535354With the parameters I chose, the model fits the data well, except for the first few data points, where we don't expect the model to be accurate.535553565357\section{Solving differential equations}5358\label{slopefunc}53595360So far we have solved differential equations by rewriting them as difference equations. In the current example, the differential equations are:5361%5362\[ \frac{dG}{dt} = -k_1 \left[ G(t) - G_b \right] - X(t) G(t) \]5363%5364\[ \frac{dX}{dt} = k_3 \left[I(t) - I_b \right] - k_2 X(t) \]5365%5366If we multiply both sides by $dt$, we have:5367%5368\[ dG = \left[ -k_1 \left[ G(t) - G_b \right] - X(t) G(t) \right] dt \]5369%5370\[ dX = \left[ k_3 \left[I(t) - I_b \right] - k_2 X(t) \right] dt \]5371%5372When $dt$ is very small, or more precisely {\bf infinitesimal}, this equation is exact. But in our simulations, $dt$ is \SI{2}{\minute}, which is not very small. In effect, the simulations assume that the derivatives $dG/dt$ and $dX/dt$ are constant during each \SI{2}{\minute} time step.53735374\index{time step}53755376This method, evaluating derivatives at discrete time steps and assuming that they are constant in between, is called {\bf Euler's method} (see \url{http://modsimpy.com/euler}).53775378\index{Euler's method}53795380Euler's method is good enough for some simple problems, but it is not very accurate. Other methods are more accurate, but many of them are substantially more complicated.53815382One of the best simple methods is called {\bf Ralston's method}. The ModSim library provides a function called \py{run_ode_solver} that implements it.53835384The ``ODE" in \py{run_ode_solver} stands for ``ordinary differential equation". The equations we are solving are ``ordinary'' because all the derivatives are with respect to the same variable; in other words, there are no partial derivatives.53855386\index{ordinary differential equation}53875388To use \py{run_ode_solver}, we have to provide a ``slope function", like this:53895390\index{slope function}5391\index{function!slope}5392\index{unpack}53935394\begin{python}5395def slope_func(state, t, system):5396G, X = state5397k1, k2, k3 = system.k1, system.k2, system.k35398I, Ib, Gb = system.I, system.Ib, system.Gb53995400dGdt = -k1 * (G - Gb) - X*G5401dXdt = k3 * (I(t) - Ib) - k2 * X54025403return dGdt, dXdt5404\end{python}54055406\py{slope_func} is similar to \py{update_func}; in fact, it takes the same parameters in the same order. But \py{slope_func} is simpler, because all we have to do is compute the derivatives, that is, the slopes. We don't have to do the updates; \py{run_ode_solver} does them for us.54075408\index{\py{run_ode_solver}}54095410Now we can call \py{run_ode_solver} like this:54115412\begin{python}5413results2, details = run_ode_solver(system, slope_func)5414\end{python}54155416\py{run_ode_solver} is similar to \py{run_simulation}: it takes a \py{System} object and a slope function as parameters. It returns two values: a \py{TimeFrame} with the solution and a \py{ModSimSeries} with additional information.54175418A \py{ModSimSeries} is like a \py{System} or \py{State} object; it contains a set of variables and their values. The \py{ModSimSeries} from \py{run_ode_solver}, which we assign to \py{details}, contains information about how the solver ran, including a success code and diagnostic message.54195420The \py{TimeFrame}, which we assign to \py{results}, has one row for each time step and one column for each state variable. In this example, the rows are time from 0 to 182 minutes; the columns are the state variables, \py{G} and \py{X}.54215422\index{TimeFrame object}54235424\begin{figure}5425\centerline{\includegraphics[height=3in]{figs/chap18-fig02.pdf}}5426\caption{Results from Euler's method (simulation) and Ralston's method (ODE solver).}5427\label{chap18-fig02}5428\end{figure}54295430Figure~\ref{chap18-fig02} shows the results from \py{run_simulation} and \py{run_ode_solver}. The difference between them is barely visible.54315432We can compute the percentage differences like this:54335434\begin{python}5435diff = results.G - results2.G5436percent_diff = diff / results2.G * 1005437\end{python}54385439The largest percentage difference is less than 2\%, which is small enough that it probably doesn't matter in practice. Later, we will see examples where Ralston's method is substantially more accurate.54405441Before you go on, you might want to read the notebook for this chapter, \py{chap18.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.544254435444\chapter{Case studies}5445\label{chap19}54465447This chapter reviews the computational patterns we have seen so far and presents exercises where you can apply them.54485449\section{Computational tools}54505451In Chapter~\ref{chap11} we saw an update function that uses multiple assignment to unpack a \py{State} object and assign the state variables to local variables.54525453\begin{python}5454def update_func(state, t, system):5455s, i, r = state54565457infected = system.beta * i * s5458recovered = system.gamma * i54595460s -= infected5461i += infected - recovered5462r += recovered54635464return State(S=s, I=i, R=r)5465\end{python}54665467And in \py{run_simulation} we used multiple assignment again to assign state variables to a row in a \py{TimeFrame}:54685469\begin{python}5470def run_simulation(system, update_func):5471frame = TimeFrame(columns=system.init.index)5472frame.row[system.t_0] = system.init54735474for t in linrange(system.t_0, system.t_end):5475frame.row[t+1] = update_func(frame.row[t], system)54765477return frame5478\end{python}54795480In Chapter~\ref{chap12} we used the functions \py{max} and \py{idxmax} to compute metrics:54815482\begin{python}5483largest_value = S.max()5484time_of_largest_value = S.idxmax()5485\end{python}54865487And we saw the logistic function, a general function which is useful for modeling relationships between variables, like the effectiveness of an intervention as a function of expenditure.54885489In Chapter~\ref{chap13} we used a \py{SweepFrame} object to sweep two parameters:54905491\begin{python}5492def sweep_parameters(beta_array, gamma_array):5493frame = SweepFrame(columns=gamma_array)5494for gamma in gamma_array:5495frame[gamma] = sweep_beta(beta_array, gamma)5496return frame5497\end{python}54985499And we used \py{contour} to generate a contour plot of a two-dimensional sweep.55005501In Chapter~\ref{chap15} we used \py{linrange} to create an array of equally spaced values. \py{linrange} is similar to \py{linspace}: the difference is that \py{linrange} lets you specify the space between values, and it computes the number of values; \py{linspace} lets you specify the number of values, and it computes the space between them.55025503Here's a version of \py{run_simulation} that uses \py{linrange}:55045505\begin{python}5506def run_simulation(system, update_func):5507init = system.init5508t_0, t_end, dt = system.t_0, system.t_end, system.dt55095510frame = TimeFrame(columns=init.index)5511frame.row[t_0] = init5512ts = linrange(t_0, t_end, dt)55135514for t in ts:5515frame.row[t+dt] = update_func(frame.row[t], t, system)55165517return frame5518\end{python}55195520In Chapter~\ref{chap16} we used \py{root_bisect} to find the value of a parameter that yields a particular result. We defined an error function:55215522\index{\py{root_bisect}}55235524\begin{python}5525system = make_system(T_init=90, r=r, volume=300, t_end=30)5526results = run_simulation(system, update_func)5527T_final = get_last_value(results.T)5528return T_final - 705529\end{python}55305531And passed it to \py{root_bisect} with an initial interval, like this:55325533\begin{python}5534res = root_bisect(error_func1, [0.01, 0.02])5535r_coffee = res.root5536\end{python}55375538In Chapter~\ref{chap17} we used \py{interpolate}, which returns a function:55395540\begin{python}5541I = interpolate(data.insulin)5542\end{python}55435544We can call \py{I} like any other function, passing as an argument either a single value or a NumPy array:55455546\begin{python}5547I(18)55485549ts = linrange(t_0, t_end)5550I(ts)5551\end{python}55525553In Chapter~\ref{chap18} we used a \py{Params} object, which is a collection of parameters.55545555\begin{python}5556params = Params(G0 = 290,5557k1 = 0.03,5558k2 = 0.02,5559k3 = 1e-05)5560\end{python}55615562Chapter~\ref{chap18} also introduces \py{run_ode_solver} which computes numerical solutions to differential equations.55635564\py{run_ode_solver} uses a slope function, which is similar to an update function:55655566\begin{python}5567G, X = state5568k1, k2, k3 = system.k1, system.k2, system.k35569I, Ib, Gb = system.I, system.Ib, system.Gb55705571dGdt = -k1 * (G - Gb) - X*G5572dXdt = k3 * (I(t) - Ib) - k2 * X55735574return dGdt, dXdt5575\end{python}55765577And we can call it like this:55785579\begin{python}5580results, details = run_ode_solver(system, slope_func)5581\end{python}55825583The rest of this chapter presents case studies you can use to practice these tools.558455855586\section{The insulin minimal model}55875588Along with the glucose minimal model in Chapter~\ref{chap17}, Berman et al.~developed an insulin minimal model, in which the concentration of insulin, $I$, is governed by this differential equation:5589%5590\[ \frac{dI}{dt} = -k I(t) + \gamma \left[ G(t) - G_T \right] t \]5591%5592where55935594\begin{itemize}55955596\item $k$ is a parameter that controls the rate of insulin disappearance independent of blood glucose.55975598\item $G(t)$ is the measured concentration of blood glucose at time $t$.55995600\item $G_T$ is the glucose threshold; when blood glucose is above this level, it triggers an increase in blood insulin.56015602\item $\gamma$ is a parameter that controls the rate of increase (or decrease) in blood insulin when glucose is above (or below) $G_T$.56035604% TODO: explain why t is there56055606\end{itemize}56075608The 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.56095610\index{insulin minimal model}5611\index{differential equation}56125613The parameters of this model can be used to estimate $\phi_1$ and $\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:5614%5615\[ \phi_1 = \frac{I_{max} - I_b}{k (G_0 - G_b)}\]5616%5617\[ \phi_2 = \gamma \times 10^4 \]5618%5619where $I_{max}$ is the maximum measured insulin level, and $I_b$ and $G_b$ are the basal levels of insulin and glucose.56205621%TODO: Clarify whether G0 here is the parameter we estimated in the previous5622% model, or the maximum observed value of G.56235624In the repository for this book, you will find a notebook, \py{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.562556265627\section{Low-Pass Filter}56285629The following circuit diagram\footnote{From \url{http://modsimpy.com/divider}} shows a low-pass filter built with one resistor and one capacitor.56305631\centerline{\includegraphics[height=1.2in]{figs/RC_Divider.pdf}}56325633A ``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.56345635A filter is ``low-pass" if it allows low-frequency signals to pass from $V_{in}$ to $V_{out}$ unchanged, but it reduces the amplitude of high-frequency signals.56365637By applying the laws of circuit analysis, we can derive a differential equation that describes the behavior of this system. By solving the differential equation, we can predict the effect of this circuit on any input signal.56385639Suppose we are given $V_{in}$ and $V_{out}$ at a particular instant in time. By Ohm's law, which is a simple model of the behavior of resistors, the instantaneous current through the resistor is:5640%5641\[ I_R = (V_{in} - V_{out}) / R \]5642%5643where $R$ is resistance in ohms (\si{\ohm}).56445645Assuming that no current flows through the output of the circuit, Kirchhoff's current law implies that the current through the capacitor is:5646%5647\[ I_C = I_R \]5648%5649According to a simple model of the behavior of capacitors, current through the capacitor causes a change in the voltage across the capacitor:5650%5651\[ I_C = C \frac{d V_{out}}{dt} \]5652%5653where $C$ is capacitance in farads (\si{\farad}). Combining these equations yields a differential equation for $V_{out}$:5654%5655\[ \frac{d V_{out}}{dt} = \frac{V_{in} - V_{out}}{R C} \]5656%5657In the repository for this book, you will find a notebook, \py{filter.ipynb}, which contains starter code for this case study. Follow the instructions to simulate the low-pass filter for input signals like this:5658%5659\[ V_{in}(t) = A \cos (2 \pi f t) \]5660%5661where $A$ is the amplitude of the input signal, say \SI{5}{\volt}, and $f$ is the frequency of the signal in \si{\hertz}.56625663In the repository for this book, you will find a notebook, \py{filter.ipynb}, which contains starter code for this case study. Read the notebook, run the code, and work on the exercises.5664566556665667\section{Thermal behavior of a wall}56685669This case study is based on a paper by Gori, et~al\footnote{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", {\it Energy and Buildings}, Volume 135, pages 398-409, \url{http://modsimpy.com/wall2}.56705671The authors put their paper under a Creative Commons license, and make their data available at \url{http://modsimpy.com/wall }. I thank them for their commitment to open, reproducible science, which made this case study possible.} that models the thermal behavior of a brick wall, with the goal of understanding the ``performance gap between the expected energy use of buildings and their measured energy use".56725673The following figure shows the scenario and their model of the wall:56745675\vspace{0.1in}5676\centerline{\includegraphics[height=1.4in]{figs/wall_model.pdf}}56775678On the interior and exterior surfaces of the wall, they measure temperature and heat flux over a period of three days. They model the wall using two thermal masses connected to the surfaces, and to each other, by thermal resistors.56795680The primary methodology of the paper is a Bayesian method for inferring the parameters of the system (two thermal masses and three thermal resistances).56815682The primary result is a comparison of two models: the one shown here 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.56835684For this case study we will implement their model and run it with the estimated parameters from the paper, and then use \py{fit_leastsq} to see if we can find parameters that yield lower errors.56855686In the repository for this book, you will find a notebook, \py{wall.ipynb} with the code and results for this case study.568756885689\chapter{Projectiles}5690\label{chap20}56915692So far the differential equations we've worked with have been {\bf first order}, which means they involve only first derivatives. In this chapter, we turn our attention to second order ODEs, which can involve both first and second derivatives.56935694\index{first order ODE}5695\index{second order ODE}56965697We'll revisit the falling penny example from Chapter~\ref{chap01}, and use \py{run_ode_solver} to find the position and velocity of the penny as it falls, with and without air resistance.569856995700\section{Newton's second law of motion}57015702First order ODEs can be written5703%5704\[ \frac{dy}{dx} = G(x, y) \]5705%5706where $G$ is some function of $x$ and $y$ (see \url{http://modsimpy.com/ode}). Second order ODEs can be written5707%5708\[ \frac{d^2y}{dx^2} = H(x, y, \frac{dy}{dt}) \]5709%5710where $H$ is a function of $x$, $y$, and $dy/dx$.57115712In this chapter, we will work with one of the most famous and useful second order ODE, Newton's second law of motion:5713%5714\[ F = m a \]5715%5716where $F$ is a force or the total of a set of forces, $m$ is the mass of a moving object, and $a$ is its acceleration.57175718\index{Newton's second law of motion}5719\index{differential equation}5720\index{acceleration}5721\index{velocity}5722\index{position}57235724Newton's law might not look like a differential equation, until we realize that acceleration, $a$, is the second derivative of position, $y$, with respect to time, $t$. With the substitution5725%5726\[ a = \frac{d^2y}{dt^2} \]5727%5728Newton's law can be written5729%5730\[ \frac{d^2y}{dt^2} = F / m \]5731%5732And that's definitely a second order ODE. In general, $F$ can be a function of time, position, and velocity.57335734Of course, this ``law" is really a model in the sense that it is a simplification of the real world. Although it is often approximately true:57355736\begin{itemize}57375738\item It only applies if $m$ is constant. If mass depends on time, position, or velocity, we have to use a more general form of Newton's law (see \url{http://modsimpy.com/varmass}).57395740\index{variable mass}57415742\item It is not a good model for very small things, which are better described by another model, quantum mechanics.57435744\index{quantum mechanics}57455746\item And it is not a good model for things moving very fast, which are better described by yet another model, relativistic mechanics.57475748\index{relativity}57495750\end{itemize}57515752However, for medium-sized things with constant mass, moving at medium-sized speeds, Newton's model is extremely useful. If we can quantify the forces that act on such an object, we can predict how it will move.575357545755\section{Dropping pennies}57565757As a first example, let's get back to the penny falling from the Empire State Building, which we considered in Section~\ref{penny}. We will implement two models of this system: first without air resistance, then with.57585759\index{falling penny}5760\index{air resistance}57615762Given that the Empire State Building is \SI{381}{\meter} high, and assuming that the penny is dropped from a standstill, the initial conditions are:57635764\index{State object}57655766\begin{python}5767init = State(y=381 * m,5768v=0 * m/s)5769\end{python}57705771where \py{y} is height above the sidewalk and \py{v} is velocity. The units \py{m} and \py{s} are from the \py{UNITS} object provided by Pint:57725773\index{unit}5774\index{Pint}57755776\begin{python}5777m = UNITS.meter5778s = UNITS.second5779\end{python}57805781The only system parameter is the acceleration of gravity:57825783\begin{python}5784g = 9.8 * m/s**25785\end{python}57865787In addition, we'll specify the duration of the simulation and the step size:57885789\begin{python}5790t_end = 10 * s5791dt = 0.1 * s5792\end{python}57935794With these parameters, the number of time steps is 100, which is good enough for many problems. Once we have a solution, we will increase the number of steps and see what effect it has on the results.57955796We need a \py{System} object to store the parameters:57975798\index{System object}57995800\begin{python}5801system = System(init=init, g=g, t_end=t_end, dt=dt)5802\end{python}58035804Now we need a slope function, and here's where things get tricky. As we have seen, \py{run_ode_solver} can solve systems of first order ODEs, but Newton's law is a second order ODE. However, if we recognize that58055806\index{slope function}5807\index{function!slope}58085809\begin{enumerate}58105811\item Velocity, $v$, is the derivative of position, $dy/dt$, and58125813\item Acceleration, $a$, is the derivative of velocity, $dv/dt$,58145815\end{enumerate}58165817we can rewrite Newton's law as a system of first order ODEs:5818%5819\[ \frac{dy}{dt} = v \]5820%5821\[ \frac{dv}{dt} = a \]5822%5823And we can translate those equations into a slope function:58245825\index{system of equations}5826\index{unpack}58275828\begin{python}5829def slope_func(state, t, system):5830y, v = state58315832dydt = v5833dvdt = -system.g58345835return dydt, dvdt5836\end{python}58375838The first parameter, \py{state}, contains the position and velocity of the penny. The last parameter, \py{system}, contains the system parameter \py{g}, which is the magnitude of acceleration due to gravity.58395840\index{State object}58415842The second parameter, \py{t}, is time. It is not used in this slope function because none of the factors of the model are time dependent (see Section~\ref{glucose}). I include it anyway because this function will be called by \py{run_ode_solver}, which always provides the same arguments, whether they are needed or not.58435844\index{time dependent}58455846The rest of the function is a straightforward translation of the differential equations, with the substitution $a = -g$, which indicates that acceleration due to gravity is in the direction of decreasing $y$. \py{slope_func} returns a sequence containing the two derivatives.58475848Before calling \py{run_ode_solver}, it is a good idea to test the slope function with the initial conditions:58495850\begin{python}5851dydt, dvdt = slope_func(system.init, 0, system)5852\end{python}58535854The result is \SI{0}{\meter\per\second} for velocity and \SI{9.8}{\meter\per\second\squared} for acceleration. Now we call \py{run_ode_solver} like this:58555856\begin{python}5857results, details = run_ode_solver(system, slope_func)5858\end{python}58595860\py{results} is a \py{TimeFrame} with two columns: \py{y} contains the height of the penny; \py{v} contains its velocity.58615862\index{TimeFrame object}5863\index{\py{run_ode_solver}}58645865We can plot the results like this:58665867\begin{python}5868def plot_position(results):5869plot(results.y)5870decorate(xlabel='Time (s)',5871ylabel='Position (m)')5872\end{python}58735874\begin{figure}5875\centerline{\includegraphics[height=3in]{figs/chap20-fig01.pdf}}5876\caption{Height of the penny versus time, with no air resistance.}5877\label{chap20-fig01}5878\end{figure}58795880Figure~\ref{chap20-fig01} shows the result. Since acceleration is constant, velocity increases linearly and position decreases quadratically; as a result, the height curve is a parabola.58815882\index{parabola}58835884The last value of \py{results.y} is \SI{-109}{\meter}, which means we ran the simulation too long. One way to solve this problem is to use the results to estimate the time when the penny hits the sidewalk.58855886The ModSim library provides \py{crossings}, which takes a \py{TimeSeries} and a value, and returns a sequence of times when the series passes through the value. We can find the time when the height of the penny is \py{0} like this:58875888\begin{python}5889t_crossings = crossings(results.y, 0)5890\end{python}58915892The result is an array with a single value, \SI{8.818}{s}. Now, we could run the simulation again with \py{t_end = 8.818}, but there's a better way.58935894\section{Events}5895\label{events}58965897As an option, \py{run_ode_solver} can take an {\bf event function}, which detects an ``event", like the penny hitting the sidewalk, and ends the simulation.58985899Event functions take the same parameters as slope functions, \py{state}, \py{t}, and \py{system}. They should return a value that passes through \py{0} when the event occurs. Here's an event function that detects the penny hitting the sidewalk:59005901\begin{python}5902def event_func(state, t, system):5903y, v = state5904return y5905\end{python}59065907The return value is the height of the penny, \py{y}, which passes through \py{0} when the penny hits the sidewalk.59085909We pass the event function to \py{run_ode_solver} like this:59105911\begin{python}5912results, details = run_ode_solver(system, slope_func,5913events=event_func)5914\end{python}59155916Then we can get the flight time and final velocity like this:59175918\begin{code}5919t_sidewalk = get_last_label(results) * s5920v_sidewalk = get_last_value(results.v)5921\end{code}59225923Before you go on, you might want to read the notebook for this chapter, \py{chap20.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.5924592559265927\chapter{Air resistance}5928\label{chap21}59295930In the previous chapter we simulated a penny falling in a vacuum, that is, without air resistance. But the computational framework we used is very general; it is easy to add additional forces, including drag.59315932In this chapter, I present a model of drag force and add it to the simulation.593359345935\section{Drag force}5936\label{drag}59375938As an object moves through a fluid, like air, the object applies force to the air and, in accordance with Newton's third law of motion, the air applies an equal and opposite force to the object (see \url{http://modsimpy.com/newton}).59395940\index{air resistance}5941\index{drag force}5942\index{force!drag}5943\index{drag equation}59445945The direction of this {\bf drag force} is opposite the direction of travel, and its magnitude is given by the drag equation (see \url{http://modsimpy.com/drageq}):5946%5947\[ F_d = \frac{1}{2}~\rho~v^2~C_d~A \]5948%5949where59505951\begin{itemize}59525953\item $F_d$ is force due to drag, in newtons (\si{\newton}).59545955\item $\rho$ is the density of the fluid in \si{\kg\per\meter\cubed}.5956\index{density}59575958\item $v$ is the magnitude of velocity in \si{\meter\per\second}.5959\index{velocity}59605961\item $A$ is the {\bf reference area} of the object, in \si{\meter\squared}. In this context, the reference area is the projected frontal area, that is, the visible area of the object as seen from a point on its line of travel (and far away).59625963\index{reference area}59645965\item $C_d$ is the {\bf drag coefficient}, a dimensionless quantity that depends on the shape of the object (including length but not frontal area), its surface properties, and how it interacts with the fluid.59665967\index{drag coefficient}59685969\end{itemize}59705971For objects moving at moderate speeds through air, typical drag coefficients are between 0.1 and 1.0, with blunt objects at the high end of the range and streamlined objects at the low end (see \url{http://modsimpy.com/dragco}).59725973For simple geometric objects we can sometimes guess the drag coefficient with reasonable accuracy; for more complex objects we usually have to take measurements and estimate $C_d$ from data.59745975Of course, the drag equation is itself a model, based on the assumption that $C_d$ does not depend on the other terms in the equation: density, velocity, and area. For objects moving in air at moderate speeds (below 45 mph or \SI{20}{\meter\per\second}), this model might be good enough, but we should remember to revisit this assumption.59765977For the falling penny, we can use measurements to estimate $C_d$. In particular, we can measure {\bf terminal velocity}, $v_{term}$, which is the speed where drag force equals force due to gravity:5978%5979\[ \frac{1}{2}~\rho~v_{term}^2~C_d~A = m g \]5980%5981where $m$ is the mass of the object and $g$ is acceleration due to gravity. Solving this equation for $C_d$ yields:5982%5983\[ C_d = \frac{2~m g}{\rho~v_{term}^2~A} \]5984%5985According to {\it Mythbusters}, the terminal velocity of a penny is between 35 and 65 mph (see \url{http://modsimpy.com/mythbust}). Using the low end of their range, 40 mph or about \SI{18}{\meter\per\second}, the estimated value of $C_d$ is 0.44, which is close to the drag coefficient of a smooth sphere.59865987\index{Mythbusters}5988\index{terminal velocity}59895990Now we are ready to add air resistance to the model.599159925993\section{Implementation}5994\label{penny_drag}59955996As the number of system parameters increases, and as we need to do more work to compute them, we will find it useful to define a \py{Params} object to contain the quantities we need to make a \py{System} object. \py{Params} objects are similar to \py{System} and \py{State} objects; in fact, all three have the same capabilities. I have given them different names to document the different roles they play.59975998% TODO: This is not actually the first use of Params59996000\index{Params object}60016002Here's the \py{Params} object for the falling penny:60036004\begin{python}6005params = Params(height = 381 * m,6006v_init = 0 * m / s,6007g = 9.8 * m/s**2,6008mass = 2.5e-3 * kg,6009diameter = 19e-3 * m,6010rho = 1.2 * kg/m**3,6011v_term = 18 * m / s)6012\end{python}60136014The mass and diameter are from \url{http://modsimpy.com/penny}. The density of air depends on temperature, barometric pressure (which depends on altitude), humidity, and composition (\url{http://modsimpy.com/density}). I chose a value that might be typical in Boston, Massachusetts at \SI{20}{\celsius}.60156016\index{System object}6017\index{\py{make_system}}60186019Here's a version of \py{make_system} that takes a \py{Params} object and returns a \py{System}:60206021\index{unpack}60226023\begin{python}6024def make_system(params):6025diameter, mass = params.diameter, params.mass6026g, rho = params.g, params.rho,6027v_init, v_term = params.v_init, params.v_term6028height = params.height60296030area = np.pi * (diameter/2)**26031C_d = 2 * mass * g / (rho * area * v_term**2)6032init = State(y=height, v=v_init)6033t_end = 30 * s6034dt = t_end / 10060356036return System(params, area=area, C_d=C_d,6037init=init, t_end=t_end, dt=dt)6038\end{python}60396040The first argument of \py{System} is \py{params}, so the result contains all of the parameters in \py{params}, \py{area}, \py{C_d}, and the rest.60416042It might not be obvious why we need \py{Params} objects, but they will turn out to be useful soon.60436044We can make a \py{System} like this:60456046\begin{python}6047system = make_system(params)6048\end{python}60496050Now here's a version of the slope function that includes drag:60516052\index{slope function}6053\index{function!slope}6054\index{unpack}60556056\begin{python}6057def slope_func(state, t, system):6058y, v = state6059rho, C_d, g = system.rho, system.C_d, system.g6060area, mass = system.area, system.mass60616062f_drag = rho * v**2 * C_d * area / 26063a_drag = f_drag / mass60646065dydt = v6066dvdt = -g + a_drag60676068return dydt, dvdt6069\end{python}60706071\py{f_drag} is force due to drag, based on the drag equation. \py{a_drag} is acceleration due to drag, based on Newton's second law.60726073\index{gravity}60746075To compute total acceleration, we add accelerations due to gravity and drag. \py{g} is negated because it is in the direction of decreasing \py{y}, and \py{a_drag} is positive because it is in the direction of increasing \py{y}. In the next chapter we will use \py{Vector} objects to keep track of the direction of forces and add them up in a less error-prone way.60766077To stop the simulation when the penny hits the sidewalk, we'll use the event function from Section~\ref{events}:60786079\begin{python}6080def event_func(state, t, system):6081y, v = state6082return y6083\end{python}60846085Now we can run the simulation like this:60866087\index{\py{run_ode_solver}}60886089\begin{python}6090results, details = run_ode_solver(system, slope_func,6091events=event_func)6092\end{python}60936094\begin{figure}6095\centerline{\includegraphics[height=3in]{figs/chap21-fig01.pdf}}6096\caption{Height of the penny versus time, with air resistance.}6097\label{chap21-fig01}6098\end{figure}60996100Figure~\ref{chap21-fig01} shows the result. It only takes a few seconds for the penny to accelerate up to terminal velocity; after that, velocity is constant, so height as a function of time is a straight line.61016102\index{terminal velocity}61036104Before you go on, you might want to read the notebook for this chapter, \py{chap21.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.6105610661076108\chapter{Projectiles in 2-D}6109\label{chap22}61106111In the previous chapter we modeled objects moving in one dimension, with and without drag. Now let's move on to two dimensions, and baseball!61126113In this chapter we model the flight of a baseball including the effect of air resistance. In the next chapter we use this model to solve an optimization problem.611461156116\section{Baseball}6117\label{baseball}61186119To model the flight of a baseball, we have to make some modeling decisions. To get started, we ignore any spin that might be on the ball, and the resulting Magnus force (see \url{http://modsimpy.com/magnus}). Under this assumption, the ball travels in a vertical plane, so we'll run simulations in two dimensions, rather than three.61206121\index{Magnus force}61226123Air resistance has a substantial effect on most projectiles in air, so we will include a drag force.61246125\index{air resistance}61266127To model air resistance, we'll need the mass, frontal area, and drag coefficient of a baseball. Mass and diameter are easy to find (see \url{http://modsimpy.com/baseball}). Drag coefficient is only a little harder; according to {\it The Physics of Baseball}\footnote{Adair, {\it The Physics of Baseball}, Third Edition, Perennial, 2002.}, the drag coefficient of a baseball is approximately 0.33 (with no units).61286129\index{drag coefficient}61306131However, this value {\em does} depend on velocity. At low velocities it might be as high as 0.5, and at high velocities as low as 0.28. Furthermore, the transition between these regimes typically happens exactly in the range of velocities we are interested in, between \SI{20}{\meter\per\second} and \SI{40}{\meter\per\second}.61326133Nevertheless, we'll start with a simple model where the drag coefficient does not depend on velocity; as an exercise at the end of the chapter, you will have a chance to implement a more detailed model and see what effect is has on the results.61346135But first we need a new computational tool, the \py{Vector} object.613661376138\section{Vectors}61396140Now that we are working in two dimensions, we will find it useful to work with {\bf vector quantities}, that is, quantities that represent both a magnitude and a direction. We will use vectors to represent positions, velocities, accelerations, and forces in two and three dimensions.61416142\index{Vector object}6143\index{array}6144\index{NumPy}61456146The ModSim library provides a \py{Vector} object that represents a vector quantity. A \py{Vector} object is a like a NumPy array; it contains elements that represent the {\bf components} of the vector. For example, in a \py{Vector} that represents a position in space, the components are the $x$ and $y$ coordinates (and a $z$ coordinate in 3-D). A \py{Vector} object can also have units, like the quantities we've seen in previous chapters.61476148\index{unit}61496150You can create a \py{Vector} by specifying its components. The following \py{Vector} represents a point \SI{3}{\meter} to the right (or east) and \SI{4}{\meter} up (or north) from an implicit origin:61516152\index{component}61536154\begin{python}6155A = Vector(3, 4) * m6156\end{python}61576158You can access the components of a \py{Vector} by name using the dot operator: for example, \py{A.x} or \py{A.y}. You can also access them by index using brackets: for example, \py{A[0]} or \py{A[1]}.61596160Similarly, you can get the magnitude and angle using the dot operator, \py{A.mag} and \py{A.angle}. {\bf Magnitude} is the length of the vector: if the \py{Vector} represents position, magnitude is the distance from the origin; if it represents velocity, magnitude is speed, that is, how fast the object is moving, regardless of direction.61616162\index{angle}6163\index{magnitude}61646165The {\bf angle} of a \py{Vector} is its direction, expressed as the angle in radians from the positive $x$ axis. In the Cartesian plane, the angle \SI{0}{\radian} is due east, and the angle \SI{\pi}{\radian} is due west.61666167\index{radian}61686169\py{Vector} objects support most mathematical operations, including addition and subtraction:61706171\begin{python}6172B = Vector(1, 2) * m6173A + B6174A - B6175\end{python}61766177For the definition and graphical interpretation of these operations, see \url{http://modsimpy.com/vecops}.61786179\index{vector operation}61806181When you add and subtract \py{Vector} objects, the ModSim library uses NumPy and Pint to check that the operands have the same number of dimensions and units. The notebook for this chapter shows examples for working with \py{Vector} objects.61826183\index{dimensions}61846185One note on working with angles: in mathematics, we almost always represent angle in radians, and most Python functions expect angles in radians. But people often think more naturally in degrees. It can be awkward, and error-prone, to use both units in the same program. Fortunately, Pint makes it possible to represent angles using quantities with units.61866187\index{degree}61886189As an example, I'll get the \py{degree} unit from \py{UNITS}, and create a quantity that represents 45 degrees:61906191\begin{python}6192degree = UNITS.degree6193angle = 45 * degree6194\end{python}61956196If we need to convert to radians we can use the \py{to} function6197\index{\py{to}}61986199\begin{python}6200radian = UNITS.radian6201rads = angle.to(radian)6202\end{python}62036204If you are given an angle and velocity, you can make a \py{Vector} using \py{pol2cart}, which converts from polar to Cartesian coordinates. To demonstrate, I'll extract the angle and magnitude of \py{A}:62056206\index{pol2cart}62076208\begin{python}6209mag = A.mag6210angle = A.angle6211\end{python}62126213And then make a new \py{Vector} with the same components:62146215\begin{python}6216x, y = pol2cart(angle, mag)6217Vector(x, y)6218\end{python}62196220Another way to represent the direction of \py{A} is a {\bf unit vector}, which is a vector with magnitude 1 that points in the same direction as \py{A}. You can compute a unit vector by dividing a vector by its magnitude:62216222\index{unit vector}6223\index{hat function}62246225\begin{python}6226A / A.mag6227\end{python}62286229We can do the same thing using the \py{hat} function, so named because unit vectors are conventionally decorated with a hat, like this: $\hat{A}$.62306231\begin{python}6232A.hat()6233\end{python}62346235Now let's get back to the game.623662376238\section{Simulating baseball flight}62396240Let's simulate the flight of a baseball that is batted from home plate at an angle of \SI{45}{\degree} and initial speed \SI{40}{\meter \per \second}.6241Using the center of home plate as the origin, the x-axis is parallel to the ground; the y-axis is vertical. The initial height is about \SI{1}{\meter}.62426243As in Section~\ref{penny_drag}, I'll create a \py{Params} object that contains the parameters of the system:62446245\index{Params object}62466247\begin{python}6248t_end = 10 * s6249dt = t_end / 10062506251params = Params(x = 0 * m,6252y = 1 * m,6253g = 9.8 * m/s**2,6254mass = 145e-3 * kg,6255diameter = 73e-3 * m,6256rho = 1.2 * kg/m**3,6257C_d = 0.33,6258angle = 45 * degree,6259velocity = 40 * m / s,6260t_end=t_end, dt=dt)6261\end{python}62626263The mass, diameter, and drag coefficient of the baseball are from the sources in Section~\ref{baseball}. The acceleration of gravity, \py{g}, is a well-known quantity, and the density of air, \py{rho}, is based on a temperature of \SI{20}{\celsius} at sea level (see \url{http://modsimpy.com/tempress}).6264I chose the value of \py{t_end} to run the simulation long enough for the ball to land on the ground.62656266\index{density}62676268The following function uses the \py{Params} object to make a \py{System} object. This two-step process makes the code more readable and makes it easier to work with functions like \py{root_bisect}.62696270\index{System object}6271\index{\py{make_system}}62726273\begin{python}6274def make_system(params):6275angle, velocity = params.angle, params.velocity62766277# convert angle to degrees6278theta = np.deg2rad(angle)62796280# compute x and y components of velocity6281vx, vy = pol2cart(theta, velocity)62826283# make the initial state6284R = Vector(params.x, params.y)6285V = Vector(vx, vy)6286init = State(R=R, V=V)62876288# compute area from diameter6289diameter = params.diameter6290area = np.pi * (diameter/2)**262916292return System(params, init=init, area=area)6293\end{python}62946295\py{make_system} uses \py{np.deg2rad} to convert \py{angle} to radians and \py{pol2cart} to compute the $x$ and $y$ components of the initial velocity.62966297Then it makes \py{Vector} objects to represent the initial position, \py{R}, and velocity \py{V}. These vectors are stored as state variables in \py{init}.62986299\py{make_system} also computes \py{area}, then creates a \py{System} object with all of the variables from \py{params} plus \py{init} and \py{area}.63006301\index{deg2rad}6302\index{State object}63036304Next we need a function to compute drag force:63056306\begin{python}6307def drag_force(V, system):6308rho, C_d, area = system.rho, system.C_d, system.area63096310mag = -rho * V.mag**2 * C_d * area / 26311direction = V.hat()6312f_drag = direction * mag6313return f_drag6314\end{python}63156316This function takes \py{V} as a \py{Vector} and returns \py{f_drag} as a \py{Vector}.6317It uses the drag equation to compute the magnitude of the drag force, and the \py{hat} function to compute the direction. \py{-V.hat()} computes a unit vector pointing in the opposite direction of \py{V}.63186319\index{unit vector}6320\index{slope function}6321\index{function!slope}63226323Now we're ready for a slope function:63246325\begin{python}6326def slope_func(state, t, system):6327R, V = state6328mass, g = system.mass, system.g63296330a_drag = drag_force(V, system) / mass6331a_grav = Vector(0, -g)63326333A = a_grav + a_drag63346335return V, A6336\end{python}63376338As usual, the parameters of the slope function are a \py{State} object, time, and a \py{System} object. In this example, we don't use \py{t}, but we can't leave it out because when \py{run_ode_solver} calls the slope function, it always provides the same arguments, whether they are needed or not.63396340The \py{State} object contains two state variables, \py{R} and \py{V}, that represent position and velocity as \py{Vector} objects.63416342\index{state variable}63436344The return values from the slope function are the derivatives of these quantities. The derivative of position is velocity, so the first return value is \py{V}, which we extracted from the \py{State} object. The derivative of velocity is acceleration, and that's what we have to compute.63456346\index{acceleration}6347\index{velocity}6348\index{position}63496350The total acceleration of the baseball is the sum of accelerations due to gravity and drag. These quantities have both magnitude and direction, so they are represented by \py{Vector} objects.63516352We already saw how \py{a_drag} is computed. \py{a_grav} is a \py{Vector} with magnitude \py{g} pointed in the negative \py{y} direction.63536354Using vectors to represent forces and accelerations makes the code concise, readable, and less error-prone. In particular, when we add \py{a_grav} and \py{a_drag}, the directions are likely to be correct, because they are encoded in the \py{Vector} objects. And the units are certain to be correct, because otherwise Pint would report an error.63556356\index{Pint}63576358As always, we can test the slope function by running it with the initial conditions:63596360\begin{python}6361slope_func(system.init, 0, system)6362\end{python}63636364We can use an event function to stop the simulation when the ball hits the ground.63656366\begin{python}6367def event_func(state, t, system):6368R, V = state6369return R.y6370\end{python}63716372The event function takes the same parameters as the slope function, and returns the y coordinate of \py{R}. When the y coordinate passes through 0, the simulation stops.63736374Now we're ready to run the simulation:63756376\begin{python}6377results, details = run_ode_solver(system, slope_func,6378events=event_func)6379\end{python}63806381\py{results} is a \py{TimeFrame} object with one column for each of the state variables, \py{R} and \py{V}.63826383\index{TimeFrame object}63846385We can get the flight time like this:63866387\begin{python}6388flight_time = get_last_label(results) * s6389\end{python}63906391And the final \py{x} coordinate like this:63926393\begin{python}6394R_final = get_last_value(results.R)6395x_dist = R_final.x6396\end{python}639763986399\section{Trajectories}64006401We can plot the $x$ and $y$ components of position like this:64026403\begin{python}6404x = results.R.extract('x')6405y = results.R.extract('y')64066407x.plot()6408y.plot()6409\end{python}64106411The \py{extract} function loops through the \py{Vector} objects in \py{R} and extracts one coordinate from each. The result is a \py{TimeSeries}.64126413\index{extract function}6414\index{TimeSeries}64156416\begin{figure}6417\centerline{\includegraphics[height=3in]{figs/chap22-fig01.pdf}}6418\caption{Simulated baseball flight, $x$ and $y$ components of position as a function of time.}6419\label{chap22-fig01}6420\end{figure}64216422Figure~\ref{chap22-fig01} shows the result. As expected, the $x$ component increases as the ball moves away from home plate. The $y$ position climbs initially and then descends, falling to \SI{0}{\meter} near \SI{5.0}{\second}.64236424\index{monotonic}64256426Another way to view the same data is to plot the $x$ component on the x-axis and the $y$ component on the y-axis, so the plotted line follows the trajectory of the ball through the plane:64276428\begin{python}6429x = results.R.extract('x')6430y = results.R.extract('y')6431plot(x, y, label='trajectory')6432\end{python}64336434\begin{figure}6435\centerline{\includegraphics[height=3in]{figs/chap22-fig02.pdf}}6436\caption{Simulated baseball flight, trajectory plot.}6437\label{chap22-fig02}6438\end{figure}64396440Figure~\ref{chap22-fig02} shows this way of visualizing the results, which is called a {\bf trajectory plot} (see \url{http://modsimpy.com/trajec}).64416442\index{trajectory plot}64436444A trajectory plot can be easier to interpret than a time series plot, because it shows what the motion of the projectile would look like (at least from one point of view). Both plots can be useful, but don't get them mixed up! If you are looking at a time series plot and interpreting it as a trajectory, you will be very confused.64456446Before you go on, you might want to read the notebook for this chapter, \py{chap22.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.644764486449\chapter{Optimization}6450\label{chap23}64516452In the previous chapter we developed a model of the flight of a baseball, including gravity and a simple version of drag, but neglecting spin, Magnus force, and the dependence of the coefficient of drag on velocity.64536454In this chapter we apply that model to an optimization problem.64556456\section{The Manny Ramirez problem}6457\label{manny}64586459Manny Ramirez is a former member of the Boston Red Sox (an American baseball team) who was notorious for his relaxed attitude and taste for practical jokes. Our objective in this chapter is to solve the following Manny-inspired problem:64606461{\it What is the minimum effort required to hit a home run in Fenway Park?}64626463Fenway Park is a baseball stadium in Boston, Massachusetts. One of its most famous features is the ``Green Monster", which is a wall in left field that is unusually close to home plate, only 310 feet away. To compensate for the short distance, the wall is unusually high, at 37 feet (see \url{http://modsimpy.com/wally}).64646465\index{Ramirez, Manny}6466\index{Fenway Park}6467\index{baseball}6468\index{Green Monster}6469\index{velocity}64706471We want to find the minimum velocity at which a ball can leave home plate and still go over the Green Monster. We'll proceed in the following steps:64726473\begin{enumerate}64746475\item For a given velocity, we'll find the optimal {\bf launch angle}, that is, the angle the ball should leave home plate to maximize its height when it reaches the wall.64766477\index{launch angle}64786479\item Then we'll find the minimal velocity that clears the wall, given that it has the optimal launch angle.64806481\end{enumerate}64826483We'll use the same model as in the previous chapter, with this \py{Params} object:64846485\begin{python}6486t_end = 20 * s6487dt = t_end / 10064886489params = Params(x = 0 * m,6490y = 1 * m,6491g = 9.8 * m/s**2,6492mass = 145e-3 * kg,6493diameter = 73e-3 * m,6494rho = 1.2 * kg/m**3,6495C_d = 0.3,6496angle = 45 * degree,6497velocity = 40 * m / s,6498t_end=t_end,6499dt=dt)6500\end{python}65016502\index{Params object}65036504This version of \py{make_system}:65056506\begin{python}6507def make_system(params):6508angle, velocity = params.angle, params.velocity65096510# convert angle to degrees6511theta = np.deg2rad(angle)65126513# compute x and y components of velocity6514vx, vy = pol2cart(theta, velocity)65156516# make the initial state6517R = Vector(params.x, params.y)6518V = Vector(vx, vy)6519init = State(R=R, V=V)65206521# compute area from diameter6522diameter = params.diameter6523area = np.pi * (diameter/2)**265246525return System(params, init=init, area=area)6526\end{python}65276528This slope function:65296530\begin{python}6531def slope_func(state, t, system):6532R, V = state6533mass, g = system.mass, system.g65346535a_drag = drag_force(V, system) / mass6536a_grav = Vector(0, -g)65376538A = a_grav + a_drag65396540return V, A6541\end{python}65426543And this event function:65446545\begin{python}6546def event_func(state, t, system):6547R, V = state6548return R.y6549\end{python}655065516552\section{Finding the range}65536554Suppose we want to find the launch angle that maximizes {\bf range}, that is, the distance the ball travels in the air before landing. We'll use a function in the ModSim library, \py{maximize}, which takes a function and finds its maximum.65556556The function we pass to \py{maximize} should take launch angle and a \py{params} object, and return range:65576558\begin{python}6559def range_func(angle, params):6560params = Params(params, angle=angle)6561system = make_system(params)6562results, details = run_ode_solver(system, slope_func,6563events=event_func)6564x_dist = get_last_value(results.R).x6565print(angle, x_dist)6566return x_dist6567\end{python}65686569\py{range_func} makes a new \py{Params} object with the given value of \py{angle}. Then it makes a \py{System} object, calls \py{run_ode_solver}, and returns the final value of \py{x} from the results.65706571We can call \py{range_func} directly like this:65726573\begin{python}6574range_func(45, params)6575\end{python}65766577And we can sweep a sequence of angles like this:65786579\index{parameter sweep}6580\index{SweepSeries object}65816582\begin{python}6583angles = linspace(20, 80, 21)6584sweep = SweepSeries()65856586for angle in angles:6587x_dist = range_func(angle, params)6588print(angle, x_dist)6589sweep[angle] = x_dist6590\end{python}65916592\begin{figure}6593\centerline{\includegraphics[height=3in]{figs/chap23-fig01.pdf}}6594\caption{Distance from home plate as a function of launch angle, with fixed velocity.}6595\label{chap23-fig01}6596\end{figure}65976598Figure~\ref{chap23-fig01} shows the results. It looks like the optimal angle is between \SI{40}{\degree} and \SI{45}{\degree}.65996600%TODO: Draw a vertical line to show this66016602We can find the optimal angle more precisely and more efficiently using \py{maximize}, like this:66036604\begin{python}6605res = maximize(range_func, [0, 90], params)6606\end{python}66076608The first parameter is the function we want to maximize. The second is the range of values we want to search; in this case it's the range of angles from \SI{0}{\degree} to \SI{90}{\degree}. The third argument can be any object; it gets passed along as an argument when \py{maximize} calls \py{range_func}.66096610The return value from \py{maximize} is an \py{ModSimSeries} that contains the results, including \py{x}, which is the angle that yielded the highest range, and \py{fun}, which is the value of \py{range_func} when it's evaluated at \py{x}, that is, range when the baseball is launched at the optimal angle.66116612\index{ModSimSeries}66136614For these parameters, the optimal angle is about \SI{42}{\degree}, which yields a range of \SI{103}{\meter}.66156616\py{maximize} uses a golden section search, which you can read about at \url{http://modsimpy.com/minimize}).66176618% TODO: update this link to point to Golden section search661966206621\section{Finishing off the problem}66226623In the notebook for this chapter, \py{chap22.ipynb}, you'll have to chance to finish off the Manny Ramirez problem. There are a few things you'll have to do:66246625\begin{itemize}66266627\item In the previous section the ``optimal" launch angle is the one that maximizes range, but that's not what we want. Rather, we want the angle that maximizes the height of the ball when it gets to the wall (310 feet from home plate). So you'll have to write a height function to compute it, and then use \py{maximize} to find the revised optimum.66286629\item Once you can find the optimal angle for any velocity, you have to find the minimum velocity that gets the ball over the wall. You'll write a function that takes a velocity as a parameter, computes the optimal angle for that velocity, and returns the height of the ball, at the wall, using the optimal angle.66306631\item Finally, you'll use \py{root_bisect} to find the velocity that makes the optimal height at the wall just barely 37 feet.66326633\index{\py{root_bisect}}66346635\end{itemize}66366637The notebook provides some additional hints, but at this point you should have everything you need. Good luck!66386639If you enjoy this exercise, you might be interested in this paper: ``How to hit home runs: Optimum baseball bat swing parameters for maximum range trajectories", by Sawicki, Hubbard, and Stronge, at \url{http://modsimpy.com/runs}.664066416642\chapter{Rotation}6643\label{chap24}66446645In this chapter we model systems that involve rotating objects. In general, rotation is complicated: in three dimensions, objects can rotate around three axes; objects are often easier to spin around some axes than others; and they may be stable when spinning around some axes but not others.66466647\index{rotation}66486649If the configuration of an object changes over time, it might become easier or harder to spin, which explains the surprising dynamics of gymnasts, divers, ice skaters, etc.66506651And when you apply a twisting force to a rotating object, the effect is often contrary to intuition. For an example, see this video on gyroscopic precession \url{http://modsimpy.com/precess}.66526653\index{gyroscopic precession}66546655In this chapter, we will not take on the physics of rotation in all its glory. Rather, we will focus on simple scenarios where all rotation and all twisting forces are around a single axis. In that case, we can treat some vector quantities as if they were scalars (in the same way that we sometimes treat velocity as a scalar with an implicit direction).66566657\index{scalar}66586659This approach makes it possible to simulate and analyze many interesting systems, but you will also encounter systems that would be better approached with the more general toolkit.66606661The fundamental ideas in this chapter and the next are {\bf angular velocity}, {\bf angular acceleration}, {\bf torque}, and {\bf moment of inertia}. If you are not already familiar with these concepts, I will define them as we go along, and I will point to additional reading.66626663At the end of the next chapter, you will use these tools to simulate the behavior of a yo-yo (see \url{http://modsimpy.com/yoyo}). But we'll work our way up to it gradually, starting with toilet paper.6664666566666667\section{The physics of toilet paper}6668\label{paper}66696670As a simple example of a system with rotation, we'll simulate the manufacture of a roll of toilet paper. Starting with a cardboard tube at the center, we will roll up \SI{47}{\meter} of paper, the typical length of a roll of toilet paper in the U.S. (see \url{http://modsimpy.com/paper}).66716672\index{toilet paper}66736674\begin{figure}6675\centerline{\includegraphics[height=2.5in]{figs/paper_roll.pdf}}6676\caption{Diagram of a roll of toilet paper, showing change in paper length as a result of a small rotation, $d\theta$.}6677\label{paper_roll}6678\end{figure}66796680Figure~\ref{paper_roll} shows a diagram of the system: $r$ represents the radius of the roll at a point in time. Initially, $r$ is the radius of the cardboard core, $R_{min}$. When the roll is complete, $r$ is $R_{max}$.66816682I'll use $\theta$ to represent the total rotation of the roll in radians. In the diagram, $d\theta$ represents a small increase in $\theta$, which corresponds to a distance along the circumference of the roll of $r~d\theta$.66836684\index{radian}66856686Finally, I'll use $y$ to represent the total length of paper that's been rolled. Initially, $\theta=0$ and $y=0$. For each small increase in $\theta$, there is a corresponding increase in $y$:6687%6688\[ dy = r~d\theta \]6689%6690If we divide both sides by a small increase in time, $dt$, we get a differential equation for $y$ as a function of time.6691%6692\[ \frac{dy}{dt} = r \frac{d\theta}{dt} \]6693%6694As we roll up the paper, $r$ increases, too. Assuming that $r$ increases by a fixed amount per revolution, we can write6695%6696\[ dr = k~d\theta \]6697%6698Where $k$ is an unknown constant we'll have to figure out. Again, we can divide both sides by $dt$ to get a differential equation in time:6699%6700\[ \frac{dr}{dt} = k \frac{d\theta}{dt} \]6701%6702Finally, let's assume that $\theta$ increases at a constant rate of \SI{10}{\radian\per\second} (about 95 revolutions per minute):6703%6704\[ \frac{d\theta}{dt} = 10 \]6705%6706This rate of change is called an {\bf angular velocity}. Now we have a system of three differential equations we can use to simulate the system.67076708\index{angular velocity}6709\index{differential equation}671067116712\section{Implementation}6713\label{papersim}67146715At this point we have a pretty standard process for writing simulations like this. First, we'll get the units we need from Pint:6716\index{Pint}67176718\begin{python}6719radian = UNITS.radian6720m = UNITS.meter6721s = UNITS.second6722\end{python}67236724And create a \py{Params} object with the parameters of the system:67256726\index{Params object}67276728\begin{python}6729params = Params(Rmin = 0.02 * m,6730Rmax = 0.055 * m,6731L = 47 * m,6732omega = 10 * radian / s,6733t_end = 130 * s,6734dt = 1*s)6735\end{python}67366737\py{Rmin} and \py{Rmax} are the initial and final values for the radius, \py{r}. \py{L} is the total length of the paper. \py{t_end} is the length of the simulation in time, and \py{dt} is the time step for the ODE solver.67386739We use the \py{Params} object to make a \py{System} object:67406741\index{System object}6742\index{\py{make_system}}67436744\begin{python}6745def make_system(params):6746init = State(theta = 0 * radian,6747y = 0 * m,6748r = params.Rmin)67496750k = estimate_k(params)67516752return System(params, init=init, k=k)6753\end{python}67546755The initial state contains three variables, \py{theta}, \py{y}, and \py{r}.67566757\py{estimate_k} computes the parameter, \py{k}, that relates \py{theta} and \py{r}. Here's how it works:67586759\begin{python}6760def estimate_k(params):6761Rmin, Rmax, L = params.Rmin, params.Rmax, params.L67626763Ravg = (Rmax + Rmin) / 26764Cavg = 2 * pi * Ravg6765revs = L / Cavg6766rads = 2 * pi * revs6767k = (Rmax - Rmin) / rads6768return k6769\end{python}67706771\py{Ravg} is the average radius, half way between \py{Rmin} and \py{Rmax}, so \py{Cavg} is the circumference of the roll when \py{r} is \py{Ravg}.67726773\py{revs} is the total number of revolutions it would take to roll up length \py{L} if \py{r} were constant at \py{Ravg}. And \py{rads} is just \py{revs} converted to radians.67746775Finally, \py{k} is the change in \py{r} for each radian of revolution. For these parameters, \py{k} is about \py{2.8e-5} \si{\meter\per\radian}.67766777Now we can use the differential equations from Section~\ref{paper} to write a slope function:67786779\index{slope function}6780\index{Function!slope}67816782\begin{python}6783def slope_func(state, t, system):6784theta, y, r = state6785k, omega = system.k, system.omega67866787dydt = r * omega6788drdt = k * omega67896790return omega, dydt, drdt6791\end{python}67926793\begin{figure}[t]6794\centerline{\includegraphics[height=4.5in]{figs/chap24-fig01.pdf}}6795\caption{Results from paper rolling simulation, showing rotation, length, and radius over time.}6796\label{chap24-fig01}6797\end{figure}67986799As usual, the slope function takes a \py{State} object, a time, and a \py{System} object. The \py{State} object contains hypothetical values of \py{theta}, \py{y}, and \py{r} at time \py{t}. The job of the slope function is to compute the time derivatives of these values. The derivative of \py{theta} is angular velocity, which is often denoted \py{omega}.68006801\index{State object}68026803We'd like to stop the simulation when the length of paper on the roll is \py{L}. We can do that with an event function that passes through 0 when \py{y} equals \py{L}:68046805\begin{python}6806def event_func(state, t, system):6807theta, y, r = state6808return y - system.L6809\end{python}68106811Now we can run the simulation like this:68126813\begin{python}6814results, details = run_ode_solver(system, slope_func,6815events=event_func)6816\end{python}681768186819Figure~\ref{chap24-fig01} shows the results. \py{theta} grows linearly over time, as we should expect. As a result, \py{r} also grows linearly. But since the derivative of \py{y} depends on \py{r}, and \py{r} is increasing, \py{y} grows with increasing slope.68206821Because this system is so simple, it is almost silly to simulate it. As we'll see in the next section, it is easy enough to solve the differential equations analytically. But it is often useful to start with a simple simulation as a way of exploring and checking assumptions.68226823In order to get the simulation working, we have to get the units right, which can help catch conceptual errors early. And by plugging in realistic parameters, we can detect errors that cause unrealistic results. For example, in this system we can check:68246825\begin{itemize}68266827\item The total time for the simulation is about 2 minutes, which seems plausible for the time it would take to roll \SI{47}{\meter} of paper.68286829\item The final value of \py{theta} is about \SI{1250}{\radian}, which corresponds to about 200 revolutions, which also seems plausible.68306831\item The initial and final values for \py{r} are consistent with \py{Rmin} and \py{Rmax}, as we intended when we chose \py{k}.68326833\end{itemize}68346835But now that we have a working simulation, it is also useful to do some analysis.683668376838\section{Analysis}6839\label{paper_analysis}68406841The differential equations in Section~\ref{paper} are simple enough that we can just solve them. Since angular velocity is constant:6842%6843\[ \frac{d\theta}{dt} = \omega \]6844%6845We can find $\theta$ as a function of time by integrating both sides:6846%6847\[ \theta(t) = \omega t + C_1 \]6848%6849With the initial condition $\theta(0)=0$, we find $C_1=0$. Similarly,6850%6851\begin{equation}6852\frac{dr}{dt} = k \omega \label{eqn1}6853\end{equation}6854%6855So6856%6857\[ r(t) = k \omega t + C_2 \]6858%6859With the initial condition $r(0)=R_{min}$, we find $C_2=R_{min}$. Then we can plug the solution for $r$ into the equation for $y$:6860%6861\begin{align}6862\frac{dy}{dt} & = r \omega \label{eqn2} \\6863& = \left[ k \omega t + R_{min} \right] \omega \nonumber6864\end{align}6865%6866%6867Integrating both sides yields:6868%6869\[ y(t) = \left[ k \omega t^2 / 2 + R_{min} t \right] \omega + C_3\]6870%6871So $y$ is a parabola, as you might have guessed. With initial condition $y(0)=0$, we find $C_3=0$.68726873\index{analysis}6874\index{integration}68756876We can also use these equations to find the relationship between $y$ and $r$, independent of time, which we can use to compute $k$. Using a move we saw in Section~\ref{contact}, I'll divide Equations~\ref{eqn1} and \ref{eqn2}, yielding6877%6878\[ \frac{dr}{dy} = \frac{k}{r}\]6879%6880Separating variables yields6881%6882\[ r~dr = k~dy\]6883%6884Integrating both sides yields6885%6886\[ r^2 / 2 = k y + C \]6887%6888When $y=0$, $r=R_{min}$, so6889%6890\[ C = \frac{1}{2} R_{min}^2 \]6891%6892Solving for $y$, we have6893%6894\begin{equation}6895y = \frac{1}{2k} (r^2 - R_{min}^2) \label{eqn3}6896\end{equation}6897%6898When $y=L$, $r=R_{max}$; substituting in those values yields6899%6900\[ L = \frac{1}{2k} (R_{max}^2 - R_{min}^2) \]6901%6902Solving for $k$ yields6903%6904\begin{equation}6905k = \frac{1}{2L} (R_{max}^2 - R_{min}^2) \label{eqn4}6906\end{equation}6907%6908Plugging in the values of the parameters yields \py{2.8e-5} \si{\meter\per\radian}, the same as the ``estimate" we computed in Section~\ref{papersim}. In this case the estimate turns out to be exact.69096910Before you go on, you might want to read the notebook for this chapter, \py{chap24.ipynb}, and work on the exercises. For instructions on downloading and running the code, see Section~\ref{code}.691169126913\chapter{Torque}6914\label{chap25}69156916In the previous chapter we modeled a scenario with constant angular velocity. In this chapter we make it more complex; we'll model a teapot, on a turntable, revolving with constant angular acceleration and deceleration.69176918\section{Angular acceleration}69196920\index{angular acceleration}6921\index{torque}69226923Just as linear acceleration is the derivative of velocity, {\bf angular acceleration} is the derivative of angular velocity. And just as linear acceleration is caused by force, angular acceleration is caused by the rotational version of force, {\bf torque}. If you are not familiar with torque, you can read about it at \url{http://modsimpy.com/torque}.69246925In general, torque is a vector quantity, defined as the {\bf cross product} of $\vec{r}$ and $\vec{F}$, where $\vec{r}$ is the {\bf lever arm}, a vector from the point of rotation to the point where the force is applied, and $\vec{F}$ is the vector that represents the magnitude and direction of the force.69266927\index{vector}6928\index{lever arm}6929\index{cross product}69306931However, for the problems in this chapter, we only need the {\em magnitude} of torque; we don't care about the direction. In that case, we can compute6932%6933\[ \tau = r F \sin \theta \]6934%6935where $\tau$ is torque, $r$ is the length of the lever arm, $F$ is the magnitude of force, and $\theta$ is the angle between $\vec{r}$ and $\vec{F}$.69366937\index{magnitude}69386939Since torque is the product of a length and a force, it is expressed in newton meters (\si{\newton\meter}).694069416942\section{Moment of inertia}69436944In the same way that linear acceleration is related to force by Newton's second law of motion, $F=ma$, angular acceleration is related to torque by another form of Newton's law:6945%6946\[ \tau = I \alpha \]6947%6948Where $\alpha$ is angular acceleration and $I$ is {\bf moment of inertia}. Just as mass is what makes it hard to accelerate an object\footnote{That might sound like a dumb way to describe mass, but its actually one of the fundamental definitions.}, moment of inertia is what makes it hard to spin an object.69496950\index{mass}6951\index{moment of inertia}69526953In the most general case, a 3-D object rotating around an arbitrary axis, moment of inertia is a tensor, which is a function that takes a vector as a parameter and returns a vector as a result.69546955\index{tensor}69566957Fortunately, in a system where all rotation and torque happens around a single axis, we don't have to deal with the most general case. We can treat moment of inertia as a scalar quantity.69586959\index{scalar}69606961For a small object with mass $m$, rotating around a point at distance $r$, the moment of inertia is $I = m r^2$. For more complex objects, we can compute $I$ by dividing the object into small masses, computing moments of inertia for each mass, and adding them up.69626963However, for most simple shapes, people have already done the calculations; you can just look up the answers. For example, see \url{http://modsimpy.com/moment}.696469656966\section{Teapots and turntables}69676968Tables in Chinese restaurants often have a rotating tray or turntable6969that makes it easy for customers to share dishes. These turntables are6970supported by low-friction bearings that allow them to turn easily and6971glide. However, they can be heavy, especially when they are loaded with6972food, so they have a high moment of inertia.69736974\index{teapot}6975\index{turntable}69766977Suppose I am sitting at a table with a pot of tea on the turntable6978directly in front of me, and the person sitting directly opposite asks6979me to pass the tea. I push on the edge of the turntable with \SI{1}{\newton} of force until it has turned \SI{0.5}{\radian}, then let go. The turntable glides until it comes to a stop \SI{1.5}{\radian} from the starting position. How much force should I apply for a second push so the teapot glides to a6980stop directly opposite me?69816982\index{force}6983\index{Newton}6984\index{friction}69856986We'll answer this question in these steps:69876988\begin{enumerate}69896990\item6991I'll use the results from the first push to estimate the coefficient6992of friction for the turntable.69936994\item6995As an exercise, you'll use that coefficient of friction to estimate the force needed to rotate the turntable through the remaining angle.69966997\end{enumerate}69986999Our simulation will use the following parameters:70007001\begin{enumerate}70027003\item7004The radius of the turntable is \SI{0.5}{\meter}, and its weight is \SI{7}{\kg}.70057006\item7007The teapot weights \SI{0.3}{\kg}, and it sits \SI{0.4}{\meter} from the center of the turntable.70087009\end{enumerate}70107011\begin{figure}7012\centerline{\includegraphics[height=2.5in]{figs/teapot.pdf}}7013\caption{Diagram of a turntable with a teapot.}7014\label{teapot}7015\end{figure}70167017Figure~\ref{teapot} shows the scenario, where $F$ is the force I apply to the turntable at the perimeter, perpendicular to the moment arm, $r$, and $\tau$ is the resulting torque. The blue circle near the bottom is the teapot.70187019Here's a \py{Params} object with the parameters of the scenario:70207021\begin{python}7022params = Params(radius_disk=0.5*m,7023mass_disk=7*kg,7024radius_pot=0.4*m,7025mass_pot=0.3*kg,7026force=1*N,7027torque_friction=0.2*N*m,7028theta_end=0.5*radian,7029t_end=20*s)7030\end{python}70317032\index{Params object}70337034\py{make_system} creates the initial state, \py{init}, and7035computes the total moment of inertia for the turntable and the teapot.70367037\begin{python}7038def make_system(params):7039mass_disk, mass_pot = params.mass_disk, params.mass_pot7040radius_disk, radius_pot = params.radius_disk, params.radius_pot70417042init = State(theta=0*radian, omega=0*radian/s)70437044I_disk = mass_disk * radius_disk**2 / 27045I_pot = mass_pot * radius_pot**270467047return System(params, init=init, I=I_disk+I_pot)7048\end{python}70497050%\index{make_system}70517052In the initial state,7053\py{theta} represents the angle of the table in \si{\radian}; \py{omega} represents the angular velocity in \si{\radian\per\second}.70547055\py{I_disk} is the moment of inertia of the turntable, which is based on the moment of inertia for a horizontal disk revolving around a vertical axis through its center:7056%7057\[ I_{disk} = m r^2 / 2 \]7058%7059\py{I_pot} is the moment of inertia of the teapot, which I treat as a point mass with:7060%7061\[ I_{point} = m r^2 \]7062%7063In SI units, moment of inertia is expressed in \si{\kilogram\meter\squared}.70647065Now we can make a \py{System} object:70667067\begin{python}7068system1 = make_system(params)7069\end{python}70707071\index{System object}70727073Here's a slope that takes the current state, which contains angle and angular velocity, and returns the derivatives, angular velocity and angular acceleration:70747075\begin{python}7076def slope_func(state, t, system):7077theta, omega = state7078radius_disk, force = system.radius_disk, system.force7079torque_friction, I = system.torque_friction, system.I70807081torque = radius_disk * force - torque_friction7082alpha = torque / I70837084return omega, alpha7085\end{python}70867087\index{slope function}70887089In this scenario, the force I apply to the turntable is always perpendicular to the lever arm, so $\sin \theta = 1$ and the torque due to force is $\tau = r F$.70907091\py{torque_friction} represents the torque due to friction. Because the turntable is rotating in the direction of positive \py{theta}, friction acts in the direction of negative \py{theta}.70927093\index{friction}70947095Now we are ready to run the simulation, but first there's a problem we have to address.70967097When I stop pushing on the turntable, the angular acceleration changes7098abruptly. We could implement the slope function with an \py{if}7099statement that checks the value of \py{theta} and sets7100\py{force} accordingly. And for a coarse model like this one, that7101might be fine. But we will get more accurate results if we simulate the7102system in two phases:71037104\begin{enumerate}7105\item7106During the first phase, force is constant, and we run until7107\py{theta} is 0.5 radians.7108\item7109During the second phase, force is 0, and we run until \py{omega}7110is 0.7111\end{enumerate}71127113Then we can combine the results of the two phases into a single7114\py{TimeFrame}.71157116\index{two-phase simulation}71177118Here's the event function I'll use for Phase 1; it stops the simulation when \py{theta} reaches \py{theta_end}, which is when I stop pushing:71197120\begin{python}7121def event_func1(state, t, system):7122theta, omega = state7123return theta - system.theta_end7124\end{python}71257126Now we can run the first phase.71277128\begin{python}7129results1, details1 = run_ode_solver(system1, slope_func,7130events=event_func1)7131\end{python}71327133%\index{run_ode_solver}71347135\begin{figure}7136\centerline{\includegraphics[height=4.0in]{figs/chap25-fig01.pdf}}7137\caption{Angle and angular velocity of a turntable with applied force and friction.}7138\label{chap25-fig01}7139\end{figure}71407141Before we run the second phase, we have to extract the final time and7142state of the first phase.71437144\begin{python}7145t_0 = get_last_label(results1) * s7146init2 = results1.last_row()7147\end{python}71487149Now we can make a \py{System} object for Phase 2, with the initial state from Phase 1, and with \py{force=0}.71507151%\index{get_last_label}7152%\index{get_last_value}71537154\begin{python}7155system2 = System(system1, t_0=t_0, init=init2, force=0*N)7156\end{python}71577158For the second phase, we need an event function that stops when the turntable stops; that is, when angular velocity is 0.71597160\begin{python}7161def event_func2(state, t, system):7162theta, omega = state7163return omega7164\end{python}71657166Now we can run the second phase.71677168\begin{python}7169results2, details2 = run_ode_solver(system2, slope_func,7170events=event_func2)7171\end{python}71727173Pandas provides \py{combine_first}, which combines7174\py{results1} and \py{results2}.71757176\index{Pandas}71777178\begin{python}7179results = results1.combine_first(results2)7180\end{python}71817182Figure~\ref{chap25-fig01} shows the results. Angular velocity, \py{omega}, increases linearly while I am pushing, and decreases linearly after I let go. The angle, \py{theta}, is the integral of angular velocity, so it forms a parabola during each phase.71837184In the next section, we'll use this simulation to estimate the torque due to friction.718571867187\section{Estimating friction}71887189Let's take the code from the previous section and wrap it in a function.71907191\index{function}71927193\begin{python}7194def run_two_phases(force, torque_friction, params):7195# put the parameters into the Params object7196params = Params(params, force=force,7197torque_friction=torque_friction)71987199# run phase 17200system1 = make_system(params)7201results1, details1 = run_ode_solver(system1, slope_func,7202events=event_func1)72037204# get the final state from phase 17205t_0 = results1.last_label() * s7206init2 = results1.last_row()72077208# run phase 27209system2 = System(system1, t_0=t_0, init=init2, force=0*N)7210results2, details2 = run_ode_solver(system2, slope_func,7211events=event_func2)72127213# combine and return the results7214results = results1.combine_first(results2)7215return TimeFrame(results)7216\end{python}72177218We can use \py{run_two_phases} to write an error function we can use, with \py{root_bisect}, to find the torque due to friction that yields the observed results from the first push, a total rotation of \SI{1.5}{\radian}.72197220\index{\py{root_bisect}}7221\index{error function}72227223\begin{python}7224def error_func1(torque_friction, params):7225force = 1 * N7226results = run_two_phases(force, torque_friction, params)7227theta_final = results.last_row().theta7228print(torque_friction, theta_final)7229return theta_final - 1.5 * radian7230\end{python}72317232Now we can use \py{root_bisect} to estimate torque due to friction.72337234\index{torque}7235\index{friction}7236\index{\py{root_bisect}}72377238\begin{python}7239res = root_bisect(error_func1, [0.5, 2], params)7240force = res.root7241\end{python}72427243The result is \SI{0.166}{\newton\meter}, a little less than the initial guess.72447245Now that we know the torque due to friction, we can compute the force needed to rotate the turntable through the remaining angle, that is, from \SI{1.5}{\radian} to \SI{3.14}{\radian}.72467247In the notebook for this chapter, \py{chap25.ipynb}, you will have a chance to finish off the exercise. For instructions on downloading and running the code, see Section~\ref{code}.724872497250\chapter{Case studies}7251\label{chap26}72527253\section{Computational tools}72547255In Chapter~\ref{chap20} we rewrote a second order differential equation as a system of first order equations, and solved them using a slope function like this:72567257\begin{python}7258def slope_func(state, t, system):7259y, v = state7260g = system.g72617262dydt = v7263dvdt = -g72647265return dydt, dvdt7266\end{python}72677268We used the \py{crossings} function to search for zero-crossings in the simulation results.72697270Then we used an event function like this:72717272\begin{python}7273def event_func(state, t, system):7274y, v = state7275return y7276\end{python}72777278To stop the simulation when an event occurs. Notice that the event function takes the same parameters as the slope function.72797280In Chapter~\ref{chap21} we developed a model of air resistance and used a \py{Params} object, which is a collection of parameters:72817282\begin{python}7283params = Params(height = 381 * m,7284v_init = 0 * m / s,7285g = 9.8 * m/s**2,7286mass = 2.5e-3 * kg,7287diameter = 19e-3 * m,7288rho = 1.2 * kg/m**3,7289v_term = 18 * m / s)7290\end{python}72917292And we saw a new way to create a \py{System} object, copying the variables from a \py{Params} object and adding or changing variables:72937294\begin{python}7295return System(params, area=area, C_d=C_d,7296init=init, t_end=t_end)7297\end{python}72987299We also used the \py{gradient} function to estimate acceleration, given velocity:73007301\begin{python}7302a = gradient(results.v)7303\end{python}73047305Chapter~\ref{chap22} introduces \py{Vector} objects, which can represent vector quantities, like position, velocity, force, and acceleration, in 2 or 3 dimensions.73067307\begin{python}7308A = Vector(3, 4) * m7309\end{python}73107311It also introduces trajectory plots, which show the path of an object in two dimensions:73127313\begin{python}7314x = results.R.extract('x')7315y = results.R.extract('y')73167317plot(x, y, label='trajectory')7318\end{python}73197320In Chapter~\ref{chap23} we define a range function that computes the distance a baseball flies as a function of launch angle:73217322\begin{python}7323def range_func(angle, params):7324params = Params(params, angle=angle)7325system = make_system(params)7326results, details = run_ode_solver(system, slope_func,7327events=event_func)7328x_dist = get_last_value(results.R).x7329return x_dist7330\end{python}73317332Then we use \py{maximize} to find the launch angle that maximizes range:73337334\begin{python}7335bounds = [0, 90] * degree7336res = maximize(range_func, bounds, params)7337\end{python}73387339With that, your toolkit is complete. Chapter~\ref{chap24} and Chapter~\ref{chap25} introduce the physics of rotation, but no new computational tools.734073417342\section{Bungee jumping}7343\label{bungee}73447345Suppose 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: \url{http://modsimpy.com/dunk}.73467347Since the record is \SI{70}{\meter}, let's design a jump for \SI{80}{\meter}. We'll start with the following modeling assumptions:73487349\begin{itemize}73507351\item Initially the bungee cord hangs from a crane with the attachment point \SI{80}{\meter} above a cup of tea.73527353\item Until the cord is fully extended, it applies no force to the jumper. It turns out this might not be a good assumption; we will revisit it.73547355\item 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 \url{http://modsimpy.com/hooke}.73567357\item The mass of the jumper is \SI{75}{\kilogram}.73587359\item The jumper is subject to drag force so that their terminal velocity is \SI{60}{\meter \per \second}.73607361\end{itemize}73627363Our objective is to choose the length of the cord, \py{L}, and its spring constant, \py{k}, so that the jumper falls all the way to the tea cup, but no farther!73647365In the repository for this book, you will find a notebook, \py{bungee.ipynb}, which contains starter code and exercises for this case study.736673677368\section{Bungee dunk revisited}73697370In the previous case study, we assume that the cord applies no force to the jumper until it is stretched.7371It is tempting to say that the cord has no effect because it falls along with the jumper, but that intuition is incorrect. As the cord falls, it transfers energy to the jumper.73727373\index{bungee jump}7374\index{bungee cord}73757376At \url{http://modsimpy.com/bungee} you'll find a paper\footnote{Heck, Uylings, and Kędzierska, ``Understanding the physics of bungee jumping", Physics Education, Volume 45, Number 1, 2010.} that explains this phenomenon and derives the acceleration of the jumper, $a$, as a function of position, $y$, and velocity, $v$:7377%7378\[ a = g + \frac{\mu v^2/2}{\mu(L+y) + 2L} \]7379%7380where $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$.73817382If you don't believe that their model is correct, this video might convince you: \url{http://modsimpy.com/drop}.73837384In the repository for this book, you will find a notebook, \py{bungee2.ipynb}, which contains starter code and exercises for this case study.7385How does the behavior of the system change as we vary the mass of the cord?7386When the mass of the cord equals the mass of the jumper, what is the net effect on the lowest point in the jump?7387738873897390\section{Spider-Man}73917392In this case study we'll develop a model of Spider-Man swinging from a7393springy cable of webbing attached to the top of the Empire State7394Building. Initially, Spider-Man is at the top of a nearby building, as7395shown in Figure~\ref{spiderman}.73967397\index{Spider-man}7398\index{Empire State Building}73997400\begin{figure}7401\centerline{\includegraphics[height=2.8in]{figs/spiderman.pdf}}7402\caption{Diagram of the initial state for the Spider-Man case study.}7403\label{spiderman}7404\end{figure}74057406The origin, \texttt{O}, is at the base of the Empire State Building. The7407vector \py{H} represents the position where the webbing is attached7408to the building, relative to \py{O}. The vector \py{P} is the7409position of Spider-Man relative to \py{O}. And \py{L} is the7410vector from the attachment point to Spider-Man.74117412\index{vector}74137414By following the arrows from \py{O}, along \py{H}, and along7415\py{L}, we can see that74167417\begin{code}7418H + L = P7419\end{code}74207421So we can compute \py{L} like this:74227423\begin{code}7424L = P - H7425\end{code}74267427The goals of this case study are:74287429\begin{enumerate}74307431\item7432Implement a model of this scenario to predict Spider-Man's trajectory.7433\index{trajectory}74347435\item7436Choose the right time for Spider-Man to let go of the webbing in order7437to maximize the distance he travels before landing.7438\index{range}74397440\item7441Choose the best angle for Spider-Man to jump off the building, and let7442go of the webbing, to maximize range.7443\index{optimization}74447445\end{enumerate}74467447We'll use the following parameters:7448\index{parameter}74497450\begin{enumerate}74517452\item According to the Spider-Man Wiki\footnote{\url{http://modsimpy.com/spider}}, Spider-Man weighs \SI{76}{\kg}.74537454\item7455Let's assume his terminal velocity is \SI{60}{\meter\per\second}.7456\index{terminal velocity}74577458\item7459The length of the web is \SI{100}{\meter}.74607461\item7462The initial angle of the web is \SI{45}{\degree} to the left of straight7463down.74647465\item7466The spring constant of the web is \SI{40}{\newton\per\meter} when the cord is stretched, and 0 when it's compressed.74677468\end{enumerate}74697470In the repository for this book, you will find a notebook, \py{spiderman.ipynb}, which contains starter code. Read through the notebook and run the code. It uses \py{minimize}, which is a SciPy function that can search for an optimal set of parameters (as contrasted with \py{minimize_scalar}, which can only search along a single axis).747174727473\section{Kittens}74747475Let's simulate a kitten unrolling toilet paper. As reference material, see this video: \url{http://modsimpy.com/kitten}.74767477\index{kitten}74787479The interactions of the kitten and the paper roll are complex. To keep things simple, let's assume that the kitten pulls down on the free end of the roll with constant force. Also, we will neglect the friction between the roll and the axle.74807481\begin{figure}7482\centerline{\includegraphics[height=2.5in]{figs/kitten.pdf}}7483\caption{Diagram of a roll of toilet paper, showing a force, lever arm, and the resulting torque.}7484\label{kitten}7485\end{figure}74867487Figure~\ref{kitten} shows the paper roll with $r$, $F$, and $\tau$. As a vector quantity, the direction of $\tau$ is into the page, but we only care about its magnitude for now.74887489Here's the \py{Params} object with the parameters we'll need:74907491\index{Params object}74927493\begin{python}7494params = Params(Rmin = 0.02 * m,7495Rmax = 0.055 * m,7496Mcore = 15e-3 * kg,7497Mroll = 215e-3 * kg,7498L = 47 * m,7499tension = 2e-4 * N,7500t_end = 180 * s)7501\end{python}75027503As before, \py{Rmin} is the minimum radius and \py{Rmax} is the maximum. \py{L} is the length of the paper. \py{Mcore} is the mass of the cardboard tube at the center of the roll; \py{Mroll} is the mass of the paper. \py{tension} is the force applied by the kitten, in \si{\newton}. I chose a value that yields plausible results.75047505At \url{http://modsimpy.com/moment} you can find moments of inertia for simple geometric shapes. I'll model the cardboard tube at the center of the roll as a ``thin cylindrical shell", and the paper roll as a ``thick-walled cylindrical tube with open ends".75067507\index{cylinder}75087509The moment of inertia for a thin shell is just $m r^2$, where $m$ is the mass and $r$ is the radius of the shell.75107511For a thick-walled tube the moment of inertia is7512%7513\[ I = \frac{\pi \rho h}{2} (r_2^4 - r_1^4) \]7514%7515where $\rho$ is the density of the material, $h$ is the height of the tube, $r_2$ is the outer diameter, and $r_1$ is the inner diameter.75167517Since the outer diameter changes as the kitten unrolls the paper, we have to compute the moment of inertia, at each point in time, as a function of the current radius, \py{r}. Here's the function that does it:75187519\index{unpack}75207521\begin{python}7522def moment_of_inertia(r, system):7523Mcore, Rmin = system.Mcore, system.Rmin7524rho_h = system.rho_h75257526Icore = Mcore * Rmin**27527Iroll = pi * rho_h / 2 * (r**4 - Rmin**4)7528return Icore + Iroll7529\end{python}75307531\py{rho_h} is the product of density and height, $\rho h$, which is the mass per area. \py{rho_h} is computed in \py{make_system}:75327533\index{density}7534\index{\py{make_system}}75357536\begin{python}7537def make_system(params):7538L, Rmax, Rmin = params.L, params.Rmax, params.Rmin7539Mroll = params.Mroll75407541init = State(theta = 0 * radian,7542omega = 0 * radian/s,7543y = L)75447545area = pi * (Rmax**2 - Rmin**2)7546rho_h = Mroll / area7547k = (Rmax**2 - Rmin**2) / 2 / L / radian75487549return System(params, init=init, area=area,7550rho_h=rho_h, k=k)7551\end{python}75527553\py{make_system} also computes \py{k} using Equation~\ref{eqn4}.75547555In the repository for this book, you will find a notebook, \py{kitten.ipynb}, which contains starter code for this case study. Use it to implement this model and check whether the results seem plausible.755675577558\section{Simulating a yo-yo}75597560Suppose 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.75617562\index{yo-yo}7563\index{torque}7564\index{lever arm}75657566\begin{figure}7567\centerline{\includegraphics[height=2.5in]{figs/yoyo.pdf}}7568\caption{Diagram of a yo-yo showing forces due to gravity and tension in the string, the lever arm of tension, and the resulting torque.}7569\label{yoyo}7570\end{figure}75717572Figure~\ref{yoyo} is a diagram of 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.75737574\index{system of equations}75757576In this model, we can't figure out the linear and angular acceleration independently; we have to solve a system of equations:7577%7578\begin{align*}7579\sum F &= m a \\7580\sum \tau &= I \alpha7581\end{align*}7582%7583where the summations indicate that we are adding up forces and torques.75847585As in the previous examples, linear and angular velocity are related because of the way the string unrolls:7586%7587\[ \frac{dy}{dt} = -r \frac{d \theta}{dt} \]7588%7589In 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.75907591Taking the derivative of both sides yields a similar relationship between linear and angular acceleration:7592%7593\[ \frac{d^2 y}{dt^2} = -r \frac{d^2 \theta}{dt^2} \]7594%7595Which we can write more concisely:7596%7597\[ a = -r \alpha \]7598%7599This 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.76007601\index{rolling}76027603Because of the way we've set up the problem, $y$ actually has two meanings: it represents the length of the rolled string and the height of the yo-yo, which decreases as the yo-yo falls. Similarly, $a$ represents acceleration in the length of the rolled string and the height of the yo-yo.76047605We can compute the acceleration of the yo-yo by adding up the linear forces:7606%7607\[ \sum F = T - mg = ma \]7608%7609Where $T$ is positive because the tension force points up, and $mg$ is negative because gravity points down.76107611Because gravity acts on the center of mass, it creates no torque, so the only torque is due to tension:7612%7613\[ \sum \tau = T r = I \alpha \]7614%7615Positive (upward) tension yields positive (counter-clockwise) angular acceleration.76167617\index{SymPy}76187619Now we have three equations in three unknowns, $T$, $a$, and $\alpha$, with $I$, $m$, $g$, and $r$ as known quantities. It is simple enough to solve these equations by hand, but we can also get SymPy to do it for us:76207621\begin{python}7622T, a, alpha, I, m, g, r = symbols('T a alpha I m g r')7623eq1 = Eq(a, -r * alpha)7624eq2 = Eq(T - m*g, m * a)7625eq3 = Eq(T * r, I * alpha)7626soln = solve([eq1, eq2, eq3], [T, a, alpha])7627\end{python}76287629The results are7630%7631\begin{align*}7632T &= m g I / I^* \\7633a &= -m g r^2 / I^* \\7634\alpha &= m g r / I^* \\7635\end{align*}7636%7637where $I^*$ is the augmented moment of inertia, $I + m r^2$.7638To simulate the system, we don't really need $T$; we can plug $a$ and $\alpha$ directly into the slope function.76397640In the repository for this book, you will find a notebook, \py{yoyo.ipynb}, which contains the derivation of these equations and starter code for this case study. Use it to implement and test this model.764176427643%\section{Rigid pendulum}76447645%\section{LRC circuit}76467647% Pendulum:76487649% Springy pendulum76507651% Stiff problem as k increases76527653% Add drag76547655% Rigid pendulum: solve those constraints76567657% Generalized coordinates765876597660\backmatter7661\printindex76627663%\afterpage{\blankpage}766476657666\end{document}76677668\end{itemize}766976707671\section{Under the hood}76727673Throughout this book, we'll use functions defined in the ModSim library. You don't have to know how they work, but you might be curious. So at the end of some chapters I'll provide additional information. If you are an experienced programmer, you might be interested by the design decisions I made. If you are a beginner, and you feel like you have your hands full already, feel free to skip these sections.76747675\index{modsim}76767677Most of the functions in \py{modsim} are based on other Python libraries; the libraries we have used so far include:76787679\begin{itemize}76807681\item {\bf Pint}, which provides units like meters and seconds, as we saw in Section~\ref{penny}.76827683\item {\bf NumPy}, which provides mathematical operations like \py{sqrt}, which we saw in Section~\ref{computation}.76847685\item {\bf Pandas}, which provides the \py{Series} object, which is the basis of the \py{State} object in Section~\ref{modeling}.76867687\item {\bf Pyplot}, which provides plotting functions, as we saw in Section~\ref{plotting}.76887689\end{itemize}76907691You could use these libraries directly, and when you have more experience, you probably will. But the functions in \py{modsim} are meant to be easier to use; they provide some additional capabilities, including error checking; and by hiding details you don't need to know about, they let you focus on more important things.76927693However, there are drawbacks. One is that it can be hard to understand the error messages. I'll have more to say about this in later chapters, but for now I have a suggestion. When you are getting started, you should practice making errors.76947695\index{debugging}76967697For each new function you learn, you should deliberately make as many mistakes as possible so you can see what happens. When you see what the errors messages are, you will understand what they mean. And that should help later, when you make errors accidentally.76987699770077017702