Skip to content

Commit 739a15a

Browse files
committed
Lab7: Added ecosystem macro exercise and hw.
1 parent 0da1aa6 commit 739a15a

File tree

2 files changed

+267
-3
lines changed

2 files changed

+267
-3
lines changed

docs/src/lecture_07/hw.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,47 @@
1-
# Homework 7:
1+
# [Homework 7: Creating world in 3 days/steps](@id hw07)
2+
3+
```@raw html
4+
<div class="admonition is-category-homework">
5+
<header class="admonition-header">Homework (2 points)</header>
6+
<div class="admonition-body">
7+
```
8+
Create a macro `@ecosystem` that should be able to define a world given a list of statements `@add # $species ${optional:sex}`
9+
```julia
10+
world = @ecosystem begin
11+
@add 10 Sheep Female # adds 10 female sheep
12+
@add 2 Sheep Male # adds 2 male sheep
13+
@add 100 Grass # adds 100 pieces of grass
14+
@add 3 Wolf # adds 5 wolf with random sex
15+
end
16+
```
17+
As this is not a small task let's break it into 3 steps
18+
1. Define method `default_config(::Type{T})` for each `T` in `Grass, Wolf,...`, which returns a tuple of default parameters for that particular agent.
19+
2. Define method `_add_agents(max_id, count::Int, species::Type{<:Species})` and `_add_agents(max_id, count::Int, species::Type{<:AnimalSpecies}, sex::Type{<:Sex})` that return an array of `count` agents of species `species` with `id` going from `max_id+1` to `max_id+count`. Default parameters should be constructed with `default_config`.
20+
3. Define the underlying function `_ecosystem(ex)`, which parses the block expression and creates a piece of code that constructs the world.
21+
22+
!!! info "`Type{T}` type"
23+
Some hints about this signature, which we have not yet introduced.
24+
25+
You can test the macro (more precisely the `_ecosystem` function) with the following expression
26+
```julia
27+
ex = :(begin
28+
@add 10 Sheep Female
29+
@add 2 Sheep Male
30+
@add 100 Grass
31+
@add 3 Wolf
32+
end)
33+
genex = _ecosystem(ex)
34+
world = eval(genex)
35+
```
36+
37+
```@raw html
38+
</div></div>
39+
<details class = "solution-body" hidden>
40+
<summary class = "solution-header">Solution:</summary><p>
41+
```
42+
43+
Nothing to see here
44+
45+
```@raw html
46+
</p></details>
47+
```

docs/src/lecture_07/lab.md

Lines changed: 220 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,9 +223,227 @@ p(2) == evalpoly(2, [10,2,3])
223223
</p></details>
224224
```
225225

226-
## Ecosystem DSL
227-
### World definition
226+
## Ecosystem macros
227+
There are at least two ways how we can make our life simpler when using our `Ecosystem` and `EcosystemCore` pkgs. Firstly, recall that in order to test our simulation we always had to write something like this:
228+
```julia
229+
function create_world()
230+
n_grass = 500
231+
regrowth_time = 17.0
232+
233+
n_sheep = 100
234+
Δenergy_sheep = 5.0
235+
sheep_reproduce = 0.5
236+
sheep_foodprob = 0.4
237+
238+
n_wolves = 8
239+
Δenergy_wolf = 17.0
240+
wolf_reproduce = 0.03
241+
wolf_foodprob = 0.02
242+
243+
gs = [Grass(id, regrowth_time) for id in 1:n_grass];
244+
ss = [Sheep(id, 2*Δenergy_sheep, Δenergy_sheep, sheep_reproduce, sheep_foodprob) for id in n_grass+1:n_grass+n_sheep];
245+
ws = [Wolf(id, 2*Δenergy_wolf, Δenergy_wolf, wolf_reproduce, wolf_foodprob) for id in n_grass+n_sheep+1:n_grass+n_sheep+n_wolves];
246+
World(vcat(gs, ss, ws))
247+
end
248+
world = create_world();
249+
```
250+
which includes the tedious process of defining the agent counts, their parameters and last but not least the unique id manipulation. As part of the [HW](@ref hw07) for this lecture you will be tasked to define a simple DSL, which can be used to define a world in a few lines.
251+
252+
Secondly, the definition of a new `Animal` or `Plant`, that did not have any special behavior currently requires quite a bit of repetitive code. For example defining a new plant type `Broccoli` goes as follows
253+
```julia
254+
abstract type Broccoli <: PlantSpecies end
255+
Base.show(io::IO,::Type{Broccoli}) = print(io,"🥦")
256+
257+
EcosystemCore.eats(::Animal{Sheep},::Plant{Broccoli}) = true
258+
```
259+
260+
and definition of a new animal like a `Rabbit` looks very similar
261+
```julia
262+
abstract type Rabbit <: AnimalSpecies end
263+
Base.show(io::IO,::Type{Rabbit}) = print(io,"🐇")
264+
265+
EcosystemCore.eats(::Animal{Rabbit},::Plant{Grass}) = true
266+
EcosystemCore.eats(::Animal{Rabbit},::Plant{Broccoli}) = true
267+
```
268+
In order to make these relation clearer we will create two macros, which can be called at one place to construct all the relations.
269+
228270
### New Animal/Plant definition
271+
Our goal is to be able to define new plants and animal species, while having a clear idea about their relations. For this we have proposed the following macros/syntax:
272+
```julia
273+
@plant begin
274+
name -> Broccoli
275+
icon -> 🥦
276+
end
277+
278+
@animal begin
279+
name -> Rabbit
280+
icon -> 🐇
281+
eats -> [Grass => 0.5ΔE, Broccoli => 1.0ΔE, Mushroom => -1.0ΔE]
282+
end
283+
```
284+
285+
286+
Unfortunately the current version of `Ecosystem` and `EcosystemCore`, already contains some definitions of species such as `Sheep`, `Wolf` and `Mushroom`, which would collide with the new definition thus there exists a modified version of those pkgs. **TODO LINK IT**
287+
288+
We can test the current definition with the following code that constructs "eating matrix"
289+
```julia
290+
using Ecosystem
291+
using Ecosystem.EcosystemCore
292+
293+
function eating_matrix()
294+
_init(ps::Type{<:PlantSpecies}) = ps(1, 10.0)
295+
_init(as::Type{<:AnimalSpecies}) = as(1, 10.0, 1.0, 0.8, 0.7)
296+
function _check(s1, s2)
297+
try
298+
if s1 !== s2
299+
EcosystemCore.eats(_init(s1), _init(s2)) ? "" : ""
300+
else
301+
return ""
302+
end
303+
catch e
304+
if e isa MethodError
305+
return ""
306+
else
307+
throw(e)
308+
end
309+
end
310+
end
311+
312+
animal_species = subtypes(AnimalSpecies)
313+
plant_species = subtypes(PlantSpecies)
314+
species = vcat(animal_species, plant_species)
315+
em = [_check(s, ss) for (s,ss) in Iterators.product(animal_species, species)]
316+
string.(hcat(["🌍", animal_species...], vcat(permutedims(species), em)))
317+
end
318+
eating_matrix()
319+
```
320+
321+
```@raw html
322+
<div class="admonition is-category-exercise">
323+
<header class="admonition-header">Exercise</header>
324+
<div class="admonition-body">
325+
```
326+
Define macros `@plant` and `@animal`, which define the functionality of agents based on the following sample syntax
327+
```julia
328+
@plant begin
329+
name => Broccoli
330+
icon => 🥦
331+
end
332+
333+
@animal begin
334+
name => Rabbit
335+
icon => 🐇
336+
eats => [Grass => 0.5ΔE, Broccoli => 1.0ΔE, Mushroom => -1.0ΔE]
337+
end
338+
```
339+
Syntax `Grass => 0.5ΔE` indicates defines the behavior of the `eat!` function, where the coefficient is used as a multiplier for the energy balance, in other words the `Rabbit` should get only `0.5` of energy for a piece of `Grass`.
340+
341+
Define first helper functions `_plant` and `_animal` to inspect the respective macro's output. This is indispensable, as we are defining new types/constants and thus we would otherwise encountered errors during repeated evaluation (though only if the type signature changed).
342+
343+
**HINTS**:
344+
- use `QuoteNode` in the show function
345+
- sdfsdf
346+
347+
```@raw html
348+
</div></div>
349+
<details class = "solution-body">
350+
<summary class = "solution-header">Solution:</summary><p>
351+
```
352+
353+
```julia
354+
355+
ex = :(begin
356+
name => Broccoli
357+
icon => 🥦
358+
end)
359+
360+
macro plant(ex)
361+
return _plant(ex)
362+
end
363+
364+
function _plant(ex)
365+
cfg = Dict{Symbol, Symbol}()
366+
for arg in ex.args
367+
if ~(arg isa LineNumberNode) && arg.head == :call && arg.args[1] == :(=>)
368+
carg = arg.args
369+
key = carg[2]
370+
val = carg[3]
371+
cfg[key] = val
372+
end
373+
end
374+
375+
println(cfg)
376+
377+
quote
378+
abstract type $(cfg[:name]) <: PlantSpecies end
379+
Base.show(io::IO, ::Type{$(cfg[:name])}) = print(io,"$(QuoteNode($(cfg[:icon])))")
380+
end
381+
end
382+
383+
_plant(ex)
384+
385+
386+
ex = :(begin
387+
name => Rabbit
388+
icon => 🐇
389+
eats => [Grass => 0.5, Broccoli => 1.0, Mushroom => -1.0]
390+
end)
391+
392+
macro animal(ex)
393+
return _animal(ex)
394+
end
395+
396+
_parse_eats(ex) = Dict(arg.args[2] => arg.args[3] for arg in ex.args if arg.head == :call && arg.args[1] == :(=>))
397+
function _generate_eat(eater::Type{<:AnimalSpecies}, food::Type{<:PlantSpecies}, multiplier)
398+
quote
399+
EcosystemCore.eats(::Animal{$(eater)}, ::Plant{$(food)}) = true
400+
function EcosystemCore.eat!(a::Animal{$(eater)}, p::Plant{$(food)}, w::World)
401+
if size(p)>0
402+
incr_energy!(a, $(multiplier)*size(p)*Δenergy(a))
403+
p.size = 0
404+
end
405+
end
406+
end
407+
end
408+
409+
function _generate_eat(eater::Type{<:AnimalSpecies}, food::Type{<:AnimalSpecies}, multiplier)
410+
quote
411+
EcosystemCore.eats(::Animal{$(eater)}, ::Animal{$(food)}) = true
412+
function EcosystemCore.eat!(ae::Animal{$(eater)}, af::Animal{$(food)}, w::World)
413+
incr_energy!(ae, $(multiplier)*energy(af)*Δenergy(ae))
414+
kill_agent!(af, w)
415+
end
416+
end
417+
end
418+
419+
function _animal(ex)
420+
cfg = Dict{Symbol, Any}()
421+
for arg in ex.args
422+
if ~(arg isa LineNumberNode) && arg.head == :call && arg.args[1] == :(=>)
423+
carg = arg.args
424+
key = carg[2]
425+
val = carg[3]
426+
cfg[key] = key == :eats ? _parse_eats(val) : val
427+
end
428+
end
429+
430+
code = quote
431+
abstract type $(cfg[:name]) <: AnimalSpecies end
432+
Base.show(io::IO, ::Type{$(cfg[:name])}) = print(io,"$(QuoteNode($(cfg[:icon])))")
433+
end
434+
435+
for (k,v) in cfg[:eats]
436+
push!(code.args, _generate_eat(cfg[:name], k, v)) # does not work without first defining the type tree
437+
end
438+
code
439+
end
440+
441+
_animal(ex)
442+
```
443+
444+
```@raw html
445+
</p></details>
446+
```
229447

230448
---
231449
# Resources

0 commit comments

Comments
 (0)