From 2db97204a8b33a5dd7005d379c5aa4c498750dc9 Mon Sep 17 00:00:00 2001 From: Daniel Vedder <daniel.vedder@idiv.de> Date: Wed, 17 Jul 2024 10:43:01 +0200 Subject: [PATCH] Implemented animal territories. (Not yet tested.) --- src/nature/macros.jl | 45 +++++++++++++++++++++++++++++-- src/nature/nature.jl | 1 + src/nature/populations.jl | 56 ++++++++++++++++++++++++++++++++++++++- src/world/landscape.jl | 37 +++++++++++++++++--------- test/nature_tests.jl | 17 ++++++------ test/runtests.jl | 4 +-- 6 files changed, 134 insertions(+), 26 deletions(-) diff --git a/src/nature/macros.jl b/src/nature/macros.jl index 237782a..264b1f6 100644 --- a/src/nature/macros.jl +++ b/src/nature/macros.jl @@ -45,8 +45,9 @@ macro species(name, body) pos::Tuple{Int64,Int64} phase::Function = (self,model)->nothing age::Int = 0 - energy::Union{EnergyBudget,Nothing} = nothing # DEB is optional + energy::Union{EnergyBudget,Nothing} = nothing # DEB is optional #TODO remove? offspring::Vector{Int64} = Vector{Int64}() + territory::Vector{Tuple{Int64,Int64}} = Vector{Tuple{Int64,Int64}}() $(body.args...) end # define a constructor giving the minimum necessary arguments as positional arguments @@ -106,7 +107,7 @@ variables: Many macros are available to make the code within the body of `@phase` more succinct. Some of the most important of these are: [`@setphase`](@ref), [`@respond`](@ref), [`@kill`](@ref), [`@reproduce`](@ref), [`@neighbours`](@ref), [`@migrate`](@ref), -[`@move`](@ref), [`@rand`](@ref). +[`@move`](@ref), [`@occupy`](@ref), [`@rand`](@ref). """ macro phase(species, phase, body) quote @@ -237,6 +238,46 @@ macro migrate(arrival) :(migrate!($(esc(:self)), $(esc(:model)), $(esc(arrival)))) end +""" + @occupy(position) + +Add the given position to this animal's territory. Use [`@vacate`](@ref) to +remove positions from the territory again. This can only be used nested within [`@phase`](@ref). +""" +macro occupy(position) + :(occupy!($(esc(:self)), $(esc(:model)), $(esc(position)))) +end + +""" + @isoccupied(position) + +Test whether this position is already occupied by an animal of this species. +This can only be used nested within [`@phase`](@ref). +""" +macro isoccupied(position) + :(isoccupied($(esc(:model)), speciesof($(esc(:self))), $(esc(position)))) +end + +""" + @vacate(position) + +Remove the given position from this animal's territory. +This can only be used nested within [`@phase`](@ref). +""" +macro vacate(position) + :(vacate!($(esc(:self)), $(esc(:model)), $(esc(position)))) +end + +""" + @vacate() + +Remove this animal's complete territory. +This can only be used nested within [`@phase`](@ref). +""" +macro vacate() + :(vacate!($(esc(:self)), $(esc(:model)))) +end + """ @habitat diff --git a/src/nature/nature.jl b/src/nature/nature.jl index 85e753d..d236343 100644 --- a/src/nature/nature.jl +++ b/src/nature/nature.jl @@ -24,6 +24,7 @@ fields, all species contain the following fields: - `phase` The update function to be called during the individual's current life phase. - `energy` A [DEBparameters](@ref) struct for calculating energy budgets. - `offspring` A vector containing the IDs of an individual's children. +- `territory` A vector of coordinates that comprise the individual's territory. """ abstract type Animal <: ModelAgent end diff --git a/src/nature/populations.jl b/src/nature/populations.jl index 187157e..1b2799a 100644 --- a/src/nature/populations.jl +++ b/src/nature/populations.jl @@ -4,6 +4,8 @@ ### reproduction, and mortality. ### +##TODO move the life-history functions into a new file `individuals.jl` + """ PopInitParams @@ -175,9 +177,14 @@ Returns true if the animal dies, false if not. function kill!(animal::Animal, model::SimulationModel, probability::Float64=1.0, cause::String="") if @rand() < probability + # remove the animal's location and territory pointers from the landscape + filter!(x -> x!=animal.id, model.landscape[animal.pos...].animals) + for pos in animal.territory + filter!(x -> x!=animal.id, model.landscape[pos...].territories) + end + # print the epitaph and remove the animal from the model postfix = isempty(cause) ? "." : " from $cause." @debug "$(animalid(animal)) has died$(postfix)" - filter!(x -> x!=animal.id, model.landscape[animal.pos...].animals) model.animals[animal.id] = nothing return true end @@ -204,6 +211,53 @@ function migrate!(animal::Animal, model::SimulationModel, arrival::Date) @debug "$(animalid(animal)) has migrated." end +""" + occupy!(animal, model, position) + +Add the given location to the animal's territory. +""" +function occupy!(animal::Animal, model::SimulationModel, position::Tuple{Int64,Int64}) + if isoccupied(model, speciesof(animal), position) #XXX should this be an error? + @warn "Position $position is already occupied by a $(speciesof(animal))." + end + push!(animal.territories, position) + push!(model.landscape[position...], animal.id) +end + +""" + isoccupied(model, position, species) + +Test whether this location is part of the territory of an animal of the given species. +""" +function isoccupied(model::SimulationModel, species::String, position::Tuple{Int64,Int64}) + for terr in model.landscape[position...].territories + (speciesof(model.animals[terr]) == species) && return true + end + return false +end + +""" + vacate!(animal, model, position) + +Remove this position from the animal's territory. +""" +function vacate!(animal::Animal, model::SimulationModel, position::Tuple{Int64,Int64}) + filter!(x -> x!=position, animal.territory) + filter!(x -> x!=animal.id, model.landscape[position...].territories) +end + +""" + vacate!(animal, model) + +Remove the animal's complete territory. +""" +function vacate!(animal::Animal, model::SimulationModel) + for pos in animal.territory + filter!(x -> x!=animal.id, model.landscape[pos...].territories) + end + animal.territory = Vector{Tuple{Int64,Int64}}() +end + """ isalive(id, model) diff --git a/src/world/landscape.jl b/src/world/landscape.jl index 4f6a930..7d356a8 100644 --- a/src/world/landscape.jl +++ b/src/world/landscape.jl @@ -9,7 +9,6 @@ "The types of management event that can be simulated" @enum Management tillage sowing fertiliser pesticide harvesting -#XXX rename to Management or similar? """ Pixel @@ -19,12 +18,17 @@ in a single object. The model landscape consists of a matrix of pixels. (Note: further landscape information may be added here in future.) """ mutable struct Pixel - landcover::LandCover - fieldid::Union{Missing,Int64} - events::Vector{Management} - animals::Vector{Int64} + landcover::LandCover # land cover class at this position + fieldid::Union{Missing,Int64} # ID of the farmplot (if any) at this position + events::Vector{Management} # management events that have been applied to this pixel + animals::Vector{Int64} # IDs of animals currently at this position + territories::Vector{Int64} # IDs of animals that claim this pixel as part of their territory end +Pixel(landcover::LandCover, fieldid::Union{Missing,Int64}) = + Pixel(landcover, fieldid, Vector{Management}(), Vector{Int64}(), Vector{Int64}()) +Pixel(landcover::LandCover) = Pixel(landcover, missing) + """ FarmEvent @@ -63,7 +67,7 @@ function initlandscape(directory::String, landcovermap::String, farmfieldsmap::S lcv = LandCover(Int(landcover[x,y][1]/10)) ff = Int64(farmfields[x,y][1]) (iszero(ff)) && (ff = missing) - landscape[x,y] = Pixel(lcv, ff, Vector{Symbol}(), []) + landscape[x,y] = Pixel(lcv, ff) end end return landscape @@ -236,6 +240,18 @@ 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) @@ -252,11 +268,6 @@ end Make sure that a given position is within the bounds of the model landscape. """ function safebounds(pos::Tuple{Int64,Int64}, model::SimulationModel) - dims = size(model.landscape) - x, y = pos - x <= 0 && (x = 1) - x > dims[1] && (x = dims[1]) - y <= 0 && (y = 1) - y > dims[2] && (y = dims[2]) - (x,y) + width, height = size(model.landscape) + (bounds(pos[1], max=width, min=1), bounds(pos[2], max=height, min=1)) end diff --git a/test/nature_tests.jl b/test/nature_tests.jl index 4d2d308..afeab5e 100644 --- a/test/nature_tests.jl +++ b/test/nature_tests.jl @@ -47,7 +47,7 @@ end) # end eval @testset "Habitat macros" begin # set up the testing landscape model = inittestmodel() - model.landscape[6,6] = Pixel(Ps.agriculture, 1, [], []) + model.landscape[6,6] = Pixel(Ps.agriculture, 1) push!(model.farmplots, FarmPlot(1, [(6,6)], model.crops["winter wheat"], Ps.janfirst, 0.0, 0.0, 0.0, 0.0, Vector{Ps.Management}())) @@ -165,6 +165,7 @@ end @test typeof(@rand()) == Float64 @test @rand([true, true]) #TODO test movement macros + #TODO test territory macros end #XXX I'm unsure whether to keep the insect submodel at all, so rather than @@ -175,12 +176,12 @@ end # date1 = Date("2023-05-08") # day 128 (season begin) # date2 = Date("2023-07-06") # day 187 (insect max) # date3 = Date("2023-09-27") # day 270 (season end) -# p1 = Pixel(Ps.agriculture, 1, [], []) -# p2 = Pixel(Ps.agriculture, 1, [Ps.pesticide], []) -# p3 = Pixel(Ps.grass, 1, [], []) -# p4 = Pixel(Ps.soil, 1, [Ps.fertiliser, Ps.pesticide], []) -# p5 = Pixel(Ps.forest, 1, [], []) -# p6 = Pixel(Ps.water, 1, [], []) +# p1 = Pixel(Ps.agriculture, 1) +# p2 = Pixel(Ps.agriculture, 1, [Ps.pesticide], [], []) +# p3 = Pixel(Ps.grass, 1) +# p4 = Pixel(Ps.soil, 1, [Ps.fertiliser, Ps.pesticide], [], []) +# p5 = Pixel(Ps.forest, 1) +# p6 = Pixel(Ps.water, 1) # # check whether the model calculates the same numbers I did by hand # model.date = date1 # #FIXME these tests are still broken because insectbiomass throws a unit error @@ -239,7 +240,7 @@ end # as no individuals are initialised for x in 1:6 for y in 5:6 - model.landscape[x,y] = Pixel(Ps.agriculture, missing, [], []) + model.landscape[x,y] = Pixel(Ps.agriculture) end end # test migration diff --git a/test/runtests.jl b/test/runtests.jl index ab6290f..b36a792 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -84,10 +84,10 @@ function smalltestlandscape() (x in (1:2)) ? lc = Ps.forest : (y == 8) ? lc = Ps.builtup : lc = Ps.grass - landscape[x,y] = Pixel(lc, missing, [], []) + landscape[x,y] = Pixel(lc) end end - landscape[6,4] = Pixel(Ps.water, 0, [], []) + landscape[6,4] = Pixel(Ps.water, 0) landscape end -- GitLab