423 lines
15 KiB
Julia
423 lines
15 KiB
Julia
using Agents, Random, GLMakie
|
|
|
|
# needed to check why the prey dies
|
|
@enum DeathCause begin
|
|
Starvation
|
|
Predation
|
|
end
|
|
|
|
# for defining animals. most of the fields dont get used directly, but converted to model parameters to change them in GLMakie
|
|
mutable struct AnimalDefinition
|
|
n::Int32
|
|
symbol::Char
|
|
color::GLMakie.ColorTypes.RGBA{Float32}
|
|
reproduction_energy_threshold::Int32
|
|
forage_energy_threshold::Int32
|
|
energy_usage::Int32
|
|
reproduction_prob::Float64
|
|
Δenergy::Float64
|
|
perception::Int32
|
|
type::String
|
|
dangers::Vector{String}
|
|
food::Vector{String}
|
|
end
|
|
|
|
# some helper functions to get generated model parameters for animals
|
|
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")]
|
|
|
|
# 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
|
|
@agent struct Animal(GridAgent{2})
|
|
energy::Float64
|
|
color::GLMakie.ColorTypes.RGBA{Float32}
|
|
symbol::Char
|
|
def::AnimalDefinition
|
|
death_cause::Union{DeathCause,Nothing}
|
|
nearby_dangers
|
|
nearby_food
|
|
scores
|
|
end
|
|
|
|
# get nearby food and danger for later when choosing the next position
|
|
function perceive!(a::Animal,model)
|
|
if perception(a, model) > 0
|
|
nearby = collect(nearby_agents(a, model, perception(a, model)))
|
|
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))
|
|
if "Grass" ∈ a.def.food
|
|
a.nearby_food = [a.nearby_food; nearby_fully_grown(a, model)]
|
|
end
|
|
end
|
|
end
|
|
|
|
# move the animal and subtract energy
|
|
function move!(a::Animal,model)
|
|
best_pos = choose_position(a,model)
|
|
if !isnothing(best_pos)
|
|
#make sure predators can step on cells with prey, but prey cannot step on other prey
|
|
ids = ids_in_position(best_pos, model)
|
|
if !isempty(ids) && model[first(ids)].def.type ∈ a.def.food
|
|
move_agent!(a, best_pos, model)
|
|
elseif isempty(best_pos, model)
|
|
move_agent!(a, best_pos, model)
|
|
end
|
|
else
|
|
randomwalk!(a, model)
|
|
end
|
|
a.energy -= energy_usage(a, model)
|
|
end
|
|
|
|
# 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)
|
|
for pos in positions
|
|
score = 0
|
|
for danger in a.nearby_dangers
|
|
distance = findmax(abs.(pos.-danger))[1]
|
|
if distance != 0
|
|
score -= 50/distance
|
|
else
|
|
score -= 100
|
|
end
|
|
end
|
|
for food in a.nearby_food
|
|
if a.energy < forage_energy_threshold(a, model)
|
|
distance = findmax(abs.(pos.-food))[1]
|
|
if distance != 0
|
|
score += 1/distance
|
|
else
|
|
score += 2
|
|
end
|
|
end
|
|
end
|
|
push!(scores, score)
|
|
end
|
|
a.scores = scores
|
|
return positions[rand(abmrng(model), findall(==(maximum(scores)),scores))]
|
|
end
|
|
|
|
# add energy if predator is on tile with prey, or prey is on tile with grass
|
|
function eat!(a::Animal, model)
|
|
prey = first_prey_in_position(a, model)
|
|
if !isnothing(prey)
|
|
#remove_agent!(dinner, model)
|
|
prey.death_cause = Predation
|
|
prey.symbol = 'x'
|
|
a.energy += Δenergy(prey, model)
|
|
end
|
|
if "Grass" ∈ a.def.food && model.fully_grown[a.pos...]
|
|
model.fully_grown[a.pos...] = false
|
|
model.growth[a.pos...] = 0
|
|
a.energy += model.Δenergy_grass
|
|
end
|
|
return
|
|
end
|
|
|
|
# dublicate the animal, based on chance and if it has enough energy
|
|
function reproduce!(a::Animal, model)
|
|
if a.energy > reproduction_energy_threshold(a, model) && rand(abmrng(model)) ≤ reproduction_prop(a, model)
|
|
a.energy /= 2
|
|
replicate!(a, model)
|
|
end
|
|
end
|
|
|
|
# usefull debug information when hovering over animals in GLMakie
|
|
function Agents.agent2string(agent::Animal)
|
|
scores = ""
|
|
f(x) = string(round(x,digits=2))
|
|
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])
|
|
end
|
|
"""
|
|
Type = $(agent.def.type)
|
|
ID = $(agent.id)
|
|
energy = $(agent.energy)
|
|
death = $(agent.death_cause)
|
|
scores = $(scores)
|
|
"""
|
|
end
|
|
|
|
# helper functions
|
|
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)
|
|
nearby_pos = nearby_positions(a.pos, model, perception(a, model))
|
|
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
|
|
pos_choice = rand(abmrng(model), positions)
|
|
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 = [],
|
|
animal_defs = [
|
|
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"])
|
|
],
|
|
dims = (20, 20),
|
|
regrowth_time = 30,
|
|
Δenergy_grass = 5,
|
|
seed = 23182,
|
|
)
|
|
rng = MersenneTwister(seed)
|
|
space = GridSpace(dims, periodic = true)
|
|
## Generate model parameters
|
|
animal_properties = generate_animal_parameters(animal_defs)
|
|
model_properties = Dict(
|
|
:events => events,
|
|
:fully_grown => falses(dims),
|
|
:growth => zeros(Int, dims),
|
|
:regrowth_time => regrowth_time,
|
|
:Δenergy_grass => Δenergy_grass,
|
|
)
|
|
properties = merge(model_properties,animal_properties)
|
|
## Initialize model
|
|
model = StandardABM(Animal, space;
|
|
agent_step! = animal_step!, model_step! = model_step!,
|
|
properties, rng, scheduler = Schedulers.Randomly(), warn = false, agents_first = false
|
|
)
|
|
## Add animals
|
|
for def in animal_defs
|
|
for _ in 1:def.n
|
|
energy = rand(abmrng(model), 1:(def.Δenergy*2)) - 1
|
|
add_agent!(Animal, model, energy, def.color, def.symbol, def, nothing, [], [], [])
|
|
end
|
|
end
|
|
## Add grass with random initial growth
|
|
for p in positions(model)
|
|
fully_grown = rand(abmrng(model), Bool)
|
|
growth = fully_grown ? regrowth_time : rand(abmrng(model), 1:regrowth_time) - 1
|
|
model.growth[p...] = growth
|
|
model.fully_grown[p...] = fully_grown
|
|
end
|
|
return model
|
|
end
|
|
|
|
# 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
|
|
function animal_step!(a::Animal, model)
|
|
#if !isnothing(a.death_cause)
|
|
#remove_agent!(a, model)
|
|
#return
|
|
#end
|
|
#perceive!(a, model)
|
|
move!(a, model)
|
|
if a.energy < 0
|
|
a.death_cause = Starvation
|
|
a.symbol = 'x'
|
|
return
|
|
end
|
|
eat!(a, model)
|
|
reproduce!(a, model)
|
|
#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
|
|
end
|
|
|
|
function model_step!(model)
|
|
handle_event!(model)
|
|
grass_step!(model)
|
|
model_animal_step!(model)
|
|
end
|
|
|
|
function model_animal_step!(model)
|
|
ids = collect(allids(model))
|
|
dead_animals = filter(id -> !isnothing(model[id].death_cause), ids)
|
|
for id in ids
|
|
if !isnothing(model[id].death_cause)
|
|
remove_agent!(id, model)
|
|
else
|
|
perceive!(model[id], model)
|
|
end
|
|
end
|
|
end
|
|
|
|
function grass_step!(model)
|
|
@inbounds for p in positions(model)
|
|
if !(model.fully_grown[p...])
|
|
if model.growth[p...] ≥ model.regrowth_time
|
|
model.fully_grown[p...] = true
|
|
else
|
|
model.growth[p...] += 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function handle_event!(model)
|
|
agents = collect(allagents(model))
|
|
for event in model.events
|
|
if event.timer == event.t_start # start event
|
|
if event.name == "Drought"
|
|
model.regrowth_time = event.value
|
|
|
|
predators = filter(a -> !("Grass" ∈ a.def.food), agents)
|
|
for a in predators
|
|
abmproperties(model)[Symbol(a.def.type*"_"*"perception")] = 2
|
|
end
|
|
|
|
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
|
|
|
|
elseif event.name == "Flood"
|
|
flood_kill_chance = event.value
|
|
for a in agents
|
|
if (flood_kill_chance ≥ rand(abmrng(model)))
|
|
remove_agent!(a, model)
|
|
end
|
|
end
|
|
|
|
for p in positions(model)
|
|
if model.fully_grown[p...]
|
|
model.growth[p...] = 0
|
|
model.fully_grown[p...] = false
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
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
|
|
model.growth[p...] = 0
|
|
model.fully_grown[p...] = false
|
|
end
|
|
i += 1
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
|
|
if event.timer == event.t_end # end event
|
|
if event.name == "Drought"
|
|
model.regrowth_time = event.pre_value
|
|
predators = filter(a -> !("Grass" ∈ a.def.food), agents)
|
|
for a in predators
|
|
abmproperties(model)[Symbol(a.def.type*"_"*"perception")] = 1
|
|
end
|
|
|
|
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
|
|
end
|
|
|
|
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.pre_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.pre_value
|
|
end
|
|
|
|
end
|
|
end
|
|
|
|
if event.timer == event.t_cycle # reset timer
|
|
event.timer = 0
|
|
else # continue timer
|
|
event.timer += 1
|
|
end
|
|
end
|
|
end
|
|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
end
|
|
return parameter_range_dict
|
|
end
|
|
|
|
|
|
mutable struct RecurringEvent
|
|
name::String
|
|
value::Float64
|
|
pre_value::Float64
|
|
t_start::Int64
|
|
t_end::Int64
|
|
t_cycle::Int64
|
|
timer::Int64
|
|
end
|