Skip to content
Pedro Andrade edited this page Jul 18, 2017 · 45 revisions

An Introduction to TerraME

Pedro R. Andrade, Tiago G. S. Carneiro, Gilberto Camara

This tutorial works with TerraME version 2.0.0-RC4 or greater.

Summary

Introduction

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.

Framework

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.

Basic Types and Functions

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

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.

Cell

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.

CellularSpace and Map

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.

Traversing a CellularSpace

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()

Neighborhood

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)

Synchronizing a CellularSpace

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.

Event and Timer

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

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.

SIR

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.

Game of Life

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.

References

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.