SCJ-PredatorPrey/predator_prey_generic.jl

377 lines
14 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
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),
:countdown => 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)
countdown = fully_grown ? regrowth_time : rand(abmrng(model), 1:regrowth_time) - 1
model.countdown[p...] = countdown
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)
event_handler!(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.countdown[p...] 0
model.fully_grown[p...] = true
model.countdown[p...] = model.regrowth_time
else
model.countdown[p...] -= 1
end
end
end
end
# Check current step and start event at step t
function event_handler!(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
for id in ids
abmproperties(model)[Symbol(model[id].def.type*"_"*"perception")] += 1
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_end # end event
if event.name == "Drought"
model.regrowth_time = event.pre_value
for id in ids
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 cycle
event.timer = 1
else
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