Something went wrong on our end
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
macros.jl 15.47 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?
##TODO -> rearrange files
## Note that this DSL consists of 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!
"""
@species(name, body)
A macro used to add new species types to the nature model. Use this to define
species-specific variables and parameters.
The macro works by creating a keyword-defined mutable struct that contains
the standard fields described for the [`Animal`](@ref) type, as well as any
new fields that the user adds:
```julia
@species <name> begin
<var1> = <value>
<var2> = <value>
...
end
```
To complete the species definition, the [`@phase`](@ref), [`@create`](@ref),
and [`@populate`](@ref) macros also need to be used.
"""
macro species(name, body)
quote
@kwdef mutable struct $name <: Animal
#TODO once Julia 1.11 is released, escape $name above
#(https://discourse.julialang.org/t/kwdef-constructor-not-available-outside-of-module/114675/4)
const id::Int64
const sex::Sex = hermaphrodite
const parents::Tuple{Int64,Int64} = (-1, -1) #XXX assumes sexual reprod.
pos::Tuple{Int64,Int64}
phase::Function = (self,model)->nothing
age::Int = 0
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
$(esc(name))(id, sex, parents, pos, phase) =
$(esc(name))(id=id, sex=sex, parents=parents, pos=pos, phase=phase)
# define a single-argument constructor for utility purposes (especially testing)
$(esc(name))(id) = $(esc(name))(id=id, parents=(-1, -1), pos=(-1, -1))
end
end
"""
@populate(species, params)
Set the parameters that are used to initialise this species' population.
For parameter options, see [`PopInitParams`](@ref).
```julia
@populate <species> begin
<parameter> = <value>
...
end
```
"""
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
"""
@phase(name, body)
Use this macro to describe a species' behaviour during a given phase of its life.
The idea behind this is that species show very different behaviour at different
times 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 all the variables
defined in the [`@species`](@ref) definition, as well as all standard [`Animal`](@ref)
variables (e.g. `self.age`, `self.sex`, `self.offspring`).
- `pos` gives the animal's current position as a coordinate tuple.
- `model` a reference to the model world (an object of type [`SimulationModel`](@ref)).
This allows access, amongst others, to `model.date` (the current simulation date)
and `model.landscape` (a two-dimensional array of pixels containing geographic
information).
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), [`@occupy`](@ref), [`@rand`](@ref).
"""
macro phase(species, phase, body)
quote
Core.@__doc__ function $(esc(phase))($(esc(:self))::$(esc(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
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
"""
@here()
Return the landscape pixel of this animal's current location.
This can only be used nested within [`@phase`](@ref).
"""
macro here()
:($(esc(:model)).landscape[$(esc(:self)).pos...])
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
"""
@killother
Kill another animal. This is a thin wrapper around [`kill!`](@ref), and passes on
any arguments. This can only be used nested within [`@phase`](@ref).
"""
macro killother(animal, args...)
:(kill!($(esc(animal)), $(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
"""
@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
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 an empty string 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 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 cropheight()
:(cropheight($(esc(:pos)), $(esc(:model))))
end
"""
@cropcover
Return the percentage ground cover of the crop at this position, 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 cropcover()
:(cropcover($(esc(:pos)), $(esc(:model))))
end
"""
@directionto
Calculate the direction to an animal or 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 directionto(target)
:(directionto($(esc(:pos)), $(esc(:model)), $(esc(target))))
end
"""
@distanceto
Calculate the distance to an animal or 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(target)
:(distanceto($(esc(:pos)), $(esc(:model)), $(esc(target))))
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
"""
@randomdirection(range=1)
Return a random direction tuple that can be passed to [`@walk`](@ref).
This is a utility wrapper that can only be used nested within [`@phase`](@ref).
"""
macro randomdirection(args...)
:(randomdirection($(esc(:model)), $(map(esc, args)...)))
end
"""
@nearby_animals(radius=0, species="")
Return an iterator over all animals in the given radius around the current position.
This can only be used nested within [`@phase`](@ref) or [`@habitat`](@ref).
"""
macro nearby_animals(args...)
#FIXME doesn't work properly when nested in `@habitat` (kwargs not recognised)
#XXX do I need this macro if I have @countanimals and @neighbours?
#XXX does it make sense to use `pos` here? What if an an animal wants to look at another place?
:(nearby_animals($(esc(:pos)), $(esc(:model)), $(map(esc, args)...)))
end
"""
@countanimals(radius=0, species="")
Count the number of animals at or near this location, optionally filtering by species.
This can only be used nested within [`@phase`](@ref) or [`@habitat`](@ref).
"""
macro countanimals(args...)
#FIXME doesn't work properly when nested in `@habitat` (kwargs not recognised)
:(countanimals($(esc(:pos)), $(esc(:model)), $(map(esc, args)...)))
end
"""
@neighbours(radius=0, conspecifics=true)
Return an iterator over all (by default 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, speed)
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(args...)
#XXX add `ifempty` keyword?
:(walk!($(esc(:self)), $(esc(:model)), $(map(esc, args)...)))
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