Source code architecture
Model components
Persefone is divided into four components, three of which are semi-independent submodels:
-
core
: This is the foundation of the model software, which sets up and executes simulation runs. It also reads in the configuration file and landscape maps, and provides data output functionality. (Eventually, it will also provide weather data.) -
nature
: This is an individual-based model of species in agricultural landscapes. It defines theAnimal
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. -
farm
: This is an agent-based model of farmer decision making. It is not yet implemented, but will provide theFarmer
agent type. -
crop
: This is a mathematical growth model for various crops. It is not yet implemented, but already provides the agent typeFarmPlot
, representing one field and its associated extent and crop type.
Conceptually, core
provides functionality that is needed by all of the submodels.
Decisions made by Farmer
s affect the FarmPlot
s they own, and (directly or indirectly)
the Animal
s in the model landscape.
Important implementation details
model
object
The A cursory reading of the source code will quickly show that most functions take an
AgentBasedModel
object as one of their arguments. This is the key data structure
of Agents.jl,
and 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 (via model[id]
, where id
is the
unique identifier of this agent). It also stores the configuration (model.settings
),
the landscape (model.landscape
, a matrix of Pixel
objects that store the local
land cover, amongst other things), and the current simulation date (model.date
).
(See Persefone.initmodel
for details.)
For more information about working with agent objects, see the Agents.jl API.
@param
macro
Model configuration/the 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.)
!!! info "@param and other macros"
As @param(parameter)
expands to model.settings["parameter"]
, it can obviously
only be used in a context where the model
object is actually available. (This is the
case for most functions in Persefone, but not for all.) Similarly, many of the nature
macros depend on specific variables being available where they are called, and can
therefore only be used in specific contexts (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/nature/ecologicaldata.jl
.
(See src/core/output.jl
for details.)
Farm events
The FarmEvent
struct is used to communicate farming-related events between
submodels. An event can be triggered with createevent!
and affects all pixels
within a FarmPlot
. (See
src/core/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
.
!!! info "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.)