Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
skylark.jl 10.46 KiB
### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
###
### This file holds the code for the Eurasian Skylark (Alauda arvensis).
###

"""
    Skylark

*Alauda arvensis* is a common and charismatic species of agricultural landscapes.
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.
      ISBN 3-89104-019-9
"""
@species Skylark begin
    
    eggtime = 11 # 11 days from laying to hatching
    eggpredationmortality = 0.03 # per-day egg mortality from predation
    nestharvestmortality = 0.9 # egg mortality after a harvest event (XXX guess)

    nestlingtime = 7:11 # 7-11 days from hatching to leaving nest
    nestlingpredationmortality = 0.03 # per-day nestling mortality from predation

    fledglingtime = 25:30 # 25-30 days from hatching to independence
    fledglingharvestmortality = 0.5 # fledgling mortality after harvest
    fledglingpredationmortality = 0.01 # per-day fledgling mortality from predation
    firstyearmortality = 0.38 # total mortality in the first year after independence
    
    migrationdates = () # is defined by each individual in `initskylark()`
    migrationmortality = 0.33 # chance of dying during the winter

    mate = -1 # the agent ID of the mate (-1 if none)
    nest = () # coordinates of current nest
    nestingbegin = (4, 10) # begin nesting in the middle of April
    nestbuildingtime = 4:5 # 4-5 days needed to build a nest (doubled for first nest)
    nestcompletion = 0 # days left until the nest is built
    eggsperclutch = 2:5 # 2-5 eggs laid per clutch
    clutch = [] # IDs of offspring in current clutch
    breedingdelay = 18 # wait 18 days after hatching to start a new brood
    
    habitats = @habitat((@landcover() == grass ||
                         # settle on grass or arable land (but not maize)
                         (@landcover() == agriculture && @cropname() != "maize")) &&
                        @distancetoedge() > 5) # at least 50m from other habitats
                        #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
    
    @initialise(habitats, phase="mating", popdensity=3000, pairs=true,
                initfunction=initskylark)
    
    """
    As an egg, simply check for mortality and hatching.
    """
    @phase egg begin
        @kill(@trait(eggpredationmortality), "predation")
        @respond(harvesting, @kill(@trait(nestharvestmortality), "harvest"))

        if @trait(age) == @trait(eggtime)
            @setphase(nestling)
        end
    end

    """
    As a nestling, simply check for mortality and fledging.
    """
    @phase nestling begin
        #TODO add feeding & growth
        @kill(@trait(nestlingpredationmortality), "predation")
        @respond(harvesting, @kill(@trait(nestharvestmortality), "harvest"))
        if @trait(age) == @trait(nestlingtime)+@trait(eggtime)
            @setphase(fledgling)
        end
    end

    """
    As a fledgling, move around a little, but mainly wait for maturity and
    check mortality.
    """
    @phase fledgling begin
        #TODO add feeding & growth
        @kill(@trait(fledglingpredationmortality), "predation")
        @randomwalk(1) #TODO add movement following the parents
        if @trait(age) == @trait(fledglingtime)+@trait(eggtime)
            @kill(@trait(firstyearmortality), "first year mortality") #XXX mechanistic?
            @setphase(nonbreeding)
        end
    end

    """
    As a non-breeding adult, move around with other individuals and check for migration.
    """
    @phase nonbreeding begin
        # flocking behaviour - follow a random neighbour or move randomly
        #TODO add feeding and mortality, respect habitat when moving
        neighbours = map(a->a.id, @neighbours(10)) #FIXME
        #isempty(neighbours) ? @randomwalk(5) : @follow(@rand(neighbours), 2)
        if isempty(neighbours)
            @randomwalk(5)
        else
            @follow(model[@rand(neighbours)], 2)
        end
        # check if the bird migrates
        leave, arrive = animal.migrationdates
        m, d = monthday(model.date)
        migrate = (((m < arrive[1]) || (m == arrive[1] && d < arrive[2])) ||
                   ((m > leave[1]) || (m == leave[1] && d >= leave[2])))
        if migrate #FIXME not all migrate?
            @kill(@trait(migrationmortality), "migration")
            returndate = Date(year(model.date), arrive[1], arrive[2])
            model.date != @param(core.startdate) && (returndate += Year(1))
            @setphase(mating)
            @migrate(returndate)
        end
    end

    """
    Move around until a mate is found.
    """
    @phase mating begin
        # if we've found a mate, wait for nesting begin and then go to the next phase
        if @trait(mate) != -1
            if !@isalive(@trait(mate))
                @trait(mate) = -1
                return
            end
            m, d = monthday(model.date)
            nest = ((m == @trait(nestingbegin)[1] && d >= @trait(nestingbegin)[2]
                     && @chance(0.05)) || (m > @trait(nestingbegin)[1]))
            nest && @setphase(nestbuilding)
            return
        end
        # look for a mate among the neighbouring birds, or move randomly
        for n in @neighbours(50)
            if n.sex != @trait(sex) && n.phase == "mating" && n.mate == -1
                @trait(mate) = n.id
                n.mate = @trait(id)
                @debug "$(animalid(animal)) and $(animalid(n)) have mated."
                return
            end
        end
        @randomwalk(10)
    end

    """
    Females select a location and build a nest. Males do nothing. (Sound familiar?)
    """
    @phase nestbuilding begin
        if !@isalive(@trait(mate))
            @setphase(nonbreeding)
            return
        end
        if @trait(sex) == female
            if isempty(@trait(nest))
                # try to find a nest in the neighbourhood, or move on
                nestlocation = @randompixel(10, @trait(habitats))
                if isnothing(nestlocation)
                    @randomwalk(20)
                else
                    # if we've found a location, start the clock on the building time
                    # (building time doubles for the first nest of the year)
                    @trait(nest) = nestlocation
                    @trait(nestcompletion) = @rand(nestbuildingtime)
                    month(model.date) == 4 && (@trait(nestcompletion) *= 2)
                    @debug "$(animalid(animal)) is building a nest."
                end
            else
                # wait while nest is being built, then lay eggs and go to next phase
                if @trait(nestcompletion) > 0
                    @trait(nestcompletion) -= 1
                else
                    #XXX more accurately, a female lays one egg per day, not all at once
                    @trait(clutch) = @reproduce(@trait(mate), @rand(eggsperclutch))
                    @animal(@trait(mate)).clutch = @trait(clutch)
                    for c in @trait(clutch) #FIXME find a cleaner solution for this
                        initskylark(@animal(c), model)
                    end
                    @setphase(breeding)
                end
            end
        else
            # males stay near the female
            @follow(model[@trait(mate)], 5)
            @animal(@trait(mate)).phase == "breeding" && @setphase(breeding)
        end
    end

    """
    Do lots of foraging (not yet implemented).
    """
    @phase breeding begin
        #TODO forage (move random)
        for offspring in @trait(clutch)
            # check if offspring are still alive and juvenile, else remove from clutch
            if !@isalive(offspring) || @animal(offspring).phase == "nonbreeding"
                deleteat!(@trait(clutch), findfirst(x->x==offspring, @trait(clutch)))
            end
        end
        # if all young have fledged, move to nonbreeding (if it's July) or breed again
        if isempty(@trait(clutch))
            @trait(nest) = ()
            month(model.date) >= 7 ? @setphase(nonbreeding) : @setphase(nestbuilding)
        end
    end
end

"""
    initskylark(skylark, model)

Initialise a skylark individual. Selects migration dates and checks if the
bird should currently be on migration. Also sets other individual-specific
variables. Called at model initialisation and when an egg is laid.
"""
function initskylark(animal::Animal, model::AgentBasedModel)
    @debug "Added $(animalid(animal)) at $(animal.pos)"
    # calculate migration dates for this individual
    animal.migrationdates = migrationdates(animal, model)
    leave, arrive = animal.migrationdates
    m, d = monthday(model.date)
    migrate = (((m < arrive[1]) || (m == arrive[1] && d < arrive[2])) ||
               ((m > leave[1]) || (m == leave[1] && d >= leave[2])))
    if migrate
        returndate = Date(year(model.date), arrive[1], arrive[2])
        model.date != @param(core.startdate) && (returndate += Year(1))
        @migrate(returndate)
    end
    # set individual life-history parameters that are defined as ranges for the species
    @trait(nestlingtime) = @rand(@trait(nestlingtime)) #FIXME no effect?
    @trait(fledglingtime) = @rand(@trait(fledglingtime))
    #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)
    #TODO this ought to be temperature-dependent and dynamic
    #XXX magic numbers!
    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(2001, minleave[1], minleave[2]) + Day(deltaleave))
    arrive = monthday(Date(2001, minarrive[1], minarrive[2]) + Day(deltaarrive))
    (leave, arrive)
end