From 74d43c052b5f85d0beaad2a1ada4163b2559ee37 Mon Sep 17 00:00:00 2001 From: Daniel Vedder <daniel.vedder@idiv.de> Date: Wed, 17 Jul 2024 10:49:23 +0200 Subject: [PATCH] Moved some functions from populations.jl to new file individuals.jl --- src/Persefone.jl | 1 + src/nature/individuals.jl | 194 +++++++++++++++++++++++++++++++++++++ src/nature/populations.jl | 195 +------------------------------------- 3 files changed, 197 insertions(+), 193 deletions(-) create mode 100644 src/nature/individuals.jl diff --git a/src/Persefone.jl b/src/Persefone.jl index 6c4df60..2c4e0b6 100644 --- a/src/Persefone.jl +++ b/src/Persefone.jl @@ -141,6 +141,7 @@ include("nature/energy.jl") include("nature/nature.jl") include("nature/macros.jl") include("nature/populations.jl") +include("nature/individuals.jl") include("nature/ecologicaldata.jl") include("nature/species/skylark.jl") include("nature/species/wolpertinger.jl") diff --git a/src/nature/individuals.jl b/src/nature/individuals.jl new file mode 100644 index 0000000..863bb8f --- /dev/null +++ b/src/nature/individuals.jl @@ -0,0 +1,194 @@ +### 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 + #TODO add DEB? + 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)" + 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 + 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. +""" +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))." + end + push!(animal.territories, position) + push!(model.landscape[position...], animal.id) +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) + distance < 0m ? steps = 1 : steps = Int(floor(distance/@param(world.mapresolution))) + 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,0,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?) diff --git a/src/nature/populations.jl b/src/nature/populations.jl index 1b2799a..e47bcd2 100644 --- a/src/nature/populations.jl +++ b/src/nature/populations.jl @@ -1,11 +1,9 @@ ### 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. +### This file contains functions that apply to all animal populations, such as for +### initialisation, or querying for neighbours. ### -##TODO move the life-history functions into a new file `individuals.jl` - """ PopInitParams @@ -141,89 +139,6 @@ 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)).birthphase - #TODO add DEB? - 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)" - 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 - 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. -""" -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))." - end - push!(animal.territories, position) - push!(model.landscape[position...], animal.id) -end - """ isoccupied(model, position, species) @@ -236,28 +151,6 @@ function isoccupied(model::SimulationModel, species::String, position::Tuple{Int return false 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 - """ isalive(id, model) @@ -346,87 +239,3 @@ function distanceto(pos::Tuple{Int64,Int64}, model::SimulationModel, animal::Ani #XXX this is very imprecise because diagonal distances are not calculated trigonometrically maximum(abs.(animal.pos .- pos)) * @param(world.mapresolution) 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) - distance < 0m ? steps = 1 : steps = Int(floor(distance/@param(world.mapresolution))) - 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,0,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?) -- GitLab