diff --git a/docs/src/lecture_04/Lab04Ecosystem.jl b/docs/src/lecture_04/Lab04Ecosystem.jl new file mode 100644 index 00000000..54a03db0 --- /dev/null +++ b/docs/src/lecture_04/Lab04Ecosystem.jl @@ -0,0 +1,198 @@ +using StatsBase + +abstract type Species end + +abstract type PlantSpecies <: Species end +abstract type Grass <: PlantSpecies end + +abstract type AnimalSpecies <: Species end +abstract type Sheep <: AnimalSpecies end +abstract type Wolf <: AnimalSpecies end + +abstract type Agent{S<:Species} end + +# instead of Symbols we can use an Enum for the sex field +# using an Enum here makes things easier to extend in case you +# need more than just binary sexes and is also more explicit than +# just a boolean +@enum Sex female male + +########## World ############################################################# + +mutable struct World{A<:Agent} + agents::Dict{Int,A} + max_id::Int +end + +function World(agents::Vector{<:Agent}) + max_id = maximum(a.id for a in agents) + World(Dict(a.id=>a for a in agents), max_id) +end + +# optional: overload Base.show +function Base.show(io::IO, w::World) + println(io, typeof(w)) + for (_,a) in w.agents + println(io," $a") + end +end + + +########## Animals ########################################################### + +mutable struct Animal{A<:AnimalSpecies} <: Agent{A} + const id::Int + energy::Float64 + const Δenergy::Float64 + const reprprob::Float64 + const foodprob::Float64 + const sex::Sex +end + +function (A::Type{<:AnimalSpecies})(id::Int,E::T,ΔE::T,pr::T,pf::T,s::Sex) where T + Animal{A}(id,E,ΔE,pr,pf,s) +end + +# get the per species defaults back +randsex() = rand(instances(Sex)) +Sheep(id; E=4.0, ΔE=0.2, pr=0.8, pf=0.6, s=randsex()) = Sheep(id, E, ΔE, pr, pf, s) +Wolf(id; E=10.0, ΔE=8.0, pr=0.1, pf=0.2, s=randsex()) = Wolf(id, E, ΔE, pr, pf, s) + + +function Base.show(io::IO, a::Animal{A}) where {A<:AnimalSpecies} + e = a.energy + d = a.Δenergy + pr = a.reprprob + pf = a.foodprob + s = a.sex == female ? "♀" : "♂" + print(io, "$A$s #$(a.id) E=$e ΔE=$d pr=$pr pf=$pf") +end + +# note that for new species we will only have to overload `show` on the +# abstract species/sex types like below! +Base.show(io::IO, ::Type{Sheep}) = print(io,"🐑") +Base.show(io::IO, ::Type{Wolf}) = print(io,"🐺") + + +########## Plants ############################################################# + +mutable struct Plant{P<:PlantSpecies} <: Agent{P} + const id::Int + size::Int + const max_size::Int +end + +# constructor for all Plant{<:PlantSpecies} callable as PlantSpecies(...) +(A::Type{<:PlantSpecies})(id, s, m) = Plant{A}(id,s,m) +(A::Type{<:PlantSpecies})(id, m) = (A::Type{<:PlantSpecies})(id,rand(1:m),m) + +# default specific for Grass +Grass(id; max_size=10) = Grass(id, rand(1:max_size), max_size) + +function Base.show(io::IO, p::Plant{P}) where P + x = p.size/p.max_size * 100 + print(io,"$P #$(p.id) $(round(Int,x))% grown") +end + +Base.show(io::IO, ::Type{Grass}) = print(io,"🌿") + + +########## Eating / Dying / Reproducing ######################################## + +function eat!(wolf::Animal{Wolf}, sheep::Animal{Sheep}, w::World) + wolf.energy += sheep.energy * wolf.Δenergy + kill_agent!(sheep,w) +end +function eat!(sheep::Animal{Sheep}, grass::Plant{Grass}, ::World) + sheep.energy += grass.size * sheep.Δenergy + grass.size = 0 +end +eat!(::Animal, ::Nothing, ::World) = nothing + +kill_agent!(a::Agent, w::World) = delete!(w.agents, a.id) + +function find_mate(a::Animal, w::World) + ms = filter(x->mates(x,a), w.agents |> values |> collect) + isempty(ms) ? nothing : sample(ms) +end +mates(a::Animal{A}, b::Animal{A}) where A<:AnimalSpecies = a.sex != b.sex +mates(::Agent, ::Agent) = false + +function reproduce!(a::Animal{A}, w::World) where A + m = find_mate(a,w) + if !isnothing(m) + a.energy = a.energy / 2 + vals = [getproperty(a,n) for n in fieldnames(Animal) if n ∉ [:id, :sex]] + new_id = w.max_id + 1 + ŝ = Animal{A}(new_id, vals..., randsex()) + w.agents[ŝ.id] = ŝ + w.max_id = new_id + end +end + +# finding food / who eats who +function find_food(a::Animal, w::World) + as = filter(x -> eats(a,x), w.agents |> values |> collect) + isempty(as) ? nothing : sample(as) +end +eats(::Animal{Sheep},g::Plant{Grass}) = g.size > 0 +eats(::Animal{Wolf},::Animal{Sheep}) = true +eats(::Agent,::Agent) = false + + +########## Stepping through time ############################################# + +function agent_step!(p::Plant, ::World) + if p.size < p.max_size + p.size += 1 + end +end +function agent_step!(a::Animal, w::World) + a.energy -= 1 + if rand() <= a.foodprob + dinner = find_food(a,w) + eat!(a, dinner, w) + end + if a.energy <= 0 + kill_agent!(a,w) + return + end + if rand() <= a.reprprob + reproduce!(a,w) + end + return a +end + +function world_step!(world::World) + # make sure that we only iterate over IDs that already exist in the + # current timestep this lets us safely add agents + ids = copy(keys(world.agents)) + + for id in ids + # agents can be killed by other agents, so make sure that we are + # not stepping dead agents forward + !haskey(world.agents,id) && continue + + a = world.agents[id] + agent_step!(a,world) + end +end + + +########## Counting agents #################################################### + +agent_count(p::Plant) = p.size / p.max_size +agent_count(::Animal) = 1 +agent_count(as::Vector{<:Agent}) = sum(agent_count,as) + +function agent_count(w::World) + function op(d::Dict,a::A) where A<:Agent + if A in keys(d) + d[A] += agent_count(a) + else + d[A] = agent_count(a) + end + return d + end + foldl(op, w.agents |> values |> collect, init=Dict()) +end diff --git a/docs/src/lecture_04/grass-sheep-wolf.jl b/docs/src/lecture_04/grass-sheep-wolf.jl new file mode 100644 index 00000000..0dab503e --- /dev/null +++ b/docs/src/lecture_04/grass-sheep-wolf.jl @@ -0,0 +1,39 @@ +using Plots +include("Lab04Ecosystem.jl") + +function make_counter() + n = 0 + counter() = n += 1 +end + +function create_world() + n_grass = 1_000 + n_sheep = 40 + n_wolves = 4 + + nextid = make_counter() + + World(vcat( + [Grass(nextid()) for _ in 1:n_grass], + [Sheep(nextid()) for _ in 1:n_sheep], + [Wolf(nextid()) for _ in 1:n_wolves], + )) +end +world = create_world(); + +counts = Dict(n=>[c] for (n,c) in agent_count(world)) +for _ in 1:100 + world_step!(world) + for (n,c) in agent_count(world) + push!(counts[n],c) + end +end + +plt = plot() +tolabel(::Type{Animal{Sheep}}) = "Sheep" +tolabel(::Type{Animal{Wolf}}) = "Wolf" +tolabel(::Type{Plant{Grass}}) = "Grass" +for (A,c) in counts + plot!(plt, c, label=tolabel(A), lw=2) +end +display(plt) diff --git a/docs/src/lecture_04/hw.md b/docs/src/lecture_04/hw.md new file mode 100644 index 00000000..133f7556 --- /dev/null +++ b/docs/src/lecture_04/hw.md @@ -0,0 +1,61 @@ +# Homework 4 + +In this homework you will have to write two additional `@testset`s for the +Ecosystem. One testset should be contained in a file `test/sheep.jl` and verify +that the function `eat!(::Animal{Sheep}, ::Plant{Grass}, ::World)` works correctly. Another +testset should be in the file `test/wolf.jl` and veryfiy that the function +`eat!(::Animal{Wolf}, ::Animal{Sheep}, ::World)` works correctly. + +## How to submit? + +Zip the whole package folder `Ecosystem.jl` and upload it to BRUTE. +The package has to include at least the following files: + +``` +├── src +│ └── Ecosystem.jl +└── test + ├── sheep.jl # contains only a single @testset + ├── wolf.jl # contains only a single @testset + └── runtests.jl +``` +Thet `test/runtests.jl` file can look like this: +``` +using Test +using Ecosystem + +include("sheep.jl") +include("wolf.jl") +# ... +``` + +## Test `Sheep` + +```@raw html +
+``` +```@example block +function agent_step!(p::Plant, w::World) + if p.size < p.max_size + p.size += 1 + end +end + +function agent_step!(a::Animal, w::World) + a.energy -= 1 + if rand() <= a.foodprob + dinner = find_food(a,w) + eat!(a, dinner, w) + end + if a.energy < 0 + kill_agent!(a,w) + return + end + if rand() <= a.reprprob + reproduce!(a,w) + end +end + +nothing # hide +``` +```@raw html +
+``` +```@example block +# make it possible to eat nothing +eat!(::Animal, ::Nothing, ::World) = nothing + +function world_step!(world::World) + # make sure that we only iterate over IDs that already exist in the + # current timestep this lets us safely add agents + ids = copy(keys(world.agents)) + + for id in ids + # agents can be killed by other agents, so make sure that we are + # not stepping dead agents forward + !haskey(world.agents,id) && continue + + a = world.agents[id] + agent_step!(a,world) + end +end +``` +```@raw html +
+``` +This should have created a new folder `Ecosystem` which looks like below. +``` +. +├── LICENSE +├── Project.toml +├── README.md +├── src +│ └── Ecosystem.jl +└── test + ├── Manifest.toml + ├── Project.toml + └── runtests.jl +``` +If you `]activate /path/to/Ecosystem` you should be able to run `]test` to run the autogenerated test (which is not doing anything) +and get the following output: +``` +(Ecosystem) pkg> test + Testing Ecosystem + Status `/private/var/folders/6h/l9_skfms2v3dt8z3zfnd2jr00000gn/T/jl_zd5Uai/Project.toml` + [e77cd98c] Ecosystem v0.1.0 `~/repos/Ecosystem` + [8dfed614] Test `@stdlib/Test` + Status `/private/var/folders/6h/l9_skfms2v3dt8z3zfnd2jr00000gn/T/jl_zd5Uai/Manifest.toml` + [e77cd98c] Ecosystem v0.1.0 `~/repos/Ecosystem` + [2a0f44e3] Base64 `@stdlib/Base64` + [b77e0a4c] InteractiveUtils `@stdlib/InteractiveUtils` + [56ddb016] Logging `@stdlib/Logging` + [d6f4376e] Markdown `@stdlib/Markdown` + [9a3f8284] Random `@stdlib/Random` + [ea8e919c] SHA v0.7.0 `@stdlib/SHA` + [9e88b42a] Serialization `@stdlib/Serialization` + [8dfed614] Test `@stdlib/Test` + Testing Running tests... +Test Summary: |Time +Ecosystem.jl | None 0.0s + Testing Ecosystem tests passed +``` +```@raw html +
+``` +```julia +# src/Ecosystem.jl +module Ecosystem + +using StatsBase + +export World +export Species, PlantSpecies, AnimalSpecies, Grass, Sheep, Wolf +export Agent, Plant, Animal +export agent_step!, eat!, eats, find_food, reproduce!, world_step!, agent_count + +# .... + +end +``` +```@raw html +
+``` +```@repl block +# using Ecosystem +using Test + +@testset "Base.show" begin + g = Grass(1,1,1) + s = Animal{Sheep}(2,1,1,1,1,male) + w = Animal{Wolf}(3,1,1,1,1,female) + @test repr(g) == "🌿 #1 100% grown" + @test repr(s) == "🐑♂ #2 E=1.0 ΔE=1.0 pr=1.0 pf=1.0" + @test repr(w) == "🐺♀ #3 E=1.0 ΔE=1.0 pr=1.0 pf=1.0" +end +``` +```@raw html +