Skip to content

Commit d6d6773

Browse files
committed
Updated file types to get from Quarto
1 parent 83026fe commit d6d6773

29 files changed

+4588
-10
lines changed
1 Byte
Binary file not shown.

NPIs/FlatteningTheCurve/SIR_ftc_JuMP.html

Lines changed: 931 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
# ‘Flattening the curve’ optimisation on an SIR model using JuMP.jl
2+
3+
4+
Initial version
5+
[here](https://github.com/epirecipes/sir-julia/blob/master/markdown/function_map_ftc_jump/function_map_ftc_jump.md)
6+
by Simon Frost (@sdwfrost)
7+
Current version Sandra Montes (@slmontes), 2025-03-10
8+
9+
## Introduction
10+
11+
This example explores the optimal control of an SIR
12+
(Susceptible-Infected-Recovered) model using a time-varying intervention
13+
that reduces the infection rate, similar to the lockdown example.
14+
15+
The following differential equations also describe this model :
16+
17+
$$
18+
\begin{aligned}
19+
\dfrac{\mathrm{d}S}{\mathrm{dt}} &= -\beta (1 - \upsilon(t)) S I, \\
20+
\dfrac{\mathrm{d}I}{\mathrm{dt}} &= \beta (1 - \upsilon(t)) S I - \gamma I, \\
21+
\dfrac{\mathrm{d}C}{\mathrm{dt}} &= \beta (1 - \upsilon(t)) S I,
22+
\end{aligned}
23+
$$
24+
25+
In this case, the optimal control problem is formulated to minimise the
26+
total intervention cost, measured as the integral of `υ(t)` over time
27+
while ensuring that the number of infected individuals, `I`, stays below
28+
a set threshold `I_max`. This constraint is introduced to achieve the
29+
objective of ‘flattening the curve,’ meaning that the optimal
30+
intervention policy `υ(t)` balances the cost of intervention with the
31+
need to keep the infection spread manageable, ensuring that the number
32+
of infected individuals never exceeds the threshold `I_max`. Again, we
33+
determine the optimal policy numerically using a simple Euler
34+
discretisation and then JuMP.jl with IPOPT to optimise.
35+
36+
## Libraries
37+
38+
``` julia
39+
using OrdinaryDiffEq
40+
using DiffEqCallbacks
41+
using JuMP
42+
using Ipopt
43+
using Plots
44+
using DataInterpolations
45+
using NonlinearSolve;
46+
```
47+
48+
## Functions
49+
50+
ODE system
51+
52+
``` julia
53+
function sir_ode!(du,u,p,t)
54+
(S, I, C) = u
55+
(β, γ, υ) = p
56+
@inbounds begin
57+
du[1] = -β*(1-υ)*S*I
58+
du[2] = β*(1-υ)*S*I - γ*I
59+
du[3] = β*(1-υ)*S*I
60+
end
61+
nothing
62+
end;
63+
```
64+
65+
## Running the model without intervention
66+
67+
Parameters
68+
69+
``` julia
70+
u0 = [0.99, 0.01, 0.0]; #S, I, C (cumulative incidence)
71+
p = [0.5, 0.25, 0]; # β, γ, υ
72+
```
73+
74+
``` julia
75+
t0 = 0.0
76+
tf = 100
77+
dt = 0.1
78+
ts = collect(t0:dt:tf)
79+
alg = Tsit5();
80+
```
81+
82+
Solve using ODEProblem
83+
84+
``` julia
85+
prob1 = ODEProblem(sir_ode!, u0, (t0, tf), p)
86+
sol1 = solve(prob1, alg, saveat=ts);
87+
```
88+
89+
Without control the peak fraction of infected individuals is:
90+
91+
``` julia
92+
peak_value, peak_index = findmax(sol1[2, :])
93+
println("The maximum fraction of infected at a `dt` time is: ", peak_value)
94+
```
95+
96+
The maximum fraction of infected at a `dt` time is: 0.15845528864997238
97+
98+
``` julia
99+
plot(sol1,
100+
xlim=(0, 100),
101+
labels=["S" "I" "C"],
102+
xlabel="Time (days)",
103+
ylabel="Fraction of population")
104+
```
105+
106+
![](SIR_ftc_JuMP_files/figure-commonmark/cell-8-output-1.svg)
107+
108+
## Searching for the optimal intervention constrained by `I_max`
109+
110+
Parameters
111+
112+
``` julia
113+
p2 = copy(p)
114+
p2[3] = 0.5; #Set υ to 0.5
115+
β = p2[1]
116+
γ = p2[2]
117+
υ_max = p2[3]
118+
I_max = 0.1
119+
120+
S0 = u0[1]
121+
I0 = u0[2]
122+
C0 = u0[3]
123+
124+
T = Int(tf/dt)
125+
126+
silent = true;
127+
```
128+
129+
Model setup
130+
131+
``` julia
132+
model = Model(Ipopt.Optimizer)
133+
set_optimizer_attribute(model, "max_iter", 1000)
134+
if !silent
135+
set_optimizer_attribute(model, "output_file", "JuMP_ftc.txt")
136+
set_optimizer_attribute(model, "print_timing_statistics", "yes")
137+
end;
138+
```
139+
140+
Variables:
141+
142+
We declare the number of timesteps, `T`, and vectors of our model
143+
variables, including the intervention level, `ν`, each `T+1` step long.
144+
We also define the total cost of the intervention, `υ_total` as a
145+
variable. From their definition, the variables `S` and `C` are
146+
constrained to values between 0 and 1. While the variables `I` and `υ`
147+
are constrained to values between 0 and their respective maximum value
148+
previously defined.
149+
150+
``` julia
151+
@variable(model, 0 <= S[1:(T+1)] <= 1)
152+
@variable(model, 0 <= I[1:(T+1)] <= I_max)
153+
@variable(model, 0 <= C[1:(T+1)] <= 1)
154+
@variable(model, 0 <= υ[1:(T+1)] <= υ_max)
155+
@variable(model, υ_total);
156+
```
157+
158+
We discretise the SIR model using a simple Euler discretisation:
159+
160+
``` julia
161+
@expressions(model, begin
162+
infection[t in 1:T], (1 - υ[t]) * β * I[t] * dt * S[t] # Linear approximation of infection rate
163+
recovery[t in 1:T], γ * dt * I[t] # Recoveries at each time step
164+
end);
165+
```
166+
167+
We constrain our model so the integral of the intervention is equal to
168+
`υ_total`, assuming that the intervention is piecewise constant during
169+
each time step.
170+
171+
``` julia
172+
@constraints(model, begin
173+
S[1]==S0
174+
I[1]==I0
175+
C[1]==C0
176+
[t=1:T], S[t+1] == S[t] - infection[t]
177+
[t=1:T], I[t+1] == I[t] + infection[t] - recovery[t]
178+
[t=1:T], C[t+1] == C[t] + infection[t]
179+
dt * sum(υ[t] for t in 1:T+1) == υ_total
180+
end);
181+
```
182+
183+
This scenario’s objective is to minimise the total cost of intervention
184+
instead of the cumulative incidence, as in the previous single lockdown
185+
scenario.
186+
187+
``` julia
188+
@objective(model, Min, υ_total);
189+
```
190+
191+
``` julia
192+
if silent
193+
set_silent(model)
194+
end
195+
optimize!(model)
196+
```
197+
198+
``` julia
199+
termination_status(model)
200+
```
201+
202+
LOCALLY_SOLVED::TerminationStatusCode = 4
203+
204+
``` julia
205+
S_opt = value.(S)
206+
I_opt = value.(I)
207+
C_opt = value.(C)
208+
υ_opt = value.(υ);
209+
```
210+
211+
``` julia
212+
plot(ts, S_opt, label="S", xlabel="Time", ylabel="Number", legend=:right, xlim=(0,60))
213+
plot!(ts, I_opt, label="I")
214+
plot!(ts, C_opt, label="C")
215+
plot!(ts, υ_opt, label="Optimized υ")
216+
hline!([I_max], color=:gray, alpha=0.5, label="Threshold I")
217+
hline!([υ_max], color=:orange, alpha=0.5, label="Threshold υ")
218+
```
219+
220+
![](SIR_ftc_JuMP_files/figure-commonmark/cell-18-output-1.svg)
221+
222+
With the optimised intervention, we can confirm that the maximum number
223+
of fraction of infected does not exceed `I_max`:
224+
225+
``` julia
226+
peak_value_opt, peak_index_opt = findmax(I_opt)
227+
println("The maximum fraction of infected at a `dt` time is: ", peak_value_opt)
228+
```
229+
230+
The maximum fraction of infected at a `dt` time is: 0.1000000099370598
231+
232+
Again, we can calculate the effective reproductive number, `Rₜ′` in the
233+
presence of the intervention:
234+
235+
``` julia
236+
Rₜ_opt = β.* S_opt ./γ #Not taking into account the intervention
237+
Rₜ′_opt = Rₜ_opt .* (1 .- υ_opt); #Taking into account the intervention
238+
```
239+
240+
And the time at which `Rₜ==1` using a root-finding approach:
241+
242+
``` julia
243+
Rₜ_interp = CubicSpline(Rₜ_opt,ts)
244+
f(u, p) = [Rₜ_interp(u[1]) - 1.0]
245+
u0 = [(tf-t0)/3]
246+
Rtprob = NonlinearProblem(f, u0)
247+
Rtsol = solve(Rtprob, NewtonRaphson(), abstol = 1e-9).u[1];
248+
```
249+
250+
``` julia
251+
plot(ts, Rₜ_opt, label="Rₜ", xlabel="Time", ylabel="Number", legend=:topright, xlim=(0,60))
252+
plot!(ts, Rₜ′_opt, label="Rₜ optimised")
253+
plot!(ts, υ_opt, label="Optimized υ")
254+
vline!([Rtsol], color=:gray, alpha=0.5, label=false)
255+
hline!([1.0], color=:gray, alpha=0.5, label=false)
256+
```
257+
258+
![](SIR_ftc_JuMP_files/figure-commonmark/cell-22-output-1.svg)
259+
260+
## Discussion
261+
262+
The optimal policy obtained involves a single lockdown that increases
263+
rapidly at or shortly before the infected population reaches its
264+
threshold level. Once this threshold is reached, the intensity of the
265+
lockdown is gradually reduced. We can view the total cost as the area
266+
under the policy curve.
267+
268+
The plot of `Rₜ` over time shows that the intervention targets `Rₜ=1`
269+
(including intervention) at the threshold level of infected individuals
270+
and that the lockdown stops when `Rₜ==1` in the absence of an
271+
intervention, ensuring that the population of infected individuals does
272+
not increase.
273+
274+
Compared to a model designed to minimise the total number of infections,
275+
the current strategy that keeps the infected population below a certain
276+
threshold while minimising intervention costs also leads to a single
277+
intervention period. However, in this case, the strength of the
278+
intervention decreases over time.
279+
280+
It is important to mention that there are significant challenges in
281+
translating this finding into actual intervention policies. Fine-tuning
282+
the intensity of the intervention over time may not be feasible;
283+
instead, a series of staged interventions with varying intensities might
284+
be necessary. The impact of the intervention may be uncertain prior to
285+
implementation, and if the efficacy is lower, it may require initiating
286+
the intervention well in advance before reaching the infected threshold.
287+
Additionally, determining when to stop the intervention requires
288+
knowledge of the effective reproduction number (the ‘R number’) in the
289+
absence of the intervention. This requires reliable estimates of `Rₜ`
290+
and the intensity of the intervention, `υ`. These uncertainties are in
291+
addition to the usual uncertainty in model structure and parameter
292+
values of the underlying model.
24.2 KB
Binary file not shown.

NPIs/FlatteningTheCurve/SIR_ftc_JuMP.qmd

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
---
22
title: '''Flattening the curve'' optimisation on an SIR model using JuMP.jl'
33
jupyter: julia-1.11
4-
format:
4+
format:
55
pdf:
6-
documentclass: scrartcl
7-
papersize: a4paper
6+
mainfont: "Arial"
7+
sansfont: "Open Sans"
8+
monofont: "Andale Mono"
9+
code-overflow: wrap
810
docx: default
11+
html:
12+
code-overflow: wrap
13+
gfm: default
914
---
1015

1116

0 commit comments

Comments
 (0)