combine score into one. add some documentation. add energy_threshold to prevent overfeeding

main
Michael Brehm 2024-06-23 20:21:52 +02:00
parent 0e1f7e8a62
commit 28712d3a7c
3 changed files with 567 additions and 169 deletions

2
env/Project.toml vendored
View File

@ -2,4 +2,6 @@
Agents = "46ada45e-f475-11e8-01d0-f70cc89e6671"
AgentsExampleZoo = "88acaeb2-2f63-4ada-bca2-2825d9da22ed"
CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
GLMakie = "e9467ef8-e4e7-5192-8a1a-b1aee30e663a"
Plots = "91a5bcdd-55d7-5caf-9e0b-520d859cae80"

View File

@ -1,14 +1,19 @@
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}
energy_threshold::Float64
reproduction_energy_threshold::Int32
forage_energy_threshold::Int32
energy_usage::Int32
reproduction_prob::Float64
Δenergy::Float64
perception::Int32
@ -16,22 +21,27 @@ mutable struct AnimalDefinition
dangers::Vector{String}
food::Vector{String}
end
# some helper functions to get generated model parameters for animals
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
reproduction_energy_threshold(a) = abmproperties(model)[Symbol(a.def.type*"_"*"reproduction_energy_threshold")]
forage_energy_threshold(a) = abmproperties(model)[Symbol(a.def.type*"_"*"forage_energy_threshold")]
energy_usage(a) = 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
def::AnimalDefinition
death_cause::Union{DeathCause,Nothing}
nearby_dangers
nearby_food
food_scores
danger_scores
scores
end
# get nearby food and danger for later when choosing the next position
function perceive!(a::Animal,model)
if perception(a) > 0
nearby = collect(nearby_agents(a, model, perception(a)))
@ -42,62 +52,56 @@ function perceive!(a::Animal,model)
end
end
end
# move the animal and subtract energy
function move!(a::Animal,model)
best_pos = calculate_best_pos(a,model)
best_pos = choose_position(a,model)
if !isnothing(best_pos)
#make sure predators can step on cells with prey by setting ifempty to false
#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_towards!(a, best_pos, model; ifempty = false)
else
move_towards!(a, best_pos, model)
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 -= 1
a.energy -= energy_usage(a)
end
function calculate_best_pos(a::Animal,model)
danger_scores = []
food_scores = []
positions = collect(nearby_positions(a, model, 1))
# weight scores with utility functions
# 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
danger_score = 0
score = 0
for danger in a.nearby_dangers
distance = findmax(abs.(pos.-danger))[1]
if distance != 0
danger_score=danger_score-1/distance
score -= 50/distance
else
danger_score-=2
score -= 100
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
if a.energy < forage_energy_threshold(a)
distance = findmax(abs.(pos.-food))[1]
if distance != 0
score += 1/distance
else
score += 2
end
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
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)
@ -112,38 +116,35 @@ function eat!(a::Animal, model)
end
return
end
# dublicate the animal, based on chance and if it has enough energy
function reproduce!(a::Animal, model)
if a.energy > energy_threshold(a) && rand(abmrng(model)) reproduction_prop(a)#a.def.reproduction_prob
if a.energy > reproduction_energy_threshold(a) && rand(abmrng(model)) reproduction_prop(a)
a.energy /= 2
replicate!(a, model)
end
end
# usefull debug information when hovering over animals in GLMakie
function Agents.agent2string(agent::Animal)
food_scores = ""
danger_scores = ""
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])
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)
perception = $(agent.def.perception)
death = $(agent.death_cause)
food_scores = $(food_scores)
danger_scores = $(danger_scores)
scores = $(scores)
"""
end
# helper functions
function move_away!(agent, pos, model)
direction = agent.pos .- pos
direction = clamp.(direction,-1,1)
@ -162,7 +163,7 @@ end
function random_empty_fully_grown(positions, model)
n_attempts = 2*length(positions)
while n_attempts != 0
pos_choice = rand(positions)
pos_choice = rand(abmrng(model), positions)
isempty(pos_choice, model) && return pos_choice
n_attempts -= 1
end
@ -177,8 +178,8 @@ 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"])
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,
@ -187,10 +188,7 @@ function initialize_model(;
)
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
## Generate model parameters
animal_properties = generate_animal_parameters(animal_defs)
model_properties = Dict(
:events => events,
@ -200,14 +198,16 @@ function initialize_model(;
:Δ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, nothing, [], [], [], [])
add_agent!(Animal, model, energy, def, nothing, [], [], [])
end
end
## Add grass with random initial growth
@ -220,21 +220,13 @@ function initialize_model(;
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.
# 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)
#if !isnothing(a.death_cause)
#remove_agent!(a, model)
#return
end
#end
#perceive!(a, model)
move!(a, model)
if a.energy < 0
@ -248,12 +240,10 @@ end
function model_step!(model)
handle_event!(model)
grass_step!(model)
model_animal_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)
function model_animal_step!(model)
ids = collect(allids(model))
dead_animals = filter(id -> !isnothing(model[id].death_cause), ids)
for id in ids
@ -263,7 +253,10 @@ function grass_step!(model)
perceive!(model[id], model)
end
end
@inbounds for p in positions(model) # we don't have to enable bound checking
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
@ -275,22 +268,34 @@ function grass_step!(model)
end
function handle_event!(model)
ids = collect(allids(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(id -> !("Grass" model[id].def.food), ids)
for id in predators
abmproperties(model)[Symbol(model[id].def.type*"_"*"perception")] = 2
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 id in ids
for a in agents
if (flood_kill_chance rand(abmrng(model)))
remove_agent!(model[id], model)
remove_agent!(a, model)
end
end
@ -300,18 +305,6 @@ function handle_event!(model)
model.fully_grown[p...] = false
end
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
@ -343,7 +336,7 @@ function handle_event!(model)
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)
predators = filter(id -> !("Grass" model[id].def.food), agents)
for id in predators
abmproperties(model)[Symbol(model[id].def.type*"_"*"perception")] = 1
end
@ -360,15 +353,15 @@ function handle_event!(model)
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
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(id -> !("Grass" model[id].def.food), ids)
for id in predators
abmproperties(model)[Symbol(model[id].def.type*"_"*"reproduction_prob")] = event.pre_value
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
@ -388,7 +381,9 @@ function generate_animal_parameters(defs::Vector{AnimalDefinition})
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
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
@ -399,7 +394,9 @@ function generate_animal_parameter_ranges(defs::Vector{AnimalDefinition})
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
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

File diff suppressed because one or more lines are too long