diff --git a/src/nature/individuals.jl b/src/nature/individuals.jl index 863bb8f82ac0d9eaf0e39422b3eb9e60957f3a4e..f1f248890de3ad7f27e9bffafd0593b3c3cd08f4 100644 --- a/src/nature/individuals.jl +++ b/src/nature/individuals.jl @@ -162,7 +162,7 @@ function walk!(animal::Animal, model::SimulationModel, direction::String, distan elseif direction == "northwest" shift = (-steps,-steps) elseif direction == "random" - shift = Tuple(@rand([-steps,0,steps], 2)) + shift = Tuple(@rand([-steps:steps], 2)) else @error "Invalid direction in @walk: "*direction end diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl index 6400bbcd0c4b2a549eef4d11f8ddf491544759a3..dd959c3daae80733946d4651e52a308058bc949d 100644 --- a/src/nature/species/skylark.jl +++ b/src/nature/species/skylark.jl @@ -3,15 +3,6 @@ ### This file holds the code for the Eurasian Skylark (Alauda arvensis). ### -#XXX global variable/function -skylarkhabitat = @habitat((@landcover() == grass || - (@landcover() == agriculture && @cropname() != "maize")) && - @distancetoedge() > 50m) -#XXX this ought to check for distance to forest and builtup, -# but that's very expensive (see below) -# @distanceto(forest) > 5 && # at least 50m from forest edges -# @distanceto(builtup) > 5) # and from anthropogenic structures - """ Skylark @@ -19,108 +10,78 @@ skylarkhabitat = @habitat((@landcover() == grass || At the moment, this implementation is still in development. **Sources:** - - Bauer, H.-G., Bezzel, E., & Fiedler, W. (Eds.). (2012). - Das Kompendium der Vögel Mitteleuropas: Ein umfassendes - Handbuch zu Biologie, Gefährdung und Schutz (Einbändige - Sonderausg. der 2., vollständig überarb. und erw. Aufl. 2005). - AULA-Verlag - - Glutz von Blotzheim, Urs N. (Ed.). (1985). Handbuch der - Vögel Mitteleuropas. Bd. 10. Passeriformes (Teil 1) - 1. Alaudidae - Hirundidae. AULA-Verlag, Wiesbaden. + - Bauer, H.-G., Bezzel, E., & Fiedler, W. (Eds.). (2012). Das Kompendium + der Vögel Mitteleuropas: Ein umfassendes Handbuch zu Biologie, Gefährdung + und Schutz (Einbändige Sonderausg. der 2., vollständig überarb. und erw. + Aufl. 2005). AULA-Verlag + - Delius, J. D. (1965). A Population Study of Skylarks Alauda Arvensis. + Ibis, 107(4), 466–492. https://doi.org/10.1111/j.1474-919X.1965.tb07332.x + - Donald et al. (2002). Survival rates, causes of failure and productivity + of Skylark Alauda arvensis nests on lowland farmland. Ibis, 144(4), 652–664. + https://doi.org/10.1046/j.1474-919X.2002.00101.x + - Glutz von Blotzheim, Urs N. (Ed.). (1985). Handbuch der Vögel Mitteleuropas. + Bd. 10. Passeriformes (Teil 1) 1. Alaudidae - Hirundidae. AULA-Verlag, Wiesbaden. ISBN 3-89104-019-9 + - Jenny, M. (1990). Territorialität und Brutbiologie der Feldlerche Alauda + arvensis in einer intensiv genutzten Agrarlandschaft. Journal für Ornithologie, + 131(3), 241–265. https://doi.org/10.1007/BF01640998 + - Püttmanns et al. (2022). Habitat use and foraging parameters of breeding Skylarks + indicate no seasonal decrease in food availability in heterogeneous farmland. + Ecology and Evolution, 12(1), e8461. https://doi.org/10.1002/ece3.8461 """ @species Skylark begin # species parameters + const movementrange = 500m #XXX arbitrary + const visionrange = 200m #XXX arbitrary + const eggtime::Int64 = 11 # days from laying to hatching const nestlingtime::UnitRange{Int64} = 7:11 # days from hatching to leaving nest const fledglingtime::UnitRange{Int64} = 25:30 # days from hatching to independence const eggpredationmortality::Float64 = 0.03 # per-day egg mortality from predation - const nestharvestmortality::Float64 = 0.9 # egg/nestling mortality after a harvest event (XXX guess) + const nestharvestmortality::Float64 = 1.0 # egg/nestling mortality after a harvest event const nestlingpredationmortality::Float64 = 0.03 # per-day nestling mortality from predation const fledglingharvestmortality::Float64 = 0.5 # fledgling mortality after harvest const fledglingpredationmortality::Float64 = 0.01 # per-day fledgling mortality from predation const firstyearmortality::Float64 = 0.38 # total mortality in the first year after independence const migrationmortality::Float64 = 0.33 # chance of dying during the winter + const minimumterritory = 5000m² # size of territory under ideal conditions + const maxforageheight = 50cm # maximum preferred vegetation height for foraging + const maxforagecover = 0.7 # maximum preferred vegetation cover for foraging + const nestingheight = (15cm, 25cm) # min and max preferred vegetation height for nesting + const nestingcover = (0.2, 0.5) # min and max preferred vegetation cover for nesting + 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 nestingend::Int64 = July # last month of nesting - - const habitats::Function = skylarkhabitat # individual variables - daystonextphase::Int64 = 0 # days remaining until fledging or maturity + #FIXME check what needs to be rewritten + timer::Int64 = 0 # a count-down timer that can be used for different purposes migrationdates::Tuple = () # is defined by each individual in @create(Skylark) - territory::Vector = [] # pixels that this skylark claims as its territory mate::Int64 = -1 # the agent ID of the mate (-1 if none) nest::Tuple = () # coordinates of current nest nestcompletion::Int64 = 0 # days left until the nest is built - clutch::Vector{Int64} = Vector{Int64}() # IDs of offspring in current clutch + clutch::Int64 = 0 # number of offspring in current clutch end -""" -As an egg, simply check for mortality and hatching. -""" -@phase Skylark egg begin - @kill(self.eggpredationmortality, "predation") - @respond(harvesting, @kill(self.nestharvestmortality, "harvest")) - if self.age == self.eggtime - self.daystonextphase = @rand(self.nestlingtime) - @setphase(nestling) - end -end -""" -As a nestling, simply check for mortality and fledging. -""" -@phase Skylark nestling begin - #TODO add feeding & growth - @kill(self.nestlingpredationmortality, "predation") - @respond(harvesting, @kill(self.nestharvestmortality, "harvest")) - # if self.age == self.nestlingtime+self.eggtime - # @setphase(fledgling) - # end - if self.daystonextphase == 0 - self.daystonextphase = @rand(self.fledglingtime) - @setphase(fledgling) - else - self.daystonextphase -= 1 - end -end +## LIFE PHASES -""" -As a fledgling, move around a little, but mainly wait for maturity and -check mortality. -""" -@phase Skylark fledgling begin - #TODO add feeding & growth - @kill(self.fledglingpredationmortality, "predation") - @walk("random", 10m) #TODO add movement following the parents - # if self.age == self.fledglingtime+self.eggtime - # @kill(self.firstyearmortality, "first year mortality") #XXX mechanistic? - # @setphase(nonbreeding) - # end - if self.daystonextphase == 0 - @kill(self.firstyearmortality, "first year mortality") #XXX mechanistic? - @setphase(nonbreeding) - else - self.daystonextphase -= 1 - end -end +#TODO respect habitat when moving """ As a non-breeding adult, move around with other individuals and check for migration. """ @phase Skylark nonbreeding begin # flocking behaviour - follow a random neighbour or move randomly - #TODO add feeding and mortality, respect habitat when moving - neighbours = @neighbours(100m) #XXX magic number + neighbours = @neighbours(self.visionrange) #XXX check for the closest neighbour(s)? isempty(neighbours) ? - @walk("random", 50m) : - @follow(@rand(neighbours), 20m) + @walk("random", self.movementrange) : + @follow(@rand(neighbours), 30m) #XXX magic number # check if the bird migrates leave, arrive = self.migrationdates month, day = monthday(model.date) @@ -129,111 +90,72 @@ As a non-breeding adult, move around with other individuals and check for migrat if migrate @kill(self.migrationmortality, "migration") returndate = Date(year(model.date)+1, arrive[1], arrive[2]) - @setphase(mating) + self.sex == male ? + @setphase(territory-search) : + @setphase(mate-search) @migrate(returndate) end end -""" -Move around until a mate is found. -""" -@phase Skylark mating begin - #TODO mortality and feeding - #TODO territoriality - # if we've found a mate, wait for nesting begin and then go to the next phase - if self.mate != -1 - if !@isalive(self.mate) - self.mate = -1 - return - end - mon, day = monthday(model.date) - nest = ((mon == self.nestingbegin[1] && day >= self.nestingbegin[2] - && @chance(0.05)) || (mon > self.nestingbegin[1])) #XXX why the chance? - nest && @setphase(nestbuilding) - return +@phase Skylark territory-search begin + #TODO move around, searching for a suitable area to establish a territory + + # 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 + @setphase(nonbreeding) + else + @walk("random", self.movementrange) end +end + +@phase Skylark mate-search begin + #TODO move around, looking for a male with an established territory + # look for a mate among the neighbouring birds, or move randomly - for n in @neighbours(500m) #XXX magic number - if n.sex != self.sex && n.phase == mating && n.mate == -1 + for n in @neighbours(self.visionrange) + if n.sex == male && n.mate == -1 && n.phase == occupation self.mate = n.id n.mate = self.id @debug "$(animalid(self)) and $(animalid(n)) have mated." - return + break end end #@debug("$(animalid(self)) didn't find a mate.") - if month(model.date) > self.nestingend # stop trying to find a mate if it's too late + 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) + elseif month(model.date) > self.nestingend # stop trying to find a mate if it's too late @setphase(nonbreeding) else - @walk("random", 100m) #XXX magic number + @walk("random", self.movementrange) end end -""" -Females select a location and build a nest. Males do nothing. (Sound familiar?) -""" -@phase Skylark nestbuilding begin - #TODO mortality and feeding - if !@isalive(self.mate) - self.mate = -1 - @setphase(nonbreeding) - return - end - if self.sex == female - if isempty(self.nest) - # try to find a nest in the neighbourhood, or move on - nestlocation = @randompixel(100m, self.habitats) #XXX magic number - if isnothing(nestlocation) - @walk("random", 200m) #XXX magic number - else - # if we've found a location, start the clock on the building time - # (building time doubles for the first nest of the year) - self.nest = nestlocation - self.nestcompletion = @rand(self.nestbuildingtime) - month(model.date) == self.nestingbegin[1] && (self.nestcompletion *= 2) - @debug "$(animalid(self)) is building a nest." - end - else - # wait while nest is being built, then lay eggs and go to next phase - if self.nestcompletion > 0 - self.nestcompletion -= 1 - else - #XXX more accurately, a female lays one egg per day, not all at once - @reproduce(@rand(self.eggsperclutch),self.mate) - @setphase(breeding) - end - end - else - # males stay near the female - mate = @animal(self.mate) - @follow(mate, 50m) - mate.phase == breeding && @setphase(breeding) - end +@phase Skylark occupation begin + #move to a random location in the territory + @move(@rand(self.territory)) + #TODO adjust territory as needed end -""" -Do lots of foraging (not yet implemented). -""" @phase Skylark breeding begin - #TODO forage (move inside the territory) - for offspring in self.clutch - # check if offspring are still alive and juvenile, else remove from clutch - if !@isalive(offspring) || @animal(offspring).phase == nonbreeding - filter!(x -> x != offspring, self.clutch) - end - end - # if all young have fledged, move to nonbreeding (if it's July) or breed again - if isempty(self.clutch) - self.nest = () - if month(model.date) >= self.nestingend - self.territory = [] - @setphase(nonbreeding) - else - @setphase(nestbuilding) - end + 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 + elseif month(model.date) > self.nestingend + #TODO restart cycle, if time + @setphase(nonbreeding) end end + + +## SUPPORTING FUNCTIONS + """ migrationdates(skylark, model) @@ -251,14 +173,40 @@ function migrationdates(skylark::Animal, model::SimulationModel) (leave, arrive) end +""" + foragequality(maxcover, maxheight, position, model) + +Calculate the relative quality of the habitat at this position for foraging. +(Approximated from Püttmanns et al., 2021; Jeromin, 2002; Jenny, 1990b.) +""" +function foragequality(maxcover::Float64, maxheight::Float64, position::Tuple{Int64,Int64}, + model::SimulationModel) + #TODO this is a key function that needs to be validated thoroughly + px = model.landscape[position...] + !(px.landcover in (grass, soil, agriculture)) && return 0.0 + quality = 1.0 + if !is.missing(px.fieldid) + f = model.farmplots[px.fieldid] + groundcoverfactor = x -> bounds(-1/maxcover + 1.0, max=1.0) + plantheightfactor = x -> bounds(-1/maxheight + 1.0, max=1.0) + #FIXME need percentage cover, not LAI + quality = bounds(groundcoverfactor(f.LAItotal) + plantheightfactor(f.height), max=1.0) + else + @warn "No field assigned to location $position" px + end + return quality +end + + +## INITIALISATION + """ Initialise a skylark individual. Selects migration dates and checks if the bird should currently be on migration. Also sets other individual-specific variables. """ @create Skylark begin @debug "Added $(animalid(self)) at $(self.pos)" - # calculate migration dates for this individual - self.migrationdates = migrationdates(self, model) + self.migrationdates = migrationdates(self, model) #XXX should be dynamic leave, arrive = self.migrationdates mon, day = monthday(model.date) migrate = (((mon < arrive[1]) || (mon == arrive[1] && day < arrive[2])) || @@ -266,15 +214,19 @@ should currently be on migration. Also sets other individual-specific variables. if migrate 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) @migrate(returndate) end - #TODO other stuff? end @populate Skylark begin - habitat = skylarkhabitat - initphase = mating - birthphase = egg + # initialise on open land, at least 60m from vertical structures + habitat = @habitat((@landcover() == grass || @landcover() == agriculture) && + @distanceto(forest) >= 60m && @distanceto(builtup) >=60m) + initphase = nonbreeding + birthphase = nonbreeding indarea = 3ha pairs = true end