diff --git a/src/Persefone.jl b/src/Persefone.jl index 2c4e0b66a7fcf4a53b918c2a1f84445798c9baf9..ec269bfd733887472526f65acaad82a1e779f3e5 100644 --- a/src/Persefone.jl +++ b/src/Persefone.jl @@ -69,10 +69,14 @@ export @killother, @reproduce, @migrate, + @occupy, + @isoccupied, + @vacate, @habitat, @landcover, @cropname, @cropheight, + @cropcover, @directionto, @distanceto, @distancetoedge, diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl index cb349be6731c5213cba98cc5d336f9cca94b85b2..13b7ad308d8e5b26b017348bd5ac0e7b3aed9c34 100644 --- a/src/crop/farmplot.jl +++ b/src/crop/farmplot.jl @@ -199,3 +199,16 @@ function cropheight(pos::Tuple{Int64,Int64}, model::SimulationModel) ismissing(model.landscape[pos...].fieldid) ? nothing : model.farmplots[model.landscape[pos...].fieldid].height end + +""" + cropcover(model, position) + +Return the percentage ground cover of the crop at this position, or nothing if there is no crop +here (utility wrapper). +""" +function cropcover(pos::Tuple{Int64,Int64}, model::SimulationModel) + #FIXME LAItotal != ground cover? + ismissing(model.landscape[pos...].fieldid) ? nothing : + model.farmplots[model.landscape[pos...].fieldid].LAItotal +end + diff --git a/src/nature/macros.jl b/src/nature/macros.jl index 264b1f675264d82c0af96f77ac2f0191db783db8..1fa21f42f0649e70f2bf41a9b156975fcada2258 100644 --- a/src/nature/macros.jl +++ b/src/nature/macros.jl @@ -348,7 +348,7 @@ end """ @cropheight -Return the height of the crop at this position, or 0 if there is no crop here. +Return the height of the crop at this position, 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). """ @@ -356,6 +356,17 @@ macro cropheight() :(cropheight($(esc(:pos)), $(esc(:model)))) end +""" + @cropcover + +Return the percentage ground cover of the crop at this position, 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 cropcover() + :(cropcover($(esc(:pos)), $(esc(:model)))) +end + """ @directionto @@ -468,6 +479,7 @@ used nested within [`@phase`](@ref). macro walk(args...) #XXX add `ifempty` keyword? :(walk!($(esc(:self)), $(esc(:model)), $(map(esc, args)...))) + #FIXME MethodError: no method matching walk!(::Persefone.Skylark, ::AgricultureModel, ::Tuple{UnitRange{Int64}, UnitRange{Int64}}) -> due to map(esc())? end #TODO add own walking functions that respect habitat descriptors diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl index dd959c3daae80733946d4651e52a308058bc949d..c5642050194db6e533b0a1f3317bee5984135c10 100644 --- a/src/nature/species/skylark.jl +++ b/src/nature/species/skylark.jl @@ -31,8 +31,8 @@ At the moment, this implementation is still in development. """ @species Skylark begin # species parameters - const movementrange = 500m #XXX arbitrary - const visionrange = 200m #XXX arbitrary + const movementrange::Length = 500m #XXX arbitrary + const visionrange::Length = 200m #XXX arbitrary const eggtime::Int64 = 11 # days from laying to hatching const nestlingtime::UnitRange{Int64} = 7:11 # days from hatching to leaving nest @@ -55,7 +55,7 @@ At the moment, this implementation is still in development. const nestingbegin::Tuple{Int64,Int64} = (April, 10) # begin nesting in the middle of April const nestbuildingtime::UnitRange{Int64} = 4:5 # 4-5 days needed to build a nest (doubled for first nest) const eggsperclutch::UnitRange{Int64} = 2:5 # eggs laid per clutch - const breedingdelay::Int64 = 18 # days after hatching before starting a new brood + const breedingdelay::Int64 = 18 # days after hatching before starting a new brood #XXX ?? const nestingend::Int64 = July # last month of nesting # individual variables @@ -74,7 +74,7 @@ end #TODO respect habitat when moving """ -As a non-breeding adult, move around with other individuals and check for migration. +Non-breeding adults move around with other individuals and check for migration. """ @phase Skylark nonbreeding begin # flocking behaviour - follow a random neighbour or move randomly @@ -91,27 +91,45 @@ As a non-breeding adult, move around with other individuals and check for migrat @kill(self.migrationmortality, "migration") returndate = Date(year(model.date)+1, arrive[1], arrive[2]) self.sex == male ? - @setphase(territory-search) : - @setphase(mate-search) + @setphase(territorysearch) : + @setphase(matesearch) @migrate(returndate) end end -@phase Skylark territory-search begin - #TODO move around, searching for a suitable area to establish a territory - +""" +Males returning from migration move around to look for suitable habitats to establish a territory. +""" +@phase Skylark territorysearch begin + #TODO + #TODO Standorttreue # If we've found a territory, or the breeding season is over, move to the next phase if !isempty(self.territory) @setphase(occupation) - elseif month(model.date) > nestingend + elseif month(model.date) > self.nestingend @setphase(nonbreeding) else @walk("random", self.movementrange) + #FIXME MethodError: no method matching walk!(::Persefone.Skylark, ::AgricultureModel, ::Tuple{UnitRange{Int64}, UnitRange{Int64}}) end end -@phase Skylark mate-search begin +""" +Once a male has found a territory, he remains in it until the breeding season is over, +adjusting it to new conditions when and as necessary. +""" +@phase Skylark occupation begin + #move to a random location in the territory + @move(@rand(self.territory)) + #TODO adjust territory as needed +end + +""" +Females returning from migration move around to look for a suitable partner with a territory. +""" +@phase Skylark matesearch begin #TODO move around, looking for a male with an established territory + #TODO teilweise Partnertreue # look for a mate among the neighbouring birds, or move randomly for n in @neighbours(self.visionrange) @@ -124,10 +142,7 @@ end end #@debug("$(animalid(self)) didn't find a mate.") if self.mate != -1 - mon, day = monthday(model.date) - nest = ((mon == self.nestingbegin[1] && day >= self.nestingbegin[2] - && @chance(0.1)) || (mon > self.nestingbegin[1])) #XXX magic number - nest && @setphase(breeding) + @setphase(nesting) elseif month(model.date) > self.nestingend # stop trying to find a mate if it's too late @setphase(nonbreeding) else @@ -135,25 +150,60 @@ end end end -@phase Skylark occupation begin - #move to a random location in the territory - @move(@rand(self.territory)) - #TODO adjust territory as needed +""" +Females that have found a partner build a nest and lay eggs in a suitable location. +""" +@phase Skylark nesting begin + if model.date < Date(year(model.date), self.nestingbegin...) + # wait for nesting to begin, moving around in the territory + @move(@rand(@animal(self.mate).territory)) + elseif isempty(nest) + # choose site, build nest & lay eggs + for pos in @shuffle!(deepcopy(@animal(self.mate).territory)) + #TODO is this condition correct? -> needs validation! + if (@landcover() == grass || @landcover() == soil || + (@landcover() == agriculture && + (self.nestingheight[1] <= @cropheight() <= self.nestingheight[2]) && + (self.nestingcover[1] <= @cropcover() <= self.nestingcover[2]))) + @move(pos) + self.nest = pos + self.clutch = @rand(self.eggsperclutch) + timer = self.nestbuildingtime + self.clutch # time to build + 1 day per egg laid + if month(model.date) == self.nestingbegin[1] + # the first nest takes twice as long to build + #XXX this may affect the first two nests + timer += self.nestbuildingtime + end + break + end + end + isempty(nest) && @warn("$(animalid(self)) didn't find a nesting location.") + elseif timer == 0 + @debug("$(animalid(self)) has laid $(self.clutch) eggs.") + @setphase(breeding) + else + self.timer -= 1 + end + # tillage and harvest destroys the nest + @respond(tillage, nest = ()) + @respond(harvesting, nest = ()) end +""" +Females that have laid eggs take care of their chicks, restarting the nesting process once the +chicks are independent or in case of brood loss. +""" @phase Skylark breeding begin - if isempty(nest) - #TODO choose site, build nest & lay eggs - elseif clutch > 0 - #TODO wait for eggs to hatch & chicks to mature, checking for mortality + #TODO wait for eggs to hatch & chicks to mature, checking for mortality + # restart breeding cycle if there is time + if clutch == 0 && month(model.date) <= self.nestingend + @setphase(nesting) #TODO breeding delay? elseif month(model.date) > self.nestingend - #TODO restart cycle, if time @setphase(nonbreeding) end end - ## SUPPORTING FUNCTIONS """ @@ -215,8 +265,8 @@ should currently be on migration. Also sets other individual-specific variables. returndate = Date(year(model.date), arrive[1], arrive[2]) model.date != @param(core.startdate) && (returndate += Year(1)) self.sex == male ? - @setphase(territory-search) : - @setphase(mate-search) + @setphase(territorysearch) : + @setphase(matesearch) @migrate(returndate) end end