Skip to content
Snippets Groups Projects
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
nature.jl 12.00 KiB
### Persephone - a socio-economic-ecological model of European agricultural landscapes.
###
### This file is responsible for managing the animal modules.
###


### FUNCTIONS AND TYPES INTEGRATING THE NATURE MODEL WITH THE REST OF PERSEPHONE

## An enum used to assign a sex to each animal
@enum Sex hermaphrodite male female

"""
    Animal

This is the generic agent type for all animals. Species are differentiated
by trait dictionaries passed by them during initialisation.
"""
@agent Animal GridAgent{2} begin
    #XXX is it (performance-)wise to use a dict for the traits?
    # Doesn't that rather obviate the point of having an agent struct?
    # If I could move the mutable traits to the struct, I wouldn't need
    # to deepcopy the speciesdict.
    traits::Dict{String,Any}
    sex::Sex
    age::Int32
end

"""
    animalid(animal)

A small utility function to return a string with the species name and ID of an animal.
"""
function animalid(a::Animal)
    return "$(a.traits["name"]) $(a.id)"
end

"""
    stepagent!(animal, model)

Update an animal by one day, executing it's currently active phase function.
"""
function stepagent!(animal::Animal, model::AgentBasedModel)
    animal.age += 1
    animal.traits[animal.traits["phase"]](animal,model)
end

"""
    initnature!(model)

Initialise the model with all simulated animal populations.
"""
function initnature!(model::AgentBasedModel)
    # The config file determines which species are simulated in this run
    for speciesname in @param(nature.targetspecies)
        species = @eval $(Symbol(speciesname))($model)
        species["initialise!"](species, model)
    end
    # Initialise the data output
    initecologicaldata(model)
end


### MACROS IMPLEMENTING THE DOMAIN-SPECIFIC LANGUAGE FOR DEFINING SPECIES

## 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 - be careful when modifying!

"""
    @species(name, body)

A macro used to create new species definitions for the nature model.
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!()`, 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!` and `@phase` for details).

Code in a species definition block can access the rest of the model using
the `model` variable (an object of type `AgentBasedModel`).
"""
macro species(name, body)
    quote
        Core.@__doc__ function $(esc(name))($(esc(:model))::AgentBasedModel)
            # create internal variables and execute the definition body
            $(esc(:name)) = string($(QuoteNode(name)))
            $(esc(:phase)) = ""
            $(esc(body))
            # extract and process the local variables into a species dict
            vardict = Base.@locals
            speciesdict = Dict{String,Any}()
            for k in keys(vardict)
                speciesdict[string(k)] = vardict[k]
            end
            delete!(speciesdict, "model")
            delete!(speciesdict, $(string(name)))
            return speciesdict
        end
        # allow species to be defined outside of the Persephone module, but still available
        # inside it (needed by `initnature!()` and `reproduce!()`)
        (@__MODULE__() != $(esc(:Persephone))) && ($(esc(:Persephone)).$name = $(esc(name)))
    end
end

"""
    @initialise(habitatdescriptor; kwargs...)

Call this macro within the body of `@species`. It passes the given habitat descriptor
function and keyword arguments on to `initpopulation()` when setting up the simulation.

Note: if this macro is not used, the variable `initialise!` must be set manually in the
species definition.
"""
macro initialise(habitatdescriptor, kwargs...)
    :($(esc(:initialise!)) = initpopulation($(esc(habitatdescriptor)); $(map(esc, kwargs)...)))
end

"""
    @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`).

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:
- `animal` a reference to the animal itself. This provides access to `animal.age`,
    `animal.sex`, and `animal.traits` (a dict that gives access to all species parameters).
- `pos` gives the animal's current position as a coordinate tuple.
- `model` a reference to the model world (an object of type `AgentBasedModel`).
    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: `@trait`, `@setphase`, `@respond`, `@here`, `@kill`,
`@reproduce`, `@neighbours`, `@rand`.

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(name, body)
    #TODO the docstrings give a lot of warnings in the log - can I fix that?
    quote
        Core.@__doc__ function $(esc(name))($(esc(:animal))::Animal, $(esc(:model))::AgentBasedModel)
            $(esc(:pos)) = $(esc(:animal)).pos
            $(esc(body))
        end
        ($(esc(:phase)) == "") && ($(esc(:phase)) = $(String(name)))
    end
end

"""
    @trait(traitname)

A utility macro to quickly access an animal's trait value.
This can only be used nested within `@phase`.
"""
macro trait(traitname)
    #XXX This would error if called in the first part of a species definition block
    # (i.e. outside of a @phase block). Although this is specified in the documentation,
    # it is unexpected and liable to be overlooked. Can we add a third clause to
    # compensate for that?
    if traitname in fieldnames(Animal)
        :($(esc(:animal)).$(traitname))
    else
        :($(esc(:animal)).traits[$(String(traitname))])
    end
end

"""
    @setphase(newphase)

Switch this animal over to a different phase. This can only be used nested within `@phase`.
"""
macro setphase(newphase)
    #XXX make this usable in the top part of a species definition?
    :($(esc(:animal)).traits["phase"] = $(String(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`.
"""
macro respond(eventname, body)
    quote
        if $(esc(eventname)) in $(esc(:model)).landscape[$(esc(:animal)).pos...].events
            $(esc(body))
        end
    end
end

"""
    @here(property)

A utility macro to quickly access a property of the animal's current position
(i.e. `landcover`, `fieldid`, or  `events` - see the `Pixel` struct).
This can only be used nested within `@phase`.
"""
macro here(property)
    :($(esc(:model)).landscape[$(esc(:animal)).pos...].$(property))
end

"""
    @kill

Kill this animal (and immediately abort its current update). This is a thin wrapper
around `kill!()`, and passes on any arguments. This can only be used nested within `@phase`.
"""
macro kill(args...)
    quote
        kill!($(esc(:animal)), $(esc(:model)), $(map(esc, args)...))
        return
    end
end

"""
    @reproduce

Let this animal reproduce. This is a thin wrapper around `reproduce!()`, and passes on
any arguments. This can only be used nested within `@phase`.
"""
macro reproduce(args...)
    :(reproduce!($(esc(:animal)), $(esc(:model)), $(map(esc, args)...)))
end

"""
    @neighbours(radius)

Return an iterator over all animals in the given radius around this animal, excluding itself.
This can only be used nested within `@phase`.
"""
macro neighbours(radius)
    #TODO enable filtering by species
    :(nearby_animals($(esc(:animal)), $(esc(:model)), $(esc(radius))))
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`, `@croptype`, `@cropheight`, `@distanceto`,
`@distancetoedge`, `@countanimals`. 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 && @croptype() != 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` or `@habitat`.
"""
macro landcover()
    :(landcover($(esc(:pos)), $(esc(:model))))
end

"""
    @croptype

Return the local croptype, or nothing if there is no crop here.
This is a utility wrapper that can only be used nested within `@phase` or `@habitat`.
"""
macro croptype()
    :(croptype($(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` or `@habitat`.
"""
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` or `@habitat`.
"""
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` or `@habitat`.
"""
macro distancetoedge()
    :(distancetoedge($(esc(:pos)), $(esc(:model))))
end

"""
    @countanimals(species="", radius=0)

Count the number of animals of the given species in this location.
This is a utility wrapper that can only be used nested within `@phase` or `@habitat`.
"""
macro countanimals(args...)
    :(countanimals($(esc(:pos)), $(esc(:model)); $(map(esc, args)...)))
end

"""
    @rand(args...)

Return a random number or element from the sample, using the model RNG.
This is a utility wrapper that can only be used nested within `@phase` or `@habitat`.
"""
macro rand(args...)
    :($(esc(:rand))($(esc(:model)).rng, $(map(esc, args)...)))
end

##TODO @chance macro: @chance(0.5) => rand(model.rng) < 0.5

##TODO add movement macros