diff --git a/src/Persefone.jl b/src/Persefone.jl index 37390f4b10628bec68e67a757899c7788af0160d..15a33b6158f65aa54689a2b554dcc06d0b0eef45 100644 --- a/src/Persefone.jl +++ b/src/Persefone.jl @@ -127,7 +127,7 @@ include("nature/ecologicaldata.jl") # for f in readdir("nature/species", join=true) # endswith(f, ".jl") && include(f) # end -#include("nature/species/skylark.jl") +include("nature/species/skylark.jl") include("nature/species/wolpertinger.jl") include("nature/species/wyvern.jl") diff --git a/src/nature/macros.jl b/src/nature/macros.jl index 9d768e6345b9d0596ab03b0f900d1bc2dedd77bd..ccb82d3efe3508947e31bddd855ba515a12c74ec 100644 --- a/src/nature/macros.jl +++ b/src/nature/macros.jl @@ -408,18 +408,6 @@ macro walk(args...) #XXX add `ifempty` keyword? :(walk!($(esc(:self)), $(esc(:model)), $(map(esc, args)...))) end - -""" - @randomwalk(distance) - -Walk in a random direction for a specified number of steps. -This is a utility wrapper that can only be used nested within [`@phase`](@ref). -""" -macro randomwalk(distance) - #FIXME remove Agents.jl code - #XXX add `ifempty` keyword? - :(randomwalk!($(esc(:self)), $(esc(:model)), $(esc(distance)))) -end #TODO add own walking functions that respect habitat descriptors """ diff --git a/src/nature/populations.jl b/src/nature/populations.jl index 44fa2b7ceab11c6922aec161d2c6b921a2f6a093..6fdb7be5e2d4020f8d76c84c879231ec54fc8e7b 100644 --- a/src/nature/populations.jl +++ b/src/nature/populations.jl @@ -11,7 +11,10 @@ A set of parameters used by [`initpopulation`](@ref) to initialise the populatio of a species at the start of a simulation. Define these parameters for each species using [`@populate`](@ref). -- `phase` determines which life phase individuals will be assigned to (required). +- `initphase` determines which life phase individuals will be assigned to at model + initialisation (required). + +- `birthphase` determines which life phase individuals will be assigned to at birth (required). - `habitat` is a function that determines whether a given location is suitable or not (create this using [`@habitat`](@ref)). By default, every cell will be occupied. @@ -33,7 +36,8 @@ using [`@populate`](@ref). is ignored. (default: false) """ @kwdef struct PopInitParams - phase::Function + initphase::Function + birthphase::Function habitat::Function = @habitat(true) popsize::Int64 = -1 popdensity::Int64 = -1 @@ -74,19 +78,19 @@ function initpopulation!(speciesname::String, model::SimulationModel) if p.habitat((x,y), model) && (p.popdensity <= 0 || @chance(1/p.popdensity)) #XXX what if pd==0? if p.pairs - a1 = species(length(model.animals)+1, male, (-1, -1), (x,y), p.phase) - a2 = species(length(model.animals)+1, female, (-1, -1), (x,y), p.phase) - create!(a1, model) - create!(a2, model) + a1 = species(length(model.animals)+1, male, (-1, -1), (x,y), p.initphase) + a2 = species(length(model.animals)+2, female, (-1, -1), (x,y), p.initphase) push!(model.animals, a1, a2) push!(model.landscape[x,y].animals, a1.id, a2.id) + create!(a1, model) + create!(a2, model) n += 2 else sex = p.asexual ? hermaphrodite : @rand([male, female]) - a = species(length(model.animals)+1, sex, (-1, -1), (x,y), p.phase) - create!(a, model) + a = species(length(model.animals)+1, sex, (-1, -1), (x,y), p.initphase) push!(model.animals, a) push!(model.landscape[x,y].animals, a.id) + create!(a, model) n += 1 end end @@ -122,12 +126,13 @@ function reproduce!(animal::Animal, model::SimulationModel, else sex = @rand([male, female]) end - bphase = populationparameters(typeof(animal)).phase + bphase = populationparameters(typeof(animal)).birthphase child = typeof(animal)(length(model.animals)+1, sex, (animal.id, mate), animal.pos, bphase) - create!(child, model) push!(model.animals, child) push!(animal.offspring, child.id) - mate > 0 && push!(models.animals[mate].offspring, child.id) + mate > 0 && push!(model.animals[mate].offspring, child.id) + push!(model.landscape[child.pos...].animals, child.id) + create!(child, model) end @debug "$(animalid(animal)) has reproduced." end @@ -165,6 +170,7 @@ function migrate!(animal::Animal, model::SimulationModel, arrival::Date) else insert!(model.migrants, i, Pair(animal, arrival)) end + filter!(x -> x!=animal.id, model.landscape[animal.pos...].animals) model.animals[animal.id] = nothing @debug "$(animalid(animal)) has migrated." end @@ -206,7 +212,7 @@ function nearby_animals(pos::Tuple{Int64,Int64}, model::SimulationModel; radius::Int64=0, species="") neighbours = nearby_ids(pos, model, radius) isempty(neighbours) && return neighbours - if species == "" + if isempty(species) model.animals[neighbours] else filter(x -> speciesof(x) == species, model.animals[neighbours]) @@ -242,7 +248,7 @@ Calculate the distance from the given position to the animal. """ function distanceto(pos::Tuple{Int64,Int64}, model::SimulationModel, animal::Animal) # have to use a coordinate as first argument rather than an animal because of @distanceto - maximum(abs.(animal.pos - pos)) + maximum(abs.(animal.pos .- pos)) end """ @@ -273,30 +279,30 @@ function move!(animal::Animal, model::SimulationModel, position::Tuple{Int64,Int end """ - walk!(animal, model, direction) + walk!(animal, model, direction, steps=1) -Let the animal move one step in the given direction ("north", "northeast", +Let the animal move a given number of step in the given direction ("north", "northeast", "east", "southeast", "south", "southwest", "west", "northwest", "random"). """ -function walk!(animal::Animal, model::SimulationModel, direction::String) +function walk!(animal::Animal, model::SimulationModel, direction::String, steps=1) if direction == "north" - shift = (0,-1) + shift = (0,-steps) elseif direction == "northeast" - shift = (1,-1) + shift = (steps,-steps) elseif direction == "east" - shift = (1,0) + shift = (steps,0) elseif direction == "southeast" - shift = (1,1) + shift = (steps,steps) elseif direction == "south" - shift = (0,1) + shift = (0,steps) elseif direction == "southwest" - shift = (-1,1) + shift = (-steps,steps) elseif direction == "west" - shift = (-1,0) + shift = (-steps,0) elseif direction == "northwest" - shift = (-1,-1) + shift = (-steps,-steps) elseif direction == "random" - shift = Tuple(@rand([-1,1], 2)) + shift = Tuple(@rand([-steps,steps], 2)) else @error "Invalid direction in @walk: "*direction end @@ -322,5 +328,4 @@ function walk!(animal::Animal, model::SimulationModel, direction::Tuple{Int64,In move!(animal, model, newpos) end -##TODO add random walk with habitat descriptor ##TODO add walktoward or similar function (incl. pathfinding?) diff --git a/src/nature/species/skylark.jl b/src/nature/species/skylark.jl index 592b526af2b0e7768aa92f10e323c93e799078a8..038c0a0f0c047b1404642939a5ccfaf6349499a8 100644 --- a/src/nature/species/skylark.jl +++ b/src/nature/species/skylark.jl @@ -3,6 +3,15 @@ ### This file holds the code for the Eurasian Skylark (Alauda arvensis). ### +skylarkhabitat = @habitat((@landcover() == grass || + # settle on grass or arable land (but not maize) + (@landcover() == agriculture && @cropname() != "maize")) && + @distancetoedge() > 5) # at least 50m from other habitats + #XXX this ought to check for distance to forest and builtup, + # but that's very expensive (see below) + # @distanceto(forest) > 5 && # at least 50m from forest edges + # @distanceto(builtup) > 5) # and from anthropogenic structures + """ Skylark @@ -21,10 +30,12 @@ At the moment, this implementation is still in development. ISBN 3-89104-019-9 """ @species Skylark begin + + #XXX use Unitful.jl eggtime = 11 # 11 days from laying to hatching eggpredationmortality = 0.03 # per-day egg mortality from predation - nestharvestmortality = 0.9 # egg mortality after a harvest event (XXX guess) + nestharvestmortality = 0.9 # egg/nestling mortality after a harvest event (XXX guess) nestlingtime = 7:11 # 7-11 days from hatching to leaving nest nestlingpredationmortality = 0.03 # per-day nestling mortality from predation @@ -34,185 +45,195 @@ At the moment, this implementation is still in development. fledglingpredationmortality = 0.01 # per-day fledgling mortality from predation firstyearmortality = 0.38 # total mortality in the first year after independence - migrationdates = () # is defined by each individual in `initskylark()` + migrationdates = () # is defined by each individual in @create(Skylark) migrationmortality = 0.33 # chance of dying during the winter mate = -1 # the agent ID of the mate (-1 if none) nest = () # coordinates of current nest - nestingbegin = (4, 10) # begin nesting in the middle of April + nestingbegin = (April, 10) # begin nesting in the middle of April nestbuildingtime = 4:5 # 4-5 days needed to build a nest (doubled for first nest) nestcompletion = 0 # days left until the nest is built eggsperclutch = 2:5 # 2-5 eggs laid per clutch clutch = [] # IDs of offspring in current clutch breedingdelay = 18 # wait 18 days after hatching to start a new brood + nestingend = July # last month of nesting - habitats = @habitat((@landcover() == grass || - # settle on grass or arable land (but not maize) - (@landcover() == agriculture && @cropname() != "maize")) && - @distancetoedge() > 5) # at least 50m from other habitats - #XXX this ought to check for distance to forest and builtup, - # but that's very expensive (see below) - # @distanceto(forest) > 5 && # at least 50m from forest edges - # @distanceto(builtup) > 5) # and from anthropogenic structures - - @initialise(habitats, phase="mating", popdensity=300, pairs=true, - initfunction=initskylark) - - """ - As an egg, simply check for mortality and hatching. - """ - @phase egg begin - @kill(self.eggpredationmortality, "predation") - @respond(harvesting, @kill(self.nestharvestmortality, "harvest")) - - if self.age == self.eggtime - @setphase(nestling) - end + habitats = skylarkhabitat +end + +""" +As an egg, simply check for mortality and hatching. +""" +@phase Skylark egg begin + @kill(self.eggpredationmortality, "predation") + @respond(harvesting, @kill(self.nestharvestmortality, "harvest")) + + if self.age == self.eggtime + @setphase(nestling) end +end - """ - As a nestling, simply check for mortality and fledging. - """ - @phase nestling begin - #TODO add feeding & growth - @kill(self.nestlingpredationmortality, "predation") - @respond(harvesting, @kill(self.nestharvestmortality, "harvest")) - if self.age == self.nestlingtime+self.eggtime) - @setphase(fledgling) - end +""" +As a nestling, simply check for mortality and fledging. +""" +@phase Skylark nestling begin + #TODO add feeding & growth + @kill(self.nestlingpredationmortality, "predation") + @respond(harvesting, @kill(self.nestharvestmortality, "harvest")) + if self.age == self.nestlingtime+self.eggtime + @setphase(fledgling) end +end - """ - As a fledgling, move around a little, but mainly wait for maturity and - check mortality. - """ - @phase fledgling begin - #TODO add feeding & growth - @kill(self.fledglingpredationmortality, "predation") - @randomwalk(1) #TODO add movement following the parents - if self.age == self.fledglingtime+self.eggtime) - @kill(self.firstyearmortality, "first year mortality") #XXX mechanistic? - @setphase(nonbreeding) - end +""" +As a fledgling, move around a little, but mainly wait for maturity and +check mortality. +""" +@phase Skylark fledgling begin + #TODO add feeding & growth + @kill(self.fledglingpredationmortality, "predation") + @walk("random") #TODO add movement following the parents + if self.age == self.fledglingtime+self.eggtime + @kill(self.firstyearmortality, "first year mortality") #XXX mechanistic? + @setphase(nonbreeding) end +end - """ - As a non-breeding adult, move around with other individuals and check for migration. - """ - @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)) #FIXME - isempty(neighbours) ? - @randomwalk(5) : - @follow(@animal(@rand(neighbours)), 2) - # check if the bird migrates - leave, arrive = animal.migrationdates - m, d = monthday(model.date) - migrate = (((m < arrive[1]) || (m == arrive[1] && d < arrive[2])) || - ((m > leave[1]) || (m == leave[1] && d >= leave[2]))) - if migrate #FIXME not all migrate? - @kill(self.migrationmortality, "migration") - returndate = Date(year(model.date), arrive[1], arrive[2]) - model.date != @param(core.startdate) && (returndate += Year(1)) - @setphase(mating) - @migrate(returndate) - end +""" +As a non-breeding adult, move around with other individuals and check for migration. +""" +@phase Skylark nonbreeding begin + # flocking behaviour - follow a random neighbour or move randomly + #TODO add feeding and mortality, respect habitat when moving + neighbours = @neighbours(10) #XXX magic number + isempty(neighbours) ? + @walk("random", 5) : + @follow(@rand(neighbours), 2) + # check if the bird migrates + leave, arrive = self.migrationdates + m, d = monthday(model.date) + migrate = (((m < arrive[1]) || (m == arrive[1] && d < arrive[2])) || + ((m > leave[1]) || (m == leave[1] && d >= leave[2]))) + if migrate #FIXME not all migrate? + @kill(self.migrationmortality, "migration") + returndate = Date(year(model.date)+1, arrive[1], arrive[2]) + @setphase(mating) + @migrate(returndate) end +end - """ - Move around until a mate is found. - """ - @phase mating begin - # if we've found a mate, wait for nesting begin and then go to the next phase - if self.mate != -1 - if !@isalive(self.mate) - self.mate = -1 - return - end - m, d = monthday(model.date) - nest = ((m == self.nestingbegin[1] && d >= self.nestingbegin[2] - && @chance(0.05)) || (m > self.nestingbegin[1])) - nest && @setphase(nestbuilding) +""" +Move around until a mate is found. +""" +@phase Skylark mating begin + # if we've found a mate, wait for nesting begin and then go to the next phase + if self.mate != -1 + if !@isalive(self.mate) + self.mate = -1 return end - # look for a mate among the neighbouring birds, or move randomly - for n in @neighbours(50) - if n.sex != self.sex && n.phase == "mating" && n.mate == -1 - self.mate = n.id - n.mate = self.id - @debug "$(animalid(animal)) and $(animalid(n)) have mated." - return - end - end - @randomwalk(10) + m, d = monthday(model.date) + nest = ((m == self.nestingbegin[1] && d >= self.nestingbegin[2] + && @chance(0.05)) || (m > self.nestingbegin[1])) #XXX why the chance? + nest && @setphase(nestbuilding) + return end - - """ - Females select a location and build a nest. Males do nothing. (Sound familiar?) - """ - @phase nestbuilding begin - if !@isalive(self.mate) - @setphase(nonbreeding) + # look for a mate among the neighbouring birds, or move randomly + for n in @neighbours(50) #XXX magic number + if n.sex != self.sex && n.phase == mating && n.mate == -1 + self.mate = n.id + n.mate = self.id + @debug "$(animalid(self)) and $(animalid(n)) have mated." return end - if self.sex == female - if isempty(self.nest) - # try to find a nest in the neighbourhood, or move on - nestlocation = @randompixel(10, self.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) - self.nest = nestlocation - self.nestcompletion = @rand(nestbuildingtime) - month(model.date) == 4 && (self.nestcompletion *= 2) - @debug "$(animalid(animal)) is building a nest." - end + end + #@debug("$(animalid(self)) didn't find a mate.") + @walk("random", 10) #XXX magic number +end + +""" +Females select a location and build a nest. Males do nothing. (Sound familiar?) +""" +@phase Skylark nestbuilding begin + if !@isalive(self.mate) + self.mate = -1 + @setphase(nonbreeding) + return + end + if self.sex == female + if isempty(self.nest) + # try to find a nest in the neighbourhood, or move on + nestlocation = @randompixel(10, self.habitats) #XXX magic number + if isnothing(nestlocation) + @walk("random", 20) #XXX magic number else - # wait while nest is being built, then lay eggs and go to next phase - if self.nestcompletion > 0 - self.nestcompletion -= 1 - else - #XXX more accurately, a female lays one egg per day, not all at once - @reproduce(@rand(self.eggsperclutch),self.mate) - @setphase(breeding) - end + # if we've found a location, start the clock on the building time + # (building time doubles for the first nest of the year) + self.nest = nestlocation + self.nestcompletion = @rand(self.nestbuildingtime) + month(model.date) == self.nestingbegin[1] && (self.nestcompletion *= 2) + @debug "$(animalid(self)) is building a nest." end else - # males stay near the female - @follow(model[self.mate], 5) - @animal(self.mate).phase == "breeding" && @setphase(breeding) + # wait while nest is being built, then lay eggs and go to next phase + if self.nestcompletion > 0 + self.nestcompletion -= 1 + else + #XXX more accurately, a female lays one egg per day, not all at once + @reproduce(@rand(self.eggsperclutch),self.mate) + @setphase(breeding) + end end + else + # males stay near the female + mate = @animal(self.mate) + @follow(mate, 5) + mate.phase == breeding && @setphase(breeding) end +end - """ - Do lots of foraging (not yet implemented). - """ - @phase breeding begin - #TODO forage (move random) - for offspring in self.clutch - # check if offspring are still alive and juvenile, else remove from clutch - if !@isalive(offspring) || @animal(offspring).phase == "nonbreeding" - deleteat!(self.clutch, findfirst(x->x==offspring, self.clutch)) - end - end - # if all young have fledged, move to nonbreeding (if it's July) or breed again - if isempty(self.clutch) - self.nest = () - month(model.date) >= 7 ? - @setphase(nonbreeding) : - @setphase(nestbuilding) +""" +Do lots of foraging (not yet implemented). +""" +@phase Skylark breeding begin + #TODO forage (move random) + for offspring in self.clutch + # check if offspring are still alive and juvenile, else remove from clutch + if !@isalive(offspring) || @animal(offspring).phase == nonbreeding + filter!(x -> x != offspring, self.clutch) end end + # if all young have fledged, move to nonbreeding (if it's July) or breed again + if isempty(self.clutch) + self.nest = () + month(model.date) >= self.nestingend ? + @setphase(nonbreeding) : + @setphase(nestbuilding) + end +end + +""" + 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::Animal, 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 = monthday(Date(2001, minleave[1], minleave[2]) + Day(deltaleave)) + arrive = monthday(Date(2001, minarrive[1], minarrive[2]) + Day(deltaarrive)) + (leave, arrive) end """ Initialise a skylark individual. Selects migration dates and checks if the bird should currently be on migration. Also sets other individual-specific variables. """ -@phase Skylark.initindividual begin +@create Skylark begin @debug "Added $(animalid(self)) at $(self.pos)" # calculate migration dates for this individual self.migrationdates = migrationdates(self, model) @@ -231,21 +252,10 @@ should currently be on migration. Also sets other individual-specific variables. #TODO other stuff? end -""" - 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::Animal, model::SimulationModel) - #TODO this ought to be temperature-dependent and dynamic - #XXX magic numbers! - minleave = skylark.sex == female ? (9, 15) : (10, 1) - minarrive = skylark.sex == male ? (2, 15) : (3, 1) - deltaleave = @rand(0:45) #XXX ought to be normally distributed - deltaarrive = @rand(0:15) #XXX ought to be normally distributed - leave = monthday(Date(2001, minleave[1], minleave[2]) + Day(deltaleave)) - arrive = monthday(Date(2001, minarrive[1], minarrive[2]) + Day(deltaarrive)) - (leave, arrive) +@populate Skylark begin + habitat = skylarkhabitat + initphase = mating + birthphase = egg + popdensity=300 #XXX use Unitful.jl + pairs=true end - diff --git a/src/nature/species/wolpertinger.jl b/src/nature/species/wolpertinger.jl index c3b59303bb5ddd97a423b4a6e7022aad88ad2bd4..83177fcebb049657ca6a87a9d30c7ab707b7737a 100644 --- a/src/nature/species/wolpertinger.jl +++ b/src/nature/species/wolpertinger.jl @@ -43,10 +43,11 @@ Wolpertingers are mythical creatures that require no special initialisation. end """ -Population densities of the endangered Wolpertinger are down to 1 animal per 10km². +Population densities of the endangered Wolpertinger are down to 1 animal per km². """ @populate Wolpertinger begin asexual = true - phase = lifephase - popdensity = 100000 #XXX use Unitful.jl for conversion? + initphase = lifephase + birthphase = lifephase + popdensity = 10000 #XXX use Unitful.jl for conversion? end diff --git a/src/nature/species/wyvern.jl b/src/nature/species/wyvern.jl index e18b99fe0ce514177fd7a2ecaf49b535a3cce8e8..d12c3952f335dca726bfb20e0228853d8633013b 100644 --- a/src/nature/species/wyvern.jl +++ b/src/nature/species/wyvern.jl @@ -67,7 +67,7 @@ prey: wolpertingers... @label reproduce (@rand() < self.fecundity) && @reproduce() # hibernate from November to March - month(model.date) >= 11 && (@setphase(winter)) + month(model.date) >= November && (@setphase(winter)) (self.age == self.maxage) && @kill(1.0, "old age") end @@ -76,7 +76,7 @@ Fortunately, wyverns hibernate in winter. """ @phase Wyvern winter begin # hibernate from November to March - if month(model.date) >= 3 + if month(model.date) >= March @setphase(summer) end end @@ -84,6 +84,7 @@ end @populate Wyvern begin asexual = true popsize = 20 - phase = winter + initphase = winter + birthphase = summer habitat = @habitat(@landcover() in (grass, soil, agriculture, builtup)) end diff --git a/src/parameters.toml b/src/parameters.toml index c3d9f57600a718ac1dc7d9a3423ed68d62bdf915..fe701fb3db2e8169edba7638ab1019e7be73c539 100644 --- a/src/parameters.toml +++ b/src/parameters.toml @@ -30,7 +30,7 @@ weatherfile = "data/regions/jena-small/weather.csv" # location of the weather da farmmodel = "FieldManager" # which version of the farm model to use (not yet implemented) [nature] -targetspecies = ["Wolpertinger", "Wyvern"]#["Skylark"] # list of target species to simulate +targetspecies = ["Skylark"] #["Wolpertinger", "Wyvern"] # list of target species to simulate popoutfreq = "daily" # output frequency population-level data, daily/monthly/yearly/end/never indoutfreq = "end" # output frequency individual-level data, daily/monthly/yearly/end/never insectmodel = ["season", "habitat", "pesticides", "weather"] # factors affecting insect growth diff --git a/src/world/landscape.jl b/src/world/landscape.jl index 1a9d09308c6ac706263020b688553a377481ccf7..ac6cc801b136fe8c02a00250ed7e5a354906c4d6 100644 --- a/src/world/landscape.jl +++ b/src/world/landscape.jl @@ -169,7 +169,7 @@ function distanceto(pos::Tuple{Int64,Int64}, model::SimulationModel, habitatdesc #TODO test target = directionto(pos, model, habitatdescriptor) isnothing(target) && return Inf - return maximum(abs.(target-pos)) + return maximum(abs.(target.-pos)) end """