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