API
The core API is defined by AgentBasedModel
, Space, AbstractAgent
and step!
, which are described in the Tutorial page. The functionality described here builds on top of the core API.
Agent information and retrieval
Agents.space_neighbors
— Functionspace_neighbors(position, model::ABM, r) → ids
Return the ids of the agents neighboring the given position
(which must match type with the spatial structure of the model
). r
is the radius to search for agents.
For DiscreteSpace
r
must be integer and defines higher degree neighbors. For example, for r=2
include first and second degree neighbors, that is, neighbors and neighbors of neighbors. Specifically for GraphSpace
, the keyword neighbor_type
can also be used as in node_neighbors
to restrict search on directed graphs.
For ContinuousSpace
, r
is real number and finds all neighbors within distance r
(based on the space's metric).
space_neighbors(agent::AbstractAgent, model::ABM [, r]) → ids
Call space_neighbors(agent.pos, model, r)
but exclude the given agent
from the neighbors.
Agents.random_agent
— Functionrandom_agent(model)
Return a random agent from the model.
Agents.nagents
— Functionnagents(model::ABM)
Return the number of agents in the model
.
Agents.allagents
— Functionallagents(model)
Return an iterator over all agents of the model.
Agents.allids
— Functionallids(model)
Return an iterator over all agent IDs of the model.
Agents.nextid
— Functionnextid(model::ABM) → id
Return a valid id
for creating a new agent with it.
Model-Agent interaction
The following API is mostly universal across all types of Space. Only some specific methods are exclusive to a specific type of space, but we think this is clear from the documentation strings (if not, please open an issue!).
Agents.add_agent!
— Functionadd_agent!(agent::AbstractAgent [, position], model::ABM) → agent
Add the agent
to the position
in the space and to the list of agents. If position
is not given, the agent
is added to a random position. The agent
's position is always updated to match position
, and therefore for add_agent!
the position of the agent
is meaningless. Use add_agent_pos!
to use the agent
's position.
add_agent!([pos,] model::ABM, args...; kwargs...)
Create and add a new agent to the model by constructing an agent of the type of the model
. Propagate all extra positional arguments and keyword arguemts to the agent constructor.
Notice that this function takes care of setting the agent's id and position and thus args...
and kwargs...
are propagated to other fields the agent has.
Optionally provide a position to add the agent to as first argument.
Example
using Agents
mutable struct Agent <: AbstractAgent
id::Int
pos::Int
w::Float64
k::Bool
end
Agent(id, pos; w, k) = Agent(id, pos, w, k) # keyword constructor
model = ABM(Agent, GraphSpace(complete_digraph(5)))
add_agent!(model, 1, 0.5, true) # incorrect: id/pos is set internally
add_agent!(model, 0.5, true) # correct: w becomes 0.5
add_agent!(5, model, 0.5, true) # add at node 5, w becomes 0.5
add_agent!(model; w = 0.5, k = true) # use keywords: w becomes 0.5
Agents.add_agent_pos!
— Functionadd_agent_pos!(agent::AbstractAgent, model::ABM) → agent
Add the agent to the model
at the agent's own position.
Agents.add_agent_single!
— Functionadd_agent_single!(agent::A, model::ABM{A, <: DiscreteSpace}) → agent
Add agent to a random node in the space while respecting a maximum one agent per node. This function throws a warning if no empty nodes remain.
add_agent_single!(model::ABM{A, <: DiscreteSpace}, properties...; kwargs...)
Same as add_agent!(model, properties...)
but ensures that it adds an agent into a node with no other agents (does nothing if no such node exists).
Agents.move_agent!
— Functionmove_agent!(agent::A, model::ABM{A, ContinuousSpace}, dt = 1.0)
Propagate the agent forwards one step according to its velocity, after updating the agent's velocity (see ContinuousSpace
). Also take care of periodic boundary conditions.
For this continuous space version of move_agent!
, the "evolution algorithm" is a trivial Euler scheme with dt
the step size, i.e. the agent position is updated as agent.pos += agent.vel * dt
.
Notice that if you want the agent to instantly move to a specified position, do agent.pos = pos
and then update_space!(agent, model)
.
move_agent!(agent::A, model::ABM{A, ContinuousSpace}, vel::NTuple{D, N}, dt = 1.0)
Propagate the agent forwards one step according to vel
and the model's space, with dt
as the time step. (update_vel!
is not used)
move_agent!(agent::A [, pos], model::ABM{A, <: DiscreteSpace}) → agent
Move agent to the given position, or to a random one if a position is not given. pos
must be the appropriate position type depending on the space type.
Agents.move_agent_single!
— Functionmove_agent_single!(agent::AbstractAgent, model::ABM) → agent
Move agent to a random node while respecting a maximum of one agent per node. If there are no empty nodes, the agent wont move. Only valid for non-continuous spaces.
Agents.kill_agent!
— Functionkill_agent!(agent::AbstractAgent, model::ABM)
Remove an agent from model, and from the space if the model has a space.
Agents.genocide!
— Functiongenocide!(model::ABM)
Kill all the agents of the model.
genocide!(model::ABM, n::Int)
Kill the agents of the model whose IDs are larger than n.
genocide!(model::ABM, f::Function)
Kill all agents where the function f(agent)
returns true
.
Agents.sample!
— Functionsample!(model::ABM, n [, weight]; kwargs...)
Replace the agents of the model
with a random sample of the current agents with size n
.
Optionally, provide a weight
: Symbol (agent field) or function (input agent out put number) to weight the sampling. This means that the higher the weight
of the agent, the higher the probability that this agent will be chosen in the new sampling.
Keywords
replace = true
: whether sampling is performed with replacement, i.e. all agents can
be chosen more than once.
rng = GLOBAL_RNG
: a random number generator to perform the sampling with.
See the Wright-Fisher example in the documentation for an application of sample!
.
Discrete space exclusives
Agents.fill_space!
— Functionfill_space!([A ,] model::ABM{A, <:DiscreteSpace}, args...; kwargs...)
fill_space!([A ,] model::ABM{A, <:DiscreteSpace}, f::Function; kwargs...)
Add one agent to each node in the model's space. Similarly with add_agent!
, the function creates the necessary agents and the args...; kwargs...
are propagated into agent creation. If instead of args...
a function f
is provided, then args = f(pos)
is the result of applying f
where pos
is each position (tuple for grid, node index for graph).
An optional first argument is an agent type to be created, and targets mixed-agent models where the agent constructor cannot be deduced (since it is a union).
Example
using Agents
mutable struct Daisy <: AbstractAgent
id::Int
pos::Tuple{Int, Int}
breed::String
end
mutable struct Land <: AbstractAgent
id::Int
pos::Tuple{Int, Int}
temperature::Float64
end
space = GridSpace((10, 10), moore = true, periodic = true)
model = ABM(Union{Daisy, Land}, space)
temperature(pos) = (pos[1]/10, ) # make it Tuple!
fill_space!(Land, model, temperature)
Agents.node_neighbors
— Functionnode_neighbors(node, model::ABM{A, <:DiscreteSpace}, r = 1) → nodes
Return all nodes that are neighbors to the given node
, which can be an Int
for GraphSpace
, or a NTuple{Int}
for GridSpace
. Use vertex2coord
to convert nodes to positions for GridSpace
.
node_neighbors(agent, model::ABM{A, <:DiscreteSpace}, r = 1) → nodes
Same as above, but uses agent.pos
as node
.
Keyword argument neighbor_type=:default
can be used to select differing neighbors depending on the underlying graph directionality type.
:default
returns neighbors of a vertex. If graph is directed, this is equivalent
to :out
. For undirected graphs, all options are equivalent to :out
.
:all
returns both:in
and:out
neighbors.:in
returns incoming vertex neighbors.:out
returns outgoing vertex neighbors.
LightGraphs.nv
— Methodnv(model::ABM)
Return the number of nodes (vertices) in the model
space.
LightGraphs.ne
— Methodne(model::ABM)
Return the number of edges in the model
space.
Agents.has_empty_nodes
— Functionhas_empty_nodes(model)
Return true if there are empty nodes in the model
.
Agents.find_empty_nodes
— Functionfind_empty_nodes(model::ABM)
Returns the indices of empty nodes on the model space.
Agents.pick_empty
— Functionpick_empty(model)
Return a random empty node or 0
if there are no empty nodes.
Agents.get_node_contents
— Functionget_node_contents(node, model)
Return the ids of agents in the node
of the model's space (which is an integer for GraphSpace
and a tuple for GridSpace
).
get_node_contents(agent::AbstractAgent, model)
Return all agents' ids in the same node as the agent
(including the agent's own id).
Agents.get_node_agents
— Functionget_node_agents(x, model)
Same as get_node_contents(x, model)
but directly returns the list of agents instead of just the list of IDs.
Base.isempty
— Methodisempty(node::Int, model::ABM)
Return true
if there are no agents in node
.
Agents.NodeIterator
— TypeNodeIterator(model) → iterator
Create an iterator that returns node coordinates, if the space is a grid, or otherwise node number, and the agent IDs in each node.
Agents.nodes
— Functionnodes(model; by = :id) -> ns
Return a vector of the node ids of the model
that you can iterate over. The ns
are sorted depending on by
:
:id
- just sorted by their number:random
- randomly sorted:population
- nodes are sorted depending on how many agents they accommodate. The more populated nodes are first.
Agents.coord2vertex
— Functioncoord2vertex(coord::NTuple{Int}, model_or_space) → n
coord2vertex(coord::AbstractAgent, model_or_space) → n
Return the node number n
of the given coordinates or the agent's position.
Agents.vertex2coord
— Functionvertex2coord(vertex::Integer, model_or_space) → coords
Returns the coordinates of a node given its number on the graph.
Continuous space exclusives
Agents.interacting_pairs
— Functioninteracting_pairs(model, r, method; scheduler = model.scheduler)
Return an iterator that yields unique pairs of agents (a1, a2)
that are close neighbors to each other, within some interaction radius r
.
This function is usefully combined with model_step!
, when one wants to perform some pairwise interaction across all pairs of close agents once (and does not want to trigger the event twice, both with a1
and with a2
, which is unavoidable when using agent_step!
).
The argument method
provides three pairing scenarios
:all
: return every pair of agents that are within radiusr
of each other, not only the nearest ones.:nearest
: agents are only paired with their true nearest neighbor (existing within radiusr
). Each agent can only belong to one pair, therefore if two agents share the same nearest neighbor only one of them (sorted by id) will be paired.:scheduler
: agents are scanned according to the given keywordscheduler
(by default the model's scheduler), and each scanned agent is paired to its nearest neighbor. Similar to:nearest
, each agent can belong to only one pair. This functionality is useful e.g. when you want some agents to be paired "guaranteed", even if some other agents might be nearest to each other.:types
: For mixed agent models only. Return every pair of agents within radiusr
(similar to:all
), only capturing pairs of differing types. For example, a model ofUnion{Sheep,Wolf}
will only return pairs of(Sheep, Wolf)
. In the case of multiple agent types, e.g.Union{Sheep, Wolf, Grass}
, skipping pairings that involveGrass
, can be achived by ascheduler
that doesn't scheduleGrass
types, i.e.:scheduler = [a.id for a in allagents(model) of !(a isa Grass)]
.
Agents.nearest_neighbor
— Functionnearest_neighbor(agent, model, r) → nearest
Return the agent that has the closest distance to given agent
, according to the space's metric. Valid only in continuous space. Return nothing
if no agent is within distance r
.
Agents.elastic_collision!
— Functionelastic_collision!(a, b, f = nothing)
Resolve a (hypothetical) elastic collision between the two agents a, b
. They are assumed to be disks of equal size touching tangentially. Their velocities (field vel
) are adjusted for an elastic collision happening between them. This function works only for two dimensions. Notice that collision only happens if both disks face each other, to avoid collision-after-collision.
If f
is a Symbol
, then the agent property f
, e.g. :mass
, is taken as a mass to weight the two agents for the collision. By default no weighting happens.
One of the two agents can have infinite "mass", and then acts as an immovable object that specularly reflects the other agent. In this case of course momentum is not conserved, but kinetic energy is still conserved.
Agents.index!
— Functionindex!(model)
Index the database underlying the ContinuousSpace
of the model.
This can drastically improve performance for finding neighboring agents, but adding new data can become slower because after each addition, index needs to be called again.
Lack of index won't be noticed for small databases. Only use it when you have many agents and not many additions of agents.
Agents.update_space!
— Functionupdate_space!(model::ABM{A, ContinuousSpace}, agent)
Update the internal representation of continuous space to match the new position of the agent (useful in custom move_agent
functions).
Data collection
The central simulation function is run!
, which is mentioned in our Tutorial. But there are other functions that are related to simulations listed here.
Agents.init_agent_dataframe
— Functioninit_agent_dataframe(model, adata) → agent_df
Initialize a dataframe to add data later with collect_agent_data!
.
Agents.collect_agent_data!
— Functioncollect_agent_data!(df, model, properties, step = 0; obtainer = identity)
Collect and add agent data into df
(see run!
for the dispatch rules of properties
and obtainer
). step
is given because the step number information is not known.
Agents.init_model_dataframe
— Functioninit_model_dataframe(model, mdata) → model_df
Initialize a dataframe to add data later with collect_model_data!
.
Agents.collect_model_data!
— Functioncollect_model_data!(df, model, properties, step = 0, obtainer = identity)
Same as collect_agent_data!
but for model data instead.
Agents.aggname
— Functionaggname(k) → name
aggname(k, agg) → name
aggname(k, agg, condition) → name
Return the name of the column of the i
-th collected data where k = adata[i]
(or mdata[i]
). aggname
also accepts tuples with aggregate and conditional values.
Agents.paramscan
— Functionparamscan(parameters, initialize; kwargs...)
Run the model with all the parameter value combinations given in parameters
while initializing the model with initialize
. This function uses DrWatson
's dict_list
internally. This means that every entry of parameters
that is a Vector
, contains many parameters and thus is scanned. All other entries of parameters
that are not Vector
s are not expanded in the scan. Keys of parameters
should be of type Symbol
.
initialize
is a function that creates an ABM. It should accept keyword arguments, of which all values in parameters
should be a subset. This means parameters
can take both model and agent constructor properties.
Keywords
All the following keywords are propagated into run!
. Defaults are also listed for convenience: agent_step! = dummystep, n = 1, when = 1:n, model_step! = dummystep
, step0::Bool = true
, parallel::Bool = false
, replicates::Int = 0
. Keyword arguments such as adata
and mdata
are also propagated.
The following keywords modify the paramscan
function:
include_constants::Bool=false
determines whether constant parameters should be included in the output DataFrame
.
progress::Bool = true
whether to show the progress of simulations.
For example, the core loop of run!
is just
df_agent = init_agent_dataframe(model, adata)
df_model = init_model_dataframe(model, mdata)
s = 0
while until(s, n, model)
if should_we_collect(s, model, when)
collect_agent_data!(df_agent, model, adata, s)
end
if should_we_collect(s, model, when_model)
collect_model_data!(df_model, model, mdata, s)
end
step!(model, agent_step!, model_step!, 1)
s += 1
end
return df_agent, df_model
(here until
and should_we_collect
are internal functions)
Schedulers
The schedulers of Agents.jl have a very simple interface. All schedulers are functions, that take as an input the ABM and return an iterator over agent IDs. Notice that this iterator can be a "true" iterator (non-allocated) or can be just a standard vector of IDs. You can define your own scheduler according to this API and use it when making an AgentBasedModel
.
Also notice that you can use Function-like-objects to make your scheduling possible of arbitrary events. For example, imagine that after the n
-th step of your simulation you want to fundamentally change the order of agents. To achieve this you can define
mutable struct MyScheduler
n::Int # step number
w::Float64
end
and then define a calling method for it like so
function (ms::MyScheduler)(model::ABM)
ms.n += 1 # increment internal counter by 1 each time its called
# be careful to use a *new* instance of this scheduler when plotting!
if ms.n < 10
return keys(model.agents) # order doesn't matter in this case
else
ids = collect(allids(model))
# filter all ids whose agents have `w` less than some amount
filter!(id -> model[id].w < ms.w, ids)
return ids
end
end
and pass it to e.g. step!
by initializing it
ms = MyScheduler(100, 0.5)
run!(model, agentstep, modelstep, 100; scheduler = ms)
Predefined schedulers
Some useful schedulers are available below as part of the Agents.jl public API:
Agents.fastest
— Functionfastest
Activate all agents once per step in the order dictated by the agent's container, which is arbitrary (the keys sequence of a dictionary). This is the fastest way to activate all agents once per step.
Agents.by_id
— Functionby_id
Activate agents at each step according to their id.
Agents.random_activation
— Functionrandom_activation
Activate agents once per step in a random order. Different random ordering is used at each different step.
Agents.partial_activation
— Functionpartial_activation(p)
At each step, activate only p
percentage of randomly chosen agents.
Agents.property_activation
— Functionproperty_activation(property)
At each step, activate the agents in an order dictated by their property
, with agents with greater property
acting first. property
is a Symbol
, which just dictates which field the agents to compare.
Agents.by_type
— Functionby_type(shuffle_types::Bool, shuffle_agents::Bool)
Useful only for mixed agent models using Union
types.
- Setting
shuffle_types = true
groups by agent type, but randomizes the type order.
Otherwise returns agents grouped in order of appearance in the Union
.
shuffle_agents = true
randomizes the order of agents within each group,false
returns
the default order of the container (equivalent to fastest
).
by_type((C, B, A), shuffle_agents::Bool)
Activate agents by type in specified order (since Union
s are not order preserving). shuffle_agents = true
randomizes the order of agents within each group.
Plotting
Plotting functionality comes from AgentsPlots
, which uses Plots.jl. You need to install both AgentsPlots
, as well as a plotting backend (we use GR) to use the following functions.
The version of AgentsPlots
is:
using Pkg
Pkg.status("AgentsPlots")
Status `~/build/JuliaDynamics/Agents.jl/docs/Project.toml` [7820620d] AgentsPlots v0.3.0
AgentsPlots.plotabm
— Functionplotabm(model::ABM{A, <: ContinuousSpace}; ac, as, am, kwargs...)
plotabm(model::ABM{A, <: DiscreteSpace}; ac, as, am, kwargs...)
Plot the model
as a scatter
-plot, by configuring the agent shape, color and size via the keywords ac, as, am
. These keywords can be constants, or they can be functions, each accepting an agent and outputting a valid value for color/shape/size.
The keyword scheduler = model.scheduler
decides the plotting order of agents (which matters only if there is overlap).
The keyword offset
is a function with argument offest(a::Agent)
. It targets scenarios where multiple agents existin within a grid cell as it adds an offset (same type as agent.pos
) to the plotted agent position.
All other keywords are propagated into Plots.scatter
and the plot is returned.
plotabm(model::ABM{A, <: GraphSpace}; ac, as, am, kwargs...)
This function is the same as plotabm
for ContinuousSpace
, but here the three key functions ac, as, am
do not get an agent as an input but a vector of agents at each node of the graph. Their output is the same.
Here as
defaults to length
. Internally, the graphplot
recipe is used, and all other kwargs...
are propagated there.