From 2673d3bae780dfd444f58ba58878080065208805 Mon Sep 17 00:00:00 2001
From: Daniel Vedder <daniel.vedder@idiv.de>
Date: Fri, 31 May 2024 16:36:35 +0200
Subject: [PATCH] Started upgrading animal tests
---
Makefile | 2 +-
src/Persefone.jl | 8 +--
src/crop/farmplot.jl | 6 +--
src/nature/macros.jl | 20 ++++++--
src/nature/populations.jl | 11 +++--
src/nature/species/skylark.jl | 7 +--
src/parameters.toml | 3 +-
test/nature_tests.jl | 92 ++++++++++++++++++++++-------------
8 files changed, 91 insertions(+), 58 deletions(-)
diff --git a/Makefile b/Makefile
index 7d1cfcc..6ef66c4 100644
--- a/Makefile
+++ b/Makefile
@@ -4,7 +4,7 @@ SHELL = /bin/bash
run:
# run an example simulation
if [ -d "example_results" ]; then rm -r example_results; fi
- ./run.jl -o example_results
+ julia run.jl -o example_results
#src/analysis/analyse_nature.R example_results
test:
diff --git a/src/Persefone.jl b/src/Persefone.jl
index 4e1c38e..fd4dbe4 100644
--- a/src/Persefone.jl
+++ b/src/Persefone.jl
@@ -79,6 +79,7 @@ export
@distancetoedge,
@randompixel,
@randomdirection,
+ @nearby_animals,
@neighbours,
@move,
@walk,
@@ -132,18 +133,13 @@ include("nature/nature.jl")
include("nature/macros.jl")
include("nature/populations.jl")
include("nature/ecologicaldata.jl")
-#TODO loop over files in nature/species directory
-# (the below doesn't work yet)
-# for f in readdir("nature/species", join=true)
-# endswith(f, ".jl") && include(f)
-# end
include("nature/species/skylark.jl")
include("nature/species/wolpertinger.jl")
include("nature/species/wyvern.jl")
include("core/simulation.jl") #this must be last
-# precompile important functions - XXX use PrecompileTools.jl
+# precompile important functions - TODO use PrecompileTools.jl
precompile(initialise, (String,Int))
precompile(stepsimulation!, (SimulationModel,))
diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl
index 328b5bc..cd18822 100644
--- a/src/crop/farmplot.jl
+++ b/src/crop/farmplot.jl
@@ -180,12 +180,12 @@ end
"""
cropname(model, position)
-Return the name of the crop at this position, or nothing if there is no crop here
+Return the name of the crop at this position, or an empty string if there is no crop here
(utility wrapper).
"""
function cropname(pos::Tuple{Int64,Int64}, model::SimulationModel)
- ismissing(model.landscape[pos...].fieldid) ? nothing :
- model.farmplots[model.landscape[pos...].fieldid].croptype.name
+ field = model.landscape[pos...].fieldid
+ ismissing(field) ? "" : model.farmplots[field].croptype.name
end
"""
diff --git a/src/nature/macros.jl b/src/nature/macros.jl
index e0df561..7b0f414 100644
--- a/src/nature/macros.jl
+++ b/src/nature/macros.jl
@@ -47,7 +47,9 @@ the `model` variable (an object of type `SimulationModel`).
"""
macro species(name, body)
quote
- @kwdef mutable struct $(name) <: Animal
+ @kwdef mutable struct $name <: Animal
+ #FIXME 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
const parents::Tuple{Int64,Int64} = (-1, -1) #XXX assumes sexual reprod.
@@ -63,9 +65,6 @@ macro species(name, body)
$(esc(name))(id=id, sex=sex, parents=parents, pos=pos, phase=phase)
# 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))
end
end
@@ -309,7 +308,7 @@ end
"""
@cropname
-Return the name of the local croptype, or nothing if there is no crop here.
+Return the name of the local croptype, or an empty string if there is no crop here.
This is a utility wrapper that can only be used nested within [`@phase`](@ref)
or [`@habitat`](@ref).
"""
@@ -383,6 +382,17 @@ macro randomdirection(args...)
:(randomdirection($(esc(:model)), $(map(esc, args)...)))
end
+"""
+ @nearby_animals(radius=0, species="")
+
+Return an iterator over all animals in the given radius around the current position.
+This can only be used nested within [`@phase`](@ref) or [`@habitat`](@ref).
+"""
+macro nearby_animals(args...)
+ #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
+end
+
"""
@neighbours(radius=0, conspecifics=true)
diff --git a/src/nature/populations.jl b/src/nature/populations.jl
index 6fdb7be..26d3cd0 100644
--- a/src/nature/populations.jl
+++ b/src/nature/populations.jl
@@ -76,7 +76,10 @@ function initpopulation!(speciesname::String, model::SimulationModel)
for x in @shuffle!(Vector(1:width))
for y in @shuffle!(Vector(1:height))
if p.habitat((x,y), model) &&
- (p.popdensity <= 0 || @chance(1/p.popdensity)) #XXX what if pd==0?
+ (p.popdensity <= 0 || n == 0 || @chance(1/p.popdensity)) #XXX what if pd==0?
+ #XXX `n==0` above guarantees that at least one individual is created, even
+ # in a landscape that is otherwise too small for the specified popdensity -
+ # do we want this?
if p.pairs
a1 = species(length(model.animals)+1, male, (-1, -1), (x,y), p.initphase)
a2 = species(length(model.animals)+2, female, (-1, -1), (x,y), p.initphase)
@@ -105,7 +108,7 @@ function initpopulation!(speciesname::String, model::SimulationModel)
end
lastn = n
end
- @info "Initialised $(n) $(species)s."
+ @info "Initialised $(n) $(speciesname)s."
end
#XXX initpopulation with dispersal from an original source?
@@ -209,7 +212,7 @@ end
Return a list of animals in the given radius around this position, optionally filtering by species.
"""
function nearby_animals(pos::Tuple{Int64,Int64}, model::SimulationModel;
- radius::Int64=0, species="")
+ radius::Int64=0, species="") #XXX add type for species
neighbours = nearby_ids(pos, model, radius)
isempty(neighbours) && return neighbours
if isempty(species)
@@ -225,7 +228,7 @@ end
Return a list of animals in the given radius around this animal, excluding itself. By default,
only return conspecific animals.
"""
-function neighbours(animal::Animal, model::SimulationModel, radius::Int64=0, conspecifics=true)
+function neighbours(animal::Animal, model::SimulationModel, radius::Int64=0, conspecifics::Bool=true)
filter(a -> a.id != animal.id,
nearby_animals(animal.pos, model, radius = radius,
species = conspecifics ? speciesof(animal) : ""))
diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl
index 038c0a0..0846dca 100644
--- a/src/nature/species/skylark.jl
+++ b/src/nature/species/skylark.jl
@@ -6,7 +6,7 @@
skylarkhabitat = @habitat((@landcover() == grass ||
# settle on grass or arable land (but not maize)
(@landcover() == agriculture && @cropname() != "maize")) &&
- @distancetoedge() > 5) # at least 50m from other habitats
+ @distancetoedge() >= 5) # at least 50m from other habitats
#XXX this ought to check for distance to forest and builtup,
# but that's very expensive (see below)
# @distanceto(forest) > 5 && # at least 50m from forest edges
@@ -32,6 +32,7 @@ At the moment, this implementation is still in development.
@species Skylark begin
#XXX use Unitful.jl
+ #XXX add type annotations
eggtime = 11 # 11 days from laying to hatching
eggpredationmortality = 0.03 # per-day egg mortality from predation
@@ -256,6 +257,6 @@ end
habitat = skylarkhabitat
initphase = mating
birthphase = egg
- popdensity=300 #XXX use Unitful.jl
- pairs=true
+ popdensity = 300 #XXX use Unitful.jl
+ pairs = true
end
diff --git a/src/parameters.toml b/src/parameters.toml
index fe701fb..3a068c7 100644
--- a/src/parameters.toml
+++ b/src/parameters.toml
@@ -30,7 +30,8 @@ weatherfile = "data/regions/jena-small/weather.csv" # location of the weather da
farmmodel = "FieldManager" # which version of the farm model to use (not yet implemented)
[nature]
-targetspecies = ["Skylark"] #["Wolpertinger", "Wyvern"] # list of target species to simulate
+#targetspecies = ["Wolpertinger", "Wyvern"] # list of target species to simulate - example species
+targetspecies = ["Skylark"] # list of target species to simulate
popoutfreq = "daily" # output frequency population-level data, daily/monthly/yearly/end/never
indoutfreq = "end" # output frequency individual-level data, daily/monthly/yearly/end/never
insectmodel = ["season", "habitat", "pesticides", "weather"] # factors affecting insect growth
diff --git a/test/nature_tests.jl b/test/nature_tests.jl
index b552dfa..f23cba5 100644
--- a/test/nature_tests.jl
+++ b/test/nature_tests.jl
@@ -5,10 +5,29 @@
## Test species definition
-@species Mermaid begin
+#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
+
+#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}()
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))."
@@ -40,7 +59,6 @@ end
## Test sets
-#FIXME
@testset "Habitat macros" begin
# set up the testing landscape
model = inittestmodel()
@@ -49,7 +67,7 @@ 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), #FIXME unsupported keyword arguments?
+ Mermaid(1, Ps.male, (-1,-1), (3,3), life),
Mermaid(2, Ps.female, (-1,-1), (4,4), life))
# create a set of habitat descriptors
h1 = @habitat(@landcover() == Ps.water)
@@ -57,7 +75,7 @@ end
@cropheight() < 2)
h3 = @habitat(@distanceto(Ps.water) > 2 &&
@distancetoedge() <= 2)
- h4 = @habitat(length(@neighbours(1)) == 1)
+ h4 = @habitat(length(@nearby_animals(radius=1)) == 1) #FIXME defining radius doesn't work
# test the descriptors
@test h1((6,4), model) == true
@test h1((5,4), model) == false
@@ -66,9 +84,9 @@ end
@test h3((3,3), model) == true
@test h3((5,4), model) == false
@test h3((6,1), model) == false
- @test h4((2,2), model) == true
- @test h4((3,3), model) == false
- @test h4((1,1), model) == false
+ @test_broken h4((2,2), model) == true
+ @test_broken h4((3,3), model) == false
+ @test_broken h4((1,1), model) == false
end
#FIXME the way initialisation works has completely changed...
@@ -213,31 +231,35 @@ end
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
+#TODO test Wolpertinger/Wyvern?
+
+@testset "Skylark submodel" begin
+ # set up a modified test landscape
+ model = inittestmodel()
+ for x in 1:6
+ for y in 5:6
+ model.landscape[x,y] = Pixel(Ps.agriculture, missing, [], [])
+ end
+ 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