Skip to content
Snippets Groups Projects
Commit 24ac928a authored by xo30xoqa's avatar xo30xoqa
Browse files

Wrote tests for @species macro

This is a major commit with plenty of bug fixes. Turns out, macrology
is *hard*! Not everything works yet, there are still several important
FIXMEs scattered throughout the code. But at least the macro
expansion/module scoping that has been bugging me for most of the past
week has been (mostly) fixed now, or at least avoided. We're getting there...
parent 4a6f9229
No related branches found
No related tags found
No related merge requests found
......@@ -18,6 +18,8 @@ by trait dictionaries passed by them during initialisation.
@agent Animal GridAgent{2} begin
#XXX is it (performance-)wise to use a dict for the traits?
# Doesn't that rather obviate the point of having an agent struct?
# If I could move the mutable traits to the struct, I wouldn't need
# to deepcopy the speciesdict.
traits::Dict{String,Any}
sex::Sex
age::Int32
......@@ -38,6 +40,7 @@ end
Update an animal by one day, executing it's currently active phase function.
"""
function stepagent!(animal::Animal, model::AgentBasedModel)
@debug "Updating $(animalid(animal))."
animal.age += 1
animal.traits[animal.traits["phase"]](animal,model)
end
......@@ -98,6 +101,7 @@ macro species(name, body)
$(esc(body))
vardict = Base.@locals
speciesdict = Dict{String,Any}()
#delete!(speciesdict, $name) #FIXME remove circular reference from speciesdict
for k in keys(vardict)
speciesdict[string(k)] = vardict[k]
end
......@@ -142,7 +146,7 @@ variables:
information).
Several utility macros can be used within the body of `@phase` as a short-hand for
common expressions: `@trait`, `@changephase`, `@respond`, `@here`, `@kill`,
common expressions: `@trait`, `@setphase`, `@respond`, `@here`, `@kill`,
`@reproduce`, `@neighbours`.
Note that the first phase that is defined in a species definition block will be
......@@ -153,10 +157,9 @@ macro phase(name, body)
#XXX make this documentable?
#FIXME Somehow, errors in the phase body are not shown?
quote
$(esc(name)) = function($(esc(:animal))::Animal, $(esc(:model))::AgentBasedModel)
Core.@__doc__ $(esc(name)) = function($(esc(:animal))::Animal, $(esc(:model))::AgentBasedModel)
$(esc(:pos)) = $(esc(:animal)).pos
#@debug "Executing phase "*$(String(name))*":\n"*$(esc(body))
$(esc(body)) #FIXME isn't being executed
$(esc(body))
end
($(esc(:phase)) == "") && ($(esc(:phase)) = $(String(name)))
end
......@@ -176,16 +179,16 @@ macro trait(traitname)
if traitname in fieldnames(Animal)
:($(esc(:animal)).$(traitname))
else
:($(esc(:animal)).traits[string($(QuoteNode(traitname)))])
:($(esc(:animal)).traits[$(String(traitname))])
end
end
"""
@changephase(newphase)
@setphase(newphase)
Switch this animal over to a different phase. This can only be used nested within `@phase`.
"""
macro changephase(newphase)
macro setphase(newphase)
:($(esc(:animal)).traits["phase"] = $(String(newphase)))
end
......@@ -197,7 +200,7 @@ This can only be used nested within `@phase`.
"""
macro respond(eventname, body)
quote
if $(esc(eventname)) in @here(events)
if $(esc(eventname)) in $(esc(:model)).landscape[$(esc(:animal)).pos...].events
$(esc(body))
end
end
......@@ -210,7 +213,7 @@ A utility macro to quickly access a property of the animal's current position.
This can only be used nested within `@phase`.
"""
macro here(property)
:($(esc(:model)).landscape[$(esc(:animal)).pos...].$(property))
:(model.landscape[animal.pos...].$(property))
end
"""
......@@ -220,7 +223,10 @@ Kill this animal. This is a thin wrapper around `kill!()`, and passes on any arg
This can only be used nested within `@phase`.
"""
macro kill(args...)
:(kill!($(esc(:animal)), $(esc(:model)), $(args...)))
quote
kill!($(esc(:animal)), $(esc(:model)), $(args...))
return
end
end
"""
......@@ -240,6 +246,7 @@ Return an iterator over all animals in the given radius around this animal, excl
This can only be used nested within `@phase`.
"""
macro neighbours(radius)
#TODO enable filtering by species
:(nearby_animals($(esc(:animal)), $(esc(:model)), $radius))
end
......@@ -283,6 +290,9 @@ macro habitat(body)
end
end
##XXX Can I make sure (e.g. through `try/catch`) that the following macros
## are not called anywhere outside @habitat/@phase?
"""
@landcover
......@@ -334,13 +344,13 @@ macro distancetoedge()
end
"""
@countanimals(speciesname, radius=0)
@countanimals(species="", radius=0)
Count the number of animals of the given species in this location.
This is a utility wrapper that can only be used nested within `@phase` or `@habitat`.
"""
macro countanimals(speciesname, radius=0)
:(countanimals($(esc(:pos)), $(esc(:model)), $speciesname, $radius))
macro countanimals(args...)
:(countanimals($(esc(:pos)), $(esc(:model)); $(map(esc, args)...)))
end
##TODO add movement macros
......@@ -32,6 +32,7 @@ This can be used to create the `initialise!` variable in a species definition bl
"""
function initpopulation(habitatdescriptor::Function; phase::Union{String,Nothing}=nothing,
popsize::Int64=-1, pairs::Bool=false, asexual::Bool=false)
#TODO add a `popdensity` argument
function(species::Dict{String,Any}, model::AgentBasedModel)
n = 0
lastn = 0
......@@ -42,13 +43,17 @@ function initpopulation(habitatdescriptor::Function; phase::Union{String,Nothing
for x in shuffle!(Vector(1:width))
for y in shuffle!(Vector(1:height))
if habitatdescriptor((x,y), model)
#FIXME we probably have to do a deepcopy here, to prevent all conspecifics
# sharing the same speciesdict object. However, this is (a) expensive
# and (b) we seem to have a self-reference in the speciesdict, leading to
# an infinity loop on deepcopying...
if pairs
add_agent!((x,y), Animal, model, species, female, 0)
add_agent!((x,y), Animal, model, species, male, 0)
add_agent!((x,y), Animal, model, deepcopy(species), female, 0)
add_agent!((x,y), Animal, model, deepcopy(species), male, 0)
n += 2
else
sex = asexual ? hermaphrodite : rand([male, female])
add_agent!((x,y), Animal, model, species, sex, 0)
add_agent!((x,y), Animal, model, deepcopy(species), sex, 0)
n += 1
end
end
......@@ -73,6 +78,7 @@ A simplified version of `initpopulation()`. Creates a function that initialises
`popsize` individuals, spread at random across the landscape.
"""
function initrandompopulation(popsize::Int64; kwargs...)
#XXX How should this be called if users are supposed to use @initialise?
initpopulation(@habitat(true); popsize=popsize, kwargs...)
end
......@@ -87,7 +93,8 @@ Produce one or more offspring for the given animal at its current location.
function reproduce!(animal::Animal, model::AgentBasedModel, n::Int64=1)
for i in 1:n
sex = (animal.sex == hermaphrodite) ? hermaphrodite : rand([male, female])
add_agent!(animal.pos, Animal, model, animal.traits, sex, 0)
species = animal.traits # @eval $(Symbol(animal.traits["name"]))($model) #FIXME
add_agent!(animal.pos, Animal, model, species, sex, 0)
end
@debug "$(animalid(animal)) has reproduced."
end
......@@ -114,6 +121,7 @@ end
Return an iterator over all animals in the given radius around this position.
"""
function nearby_animals(pos::Tuple{Int64,Int64}, model::AgentBasedModel, radius::Int64)
#TODO enable filtering by species
neighbours = (model[id] for id in nearby_ids(pos, model, radius))
Iterators.filter(a -> typeof(a) == Animal, neighbours)
end
......@@ -124,21 +132,22 @@ end
Return an iterator over all animals in the given radius around this animal, excluding itself.
"""
function nearby_animals(animal::Animal, model::AgentBasedModel, radius::Int64)
#TODO enable filtering by species
neighbours = (model[id] for id in nearby_ids(animal.pos, model, radius))
Iterators.filter(a -> typeof(a) == Animal && a.id != animal.id, neighbours)
end
"""
countanimals(pos, model, speciesname, radius=0)
countanimals(pos, model; species="", radius=0)
Count the number of animals of the given species in this location (optionally supplying a radius).
Count the number of animals in this location (optionally supplying a species name and radius).
"""
function countanimals(pos::Tuple{Int64,Int64}, model::AgentBasedModel,
speciesname::String, radius::Int64=0)
function countanimals(pos::Tuple{Int64,Int64}, model::AgentBasedModel;
species::String="", radius::Int64=0)
n = 0
#XXX can we ignore capitalisation in the spelling of `speciesname`?
#XXX can we ignore capitalisation in the spelling of `species`?
for a in nearby_animals(pos, model, radius)
a.traits["name"] == speciesname && (n += 1)
(species == "" || a.traits["name"] == species) && (n += 1)
end
return n
end
......
......@@ -3,10 +3,6 @@
### These are the tests for the nature model (excluding individual species).
###
@testset "Species macros" begin
#TODO
end
@testset "Habitat macros" begin
# set up the testing landscape
model = smalltestlandscape(Union{Animal,FarmPlot})
......@@ -16,12 +12,12 @@ end
add_agent!((3,3), Animal, model, species, Ps.male, 1)
add_agent!((4,4), Animal, model, species, Ps.female, 1)
# create a set of habitat descriptors
h1 = @habitat(Ps.@landcover() == Ps.water)
h2 = @habitat(Ps.@croptype() == Ps.wheat &&
Ps.@cropheight() < 2)
h3 = @habitat(Ps.@distanceto(water) > 2 &&
Ps.@distancetoedge() <= 2)
h4 = @habitat(Ps.@countanimals("test_animal", 1) == 1)
h1 = @habitat(@landcover() == Ps.water)
h2 = @habitat(@croptype() == Ps.wheat &&
@cropheight() < 2)
h3 = @habitat(@distanceto(water) > 2 &&
@distancetoedge() <= 2)
h4 = @habitat(@countanimals(species="test_animal", radius=1) == 1)
# test the descriptors
@test h1((6,4), model) == true
@test h1((5,4), model) == false
......@@ -42,31 +38,104 @@ end
# create a set of initialisation functions
initfun1 = Ps.initrandompopulation(10)
initfun2 = Ps.initrandompopulation(6*6*3, asexual=true)
initfun3 = Ps.initpopulation(@habitat(Ps.@landcover() == Ps.grass), pairs=true)
initfun4 = Ps.initpopulation(@habitat(Ps.@landcover() == Ps.water &&
Ps.@countanimals("test_animal", 0) < 5),
initfun3 = Ps.initpopulation(@habitat(@landcover() == Ps.grass), pairs=true)
initfun4 = Ps.initpopulation(@habitat(@landcover() == Ps.water &&
@countanimals(species="test_animal", radius=0) < 5),
popsize=10)
# apply and test the initialisation functions
@test_logs (:info, "Initialised 10 $(spec)s.") initfun1(species, model)
@test all(a -> a.sex in (Ps.male, Ps.female), allagents(model))
genocide!(model)
@test_logs (:info, "Initialised 108 $(spec)s.") initfun2(species, model)
@test Ps.countanimals((1,1), model, spec, 0) == Ps.countanimals((6,6), model, spec, 0) == 3
@test Ps.countanimals((1,1), model, species=spec, radius=0) ==
Ps.countanimals((6,6), model, species=spec, radius=0) == 3
@test all(a -> a.sex == Ps.hermaphrodite, allagents(model))
genocide!(model)
@test_logs (:info, "Initialised 16 $(spec)s.") initfun3(species, model)
@test Ps.countanimals((2,2), model, spec, 2) == Ps.countanimals((5,3), model, spec, 1) == 0
@test Ps.countanimals((5,5), model, spec, 0) == Ps.countanimals((6,6), model, spec, 0) == 2
@test Ps.countanimals((2,2), model, species=spec, radius=2) ==
Ps.countanimals((5,3), model, species=spec, radius=1) == 0
@test Ps.countanimals((5,5), model, species=spec, radius=0) ==
Ps.countanimals((6,6), model, species=spec, radius=0) == 2
a1, a2 = Ps.nearby_animals((6,6), model, 0)
@test a1.sex != a2.sex
genocide!(model)
@test_logs((:warn, "There are not enough suitable locations for $(spec) in the landscape."),
(:info, "Initialised 5 $(spec)s."),
initfun4(species, model))
@test Ps.countanimals((1,1), model, spec, 4) == 0
@test Ps.countanimals((6,4), model, spec, 0) == 5
@test Ps.countanimals((1,1), model, species=spec, radius=4) == 0
@test Ps.countanimals((6,4), model, species=spec, radius=0) == 5
end
@testset "Population functions" begin
#TODO
@testset "Species macros" begin
# set up an example landscape and species
# (note: have to use `Persephone` instead of `Ps` due to macro expansion issues)
#FIXME I am having tons of trouble here with module scoping and macro expansion,
# a known tricky issue in Julia (https://github.com/JuliaLang/julia/issues/23221,
# https://github.com/p-i-/MetaGuideJulia/wiki#example-swap-macro-to-illustrate-esc).
# Basically, one needs to avoid nesting modules too deeply (i.e. more than one nesting level)
model = smalltestlandscape(Union{Animal,Farmer,FarmPlot})
@species Mermaid begin
ageofmaturity = 2
#@initialise!(@habitat(@landcover() == Persephone.water), pairs=true) #FIXME
initialise! = Persephone.initpopulation(@habitat(@landcover() == Persephone.water), pairs=true)
@phase life begin
@debug "$(Persephone.animalid(animal)) is swimming happily in its pond."
@respond Persephone.pesticide @kill()
@respond Persephone.harvest @setphase(drought)
@debug "Animal: $animal"
if @trait(sex) == Persephone.female && @countanimals() < 3 &&
@trait(age) >= @trait(ageofmaturity)
@debug "$(Persephone.animalid(animal)) is reproducing."
@reproduce()
end
end
@phase drought begin
@debug "$(Persephone.animalid(animal)) is trying to survive a drought."
@respond Persephone.sowing @setphase(life)
end
end
# test a complete mermaid life cycle
pond = (6,4)
mermaid = Mermaid(model)
@test typeof(Mermaid) <: Function
@test typeof(mermaid["life"]) <: Function
@test typeof(mermaid["initialise!"]) <: Function
@test typeof(mermaid[mermaid["phase"]]) <: Function
@test mermaid["phase"] == "life"
@test_logs (:info, "Initialised 2 Mermaids.") mermaid["initialise!"](mermaid, model)
@test Ps.countanimals((1,1), model, radius=4) == 0
@test Ps.countanimals(pond, model) == 2
@test model[1].age == 0
createevent!(model, [pond], Ps.harvest)
@test_logs((:debug, "Mermaid 1 is swimming happily in its pond."),
(:debug, "Mermaid 2 is swimming happily in its pond."),
min_level=Logging.Debug, match_mode=:any,
stepsimulation!(model))
@test model[1].age == 1
@test model[2].traits["phase"] == "drought"
createevent!(model, [pond], Ps.sowing)
@test_logs((:debug, "Mermaid 1 is trying to survive a drought."),
(:debug, "Mermaid 2 is trying to survive a drought."),
min_level=Logging.Debug, match_mode=:any,
stepsimulation!(model))
@test_logs((:debug, "Mermaid 1 is swimming happily in its pond."),
(:debug, "Mermaid 1 has reproduced."),
(:debug, "Mermaid 2 is swimming happily in its pond."),
min_level=Logging.Debug, match_mode=:any,
stepsimulation!(model))
@test Ps.countanimals(pond, model) == 3
createevent!(model, [pond], Ps.pesticide)
@test_logs((:debug, "Mermaid 1 is swimming happily in its pond."),
(:debug, "Mermaid 1 has died."),
(:debug, "Mermaid 2 is swimming happily in its pond."),
(:debug, "Mermaid 2 has died."),
(:debug, "Mermaid 3 is swimming happily in its pond."),
(:debug, "Mermaid 3 has died."),
min_level=Logging.Debug, match_mode=:any,
stepsimulation!(model))
@test Ps.countanimals(pond, model) == 0
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment