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