Skip to content
Snippets Groups Projects
Select Git revision
  • 290411d83801f8ca89d2120f41bbfe7b6116e906
  • master default protected
  • development
  • precompile-statements
  • precompile-tools
  • tmp-faster-loading
  • skylark
  • testsuite
  • code-review
  • v0.7.0
  • v0.6.1
  • v0.6.0
  • v0.5.5
  • v0.5.4
  • v0.5.3
  • v0.5.2
  • v0.2
  • v0.3.0
  • v0.4.1
  • v0.5
20 results

crop_data.csv

Blame
  • 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