Ants
Study this example to learn about:
- Simple agent properties with complex model interactions
- Diffusion of a quantity in a grid space
- Including a "surface property" in the model
- Counting time in the model and having time-dependent dynamics
- Performing interactive scientific research
Overview of Ants
This model explores the behavior of ant colonies in collecting food and communicating using chemicals that individual ants leave in the world.
AntWorld has one nest and three sources of food at varying distances from the nest. Each ant has the tendency to keep moving in the direction they are currently facing, with some randomness. They will follow pheromone trails to food. This pheromone trail dissipates (spreads) to surrounding areas. It also evaporates over time.
In addition, they have a chemical trail to follow back to the nest. This trail is static.
Ants set out from the nest in a random manner, seeking out sources of food. When they find food, they pick up one unit of it, turn around, and head back to the nest while depositing pheromones for other ants to find and follow back to the food.
Defining the ant type
Ant
has three values (other than the required id
and pos
for any agent that lives on a GridSpaceSingle
). Each ant has a has_food
attribute, showing if the ant is currently carrying food, a facing_direction
(a number between 1 and 8) describing the direction they are heading, and a food_collected
which is the amount of food an individual ant has collected.
using Agents
using Random
using Logging
@agent struct Ant(GridAgent{2})
has_food::Bool
facing_direction::Int
food_collected::Int
food_collected_once::Bool
end
adjacent_dict
defines the adjacent positions to the current position, and is also associated with the direction the ant is facing.
const adjacent_dict = Dict(
1 => (0, -1), # S
2 => (1, -1), # SE
3 => (1, 0), # E
4 => (1, 1), # NE
5 => (0, 1), # N
6 => (-1, 1), # NW
7 => (-1, 0), # W
8 => (-1, -1), # SW
)
const number_directions = length(adjacent_dict)
8
Model Properties
The AntWorldProperties
structure defines the properties of the model.
mutable struct AntWorldProperties
pheromone_trails::Matrix
food_amounts::Matrix
nest_locations::Matrix
food_source_number::Matrix
food_collected::Int
diffusion_rate::Int
x_dimension::Int
y_dimension::Int
nest_size::Int
evaporation_rate::Int
pheromone_amount::Int
spread_pheromone::Bool
pheromone_floor::Int
pheromone_ceiling::Int
end
A convenience method to truncate a Float to an integer.
int(x::Float64) = trunc(Int, x)
int (generic function with 1 method)
Initialize Model
This method sets up the model and the agents for AntWorld.
It starts by setting the random number generator's seed. Then, it calculates the furthest distance possible an agent could be from another point in the world, for normalization purposes. It then calculates the center of the grid.
Then it initializes matrices for the nest location, pheromone trails, amounts of food, and food source numbers. These are the same dimensions as AntWorld grid and are standard Julia arrays.
Next it establishes the center positions of each food source. Now we iterate over all the positions in the grid and set values in the corresponding Julia array which is then sent to the model properties. The model properties is then used to create the model and subsequently the Ants (agents) are created.
function initialize_model(;number_ants::Int = 125, dimensions::Tuple = (70, 70), diffusion_rate::Int = 50, food_size::Int = 7, random_seed::Int = 2954, nest_size::Int = 5, evaporation_rate::Int = 10, pheromone_amount::Int = 60, spread_pheromone::Bool = false, pheromone_floor::Int = 5, pheromone_ceiling::Int = 100)
@info "Starting the model initialization \n number_ants: $(number_ants)\n dimensions: $(dimensions)\n diffusion_rate: $(diffusion_rate)\n food_size: $(food_size)\n random_seed: $(random_seed)"
rng = Random.Xoshiro(random_seed)
furthest_distance = sqrt(dimensions[1] ^ 2 + dimensions[2] ^ 2)
x_center = dimensions[1] / 2
y_center = dimensions[2] / 2
@debug "x_center: $(x_center) y_center: $(y_center)"
nest_locations = zeros(Float32, dimensions)
pheromone_trails = zeros(Float32, dimensions)
food_amounts = zeros(dimensions)
food_source_number = zeros(dimensions)
food_center_1 = (int(x_center + 0.6 * x_center), int(y_center))
food_center_2 = (int(0.4 * x_center), int(0.4 * y_center))
food_center_3 = (int(0.2 * x_center), int(y_center + 0.8 * y_center))
@debug "Food Center 1: $(food_center_1) Food Center 2: $(food_center_2) Food Center 3: $(food_center_3)"
food_collected = 0
for x_val in 1:dimensions[1]
for y_val in 1:dimensions[2]
nest_locations[x_val, y_val] = ((furthest_distance - sqrt((x_val - x_center) ^ 2 + (y_val - y_center) ^ 2)) / furthest_distance) * 100
food_1 = (sqrt((x_val - food_center_1[1]) ^ 2 + (y_val - food_center_1[2]) ^ 2)) < food_size
food_2 = (sqrt((x_val - food_center_2[1]) ^ 2 + (y_val - food_center_2[2]) ^ 2)) < food_size
food_3 = (sqrt((x_val - food_center_3[1]) ^ 2 + (y_val - food_center_3[2]) ^ 2)) < food_size
food_amounts[x_val, y_val] = food_1 || food_2 || food_3 ? rand(rng, [1, 2]) : 0
if food_1
food_source_number[x_val, y_val] = 1
elseif food_2
food_source_number[x_val, y_val] = 2
elseif food_3
food_source_number[x_val, y_val] = 3
end
end
end
properties = AntWorldProperties(
pheromone_trails,
food_amounts,
nest_locations,
food_source_number,
food_collected,
diffusion_rate,
dimensions[1],
dimensions[2],
nest_size,
evaporation_rate,
pheromone_amount,
spread_pheromone,
pheromone_floor,
pheromone_ceiling
)
model = StandardABM(
Ant,
GridSpace(dimensions, periodic = false);
properties,
rng,
agent_step! = ant_step!,
model_step! = antworld_step!,
scheduler = Schedulers.Randomly(),
container = Vector
)
for n in 1:number_ants
add_agent!((x_center, y_center), model, false, rand(abmrng(model), 1:8), 0, false)
end
@info "Finished the model initialization"
return model
end
initialize_model (generic function with 1 method)
Support Methods
Change direction
This method is used to detect chemical gradients for the ant to turn towards. By accepting a Matrix
, we can generically use this for both following pheromone trails and the way home.
function detect_change_direction(agent::Ant, model_layer::Matrix)
x_dimension = size(model_layer)[1]
y_dimension = size(model_layer)[2]
left_pos = adjacent_dict[mod1(agent.facing_direction - 1, number_directions)]
right_pos = adjacent_dict[mod1(agent.facing_direction + 1, number_directions)]
scent_ahead = model_layer[mod1(agent.pos[1] + adjacent_dict[agent.facing_direction][1], x_dimension),
mod1(agent.pos[2] + adjacent_dict[agent.facing_direction][2], y_dimension)]
scent_left = model_layer[mod1(agent.pos[1] + left_pos[1], x_dimension),
mod1(agent.pos[2] + left_pos[2], y_dimension)]
scent_right = model_layer[mod1(agent.pos[1] + right_pos[1], x_dimension),
mod1(agent.pos[2] + right_pos[2], y_dimension)]
if (scent_right > scent_ahead) || (scent_left > scent_ahead)
if scent_right > scent_left
agent.facing_direction = mod1(agent.facing_direction + 1, number_directions)
else
agent.facing_direction = mod1(agent.facing_direction - 1, number_directions)
end
end
end
detect_change_direction (generic function with 1 method)
Convenience function to have the Ant turn around.
turn_around(agent) = agent.facing_direction = mod1(agent.facing_direction + number_directions / 2, number_directions)
turn_around (generic function with 1 method)
Wiggle
Introduces the ability for some randomness in the ants behavior. Even when following a trail, this will cause ants to Randomly() face somewhere in a 45 degree direction of what is ideal for them.
function wiggle(agent::Ant, model)
direction = rand(abmrng(model), [0, rand(abmrng(model), [-1, 1])])
agent.facing_direction = mod1(agent.facing_direction + direction, number_directions)
end
wiggle (generic function with 1 method)
Apply pheromones
Applies pheromone to the grid. Used by the Ant
when carrying food back to the nest. By default it only applies pheromone to the grid the Ant
is currently on, but there is an option to spread the pheromone to perpendicular spaces at the same time.
function apply_pheromone(agent::Ant, model; pheromone_val::Int = 60, spread_pheromone::Bool = false)
model.pheromone_trails[agent.pos...] += pheromone_val
model.pheromone_trails[agent.pos...] = model.pheromone_trails[agent.pos...] ≥ model.pheromone_floor ? model.pheromone_trails[agent.pos...] : 0
if spread_pheromone
left_pos = adjacent_dict[mod1(agent.facing_direction - 2, number_directions)]
right_pos = adjacent_dict[mod1(agent.facing_direction + 2, number_directions)]
model.pheromone_trails[mod1(agent.pos[1] + left_pos[1], model.x_dimension),
mod1(agent.pos[2] + left_pos[2], model.y_dimension)] += (pheromone_val / 2)
model.pheromone_trails[mod1(agent.pos[1] + right_pos[1], model.x_dimension),
mod1(agent.pos[2] + right_pos[2], model.y_dimension)] += (pheromone_val / 2)
end
end
apply_pheromone (generic function with 1 method)
Diffuse
Diffuse is the method used by the world to spread the pheromone chemicals to adjacent cells. The spread will place (the current amount on grid space * diffusion rate / number of directions) to each adjacent grid space. Then the current space is reduced by the amount that was spread to the surrounding areas.
function diffuse(model_layer::Matrix, diffusion_rate::Int)
x_dimension = size(model_layer)[1]
y_dimension = size(model_layer)[2]
for x_val in 1:x_dimension
for y_val in 1:y_dimension
sum_for_adjacent = model_layer[x_val, y_val] * (diffusion_rate / 100) / number_directions
for (_, i) in adjacent_dict
model_layer[mod1(x_val + i[1], x_dimension), mod1(y_val + i[2], y_dimension)] += sum_for_adjacent
end
model_layer[x_val, y_val] *= ((100 - diffusion_rate) / 100)
end
end
end
diffuse (generic function with 1 method)
Agent Step
The function to perform the ant steps. It is split into two main branches - does the Ant
have food or not.
If the Ant
has food, then if it is at a nest location, it drops off the food and turns around. If not at a nest location, then it determines the best way back to the nest. Finally it lays down some pheromone.
If the Ant
doesn't have food, but it is at a food location, it picks up food and turns around. If it's not at a food location, it tries to follow a pheromone trail to food.
Then it applies a wiggle (random search if without food) and then moves the agent.
function ant_step!(agent::Ant, model)
@debug "Agent State: \n pos: $(agent.pos)\n pos_type:$(typeof(agent.pos)) facing_direction: $(agent.facing_direction)\n has_food: $(agent.has_food)"
if agent.has_food
if model.nest_locations[agent.pos...] > 100 - model.nest_size
@debug "$(agent.n) arrived at nest with food"
agent.food_collected += 1
agent.food_collected_once = true
model.food_collected += 1
agent.has_food = false
turn_around(agent)
else
detect_change_direction(agent, model.nest_locations)
end
apply_pheromone(agent, model, pheromone_val = model.pheromone_amount)
else
if model.food_amounts[agent.pos...] > 0
@debug "$(agent.n) has found food."
agent.has_food = true
model.food_amounts[agent.pos...] -= 1
apply_pheromone(agent, model, pheromone_val = model.pheromone_amount)
turn_around(agent)
elseif model.pheromone_trails[agent.pos...] > model.pheromone_floor
detect_change_direction(agent, model.pheromone_trails)
end
end
wiggle(agent, model)
move_agent!(agent, (mod1(agent.pos[1] + adjacent_dict[agent.facing_direction][1], model.x_dimension), mod1(agent.pos[2] + adjacent_dict[agent.facing_direction][2], model.y_dimension)), model)
end
ant_step! (generic function with 1 method)
Model Step
The model step for AntWorld
. First, it diffuses the chemicals out across the grid.
Then it evaporates some of pheromone from every grid space.
The map! function reduces the amount of pheromone_trails.
function antworld_step!(model)
diffuse(model.pheromone_trails, model.diffusion_rate)
map!((x) -> x ≥ model.pheromone_floor ? x * (100 - model.evaporation_rate) / 100 : 0., model.pheromone_trails, model.pheromone_trails)
if mod1(abmtime(model), 100) == 100
@info "Step $(abmtime(model))"
end
end
antworld_step! (generic function with 1 method)
Displaying and Running
using CairoMakie
Establish a ConsoleLogger
to follow what is happening in the model run.
debuglogger = ConsoleLogger(stderr, Logging.Info)
Base.CoreLogging.ConsoleLogger(IOContext(Base.PipeEndpoint(RawFD(-1) closed, 0 bytes waiting)), Info, Base.CoreLogging.default_metafmt, true, 0, Dict{Any, Int64}())
Displaying heatmap
This function builds a heat map based on various map properties to display in the grid.
It shows the nest location, food locations, and pheromone trails.
Set the value of the heatmap to NaN so it displays as white
function heatmap(model)
heatmap = zeros((model.x_dimension, model.y_dimension))
for x_val in 1:model.x_dimension
for y_val in 1:model.y_dimension
if model.nest_locations[x_val, y_val] > 100 - model.nest_size
heatmap[x_val, y_val] = 150
elseif model.food_amounts[x_val, y_val] > 0
heatmap[x_val, y_val] = 200
elseif model.pheromone_trails[x_val, y_val] > model.pheromone_floor
heatmap[x_val, y_val] = model.pheromone_trails[x_val, y_val] ≥ model.pheromone_floor ? clamp(model.pheromone_trails[x_val, y_val], model.pheromone_floor, model.pheromone_ceiling) : 0
else
heatmap[x_val, y_val] = NaN
end
end
end
return heatmap
end
heatmap (generic function with 1 method)
Turn the ant red when it has food.
ant_color(ant::Ant) = ant.has_food ? :red : :black
ant_color (generic function with 1 method)
Keywords to use for plotting.
ac (agent color) = ant_color function
as (agent size) = the size of the agent
am (agent model) = the icon to use for the agent, here a diamond.
plotkwargs = (
agent_color = ant_color, agent_size = 20, agent_marker = '♦',
heatarray = heatmap,
heatkwargs = (colormap = Reverse(:viridis), colorrange = (0, 200),)
)
(agent_color = Main.ant_color, agent_size = 20, agent_marker = '♦', heatarray = Main.heatmap, heatkwargs = (colormap = Makie.Reverse{Symbol}(:viridis), colorrange = (0, 200)))
Running the model.
There are two options, to explore or to simply get a video of the run.
video = true
with_logger(debuglogger) do
model = initialize_model(;number_ants = 125, random_seed = 6666, pheromone_amount = 60, evaporation_rate = 5)
if !video
params = Dict(
:evaporation_rate => 0:1:100,
:diffusion_rate => 0:1:100,
)
has_food(agent) = agent.has_food
adata = [(:food_collected_once, count)]
mdata = [:food_collected]
@info "Starting exploration"
fig, ax, abmobs = abmplot(
model;
params,
plotkwargs...,
adata, alabels = ["Num Ants Collected"],
mdata, mlabels = ["Total Food Collected"]
)
display(fig)
@info "fig: $(fig)\n ax: $(ax)\n abmobs: $(abmobs)"
else
@info "Starting creating a video"
abmvideo(
"antworld.mp4",
model;
title = "Ant World",
frames = 1000,
plotkwargs...,
)
end
end
┌ Info: Starting the model initialization
│ number_ants: 125
│ dimensions: (70, 70)
│ diffusion_rate: 50
│ food_size: 7
└ random_seed: 6666
[ Info: Finished the model initialization
[ Info: Starting creating a video
[ Info: Step 0
[ Info: Step 100
[ Info: Step 200
[ Info: Step 300
[ Info: Step 400
[ Info: Step 500
[ Info: Step 600
[ Info: Step 700
[ Info: Step 800
[ Info: Step 900