diff --git a/examples/optim.jl b/examples/optim.jl index 76434bdb81..372c3133d7 100644 --- a/examples/optim.jl +++ b/examples/optim.jl @@ -11,11 +11,26 @@ # to optimize the parameters of an epidemiological model (SIR). We explain this model # in detail in [SIR model for the spread of COVID-19](@ref). -# For brevity here, we just import - -# ```julia -# include("siroptim.jl") # From the examples directory -# ``` +# For brevity here, we just import it and create a initialization function which +# will be used to optimize the parameters + +using AgentsExampleZoo: sir + +function model_init(x, seed) + C = 3 + sir(; + C = C, + Ns = [50, 50, 50], + max_travel_rate = x[1], + death_rate = x[2], + β_det = [x[3] for _ in 1:C], + β_und = [x[4] for _ in 1:C], + infection_period = x[5], + reinfection_probability = x[6], + detection_time = x[7], + seed = seed + ) +end # which provides us a `model_initiation` helper function to build a SIR model, # and an `agent_step!` function. @@ -28,36 +43,22 @@ # is detected. The function returns an *objective*: this value takes the form one # or more numbers, which the optimiser will attempt to minimize. -# ```julia -# using BlackBoxOptim, Random -# using Statistics: mean -# -# function cost(x) -# model = sir(; -# Ns = [500, 500, 500], -# migration_rate = x[1], -# death_rate = x[2], -# β_det = x[3], -# β_und = x[4], -# infection_period = x[5], -# reinfection_probability = x[6], -# detection_time = x[7], -# ) -# -# infected_fraction(model) = -# count(a.status == :I for a in allagents(model)) / nagents(model) -# -# _, mdf = run!( -# model, -# 50; -# mdata = [infected_fraction], -# when_model = [50], -# replicates = 10, -# ) -# -# return mean(mdf.infected_fraction) -# end -# ``` +using BlackBoxOptim, Random +using Statistics: mean + +infected_fraction(model) = count(a.status == :I for a in allagents(model)) / max(1, nagents(model)) + +function cost(x) + rng = Xoshiro(43) + _, mdf = ensemblerun!( + [model_init(x, rand(rng, 1:1000)) for _ in 1:10], + 50; + mdata = [infected_fraction], + when_model = [50], + init = false + ) + return mean(mdf.infected_fraction) +end # This cost function runs our model 10 times for 50 days, then returns the average number # of infected people. When we pass this function to an optimiser, we will effectively be @@ -65,23 +66,17 @@ # lowest possible number. # We can now test the function cost with some reasonable parameter values. -# ```julia -# Random.seed!(10) -# -# x0 = [ -# 0.2, # migration_rate -# 0.1, # death_rate -# 0.05, # β_det -# 0.3, # β_und -# 10, # infection_period -# 0.1, # reinfection_probability -# 5, # detection_time -# ] -# cost(x0) -# ``` -# ```@raw html -#
0.9059485530546623-# ``` + +x0 = [ + 0.2, # max_travel_rate + 0.1, # death_rate + 0.05, # β_det + 0.3, # β_und + 10, # infection_period + 0.1, # reinfection_probability + 5, # detection_time +] +cost(x0) # With these initial values, 94% of the population is infected after the 50 day period. @@ -92,44 +87,26 @@ # cutoff time in the event that certain parameter sets are unfeasible and cause our model # to never converge to a solution. -# ```julia -# result = bboptimize( -# cost, -# SearchRange = [ -# (0.0, 1.0), -# (0.0, 1.0), -# (0.0, 1.0), -# (0.0, 1.0), -# (7.0, 13.0), -# (0.0, 1.0), -# (2.0, 6.0), -# ], -# NumDimensions = 7, -# MaxTime = 20, -# ) -# best_fitness(result) -# ``` -# ```@raw html -#
0.0-# ``` +result = bboptimize( + cost, + SearchRange = [ + (0.0, 1.0), + (0.0, 1.0), + (0.0, 1.0), + (0.0, 1.0), + (7.0, 13.0), + (0.0, 1.0), + (2.0, 6.0), + ], + MaxTime = 10, +) +best_fitness(result) # With the new parameter values found in `result`, we find that the # fraction of the infected population can be dropped down to 11%. # These values of these parameters are now: -# ```julia -# best_candidate(result) -# ``` -# ```@raw html -#
7-element Array{Float64,1}:
-# 0.1545049978104396
-# 0.886202142470518
-# 0.8258299702140992
-# 0.7411762981538305
-# 9.172098752376595
-# 0.17302035312870545
-# 5.907046385323653
-#
-# ```
+
+best_candidate(result)
# Unfortunately we've not given the optimiser information we probably needed to.
# Notice that the death rate is 96%, with reinfection quite low.
@@ -140,79 +117,40 @@
# Let's modify the cost function to also keep the mortality rate low.
# First, we'll run the model with our new-found parameters:
-# ```julia
-# x = best_candidate(result)
-#
-# Random.seed!(0)
-#
-# model = model_initiation(;
-# Ns = [500, 500, 500],
-# migration_rate = x[1],
-# death_rate = x[2],
-# β_det = x[3],
-# β_und = x[4],
-# infection_period = x[5],
-# reinfection_probability = x[6],
-# detection_time = x[7],
-# )
-#
-# _, data =
-# run!(model, agent_step!, 50; mdata = [nagents], when_model = [50], replicates = 10)
-#
-# mean(data.nagents)
-# ```
-# ```@raw html
-# 2.0-# ``` + +x = best_candidate(result) + +rng = Xoshiro(43) +models = [model_init(x, rand(rng, 1:1000)) for _ in 1:10] +ensemblerun!(models, 50) +mean(nagents.(models)) # About 10% of the population dies with these parameters over our 50 day window. # We can define a multi-objective cost function that minimizes the number of infected and # deaths by returning more than one value in our cost function. -# ```julia -# function cost_multi(x) -# model = model_initiation(; -# Ns = [500, 500, 500], -# migration_rate = x[1], -# death_rate = x[2], -# β_det = x[3], -# β_und = x[4], -# infection_period = x[5], -# reinfection_probability = x[6], -# detection_time = x[7], -# ) -# -# initial_size = nagents(model) -# -# infected_fraction(model) = -# count(a.status == :I for a in allagents(model)) / nagents(model) -# n_fraction(model) = -1.0 * nagents(model) / initial_size -# -# mdata = [infected_fraction, n_fraction] -# _, data = run!( -# model, -# agent_step!, -# 50; -# mdata, -# when_model = [50], -# replicates = 10, -# ) -# -# return mean(data[!, dataname(mdata[1])), mean(data[!, dataname(mdata[2])) -# end -# ``` + +function cost_multi(x) + model = model_init(x, 1) + initial_size = nagents(model) + n_fraction(model) = -1.0 * nagents(model) / initial_size + rng = Xoshiro(43) + mdata = [infected_fraction, n_fraction] + _, data = ensemblerun!( + [model_init(x, rand(rng, 1:1000)) for _ in 1:10], + 50; + mdata, + when_model = [50], + init = false + ) + return mean(data[!, dataname(mdata[1])]), mean(data[!, dataname(mdata[2])]) +end # Notice that our new objective `n_fraction` is negative. It would be simpler to # state we'd like to 'maximise the living population', but the optimiser we're using # here focuses on minimising objectives only, therefore we must 'minimise the number of # agents dying. - -# ```julia -# cost_multi(x0) -# ``` -# ```@raw html -#
(0.9812286689419796, -0.7813333333333333)-# ``` +cost_multi(x0) # The cost of our initial parameter values is high: most of the population (96%) is # infected and 22% die. @@ -220,64 +158,36 @@ # Let's minimize this multi-objective cost function. # There is more than one way to approach such an optimisation. # Again, refer to the BlackBoxOptim.jl documentation for specifics. -# ```julia -# result = bboptimize( -# cost_multi, -# Method = :borg_moea, -# FitnessScheme = ParetoFitnessScheme{2}(is_minimizing = true), -# SearchRange = [ -# (0.0, 1.0), -# (0.0, 1.0), -# (0.0, 1.0), -# (0.0, 1.0), -# (7.0, 13.0), -# (0.0, 1.0), -# (2.0, 6.0), -# ], -# NumDimensions = 7, -# MaxTime = 55, -# ) -# best_fitness(result) -# ``` -# ```@raw html -#
(0.0047011417058428475, -0.9926666666666668)-# ``` - -# ```julia -# best_candidate(result) -# ``` -# ```@raw html -#
7-element Array{Float64,1}:
-# 0.8798741355149663
-# 0.6703698358420607
-# 0.07093587652308599
-# 0.07760264834010584
-# 10.65213641721431
-# 0.9911248984077646
-# 5.869646301829334
-#
-# ```
-
-# These parameters look better: about 0.3% of the population dies and 0.02% are infected:
-
-# The algorithm managed to minimize the number of infected and deaths while still
-# increasing death rate to 42%, reinfection probability to 53%, and migration rates to 33%.
-# The most important change however, was decreasing the transmission rate when individuals
-# are infected and undetected from 30% in our initial calculation, to 0.2%.
-
-# Over a longer period of time than 50 days, that high death rate will take its toll though.
+
+result = bboptimize(
+ cost_multi,
+ Method = :borg_moea,
+ FitnessScheme = ParetoFitnessScheme{2}(is_minimizing = true),
+ SearchRange = [
+ (0.0, 1.0),
+ (0.0, 1.0),
+ (0.0, 1.0),
+ (0.0, 1.0),
+ (7.0, 13.0),
+ (0.0, 1.0),
+ (2.0, 6.0),
+ ],
+ MaxTime = 20,
+)
+best_fitness(result)
+
+best_candidate(result)
+
+# These parameters look better: about 3% of the population dies and 31% are infected:
+
+# Over a longer period of time than 50 days, the high death rate will take its toll though.
# Let's reduce that rate and check the cost.
-# ```julia
-# x = best_candidate(result)
-# x[2] = 0.02
-# cost_multi(x)
-# ```
-# ```@raw html
-# (0.03933333333333333, -1.0)-# ``` +x = best_candidate(result) +x[2] = 0.02 +cost_multi(x) -# The fraction of infected increases to 0.04%. This is an interesting result: +# The fraction of infected decreases to 0.04%. This is an interesting result: # since this virus model is not as deadly, the chances of re-infection increase. # We now have a set of parameters to strive towards in the real world. Insights # such as these assist us to enact countermeasures like social distancing to mitigate