Something went wrong on our end
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
macros.jl 13.22 KiB
### Persefone.jl - a model of agricultural landscapes and ecosystems in Europe.
###
### This file contains all macros implementing the domain-specific language
### for defining species.
###
##XXX does it make sense to have all of the macros in one place,
## or shouldn't they rather be next to the functions they wrap?
## Note that this DSL consists of lots of deeply nested macro calls, which
## is a known tricky issue in Julia (https://github.com/JuliaLang/julia/issues/23221,
## https://github.com/p-i-/MetaGuideJulia/wiki#example-swap-macro-to-illustrate-esc).
## Hence all the `esc`apes in the following code - take care when modifying!
##FIXME update documentation
"""
@species(name, body)
A macro used to create new species definitions for the nature model.
All species include the standard fields described for the [Animal](@ref) type.
This is effectively a simple domain-specific language, establishing a
custom syntax to describe species' biology:
```julia
@species name begin
@initialise(@habitat(...))
speciesvar1 = 3.14
...
@phase phase1 begin
...
end
end
```
The definition body (enclosed in the begin/end block) has two sections.
First comes a call to [`@initialise`](@ref), and optionally a list of
species-specific parameters, which are assigned just like normal variables.
Second come one or more phase definitions, that describe the behaviour
of the species during various parts of its life cycle. (See the documentation
to [`@initialise`](@ref) and [`@phase`](@ref) for details).
Code in a species definition block can access the rest of the model using
the `model` variable (an object of type `SimulationModel`).
"""
macro species(name, body)
quote
#XXX species are created/referenced as Persefone.<speciesname>, is this relevant?
@kwdef mutable struct $(name) <: Animal
const id::Int64
const sex::Sex = hermaphrodite
const parents::Tuple{Int64,Int64} = (-1, -1) #XXX assumes sexual reprod.
pos::Tuple{Int64,Int64}
phase::Function = ()->0
age::Int = 0
energy::Union{EnergyBudget,Nothing} = nothing # DEB is optional
offspring::Vector{Int64} = Vector{Int64}()
$(body.args...)
end
# define a constructor giving the minimum necessary arguments as positional arguments
$(esc(name))(id, sex, parents, pos, phase) =
$(esc(name))(id=id, sex=sex, parents=parents, pos=pos, phase=phase)
# define a zero-argument constructor to access default field values #XXX probably not needed
#$(esc(name))(id) = $(esc(name))(id=id, parents=(-1, -1), pos=(-1, -1))
# allow species to be defined outside of the Persefone module, but still
# available inside it (needed by `initnature!()` and `reproduce!()`)
(@__MODULE__() != $(esc(:Persefone))) && ($(esc(:Persefone)).$name = $(name))
end
end
"""
@populate(species, params)
Set the parameters that are used to initialise this species' population.
For parameter options, see [`PopInitParams`](@ref).
"""
macro populate(species, params)
quote
# convert the macro body to a dict
Core.@__doc__ fun = function() # metaprogramming is great - this is a fun fun function ;-)
$(esc(params))
return Base.@locals
end
# construct a parametric method that returns a parameterised PopInitParams object
# when called with the species Type
$(esc(:populationparameters))(s::Type{$(esc(species))}) = PopInitParams(; fun()...)
end
end
#FIXME update documentation
"""
@phase(name, body)
This macro is designed to be used within a species definition block (i.e. within
the body of a call to [`@species`](@ref)).
The idea behind this is that species show very different behaviour during different
phases of their lives. Therefore, `@phase` can be used define the behaviour for one
such phase, and the conditions under which the animal transitions to another phase.
`@phase` works by creating a function that will be called by the model if the
animal is in the relevant phase. When it is called, it has access to the following
variables:
- `self` a reference to the animal itself. This provides access to `animal.age`,
`self.sex`, and `animal.<trait>` (where <trait> is a variable that was defined
in the top part of the species definition body).
- `pos` gives the animal's current position as a coordinate tuple.
- `model` a reference to the model world (an object of type `SimulationModel`).
This allows access to `model.date` (the current simulation date) and
`model.landscape` (a two-dimensional array of pixels containing geographic
information).
Several utility macros can be used within the body of `@phase` as a
short-hand for common expressions: [`@setphase`](@ref), [`@respond`](@ref),
[`@kill`](@ref), [`@reproduce`](@ref), [`@neighbours`](@ref),
[`@rand`](@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 explicitly defined by the user in the species definition block.
"""
macro phase(species, phase, body)
quote
Core.@__doc__ function $(esc(phase))($(esc(:self))::$(species),
$(esc(:model))::SimulationModel)
$(esc(:pos)) = $(esc(:self)).pos # needed for landscape macros
$(esc(body))
end
end
end
"""
@create(species, body)
Define a special phase function ([`create!`](@ref)()) that will be called when an
individual of this species is created, at the initialisation of the simulation
or at birth.
As for [`@phase`](@ref), the body of this macro has access to the variables
`self` (the individual being created) and `model` (the simulation world), and
can thus use all macros available in [`@phase`](@ref).
"""
macro create(species, body)
quote
#XXX species are created/referenced as Persefone.<speciesname>, is this relevant?
Core.@__doc__ function $(esc(:create!))($(esc(:self))::$(esc(species)),
$(esc(:model))::SimulationModel)
$(esc(:pos)) = $(esc(:self)).pos # needed for landscape macros
$(esc(body))
end
end
end
"""
@animal(id)
Return the animal 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)).animals[$(esc(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($(esc(id)), $(esc(:model))))
end
"""
@setphase(newphase)
Switch this animal over to a different phase. This can only be used nested within [`@phase`](@ref).
"""
macro setphase(newphase)
:($(esc(:self)).phase = $(newphase))
end
"""
@respond(eventname, body)
Define how an animal responds to a landscape event that affects its current position.
This can only be used nested within [`@phase`](@ref).
"""
macro respond(eventname, body)
quote
if $(esc(eventname)) in $(esc(:model)).landscape[$(esc(:self)).pos...].events
$(esc(body))
end
end
end
"""
@kill
Kill this animal (and immediately abort its current update if it dies). This is a
thin wrapper around [`kill!`](@ref), and passes on any arguments. This can only be
used nested within [`@phase`](@ref).
"""
macro kill(args...)
:(kill!($(esc(:self)), $(esc(:model)), $(map(esc, args)...)) && return)
end
"""
@reproduce
Let this animal reproduce. This is a thin wrapper around [`reproduce!`](@ref), and
passes on any arguments. This can only be used nested within [`@phase`](@ref).
"""
macro reproduce(args...)
:(reproduce!($(esc(:self)), $(esc(:model)), $(map(esc, args)...)))
end
"""
@migrate(arrival)
Remove this animal from the map and add it to the migrant species pool.
It will be returned to its current location at the specified `arrival` date.
This can only be used nested within [`@phase`](@ref).
"""
macro migrate(arrival)
:(migrate!($(esc(:self)), $(esc(:model)), $(esc(arrival))))
end
"""
@habitat
Specify habitat suitability for spatial ecological processes.
This macro works by creating an anonymous function that takes in a model object
and a position, and returns `true` or `false` depending on the conditions
specified in the macro body.
Several utility macros can be used within the body of `@habitat` as a short-hand for
common expressions: [`@landcover`](@ref), [`@cropname`](@ref), [`@cropheight`](@ref),
[`@distanceto`](@ref), [`@distancetoedge`](@ref), [`@countanimals`](@ref).
The variables `model` and `pos` can be used for checks that don't have a macro available.
Two example uses of `@habitat` might look like this:
```julia
movementhabitat = @habitat(@landcover() in (grass agriculture soil))
nestinghabitat = @habitat((@landcover() == grass ||
(@landcover() == agriculture && @cropname() != "maize" &&
@cropheight() < 10)) &&
@distanceto(forest) > 20)
```
For more complex habitat suitability checks, the use of this macro can be
circumvented by directly creating an equivalent function.
"""
macro habitat(body)
#XXX I suspect that I may have less problems with macro expansion and
# module scoping if @habitat did not create a new function. But is
# there a different way?
quote
function($(esc(:pos)), $(esc(:model)))
if $(esc(body))
return true
else
return false
end
end
end
end
##XXX Can I make sure (e.g. through `try/catch`) that the following macros
## are not called anywhere outside @habitat/@phase?
"""
@landcover
Returns the local landcover. This is a utility wrapper that can only be used
nested within [`@phase`](@ref) or [`@habitat`](@ref).
"""
macro landcover()
:(landcover($(esc(:pos)), $(esc(:model))))
end
"""
@cropname
Return the name of the local croptype, or nothing if there is no crop here.
This is a utility wrapper that can only be used nested within [`@phase`](@ref)
or [`@habitat`](@ref).
"""
macro cropname()
:(cropname($(esc(:pos)), $(esc(:model))))
end
"""
@cropheight
Return the height of the crop at this position, or 0 if there is no crop here.
This is a utility wrapper that can only be used nested within [`@phase`](@ref)
or [`@habitat`](@ref).
"""
macro cropheight()
:(cropheight($(esc(:pos)), $(esc(:model))))
end
"""
@distanceto(habitat)
Calculate the distance to the closest habitat of the specified type or descriptor.
This is a utility wrapper that can only be used nested within [`@phase`](@ref)
or [`@habitat`](@ref).
"""
macro distanceto(habitat)
:(distanceto($(esc(:pos)), $(esc(:model)), $(esc(habitat))))
end
"""
@distancetoedge
Calculate the distance to the closest neighbouring habitat.
This is a utility wrapper that can only be used nested within [`@phase`](@ref)
or [`@habitat`](@ref).
"""
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
"""
@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
##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!($(esc(:self)), $(esc(:model)), $(esc(position))))
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(:self)), $(esc(:model)), $(esc(direction))))
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
"""
@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(:self)), $(esc(leader)), $(esc(:model)), $(esc(distance))))
end