diff --git a/src/core/landscape.jl b/src/core/landscape.jl index 665e94fe981f7610b4270ddf891c5e07dcfcc1a0..eba5b73b9b86daff20e109cf9cb3522e9d58ff94 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 ec4520fb6d71aff653bafd6e6870ad91d72153a2..c96c7c075cc68232410f7da0d5459efe4896a7df 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 c48618a11b1a3a206d0c779eece24611fb0db2fa..fd2f5c3a790f0ecb7e79bf7d7939974319761680 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 a13faefafcf0d20213f1b61cbfdc654b6aca6b74..51e21bfa71878ef209abd0e92546e2ea63148b3e 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 4843816234b5afa6f9720df61a2a98d6303b7bea..9542cd420c17743d26e762eb5a0c0e1cccf25efb 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 b5cb2cf609c95f621b1b095504717c0cff76f278..fb15b97effa5e6ea9f6edba4abeff9cb66d4336b 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