From bf5024876b440bbd1eecccc4179c10e23cec2bd4 Mon Sep 17 00:00:00 2001 From: Daniel Vedder <daniel.vedder@idiv.de> Date: Fri, 24 May 2024 10:33:45 +0200 Subject: [PATCH] Started upgrading nature tests --- src/core/simulation.jl | 2 +- src/nature/macros.jl | 7 +- test/nature_tests.jl | 306 ++++++++++++++++++++++------------------- 3 files changed, 168 insertions(+), 147 deletions(-) diff --git a/src/core/simulation.jl b/src/core/simulation.jl index b4a0228..dd454c7 100644 --- a/src/core/simulation.jl +++ b/src/core/simulation.jl @@ -35,7 +35,7 @@ end Return the total number of agents in a model object. """ function nagents(model::AgricultureModel) - length(model.animals)+length(model.farmers)+length(model.farmplots) + length(model.animals)+length(model.migrants)+length(model.farmers)+length(model.farmplots) end """ diff --git a/src/nature/macros.jl b/src/nature/macros.jl index fb2f107..e0df561 100644 --- a/src/nature/macros.jl +++ b/src/nature/macros.jl @@ -47,13 +47,12 @@ the `model` variable (an object of type `SimulationModel`). """ macro species(name, body) quote - #XXX species are created/referenced as Persefone.<speciesname>, is this relevant? @kwdef mutable struct $(name) <: Animal const id::Int64 const sex::Sex = hermaphrodite const parents::Tuple{Int64,Int64} = (-1, -1) #XXX assumes sexual reprod. pos::Tuple{Int64,Int64} - phase::Function = ()->0 + phase::Function = (self,model)->nothing age::Int = 0 energy::Union{EnergyBudget,Nothing} = nothing # DEB is optional offspring::Vector{Int64} = Vector{Int64}() @@ -62,8 +61,8 @@ macro species(name, body) # define a constructor giving the minimum necessary arguments as positional arguments $(esc(name))(id, sex, parents, pos, phase) = $(esc(name))(id=id, sex=sex, parents=parents, pos=pos, phase=phase) - # define a zero-argument constructor to access default field values #XXX probably not needed - #$(esc(name))(id) = $(esc(name))(id=id, parents=(-1, -1), pos=(-1, -1)) + # define a single-argument constructor for utility purposes (especially testing) + $(esc(name))(id) = $(esc(name))(id=id, parents=(-1, -1), pos=(-1, -1)) # allow species to be defined outside of the Persefone module, but still # available inside it (needed by `initnature!()` and `reproduce!()`) (@__MODULE__() != $(esc(:Persefone))) && ($(esc(:Persefone)).$name = $(name)) diff --git a/test/nature_tests.jl b/test/nature_tests.jl index ef945ff..b552dfa 100644 --- a/test/nature_tests.jl +++ b/test/nature_tests.jl @@ -3,23 +3,61 @@ ### These are the tests for the nature model (excluding individual species). ### +## Test species definition + +@species Mermaid begin + ageofmaturity = 2 + pesticidemortality = 1.0 +end + +@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 + @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) +end + +@populate Mermaid begin + birthphase = life + initphase = life + habitat = @habitat(@landcover() == Persefone.water) + pairs=true +end + +## Test sets + +#FIXME @testset "Habitat macros" begin # set up the testing landscape model = inittestmodel() - model.landscape[6,6] = Pixel(Ps.agriculture, 1, []) - species::Dict{String,Any} = Dict("name"=>"test_animal") - add_agent!((6,6), FarmPlot, model, - [(6,6)], model.crops["winter wheat"], Ps.janfirst, - 0.0, 0.0, 0.0, 0.0, Vector{Ps.EventType}()) - add_agent!((3,3), Animal, model, species, Ps.male, 1) - add_agent!((4,4), Animal, model, species, Ps.female, 1) + model.landscape[6,6] = Pixel(Ps.agriculture, 1, [], []) + push!(model.farmplots, + 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), #FIXME unsupported keyword arguments? + Mermaid(2, Ps.female, (-1,-1), (4,4), 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(@countanimals(species="test_animal", radius=1) == 1) + h4 = @habitat(length(@neighbours(1)) == 1) # test the descriptors @test h1((6,4), model) == true @test h1((5,4), model) == false @@ -33,111 +71,93 @@ @test h4((1,1), model) == false end -@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 && - @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 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 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 -@testset "Species macros" begin - # create a model landscape and a test species - model = inittestmodel() - - @species Mermaid begin - ageofmaturity = 2 - pesticidemortality = 1.0 - @initialise(@habitat(@landcover() == Persefone.water), pairs=true) - @phase life begin - @debug "$(Persefone.animalid(animal)) is swimming happily in its pond." - @respond Persefone.pesticide @kill(@trait(pesticidemortality), "poisoning") - @respond Persefone.harvesting @setphase(drought) - @debug "Animal: $animal" - if @trait(sex) == Persefone.female && @countanimals() < 3 && - @trait(age) >= @trait(ageofmaturity) && @landcover() == Persefone.water - @reproduce(-1) - end - end - @phase drought begin - n = sum(1 for a in @neighbours(0)) - @debug "$(Persefone.animalid(animal)) is experiencing drought with $n neighbour(s)." - @respond Persefone.sowing @setphase(life) - end - 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 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.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]) - # test @rand (this is done more easily outside of @species) - @test typeof(@rand()) == Float64 - @test @rand([true, true]) -end +# #TODO test movement macros +# end @testset "Insect submodel" begin # create a set of pixels and dates for testing @@ -145,12 +165,12 @@ end date1 = Date("2023-05-08") # day 128 (season begin) date2 = Date("2023-07-06") # day 187 (insect max) date3 = Date("2023-09-27") # day 270 (season end) - p1 = Pixel(Ps.agriculture, 1, []) - p2 = Pixel(Ps.agriculture, 1, [Ps.pesticide]) - p3 = Pixel(Ps.grass, 1, []) - p4 = Pixel(Ps.soil, 1, [Ps.fertiliser, Ps.pesticide]) - p5 = Pixel(Ps.forest, 1, []) - p6 = Pixel(Ps.water, 1, []) + p1 = Pixel(Ps.agriculture, 1, [], []) + p2 = Pixel(Ps.agriculture, 1, [Ps.pesticide], []) + p3 = Pixel(Ps.grass, 1, [], []) + p4 = Pixel(Ps.soil, 1, [Ps.fertiliser, Ps.pesticide], []) + p5 = Pixel(Ps.forest, 1, [], []) + p6 = Pixel(Ps.water, 1, [], []) # check whether the model calculates the same numbers I did by hand model.date = date1 @test Ps.insectbiomass(p1, model) ≈ 0.00411 atol=0.0001 @@ -193,29 +213,31 @@ end end -@testset "Skylark submodel" begin - # set up a modified test landscape - model = inittestmodel() - model.landscape[3,7] = Pixel(Ps.forest, missing, []) - species = Ps.Skylark(model) - # test migration - @test_logs((:info, "Initialised 2 Skylarks."), - (:debug, "Skylark 1 has migrated."), - (:debug, "Skylark 2 has migrated."), - min_level=Logging.Debug, match_mode=:any, - species["initialise!"](species, Ps.withtestlogger(model))) - @test nagents(model) == 0 - @test length(model.migrants) == 2 - @test model.migrants[1].first.sex != model.migrants[2].first.sex - for a in model.migrants - leave, arrive = a.first.migrationdates - @test leave[1] in (9, 10) || (leave[1] == 11 && leave[2] <= 15) - @test (arrive[1] == 2 && arrive[2] >= 15) || (arrive[1] == 3 && arrive[2] <= 15) - end - model.date = Date(year(model.date), 3, 17) - @test_logs((:debug, "Skylark 1 has returned."), - (:debug, "Skylark 2 has returned."), - min_level=Logging.Debug, match_mode=:any, - Ps.updatenature!(Ps.withtestlogger(model))) - #TODO -end +# @testset "Skylark submodel" begin +# # set up a modified test landscape +# model = inittestmodel() +# for x in 1:4 +# model.landscape[x,4] = Pixel(Ps.agriculture, missing, [], []) +# end +# # test migration +# @test_logs((:info, "Initialised 2 Skylarks."), +# (:debug, "Skylark 1 has migrated."), +# (:debug, "Skylark 2 has migrated."), +# min_level=Logging.Debug, match_mode=:any, +# Ps.initpopulation!("Skylark", Ps.withtestlogger(model))) +# @test length(model.animals) == 2 +# @test all(isnothing, model.animals) +# @test length(model.migrants) == 2 +# @test model.migrants[1].first.sex != model.migrants[2].first.sex +# for a in model.migrants +# leave, arrive = a.first.migrationdates +# @test leave[1] in (9, 10) || (leave[1] == 11 && leave[2] <= 15) +# @test (arrive[1] == 2 && arrive[2] >= 15) || (arrive[1] == 3 && arrive[2] <= 15) +# end +# model.date = Date(year(model.date), 3, 17) +# @test_logs((:debug, "Skylark 1 has returned."), +# (:debug, "Skylark 2 has returned."), +# min_level=Logging.Debug, match_mode=:any, +# Ps.updatenature!(Ps.withtestlogger(model))) +# #TODO +# end -- GitLab