Source code architecture

Model components

model architecture

The Persefone source code is divided into five components, three of which we call "submodels":

  1. core: This provides the foundation of the model software, and sets up and executes simulation runs. It also reads the configuration file and provides data output functionality.

  2. world: This manages the environment, i.e. the landscape (including reading in map data and providing spatial operations) and the weather.

  3. farm: This is an agent-based model of farmer decision making. It provides the Farmer agent type, which can be subtyped to provide different decision models. Currently the only implemented farmer is very basic, only carrying out a static crop rotation and simple grassland mowing regimes.

  4. crop: This component simulates the growth of crops in the landscape. It provides the agent type AbstractCropState, representing the crop plants growing on one field. Currently two different models are used here: AquaCrop for the most important crops, and ALMaSS for the rest.

  5. animal: This is a collection of individual-based model of species in agricultural landscapes. It defines the Animal agent type, and a set of macros that can be used to rapidly create new species. It also includes ecological process functions that are useful for all species.

Conceptually, core and world provide functionality that is needed by all the submodels. Decisions made by Farmers affect the AstractCropStates on their fields, and (directly or indirectly) the Animals in the model landscape.

Important implementation details

the model object

The model object

A cursory reading of the source code will quickly show that most functions take an SimulationModel object as one of their arguments. The concrete type for this is AgricultureModel, a struct that holds all state that is in any way relevant to a simulation run. (Persefone has a strict "no global state" policy to avoid state-dependent bugs and allow parallelisation.) The model object gives access to all agent instances. It also stores the configuration (model.settings), the landscape (model.landscape, a spatially-index collection of LandscapeElement objects that store the local land cover, amongst other things), and the current simulation date (model.date). (See Persefone.initmodel for details.)

Model configuration/the @param macro

The model is configured via a TOML file, the default version of which is at src/parameters.toml. An individual run can be configured using a user-defined configuration file, commandline arguments, or function calls (when Persefone is used as a package rather than an application). During a model run, the @param macro can be used to access parameter values. Note that parameter names are prepended with the name of the component they are associated with. For example, the outdir parameter belongs to the [core] section of the TOML file, and must therefore be referenced as @param(core.outdir). (See src/core/input.jl for details.)

@param and other macros

As @param(parameter) expands to model.settings["parameter"], it can obviously only be used in a context where a model object is actually available. This is the case for most functions in Persefone, but not for all. Similarly, most other macros in the Persefone.jl package depend on the model object being available in-scope (this is indicated in their documentation).

Output data

Persefone can output model data into text files with a specified frequency (daily, monthly, yearly, or at the simulation end). Submodels can use Persefone.newdataoutput! to plug into this system. For an example of how to use this, see src/animal/ecologicaldata.jl. (See src/core/output.jl for details.)

Farm events

The FarmEvent struct is used to communicate farming-related events between submodels (e.g. sowing or harvest). An event can be triggered with createevent! and affects one LandscapeElement. (See src/world/landscape.jl for details.)

Random numbers and logging

By default in Julia, the random number generator (RNG) and the system logger are two globally accessible variables. As Persefone needs to avoid all global data (since this would interfere with reproducibility in parallel runs), the model object stores a local logger and a local RNG. The local logger generally does not change the way the model uses log statements, it is only relevant for some functions in src/core/simulation.jl.

Using the model RNG

Whenever you need to use a random number, you must use the model.rng. The easiest way to do this is with the @rand and @shuffle! macros. (Note that these, too, require access to the model object.)