Skip to content
Snippets Groups Projects
Select Git revision
  • c587740215262e6dbbcfff6f5f71ec19de186b63
  • master default protected
  • development
  • marco-development
  • fix-missing-weatherdata
  • fix-eichsfeld-weather
  • marco/dev-aquacrop
  • 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
24 results

populations.jl

Blame
  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    populations.jl 11.17 KiB
    ### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
    ###
    ### This file contains a set of utility functions for species, including initialisation,
    ### reproduction, and mortality.
    ###
    
    """
        PopInitParams
    
    A set of parameters used by [`initpopulation`](@ref) to initialise the population
    of a species at the start of a simulation. Define these parameters for each species
    using [`@populate`](@ref).
    
    - `phase` determines which life phase individuals will be assigned to (required).
    
    - `habitat` is a function that determines whether a given location is suitable
        or not (create this using [`@habitat`](@ref)). By default, every cell will be occupied.
    
    - `popsize` determines the number of individuals that will be created, dispersed over the
        suitable locations in the landscape. If this is zero or negative, one individual will
        be created in every suitable location. If it is greater than the number of suitable
        locations, multiple individuals will be created per location. Alternately, use `popdensity`.
    
    - `popdensity`: if this is greater than zero, the chance of creating an individual (or
        pair of individuals) at a suitable location is 1/popdensity. Use this as an alternative
        to `popsize`.
    
    - If `pairs` is true, a male and a female individual will be created in each selected
        location, otherwise, only one individual will be created at a time. (default: false)
    
    - If `asexual` is true, all created individuals are assigned the sex `hermaphrodite`,
        otherwise, they are randomly assigned `male` or `female`. If `pairs` is true, `asexual`
        is ignored. (default: false)
    """
    @kwdef struct PopInitParams
        phase::Function
        habitat::Function = @habitat(true)
        popsize::Int64 = -1
        popdensity::Int64 = -1
        pairs::Bool = false
        asexual::Bool = false
    end
    
    """
        populationparameters(type)
    
    A function that returns a [`PopInitParams`](@ref) object for the given species type.
    Parametric methods for each species are defined with [`@populate`](@ref). This is the
    catch-all method, which throws an error if no species-specific function is defined.
    """
    populationparameters(t::Type) = @error("No population parameters defined for $(t), use @populate.")
    
    """
        initpopulation!(speciesname, model)
    
    Initialise the population of the given species, based on the parameters stored
    in [`PopInitParams`](@ref). Define these using [`@populate`](@ref).
    """
    function initpopulation!(speciesname::String, model::SimulationModel)
        # get the PopInitParams and check for validity
        species = speciestype(speciesname)
        p = populationparameters(species)
        (p.popsize <= 0 && p.popdensity <= 0) && #XXX not sure what this would do
            @warn("initpopulation() called with popsize and popdensity both <= 0")
        (p.popsize > 0 && p.popdensity > 0) && #XXX not sure what this would do
            @warn("initpopulation() called with popsize and popdensity both > 0")
        # create as many individuals as necessary in the landscape
        n = 0
        lastn = 0
        width, height = size(model.landscape)
        while n == 0 || n < p.popsize
            for x in @shuffle!(Vector(1:width))
                for y in @shuffle!(Vector(1:height))
                    if p.habitat((x,y), model) &&
                        (p.popdensity <= 0 || @chance(1/p.popdensity)) #XXX what if pd==0?
                        if p.pairs
                            a1 = species(length(model.animals)+1, male, (-1, -1), (x,y), p.phase)
                            a2 = species(length(model.animals)+1, female, (-1, -1), (x,y), p.phase)
                            create!(a1, model)
                            create!(a2, model)
                            push!(model.animals, a1, a2)
                            push!(model.landscape[x,y].animals, a1.id, a2.id)
                            n += 2
                        else
                            sex = p.asexual ? hermaphrodite : @rand([male, female])
                            a = species(length(model.animals)+1, sex, (-1, -1), (x,y), p.phase)
                            create!(a, model)
                            push!(model.animals, a)
                            push!(model.landscape[x,y].animals, a.id)
                            n += 1
                        end
                    end
                    #XXX break randomly to avoid initialising all individuals in a single column?
                    (p.popsize > 0 && n >= p.popsize) && break
                end
                (p.popsize > 0 && n >= p.popsize) && break
            end
            if lastn == n # prevent an infinite loop - we don't have a Cray...
                @warn "There are not enough suitable locations for $(species) in the landscape."
                break
            end
            lastn = n
        end
        @info "Initialised $(n) $(species)s."
    end
    
    #XXX initpopulation with dispersal from an original source?
    #XXX initpopulation based on known occurences in real-life?
    
    """
        reproduce!(animal, model, mate, n=1)
    
    Produce one or more offspring for the given animal at its current location.
    The `mate` argument gives the ID of the reproductive partner.
    """
    function reproduce!(animal::Animal, model::SimulationModel,
                        n::Int64=1, mate::Int64=-1)
        (animal.sex == male) && @warn "Male $(animalid(animal)) is reproducing."
        for i in 1:n
            if animal.sex == hermaphrodite
                sex = hermaphrodite
            else
                sex = @rand([male, female])
            end
            bphase = populationparameters(typeof(animal)).phase
            child = typeof(animal)(length(model.animals)+1, sex, (animal.id, mate), animal.pos, bphase)
            create!(child, model)
            push!(model.animals, child)
            push!(animal.offspring, child.id)
            mate > 0 && push!(models.animals[mate].offspring, child.id)
        end
        @debug "$(animalid(animal)) has reproduced."
    end
    
    """
        kill(animal, model, probability=1.0, cause="")
    
    Kill this animal, optionally with a given percentage probability.
    Returns true if the animal dies, false if not.
    """
    function kill!(animal::Animal, model::SimulationModel,
                   probability::Float64=1.0, cause::String="")
        if @rand() < probability
            postfix = isempty(cause) ? "." : " from $cause."
            @debug "$(animalid(animal)) has died$(postfix)"
            filter!(x -> x!=animal.id, model.landscape[animal.pos...].animals)
            model.animals[animal.id] = nothing
            return true
        end
        return false
    end
    
    """
        migrate!(animal, model, arrival)
    
    Remove this animal from the map and add it to the migrant species pool.
    It will be returned to its current location at the specified `arrival` date.
    """
    function migrate!(animal::Animal, model::SimulationModel, arrival::Date)
        # keep model.migrants sorted by inserting the new migrant after migrants
        # that will return earlier than it
        i = findfirst(m -> m.second >= arrival, model.migrants)
        if isnothing(i)
            push!(model.migrants, Pair(animal, arrival))
        else
            insert!(model.migrants, i, Pair(animal, arrival))
        end
        model.animals[animal.id] = nothing
        @debug "$(animalid(animal)) has migrated."
    end
    
    """
       isalive(id, model)
    
    Test whether the animal with the given ID is still alive.
    """
    function isalive(animalid::Int64, model::SimulationModel)
        !isnothing(model.animals[animalid]) || any(m->m.first.id==animalid, model.migrants)
    end
    
    """
        nearby_ids(pos, model, radius)
    
    Return a list of IDs of the animals within a given radius of the position.
    """
    function nearby_ids(pos::Tuple{Int64,Int64}, model::SimulationModel, radius::Int64)
        ids = []
        msize = size(model.landscape)
        for x in (pos[1]-radius):(pos[1]+radius)
            (x < 1 || x > msize[1]) && continue
            for y in (pos[2]-radius):(pos[2]+radius)
                (y < 1 || y > msize[2]) && continue
                append!(ids, model.landscape[x,y].animals)
            end
        end
        ids
    end
    
    
    """
        nearby_animals(pos, model; radius= 0, species="")
    
    Return a list of animals in the given radius around this position, optionally filtering by species.
    """
    function nearby_animals(pos::Tuple{Int64,Int64}, model::SimulationModel;
                            radius::Int64=0, species="")
        neighbours = nearby_ids(pos, model, radius)
        isempty(neighbours) && return neighbours
        if species == ""
            model.animals[neighbours]
        else
            filter(x -> speciesof(x) == species, model.animals[neighbours])
        end
    end
    
    """
        neighbours(animal, model; radius=0)
    
    Return a list of conspecific animals in the given radius around this animal, excluding itself.
    """
    function neighbours(animal::Animal, model::SimulationModel; radius::Int64=0)
        filter(a -> a.id != animal.id,
               nearby_animals(animal.pos, model, radius=radius, species=speciesof(animal)))
    end
    
    """
        countanimals(pos, model; radius= 0, species="")
    
    Count the number of animals in this location (optionally supplying a species name and radius).
    """
    function countanimals(pos::Tuple{Int64,Int64}, model::SimulationModel;
                          radius::Int64=0, species="")
        #XXX do I want/need this?
        length(nearby_animals(pos, model, radius=radius, species=species))
    end
    
    """
        followanimal!(follower, leader, model, distance=0)
    
    Move the follower animal to a location near the leading animal.
    """
    function followanimal!(follower::Animal, leader::Animal, model::SimulationModel,
                           distance::Int64=0)
        #TODO test function
        spacing = Tuple(@rand(-distance:distance, 2))
        targetposition = safebounds(spacing .+ leader.pos, model)
        move_agent!(follower, targetposition, model)
    end
    
    """
        move!(animal, model, position)
    
    Move the animal to the given position, making sure that this is in-bounds.
    If the position is out of bounds, the animal stops at the map edge.
    """
    function move!(animal::Animal, model::SimulationModel, position::Tuple{Int64,Int64})
        #XXX should this function give some sort of warning (e.g. return false)
        # if the original position is not reachable?
        filter!(x -> x != animal.id, model.landscape[animal.pos...].animals)
        animal.pos = safebounds(position, model)
        push!(model.landscape[animal.pos...].animals, animal.id)
    end
    
    """
        walk!(animal, model, direction)
    
    Let the animal move one step in the given direction ("north", "northeast",
    "east", "southeast", "south", "southwest", "west", "northwest", "random").
    """
    function walk!(animal::Animal, model::SimulationModel, direction::String)
        if direction == "north"
            shift = (0,-1)
        elseif direction == "northeast"
            shift = (1,-1)
        elseif direction == "east"
            shift = (1,0)
        elseif direction == "southeast"
            shift = (1,1)
        elseif direction == "south"
            shift = (0,1)
        elseif direction == "southwest"
            shift = (-1,1)
        elseif direction == "west"
            shift = (-1,0)
        elseif direction == "northwest"
            shift = (-1,-1)
        elseif direction == "random"
            shift = Tuple(@rand([-1,1], 2))
        else
            @error "Invalid direction in @walk: "*direction
        end
        move!(animal, model, animal.pos .+ shift)
    end
    
    """
        walk!(animal, model, direction)
    
    Let the animal move in the given direction, where the direction is
    defined by an (x, y) tuple to specify the shift in coordinates.
    """
    function walk!(animal::Animal, model::SimulationModel, direction::Tuple{Int64,Int64})
        move!(animal, model, animal.pos .+ direction)
    end
    
    
    
    ##TODO add random walk with habitat descriptor
    ##TODO add walktoward or similar function (incl. pathfinding?)