From c587740215262e6dbbcfff6f5f71ec19de186b63 Mon Sep 17 00:00:00 2001
From: Daniel Vedder <daniel.vedder@idiv.de>
Date: Thu, 16 May 2024 12:22:14 +0200
Subject: [PATCH] Reimplemented neighbour-finding functions

This used to be done by Agents.jl, is now done natively
---
 src/nature/macros.jl               |  25 +++---
 src/nature/nature.jl               |  19 ++++-
 src/nature/populations.jl          | 118 ++++++++++++++++-------------
 src/nature/species/wolpertinger.jl |  12 +--
 src/world/landscape.jl             |   5 +-
 5 files changed, 105 insertions(+), 74 deletions(-)

diff --git a/src/nature/macros.jl b/src/nature/macros.jl
index c4be686..5d6dcd6 100644
--- a/src/nature/macros.jl
+++ b/src/nature/macros.jl
@@ -77,7 +77,6 @@ Set the parameters that are used to initialise this species' population.
 For parameter options, see [`PopInitParams`](@ref).
 """
 macro populate(species, params)
-    println("Will register $(species).")
     quote
         # convert the macro body to a dict
         Core.@__doc__ fun = function() # metaprogramming is great - this is a fun fun function ;-)
@@ -232,17 +231,6 @@ macro migrate(arrival)
     :(migrate!($(esc(:self)), $(esc(:model)), $(esc(arrival))))
 end
 
-"""
-    @neighbours(radius)
-
-Return an iterator over all animals in the given radius around this animal, excluding itself.
-This can only be used nested within [`@phase`](@ref).
-"""
-macro neighbours(radius)
-    #TODO enable filtering by species
-    :(nearby_animals($(esc(:self)), $(esc(:model)), $(esc(radius))))
-end
-
 """
     @habitat
 
@@ -356,13 +344,24 @@ macro randompixel(args...)
 end
 
 """
-    @countanimals(species="", radius=0)
+    @neighbours(radius=0)
+
+Return an iterator over all conspecific animals in the given radius around this animal,
+excluding itself. This can only be used nested within [`@phase`](@ref).
+"""
+macro neighbours(args...)
+    :(neighbours($(esc(:self)), $(esc(:model)); $(map(esc, args)...)))
+end
+
+"""
+    @countanimals(radius=0, species="")
 
 Count the number of animals of the given species in this location.
 This is a utility wrapper that can only be used nested within
 [`@phase`](@ref) or [`@habitat`](@ref).
 """
 macro countanimals(args...)
+    #XXX do I want/need this?
     :(countanimals($(esc(:pos)), $(esc(:model)); $(map(esc, args)...)))
 end
 
diff --git a/src/nature/nature.jl b/src/nature/nature.jl
index b1aeb9e..11975b6 100644
--- a/src/nature/nature.jl
+++ b/src/nature/nature.jl
@@ -33,7 +33,24 @@ abstract type Animal <: ModelAgent end
 Return the species name of this animal as a string.
 """
 function speciesof(a::Animal)
-    string(typeof(a))
+    # strip out the module name if necessary (`Persefone.<species>`)
+    spstrings = split(string(typeof(a)), ".")
+    length(spstrings) == 1 ? spstrings[1] : spstrings[2]
+end
+
+"""
+    speciestype(animal)
+
+Return the Type of this species.
+"""
+function speciestype(species::String)
+    # get the species Type from its namestring by looking in the module namespace
+    speciestype = nothing
+    for name in names(Persefone, all=true)
+        string(name) == species && (speciestype = getfield(Persefone, name))
+    end
+    isnothing(speciestype) && @error("Species $species is not defined.")
+    speciestype
 end
     
 """
diff --git a/src/nature/populations.jl b/src/nature/populations.jl
index 75a80c0..e7a8818 100644
--- a/src/nature/populations.jl
+++ b/src/nature/populations.jl
@@ -41,25 +41,25 @@ using [`@populate`](@ref).
     asexual::Bool = false
 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!(species, model)
+    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!(species::String, model::SimulationModel)
-    # get the species Type from its namestring
-    speciestype = nothing
-    for name in names(Persefone, all=true)
-        string(name) == species && (speciestype = getfield(Persefone, name))
-    end
-    isnothing(speciestype) && @error "Species $species is not defined."
-    #XXX can we get rid of the Persefone.<species> in the speciestype?
-    @debug "Initialising population of $species/$speciestype."
+function initpopulation!(speciesname::String, model::SimulationModel)
     # get the PopInitParams and check for validity
-    p = populationparameters(speciestype)
+    species = speciestype(speciesname)
+    p = populationparameters(species)
     (p.popsize <= 0 && p.popdensity <= 0) && #XXX not sure what this would do
         @warn("initpopulation() called with popsize and popdensity both <= 0")
     (p.popsize > 0 && p.popdensity > 0) && #XXX not sure what this would do
@@ -74,18 +74,19 @@ function initpopulation!(species::String, model::SimulationModel)
                 if p.habitat((x,y), model) &&
                     (p.popdensity <= 0 || @chance(1/p.popdensity)) #XXX what if pd==0?
                     if p.pairs
-                        a1 = speciestype(length(model.animals)+1, male, (-1, -1), (x,y), p.phase)
-                        a2 = speciestype(length(model.animals)+1, female, (-1, -1), (x,y), p.phase)
+                        a1 = species(length(model.animals)+1, male, (-1, -1), (x,y), p.phase)
+                        a2 = species(length(model.animals)+1, female, (-1, -1), (x,y), p.phase)
                         create!(a1, model)
                         create!(a2, model)
-                        push!(model.animals, a1)
-                        push!(model.animals, a2)
+                        push!(model.animals, a1, a2)
+                        push!(model.landscape[x,y].animals, a1.id, a2.id)
                         n += 2
                     else
                         sex = p.asexual ? hermaphrodite : @rand([male, female])
-                        a = speciestype(length(model.animals)+1, sex, (-1, -1), (x,y), p.phase)
+                        a = species(length(model.animals)+1, sex, (-1, -1), (x,y), p.phase)
                         create!(a, model)
                         push!(model.animals, a)
+                        push!(model.landscape[x,y].animals, a.id)
                         n += 1
                     end
                 end
@@ -121,10 +122,9 @@ function reproduce!(animal::Animal, model::SimulationModel,
         else
             sex = @rand([male, female])
         end
-        #FIXME get birth phase
-        child = typeof(animal)(length(model.animals)+1, sex,
-                               (animal.id, mate), animal.pos)
-        initindividual(child, model)
+        bphase = populationparameters(typeof(animal)).phase
+        child = typeof(animal)(length(model.animals)+1, sex, (animal.id, mate), animal.pos, bphase)
+        create!(child, model)
         push!(model.animals, child)
         push!(animal.offspring, child.id)
         mate > 0 && push!(models.animals[mate].offspring, child.id)
@@ -143,6 +143,7 @@ function kill!(animal::Animal, model::SimulationModel,
     if @rand() < probability
         postfix = isempty(cause) ? "." : " from $cause."
         @debug "$(animalid(animal)) has died$(postfix)"
+        filter!(x -> x!=animal.id, model.landscape[animal.pos...].animals)
         model.animals[animal.id] = nothing
         return true
     end
@@ -178,42 +179,59 @@ function isalive(animalid::Int64, model::SimulationModel)
 end
 
 """
-    nearby_animals(pos, model, radius)
+    nearby_ids(pos, model, radius)
 
-Return an iterator over all animals in the given radius around this position.
+Return a list of IDs of the animals within a given radius of the position.
 """
-function nearby_animals(pos::Tuple{Int64,Int64}, model::SimulationModel, radius::Int64)
-    #FIXME remove agents.jl code
-    #TODO enable filtering by species
-    neighbours = (model[id] for id in nearby_ids(pos, model, radius))
-    Iterators.filter(a -> typeof(a) == Animal, neighbours)
+function nearby_ids(pos::Tuple{Int64,Int64}, model::SimulationModel, radius::Int64)
+    ids = []
+    msize = size(model.landscape)
+    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(animal, model, radius)
+    nearby_animals(pos, model; radius= 0, species="")
 
-Return an iterator over all animals in the given radius around this animal, excluding itself.
+Return a list of animals in the given radius around this position, optionally filtering by species.
 """
-function nearby_animals(animal::Animal, model::SimulationModel, radius::Int64)
-    #FIXME remove agents.jl code
-    #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)
+function nearby_animals(pos::Tuple{Int64,Int64}, model::SimulationModel;
+                        radius::Int64=0, species="")
+    neighbours = nearby_ids(pos, model, radius)
+    isempty(neighbours) && return neighbours
+    if species == ""
+        model.animals[neighbours]
+    else
+        filter(x -> speciesof(x) == species, model.animals[neighbours])
+    end
 end
 
 """
-    countanimals(pos, model; species="", radius=0)
+    neighbours(animal, model; radius=0)
+
+Return a list of conspecific animals in the given radius around this animal, excluding itself.
+"""
+function neighbours(animal::Animal, model::SimulationModel; radius::Int64=0)
+    filter(a -> a.id != animal.id,
+           nearby_animals(animal.pos, model, radius=radius, species=speciesof(animal)))
+end
+
+"""
+    countanimals(pos, model; radius= 0, species="")
 
 Count the number of animals in this location (optionally supplying a species name and radius).
 """
 function countanimals(pos::Tuple{Int64,Int64}, model::SimulationModel;
-                      species::String="", radius::Int64=0)
-    n = 0 #FIXME fix nearby_animals
-    #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
+                      radius::Int64=0, species="")
+    #XXX do I want/need this?
+    length(nearby_animals(pos, model, radius=radius, species=species))
 end
 
 """
@@ -233,18 +251,14 @@ end
     move!(animal, model, position)
 
 Move the animal to the given position, making sure that this is in-bounds.
-Return true if successful, return false and leave the animal where it is if
-the position is out of 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})
-    width, height = size(model.landscape)
-    if position[1] < 0 || position[2] < 0 || position[1] > width || position[2] > height
-        #XXX should this be a warning? or just pass silently?
-        @debug "$(animalid(animal)) tried to move out of bounds to $position."
-        return false
-    end
-    animal.pos = position
-    return true
+    #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
 
 """
diff --git a/src/nature/species/wolpertinger.jl b/src/nature/species/wolpertinger.jl
index 20dabb5..02ea9c4 100644
--- a/src/nature/species/wolpertinger.jl
+++ b/src/nature/species/wolpertinger.jl
@@ -19,7 +19,7 @@ end
 
 """
 Wolpertingers are rather stupid creatures, all they do is move around randomly
-and occasionally reproduce by spontaneous parthogenesis...
+and occasionally reproduce by spontaneous parthenogenesis...
 """
 @phase Wolpertinger lifephase begin
     for i in 1:@rand(1:self.maxspeed)
@@ -27,11 +27,10 @@ and occasionally reproduce by spontaneous parthogenesis...
         #walk!(animal, direction, model; ifempty=false)
     end
     
-    #FIXME need to fix countanimals() and reproduce() first
-    # if @rand() < self.fecundity &&
-    #     @countanimals(species="Wolpertinger") < self.crowding
-    #     @reproduce()
-    # end
+    if @rand() < self.fecundity &&
+        @countanimals(species="Wolpertinger") < self.crowding
+        @reproduce()
+    end
 
     @kill self.mortality
 end
@@ -48,6 +47,7 @@ end
 Population densities of the endangered Wolpertinger are down to 1 animal per 10km².
 """
 @populate Wolpertinger begin
+    asexual = true
     phase = lifephase
     popdensity = 100000 #XXX use Unitful.jl for conversion?
 end
diff --git a/src/world/landscape.jl b/src/world/landscape.jl
index 9beb16d..bd9af0f 100644
--- a/src/world/landscape.jl
+++ b/src/world/landscape.jl
@@ -9,6 +9,7 @@
 
 "The types of landscape event that can be simulated"
 @enum EventType tillage sowing fertiliser pesticide harvesting
+#XXX rename to Management or similar?
 
 """
     Pixel
@@ -21,7 +22,7 @@ mutable struct Pixel
     landcover::LandCover
     fieldid::Union{Missing,Int64}
     events::Vector{EventType}
-    #TODO add list of animal IDs
+    animals::Vector{Int64}
 end
 
 """
@@ -60,7 +61,7 @@ function initlandscape(landcovermap::String, farmfieldsmap::String)
             lcv = LandCover(Int(landcover[x,y][1]/10))
             ff = Int64(farmfields[x,y][1])
             (iszero(ff)) && (ff = missing)
-            landscape[x,y] = Pixel(lcv, ff, Vector{Symbol}())
+            landscape[x,y] = Pixel(lcv, ff, Vector{Symbol}(), [])
         end
     end
     return landscape
-- 
GitLab