diff --git a/Makefile b/Makefile index 7d1cfcc182c88405249766ce1ce8ab01a3f1fc4d..6ef66c43114fc977d5235100e836e76a6907ca46 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 4e1c38e054ae48c43299d8d3056e14fadb5911a6..fd4dbe42036758ad45bdbb68b3c3dc481cf7c01e 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 328b5bc3db2122365abfcd6f066e382433771a0d..cd18822843a68bbaf3e4c8511e2c6cb2362a5282 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 e0df561b5e2eea5e1eeb26b72c6791c167412c73..7b0f414b595de75b683df5d7f6a5054f3eba81f8 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 6fdb7be5e2d4020f8d76c84c879231ec54fc8e7b..26d3cd0319e9e9fe32e90a49288459987d51ea78 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 038c0a0f0c047b1404642939a5ccfaf6349499a8..0846dca8c2c359e8d7c198804675761bc975eff0 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 fe701fb3db2e8169edba7638ab1019e7be73c539..3a068c7f39236929752b7795e0cc08c36e68b9fc 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 b552dfa38f33920923443b89bb57b45db7b85169..f23cba579f39e698116079b422c607a6e49b3aec 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