### 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. ### """ initpopulation(habitatdescriptor; popsize=-1, pairs=false, asexual=false) Creates a function that initialises individuals at random locations across the landscape. This can be used to create the `initialise!` variable in a species definition block. - `habitatdescriptor` is a function that determines whether a given location is suitable or not (create this using [`@habitat`](@ref)). - `phase` determines which life phase individuals will be assigned to. If this is `nothing`, the species' default post-natal life stage will be used (although note that this is probably not what you want). - `popsize` determines the number of individuals that will be created. If this is zero or negative, one individual will be created in every suitable location in the landscape. If `popsize` is greater than the number of suitable locations, multiple individuals will be created in one place. (Maximum population density can be set in the habitat descriptor using the [`@countanimals`](@ref) macro.) - 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. - If `asexual` is true, all created individuals are assigned the sex `hermaphrodite`, otherwise, they are randomly assigned male of female. (If `pairs` is true, `asexual` is ignored.) """ function initpopulation(habitatdescriptor::Function; phase::Union{String,Nothing}=nothing, popsize::Int64=-1, pairs::Bool=false, asexual::Bool=false) #TODO add a `popdensity` argument function(species::Dict{String,Any}, model::AgentBasedModel) n = 0 lastn = 0 specname = species["name"] (!isnothing(phase)) && (species["phase"] = phase) width, height = size(model.landscape) while n == 0 || n < popsize for x in @shuffle!(Vector(1:width)) for y in @shuffle!(Vector(1:height)) if habitatdescriptor((x,y), model) if pairs add_agent!((x,y), Animal, model, deepcopy(species), female, 0) add_agent!((x,y), Animal, model, deepcopy(species), male, 0) n += 2 else sex = asexual ? hermaphrodite : @rand([male, female]) add_agent!((x,y), Animal, model, deepcopy(species), sex, 0) n += 1 end end (popsize > 0 && n >= popsize) && break end (popsize > 0 && n >= popsize) && break end if lastn == n # prevent an infinite loop - we don't have a Cray... @warn "There are not enough suitable locations for $(specname) in the landscape." break end lastn = n end @info "Initialised $(n) $(specname)s." end end """ initrandompopulation(popsize; kwargs...) A simplified version of [`initpopulation`](@ref). Creates a function that initialises `popsize` individuals, spread at random across the landscape. """ function initrandompopulation(popsize::Int64; kwargs...) #XXX How should this be called if users are supposed to use @initialise? initpopulation(@habitat(true); popsize=popsize, kwargs...) end #XXX initpopulation with dispersal from an original source? #XXX initpopulation based on known occurences in real-life? """ reproduce!(animal, model, n=1) Produce one or more offspring for the given animal at its current location. """ function reproduce!(animal::Animal, model::AgentBasedModel, n::Int64=1) for i in 1:n sex = (animal.sex == hermaphrodite) ? hermaphrodite : @rand([male, female]) # We need to generate a fresh species dict here species = @eval $(Symbol(animal.traits["name"]))($model) add_agent!(animal.pos, Animal, model, species, sex, 0) 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::AgentBasedModel, probability::Float64=1.0, cause::String="") if @rand() < probability postfix = isempty(cause) ? "." : " from $cause." @debug "$(animalid(animal)) has died$(postfix)" kill_agent!(animal, model) return true end return false end """ nearby_animals(pos, model, radius) Return an iterator over all animals in the given radius around this position. """ function nearby_animals(pos::Tuple{Int64,Int64}, model::AgentBasedModel, radius::Int64) #TODO enable filtering by species neighbours = (model[id] for id in nearby_ids(pos, model, radius)) Iterators.filter(a -> typeof(a) == Animal, neighbours) end """ nearby_animals(animal, model, radius) Return an iterator over all animals in the given radius around this animal, excluding itself. """ function nearby_animals(animal::Animal, model::AgentBasedModel, radius::Int64) #TODO enable filtering by species neighbours = (model[id] for id in nearby_ids(animal.pos, model, radius)) Iterators.filter(a -> typeof(a) == Animal && a.id != animal.id, neighbours) end """ countanimals(pos, model; species="", radius=0) Count the number of animals in this location (optionally supplying a species name and radius). """ function countanimals(pos::Tuple{Int64,Int64}, model::AgentBasedModel; species::String="", radius::Int64=0) n = 0 #XXX can we ignore capitalisation in the spelling of `species`? for a in nearby_animals(pos, model, radius) (species == "" || a.traits["name"] == species) && (n += 1) end return n end ##TODO add movement functions