Developer Docs

Internal infrastructure overview

When it comes to development of new code, the overwhelming majority of Agents.jl is composed of three parts:

  1. The model time-stepping dynamics and agent storage and retrieval logic
  2. The space agent storage, movement, and neighborhood searching logic
  3. 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:

  1. Think about what the agent position type should be.
  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(pos, 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.

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:

  1. 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. Inside ABMPlot 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. with p.color or p.plotkwargs, and use them as needed.
  2. Replace Agents.AbstractSpace with the name of your space type.
  3. Implement at least the required methods.
  4. 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 of Agents.AbstractGridSpace. As a general rule of thumb: The more abstract your CustomSpace's supertype is, the more methods you will have to extend/adapt.
Checking for missing methods

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
Changing visualization of existing space types

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