-
Notifications
You must be signed in to change notification settings - Fork 13
Introduction
Pedro R. Andrade, Tiago G. S. Carneiro, Gilberto Camara
This tutorial works with TerraME version 2.0.0-RC4 or greater.
TerraME is a simulation toolbox that supports three modeling paradigms: system dynamics, cellular automata, and agent-based modeling.
This tutorial provides an introduction to the basic features of TerraME. It presents the main types of TerraME's base package
and how they can work together.
It is important to note that not every type presented here is required to create a model.
For example, if the model does not have a spatial representation, the concepts
of Cell
, CellularSpace
, Neighborhood
, and Trajectory
are not necessary.
Readers interested in an introduction to the principles of modeling and simulation should refer to Odum (1983). Some useful discussions on spatial dynamic modeling applications include Wesseling et al. (1996), Gibson et al. (2000), Kok and Veldkamp (2001), Parker (2001), and Verburg et al. (2004).
Some scripts described in the next sections are presented in parts. A full version can be found here.
The TerraME development framework is presented below. It consists of the following parts:
- Source code: Models and scripts are stored as Lua files. Lua is a programming language with extensible semantics designed to support differnt programming paradigms with data description facilities (Ierusalimschy, Figueiredo et al. 1996). As an introduction to Lua language focused on TerraME modelers, see Lua for TerraME: A Short Introduction or Programming in Lua for a more general view of the language.
- Code editor: You can choose your favorite editor to write source code. We currently recommend ZeroBrane, as it is possible to run TerraME directly from it.
- Data: Geospatial data can be stored in different data sources, such as shapefiles, PostGIS databases, or Web Feature Services (WFS).
- Geographic Information System (GIS): You can use your favorite GIS to manage geospatial data. When working with modeling, GIS are particularly useful for exploratory analysis, image processing, and post-simulation issues.
- TerraME: The key part of the whole framework. It is responsible for reading the source code as well as data, execute the simulation, and display or save the output.
TerraME has a set of concepts to facilitate modeling. Some of them are presented in this tutorial. A complete description of each basic type of TerraME can be opened from the Documentation button in the graphical interface and also here.
Random
is a type to generate random numbers.
Its arguments define which statistical distribution is going to be used.
For example, to get boolean values from a Bernoulli distribution, it is only necessary to define a probability p
.
Random values can then be produced by calling sample()
.
bernoulli = Random{p = 0.4}
print(bernoulli:sample()) -- true (40%) or false (60%)
Other common distributions are:
- Continuous uniform distribution:
range = Random{min = 3, max = 7}
print(range:sample()) -- a value between 3 and 7
- Categorical distribution with probabilities associated to values:
gender = Random{male = 0.49, female = 0.51}
print(gender:sample()) -- "male" (49%) or "female" (51%)
- Discrete distribution:
cover = Random{"pasture", "forest", "clearcut"}
print(cover:sample()) -- "pasture", "forest", or "clearcut" (33.33% for each)
Calling sample()
is useful to create attributes in the beginning of a simulation as well as to define random decisions along the simulation.
TerraME has some facilities when creating an initial representation of the model that avoids calling sample()
explicitly.
Such facilities will be presented along this tutorial.
A Cell
represents a spatial location, its properties, and its nearness relationships.
It is assumed that a Cell has homogeneous internal representation.
It means that, given two disjoint points within a given Cell, they will have the same properties.
In TerraME, a Cell is represented as a Lua table that might contain persistent and runtime attributes, as well as functions.
Persistent attributes are loaded from external sources such as shapefiles.
Runtime attributes are created along the simulation.
Functions implement some user-defined behavior.
For example, in the code below, a Cell
is created with three runtime attributes (cover
, distRoad
, and distUrban
) and two functions (averageDist
and deforest
).
In these functions, the name of the first argument is user-defined, but its value will always be the Cell
itself.
It is a common procedure to call it self
.
mycell = Cell{
cover = "forest",
distRoad = 52,
distUrban = 28,
averageDist = function(self)
return (self.distRoad + self.distUrban) / 2
end,
deforest = function(self)
self.cover = "deforested"
end
}
print(type(mycell)) -- "Cell"
After declaring this mycell
, it is possible to handle its attributes and call its functions. For example:
print(mycell:averageDist()) -- 40.0
mycell.distRoad = mycell.distRoad / 2
print(mycell:averageDist()) -- 27.0
print(mycell.cover) -- "forest"
mycell:deforest()
print(mycell.cover) -- "deforested"
Cells can have as many attributes as needed, and there is no restriction on the type of its attributes.
It is also possible to add new attributes and functions after declaring the Cell
. However, it is recommended to
declare them in the definition of the Cell
because it increases
readability of the source code and speeds up the simulation.
Cells also have additional built-in functions that are going to be presented later in this tutorial.
A CellularSpace
is a set of Cells representing a given spatial region. The most basic way to create a CellularSpace
is by defining
its x dimension (xdim
). As default, its y dimension is equals to the x dimension. For example, the code below creates
a 10x10 CellularSpace
.
space = CellularSpace{
xdim = 10
}
print(#space) -- 100 Cells
print(type(space:sample())) -- "Cell"
When one creates the CellularSpace
as above, each Cell
has only three attributes: x
, y
, and id
(its unique identifier).
However, a model usually needs more attributes.
It is possible to add other attributes to each Cell
by defining an instance
to the CellularSpace
. It is a Cell
that contains all the attributes needed. For example:
cell = Cell{
state = Random{"alive", "dead"}
}
space = CellularSpace{
instance = cell,
xdim = 30
}
Now each Cell
will have a state
, which is a sample()
from the Random
value defined in the instance
.
To visualize space
, it is necessary to create a Map
.
It takes a CellularSpace
as a target
, an attribute name as select
, and two tables, one describes each possible
value
for the attribute, and the other indicates the color
for the respective value
.
map = Map{
target = space,
select = "state",
value = {"alive", "dead"},
color = {"black", "gray"}
}
The output is shown below.
When want to observe some value that depends on attributes of the Cell
that might change
along the simulation, it is possible to select
the name of the function.
In this case, Map
observes the returning value of such function in each Cell
.
In the example below, cell
has a function
called value
that returns its state
multiplied by its x
location.
As the value is now has a range, it is possible to define min
, max
, and slices
to color the Map
.
The colors can then be defined using the schemas defined by Color Brewer.
cell = Cell{
state = Random{min = 1, max = 3},
value = function(self)
return self.x * self.state
end
}
space = CellularSpace{
instance = cell,
xdim = 30
}
map = Map{
target = space,
select = "value",
min = 0,
max = 90,
slices = 5,
color = "RdPu"
}
Note that the minimum value is zero, and not one, which would be expected as Lua tables start with the first position.
In TerraME, the first x position is zero in CellularSpaces
because usually data from external sources use this value.
CellularSpaces
can also represent geospatial areas, being saved and recovered from data sources such as shapefiles, PogsGIS databases, or even a WFS.
Each entity of a geospatial database (point, line, polygon, or pixel) is loaded as a Cell
in TerraME.
The example below uses a shapefile called amazonia
available within base package. This file contains several attributes related to deforestation in the Brazilian Amazonia.
As it is stored in a package, it can be accessed through filePath()
.
TerraME uses col
and row
as default attributes to store (x, y) locations.
As amazonia
does not have these attributes, it is necessary to define which attributes are going to be used using argument xy
.
After declaring amazonia
, every spatial object within the shapefile is read as a Cell
.
All attributes stored in the shapefile are now attributes of the Cell
.
If needed, some attribute names can be renamed, such as in the example below, where "defor_10"
is replaced by "defor"
.
amazonia = CellularSpace{
file = filePath("amazonia.shp", "base"),
xy = {"Col", "Lin"},
as = {defor = "defor_10"}
}
print(#amazonia) -- 590 Cells
Map{
target = amazonia,
select = "defor",
min = 0,
max = 1,
slices = 8,
color = "RdYlGn",
invert = true -- "GnYlRd"
}
The output of this script is shown below.
A common procedure when modelling is to apply a function over each Cell
of the CellularSpace
.
The most basic procedure is by using instance
.
In the code below, cell
has a function rain()
that increases its water by one.
As world
uses cell
as its instance
, it will automatically have a function rain()
, that
calls rain()
for each of its cells. In the end of the code below, each Cell
of the
world
will have one unit of water.
cell = Cell{
water = 0,
rain = function(self)
self.water = self.water + 1
end
}
world = CellularSpace{
xdim = 20,
instance = cell
}
world:rain()
Each Cell
can have one or more neighborhoods to represent its proximity relations.
A Neighborhood
is a set of pairs (cell, weight), where cell is a neighbor Cell
and weight is an optional number representing the relationship's strength.
There are two ways of building a neighborhood in TerraME.
The first way is by creating relations from scratch using CellularSpace:createNeighborhood()
.
As default, createNeighborhood()
creates a Moore neighborhood, connecting each Cell
to its eight surrounding neighbors. There are other strategies such as "mxn"
, "vonneumann"
, and "diagonal"
.
The code below creates a von Neumann neighborhood connecting the Cell
to itself (self = true
).
amazonia:createNeighborhood{
strategy = "vonneumann",
self = true
}
The second way is by loading an existing neighborhood with CellularSpace:loadNeighborhood()
using a neighborhood file ("gal", "gpm" or "gwt" files), which contains a neighborhood previously created.
The only argument of loadNeighborhood()
is a file path. Code below shows an example of how to use this function.
csCabecaDeBoi = CellularSpace{
file = filePath("cabecadeboi.shp")
}
csCabecaDeBoi:loadNeighborhood(filePath("cabecadeboi-neigh.gpm"))
Independently on the way the neighborhood was built, it is possible to traverse the neighbors of a given cell by using a second-order function (a function that takes another function as argument).
Function forEachNeighbor()
takes as argument a Cell
and a user-defined function that takes a neighbor as argument. For
example, the code below sums the amount of forest
in the neighbors of cell
.
cell = amazonia:sample()
neighForest = 0
forEachNeighbor(cell, function(neigh)
neighForest = neighForest + neigh.forest
end)
When a given process occurs in parallel in space, the changes of a Cell
in a given time instant must not affect the process that occur in any other Cell
in the same instant.
To handle that, TerraME can keep two copies of a CellularSpace
in memory.
One stores the past values of the cell attributes while the other stores the current (present) values of the attributes.
The model must then read the values from the past copy and write to the present copy.
Before updating the cells, it necessary to synchronize
the two copies, updating the past copy.
A Cell
has a special attribute called past
, which is a copy of its attribute values in the instant of the last synchronization.
Figure below shows how synchronization works.
In a given Fire in the Forest model, one forest Cell
decides whether to burn if some neighbor
is burning. To guarantee that a cell that gets burning in the current time step does not affect other cells,
each Cell
should read the state of its neighbors in the past and write in the present.
This way, the neighbor's attributes are read-only. The flow of information is always from the neighbors to the central Cell
.
TerraME provides two types for building models that take into consideration several process that occurs over time.
These types are Event
and Timer
.
Event
represents a procedure that is sheduled to be executed in a given time.
Timer
is a scheduler that manages a queue of Events
and controls the simulation clock.
An Event
is defined by a mandatory argument called action
.
It is a user-defined function that defines what is going to be executed when the Event
is activated.
For example, the code below contains a Timer
with a single Event
. Each time
the Event
is activated, it prints "Rained"
. The last line of the script calls run()
.
It gets the final simulation time as argument. Therefore,
the output of this script will contain ten lines, each one with the text Rained
.
timer = Timer{
Event{action = function()
print("Rained")
end}
}
timer:run(10)
Besides a function, an Event
can also get as argument an object from other TerraME types.
Each type has a predefined behavior.
For example, when a Map
is used, it redraws the Map
each time the Event
is activated.
When a CellularSpace
is used, it synchronizes the CellularSpace
and then calls execute()
(if its instance
has this function).
The example below shows a simulation of rain in each cell along 20 simulation steps. As observation of a simulation usually occurs after the processing
a given time step, Event
sets automatically the priority of a Map
to "verylow"
as default.
cell = Cell{
water = 0,
execute = function(self)
self.water = self.water + 1
end
}
world = CellularSpace{
xdim = 20,
instance = cell
}
map = Map{
target = world,
select = "water",
min = 0,
max = 20,
color = "Blues",
slices = 5
}
timer = Timer{
Event{action = world},
Event{action = map}
}
timer:run(20)
Model
is a concept that encapsulate the parameters and how to instantiate a simulation model.
A Model
has a set of parameters and a function called init()
to describe how the model is created.
This function gets the model instance as first argument.
All the objects of the Model
must be created inside this function and
must be stored inside of the model instance itself.
A complete description of Model can be found here.
This section describes two basic models using different modelling paradigms:
System Dynamics and Cellular Automata.
A Susceptible-Infected-Recovered model describes the spread of an infection. It is a
System Dynamics model where a population is divided into three stocks: susceptible
,
infected
, and recovered
. These values are parameters of the model that change along
the simulation. Other parameters are read-only: duration
, contacts
, probability
, and
finalTime
. SIR
then is declared as a Model
and have the following default values
for each of its parameters.
SIR = Model{
susceptible = 9998,
infected = 2,
recovered = 0,
duration = 2,
contacts = 6,
probability = 0.25,
finalTime = 30,
Function init()
then describes how to initialize the Model
. First, it creates
a Chart
to plot the
three variables that change along the simulation.
init = function(self)
self.chart = Chart{
target = self,
select = {"susceptible", "infected", "recovered"},
color = {"green", "red", "blue"}
}
Then the Model
needs to initialize two variables, total
and alpha
, which are the total size of the
population and a constant value based on the number of contacts and on the probability that one contacted individual becomes sick, respectively.
It is important to declare such variables as local
in order to avoid such variables to have some side effects
in other simulations.
local total = self.susceptible + self.infected + self.recovered
local alpha = self.contacts * self.probability
Finally, the model has a Timer
with two Events
, one to implement the dynamics of the model, and the other to update the Chart
.
self.timer = Timer{
Event{action = function()
local prop = self.susceptible / total
local newInfected = self.infected * alpha * prop
local newRecovered = self.infected / self.duration
self.susceptible = self.susceptible - newInfected
self.recovered = self.recovered + newRecovered
self.infected = self.infected + newInfected - newRecovered
end},
Event{action = self.chart}
}
end
}
This Model
is implemented in package sysdyn.
With SIR
implemented, it is possible to simulate it with its default parameters.
SIR:run()
It will produce the following outcome:
It is possible to configure and run the Model
with other parameters. For example, the code below
instantiate SIR
using duration
four and contacts
one.
instance = SIR{
duration = 4,
contacts = 1
}
instance:run()
instance.chart:save("sir-instance.png")
SIR
can also be configured using a graphical interface by calling configure()
.
SIR:configure()
The code above will show the following graphical interface to set the parameters and run the simulation.
Another common use of a Model
is to simulate different scenarios to see how they behave according to changes in a given parameter.
The code below instantiates SIR
with three values for parameter duration
.
It creates an Environment
to put together the three instances of SIR
.
This allows the instances of SIR
to be simulated at the same time.
A Chart
then is created to visualize how the variable infected
of the three parameters change along the simulations.
It is necessary to add an Event
to the Environment
in order to update the Chart
after each simulation step.
Finally, Environment
can run()
, executing the SIR instances.
env = Environment{
SIR{},
SIR{duration = 4},
SIR{duration = 8}
}
clean() -- remove the three Charts previously created by SIR calls
chart = Chart{
target = env,
select = "infected"
}
env:add(Event{action = chart})
env:run()
The output is shown in the figure below. Note that each line comes from a different simulation.
The next example is a Cellular Automata called Game of Life. The game simulates a grid of cells, each one being dead or alive. Each cell takes its decision independently of each other, using the state of its neighborhood:
- A cell with less than two alive neighbors dies by loneliness.
- A cell with more than threee alive neighbors dies by overcrowding.
- A dead cell with three alive neighbors becomes alive by reproduction.
- An alive cell remains in its state if it has two alive neighbors.
The code for this Model
starts by defining three parameters:
-
dim
: the size of each spatial dimension. -
probability
: the probability of a cell to be alive in the beginning of the simulation. -
finalTime
: the final simulation time.
Life = Model{
dim = 50,
probability = 0.5,
finalTime = 200,
Function init()
for Game of Life starts by definint a Cell
. It will have its initial state
defined randomly according to the parameter probability
.
The decision of a Cell
is then implemented in its execute()
.
In the Game of Live, a Cell
counts how many alive neighbors it has using forEachNeighbor()
.
Note that it must read the state of its neighbors in the past
in order to guarantee that it will get values independently of the decision of its neighbors in the current time step.
init = function(self)
self.cell = Cell{
state = Random{alive = self.probability, dead = 1 - self.probability},
execute = function(cell)
local alive = 0
forEachNeighbor(cell, function(neigh)
if neigh.past.state == "alive" then
alive = alive + 1
end
end)
if alive < 2 then -- loneliness
cell.state = "dead"
elseif alive > 3 then -- overpopulation
cell.state = "dead"
elseif alive == 3 then -- reproduction
cell.state = "alive"
end
end
}
After defining a Cell
, a CellularSpace
can be created using it as instance
.
Note that xdim
gets a value from the instance, which can be configured after implementing the Model
.
The CellularSpace
needs to have a Moore neighborhood wrapping the border in order to guarantee
that all the Cells
have the same number of neighhors.
self.cs = CellularSpace{
xdim = self.dim,
instance = self.cell
}
self.cs:createNeighborhood{wrap = true}
To visualize how the CellularSpace
changes along the simulation, the code below creates a Map
,
coloring alive cells with white and dead cells with black.
self.map = Map{
target = self.cs,
select = "state",
value = {"dead", "alive"},
color = {"black", "white"}
}
Finally, the model needs to create a Timer
to schedule the CellularSpace
and the Map
as shown
below. Note the end
here finalizes function init()
.
self.timer = Timer{
Event{action = self.cs},
Event{action = self.map}
}
end
}
The output for simulating Life
with its default parameters is shown below.
Life:run()
The same facilities available for SIR
model above can also be used for Life
.
An extended version of this Model with a set of initial patterns is available in package ca.
Gibson, C. C., E. Ostrom, et al. (2000). "The concept of scale and the human dimension of global change: a survey." Ecological Economic 32: 217-239.
Ierusalimschy, R., L. H. Figueiredo, et al. (1996). "Lua-an extensible extension language." Software: Practice & Experience 26(6): 635-652.
Kok, K. and A. Veldkamp (2001). "Evaluating impact of spatial scales on land use pattern analysis in Central America." Agriculture Ecosystems & Environment 85(1-3): 205-221. Minsky, L. M. (1967). Computation: finite and infinite machines, Prentice-Hall. Odum, H. T. (1983). Systems Ecology: An Introduction, John Wiley and Sons.
Parker, D. C., T. Berger, and S.M. Manson (2001). Agent-Based Models of Land-Use and Land-Cover Change. Report and Review of an International Workshop, L.R. No.6. Irvine, California, USA.
Verburg, P., P. Schot, et al. (2004). "Land use change modeling: current practices and research priorities." GeoJournal 61(4): 309-324.
Wesseling, C. G., D. Karssenberg, et al. (1996). "Integrating dynamic environmental models in GIS: the development of a Dynamic Modelling language." Transactions in GIS 1: 40-48.
If you have comments, doubts or suggestions related to this document, please write a feedback to pedro.andrade <at> inpe.br.
Back to wiki or terrame.org.