From f925cb99fe53e487df1cd81899f474483ef2cda7 Mon Sep 17 00:00:00 2001
From: Daniel Vedder <daniel.vedder@idiv.de>
Date: Mon, 5 Aug 2024 10:54:17 +0200
Subject: [PATCH] Made skylark dates normally distributed, added first-year
 mortality

---
 src/analysis/makieplots.jl    |  1 -
 src/core/utils.jl             | 26 ++++++++++++++++-----
 src/nature/individuals.jl     |  1 -
 src/nature/species/skylark.jl | 43 +++++++++++++++++++----------------
 src/world/landscape.jl        | 12 ----------
 5 files changed, 44 insertions(+), 39 deletions(-)

diff --git a/src/analysis/makieplots.jl b/src/analysis/makieplots.jl
index 1ec57dc..9721aca 100644
--- a/src/analysis/makieplots.jl
+++ b/src/analysis/makieplots.jl
@@ -79,7 +79,6 @@ Returns a Makie figure object.
 """
 function skylarkpopulation(model::SimulationModel)
     pops = model.datatables["skylark_abundance"]
-    update_theme!(palette=(color=cgrad(:seaborn_bright, 6),), cycle=[:color])
     f = Figure()
     dates = @param(core.startdate):@param(core.enddate)
     axlimits = (1, length(dates), 0, maximum(pops[!,:TotalAbundance]))
diff --git a/src/core/utils.jl b/src/core/utils.jl
index 857d3e9..358d9b8 100644
--- a/src/core/utils.jl
+++ b/src/core/utils.jl
@@ -98,22 +98,22 @@ lastyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date)-1, ad)
 Return a random element from the given vector, following a (mostly) normal distribution based on
 index values (i.e. elements in the middle of the vector will be returned most frequently).
 """
-function randn(v::Vector, rng=default_rng())
-    r = randn(rng) + 3 # normal distribution centered around 3, gives values from 0 to 6
-    step = 6 / length(v)
+function Base.randn(v::AbstractVector, rng=Random.default_rng())
+    r = bounds(randn(rng) + 4, min = 1, max=7) # normal distribution with mean 4, values of [1,7]
+    step = 7 / length(v)
     i = Int(round(r / step))
     v[i]
 end
 
 """
-    @randn(args...)
+    @randn(vector)
 
 Return a normally-distributed random number or element from the sample, using the model RNG.
 This is a utility wrapper that can only be used a context where the
 `model` object is available.
 """
-macro randn(args...)
-    :($(esc(:randn))($(esc(:model)).rng, $(map(esc, args)...)))
+macro randn(v)
+    :($(esc(:randn))($(esc(v)), $(esc(:model)).rng))
 end
 
 """
@@ -148,3 +148,17 @@ where the `model` object is available.
 macro chance(odds)
     :($(esc(:rand))($(esc(:model)).rng) < $(esc(odds)))
 end
+
+### MISCELLANEOUS
+
+"""
+    bounds(x; max=Inf, min=0)
+
+A utility function to make sure that a number is within a given set of bounds.
+Returns `max`/`min` if `x` is greater/less than this.
+"""
+function bounds(x::Number; max::Number=Inf, min::Number=0)
+    x > max ? max :
+        x < min ? min :
+        x
+end
diff --git a/src/nature/individuals.jl b/src/nature/individuals.jl
index 681e29c..8ba9098 100644
--- a/src/nature/individuals.jl
+++ b/src/nature/individuals.jl
@@ -20,7 +20,6 @@ function reproduce!(animal::Animal, model::SimulationModel,
             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)
diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl
index d45fc07..94c3bd1 100644
--- a/src/nature/species/skylark.jl
+++ b/src/nature/species/skylark.jl
@@ -4,7 +4,6 @@
 ###
 
 ##TODO
-## - first-year mortality
 ## - habitat-dependent juvenile predation mortality
 ## - habitat-dependent dispersal movement
 ## - mid-season territory adjustment
@@ -51,6 +50,8 @@
     const fledglingpredationmortality::Float64 = 0.01 # per-day fledgling mortality from predation
     const firstyearmortality::Float64 = 0.38 # total mortality in the first year after independence
 
+    # skylarks migrate from autumn to early spring, with the females leaving earlier
+    # and arriving later than the males
     const migrationdeparture::Tuple{AnnualDate,AnnualDate} = ((September, 15), (November, 1))
     const migrationarrival::Tuple{AnnualDate,AnnualDate} = ((February, 15), (March, 1))
     const migrationdelayfemales::Day = Day(15)
@@ -64,13 +65,14 @@
     const nestingcover = (0.2, 0.5) # min and max preferred vegetation cover for nesting
 
     const matefaithfulness = 0.5 # chance of a female retaining her previous partner
-    const nestingbegin::AnnualDate = (April, 10) # begin nesting in the middle of April
+    const nestingbegin::Tuple{AnnualDate,AnnualDate} = ((April, 10), (April, 20)) # begin nesting in the middle of April
     const nestbuildingtime::UnitRange{Int64} = 4:5 # 4-5 days needed to build a nest (doubled for first nest)
     const eggsperclutch::UnitRange{Int64} = 2:5 # eggs laid per clutch
     const nestingend::AnnualDate = (August, 15) # end of the nesting period
     
     # individual variables
     timer::Int64 = 0 # a counter that can be used for different purposes
+    firstnest::AnnualDate = (April, 15) # is redefined by each individual in @create(Skylark)
     migrationdates::Tuple = () # is defined by each individual in @create(Skylark)
     mate::Int64 = -1 # the agent ID of the mate (-1 if none)
     nest::Tuple = () # coordinates of current nest
@@ -86,6 +88,7 @@ end
 Non-breeding adults move around with other individuals and check for migration.
 """
 @phase Skylark nonbreeding begin
+    #XXX is flocking behaviour important? It may be quite computationally expensive...
     # flocking behaviour - follow a random neighbour or move randomly
     neighbours = @neighbours(self.visionrange) #XXX check for the closest neighbour(s)?
     isempty(neighbours) ?
@@ -183,10 +186,10 @@ end
 Females that have found a partner build a nest and lay eggs in a suitable location.
 """
 @phase Skylark nesting begin
-    if model.date < self.nestingbegin
+    if model.date < self.firstnest #XXX dependent on weather?
         # wait for nesting to begin, moving around in the territory
         @move(@rand(@animal(self.mate).territory))
-    elseif model.date > self.nestingend - Month(1)
+    elseif self.clutch == 0 && model.date > self.nestingend - Month(1)
         @setphase(nonbreeding) # stop trying to nest if it's too late (should be rare)
     elseif isempty(self.nest)
         # choose site, build nest & lay eggs
@@ -195,15 +198,9 @@ Females that have found a partner build a nest and lay eggs in a suitable locati
             if allowsnesting(self, model, pos)
                 @move(pos)
                 self.nest = pos
-                self.clutch = @rand(self.eggsperclutch)
                 #XXX all skylarks laying on the same day lay the same number of eggs? RNG?!
-                # time to build + 1 day per egg laid
-                self.timer = @rand(self.nestbuildingtime) + self.clutch
-                if month(model.date) == month(self.nestingbegin)
-                    # the first nest takes twice as long to build
-                    #XXX this may affect the first two nests
-                    self.timer += self.timer*2-self.clutch
-                end
+                self.timer = @rand(self.nestbuildingtime)
+                (model.date == self.firstnest) && (self.timer *= 2) # the first nest takes twice as long to build
                 break
             end
         end
@@ -211,8 +208,13 @@ Females that have found a partner build a nest and lay eggs in a suitable locati
         # all agricultural areas have height == cover == 0)
         #isempty(self.nest) && @warn("$(animalid(self)) didn't find a nesting location.")
     elseif self.timer == 0
-        @debug("$(animalid(self)) has laid $(self.clutch) eggs.")
-        @setphase(breeding)
+        if self.clutch == 0 # one egg is laid per day
+            self.clutch = @randn(self.eggsperclutch)
+            self.timer = self.clutch
+        else
+            @debug("$(animalid(self)) has laid $(self.clutch) eggs.")
+            @setphase(breeding)
+        end
     else
         self.timer -= 1
     end
@@ -226,7 +228,6 @@ Females that have laid eggs take care of their chicks, restarting the nesting pr
 chicks are independent or in case of brood loss.
 """
 @phase Skylark breeding begin
-    #FIXME juveniles remain much too long
     #XXX Schachtelbruten - sometimes skylarks start a new nest before the previous young are gone
     # wait for eggs to hatch & chicks to mature, checking for predation and disturbance mortality
     self.timer += 1
@@ -239,8 +240,11 @@ chicks are independent or in case of brood loss.
         @chance(self.fledglingpredationmortality) && destroynest!(self, "predation")
     else
         # create new young, reset timer and clutch counter
-        #FIXME first year mortality
-        @reproduce(self.clutch, self.mate)
+        for o in 1:self.clutch #XXX is this the best way of doing first-year mortality?
+            @chance(self.firstyearmortality) ?
+                @debug("A skylark has died from first year mortality") : 
+                @reproduce(1, self.mate)
+        end
         self.clutch = 0
     end
     if self.clutch > 0
@@ -364,8 +368,8 @@ should currently be on migration. Also sets other individual-specific variables.
 @create Skylark begin
     @debug "Added $(animalid(self)) at $(self.pos)"
     #XXX migration dates should be temperature-dependent and dynamic
-    arrive = @rand(self.migrationarrival[1]:self.migrationarrival[2])
-    depart = @rand(self.migrationdeparture[1]:self.migrationdeparture[2])
+    arrive = @randn(self.migrationarrival[1]:self.migrationarrival[2])
+    depart = @randn(self.migrationdeparture[1]:self.migrationdeparture[2])
     if self.sex == female
         arrive += self.migrationdelayfemales
         depart += self.migrationdelayfemales
@@ -377,6 +381,7 @@ should currently be on migration. Also sets other individual-specific variables.
             @setphase(matesearch)
         @migrate(arrive)
     end
+    self.firstnest = @randn(self.nestingbegin[1]:self.nestingbegin[2])
 end
 
 """
diff --git a/src/world/landscape.jl b/src/world/landscape.jl
index 7d356a8..b6fc167 100644
--- a/src/world/landscape.jl
+++ b/src/world/landscape.jl
@@ -240,18 +240,6 @@ function randomdirection(model::SimulationModel, distance::Length)
     Tuple(@rand(-range:range, 2))
 end
 
-"""
-    bounds(x; max=Inf, min=0)
-
-A utility function to make sure that a number is within a given set of bounds.
-Returns `max`/`min` if `x` is greater/less than this.
-"""
-function bounds(x::Number; max::Number=Inf, min::Number=0)
-    x > max ? max :
-        x < min ? min :
-        x
-end
-
 """
     inbounds(pos, model)
 
-- 
GitLab