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
end
@create Mermaid begin
@debug "Created $(animalid(self))."
end
@phase Mermaid life begin
@debug "$(animalid(self)) is swimming happily in its pond."
@respond pesticide @kill(self.pesticidemortality, "poisoning")
@respond harvesting @setphase(drought)
if self.sex == female && length(@neighbours()) < 3 &&
self.age >= self.ageofmaturity && @landcover() == water
@reproduce()
end
end
@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)
end
@populate Mermaid begin
birthphase = life
initphase = life
habitat = @habitat(@landcover() == water)
pairs=true
end
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
.