11 lines
19 KiB
JSON
11 lines
19 KiB
JSON
{
|
||
"hash": "38c881eff4d5888426314c6c9c390bcf",
|
||
"result": {
|
||
"markdown": "# The `DifferentialEquations` suite\n\n\n\nThis section uses these add-on packages:\n\n``` {.julia .cell-code}\nusing OrdinaryDiffEq\nusing Plots\nusing ModelingToolkit\n```\n\n\n\n\n---\n\n\nThe [`DifferentialEquations`](https://github.com/SciML/DifferentialEquations.jl) suite of packages contains solvers for a wide range of various differential equations. This section just briefly touches touch on ordinary differential equations (ODEs), and so relies only on `OrdinaryDiffEq` part of the suite. For more detail on this type and many others covered by the suite of packages, there are many other resources, including the [documentation](https://diffeq.sciml.ai/stable/) and accompanying [tutorials](https://github.com/SciML/SciMLTutorials.jl).\n\n\n## SIR Model\n\n\nWe follow along with an introduction to the SIR model for the spread of disease by [Smith and Moore](https://www.maa.org/press/periodicals/loci/joma/the-sir-model-for-spread-of-disease-introduction). This model received a workout due to the COVID-19 pandemic.\n\n\nThe basic model breaks a population into three cohorts: The **susceptible** individuals, the **infected** individuals, and the **recovered** individuals. These add to the population size, $N$, which is fixed, but the cohort sizes vary in time. We name these cohort sizes $S(t)$, $I(t)$, and $R(t)$ and define $s(t)=S(t)/N$, $i(t) = I(t)/N$ and $r(t) = R(t)/N$ to be the respective proportions.\n\n\nThe following *assumptions* are made about these cohorts by Smith and Moore:\n\n\n> No one is added to the susceptible group, since we are ignoring births and immigration. The only way an individual leaves the susceptible group is by becoming infected.\n\n\n\nThis implies the rate of change in time of $S(t)$ depends on the current number of susceptibles, and the amount of interaction with the infected cohorts. The model *assumes* each infected person has $b$ contacts per day that are sufficient to spread the disease. Not all contacts will be with susceptible people, but if people are assumed to mix within the cohorts, then there will be on average $b \\cdot S(t)/N$ contacts with susceptible people per infected person. As each infected person is modeled identically, the time rate of change of $S(t)$ is:\n\n\n\n$$\n\\frac{dS}{dt} = - b \\cdot \\frac{S(t)}{N} \\cdot I(t) = -b \\cdot s(t) \\cdot I(t)\n$$\n\n\nIt is negative, as no one is added, only taken off. After dividing by $N$, this can also be expressed as $s'(t) = -b s(t) i(t)$.\n\n\n> assume that a fixed fraction $k$ of the infected group will recover during any given day.\n\n\n\nThis means the change in time of the recovered depends on $k$ and the number infected, giving rise to the equation\n\n\n\n$$\n\\frac{dR}{dt} = k \\cdot I(t)\n$$\n\n\nwhich can also be expressed in proportions as $r'(t) = k \\cdot i(t)$.\n\n\nFinally, from $S(t) + I(T) + R(t) = N$ we have $S'(T) + I'(t) + R'(t) = 0$ or $s'(t) + i'(t) + r'(t) = 0$.\n\n\nCombining, it is possible to express the rate of change of the infected population through:\n\n\n\n$$\n\\frac{di}{dt} = b \\cdot s(t) \\cdot i(t) - k \\cdot i(t)\n$$\n\n\nThe author's apply this model to flu statistics from Hong Kong where:\n\n\n\n$$\n\\begin{align*}\nS(0) &= 7,900,000\\\\\nI(0) &= 10\\\\\nR(0) &= 0\\\\\n\\end{align*}\n$$\n\n\nIn `Julia` we define these, `N` to model the total population, and `u0` to be the proportions.\n\n::: {.cell execution_count=4}\n``` {.julia .cell-code}\nS0, I0, R0 = 7_900_000, 10, 0\nN = S0 + I0 + R0\nu0 = [S0, I0, R0]/N # initial proportions\n```\n\n::: {.cell-output .cell-output-display execution_count=5}\n```\n3-element Vector{Float64}:\n 0.9999987341788175\n 1.2658211825048323e-6\n 0.0\n```\n:::\n:::\n\n\nAn *estimated* set of values for $k$ and $b$ are $k=1/3$, coming from the average period of infectiousness being estimated at three days and $b=1/2$, which seems low in normal times, but not for an infected person who may be feeling quite ill and staying at home. (The model for COVID would certainly have a larger $b$ value).\n\n\nOkay, the mathematical modeling is done; now we try to solve for the unknown functions using `DifferentialEquations`.\n\n\nTo warm up, if $b=0$ then $i'(t) = -k \\cdot i(t)$ describes the infected. (There is no circulation of people in this case.) The solution would be achieved through:\n\n::: {.cell hold='true' execution_count=5}\n``` {.julia .cell-code}\nk = 1/3\n\nf(u,p,t) = -k * u # solving u′(t) = - k u(t)\ntime_span = (0.0, 20.0)\n\nprob = ODEProblem(f, I0/N, time_span)\nsol = solve(prob, Tsit5(), reltol=1e-8, abstol=1e-8)\n\nplot(sol)\n```\n\n::: {.cell-output .cell-output-display execution_count=6}\n{}\n:::\n:::\n\n\nThe `sol` object is a set of numbers with a convenient `plot` method. As may have been expected, this graph shows exponential decay.\n\n\nA few comments are in order. The problem we want to solve is\n\n\n\n$$\n\\frac{di}{dt} = -k \\cdot i(t) = F(i(t), k, t)\n$$\n\n\nwhere $F$ depends on the current value ($i$), a parameter ($k$), and the time ($t$). We did not utilize $p$ above for the parameter, as it was easy not to, but could have, and will in the following. The time variable $t$ does not appear by itself in our equation, so only `f(u, p, t) = -k * u` was used, `u` the generic name for a solution which in this case is $i$.\n\n\nThe problem we set up needs an initial value (the $u0$) and a time span to solve over. Here we want time to model real time, so use floating point values.\n\n\nThe plot shows steady decay, as there is no mixing of infected with others.\n\n\nAdding in the interaction requires a bit more work. We now have what is known as a *system* of equations:\n\n\n\n$$\n\\begin{align*}\n\\frac{ds}{dt} &= -b \\cdot s(t) \\cdot i(t)\\\\\n\\frac{di}{dt} &= b \\cdot s(t) \\cdot i(t) - k \\cdot i(t)\\\\\n\\frac{dr}{dt} &= k \\cdot i(t)\\\\\n\\end{align*}\n$$\n\n\nSystems of equations can be solved in a similar manner as a single ordinary differential equation, though adjustments are made to accommodate the multiple functions.\n\n\nWe use a style that updates values in place, and note that `u` now holds $3$ different functions at once:\n\n::: {.cell execution_count=6}\n``` {.julia .cell-code}\nfunction sir!(du, u, p, t)\n k, b = p\n s, i, r = u[1], u[2], u[3]\n\n ds = -b * s * i\n di = b * s * i - k * i\n dr = k * i\n\n du[1], du[2], du[3] = ds, di, dr\nend\n```\n\n::: {.cell-output .cell-output-display execution_count=7}\n```\nsir! (generic function with 1 method)\n```\n:::\n:::\n\n\nThe notation `du` is suggestive of both the derivative and a small increment. The mathematical formulation follows the derivative, the numeric solution uses a time step and increments the solution over this time step. The `Tsit5()` solver, used here, adaptively chooses a time step, `dt`; were the `Euler` method used, this time step would need to be explicit.\n\n\n:::{.callout-note}\n## Mutation not re-binding\nThe `sir!` function has the trailing `!` indicating – by convention – it *mutates* its first value, `du`. In this case, through an assignment, as in `du[1]=ds`. This could use some explanation. The *binding* `du` refers to the *container* holding the $3$ values, whereas `du[1]` refers to the first value in that container. So `du[1]=ds` changes the first value, but not the *binding* of `du` to the container. That is, `du` mutates. This would be quite different were the call `du = [ds,di,dr]` which would create a new *binding* to a new container and not mutate the values in the original container.\n\n:::\n\nWith the update function defined, the problem is setup and a solution found with in the same manner:\n\n::: {.cell execution_count=7}\n``` {.julia .cell-code}\np = (k=1/3, b=1/2) # parameters\ntime_span = (0.0, 150.0) # time span to solve over, 5 months\n\nprob = ODEProblem(sir!, u0, time_span, p)\nsol = solve(prob, Tsit5())\n\nplot(sol)\nplot!(x -> 0.5, linewidth=2) # mark 50% line\n```\n\n::: {.cell-output .cell-output-display execution_count=8}\n{}\n:::\n:::\n\n\nThe lower graph shows the number of infected at each day over the five-month period displayed. The peak is around 6-7% of the population at any one time. However, over time the recovered part of the population reaches over 50%, meaning more than half the population is modeled as getting sick.\n\n\nNow we change the parameter $b$ and observe the difference. We passed in a value `p` holding our two parameters, so we just need to change that and run the model again:\n\n::: {.cell hold='true' execution_count=8}\n``` {.julia .cell-code}\np = (k=1/2, b=2) # change b from 1/2 to 2 -- more daily contact\nprob = ODEProblem(sir!, u0, time_span, p)\nsol = solve(prob, Tsit5())\n\nplot(sol)\n```\n\n::: {.cell-output .cell-output-display execution_count=9}\n{}\n:::\n:::\n\n\nThe graphs are somewhat similar, but the steady state is reached much more quickly and nearly everyone became infected.\n\n\nWhat about if $k$ were bigger?\n\n::: {.cell hold='true' execution_count=9}\n``` {.julia .cell-code}\np = (k=2/3, b=1/2)\nprob = ODEProblem(sir!, u0, time_span, p)\nsol = solve(prob, Tsit5())\n\nplot(sol)\n```\n\n::: {.cell-output .cell-output-display execution_count=10}\n{}\n:::\n:::\n\n\nThe graphs show that under these conditions the infections never take off; we have $i' = (b\\cdot s-k)i = k\\cdot((b/k) s - 1) i$ which is always negative, since `(b/k)s < 1`, so infections will only decay.\n\n\nThe solution object is indexed by time, then has the `s`, `i`, `r` estimates. We use this structure below to return the estimated proportion of recovered individuals at the end of the time span.\n\n::: {.cell execution_count=10}\n``` {.julia .cell-code}\nfunction recovered(k,b)\n prob = ODEProblem(sir!, u0, time_span, (k,b));\n sol = solve(prob, Tsit5());\n s,i,r = last(sol)\n r\nend\n```\n\n::: {.cell-output .cell-output-display execution_count=11}\n```\nrecovered (generic function with 1 method)\n```\n:::\n:::\n\n\nThis function makes it easy to see the impact of changing the parameters. For example, fixing $k=1/3$ we have:\n\n::: {.cell execution_count=11}\n``` {.julia .cell-code}\nf(b) = recovered(1/3, b)\nplot(f, 0, 2)\n```\n\n::: {.cell-output .cell-output-display execution_count=12}\n{}\n:::\n:::\n\n\nThis very clearly shows the sharp dependence on the value of $b$; below some level, the proportion of people who are ever infected (the recovered cohort) remains near $0$; above that level it can climb quickly towards $1$.\n\n\nThe function `recovered` is of two variables returning a single value. In subsequent sections we will see a few $3$-dimensional plots that are common for such functions, here we skip ahead and show how to visualize multiple function plots at once using \"`z`\" values in a graph.\n\n::: {.cell hold='true' execution_count=12}\n``` {.julia .cell-code}\nk, ks = 0.1, 0.2:0.1:0.9 # first `k` and then the rest\nbs = range(0, 2, length=100)\nzs = recovered.(k, bs) # find values for fixed k, each of bs\np = plot(bs, k*one.(bs), zs, legend=false) # k*one.(ks) is [k,k,...,k]\nfor k in ks\n plot!(p, bs, k*one.(bs), recovered.(k, bs))\nend\np\n```\n\n::: {.cell-output .cell-output-display execution_count=13}\n{}\n:::\n:::\n\n\nThe 3-dimensional graph with `plotly` can have its viewing angle adjusted with the mouse. When looking down on the $x-y$ plane, which code `b` and `k`, we can see the rapid growth along a line related to $b/k$.\n\n\nSmith and Moore point out that $k$ is roughly the reciprocal of the number of days an individual is sick enough to infect others. This can be estimated during a breakout. However, they go on to note that there is no direct way to observe $b$, but there is an indirect way.\n\n\nThe ratio $c = b/k$ is the number of close contacts per day times the number of days infected which is the number of close contacts per infected individual.\n\n\nThis can be estimated from the curves once steady state has been reached (at the end of the pandemic).\n\n\n\n$$\n\\frac{di}{ds} = \\frac{di/dt}{ds/dt} = \\frac{b \\cdot s(t) \\cdot i(t) - k \\cdot i(t)}{-b \\cdot s(t) \\cdot i(t)} = -1 + \\frac{1}{c \\cdot s}\n$$\n\n\nThis equation does not depend on $t$; $s$ is the dependent variable. It could be solved numerically, but in this case affords an algebraic solution: $i = -s + (1/c) \\log(s) + q$, where $q$ is some constant. The quantity $q = i + s - (1/c) \\log(s)$ does not depend on time, so is the same at time $t=0$ as it is as $t \\rightarrow \\infty$. At $t=0$ we have $s(0) \\approx 1$ and $i(0) \\approx 0$, whereas $t \\rightarrow \\infty$, $i(t) \\rightarrow 0$ and $s(t)$ goes to the steady state value, which can be estimated. Solving with $t=0$, we see $q=0 + 1 - (1/c)\\log(1) = 1$. In the limit them $1 = 0 + s_{\\infty} - (1/c)\\log(s_\\infty)$ or $c = \\log(s_\\infty)/(1-s_\\infty)$.\n\n\n## Trajectory with drag\n\n\nWe now solve numerically the problem of a trajectory with a drag force from air resistance.\n\n\nThe general model is:\n\n\n\n$$\n\\begin{align*}\nx''(t) &= - W(t,x(t), x'(t), y(t), y'(t)) \\cdot x'(t)\\\\\ny''(t) &= -g - W(t,x(t), x'(t), y(t), y'(t)) \\cdot y'(t)\\\\\n\\end{align*}\n$$\n\n\nwith initial conditions: $x(0) = y(0) = 0$ and $x'(0) = v_0 \\cos(\\theta), y'(0) = v_0 \\sin(\\theta)$.\n\n\nThis is turned into an ODE by a standard trick. Here we define our function for updating a step. As can be seen the vector `u` contains both $\\langle x,y \\rangle$ and $\\langle x',y' \\rangle$\n\n::: {.cell execution_count=13}\n``` {.julia .cell-code}\nfunction xy!(du, u, p, t)\n\tg, γ = p.g, p.k\n\tx, y = u[1], u[2]\n\tx′, y′ = u[3], u[4] # unicode \\prime[tab]\n\n W = γ\n\n\tdu[1] = x′\n\tdu[2] = y′\n\tdu[3] = 0 - W * x′\n\tdu[4] = -g - W * y′\nend\n```\n\n::: {.cell-output .cell-output-display execution_count=14}\n```\nxy! (generic function with 1 method)\n```\n:::\n:::\n\n\nThis function $W$ is just a constant above, but can be easily modified as desired.\n\n\n:::{.callout-note}\n## A second-order ODE is a coupled first-order ODE\nThe \"standard\" trick is to take a second order ODE like $u''(t)=u$ and turn this into two coupled ODEs by using a new name: $v=u'(t)$ and then $v'(t) = u(t)$. In this application, there are $4$ equations, as we have *both* $x''$ and $y''$ being so converted. The first and second components of $du$ are new variables, the third and fourth show the original equation.\n\n:::\n\nThe initial conditions are specified through:\n\n::: {.cell execution_count=14}\n``` {.julia .cell-code}\nθ = pi/4\nv₀ = 200\nxy₀ = [0.0, 0.0]\nvxy₀ = v₀ * [cos(θ), sin(θ)]\nINITIAL = vcat(xy₀, vxy₀)\n```\n\n::: {.cell-output .cell-output-display execution_count=15}\n```\n4-element Vector{Float64}:\n 0.0\n 0.0\n 141.4213562373095\n 141.42135623730948\n```\n:::\n:::\n\n\nThe time span can be computed using an *upper* bound of no drag, for which the classic physics formulas give (when $y_0=0$) $(0, 2v_{y0}/g)$\n\n::: {.cell execution_count=15}\n``` {.julia .cell-code}\ng = 9.8\nTSPAN = (0, 2*vxy₀[2] / g)\n```\n\n::: {.cell-output .cell-output-display execution_count=16}\n```\n(0, 28.8615012729203)\n```\n:::\n:::\n\n\nThis allows us to define an `ODEProblem`:\n\n::: {.cell execution_count=16}\n``` {.julia .cell-code}\ntrajectory_problem = ODEProblem(xy!, INITIAL, TSPAN)\n```\n\n::: {.cell-output .cell-output-display execution_count=17}\n\n::: {.ansi-escaped-output}\n```{=html}\n<pre><span class=\"ansi-cyan-fg\">ODEProblem</span> with uType <span class=\"ansi-cyan-fg\">Vector{Float64}</span> and tType <span class=\"ansi-cyan-fg\">Float64</span>. In-place: <span class=\"ansi-cyan-fg\">true</span>\ntimespan: (0.0, 28.8615012729203)\nu0: 4-element Vector{Float64}:\n 0.0\n 0.0\n 141.4213562373095\n 141.42135623730948</pre>\n```\n:::\n\n:::\n:::\n\n\nWhen $\\gamma = 0$ there should be no drag and we expect to see a parabola:\n\n::: {.cell hold='true' execution_count=17}\n``` {.julia .cell-code}\nps = (g=9.8, k=0)\nSOL = solve(trajectory_problem, Tsit5(); p = ps)\n\nplot(t -> SOL(t)[1], t -> SOL(t)[2], TSPAN...; legend=false)\n```\n\n::: {.cell-output .cell-output-display execution_count=18}\n{}\n:::\n:::\n\n\nThe plot is a parametric plot of the $x$ and $y$ parts of the solution over the time span. We can see the expected parabolic shape.\n\n\nOn a *windy* day, the value of $k$ would be positive. Repeating the above with $k=1/4$ gives:\n\n::: {.cell hold='true' execution_count=18}\n``` {.julia .cell-code}\nps = (g=9.8, k=1/4)\nSOL = solve(trajectory_problem, Tsit5(); p = ps)\n\nplot(t -> SOL(t)[1], t -> SOL(t)[2], TSPAN...; legend=false)\n```\n\n::: {.cell-output .cell-output-display execution_count=19}\n{}\n:::\n:::\n\n\nWe see that the $y$ values have gone negative. The `DifferentialEquations` package can adjust for that with a *callback* which terminates the problem once $y$ has gone negative. This can be implemented as follows:\n\n::: {.cell hold='true' execution_count=19}\n``` {.julia .cell-code}\ncondition(u,t,integrator) = u[2] # called when `u[2]` is negative\naffect!(integrator) = terminate!(integrator) # stop the process\ncb = ContinuousCallback(condition, affect!)\n\nps = (g=9.8, k = 1/4)\nSOL = solve(trajectory_problem, Tsit5(); p = ps, callback=cb)\n\nplot(t -> SOL(t)[1], t -> SOL(t)[2], TSPAN...; legend=false)\n```\n\n::: {.cell-output .cell-output-display execution_count=20}\n{}\n:::\n:::\n\n\nFinally, we note that the `ModelingToolkit` package provides symbolic-numeric computing. This allows the equations to be set up symbolically, as in `SymPy` before being passed off to `DifferentialEquations` to solve numerically. The above example with no wind resistance could be translated into the following:\n\n::: {.cell hold='true' execution_count=20}\n``` {.julia .cell-code}\n@parameters t γ g\n@variables x(t) y(t)\nD = Differential(t)\n\neqs = [D(D(x)) ~ -γ * D(x),\n D(D(y)) ~ -g - γ * D(y)]\n\n@named sys = ODESystem(eqs)\nsys = ode_order_lowering(sys) # turn 2nd order into 1st\n\nu0 = [D(x) => vxy₀[1],\n D(y) => vxy₀[2],\n x => 0.0,\n y => 0.0]\n\np = [γ => 0.0,\n g => 9.8]\n\nprob = ODEProblem(sys, u0, TSPAN, p, jac=true)\nsol = solve(prob,Tsit5())\n\nplot(t -> sol(t)[3], t -> sol(t)[4], TSPAN..., legend=false)\n```\n\n::: {.cell-output .cell-output-display execution_count=21}\n{}\n:::\n:::\n\n\nThe toolkit will automatically generate fast functions and can perform transformations (such as is done by `ode_order_lowering`) before passing along to the numeric solves.\n\n",
|
||
"supporting": [
|
||
"differential_equations_files/figure-html"
|
||
],
|
||
"filters": [],
|
||
"includes": {}
|
||
}
|
||
} |