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