SCJ-PredatorPrey/predator_prey_generic.jl

423 lines
15 KiB
Julia
Raw Permalink Normal View History

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
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")]
# 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
2024-06-27 00:22:20 +02:00
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)
2024-06-27 00:22:20 +02:00
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
2024-06-27 00:22:20 +02:00
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
2024-06-27 00:22:20 +02:00
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
2024-06-27 00:22:20 +02:00
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)
2024-06-27 00:22:20 +02:00
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)
2024-06-27 00:22:20 +02:00
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
2024-06-27 00:22:20 +02:00
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
2024-06-27 00:22:20 +02:00
a.symbol = 'x'
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
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
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
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