Basics of agent-based modeling: spatial epidemic dynamics with Agents.jl
1 Setting up the environment
using Pkg
Pkg.add(["Agents", "CairoMakie", "DataFrames", "Random", "StatsBase"])using Agents
using CairoMakie
using DataFrames
using Random
using StatsBase: sample, Weights
NoteAgents.jl version
This chapter targets Agents.jl v6.x.
In v6, stepping functions are attached to the model when constructing the StandardABM, using the agent_step! (and optionally model_step!) keyword arguments. After that, step!(model, n) and run!(model, n) advance the model for n steps.
2 Defining the agent
@agent struct Host(GridAgent{2})
status::Symbol
days_infected::Int
end3 Model parameters and initialization
function initialize_sir(;
grid_size = (50, 50),
n_initial_infected = 5,
infection_prob = 0.06,
recovery_time = 14,
seed = 42
)
rng = Random.Xoshiro(seed)
space = GridSpace(grid_size; periodic = false, metric = :chebyshev)
properties = (
infection_prob = infection_prob,
recovery_time = recovery_time,
rng = rng
)
model = StandardABM(
Host, space;
properties = properties,
rng = rng,
scheduler = Schedulers.Randomly(),
agent_step! = sir_step!,
)
for pos in positions(model)
add_agent!(pos, model, :S, 0)
end
initial_infected = sample(
rng,
collect(positions(model)),
n_initial_infected;
replace = false
)
for pos in initial_infected
agent = model[ids_in_position(pos, model)[1]]
agent.status = :I
agent.days_infected = 1
end
return model
end4 The stepping function
In Agents.jl v6.x, the stepping function must have signature
agent_step!(agent, model)and is attached to the model via the agent_step! keyword in the StandardABM constructor.
function sir_step!(agent::Host, model)
if agent.status == :S
for neighbor in nearby_agents(agent, model, 1)
if neighbor.status == :I
if rand(model.rng) < model.infection_prob
agent.status = :I
agent.days_infected = 1
break
end
end
end
elseif agent.status == :I
agent.days_infected += 1
if agent.days_infected >= model.recovery_time
agent.status = :R
end
end
return nothing
end5 Running the model
5.1 Single step
model = initialize_sir(seed = 42)
step!(model, 1)5.2 Running for many steps and collecting data
n_susceptible(agent) = agent.status == :S
n_infected(agent) = agent.status == :I
n_recovered(agent) = agent.status == :R
adata = [
(n_susceptible, count),
(n_infected, count),
(n_recovered, count)
]
model = initialize_sir(seed = 42)
agent_data, _ = run!(model, 200; adata)
first(agent_data, 8)6 Visualizing the epidemic time series
fig = Figure(size = (800, 380))
ax = Axis(
fig[1, 1];
xlabel = "time step",
ylabel = "number of agents",
title = "SIR epidemic dynamics on a 50 × 50 spatial grid"
)
lines!(ax, agent_data.step, agent_data.count_n_susceptible;
color = :steelblue3, linewidth = 2, label = "Susceptible (S)")
lines!(ax, agent_data.step, agent_data.count_n_infected;
color = :firebrick3, linewidth = 2, label = "Infected (I)")
lines!(ax, agent_data.step, agent_data.count_n_recovered;
color = :seagreen, linewidth = 2, label = "Recovered (R)")
axislegend(ax; position = :rc)
fig7 Visualizing the spatial state
model_snap = initialize_sir(seed = 42)
step!(model_snap, 40)
status_color(agent) =
agent.status == :S ? :steelblue3 :
agent.status == :I ? :firebrick3 :
:seagreen
fig_snap, ax_snap, abmobs = abmplot(
model_snap;
agent_color = status_color,
agent_size = 6,
figure = (; size = (520, 520)),
axis = (; title = "Spatial state at step 40")
)
fig_snap7.1 Multi-panel snapshot sequence
snapshots = [5, 20, 40, 80, 120, 200]
fig_seq = Figure(size = (1050, 700))
for (i, t) in enumerate(snapshots)
row = div(i - 1, 3) + 1
col = mod(i - 1, 3) + 1
m = initialize_sir(seed = 42)
step!(m, t)
ax = Axis(fig_seq[row, col];
title = "t = $t",
aspect = DataAspect())
hidedecorations!(ax)
xs = [a.pos[1] for a in allagents(m)]
ys = [a.pos[2] for a in allagents(m)]
cs = [status_color(a) for a in allagents(m)]
scatter!(ax, xs, ys; color = cs, markersize = 4)
end
fig_seq8 Parameter sweeps with paramscan
parameters = Dict(
:infection_prob => [0.02, 0.04, 0.06, 0.10],
:recovery_time => [7, 14, 21],
:seed => [1, 2, 3]
)
scan_data, _ = paramscan(
parameters,
initialize_sir;
n = 200,
adata = [(n_recovered, count)]
)
final_sizes = combine(
groupby(
filter(:step => s -> s == 200, scan_data),
[:infection_prob, :recovery_time]
),
:count_n_recovered => mean => :mean_recovered,
:count_n_recovered => std => :std_recovered
)
final_sizes9 Extending the model: heterogeneous transmission
@agent struct VariableHost(GridAgent{2})
status::Symbol
days_infected::Int
susceptibility::Float64
endfunction variable_sir_step!(agent::VariableHost, model)
if agent.status == :S
for neighbor in nearby_agents(agent, model, 1)
if neighbor.status == :I
if rand(model.rng) <
model.infection_prob * agent.susceptibility
agent.status = :I
agent.days_infected = 1
break
end
end
end
elseif agent.status == :I
agent.days_infected += 1
if agent.days_infected >= model.recovery_time
agent.status = :R
end
end
return nothing
endfunction initialize_variable_sir(;
grid_size = (50, 50),
n_initial_infected = 5,
infection_prob = 0.08,
recovery_time = 14,
susceptibility_mean = 0.8,
susceptibility_sd = 0.2,
seed = 42
)
rng = Random.Xoshiro(seed)
space = GridSpace(grid_size; periodic = false, metric = :chebyshev)
properties = (
infection_prob = infection_prob,
recovery_time = recovery_time,
rng = rng
)
model = StandardABM(
VariableHost, space;
properties = properties,
rng = rng,
scheduler = Schedulers.Randomly(),
agent_step! = variable_sir_step!,
)
for pos in positions(model)
s = clamp(
susceptibility_mean +
susceptibility_sd * randn(rng),
0.0, 1.0
)
add_agent!(pos, model, :S, 0, s)
end
initial_infected = sample(
rng,
collect(positions(model)),
n_initial_infected;
replace = false
)
for pos in initial_infected
agent = model[ids_in_position(pos, model)[1]]
agent.status = :I
agent.days_infected = 1
end
return model
end