Select Git revision
crop_data.csv
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
skylark.jl 18.42 KiB
### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
###
### This file holds the code for the Eurasian Skylark (Alauda arvensis).
###
##TODO
## - habitat-dependent juvenile predation mortality
## - habitat-dependent dispersal movement
## - mid-season territory adjustment
## - temperature-dependent migration, breeding begin, and mortality
"""
Skylark
*Alauda arvensis* is a common and charismatic species of agricultural landscapes.
**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
- 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
# (parameters that are not `const`ants can be changed via the configuration file)
# juveniles
const eggtime::Int64 = 11 # days from laying to hatching
const nestlingtime::Int64 = 9 # days from hatching to leaving nest
const fledglingtime::Int64 = 21 # days from leaving the nest to independence
#XXX predation mortality should be habitat-dependent
const eggpredationmortality::Float64 = 0.03 # per-day egg mortality from predation
const nestlingpredationmortality::Float64 = 0.03 # per-day nestling mortality from predation
const fledglingpredationmortality::Float64 = 0.01 # per-day fledgling mortality from predation
firstyearmortality::Float64 = 0.38 # total mortality in the first year after independence
# migration
const migrationdeparture::Tuple{AnnualDate,AnnualDate} = ((September, 15), (November, 1))
const migrationarrival::Tuple{AnnualDate,AnnualDate} = ((February, 15), (March, 1))
const migrationdelayfemales::Day = Day(15)
migrationmortality::Float64 = 0.33 # chance of dying during the winter
# habitat requirements
minimumterritory = 5000m² # size of territory under ideal conditions
mindistancetoedge = 60m # minimum distance of habitat to vertical structures
maxforageheight = 50cm # maximum preferred vegetation height for foraging
maxforagecover = 0.7 # maximum preferred vegetation cover for foraging
nestingheight = (15cm, 25cm) # min and max preferred vegetation height for nesting
nestingcover = (0.2, 0.5) # min and max preferred vegetation cover for nesting
# breeding
matefaithfulness = 0.5 # chance of a female retaining her previous partner
const nestingbegin::Tuple{AnnualDate,AnnualDate} = ((April, 10), (April, 20)) # 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 nestingend::AnnualDate = (August, 15) # end of the nesting period
# mating / non-breeding
movementrange::Length = 500m #XXX arbitrary
const visionrange::Length = 200m #XXX arbitrary
const flockingdistance::Length = 30m #XXX arbitrary
## INDIVIDUAL VARIABLES
timer::Int64 = 0 # a counter that can be used for different purposes
firstnest::AnnualDate = (April, 15) # is redefined by each individual in @create(Skylark)
migrationdates::Tuple = () # is defined by each individual in @create(Skylark)
mate::Int64 = -1 # the agent ID of the mate (-1 if none)
nest::Tuple = () # coordinates of current nest
clutch::Int64 = 0 # number and life stage of offspring in current clutch
following::Int64 = -1 # ID of the individual being followed in the non-breeding phase
end
## LIFE PHASES
#TODO respect habitat when moving
"""
Non-breeding adults move around with other individuals and check for migration.
"""
@phase Skylark nonbreeding begin
# check if the bird migrates
if model.date >= self.migrationdates[2]
@kill(self.migrationmortality, "migration")
self.sex == male ?
@setphase(territorysearch) :
@setphase(matesearch)
@migrate(self.migrationdates[1])
return
end
# flocking behaviour - follow a neighbour or move randomly
self.following > 0 && isnothing(@animal(self.following)) && (self.following = -1)
if self.following == -1
neighbours = @neighbours(self.visionrange)
if isempty(neighbours)
@walk("random", self.movementrange)
return
else
self.following = @rand(neighbours).id
end
end
@follow(@animal(self.following), self.flockingdistance)
end
"""
Males returning from migration move around to look for suitable habitats to establish a territory.
"""
@phase Skylark territorysearch begin
if !isempty(self.territory)
@setphase(occupation) # If the male still has a territory, it is occupied again
elseif model.date > self.nestingend - Month(1)
@setphase(nonbreeding) # If the breeding season is almost over, give up looking
else # otherwise check whether this is a suitable location
newterritory = findterritory(self, model)
if isempty(newterritory)
@walk("random", self.movementrange)
else
for p in newterritory
@occupy(p)
end
@debug("$(animalid(self)) has found a territory.")
end
end
end
"""
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))
if model.date > self.nestingend
# once the breeding season is over and all the young have left the nest, stop breeding
if self.mate == -1 || isnothing(@animal(self.mate)) || @animal(self.mate).clutch == 0
@setphase(nonbreeding)
end
end
#TODO adjust territory as needed (e.g. once a week, or when a brood is done?)
end
"""
Females returning from migration move around to look for a suitable partner with a territory.
"""
@phase Skylark matesearch begin
if self.mate != -1 # if the female already had a partner last year...
if @isalive(self.mate)
if @chance(self.matefaithfulness)
# ...check if he is still alive and the female wants to stay with him
#XXX is mate-faithfulness decided by the female when she returns,
# or by the male taking whichever female comes to him first?
@debug "$(animalid(self)) and $(self.mate) have mated again."
@setphase(nesting)
return
else
@animal(self.mate).mate = -1
self.mate = -1
end
else
self.mate = -1
end
end
# look for a mate among the neighbouring birds, or move randomly
for n in @neighbours(self.visionrange)
if n.sex == male && n.mate == -1 && n.phase == occupation
self.mate = n.id #XXX may be the old partner again by chance
n.mate = self.id
@setphase(nesting)
@debug "$(animalid(self)) and $(animalid(n)) have mated."
return
end
end
#@debug("$(animalid(self)) didn't find a mate.")
if model.date > self.nestingend - Month(1)
@setphase(nonbreeding) # Stop trying to find a mate if it's too late
else
@walk("random", self.movementrange)
end
end
"""
Females that have found a partner build a nest and lay eggs in a suitable location.
"""
@phase Skylark nesting begin
if model.date < self.firstnest #XXX dependent on weather?
# wait for nesting to begin, moving around in the territory
@move(@rand(@animal(self.mate).territory))
elseif self.clutch == 0 && model.date > self.nestingend - Month(1)
@setphase(nonbreeding) # stop trying to nest if it's too late (should be rare)
elseif isempty(self.nest)
# choose site, build nest & lay eggs
#XXX can I find a better solution that deepcopying & shuffling to randomise the location?
for pos in @shuffle!(deepcopy(@animal(self.mate).territory))
if allowsnesting(self, model, pos)
@move(pos)
self.nest = pos
#XXX all skylarks laying on the same day lay the same number of eggs? RNG?!
self.timer = @rand(self.nestbuildingtime)
(model.date == self.firstnest) && (self.timer *= 2) # the first nest takes twice as long to build
@record("skylark_breeding",
[model.date, self.id, self.mate, pos[1], pos[2], string(@landcover()),
@cropname(), territorysize(self.mate, model, ha, true)])
break
end
end
#FIXME happens quite often (probably because the crop models don't work yet, so
# all agricultural areas have height == cover == 0)
#isempty(self.nest) && @warn("$(animalid(self)) didn't find a nesting location.")
elseif self.timer == 0
if self.clutch == 0 # one egg is laid per day
self.clutch = @randn(self.eggsperclutch)
self.timer = self.clutch
else
@debug("$(animalid(self)) has laid $(self.clutch) eggs.")
@setphase(breeding)
end
else
self.timer -= 1
end
# tillage and harvest destroys the nest
@respond(tillage, @destroynest("tillage"))
@respond(harvesting, @destroynest("harvesting"))
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
#XXX Schachtelbruten - sometimes skylarks start a new nest before the previous young are gone
# wait for eggs to hatch & chicks to mature, checking for predation and disturbance mortality
self.timer += 1
#TODO this should be habitat-dependent!
if self.timer <= self.eggtime
@chance(self.eggpredationmortality) && @destroynest("predation")
elseif self.timer <= self.eggtime + self.nestlingtime
@chance(self.nestlingpredationmortality) && @destroynest("predation")
elseif self.timer <= self.eggtime + self.nestlingtime + self.fledglingtime
@chance(self.fledglingpredationmortality) && @destroynest("predation")
else
# create new young, reset timer and clutch counter
for o in 1:self.clutch #XXX is this the best way of doing first-year mortality?
if @chance(self.firstyearmortality)
@debug("A skylark has died from first year mortality")
@record("mortality", [model.date, "Skylark", "firstyearmortality"])
else
@reproduce(1, self.mate)
end
end
self.clutch = 0
end
if self.clutch > 0
# tillage and harvest destroys the nest
@respond(tillage, @destroynest("tillage"))
@respond(harvesting, @destroynest("harvesting"))
else # restart breeding cycle if there is time
self.timer = 0
self.nest = ()
if model.date <= self.nestingend - Month(1)
@setphase(nesting)
else
@setphase(nonbreeding)
end
end
end
## SUPPORTING FUNCTIONS
"""
findterritory(skylark, model)
Check whether the habitat surrounding the skylark is suitable for establishing a territory.
If it is, return the list of coordinates that make up the new territory, else return an empty list.
"""
function findterritory(skylark::Skylark, model::SimulationModel)
effectivesize::Area = 0m² # the usable size of the territory, weighted by habitat quality
territory::Vector{Tuple{Int64,Int64}} = []
width, height = size(model.landscape)
radius = 0
constrained = false
# Inspect the landscape in concentric circles around the individual until enough pixels have
# been found to provide a territory of sufficient size and quality. If there are no suitable
# pixels in one circle, break off the search (territories must be contiguous).
while !constrained && effectivesize < skylark.minimumterritory
constrained = true
if radius == 0
coords = [skylark.pos]
else # list all coordinates in the next circle...
coords = []
xrange = (skylark.pos[1]-radius, skylark.pos[1]+radius)
yrange = (skylark.pos[2]-radius, skylark.pos[2]+radius)
for x in xrange[1]:xrange[2]
push!(coords, (x, yrange[1]))
push!(coords, (x, yrange[2]))
end
for y in (yrange[1]+1):(yrange[2]-1) #avoid duplicating the corners
push!(coords, (y, xrange[1]))
push!(coords, (y, xrange[2]))
end
end
#FIXME some duplicates remain?
for c in coords # ...then inspect them
(c[1] <= 0 || c[2] <= 0 || c[1] > width || c[2] > height) && continue
(isoccupied(model, "Skylark", c)) && continue
push!(territory, c)
quality = foragequality(skylark, model, c)
effectivesize += @areaof(quality)
(quality > 0) && (constrained = false)
#XXX check for nesting habitats?
end
radius +=1
end
constrained ? [] : territory
end
"""
foragequality(skylark, model, pos)
Calculate the relative quality of the habitat at this position for foraging.
This assumes that open habitat is best (quality = 1.0), and steadily decreases as vegetation
height and/or cover increase. (Linear regressions based on Püttmanns et al., 2021; Jeromin, 2002;
Jenny, 1990b.)
"""
function foragequality(skylark::Skylark, model::SimulationModel, pos::Tuple{Int64,Int64})
#TODO this is a key function that needs to be validated thoroughly
if !(@landcover() in (grass, soil, agriculture)) ||
(@distanceto(forest) < skylark.mindistancetoedge) ||
(@distanceto(builtup) < skylark.mindistancetoedge)
return 0.0
end
groundcoverfactor = x -> bounds((-1/skylark.maxforagecover)*x + 1.0, max=1.0)
plantheightfactor = x -> bounds((-1/skylark.maxforageheight)*(x |> cm) + 1.0, max=1.0)
return bounds(groundcoverfactor(@cropcover()) + plantheightfactor(@cropheight()), max=1.0)
end
"""
allowsnesting(skylark, model, pos)
Check whether the given position is suitable for nesting.
"""
function allowsnesting(skylark::Skylark, model::SimulationModel, pos::Tuple{Int64,Int64})
#TODO is this condition correct? -> needs validation!
(@landcover() == grass ||
(@landcover() == agriculture &&
(skylark.nestingheight[1] <= @cropheight() <= skylark.nestingheight[2]) &&
(skylark.nestingcover[1] <= @cropcover() <= skylark.nestingcover[2])) &&
(@distanceto(forest) < skylark.mindistancetoedge) &&
(@distanceto(builtup) < skylark.mindistancetoedge))
end
"""
destroynest!(skylark, model, reason)
Remove the skylark's nest and offspring due to disturbance or predation.
"""
function destroynest!(self::Skylark, model::SimulationModel, reason::String)
for c in self.clutch
@record("mortality", [model.date, "Skylark", reason])
end
self.nest = ()
self.clutch = 0
@debug("$(animalid(self)) had her nest destroyed by $reason.")
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)"
# set species parameters from the configs
self.minimumterritory = @param(nature.skylark_minimumterritory)*1m²
self.movementrange = @param(nature.skylark_movementrange)*1m
self.mindistancetoedge = @param(nature.skylark_mindistancetoedge)*1m
self.maxforageheight = @param(nature.skylark_maxforageheight)*1cm
self.maxforagecover = @param(nature.skylark_maxforagecover)
self.nestingheight = (@param(nature.skylark_minnestingheight)*1cm,
@param(nature.skylark_maxnestingheight)*1cm)
self.nestingcover = (@param(nature.skylark_minnestingcover),
@param(nature.skylark_maxnestingcover))
self.firstyearmortality = @param(nature.skylark_firstyearmortality)
self.migrationmortality = @param(nature.skylark_migrationmortality)
self.matefaithfulness = @param(nature.skylark_matefaithfulness)
# calculate migration dates
#XXX migration dates should be temperature-dependent and dynamic
arrive = @randn(self.migrationarrival[1]:self.migrationarrival[2])
depart = @randn(self.migrationdeparture[1]:self.migrationdeparture[2])
if self.sex == female
arrive += self.migrationdelayfemales
depart += self.migrationdelayfemales
end
self.migrationdates = (arrive, depart)
if model.date < arrive || model.date >= depart
self.sex == male ?
@setphase(territorysearch) :
@setphase(matesearch)
@migrate(arrive)
end
self.firstnest = @randn(self.nestingbegin[1]:self.nestingbegin[2])
end
"""
Initialise the skylark population. Creates pairs of skylarks on grassland and agricultural
land, keeping a distance of 60m to vertical structures and giving each pair an area of 3ha.
"""
@populate Skylark begin
# initialise on open land, at least 60m from vertical structures
habitat = @habitat((@landcover() == grass || @landcover() == agriculture) &&
@distanceto(forest) >= @param(nature.skylark_mindistancetoedge)*1m &&
@distanceto(builtup) >= @param(nature.skylark_mindistancetoedge)*1m)
initphase = nonbreeding
birthphase = nonbreeding
indarea = @param(nature.skylark_initialdensity)*1ha
sex = :pairs
end