2024-06-20 11:51:20 +02:00
using Agents , Random , GLMakie
2024-06-23 20:21:52 +02:00
# needed to check why the prey dies
2024-06-20 11:51:20 +02:00
@enum DeathCause begin
Starvation
Predation
end
2024-06-23 20:21:52 +02:00
# for defining animals. most of the fields dont get used directly, but converted to model parameters to change them in GLMakie
2024-06-20 11:51:20 +02:00
mutable struct AnimalDefinition
2024-06-20 21:14:37 +02:00
n :: Int32
2024-06-20 11:51:20 +02:00
symbol :: Char
color :: GLMakie . ColorTypes . RGBA { Float32 }
2024-06-23 20:21:52 +02:00
reproduction_energy_threshold :: Int32
forage_energy_threshold :: Int32
energy_usage :: Int32
2024-06-20 11:51:20 +02:00
reproduction_prob :: Float64
Δenergy :: Float64
perception :: Int32
type :: String
dangers :: Vector { String }
food :: Vector { String }
end
2024-06-23 20:21:52 +02:00
# some helper functions to get generated model parameters for animals
2024-06-27 00:22:20 +02:00
reproduction_prop ( a , model ) = abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " reproduction_prob " ) ]
Δenergy ( a , model ) = abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " Δenergy " ) ]
perception ( a , model ) = abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " perception " ) ]
reproduction_energy_threshold ( a , model ) = abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " reproduction_energy_threshold " ) ]
forage_energy_threshold ( a , model ) = abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " forage_energy_threshold " ) ]
energy_usage ( a , model ) = abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " energy_usage " ) ]
2024-06-23 20:21:52 +02:00
# Animal with AnimalDefinition and fields that change during simulation
# might be better to use @multiagent and @subagent with predator prey as subtypes. Allows to dispatch different functions per kind and change execution order with schedulers.bykind
2024-06-20 11:51:20 +02:00
@agent struct Animal ( GridAgent { 2 } )
energy :: Float64
2024-06-27 00:22:20 +02:00
color :: GLMakie . ColorTypes . RGBA { Float32 }
symbol :: Char
2024-06-20 11:51:20 +02:00
def :: AnimalDefinition
death_cause :: Union { DeathCause , Nothing }
nearby_dangers
nearby_food
2024-06-23 20:21:52 +02:00
scores
2024-06-20 11:51:20 +02:00
end
2024-06-23 20:21:52 +02:00
# get nearby food and danger for later when choosing the next position
2024-06-20 11:51:20 +02:00
function perceive! ( a :: Animal , model )
2024-06-27 00:22:20 +02:00
if perception ( a , model ) > 0
nearby = collect ( nearby_agents ( a , model , perception ( a , model ) ) )
2024-06-20 20:47:23 +02:00
a . nearby_dangers = map ( x -> x . pos , filter ( x -> isa ( x , Animal ) && x . def . type ∈ a . def . dangers , nearby ) )
a . nearby_food = map ( x -> x . pos , filter ( x -> isa ( x , Animal ) && x . def . type ∈ a . def . food , nearby ) )
2024-06-20 11:51:20 +02:00
if " Grass " ∈ a . def . food
a . nearby_food = [ a . nearby_food ; nearby_fully_grown ( a , model ) ]
end
end
end
2024-06-23 20:21:52 +02:00
# move the animal and subtract energy
2024-06-20 11:51:20 +02:00
function move! ( a :: Animal , model )
2024-06-23 20:21:52 +02:00
best_pos = choose_position ( a , model )
2024-06-20 11:51:20 +02:00
if ! isnothing ( best_pos )
2024-06-23 20:21:52 +02:00
#make sure predators can step on cells with prey, but prey cannot step on other prey
2024-06-20 11:51:20 +02:00
ids = ids_in_position ( best_pos , model )
if ! isempty ( ids ) && model [ first ( ids ) ] . def . type ∈ a . def . food
2024-06-23 20:21:52 +02:00
move_agent! ( a , best_pos , model )
elseif isempty ( best_pos , model )
move_agent! ( a , best_pos , model )
2024-06-20 11:51:20 +02:00
end
else
randomwalk! ( a , model )
end
2024-06-27 00:22:20 +02:00
a . energy -= energy_usage ( a , model )
2024-06-20 11:51:20 +02:00
end
2024-06-23 20:21:52 +02:00
# choose best position based on scoring
# could have also used the AStar pathfinding from Agents.jl with custom cost_function, but this seemed easier
function choose_position ( a :: Animal , model )
scores = [ ]
positions = push! ( collect ( nearby_positions ( a , model , 1 ) ) , a . pos )
2024-06-20 11:51:20 +02:00
for pos in positions
2024-06-23 20:21:52 +02:00
score = 0
2024-06-20 20:47:23 +02:00
for danger in a . nearby_dangers
distance = findmax ( abs . ( pos .- danger ) ) [ 1 ]
if distance != 0
2024-06-23 20:21:52 +02:00
score -= 50 / distance
2024-06-20 20:47:23 +02:00
else
2024-06-23 20:21:52 +02:00
score -= 100
2024-06-20 20:47:23 +02:00
end
2024-06-20 11:51:20 +02:00
end
2024-06-20 20:47:23 +02:00
for food in a . nearby_food
2024-06-27 00:22:20 +02:00
if a . energy < forage_energy_threshold ( a , model )
2024-06-23 20:21:52 +02:00
distance = findmax ( abs . ( pos .- food ) ) [ 1 ]
if distance != 0
score += 1 / distance
else
score += 2
end
2024-06-20 20:47:23 +02:00
end
2024-06-20 11:51:20 +02:00
end
2024-06-23 20:21:52 +02:00
push! ( scores , score )
2024-06-20 11:51:20 +02:00
end
2024-06-23 20:21:52 +02:00
a . scores = scores
return positions [ rand ( abmrng ( model ) , findall ( == ( maximum ( scores ) ) , scores ) ) ]
2024-06-20 11:51:20 +02:00
end
2024-06-23 20:21:52 +02:00
# add energy if predator is on tile with prey, or prey is on tile with grass
2024-06-20 11:51:20 +02:00
function eat! ( a :: Animal , model )
prey = first_prey_in_position ( a , model )
if ! isnothing ( prey )
#remove_agent!(dinner, model)
prey . death_cause = Predation
2024-06-27 00:22:20 +02:00
prey . symbol = 'x'
a . energy += Δenergy ( prey , model )
2024-06-20 11:51:20 +02:00
end
if " Grass " ∈ a . def . food && model . fully_grown [ a . pos ... ]
model . fully_grown [ a . pos ... ] = false
2024-06-23 02:16:55 +02:00
model . growth [ a . pos ... ] = 0
2024-06-20 11:51:20 +02:00
a . energy += model . Δenergy_grass
end
return
end
2024-06-23 20:21:52 +02:00
# dublicate the animal, based on chance and if it has enough energy
2024-06-20 11:51:20 +02:00
function reproduce! ( a :: Animal , model )
2024-06-27 00:22:20 +02:00
if a . energy > reproduction_energy_threshold ( a , model ) && rand ( abmrng ( model ) ) ≤ reproduction_prop ( a , model )
2024-06-20 11:51:20 +02:00
a . energy /= 2
replicate! ( a , model )
end
end
2024-06-23 20:21:52 +02:00
# usefull debug information when hovering over animals in GLMakie
2024-06-20 11:51:20 +02:00
function Agents . agent2string ( agent :: Animal )
2024-06-23 20:21:52 +02:00
scores = " "
2024-06-20 20:47:23 +02:00
f ( x ) = string ( round ( x , digits = 2 ) )
2024-06-23 20:21:52 +02:00
if ! isempty ( agent . scores )
scores = " \n " *
f ( agent . scores [ 6 ] ) * " | " * f ( agent . scores [ 7 ] ) * " | " * f ( agent . scores [ 8 ] ) * " \n " *
f ( agent . scores [ 4 ] ) * " | " * f ( agent . scores [ 9 ] ) * " | " * f ( agent . scores [ 5 ] ) * " \n " *
f ( agent . scores [ 1 ] ) * " | " * f ( agent . scores [ 2 ] ) * " | " * f ( agent . scores [ 3 ] )
2024-06-20 20:47:23 +02:00
end
2024-06-20 11:51:20 +02:00
"""
Type = $ ( agent . def . type )
ID = $ ( agent . id )
energy = $ ( agent . energy )
death = $ ( agent . death_cause )
2024-06-23 20:21:52 +02:00
scores = $ ( scores )
2024-06-20 11:51:20 +02:00
"""
end
2024-06-23 20:21:52 +02:00
# helper functions
2024-06-20 11:51:20 +02:00
function move_away! ( agent , pos , model )
direction = agent . pos .- pos
direction = clamp . ( direction , - 1 , 1 )
walk! ( agent , direction , model )
end
function move_towards! ( agent , pos , model ; ifempty = true )
direction = pos .- agent . pos
direction = clamp . ( direction , - 1 , 1 )
walk! ( agent , direction , model ; ifempty = ifempty )
end
function nearby_fully_grown ( a :: Animal , model )
2024-06-27 00:22:20 +02:00
nearby_pos = nearby_positions ( a . pos , model , perception ( a , model ) )
2024-06-20 11:51:20 +02:00
fully_grown_positions = filter ( x -> model . fully_grown [ x ... ] , collect ( nearby_pos ) )
return fully_grown_positions
end
function random_empty_fully_grown ( positions , model )
n_attempts = 2 * length ( positions )
while n_attempts != 0
2024-06-23 20:21:52 +02:00
pos_choice = rand ( abmrng ( model ) , positions )
2024-06-20 11:51:20 +02:00
isempty ( pos_choice , model ) && return pos_choice
n_attempts -= 1
end
return positions [ 1 ]
end
function first_prey_in_position ( a , model )
ids = ids_in_position ( a . pos , model )
j = findfirst ( id -> model [ id ] isa Animal && model [ id ] . def . type ∈ a . def . food && isnothing ( model [ id ] . death_cause ) , ids )
isnothing ( j ) ? nothing : model [ ids [ j ] ] :: Animal
end
function initialize_model ( ;
events = [ ] ,
2024-06-20 21:14:37 +02:00
animal_defs = [
2024-06-23 20:21:52 +02:00
AnimalDefinition ( 100 , '●' , RGBAf ( 1.0 , 1.0 , 1.0 , 0.8 ) , 20 , 20 , 1 , 0.3 , 6 , 1 , " Sheep " , [ " Wolf " ] , [ " Grass " ] ) ,
AnimalDefinition ( 20 , '▲' , RGBAf ( 0.2 , 0.2 , 0.3 , 0.8 ) , 20 , 20 , 1 , 0.07 , 20 , 1 , " Wolf " , [ ] , [ " Sheep " ] )
2024-06-20 11:51:20 +02:00
] ,
dims = ( 20 , 20 ) ,
regrowth_time = 30 ,
Δenergy_grass = 5 ,
seed = 23182 ,
)
rng = MersenneTwister ( seed )
space = GridSpace ( dims , periodic = true )
2024-06-23 20:21:52 +02:00
## Generate model parameters
2024-06-20 15:04:24 +02:00
animal_properties = generate_animal_parameters ( animal_defs )
2024-06-20 20:47:23 +02:00
model_properties = Dict (
2024-06-20 11:51:20 +02:00
:events => events ,
:fully_grown => falses ( dims ) ,
2024-06-23 02:16:55 +02:00
:growth => zeros ( Int , dims ) ,
2024-06-20 11:51:20 +02:00
:regrowth_time => regrowth_time ,
:Δenergy_grass => Δenergy_grass ,
)
2024-06-20 20:47:23 +02:00
properties = merge ( model_properties , animal_properties )
2024-06-23 20:21:52 +02:00
## Initialize model
2024-06-20 11:51:20 +02:00
model = StandardABM ( Animal , space ;
agent_step! = animal_step! , model_step! = model_step! ,
properties , rng , scheduler = Schedulers . Randomly ( ) , warn = false , agents_first = false
)
2024-06-23 20:21:52 +02:00
## Add animals
2024-06-20 21:14:37 +02:00
for def in animal_defs
for _ in 1 : def . n
energy = rand ( abmrng ( model ) , 1 : ( def . Δenergy * 2 ) ) - 1
2024-06-27 00:22:20 +02:00
add_agent! ( Animal , model , energy , def . color , def . symbol , def , nothing , [ ] , [ ] , [ ] )
2024-06-20 11:51:20 +02:00
end
end
## Add grass with random initial growth
for p in positions ( model )
fully_grown = rand ( abmrng ( model ) , Bool )
2024-06-23 02:16:55 +02:00
growth = fully_grown ? regrowth_time : rand ( abmrng ( model ) , 1 : regrowth_time ) - 1
model . growth [ p ... ] = growth
2024-06-20 11:51:20 +02:00
model . fully_grown [ p ... ] = fully_grown
end
return model
end
2024-06-23 20:21:52 +02:00
# Animals move every step and loose energy. If they dont have enough, they die, otherwise they consume energy and reproduce.
# For fair behaviour we move perception into the model step, so every animal makes its decision on one the same state
2024-06-20 11:51:20 +02:00
function animal_step! ( a :: Animal , model )
2024-06-23 20:21:52 +02:00
#if !isnothing(a.death_cause)
2024-06-20 11:51:20 +02:00
#remove_agent!(a, model)
#return
2024-06-23 20:21:52 +02:00
#end
2024-06-20 20:47:23 +02:00
#perceive!(a, model)
2024-06-20 11:51:20 +02:00
move! ( a , model )
if a . energy < 0
a . death_cause = Starvation
2024-06-27 00:22:20 +02:00
a . symbol = 'x'
2024-06-20 11:51:20 +02:00
return
end
eat! ( a , model )
reproduce! ( a , model )
2024-06-27 00:22:20 +02:00
#if a.energy < 10 && a.def.type != "Wolf"
# a.color = RGBAf(a.energy/10,a.energy/10,a.energy/10,1)
#elseif a.def.type != "Wolf"
# a.color = RGBAf(1,1,1,1)
#end
2024-06-20 11:51:20 +02:00
end
function model_step! ( model )
2024-06-23 02:16:55 +02:00
handle_event! ( model )
2024-06-20 11:51:20 +02:00
grass_step! ( model )
2024-06-23 20:21:52 +02:00
model_animal_step! ( model )
2024-06-20 11:51:20 +02:00
end
2024-06-23 20:21:52 +02:00
function model_animal_step! ( model )
2024-06-20 11:51:20 +02:00
ids = collect ( allids ( model ) )
dead_animals = filter ( id -> ! isnothing ( model [ id ] . death_cause ) , ids )
2024-06-20 20:47:23 +02:00
for id in ids
if ! isnothing ( model [ id ] . death_cause )
remove_agent! ( id , model )
else
perceive! ( model [ id ] , model )
end
2024-06-20 11:51:20 +02:00
end
2024-06-23 20:21:52 +02:00
end
function grass_step! ( model )
@inbounds for p in positions ( model )
2024-06-20 11:51:20 +02:00
if ! ( model . fully_grown [ p ... ] )
2024-06-23 18:29:04 +02:00
if model . growth [ p ... ] ≥ model . regrowth_time
2024-06-20 11:51:20 +02:00
model . fully_grown [ p ... ] = true
else
2024-06-23 02:16:55 +02:00
model . growth [ p ... ] += 1
2024-06-20 11:51:20 +02:00
end
end
end
end
2024-06-23 02:16:55 +02:00
function handle_event! ( model )
2024-06-23 20:21:52 +02:00
agents = collect ( allagents ( model ) )
2024-06-20 11:51:20 +02:00
for event in model . events
if event . timer == event . t_start # start event
if event . name == " Drought "
2024-06-23 18:29:04 +02:00
model . regrowth_time = event . value
2024-06-23 02:16:55 +02:00
2024-06-23 20:21:52 +02:00
predators = filter ( a -> ! ( " Grass " ∈ a . def . food ) , agents )
for a in predators
abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " perception " ) ] = 2
2024-06-20 11:51:20 +02:00
end
2024-06-23 20:21:52 +02:00
elseif event . name == " PreyReproduceSeasonal "
prey = filter ( a -> " Grass " ∈ a . def . food , agents )
for a in prey
abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " reproduction_prob " ) ] = event . value
end
elseif event . name == " PredatorReproduceSeasonal "
predators = filter ( a -> ! ( " Grass " ∈ a . def . food ) , agents )
for a in predators
abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " reproduction_prob " ) ] = event . value
end
2024-06-20 11:51:20 +02:00
elseif event . name == " Flood "
2024-06-23 18:29:04 +02:00
flood_kill_chance = event . value
2024-06-23 20:21:52 +02:00
for a in agents
2024-06-23 18:29:04 +02:00
if ( flood_kill_chance ≥ rand ( abmrng ( model ) ) )
2024-06-23 20:21:52 +02:00
remove_agent! ( a , model )
2024-06-23 18:29:04 +02:00
end
end
for p in positions ( model )
if model . fully_grown [ p ... ]
model . growth [ p ... ] = 0
model . fully_grown [ p ... ] = false
end
2024-06-20 11:51:20 +02:00
end
end
end
2024-06-23 02:16:55 +02:00
if ( event . timer ≥ event . t_start ) && ( event . timer < event . t_end )
if event . name == " Drought "
for p in positions ( model )
dry_out_chance = 0.4 * ( model . growth [ p ... ] / model . regrowth_time )
if model . fully_grown [ p ... ] && ( dry_out_chance ≥ rand ( abmrng ( model ) ) )
model . growth [ p ... ] = rand ( abmrng ( model ) , 1 : model . regrowth_time ) - 1
model . fully_grown [ p ... ] = false
end
end
elseif event . name == " Winter "
block_field_every = 2
i = 1
for p in positions ( model )
if i % block_field_every == 0
2024-06-23 18:29:04 +02:00
model . growth [ p ... ] = 0
2024-06-23 02:16:55 +02:00
model . fully_grown [ p ... ] = false
end
i += 1
end
end
end
2024-06-20 11:51:20 +02:00
if event . timer == event . t_end # end event
if event . name == " Drought "
2024-06-23 18:29:04 +02:00
model . regrowth_time = event . pre_value
2024-06-23 20:53:20 +02:00
predators = filter ( a -> ! ( " Grass " ∈ a . def . food ) , agents )
for a in predators
abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " perception " ) ] = 1
2024-06-23 18:29:04 +02:00
end
2024-06-20 11:51:20 +02:00
2024-06-23 18:29:04 +02:00
elseif event . name == " Winter "
adjust_field_every = 2
i = 1
for p in positions ( model )
if i % adjust_field_every == 0
model . growth [ p ... ] = rand ( abmrng ( model ) , 1 : ( model . regrowth_time ) )
model . fully_grown [ p ... ] = false
end
i += 1
2024-06-20 11:51:20 +02:00
end
elseif event . name == " PreyReproduceSeasonal "
2024-06-23 20:21:52 +02:00
prey = filter ( a -> " Grass " ∈ a . def . food , agents )
for a in prey
abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " reproduction_prob " ) ] = event . pre_value
2024-06-20 11:51:20 +02:00
end
elseif event . name == " PredatorReproduceSeasonal "
2024-06-23 20:21:52 +02:00
predators = filter ( a -> ! ( " Grass " ∈ a . def . food ) , agents )
for a in predators
abmproperties ( model ) [ Symbol ( a . def . type * " _ " * " reproduction_prob " ) ] = event . pre_value
2024-06-20 11:51:20 +02:00
end
end
end
2024-06-23 02:16:55 +02:00
if event . timer == event . t_cycle # reset timer
event . timer = 0
else # continue timer
2024-06-20 11:51:20 +02:00
event . timer += 1
end
end
end
2024-06-20 15:04:24 +02:00
function generate_animal_parameters ( defs :: Vector { AnimalDefinition } )
parameter_dict = Dict ( )
for def in defs
parameter_dict [ Symbol ( def . type * " _ " * " Δenergy " ) ] = def . Δenergy
parameter_dict [ Symbol ( def . type * " _ " * " reproduction_prob " ) ] = def . reproduction_prob
parameter_dict [ Symbol ( def . type * " _ " * " perception " ) ] = def . perception
2024-06-23 20:21:52 +02:00
parameter_dict [ Symbol ( def . type * " _ " * " forage_energy_threshold " ) ] = def . forage_energy_threshold
parameter_dict [ Symbol ( def . type * " _ " * " reproduction_energy_threshold " ) ] = def . reproduction_energy_threshold
parameter_dict [ Symbol ( def . type * " _ " * " energy_usage " ) ] = def . energy_usage
2024-06-20 15:04:24 +02:00
end
return parameter_dict
end
function generate_animal_parameter_ranges ( defs :: Vector { AnimalDefinition } )
parameter_range_dict = Dict ( )
for def in defs
parameter_range_dict [ Symbol ( def . type * " _ " * " Δenergy " ) ] = 0 : 1 : 100
parameter_range_dict [ Symbol ( def . type * " _ " * " reproduction_prob " ) ] = 0 : 0.01 : 1
parameter_range_dict [ Symbol ( def . type * " _ " * " perception " ) ] = 0 : 1 : 10
2024-06-23 20:21:52 +02:00
parameter_range_dict [ Symbol ( def . type * " _ " * " forage_energy_threshold " ) ] = 0 : 1 : 100
parameter_range_dict [ Symbol ( def . type * " _ " * " reproduction_energy_threshold " ) ] = 0 : 1 : 100
parameter_range_dict [ Symbol ( def . type * " _ " * " energy_usage " ) ] = 0 : 1 : 10
2024-06-20 15:04:24 +02:00
end
return parameter_range_dict
end
2024-06-20 11:51:20 +02:00
mutable struct RecurringEvent
name :: String
value :: Float64
pre_value :: Float64
t_start :: Int64
t_end :: Int64
t_cycle :: Int64
timer :: Int64
end