Skip to content

Commit 44476eb

Browse files
committed
Add lecture 4
1 parent 2620a8e commit 44476eb

File tree

6 files changed

+1252
-5
lines changed

6 files changed

+1252
-5
lines changed

docs_vitepress/make.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,11 @@ pages = [
4646
"Lab" => "lab.md",
4747
"Homework" => "hw.md",
4848
]),
49-
# "4: Package development, unit tests & CI" => add_prefix("lecture_04", [
50-
# "Lecture" => "lecture.md",
51-
# "Lab" => "lab.md",
52-
# "Homework" => "hw.md",
53-
# ]),
49+
"4: Package development, unit tests & CI" => add_prefix("lecture_04", [
50+
"Lecture" => "lecture.md",
51+
"Lab" => "lab.md",
52+
"Homework" => "hw.md",
53+
]),
5454
# "5: Performance benchmarking" => add_prefix("lecture_05", [
5555
# "Lecture" => "lecture.md",
5656
# "Lab" => "lab.md",
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
using StatsBase
2+
3+
abstract type Species end
4+
5+
abstract type PlantSpecies <: Species end
6+
abstract type Grass <: PlantSpecies end
7+
8+
abstract type AnimalSpecies <: Species end
9+
abstract type Sheep <: AnimalSpecies end
10+
abstract type Wolf <: AnimalSpecies end
11+
12+
abstract type Agent{S<:Species} end
13+
14+
# instead of Symbols we can use an Enum for the sex field
15+
# using an Enum here makes things easier to extend in case you
16+
# need more than just binary sexes and is also more explicit than
17+
# just a boolean
18+
@enum Sex female male
19+
20+
########## World #############################################################
21+
22+
mutable struct World{A<:Agent}
23+
agents::Dict{Int,A}
24+
max_id::Int
25+
end
26+
27+
function World(agents::Vector{<:Agent})
28+
max_id = maximum(a.id for a in agents)
29+
World(Dict(a.id=>a for a in agents), max_id)
30+
end
31+
32+
# optional: overload Base.show
33+
function Base.show(io::IO, w::World)
34+
println(io, typeof(w))
35+
for (_,a) in w.agents
36+
println(io," $a")
37+
end
38+
end
39+
40+
41+
########## Animals ###########################################################
42+
43+
mutable struct Animal{A<:AnimalSpecies} <: Agent{A}
44+
const id::Int
45+
energy::Float64
46+
const Δenergy::Float64
47+
const reprprob::Float64
48+
const foodprob::Float64
49+
const sex::Sex
50+
end
51+
52+
function (A::Type{<:AnimalSpecies})(id::Int,E::T,ΔE::T,pr::T,pf::T,s::Sex) where T
53+
Animal{A}(id,E,ΔE,pr,pf,s)
54+
end
55+
56+
# get the per species defaults back
57+
randsex() = rand(instances(Sex))
58+
Sheep(id; E=4.0, ΔE=0.2, pr=0.8, pf=0.6, s=randsex()) = Sheep(id, E, ΔE, pr, pf, s)
59+
Wolf(id; E=10.0, ΔE=8.0, pr=0.1, pf=0.2, s=randsex()) = Wolf(id, E, ΔE, pr, pf, s)
60+
61+
62+
function Base.show(io::IO, a::Animal{A}) where {A<:AnimalSpecies}
63+
e = a.energy
64+
d = a.Δenergy
65+
pr = a.reprprob
66+
pf = a.foodprob
67+
s = a.sex == female ? "" : ""
68+
print(io, "$A$s #$(a.id) E=$e ΔE=$d pr=$pr pf=$pf")
69+
end
70+
71+
# note that for new species we will only have to overload `show` on the
72+
# abstract species/sex types like below!
73+
Base.show(io::IO, ::Type{Sheep}) = print(io,"🐑")
74+
Base.show(io::IO, ::Type{Wolf}) = print(io,"🐺")
75+
76+
77+
########## Plants #############################################################
78+
79+
mutable struct Plant{P<:PlantSpecies} <: Agent{P}
80+
const id::Int
81+
size::Int
82+
const max_size::Int
83+
end
84+
85+
# constructor for all Plant{<:PlantSpecies} callable as PlantSpecies(...)
86+
(A::Type{<:PlantSpecies})(id, s, m) = Plant{A}(id,s,m)
87+
(A::Type{<:PlantSpecies})(id, m) = (A::Type{<:PlantSpecies})(id,rand(1:m),m)
88+
89+
# default specific for Grass
90+
Grass(id; max_size=10) = Grass(id, rand(1:max_size), max_size)
91+
92+
function Base.show(io::IO, p::Plant{P}) where P
93+
x = p.size/p.max_size * 100
94+
print(io,"$P #$(p.id) $(round(Int,x))% grown")
95+
end
96+
97+
Base.show(io::IO, ::Type{Grass}) = print(io,"🌿")
98+
99+
100+
########## Eating / Dying / Reproducing ########################################
101+
102+
function eat!(wolf::Animal{Wolf}, sheep::Animal{Sheep}, w::World)
103+
wolf.energy += sheep.energy * wolf.Δenergy
104+
kill_agent!(sheep,w)
105+
end
106+
function eat!(sheep::Animal{Sheep}, grass::Plant{Grass}, ::World)
107+
sheep.energy += grass.size * sheep.Δenergy
108+
grass.size = 0
109+
end
110+
eat!(::Animal, ::Nothing, ::World) = nothing
111+
112+
kill_agent!(a::Agent, w::World) = delete!(w.agents, a.id)
113+
114+
function find_mate(a::Animal, w::World)
115+
ms = filter(x->mates(x,a), w.agents |> values |> collect)
116+
isempty(ms) ? nothing : sample(ms)
117+
end
118+
mates(a::Animal{A}, b::Animal{A}) where A<:AnimalSpecies = a.sex != b.sex
119+
mates(::Agent, ::Agent) = false
120+
121+
function reproduce!(a::Animal{A}, w::World) where A
122+
m = find_mate(a,w)
123+
if !isnothing(m)
124+
a.energy = a.energy / 2
125+
vals = [getproperty(a,n) for n in fieldnames(Animal) if n [:id, :sex]]
126+
new_id = w.max_id + 1
127+
ŝ = Animal{A}(new_id, vals..., randsex())
128+
w.agents[ŝ.id] = ŝ
129+
w.max_id = new_id
130+
end
131+
end
132+
133+
# finding food / who eats who
134+
function find_food(a::Animal, w::World)
135+
as = filter(x -> eats(a,x), w.agents |> values |> collect)
136+
isempty(as) ? nothing : sample(as)
137+
end
138+
eats(::Animal{Sheep},g::Plant{Grass}) = g.size > 0
139+
eats(::Animal{Wolf},::Animal{Sheep}) = true
140+
eats(::Agent,::Agent) = false
141+
142+
143+
########## Stepping through time #############################################
144+
145+
function agent_step!(p::Plant, ::World)
146+
if p.size < p.max_size
147+
p.size += 1
148+
end
149+
end
150+
function agent_step!(a::Animal, w::World)
151+
a.energy -= 1
152+
if rand() <= a.foodprob
153+
dinner = find_food(a,w)
154+
eat!(a, dinner, w)
155+
end
156+
if a.energy <= 0
157+
kill_agent!(a,w)
158+
return
159+
end
160+
if rand() <= a.reprprob
161+
reproduce!(a,w)
162+
end
163+
return a
164+
end
165+
166+
function world_step!(world::World)
167+
# make sure that we only iterate over IDs that already exist in the
168+
# current timestep this lets us safely add agents
169+
ids = copy(keys(world.agents))
170+
171+
for id in ids
172+
# agents can be killed by other agents, so make sure that we are
173+
# not stepping dead agents forward
174+
!haskey(world.agents,id) && continue
175+
176+
a = world.agents[id]
177+
agent_step!(a,world)
178+
end
179+
end
180+
181+
182+
########## Counting agents ####################################################
183+
184+
agent_count(p::Plant) = p.size / p.max_size
185+
agent_count(::Animal) = 1
186+
agent_count(as::Vector{<:Agent}) = sum(agent_count,as)
187+
188+
function agent_count(w::World)
189+
function op(d::Dict,a::Agent{S}) where S<:Species
190+
n = nameof(S)
191+
d[n] = haskey(d,n) ? d[n]+agent_count(a) : agent_count(a)
192+
return d
193+
end
194+
reduce(op, w.agents |> values, init=Dict{Symbol,Float64}())
195+
end
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Plots
2+
include("Lab04Ecosystem.jl")
3+
4+
function make_counter()
5+
n = 0
6+
counter() = n += 1
7+
end
8+
9+
function create_world()
10+
n_grass = 1_000
11+
n_sheep = 40
12+
n_wolves = 4
13+
14+
nextid = make_counter()
15+
16+
World(vcat(
17+
[Grass(nextid()) for _ in 1:n_grass],
18+
[Sheep(nextid()) for _ in 1:n_sheep],
19+
[Wolf(nextid()) for _ in 1:n_wolves],
20+
))
21+
end
22+
world = create_world();
23+
24+
counts = Dict(n=>[c] for (n,c) in agent_count(world))
25+
for _ in 1:100
26+
world_step!(world)
27+
for (n,c) in agent_count(world)
28+
push!(counts[n],c)
29+
end
30+
end
31+
32+
plt = plot()
33+
tolabel(::Type{Animal{Sheep}}) = "Sheep"
34+
tolabel(::Type{Animal{Wolf}}) = "Wolf"
35+
tolabel(::Type{Plant{Grass}}) = "Grass"
36+
for (A,c) in counts
37+
plot!(plt, c, label=tolabel(A), lw=2)
38+
end
39+
display(plt)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# [Homework 4](@id hw4)
2+
3+
In this homework you will have to write two additional `@testset`s for the
4+
Ecosystem. One testset should be contained in a file `test/sheep.jl` and verify
5+
that the function `eat!(::Animal{Sheep}, ::Plant{Grass}, ::World)` works correctly. Another
6+
testset should be in the file `test/wolf.jl` and veryfiy that the function
7+
`eat!(::Animal{Wolf}, ::Animal{Sheep}, ::World)` works correctly.
8+
9+
## How to submit?
10+
11+
Zip the whole package folder `Ecosystem.jl` and upload it to BRUTE.
12+
The package has to include at least the following files:
13+
14+
```
15+
├── src
16+
│ └── Ecosystem.jl
17+
└── test
18+
├── sheep.jl # contains only a single @testset
19+
├── wolf.jl # contains only a single @testset
20+
└── runtests.jl
21+
```
22+
23+
Thet `test/runtests.jl` file can look like this:
24+
25+
```julia
26+
using Test
27+
using Ecosystem
28+
29+
include("sheep.jl")
30+
include("wolf.jl")
31+
# ...
32+
```
33+
34+
## Test `Sheep`
35+
36+
::: danger Homework
37+
38+
1. Create a `Sheep` with food probability $p_f=1$
39+
2. Create *fully grown* `Grass` and a `World` with the two agents.
40+
3. Execute `eat!(::Animal{Sheep}, ::Plant{Grass}, ::World)`
41+
4. `@test` that the size of the `Grass` now has `size == 0`
42+
43+
:::
44+
45+
## Test `Wolf`
46+
47+
::: danger Homework
48+
49+
1. Create a `Wolf` with food probability $p_f=1$
50+
2. Create a `Sheep` and a `World` with the two agents.
51+
3. Execute `eat!(::Animal{Wolf}, ::Animal{Sheep}, ::World)`
52+
4. `@test` that the World only has one agent left in the agents dictionary
53+
54+
:::

0 commit comments

Comments
 (0)