Introduction

Structure

An AgentBasedModel object embeds all the properties of the system to be simulated and maps unique IDs to microbe instances. During the simulation, the model is evolved in discrete time steps, with each microbe's position, velocity and "state" being updated according to specified rules. Standard rules for motion, reorientations and chemotaxis are available by default, but custom behaviors can be implemented via user-defined functions.

The typical workflow to run a simulation in MicrobeAgents.jl goes as follows:

  1. Define the size and properties of the space in which the microbes will move.
  2. Choose the behaviors (e.g. chemotaxis models) that the microbes will have, or define new ones.
  3. Initialize an AgentBasedModel object with the desired space, the Microbe type, integration timestep, and any extra property needed for the simulation.
  4. Populate the ABM with microbe instances.
  5. Choose the observables to collect during production and run the model.

MicrobeAgents.jl re-exports and extends various function from Agents.jl in order to work as a standalone, but it is generally recommended to use it in combination with Agents.jl for extra goodies.

Space

MicrobeAgents.jl only supports continuous spaces with dimensions 1, 2 or 3. Spaces can be created with the ContinuousSpace function (reexported from Agents.jl). The extent of the space must be given as a tuple or SVector, and periodicity is set with the periodic kwarg (defaults to true).

# one-dimensional periodic space
extent = (1.0,)
ContinuousSpace(extent)

# two-dimensional non-periodic space
extent = (1.0, 2.0)
ContinuousSpace(extent; periodic=false)

# three-dimensional space periodic only along the x direction
extent = (100.0, 20.0, 20.0)
ContinuousSpace(extent; periodic=(true,false,false))

Microbes

Microbes are represented by subtypes of the AbstractMicrobe type, which is in turn a subtype of AbstractAgent from Agents.jl

MicrobeAgents.AbstractMicrobe — Type
AbstractMicrobe{D,N} <: AbstractAgent where {D<:Integer,N}

All microbe types in MicrobeAgents.jl simulations must be instances of user-defined types that are subtypes of AbstractMicrobe. YourMicrobeType{D,N} <: AbstractMicrobe{D,N} The parameter D defines the dimensionality of the space in which the microbe type lives (1, 2 and 3 are supported); N is the number of motile states of its motility.

All microbe types must have at least the following fields:

  • id::Int id of the microbe (used internally by Agents.jl)
  • pos::SVector{D,Float64} position of the microbe
  • vel::SVector{D,Float64} velocity of the microbe
  • speed::Real base speed of the microbe (see speed)
  • motility::Motility{N} motile pattern of the microbe
  • rotational_diffusivity::Real coefficient of brownian rotational diffusion
  • radius::Real equivalent spherical radius of the microbe

Optionally, define a method behaviors(m) returning a Tuple or NamedTuple of behaviors (default: ()).

source

The concrete microbe type is Microbe. Models of chemotaxis from the scientific literature are implemented as behaviors attached to microbes (see below).

See Microbe in the API. Microbe instances should only be created within an AgentBasedModel, the fundamental structure which embeds everything that has to do with the agent-based simulations you want to run. In MicrobeAgents.jl, models are created through the StandardABM function.

Agents.StandardABM — Type
StandardABM(MicrobeType, space, timestep; kwargs...)

Extension of the Agents.StandardABM method for microbe types. Implementation of AgentBasedModel where agents can be added and removed at any time. If agents removal is not required, it is recommended to use the keyword argument container = Vector for better performance. See Agents.AgentBasedModel for detailed information on the keyword arguments.

Arguments

  • MicrobeType: subtype of AbstractMicrobe{D}, with explicitly specified dimensionality D.
  • space: a ContinuousSpace{D} with the same dimensionality D as MicrobeType which specifies the spatial properties of the simulation domain.
  • timestep: the integration timestep of the simulation.

Keywords

  • properties: additional container of data to specify model-level properties. MicrobeAgents.jl includes a set of default properties (detailed at the end).
  • scheduler = Schedulers.fastest
  • rng = Random.default_rng()
  • warn = true

Default properties

When a model is created, a default set of properties is included in the model (chemoattractant, field_cache and obstacles):

Dict(
    :chemoattractant => ChemicalField{D}(),
    :field_cache => FieldCache{D}(), # internal, per-step cache of field quantities
    :obstacles => () # Tuple of `Obstacles` sets (a single set is also accepted)
)

By including these default properties, we make sure that chemotactic behaviors will work even without extra user intervention. All these properties can be overwritten by simply passing an equivalent key to the properties dictionary when creating the model.

source

To initialize a model we must provide the microbe type, the simulation space, and the integration timestep of the simulation. All other parameters are optional. To setup a model for Microbes living in a 1-dimensional space we can therefore run

space = ContinuousSpace((100.0,); periodic=false)
dt = 0.1
model = StandardABM(Microbe{1}, space, dt)

Now, with the add_agent! function we will populate the model with microbes of the specified type (Microbe{1}). The only argument we must always specify for add_agent! is the motility of the microbe, via the motility keyword. An overview of the motility interface is given later; For now we will just use a Run-Tumble motility with an average run duration of 1 second and a constant swimming speed of 20 micron / second. The third required argument to the call is the distribution of reorientation angles, but it is irrelevant in 1-dimensional systems so we can just pass an empty array. If unspecified, position, direction and speed of the microbe will be assigned randomly; all the other fields will be assigned default values from the constructor (unless specified). To select a position, it can be passed as the first argument to the add_agent! call, and any other bacterial parameter can be defined via keyword arguments. All of the following are valid calls

motility = RunTumble(1.0, [20.0], [])
# a Microbe with large radius
add_agent!(model; motility, radius=10.0)
# a Microbe with custom position and high coefficient of rotational diffusion
add_agent!((53.2,), model; motility, rotational_diffusivity=0.5)
# a Microbe initialized with velocity to the right
add_agent!(model; motility, vel=(1.0,))

Chemotaxis and other behaviors are attached to microbes through the behaviors keyword of add_agent!, e.g. add_agent!(model; motility, behaviors = (BrownBerg(),)); see Behaviors.

More about models

MicrobeAgents.jl exploits the AgentBasedModel interface from Agents.jl. While the standard Agents.jl syntax will always work, it is typically more convenient to use the method extensions provided by MicrobeAgents.jl, which also includes some default parameters required by the simulations. If the simulation requires removal/addition of microbes, it is recommended to call StandardABM with the container=Dict keyword argument, otherwise MicrobeAgents.jl defaults to container=Vector which provides better performance.

In addition to the microbe instances, the model should also wrap all the other information required to perform the simulation.

MicrobeAgents.jl defines default timestepping functions which are used to evolve the microbes and the model, and are accessible through the microbe_step! and model_step! keywords in StandardABM. By default, the microbe_step! function performs, in order:

  • update microbe position according to current velocity
  • randomize the microbe orientation through rotational diffusion (if present)
  • call the affect! hook of each behavior (e.g. chemotaxis or other user-defined behavior)
  • perform reorientation events following Poissonian statistics

The model_step! function instead defaults to a dummy function which does nothing. Any custom behavior can be implemented by simply modifying these two functions.

The microbe_step! function is split into 3 subroutines, corresponding to movement, internal state changes, and active reorientations, applied in this order:

MicrobeAgents.move_step! — Function
move_step!(microbe::AbstractMicrobe, model::ABM)

Subroutine to perform one step of translation followed by reorientation due to rotational diffusion. Translation is constrained by the obstacles of the model (obstacles(model)).

source
MicrobeAgents.affect_step! — Function
affect_step!(microbe::AbstractMicrobe, model::ABM)

Resets the per-step cache of field quantities, then calls affect! for each behavior of the microbe, in order.

source
MicrobeAgents.reorient_step! — Function
reorient_step!(microbe::AbstractMicrobe, model::ABM)

Subroutine to perform active reorientation and/or switch the microbe to a new motile state.

source

Custom behaviors are normally added as behaviors (see Behaviors); the subroutines are exported and can still be reordered or replaced in a custom agent_step!.

Any type of external parameter that should be used during the simulation should be passed to StandardABM through the properties dictionary.

Running a model

After the model has been created and populated with the desired number of microbes, we are ready to run the simulation. We just need to specify how many steps we want to simulate and what data to collect during the run:

nsteps = 100
adf, mdf = run!(model, nsteps; adata=[position])

run! will return two dataframes, one for the agent-level data (adf) and one for the model-level data (mdf, which in this case will be empty). This way, we have produced our first random walk. Since adf.position is a vector of static vectors, we first have to unpack the x and y values and then we are ready to plot our trajectory.

using Plots
x = first.(adf.position)
y = last.(adf.position)
plot(x, y)

Motility patterns

In MicrobeAgents.jl, motility patterns are represented through the Motility{N} type. A Motility is composed of N instances of MotileState and of a set of transition probabilities between these N states. A MotileState contains information about the speed distribution, turn angle distributions, and average lifetime of a particular state of motion.

There are two kinds of MotileStates, RunState and TurnState. A RunState is used to represent states associated with translational motion and no angular reorientation (e.g. Run). A TurnState is instead used to represent states where no translational motion happens, but where reorientations may occur (e.g. Tumble, Reverse, Flick, Stop).

Instances of MotileStates can be arbitrarily combined into a Motility.

MicrobeAgents.Motility — Type
Motility(motile_states::NTuple{N,MotileState}, rates...)

Construct a Motility from a sequence of MotileStates and their transition rates.

motile_states must be a tuple of MotileState instances. rates must be specified in the form (i => j, p) where i is the index of the starting state (as ordered in the motile_states tuple), j is the index of the state towards which the transition occurs, and p is the probability that, after the duration of i is over, the transition occurs towards the state j. It is not necessary to indicate impossible transitions; any pair i => j which is not explicitly specified is assumed to have a transition probability p=0.

For example, if we want to define a 3-state motility, composed by a slow but long run, an instantaneous tumble with some angle_pdf, and a fast but short run, where the two runs can only transition towards the tumble, but the tumble can transition with probability 30% towards the slow run and 70% towards the fast run, we will call:

Motility((Run(10.0, 2.0), Tumble(0.0, angle_pdf), Run(60.0, 0.5)),
    (1 => 2, 1.0),
    (2 => 1, 0.3), (2 => 3, 0.7),
    (3 => 2, 1.0)
)

Some default constructors for common motility patterns are provided: RunTumble, RunReverse, RunReverseFlick, RunStop

source

The most common motility patterns are, however, pre-implemented.

MicrobeAgents.RunReverse — Function
RunReverse(
    run_speed_forward, run_duration_forward,
    run_speed_backward, run_duration_backward;
    angle=(π,), reverse_duration=0.0,
source
MicrobeAgents.RunReverseFlick — Function
RunReverseFlick(
    run_speed_forward, run_duration_forward,
    run_speed_backward, run_duration_backward;
    reverse_angle=(π,) flick_angle=(-π/2, +π/2),
    reverse_duration=0.0, flick_duration=0.0,
)
source