Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
individuals.jl 7.14 KiB
### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
###
### This file contains life-history and other ecological functions that apply to
### all animal individuals, such reproduction, death, and movement.
###

"""
    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)).birthphase
        child = typeof(animal)(length(model.animals)+1, sex, (animal.id, mate), animal.pos, bphase)
        push!(model.animals, child)
        push!(animal.offspring, child.id)
        mate > 0 && push!(model.animals[mate].offspring, child.id)
        push!(model.landscape[child.pos...].animals, child.id)
        create!(child, model)
    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
        # remove the animal's location and territory pointers from the landscape
        filter!(x -> x!=animal.id, model.landscape[animal.pos...].animals)
        for pos in animal.territory
            filter!(x -> x!=animal.id, model.landscape[pos...].territories)
        end
        # print the epitaph and remove the animal from the model
        postfix = isempty(cause) ? "." : " from $cause."
        @debug "$(animalid(animal)) has died$(postfix)"
        @record("mortality", [model.date, speciesof(animal), cause])
        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::AnnualDate)
    # 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
    filter!(x -> x!=animal.id, model.landscape[animal.pos...].animals)
    model.animals[animal.id] = nothing
    @debug "$(animalid(animal)) has migrated."
end

"""
    occupy!(animal, model, position)

Add the given location to the animal's territory. Returns `true` if successful
(i.e. if the location was not already occupied by a conspecific), `false` if not.
"""
function occupy!(animal::Animal, model::SimulationModel, position::Tuple{Int64,Int64})
    if isoccupied(model, speciesof(animal), position) #XXX should this be an error?
        @warn "Position $position is already occupied by a $(speciesof(animal))." animal
        return false
    else
        push!(animal.territory, position)
        push!(model.landscape[position...].territories, animalid(animal))
        return true
    end
end

"""
    vacate!(animal, model, position)

Remove this position from the animal's territory.
"""
function vacate!(animal::Animal, model::SimulationModel, position::Tuple{Int64,Int64})
    filter!(x -> x!=position, animal.territory)
    filter!(x -> x!=animal.id, model.landscape[position...].territories)
end

"""
    vacate!(animal, model)

Remove the animal's complete territory.
"""
function vacate!(animal::Animal, model::SimulationModel)
    for pos in animal.territory
        filter!(x -> x!=animal.id, model.landscape[pos...].territories)
    end
    animal.territory = Vector{Tuple{Int64,Int64}}()
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::Length=0m)
    #TODO test function
    dist = Int(floor(distance / @param(world.mapresolution)))
    spacing = Tuple(@rand(-dist:dist, 2))
    targetposition = safebounds(spacing .+ leader.pos, model)
    move!(follower, model, targetposition)
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, distance=1pixel)

Let the animal move a given number of steps in the given direction ("north", "northeast",
"east", "southeast", "south", "southwest", "west", "northwest", "random").
"""
function walk!(animal::Animal, model::SimulationModel, direction::String, distance::Length=-1m)
    steps = 1
    if distance > @param(world.mapresolution)
        steps = Int(floor(distance/@param(world.mapresolution)))
    end
    if direction == "north"
        shift = (0,-steps)
    elseif direction == "northeast"
        shift = (steps,-steps)
    elseif direction == "east"
        shift = (steps,0)
    elseif direction == "southeast"
        shift = (steps,steps)
    elseif direction == "south"
        shift = (0,steps)
    elseif direction == "southwest"
        shift = (-steps,steps)
    elseif direction == "west"
        shift = (-steps,0)
    elseif direction == "northwest"
        shift = (-steps,-steps)
    elseif direction == "random"
        shift = Tuple(@rand(-steps:steps, 2))
    else
        @error "Invalid direction in @walk: "*direction
    end
    walk!(animal, model, shift)
end

"""
    walk!(animal, model, direction, distance=-1)

Let the animal move in the given direction, where the direction is
defined by an (x, y) tuple to specify the shift in coordinates.
If maxdist >= 0, move no further than the specified distance.
"""
function walk!(animal::Animal, model::SimulationModel, direction::Tuple{Int64,Int64},
               maxdist::Length=-1m)
    #TODO test
    distance = Int(floor(maxdist/@param(world.mapresolution)))
    if distance >= 0
        direction[1] > distance && (direction[1] = distance)
        direction[2] > distance && (direction[2] = distance)
        direction[1] < -distance && (direction[1] = -distance)
        direction[2] < -distance && (direction[2] = -distance)
    end
    newpos = animal.pos .+ direction
    move!(animal, model, newpos)
end

#TODO add a walk function with a habitat descriptor
##TODO add walktoward or similar function (incl. pathfinding?)