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