From e81536f7042cbc217958ad8a539b3d94a4b9d805 Mon Sep 17 00:00:00 2001 From: Daniel Vedder <daniel.vedder@idiv.de> Date: Fri, 2 Aug 2024 22:47:59 +0200 Subject: [PATCH] Integrated AnnualDates into the nature submodel --- src/core/simulation.jl | 2 +- src/core/utils.jl | 45 +++++++++++++------- src/nature/individuals.jl | 2 +- src/nature/nature.jl | 2 +- src/nature/species/skylark.jl | 79 ++++++++++++++++------------------- 5 files changed, 69 insertions(+), 61 deletions(-) diff --git a/src/core/simulation.jl b/src/core/simulation.jl index ac9a457..420b24d 100644 --- a/src/core/simulation.jl +++ b/src/core/simulation.jl @@ -25,7 +25,7 @@ mutable struct AgricultureModel{Tcroptype,Tcropstate} <: SimulationModel farmers::Vector{Farmer} farmplots::Vector{FarmPlot{Tcropstate}} animals::Vector{Union{Animal,Nothing}} - migrants::Vector{Pair{Animal,Date}} + migrants::Vector{Pair{Animal,AnnualDate}} events::Vector{FarmEvent} end diff --git a/src/core/utils.jl b/src/core/utils.jl index d8077c3..26882de 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -47,20 +47,10 @@ Base.convert(::Type{AnnualDate}, ad::Tuple{Int64,Int64}) = AnnualDate(ad) # Interface with Dates AnnualDate(date::Date) = AnnualDate(month(date), day(date)) +Base.convert(::Type{AnnualDate}, ad::Date) = AnnualDate(ad) Dates.month(ad::AnnualDate) = ad.month Dates.day(ad::AnnualDate) = ad.day - -# Instantiate a recurring date for a given year -""" - thisyear(annualdate, model) - nextyear(annualdate, model) - lastyear(annualdate, model) - -Convert an AnnualDate to a Date, using the current/next/previous year of the simulation run. -""" -thisyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date), ad.month, ad.day) -nextyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date)+1, ad.month, ad.day) -lastyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date)-1, ad.month, ad.day) +Date(year::Int64, ad::AnnualDate) = Date(year, ad.month, ad.day) # Comparison between AnnualDates and with Dates Base.:(==)(ad1::AnnualDate, ad2::AnnualDate) = (month(ad1) == month(ad2) && day(ad1) == day(ad2)) @@ -68,9 +58,32 @@ Base.:(==)(ad::AnnualDate, d::Date) = (month(d) == month(ad) && day(d) == day(ad Base.:(==)(d::Date, ad::AnnualDate) = (ad == d) Base.:(<)(ad1::AnnualDate, ad2::AnnualDate) = (month(ad1) < month(ad2) || (month(ad1) == month(ad2) && day(ad1) < day(ad2))) -Base.:(<)(ad::AnnualDate, d::Date) = (thisyear(ad) < d) -Base.:(<)(d::Date, ad::AnnualDate) = (d < thisyear(ad)) +Base.:(<)(ad::AnnualDate, d::Date) = ad < AnnualDate(d) +Base.:(<)(d::Date, ad::AnnualDate) = AnnualDate(d) < ad # Addition and subtraction of date periods -Base.:(+)(ad::AnnualDate, time::DatePeriod) = AnnualDate(thisyear(ad) + time) -Base.:(-)(ad::AnnualDate, time::DatePeriod) = AnnualDate(thisyear(ad) - time) +Base.:(+)(ad::AnnualDate, time::DatePeriod) = AnnualDate(Date(2022, ad) + time) +Base.:(-)(ad::AnnualDate, time::DatePeriod) = AnnualDate(Date(2022, ad) - time) +Base.:(-)(ad1::AnnualDate, ad2::AnnualDate) = ad1 > ad2 ? + Date(2022, ad1) - Date(2022, ad2) : + Date(2022, ad1) - Date(2021, ad2) + +# Taking ranges +Base.:(:)(start::AnnualDate, stop::AnnualDate) = + if start < stop # normal case, e.g. Easter:Christmas + AnnualDate.(Date(2022, start):Date(2022, stop)) + else # handle wrap-around, e.g. Christmas:Easter #XXX ignores leap years + AnnualDate.(Date(2021, start):Date(2022, stop)) + end + +# Instantiate a recurring date for a given year +""" + thisyear(annualdate, model) + nextyear(annualdate, model) + lastyear(annualdate, model) + +Convert an AnnualDate to a Date, using the current/next/previous year of the simulation run. +""" +thisyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date), ad) +nextyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date)+1, ad) +lastyear(ad::AnnualDate, model::SimulationModel) = Date(year(model.date)-1, ad) diff --git a/src/nature/individuals.jl b/src/nature/individuals.jl index 5a056e7..681e29c 100644 --- a/src/nature/individuals.jl +++ b/src/nature/individuals.jl @@ -60,7 +60,7 @@ end 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) +function migrate!(animal::Animal, model::SimulationModel, arrival::AnnualDate) # 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) diff --git a/src/nature/nature.jl b/src/nature/nature.jl index 8344bd1..50bb104 100644 --- a/src/nature/nature.jl +++ b/src/nature/nature.jl @@ -117,7 +117,7 @@ function updatenature!(model::SimulationModel) end # The migrant pool is sorted by date of return, so we can simply look at the top # of the stack to check whether any animals are returning today. - while !isempty(model.migrants) && model.migrants[1].second <= model.date + while !isempty(model.migrants) && model.migrants[1].second == model.date returnee = model.migrants[1].first model.animals[returnee.id] = returnee @debug "$(animalid(returnee)) has returned." diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl index 5a75f7e..8f511df 100644 --- a/src/nature/species/skylark.jl +++ b/src/nature/species/skylark.jl @@ -4,11 +4,11 @@ ### ##TODO +## - first-year mortality ## - habitat-dependent juvenile predation mortality ## - habitat-dependent dispersal movement ## - mid-season territory adjustment ## - temperature-dependent migration, breeding begin, and mortality -## - first-year mortality """ @@ -50,6 +50,10 @@ const nestlingpredationmortality::Float64 = 0.03 # per-day nestling mortality from predation const fledglingpredationmortality::Float64 = 0.01 # per-day fledgling mortality from predation const firstyearmortality::Float64 = 0.38 # total mortality in the first year after independence + + const migrationdeparture::Tuple{AnnualDate,AnnualDate} = ((September, 15), (November, 1)) + const migrationarrival::Tuple{AnnualDate,AnnualDate} = ((February, 15), (March, 1)) + const migrationdelayfemales::Day = Day(15) const migrationmortality::Float64 = 0.33 # chance of dying during the winter const minimumterritory = 5000m² # size of territory under ideal conditions @@ -60,11 +64,11 @@ 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::Tuple{Int64,Int64} = (April, 10) # begin nesting in the middle of April + const nestingbegin::AnnualDate = (April, 10) # 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::Int64 = July # last month of nesting - + const nestingend::AnnualDate = (August, 15) # end of the nesting period + # individual variables timer::Int64 = 0 # a counter that can be used for different purposes migrationdates::Tuple = () # is defined by each individual in @create(Skylark) @@ -88,14 +92,13 @@ Non-breeding adults move around with other individuals and check for migration. @walk("random", self.movementrange) : @follow(@rand(neighbours), 30m) #XXX magic number # check if the bird migrates - leave, arrive = self.migrationdates - if leave <= model.date < arrive + arrive, depart = self.migrationdates + if model.date >= depart @kill(self.migrationmortality, "migration") self.sex == male ? @setphase(territorysearch) : @setphase(matesearch) @migrate(arrive) - self.migrationdates = self.migrationdates .+ Year(1) end end @@ -105,8 +108,8 @@ Males returning from migration move around to look for suitable habitats to esta @phase Skylark territorysearch begin if !isempty(self.territory) @setphase(occupation) # If the male still has a territory, it is occupied again - elseif month(model.date) > self.nestingend - @setphase(nonbreeding) # If the breeding season is over, give up looking + elseif model.date > self.nestingend - Month(1) + @setphase(nonbreeding) # If the breeding season is almost over, give up looking else # otherwise check whether this is a suitable location newterritory = findterritory(self, model) if isempty(newterritory) @@ -127,9 +130,10 @@ adjusting it to new conditions when and as necessary. @phase Skylark occupation begin # move to a random location in the territory @move(@rand(self.territory)) - if month(model.date) > self.nestingend + if model.date > self.nestingend # once the breeding season is over and all the young have left the nest, stop breeding - if self.mate == -1 || @animal(self.mate).clutch == 0 + #FIXME under which conditions would @animal(self.mate) == nothing? + if self.mate == -1 || !isnothing(@animal(self.mate)) || @animal(self.mate).clutch == 0 @setphase(nonbreeding) end end @@ -141,7 +145,7 @@ Females returning from migration move around to look for a suitable partner with """ @phase Skylark matesearch begin if self.mate != -1 # if the female already had a partner last year... - if isalive(@animal(self.mate)) + if @isalive(self.mate) if @chance(self.matefaithfulness) # ...check if he is still alive and the female wants to stay with him #XXX is mate-faithfulness decided by the female when she returns, @@ -168,8 +172,8 @@ Females returning from migration move around to look for a suitable partner with end end #@debug("$(animalid(self)) didn't find a mate.") - if month(model.date) > self.nestingend # stop trying to find a mate if it's too late - @setphase(nonbreeding) + if model.date > self.nestingend - Month(1) + @setphase(nonbreeding) # Stop trying to find a mate if it's too late else @walk("random", self.movementrange) end @@ -179,10 +183,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 < Date(year(model.date), self.nestingbegin...) + if model.date < self.nestingbegin # wait for nesting to begin, moving around in the territory @move(@rand(@animal(self.mate).territory)) - elseif month(model.date) > self.nestingend + elseif 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 @@ -193,7 +197,7 @@ Females that have found a partner build a nest and lay eggs in a suitable locati self.clutch = @rand(self.eggsperclutch) # time to build + 1 day per egg laid self.timer = @rand(self.nestbuildingtime) + self.clutch - if month(model.date) == self.nestingbegin[1] + 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 @@ -201,9 +205,11 @@ Females that have found a partner build a nest and lay eggs in a suitable locati break end end + #FIXME happens quite often (probably because the crop models don't work yet, so + # 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.") + @debug("$(animalid(self)) has laid $(self.clutch) eggs.") #FIXME 0 eggs laid? @setphase(breeding) else self.timer -= 1 @@ -240,9 +246,9 @@ chicks are independent or in case of brood loss. @respond(harvesting, destroynest!(self, "harvesting")) else # restart breeding cycle if there is time self.timer = 0 - if month(model.date) <= self.nestingend + if model.date <= self.nestingend - Month(1) @setphase(nesting) - elseif month(model.date) > self.nestingend + else @setphase(nonbreeding) end end @@ -251,23 +257,6 @@ end ## SUPPORTING FUNCTIONS -""" - migrationdates(skylark, model) - -Select the dates on which this skylark will leave for / return from its migration, -based on observed migration patterns. -""" -function migrationdates(skylark::Skylark, model::SimulationModel) - #TODO this ought to be temperature-dependent and dynamic - minleave = skylark.sex == female ? (September, 15) : (October, 1) - minarrive = skylark.sex == male ? (February, 15) : (March, 1) - deltaleave = @rand(0:45) #XXX ought to be normally distributed - deltaarrive = @rand(0:15) #XXX ought to be normally distributed - leave = Date(year(model.date), minleave[1], minleave[2]) + Day(deltaleave) - arrive = Date(year(model.date)+1, minarrive[1], minarrive[2]) + Day(deltaarrive) - (leave, arrive) -end - """ findterritory(skylark, model) @@ -344,7 +333,7 @@ function allowsnesting(skylark::Skylark, model::SimulationModel, pos::Tuple{Int6 #TODO is this condition correct? -> needs validation! (@landcover() == grass || (@landcover() == agriculture && - (skylark.nestingheight[1] <= (@cropheight()) <= skylark.nestingheight[2]) && + (skylark.nestingheight[1] <= @cropheight() <= skylark.nestingheight[2]) && (skylark.nestingcover[1] <= @cropcover() <= skylark.nestingcover[2]))) #&& #FIXME if we add the distance requirement, females don't find a nesting spot? #(@distanceto(forest) < skylark.mindistancetoedge) && @@ -370,14 +359,19 @@ should currently be on migration. Also sets other individual-specific variables. """ @create Skylark begin @debug "Added $(animalid(self)) at $(self.pos)" - self.migrationdates = migrationdates(self, model) .- Year(1) #XXX should be dynamic - leave, arrive = self.migrationdates - if leave <= model.date < arrive + #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]) + if self.sex == female + arrive += self.migrationdelayfemales + depart += self.migrationdelayfemales + end + self.migrationdates = (arrive, depart) + if model.date < arrive || model.date >= depart self.sex == male ? @setphase(territorysearch) : @setphase(matesearch) @migrate(arrive) - self.migrationdates = self.migrationdates .+ Year(1) end end @@ -394,3 +388,4 @@ land, keeping a distance of 60m to vertical structures and giving each pair an a indarea = 3ha #XXX magic number pairs = true end + -- GitLab