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_neighborsFunction
space_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 DiscreteSpacer 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.nagentsFunction
nagents(model::ABM)

Return the number of agents in the model.

Agents.allagentsFunction
allagents(model)

Return an iterator over all agents of the model.

Agents.allidsFunction
allids(model)

Return an iterator over all agent IDs of the model.

Agents.nextidFunction
nextid(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!Function
add_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!Function
add_agent_pos!(agent::AbstractAgent, model::ABM) → agent

Add the agent to the model at the agent's own position.

Agents.add_agent_single!Function
add_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!Function
move_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!Function
move_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!Function
kill_agent!(agent::AbstractAgent, model::ABM)

Remove an agent from model, and from the space if the model has a space.

Agents.genocide!Function
genocide!(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!Function
sample!(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!Function
fill_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_neighborsFunction
node_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.nvMethod
nv(model::ABM)

Return the number of nodes (vertices) in the model space.

LightGraphs.neMethod
ne(model::ABM)

Return the number of edges in the model space.

Agents.find_empty_nodesFunction
find_empty_nodes(model::ABM)

Returns the indices of empty nodes on the model space.

Agents.pick_emptyFunction
pick_empty(model)

Return a random empty node or 0 if there are no empty nodes.

Agents.get_node_contentsFunction
get_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_agentsFunction
get_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.isemptyMethod
isempty(node::Int, model::ABM)

Return true if there are no agents in node.

Agents.NodeIteratorType
NodeIterator(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.nodesFunction
nodes(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.coord2vertexFunction
coord2vertex(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.vertex2coordFunction
vertex2coord(vertex::Integer, model_or_space) → coords

Returns the coordinates of a node given its number on the graph.

Continuous space exclusives

Agents.interacting_pairsFunction
interacting_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 radius r of each other, not only the nearest ones.
  • :nearest: agents are only paired with their true nearest neighbor (existing within radius r). 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 keyword scheduler (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 radius r (similar to :all), only capturing pairs of differing types. For example, a model of Union{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 involve Grass, can be achived by a scheduler that doesn't schedule Grass types, i.e.: scheduler = [a.id for a in allagents(model) of !(a isa Grass)].
Agents.nearest_neighborFunction
nearest_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!Function
elastic_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!Function
index!(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!Function
update_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.collect_agent_data!Function
collect_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.aggnameFunction
aggname(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.paramscanFunction
paramscan(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 Vectors 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.fastestFunction
fastest

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_idFunction
by_id

Activate agents at each step according to their id.

Agents.random_activationFunction
random_activation

Activate agents once per step in a random order. Different random ordering is used at each different step.

Agents.partial_activationFunction
partial_activation(p)

At each step, activate only p percentage of randomly chosen agents.

Agents.property_activationFunction
property_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_typeFunction
by_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 Unions 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 `~/.julia/packages/Agents/MAyLv/docs/Project.toml`
  [7820620d] AgentsPlots v0.3.0
AgentsPlots.plotabmFunction
plotabm(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.