Skip to content
Snippets Groups Projects
Commit 36f8a06f authored by xo30xoqa's avatar xo30xoqa
Browse files

Added lots of functions and macros for the species DSL

parent 4de6c065
No related branches found
No related tags found
No related merge requests found
...@@ -9,6 +9,15 @@ Modules = [Persefone] ...@@ -9,6 +9,15 @@ Modules = [Persefone]
Pages = ["nature/nature.jl"] Pages = ["nature/nature.jl"]
``` ```
## macros.jl
This file contains all the macros that can be used in the species DSL.
```@autodocs
Modules = [Persefone]
Pages = ["nature/macros.jl"]
```
## populations.jl ## populations.jl
This file contains a set of utility functions for species, including initialisation, This file contains a set of utility functions for species, including initialisation,
......
...@@ -31,7 +31,7 @@ function visualisemap(model::AgentBasedModel,date=nothing,landcover=nothing) ...@@ -31,7 +31,7 @@ function visualisemap(model::AgentBasedModel,date=nothing,landcover=nothing)
#XXX other colour schemes: :tab10, :Accent_8, :Dark2_8, :Paired_12, :Set1_9 #XXX other colour schemes: :tab10, :Accent_8, :Dark2_8, :Paired_12, :Set1_9
# https://juliagraphics.github.io/ColorSchemes.jl/stable/catalogue/ # https://juliagraphics.github.io/ColorSchemes.jl/stable/catalogue/
update_theme!(palette=(color=cgrad(:seaborn_bright, update_theme!(palette=(color=cgrad(:seaborn_bright,
length(@param(nature.targetspecies))),), max(2, length(@param(nature.targetspecies)))),),
cycle=[:color]) cycle=[:color])
for s in @param(nature.targetspecies) for s in @param(nature.targetspecies)
points = @select!(@subset(inds, :Species .== s, :Date .== date), points = @select!(@subset(inds, :Species .== s, :Date .== date),
...@@ -54,12 +54,13 @@ Returns a Makie figure object. ...@@ -54,12 +54,13 @@ Returns a Makie figure object.
function populationtrends(model::AgentBasedModel) function populationtrends(model::AgentBasedModel)
pops = model.datatables["populations"] pops = model.datatables["populations"]
update_theme!(palette=(color=cgrad(:seaborn_bright, update_theme!(palette=(color=cgrad(:seaborn_bright,
length(@param(nature.targetspecies))),), max(2, length(@param(nature.targetspecies)))),),
cycle=[:color]) cycle=[:color])
f = Figure() f = Figure()
dates = @param(core.startdate):@param(core.enddate) dates = @param(core.startdate):@param(core.enddate)
ax = Axis(f[1,1], xlabel="Date", ylabel="Population size", ax = Axis(f[1,1], xlabel="Date", ylabel="Population size",
limits=((1, length(dates)), nothing), xticks = gettickmarks(dates)) limits=((1, length(dates)), nothing), xticks = gettickmarks(dates))
#XXX Y axis doesn't reach 0?
for s in @param(nature.targetspecies) for s in @param(nature.targetspecies)
points = @select!(@subset(pops, :Species .== s), :Abundance) points = @select!(@subset(pops, :Species .== s), :Abundance)
iszero(size(points)[1]) && continue iszero(size(points)[1]) && continue
......
...@@ -28,6 +28,7 @@ Carry out a complete simulation run using a pre-initialised model object. ...@@ -28,6 +28,7 @@ Carry out a complete simulation run using a pre-initialised model object.
function simulate!(model::AgentBasedModel) function simulate!(model::AgentBasedModel)
@info "Simulation run started at $(Dates.now())." @info "Simulation run started at $(Dates.now())."
runtime = Dates.value(@param(core.enddate)-@param(core.startdate))+1 runtime = Dates.value(@param(core.enddate)-@param(core.startdate))+1
#TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
step!(model, dummystep, stepsimulation!, runtime) step!(model, dummystep, stepsimulation!, runtime)
finalise!(model) finalise!(model)
end end
...@@ -58,6 +59,7 @@ Initialise a model object using a ready-made settings dict. This is ...@@ -58,6 +59,7 @@ Initialise a model object using a ready-made settings dict. This is
a helper function for `initialise()`. a helper function for `initialise()`.
""" """
function initmodel(settings::Dict{String, Any}) function initmodel(settings::Dict{String, Any})
#TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
@debug "Initialising model object." @debug "Initialising model object."
createdatadir(settings["core.outdir"], settings["core.overwrite"]) createdatadir(settings["core.outdir"], settings["core.overwrite"])
logger = modellogger(settings["core.loglevel"], settings["core.outdir"]) logger = modellogger(settings["core.loglevel"], settings["core.outdir"])
...@@ -127,6 +129,7 @@ Execute one update of the model. ...@@ -127,6 +129,7 @@ Execute one update of the model.
function stepsimulation!(model::AgentBasedModel) function stepsimulation!(model::AgentBasedModel)
with_logger(model.logger) do with_logger(model.logger) do
@info "Simulating day $(model.date)." @info "Simulating day $(model.date)."
#TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
for a in Schedulers.ByType((Farmer,FarmPlot,Animal), true)(model) for a in Schedulers.ByType((Farmer,FarmPlot,Animal), true)(model)
try #The animal may have been killed try #The animal may have been killed
stepagent!(model[a], model) stepagent!(model[a], model)
......
...@@ -12,6 +12,7 @@ This represents one field, i.e. a collection of pixels with the same management. ...@@ -12,6 +12,7 @@ This represents one field, i.e. a collection of pixels with the same management.
This is the spatial unit with which the crop growth model and the farm model work. This is the spatial unit with which the crop growth model and the farm model work.
""" """
@agent FarmPlot GridAgent{2} begin @agent FarmPlot GridAgent{2} begin
#TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
pixels::Vector{Tuple{Int64, Int64}} pixels::Vector{Tuple{Int64, Int64}}
croptype::CropType croptype::CropType
phase::GrowthPhase phase::GrowthPhase
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
This is the agent type for the farm ABM. (Not yet implemented.) This is the agent type for the farm ABM. (Not yet implemented.)
""" """
@agent Farmer GridAgent{2} begin @agent Farmer GridAgent{2} begin
#TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
fields::Vector{FarmPlot} fields::Vector{FarmPlot}
croprotation::Vector{CropType} croprotation::Vector{CropType}
#TODO add AES #TODO add AES
......
...@@ -27,7 +27,10 @@ function savepopulationdata(model::AgentBasedModel) ...@@ -27,7 +27,10 @@ function savepopulationdata(model::AgentBasedModel)
pops = Dict{String,Int}(s=>0 for s = @param(nature.targetspecies)) pops = Dict{String,Int}(s=>0 for s = @param(nature.targetspecies))
for a in allagents(model) for a in allagents(model)
(typeof(a) != Animal) && continue (typeof(a) != Animal) && continue
pops[a.traits["name"]] += 1 pops[a.name] += 1
end
for a in model.migrants
pops[a.first.name] += 1
end end
data = [] data = []
for p in keys(pops) for p in keys(pops)
...@@ -45,6 +48,7 @@ monthly, yearly, or at the end of a simulation, depending on the parameter ...@@ -45,6 +48,7 @@ monthly, yearly, or at the end of a simulation, depending on the parameter
`nature.indoutfreq`. WARNING: Produces very big files! `nature.indoutfreq`. WARNING: Produces very big files!
""" """
function saveindividualdata(model::AgentBasedModel) function saveindividualdata(model::AgentBasedModel)
#XXX doesn't include migrants!
data = [] data = []
for a in allagents(model) for a in allagents(model)
(typeof(a) != Animal) && continue (typeof(a) != Animal) && continue
......
...@@ -103,7 +103,7 @@ variables: ...@@ -103,7 +103,7 @@ variables:
Several utility macros can be used within the body of `@phase` as a short-hand for Several utility macros can be used within the body of `@phase` as a short-hand for
common expressions: [`@trait`](@ref), [`@setphase`](@ref), [`@respond`](@ref), common expressions: [`@trait`](@ref), [`@setphase`](@ref), [`@respond`](@ref),
[`@kill`](@ref), [`@reproduce`](@ref), [`@neighbours`](@ref), [`@rand`](@ref), [`@kill`](@ref), [`@reproduce`](@ref), [`@neighbours`](@ref), [`@rand`](@ref),
[`@shuffle!`](@ref). [`@shuffle!`](@ref), [`@move`](@ref), [`@walk`](@ref), [`@follow`](@ref).
Note that the first phase that is defined in a species definition block will be Note that the first phase that is defined in a species definition block will be
the phase that animals are assigned at birth, unless the variable `phase` is the phase that animals are assigned at birth, unless the variable `phase` is
...@@ -114,6 +114,7 @@ macro phase(name, body) ...@@ -114,6 +114,7 @@ macro phase(name, body)
quote quote
Core.@__doc__ function $(esc(name))($(esc(:animal))::Animal, $(esc(:model))::AgentBasedModel) Core.@__doc__ function $(esc(name))($(esc(:animal))::Animal, $(esc(:model))::AgentBasedModel)
$(esc(:pos)) = $(esc(:animal)).pos $(esc(:pos)) = $(esc(:animal)).pos
#$(esc(:date)) = $(esc(:model)).date #XXX does this make sense?
$(esc(body)) $(esc(body))
end end
($(esc(:phase)) == "") && ($(esc(:phase)) = $(String(name))) ($(esc(:phase)) == "") && ($(esc(:phase)) = $(String(name)))
...@@ -127,6 +128,8 @@ A utility macro to quickly access an animal's trait value. ...@@ -127,6 +128,8 @@ A utility macro to quickly access an animal's trait value.
This can only be used nested within [`@phase`](@ref). This can only be used nested within [`@phase`](@ref).
""" """
macro trait(traitname) macro trait(traitname)
#TODO provide a version that can access another animal's traits
#XXX replace with an @v macro? (shorter, and not all variables are "traits")
#XXX This would error if called in the first part of a species definition block #XXX This would error if called in the first part of a species definition block
# (i.e. outside of a @phase block). Although this is specified in the documentation, # (i.e. outside of a @phase block). Although this is specified in the documentation,
# it is unexpected and liable to be overlooked. Can we add a third clause to # it is unexpected and liable to be overlooked. Can we add a third clause to
...@@ -134,6 +137,28 @@ macro trait(traitname) ...@@ -134,6 +137,28 @@ macro trait(traitname)
:($(esc(:animal)).$(traitname)) :($(esc(:animal)).$(traitname))
end end
"""
@animal(id)
Return the animal/agent object associated with this ID number.
This can only be used in a context where the `model` object is available
(e.g. nested within [`@phase`](@ref)).
"""
macro animal(id)
:($(esc(:model))[$(id)])
end
"""
@isalive(id)
Test whether the animal with the given ID is still alive.
This can only be used in a context where the `model` object is available
(e.g. nested within [`@phase`](@ref)).
"""
macro isalive(id)
:(isalive($(id), $(esc(:model))))
end
""" """
@setphase(newphase) @setphase(newphase)
...@@ -300,6 +325,17 @@ macro distancetoedge() ...@@ -300,6 +325,17 @@ macro distancetoedge()
:(distancetoedge($(esc(:pos)), $(esc(:model)))) :(distancetoedge($(esc(:pos)), $(esc(:model))))
end end
"""
@randompixel(range, habitatdescriptor)
Find a random pixel within a given `range` of the animal's location that matches the
habitatdescriptor (create this using [`@habitat`](@ref)). This is a utility wrapper
that can only be used nested within [`@phase`](@ref).
"""
macro randompixel(args...)
:(randompixel($(esc(:pos)), $(esc(:model)), $(map(esc, args)...)))
end
""" """
@countanimals(species="", radius=0) @countanimals(species="", radius=0)
...@@ -308,7 +344,53 @@ This is a utility wrapper that can only be used nested within [`@phase`](@ref) ...@@ -308,7 +344,53 @@ This is a utility wrapper that can only be used nested within [`@phase`](@ref)
or [`@habitat`](@ref). or [`@habitat`](@ref).
""" """
macro countanimals(args...) macro countanimals(args...)
:(countanimals($(esc(:pos)), $(esc(:model)); $(map(esc, args)...))) :(countanimals($(esc(:pos)), $(esc(:model))))
end
##TODO test movement macros
"""
@move(position)
Move the current individual to a new position.
This is a utility wrapper that can only be used nested within [`@phase`](@ref).
"""
macro move(position)
:(move_agent!($(esc(:animal)), $(esc(position)), $(esc(:model))))
end
"""
@walk(direction)
Walk the animal in a given direction, which is specified by a tuple of coordinates
relative to the animal's current position (i.e. `(2, -3)` increments the X coordinate
by 2 and decrements the Y coordinate by 3.) This is a utility wrapper that can only be
used nested within [`@phase`](@ref).
"""
macro walk(direction)
#XXX add `ifempty` keyword?
:(walk!($(esc(:animal)), $(esc(direction)), $(esc(:model))))
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)
#XXX add `ifempty` keyword?
:(randomwalk!($(esc(:animal)), $(esc(:model)), $(esc(distance))))
end
#TODO add own walking functions that respect habitat descriptors
"""
@follow(leader, distance)
Move to a location within the given distance of the leading animal.
This is a utility wrapper that can only be used nested within [`@phase`](@ref).
"""
macro follow(leader, distance)
:(followanimal!($(esc(:animal)), $(esc(leader)), $(esc(:model)), $(esc(distance))))
end end
##TODO add movement macros
...@@ -18,6 +18,7 @@ trait variable can still be accessed as if it were a normal field name, ...@@ -18,6 +18,7 @@ trait variable can still be accessed as if it were a normal field name,
i.e. the trait `phase` can be accessed and modified with `animal.phase`.) i.e. the trait `phase` can be accessed and modified with `animal.phase`.)
""" """
@agent Animal GridAgent{2} begin @agent Animal GridAgent{2} begin
#TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
#XXX is it (performance-)wise to use a dict for the traits? #XXX is it (performance-)wise to use a dict for the traits?
# Doesn't that rather obviate the point of having an agent struct? # Doesn't that rather obviate the point of having an agent struct?
# If I could move the mutable traits to the struct, I wouldn't need # If I could move the mutable traits to the struct, I wouldn't need
...@@ -72,7 +73,7 @@ Update an animal by one day, executing it's currently active phase function. ...@@ -72,7 +73,7 @@ Update an animal by one day, executing it's currently active phase function.
""" """
function stepagent!(animal::Animal, model::AgentBasedModel) function stepagent!(animal::Animal, model::AgentBasedModel)
animal.age += 1 animal.age += 1
animal.traits[animal.phase](animal,model) #FIXME animal.traits[animal.phase](animal,model) #FIXME -> note to self: why?
end end
""" """
......
...@@ -103,13 +103,16 @@ The `mate` argument gives the ID of the reproductive partner. ...@@ -103,13 +103,16 @@ The `mate` argument gives the ID of the reproductive partner.
""" """
function reproduce!(animal::Animal, model::AgentBasedModel, mate::Int64, n::Int64=1) function reproduce!(animal::Animal, model::AgentBasedModel, mate::Int64, n::Int64=1)
(animal.sex == male) && @warn "Male $(animalid(animal)) is reproducing." (animal.sex == male) && @warn "Male $(animalid(animal)) is reproducing."
offspring = []
for i in 1:n for i in 1:n
sex = (animal.sex == hermaphrodite) ? hermaphrodite : @rand([male, female]) sex = (animal.sex == hermaphrodite) ? hermaphrodite : @rand([male, female])
# We need to generate a fresh species dict here # We need to generate a fresh species dict here
species = @eval $(Symbol(animal.traits["name"]))($model) species = @eval $(Symbol(animal.traits["name"]))($model)
add_agent!(animal.pos, Animal, model, species, (animal.id, mate), sex, 0) a = add_agent!(animal.pos, Animal, model, species, (animal.id, mate), sex, 0)
push!(offspring, a.id)
end end
@debug "$(animalid(animal)) has reproduced." @debug "$(animalid(animal)) has reproduced."
offspring
end end
""" """
...@@ -146,6 +149,15 @@ function migrate!(animal::Animal, model::AgentBasedModel, arrival::Date) ...@@ -146,6 +149,15 @@ function migrate!(animal::Animal, model::AgentBasedModel, arrival::Date)
@debug "$(animalid(animal)) has migrated." @debug "$(animalid(animal)) has migrated."
end end
"""
isalive(id, model)
Test whether the animal with the given ID is still alive.
"""
function isalive(animalid::Int64, model::AgentBasedModel)
animalid in allids(model)
end
""" """
nearby_animals(pos, model, radius) nearby_animals(pos, model, radius)
...@@ -183,4 +195,18 @@ function countanimals(pos::Tuple{Int64,Int64}, model::AgentBasedModel; ...@@ -183,4 +195,18 @@ function countanimals(pos::Tuple{Int64,Int64}, model::AgentBasedModel;
return n return n
end end
##TODO add movement functions """
followanimal!(follower, leader, model, distance=0)
Move the follower animal to a location near the leading animal.
"""
function followanimal!(follower::Animal, leader::Animal, model::AgentBasedModel,
distance::Int64=0)
#TODO test function
spacing = Tuple(@rand(-distance:distance, 2))
targetposition = safebounds(spacing .+ leader.pos, model)
move_agent!(follower, targetposition, model)
end
##TODO add random walk with habitat descriptor
...@@ -126,6 +126,7 @@ Calculate the distance from the given location to the closest location matching ...@@ -126,6 +126,7 @@ Calculate the distance from the given location to the closest location matching
habitat descriptor function. Caution: can be computationally expensive! habitat descriptor function. Caution: can be computationally expensive!
""" """
function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitatdescriptor::Function) function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitatdescriptor::Function)
#XXX allow testing for multiple habitat types?
(habitatdescriptor(pos, model)) && (return 0) (habitatdescriptor(pos, model)) && (return 0)
dist = 1 dist = 1
width, height = size(model.landscape) width, height = size(model.landscape)
...@@ -160,8 +161,7 @@ Calculate the distance from the given location to the closest habitat of the spe ...@@ -160,8 +161,7 @@ Calculate the distance from the given location to the closest habitat of the spe
Caution: can be computationally expensive! Caution: can be computationally expensive!
""" """
function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitattype::LandCover) function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitattype::LandCover)
#XXX allow testing for multiple habitat types? # can't use @habitat here because macros.jl is loaded later than this file
# can't use @habitat here because nature.jl is loaded later than this file
distanceto(pos, model, function(p,m) landcover(p,m) == habitattype end) distanceto(pos, model, function(p,m) landcover(p,m) == habitattype end)
end end
...@@ -172,7 +172,48 @@ Calculate the distance from the given location to the closest neighbouring habit ...@@ -172,7 +172,48 @@ Calculate the distance from the given location to the closest neighbouring habit
Caution: can be computationally expensive! Caution: can be computationally expensive!
""" """
function distancetoedge(pos::Tuple{Int64,Int64}, model::AgentBasedModel) function distancetoedge(pos::Tuple{Int64,Int64}, model::AgentBasedModel)
# can't use @habitat here because nature.jl is loaded later than this file # can't use @habitat here because macros.jl is loaded later than this file
distanceto(pos, model, function(p,m) landcover(p,m) != landcover(pos, model) end) distanceto(pos, model, function(p,m) landcover(p,m) != landcover(pos, model) end)
end end
"""
randompixel(position, model, range, habitatdescriptor)
Find a random pixel within a given `range` of the `position` that matches the
habitatdescriptor (create this using [`@habitat`](@ref)).
"""
function randompixel(pos::Tuple{Int64,Int64}, model::AgentBasedModel, range::Int64=1,
habitatdescriptor::Function=(pos,model)->nothing)
for x in @shuffle!((pos[1]-range):(pos[1]+range))
for y in @shuffle!((pos[2]-range):(pos[2]+range))
!inbounds((x,y), model) && continue
habitatdescriptor((x,y), model) && return (x,y)
end
end
nothing
end
"""
inbounds(pos, model)
Is the given position within the bounds of the model landscape?
"""
function inbounds(pos, model)
dims = size(model.landscape)
pos[1] > 0 && pos[1] <= dims[1] && pos[2] > 0 && pos[2] <= dims[2]
end
"""
safebounds(pos, model)
Make sure that a given position is within the bounds of the model landscape.
"""
function safebounds(pos::Tuple{Int64,Int64}, model::AgentBasedModel)
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)
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please to comment