From c587740215262e6dbbcfff6f5f71ec19de186b63 Mon Sep 17 00:00:00 2001 From: Daniel Vedder <daniel.vedder@idiv.de> Date: Thu, 16 May 2024 12:22:14 +0200 Subject: [PATCH] Reimplemented neighbour-finding functions This used to be done by Agents.jl, is now done natively --- src/nature/macros.jl | 25 +++--- src/nature/nature.jl | 19 ++++- src/nature/populations.jl | 118 ++++++++++++++++------------- src/nature/species/wolpertinger.jl | 12 +-- src/world/landscape.jl | 5 +- 5 files changed, 105 insertions(+), 74 deletions(-) diff --git a/src/nature/macros.jl b/src/nature/macros.jl index c4be686..5d6dcd6 100644 --- a/src/nature/macros.jl +++ b/src/nature/macros.jl @@ -77,7 +77,6 @@ Set the parameters that are used to initialise this species' population. For parameter options, see [`PopInitParams`](@ref). """ macro populate(species, params) - println("Will register $(species).") quote # convert the macro body to a dict Core.@__doc__ fun = function() # metaprogramming is great - this is a fun fun function ;-) @@ -232,17 +231,6 @@ macro migrate(arrival) :(migrate!($(esc(:self)), $(esc(:model)), $(esc(arrival)))) end -""" - @neighbours(radius) - -Return an iterator over all animals in the given radius around this animal, excluding itself. -This can only be used nested within [`@phase`](@ref). -""" -macro neighbours(radius) - #TODO enable filtering by species - :(nearby_animals($(esc(:self)), $(esc(:model)), $(esc(radius)))) -end - """ @habitat @@ -356,13 +344,24 @@ macro randompixel(args...) end """ - @countanimals(species="", radius=0) + @neighbours(radius=0) + +Return an iterator over all conspecific animals in the given radius around this animal, +excluding itself. This can only be used nested within [`@phase`](@ref). +""" +macro neighbours(args...) + :(neighbours($(esc(:self)), $(esc(:model)); $(map(esc, args)...))) +end + +""" + @countanimals(radius=0, species="") Count the number of animals of the given species in this location. This is a utility wrapper that can only be used nested within [`@phase`](@ref) or [`@habitat`](@ref). """ macro countanimals(args...) + #XXX do I want/need this? :(countanimals($(esc(:pos)), $(esc(:model)); $(map(esc, args)...))) end diff --git a/src/nature/nature.jl b/src/nature/nature.jl index b1aeb9e..11975b6 100644 --- a/src/nature/nature.jl +++ b/src/nature/nature.jl @@ -33,7 +33,24 @@ abstract type Animal <: ModelAgent end Return the species name of this animal as a string. """ function speciesof(a::Animal) - string(typeof(a)) + # strip out the module name if necessary (`Persefone.<species>`) + spstrings = split(string(typeof(a)), ".") + length(spstrings) == 1 ? spstrings[1] : spstrings[2] +end + +""" + speciestype(animal) + +Return the Type of this species. +""" +function speciestype(species::String) + # get the species Type from its namestring by looking in the module namespace + speciestype = nothing + for name in names(Persefone, all=true) + string(name) == species && (speciestype = getfield(Persefone, name)) + end + isnothing(speciestype) && @error("Species $species is not defined.") + speciestype end """ diff --git a/src/nature/populations.jl b/src/nature/populations.jl index 75a80c0..e7a8818 100644 --- a/src/nature/populations.jl +++ b/src/nature/populations.jl @@ -41,25 +41,25 @@ using [`@populate`](@ref). asexual::Bool = false end +""" + populationparameters(type) + +A function that returns a [`PopInitParams`](@ref) object for the given species type. +Parametric methods for each species are defined with [`@populate`](@ref). This is the +catch-all method, which throws an error if no species-specific function is defined. +""" populationparameters(t::Type) = @error("No population parameters defined for $(t), use @populate.") """ - initpopulation!(species, model) + initpopulation!(speciesname, model) Initialise the population of the given species, based on the parameters stored in [`PopInitParams`](@ref). Define these using [`@populate`](@ref). """ -function initpopulation!(species::String, model::SimulationModel) - # get the species Type from its namestring - speciestype = nothing - for name in names(Persefone, all=true) - string(name) == species && (speciestype = getfield(Persefone, name)) - end - isnothing(speciestype) && @error "Species $species is not defined." - #XXX can we get rid of the Persefone.<species> in the speciestype? - @debug "Initialising population of $species/$speciestype." +function initpopulation!(speciesname::String, model::SimulationModel) # get the PopInitParams and check for validity - p = populationparameters(speciestype) + species = speciestype(speciesname) + p = populationparameters(species) (p.popsize <= 0 && p.popdensity <= 0) && #XXX not sure what this would do @warn("initpopulation() called with popsize and popdensity both <= 0") (p.popsize > 0 && p.popdensity > 0) && #XXX not sure what this would do @@ -74,18 +74,19 @@ function initpopulation!(species::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 = speciestype(length(model.animals)+1, male, (-1, -1), (x,y), p.phase) - a2 = speciestype(length(model.animals)+1, female, (-1, -1), (x,y), p.phase) + 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) - push!(model.animals, a1) - push!(model.animals, a2) + push!(model.animals, a1, a2) + push!(model.landscape[x,y].animals, a1.id, a2.id) n += 2 else sex = p.asexual ? hermaphrodite : @rand([male, female]) - a = speciestype(length(model.animals)+1, sex, (-1, -1), (x,y), p.phase) + a = species(length(model.animals)+1, sex, (-1, -1), (x,y), p.phase) create!(a, model) push!(model.animals, a) + push!(model.landscape[x,y].animals, a.id) n += 1 end end @@ -121,10 +122,9 @@ function reproduce!(animal::Animal, model::SimulationModel, else sex = @rand([male, female]) end - #FIXME get birth phase - child = typeof(animal)(length(model.animals)+1, sex, - (animal.id, mate), animal.pos) - initindividual(child, model) + bphase = populationparameters(typeof(animal)).phase + 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) @@ -143,6 +143,7 @@ function kill!(animal::Animal, model::SimulationModel, if @rand() < probability 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 @@ -178,42 +179,59 @@ function isalive(animalid::Int64, model::SimulationModel) end """ - nearby_animals(pos, model, radius) + nearby_ids(pos, model, radius) -Return an iterator over all animals in the given radius around this position. +Return a list of IDs of the animals within a given radius of the position. """ -function nearby_animals(pos::Tuple{Int64,Int64}, model::SimulationModel, radius::Int64) - #FIXME remove agents.jl code - #TODO enable filtering by species - neighbours = (model[id] for id in nearby_ids(pos, model, radius)) - Iterators.filter(a -> typeof(a) == Animal, neighbours) +function nearby_ids(pos::Tuple{Int64,Int64}, model::SimulationModel, radius::Int64) + ids = [] + msize = size(model.landscape) + for x in (pos[1]-radius):(pos[1]+radius) + (x < 1 || x > msize[1]) && continue + for y in (pos[2]-radius):(pos[2]+radius) + (y < 1 || y > msize[2]) && continue + append!(ids, model.landscape[x,y].animals) + end + end + ids end + """ - nearby_animals(animal, model, radius) + nearby_animals(pos, model; radius= 0, species="") -Return an iterator over all animals in the given radius around this animal, excluding itself. +Return a list of animals in the given radius around this position, optionally filtering by species. """ -function nearby_animals(animal::Animal, model::SimulationModel, radius::Int64) - #FIXME remove agents.jl code - #TODO enable filtering by species - neighbours = (model[id] for id in nearby_ids(animal.pos, model, radius)) - Iterators.filter(a -> typeof(a) == Animal && a.id != animal.id, neighbours) +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 == "" + model.animals[neighbours] + else + filter(x -> speciesof(x) == species, model.animals[neighbours]) + end end """ - countanimals(pos, model; species="", radius=0) + neighbours(animal, model; radius=0) + +Return a list of conspecific animals in the given radius around this animal, excluding itself. +""" +function neighbours(animal::Animal, model::SimulationModel; radius::Int64=0) + filter(a -> a.id != animal.id, + nearby_animals(animal.pos, model, radius=radius, species=speciesof(animal))) +end + +""" + countanimals(pos, model; radius= 0, species="") Count the number of animals in this location (optionally supplying a species name and radius). """ function countanimals(pos::Tuple{Int64,Int64}, model::SimulationModel; - species::String="", radius::Int64=0) - n = 0 #FIXME fix nearby_animals - #XXX can we ignore capitalisation in the spelling of `species`? - for a in nearby_animals(pos, model, radius) - (species == "" || a.traits["name"] == species) && (n += 1) - end - return n + radius::Int64=0, species="") + #XXX do I want/need this? + length(nearby_animals(pos, model, radius=radius, species=species)) end """ @@ -233,18 +251,14 @@ end move!(animal, model, position) Move the animal to the given position, making sure that this is in-bounds. -Return true if successful, return false and leave the animal where it is if -the position is out of bounds. +If the position is out of bounds, the animal stops at the map edge. """ function move!(animal::Animal, model::SimulationModel, position::Tuple{Int64,Int64}) - width, height = size(model.landscape) - if position[1] < 0 || position[2] < 0 || position[1] > width || position[2] > height - #XXX should this be a warning? or just pass silently? - @debug "$(animalid(animal)) tried to move out of bounds to $position." - return false - end - animal.pos = position - return true + #XXX should this function give some sort of warning (e.g. return false) + # if the original position is not reachable? + filter!(x -> x != animal.id, model.landscape[animal.pos...].animals) + animal.pos = safebounds(position, model) + push!(model.landscape[animal.pos...].animals, animal.id) end """ diff --git a/src/nature/species/wolpertinger.jl b/src/nature/species/wolpertinger.jl index 20dabb5..02ea9c4 100644 --- a/src/nature/species/wolpertinger.jl +++ b/src/nature/species/wolpertinger.jl @@ -19,7 +19,7 @@ end """ Wolpertingers are rather stupid creatures, all they do is move around randomly -and occasionally reproduce by spontaneous parthogenesis... +and occasionally reproduce by spontaneous parthenogenesis... """ @phase Wolpertinger lifephase begin for i in 1:@rand(1:self.maxspeed) @@ -27,11 +27,10 @@ and occasionally reproduce by spontaneous parthogenesis... #walk!(animal, direction, model; ifempty=false) end - #FIXME need to fix countanimals() and reproduce() first - # if @rand() < self.fecundity && - # @countanimals(species="Wolpertinger") < self.crowding - # @reproduce() - # end + if @rand() < self.fecundity && + @countanimals(species="Wolpertinger") < self.crowding + @reproduce() + end @kill self.mortality end @@ -48,6 +47,7 @@ end Population densities of the endangered Wolpertinger are down to 1 animal per 10km². """ @populate Wolpertinger begin + asexual = true phase = lifephase popdensity = 100000 #XXX use Unitful.jl for conversion? end diff --git a/src/world/landscape.jl b/src/world/landscape.jl index 9beb16d..bd9af0f 100644 --- a/src/world/landscape.jl +++ b/src/world/landscape.jl @@ -9,6 +9,7 @@ "The types of landscape event that can be simulated" @enum EventType tillage sowing fertiliser pesticide harvesting +#XXX rename to Management or similar? """ Pixel @@ -21,7 +22,7 @@ mutable struct Pixel landcover::LandCover fieldid::Union{Missing,Int64} events::Vector{EventType} - #TODO add list of animal IDs + animals::Vector{Int64} end """ @@ -60,7 +61,7 @@ function initlandscape(landcovermap::String, farmfieldsmap::String) 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, Vector{Symbol}(), []) end end return landscape -- GitLab