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