From f61ba8948d6a16987f6c477f62551b9143ab1732 Mon Sep 17 00:00:00 2001 From: Daniel Vedder <daniel.vedder@idiv.de> Date: Wed, 4 Jan 2023 13:13:32 +0100 Subject: [PATCH] Wrote @habitat and associated macros Not yet functional - the necessary functions still need to be implemented. --- src/core/landscape.jl | 33 +++++++- src/crop/crops.jl | 31 +++++++- src/nature/lifehistory.jl | 17 ++++- src/nature/nature.jl | 138 +++++++++++++++++++++++++++++----- src/nature/species/skylark.jl | 7 +- test/nature_tests.jl | 4 + 6 files changed, 202 insertions(+), 28 deletions(-) diff --git a/src/core/landscape.jl b/src/core/landscape.jl index 665e94f..eba5b73 100644 --- a/src/core/landscape.jl +++ b/src/core/landscape.jl @@ -101,16 +101,41 @@ end Return the land cover class at this position (utility wrapper). """ -function landcover(model::AgentBasedModel, pos::Tuple{Int64,Int64}) +function landcover(pos::Tuple{Int64,Int64}, model::AgentBasedModel) model.landscape[pos...].landcover end """ farmplot(model, position) -Return the farm plot at this position (utility wrapper). +Return the farm plot at this position, or nothing if there is none (utility wrapper). """ -function farmplot(model::AgentBasedModel, pos::Tuple{Int64,Int64}) - model[model.landscape[pos...].fieldid] +function farmplot(pos::Tuple{Int64,Int64}, model::AgentBasedModel) + ismissing(model.landscape[pos...].fieldid) ? nothing : + model[model.landscape[pos...].fieldid] +end + +""" + distanceto(model, pos, habitattype) + +Calculate the distance from the given location to the closest habitat of the specified type. +Caution: can be computationally expensive! +""" +function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitattype::LandCover) + #XXX can I make this check for both land cover type and crop type? + # (Or even take a full habitat descriptor?) + #TODO +end + +""" + distancetoedge(model, pos) + +Calculate the distance from the given location to the closest neighbouring habitat. +Caution: can be computationally expensive! +""" +function distancetoedge(pos::Tuple{Int64,Int64}, model::AgentBasedModel) + lc = landcover(model, pos) + dist = 1 + #TODO end diff --git a/src/crop/crops.jl b/src/crop/crops.jl index ec4520f..c96c7c0 100644 --- a/src/crop/crops.jl +++ b/src/crop/crops.jl @@ -3,6 +3,9 @@ ### This file is responsible for managing the crop growth modules. ### +"The crop types simulated by the model" +@enum CropType fallow wheat barley maize rapeseed + #XXX not sure whether it makes sense to have this as an agent type, # or perhaps better a grid property? """ @@ -11,9 +14,9 @@ This represents one field, i.e. a collection of pixels with the same management. This is the spatial unit with which the crop growth model and the farm model work. """ -@agent FarmPlot NoSpaceAgent begin +@agent FarmPlot GridAgent{2} begin pixels::Vector{Tuple{Int64, Int64}} - croptype::String + croptype::CropType height::Float64 biomass::Float64 #TODO @@ -49,7 +52,7 @@ function initfields!(model::AgentBasedModel) model.landscape[x,y].fieldid = objectid push!(model[objectid].pixels, (x,y)) else - fp = add_agent!(FarmPlot, model, [(x,y)], "fallow", 0.0, 0.0) + fp = add_agent!(FarmPlot, model, [(x,y)], fallow, 0.0, 0.0) model.landscape[x,y].fieldid = fp.id convertid[rawid] = fp.id n += 1 @@ -86,5 +89,25 @@ function averagefieldsize(model::AgentBasedModel) push!(sizes, size(a.pixels)[1]/conversionfactor) end round(sum(sizes)/size(sizes)[1], digits=2) - #sizes +end + +""" + croptype(model, position) + +Return the crop at this position, or nothing if there is no crop here (utility wrapper). +""" +function croptype(pos::Tuple{Int64,Int64}, model::AgentBasedModel) + ismissing(model.landscape[pos...].fieldid) ? nothing : + model[model.landscape[pos...].fieldid].croptype +end + +""" + cropheight(model, position) + +Return the height of the crop at this position, or nothing if there is no crop here +(utility wrapper). +""" +function cropheight(pos::Tuple{Int64,Int64}, model::AgentBasedModel) + ismissing(model.landscape[pos...].fieldid) ? nothing : + model[model.landscape[pos...].fieldid].height end diff --git a/src/nature/lifehistory.jl b/src/nature/lifehistory.jl index c48618a..fd2f5c3 100644 --- a/src/nature/lifehistory.jl +++ b/src/nature/lifehistory.jl @@ -25,7 +25,9 @@ function initrandompopulation(popsize::Union{Int64,Float64}, asexual::Bool=true) return initfunc end -#TODO initpopulation +#TODO initpopulation with habitat descriptor +#TODO initpopulation with dispersal from an original source +#TODO initpopulation based on known occurences in real-life """ reproduce!(animal, model, n=1) @@ -57,3 +59,16 @@ function kill!(animal::Animal, model::AgentBasedModel, probability::Float64=1.0, end return false end + +""" + countanimals(model, pos, speciesname) + +Count the number of animals of the given species in this location. +""" +macro countanimals(model::AgentBasedModel, pos::Tuple{Int64,Int64}, speciesname::String) + n = 0 + for a in nearby_ids(pos, model, 0) + typeof(model[a]) == Animal && model[a].traits.name == speciesname && (n += 1) + end + return n +end diff --git a/src/nature/nature.jl b/src/nature/nature.jl index a13faef..51e21bf 100644 --- a/src/nature/nature.jl +++ b/src/nature/nature.jl @@ -3,6 +3,9 @@ ### This file is responsible for managing the animal modules. ### + +### FUNCTIONS AND TYPES INTEGRATING THE NATURE MODEL WITH THE REST OF PERSEPHONE + ## An enum used to assign a sex to each animal @enum Sex hermaphrodite male female @@ -30,6 +33,31 @@ function animalid(a::Animal) return "$(a.traits["name"]) $(a.id)" end +""" + stepagent!(animal, model) + +Update an animal by one day, executing it's currently active phase function. +""" +function stepagent!(animal::Animal, model::AgentBasedModel) + animal.age += 1 + animal.traits[animal.traits["phase"]](animal,model) +end + +""" + initnature!(model) + +Initialise the model with all simulated animal populations. +""" +function initnature!(model::AgentBasedModel) + # The config file determines which species are simulated in this run + for speciesname in param("nature.targetspecies") + species = @eval $(Symbol(speciesname))($model) + species["initialise!"](species, model) + end + # Initialise the data output + initecologicaldata() +end + ### MACROS IMPLEMENTING THE DOMAIN-SPECIFIC LANGUAGE FOR DEFINING SPECIES @@ -104,7 +132,7 @@ variables: information). Several utility macros can be used within the body of `@phase` as a short-hand for -common expressions: `@respond`, `@trait`, `@here`, `@kill`, `@reproduce` +common expressions: `@respond`, `@trait`, `@here`, `@kill`, `@reproduce`. To transition an individual to another phase, simply redefine its phase variable: `@trait(phase) = "newphase"`. @@ -122,7 +150,6 @@ This can only be used nested within `@phase`. """ macro respond(eventname, body) quote - #TODO test this if $(esc(eventname)) in @here(events) $body end @@ -175,30 +202,105 @@ end #XXX add a macro @f to call a function with animal and model as first parameters? # e.g. @f(nearby_agents, distance) +# -> rather create wrapper macros + +""" + @habitat + +Specify habitat suitability for spatial ecological processes. -### FUNCTIONS INTEGRATING THE NATURE MODEL WITH THE REST OF PERSEPHONE +This macro works by creating an anonymous function that takes in a model object +and a position, and returns `true` or `false` depending on the conditions +specified in the macro body. +Several utility macros can be used within the body of `@habitat` as a short-hand for +common expressions: `@landcover`, `@croptype`, `@cropheight`, `@distanceto`, +`@distancetoedge`, `@countanimals`. The variables `model` and `pos` can be used +for checks that don't have a macro available. + +Two example uses of `@habitat` might look like this: + +```julia +movementhabitat = @habitat(@landcover() in (grass agriculture soil)) + +nestinghabitat = @habitat((@landcover() == grass || + (@landcover() == agriculture && @croptype() != maize && + @cropheight() < 10)) && + @distanceto(forest) > 20) +``` + +For more complex habitat suitability checks, the use of this macro can be +circumvented by directly creating an equivalent function. """ - stepagent!(animal, model) +macro habitat(body) + quote + function($(esc(:pos)), $(esc(:model))) + if $(esc(body)) + return true + else + return false + end + end + end +end -Update an animal by one day, executing it's currently active phase function. """ -function stepagent!(animal::Animal, model::AgentBasedModel) - animal.age += 1 - animal.traits[animal.traits["phase"]](animal,model) + @landcover + +Returns the local landcover. This is a utility wrapper that can only be used +nested within `@habitat`. +""" +macro landcover() + :(landcover($(esc(:pos)), $(esc(:model)))) end """ - initnature!(model) + @croptype -Initialise the model with all simulated animal populations. +Return the local croptype, or nothing if there is no crop here. +This is a utility wrapper that can only be used nested within `@habitat`. """ -function initnature!(model::AgentBasedModel) - # The config file determines which species are simulated in this run - for speciesname in param("nature.targetspecies") - species = @eval $(Symbol(speciesname))($model) - species["initialise!"](species, model) - end - # Initialise the data output - initecologicaldata() +macro croptype() + :(croptype($(esc(:pos)), $(esc(:model)))) +end + +""" + @cropheight + +Return the height of the crop at this position, or 0 if there is no crop here. +This is a utility wrapper that can only be used nested within `@habitat`. +""" +macro cropheight() + :(cropheight($(esc(:pos)), $(esc(:model)))) +end + +""" + @distanceto(habitattype) + +Calculate the distance to the closest habitat of the specified type. +This is a utility wrapper that can only be used nested within `@habitat`. +""" +macro distanceto(habitattype) + :(distanceto($(esc(:pos)), $(esc(:model)), $habitattype)) +end + +""" + @distancetoedge + +Calculate the distance to the closest neighbouring habitat. +This is a utility wrapper that can only be used nested within `@habitat`. +""" +macro distancetoedge() + :(distancetoedge($(esc(:pos)), $(esc(:model)))) +end + +""" + @countanimals(speciesname) + +Count the number of animals of the given species in this location. +This is a utility wrapper that can only be used nested within `@habitat`. +""" +macro countanimals(speciesname) + #XXX this also counts the enquiring agent + :(countanimals($(esc(:pos)), $(esc(:model)), $speciesname)) end diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl index 4843816..9542cd4 100644 --- a/src/nature/species/skylark.jl +++ b/src/nature/species/skylark.jl @@ -4,7 +4,7 @@ ### ##XXX At the moment, this is just a skeleton to show what I want to be able to interpret -## with the @species and @phase macros +## with the @species, @phase, and @habitat macros (and their helper macros) @species Skylark begin @@ -16,6 +16,11 @@ initialise! = initrandompopulation(popsize) phase = "egg" + + habitats = @habitat((@landcover() == grass || + (@landcover() == agriculture && @croptype() != maize && + @cropheight() < 10)) && + @distanceto(forest) > 20) @phase egg begin @kill(@trait(eggpredationmortality), "predation") diff --git a/test/nature_tests.jl b/test/nature_tests.jl index b5cb2cf..fb15b97 100644 --- a/test/nature_tests.jl +++ b/test/nature_tests.jl @@ -3,4 +3,8 @@ ### These are the tests for the nature model (excluding individual species). ### +@testset "Species macros" begin + #TODO +end + #TODO -- GitLab