Skip to content
Snippets Groups Projects
Select Git revision
  • 05d9bfb49fc92353343ff9ba6a49de79add8b797
  • main default protected
  • test_coef
  • 21-things-to-take-care-of-before-submission
4 results

mxl_wtp_space_NR_caseC.R

  • Code owners
    Assign users and groups as approvers for specific file changes. Learn more.
    populations.jl 9.64 KiB
    ### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
    ###
    ### This file contains functions that apply to all animal populations, such as for
    ### initialisation, or querying for neighbours.
    ###
    
    """
        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).
    
    - `initphase` determines which life phase individuals will be assigned to at model
        initialisation (required).
    
    - `birthphase` determines which life phase individuals will be assigned to at birth (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 `indarea`.
    
    - `indarea`: if this is greater than zero, it determines the habitat area allocated to each
        individual or pair. To be precise, the chance of creating an individual (or pair of
        individuals) at a suitable location is 1/indarea. Use this as an alternative to `popsize`.
    
    - `sex` determines how indidivduals are assigned a sex. If this is `:pairs` (the default),
        a male and a female will be created in each selected location. If `:random`, a male or
        a female will be created. Otherwise, `male`, `female`, or `hermaphrodite` can be chosen
        to force all individuals to be of the same sex.
    """
    @kwdef struct PopInitParams
        initphase::Function
        birthphase::Function
        habitat::Function = @habitat(true)
        popsize::Int64 = -1
        indarea::Area = -1m²
        sex::Union{Sex,Symbol} = :pairs
    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)
        species = speciestype(speciesname)
        p = populationparameters(species)
        initpopulation!(species, p, model)
    end
    
    """
        initpopulation!(speciestype, popinitparams, model)
    
    Initialise the population of the given species, based on the given initialisation parameters.
    This is an internal function called by `initpopulation!()`, and was split off from it to allow
    better testing.
    """
    function initpopulation!(species::Type, p::PopInitParams, model::SimulationModel)
        #XXX this is a pretty complicated function - can we make it simpler?
        (p.popsize > 0 && p.indarea > 0m²) && #XXX not sure what this would do
            @warn("initpopulation!() called with popsize and indarea both > 0")
        # create as many individuals as necessary in the landscape
        n = 0
        lastn = 0
        width, height = size(model.landscape)
        if p.indarea > 0m²
            pixelsperind = Int(round(p.indarea / @areaof(1)))
        end
        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.indarea <= 0m² || n == 0 || @chance(1/pixelsperind)) #XXX what if ppi==0?
                        #XXX `n==0` above guarantees that at least one individual is created, even
                        # in a landscape that is otherwise too small for the specified indarea -
                        # do we want this?
                        n += initindividuals!(species, (x,y), p, model)
                    end
                    #XXX break randomly to avoid initialising all individuals in a single column?
                    # Break if a popsize was specified and has been exceeded
                    (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 $(speciesof(species)) in the landscape."
                break
            end
            lastn = n
        end
        @info "Initialised $(n) $(speciesof(species))s."
    end
    
    """
        initindividuals!(species, pos, popinitparams, model)
    
    Initialise one or two individuals (depending on the `pairs` parameter) in the
    given location. Returns the number of created individuals. (Internal helper
    function for `initpopulation!()`.)
    """
    function initindividuals!(species::Type, pos::Tuple{Int64,Int64}, p::PopInitParams, model::SimulationModel)
        if p.sex == :pairs
            a1 = species(length(model.animals)+1, male, (-1, -1), pos, p.initphase)
            a2 = species(length(model.animals)+2, female, (-1, -1), pos, p.initphase)
            push!(model.animals, a1, a2)
            push!(model.landscape[pos...].animals, a1.id, a2.id)
            create!(a1, model)
            create!(a2, model)
            return 2
        else
            sex = p.sex
            sex == :random && (sex = @rand([male, female]))
            !(sex isa Sex) && @error("Invalid `sex` argument passed to `@populate`: $(p.sex)")
            a = species(length(model.animals)+1, sex, (-1, -1), pos, p.initphase)
            push!(model.animals, a)
            push!(model.landscape[pos...].animals, a.id)
            create!(a, model)
            return 1
        end
    end
    
    #XXX initpopulation with dispersal from an original source?
    #XXX initpopulation based on known occurences in real-life?
    
    """
        isoccupied(model, position, species)
    
    Test whether this location is part of the territory of an animal of the given species.
    """
    function isoccupied(model::SimulationModel, species::String, position::Tuple{Int64,Int64})
        for id in model.landscape[position...].territories
            occursin(species, id) && return true
        end
        return false
    end
    
    """
        territorysize(animal, model, stripunits=false)
    
    Calculate the size of this animal's territory in the given unit. If `stripunits` is true,
    return the size as a plain number.
    """
    function territorysize(a::Union{Animal,Int64}, model::SimulationModel,
                           units::Unitful.Units=ha, stripunits::Bool=false)
        a isa Int && (a = @animal(a))
        size = length(a.territory) * @areaof(1) |> Float64
        stripunits ? ustrip(units, size) : size |> units
    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::Length)
        ids = []
        msize = size(model.landscape)
        radius = Int(floor(radius / @param(world.mapresolution)))
        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::Length=0m, species="") #XXX add type for species
        neighbours = nearby_ids(pos, model, radius)
        isempty(neighbours) && return neighbours
        if isempty(species)
            model.animals[neighbours]
        else
            filter(x -> speciesof(x) == species, model.animals[neighbours])
        end
    end
    
    """
        countanimals(pos, model; radius=0, species="")
    
    Return the number of animals in the given radius around this position, optionally filtering
    by species.
    """
    function countanimals(pos::Tuple{Int64,Int64}, model::SimulationModel;
                          radius::Length=0m, species="") #XXX add type for species
        length(nearby_animals(pos, model, radius=radius, species=species))
    end
    
    """
        neighbours(animal, model, radius=0, conspecifics=true)
    
    Return a list of animals in the given radius around this animal, excluding itself. By default,
    only return conspecific animals.
    """
    function neighbours(animal::Animal, model::SimulationModel,
                        radius::Length=0m, conspecifics::Bool=true)
        filter(a -> a.id != animal.id,
               nearby_animals(animal.pos, model, radius = radius,
                              species = conspecifics ? speciesof(animal) : ""))
    end
    
    """
        directionto(pos, model, animal)
    
    Calculate the direction from the given position to the animal.
    """
    function directionto(pos::Tuple{Int64,Int64}, model::SimulationModel, animal::Animal)
        # have to use a coordinate as first argument rather than an animal because of @directionto
        animal.pos - pos
    end
    
    """
        distanceto(pos, model, animal)
    
    Calculate the distance from the given position to the animal.
    """
    function distanceto(pos::Tuple{Int64,Int64}, model::SimulationModel, animal::Animal)
        # have to use a coordinate as first argument rather than an animal because of @distanceto
        #XXX this is very imprecise because diagonal distances are not calculated trigonometrically
        maximum(abs.(animal.pos .- pos)) * @param(world.mapresolution)
    end