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]
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
This file contains a set of utility functions for species, including initialisation,
......
......@@ -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
# https://juliagraphics.github.io/ColorSchemes.jl/stable/catalogue/
update_theme!(palette=(color=cgrad(:seaborn_bright,
length(@param(nature.targetspecies))),),
max(2, length(@param(nature.targetspecies)))),),
cycle=[:color])
for s in @param(nature.targetspecies)
points = @select!(@subset(inds, :Species .== s, :Date .== date),
......@@ -54,12 +54,13 @@ Returns a Makie figure object.
function populationtrends(model::AgentBasedModel)
pops = model.datatables["populations"]
update_theme!(palette=(color=cgrad(:seaborn_bright,
length(@param(nature.targetspecies))),),
max(2, length(@param(nature.targetspecies)))),),
cycle=[:color])
f = Figure()
dates = @param(core.startdate):@param(core.enddate)
ax = Axis(f[1,1], xlabel="Date", ylabel="Population size",
limits=((1, length(dates)), nothing), xticks = gettickmarks(dates))
#XXX Y axis doesn't reach 0?
for s in @param(nature.targetspecies)
points = @select!(@subset(pops, :Species .== s), :Abundance)
iszero(size(points)[1]) && continue
......
......@@ -28,6 +28,7 @@ Carry out a complete simulation run using a pre-initialised model object.
function simulate!(model::AgentBasedModel)
@info "Simulation run started at $(Dates.now())."
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)
finalise!(model)
end
......@@ -58,6 +59,7 @@ Initialise a model object using a ready-made settings dict. This is
a helper function for `initialise()`.
"""
function initmodel(settings::Dict{String, Any})
#TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
@debug "Initialising model object."
createdatadir(settings["core.outdir"], settings["core.overwrite"])
logger = modellogger(settings["core.loglevel"], settings["core.outdir"])
......@@ -127,6 +129,7 @@ Execute one update of the model.
function stepsimulation!(model::AgentBasedModel)
with_logger(model.logger) do
@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)
try #The animal may have been killed
stepagent!(model[a], model)
......
......@@ -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.
"""
@agent FarmPlot GridAgent{2} begin
#TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
pixels::Vector{Tuple{Int64, Int64}}
croptype::CropType
phase::GrowthPhase
......
......@@ -9,6 +9,7 @@
This is the agent type for the farm ABM. (Not yet implemented.)
"""
@agent Farmer GridAgent{2} begin
#TODO remove Agents.jl-related code, reimplement this more cleanly (#72)
fields::Vector{FarmPlot}
croprotation::Vector{CropType}
#TODO add AES
......
......@@ -27,7 +27,10 @@ function savepopulationdata(model::AgentBasedModel)
pops = Dict{String,Int}(s=>0 for s = @param(nature.targetspecies))
for a in allagents(model)
(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
data = []
for p in keys(pops)
......@@ -45,6 +48,7 @@ monthly, yearly, or at the end of a simulation, depending on the parameter
`nature.indoutfreq`. WARNING: Produces very big files!
"""
function saveindividualdata(model::AgentBasedModel)
#XXX doesn't include migrants!
data = []
for a in allagents(model)
(typeof(a) != Animal) && continue
......
......@@ -103,7 +103,7 @@ variables:
Several utility macros can be used within the body of `@phase` as a short-hand for
common expressions: [`@trait`](@ref), [`@setphase`](@ref), [`@respond`](@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
the phase that animals are assigned at birth, unless the variable `phase` is
......@@ -114,6 +114,7 @@ macro phase(name, body)
quote
Core.@__doc__ function $(esc(name))($(esc(:animal))::Animal, $(esc(:model))::AgentBasedModel)
$(esc(:pos)) = $(esc(:animal)).pos
#$(esc(:date)) = $(esc(:model)).date #XXX does this make sense?
$(esc(body))
end
($(esc(:phase)) == "") && ($(esc(:phase)) = $(String(name)))
......@@ -127,6 +128,8 @@ A utility macro to quickly access an animal's trait value.
This can only be used nested within [`@phase`](@ref).
"""
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
# (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
......@@ -134,6 +137,28 @@ macro trait(traitname)
:($(esc(:animal)).$(traitname))
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)
......@@ -300,6 +325,17 @@ macro distancetoedge()
:(distancetoedge($(esc(:pos)), $(esc(:model))))
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)
......@@ -308,7 +344,53 @@ This is a utility wrapper that can only be used nested within [`@phase`](@ref)
or [`@habitat`](@ref).
"""
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
##TODO add movement macros
......@@ -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`.)
"""
@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?
# 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
......@@ -72,7 +73,7 @@ Update an animal by one day, executing it's currently active phase function.
"""
function stepagent!(animal::Animal, model::AgentBasedModel)
animal.age += 1
animal.traits[animal.phase](animal,model) #FIXME
animal.traits[animal.phase](animal,model) #FIXME -> note to self: why?
end
"""
......
......@@ -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)
(animal.sex == male) && @warn "Male $(animalid(animal)) is reproducing."
offspring = []
for i in 1:n
sex = (animal.sex == hermaphrodite) ? hermaphrodite : @rand([male, female])
# We need to generate a fresh species dict here
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
@debug "$(animalid(animal)) has reproduced."
offspring
end
"""
......@@ -146,6 +149,15 @@ function migrate!(animal::Animal, model::AgentBasedModel, arrival::Date)
@debug "$(animalid(animal)) has migrated."
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)
......@@ -183,4 +195,18 @@ function countanimals(pos::Tuple{Int64,Int64}, model::AgentBasedModel;
return n
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
habitat descriptor function. Caution: can be computationally expensive!
"""
function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitatdescriptor::Function)
#XXX allow testing for multiple habitat types?
(habitatdescriptor(pos, model)) && (return 0)
dist = 1
width, height = size(model.landscape)
......@@ -160,8 +161,7 @@ Calculate the distance from the given location to the closest habitat of the spe
Caution: can be computationally expensive!
"""
function distanceto(pos::Tuple{Int64,Int64}, model::AgentBasedModel, habitattype::LandCover)
#XXX allow testing for multiple habitat types?
# 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) == habitattype end)
end
......@@ -172,7 +172,48 @@ Calculate the distance from the given location to the closest neighbouring habit
Caution: can be computationally expensive!
"""
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)
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 register or to comment