Event-driven continuous-time multiagent model#
The spatial rock-paper-scissors (RPS) is an ABM with the following rules:
Agents can be any of three “kinds”: Rock, Paper, or Scissors.
Agents live in a 2D periodic grid space allowing only one agent per cell.
When an agent activates, it can do one of three actions:
Attack: choose a random nearby agent and attack it. If the agent loses the RPS game it gets removed.
Move: choose a random nearby position. If it is empty move to it, otherwise swap positions with the agent there.
Reproduce: choose a random empty nearby position (if any exist). Generate there a new agent of the same type.
using Agents
using Random
using LinearAlgebra
using Base64
using Agents.DataFrames
using CairoMakie
CairoMakie.activate!(px_per_unit = 1.0)
function display_mp4(filename)
display("text/html", string("""<video autoplay controls><source src="data:video/x-m4v;base64,""",
Base64.base64encode(open(read, filename)), """" type="video/mp4"></video>"""))
end
display_mp4 (generic function with 1 method)
Rock, Paper, or Scissors (RPS) agent
The agent type could be obtained via kindof(agent)
@multiagent struct RPS(GridAgent{2})
@subagent struct Rock end
@subagent struct Paper end
@subagent struct Scissors end
end
Attack actions
function attack!(agent, model)
# Randomly pick a nearby agent
contender = random_nearby_agent(agent, model)
# do nothing if there isn't anyone nearby
isnothing(contender) && return
# else perform standard rock paper scissors logic
# and remove the contender if you win.
# Remember to compare agents with `kindof` instead of
# `typeof` since we use `@multiagent`
kind = kindof(agent)
kindc = kindof(contender)
if kind === :Rock && kindc === :Scissors
remove_agent!(contender, model)
elseif kind === :Scissors && kindc === :Paper
remove_agent!(contender, model)
elseif kind === :Paper && kindc === :Rock
remove_agent!(contender, model)
end
return
end
attack! (generic function with 1 method)
Move actions
Use move_agent!
and swap_agents!
functions
function move!(agent, model)
rand_pos = random_nearby_position(agent.pos, model)
if isempty(rand_pos, model)
move_agent!(agent, rand_pos, model)
else
occupant_id = id_in_position(rand_pos, model)
occupant = model[occupant_id]
swap_agents!(agent, occupant, model)
end
return
end
move! (generic function with 1 method)
Reproduce actions
Use replicate!
function
function reproduce!(agent, model)
pos = random_nearby_position(agent, model, 1, pos -> isempty(pos, model))
isnothing(pos) && return
# pass target position as a keyword argument
replicate!(agent, model; pos)
return
end
reproduce! (generic function with 1 method)
Defining the propensity (“rate” in Gillespie stochastic simulations) and timing of the events
attack_propensity = 1.0
movement_propensity = 0.5
reproduction_propensity(agent, model) = cos(abmtime(model))^2
reproduction_propensity (generic function with 1 method)
Register events with AgentEvent
structures
attack_event = AgentEvent(action! = attack!, propensity = attack_propensity)
reproduction_event = AgentEvent(action! = reproduce!, propensity = reproduction_propensity)
AgentEvent{typeof(reproduce!), typeof(reproduction_propensity), Nothing, typeof(Agents.exp_propensity)}(reproduce!, reproduction_propensity, nothing, Agents.exp_propensity)
We want a different distribution other than exponential dist. for movement time
function movement_time(agent, model, propensity)
# Make time around 1
t = 0.1 * randn(abmrng(model)) + 1
return clamp(t, 0, Inf)
end
movement_time (generic function with 1 method)
Also the rocks do not move
movement_event = AgentEvent(
action! = move!, propensity = movement_propensity,
kinds = (:Scissors, :Paper), timing = movement_time
)
AgentEvent{typeof(move!), Float64, Tuple{Symbol, Symbol}, typeof(movement_time)}(move!, 0.5, (:Scissors, :Paper), movement_time)
Those are all the events
events = (attack_event, reproduction_event, movement_event)
(AgentEvent{typeof(attack!), Float64, Nothing, typeof(Agents.exp_propensity)}(attack!, 1.0, nothing, Agents.exp_propensity), AgentEvent{typeof(reproduce!), typeof(reproduction_propensity), Nothing, typeof(Agents.exp_propensity)}(reproduce!, reproduction_propensity, nothing, Agents.exp_propensity), AgentEvent{typeof(move!), Float64, Tuple{Symbol, Symbol}, typeof(movement_time)}(move!, 0.5, (:Scissors, :Paper), movement_time))
Model factory function
EventQueueABM
for an event-driven ABM
function initialize_rps(; n = 100, nx = n, ny = n, seed = 42)
space = GridSpaceSingle((nx, ny))
rng = Xoshiro(seed)
model = EventQueueABM(RPS, events, space; rng, warn = false)
for p in positions(model)
# Randomly assign one of the agent
type = rand(abmrng(model), (Rock, Paper, Scissors))
add_agent!(p, type, model)
end
return model
end
initialize_rps (generic function with 1 method)
Create model
model = initialize_rps()
EventQueueABM{GridSpaceSingle{2, true}, RPS, Dict{Int64, RPS}, Nothing, Tuple{AgentEvent{typeof(attack!), Float64, Nothing, typeof(Agents.exp_propensity)}, AgentEvent{typeof(reproduce!), typeof(reproduction_propensity), Nothing, typeof(Agents.exp_propensity)}, AgentEvent{typeof(move!), Float64, Tuple{Symbol, Symbol}, typeof(movement_time)}}, Xoshiro, Vector{Vector{Int64}}, Vector{Vector{Float64}}, Vector{Vector{Int64}}, DataStructures.BinaryHeap{Pair{Tuple{Int64, Int64}, Float64}, Base.Order.By{typeof(last), Base.Order.ForwardOrdering}}}(Dict{Int64, RPS}(4986 => Rock(4986, (86, 50))::RPS, 7329 => Scissors(7329, (29, 74))::RPS, 4700 => Rock(4700, (100, 47))::RPS, 4576 => Scissors(4576, (76, 46))::RPS, 7144 => Scissors(7144, (44, 72))::RPS, 6073 => Rock(6073, (73, 61))::RPS, 2288 => Scissors(2288, (88, 23))::RPS, 1703 => Paper(1703, (3, 18))::RPS, 1956 => Paper(1956, (56, 20))::RPS, 8437 => Paper(8437, (37, 85))::RPS…), GridSpaceSingle with size (100, 100), metric=chebyshev, periodic=true, nothing, Xoshiro(0xa405ec0752c2028c, 0x3d4f218ef98f3afe, 0x91f0bfc460469f65, 0x1c338e1be8e3d11e, 0xc90c4a0730db3f7e), Base.RefValue{Int64}(10000), Base.RefValue{Float64}(0.0), (AgentEvent{typeof(attack!), Float64, Nothing, typeof(Agents.exp_propensity)}(attack!, 1.0, nothing, Agents.exp_propensity), AgentEvent{typeof(reproduce!), typeof(reproduction_propensity), Nothing, typeof(Agents.exp_propensity)}(reproduce!, reproduction_propensity, nothing, Agents.exp_propensity), AgentEvent{typeof(move!), Float64, Tuple{Symbol, Symbol}, typeof(movement_time)}(move!, 0.5, (:Scissors, :Paper), movement_time)), Dict(:Paper => 2, :Rock => 1, :Scissors => 3), [[1, 2], [1, 2, 3], [1, 2, 3]], [[1.0, 1.0], [1.0, 1.0, 0.5], [1.0, 1.0, 0.5]], [[2], [2], [2]], DataStructures.BinaryHeap{Pair{Tuple{Int64, Int64}, Float64}, Base.Order.By{typeof(last), Base.Order.ForwardOrdering}}(Base.Order.By{typeof(last), Base.Order.ForwardOrdering}(last, Base.Order.ForwardOrdering()), [(6080, 1) => 2.9790681020079457e-6, (3987, 1) => 0.0003364593569030146, (6221, 2) => 0.0004887948187761659, (8424, 1) => 0.0006778319693496694, (5035, 1) => 0.0012044922191219774, (7014, 2) => 0.0005771225059120387, (3301, 2) => 0.0011396494447446436, (2505, 1) => 0.0014309955844005063, (5041, 2) => 0.001449028423933035, (5539, 2) => 0.0024520873429744127 … (9990, 1) => 1.0666363792018059, (9992, 2) => 0.7429399475482082, (9993, 1) => 1.9378465659189394, (4997, 1) => 1.7907183401427267, (2496, 1) => 0.5304394096759053, (2499, 1) => 2.7442278746073216, (4998, 3) => 1.0804360033507288, (4999, 1) => 5.80807081086072, (9998, 1) => 0.9433255253597445, (1250, 3) => 1.258669914970242]), true, true)
Have a look at the event queue
abmqueue(model)
DataStructures.BinaryHeap{Pair{Tuple{Int64, Int64}, Float64}, Base.Order.By{typeof(last), Base.Order.ForwardOrdering}}(Base.Order.By{typeof(last), Base.Order.ForwardOrdering}(last, Base.Order.ForwardOrdering()), [(6080, 1) => 2.9790681020079457e-6, (3987, 1) => 0.0003364593569030146, (6221, 2) => 0.0004887948187761659, (8424, 1) => 0.0006778319693496694, (5035, 1) => 0.0012044922191219774, (7014, 2) => 0.0005771225059120387, (3301, 2) => 0.0011396494447446436, (2505, 1) => 0.0014309955844005063, (5041, 2) => 0.001449028423933035, (5539, 2) => 0.0024520873429744127 … (9990, 1) => 1.0666363792018059, (9992, 2) => 0.7429399475482082, (9993, 1) => 1.9378465659189394, (4997, 1) => 1.7907183401427267, (2496, 1) => 0.5304394096759053, (2499, 1) => 2.7442278746073216, (4998, 3) => 1.0804360033507288, (4999, 1) => 5.80807081086072, (9998, 1) => 0.9433255253597445, (1250, 3) => 1.258669914970242])
The time in EventQueueABM
is continuous, so we can pass real-valued time
step!(model, 123.456)
nagents(model)
9760
step! also accepts a terminating condition
function terminate(model, t)
willterm = length(allagents(model)) < 5000
return willterm || (t > 1000.0)
end
model = initialize_rps()
step!(model, terminate)
abmtime(model)
1000.0004228831673
Data collection#
adata: aggregated data to extract information from the execution stats adf: agent data frame
model = initialize_rps()
adata = [(a -> kindof(a) === X, count) for X in allkinds(RPS)]
adf, mdf = run!(model, 100.0; adata, when = 0.5, dt = 0.01)
adf[1:10, :]
Row | time | count_#21_X=Rock | count_#21_X=Paper | count_#21_X=Scissors |
---|---|---|---|---|
Float64 | Int64 | Int64 | Int64 | |
1 | 0.0 | 3293 | 3372 | 3335 |
2 | 0.5 | 3215 | 3251 | 3185 |
3 | 1.0 | 3206 | 3204 | 3049 |
4 | 1.5 | 3151 | 3106 | 2887 |
5 | 2.0 | 3044 | 2989 | 2667 |
6 | 2.51 | 2944 | 2882 | 2406 |
7 | 3.02 | 2942 | 2887 | 2319 |
8 | 3.53 | 3108 | 2985 | 2345 |
9 | 4.04 | 3240 | 3106 | 2360 |
10 | 4.55 | 3241 | 3135 | 2276 |
Visualize population change#
tvec = adf[!, :time] ## time as x axis
populations = adf[:, Not(:time)] ## agents as data
alabels = ["rocks", "papers", "scissors"]
fig = Figure();
ax = Axis(fig[1,1]; xlabel = "time", ylabel = "population")
for (i, l) in enumerate(alabels)
lines!(ax, tvec, populations[!, i]; label = l)
end
axislegend(ax)
fig
Visualize agent distribution#
const colormap = Dict(:Rock => "black", :Scissors => "gray", :Paper => "orange")
agent_color(agent) = colormap[kindof(agent)]
plotkw = (agent_color, agent_marker = :rect, agent_size = 5)
fig, ax, abmobs = abmplot(model; plotkw...)
fig
Animation#
model = initialize_rps()
abmvideo("rps_eventqueue.mp4", model;
dt = 0.5, frames = 300,
title = "Rock Paper Scissors (event based)",
plotkw...,
)
display_mp4("rps_eventqueue.mp4")
This notebook was generated using Literate.jl.