406 lines
15 KiB
Julia
406 lines
15 KiB
Julia
using Agents, Random, GLMakie
|
|
|
|
@enum DeathCause begin
|
|
Starvation
|
|
Predation
|
|
end
|
|
mutable struct AnimalDefinition
|
|
n::Int32
|
|
symbol::Char
|
|
color::GLMakie.ColorTypes.RGBA{Float32}
|
|
energy_threshold::Float64
|
|
reproduction_prob::Float64
|
|
Δenergy::Float64
|
|
perception::Int32
|
|
type::String
|
|
dangers::Vector{String}
|
|
food::Vector{String}
|
|
end
|
|
reproduction_prop(a) = abmproperties(model)[Symbol(a.def.type*"_"*"reproduction_prob")]
|
|
Δenergy(a) = abmproperties(model)[Symbol(a.def.type*"_"*"Δenergy")]
|
|
perception(a) = abmproperties(model)[Symbol(a.def.type*"_"*"perception")]
|
|
energy_threshold(a) = abmproperties(model)[Symbol(a.def.type*"_"*"energy_threshold")]
|
|
#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
|
|
def::AnimalDefinition
|
|
death_cause::Union{DeathCause,Nothing}
|
|
nearby_dangers
|
|
nearby_food
|
|
food_scores
|
|
danger_scores
|
|
end
|
|
|
|
|
|
function perceive!(a::Animal,model)
|
|
if perception(a) > 0
|
|
nearby = collect(nearby_agents(a, model, perception(a)))
|
|
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
|
|
function move!(a::Animal,model)
|
|
best_pos = calculate_best_pos(a,model)
|
|
if !isnothing(best_pos)
|
|
#make sure predators can step on cells with prey by setting ifempty to false
|
|
ids = ids_in_position(best_pos, model)
|
|
if !isempty(ids) && model[first(ids)].def.type ∈ a.def.food
|
|
move_towards!(a, best_pos, model; ifempty = false)
|
|
else
|
|
move_towards!(a, best_pos, model)
|
|
end
|
|
else
|
|
randomwalk!(a, model)
|
|
end
|
|
a.energy -= 1
|
|
end
|
|
function calculate_best_pos(a::Animal,model)
|
|
danger_scores = []
|
|
food_scores = []
|
|
positions = collect(nearby_positions(a, model, 1))
|
|
# weight scores with utility functions
|
|
for pos in positions
|
|
danger_score = 0
|
|
for danger in a.nearby_dangers
|
|
distance = findmax(abs.(pos.-danger))[1]
|
|
if distance != 0
|
|
danger_score=danger_score-1/distance
|
|
else
|
|
danger_score-=2
|
|
end
|
|
end
|
|
food_score = 0
|
|
for food in a.nearby_food
|
|
distance = findmax(abs.(pos.-food))[1]
|
|
if distance != 0
|
|
food_score=food_score+1/distance
|
|
else
|
|
food_score+=2
|
|
end
|
|
end
|
|
push!(danger_scores,danger_score)
|
|
push!(food_scores,food_score)
|
|
end
|
|
a.danger_scores = danger_scores
|
|
a.food_scores = food_scores
|
|
#findall(==(minimum(x)),x) to find all mins
|
|
#best to filter out all positions where there is already an agent and take into account the current position, so sheeps dont just stand still when the position is occupied
|
|
if !isempty(a.nearby_dangers) #&& a.energy > a.def.energy_threshold
|
|
safest_position = positions[findmax(danger_scores)[2]]
|
|
return safest_position
|
|
elseif !isempty(a.nearby_food) #&& a.energy < a.def.energy_threshold
|
|
foodiest_position = positions[findmax(food_scores)[2]]
|
|
return foodiest_position
|
|
else
|
|
return nothing
|
|
end
|
|
end
|
|
function eat!(a::Animal, model)
|
|
prey = first_prey_in_position(a, model)
|
|
if !isnothing(prey)
|
|
#remove_agent!(dinner, model)
|
|
prey.death_cause = Predation
|
|
a.energy += Δenergy(prey)
|
|
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
|
|
function reproduce!(a::Animal, model)
|
|
if a.energy > energy_threshold(a) && rand(abmrng(model)) ≤ reproduction_prop(a)#a.def.reproduction_prob
|
|
a.energy /= 2
|
|
replicate!(a, model)
|
|
end
|
|
end
|
|
|
|
function Agents.agent2string(agent::Animal)
|
|
food_scores = ""
|
|
danger_scores = ""
|
|
f(x) = string(round(x,digits=2))
|
|
if !isempty(agent.food_scores)
|
|
food_scores = "\n" * f(agent.food_scores[6]) * "|" * f(agent.food_scores[7]) * "|" * f(agent.food_scores[8]) * "\n" *
|
|
f(agent.food_scores[4]) * "|" * " " * "|" * f(agent.food_scores[5]) * "\n" *
|
|
f(agent.food_scores[1]) * "|" * f(agent.food_scores[2]) * "|" * f(agent.food_scores[3])
|
|
end
|
|
if !isempty(agent.danger_scores)
|
|
danger_scores = "\n" * f(agent.danger_scores[6]) * "|" * f(agent.danger_scores[7]) * "|" * f(agent.danger_scores[8]) * "\n" *
|
|
f(agent.danger_scores[4]) * "|" * " " * "|" * f(agent.danger_scores[5]) * "\n" *
|
|
f(agent.danger_scores[1]) * "|" * f(agent.danger_scores[2]) * "|" * f(agent.danger_scores[3])
|
|
end
|
|
"""
|
|
Type = $(agent.def.type)
|
|
ID = $(agent.id)
|
|
energy = $(agent.energy)
|
|
perception = $(agent.def.perception)
|
|
death = $(agent.death_cause)
|
|
food_scores = $(food_scores)
|
|
danger_scores = $(danger_scores)
|
|
"""
|
|
end
|
|
|
|
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))
|
|
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(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, 0.3, 6, 1, "Sheep", ["Wolf"], ["Grass"]),
|
|
AnimalDefinition(20,'▲',RGBAf(0.2, 0.2, 0.3, 0.8),20, 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)
|
|
## Model properties contain the grass as two arrays: whether it is fully grown
|
|
## and the time to regrow. Also have static parameter `regrowth_time`.
|
|
## Notice how the properties are a `NamedTuple` to ensure type stability.
|
|
## define as dictionary(mutable) instead of tuples(immutable) as per https://github.com/JuliaDynamics/Agents.jl/issues/727
|
|
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)
|
|
model = StandardABM(Animal, space;
|
|
agent_step! = animal_step!, model_step! = model_step!,
|
|
properties, rng, scheduler = Schedulers.Randomly(), warn = false, agents_first = false
|
|
)
|
|
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, 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
|
|
|
|
# ## Defining the stepping functions
|
|
# Sheep and wolves behave similarly:
|
|
# both lose 1 energy unit by moving to an adjacent position and both consume
|
|
# a food source if available. If their energy level is below zero, they die.
|
|
# Otherwise, they live and reproduce with some probability.
|
|
# They move to a random adjacent position with the [`randomwalk!`](@ref) function.
|
|
|
|
# Notice how the function `sheepwolf_step!`, which is our `agent_step!`,
|
|
# is dispatched to the appropriate agent type via Julia's Multiple Dispatch system.
|
|
|
|
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
|
|
return
|
|
end
|
|
eat!(a, model)
|
|
reproduce!(a, model)
|
|
end
|
|
|
|
function model_step!(model)
|
|
handle_event!(model)
|
|
grass_step!(model)
|
|
end
|
|
|
|
# The behavior of grass function differently. If it is fully grown, it is consumable.
|
|
# Otherwise, it cannot be consumed until it regrows after a delay specified by
|
|
# `regrowth_time`. The dynamics of the grass is our `model_step!` function.
|
|
function grass_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
|
|
@inbounds for p in positions(model) # we don't have to enable bound checking
|
|
if !(model.fully_grown[p...])
|
|
if model.growth[p...] ≥ model.regrowth_time#≤ 0
|
|
model.fully_grown[p...] = true
|
|
#model.growth[p...] = model.regrowth_time
|
|
else
|
|
model.growth[p...] += 1
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function handle_event!(model)
|
|
ids = collect(allids(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(id -> !("Grass" ∈ model[id].def.food), ids)
|
|
for id in predators
|
|
abmproperties(model)[Symbol(model[id].def.type*"_"*"perception")] = 2
|
|
end
|
|
|
|
elseif event.name == "Flood"
|
|
model.regrowth_time = event.value
|
|
for id in ids
|
|
abmproperties(model)[Symbol(model[id].def.type*"_"*"Δenergy")] -= 1
|
|
end
|
|
|
|
|
|
elseif event.name == "PreyReproduceSeasonal"
|
|
prey = filter(id -> "Grass" ∈ model[id].def.food, ids)
|
|
for id in prey
|
|
abmproperties(model)[Symbol(model[id].def.type*"_"*"reproduction_prob")] = event.value
|
|
end
|
|
|
|
elseif event.name == "PredatorReproduceSeasonal"
|
|
predators = filter(id -> !("Grass" ∈ model[id].def.food), ids)
|
|
for id in predators
|
|
abmproperties(model)[Symbol(model[id].def.type*"_"*"reproduction_prob")] = event.value
|
|
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...] = 0
|
|
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...] = rand(abmrng(model), 1:(model.regrowth_time / 2))
|
|
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(id -> !("Grass" ∈ model[id].def.food), ids)
|
|
for id in predators
|
|
abmproperties(model)[Symbol(model[id].def.type*"_"*"perception")] = 1
|
|
end
|
|
|
|
elseif event.name == "Flood"
|
|
model.regrowth_time = event.pre_value
|
|
for id in ids
|
|
abmproperties(model)[Symbol(model[id].def.type*"_"*"Δenergy")] += 1
|
|
end
|
|
|
|
elseif event.name == "PreyReproduceSeasonal"
|
|
prey = filter(id -> "Grass" ∈ model[id].def.food, ids)
|
|
for id in prey
|
|
abmproperties(model)[Symbol(model[id].def.type*"_"*"reproduction_prob")] = event.pre_value
|
|
end
|
|
|
|
elseif event.name == "PredatorReproduceSeasonal"
|
|
predators = filter(id -> !("Grass" ∈ model[id].def.food), ids)
|
|
for id in predators
|
|
abmproperties(model)[Symbol(model[id].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*"_"*"energy_threshold")]=def.energy_threshold
|
|
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*"_"*"energy_threshold")]=0:1:100
|
|
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
|