Developer Docs
Internal infrastructure overview
When it comes to development of new code, the overwhelming majority of Agents.jl is composed of three parts:
- The model time-stepping dynamics and agent storage and retrieval logic
- The space agent storage, movement, and neighborhood searching logic
- The rest of the API which is largely agnostic to the above two
Arguably the most important aspect of the Agents.jl design is that the above three pillars of the infrastructure are orthogonal. That is, if someone wanted to add a new space, they would not have to care about neither the model stepping dynamics, nor the majority of the remaining Agents.jl API, such as sampling, data collection, etc. etc.
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 model type
Note that new model types target applications where a fundamentally different way to define the "time evolution" or "dynamic rule" is required. If any of the existing AgentBasedModel
subtypes satisfy the type of time stepping, then you don't have to create a new type of model.
Creating a new model type within Agents.jl is simple. Although it requires extending a bit more than a dozen functions, the majority of them are 1-liner "accessor" functions (that return e.g., the rng, or the space instance).
The most important mandatory method is to extend step!
for your new type. You can see the existing implementations of step!
for e.g., StandardABM
or EventQueueABM
to get an idea.
All other mandatory method extensions for e.g., accessor functions are in the file src/core/model_abstract.jl
. As you will notice, the overwhelming majority of required methods have a default implementation that e.g., attempts to return a field named :rng
. The rest of the methods by default return a "not implemented" error message (and those you also need to extend mandatorily).
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:
- Think about what the agent position type should be.
- Think about how the space type will keep track of the agent positions, so that it is possible to implement the function
nearby_ids
. - Implement the
struct
that represents your new space, while making it a subtype ofAbstractSpace
. - Extend
random_position(model)
. - Extend
add_agent_to_space!(agent, model), remove_agent_from_space!(agent, model)
. This already provides access toadd_agent!, kill_agent!
andmove_agent!
. - Extend
nearby_ids(pos, model, r)
. - Create a new "minimal" agent type to be used with
@agent
(see the source code ofGraphAgent
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!
or remove_all_from_space!
for performance reasons.
Visualization of a custom space
Visualization of a new space type within Agents.jl works in a very similar fashion to creating a new space type. As before, all custom space types should implement this API and be subtypes of AbstractSpace
. To implement this API for your custom space:
- Copy the methods from the list below. Make sure to also copy the first line defining the
ABMPlot
type in your working environment which is necessary to be able to extend the API. InsideABMPlot
you can find all the plot args and kwargs as well as the plot properties that have been lifted from them (see the "Lifting" section of this API). You can then easily access them inside your functions via the dot syntax, e.g. withp.color
orp.plotkwargs
, and use them as needed. - Replace
Agents.AbstractSpace
with the name of your space type. - Implement at least the required methods.
- Implement optional methods as needed. Some methods DO NOT need to be implemented for every space, they are optional. The necessity to implement these methods depends on the supertypes of your custom type. For example, you will get a lot of methods "for free" if your
CustomType
is a subtype ofAgents.AbstractGridSpace
. As a general rule of thumb: The more abstract yourCustomSpace
's supertype is, the more methods you will have to extend/adapt.
We provide a convenient function Agents.check_space_visualization_API(::ABM)
to check for the availability of methods used to plot ABMs with custom spaces via abmplot
. By default, the function is called whenever you want to plot a custom space. This behavior can be disabled by passing enable_space_checks = false
as a keyword argument to abmplot
.
The methods to be extended for visualization of a new space type are structured into four groups:
## Required
const ABMPlot = Agents.get_ABMPlot_type()
function Agents.agents_space_dimensionality(space::Agents.AbstractSpace)
end
function Agents.get_axis_limits(model::ABM{<:Agents.AbstractSpace})
end
function Agents.agentsplot!(ax, p::ABMPlot)
end
## Preplots (optional)
function Agents.spaceplot!(ax, p::ABMPlot; spaceplotkwargs...)
end
function Agents.static_preplot!(ax, p::ABMPlot)
end
## Lifting (optional)
function Agents.abmplot_heatobs(model::ABM{<:Agents.AbstractSpace}, heatarray)
end
function Agents.abmplot_pos(model::ABM{<:Agents.AbstractSpace}, offset)
end
function Agents.abmplot_colors(model::ABM{<:Agents.AbstractSpace}, ac)
end
function Agents.abmplot_colors(model::ABM{<:Agents.AbstractSpace}, ac::Function)
end
function Agents.abmplot_markers(model::ABM{<:Agents.AbstractSpace}, am, pos)
end
function Agents.abmplot_markers(model::ABM{<:Agents.AbstractSpace}, am::Function, pos)
end
function Agents.abmplot_markersizes(model::ABM{<:Agents.AbstractSpace}, as)
end
function Agents.abmplot_markersizes(model::ABM{<:Agents.AbstractSpace}, as::Function)
end
#### Inspection (optional)
function Agents.convert_element_pos(::S, pos) where {S<:Agents.AbstractSpace}
end
function Agents.ids_to_inspect(model::ABM{<:Agents.AbstractSpace}, pos)
end
The same approach outlined above also applies in cases when you want to overwrite the default methods for an already existing space type. For instance, this might often be the case for models with Nothing
space.
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.CostMetric
— TypePathfinding.CostMetric{D}
An abstract type representing a metric that measures the approximate cost of travelling between two points in a D
dimensional grid.
Agents.Pathfinding.delta_cost
— FunctionPathfinding.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
.
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_serializable
— FunctionAgentsIO.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.
Agents.AgentsIO.from_serializable
— FunctionAgentsIO.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.
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
.