diff --git a/predator_prey_generic.jl b/predator_prey_generic.jl new file mode 100644 index 0000000..3ed0548 --- /dev/null +++ b/predator_prey_generic.jl @@ -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 diff --git a/test_predator_prey_generic.ipynb b/test_predator_prey_generic.ipynb new file mode 100644 index 0000000..417193f --- /dev/null +++ b/test_predator_prey_generic.ipynb @@ -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 +}