Defining new species

In order to make implementing new species as easy as possible, Persefone includes a domain-specific language (DSL) built from a collection of macros and functions.

Here is an example of what this looks like, using a hypothetical mermaid species:

@species Mermaid begin
    ageofmaturity = 2
    pesticidemortality = 1.0

@create Mermaid begin
    @debug "Created $(animalid(self))."

@phase Mermaid life begin
    @debug "$(animalid(self)) is swimming happily in its pond."
    @respond pesticide @kill(self.pesticidemortality, "poisoning")
    @respond harvesting @setphase(drought)
    if == female && length(@neighbours()) < 3 &&
        self.age >= self.ageofmaturity && @landcover() == water

@phase Mermaid drought begin
    n = sum(1 for a in @neighbours())
    @debug "$(animalid(self)) is experiencing drought with $n neighbour(s)."
    @respond sowing @setphase(life)

@populate Mermaid begin
    birthphase = life
    initphase = life
    habitat = @habitat(@landcover() == water)

A complete species definition consists of one call each to @species, @create, @populate, and one or more calls to @phase. Another important macro is @habitat. Further macros are available to provide convenience wrappers for common functions. (See src/nature/nature.jl for details.)

The first macro to call is @species. This takes two arguments: a species name and a definition block (enclosed in begin and end tags). Within the block, species-specific parameters and variables can be defined (and optionally given values) that should be available throughout a species' lifetime.

Next, each species must define one or more @phase blocks. The concept behind this is that species show different behaviours at different phases of their lifecycle. Each @phase block defines the behaviour in one of these phases. (Technically, it defines a function that will be called daily, so long as the species' phase variable is set to this phase.) Code in this section has access to the model object as well as a self object, which is the currently active Animal agent. Within a phase block, @respond can be used to define the species' response to a FarmEvent that affects the species' current location, while a variety of other macros provide wrappers to life history and movement functions from src/nature/populations.jl.

The third macro to call is @create. Like @phase, this defines a function with access to the world and self objects. This function is called whenever a new individual of this species is created (either at birth, or when the model is initialised).

The last macro that must be called is [@populate]. Whereas @create regulates the creation of individual animals, @populate determines how the population of a species is initialised at the start of a simulation. It does this by defining values for the parameters used by initpopulation!. The full list of parameters that can be used is documented under PopInitParams.

The final important macro is @habitat. This defines a "habitat descriptor", i.e. a predicate function that tests whether or not a given landscape pixel is suitable for a specified purpose. Such habitat descriptors are used as arguments to various functions, for example for population initialisation or movement. The argument to @habitat consists of a logical expression, which has access to the animal's current position (the pos tuple variable) and the model. Various macros are available to easily reference information about the current location, such as @landcover or @distancetoedge.

All of these macros are defined in src/nature/macros.jl.