Agents.jl Integration

We instantiate an agent-based SIR model based on Agents.jl: SIR model for the spread of COVID-19 make use of a SIR model constructor from an Agents.jl' SIR model for the spread of COVID-19, and then we simulate the model using AlgebraicAgents.jl

SIR Model in Agents.jl

To begin with, we define the Agents.jl model:

# SIR model for the spread of COVID-19
# taken from https://juliadynamics.github.io/Agents.jl/stable/examples/sir/
using AlgebraicAgents
using Agents, Random
using Agents.DataFrames, Agents.Graphs
using Distributions: Poisson, DiscreteNonParametric
using DrWatson: @dict
using Plots

@agent struct PoorSoul(GraphAgent)
    days_infected::Int  ## number of days since is infected
    status::Symbol  ## 1: S, 2: I, 3:R
end

Let's provide the constructors:

function model_initiation(;
                          Ns,
                          migration_rates,
                          β_und,
                          β_det,
                          infection_period = 30,
                          reinfection_probability = 0.05,
                          detection_time = 14,
                          death_rate = 0.02,
                          Is = [zeros(Int, length(Ns) - 1)..., 1],
                          seed = 0)
    rng = MersenneTwister(seed)
    @assert length(Ns)==
            length(Is)==
            length(β_und)==
            length(β_det)==
            size(migration_rates, 1) "length of Ns, Is, and B, and number of rows/columns in migration_rates should be the same "
    @assert size(migration_rates, 1)==size(migration_rates, 2) "migration_rates rates should be a square matrix"

    C = length(Ns)
    # normalize migration_rates
    migration_rates_sum = sum(migration_rates, dims = 2)
    for c in 1:C
        migration_rates[c, :] ./= migration_rates_sum[c]
    end

    properties = @dict(Ns,
                       Is,
                       β_und,
                       β_det,
                       β_det,
                       migration_rates,
                       infection_period,
                       infection_period,
                       reinfection_probability,
                       detection_time,
                       C,
                       death_rate)
    space = GraphSpace(complete_digraph(C))
    model = ABM(PoorSoul, space; properties, rng, model_step! = identity)

    # Add initial individuals
    for city in 1:C, n in 1:Ns[city]
        ind = add_agent!(city, model, 0, :S) ## Susceptible
    end
    # add infected individuals
    for city in 1:C
        inds = ids_in_position(city, model)
        for n in 1:Is[city]
            agent = model[inds[n]]
            agent.status = :I ## Infected
            agent.days_infected = 1
        end
    end
    return model
end

using LinearAlgebra: diagind

function create_params(;
                       C,
                       max_travel_rate,
                       infection_period = 30,
                       reinfection_probability = 0.05,
                       detection_time = 14,
                       death_rate = 0.02,
                       Is = [zeros(Int, C - 1)..., 1],
                       seed = 19)
    Random.seed!(seed)
    Ns = rand(50:5000, C)
    β_und = rand(0.3:0.02:0.6, C)
    β_det = β_und ./ 10

    Random.seed!(seed)
    migration_rates = zeros(C, C)
    for c in 1:C
        for c2 in 1:C
            migration_rates[c, c2] = (Ns[c] + Ns[c2]) / Ns[c]
        end
    end
    maxM = maximum(migration_rates)
    migration_rates = (migration_rates .* max_travel_rate) ./ maxM
    migration_rates[diagind(migration_rates)] .= 1.0

    params = @dict(Ns,
                   β_und,
                   β_det,
                   migration_rates,
                   infection_period,
                   reinfection_probability,
                   detection_time,
                   death_rate,
                   Is)

    return params
end
create_params (generic function with 1 method)

It remains to provide the SIR stepping functions:

function agent_step!(agent, model)
    @get_model model
    extract_agent(model, agent)
    migrate!(agent, model)
    transmit!(agent, model)
    update!(agent, model)
    recover_or_die!(agent, model)
end

function migrate!(agent, model)
    pid = agent.pos
    d = DiscreteNonParametric(1:(model.C), model.migration_rates[pid, :])
    m = rand(model.rng, d)
    if m ≠ pid
        move_agent!(agent, m, model)
    end
end

function transmit!(agent, model)
    agent.status == :S && return
    rate = if agent.days_infected < model.detection_time
        model.β_und[agent.pos]
    else
        model.β_det[agent.pos]
    end

    d = Poisson(rate)
    n = rand(model.rng, d)
    n == 0 && return

    for contactID in ids_in_position(agent, model)
        contact = model[contactID]
        if contact.status == :S ||
           (contact.status == :R && rand(model.rng) ≤ model.reinfection_probability)
            contact.status = :I
            n -= 1
            n == 0 && return
        end
    end
end

update!(agent, model) = agent.status == :I && (agent.days_infected += 1)

function recover_or_die!(agent, model)
    if agent.days_infected ≥ model.infection_period
        if rand(model.rng) ≤ model.death_rate
            @a kill_agent!(agent, model)
        else
            agent.status = :R
            agent.days_infected = 0
        end
    end
end
recover_or_die! (generic function with 1 method)

That's it!

Simulation Using AlgebraicAgents.jl

We instantiate a sample ABM model:

# create a sample agent based model
params = create_params(C = 8, max_travel_rate = 0.01)
abm = model_initiation(; params...)
StandardABM with 28540 agents of type PoorSoul
 agents container: Dict
 space: GraphSpace with 8 positions and 56 edges
 scheduler: fastest
 properties: Is, death_rate, infection_period, β_und, Ns, migration_rates, detection_time, reinfection_probability, β_det, C

Let's specify what data to collect:

infected(x) = count(i == :I for i in x)
recovered(x) = count(i == :R for i in x)
to_collect = [(:status, f) for f in (infected, recovered, length)]
3-element Vector{Tuple{Symbol, Function}}:
 (:status, Main.infected)
 (:status, Main.recovered)
 (:status, length)

We wrap the model as an agent:

m = ABMAgent("sir_model", abm; tspan=(0., 100.), adata=to_collect)
agent sir_model with uuid 30943f02 of type ABMAgent 
   custom properties:
   abm: 
StandardABM with 28540 agents of type PoorSoul
 agents container: Dict
 space: GraphSpace with 8 positions and 56 edges
 scheduler: fastest
 properties: migration_rates, Is, infection_period, __aagent__, detection_time, reinfection_probability, Ns, β_det, death_rate, β_und, C
   df_agents: 
0×0 DataFrame
   df_model: 
0×0 DataFrame
   inner agents: 
    agent 5461 with uuid cf5e1333 of type AAgent 
       custom properties:
       agent: 
Main.PoorSoul(5461, 2, 0, :S)
    agent 23986 with uuid 97381e00 of type AAgent 
       custom properties:
       agent: 
Main.PoorSoul(23986, 7, 0, :S)
    agent 10467 with uuid 1939fbea of type AAgent 
       custom properties:
       agent: 
Main.PoorSoul(10467, 3, 0, :S)
    agent 18565 with uuid 801dea46 of type AAgent 
       custom properties:
       agent: 
Main.PoorSoul(18565, 5, 0, :S)
    agent 26767 with uuid fe91cb9c of type AAgent 
       custom properties:
       agent: 
Main.PoorSoul(26767, 7, 0, :S)
    28535 more agent(s) not shown ...

And we simulate the dynamics:

simulate(m)
agent sir_model with uuid 30943f02 of type ABMAgent 
   custom properties:
   abm: 
StandardABM with 28540 agents of type PoorSoul
 agents container: Dict
 space: GraphSpace with 8 positions and 56 edges
 scheduler: fastest
 properties: migration_rates, Is, infection_period, __aagent__, detection_time, reinfection_probability, Ns, β_det, death_rate, β_und, C
   df_agents: 
0×0 DataFrame
   df_model: 
101×4 DataFrame
 Row │ time   infected_status  recovered_status  length_status
     │ Int64  Int64            Int64             Int64
─────┼─────────────────────────────────────────────────────────
   1 │     0                1                 0          28540
   2 │     1                1                 0          28540
   3 │     2                1                 0          28540
   4 │     3                1                 0          28540
   5 │     4                1                 0          28540
   6 │     5                1                 0          28540
   7 │     6                1                 0          28540
   8 │     7                1                 0          28540
  ⋮  │   ⋮           ⋮                ⋮                ⋮
  95 │    94                1                 0          28540
  96 │    95                1                 0          28540
  97 │    96                1                 0          28540
  98 │    97                1                 0          28540
  99 │    98                1                 0          28540
 100 │    99                1                 0          28540
 101 │   100                1                 0          28540
                                                86 rows omitted
   inner agents: 
    agent 5461 with uuid cf5e1333 of type AAgent 
       custom properties:
       agent: 
Main.PoorSoul(5461, 2, 0, :S)
    agent 23986 with uuid 97381e00 of type AAgent 
       custom properties:
       agent: 
Main.PoorSoul(23986, 7, 0, :S)
    agent 10467 with uuid 1939fbea of type AAgent 
       custom properties:
       agent: 
Main.PoorSoul(10467, 3, 0, :S)
    agent 18565 with uuid 801dea46 of type AAgent 
       custom properties:
       agent: 
Main.PoorSoul(18565, 5, 0, :S)
    agent 26767 with uuid fe91cb9c of type AAgent 
       custom properties:
       agent: 
Main.PoorSoul(26767, 7, 0, :S)
    28535 more agent(s) not shown ...
draw(m)