|
| 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 | + |
| 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 | + |
| 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 | + |
| 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. |
0 commit comments