implement AnimalDefinitions in new model
* animals can be defined through AnimalDefinitions. So multiple prey/predator types are possible * Δenergy now decides how much energy the predator gets when the animal is preyed on. It does nothing on predators that have no enemies * added some metrics through death_cause field to see how much prey dies through starvation/predation * predator and prey now use the same functions * currently its not possible to change the animal parameters through the interactive app * implemented an energy_threshold so animals only reproduce when they have enough energy. Leads to less animals starving * implemented new movement logic. Now animals score each neighboring cell based on distance to food and danger. Prevents prey from running into predators TODO: generate the parameter dict for the model so animal parameters can be changed during simulation combine danger and foodscore into one check for occupied cells so they are not scored/chosenmain
parent
41e3a4caad
commit
78552bb044
|
@ -0,0 +1,329 @@
|
|||
using Agents, Random, GLMakie
|
||||
|
||||
@enum DeathCause begin
|
||||
Starvation
|
||||
Predation
|
||||
end
|
||||
mutable struct AnimalDefinition
|
||||
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
|
||||
struct StartDefinition
|
||||
n::Int32
|
||||
def::AnimalDefinition
|
||||
end
|
||||
#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
|
||||
end
|
||||
|
||||
|
||||
function perceive!(a::Animal,model)
|
||||
if a.def.perception > 0
|
||||
nearby = collect(nearby_agents(a, model, a.def.perception))
|
||||
a.nearby_dangers = map(x -> x.pos, filter(x -> isa(x, Animal) && x.def.type ∈ a.def.dangers && isnothing(x.death_cause), nearby))
|
||||
a.nearby_food = map(x -> x.pos, filter(x -> isa(x, Animal) && x.def.type ∈ a.def.food && isnothing(x.death_cause), 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
|
||||
if !isempty(a.nearby_dangers)
|
||||
danger_score = sum(map(danger -> findmax(abs.(pos.-danger))[1], a.nearby_dangers))
|
||||
push!(danger_scores,danger_score)
|
||||
end
|
||||
if !isempty(a.nearby_food)
|
||||
food_score = sum(map(food -> findmax(abs.(pos.-danger))[1], a.nearby_food))
|
||||
push!(food_scores,food_score)
|
||||
end
|
||||
end
|
||||
#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[findmin(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 += prey.def.Δenergy
|
||||
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 > a.def.energy_threshold && rand(abmrng(model)) ≤ a.def.reproduction_prob
|
||||
a.energy /= 2
|
||||
replicate!(a, model)
|
||||
end
|
||||
end
|
||||
|
||||
function Agents.agent2string(agent::Animal)
|
||||
"""
|
||||
Type = $(agent.def.type)
|
||||
ID = $(agent.id)
|
||||
energy = $(agent.energy)
|
||||
perception = $(agent.def.perception)
|
||||
death = $(agent.death_cause)
|
||||
"""
|
||||
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, a.def.perception)
|
||||
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 = [],
|
||||
start_defs = [
|
||||
StartDefinition(100,AnimalDefinition('●',RGBAf(1.0, 1.0, 1.0, 0.8),20, 0.3, 6, 1, "Sheep", ["Wolf"], ["Grass"])),
|
||||
StartDefinition(20,AnimalDefinition('▲',RGBAf(0.2, 0.2, 0.3, 0.8),20, 0.07, 20, 1, "Wolf", [], ["Sheep"]))
|
||||
],
|
||||
dims = (20, 20),
|
||||
regrowth_time = 30,
|
||||
Δenergy_sheep = 4,
|
||||
Δenergy_wolf = 20,
|
||||
Δenergy_grass = 5,
|
||||
sheep_reproduce = 0.04,
|
||||
wolf_reproduce = 0.05,
|
||||
sheep_perception = 0,
|
||||
wolf_perception = 0,
|
||||
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
|
||||
## maybe instead of AnimalDefinition we build the properties dict dynamically and use model properties during the simulation
|
||||
properties = Dict(
|
||||
:events => events,
|
||||
:fully_grown => falses(dims),
|
||||
:countdown => zeros(Int, dims),
|
||||
:regrowth_time => regrowth_time,
|
||||
:Δenergy_sheep => Δenergy_sheep,
|
||||
:Δenergy_wolf => Δenergy_wolf,
|
||||
:Δenergy_grass => Δenergy_grass,
|
||||
:sheep_reproduce => sheep_reproduce,
|
||||
:wolf_reproduce => wolf_reproduce,
|
||||
:sheep_perception => sheep_perception,
|
||||
:wolf_perception => wolf_perception
|
||||
)
|
||||
model = StandardABM(Animal, space;
|
||||
agent_step! = animal_step!, model_step! = model_step!,
|
||||
properties, rng, scheduler = Schedulers.Randomly(), warn = false, agents_first = false
|
||||
)
|
||||
for start_def in start_defs
|
||||
for _ in 1:start_def.n
|
||||
energy = rand(abmrng(model), 1:(start_def.def.Δenergy*2)) - 1
|
||||
add_agent!(Animal, model, energy, start_def.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 a in dead_animals
|
||||
remove_agent!(a, model)
|
||||
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
|
||||
model[id].def.perception += 1
|
||||
end
|
||||
|
||||
elseif event.name == "Flood"
|
||||
model.regrowth_time = event.value
|
||||
for id in ids
|
||||
model[id].def.Δenergy -= 1
|
||||
end
|
||||
|
||||
elseif event.name == "PreyReproduceSeasonal"
|
||||
prey = filter(id -> "Grass" ∈ model[id].def.food, ids)
|
||||
for id in prey
|
||||
model[id].def.reproduction_prob = event.value
|
||||
end
|
||||
|
||||
elseif event.name == "PredatorReproduceSeasonal"
|
||||
predators = filter(id -> !("Grass" ∈ model[id].def.food), ids)
|
||||
for id in predators
|
||||
model[id].def.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
|
||||
model[id].def.perception -= 1
|
||||
end
|
||||
|
||||
elseif event.name == "Flood"
|
||||
model.regrowth_time = event.pre_value
|
||||
for id in ids
|
||||
model[id].def.Δenergy += 1
|
||||
end
|
||||
|
||||
elseif event.name == "PreyReproduceSeasonal"
|
||||
prey = filter(id -> "Grass" ∈ model[id].def.food, ids)
|
||||
for id in prey
|
||||
model[id].def.reproduction_prob = event.pre_value
|
||||
end
|
||||
|
||||
elseif event.name == "PredatorReproduceSeasonal"
|
||||
predators = filter(id -> !("Grass" ∈ model[id].def.food), ids)
|
||||
for id in predators
|
||||
model[id].def.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
|
||||
|
||||
|
||||
mutable struct RecurringEvent
|
||||
name::String
|
||||
value::Float64
|
||||
pre_value::Float64
|
||||
t_start::Int64
|
||||
t_end::Int64
|
||||
t_cycle::Int64
|
||||
timer::Int64
|
||||
end
|
|
@ -0,0 +1,143 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stderr",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[32m\u001b[1m Activating\u001b[22m\u001b[39m project at `~/SCJ/Projekt/SCJ-PredatorPrey/env`\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"import Pkg\n",
|
||||
"Pkg.activate(\"./env\")\n",
|
||||
"Pkg.instantiate()"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 2,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"data": {
|
||||
"text/plain": [
|
||||
"(agent_color = acolor, agent_size = 25, agent_marker = ashape, agentsplotkwargs = (strokewidth = 1.0, strokecolor = :black), heatarray = grasscolor, heatkwargs = (colormap = [:brown, :green], colorrange = (0, 1)))"
|
||||
]
|
||||
},
|
||||
"metadata": {},
|
||||
"output_type": "display_data"
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"# To view our starting population, we can build an overview plot using [`abmplot`](@ref).\n",
|
||||
"# We define the plotting details for the wolves and sheep:\n",
|
||||
"#offset(a) = a.def.type == \"Sheep\" ? (-0.1, -0.1*rand()) : (+0.1, +0.1*rand())\n",
|
||||
"ashape(a) = a.def.symbol\n",
|
||||
"acolor(a) = a.def.color\n",
|
||||
"\n",
|
||||
"# and instruct [`abmplot`](@ref) how to plot grass as a heatmap:\n",
|
||||
"grasscolor(model) = model.countdown ./ model.regrowth_time\n",
|
||||
"# and finally define a colormap for the grass:\n",
|
||||
"heatkwargs = (colormap = [:brown, :green], colorrange = (0, 1))\n",
|
||||
"\n",
|
||||
"# and put everything together and give it to [`abmplot`](@ref)\n",
|
||||
"return plotkwargs = (;\n",
|
||||
" agent_color = acolor,\n",
|
||||
" agent_size = 25,\n",
|
||||
" agent_marker = ashape,\n",
|
||||
" #offset,\n",
|
||||
" agentsplotkwargs = (strokewidth = 1.0, strokecolor = :black),\n",
|
||||
" heatarray = grasscolor,\n",
|
||||
" heatkwargs = heatkwargs,\n",
|
||||
")"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 10,
|
||||
"metadata": {},
|
||||
"outputs": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"output_type": "stream",
|
||||
"text": [
|
||||
"\u001b[32m\u001b[1mStatus\u001b[22m\u001b[39m `~/SCJ/Projekt/SCJ-PredatorPrey/env/Manifest.toml`\n",
|
||||
" \u001b[90m[46ada45e] \u001b[39mAgents v6.0.13\n",
|
||||
" \u001b[90m[e9467ef8] \u001b[39mGLMakie v0.10.2\n"
|
||||
]
|
||||
}
|
||||
],
|
||||
"source": [
|
||||
"include(\"./predator_prey_generic.jl\")\n",
|
||||
"Pkg.status([\"Agents\",\"GLMakie\"]; mode = Pkg.Types.PKGMODE_MANIFEST, io=stdout)\n",
|
||||
"using GLMakie\n",
|
||||
"GLMakie.activate!()\n",
|
||||
"events = RecurringEvent[]\n",
|
||||
"#push!(events, RecurringEvent(\"Drought\", 80, 30, 30, 50, 120, 1))\n",
|
||||
"#push!(events, RecurringEvent(\"Flood\", 50, 30, 70, 80, 120, 1))\n",
|
||||
"push!(events, RecurringEvent(\"PreyReproduceSeasonal\", 0.5, 0.1, 1, 4, 12, 1))\n",
|
||||
"push!(events, RecurringEvent(\"PredatorReproduceSeasonal\", 0.1, 0.07, 4, 6, 12, 1))\n",
|
||||
"sheep_def = AnimalDefinition('●',RGBAf(1.0, 1.0, 1.0, 0.8),20, 0.3, 20, 3, \"Sheep\", [\"Wolf\",\"Bear\"], [\"Grass\"])\n",
|
||||
"wolf_def = AnimalDefinition('▲',RGBAf(0.2, 0.2, 0.3, 0.8),20, 0.07, 20, 1, \"Wolf\", [], [\"Sheep\"])\n",
|
||||
"bear_def = AnimalDefinition('■',RGBAf(1.0, 0.8, 0.5, 0.8),20, 0.07, 20, 1, \"Bear\", [], [\"Sheep\"])\n",
|
||||
"\n",
|
||||
"stable_params = (;\n",
|
||||
" events = events,\n",
|
||||
" start_defs = [StartDefinition(30,sheep_def),StartDefinition(3,wolf_def),StartDefinition(3,bear_def)],\n",
|
||||
" dims = (30, 30),\n",
|
||||
" regrowth_time = 30,\n",
|
||||
" Δenergy_grass = 6,\n",
|
||||
" seed = 71758,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"params = Dict(\n",
|
||||
" :regrowth_time => 0:1:100,\n",
|
||||
" :Δenergy_grass => 0:1:50,\n",
|
||||
")\n",
|
||||
"\n",
|
||||
"sheep(a) = a.def.type == \"Sheep\"\n",
|
||||
"wolf(a) = a.def.type == \"Wolf\"\n",
|
||||
"eaten(a) = a.def.type == \"Sheep\" && a.death_cause == Predation\n",
|
||||
"starved(a) = a.def.type == \"Sheep\" && a.death_cause == Starvation\n",
|
||||
"count_grass(model) = count(model.fully_grown)\n",
|
||||
"adata = [(sheep, count), (wolf, count), (eaten, count), (starved, count)]\n",
|
||||
"mdata = [count_grass]\n",
|
||||
"model = initialize_model(;stable_params...)\n",
|
||||
"fig, abmobs = abmexploration(\n",
|
||||
" model;\n",
|
||||
" params,\n",
|
||||
" plotkwargs...,\n",
|
||||
" adata,\n",
|
||||
" alabels = [\"Sheep\", \"Wolf\", \"Eaten\", \"Starved\"],\n",
|
||||
" mdata, mlabels = [\"Grass\"]\n",
|
||||
")\n",
|
||||
"#, step! = (model) -> begin event_handler!(model, \"Dürre\") model.wolf_reproduce = 0.1 Agents.step!() end\n",
|
||||
"#fig, ax, abmobs = abmplot(model; add_controls=true, plotkwargs...)\n",
|
||||
"\n",
|
||||
"fig\n",
|
||||
"#run!(model, 100)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Julia 1.10.3",
|
||||
"language": "julia",
|
||||
"name": "julia-1.10"
|
||||
},
|
||||
"language_info": {
|
||||
"file_extension": ".jl",
|
||||
"mimetype": "application/julia",
|
||||
"name": "julia",
|
||||
"version": "1.10.3"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 2
|
||||
}
|
Loading…
Reference in New Issue