diff --git a/src/Persefone.jl b/src/Persefone.jl index 206ab2ac860ad2ce03eeaa6b4eaa2b0777b57973..e02955ff3b27961baa22986d16a3c25f11287eac 100644 --- a/src/Persefone.jl +++ b/src/Persefone.jl @@ -102,6 +102,7 @@ include("farm/farm.jl") include("nature/insects.jl") include("nature/energy.jl") include("nature/nature.jl") +include("nature/macros.jl") include("nature/populations.jl") include("nature/ecologicaldata.jl") #TODO loop over files in nature/species directory diff --git a/src/core/input.jl b/src/core/input.jl index e57b98168148502a7dfd1072dd6914dc3d3097bc..6b8987a6425fe37a904a424a138be4ba1d9cf600 100644 --- a/src/core/input.jl +++ b/src/core/input.jl @@ -137,6 +137,17 @@ macro shuffle!(collection) :($(esc(:shuffle!))($(esc(:model)).rng, $(esc(collection)))) end +""" + @chance(odds) + +Return true if a random number is less than the odds (0.0 <= `odds` <= 1.0), +using the model RNG. This is a utility wrapper that can only be used a context +where the `model` object is available. +""" +macro chance(odds) + :($(esc(:rand))($(esc(:model)).rng) < $(esc(odds))) +end + """ parsecommandline() diff --git a/src/core/simulation.jl b/src/core/simulation.jl index dd33eff0224235ce5febf1cd50f48306c7ca1b24..10d6d54da2271f1fe4f0dc18b165888a3f4b78e9 100644 --- a/src/core/simulation.jl +++ b/src/core/simulation.jl @@ -76,6 +76,7 @@ function initmodel(settings::Dict{String, Any}) :landscape=>landscape, :weather=>weather, :crops=>crops, + :migrants=>Vector{Pair{Animal, Date}}(), :dataoutputs=>Vector{DataOutput}(), :datatables=>Dict{String, DataFrame}(), :events=>Vector{FarmEvent}()) @@ -135,6 +136,7 @@ function stepsimulation!(model::AgentBasedModel) end end updateevents!(model) + updatenature!(model) outputdata(model) model.date += Day(1) model diff --git a/src/nature/macros.jl b/src/nature/macros.jl new file mode 100644 index 0000000000000000000000000000000000000000..0b2dfcaec87d7db022765cc3d39ec032250d8405 --- /dev/null +++ b/src/nature/macros.jl @@ -0,0 +1,314 @@ +### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe. +### +### This file contains all macros implementing the domain-specific language +### for defining species. +### + +## Note that this DSL consists of lots of deeply nested macro calls, which is a +## known tricky issue in Julia (https://github.com/JuliaLang/julia/issues/23221, +## https://github.com/p-i-/MetaGuideJulia/wiki#example-swap-macro-to-illustrate-esc). +## Hence all the `esc`apes in the following code - be careful when modifying! + +""" + @species(name, body) + +A macro used to create new species definitions for the nature model. +This is effectively a simple domain-specific language, establishing a +custom syntax to describe species' biology: + +```julia +@species name begin + + @initialise(@habitat(...)) + speciesvar1 = 3.14 + ... + + @phase phase1 begin + ... + end +end +``` + +The definition body (enclosed in the begin/end block) has two sections. +First comes a call to [`@initialise`](@ref), and optionally a list of +species-specific parameters, which are assigned just like normal variables. +Second come one or more phase definitions, that describe the behaviour +of the species during various parts of its life cycle. (See the documentation +to [`@initialise`](@ref) and [`@phase`](@ref) for details). + +Code in a species definition block can access the rest of the model using +the `model` variable (an object of type `AgentBasedModel`). +""" +macro species(name, body) + quote + Core.@__doc__ function $(esc(name))($(esc(:model))::AgentBasedModel) + # create internal variables and execute the definition body + $(esc(:name)) = string($(QuoteNode(name))) + $(esc(:phase)) = "" + $(esc(body)) + # extract and process the local variables into a species dict + vardict = Base.@locals + speciesdict = Dict{String,Any}() + for k in keys(vardict) + speciesdict[string(k)] = vardict[k] + end + delete!(speciesdict, "model") + delete!(speciesdict, $(string(name))) + return speciesdict + end + # 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 = $(esc(name))) + end +end + +""" + @initialise(habitatdescriptor; kwargs...) + +Call this macro within the body of [`@species`](@ref). It passes the given habitat +descriptor function and keyword arguments on to [`initpopulation`](@ref) when setting +up the simulation. + +Note: if this macro is not used, the variable `initialise!` must be set manually in the +species definition. +""" +macro initialise(habitatdescriptor, kwargs...) + :($(esc(:initialise!)) = initpopulation($(esc(habitatdescriptor)); $(map(esc, kwargs)...))) +end + +#TODO add an individual-level initialisation function! + +""" + @phase(name, body) + +This macro is designed to be used within a species definition block (i.e. within +the body of a call to [`@species`](@ref)). + +The idea behind this is that species show very different behaviour during different +phases of their lives. Therefore, `@phase` can be used define the behaviour for one +such phase, and the conditions under which the animal transitions to another phase. + +`@phase` works by creating a function that will be called by the model if the +animal is in the relevant phase. When it is called, it has access to the following +variables: +- `animal` a reference to the animal itself. This provides access to `animal.age`, + `animal.sex`, and `animal.<trait>` (where <trait> is a variable that was defined + in the top part of the species definition body). +- `pos` gives the animal's current position as a coordinate tuple. +- `model` a reference to the model world (an object of type `AgentBasedModel`). + This allows access to `model.date` (the current simulation date) and + `model.landscape` (a two-dimensional array of pixels containing geographic + information). + +Several utility macros can be used within the body of `@phase` as a short-hand for +common expressions: [`@trait`](@ref), [`@setphase`](@ref), [`@respond`](@ref), +[`@kill`](@ref), [`@reproduce`](@ref), [`@neighbours`](@ref), [`@rand`](@ref), +[`@shuffle!`](@ref). + +Note that the first phase that is defined in a species definition block will be +the phase that animals are assigned at birth, unless the variable `phase` is +explicitly defined by the user in the species definition block. +""" +macro phase(name, body) + #TODO the docstrings give a lot of warnings in the log - can I fix that? + quote + Core.@__doc__ function $(esc(name))($(esc(:animal))::Animal, $(esc(:model))::AgentBasedModel) + $(esc(:pos)) = $(esc(:animal)).pos + $(esc(body)) + end + ($(esc(:phase)) == "") && ($(esc(:phase)) = $(String(name))) + end +end + +""" + @trait(traitname) + +A utility macro to quickly access an animal's trait value. +This can only be used nested within [`@phase`](@ref). +""" +macro trait(traitname) + #XXX This would error if called in the first part of a species definition block + # (i.e. outside of a @phase block). Although this is specified in the documentation, + # it is unexpected and liable to be overlooked. Can we add a third clause to + # compensate for that? + :($(esc(:animal)).$(traitname)) +end + +""" + @setphase(newphase) + +Switch this animal over to a different phase. This can only be used nested within [`@phase`](@ref). +""" +macro setphase(newphase) + #XXX make this usable in the top part of a species definition? + :($(esc(:animal)).phase = $(String(newphase))) +end + +""" + @respond(eventname, body) + +Define how an animal responds to a landscape event that affects its current position. +This can only be used nested within [`@phase`](@ref). +""" +macro respond(eventname, body) + quote + if $(esc(eventname)) in $(esc(:model)).landscape[$(esc(:animal)).pos...].events + $(esc(body)) + end + end +end + +""" + @kill + +Kill this animal (and immediately abort its current update if it dies). This is a +thin wrapper around [`kill!`](@ref), and passes on any arguments. This can only be +used nested within [`@phase`](@ref). +""" +macro kill(args...) + :(kill!($(esc(:animal)), $(esc(:model)), $(map(esc, args)...)) && return) +end + +""" + @reproduce + +Let this animal reproduce. This is a thin wrapper around [`reproduce!`](@ref), and +passes on any arguments. This can only be used nested within [`@phase`](@ref). +""" +macro reproduce(args...) + :(reproduce!($(esc(:animal)), $(esc(:model)), $(map(esc, args)...))) +end + +""" + @migrate(arrival) + +Remove this animal from the map and add it to the migrant species pool. +It will be returned to its current location at the specified `arrival` date. +This can only be used nested within [`@phase`](@ref). +""" +macro migrate(arrival) + :(migrate!($(esc(:animal)), $(esc(:model)), $(esc(arrival)))) +end + +""" + @neighbours(radius) + +Return an iterator over all animals in the given radius around this animal, excluding itself. +This can only be used nested within [`@phase`](@ref). +""" +macro neighbours(radius) + #TODO enable filtering by species + :(nearby_animals($(esc(:animal)), $(esc(:model)), $(esc(radius)))) +end + +""" + @habitat + +Specify habitat suitability for spatial ecological processes. + +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`](@ref), [`@cropname`](@ref), [`@cropheight`](@ref), +[`@distanceto`](@ref), [`@distancetoedge`](@ref), [`@countanimals`](@ref). +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 && @cropname() != "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. +""" +macro habitat(body) + #XXX I suspect that I may have less problems with macro expansion and module + # scoping if @habitat did not create a new function. But is there a different way? + quote + function($(esc(:pos)), $(esc(:model))) + if $(esc(body)) + return true + else + return false + end + end + end +end + +##XXX Can I make sure (e.g. through `try/catch`) that the following macros +## are not called anywhere outside @habitat/@phase? + +""" + @landcover + +Returns the local landcover. This is a utility wrapper that can only be used +nested within [`@phase`](@ref) or [`@habitat`](@ref). +""" +macro landcover() + :(landcover($(esc(:pos)), $(esc(:model)))) +end + +""" + @cropname + +Return the name of the local croptype, or nothing if there is no crop here. +This is a utility wrapper that can only be used nested within [`@phase`](@ref) +or [`@habitat`](@ref). +""" +macro cropname() + :(cropname($(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 [`@phase`](@ref) +or [`@habitat`](@ref). +""" +macro cropheight() + :(cropheight($(esc(:pos)), $(esc(:model)))) +end + +""" + @distanceto(habitat) + +Calculate the distance to the closest habitat of the specified type or descriptor. +This is a utility wrapper that can only be used nested within [`@phase`](@ref) +or [`@habitat`](@ref). +""" +macro distanceto(habitat) + :(distanceto($(esc(:pos)), $(esc(:model)), $(esc(habitat)))) +end + +""" + @distancetoedge + +Calculate the distance to the closest neighbouring habitat. +This is a utility wrapper that can only be used nested within [`@phase`](@ref) +or [`@habitat`](@ref). +""" +macro distancetoedge() + :(distancetoedge($(esc(:pos)), $(esc(:model)))) +end + +""" + @countanimals(species="", radius=0) + +Count the number of animals of the given species in this location. +This is a utility wrapper that can only be used nested within [`@phase`](@ref) +or [`@habitat`](@ref). +""" +macro countanimals(args...) + :(countanimals($(esc(:pos)), $(esc(:model)); $(map(esc, args)...))) +end + +##TODO add movement macros diff --git a/src/nature/nature.jl b/src/nature/nature.jl index 105644fc24538b327e47d386c5d15fd05fa0a806..b9e96ee85b2ac4c335e0ced081ae1d444ae55869 100644 --- a/src/nature/nature.jl +++ b/src/nature/nature.jl @@ -1,11 +1,10 @@ ### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe. ### -### This file is responsible for managing the animal modules. +### This file is responsible for managing the animal modules and contains functions +### and types integrating the nature submodel with the rest of Persefone. ### -### 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 @@ -88,305 +87,20 @@ function initnature!(model::AgentBasedModel) initecologicaldata(model) end - -### MACROS IMPLEMENTING THE DOMAIN-SPECIFIC LANGUAGE FOR DEFINING SPECIES - -## Note that this DSL consists of lots of deeply nested macro calls, which is a -## known tricky issue in Julia (https://github.com/JuliaLang/julia/issues/23221, -## https://github.com/p-i-/MetaGuideJulia/wiki#example-swap-macro-to-illustrate-esc). -## Hence all the `esc`apes in the following code - be careful when modifying! - -""" - @species(name, body) - -A macro used to create new species definitions for the nature model. -This is effectively a simple domain-specific language, establishing a -custom syntax to describe species' biology: - -```julia -@species name begin - - @initialise(@habitat(...)) - speciesvar1 = 3.14 - ... - - @phase phase1 begin - ... - end -end -``` - -The definition body (enclosed in the begin/end block) has two sections. -First comes a call to [`@initialise`](@ref), and optionally a list of -species-specific parameters, which are assigned just like normal variables. -Second come one or more phase definitions, that describe the behaviour -of the species during various parts of its life cycle. (See the documentation -to [`@initialise`](@ref) and [`@phase`](@ref) for details). - -Code in a species definition block can access the rest of the model using -the `model` variable (an object of type `AgentBasedModel`). -""" -macro species(name, body) - quote - Core.@__doc__ function $(esc(name))($(esc(:model))::AgentBasedModel) - # create internal variables and execute the definition body - $(esc(:name)) = string($(QuoteNode(name))) - $(esc(:phase)) = "" - $(esc(body)) - # extract and process the local variables into a species dict - vardict = Base.@locals - speciesdict = Dict{String,Any}() - for k in keys(vardict) - speciesdict[string(k)] = vardict[k] - end - delete!(speciesdict, "model") - delete!(speciesdict, $(string(name))) - return speciesdict - end - # 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 = $(esc(name))) - end -end - -""" - @initialise(habitatdescriptor; kwargs...) - -Call this macro within the body of [`@species`](@ref). It passes the given habitat descriptor -function and keyword arguments on to [`initpopulation`](@ref) when setting up the simulation. - -Note: if this macro is not used, the variable `initialise!` must be set manually in the -species definition. -""" -macro initialise(habitatdescriptor, kwargs...) - :($(esc(:initialise!)) = initpopulation($(esc(habitatdescriptor)); $(map(esc, kwargs)...))) -end - -""" - @phase(name, body) - -This macro is designed to be used within a species definition block (i.e. within -the body of a call to [`@species`](@ref)). - -The idea behind this is that species show very different behaviour during different -phases of their lives. Therefore, `@phase` can be used define the behaviour for one -such phase, and the conditions under which the animal transitions to another phase. - -`@phase` works by creating a function that will be called by the model if the -animal is in the relevant phase. When it is called, it has access to the following -variables: -- `animal` a reference to the animal itself. This provides access to `animal.age`, - `animal.sex`, and `animal.<trait>` (where <trait> is a variable that was defined - in the top part of the species definition body). -- `pos` gives the animal's current position as a coordinate tuple. -- `model` a reference to the model world (an object of type `AgentBasedModel`). - This allows access to `model.date` (the current simulation date) and - `model.landscape` (a two-dimensional array of pixels containing geographic - information). - -Several utility macros can be used within the body of `@phase` as a short-hand for -common expressions: [`@trait`](@ref), [`@setphase`](@ref), [`@respond`](@ref), -[`@kill`](@ref), [`@reproduce`](@ref), [`@neighbours`](@ref), [`@rand`](@ref), -[`@shuffle!`](@ref). - -Note that the first phase that is defined in a species definition block will be -the phase that animals are assigned at birth, unless the variable `phase` is -explicitly defined by the user in the species definition block. -""" -macro phase(name, body) - #TODO the docstrings give a lot of warnings in the log - can I fix that? - quote - Core.@__doc__ function $(esc(name))($(esc(:animal))::Animal, $(esc(:model))::AgentBasedModel) - $(esc(:pos)) = $(esc(:animal)).pos - $(esc(body)) - end - ($(esc(:phase)) == "") && ($(esc(:phase)) = $(String(name))) - end -end - -""" - @trait(traitname) - -A utility macro to quickly access an animal's trait value. -This can only be used nested within [`@phase`](@ref). -""" -macro trait(traitname) - #XXX This would error if called in the first part of a species definition block - # (i.e. outside of a @phase block). Although this is specified in the documentation, - # it is unexpected and liable to be overlooked. Can we add a third clause to - # compensate for that? - :($(esc(:animal)).$(traitname)) -end - -""" - @setphase(newphase) - -Switch this animal over to a different phase. This can only be used nested within [`@phase`](@ref). -""" -macro setphase(newphase) - #XXX make this usable in the top part of a species definition? - :($(esc(:animal)).phase = $(String(newphase))) -end - """ - @respond(eventname, body) + updatenature!(model) -Define how an animal responds to a landscape event that affects its current position. -This can only be used nested within [`@phase`](@ref). +Run processes that affect all animals. """ -macro respond(eventname, body) - quote - if $(esc(eventname)) in $(esc(:model)).landscape[$(esc(:animal)).pos...].events - $(esc(body)) - end +function updatenature!(model::AgentBasedModel) + # The migrant pool is sorted by date of return, so we can simply look at the top + # of the stack to check whether any animals are returning today. + while !isempty(model.migrants) && model.migrants[1].second == model.date + add_agent_pos!(model.migrants[1].first, model) + @debug "$(animalid(model.migrants[1].first)) has returned." + deleteat!(model.migrants, 1) end + #XXX what else needs to go here? end -""" - @kill - -Kill this animal (and immediately abort its current update). This is a thin wrapper -around [`kill!`](@ref), and passes on any arguments. This can only be used nested within -[`@phase`](@ref). -""" -macro kill(args...) - quote - kill!($(esc(:animal)), $(esc(:model)), $(map(esc, args)...)) - return - end -end - -""" - @reproduce - -Let this animal reproduce. This is a thin wrapper around [`reproduce!`](@ref), and passes on -any arguments. This can only be used nested within [`@phase`](@ref). -""" -macro reproduce(args...) - :(reproduce!($(esc(:animal)), $(esc(:model)), $(map(esc, args)...))) -end - -""" - @neighbours(radius) - -Return an iterator over all animals in the given radius around this animal, excluding itself. -This can only be used nested within [`@phase`](@ref). -""" -macro neighbours(radius) - #TODO enable filtering by species - :(nearby_animals($(esc(:animal)), $(esc(:model)), $(esc(radius)))) -end - -""" - @habitat - -Specify habitat suitability for spatial ecological processes. - -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`](@ref), [`@cropname`](@ref), [`@cropheight`](@ref), -[`@distanceto`](@ref), [`@distancetoedge`](@ref), [`@countanimals`](@ref). -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 && @cropname() != "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. -""" -macro habitat(body) - #XXX I suspect that I may have less problems with macro expansion and module - # scoping if @habitat did not create a new function. But is there a different way? - quote - function($(esc(:pos)), $(esc(:model))) - if $(esc(body)) - return true - else - return false - end - end - end -end - -##XXX Can I make sure (e.g. through `try/catch`) that the following macros -## are not called anywhere outside @habitat/@phase? - -""" - @landcover - -Returns the local landcover. This is a utility wrapper that can only be used -nested within [`@phase`](@ref) or [`@habitat`](@ref). -""" -macro landcover() - :(landcover($(esc(:pos)), $(esc(:model)))) -end - -""" - @cropname - -Return the name of the local croptype, or nothing if there is no crop here. -This is a utility wrapper that can only be used nested within [`@phase`](@ref) -or [`@habitat`](@ref). -""" -macro cropname() - :(cropname($(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 [`@phase`](@ref) -or [`@habitat`](@ref). -""" -macro cropheight() - :(cropheight($(esc(:pos)), $(esc(:model)))) -end - -""" - @distanceto(habitat) - -Calculate the distance to the closest habitat of the specified type or descriptor. -This is a utility wrapper that can only be used nested within [`@phase`](@ref) -or [`@habitat`](@ref). -""" -macro distanceto(habitat) - :(distanceto($(esc(:pos)), $(esc(:model)), $(esc(habitat)))) -end - -""" - @distancetoedge - -Calculate the distance to the closest neighbouring habitat. -This is a utility wrapper that can only be used nested within [`@phase`](@ref) -or [`@habitat`](@ref). -""" -macro distancetoedge() - :(distancetoedge($(esc(:pos)), $(esc(:model)))) -end - -""" - @countanimals(species="", radius=0) - -Count the number of animals of the given species in this location. -This is a utility wrapper that can only be used nested within [`@phase`](@ref) -or [`@habitat`](@ref). -""" -macro countanimals(args...) - :(countanimals($(esc(:pos)), $(esc(:model)); $(map(esc, args)...))) -end - -##TODO @chance macro: @chance(0.5) => rand(model.rng) < 0.5 - -##TODO add movement macros +#TODO test migration diff --git a/src/nature/populations.jl b/src/nature/populations.jl index f2e80c0dc7e735a0e532b20b4905c55b1ba03a8d..c0029a1c4562fa4116d0357137402f6a1381d7e1 100644 --- a/src/nature/populations.jl +++ b/src/nature/populations.jl @@ -29,10 +29,15 @@ This can be used to create the `initialise!` variable in a species definition bl - If `asexual` is true, all created individuals are assigned the sex `hermaphrodite`, otherwise, they are randomly assigned male of female. (If `pairs` is true, `asexual` is ignored.) + +- `initfunction` is an optional function that takes an animal object and the model, + and performs individual-specific initialisation tasks. It is called after the + individual has been created and placed. """ function initpopulation(habitatdescriptor::Function; phase::Union{String,Nothing}=nothing, - popsize::Int64=-1, pairs::Bool=false, asexual::Bool=false) - #TODO add a `popdensity` argument + popsize::Int64=-1, pairs::Bool=false, asexual::Bool=false, + initfunction::Function=(a,m)->nothing) + #TODO add a constructor function for the individual? function(species::Dict{String,Any}, model::AgentBasedModel) n = 0 lastn = 0 @@ -44,12 +49,17 @@ function initpopulation(habitatdescriptor::Function; phase::Union{String,Nothing for y in @shuffle!(Vector(1:height)) if habitatdescriptor((x,y), model) if pairs - add_agent!((x,y), Animal, model, deepcopy(species), female, 0) - add_agent!((x,y), Animal, model, deepcopy(species), male, 0) + a1 = add_agent!((x,y), Animal, model, + deepcopy(species), female, 0) + a2 = add_agent!((x,y), Animal, model, + deepcopy(species), male, 0) + initfunction(a1, model) + initfunction(a2, model) n += 2 else sex = asexual ? hermaphrodite : @rand([male, female]) - add_agent!((x,y), Animal, model, deepcopy(species), sex, 0) + a = add_agent!((x,y),Animal,model,deepcopy(species),sex,0) + initfunction(a, model) n += 1 end end @@ -112,6 +122,24 @@ function kill!(animal::Animal, model::AgentBasedModel, probability::Float64=1.0, return false end +""" + migrate!(animal, model, arrival) + +Remove this animal from the map and add it to the migrant species pool. +It will be returned to its current location at the specified `arrival` date. +""" +function migrate!(animal::Animal, model::AgentBasedModel, arrival::Date) + i = 1 + while i <= length(model.migrants) && model.migrants[i].second < arrival + i += 1 + end + i <= length(model.migrants) ? + insert!(model.migrants, i, Pair(animal, arrival)) : + push!(model.migrants, Pair(animal, arrival)) + remove_agent!(animal, model) + @debug "$(animalid(animal)) has migrated." +end + """ nearby_animals(pos, model, radius) diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl index c5ecc0c708fcb38752147643a61ca03aa84ea982..98d71935b57d9e2ab79c1b0653cdbc13aa26807f 100644 --- a/src/nature/species/skylark.jl +++ b/src/nature/species/skylark.jl @@ -18,16 +18,17 @@ At the moment, this implementation is still in development. """ @species Skylark begin - eggmaturationtime = - + eggmaturationtime = 11 eggharvestmortality = 0.9 #??? eggpredationmortality = 0.1 #??? + migrationdates = () + habitats = @habitat((@landcover() == grass || (@landcover() == agriculture && @croptype() != maize)) && @distanceto(forest) > 5) - @initialise(habitats, pairs=true) + @initialise(habitats, pairs=true, initfunction=initskylark) @phase egg begin @kill(@trait(eggpredationmortality), "predation") @@ -60,3 +61,36 @@ At the moment, this implementation is still in development. #TODO end end + +""" + initskylark(skylark, model) + +Initialise a skylark individual. Selects migration dates and checks if the +bird should currently be on migration. +""" +function initskylark(animal::Animal, model::AgentBasedModel) + animal.migrationdates = migrationdates(animal, model) + leave = animal.migrationdates[1] + arrive = animal.migrationdates[2] + m, d = monthday(model.date) + migrate = (((m < arrive[1]) || (m == arrive[1] && d < arrive[2])) || + ((m > leave[1]) || (m == leave[1] && d >= leave[2]))) + migrate && @migrate(Date(year(model.date)+year(1), arrive[1], arrive[2])) + #TODO other stuff? +end + +""" + migrationdates(skylark, model) + +Select the dates on which this skylark will leave for / return from its migration, +based on observed migration patterns. +""" +function migrationdates(skylark::Animal, model::AgentBasedModel) + minleave = skylark.sex == female ? (9, 15) : (10, 1) + minarrive = skylark.sex == male ? (2, 15) : (3, 1) + deltaleave = @rand(0:45) #XXX ought to be normally distributed + deltaarrive = @rand(0:15) #XXX ought to be normally distributed + leave = monthday(Date(2000, minleave[1], minleave[2]) + Day(deltaleave)) + arrive = monthday(Date(2000, minarrive[1], minarrive[2]) + Day(deltaarrive)) + (leave, arrive) +end diff --git a/src/parameters.toml b/src/parameters.toml index 80d845fa127b4a66d754c160dbb0065b8cef7ad9..45a4519b968dd80730b1c485af187459b19d9624 100644 --- a/src/parameters.toml +++ b/src/parameters.toml @@ -29,7 +29,7 @@ weatherfile = "data/regions/jena/weather.csv" # location of the weather data fil farmmodel = "FieldManager" # which version of the farm model to use (not yet implemented) [nature] -targetspecies = ["Wolpertinger", "Wyvern"] # list of target species to simulate +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/runtests.jl b/test/runtests.jl index f2dca73a018944e26ae461def5c1558ae988476e..78d32931d147e946905dfd86d639bf908cc6c397 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -46,6 +46,7 @@ function inittestmodel(smallmap=true) :weather=>weather, :crops=>crops, :events=>Vector{FarmEvent}(), + :migrants=>Vector{Pair{Animal, Date}}(), :logger=>global_logger(), :dataoutputs=>Vector{DataOutput}(), :datatables=>Dict{String,DataFrame}(),