4  Geosimulation

The previous lesson established what geosimulation is, and when it is the right choice for a problem. This lesson builds one. You will implement an agent-based model and a cellular automaton that both read real geospatial data, combine them into an integrated model of a cattle pasture, and meet the question that every spatial model has to answer: whether its entities update all at once, or one after another.

Learning Objectives

By the end of this lesson, you will be able to:

  • implement an agent-based model that loads and works with geospatial data,
  • implement a cellular automaton whose cells are initialised from spatial data,
  • integrate both into a combined geosimulation model of a real study area,
  • decide whether a process should be updated synchronously or asynchronously, and implement it accordingly.

4.1 Agent-based models

In Agent-based models, spatial patterns emerge from heterogeneous individuals that interact locally. Even if agents are located in homogeneous environments, the expected result of such models is a self-organised, spatial pattern. The classic example is the clustering behaviour of social animals, such as flocking birds or schooling fish. The same phenomenon can be observed in groups of pedestrians.

There is no need to define an abstract, system-level parameter like “birth-rate”. Instead, the actual behaviour is implemented in a mechanistic way: individual females meet at the same location and at the same time with a male to mate and to produce offspring. So, Agent-based modellers are programmers that tell the story of the system in a formalised way.

Exercise: Geospatial ABM

In this exercise, cows will move on a pasture, where the geometry of the pasture is loaded from a .geojson file. If you successfully implemented the Hello World model of Lesson 1, you are ready to model with geospatial data.

I have commented my code, so that another modeller, who reads the code can easily understand what the code does. This is good practice in programming: I highly recommend that you do the same. Don’t wait with code commenting until you’ve finished, but comment as you go.

And again: make sure that you set your indents correctly. A well-structured code is easier to understand and it is much easier to spot errors!

Prepare the model

  1. Download the .geojson file of the pasture “Vierkaseralm” near Salzburg. GAMA has a specific folder structure to make referencing geospatial data easier. If you look into your Gama Project, you will find a folder called includes. Store the pasture file there.
  2. Create a new model in which 5 cows walk around randomly.

Load the geospatial data into your model

  1. Load the geospatial data into a global variable of type “file”. Always use relative paths to access files, otherwise your model won’t run on someone else’s computer.
//Load polygon file
file pasture_file <- file("../includes/vierkaser.geojson");
  1. When you work with geospatial data in GAMA, you always need to explicitely define the bounding box of your model and read it into the predefined global “shape” variable. The command to get the bounding box of a geometry is envelope():
//Define the extent of the study area
geometry shape <- envelope(pasture_file);
  1. Read the geometry of the pasture into a variable:
//read the geometry of the pasture file into a variable
geometry pasture_geom <- geometry(pasture_file);

Restrict the cows’ movement

  1. Set the built-in agent variable “location” to any_location_in the pasture, when you create the cows.
//create 5 cow agents that are located within the pasture
create cows number: 5 {
  location <- any_location_in (pasture_geom);
}
  1. In the agent section, adjust the movement. The default units, when you work with geospatial data in GAMA are metres and seconds. A speed of 1 would thus be 1 m/s = 3.6 km/h. Let’s slow down the cows and set the speed to 0.5 m/s. You can also set a maximum turning amplitude. Let’s set it to 90 degrees. Finally, you want your cows to stay in the pasture. This can be done with the “bounds” facet.
//behaviour: movement
reflex moveAround {
  do wander amplitude: 90.0 speed: 10 #m/#s bounds:pasture_geom;
}

Visualise the cows and the pasture

  1. In the agent section, you need to think about the size of the cow. Remember, that the default unit is a metre. So a circle(1) would be a cow symbol of 1m radius. It will be hard to spot on a pasture that has an extent of 2 x 2km. Try to find a good size!
  2. In the experiment section, draw the geometry of the pasture. The layers are drawn in the order that you write them. So, first draw the pasture, then the cows. Otherwise the cows would be hidden.
//draw the geometry of the pasture
graphics "pasture_layer"{
  draw pasture_geom color: #green;
}

Done - we have a basic model. Before we use this model to play around a little bit with it, I share the full code. So that you can compare and continue with the exercise, if anything doesn’t work for you.

/***
* Name: ExL4a_geospatialABM
* Author: WALLENTIN, Gudrun
* Description: Exercise of the UNIGIS Salzburg optional module
* working with geospatial data
***/

model ExL4a_geospatialABM

global  {
    //Load polygon file
    file pasture_file <- file("../includes/vierkaser.geojson");
    //Define the extent of the study area
    geometry shape <- envelope(pasture_file);
    //read the geometry of the pasture file into a variable
    geometry pasture_geom <- geometry(pasture_file);

    //Create the agents
    init {
        //create 5 cow agents that are located within the pasture
        create cows number: 5 {
            location <- any_location_in (pasture_geom);
        }
    }
}

// cow agents
species cows skills: [moving] {

    //behaviour: movement
    reflex moveAround {
        do wander amplitude: 90.0 speed: 10 #m/#s bounds:pasture_geom;
    }

    //visualisation
    aspect base {
        draw circle (5) color: #black;
    }
}

//Simulation
experiment virtual_pasture type:gui {
    output {
        display map type: opengl{
            //draw the geometry of the pasture
            graphics "pasture_layer"{
                draw pasture_geom color: #green;
            }
            species cows aspect:base;
        }
    }
}

4.2 Cellular Automata

In Cellular Automata, physical, social or ecological processes emerge from heterogeneous landscapes. The size, the shape and the connectivity of a landscape greatly impacts geospatial processes such as floodings, range shifts, dispersal, and colonisation processes, diffusion of air pollution, spread of wild-fires, or land-use change. In Lesson 1, you got acquainted with constructing and visualising a Cellular Automaton with GAMA. In the following exercise we move on to build a geospatial CA.

Exercise: Geospatial CA

This exercises complements the geospatial ABM of this lesson. It adds a grazeland-environment to the cow agents.

A Cellular Automaton can be created directly from raster data that is read into a global variable of type “file”. Possible formats are ASCII and GeoTiff. However, in this exercise we construct a grid from vector geometries.

Within the “Vierkaser” pasture property, there are only some regions with open grassland, whereas the rest of the pasture has been overgrown with shrubs and trees.

  • The “study_area.geojson” is the bounding box of the Vierkaser property Download,
  • the “vierkaser.geojson” is the fenced Vierkaser property Download,
  • the “lower_pasture.geojson” is an area with well growing grass within the fenced area Download, and
  • the “Hirschanger” pasture is of lower quality within the fenced area Download.

Download each of the files and copy them into your /includes folder. With the following code you can load the data into your model.

    //Load geospatial data
    file study_area_file <- file("../includes/study_area.geojson");
    //fenced area includes shrubs and grassland
    file fenced_area_file <- file("../includes/vierkaser.geojson");
    //high-quality pasture area
    file lower_pasture_file <- file("../includes/lower_pasture.geojson");
    //low-quality pasture area
    file hirschanger_file <- file("../includes/hirschanger.geojson");

Next, we need to set the shape geometry that defines the extent of the model and read the geometry from the vector files, like we have done it in the previous exercise.

    //Define the extent of the study area as the envelope (=bounding box) of the study area
    geometry shape <- envelope (study_area_file);

    //extract the geometry from the vector data
    geometry fence_geom <- geometry(fenced_area_file);
    geometry lower_pasture_geom <- geometry(lower_pasture_file);
    geometry hirschanger_geom <- geometry(hirschanger_file);

Initialise the CA

When we set up (=initialise) the CA, we construct a grid with a cell size of 5m x 5m. Each of the cells has two attributes: the current biomass and the maximum potential biomass per cell with a default value of 0.

grid grass cell_width:5 cell_height:5 {
    float biomass;
    float max_biomass <- 0.0;
}

To initialise the model we want to set the maximum biomass for each of the pasture areas: the lower pasture can grow up to 10 units of biomass per cell, the Hirschanger has a maximum of 7. Everywhere else within the fence, there are shrubs and the grass biomass can’t grow beyond 1.

To do so, we need to implement a conditional structure (if / else) and we need to do a spatial overlay operation.

Note

if and else

Conditional structures with “if” and “else” are very important in Geosimulation: they can encode behaviour.

IF a condition is true, do something

if <condition> {
  do something
}

ELSE (the condition above is false) and IF another condition is true, do something else

else if <condition> {
  do something else
}

ELSE (if nothing of the above is true), do yet another thing

else {
  do yet another thing
}

for example:

if biomass > 5 {
  write "yummieh";
}
Note

Spatial overlay

GAMA offers all relevant functions that we need to evaluate overlay. Have a look at the GAMA Documentation for spatial operators to find out, which spatial operators are available. You will see, it’s quite a few – next to simple distance operators (e.g. closest_to) you will probably recognise the classical topological relations: touches, covers, crosses, intersects, overlaps. Here lies a lot of the power of GAMA’s spatial functionality.

To evaluate, whether a particular cell in the CA grid overlaps the Hirschanger pasture, we could write:

if self overlaps(hirschanger_geom) {
  write "This is Hirschanger";
}

Of course, it is not only possible to evaluate overlay, but also to perform an overlay operation. A union operation for example is as simple as that:

//union_geom is the union of polygon_A and polygon_B
geometry union_geom <- polygon_A + polygon_B;
Note

Creating a CA from a raster file

You can create a Cellular Automaton directly from a raster file. The input data needs to be either in ESRI’s .asc or the .tif format, and you also should provide the epsg code (here it is Web Mercator, epsg 3857) to make sure the raster projected correctly.

In the grid section, you can then construct the CA directly from the raster file. It will read the raster’s cell values into a built-in grid variable that is called grid_value. As it is built-in, you don’t have to declare grid_value, but can use it right away. The below code, simply writes each of the cell values into the console:

global {
    //declare the raster import file
    file my_raster_data <- grid_file("../includes/some_raster.tif", 3857);
}

grid myCA file: my_raster_data {
  init {
    write grid_value;
  }
}
Exercise: Geospatial CA – Biomass & Growth

Now, try to implement the code that sets the current and maximum biomass:

  • Lower pasture: max_biomass = 10, and biomass = 2
  • Hirschanger pasture: max_biomass = 7, and biomass = 2
  • everywhere else within the fenced area: max_biomass = 1, and biomass = 1
//assign the value of 1 as the max. biomass within the fenced area
if self overlaps(fence_geom){
  //set the maximum biomass of a grazeland cell to 1
  max_biomass <- 1.0;
  biomass <- 1.0;
}
//if the cell overlaps the lower pasture: assign the value of 10 to the biomass (this overwrites the biomass=1 within the fenced area)
if self overlaps(lower_pasture_geom){
  max_biomass <- 10.0;
  biomass <- 2.0;
}
//if it is not in lower pasture, but it overlaps Hirschanger: assign the value of 7 to the biomass ("else if" overwrites the fenced area, but not the lower pasture)
else if self overlaps(hirschanger_geom){
  max_biomass <- 7.0;
  biomass <- 2.0;
}

Make the CA dynamic

Finally, we let the CA grow grass in a reflex. Add a reflex that increases the biomass of each cell at each time step. The logistic growth function makes sure that the grass can’t grow beyond its maximum, and the update_colour reflex updates the colour to visualise the biomass change in the simulation.

To avoid a division by zero, we also need to restrict this reflex to cells within the fenced area with a max_biomass > 0. This could be done by using an if-condition within the reflex, but alternatively and more efficiently also the when facet can be used:

    // let grass grow until its maximum potential
    reflex grow_grass when: max_biomass > 0 {
        //logistic growth: N + r*N*(1-N/K)
        biomass <- biomass + grass_growth_rate * biomass * (1 - biomass / max_biomass);
        color <- rgb([0, biomass * 15, 0]);
    }

Done. The Cellular Automaton model reads a geospatial pasture polygon and models grass growth on that pasture.

Watch the grazeland get a more intense green colour. If that happens too quickly: refresh the simulation, reduce the simulation speed slider and start again.

/***
* Name: Ex L4b_geospatialCA
* Author: WALLENTIN, Gudrun
* Description: Exercise of the UNIGIS Salzburg optional module
* Constructing a Cellular Automaton of grass growth from geospatial data
***/

model ExL4b_geospatialCA

global  {
    //Load geospatial data
    file study_area_file <- file("../includes/study_area.geojson");
    //fenced area includes shrubs and grassland
    file fenced_area_file <- file("../includes/vierkaser.geojson");
    //high-quality pasture area
    file lower_pasture_file <- file("../includes/lower_pasture.geojson");
    //low-quality pasture area
    file hirschanger_file <- file("../includes/hirschanger.geojson");

    //grass regrowh rate
    float grass_growth_rate <- 0.1;

    //Define the extent of the study area as the envelope (=bounding box) of the study area
    geometry shape <- envelope (study_area_file);

    //extract the geometry from the vector data
    geometry fence_geom <- geometry(fenced_area_file);
    geometry lower_pasture_geom <- geometry(lower_pasture_file);
    geometry hirschanger_geom <- geometry(hirschanger_file);

    init {
        ask grass {
            color <- rgb([0, biomass * 15, 0]);
        }
    }
}

//The cellular automaton represents the grazeland
grid grass cell_width:5 cell_height:5 {
    float biomass;
    float max_biomass <- 0.0;

    init {
        //assign the value of 1 as the max. biomass within the fenced area
        if self overlaps(fence_geom){
            //set the maximum biomass of a grazeland cell to 1
            max_biomass <- 1.0;
            biomass <- 1.0;
        }
        //if the cell overlaps the lower pasture: assign the value of 10 to the biomass (this overwrites the biomass=1 within the fenced area)
        if self overlaps(lower_pasture_geom){
            max_biomass <- 10.0;
            biomass <- 2.0;
        }
        //if it is not in lower pasture, but it overlaps Hirschanger: assign the value of 7 to the biomass ("else if" overwrites the fenced area, but not the lower pasture)
        else if self overlaps(hirschanger_geom){
            max_biomass <- 7.0;
            biomass <- 2.0;
        }
    }

    // let grass grow until its maximum potential
    reflex grow_grass when: max_biomass > 0 {
        //logistic growth: N + r*N*(1-N/K)
        biomass <- biomass + grass_growth_rate * biomass * (1 - biomass / max_biomass);
        color <- rgb([0, biomass * 15, 0]);
    }
}

//Simulation
experiment virtual_pasture type:gui {
    output {
        display map type: opengl{
            //draw the geometry of the pasture with the defined color value
            grid grass ;
        }
    }
}

4.3 Combined Geosimulation model

We could now combine the Agent-based cow model and the dynamic grass growth of the Cellular Automaton to a spatially explicit cow-pasture model. Actually, such combination of ABM and CA is the rule rather than the exception. It is the combination of smart individuals interacting in and with a dynamic environment (Figure 4.1).

Figure 4.1: A geosimulation model often integrates Agent-based models with Cellular Automata to represent complex living systems.

Another common representation of space is a network, e.g. for traffic simulation or for an abstract representation of topological relationships as for example in communication networks. Recent progress in the coupling of modelling frameworks with GIS enable advanced spatial data handling in agent-based models that to date were restricted to comparatively simple functionalities in terms of spatial analysis.

Exercise: Geosimulation Pasture Model

The next exercise of this lesson combines the two geospatial models of this lesson. Try to integrate the code yourself, before you look at the solution!

/***
* Name: Ex L4c - geospatial CA-ABM
* Author: WALLENTIN, Gudrun
* Description: Exercise of the UNIGIS Salzburg optional module
* working with geospatial data
***/

model ExL4c_geospatialABM

global  {

    //Load geospatial data
    file study_area_file <- file("../includes/study_area.geojson");
    //fenced area includes shrubs and grassland
    file fenced_area_file <- file("../includes/vierkaser.geojson");
    //high-quality pasture area
    file lower_pasture_file <- file("../includes/lower_pasture.geojson");
    //low-quality pasture area
    file hirschanger_file <- file("../includes/hirschanger.geojson");

    //grass regrowh rate
    float grass_growth_rate <- 0.1;

    //Define the extent of the study area as the envelope (=bounding box) of the study area
    geometry shape <- envelope (study_area_file);

    //extract the geometry from the vector data
    geometry fence_geom <- geometry(fenced_area_file);
    geometry lower_pasture_geom <- geometry(lower_pasture_file);
    geometry hirschanger_geom <- geometry(hirschanger_file);
    geometry pasture_geom <- lower_pasture_geom + hirschanger_geom;

    //Create the agents
    init {
        //create 5 cow agents that are located within the pasture
        create cows number: 5 {
            location <- any_location_in (pasture_geom );
        }
        ask grass {
            color <- rgb([0, biomass * 15, 0]);
        }
    }
}

// cow agents
species cows skills: [moving] {

    //behaviour: movement
    reflex moveAround {
        do wander amplitude: 90.0 speed: 10 #m/#s bounds:pasture_geom;
    }

    //visualisation
    aspect base {
        draw circle (5) color: #black;
    }
}

//The cellular automaton represents the grazeland
grid grass cell_width:5 cell_height:5  {
    float biomass;
    float max_biomass <- 0.0;

    init {
        //assign the value of 1 as the max. biomass within the fenced area
        if self overlaps(fence_geom){
            //set the maximum biomass of a grazeland cell to 1
            max_biomass <- 1.0;
            biomass <- 1.0;
        }
        //if the cell overlaps the lower pasture: assign the value of 10 to the biomass (this overwrites the biomass=1 within the fenced area)
        if self overlaps(lower_pasture_geom){
            max_biomass <- 10.0;
            biomass <- 2.0;
        }
        //if it is not in lower pasture, but it overlaps Hirschanger: assign the value of 7 to the biomass ("else if" overwrites the fenced area, but not the lower pasture)
        else if self overlaps(hirschanger_geom){
            max_biomass <- 7.0;
            biomass <- 2.0;
        }
    }

    // let grass grow until its maximum potential
    reflex grow_grass when: max_biomass > 0 {
        //logistic growth: N + r*N*(1-N/K)
        biomass <- biomass + grass_growth_rate * biomass * (1 - biomass / max_biomass);
        color <- rgb([0, biomass * 15, 0]);
    }
}

//Simulation
experiment virtual_pasture type:gui {
    output {
        display map type: opengl{
            //draw the geometry of the pasture
            grid grass;
            species cows aspect:base;
        }
    }
}

4.4 Asynchronous and synchronous update

Interacting agents and cells in a spatial model read the state of their neighbours and act according to their neighbours’ state. To model this process there is one important question: when does an entity update? Does it see its neighbours as they were at the start of the time step, or as they are right now — when some of them already have changed?

Both variations are valid. Whether asynchronous or synchronous updates are correct depends on the type of the process:

  • Asynchronous. Each entity reads the current state and writes immediately, so entities computed later see the results of those computed earlier. Such asynchronous updates are typical for processes that compete for the same resource on a first-come-first-serve basis, for example citizens that look for an empty flat, cows that graze grass, or a lion mating a lioness. Biological processes are usually asynchronous.

In GAMA asynchronous updates execute the processes for cells or agents one at a time — one cell finishes all of its reflexes before the next cell starts. Reflexes within grid or species sections are therefore asynchronous by default. Two reflexes in the same grid are not two phases. Note the schedules: shuffle(my_CA) facet of the grid. This facet makes sure that the agents’ order is shuffled each time, which avoids artifacts that would emerge, if the process always executed in the order of the index.

Note

For further reference, you can download the GAMA building block model “asynchronous update”.

model L4d_async_update

global {
    //reports final state of previous step
    reflex report_state {
        ask my_CA {write "" + self + ": " + CA_var;}
    }
}

grid my_CA width:2 height:2 neighbors:4 schedules: shuffle(my_CA){
    float CA_var <- 1.0;

    //take from neighbours
    reflex asynchronous_process {
        ask neighbors {
            float taken <- CA_var * 0.2;
            CA_var <- CA_var - taken;
            myself.CA_var <- myself.CA_var + taken;
        }
    }
}

experiment simulate type: gui {}
  • Synchronous. All entities read the same frozen state; the new values are written afterwards. No entity gains anything from being computed first. Synchronous updates represent processes where nothing is consumed, for example heat diffusion or water run-off. Physical processes are often synchronous.

In GAMA this is solved with a global ask that sweeps the whole population through one statement block before the next block begins. Two consecutive ask blocks in a global reflex divide the process in two phases, where a temporary variable is needed to freeze the state at the beginning of the time step.

Note

For further reference, you can download the GAMA building block model “synchronous update”.

model L4e_sync_update

global {
    init {
        ask my_CA where (each.grid_x = 0 and each.grid_y = 0) { 
            CA_var <- 5.0;
        }
    }   
    
    reflex synchronous_process {
        ask my_CA { next_CA_var <- CA_var + 0.5 * ((neighbors mean_of (each.CA_var)) - CA_var); }
        ask my_CA { CA_var <- next_CA_var; } 
    }
    
    //reports final state of current step
    reflex report_state {
        ask my_CA {write "final value from global sync process: " + CA_var;} 
    }
}

grid my_CA width:2 height:2 neighbors:4 {
    float CA_var <- 1.0;
    float next_CA_var;
}

experiment simulate type: gui {}

References