diff --git a/previews/PR887/api/index.html b/previews/PR887/api/index.html index 4ee22b8e2a..922dd3e97d 100644 --- a/previews/PR887/api/index.html +++ b/previews/PR887/api/index.html @@ -1,14 +1,14 @@ -API · Agents.jl

API

The API of Agents.jl is defined on top of the fundamental structures AgentBasedModel, Space, AbstractAgent which are described in the Tutorial page. In this page we list the remaining API functions, which constitute the bulk of Agents.jl functionality.

Concrete ABM implementations

Agents.UnremovableABMType
UnremovableABM(AgentType [, space]; properties, kwargs...) → model

Similar to StandardABM, but agents cannot be removed, only added. This allows storing agents more efficiently in a standard Julia Vector (as opposed to the Dict used by StandardABM, yielding faster retrieval and iteration over agents.

It is mandatory that the agent ID is exactly the same as the agent insertion order (i.e., the 5th agent added to the model must have ID 5). If not, an error will be thrown by add_agent!.

source

Agent/model retrieval and access

Base.getindexMethod
model[id]
-getindex(model::ABM, id::Integer)

Return an agent given its ID.

source
Base.getpropertyMethod
model.prop
-getproperty(model::ABM, :prop)

Return a property with name :prop from the current model, assuming the model properties are either a dictionary with key type Symbol or a Julia struct. For example, if a model has the set of properties Dict(:weight => 5, :current => false), retrieving these values can be obtained via model.weight.

The property names :agents, :space, :scheduler, :properties, :maxid are internals and should not be accessed by the user. In the next release, getting those will error.

source
Agents.random_agentFunction
random_agent(model) → agent

Return a random agent from the model.

source
random_agent(model, condition; optimistic=true, alloc = false) → agent

Return a random agent from the model that satisfies condition(agent) == true. The function generates a random permutation of agent IDs and iterates through them. If no agent satisfies the condition, nothing is returned instead.

Keywords

optimistic = true changes the algorithm used to be non-allocating but potentially more variable in performance. This should be faster if the condition is true for a large proportion of the population (for example if the agents are split into groups).

alloc can be used to employ a different fallback strategy in case the optimistic version doesn't find any agent satisfying the condition: if the filtering condition is expensive an allocating fallback can be more performant.

source
Agents.allidsFunction
allids(model)

Return an iterator over all agent IDs of the model.

source
Agents.abmrngFunction
abmrng(model::ABM)

Return the random number generator stored in the model.

source

Available spaces

Here we list the spaces that are available "out of the box" from Agents.jl. To create your own, see Creating a new space type.

Discrete spaces

Agents.GraphSpaceType
GraphSpace(graph::AbstractGraph)

Create a GraphSpace instance that is underlined by an arbitrary graph from Graphs.jl. GraphSpace represents a space where each node (i.e. position) of a graph can hold an arbitrary amount of agents, and each agent can move between the nodes of the graph. The position type for this space is Int, use GraphAgent for convenience.

Graphs.nv and Graphs.ne can be used in a model with a GraphSpace to obtain the number of nodes or edges in the graph. The underlying graph can be altered using add_vertex! and rem_vertex!.

An example using GraphSpace is SIR model for the spread of COVID-19.

If you want to model social networks, where each agent is equivalent with a node of a graph, you're better of using nothing as the model space, and using a graph from Graphs.jl directly in the model parameters, as shown in the Social networks with Graphs.jl integration example.

Distance specification

In functions like nearby_ids, distance for GraphSpace means the degree of neighbors in the graph (thus distance is always an integer). For example, for r=2 includes first and second degree neighbors. For 0 distance, the search occurs only on the origin node.

In functions like nearby_ids the keyword neighbor_type=:default can be used to select differing neighbors depending on the underlying graph directionality type.

  • :default returns neighbors of a vertex (position). 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.
source
Agents.GridSpaceType
GridSpace(d::NTuple{D, Int}; periodic = true, metric = :chebyshev)

Create a GridSpace that has size given by the tuple d, having D ≥ 1 dimensions. Optionally decide whether the space will be periodic and what will be the distance metric. The position type for this space is NTuple{D, Int}, use GridAgent for convenience. Valid positions have indices in the range 1:d[i] for the i-th dimension.

An example using GridSpace is Schelling's segregation model.

Distance specification

The typical terminology when searching neighbors in agent based modelling is "Von Neumann" neighborhood or "Moore" neighborhoods. However, because Agents.jl provides a much more powerful infrastructure for finding neighbors, both in arbitrary dimensions but also of arbitrary neighborhood size, this established terminology is no longer appropriate. Instead, distances that define neighborhoods are specified according to a proper metric space, that is both well defined for any distance, and applicable to any dimensionality.

The allowed metrics are (and see docs online for a plotted example):

  • :chebyshev metric means that the r-neighborhood of a position are all positions within the hypercube having side length of 2*floor(r) and being centered in the origin position. This is similar to "Moore" for r = 1 and two dimensions.

  • :manhattan metric means that the r-neighborhood of a position are all positions whose cartesian indices have Manhattan distance ≤ r from the cartesian index of the origin position. This similar to "Von Neumann" for r = 1 and two dimensions.

  • :euclidean metric means that the r-neighborhood of a position are all positions whose cartesian indices have Euclidean distance ≤ r from the cartesian index of the origin position.

Advanced dimension-dependent distances in Chebyshev metric

If metric = :chebyshev, some advanced specification of distances is allowed when providing r to functions like nearby_ids.

  1. r::NTuple{D,Int} such as r = (5, 2). This would mean a distance of 5 in the first dimension and 2 in the second. This can be useful when different coordinates in the space need to be searched with different ranges, e.g., if the space corresponds to a full building, with the third dimension the floor number.
  2. r::Vector{Tuple{Int,UnitRange{Int}}} such as r = [(1, -1:1), (3, 1:2)]. This allows explicitly specifying the difference between position indices in each specified dimension. The example r = [(1, -1:1), (3, 1:2)] when given to e.g., nearby_ids, would search dimension 1 one step of either side of the current position (as well as the current position since 0 ∈ -1:1) and would search the third dimension one and two positions above current. Unspecified dimensions (like the second in this example) are searched throughout all their possible ranges.

See the Battle Royale example for usage of this advanced specification of dimension-dependent distances where one dimension is used as a categorical one.

source
Agents.GridSpaceSingleType
GridSpaceSingle(d::NTuple{D, Int}; periodic = true, metric = :chebyshev)

This is a specialized version of GridSpace that allows only one agent per position, and utilizes this knowledge to offer significant performance gains versus GridSpace.

This space reserves agent ID = 0 for internal usage. Agents should be initialized with non-zero IDs, either positive or negative. This is not checked internally.

All arguments and keywords behave exactly as in GridSpace.

source

Here is a specification of how the metrics look like:

Continuous spaces

Agents.ContinuousSpaceType
ContinuousSpace(extent::NTuple{D, <:Real}; kwargs...)

Create a D-dimensional ContinuousSpace in range 0 to (but not including) extent. Your agent positions (field pos) must be of type SVector{D, <:Real}, and it is strongly recommend that agents also have a field vel::SVector{D, <:Real} to use in conjunction with move_agent!. Use ContinuousAgent for convenience.

ContinuousSpace is a representation of agent dynamics on a continuous medium where agent position, orientation, and speed, are true floats. In addition, support is provided for representing spatial properties in a model that contains a ContinuousSpace. Spatial properties (which typically are contained in the model properties) can either be functions of the position vector, f(pos) = value, or AbstractArrays, representing discretizations of spatial data that may not be available in analytic form. In the latter case, the position is automatically mapped into the discretization represented by the array. Use get_spatial_property to access spatial properties in conjunction with ContinuousSpace.

See also Continuous space exclusives on the online docs for more functionality. An example using continuous space is the Flocking model.

Distance specification

Distances specified by r in functions like nearby_ids are always based on the Euclidean distance between two points in ContinuousSpace.

In ContinuousSpace nearby_* searches are accelerated using a grid system, see discussion around the keyword spacing below. nearby_ids is not an exact search, but can be a possible over-estimation, including agent IDs whose distance slightly exceeds r with "slightly" being as much as spacing. If you want exact searches use the slower nearby_ids_exact.

Keywords

  • periodic = true: Whether the space is periodic or not. If set to false an error will occur if an agent's position exceeds the boundary.
  • spacing::Real = minimum(extent)/20: Configures an internal compartment spacing that is used to accelerate nearest neighbor searches like nearby_ids. The compartments are actually a full instance of GridSpace in which agents move. All dimensions in extent must be completely divisible by spacing. There is no best choice for the value of spacing and if you need optimal performance it's advised to set up a benchmark over a range of choices. The finer the spacing, the faster and more accurate the inexact version of nearby_ids becomes. However, a finer spacing also means slower move_agent!, as agents change compartments more often.
  • update_vel!: A function, update_vel!(agent, model) that updates the agent's velocity before the agent has been moved, see move_agent!. You can of course change the agents' velocities during the agent interaction, the update_vel! functionality targets spatial force fields acting on the agents individually (e.g. some magnetic field). If you use update_vel!, the agent type must have a field vel::SVector{D, <:Real}.
source
Agents.OpenStreetMapSpaceType
OpenStreetMapSpace(path::AbstractString; kwargs...)

Create a space residing on the Open Street Map (OSM) file provided via path. This space represents the underlying map as a continuous entity choosing accuracy over performance. The map is represented as a graph, consisting of nodes connected by edges. Nodes are not necessarily intersections, and there may be multiple nodes on a road joining two intersections. Agents move along the available roads of the map using routing, see below.

The functionality related to Open Street Map spaces is in the submodule OSM. An example of its usage can be found in Zombie Outbreak in a City.

The OSMAgent

The base properties for an agent residing on an OSMSpace are as follows:

mutable struct Agent <: AbstractAgent
+API · Agents.jl

API

The API of Agents.jl is defined on top of the fundamental structures AgentBasedModel, Space, AbstractAgent which are described in the Tutorial page. In this page we list the remaining API functions, which constitute the bulk of Agents.jl functionality.

Concrete ABM implementations

Agents.UnremovableABMType
UnremovableABM(AgentType [, space]; properties, kwargs...) → model

Similar to StandardABM, but agents cannot be removed, only added. This allows storing agents more efficiently in a standard Julia Vector (as opposed to the Dict used by StandardABM, yielding faster retrieval and iteration over agents.

It is mandatory that the agent ID is exactly the same as the agent insertion order (i.e., the 5th agent added to the model must have ID 5). If not, an error will be thrown by add_agent!.

source

Agent/model retrieval and access

Base.getindexMethod
model[id]
+getindex(model::ABM, id::Integer)

Return an agent given its ID.

source
Base.getpropertyMethod
model.prop
+getproperty(model::ABM, :prop)

Return a property with name :prop from the current model, assuming the model properties are either a dictionary with key type Symbol or a Julia struct. For example, if a model has the set of properties Dict(:weight => 5, :current => false), retrieving these values can be obtained via model.weight.

The property names :agents, :space, :scheduler, :properties, :maxid are internals and should not be accessed by the user. In the next release, getting those will error.

source
Agents.random_agentFunction
random_agent(model) → agent

Return a random agent from the model.

source
random_agent(model, condition; optimistic=true, alloc = false) → agent

Return a random agent from the model that satisfies condition(agent) == true. The function generates a random permutation of agent IDs and iterates through them. If no agent satisfies the condition, nothing is returned instead.

Keywords

optimistic = true changes the algorithm used to be non-allocating but potentially more variable in performance. This should be faster if the condition is true for a large proportion of the population (for example if the agents are split into groups).

alloc can be used to employ a different fallback strategy in case the optimistic version doesn't find any agent satisfying the condition: if the filtering condition is expensive an allocating fallback can be more performant.

source
Agents.allidsFunction
allids(model)

Return an iterator over all agent IDs of the model.

source
Agents.abmrngFunction
abmrng(model::ABM)

Return the random number generator stored in the model.

source

Available spaces

Here we list the spaces that are available "out of the box" from Agents.jl. To create your own, see Creating a new space type.

Discrete spaces

Agents.GraphSpaceType
GraphSpace(graph::AbstractGraph)

Create a GraphSpace instance that is underlined by an arbitrary graph from Graphs.jl. GraphSpace represents a space where each node (i.e. position) of a graph can hold an arbitrary amount of agents, and each agent can move between the nodes of the graph. The position type for this space is Int, use GraphAgent for convenience.

Graphs.nv and Graphs.ne can be used in a model with a GraphSpace to obtain the number of nodes or edges in the graph. The underlying graph can be altered using add_vertex! and rem_vertex!.

An example using GraphSpace is SIR model for the spread of COVID-19.

If you want to model social networks, where each agent is equivalent with a node of a graph, you're better of using nothing as the model space, and using a graph from Graphs.jl directly in the model parameters, as shown in the Social networks with Graphs.jl integration example.

Distance specification

In functions like nearby_ids, distance for GraphSpace means the degree of neighbors in the graph (thus distance is always an integer). For example, for r=2 includes first and second degree neighbors. For 0 distance, the search occurs only on the origin node.

In functions like nearby_ids the keyword neighbor_type=:default can be used to select differing neighbors depending on the underlying graph directionality type.

  • :default returns neighbors of a vertex (position). 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.
source
Agents.GridSpaceType
GridSpace(d::NTuple{D, Int}; periodic = true, metric = :chebyshev)

Create a GridSpace that has size given by the tuple d, having D ≥ 1 dimensions. Optionally decide whether the space will be periodic and what will be the distance metric. The position type for this space is NTuple{D, Int}, use GridAgent for convenience. Valid positions have indices in the range 1:d[i] for the i-th dimension.

An example using GridSpace is Schelling's segregation model.

Distance specification

The typical terminology when searching neighbors in agent based modelling is "Von Neumann" neighborhood or "Moore" neighborhoods. However, because Agents.jl provides a much more powerful infrastructure for finding neighbors, both in arbitrary dimensions but also of arbitrary neighborhood size, this established terminology is no longer appropriate. Instead, distances that define neighborhoods are specified according to a proper metric space, that is both well defined for any distance, and applicable to any dimensionality.

The allowed metrics are (and see docs online for a plotted example):

  • :chebyshev metric means that the r-neighborhood of a position are all positions within the hypercube having side length of 2*floor(r) and being centered in the origin position. This is similar to "Moore" for r = 1 and two dimensions.

  • :manhattan metric means that the r-neighborhood of a position are all positions whose cartesian indices have Manhattan distance ≤ r from the cartesian index of the origin position. This similar to "Von Neumann" for r = 1 and two dimensions.

  • :euclidean metric means that the r-neighborhood of a position are all positions whose cartesian indices have Euclidean distance ≤ r from the cartesian index of the origin position.

Advanced dimension-dependent distances in Chebyshev metric

If metric = :chebyshev, some advanced specification of distances is allowed when providing r to functions like nearby_ids.

  1. r::NTuple{D,Int} such as r = (5, 2). This would mean a distance of 5 in the first dimension and 2 in the second. This can be useful when different coordinates in the space need to be searched with different ranges, e.g., if the space corresponds to a full building, with the third dimension the floor number.
  2. r::Vector{Tuple{Int,UnitRange{Int}}} such as r = [(1, -1:1), (3, 1:2)]. This allows explicitly specifying the difference between position indices in each specified dimension. The example r = [(1, -1:1), (3, 1:2)] when given to e.g., nearby_ids, would search dimension 1 one step of either side of the current position (as well as the current position since 0 ∈ -1:1) and would search the third dimension one and two positions above current. Unspecified dimensions (like the second in this example) are searched throughout all their possible ranges.

See the Battle Royale example for usage of this advanced specification of dimension-dependent distances where one dimension is used as a categorical one.

source
Agents.GridSpaceSingleType
GridSpaceSingle(d::NTuple{D, Int}; periodic = true, metric = :chebyshev)

This is a specialized version of GridSpace that allows only one agent per position, and utilizes this knowledge to offer significant performance gains versus GridSpace.

This space reserves agent ID = 0 for internal usage. Agents should be initialized with non-zero IDs, either positive or negative. This is not checked internally.

All arguments and keywords behave exactly as in GridSpace.

source

Here is a specification of how the metrics look like:

Continuous spaces

Agents.ContinuousSpaceType
ContinuousSpace(extent::NTuple{D, <:Real}; kwargs...)

Create a D-dimensional ContinuousSpace in range 0 to (but not including) extent. Your agent positions (field pos) must be of type SVector{D, <:Real}, and it is strongly recommend that agents also have a field vel::SVector{D, <:Real} to use in conjunction with move_agent!. Use ContinuousAgent for convenience.

ContinuousSpace is a representation of agent dynamics on a continuous medium where agent position, orientation, and speed, are true floats. In addition, support is provided for representing spatial properties in a model that contains a ContinuousSpace. Spatial properties (which typically are contained in the model properties) can either be functions of the position vector, f(pos) = value, or AbstractArrays, representing discretizations of spatial data that may not be available in analytic form. In the latter case, the position is automatically mapped into the discretization represented by the array. Use get_spatial_property to access spatial properties in conjunction with ContinuousSpace.

See also Continuous space exclusives on the online docs for more functionality. An example using continuous space is the Flocking model.

Distance specification

Distances specified by r in functions like nearby_ids are always based on the Euclidean distance between two points in ContinuousSpace.

In ContinuousSpace nearby_* searches are accelerated using a grid system, see discussion around the keyword spacing below. nearby_ids is not an exact search, but can be a possible over-estimation, including agent IDs whose distance slightly exceeds r with "slightly" being as much as spacing. If you want exact searches use the slower nearby_ids_exact.

Keywords

  • periodic = true: Whether the space is periodic or not. If set to false an error will occur if an agent's position exceeds the boundary.
  • spacing::Real = minimum(extent)/20: Configures an internal compartment spacing that is used to accelerate nearest neighbor searches like nearby_ids. The compartments are actually a full instance of GridSpace in which agents move. All dimensions in extent must be completely divisible by spacing. There is no best choice for the value of spacing and if you need optimal performance it's advised to set up a benchmark over a range of choices. The finer the spacing, the faster and more accurate the inexact version of nearby_ids becomes. However, a finer spacing also means slower move_agent!, as agents change compartments more often.
  • update_vel!: A function, update_vel!(agent, model) that updates the agent's velocity before the agent has been moved, see move_agent!. You can of course change the agents' velocities during the agent interaction, the update_vel! functionality targets spatial force fields acting on the agents individually (e.g. some magnetic field). If you use update_vel!, the agent type must have a field vel::SVector{D, <:Real}.
source
Agents.OpenStreetMapSpaceType
OpenStreetMapSpace(path::AbstractString; kwargs...)

Create a space residing on the Open Street Map (OSM) file provided via path. This space represents the underlying map as a continuous entity choosing accuracy over performance. The map is represented as a graph, consisting of nodes connected by edges. Nodes are not necessarily intersections, and there may be multiple nodes on a road joining two intersections. Agents move along the available roads of the map using routing, see below.

The functionality related to Open Street Map spaces is in the submodule OSM. An example of its usage can be found in Zombie Outbreak in a City.

The OSMAgent

The base properties for an agent residing on an OSMSpace are as follows:

mutable struct Agent <: AbstractAgent
     id::Int
     pos::Tuple{Int,Int,Float64}
 end

Current position tuple is represented as (first intersection index, second intersection index, distance travelled). The indices are the indices of the nodes of the graph that internally represents the map. Functions like OSM.nearest_node or OSM.nearest_road can help find those node indices from a (lon, lat) real world coordinate. The distance travelled is in the units of weight_type. This ensures that the map is a continuous kind of space, as an agent can truly be at any possible point on an existing road.

Use OSMAgent for convenience.

Obtaining map files

Maps files can be downloaded using the functions provided by LightOSM.jl. Agents.jl also re-exports OSM.download_osm_network, the main function used to download maps and provides a test map in OSM.test_map. An example usage to download the map of London to "london.json":

OSM.download_osm_network(
     :place_name;
     place_name = "London",
     save_to_file_location = "london.json"
-)

The length of an edge between two nodes is specified in the units of the map's weight_type as listed in the documentation for LightOSM.OSMGraph. The possible weight_types are:

  • :distance: The distance in kilometers of an edge
  • :time: The time in hours to travel along an edge at the maximum speed allowed on that road
  • :lane_efficiency: Time scaled by number of lanes

The default weight_type used is :distance.

All kwargs are propagated to LightOSM.graph_from_file.

Routing with OSM

You can use plan_route! or plan_random_route!. To actually move along a planned route use move_along_route!.

source

Adding agents

Agents.add_agent!Function
add_agent!([pos,] A::Type, model::ABM, args...) → newagent
+)

The length of an edge between two nodes is specified in the units of the map's weight_type as listed in the documentation for LightOSM.OSMGraph. The possible weight_types are:

  • :distance: The distance in kilometers of an edge
  • :time: The time in hours to travel along an edge at the maximum speed allowed on that road
  • :lane_efficiency: Time scaled by number of lanes

The default weight_type used is :distance.

All kwargs are propagated to LightOSM.graph_from_file.

Routing with OSM

You can use plan_route! or plan_random_route!. To actually move along a planned route use move_along_route!.

source

Adding agents

Agents.add_agent!Function
add_agent!([pos,] A::Type, model::ABM, args...) → newagent
 add_agent!([pos,] A::Type, model::ABM; kwargs...) → newagent

Use one of these two versions to create and add a new agent to the model using the constructor of the agent type of the model. Optionally provide a position to add the agent to as first argument, which must match the space position type.

This function takes care of setting the agent's id and position. The extra provided args... or kwargs... are propagated to other fields of the agent constructor (see example below). Mixing args... and kwargs... is not possible, only one of the two can be used to set the fields.

add_agent!([pos,] A::Type, model::ABM, args...) → newagent
 add_agent!([pos,] A::Type, model::ABM; kwargs...) → newagent

Use one of these two versions for mixed agent models, with A the agent type you wish to create, because it is otherwise not possible to deduce a constructor for A.

Example

using Agents
 @agent struct Agent(GraphAgent)
@@ -21,7 +21,7 @@
 add_agent!(model, 0.5, true) # correct: w becomes 0.5
 add_agent!(5, model, 0.5, true) # add at position 5, w becomes 0.5
 add_agent!(model; w = 0.5) # use keywords: w becomes 0.5, k becomes false
-add_agent!(model; w = 0.5, k = true) # use keywords: w becomes 0.5, k becomes true
source
add_agent!(agent::AbstractAgent [, pos], model::ABM) → agent

Add the agent to the model in the given position. If pos 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.

The type of pos must match the underlying space position type.

source
Agents.replicate!Function
replicate!(agent, model; kwargs...)

Add a new agent to the model copying the values of the fields of the given agent. With the kwargs it is possible to override the values by specifying new ones for some fields (except for the id field which is set to a new one automatically). Return the new agent instance.

Example

using Agents
+add_agent!(model; w = 0.5, k = true) # use keywords: w becomes 0.5, k becomes true
source
add_agent!(agent::AbstractAgent [, pos], model::ABM) → agent

Add the agent to the model in the given position. If pos 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.

The type of pos must match the underlying space position type.

source
Agents.replicate!Function
replicate!(agent, model; kwargs...)

Add a new agent to the model copying the values of the fields of the given agent. With the kwargs it is possible to override the values by specifying new ones for some fields (except for the id field which is set to a new one automatically). Return the new agent instance.

Example

using Agents
 @agent struct A(GridAgent{2})
     k::Float64
     w::Float64
@@ -29,24 +29,24 @@
 
 model = ABM(A, GridSpace((5, 5)))
 a = A(1, (2, 2), 0.5, 0.5)
-b = replicate!(a, model; w = 0.8)
source
Agents.nextidFunction
nextid(model::ABM) → id

Return a valid id for creating a new agent with it.

source
Agents.random_positionFunction
random_position(model) → pos

Return a random position in the model's space (always with appropriate Type).

source

Moving agents

Agents.move_agent!Function
move_agent!(agent [, pos], model::ABM) → agent

Move agent to the given position, or to a random one if a position is not given. pos must have the appropriate position type depending on the space type.

The agent's position is updated to match pos after the move.

source
move_agent!(agent::A, model::ABM{<:ContinuousSpace,A}, dt::Real)

Propagate the agent forwards one step according to its velocity, after updating the agent's velocity (if configured using update_vel!, see ContinuousSpace).

For this continuous space version of move_agent!, the "time evolution" is a trivial Euler scheme with dt the step size, i.e. the agent position is updated as agent.pos += agent.vel * dt.

Unlike move_agent!(agent, [pos,] model), this function respects the space size. For non-periodic spaces, agents will walk up to, but not reach, the space extent. For periodic spaces movement properly wraps around the extent.

source
Agents.walk!Function
walk!(agent, direction::NTuple, model::ABM{<:AbstractGridSpace}; ifempty = true)
-walk!(agent, direction::SVector, model::ABM{<:ContinuousSpace})

Move agent in the given direction respecting periodic boundary conditions. For non-periodic spaces, agents will walk to, but not exceed the boundary value. Available for both AbstractGridSpace and ContinuousSpaces.

The type of direction must be the same as the space position. AbstractGridSpace asks for Int tuples, and ContinuousSpace for Float64 static vectors, describing the walk distance in each direction. direction = (2, -3) is an example of a valid direction on a AbstractGridSpace, which moves the agent to the right 2 positions and down 3 positions. Agent velocity is ignored for this operation in ContinuousSpace.

Keywords

  • ifempty will check that the target position is unoccupied and only move if that's true. Available only on AbstractGridSpace.

Example usage in Battle Royale.

source
walk!(agent, rand, model)

Invoke a random walk by providing the rand function in place of direction. For AbstractGridSpace, the walk will cover ±1 positions in all directions, ContinuousSpace will reside within [-1, 1].

This functionality is deprecated. Use randomwalk! instead.

source
Agents.randomwalk!Function
randomwalk!(agent, model::ABM{<:AbstractGridSpace}, r::Real = 1; kwargs...)

Move agent for a distance r in a random direction respecting boundary conditions and space metric. For Chebyshev and Manhattan metric, the step size r is rounded to floor(Int,r); for Euclidean metric in a GridSpace, random walks are ill defined and hence not supported.

For example, for Chebyshev metric and r=1, this will move the agent with equal probability to any of the 8 surrounding cells. For Manhattan metric, it will move to any of the 4 surrounding cells.

Keywords

  • ifempty will check that the target position is unoccupied and only move if that's true. So if ifempty is true, this can result in the agent not moving even if there are available positions. By default this is true, set it to false if different agents can occupy the same position. In a GridSpaceSingle, agents cannot overlap anyways and this keyword has no effect.
  • force_motion has an effect only if ifempty is true or the space is a GridSpaceSingle. If set to true, the search for the random walk will be done only on the empty positions, so in this case the agent will always move if there is at least one empty position to choose from. By default this is false.
source
randomwalk!(agent, model::ABM{<:ContinuousSpace} [, r];
+b = replicate!(a, model; w = 0.8)
source
Agents.nextidFunction
nextid(model::ABM) → id

Return a valid id for creating a new agent with it.

source
Agents.random_positionFunction
random_position(model) → pos

Return a random position in the model's space (always with appropriate Type).

source

Moving agents

Agents.move_agent!Function
move_agent!(agent [, pos], model::ABM) → agent

Move agent to the given position, or to a random one if a position is not given. pos must have the appropriate position type depending on the space type.

The agent's position is updated to match pos after the move.

source
move_agent!(agent::A, model::ABM{<:ContinuousSpace,A}, dt::Real)

Propagate the agent forwards one step according to its velocity, after updating the agent's velocity (if configured using update_vel!, see ContinuousSpace).

For this continuous space version of move_agent!, the "time evolution" is a trivial Euler scheme with dt the step size, i.e. the agent position is updated as agent.pos += agent.vel * dt.

Unlike move_agent!(agent, [pos,] model), this function respects the space size. For non-periodic spaces, agents will walk up to, but not reach, the space extent. For periodic spaces movement properly wraps around the extent.

source
Agents.walk!Function
walk!(agent, direction::NTuple, model::ABM{<:AbstractGridSpace}; ifempty = true)
+walk!(agent, direction::SVector, model::ABM{<:ContinuousSpace})

Move agent in the given direction respecting periodic boundary conditions. For non-periodic spaces, agents will walk to, but not exceed the boundary value. Available for both AbstractGridSpace and ContinuousSpaces.

The type of direction must be the same as the space position. AbstractGridSpace asks for Int tuples, and ContinuousSpace for Float64 static vectors, describing the walk distance in each direction. direction = (2, -3) is an example of a valid direction on a AbstractGridSpace, which moves the agent to the right 2 positions and down 3 positions. Agent velocity is ignored for this operation in ContinuousSpace.

Keywords

  • ifempty will check that the target position is unoccupied and only move if that's true. Available only on AbstractGridSpace.

Example usage in Battle Royale.

source
walk!(agent, rand, model)

Invoke a random walk by providing the rand function in place of direction. For AbstractGridSpace, the walk will cover ±1 positions in all directions, ContinuousSpace will reside within [-1, 1].

This functionality is deprecated. Use randomwalk! instead.

source
Agents.randomwalk!Function
randomwalk!(agent, model::ABM{<:AbstractGridSpace}, r::Real = 1; kwargs...)

Move agent for a distance r in a random direction respecting boundary conditions and space metric. For Chebyshev and Manhattan metric, the step size r is rounded to floor(Int,r); for Euclidean metric in a GridSpace, random walks are ill defined and hence not supported.

For example, for Chebyshev metric and r=1, this will move the agent with equal probability to any of the 8 surrounding cells. For Manhattan metric, it will move to any of the 4 surrounding cells.

Keywords

  • ifempty will check that the target position is unoccupied and only move if that's true. So if ifempty is true, this can result in the agent not moving even if there are available positions. By default this is true, set it to false if different agents can occupy the same position. In a GridSpaceSingle, agents cannot overlap anyways and this keyword has no effect.
  • force_motion has an effect only if ifempty is true or the space is a GridSpaceSingle. If set to true, the search for the random walk will be done only on the empty positions, so in this case the agent will always move if there is at least one empty position to choose from. By default this is false.
source
randomwalk!(agent, model::ABM{<:ContinuousSpace} [, r];
     [polar=Uniform(-π,π), azimuthal=Arccos(-1,1)]
-)

Re-orient and move agent for a distance r in a random direction respecting space boundary conditions. By default r = norm(agent.vel).

The ContinuousSpace version is slightly different than the grid space. Here, the agent's velocity is updated by the random vector generated for the random walk.

Uniform/isotropic random walks are supported in any number of dimensions while an angles distribution can be specified for 2D and 3D random walks. In this case, the velocity vector is rotated using random angles given by the distributions for polar (2D and 3D) and azimuthal (3D only) angles, and scaled to have measure r. After the re-orientation the agent is moved for r in the new direction.

Anything that supports rand can be used as an angle distribution instead. This can be useful to create correlated random walks.

source
Agents.get_directionFunction
get_direction(from, to, model::ABM)

Return the direction vector from the position from to position to taking into account periodicity of the space.

source

Movement with paths

For OpenStreetMapSpace, and GridSpace/ContinuousSpace using Pathfinding.Pathfinder, a special movement method is available.

Agents.plan_route!Function
plan_route!(agent, dest, pathfinder::AStar{D})

Calculate and store the shortest path to move the agent from its current position to dest (a position e.g. (1, 5) or (1.3, 5.2)) using the provided pathfinder.

Use this method in conjunction with move_along_route!.

source
plan_route!(agent, dest, model::ABM{<:OpenStreetMapSpace};
-            return_trip = false, kwargs...) → success

Plan a route from the current position of agent to the location specified in dest, which can be an intersection or a point on a road. Overwrite any existing route.

If return_trip = true, a route will be planned from start ⟶ finish ⟶ start. All other keywords are passed to LightOSM.shortest_path.

Return true if a path to dest exists, and hence the route planning was successful. Otherwise return false. Specifying return_trip = true also requires the existence of a return path for a route to be planned.

source
Agents.plan_best_route!Function
plan_best_route!(agent, dests, pathfinder::AStar{D}; kwargs...)

Calculate, store, and return the best path to move the agent from its current position to a chosen destination taken from dests using pathfinder.

The condition = :shortest keyword returns the shortest path which is shortest out of the possible destinations. Alternatively, the :longest path may also be requested.

Return the position of the chosen destination. Return nothing if none of the supplied destinations are reachable.

source
Agents.move_along_route!Function
move_along_route!(agent, model::ABM{<:GridSpace{D}}, pathfinder::AStar{D})

Move agent for one step along the route toward its target set by plan_route!

For pathfinding in models with GridSpace.

If the agent does not have a precalculated path or the path is empty, it remains stationary.

source
move_along_route!(agent, model::ABM{<:ContinuousSpace{D}}, pathfinder::AStar{D}, speed, dt = 1.0)

Move agent for one step along the route toward its target set by plan_route! at the given speed and timestep dt.

For pathfinding in models with ContinuousSpace

If the agent does not have a precalculated path or the path is empty, it remains stationary.

source
move_along_route!(agent, model::ABM{<:OpenStreetMapSpace}, distance::Real) → remaining

Move an agent by distance along its planned route. Units of distance are as specified by the underlying graph's weight_type. If the provided distance is greater than the distance to the end of the route, return the remaining distance. Otherwise, return 0. 0 is also returned if is_stationary(agent, model).

source
Agents.is_stationaryFunction
is_stationary(agent, astar::AStar)

Same, but for pathfinding with A*.

source
is_stationary(agent, model)

Return true if agent has reached the end of its route, or no route has been set for it. Used in setups where using move_along_route! is valid.

source

Removing agents

Agents.remove_agent!Function
Pathfinding.remove_agent!(agent, model, pathfinder)

The same as remove_agent!(agent, model), but also removes the agent's path data from pathfinder.

source
remove_agent!(agent::AbstractAgent, model::ABM)
-remove_agent!(id::Int, model::ABM)

Remove an agent from the model.

source
Agents.remove_all!Function
remove_all!(model::ABM)

Remove all the agents of the model.

source
remove_all!(model::ABM, n::Int)

Remove the agents whose IDs are larger than n.

source
remove_all!(model::ABM, IDs)

Remove the agents with the given IDs.

source
remove_all!(model::ABM, f::Function)

Remove all agents where the function f(agent) returns true.

source
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.

Example usage in Wright-Fisher model of evolution.

source

Space utility functions

Agents.normalize_positionFunction
normalize_position(pos, model::ABM{<:Union{AbstractGridSpace,ContinuousSpace}})

Return the position pos normalized for the extents of the space of the given model. For periodic spaces, this wraps the position along each dimension, while for non-periodic spaces this clamps the position to the space extent.

source

Discrete space exclusives

Agents.positionsFunction
positions(model::ABM{<:DiscreteSpace}) → ns

Return an iterator over all positions of a model with a discrete space.

positions(model::ABM{<:DiscreteSpace}, by::Symbol) → ns

Return all positions of a model with a discrete space, sorting them using the argument by which can be:

  • :random - randomly sorted
  • :population - positions are sorted depending on how many agents they accommodate. The more populated positions are first.
source
Agents.npositionsFunction
npositions(model::ABM{<:DiscreteSpace})

Return the number of positions of a model with a discrete space.

source
Agents.ids_in_positionFunction
ids_in_position(position, model::ABM{<:DiscreteSpace})
-ids_in_position(agent, model::ABM{<:DiscreteSpace})

Return the ids of agents in the position corresponding to position or position of agent.

source
Agents.id_in_positionFunction
id_in_position(pos, model::ABM{<:GridSpaceSingle}) → id

Return the agent ID in the given position. This will be 0 if there is no agent in this position.

This is similar to ids_in_position, but specialized for GridSpaceSingle. See also isempty.

source
Agents.agents_in_positionFunction
agents_in_position(position, model::ABM{<:DiscreteSpace})
-agents_in_position(agent, model::ABM{<:DiscreteSpace})

Return an iterable of the agents in position, or in the position ofagent`.

source
Agents.random_id_in_positionFunction
random_id_in_position(pos, model::ABM, [f, alloc = false]) → id

Return a random id in the position specified in pos.

A filter function f(id) can be passed so that to restrict the sampling on only those agents for which the function returns true. The argument alloc can be used if the filtering condition is expensive since in this case the allocating version can be more performant. nothing is returned if no nearby position satisfies f.

Use random_nearby_id instead to return the id of a random agent near the position of a given agent.

source
Agents.random_agent_in_positionFunction
random_agent_in_position(pos, model::ABM, [f, alloc = false]) → agent

Return a random agent in the position specified in pos.

A filter function f(agent) can be passed so that to restrict the sampling on only those agents for which the function returns true. The argument alloc can be used if the filtering condition is expensive since in this case the allocating version can be more performant. nothing is returned if no nearby position satisfies f.

Use random_nearby_agent instead to return a random agent near the position of a given agent.

source
Agents.fill_space!Function
fill_space!([A ,] model::ABM{<:DiscreteSpace,A}, args...; kwargs...)
-fill_space!([A ,] model::ABM{<:DiscreteSpace,A}, f::Function; kwargs...)

Add one agent to each position 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, integer 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).

source
Agents.has_empty_positionsFunction
has_empty_positions(model::ABM{<:DiscreteSpace})

Return true if there are any positions in the model without agents.

source
Agents.empty_nearby_positionsFunction
empty_nearby_positions(pos, model::ABM{<:DiscreteSpace}, r = 1; kwargs...)
-empty_nearby_positions(agent, model::ABM{<:DiscreteSpace}, r = 1; kwargs...)

Return an iterable of all empty positions within radius r from the given position or the given agent.

The value of r and possible keywords operate identically to nearby_positions.

source
Agents.random_emptyFunction
random_empty(model::ABM{<:DiscreteSpace})

Return a random position without any agents, or nothing if no such positions exist.

source
Agents.add_agent_single!Function
add_agent_single!(model::ABM{<:DiscreteSpace}, properties...; kwargs...)

Same as add_agent!(model, properties...; kwargs...) but ensures that it adds an agent into a position with no other agents (does nothing if no such position exists).

source
add_agent_single!(A, model::ABM{<:DiscreteSpace}, properties...; kwargs...)

Same as add_agent!(A, model, properties...; kwargs...) but ensures that it adds an agent into a position with no other agents (does nothing if no such position exists).

source
add_agent_single!(agent, model::ABM{<:DiscreteSpace}) → agent

Add the agent to a random position in the space while respecting a maximum of one agent per position, updating the agent's position to the new one.

This function does nothing if there aren't any empty positions.

source
Agents.move_agent_single!Function
move_agent_single!(agent, model::ABM{<:DiscreteSpace}; cutoff) → agent

Move agent to a random position while respecting a maximum of one agent per position. If there are no empty positions, the agent won't move.

The keyword cutoff = 0.998 is sent to random_empty.

source
Agents.swap_agents!Function
swap_agents!(agent1, agent2, model::ABM{<:DiscreteSpace})

Swap the given agents positions, moving each of them to the position of the other agent.

source
Base.isemptyMethod
isempty(position, model::ABM{<:DiscreteSpace})

Return true if there are no agents in position.

source

GraphSpace exclusives

Graphs.SimpleGraphs.add_edge!Function
add_edge!(model::ABM{<:GraphSpace},  args...; kwargs...)

Add a new edge (relationship between two positions) to the graph. Returns a boolean, true if the operation was successful.

args and kwargs are directly passed to the add_edge! dispatch that acts the underlying graph type.

source
Graphs.SimpleGraphs.rem_edge!Function
rem_edge!(model::ABM{<:GraphSpace}, n, m)

Remove an edge (relationship between two positions) from the graph. Returns a boolean, true if the operation was successful.

source
Graphs.SimpleGraphs.rem_vertex!Function
rem_vertex!(model::ABM{<:GraphSpace}, n::Int)

Remove node (i.e. position) n from the model's graph. All agents in that node are removed from the model.

Warning: Graphs.jl (and thus Agents.jl) swaps the index of the last node with that of the one to be removed, while every other node remains as is. This means that when doing rem_vertex!(n, model) the last node becomes the n-th node while the previous n-th node (and all its edges and agents) are deleted.

source

ContinuousSpace exclusives

Agents.nearby_ids_exactFunction
nearby_ids_exact(x, model, r = 1)

Return an iterator over agent IDs nearby x (a position or an agent). Only valid for ContinuousSpace models. Use instead of nearby_ids for a slower, but 100% accurate version. See ContinuousSpace for more details.

source
Agents.nearest_neighborFunction
nearest_neighbor(agent, model::ABM{<:ContinuousSpace}, r) → nearest

Return the agent that has the closest distance to given agent. Return nothing if no agent is within distance r.

source
Agents.get_spatial_propertyFunction
get_spatial_property(pos, property::AbstractArray, model::ABM)

Convert the continuous agent position into an appropriate index of property, which represents some discretization of a spatial field over a ContinuousSpace. Then, return property[index]. To get the index directly, for e.g. mutating the property in-place, use get_spatial_index.

source
get_spatial_property(pos, property::Function, model::ABM)

Literally equivalent with property(pos, model), provided just for syntax consistency.

source
Agents.get_spatial_indexFunction
get_spatial_index(pos, property::AbstractArray, model::ABM)

Convert the continuous agent position into an appropriate index of property, which represents some discretization of a spatial field over a ContinuousSpace.

The dimensionality of property and the continuous space do not have to match. If property has lower dimensionality than the space (e.g. representing some surface property in 3D space) then the front dimensions of pos will be used to index.

source
Agents.interacting_pairsFunction
interacting_pairs(model, r, method; scheduler = abmscheduler(model)) → piter

Return an iterator that yields unique pairs of agents (a, b) 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 a and with b, which would be unavoidable when using agent_step!). This means, that if a pair (a, b) exists, the pair (b, a) is not included in the iterator!

Use piter.pairs to get a vector of pair IDs from the iterator.

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 distance, then by next id in scheduler) will be paired.
  • :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 achieved by a scheduler that doesn't schedule Grass types, i.e.: scheduler(model) = (a.id for a in allagents(model) if !(a isa Grass)).

The following keywords can be used:

  • scheduler = abmscheduler(model), which schedulers the agents during iteration for finding pairs. Especially in the :nearest case, this is important, as different sequencing for the agents may give different results (if b is the nearest agent for a, but a is not the nearest agent for b, whether you get the pair (a, b) or not depends on whether a was scheduler first or not).
  • nearby_f = nearby_ids_exact is the function that decides how to find nearby IDs in the :all, :types cases. Must be nearby_ids_exact or nearby_ids.

Example usage in https://juliadynamics.github.io/AgentsExampleZoo.jl/dev/examples/growing_bacteria/.

Better performance with CellListMap.jl

Notice that in most applications that interacting_pairs is useful, there is significant (10x-100x) performance gain to be made by integrating with CellListMap.jl. Checkout the Integrating Agents.jl with CellListMap.jl integration example for how to do this.

source
Agents.elastic_collision!Function
elastic_collision!(a, b, f = nothing) → happened

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 momentum is not conserved, but kinetic energy is still conserved.

Return a boolean encoding whether the collision happened.

Example usage in Continuous space social distancing.

source
Agents.euclidean_distanceFunction
euclidean_distance(a, b, model::ABM)

Return the euclidean distance between a and b (either agents or agent positions), respecting periodic boundary conditions (if in use). Works with any space where it makes sense: currently AbstractGridSpace and ContinuousSpace.

Example usage in the Flocking model.

source
Agents.manhattan_distanceFunction
manhattan_distance(a, b, model::ABM)

Return the manhattan distance between a and b (either agents or agent positions), respecting periodic boundary conditions (if in use). Works with any space where it makes sense: currently AbstractGridSpace and ContinuousSpace.

source

OpenStreetMapSpace exclusives

Agents.OSMModule
OSM

Submodule for functionality related to OpenStreetMapSpace. See the docstring of the space for more info.

source
Agents.OSM.lonlatFunction
OSM.lonlat(pos, model)
-OSM.lonlat(agent, model)

Return (longitude, latitude) of current road or intersection position.

source
Agents.OSM.nearest_nodeFunction
OSM.nearest_node(lonlat::Tuple{Float64,Float64}, model::ABM{<:OpenStreetMapSpace})

Return the nearest intersection position to (longitude, latitude). Quicker, but less precise than OSM.nearest_road.

source
Agents.OSM.nearest_roadFunction
OSM.nearest_road(lonlat::Tuple{Float64,Float64}, model::ABM{<:OpenStreetMapSpace})

Return a location on a road nearest to (longitude, latitude). Slower, but more precise than OSM.nearest_node.

source
Agents.OSM.random_road_positionFunction
OSM.random_road_position(model::ABM{<:OpenStreetMapSpace})

Similar to random_position, but rather than providing only intersections, this method returns a location somewhere on a road heading in a random direction.

source
Agents.OSM.plan_random_route!Function
OSM.plan_random_route!(agent, model::ABM{<:OpenStreetMapSpace}; kwargs...) → success

Plan a new random route for the agent, by selecting a random destination and planning a route from the agent's current position. Overwrite any existing route.

The keyword limit = 10 specifies the limit on the number of attempts at planning a random route, as no connection may be possible given the random destination. Return true if a route was successfully planned, false otherwise. All other keywords are passed to plan_route!

source
Agents.OSM.road_lengthFunction
OSM.road_length(start::Int, finish::Int, model)
-OSM.road_length(pos::Tuple{Int,Int,Float64}, model)

Return the road length between two intersections. This takes into account the direction of the road, so OSM.road_length(pos_1, pos_2, model) may not be the same as OSM.road_length(pos_2, pos_1, model). Units of the returned quantity are as specified by the underlying graph's weight_type. If start and finish are the same or pos[1] and pos[2] are the same, then return 0.

source
Agents.OSM.route_lengthFunction
OSM.route_length(agent, model::ABM{<:OpenStreetMapSpace})

Return the length of the route planned for the given agent, correctly taking into account the amount of route already traversed by the agent. Return 0 if is_stationary(agent, model).

source
Agents.OSM.same_positionFunction
OSM.same_position(a::Tuple{Int,Int,Float64}, b::Tuple{Int,Int,Float64}, model::ABM{<:OpenStreetMapSpace})

Return true if the given positions a and b are (approximately) identical

source
Agents.OSM.same_roadFunction
OSM.same_road(a::Tuple{Int,Int,Float64}, b::Tuple{Int,Int,Float64})

Return true if both points lie on the same road of the graph

source
Agents.OSM.test_mapFunction
OSM.test_map()

Download a small test map of Göttingen as an artifact. Return a path to the downloaded file.

Using this map requires network_type = :none to be passed as a keyword to OSMSpace. The unit of distance used for this map is :time.

source
LightOSM.download_osm_networkFunction
download_osm_network(download_method::Symbol;
+)

Re-orient and move agent for a distance r in a random direction respecting space boundary conditions. By default r = norm(agent.vel).

The ContinuousSpace version is slightly different than the grid space. Here, the agent's velocity is updated by the random vector generated for the random walk.

Uniform/isotropic random walks are supported in any number of dimensions while an angles distribution can be specified for 2D and 3D random walks. In this case, the velocity vector is rotated using random angles given by the distributions for polar (2D and 3D) and azimuthal (3D only) angles, and scaled to have measure r. After the re-orientation the agent is moved for r in the new direction.

Anything that supports rand can be used as an angle distribution instead. This can be useful to create correlated random walks.

source
Agents.get_directionFunction
get_direction(from, to, model::ABM)

Return the direction vector from the position from to position to taking into account periodicity of the space.

source

Movement with paths

For OpenStreetMapSpace, and GridSpace/ContinuousSpace using Pathfinding.Pathfinder, a special movement method is available.

Agents.plan_route!Function
plan_route!(agent, dest, pathfinder::AStar{D})

Calculate and store the shortest path to move the agent from its current position to dest (a position e.g. (1, 5) or (1.3, 5.2)) using the provided pathfinder.

Use this method in conjunction with move_along_route!.

source
plan_route!(agent, dest, model::ABM{<:OpenStreetMapSpace};
+            return_trip = false, kwargs...) → success

Plan a route from the current position of agent to the location specified in dest, which can be an intersection or a point on a road. Overwrite any existing route.

If return_trip = true, a route will be planned from start ⟶ finish ⟶ start. All other keywords are passed to LightOSM.shortest_path.

Return true if a path to dest exists, and hence the route planning was successful. Otherwise return false. Specifying return_trip = true also requires the existence of a return path for a route to be planned.

source
Agents.plan_best_route!Function
plan_best_route!(agent, dests, pathfinder::AStar{D}; kwargs...)

Calculate, store, and return the best path to move the agent from its current position to a chosen destination taken from dests using pathfinder.

The condition = :shortest keyword returns the shortest path which is shortest out of the possible destinations. Alternatively, the :longest path may also be requested.

Return the position of the chosen destination. Return nothing if none of the supplied destinations are reachable.

source
Agents.move_along_route!Function
move_along_route!(agent, model::ABM{<:GridSpace{D}}, pathfinder::AStar{D})

Move agent for one step along the route toward its target set by plan_route!

For pathfinding in models with GridSpace.

If the agent does not have a precalculated path or the path is empty, it remains stationary.

source
move_along_route!(agent, model::ABM{<:ContinuousSpace{D}}, pathfinder::AStar{D}, speed, dt = 1.0)

Move agent for one step along the route toward its target set by plan_route! at the given speed and timestep dt.

For pathfinding in models with ContinuousSpace

If the agent does not have a precalculated path or the path is empty, it remains stationary.

source
move_along_route!(agent, model::ABM{<:OpenStreetMapSpace}, distance::Real) → remaining

Move an agent by distance along its planned route. Units of distance are as specified by the underlying graph's weight_type. If the provided distance is greater than the distance to the end of the route, return the remaining distance. Otherwise, return 0. 0 is also returned if is_stationary(agent, model).

source
Agents.is_stationaryFunction
is_stationary(agent, model)

Return true if agent has reached the end of its route, or no route has been set for it. Used in setups where using move_along_route! is valid.

source
is_stationary(agent, astar::AStar)

Same, but for pathfinding with A*.

source

Removing agents

Agents.remove_agent!Function
remove_agent!(agent::AbstractAgent, model::ABM)
+remove_agent!(id::Int, model::ABM)

Remove an agent from the model.

source
Pathfinding.remove_agent!(agent, model, pathfinder)

The same as remove_agent!(agent, model), but also removes the agent's path data from pathfinder.

source
Agents.remove_all!Function
remove_all!(model::ABM)

Remove all the agents of the model.

source
remove_all!(model::ABM, n::Int)

Remove the agents whose IDs are larger than n.

source
remove_all!(model::ABM, IDs)

Remove the agents with the given IDs.

source
remove_all!(model::ABM, f::Function)

Remove all agents where the function f(agent) returns true.

source
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.

Example usage in Wright-Fisher model of evolution.

source

Space utility functions

Agents.normalize_positionFunction
normalize_position(pos, model::ABM{<:Union{AbstractGridSpace,ContinuousSpace}})

Return the position pos normalized for the extents of the space of the given model. For periodic spaces, this wraps the position along each dimension, while for non-periodic spaces this clamps the position to the space extent.

source

Discrete space exclusives

Agents.positionsFunction
positions(model::ABM{<:DiscreteSpace}) → ns

Return an iterator over all positions of a model with a discrete space.

positions(model::ABM{<:DiscreteSpace}, by::Symbol) → ns

Return all positions of a model with a discrete space, sorting them using the argument by which can be:

  • :random - randomly sorted
  • :population - positions are sorted depending on how many agents they accommodate. The more populated positions are first.
source
Agents.npositionsFunction
npositions(model::ABM{<:DiscreteSpace})

Return the number of positions of a model with a discrete space.

source
Agents.ids_in_positionFunction
ids_in_position(position, model::ABM{<:DiscreteSpace})
+ids_in_position(agent, model::ABM{<:DiscreteSpace})

Return the ids of agents in the position corresponding to position or position of agent.

source
Agents.id_in_positionFunction
id_in_position(pos, model::ABM{<:GridSpaceSingle}) → id

Return the agent ID in the given position. This will be 0 if there is no agent in this position.

This is similar to ids_in_position, but specialized for GridSpaceSingle. See also isempty.

source
Agents.agents_in_positionFunction
agents_in_position(position, model::ABM{<:DiscreteSpace})
+agents_in_position(agent, model::ABM{<:DiscreteSpace})

Return an iterable of the agents in position, or in the position ofagent`.

source
Agents.random_id_in_positionFunction
random_id_in_position(pos, model::ABM, [f, alloc = false]) → id

Return a random id in the position specified in pos.

A filter function f(id) can be passed so that to restrict the sampling on only those agents for which the function returns true. The argument alloc can be used if the filtering condition is expensive since in this case the allocating version can be more performant. nothing is returned if no nearby position satisfies f.

Use random_nearby_id instead to return the id of a random agent near the position of a given agent.

source
Agents.random_agent_in_positionFunction
random_agent_in_position(pos, model::ABM, [f, alloc = false]) → agent

Return a random agent in the position specified in pos.

A filter function f(agent) can be passed so that to restrict the sampling on only those agents for which the function returns true. The argument alloc can be used if the filtering condition is expensive since in this case the allocating version can be more performant. nothing is returned if no nearby position satisfies f.

Use random_nearby_agent instead to return a random agent near the position of a given agent.

source
Agents.fill_space!Function
fill_space!([A ,] model::ABM{<:DiscreteSpace,A}, args...; kwargs...)
+fill_space!([A ,] model::ABM{<:DiscreteSpace,A}, f::Function; kwargs...)

Add one agent to each position 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, integer 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).

source
Agents.has_empty_positionsFunction
has_empty_positions(model::ABM{<:DiscreteSpace})

Return true if there are any positions in the model without agents.

source
Agents.empty_nearby_positionsFunction
empty_nearby_positions(pos, model::ABM{<:DiscreteSpace}, r = 1; kwargs...)
+empty_nearby_positions(agent, model::ABM{<:DiscreteSpace}, r = 1; kwargs...)

Return an iterable of all empty positions within radius r from the given position or the given agent.

The value of r and possible keywords operate identically to nearby_positions.

source
Agents.random_emptyFunction
random_empty(model::ABM{<:DiscreteSpace})

Return a random position without any agents, or nothing if no such positions exist.

source
Agents.add_agent_single!Function
add_agent_single!(model::ABM{<:DiscreteSpace}, properties...; kwargs...)

Same as add_agent!(model, properties...; kwargs...) but ensures that it adds an agent into a position with no other agents (does nothing if no such position exists).

source
add_agent_single!(A, model::ABM{<:DiscreteSpace}, properties...; kwargs...)

Same as add_agent!(A, model, properties...; kwargs...) but ensures that it adds an agent into a position with no other agents (does nothing if no such position exists).

source
add_agent_single!(agent, model::ABM{<:DiscreteSpace}) → agent

Add the agent to a random position in the space while respecting a maximum of one agent per position, updating the agent's position to the new one.

This function does nothing if there aren't any empty positions.

source
Agents.move_agent_single!Function
move_agent_single!(agent, model::ABM{<:DiscreteSpace}; cutoff) → agent

Move agent to a random position while respecting a maximum of one agent per position. If there are no empty positions, the agent won't move.

The keyword cutoff = 0.998 is sent to random_empty.

source
Agents.swap_agents!Function
swap_agents!(agent1, agent2, model::ABM{<:DiscreteSpace})

Swap the given agents positions, moving each of them to the position of the other agent.

source
Base.isemptyMethod
isempty(position, model::ABM{<:DiscreteSpace})

Return true if there are no agents in position.

source

GraphSpace exclusives

Graphs.SimpleGraphs.add_edge!Function
add_edge!(model::ABM{<:GraphSpace},  args...; kwargs...)

Add a new edge (relationship between two positions) to the graph. Returns a boolean, true if the operation was successful.

args and kwargs are directly passed to the add_edge! dispatch that acts the underlying graph type.

source
Graphs.SimpleGraphs.rem_edge!Function
rem_edge!(model::ABM{<:GraphSpace}, n, m)

Remove an edge (relationship between two positions) from the graph. Returns a boolean, true if the operation was successful.

source
Graphs.SimpleGraphs.rem_vertex!Function
rem_vertex!(model::ABM{<:GraphSpace}, n::Int)

Remove node (i.e. position) n from the model's graph. All agents in that node are removed from the model.

Warning: Graphs.jl (and thus Agents.jl) swaps the index of the last node with that of the one to be removed, while every other node remains as is. This means that when doing rem_vertex!(n, model) the last node becomes the n-th node while the previous n-th node (and all its edges and agents) are deleted.

source

ContinuousSpace exclusives

Agents.nearby_ids_exactFunction
nearby_ids_exact(x, model, r = 1)

Return an iterator over agent IDs nearby x (a position or an agent). Only valid for ContinuousSpace models. Use instead of nearby_ids for a slower, but 100% accurate version. See ContinuousSpace for more details.

source
Agents.nearest_neighborFunction
nearest_neighbor(agent, model::ABM{<:ContinuousSpace}, r) → nearest

Return the agent that has the closest distance to given agent. Return nothing if no agent is within distance r.

source
Agents.get_spatial_propertyFunction
get_spatial_property(pos, property::AbstractArray, model::ABM)

Convert the continuous agent position into an appropriate index of property, which represents some discretization of a spatial field over a ContinuousSpace. Then, return property[index]. To get the index directly, for e.g. mutating the property in-place, use get_spatial_index.

source
get_spatial_property(pos, property::Function, model::ABM)

Literally equivalent with property(pos, model), provided just for syntax consistency.

source
Agents.get_spatial_indexFunction
get_spatial_index(pos, property::AbstractArray, model::ABM)

Convert the continuous agent position into an appropriate index of property, which represents some discretization of a spatial field over a ContinuousSpace.

The dimensionality of property and the continuous space do not have to match. If property has lower dimensionality than the space (e.g. representing some surface property in 3D space) then the front dimensions of pos will be used to index.

source
Agents.interacting_pairsFunction
interacting_pairs(model, r, method; scheduler = abmscheduler(model)) → piter

Return an iterator that yields unique pairs of agents (a, b) 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 a and with b, which would be unavoidable when using agent_step!). This means, that if a pair (a, b) exists, the pair (b, a) is not included in the iterator!

Use piter.pairs to get a vector of pair IDs from the iterator.

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 distance, then by next id in scheduler) will be paired.
  • :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 achieved by a scheduler that doesn't schedule Grass types, i.e.: scheduler(model) = (a.id for a in allagents(model) if !(a isa Grass)).

The following keywords can be used:

  • scheduler = abmscheduler(model), which schedulers the agents during iteration for finding pairs. Especially in the :nearest case, this is important, as different sequencing for the agents may give different results (if b is the nearest agent for a, but a is not the nearest agent for b, whether you get the pair (a, b) or not depends on whether a was scheduler first or not).
  • nearby_f = nearby_ids_exact is the function that decides how to find nearby IDs in the :all, :types cases. Must be nearby_ids_exact or nearby_ids.

Example usage in https://juliadynamics.github.io/AgentsExampleZoo.jl/dev/examples/growing_bacteria/.

Better performance with CellListMap.jl

Notice that in most applications that interacting_pairs is useful, there is significant (10x-100x) performance gain to be made by integrating with CellListMap.jl. Checkout the Integrating Agents.jl with CellListMap.jl integration example for how to do this.

source
Agents.elastic_collision!Function
elastic_collision!(a, b, f = nothing) → happened

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 momentum is not conserved, but kinetic energy is still conserved.

Return a boolean encoding whether the collision happened.

Example usage in Continuous space social distancing.

source
Agents.euclidean_distanceFunction
euclidean_distance(a, b, model::ABM)

Return the euclidean distance between a and b (either agents or agent positions), respecting periodic boundary conditions (if in use). Works with any space where it makes sense: currently AbstractGridSpace and ContinuousSpace.

Example usage in the Flocking model.

source
Agents.manhattan_distanceFunction
manhattan_distance(a, b, model::ABM)

Return the manhattan distance between a and b (either agents or agent positions), respecting periodic boundary conditions (if in use). Works with any space where it makes sense: currently AbstractGridSpace and ContinuousSpace.

source

OpenStreetMapSpace exclusives

Agents.OSMModule
OSM

Submodule for functionality related to OpenStreetMapSpace. See the docstring of the space for more info.

source
Agents.OSM.lonlatFunction
OSM.lonlat(pos, model)
+OSM.lonlat(agent, model)

Return (longitude, latitude) of current road or intersection position.

source
Agents.OSM.nearest_nodeFunction
OSM.nearest_node(lonlat::Tuple{Float64,Float64}, model::ABM{<:OpenStreetMapSpace})

Return the nearest intersection position to (longitude, latitude). Quicker, but less precise than OSM.nearest_road.

source
Agents.OSM.nearest_roadFunction
OSM.nearest_road(lonlat::Tuple{Float64,Float64}, model::ABM{<:OpenStreetMapSpace})

Return a location on a road nearest to (longitude, latitude). Slower, but more precise than OSM.nearest_node.

source
Agents.OSM.random_road_positionFunction
OSM.random_road_position(model::ABM{<:OpenStreetMapSpace})

Similar to random_position, but rather than providing only intersections, this method returns a location somewhere on a road heading in a random direction.

source
Agents.OSM.plan_random_route!Function
OSM.plan_random_route!(agent, model::ABM{<:OpenStreetMapSpace}; kwargs...) → success

Plan a new random route for the agent, by selecting a random destination and planning a route from the agent's current position. Overwrite any existing route.

The keyword limit = 10 specifies the limit on the number of attempts at planning a random route, as no connection may be possible given the random destination. Return true if a route was successfully planned, false otherwise. All other keywords are passed to plan_route!

source
Agents.OSM.road_lengthFunction
OSM.road_length(start::Int, finish::Int, model)
+OSM.road_length(pos::Tuple{Int,Int,Float64}, model)

Return the road length between two intersections. This takes into account the direction of the road, so OSM.road_length(pos_1, pos_2, model) may not be the same as OSM.road_length(pos_2, pos_1, model). Units of the returned quantity are as specified by the underlying graph's weight_type. If start and finish are the same or pos[1] and pos[2] are the same, then return 0.

source
Agents.OSM.route_lengthFunction
OSM.route_length(agent, model::ABM{<:OpenStreetMapSpace})

Return the length of the route planned for the given agent, correctly taking into account the amount of route already traversed by the agent. Return 0 if is_stationary(agent, model).

source
Agents.OSM.same_positionFunction
OSM.same_position(a::Tuple{Int,Int,Float64}, b::Tuple{Int,Int,Float64}, model::ABM{<:OpenStreetMapSpace})

Return true if the given positions a and b are (approximately) identical

source
Agents.OSM.same_roadFunction
OSM.same_road(a::Tuple{Int,Int,Float64}, b::Tuple{Int,Int,Float64})

Return true if both points lie on the same road of the graph

source
Agents.OSM.test_mapFunction
OSM.test_map()

Download a small test map of Göttingen as an artifact. Return a path to the downloaded file.

Using this map requires network_type = :none to be passed as a keyword to OSMSpace. The unit of distance used for this map is :time.

source
LightOSM.download_osm_networkFunction
download_osm_network(download_method::Symbol;
                      network_type::Symbol=:drive,
                      metadata::Bool=false,
                      download_format::Symbol=:json,
                      save_to_file_location::Union{String,Nothing}=nothing,
                      download_kwargs...
-                     )::Union{XMLDocument,Dict{String,Any}}

Downloads an OpenStreetMap network by querying with a place name, bounding box, or centroid point.

Arguments

  • download_method::Symbol: Download method, choose from :place_name, :bbox or :point.
  • network_type::Symbol=:drive: Network type filter, pick from :drive, :drive_service, :walk, :bike, :all, :all_private, :none, :rail
  • metadata::Bool=false: Set true to return metadata.
  • download_format::Symbol=:json: Download format, either :osm, :xml or json.
  • save_to_file_location::Union{String,Nothing}=nothing: Specify a file location to save downloaded data to disk.

Required Kwargs for each Download Method

download_method=:place_name

  • place_name::String: Any place name string used as a search argument to the Nominatim API.

download_method=:bbox

  • minlat::AbstractFloat: Bottom left bounding box latitude coordinate.
  • minlon::AbstractFloat: Bottom left bounding box longitude coordinate.
  • maxlat::AbstractFloat: Top right bounding box latitude coordinate.
  • maxlon::AbstractFloat: Top right bounding box longitude coordinate.

download_method=:point

  • point::GeoLocation: Centroid point to draw the bounding box around.
  • radius::Number: Distance (km) from centroid point to each bounding box corner.

download_method=:polygon

  • polygon::AbstractVector: Vector of longitude-latitude pairs.

download_method=:custom_filters

  • custom_filters::String: Filters for the query, e.g. polygon filter, highways only, traffic lights only, etc.
  • metadata::Bool=false: Set true to return metadata.
  • download_format::Symbol=:json: Download format, either :osm, :xml or json.
  • bbox::Union{Vector{AbstractFloat},Nothing}=nothing: Optional bounding box filter.

Network Types

  • :drive: Motorways excluding private and service ways.
  • :drive_service: Motorways including private and service ways.
  • :walk: Walkways only.
  • :bike: Cycleways only.
  • :all: All motorways, walkways and cycleways excluding private ways.
  • :all_private: All motorways, walkways and cycleways including private ways.
  • :none: No network filters.
  • :rail: Railways excluding proposed and platform.

Return

  • Union{XMLDocument,Dict{String,Any}}: OpenStreetMap network data parsed as either XML or Dictionary object depending on the download method.

Nearby Agents

Agents.nearby_idsFunction
nearby_ids(position, model::ABM, r = 1; kwargs...) → ids

Return an iterable over the IDs of the agents within distance r (inclusive) from the given position. The position must match type with the spatial structure of the model. The specification of what "distance" means depends on the space, hence it is explained in each space's documentation string. Keyword arguments are space-specific and also described in each space's documentation string.

nearby_ids always includes IDs with 0 distance to position.

source
nearby_ids(agent::AbstractAgent, model::ABM, r=1)

Same as nearby_ids(agent.pos, model, r) but the iterable excludes the given agent's id.

source
Agents.nearby_agentsFunction
nearby_agents(agent, model::ABM, r = 1; kwargs...) -> agent

Return an iterable of the agents near the position of the given agent.

The value of the argument r and possible keywords operate identically to nearby_ids.

source
Agents.nearby_positionsFunction
nearby_positions(position, model::ABM{<:DiscreteSpace}, r=1; kwargs...)

Return an iterable of all positions within "radius" r of the given position (which excludes given position). The position must match type with the spatial structure of the model.

The value of r and possible keywords operate identically to nearby_ids.

This function only exists for discrete spaces with a finite amount of positions.

nearby_positions(position, model::ABM{<:OpenStreetMapSpace}; kwargs...) → positions

For OpenStreetMapSpace this means "nearby intersections" and operates directly on the underlying graph of the OSM, providing the intersection nodes nearest to the given position.

source
nearby_positions(agent::AbstractAgent, model::ABM, r=1)

Same as nearby_positions(agent.pos, model, r).

source
Agents.random_nearby_idFunction
random_nearby_id(agent, model::ABM, r = 1, f = nothing, alloc = false; kwargs...) → id

Return the id of a random agent near the position of the given agent using an optimized algorithm from Reservoir sampling. Return nothing if no agents are nearby.

The value of the argument r and possible keywords operate identically to nearby_ids.

A filter function f(id) can be passed so that to restrict the sampling on only those ids for which the function returns true. The argument alloc can be used if the filtering condition is expensive since in this case the allocating version can be more performant. nothing is returned if no nearby id satisfies f.

For discrete spaces, use random_id_in_position instead to return a random id at a given position.

source
Agents.random_nearby_agentFunction
random_nearby_agent(agent, model::ABM, r = 1, f = nothing, alloc = false; kwargs...) → agent

Return a random agent near the position of the given agent or nothing if no agent is nearby.

The value of the argument r and possible keywords operate identically to nearby_ids.

A filter function f(agent) can be passed so that to restrict the sampling on only those agents for which the function returns true. The argument alloc can be used if the filtering condition is expensive since in this case the allocating version can be more performant. nothing is returned if no nearby agent satisfies f.

For discrete spaces, use random_agent_in_position instead to return a random agent at a given position.

source
Agents.random_nearby_positionFunction
random_nearby_position(position, model::ABM, r=1, f = nothing, alloc = false; kwargs...) → position

Return a random position near the given position. Return nothing if the space doesn't allow for nearby positions.

The value of the argument r and possible keywords operate identically to nearby_positions.

A filter function f(pos) can be passed so that to restrict the sampling on only those positions for which the function returns true. The argument alloc can be used if the filtering condition is expensive since in this case the allocating version can be more performant. nothing is returned if no nearby position satisfies f.

source

A note on iteration

Most iteration in Agents.jl is dynamic and lazy, when possible, for performance reasons.

Dynamic means that when iterating over the result of e.g. the ids_in_position function, the iterator will be affected by actions that would alter its contents. Specifically, imagine the scenario

using Agents
+                     )::Union{XMLDocument,Dict{String,Any}}

Downloads an OpenStreetMap network by querying with a place name, bounding box, or centroid point.

Arguments

  • download_method::Symbol: Download method, choose from :place_name, :bbox or :point.
  • network_type::Symbol=:drive: Network type filter, pick from :drive, :drive_service, :walk, :bike, :all, :all_private, :none, :rail
  • metadata::Bool=false: Set true to return metadata.
  • download_format::Symbol=:json: Download format, either :osm, :xml or json.
  • save_to_file_location::Union{String,Nothing}=nothing: Specify a file location to save downloaded data to disk.

Required Kwargs for each Download Method

download_method=:place_name

  • place_name::String: Any place name string used as a search argument to the Nominatim API.

download_method=:bbox

  • minlat::AbstractFloat: Bottom left bounding box latitude coordinate.
  • minlon::AbstractFloat: Bottom left bounding box longitude coordinate.
  • maxlat::AbstractFloat: Top right bounding box latitude coordinate.
  • maxlon::AbstractFloat: Top right bounding box longitude coordinate.

download_method=:point

  • point::GeoLocation: Centroid point to draw the bounding box around.
  • radius::Number: Distance (km) from centroid point to each bounding box corner.

download_method=:polygon

  • polygon::AbstractVector: Vector of longitude-latitude pairs.

download_method=:custom_filters

  • custom_filters::String: Filters for the query, e.g. polygon filter, highways only, traffic lights only, etc.
  • metadata::Bool=false: Set true to return metadata.
  • download_format::Symbol=:json: Download format, either :osm, :xml or json.
  • bbox::Union{Vector{AbstractFloat},Nothing}=nothing: Optional bounding box filter.

Network Types

  • :drive: Motorways excluding private and service ways.
  • :drive_service: Motorways including private and service ways.
  • :walk: Walkways only.
  • :bike: Cycleways only.
  • :all: All motorways, walkways and cycleways excluding private ways.
  • :all_private: All motorways, walkways and cycleways including private ways.
  • :none: No network filters.
  • :rail: Railways excluding proposed and platform.

Return

  • Union{XMLDocument,Dict{String,Any}}: OpenStreetMap network data parsed as either XML or Dictionary object depending on the download method.

Nearby Agents

Agents.nearby_idsFunction
nearby_ids(position, model::ABM, r = 1; kwargs...) → ids

Return an iterable over the IDs of the agents within distance r (inclusive) from the given position. The position must match type with the spatial structure of the model. The specification of what "distance" means depends on the space, hence it is explained in each space's documentation string. Keyword arguments are space-specific and also described in each space's documentation string.

nearby_ids always includes IDs with 0 distance to position.

source
nearby_ids(agent::AbstractAgent, model::ABM, r=1)

Same as nearby_ids(agent.pos, model, r) but the iterable excludes the given agent's id.

source
Agents.nearby_agentsFunction
nearby_agents(agent, model::ABM, r = 1; kwargs...) -> agent

Return an iterable of the agents near the position of the given agent.

The value of the argument r and possible keywords operate identically to nearby_ids.

source
Agents.nearby_positionsFunction
nearby_positions(position, model::ABM{<:DiscreteSpace}, r=1; kwargs...)

Return an iterable of all positions within "radius" r of the given position (which excludes given position). The position must match type with the spatial structure of the model.

The value of r and possible keywords operate identically to nearby_ids.

This function only exists for discrete spaces with a finite amount of positions.

nearby_positions(position, model::ABM{<:OpenStreetMapSpace}; kwargs...) → positions

For OpenStreetMapSpace this means "nearby intersections" and operates directly on the underlying graph of the OSM, providing the intersection nodes nearest to the given position.

source
nearby_positions(agent::AbstractAgent, model::ABM, r=1)

Same as nearby_positions(agent.pos, model, r).

source
Agents.random_nearby_idFunction
random_nearby_id(agent, model::ABM, r = 1, f = nothing, alloc = false; kwargs...) → id

Return the id of a random agent near the position of the given agent using an optimized algorithm from Reservoir sampling. Return nothing if no agents are nearby.

The value of the argument r and possible keywords operate identically to nearby_ids.

A filter function f(id) can be passed so that to restrict the sampling on only those ids for which the function returns true. The argument alloc can be used if the filtering condition is expensive since in this case the allocating version can be more performant. nothing is returned if no nearby id satisfies f.

For discrete spaces, use random_id_in_position instead to return a random id at a given position.

source
Agents.random_nearby_agentFunction
random_nearby_agent(agent, model::ABM, r = 1, f = nothing, alloc = false; kwargs...) → agent

Return a random agent near the position of the given agent or nothing if no agent is nearby.

The value of the argument r and possible keywords operate identically to nearby_ids.

A filter function f(agent) can be passed so that to restrict the sampling on only those agents for which the function returns true. The argument alloc can be used if the filtering condition is expensive since in this case the allocating version can be more performant. nothing is returned if no nearby agent satisfies f.

For discrete spaces, use random_agent_in_position instead to return a random agent at a given position.

source
Agents.random_nearby_positionFunction
random_nearby_position(position, model::ABM, r=1, f = nothing, alloc = false; kwargs...) → position

Return a random position near the given position. Return nothing if the space doesn't allow for nearby positions.

The value of the argument r and possible keywords operate identically to nearby_positions.

A filter function f(pos) can be passed so that to restrict the sampling on only those positions for which the function returns true. The argument alloc can be used if the filtering condition is expensive since in this case the allocating version can be more performant. nothing is returned if no nearby position satisfies f.

source

A note on iteration

Most iteration in Agents.jl is dynamic and lazy, when possible, for performance reasons.

Dynamic means that when iterating over the result of e.g. the ids_in_position function, the iterator will be affected by actions that would alter its contents. Specifically, imagine the scenario

using Agents
 # We don't need to make a new agent type here,
 # we use the minimal agent for 4-dimensional grid spaces
 model = ABM(GridAgent{4}, GridSpace((5, 5, 5, 5)))
@@ -61,9 +61,9 @@
  3

You will notice that only 1 agent was removed. This is simply because the final state of the iteration of ids_in_position was reached unnaturally, because the length of its output was reduced by 1 during iteration. To avoid problems like these, you need to collect the iterator to have a non dynamic version.

Lazy means that when possible the outputs of the iteration are not collected and instead are generated on the fly. A good example to illustrate this is nearby_ids, where doing something like

a = random_agent(model)
 sort!(nearby_ids(random_agent(model), model))

leads to error, since you cannot sort! the returned iterator. This can be easily solved by adding a collect in between:

a = random_agent(model)
 sort!(collect(nearby_agents(a, model)))
1-element Vector{GridAgent{4}}:
- GridAgent{4}(3, (2, 1, 1, 1))

Higher-order interactions

There may be times when pair-wise, triplet-wise or higher interactions need to be accounted for across most or all of the model's agent population. The following methods provide an interface for such calculation.

These methods follow the conventions outlined above in A note on iteration.

Agents.iter_agent_groupsFunction
iter_agent_groups(order::Int, model::ABM; scheduler = Schedulers.by_id)

Return an iterator over all agents of the model, grouped by order. When order = 2, the iterator returns agent pairs, e.g (agent1, agent2) and when order = 3: agent triples, e.g. (agent1, agent7, agent8). order must be larger than 1 but has no upper bound.

Index order is provided by the model scheduler by default, but can be altered with the scheduler keyword.

source
Agents.map_agent_groupsFunction
map_agent_groups(order::Int, f::Function, model::ABM; kwargs...)
-map_agent_groups(order::Int, f::Function, model::ABM, filter::Function; kwargs...)

Applies function f to all grouped agents of an iter_agent_groups iterator. kwargs are passed to the iterator method. f must take the form f(NTuple{O,AgentType}), where the dimension O is equal to order.

Optionally, a filter function that accepts an iterable and returns a Bool can be applied to remove unwanted matches from the results. Note: This option cannot keep matrix order, so should be used in conjunction with index_mapped_groups to associate agent ids with the resultant data.

source
Agents.index_mapped_groupsFunction
index_mapped_groups(order::Int, model::ABM; scheduler = Schedulers.by_id)
-index_mapped_groups(order::Int, model::ABM, filter::Function; scheduler = Schedulers.by_id)

Return an iterable of agent ids in the model, meeting the filter criteria if used.

source

Minimal agent types

The @agent macro can be used to define new agent types from the minimal agent types that are listed below:

Agents.NoSpaceAgentType
NoSpaceAgent <: AbstractAgent

The minimal agent struct for usage with nothing as space (i.e., no space). It has the field id::Int, and potentially other internal fields that are not documented as part of the public API. See also @agent.

source
Agents.GridAgentType
GridAgent{D} <: AbstractAgent

The minimal agent struct for usage with D-dimensional GridSpace. It has an additional pos::NTuple{D,Int} field. See also @agent.

source
Agents.ContinuousAgentType
ContinuousAgent{D,T} <: AbstractAgent

The minimal agent struct for usage with D-dimensional ContinuousSpace. It has the additional fields pos::SVector{D,T}, vel::SVector{D,T} where T can be any AbstractFloat type. See also @agent.

source

Parameter scanning

Agents.paramscanFunction
paramscan(parameters::AbstractDict, initialize; kwargs...) → adf, mdf

Perform a parameter scan of an ABM simulation output by collecting data from all parameter combinations into dataframes (one for agent data, one for model data). The dataframes columns are both the collected data (as in run!) but also the input parameter values used.

parameters is a dictionary with key type Symbol. Each entry of the dictionary maps a parameter key to the parameter values that should be scanned over (or to a single parameter value that will remain constant throughout the scans). The approach regarding parameters is as follows:

  • If the value of a specific key is a Vector, all values of the vector are expended as values for the parameter to scan over.
  • If the value of a specific key is not a Vector, it is assumed that whatever this value is, it corresponds to a single and constant parameter value and therefore it is not expanded or scanned over.

This is done so that parameter values that are inherently iterable (such as a String) are not wrongly expanded into their constituents. (if the value of a parameter is itself a Vector, then you need to pass in a vector of vectors to scan the parameter)

The second argument initialize is a function that creates an ABM and returns it. It must accept keyword arguments which are the keys of the parameters dictionary. Since the user decides how to use input arguments to make an ABM, parameters can be used to affect model properties, space type and creation as well as agent properties, see the example below.

Keywords

The following keywords modify the paramscan function:

  • include_constants::Bool = false: by default, only the varying parameters (Vector values in parameters) will be included in the output DataFrame. If true, constant parameters (non-Vector in parameters) will also be included.
  • parallel::Bool = false whether Distributed.pmap is invoked to run simulations in parallel. This must be used in conjunction with @everywhere (see Performance Tips).
  • showprogress::Bool = false whether a progressbar will be displayed to indicate % runs finished.

All other keywords are propagated into run!. Furthermore, agent_step!, model_step!, n are also keywords here, that are given to run! as arguments. Naturally, stepping functions and the number of time steps (agent_step!, model_step!, and n) and at least one of adata, mdata are mandatory. The adata, mdata lists shouldn't contain the parameters that are already in the parameters dictionary to avoid duplication.

Example

A runnable example that uses paramscan is shown in Schelling's segregation model. There, we define

function initialize(; numagents = 320, griddims = (20, 20), min_to_be_happy = 3)
+ GridAgent{4}(2, (1, 1, 1, 1))

Higher-order interactions

There may be times when pair-wise, triplet-wise or higher interactions need to be accounted for across most or all of the model's agent population. The following methods provide an interface for such calculation.

These methods follow the conventions outlined above in A note on iteration.

Agents.iter_agent_groupsFunction
iter_agent_groups(order::Int, model::ABM; scheduler = Schedulers.by_id)

Return an iterator over all agents of the model, grouped by order. When order = 2, the iterator returns agent pairs, e.g (agent1, agent2) and when order = 3: agent triples, e.g. (agent1, agent7, agent8). order must be larger than 1 but has no upper bound.

Index order is provided by the model scheduler by default, but can be altered with the scheduler keyword.

source
Agents.map_agent_groupsFunction
map_agent_groups(order::Int, f::Function, model::ABM; kwargs...)
+map_agent_groups(order::Int, f::Function, model::ABM, filter::Function; kwargs...)

Applies function f to all grouped agents of an iter_agent_groups iterator. kwargs are passed to the iterator method. f must take the form f(NTuple{O,AgentType}), where the dimension O is equal to order.

Optionally, a filter function that accepts an iterable and returns a Bool can be applied to remove unwanted matches from the results. Note: This option cannot keep matrix order, so should be used in conjunction with index_mapped_groups to associate agent ids with the resultant data.

source
Agents.index_mapped_groupsFunction
index_mapped_groups(order::Int, model::ABM; scheduler = Schedulers.by_id)
+index_mapped_groups(order::Int, model::ABM, filter::Function; scheduler = Schedulers.by_id)

Return an iterable of agent ids in the model, meeting the filter criteria if used.

source

Minimal agent types

The @agent macro can be used to define new agent types from the minimal agent types that are listed below:

Agents.NoSpaceAgentType
NoSpaceAgent <: AbstractAgent

The minimal agent struct for usage with nothing as space (i.e., no space). It has the field id::Int, and potentially other internal fields that are not documented as part of the public API. See also @agent.

source
Agents.GridAgentType
GridAgent{D} <: AbstractAgent

The minimal agent struct for usage with D-dimensional GridSpace. It has an additional pos::NTuple{D,Int} field. See also @agent.

source
Agents.ContinuousAgentType
ContinuousAgent{D,T} <: AbstractAgent

The minimal agent struct for usage with D-dimensional ContinuousSpace. It has the additional fields pos::SVector{D,T}, vel::SVector{D,T} where T can be any AbstractFloat type. See also @agent.

source

Parameter scanning

Agents.paramscanFunction
paramscan(parameters::AbstractDict, initialize; kwargs...) → adf, mdf

Perform a parameter scan of an ABM simulation output by collecting data from all parameter combinations into dataframes (one for agent data, one for model data). The dataframes columns are both the collected data (as in run!) but also the input parameter values used.

parameters is a dictionary with key type Symbol. Each entry of the dictionary maps a parameter key to the parameter values that should be scanned over (or to a single parameter value that will remain constant throughout the scans). The approach regarding parameters is as follows:

  • If the value of a specific key is a Vector, all values of the vector are expended as values for the parameter to scan over.
  • If the value of a specific key is not a Vector, it is assumed that whatever this value is, it corresponds to a single and constant parameter value and therefore it is not expanded or scanned over.

This is done so that parameter values that are inherently iterable (such as a String) are not wrongly expanded into their constituents. (if the value of a parameter is itself a Vector, then you need to pass in a vector of vectors to scan the parameter)

The second argument initialize is a function that creates an ABM and returns it. It must accept keyword arguments which are the keys of the parameters dictionary. Since the user decides how to use input arguments to make an ABM, parameters can be used to affect model properties, space type and creation as well as agent properties, see the example below.

Keywords

The following keywords modify the paramscan function:

  • include_constants::Bool = false: by default, only the varying parameters (Vector values in parameters) will be included in the output DataFrame. If true, constant parameters (non-Vector in parameters) will also be included.
  • parallel::Bool = false whether Distributed.pmap is invoked to run simulations in parallel. This must be used in conjunction with @everywhere (see Performance Tips).
  • showprogress::Bool = false whether a progressbar will be displayed to indicate % runs finished.

All other keywords are propagated into run!. Furthermore, agent_step!, model_step!, n are also keywords here, that are given to run! as arguments. Naturally, stepping functions and the number of time steps (agent_step!, model_step!, and n) and at least one of adata, mdata are mandatory. The adata, mdata lists shouldn't contain the parameters that are already in the parameters dictionary to avoid duplication.

Example

A runnable example that uses paramscan is shown in Schelling's segregation model. There, we define

function initialize(; numagents = 320, griddims = (20, 20), min_to_be_happy = 3)
     space = GridSpaceSingle(griddims, periodic = false)
     properties = Dict(:min_to_be_happy => min_to_be_happy)
     model = ABM(SchellingAgent, space;
@@ -81,7 +81,7 @@
     :griddims => (20, 20),            # not Vector = not expanded
 )
 
-adf, _ = paramscan(parameters, initialize; adata, agent_step!, n = 3)
source

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. Specifically, these functions aid in making custom data collection loops, instead of using the run! function.

For example, the core loop of run! is just

df_agent = init_agent_dataframe(model, adata)
+adf, _ = paramscan(parameters, initialize; adata, agent_step!, n = 3)
source

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. Specifically, these functions aid in making custom data collection loops, instead of using the run! function.

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
@@ -95,7 +95,7 @@
   step!(model, agent_step!, model_step!, 1)
   s += 1
 end
-return df_agent, df_model

(here until and should_we_collect are internal functions)

run! uses the following functions:

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.

source
Agents.datanameFunction
dataname(k) → name

Return the name of the column of the i-th collected data where k = adata[i] (or mdata[i]). dataname also accepts tuples with aggregate and conditional values.

source

Schedulers

Agents.SchedulersModule
Schedulers

Submodule containing all predefined schedulers of Agents.jl and the scheduling API. Schedulers have a very simple interface. They 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. You can also use the function schedule(model) to obtain the scheduled ID list, if you prefer to write your own step!-like loop.

See also Advanced scheduling for making more advanced schedulers.

Notice that schedulers can be given directly to model creation, and thus become the "default" scheduler a model uses, but they can just as easily be incorporated in a model_step! function as shown in Advanced stepping. The scheduler that is stored in the model is only meaningful if an agent-stepping function is defined for step! or run!, otherwise a user decides a scheduler in the model-stepping function, as illustrated in the Advanced stepping part of the tutorial.

source

Predefined schedulers

Some useful schedulers are available below as part of the Agents.jl API:

Agents.Schedulers.fastestFunction
Schedulers.fastest

A scheduler that activates 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.

source
Agents.Schedulers.ByIDType
Schedulers.ByID()

A non-allocating scheduler that activates all agents at each step according to their id.

source
Agents.Schedulers.RandomlyType
Schedulers.Randomly()

A non-allocating scheduler that activates all agents once per step in a random order. Different random ordering is used at each different step.

source
Agents.Schedulers.ByPropertyType
Schedulers.ByProperty(property)

A non-allocating scheduler that at each step activates the agents in an order dictated by their property, with agents with greater property acting first. property can be a Symbol, which just dictates which field of the agents to compare, or a function which inputs an agent and outputs a real number.

source
Agents.Schedulers.ByTypeType
Schedulers.ByType(shuffle_types::Bool, shuffle_agents::Bool, agent_union)

A non-allocating scheduler 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 Schedulers.fastest).

  • agent_union is a Union of all valid agent types (as passed to ABM)
source
Schedulers.ByType((C, B, A), shuffle_agents::Bool)

A non-allocating scheduler that activates agents by type in specified order (since Unions are not order preserving). shuffle_agents = true randomizes the order of agents within each group.

source

Advanced scheduling

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
+return df_agent, df_model

(here until and should_we_collect are internal functions)

run! uses the following functions:

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.

source
Agents.datanameFunction
dataname(k) → name

Return the name of the column of the i-th collected data where k = adata[i] (or mdata[i]). dataname also accepts tuples with aggregate and conditional values.

source

Schedulers

Agents.SchedulersModule
Schedulers

Submodule containing all predefined schedulers of Agents.jl and the scheduling API. Schedulers have a very simple interface. They 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. You can also use the function schedule(model) to obtain the scheduled ID list, if you prefer to write your own step!-like loop.

See also Advanced scheduling for making more advanced schedulers.

Notice that schedulers can be given directly to model creation, and thus become the "default" scheduler a model uses, but they can just as easily be incorporated in a model_step! function as shown in Advanced stepping. The scheduler that is stored in the model is only meaningful if an agent-stepping function is defined for step! or run!, otherwise a user decides a scheduler in the model-stepping function, as illustrated in the Advanced stepping part of the tutorial.

source

Predefined schedulers

Some useful schedulers are available below as part of the Agents.jl API:

Agents.Schedulers.fastestFunction
Schedulers.fastest

A scheduler that activates 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.

source
Agents.Schedulers.ByIDType
Schedulers.ByID()

A non-allocating scheduler that activates all agents at each step according to their id.

source
Agents.Schedulers.RandomlyType
Schedulers.Randomly()

A non-allocating scheduler that activates all agents once per step in a random order. Different random ordering is used at each different step.

source
Agents.Schedulers.ByPropertyType
Schedulers.ByProperty(property)

A non-allocating scheduler that at each step activates the agents in an order dictated by their property, with agents with greater property acting first. property can be a Symbol, which just dictates which field of the agents to compare, or a function which inputs an agent and outputs a real number.

source
Agents.Schedulers.ByTypeType
Schedulers.ByType(shuffle_types::Bool, shuffle_agents::Bool, agent_union)

A non-allocating scheduler 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 Schedulers.fastest).

  • agent_union is a Union of all valid agent types (as passed to ABM)
source
Schedulers.ByType((C, B, A), shuffle_agents::Bool)

A non-allocating scheduler that activates agents by type in specified order (since Unions are not order preserving). shuffle_agents = true randomizes the order of agents within each group.

source

Advanced scheduling

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)
@@ -110,7 +110,7 @@
         return ids
     end
 end

and pass it to e.g. step! by initializing it

ms = MyScheduler(100, 0.5)
-step!(model, agentstep, modelstep, 100; scheduler = ms)

Ensemble runs and Parallelization

Agents.ensemblerun!Function
ensemblerun!(models::Vector, agent_step!, model_step!, n; kwargs...)

Perform an ensemble simulation of run! for all model ∈ models. Each model should be a (different) instance of an AgentBasedModel but probably initialized with a different random seed or different initial agent distribution. All models obey the same rules agent_step!, model_step! and are evolved for n.

Similarly to run! this function will collect data. It will furthermore add one additional column to the dataframe called :ensemble, which has an integer value counting the ensemble member. The function returns agent_df, model_df, models.

If you want to scan parameters and at the same time run multiple simulations at each parameter combination, simply use seed as a parameter, and use that parameter to tune the model's initial random seed and/or agent distribution.

See example usage in Schelling's segregation model.

Keywords

The following keywords modify the ensemblerun! function:

  • parallel::Bool = false whether Distributed.pmap is invoked to run simulations in parallel. This must be used in conjunction with @everywhere (see Performance Tips).
  • showprogress::Bool = false whether a progressbar will be displayed to indicate % runs finished.

All other keywords are propagated to run! as-is.

source
ensemblerun!(generator, agent_step!, model_step!, n; kwargs...)

Generate many ABMs and propagate them into ensemblerun!(models, ...) using the provided generator which is a one-argument function whose input is a seed.

This method has additional keywords ensemble = 5, seeds = rand(UInt32, ensemble).

source

How to use Distributed

To use the parallel=true option of ensemblerun! you need to load Agents and define your fundamental types at all processors. How to do this is shown in Ensembles and distributed computing section of Schelling's Segregation Model example. See also the Performance Tips page for parallelization.

Path-finding

Agents.PathfindingModule
Pathfinding

Submodule containing functionality for path-finding based on the A* algorithm. Currently available for GridSpace and ContinuousSpace. Discretization of ContinuousSpace is taken care of internally.

You can enable path-finding and set its options by creating an instance of a Pathfinding.AStar struct. This must be passed to the relevant pathfinding functions during the simulation. Call plan_route! to set the destination for an agent. This triggers the algorithm to calculate a path from the agent's current position to the one specified. You can alternatively use plan_best_route! to choose the best target from a list. Once a target has been set, you can move an agent one step along its precalculated path using the move_along_route! function.

Refer to the Maze Solver, Mountain Runners and Rabbit, Fox, Hawk examples using path-finding and see the available functions below as well.

source
Agents.Pathfinding.AStarType
Pathfinding.AStar(space; kwargs...)

Enables pathfinding for agents in the provided space (which can be a GridSpace or ContinuousSpace) using the A* algorithm. This struct must be passed into any pathfinding functions.

For ContinuousSpace, a walkmap or instance of PenaltyMap must be provided to specify the level of discretisation of the space.

Keywords

  • diagonal_movement = true specifies if movement can be to diagonal neighbors of a tile, or only orthogonal neighbors. Only available for GridSpace
  • admissibility = 0.0 allows the algorithm to approximate paths to speed up pathfinding. A value of admissibility allows paths with at most (1+admissibility) times the optimal length.
  • walkmap = trues(size(space)) specifies the (un)walkable positions of the space. If specified, it should be a BitArray of the same size as the corresponding GridSpace. By default, agents can walk anywhere in the space.
  • cost_metric = DirectDistance{D}() is an instance of a cost metric and specifies the metric used to approximate the distance between any two points.

Utilization of all features of AStar occurs in the 3D Mixed-Agent Ecosystem with Pathfinding example.

source
Agents.Pathfinding.nearby_walkableFunction
Pathfinding.nearby_walkable(position, model::ABM{<:GridSpace{D}}, pathfinder::AStar{D}, r = 1)

Return an iterator over all nearby_positions within "radius" r of the given position (excluding position), which are walkable as specified by the given pathfinder.

source
Agents.Pathfinding.random_walkableFunction
Pathfinding.random_walkable(model, pathfinder::AStar{D})

Return a random position in the given model that is walkable as specified by the given pathfinder.

source
Pathfinding.random_walkable(pos, model::ABM{<:ContinuousSpace{D}}, pathfinder::AStar{D}, r = 1.0)

Return a random position within radius r of pos which is walkable, as specified by pathfinder. Return pos if no such position exists.

source

Pathfinding Metrics

Agents.Pathfinding.DirectDistanceType
Pathfinding.DirectDistance{D}([direction_costs::Vector{Int}]) <: CostMetric{D}

Distance is approximated as the shortest path between the two points, provided the walkable property of Pathfinding.AStar allows. Optionally provide a Vector{Int} that represents the cost of going from a tile to the neighboring tile on the i dimensional diagonal (default is 10√i).

If diagonal_movement=false in Pathfinding.AStar, neighbors in diagonal positions will be excluded. Cost defaults to the first value of the provided vector.

source
Agents.Pathfinding.MaxDistanceType
Pathfinding.MaxDistance{D}() <: CostMetric{D}

Distance between two tiles is approximated as the maximum of absolute difference in coordinates between them.

source
Agents.Pathfinding.PenaltyMapType
Pathfinding.PenaltyMap(pmap::Array{Int,D} [, base_metric::CostMetric]) <: CostMetric{D}

Distance between two positions is the sum of the shortest distance between them and the absolute difference in penalty.

A penalty map (pmap) is required. For pathfinding in GridSpace, this should be the same dimensions as the space. For pathfinding in ContinuousSpace, the size of this map determines the granularity of the underlying grid, and should agree with the size of the walkable map.

Distance is calculated using Pathfinding.DirectDistance by default, and can be changed by specifying base_metric.

An example usage can be found in Mountain Runners.

source

Building a custom metric is straightforward, if the provided ones do not suit your purpose. See the Developer Docs for details.

Save, Load, Checkpoints

There may be scenarios where interacting with data in the form of files is necessary. The following functions provide an interface to save/load data to/from files.

Agents.AgentsIO.save_checkpointFunction
AgentsIO.save_checkpoint(filename, model::ABM)

Write the entire model to file specified by filename. The following points should be considered before using this functionality:

  • OpenStreetMap data is not saved. The path to the map should be specified when loading the model using the map keyword of AgentsIO.load_checkpoint.
  • Functions are not saved, including stepping functions, schedulers, and update_vel!. The last two can be provided to AgentsIO.load_checkpoint using the appropriate keyword arguments.
source
Agents.AgentsIO.load_checkpointFunction
AgentsIO.load_checkpoint(filename; kwargs...)

Load the model saved to the file specified by filename.

Keywords

  • scheduler = Schedulers.fastest specifies what scheduler should be used for the model.
  • warn = true can be used to disable warnings from type checks on the agent type.

ContinuousSpace specific:

  • update_vel! specifies a function that should be used to update each agent's velocity before it is moved. Refer to ContinuousSpace for details.

OpenStreetMapSpace specific:

  • map is a path to the OpenStreetMap to be used for the space. This is a required parameter if the space is OpenStreetMapSpace.
  • use_cache = false, trim_to_connected_graph = true refer to OpenStreetMapSpace
source
Agents.AgentsIO.populate_from_csv!Function
AgentsIO.populate_from_csv!(model, filename [, agent_type, col_map]; row_number_is_id, kwargs...)

Populate the given model using CSV data contained in filename. Use agent_type to specify the type of agent to create (In the case of multi-agent models) or a function that returns an agent to add to the model. The CSV row is splatted into the agent_type constructor/function.

col_map is a Dict{Symbol,Int} specifying a mapping of keyword-arguments to row number. If col_map is specified, the specified data is splatted as keyword arguments.

The keyword row_number_is_id = false specifies whether the row number will be passed as the first argument (or as id keyword) to agent_type.

Any other keyword arguments are forwarded to CSV.Rows. If the types keyword is not specified and agent_type is a struct, then the mapping from struct field to type will be used. Tuple{...} fields will be suffixed with _1, _2, ... similarly to AgentsIO.dump_to_csv

For example,

struct Foo <: AbstractAgent
+step!(model, agentstep, modelstep, 100; scheduler = ms)

Ensemble runs and Parallelization

Agents.ensemblerun!Function
ensemblerun!(models::Vector, agent_step!, model_step!, n; kwargs...)

Perform an ensemble simulation of run! for all model ∈ models. Each model should be a (different) instance of an AgentBasedModel but probably initialized with a different random seed or different initial agent distribution. All models obey the same rules agent_step!, model_step! and are evolved for n.

Similarly to run! this function will collect data. It will furthermore add one additional column to the dataframe called :ensemble, which has an integer value counting the ensemble member. The function returns agent_df, model_df, models.

If you want to scan parameters and at the same time run multiple simulations at each parameter combination, simply use seed as a parameter, and use that parameter to tune the model's initial random seed and/or agent distribution.

See example usage in Schelling's segregation model.

Keywords

The following keywords modify the ensemblerun! function:

  • parallel::Bool = false whether Distributed.pmap is invoked to run simulations in parallel. This must be used in conjunction with @everywhere (see Performance Tips).
  • showprogress::Bool = false whether a progressbar will be displayed to indicate % runs finished.

All other keywords are propagated to run! as-is.

source
ensemblerun!(generator, agent_step!, model_step!, n; kwargs...)

Generate many ABMs and propagate them into ensemblerun!(models, ...) using the provided generator which is a one-argument function whose input is a seed.

This method has additional keywords ensemble = 5, seeds = rand(UInt32, ensemble).

source

How to use Distributed

To use the parallel=true option of ensemblerun! you need to load Agents and define your fundamental types at all processors. How to do this is shown in Ensembles and distributed computing section of Schelling's Segregation Model example. See also the Performance Tips page for parallelization.

Path-finding

Agents.PathfindingModule
Pathfinding

Submodule containing functionality for path-finding based on the A* algorithm. Currently available for GridSpace and ContinuousSpace. Discretization of ContinuousSpace is taken care of internally.

You can enable path-finding and set its options by creating an instance of a Pathfinding.AStar struct. This must be passed to the relevant pathfinding functions during the simulation. Call plan_route! to set the destination for an agent. This triggers the algorithm to calculate a path from the agent's current position to the one specified. You can alternatively use plan_best_route! to choose the best target from a list. Once a target has been set, you can move an agent one step along its precalculated path using the move_along_route! function.

Refer to the Maze Solver, Mountain Runners and Rabbit, Fox, Hawk examples using path-finding and see the available functions below as well.

source
Agents.Pathfinding.AStarType
Pathfinding.AStar(space; kwargs...)

Enables pathfinding for agents in the provided space (which can be a GridSpace or ContinuousSpace) using the A* algorithm. This struct must be passed into any pathfinding functions.

For ContinuousSpace, a walkmap or instance of PenaltyMap must be provided to specify the level of discretisation of the space.

Keywords

  • diagonal_movement = true specifies if movement can be to diagonal neighbors of a tile, or only orthogonal neighbors. Only available for GridSpace
  • admissibility = 0.0 allows the algorithm to approximate paths to speed up pathfinding. A value of admissibility allows paths with at most (1+admissibility) times the optimal length.
  • walkmap = trues(size(space)) specifies the (un)walkable positions of the space. If specified, it should be a BitArray of the same size as the corresponding GridSpace. By default, agents can walk anywhere in the space.
  • cost_metric = DirectDistance{D}() is an instance of a cost metric and specifies the metric used to approximate the distance between any two points.

Utilization of all features of AStar occurs in the 3D Mixed-Agent Ecosystem with Pathfinding example.

source
Agents.Pathfinding.nearby_walkableFunction
Pathfinding.nearby_walkable(position, model::ABM{<:GridSpace{D}}, pathfinder::AStar{D}, r = 1)

Return an iterator over all nearby_positions within "radius" r of the given position (excluding position), which are walkable as specified by the given pathfinder.

source
Agents.Pathfinding.random_walkableFunction
Pathfinding.random_walkable(model, pathfinder::AStar{D})

Return a random position in the given model that is walkable as specified by the given pathfinder.

source
Pathfinding.random_walkable(pos, model::ABM{<:ContinuousSpace{D}}, pathfinder::AStar{D}, r = 1.0)

Return a random position within radius r of pos which is walkable, as specified by pathfinder. Return pos if no such position exists.

source

Pathfinding Metrics

Agents.Pathfinding.DirectDistanceType
Pathfinding.DirectDistance{D}([direction_costs::Vector{Int}]) <: CostMetric{D}

Distance is approximated as the shortest path between the two points, provided the walkable property of Pathfinding.AStar allows. Optionally provide a Vector{Int} that represents the cost of going from a tile to the neighboring tile on the i dimensional diagonal (default is 10√i).

If diagonal_movement=false in Pathfinding.AStar, neighbors in diagonal positions will be excluded. Cost defaults to the first value of the provided vector.

source
Agents.Pathfinding.MaxDistanceType
Pathfinding.MaxDistance{D}() <: CostMetric{D}

Distance between two tiles is approximated as the maximum of absolute difference in coordinates between them.

source
Agents.Pathfinding.PenaltyMapType
Pathfinding.PenaltyMap(pmap::Array{Int,D} [, base_metric::CostMetric]) <: CostMetric{D}

Distance between two positions is the sum of the shortest distance between them and the absolute difference in penalty.

A penalty map (pmap) is required. For pathfinding in GridSpace, this should be the same dimensions as the space. For pathfinding in ContinuousSpace, the size of this map determines the granularity of the underlying grid, and should agree with the size of the walkable map.

Distance is calculated using Pathfinding.DirectDistance by default, and can be changed by specifying base_metric.

An example usage can be found in Mountain Runners.

source

Building a custom metric is straightforward, if the provided ones do not suit your purpose. See the Developer Docs for details.

Save, Load, Checkpoints

There may be scenarios where interacting with data in the form of files is necessary. The following functions provide an interface to save/load data to/from files.

Agents.AgentsIO.save_checkpointFunction
AgentsIO.save_checkpoint(filename, model::ABM)

Write the entire model to file specified by filename. The following points should be considered before using this functionality:

  • OpenStreetMap data is not saved. The path to the map should be specified when loading the model using the map keyword of AgentsIO.load_checkpoint.
  • Functions are not saved, including stepping functions, schedulers, and update_vel!. The last two can be provided to AgentsIO.load_checkpoint using the appropriate keyword arguments.
source
Agents.AgentsIO.load_checkpointFunction
AgentsIO.load_checkpoint(filename; kwargs...)

Load the model saved to the file specified by filename.

Keywords

  • scheduler = Schedulers.fastest specifies what scheduler should be used for the model.
  • warn = true can be used to disable warnings from type checks on the agent type.

ContinuousSpace specific:

  • update_vel! specifies a function that should be used to update each agent's velocity before it is moved. Refer to ContinuousSpace for details.

OpenStreetMapSpace specific:

  • map is a path to the OpenStreetMap to be used for the space. This is a required parameter if the space is OpenStreetMapSpace.
  • use_cache = false, trim_to_connected_graph = true refer to OpenStreetMapSpace
source
Agents.AgentsIO.populate_from_csv!Function
AgentsIO.populate_from_csv!(model, filename [, agent_type, col_map]; row_number_is_id, kwargs...)

Populate the given model using CSV data contained in filename. Use agent_type to specify the type of agent to create (In the case of multi-agent models) or a function that returns an agent to add to the model. The CSV row is splatted into the agent_type constructor/function.

col_map is a Dict{Symbol,Int} specifying a mapping of keyword-arguments to row number. If col_map is specified, the specified data is splatted as keyword arguments.

The keyword row_number_is_id = false specifies whether the row number will be passed as the first argument (or as id keyword) to agent_type.

Any other keyword arguments are forwarded to CSV.Rows. If the types keyword is not specified and agent_type is a struct, then the mapping from struct field to type will be used. Tuple{...} fields will be suffixed with _1, _2, ... similarly to AgentsIO.dump_to_csv

For example,

struct Foo <: AbstractAgent
     id::Int
     pos::NTuple{2,Int}
     foo::Tuple{Int,String}
@@ -123,7 +123,7 @@
     :pos_2 => Int,
     :foo_1 => Int,
     :foo_2 => String,
-)

It is not necessary for all these fields to be present as columns in the CSV. Any column names that match will be converted to the appropriate type. There should exist a constructor for Foo taking the appropriate combination of fields as parameters.

If "test.csv" contains the following columns: pos_1, pos_2, foo_1, foo_2, then model can be populated as AgentsIO.populate_from_csv!(model, "test.csv"; row_number_is_id = true).

source
Agents.AgentsIO.dump_to_csvFunction
AgentsIO.dump_to_csv(filename, agents [, fields]; kwargs...)

Dump agents to the CSV file specified by filename. agents is any iterable sequence of types, such as from allagents. fields is an iterable sequence of Symbols specifying which fields of each agent are dumped. If not explicitly specified, it is automatically inferred using eltype(agents). All kwargs... are forwarded to CSV.write.

All Tuple{...} fields are flattened to multiple columns suffixed by _1, _2... similarly to AgentsIO.populate_from_csv!

For example,

struct Foo <: AbstractAgent
+)

It is not necessary for all these fields to be present as columns in the CSV. Any column names that match will be converted to the appropriate type. There should exist a constructor for Foo taking the appropriate combination of fields as parameters.

If "test.csv" contains the following columns: pos_1, pos_2, foo_1, foo_2, then model can be populated as AgentsIO.populate_from_csv!(model, "test.csv"; row_number_is_id = true).

source
Agents.AgentsIO.dump_to_csvFunction
AgentsIO.dump_to_csv(filename, agents [, fields]; kwargs...)

Dump agents to the CSV file specified by filename. agents is any iterable sequence of types, such as from allagents. fields is an iterable sequence of Symbols specifying which fields of each agent are dumped. If not explicitly specified, it is automatically inferred using eltype(agents). All kwargs... are forwarded to CSV.write.

All Tuple{...} fields are flattened to multiple columns suffixed by _1, _2... similarly to AgentsIO.populate_from_csv!

For example,

struct Foo <: AbstractAgent
     id::Int
     pos::NTuple{2,Int}
     foo::Tuple{Int,String}
@@ -131,5 +131,5 @@
 
 model = ABM(Foo, ...)
 ...
-AgentsIO.dump_to_csv("test.csv", allagents(model))

The resultant "test.csv" file will contain the following columns: id, pos_1, pos_2, foo_1, foo_2.

source

It is also possible to write data to file at predefined intervals while running your model, instead of storing it in memory:

Agents.offline_run!Function
offline_run!(model, agent_step! [, model_step!], n::Integer; kwargs...)
-offline_run!(model, agent_step!, model_step!, n::Function; kwargs...)

Do the same as run, but instead of collecting the whole run into an in-memory dataframe, write the output to a file after collecting data writing_interval times and empty the dataframe after each write. Useful when the amount of collected data is expected to exceed the memory available during execution.

Keywords

  • backend=:csv : backend to use for writing data. Currently supported backends: :csv, :arrow
  • adata_filename="adata.$backend" : a file to write agent data on. Appends to the file if it already exists, otherwise creates the file.
  • mdata_filename="mdata.$backend": a file to write the model data on. Appends to the file if it already exists, otherwise creates the file.
  • writing_interval=1 : write to file every writing_interval times data collection is triggered. If the when keyword is not set, this corresponds to writing to file every writing_interval steps; otherwise, the data will be written every writing_interval times the when condition is satisfied (the same applies to when_model).
source

In case you require custom serialization for model properties, refer to the Developer Docs for details.

+AgentsIO.dump_to_csv("test.csv", allagents(model))

The resultant "test.csv" file will contain the following columns: id, pos_1, pos_2, foo_1, foo_2.

source

It is also possible to write data to file at predefined intervals while running your model, instead of storing it in memory:

Agents.offline_run!Function
offline_run!(model, agent_step! [, model_step!], n::Integer; kwargs...)
+offline_run!(model, agent_step!, model_step!, n::Function; kwargs...)

Do the same as run, but instead of collecting the whole run into an in-memory dataframe, write the output to a file after collecting data writing_interval times and empty the dataframe after each write. Useful when the amount of collected data is expected to exceed the memory available during execution.

Keywords

  • backend=:csv : backend to use for writing data. Currently supported backends: :csv, :arrow
  • adata_filename="adata.$backend" : a file to write agent data on. Appends to the file if it already exists, otherwise creates the file.
  • mdata_filename="mdata.$backend": a file to write the model data on. Appends to the file if it already exists, otherwise creates the file.
  • writing_interval=1 : write to file every writing_interval times data collection is triggered. If the when keyword is not set, this corresponds to writing to file every writing_interval steps; otherwise, the data will be written every writing_interval times the when condition is satisfied (the same applies to when_model).
source

In case you require custom serialization for model properties, refer to the Developer Docs for details.

diff --git a/previews/PR887/comparison/index.html b/previews/PR887/comparison/index.html index f4d428b662..4cac29f952 100644 --- a/previews/PR887/comparison/index.html +++ b/previews/PR887/comparison/index.html @@ -1,2 +1,2 @@ -ABM Framework Comparison · Agents.jl

ABM Framework Comparison

Many agent-based modeling frameworks have been constructed to ease the process of building and analyzing ABMs (see here for a review). Notable examples are NetLogo, Repast, MASON, and Mesa.

We used the following models for the comparison:

  • Predator-prey dynamics (Wolf Sheep Grass), a GridSpace model, which requires agents to be added, removed and moved; as well as identify properties of neighbouring positions.
  • The Flocking model (Flocking), a ContinuousSpace model, chosen over other models to include a MASON benchmark. Agents must move in accordance with social rules over the space.
  • The Forest fire model, provides comparisons for cellular automata type ABMs (i.e. when agents do not move). NOTE: The Agents.jl implementation of this model has been changed in v4.0 to be directly comparable to Mesa and NetLogo. As a consequence it no longer follows the original rule-set.
  • Schelling's-segregation-model (Schelling), an additional GridSpace model to compare with MASON. Simpler rules than Wolf Sheep Grass.

The results are characterised in two ways: how long it took each model to perform the same scenario (initial conditions, grid size, run length etc. are the same across all frameworks), and how many lines of code (LOC) it took to describe each model and its dynamics. We use this result as a metric to represent the complexity of learning and working with a framework.

Time taken is presented in normalised units, measured against the runtime of Agents.jl. In other words: the results can only vary slightly from the ones presented here with a different hardware.

For LOC, we use the following convention: code is formatted using standard practices & linting for the associated language. Documentation strings and in-line comments (residing on lines of their own) are discarded, as well as any benchmark infrastructure. NetLogo is assigned two values since its files have a code base section and an encoding of the GUI. Since many parameters live in the GUI, we must take this into account. Thus 375 (785) in a NetLogo count means 375 lines in the code section, 785 lines total in the file. An additional complication to this value in NetLogo is that it stores plotting information (colours, shapes, sizes) as agent properties, and as such the number outside of the bracket may be slightly inflated.

You can find the latest results in the README.md of the ABMFrameworkComparisons repository where you can find also the details on the parameters used for each comparison in the DECLARATION.md files inside each model subfolder.

Across all four models, Agents.jl's performance is exceptional whilst using the least amount of code. This removes many frustrating barriers-to-entry for new users, and streamlines the development process for established ones.

Table-based comparison

In our paper discussing Agents.jl, we compiled a comparison over a large list of features and metrics from the four frameworks discussed above. They are shown below in a table-based format:

Table 1 Table 1 continued

+ABM Framework Comparison · Agents.jl

ABM Framework Comparison

Many agent-based modeling frameworks have been constructed to ease the process of building and analyzing ABMs (see here for a review). Notable examples are NetLogo, Repast, MASON, and Mesa.

In the ABMFrameworkComparisons repository we compare Agents.jl with many other popular alternatives, to assess where Agents.jl excels and also may need some future improvement.

The results are characterised in two ways: how long it took each model to perform the same scenario (initial conditions, grid size, run length etc. are the same across all frameworks), and how many lines of code (LOC) it took to describe each model and its dynamics. We use this result as a metric to represent the complexity of learning and working with a framework.

Time taken is presented in normalised units, measured against the runtime of Agents.jl. In other words: the results can only vary slightly from the ones presented here with a different hardware.

For LOC, we use the following convention: code is formatted using standard practices & linting for the associated language. Documentation strings and in-line comments (residing on lines of their own) are discarded, as well as any benchmark infrastructure. NetLogo is assigned two values since its files have a code base section and an encoding of the GUI. Since many parameters live in the GUI, we must take this into account. Thus 375 (785) in a NetLogo count means 375 lines in the code section, 785 lines total in the file. An additional complication to this value in NetLogo is that it stores plotting information (colours, shapes, sizes) as agent properties, and as such the number outside of the bracket may be slightly inflated.

The latest results are available at the README.md of the ABMFrameworkComparisons repository, where you can also find inside each model subfolder a DECLARATION.md file with the details on the parameters used for each comparison.

In the majority of cases, Agents.jl's performance is exceptional whilst using the least amount of code. This removes many frustrating barriers-to-entry for new users, and streamlines the development process for established ones.

Table-based comparison

In our paper discussing Agents.jl, we compiled a comparison over a large list of features and metrics from the four frameworks discussed above. They are shown below in a table-based format:

Table 1 Table 1 continued

diff --git a/previews/PR887/devdocs/index.html b/previews/PR887/devdocs/index.html index f9fbe66449..81eabac6f8 100644 --- a/previews/PR887/devdocs/index.html +++ b/previews/PR887/devdocs/index.html @@ -1,2 +1,2 @@ -Developer Docs · Agents.jl

Developer Docs

Cloning the repository

Since we include documentation with many animated gifs and videos in the repository, a standard clone can be larger than expected. If you wish to do any development work, it is better to use

git clone https://github.com/JuliaDynamics/Agents.jl.git --single-branch

Creating a new space type

Creating a new space type within Agents.jl is quite simple and requires the extension of only 5 methods to support the entire Agents.jl API. The exact specifications on how to create a new space type are contained within the source file: src/core/space_interaction_API.jl.

In principle, the following should be done:

  1. Think about what the agent position type should be. Add this type to the ValidPos union type in src/core/model_abstract.jl.
  2. Think about how the space type will keep track of the agent positions, so that it is possible to implement the function nearby_ids.
  3. Implement the struct that represents your new space, while making it a subtype of AbstractSpace.
  4. Extend random_position(model).
  5. Extend add_agent_to_space!(agent, model), remove_agent_from_space!(agent, model). This already provides access to add_agent!, kill_agent! and move_agent!.
  6. Extend nearby_ids(position, model, r).
  7. Create a new "minimal" agent type to be used with @agent (see the source code of GraphAgent for an example).

And that's it! Every function of the main API will now work. In some situations you might want to explicitly extend other functions such as move_agent! for performance reasons.

Designing a new Pathfinder Cost Metric

To define a new cost metric, simply make a struct that subtypes CostMetric and provide a delta_cost function for it. These methods work solely for A* at present, but will be available for other pathfinder algorithms in the future.

Agents.Pathfinding.CostMetricType
Pathfinding.CostMetric{D}

An abstract type representing a metric that measures the approximate cost of travelling between two points in a D dimensional grid.

source
Agents.Pathfinding.delta_costFunction
Pathfinding.delta_cost(pathfinder::GridPathfinder{D}, metric::M, from, to) where {M<:CostMetric}

Calculate an approximation for the cost of travelling from from to to (both of type NTuple{N,Int}. Expects a return value of Float64.

source

Implementing custom serialization

For model properties

Custom serialization may be required if your properties contain non-serializable data, such as functions. Alternatively, if it is possible to recalculate some properties during deserialization it may be space-efficient to not save them. To implement custom serialization, define methods for the to_serializable and from_serializable functions:

Agents.AgentsIO.to_serializableFunction
AgentsIO.to_serializable(t)

Return the serializable form of the passed value. This defaults to the value itself, unless a more specific method is defined. Define a method for this function and for AgentsIO.from_serializable if you need custom serialization for model properties. This also enables passing keyword arguments to AgentsIO.load_checkpoint and having access to them during deserialization of the properties. Some possible scenarios where this may be required are:

  • Your properties contain functions (or any type not supported by JLD2.jl). These may not be (de)serialized correctly. This could result in checkpoint files that cannot be loaded back in, or contain reconstructed types that do not retain their data/functionality.
  • Your properties contain data that can be recalculated during deserialization. Omitting such properties can reduce the size of the checkpoint file, at the expense of some extra computation at deserialization.

If your model properties do not fall in the above scenarios, you do not need to use this function.

This function, and AgentsIO.from_serializable is not called recursively on every type/value during serialization. The final serialization functionality is enabled by JLD2.jl. To define custom serialization for every occurrence of a specific type (such as agent structs), refer to the Custom Serialization section of JLD2.jl documentation.

source
Agents.AgentsIO.from_serializableFunction
AgentsIO.from_serializable(t; kwargs...)

Given a value in its serializable form, return the original version. This defaults to the value itself, unless a more specific method is defined. Define a method for this function and for AgentsIO.to_serializable if you need custom serialization for model properties. This also enables passing keyword arguments to AgentsIO.load_checkpoint and having access to them through kwargs.

Refer to AgentsIO.to_serializable for more info.

source

For agent structs

Similarly to model properties, you may need to implement custom serialization for agent structs. from_serializable and to_serializable are not called during (de)serialization of agent structs. Instead, JLD2's custom serialization functionality should be used. All instances of the agent struct will be converted to and from the specified type during serialization. For OpenStreetMap agents, the position, destination and route are saved separately. These values will be loaded back in during deserialization of the model and override any values in the agent structs. To save space, the agents in the serialized model will have empty route fields.

OpenStreetMapSpace internals

Details about the internal details of the OSMSpace are discussed in the docstring of OSM.OpenStreetMapPath.

Benchmarking

As Agents.jl is developed we want to monitor code efficiency through benchmarks. A benchmark is a function or other bit of code whose execution is timed so that developers and users can keep track of how long different API functions take when used in various ways. Individual benchmarks can be organized into suites of benchmark tests. See the benchmark directory to view Agents.jl's benchmark suites. Follow these examples to add your own benchmarks for your Agents.jl contributions. See the BenchmarkTools quickstart guide, toy example benchmark suite, and the BenchmarkTools.jl manual for more information on how to write your own benchmarks.

Creating a new AgentBasedModel implementation

The interface defined by AgentBasedModel, that needs to be satisfied by new implementations, is very small. It is contained in the file src/core/model_abstract.jl.

+Developer Docs · Agents.jl

Developer Docs

Cloning the repository

Since we include documentation with many animated gifs and videos in the repository, a standard clone can be larger than expected. If you wish to do any development work, it is better to use

git clone https://github.com/JuliaDynamics/Agents.jl.git --single-branch

Creating a new space type

Creating a new space type within Agents.jl is quite simple and requires the extension of only 5 methods to support the entire Agents.jl API. The exact specifications on how to create a new space type are contained within the source file: src/core/space_interaction_API.jl.

In principle, the following should be done:

  1. Think about what the agent position type should be. Add this type to the ValidPos union type in src/core/model_abstract.jl.
  2. Think about how the space type will keep track of the agent positions, so that it is possible to implement the function nearby_ids.
  3. Implement the struct that represents your new space, while making it a subtype of AbstractSpace.
  4. Extend random_position(model).
  5. Extend add_agent_to_space!(agent, model), remove_agent_from_space!(agent, model). This already provides access to add_agent!, kill_agent! and move_agent!.
  6. Extend nearby_ids(position, model, r).
  7. Create a new "minimal" agent type to be used with @agent (see the source code of GraphAgent for an example).

And that's it! Every function of the main API will now work. In some situations you might want to explicitly extend other functions such as move_agent! for performance reasons.

Designing a new Pathfinder Cost Metric

To define a new cost metric, simply make a struct that subtypes CostMetric and provide a delta_cost function for it. These methods work solely for A* at present, but will be available for other pathfinder algorithms in the future.

Agents.Pathfinding.CostMetricType
Pathfinding.CostMetric{D}

An abstract type representing a metric that measures the approximate cost of travelling between two points in a D dimensional grid.

source
Agents.Pathfinding.delta_costFunction
Pathfinding.delta_cost(pathfinder::GridPathfinder{D}, metric::M, from, to) where {M<:CostMetric}

Calculate an approximation for the cost of travelling from from to to (both of type NTuple{N,Int}. Expects a return value of Float64.

source

Implementing custom serialization

For model properties

Custom serialization may be required if your properties contain non-serializable data, such as functions. Alternatively, if it is possible to recalculate some properties during deserialization it may be space-efficient to not save them. To implement custom serialization, define methods for the to_serializable and from_serializable functions:

Agents.AgentsIO.to_serializableFunction
AgentsIO.to_serializable(t)

Return the serializable form of the passed value. This defaults to the value itself, unless a more specific method is defined. Define a method for this function and for AgentsIO.from_serializable if you need custom serialization for model properties. This also enables passing keyword arguments to AgentsIO.load_checkpoint and having access to them during deserialization of the properties. Some possible scenarios where this may be required are:

  • Your properties contain functions (or any type not supported by JLD2.jl). These may not be (de)serialized correctly. This could result in checkpoint files that cannot be loaded back in, or contain reconstructed types that do not retain their data/functionality.
  • Your properties contain data that can be recalculated during deserialization. Omitting such properties can reduce the size of the checkpoint file, at the expense of some extra computation at deserialization.

If your model properties do not fall in the above scenarios, you do not need to use this function.

This function, and AgentsIO.from_serializable is not called recursively on every type/value during serialization. The final serialization functionality is enabled by JLD2.jl. To define custom serialization for every occurrence of a specific type (such as agent structs), refer to the Custom Serialization section of JLD2.jl documentation.

source
Agents.AgentsIO.from_serializableFunction
AgentsIO.from_serializable(t; kwargs...)

Given a value in its serializable form, return the original version. This defaults to the value itself, unless a more specific method is defined. Define a method for this function and for AgentsIO.to_serializable if you need custom serialization for model properties. This also enables passing keyword arguments to AgentsIO.load_checkpoint and having access to them through kwargs.

Refer to AgentsIO.to_serializable for more info.

source

For agent structs

Similarly to model properties, you may need to implement custom serialization for agent structs. from_serializable and to_serializable are not called during (de)serialization of agent structs. Instead, JLD2's custom serialization functionality should be used. All instances of the agent struct will be converted to and from the specified type during serialization. For OpenStreetMap agents, the position, destination and route are saved separately. These values will be loaded back in during deserialization of the model and override any values in the agent structs. To save space, the agents in the serialized model will have empty route fields.

OpenStreetMapSpace internals

Details about the internal details of the OSMSpace are discussed in the docstring of OSM.OpenStreetMapPath.

Benchmarking

As Agents.jl is developed we want to monitor code efficiency through benchmarks. A benchmark is a function or other bit of code whose execution is timed so that developers and users can keep track of how long different API functions take when used in various ways. Individual benchmarks can be organized into suites of benchmark tests. See the benchmark directory to view Agents.jl's benchmark suites. Follow these examples to add your own benchmarks for your Agents.jl contributions. See the BenchmarkTools quickstart guide, toy example benchmark suite, and the BenchmarkTools.jl manual for more information on how to write your own benchmarks.

Creating a new AgentBasedModel implementation

The interface defined by AgentBasedModel, that needs to be satisfied by new implementations, is very small. It is contained in the file src/core/model_abstract.jl.

diff --git a/previews/PR887/examples/agents_visualizations/index.html b/previews/PR887/examples/agents_visualizations/index.html index 4c8ccd0b64..ea8bfe024e 100644 --- a/previews/PR887/examples/agents_visualizations/index.html +++ b/previews/PR887/examples/agents_visualizations/index.html @@ -19,7 +19,7 @@ am = '✿' # agent marker scatterkwargs = (strokewidth = 1.0,) # add stroke around each agent fig, ax, abmobs = abmplot(model; ac = daisycolor, as, am, scatterkwargs) -fig

Besides agents, we can also plot spatial properties as a heatmap. Here we plot the temperature of the planet by providing the name of the property as the "heat array":

heatarray = :temperature
+fig

Besides agents, we can also plot spatial properties as a heatmap. Here we plot the temperature of the planet by providing the name of the property as the "heat array":

heatarray = :temperature
 heatkwargs = (colorrange = (-20, 60), colormap = :thermal)
 plotkwargs = (;
     ac = daisycolor, as, am,
@@ -28,20 +28,20 @@
 )
 
 fig, ax, abmobs = abmplot(model; plotkwargs...)
-fig
Agents.abmplotFunction
abmplot(model::ABM; kwargs...) → fig, ax, abmobs
+fig
Agents.abmplotFunction
abmplot(model::ABM; kwargs...) → fig, ax, abmobs
 abmplot!(ax::Axis/Axis3, model::ABM; kwargs...) → abmobs

Plot an agent based model by plotting each individual agent as a marker and using the agent's position field as its location on the plot. The same function is used to make custom composite plots and animations for the model evolution using the returned abmobs. abmplot is also used to launch interactive GUIs for evolving agent based models, see "Interactivity" below.

See also abmvideo and abmexploration.

Keyword arguments

Agent related

  • ac, as, am : These three keywords decide the color, size, and marker, that each agent will be plotted as. They can each be either a constant or a function, which takes as an input a single agent and outputs the corresponding value. If the model uses a GraphSpace, ac, as, am functions instead take an iterable of agents in each position (i.e. node of the graph).

    Using constants: ac = "#338c54", as = 15, am = :diamond

    Using functions:

    ac(a) = a.status == :S ? "#2b2b33" : a.status == :I ? "#bf2642" : "#338c54"
     as(a) = 10rand()
    -am(a) = a.status == :S ? :circle : a.status == :I ? :diamond : :rect

    Notice that for 2D models, am can be/return a Makie.Polygon instance, which plots each agent as an arbitrary polygon. It is assumed that the origin (0, 0) is the agent's position when creating the polygon. In this case, the keyword as is meaningless, as each polygon has its own size. Use the functions scale, rotate_polygon to transform this polygon.

    3D models currently do not support having different markers. As a result, am cannot be a function. It should be a Mesh or 3D primitive (such as Sphere or Rect3D).

  • offset = nothing : If not nothing, it must be a function taking as an input an agent and outputting an offset position tuple to be added to the agent's position (which matters only if there is overlap).

  • scatterkwargs = () : Additional keyword arguments propagated to the scatter! call.

Preplot related

  • heatarray = nothing : A keyword that plots a model property (that is a matrix) as a heatmap over the space. Its values can be standard data accessors given to functions like run!, i.e. either a symbol (directly obtain model property) or a function of the model. If the space is AbstractGridSpace then matrix must be the same size as the underlying space. For ContinuousSpace any size works and will be plotted over the space extent. For example heatarray = :temperature is used in the Daisyworld example. But you could also define f(model) = create_matrix_from_model... and set heatarray = f. The heatmap will be updated automatically during model evolution in videos and interactive applications.
  • heatkwargs = NamedTuple() : Keywords given to Makie.heatmap function if heatarray is not nothing.
  • add_colorbar = true : Whether or not a Colorbar should be added to the right side of the heatmap if heatarray is not nothing. It is strongly recommended to use abmplot instead of the abmplot! method if you use heatarray, so that a colorbar can be placed naturally.
  • static_preplot! : A function f(ax, model) that plots something after the heatmap but before the agents.
  • osmkwargs = NamedTuple() : keywords directly passed to OSMMakie.osmplot! if model space is OpenStreetMapSpace.
  • graphplotkwargs = NamedTuple() : keywords directly passed to GraphMakie.graphplot! if model space is GraphSpace.
  • adjust_aspect = true: Adjust axis aspect ratio to be the model's space aspect ratio.

The stand-alone function abmplot also takes two optional NamedTuples named figure and axis which can be used to change the automatically created Figure and Axis objects.

Interactivity

Evolution related

  • agent_step!, model_step! = Agents.dummystep: Stepping functions to pass to ABMObservable which itself passes to Agents.step!.
  • add_controls::Bool: If true, abmplot switches to "interactive application" mode. This is by default true if either agent_step! or model_step! keywords are provided. These stepping functions are used to evolve the model interactively using Agents.step!. The application has the following interactive elements:
    1. "step": advances the simulation once for spu steps.
    2. "run": starts/stops the continuous evolution of the model.
    3. "reset model": resets the model to its initial state from right after starting the interactive application.
    4. Two sliders control the animation speed: "spu" decides how many model steps should be done before the plot is updated, and "sleep" the sleep() time between updates.
  • enable_inspection = add_controls: If true, enables agent inspection on mouse hover.
  • spu = 1:50: The values of the "spu" slider.
  • params = Dict() : This is a dictionary which decides which parameters of the model will be configurable from the interactive application. Each entry of params is a pair of Symbol to an AbstractVector, and provides a range of possible values for the parameter named after the given symbol (see example online). Changing a value in the parameter slides is only propagated to the actual model after a press of the "update" button.

Data collection related

  • adata, mdata, when: Same as the keyword arguments of Agents.run!. If either or both adata, mdata are given, data are collected and stored in the abmobs, see ABMObservable. The same keywords provide the data plots of abmexploration. This also adds the button "clear data" which deletes previously collected agent and model data by emptying the underlying DataFrames adf/mdf. Reset model and clear data are independent processes.

See the documentation string of ABMObservable for custom interactive plots.

source

Interactive ABM Applications

Continuing from the Daisyworld plots above, we can turn them into interactive applications straightforwardly, simply by providing the stepping functions as illustrated in the documentation of abmplot. Note that GLMakie should be used instead of CairoMakie when wanting to use the interactive aspects of the plots.

fig, ax, abmobs = abmplot(model;
+am(a) = a.status == :S ? :circle : a.status == :I ? :diamond : :rect

Notice that for 2D models, am can be/return a Makie.Polygon instance, which plots each agent as an arbitrary polygon. It is assumed that the origin (0, 0) is the agent's position when creating the polygon. In this case, the keyword as is meaningless, as each polygon has its own size. Use the functions scale, rotate_polygon to transform this polygon.

3D models currently do not support having different markers. As a result, am cannot be a function. It should be a Mesh or 3D primitive (such as Sphere or Rect3D).

  • offset = nothing : If not nothing, it must be a function taking as an input an agent and outputting an offset position tuple to be added to the agent's position (which matters only if there is overlap).

  • scatterkwargs = () : Additional keyword arguments propagated to the scatter! call.

  • Preplot related

    • heatarray = nothing : A keyword that plots a model property (that is a matrix) as a heatmap over the space. Its values can be standard data accessors given to functions like run!, i.e. either a symbol (directly obtain model property) or a function of the model. If the space is AbstractGridSpace then matrix must be the same size as the underlying space. For ContinuousSpace any size works and will be plotted over the space extent. For example heatarray = :temperature is used in the Daisyworld example. But you could also define f(model) = create_matrix_from_model... and set heatarray = f. The heatmap will be updated automatically during model evolution in videos and interactive applications.
    • heatkwargs = NamedTuple() : Keywords given to Makie.heatmap function if heatarray is not nothing.
    • add_colorbar = true : Whether or not a Colorbar should be added to the right side of the heatmap if heatarray is not nothing. It is strongly recommended to use abmplot instead of the abmplot! method if you use heatarray, so that a colorbar can be placed naturally.
    • static_preplot! : A function f(ax, model) that plots something after the heatmap but before the agents.
    • osmkwargs = NamedTuple() : keywords directly passed to OSMMakie.osmplot! if model space is OpenStreetMapSpace.
    • graphplotkwargs = NamedTuple() : keywords directly passed to GraphMakie.graphplot! if model space is GraphSpace.
    • adjust_aspect = true: Adjust axis aspect ratio to be the model's space aspect ratio.

    The stand-alone function abmplot also takes two optional NamedTuples named figure and axis which can be used to change the automatically created Figure and Axis objects.

    Interactivity

    Evolution related

    • agent_step!, model_step! = Agents.dummystep: Stepping functions to pass to ABMObservable which itself passes to Agents.step!.
    • add_controls::Bool: If true, abmplot switches to "interactive application" mode. This is by default true if either agent_step! or model_step! keywords are provided. These stepping functions are used to evolve the model interactively using Agents.step!. The application has the following interactive elements:
      1. "step": advances the simulation once for spu steps.
      2. "run": starts/stops the continuous evolution of the model.
      3. "reset model": resets the model to its initial state from right after starting the interactive application.
      4. Two sliders control the animation speed: "spu" decides how many model steps should be done before the plot is updated, and "sleep" the sleep() time between updates.
    • enable_inspection = add_controls: If true, enables agent inspection on mouse hover.
    • spu = 1:50: The values of the "spu" slider.
    • params = Dict() : This is a dictionary which decides which parameters of the model will be configurable from the interactive application. Each entry of params is a pair of Symbol to an AbstractVector, and provides a range of possible values for the parameter named after the given symbol (see example online). Changing a value in the parameter slides is only propagated to the actual model after a press of the "update" button.

    Data collection related

    • adata, mdata, when: Same as the keyword arguments of Agents.run!. If either or both adata, mdata are given, data are collected and stored in the abmobs, see ABMObservable. The same keywords provide the data plots of abmexploration. This also adds the button "clear data" which deletes previously collected agent and model data by emptying the underlying DataFrames adf/mdf. Reset model and clear data are independent processes.

    See the documentation string of ABMObservable for custom interactive plots.

    source

    Interactive ABM Applications

    Continuing from the Daisyworld plots above, we can turn them into interactive applications straightforwardly, simply by providing the stepping functions as illustrated in the documentation of abmplot. Note that GLMakie should be used instead of CairoMakie when wanting to use the interactive aspects of the plots.

    fig, ax, abmobs = abmplot(model;
         agent_step! = daisy_step!, model_step! = daisyworld_step!,
         plotkwargs...)
    -fig

    One could click the run button and see the model evolve. Furthermore, one can add more sliders that allow changing the model parameters.

    params = Dict(
    +fig

    One could click the run button and see the model evolve. Furthermore, one can add more sliders that allow changing the model parameters.

    params = Dict(
         :surface_albedo => 0:0.01:1,
         :solar_change => -0.1:0.01:0.1,
     )
     fig, ax, abmobs = abmplot(model;
         agent_step! = daisy_step!, model_step! = daisyworld_step!,
         params, plotkwargs...)
    -fig

    One can furthermore collect data while the model evolves and visualize them using the convenience function abmexploration

    using Statistics: mean
    +fig

    One can furthermore collect data while the model evolves and visualize them using the convenience function abmexploration

    using Statistics: mean
     black(a) = a.breed == :black
     white(a) = a.breed == :white
     adata = [(black, count), (white, count)]
    @@ -52,7 +52,7 @@
         adata, alabels = ["Black daisys", "White daisys"], mdata, mlabels = ["T", "L"]
     )
    Agents.abmexplorationFunction
    abmexploration(model::ABM; alabels, mlabels, kwargs...)

    Open an interactive application for exploring an agent based model and the impact of changing parameters on the time evolution. Requires Agents.

    The application evolves an ABM interactively and plots its evolution, while allowing changing any of the model parameters interactively and also showing the evolution of collected data over time (if any are asked for, see below). The agent based model is plotted and animated exactly as in abmplot, and the model argument as well as splatted kwargs are propagated there as-is. This convencience function only works for aggregated agent data.

    Calling abmexploration returns: fig::Figure, abmobs::ABMObservable. So you can save and/or further modify the figure and it is also possible to access the collected data (if any) via the ABMObservable.

    Clicking the "reset" button will add a red vertical line to the data plots for visual guidance.

    Keywords arguments (in addition to those in abmplot)

    • alabels, mlabels: If data are collected from agents or the model with adata, mdata, the corresponding plots' y-labels are automatically named after the collected data. It is also possible to provide alabels, mlabels (vectors of strings with exactly same length as adata, mdata), and these labels will be used instead.
    • figure = NamedTuple(): Keywords to customize the created Figure.
    • axis = NamedTuple(): Keywords to customize the created Axis.
    • plotkwargs = NamedTuple(): Keywords to customize the styling of the resulting scatterlines plots.
    source

    ABM Videos

    Agents.abmvideoFunction
    abmvideo(file, model, agent_step! [, model_step!]; kwargs...)

    This function exports the animated time evolution of an agent based model into a video saved at given path file, by recording the behavior of the interactive version of abmplot (without sliders). The plotting is identical as in abmplot and applicable keywords are propagated.

    Keywords

    • spf = 1: Steps-per-frame, i.e. how many times to step the model before recording a new frame.
    • framerate = 30: The frame rate of the exported video.
    • frames = 300: How many frames to record in total, including the starting frame.
    • title = "": The title of the figure.
    • showstep = true: If current step should be shown in title.
    • figure = NamedTuple(): Figure related keywords (e.g. resolution, backgroundcolor).
    • axis = NamedTuple(): Axis related keywords (e.g. aspect).
    • recordkwargs = NamedTuple(): Keyword arguments given to Makie.record. You can use (compression = 1, profile = "high") for a higher quality output, and prefer the CairoMakie backend. (compression 0 results in videos that are not playable by some software)
    • kwargs...: All other keywords are propagated to abmplot.
    source

    E.g., continuing from above,

    model, daisy_step!, daisyworld_step! = Models.daisyworld()
    +
    Agents.abmexplorationFunction
    abmexploration(model::ABM; alabels, mlabels, kwargs...)

    Open an interactive application for exploring an agent based model and the impact of changing parameters on the time evolution. Requires Agents.

    The application evolves an ABM interactively and plots its evolution, while allowing changing any of the model parameters interactively and also showing the evolution of collected data over time (if any are asked for, see below). The agent based model is plotted and animated exactly as in abmplot, and the model argument as well as splatted kwargs are propagated there as-is. This convencience function only works for aggregated agent data.

    Calling abmexploration returns: fig::Figure, abmobs::ABMObservable. So you can save and/or further modify the figure and it is also possible to access the collected data (if any) via the ABMObservable.

    Clicking the "reset" button will add a red vertical line to the data plots for visual guidance.

    Keywords arguments (in addition to those in abmplot)

    • alabels, mlabels: If data are collected from agents or the model with adata, mdata, the corresponding plots' y-labels are automatically named after the collected data. It is also possible to provide alabels, mlabels (vectors of strings with exactly same length as adata, mdata), and these labels will be used instead.
    • figure = NamedTuple(): Keywords to customize the created Figure.
    • axis = NamedTuple(): Keywords to customize the created Axis.
    • plotkwargs = NamedTuple(): Keywords to customize the styling of the resulting scatterlines plots.
    source

    ABM Videos

    Agents.abmvideoFunction
    abmvideo(file, model, agent_step! [, model_step!]; kwargs...)

    This function exports the animated time evolution of an agent based model into a video saved at given path file, by recording the behavior of the interactive version of abmplot (without sliders). The plotting is identical as in abmplot and applicable keywords are propagated.

    Keywords

    • spf = 1: Steps-per-frame, i.e. how many times to step the model before recording a new frame.
    • framerate = 30: The frame rate of the exported video.
    • frames = 300: How many frames to record in total, including the starting frame.
    • title = "": The title of the figure.
    • showstep = true: If current step should be shown in title.
    • figure = NamedTuple(): Figure related keywords (e.g. resolution, backgroundcolor).
    • axis = NamedTuple(): Axis related keywords (e.g. aspect).
    • recordkwargs = NamedTuple(): Keyword arguments given to Makie.record. You can use (compression = 1, profile = "high") for a higher quality output, and prefer the CairoMakie backend. (compression 0 results in videos that are not playable by some software)
    • kwargs...: All other keywords are propagated to abmplot.
    source

    E.g., continuing from above,

    model, daisy_step!, daisyworld_step! = Models.daisyworld()
     abmvideo(
         "daisyworld.mp4",
         model,  daisy_step!, daisyworld_step!;
    @@ -67,12 +67,12 @@
         Main weapon = $(agent.charisma)
         Side weapon = $(agent.pistol)
         """
    -end
    source

    Creating custom ABM plots

    The existing convenience function abmexploration will always display aggregated collected data as scatterpoints connected with lines. In cases where more granular control over the displayed plots is needed, we need to take a few extra steps and utilize the ABMObservable returned by abmplot. The same steps are necessary when we want to create custom plots that compose animations of the model space and other aspects.

    Agents.ABMObservableType
    ABMObservable(model; agent_step!, model_step!, adata, mdata, when) → abmobs

    abmobs contains all information necessary to step an agent based model interactively. It is also returned by abmplot.

    Calling Agents.step!(abmobs, n) will step the model for n using the provided agent_step!, model_step!, n as in Agents.step!.

    The fields abmobs.model, abmobs.adf, abmobs.mdf are observables that contain the AgentBasedModel, and the agent and model dataframes with collected data. Data are collected as described in Agents.run! using the adata, mdata, when keywords. All three observables are updated on stepping (when it makes sense). The field abmobs.s is also an observable containing the current step number.

    All plotting and interactivity should be defined by lifting these observables.

    source

    To do custom animations you need to have a good idea of how Makie's animation system works. Have a look at this tutorial if you are not familiar yet.

    create a basic abmplot with controls and sliders

    model, = Models.daisyworld(; solar_luminosity = 1.0, solar_change = 0.0, scenario = :change)
    +end
    source

    Creating custom ABM plots

    The existing convenience function abmexploration will always display aggregated collected data as scatterpoints connected with lines. In cases where more granular control over the displayed plots is needed, we need to take a few extra steps and utilize the ABMObservable returned by abmplot. The same steps are necessary when we want to create custom plots that compose animations of the model space and other aspects.

    Agents.ABMObservableType
    ABMObservable(model; agent_step!, model_step!, adata, mdata, when) → abmobs

    abmobs contains all information necessary to step an agent based model interactively. It is also returned by abmplot.

    Calling Agents.step!(abmobs, n) will step the model for n using the provided agent_step!, model_step!, n as in Agents.step!.

    The fields abmobs.model, abmobs.adf, abmobs.mdf are observables that contain the AgentBasedModel, and the agent and model dataframes with collected data. Data are collected as described in Agents.run! using the adata, mdata, when keywords. All three observables are updated on stepping (when it makes sense). The field abmobs.s is also an observable containing the current step number.

    All plotting and interactivity should be defined by lifting these observables.

    source

    To do custom animations you need to have a good idea of how Makie's animation system works. Have a look at this tutorial if you are not familiar yet.

    create a basic abmplot with controls and sliders

    model, = Models.daisyworld(; solar_luminosity = 1.0, solar_change = 0.0, scenario = :change)
     fig, ax, abmobs = abmplot(model;
         agent_step! = daisy_step!, model_step! = daisyworld_step!, params, plotkwargs...,
         adata, mdata, figure = (; resolution = (1600,800))
     )
    -fig
    abmobs
    ABMObservable with model:
    +fig
    abmobs
    ABMObservable with model:
     StandardABM with 360 agents of type Daisy
      space: GridSpaceSingle with size (30, 30), metric=chebyshev, periodic=true
      scheduler: fastest
    @@ -93,17 +93,17 @@
         strokewidth = 2, strokecolor = (:black, 0.5),
     )
     
    -fig

    Now, once we step the abmobs::ABMObservable, the whole plot will be updated

    Agents.step!(abmobs, 1)
    +fig

    Now, once we step the abmobs::ABMObservable, the whole plot will be updated

    Agents.step!(abmobs, 1)
     Agents.step!(abmobs, 1)
    -fig

    Of course, you need to actually adjust axis limits given that the plot is interactive

    autolimits!(ax_counts)
    +fig

    Of course, you need to actually adjust axis limits given that the plot is interactive

    autolimits!(ax_counts)
     autolimits!(ax_hist)

    Or, simply trigger them on any update to the model observable:

    on(abmobs.model) do m
         autolimits!(ax_counts)
         autolimits!(ax_hist)
    -end
    ObserverFunction defined at none:2 operating on Observable(StandardABM with 754 agents of type Daisy
    +end
    ObserverFunction defined at none:2 operating on Observable(StandardABM with 752 agents of type Daisy
      space: GridSpaceSingle with size (30, 30), metric=chebyshev, periodic=true
      scheduler: fastest
      properties: temperature, solar_luminosity, max_age, surface_albedo, ratio, solar_change, tick, scenario)

    and then marvel at everything being auto-updated by calling step! :)

    for i in 1:100; step!(abmobs, 1); end
    -fig

    GraphSpace models

    While the ac, as, am keyword arguments generally relate to agent colors, markersizes, and markers, they are handled a bit differently in the case of GraphSpace models. Here, we collect those plot attributes for each node of the underlying graph which can contain multiple agents. If we want to use a function for this, we therefore need to handle an iterator of agents. Keeping this in mind, we can create an exemplary GraphSpace model and plot it with abmplot.

    using Graphs
    +fig

    GraphSpace models

    While the ac, as, am keyword arguments generally relate to agent colors, markersizes, and markers, they are handled a bit differently in the case of GraphSpace models. Here, we collect those plot attributes for each node of the underlying graph which can contain multiple agents. If we want to use a function for this, we therefore need to handle an iterator of agents. Keeping this in mind, we can create an exemplary GraphSpace model and plot it with abmplot.

    using Graphs
     using ColorTypes
     sir_model, sir_agent_step!, sir_model_step! = Models.sir()
     city_size(agents_here) = 0.005 * length(agents_here)
    @@ -133,4 +133,4 @@
     fig, ax, abmobs = abmplot(sir_model;
         agent_step! = sir_agent_step!, model_step! = sir_model_step!,
         as = city_size, ac = city_color, graphplotkwargs)
    -fig
    +fig
    diff --git a/previews/PR887/examples/celllistmap.mp4 b/previews/PR887/examples/celllistmap.mp4 index 9f0bc0a5e6..1ca44c63b8 100644 Binary files a/previews/PR887/examples/celllistmap.mp4 and b/previews/PR887/examples/celllistmap.mp4 differ diff --git a/previews/PR887/examples/celllistmap/index.html b/previews/PR887/examples/celllistmap/index.html index 8a49b7cbab..580a2ca3b9 100644 --- a/previews/PR887/examples/celllistmap/index.html +++ b/previews/PR887/examples/celllistmap/index.html @@ -98,7 +98,7 @@ model, agent_step!, model_step!, nsteps, false, ) end
    simulate (generic function with 2 methods)

    Which should be quite fast

    model = initialize_bouncing()
    -@time simulate(model)
     17.141929 seconds (589.77 k allocations: 40.575 MiB, 0.27% gc time, 4.20% compilation time)

    and let's make a nice video with less particles, to see them bouncing around. The marker size is set by the radius of each particle, and the marker color by the corresponding repulsion constant.

    using CairoMakie
    +@time simulate(model)
     19.970925 seconds (589.95 k allocations: 40.573 MiB, 0.25% gc time, 3.80% compilation time)

    and let's make a nice video with less particles, to see them bouncing around. The marker size is set by the radius of each particle, and the marker color by the corresponding repulsion constant.

    using CairoMakie
     model = initialize_bouncing(number_of_particles=1000)
     abmvideo(
         "celllistmap.mp4", model, agent_step!, model_step!;
    @@ -108,4 +108,4 @@
         ac=p -> p.k # marker color
     )
    + diff --git a/previews/PR887/examples/daisyworld.mp4 b/previews/PR887/examples/daisyworld.mp4 index 4722b3c262..c7dff3de24 100644 Binary files a/previews/PR887/examples/daisyworld.mp4 and b/previews/PR887/examples/daisyworld.mp4 differ diff --git a/previews/PR887/examples/diffeq/index.html b/previews/PR887/examples/diffeq/index.html index d03e231644..061aa78dee 100644 --- a/previews/PR887/examples/diffeq/index.html +++ b/previews/PR887/examples/diffeq/index.html @@ -115,7 +115,7 @@ f

    Baseline benchmark

    Lets get a baseline performance result for our model.

    using BenchmarkTools
     
     @btime Agents.step!(model, agent_step!, model_step!, 20 * 365) setup =
    -    (model = initialise())
      28.821 ms (115267 allocations: 3.21 MiB)

    So this is fairly quick since the model is a simple one, but it's certainly not as efficient as it could be. We calculate the stock value every single day, since the forward Eulerian method requires us to, so it can evolve correctly. In addition to this, Eulerian expansion introduces uncertainty into our results, which is tied to the choice of step size. For accurate results, one should never really use this approximate method - although it is almost ubiquitous throughout contemporary research code. For a thorough exposé on this, have a read of Why you shouldn't use Euler's method to solve ODEs.

    Coupling DifferentialEquations.jl to Agents.jl

    Lets therefore modify our system to solve the logistic equation in a continuous context, but discretely monitor and harvest.

    import OrdinaryDiffEq
    +    (model = initialise())
      34.720 ms (115267 allocations: 3.21 MiB)

    So this is fairly quick since the model is a simple one, but it's certainly not as efficient as it could be. We calculate the stock value every single day, since the forward Eulerian method requires us to, so it can evolve correctly. In addition to this, Eulerian expansion introduces uncertainty into our results, which is tied to the choice of step size. For accurate results, one should never really use this approximate method - although it is almost ubiquitous throughout contemporary research code. For a thorough exposé on this, have a read of Why you shouldn't use Euler's method to solve ODEs.

    Coupling DifferentialEquations.jl to Agents.jl

    Lets therefore modify our system to solve the logistic equation in a continuous context, but discretely monitor and harvest.

    import OrdinaryDiffEq
     
     function agent_diffeq_step!(agent, model)
         agent.yearly_catch = rand(abmrng(model), Poisson(agent.competence))
    @@ -180,7 +180,7 @@
         )
     lines!(ax, resultsdeq.stock, linewidth = 2, color = :blue)
     f

    The small complexity addition yields us a generous speed up of around 4.5x.

    @btime Agents.step!(model, agent_diffeq_step!, model_diffeq_step!, 20) setup =
    -    (model = initialise_diffeq())
      3.352 ms (125029 allocations: 3.07 MiB)

    Digging into the results a little more, we can see that the DifferentialEquations solver did not need to solve the logistic equation at every agent step to achieve a stable solution for us:

    length(modeldeq.i.sol.t)
    2191
    365 * 20 > length(modeldeq.i.sol.t)
    true

    With other initial conditions, there's the possibility that this may not be the case. When this occurs, these additional samples provide mathematical guarantees that the results are accurate (to a given tolerance), which is a safeguard not possible for our Euler example.

    Compare our two results directly, both start with the same random seed and evolve in precisely the same manner:

    f = Figure(resolution = (600, 400))
    +    (model = initialise_diffeq())
      3.376 ms (125029 allocations: 3.07 MiB)

    Digging into the results a little more, we can see that the DifferentialEquations solver did not need to solve the logistic equation at every agent step to achieve a stable solution for us:

    length(modeldeq.i.sol.t)
    2191
    365 * 20 > length(modeldeq.i.sol.t)
    true

    With other initial conditions, there's the possibility that this may not be the case. When this occurs, these additional samples provide mathematical guarantees that the results are accurate (to a given tolerance), which is a safeguard not possible for our Euler example.

    Compare our two results directly, both start with the same random seed and evolve in precisely the same manner:

    f = Figure(resolution = (600, 400))
     ax =
         f[1, 1] = Axis(
             f,
    @@ -246,4 +246,4 @@
             title = "Fishery Inventory",
         )
     lines!(ax, discrete, linewidth = 2, color = :blue)
    -f

    The results are different here, since the construction of this version and the one above are quite different and cannot be randomly seeded in the same manner.

    However, as you can see, it is for the most part just a re-arranged implementation of the integrator method - giving users flexibility in their architecture choices.

    +f

    The results are different here, since the construction of this version and the one above are quite different and cannot be randomly seeded in the same manner.

    However, as you can see, it is for the most part just a re-arranged implementation of the integrator method - giving users flexibility in their architecture choices.

    diff --git a/previews/PR887/examples/flock/index.html b/previews/PR887/examples/flock/index.html index eb00dcddff..eb069761a0 100644 --- a/previews/PR887/examples/flock/index.html +++ b/previews/PR887/examples/flock/index.html @@ -87,4 +87,4 @@ title = "Flocking" ) + diff --git a/previews/PR887/examples/index.html b/previews/PR887/examples/index.html index e55e4e0da7..4f343cbc64 100644 --- a/previews/PR887/examples/index.html +++ b/previews/PR887/examples/index.html @@ -1,2 +1,2 @@ -More Examples for Agents.jl · Agents.jl

    More Examples for Agents.jl

    In order to keep the documentation of Agents.jl lean and focused, the majority of model examples for Agents.jl are not hosted in this documentation, but rather in a different one: Agents.jl Example Zoo. Any kind of user-written example is welcomed to be contributed there as well!

    +More Examples for Agents.jl · Agents.jl

    More Examples for Agents.jl

    In order to keep the documentation of Agents.jl lean and focused, the majority of model examples for Agents.jl are not hosted in this documentation, but rather in a different one: Agents.jl Example Zoo. Any kind of user-written example is welcomed to be contributed there as well!

    diff --git a/previews/PR887/examples/measurements/index.html b/previews/PR887/examples/measurements/index.html index 48ed9f7f9e..8a19eb4025 100644 --- a/previews/PR887/examples/measurements/index.html +++ b/previews/PR887/examples/measurements/index.html @@ -183,4 +183,4 @@ linewidth = 2, color = :blue, ) -f +f diff --git a/previews/PR887/examples/optim/index.html b/previews/PR887/examples/optim/index.html index 788cb8715c..83abd0dc71 100644 --- a/previews/PR887/examples/optim/index.html +++ b/previews/PR887/examples/optim/index.html @@ -133,4 +133,4 @@ 5.869646301829334

    These parameters look better: about 0.3% of the population dies and 0.02% are infected:

    The algorithm managed to minimize the number of infected and deaths while still increasing death rate to 42%, reinfection probability to 53%, and migration rates to 33%. The most important change however, was decreasing the transmission rate when individuals are infected and undetected from 30% in our initial calculation, to 0.2%.

    Over a longer period of time than 50 days, that high death rate will take its toll though. Let's reduce that rate and check the cost.

    x = best_candidate(result)
     x[2] = 0.02
    -cost_multi(x)
    (0.03933333333333333, -1.0)

    The fraction of infected increases to 0.04%. This is an interesting result: since this virus model is not as deadly, the chances of re-infection increase. We now have a set of parameters to strive towards in the real world. Insights such as these assist us to enact countermeasures like social distancing to mitigate infection risks.

    +cost_multi(x)
    (0.03933333333333333, -1.0)

    The fraction of infected increases to 0.04%. This is an interesting result: since this virus model is not as deadly, the chances of re-infection increase. We now have a set of parameters to strive towards in the real world. Insights such as these assist us to enact countermeasures like social distancing to mitigate infection risks.

    diff --git a/previews/PR887/examples/predator_prey/index.html b/previews/PR887/examples/predator_prey/index.html index 0e8113c16c..465d221907 100644 --- a/previews/PR887/examples/predator_prey/index.html +++ b/previews/PR887/examples/predator_prey/index.html @@ -217,4 +217,4 @@ plotkwargs..., ) + diff --git a/previews/PR887/examples/rabbit_fox_hawk/index.html b/previews/PR887/examples/rabbit_fox_hawk/index.html index 3bc0291528..f2267e8902 100644 --- a/previews/PR887/examples/rabbit_fox_hawk/index.html +++ b/previews/PR887/examples/rabbit_fox_hawk/index.html @@ -300,4 +300,4 @@ title = "Rabbit Fox Hawk with pathfinding" ) + diff --git a/previews/PR887/examples/schelling/index.html b/previews/PR887/examples/schelling/index.html index ef47060bea..4ccfaa2ce7 100644 --- a/previews/PR887/examples/schelling/index.html +++ b/previews/PR887/examples/schelling/index.html @@ -168,4 +168,4 @@ ) adf, _ = paramscan(parameters, initialize; adata, agent_step!, n = 3) -adf
    32×4 DataFrame
    Rowstephappyperc_moodmin_to_be_happytotal_agents
    Int64Float64Int64Int64
    100.02200
    210.6552200
    320.8252200
    430.912200
    500.03200
    610.3953200
    720.63200
    830.6653200
    900.04200
    1010.14200
    1120.2254200
    1230.214200
    1300.05200
    1410.025200
    1520.025200
    1630.0155200
    1700.02300
    1810.8433332300
    1920.9533332300
    2030.9633332300
    2100.03300
    2210.623300
    2320.83300
    2430.8533333300
    2500.04300
    2610.324300
    2720.5266674300
    2830.6366674300
    2900.05300
    3010.1733335300
    3120.2433335300
    3230.2933335300

    We nicely see that the larger :min_to_be_happy is, the slower the convergence to "total happiness".

    +adf
    32×4 DataFrame
    Rowstephappyperc_moodmin_to_be_happytotal_agents
    Int64Float64Int64Int64
    100.02200
    210.6552200
    320.8252200
    430.912200
    500.03200
    610.3953200
    720.63200
    830.6653200
    900.04200
    1010.14200
    1120.2254200
    1230.214200
    1300.05200
    1410.025200
    1520.025200
    1630.0155200
    1700.02300
    1810.8433332300
    1920.9533332300
    2030.9633332300
    2100.03300
    2210.623300
    2320.83300
    2430.8533333300
    2500.04300
    2610.324300
    2720.5266674300
    2830.6366674300
    2900.05300
    3010.1733335300
    3120.2433335300
    3230.2933335300

    We nicely see that the larger :min_to_be_happy is, the slower the convergence to "total happiness".

    diff --git a/previews/PR887/examples/schoolyard/index.html b/previews/PR887/examples/schoolyard/index.html index 17a14ebc58..2d0183b412 100644 --- a/previews/PR887/examples/schoolyard/index.html +++ b/previews/PR887/examples/schoolyard/index.html @@ -91,4 +91,4 @@ static_preplot!, ) + diff --git a/previews/PR887/examples/sir/index.html b/previews/PR887/examples/sir/index.html index 2d2bb2a391..c05081d41a 100644 --- a/previews/PR887/examples/sir/index.html +++ b/previews/PR887/examples/sir/index.html @@ -208,4 +208,4 @@ dead = log10.(N .- data[:, aggname(:status, length)]) ld = lines!(ax, x, dead, color = :green) Legend(fig[1, 2], [li, lr, ld], ["infected", "recovered", "dead"]) -fig

    The exponential growth is clearly visible since the logarithm of the number of infected increases linearly, until everyone is infected.

    +fig

    The exponential growth is clearly visible since the logarithm of the number of infected increases linearly, until everyone is infected.

    diff --git a/previews/PR887/examples/zombies/index.html b/previews/PR887/examples/zombies/index.html index bd3ad3bfdc..b360884433 100644 --- a/previews/PR887/examples/zombies/index.html +++ b/previews/PR887/examples/zombies/index.html @@ -65,4 +65,4 @@ ┌ Warning: Since there are a lot of edges (3639 > 500), they will be drawn as straight lines even though they contain curvy edges. If you really want to plot them as bezier curves pass `edge_plottype=:beziersegments` explicitly. This will have much worse performance! └ @ GraphMakie ~/.julia/packages/GraphMakie/yyvus/src/recipes.jl:526 + diff --git a/previews/PR887/index.html b/previews/PR887/index.html index 0e67c8d01f..1caefdf9c9 100644 --- a/previews/PR887/index.html +++ b/previews/PR887/index.html @@ -11,7 +11,7 @@ journal = {{SIMULATION}}, volume = {0}, number = {0}, -}source
    Star us on GitHub!

    If you have found this package useful, please consider starring it on GitHub. This gives us an accurate lower bound of the (satisfied) user count.

    Latest news: Agents.jl v5.15

    Highlights

    Software quality