From faed2eae8f4091c703e5ddad26c3d4ad0a032af7 Mon Sep 17 00:00:00 2001
From: Daniel Vedder <daniel.vedder@idiv.de>
Date: Tue, 14 Nov 2023 12:13:59 +0100
Subject: [PATCH] Fixed errors in basic skylark

Still need to write tests to make sure it works properly, but at least
it runs without errors now and seems to do what it's supposed to.
---
 src/core/simulation.jl        |  4 ++++
 src/nature/macros.jl          | 10 +++++-----
 src/nature/nature.jl          |  4 +---
 src/nature/populations.jl     |  4 ++--
 src/nature/species/skylark.jl | 28 +++++++++++++++++++---------
 src/world/landscape.jl        |  4 ++--
 6 files changed, 33 insertions(+), 21 deletions(-)

diff --git a/src/core/simulation.jl b/src/core/simulation.jl
index b77917b..1a6f390 100644
--- a/src/core/simulation.jl
+++ b/src/core/simulation.jl
@@ -59,6 +59,7 @@ Initialise a model object using a ready-made settings dict. This is
 a helper function for `initialise()`.
 """
 function initmodel(settings::Dict{String, Any})
+    #TODO catch exceptions and print them to the log file
     #TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
     @debug "Initialising model object."
     createdatadir(settings["core.outdir"], settings["core.overwrite"])
@@ -127,6 +128,7 @@ end
 Execute one update of the model.
 """
 function stepsimulation!(model::AgentBasedModel)
+    #TODO catch exceptions and print them to the log file
     with_logger(model.logger) do
         @info "Simulating day $(model.date)."
         #TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
@@ -135,6 +137,8 @@ function stepsimulation!(model::AgentBasedModel)
                 stepagent!(model[a], model)
             catch exc
                 # check if the KeyError comes from the `model[a]` or the function call
+                #FIXME this also silences KeyErrors caused in the species code (e.g.
+                # by accessing dead mates) - will be fixed once I reorganise the code
                 isa(exc, KeyError) && isa(exc.key, Int) ? continue : throw(exc)
             end
         end
diff --git a/src/nature/macros.jl b/src/nature/macros.jl
index 6ca2086..ccf9386 100644
--- a/src/nature/macros.jl
+++ b/src/nature/macros.jl
@@ -113,7 +113,8 @@ macro phase(name, body)
     #TODO the docstrings give a lot of warnings in the log - can I fix that?
     quote
         Core.@__doc__ function $(esc(name))($(esc(:animal))::Animal, $(esc(:model))::AgentBasedModel)
-            $(esc(:pos)) = $(esc(:animal)).pos
+            #TODO add `self` as a synonym for `animal`
+            $(esc(:pos)) = $(esc(:animal)).pos #XXX does this make sense?
             #$(esc(:date)) = $(esc(:model)).date #XXX does this make sense?
             $(esc(body))
         end
@@ -128,8 +129,7 @@ A utility macro to quickly access an animal's trait value.
 This can only be used nested within [`@phase`](@ref).
 """
 macro trait(traitname)
-    #TODO provide a version that can access another animal's traits
-    #XXX replace with an @v macro? (shorter, and not all variables are "traits")
+    #FIXME actually, we can get rid of this altogether if we add a Python-style `self`
     #XXX This would error if called in the first part of a species definition block
     # (i.e. outside of a @phase block). Although this is specified in the documentation,
     # it is unexpected and liable to be overlooked. Can we add a third clause to
@@ -145,7 +145,7 @@ This can only be used in a context where the `model` object is available
 (e.g. nested within [`@phase`](@ref)).
 """
 macro animal(id)
-    :($(esc(:model))[$(id)])
+    :($(esc(:model))[$(esc(id))])
 end
 
 """
@@ -156,7 +156,7 @@ This can only be used in a context where the `model` object is available
 (e.g. nested within [`@phase`](@ref)).
 """
 macro isalive(id)
-    :(isalive($(id), $(esc(:model))))
+    :(isalive($(esc(id)), $(esc(:model))))
 end
 
 """
diff --git a/src/nature/nature.jl b/src/nature/nature.jl
index 4cd5599..2edc9fa 100644
--- a/src/nature/nature.jl
+++ b/src/nature/nature.jl
@@ -73,7 +73,7 @@ Update an animal by one day, executing it's currently active phase function.
 """
 function stepagent!(animal::Animal, model::AgentBasedModel)
     animal.age += 1
-    animal.traits[animal.phase](animal,model) #FIXME -> note to self: why?
+    animal.traits[animal.phase](animal,model) #FIXME -> note to self: why, what's wrong?
 end
 
 """
@@ -106,5 +106,3 @@ function updatenature!(model::AgentBasedModel)
     end
     #XXX what else needs to go here?
 end
-
-#TODO test migration
diff --git a/src/nature/populations.jl b/src/nature/populations.jl
index d7680ec..2288ecd 100644
--- a/src/nature/populations.jl
+++ b/src/nature/populations.jl
@@ -39,7 +39,7 @@ This can be used to create the `initialise!` variable in a species definition bl
 function initpopulation(habitatdescriptor::Function; phase::Union{String,Nothing}=nothing,
                         popsize::Int64=-1, popdensity::Int64=-1, pairs::Bool=false,
                         asexual::Bool=false, initfunction::Function=(a,m)->nothing)
-    #TODO add a constructor function for the individual?
+    #TODO add a constructor function/macro for the individual
     function(species::Dict{String,Any}, model::AgentBasedModel)
         n = 0
         lastn = 0
@@ -107,7 +107,7 @@ function reproduce!(animal::Animal, model::AgentBasedModel, mate::Int64, n::Int6
     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)
+        species = @eval $(Symbol(animal.name))($model)
         a = add_agent!(animal.pos, Animal, model, species, (animal.id, mate), sex, 0)
         push!(offspring, a.id)
     end
diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl
index 5026868..f79c9fe 100644
--- a/src/nature/species/skylark.jl
+++ b/src/nature/species/skylark.jl
@@ -55,7 +55,8 @@ At the moment, this implementation is still in development.
                         # @distanceto(forest) > 5 && # at least 50m from forest edges
                         # @distanceto(builtup) > 5) # and from anthropogenic structures
     
-    @initialise(habitats, popdensity=300, pairs=true, initfunction=initskylark)
+    @initialise(habitats, phase="mating", popdensity=300, pairs=true,
+                initfunction=initskylark)
     
     """
     As an egg, simply check for mortality and hatching.
@@ -101,7 +102,7 @@ At the moment, this implementation is still in development.
     @phase nonbreeding begin
         # flocking behaviour - follow a random neighbour or move randomly
         #TODO add feeding and mortality, respect habitat when moving
-        neighbours = map(a->a.id, @neighbours(10))
+        neighbours = map(a->a.id, @neighbours(10)) #FIXME
         #isempty(neighbours) ? @randomwalk(5) : @follow(@rand(neighbours), 2)
         if isempty(neighbours)
             @randomwalk(5)
@@ -133,17 +134,17 @@ At the moment, this implementation is still in development.
                 return
             end
             m, d = monthday(model.date)
-            nest = ((m == @trait(nestingbegin)[1] && d > @trait(nestingbegin)[2]
+            nest = ((m == @trait(nestingbegin)[1] && d >= @trait(nestingbegin)[2]
                      && @chance(0.05)) || (m > @trait(nestingbegin)[1]))
             nest && @setphase(nestbuilding)
             return
         end
         # look for a mate among the neighbouring birds, or move randomly
         for n in @neighbours(50)
-            if n.sex != @trait(sex) && n.mate == -1
+            if n.sex != @trait(sex) && n.phase == "mating" && n.mate == -1
                 @trait(mate) = n.id
                 n.mate = @trait(id)
-                @debug "$(animalid(@trait(id))) and $(animalid(n.id)) have mated."
+                @debug "$(animalid(animal)) and $(animalid(n)) have mated."
                 return
             end
         end
@@ -154,26 +155,35 @@ At the moment, this implementation is still in development.
     Females select a location and build a nest. Males do nothing. (Sound familiar?)
     """
     @phase nestbuilding begin
+        if !@isalive(@trait(mate))
+            @setphase(nonbreeding)
+            return
+        end
         if @trait(sex) == female
             if isempty(@trait(nest))
                 # try to find a nest in the neighbourhood, or move on
-                nest = @randompixel(10, @trait(habitats))
-                if isnothing(nest)
-                    nest = ()
+                nestlocation = @randompixel(10, @trait(habitats))
+                if isnothing(nestlocation)
                     @randomwalk(20)
                 else
                     # if we've found a location, start the clock on the building time
                     # (building time doubles for the first nest of the year)
+                    @trait(nest) = nestlocation
                     @trait(nestcompletion) = @rand(nestbuildingtime)
                     month(model.date) == 4 && (@trait(nestcompletion) *= 2)
+                    @debug "$(animalid(animal)) is building a nest."
                 end
             else
                 # wait while nest is being built, then lay eggs and go to next phase
                 if @trait(nestcompletion) > 0
                     @trait(nestcompletion) -= 1
                 else
+                    #XXX more accurately, a female lays one egg per day, not all at once
                     @trait(clutch) = @reproduce(@trait(mate), @rand(eggsperclutch))
                     @animal(@trait(mate)).clutch = @trait(clutch)
+                    for c in @trait(clutch) #FIXME find a cleaner solution for this
+                        initskylark(@animal(c), model)
+                    end
                     @setphase(breeding)
                 end
             end
@@ -224,7 +234,7 @@ function initskylark(animal::Animal, model::AgentBasedModel)
         @migrate(returndate)
     end
     # set individual life-history parameters that are defined as ranges for the species
-    @trait(nestlingtime) = @rand(@trait(nestlingtime))
+    @trait(nestlingtime) = @rand(@trait(nestlingtime)) #FIXME no effect?
     @trait(fledglingtime) = @rand(@trait(fledglingtime))
     #TODO other stuff?
 end
diff --git a/src/world/landscape.jl b/src/world/landscape.jl
index b324298..220e51f 100644
--- a/src/world/landscape.jl
+++ b/src/world/landscape.jl
@@ -184,8 +184,8 @@ habitatdescriptor (create this using [`@habitat`](@ref)).
 """
 function randompixel(pos::Tuple{Int64,Int64}, model::AgentBasedModel, range::Int64=1,
                      habitatdescriptor::Function=(pos,model)->nothing)
-    for x in @shuffle!((pos[1]-range):(pos[1]+range))
-        for y in @shuffle!((pos[2]-range):(pos[2]+range))
+    for x in @shuffle!(collect((pos[1]-range):(pos[1]+range)))
+        for y in @shuffle!(collect((pos[2]-range):(pos[2]+range)))
             !inbounds((x,y), model) && continue
             habitatdescriptor((x,y), model) && return (x,y)
         end
-- 
GitLab