Skip to content
Snippets Groups Projects
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