### 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