diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl index 596da80ea50f0db4bba5bdbe50da85270f99b236..359c0c00d53ac50f5614f8cd71f8f3a7faf5ffad 100644 --- a/src/crop/farmplot.jl +++ b/src/crop/farmplot.jl @@ -192,23 +192,23 @@ end """ cropheight(model, position) -Return the height of the crop at this position, or nothing if there is no crop here +Return the height of the crop at this position, or 0 if there is no crop here (utility wrapper). """ function cropheight(pos::Tuple{Int64,Int64}, model::SimulationModel) - ismissing(model.landscape[pos...].fieldid) ? 0cm : #FIXME should not return 0 - model.farmplots[model.landscape[pos...].fieldid].height + ismissing(model.landscape[pos...].fieldid) ? 0cm : #FIXME can I return something better than 0? + model.farmplots[model.landscape[pos...].fieldid].height*1cm #FIXME units end """ cropcover(model, position) -Return the percentage ground cover of the crop at this position, or nothing if there is no crop +Return the percentage ground cover of the crop at this position, or 0 if there is no crop here (utility wrapper). """ function cropcover(pos::Tuple{Int64,Int64}, model::SimulationModel) #FIXME LAItotal != ground cover? - ismissing(model.landscape[pos...].fieldid) ? 0 : #FIXME should not return 0 + ismissing(model.landscape[pos...].fieldid) ? 0 : #FIXME can I return something better than 0? model.farmplots[model.landscape[pos...].fieldid].LAItotal end diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl index 606ebc478ecad982421ba6d38cdbbd0218251cb0..6da29352cc3eed3f90ee3e2d67396db9bff86542 100644 --- a/src/nature/species/skylark.jl +++ b/src/nature/species/skylark.jl @@ -35,13 +35,12 @@ At the moment, this implementation is still in development. const visionrange::Length = 200m #XXX arbitrary const eggtime::Int64 = 11 # days from laying to hatching - const nestlingtime::UnitRange{Int64} = 7:11 # days from hatching to leaving nest - const fledglingtime::UnitRange{Int64} = 25:30 # days from hatching to independence + const nestlingtime::Int64 = 9 # days from hatching to leaving nest + const fledglingtime::Int64 = 21 # days from leaving the nest to independence + #XXX predation mortality should be habitat-dependent const eggpredationmortality::Float64 = 0.03 # per-day egg mortality from predation - const nestharvestmortality::Float64 = 1.0 # egg/nestling mortality after a harvest event const nestlingpredationmortality::Float64 = 0.03 # per-day nestling mortality from predation - const fledglingharvestmortality::Float64 = 0.5 # fledgling mortality after harvest 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 migrationmortality::Float64 = 0.33 # chance of dying during the winter @@ -57,17 +56,14 @@ At the moment, this implementation is still in development. const nestingbegin::Tuple{Int64,Int64} = (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 breedingdelay::Int64 = 18 # days after hatching before starting a new brood #XXX ?? const nestingend::Int64 = July # last month of nesting # individual variables - #FIXME check what needs to be rewritten - timer::Int64 = 0 # a count-down timer that can be used for different purposes + timer::Int64 = 0 # a counter that can be used for different purposes 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 - nestcompletion::Int64 = 0 # days left until the nest is built - clutch::Int64 = 0 # number of offspring in current clutch + clutch::Int64 = 0 # number and life stage of offspring in current clutch end @@ -203,8 +199,8 @@ Females that have found a partner build a nest and lay eggs in a suitable locati self.timer -= 1 end # tillage and harvest destroys the nest - @respond(tillage, self.nest = ()) - @respond(harvesting, self.nest = ()) + @respond(tillage, destroynest!(self, "tillage")) + @respond(harvesting, destroynest!(self, "harvesting")) end """ @@ -212,12 +208,32 @@ 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 - #TODO wait for eggs to hatch & chicks to mature, checking for mortality - # restart breeding cycle if there is time - if self.clutch == 0 && month(model.date) <= self.nestingend - @setphase(nesting) #TODO breeding delay? - elseif month(model.date) > self.nestingend - @setphase(nonbreeding) + #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 + #XXX this should be habitat-dependent! + if self.timer <= self.eggtime + @chance(self.eggpredationmortality) && destroynest!(self, "predation") + elseif self.timer <= self.eggtime + self.nestlingtime + @chance(self.nestlingpredationmortality) && destroynest!(self, "predation") + elseif self.timer <= self.eggtime + self.nestlingtime + self.fledglingtime + @chance(self.fledglingpredationmortality) && destroynest!(self, "predation") + else + # create new young, reset timer and clutch counter + @reproduce(self.clutch, self.mate) + self.clutch = 0 + end + if self.clutch > 0 + # tillage and harvest destroys the nest + @respond(tillage, destroynest!(self, "tillage")) + @respond(harvesting, destroynest!(self, "harvesting")) + else # restart breeding cycle if there is time + self.timer = 0 + if month(model.date) <= self.nestingend + @setphase(nesting) + elseif month(model.date) > self.nestingend + @setphase(nonbreeding) + end end end @@ -236,8 +252,8 @@ function migrationdates(skylark::Skylark, model::SimulationModel) 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 = 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 @@ -292,7 +308,9 @@ end foragequality(skylark, model, pos) Calculate the relative quality of the habitat at this position for foraging. -(Approximated from Püttmanns et al., 2021; Jeromin, 2002; Jenny, 1990b.) +This assumes that open habitat is best (quality = 1.0), and steadily decreases as vegetation +height and/or cover increase. (Linear regressions based on Püttmanns et al., 2021; Jeromin, 2002; +Jenny, 1990b.) """ function foragequality(skylark::Skylark, model::SimulationModel, pos::Tuple{Int64,Int64}) #TODO this is a key function that needs to be validated thoroughly @@ -301,18 +319,9 @@ function foragequality(skylark::Skylark, model::SimulationModel, pos::Tuple{Int6 (@distanceto(builtup) < skylark.mindistancetoedge) return 0.0 end - quality = 1.0 - f = farmplot(pos, model) - # Assume that grass and soil have a habitat quality of 1.0. For fields, calculate quality - # as the sum of cover quality and height quality (each modelled using a linear regression). - if !isnothing(f) - groundcoverfactor = x -> bounds((-1/skylark.maxforagecover)*x + 1.0, max=1.0) - plantheightfactor = x -> bounds((-1/skylark.maxforageheight)*(x |> cm) + 1.0, max=1.0) - #FIXME need percentage cover in FarmPlot, not LAI - #FIXME height is currently dimensionless in FarmPlot, hence the conversion to cm - quality = bounds(groundcoverfactor(f.LAItotal) + plantheightfactor(f.height*1cm), max=1.0) - end - return quality + groundcoverfactor = x -> bounds((-1/skylark.maxforagecover)*x + 1.0, max=1.0) + plantheightfactor = x -> bounds((-1/skylark.maxforageheight)*(x |> cm) + 1.0, max=1.0) + return bounds(groundcoverfactor(@cropcover()) + plantheightfactor(@cropheight()), max=1.0) end """ @@ -324,12 +333,24 @@ 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) && #(@distanceto(builtup) < skylark.mindistancetoedge) end +""" + destroynest!(skylark, reason) + +Remove the skylark's nest and offspring due to disturbance or predation. +""" +function destroynest!(self::Skylark, reason::String) + self.nest = () + self.clutch = 0 + @debug("$(animalid(self)) had her nest destroyed by $reason.") +end + ## INITIALISATION """ @@ -345,7 +366,7 @@ should currently be on migration. Also sets other individual-specific variables. @setphase(territorysearch) : @setphase(matesearch) @migrate(arrive) - self.migrationdates = self.migrationdates .+ Year(1)) + self.migrationdates = self.migrationdates .+ Year(1) end end