Skip to content
Snippets Groups Projects
Commit 4f3d4f44 authored by xo30xoqa's avatar xo30xoqa
Browse files

Updated remaining animal tests

parent 6c2d8898
Branches
Tags
No related merge requests found
......@@ -21,9 +21,9 @@ way the species definition macros work and are used.
- functions (and associated macros) to replace Agents.jl functionality:
- `move!()` and `walk!()`
- `nearby_ids()`, `nearby_animals()`, `neighbours()`
- `nearby_ids()`, `nearby_animals()`, `countanimals()`, `neighbours()`
- `directionto()`, `distanceto()`, `randomdirection()`
- `nagents()`
- `nagents()`, `killallanimals!()`
- `@here`
......
......@@ -80,6 +80,7 @@ export
@randompixel,
@randomdirection,
@nearby_animals,
@countanimals,
@neighbours,
@move,
@walk,
......
......@@ -35,7 +35,7 @@ end
Return the total number of agents in a model object.
"""
function nagents(model::AgricultureModel)
length(model.animals)+length(model.migrants)+length(model.farmers)+length(model.farmplots)
length(model.animals)+length(model.farmers)+length(model.farmplots)
end
"""
......
......@@ -48,7 +48,7 @@ the `model` variable (an object of type `SimulationModel`).
macro species(name, body)
quote
@kwdef mutable struct $name <: Animal
#FIXME once Julia 1.11 is released, escape $name above
#TODO once Julia 1.11 is released, escape $name above
#(https://discourse.julialang.org/t/kwdef-constructor-not-available-outside-of-module/114675/4)
const id::Int64
const sex::Sex = hermaphrodite
......@@ -389,8 +389,21 @@ Return an iterator over all animals in the given radius around the current posit
This can only be used nested within [`@phase`](@ref) or [`@habitat`](@ref).
"""
macro nearby_animals(args...)
#FIXME doesn't work properly when nested in `@habitat` (kwargs not recognised)
#XXX do I need this macro if I have @countanimals and @neighbours?
#XXX does it make sense to use `pos` here? What if an an animal wants to look at another place?
:(nearby_animals($(esc(:pos)), $(esc(:model)), $(map(esc, args)...))) #FIXME
:(nearby_animals($(esc(:pos)), $(esc(:model)), $(map(esc, args)...)))
end
"""
@countanimals(radius=0, species="")
Count the number of animals at or near this location, optionally filtering by species.
This can only be used nested within [`@phase`](@ref) or [`@habitat`](@ref).
"""
macro countanimals(args...)
#FIXME doesn't work properly when nested in `@habitat` (kwargs not recognised)
:(countanimals($(esc(:pos)), $(esc(:model)), $(map(esc, args)...)))
end
"""
......
......@@ -32,9 +32,10 @@ abstract type Animal <: ModelAgent end
Return the species name of this animal as a string.
"""
function speciesof(a::Animal)
function speciesof(a::Union{Animal,Type})
# strip out the module name if necessary (`Persefone.<species>`)
spstrings = split(string(typeof(a)), ".")
(a isa Animal) && (a = typeof(a))
spstrings = split(string(a), ".")
length(spstrings) == 1 ? spstrings[1] : spstrings[2]
end
......@@ -122,3 +123,17 @@ function updatenature!(model::SimulationModel)
deleteat!(model.migrants, 1)
end
end
"""
killallanimals!(model)
Remove all animal individuals from the simulation.
"""
function killallanimals!(model)
for a in model.animals
kill!(a, model)
end
model.migrants = Vector{Pair{Animal, Date}}()
model.animals = Vector{Union{Animal,Nothing}}()
return
end
......@@ -40,7 +40,7 @@ using [`@populate`](@ref).
birthphase::Function
habitat::Function = @habitat(true)
popsize::Int64 = -1
popdensity::Int64 = -1
popdensity::Int64 = -1 #XXX this is counterintuitive
pairs::Bool = false
asexual::Bool = false
end
......@@ -61,13 +61,23 @@ Initialise the population of the given species, based on the parameters stored
in [`PopInitParams`](@ref). Define these using [`@populate`](@ref).
"""
function initpopulation!(speciesname::String, model::SimulationModel)
# get the PopInitParams and check for validity
species = speciestype(speciesname)
p = populationparameters(species)
(p.popsize <= 0 && p.popdensity <= 0) && #XXX not sure what this would do
@warn("initpopulation() called with popsize and popdensity both <= 0")
initpopulation!(species, p, model)
end
"""
initpopulation!(speciestype, popinitparams, model)
Initialise the population of the given species, based on the given initialisation parameters.
This is an internal function called by initpopulation!(), and was split off from it to allow
better testing.
"""
function initpopulation!(species::Type, p::PopInitParams, model::SimulationModel)
(p.popsize <= 0 && p.popdensity <= 0) && # can be legit if a habitat descriptor is provided
@warn("initpopulation!() called with popsize and popdensity both <= 0")
(p.popsize > 0 && p.popdensity > 0) && #XXX not sure what this would do
@warn("initpopulation() called with popsize and popdensity both > 0")
@warn("initpopulation!() called with popsize and popdensity both > 0")
# create as many individuals as necessary in the landscape
n = 0
lastn = 0
......@@ -103,12 +113,12 @@ function initpopulation!(speciesname::String, model::SimulationModel)
(p.popsize > 0 && n >= p.popsize) && break
end
if lastn == n # prevent an infinite loop - we don't have a Cray...
@warn "There are not enough suitable locations for $(species) in the landscape."
@warn "There are not enough suitable locations for $(speciesof(species)) in the landscape."
break
end
lastn = n
end
@info "Initialised $(n) $(speciesname)s."
@info "Initialised $(n) $(speciesof(species))s."
end
#XXX initpopulation with dispersal from an original source?
......@@ -222,6 +232,17 @@ function nearby_animals(pos::Tuple{Int64,Int64}, model::SimulationModel;
end
end
"""
countanimals(pos, model; radius=0, species="")
Return the number of animals in the given radius around this position, optionally filtering
by species.
"""
function countanimals(pos::Tuple{Int64,Int64}, model::SimulationModel;
radius::Int64=0, species="") #XXX add type for species
length(nearby_animals(pos, model, radius=radius, species=species))
end
"""
neighbours(animal, model, radius=0, conspecifics=true)
......
......@@ -32,34 +32,34 @@ At the moment, this implementation is still in development.
@species Skylark begin
#XXX use Unitful.jl
#XXX add type annotations
#TODO add type annotations
eggtime = 11 # 11 days from laying to hatching
eggpredationmortality = 0.03 # per-day egg mortality from predation
nestharvestmortality = 0.9 # egg/nestling mortality after a harvest event (XXX guess)
eggtime::Int64 = 11 # 11 days from laying to hatching
eggpredationmortality::Float64 = 0.03 # per-day egg mortality from predation
nestharvestmortality::Float64 = 0.9 # egg/nestling mortality after a harvest event (XXX guess)
nestlingtime = 7:11 # 7-11 days from hatching to leaving nest
nestlingpredationmortality = 0.03 # per-day nestling mortality from predation
nestlingpredationmortality::Float64 = 0.03 # per-day nestling mortality from predation
fledglingtime = 25:30 # 25-30 days from hatching to independence
fledglingharvestmortality = 0.5 # fledgling mortality after harvest
fledglingpredationmortality = 0.01 # per-day fledgling mortality from predation
firstyearmortality = 0.38 # total mortality in the first year after independence
fledglingharvestmortality::Float64 = 0.5 # fledgling mortality after harvest
fledglingpredationmortality::Float64 = 0.01 # per-day fledgling mortality from predation
firstyearmortality::Float64 = 0.38 # total mortality in the first year after independence
migrationdates = () # is defined by each individual in @create(Skylark)
migrationmortality = 0.33 # chance of dying during the winter
migrationmortality::Float64 = 0.33 # chance of dying during the winter
mate = -1 # the agent ID of the mate (-1 if none)
mate::Int64 = -1 # the agent ID of the mate (-1 if none)
nest = () # coordinates of current nest
nestingbegin = (April, 10) # begin nesting in the middle of April
nestbuildingtime = 4:5 # 4-5 days needed to build a nest (doubled for first nest)
nestcompletion = 0 # days left until the nest is built
nestcompletion::Int64 = 0 # days left until the nest is built
eggsperclutch = 2:5 # 2-5 eggs laid per clutch
clutch = [] # IDs of offspring in current clutch
breedingdelay = 18 # wait 18 days after hatching to start a new brood
nestingend = July # last month of nesting
breedingdelay::Int64 = 18 # wait 18 days after hatching to start a new brood
nestingend::Int64 = July # last month of nesting
habitats = skylarkhabitat
habitats::Function = skylarkhabitat
end
"""
......
......@@ -5,58 +5,43 @@
## Test species definition
#FIXME reactivate once Julia 1.11 is released and @species is fixed
#(https://discourse.julialang.org/t/kwdef-constructor-not-available-outside-of-module/114675/4)
# @species Mermaid begin
# ageofmaturity = 2
# pesticidemortality = 1.0
# end
# new species have to be defined within the Persefone module scope, otherwise things don't work
Persefone.eval(quote
#FIXME remove manual definition of Mermaid once Julia 1.11 is released (see above)
@kwdef mutable struct Mermaid <: Animal
const id::Int64
const sex::Persefone.Sex = Persefone.hermaphrodite
const parents::Tuple{Int64,Int64} = (-1, -1)
pos::Tuple{Int64,Int64}
phase::Function = (self,model)->nothing
age::Int = 0
energy::Union{Persefone.EnergyBudget,Nothing} = nothing
offspring::Vector{Int64} = Vector{Int64}()
@species Mermaid begin
ageofmaturity = 2
pesticidemortality = 1.0
end
Mermaid(id, sex, parents, pos, phase) =
Mermaid(id=id, sex=sex, parents=parents, pos=pos, phase=phase)
Mermaid(id) = Mermaid(id=id, parents=(-1, -1), pos=(-1, -1))
@create Mermaid begin
@debug "Created $(animalid(self))."
end
@phase Mermaid life begin
@debug "$(Persefone.animalid(self)) is swimming happily in its pond."
@respond Persefone.pesticide @kill(self.pesticidemortality, "poisoning")
@respond Persefone.harvesting @setphase(drought)
@debug "Animal: $self"
if self.sex == Persefone.female && length(@neighbours()) < 3 &&
self.age >= self.ageofmaturity && @landcover() == Persefone.water
@debug "$(animalid(self)) is swimming happily in its pond."
@respond pesticide @kill(self.pesticidemortality, "poisoning")
@respond harvesting @setphase(drought)
if self.sex == female && length(@neighbours()) < 3 &&
self.age >= self.ageofmaturity && @landcover() == water
@reproduce()
end
end
@phase Mermaid drought begin
n = sum(1 for a in @neighbours())
@debug "$(Persefone.animalid(self)) is experiencing drought with $n neighbour(s)."
@respond Persefone.sowing @setphase(life)
@debug "$(animalid(self)) is experiencing drought with $n neighbour(s)."
@respond sowing @setphase(life)
end
@populate Mermaid begin
birthphase = life
initphase = life
habitat = @habitat(@landcover() == Persefone.water)
habitat = @habitat(@landcover() == water)
pairs=true
end
end) # end eval
## Test sets
@testset "Habitat macros" begin
......@@ -67,15 +52,16 @@ end
FarmPlot(1, [(6,6)], model.crops["winter wheat"], Ps.janfirst,
0.0, 0.0, 0.0, 0.0, Vector{Ps.EventType}()))
push!(model.animals,
Mermaid(1, Ps.male, (-1,-1), (3,3), life),
Mermaid(2, Ps.female, (-1,-1), (4,4), life))
Ps.Mermaid(1, Ps.male, (-1,-1), (3,3), Ps.life),
Ps.Mermaid(2, Ps.female, (-1,-1), (4,4), Ps.life))
# create a set of habitat descriptors
h1 = @habitat(@landcover() == Ps.water)
h2 = @habitat(@cropname() == "winter wheat" &&
@cropheight() < 2)
h3 = @habitat(@distanceto(Ps.water) > 2 &&
@distancetoedge() <= 2)
h4 = @habitat(length(@nearby_animals(radius=1)) == 1) #FIXME defining radius doesn't work
#FIXME nested macros don't work properly, counting seems wrong
h4 = @habitat(@countanimals(radius=1) == 1)
# test the descriptors
@test h1((6,4), model) == true
@test h1((5,4), model) == false
......@@ -89,93 +75,97 @@ end
@test_broken h4((1,1), model) == false
end
#FIXME the way initialisation works has completely changed...
# @testset "Species initialisation" begin
# model = inittestmodel()
# spec = "test_animal"
# species::Dict{String,Any} = Dict("name"=>spec)
# # create a set of initialisation functions
# initfun1 = Ps.initrandompopulation(10)
# initfun2 = Ps.initrandompopulation(8*8*3, asexual=true)
# initfun3 = Ps.initpopulation(@habitat(@landcover() == Ps.grass), pairs=true)
# initfun4 = Ps.initpopulation(@habitat(@landcover() == Ps.water &&
# length(@neighbours(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 192 $(spec)s.") initfun2(species, model)
# @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 36 $(spec)s.") initfun3(species, model)
# @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, species=spec, radius=4) == 0
# @test Ps.countanimals((6,4), model, species=spec, radius=0) == 5
# end
#FIXME the way species work has completely changed...
# @testset "Species macros" begin
# # create a model landscape and a test species
# model = inittestmodel()
# # test a complete mermaid life cycle
# pond = (6,4)
# mermaid = Mermaid(1)
# @test typeof(mermaid) <: Animal
# @test typeof(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.harvesting)
# @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!(Ps.withtestlogger(model)))
# @test model[1].age == 1
# @test model[2].traits["phase"] == "drought"
# createevent!(model, [pond], Ps.sowing)
# @test_logs((:debug, "Mermaid 1 is experiencing drought with 1 neighbour(s)."),
# (:debug, "Mermaid 2 is experiencing drought with 1 neighbour(s)."),
# min_level=Logging.Debug, match_mode=:any,
# stepsimulation!(Ps.withtestlogger(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!(Ps.withtestlogger(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 from poisoning."),
# (:debug, "Mermaid 2 is swimming happily in its pond."),
# (:debug, "Mermaid 2 has died from poisoning."),
# (:debug, "Mermaid 3 is swimming happily in its pond."),
# (:debug, "Mermaid 3 has died from poisoning."),
# min_level=Logging.Debug, match_mode=:any,
# stepsimulation!(Ps.withtestlogger(model)))
# @test Ps.countanimals(pond, model) == 0
# # test @rand (this is done more easily outside of @species)
# @test typeof(@rand()) == Float64
# @test @rand([true, true])
@testset "Species initialisation" begin
model = inittestmodel()
# initialisation parameters, set 1
initparams1 = Ps.PopInitParams(birthphase = Ps.life, initphase = Ps.life, popsize = 10)
@test_logs((:info, "Initialised 10 Mermaids."),
Ps.initpopulation!(Ps.Mermaid, initparams1, model))
@test all(a -> a.sex in (Ps.male, Ps.female), model.animals)
Ps.killallanimals!(model)
# initialisation parameters, set 2
initparams2 = Ps.PopInitParams(birthphase = Ps.life, initphase = Ps.life,
popsize = 8*8*3, asexual=true)
@test_logs((:info, "Initialised 192 Mermaids."),
Ps.initpopulation!(Ps.Mermaid, initparams2, model))
@test Ps.countanimals((1,1), model) == Ps.countanimals((6,6), model) == 3
@test all(a -> a.sex == Ps.hermaphrodite, model.animals)
Ps.killallanimals!(model)
# initialisation parameters, set 3
initparams3 = Ps.PopInitParams(birthphase = Ps.life, initphase = Ps.life, pairs = true,
habitat = @habitat(@landcover() == Ps.grass))
@test_logs((:warn, "initpopulation!() called with popsize and popdensity both <= 0"),
(:info, "Initialised 36 Mermaids."),
Ps.initpopulation!(Ps.Mermaid, initparams3, model))
@test Ps.countanimals((2,2), model, radius=2) == Ps.countanimals((5,3), model, radius=1) == 0
@test Ps.countanimals((5,5), model) == Ps.countanimals((6,6), model) == 2
a1, a2 = Ps.nearby_animals((6,6), model, radius=0)
@test a1.sex != a2.sex
Ps.killallanimals!(model)
# initialisation parameters, set 4
initparams4 = Ps.PopInitParams(birthphase = Ps.life, initphase = Ps.life, popsize = 10,
habitat = @habitat(@landcover() == Ps.water &&
@countanimals() < 5))
@test_logs((:warn, "There are not enough suitable locations for Mermaid in the landscape."),
(:info, "Initialised 5 Mermaids."),
Ps.initpopulation!(Ps.Mermaid, initparams4, model))
@test Ps.countanimals((1,1), model, radius=4) == 0
@test Ps.countanimals((6,4), model) == 5
end
# #TODO test movement macros
# end
@testset "Species macros" begin
model = inittestmodel()
pond = (6,4)
# test individual initialisation
mermaid = Ps.Mermaid(1)
@test typeof(mermaid) <: Animal
@test typeof(Ps.life) <: Function
@test typeof(mermaid.phase) <: Function
@test_logs((:debug, "Created Mermaid 1."), min_level=Logging.Debug,
Ps.create!(mermaid, Ps.withtestlogger(model)))
# test population initialisation
@test_logs((:warn, "initpopulation!() called with popsize and popdensity both <= 0"),
(:info, "Initialised 2 Mermaids."),
Ps.initpopulation!("Mermaid", model))
@test Ps.nagents(model) == 2
@test Ps.countanimals((1,1), model, radius=4) == 0
@test Ps.countanimals(pond, model) == 2
@test model.animals[1].age == 0
# test event handling
createevent!(model, [pond], Ps.harvesting)
@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!(Ps.withtestlogger(model)))
@test model.animals[1].age == 1
@test model.animals[2].phase == Ps.drought
createevent!(model, [pond], Ps.sowing)
@test_logs((:debug, "Mermaid 1 is experiencing drought with 1 neighbour(s)."),
(:debug, "Mermaid 2 is experiencing drought with 1 neighbour(s)."),
min_level=Logging.Debug, match_mode=:any,
stepsimulation!(Ps.withtestlogger(model)))
@test_logs((:debug, "Mermaid 1 is swimming happily in its pond."),
(:debug, "Mermaid 2 is swimming happily in its pond."),
(:debug, "Mermaid 2 has reproduced."),
(:debug, "Created Mermaid 3."),
min_level=Logging.Debug, match_mode=:any,
stepsimulation!(Ps.withtestlogger(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 from poisoning."),
(:debug, "Mermaid 2 is swimming happily in its pond."),
(:debug, "Mermaid 2 has died from poisoning."),
(:debug, "Mermaid 3 is swimming happily in its pond."),
(:debug, "Mermaid 3 has died from poisoning."),
min_level=Logging.Debug, match_mode=:any,
stepsimulation!(Ps.withtestlogger(model)))
@test Ps.countanimals(pond, model) == 0
# test @rand (this is done more easily outside of @species)
@test typeof(@rand()) == Float64
@test @rand([true, true])
#TODO test movement macros
end
@testset "Insect submodel" begin
# create a set of pixels and dates for testing
......@@ -231,7 +221,7 @@ end
end
#TODO test Wolpertinger/Wyvern?
#XXX test Wolpertinger/Wyvern?
@testset "Skylark submodel" begin
# set up a modified test landscape
......
......@@ -12,7 +12,7 @@
@test typeof(model.logger) == TeeLogger{Tuple{ConsoleLogger, ConsoleLogger}}
@test length(model.dataoutputs) == 2
@test model.events == Vector{FarmEvent}()
@test Ps.nagents(model) == 2092+10+28 #FIXME 2412 == 2130
@test Ps.nagents(model) == 2092+0+321 # farmplots+farmers+animals
end
@testset "Parameter scanning" begin
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment