Skip to content
Pedro Andrade edited this page Jan 8, 2018 · 32 revisions

This tutorial is under development. It uses code that will work properly only in the next release of TerraME.

Agent-Based Modeling with TerraME

Pedro R. Andrade

Summary

Introduction

TerraME is a multi-paradigm toolkit for developing dynamic models of geospatial phenomena. In social simulation, the lowest level of modeling where it is possible to describe the minimum entities that act over space is through agent-based modeling, or simply ABM. This tutorial describes the main TerraME functionalities related to this topic. Some good introductions to ABM can be found in Gilbert (2007) and Gilbert and Troitzsch (2005). The concepts of the architecture for ABM that will be presented in this document are shown below.

Some basic definitions related to the syntax adopted in this document are noteworthy. To allow a smooth reading, we use names of classes and objects in plain English, avoiding capital letters and words without space between them whenever possible (e.g. cellular space instead of CellularSpace). Functions are always described with () in the end, with the ones that belong to types being described as Type:function(). The code presented in this tutorial does not use the concept of Model. For versions using Model, please see the package abm.

Agent

The basic entity of any agent-based model is the agent. In TerraME, one agent can be built simply by using a table with attributes and functions, instantiated using the type Agent. It enables the agent to perform basic actions that will be described along this chapter. The only restriction is that it must own at least a user-defined function named execute(), which gets the agent itself as argument and describes its behavior. Take as example the code below. In this simple script, a singleFooAgent has no attributes and performs a random walk each time step. The type Agent provides basic functions such as Agent:walk(), used in this example.

singleFooAgent = Agent{
    execute = function(self)
        self:walk()
    end
}

Before performing any action, singleFooAgent needs to belong to a Cell, as it will use walk() to change its spatial location. Let us put it in a random Cell of a 10x10 CellularSpace with Moore neighborhood.

cs = CellularSpace{
    xdim = 10 -- ydim equals to xdim as default
}

cs:createNeighborhood{} -- Moore neighborhood as default

To connect singleFooAgent and cs, it is necessary to create an Environment. Then, Environment:createPlacement() puts every Agent within the Environment in random cells, as shown in the code below.

e = Environment{
    cs,
    singleFooAgent
}

e:createPlacement{} -- randomly as default

To visualize where singleFooAgent is located, we can create a Map using cs as target. The location of singleFooAgent (the Cell where there is one agent) is painted as blue and the other cells are painted as gray.

map = Map{
    target = cs,
    grouping = "placement",
    value = {0, 1}, -- zero agents or one agent
    color = {"gray", "blue"}
}

In the end, we create a Timer with two Events, one to activate singleFooAgent and the other to update the map. The simulation then executes for a hundred times.

t = Timer{
    Event{action = singleFooAgent},
    Event{action = map}
}

t:run(100)

This simulation will produce the output shown below. The complete source code of this example is available here.

The output above could also represent the beginning of the simulation, as it only shows where singleFooAgent is located. Let us now trace the cells where singleFooAgent was along the simulation. Its behaveior now sets attribute washere of its current cell (getCell()) to "yes" before walking, to indicate that singleFooAgent has visited the cell along the simulation.

singleFooAgent = Agent{
    execute = function(self)
        self:getCell().washere = "yes"
        self:walk()
    end
}

In the beginning of the simulation, it is necessary to indicate that no cell was visited. The code below declares a Cell named cell, that initializes attribute washere with "no". It also has a function named state, that gets itself as argument and returns the value of washere if it isEmpty. Otherwise, when singleFooAgent is within the cell, it returns "foo".

cell = Cell{
    washere = "no",
    state = function(self)
        if self:isEmpty() then
            return self.washere
        else
            return "foo" -- singleFooAgent is here
        end
    end
}

To allow every Cell of the CellularSpace to have these properties, it is necessary to set argument instance when the CellularSpace is created, as shown below. By doing so, every Cell will have the same properties defined in the instance. This idea of instance is also used for agents, as presented in the next examples.

cs = CellularSpace{
    xdim = 10,
    instance = cell
}

The map now has three possible values: "no" (cells not visited by the agent), "yes" (cells visited by the agent), and "foo" (where the agent is). Note also that we select the function "state". As it is a function, Map draws the returning value of this function in each Cell.

map = Map{
    target = cs,
    select = "state",
    value = {"no", "yes", "foo"},
    color = {"lightGray", "gray", "blue"}
}

It is also necessary to create an Environment, call createPlacement, and instantiate a Timer with two Events as in the previous example. The complete source code of this example is available here. The final output is shown below.

Society

Creating agents as presented above is simple, but it may be contraproductive when one needs to have even a small set of agents acting and interacting with each other. The type that represents a collection of agents with the same set of properties and temporal resolution is called Society. The constructor of a Society always requires a quantity and an instance that contains the basic properties and function execute(). Code below describes a non-foo society with 50 agents that look for an Agent named "foo" in their neighborhood. In this example, every agent will start with name "nonfoo". In the execute(), the agent selects a random neighbor and moves to it if empty (walkIfEmpty()). Then it looks in the neighborhood of its current cell (getCell) using forEachNeighbor. Each time an Agent finds an agent named "foo" in the neighborhood of the Cell it belongs , it prints "Found a foo agent in the neighborhood" in the screen.

myAgent = Agent{
    name = "nonfoo",
    execute = function(self)
        self:walkIfEmpty()

        forEachNeighbor(self:getCell(), function(neigh)
            if neigh:isEmpty() then return end

            if neigh:getAgent().name == "foo" then
                print("Found a foo agent in the neighborhood")
            end
        end)
    end
}

mySociety = Society{
    instance = myAgent,
    quantity = 50
}

mySociety:sample().name = "foo"

Each Cell of the CellularSpace will now have an owner, which is "none" if it isEmpty(), otherwise the owner will be the name of the Agent within the Cell. The CellularSpace will have 400 cells and von Neumann neighborhoods. The wrap argument connects the opposite borders, guaranteeing that all Cells will have exactly four neighbors.

cell = Cell{
    owner = function(self)
        if self:isEmpty() then
            return "none"
        else
            return self:getAgent().name
        end
    end
}

cs = CellularSpace{
    xdim = 20,
    instance = cell
}

cs:createNeighborhood{
    strategy = "vonneumann",
    wrap = true
}

The same procedure of the last example can be used to distribute the society over Space, as shown below. A call to Environment:createPlacement() puts every agent within the Environment in the CellularSpace. The default allocation puts at most one Agent in each Cell. Note that this restriction is only applied to the placement at the beginning of the simulation. Controlling the maximum number of Agents in each Cell along the simulation is always up to the modeler in TerraME.

env = Environment{mySociety, cs}

env:createPlacement{}

The final part of the code creates a Map with the owner of each Cell. It displays a grid arount each Cell and uses three colors, "gray" where there is no agent, "blue" for "foo" and "yellow" for the other agents. The simulation has two Events and simulates for 100 time steps.

map = Map{
    target = cs,
    select = "owner",
    value = {"none", "foo", "nonfoo"},
    color = {"gray", "blue", "yellow"},
    grid = true
}

t = Timer{
    Event{action = mySociety},
    Event{action = map}
}

t:run(100)

The simulation prints "Found a foo agent in the neighborhood" in the screen a couple of times. The final state is shown below. Note that there is only one blue cell, where the "foo" agent is located.

Argument instance can also be used to easily create initial attributes for agents from statistical distributions. For example, a population whose individuals have two attributes, age and number of children. The age is based on a normal distribution with mean 30 and standard deviation 4, while the number of children is based on a Poisson distribution with lambda 2. The two attributes can be described as their distributions, as shown in the code below, using Random. When the population is created, each of its Agents has as initial value a Random:sample() over these distributions.

In each time step, an Agent increases its age by one. If it has less than 50 years old it can have a new child with 10% of probability (p = 0.1), according to reproduce.

reproduce = Random{p = 0.1}

person = Agent{
    age = Random{mean = 30, sd = 4},
    children = Random{lambda = 2},
    execute = function(self)
        self.age = self.age + 1

        if self.age < 50 and reproduce:sample() then
            self.children = self.children + 1
        end
    end
}

population = Society{
    instance = person,
    quantity = 100
}

To see how the number of children evolves over time, we can create a Chart using population as target and simulate the model for 30 time steps.

chart = Chart{
    target = population,
    select = "children"
}

t = Timer{
    Event{action = chart},
    Event{action = population}
}

t:run(30)

The output of a simulation is shown in the figure below.

In TerraME, agents can also have an optional user-defined function Agent:init(), called when the Agent is created. It is useful when one wants to create more complex properties, such as values that depend on other attributes of the Agent itself.

Group

Another basic type for agent-based models is Group, which is an ordered subset of a Society. Groups are created in the same way of trajectories, except by the fact that they use societies as target instead of cellular spaces. A filter function returns whether an agent will belong to the group, while a sort defines the group’s traversing order. Groups are created independently from each other, which means that an agent may enter, leave, or belong to different groups. Code below shows an example of creating a group that selects agents that have size greater than ten.

biggers = Group{
    target = society,
    filter = function(agent)
        return agent.size > 10
    end
}

biggers:execute()

Groups are similar to trajectories and have properties that can facilitate using agents in the same way that trajectories does for cells. For example, every time a society is activated, its agents are executed in the same order they were created. Because of that, calling Society:execute() directly is only recommended when the execution order makes no difference in the simulation results. Whenever it may affect the results, it is better to use groups instead of societies. Groups can be used to establish a traversing order, such as shown in Code below, where agents that have larger size will be executed first. In the code, before executing the agents a second time, the group is rebuilt, which is necessary when the attributes used in the sort functions change along the simulation and these changes need to be taken into account by the model.

biggersFirst = Group{
    target = society,
    sort = function(a1, a2)
        return a1.size > a2.size
    end

biggersFirst:execute()
biggersFirst:rebuild()
biggersFirst:execute()

Argument select is optional, making possible to create a group covering the whole society to establish a new traversing order according to some rule. Sort is also optional, which is useful when one wants to create subsets of a society where the execution order makes difference, or when the group needs to be executed randomly by calling Group:randomize(), as shown in code below. Group inherits all functions of Society. Functions such as Society:execute() can be called directly from the Group, applying such functions only for the agents within the group.

males = Group{
    target = society,
    filter = function(agent)
        return agent.sex == "male"
    end
}

males:randomize()
males:execute()

SocialNetwork

Agents can be connected directly to each other to represent relations such as a friendship, family, commercial relations, or just a contact. TerraME provides a type called SocialNetwork to work with such connections. A SocialNetwork is simply a set of agents. It is possible to create a SocialNetwork from scratch, but TerraME contains a set of options to create them using strategies available in the literature. Function Society:createSocialNetwork{} creates a SocialNetwork for each Agent within a given Society. Code below creates a SocialNetwork where each Agent is connected to five other random Agents. The second-order function forEachConnection() allows one to traverse the SocialNetwork.

soc:createSocialNetwork{quantity = 5}

ag = soc:sample()

if ag:isSick() then
    forEachConnection(ag, function(friend)
        friend:getSick() -- spreading a disease
    end)
end

When agents need to have more than one SocialNetwork, it is possible to give names to them. For example, the same code above can be rewritten to use a name to the connections (in this case, veryfriendones) as shown below. The name must be used within createSocialNetwork{} as well as second argument of forEachConnection().

soc:createSocialNetwork{quantity = 5, name = "veryfriendones"}

ag = soc:sample()

if ag:isSick() then
    forEachConnection(ag, "veryfriendones", function(friend)
        friend:getSick()
    end)
end

In the two SocialNetworks build so far, after creating them, the connections will not change unless explicitly defined by the modeller (for example, by using add and remove). However, it is also possible to define the SocialNetwork without explicitly storing the connections by using argument inmemory = false. When one does this, every time the SocialNetwork is manipulated, it computes the connections from scratch according to the defined strategy. The code below shows an example of using forEachConnection twice. Each of them select five random Agents randomly from the Society.

soc:createSocialNetwork{quantity = 5, inmemory = false}

ag = soc:sample()

if ag:isSick() then
    forEachConnection(ag, function(friend)
        friend:getSick() -- five random agents will get sick
    end)

    forEachConnection(ag, function(friend)
        friend:getSick() -- other five random agents
    end)
end

When it is required to build a SocialNetwork between Agents of two Societies, it possible to use argument target. Code below connects five students to each professor. Using symmetric = true indicates that, if a student is connected to a given professor, the professor will also be connected to the student, and vice-versa.

professors:createSocialNetwork{
    target = students,
    quantity = 5,
    symmetric = true
}

More available strategies to create SocialNetworks can be found in the documentation of Society:createSocialNetwork{}.

Messaging

TerraME has a very simple environment for exchanging messages between agents. It uses Lua facilities to describe the content of a message, storing them as tables. Function Agent:message{} is the way an agent can exchange information with other agents. The only compulsory argument is receiver. The other ones depend on the model and can be used freely by the modeler. Code below describes a simple message, where an agent sends a message to its fellow with 100 units of money.

agent:message{receiver = myFellow, content = "money", value = 100}

When a message is sent, the receiver gets it through an internal function called Agent:on_message{}, which must be implemented in the constructor of the Agent. If Agent:on_message() is not implemented for an Agent that needs to receive a message then a warning will be generated by the simulation. Code below shows an example of receiving a message. The message is a table with several attributes. The receiver gets the same table used to send the message, plus attribute sender (who sent the message) and without receiver (which is unnecessary). Every attribute created by the sender when the message was sent is available in this table.

myAgent = Agent{
    on_message(self, message)
        if message.content == "money" then
            self.money = self.money + message.value
        end
    end,
    -- ...
}

When needed, messages might be answered in two ways. First, the receiver can send a new message normally using Agent:message(), forcing the original sender to get the answer in its own Agent:on_message(), interpret it, and then continue its execution at the point where the first message was sent.

myAgent = Agent{
    on_message(self, message)
        if message.content == "money" then
            self.money = self.money + message.value
            self:message{receiver = message.sender, content = "thanks"}
        end
    end,
    -- ...
}

Another option is to send a message through the returning value of Agent:on_message(), as shown in code below. In this case, the sender will receive the answer as a result of its call to Agent:message(), avoiding large stacks of messages when the communication involves exchanging lots of messages.

myAgent = Agent{
    on_message = function(self, message)
        if message.content == "money" then
            self.money = self.money + message.value
            return {content = "thanks"}
        end
    end,
    -- ...
}

The messages presented so far are delivered as soon as they are sent. This type of message is called synchronous. Another option to exchange messages in TerraME is by using asynchronous messages. Messages sent asynchronously go to a pool within the Society the Agent belongs and stay there until some call to Society:synchronize(). In this case, a message has a temporal delay that indicates how long the message needs to wait until finally delivered. Code below shows an example of sending asynchronous messages between Agents. Note that Society:synchronize() is semantically very different from CellularSpace:synchronize().

paul:message{receiver = john, delay = 1, content = "greetings"}
paul:message{receiver = john, delay = 3, content = "farewell"}

society:synchronize() -- greetings
society:synchronize()
society:synchronize() -- farewell

Messages sent asynchronously are not directly connected to the simulation time by default. To link them, it is possible to use a Society as action of an Event, as described in code below. Every time this Event is activated, the Society synchronizes its messages after executing its agents, connecting the synchronization interval to the period of the Event.

t = Timer{
    Event{action = soc} -- soc:execute() then soc:synchronize()
}

Life span

Mainly in models that do not use real world data, agents may have a life span. TerraME provides functions Agent:die() and Agent:reproduce() to deal with this. Agent:die() removes the agent from the Society it belongs as well as its placement relations. It is recommended that an agent should die at the end of its execution because calling Agent:die() does not stop the agent immediately. The other function, Agent:reproduce(), creates a new Agent similar to the parent, with the same placement relations, and puts it into the same Society.

Code below shows part of a predator-prey model. In this model, there are two types of agents, predators and preys, which belong to two different societies. Each predator feeds preys, killing at most one prey each time it is executed. Because of that, when it finds a prey in the cell it belongs, the function taken as argument of forEachAgent() returns false to stop looking for other Agents within the same Cell. Whenever a predator reaches 50 of energy, it reproduces. In the end, if its energy ends up, it dies.

model.wolf = Agent{
    energy = 40,
    name = "wolf",
    execute = function(self)
        local cell = self:getCell():getNeighborhood():sample()

        if cell:isEmpty() then
            if self.energy >= 50 then
                local child = self:reproduce()
                child:move(cell)
                self.energy = self.energy / 2
            else
                self:move(cell)
            end
        elseif cell:getAgent().name == "rabbit" then
            local prey = cell:getAgent()

            self.energy = self.energy + prey.energy * 0.2
            prey:die()
        end

        self.energy = self.energy - 4

        if self.energy < 0 then
            self:die()
        end
    end
}

It is possible to call reproduce() from any place in the source code. The same is valid for die(), as long as the Agents belong to different Societies. Killing Agents that belong to the same Society requires a more elaborated solution.

Database Access

Societies can be retrieved from external sources, instead of being built from scratch along the simulation. Any data set can be materialized as a Society, in such a way that each entity of the given set will produce an agent. For example, suppose that a database contains a layer of polygons representing the farms of a given region, as shown below.

Using this database, we can build a society describing where the data is stored (dbms, database, user and layer) and an instance to describe the overall behavior of its agents. Code below shows an example of loading a society from a database. For each polygon of the layer santarem, an Agent will be created with the attributes of the polygon plus the attributes and functions defined by the instance farmer.

farmer = Agent{
    -- ...
}

soc = Society{
    file = "santarem.shp"
}

soc:execute()

In the case where the external source is a geospatial database, the relations within and between societies as well as placement relations can also be retrieved. To accomplish that, the modeler needs to execute the necessary algorithms to create the graph with the specific operations and save the results. Examples of operations related to each of the four possible types of relations are depicted in Table 1. They are:

  • agent→cell: A layer of polygons representing farms can be connected to a layer of squared cells to represent the minimum spatial partitions where the agent, a farmer, can take its land change decisions. Each farm can be connected to the cells whose overlay is more than half the cell.
  • cell→agent: A set of cells can be connected to a set of points, representing the locations of human beings, according to the result of the within predicate.
  • cell→cell: A cellular space can have its cells connected to those within a given traveling time radius, considering different velocities of each road.
  • agent→agent: Factories represented as points can be connected to those within a given distance radius that depends on some property of the agents.

amaz

TerraME loads only attributes from a database. It considers that, instead of agents having to query a geographic database whenever they need an answer about a specific spatial structure, the representation of space within the model is already filled with all the necessary data by means of attributes or relations. We assume the modeler previously knows the queries the agents may perform along the simulation. For example, a neighborhood could represent a suitability map, where the neighbors of a cell represent a criteria such as visibility or possible movements. It can be considered a limitation of this approach, but it is a way to consider the problem of using geospatial data, with the advantage of separating GIS functions from the simulation. The idea is that both applications can work harmonically but separately.

Filling the whole space with the results of the queries commonly requires more time than performing a couple simulations. However, once the data is already in the database, the simulation runs faster because there is no need to maintain a connection to a geographic database to perform the same queries repeatedly. Depending on the application, it can even be considered an advantage, once repeating simulations is a common procedure for studying the overall behavior of a model. Reading relations that involve a single society can be performed directly trough Society:loadSocialNetwork(). When reading from a society loaded from a database, it is possible to load the relations simply by passing the id of the GPM in the database, as shown in code below. Loading relations from a database is safer because verifying whether the correct collection of objects is being used is in charge of TerraME. Otherwise, it will be up to the modeler.

soc:loadSocialNetwork{file = "myfile.gal"}

To create relations involving a Society and another set, be it a Society or a CellularSpace, it is necessary to instantiate an Environment previously. Two functions can then be used to retrieve the relations: Environment:loadPlacement() and Environment:loadSocialNetwork(). The first one establishes relations between a society and a cellular space, while the latter loads relations between two societies. The same rules are applied to manipulate relations created from these two functions or from scratch. Take as example the Society presented in Code 21, a layer of cells of Figure 3 stored in the same database, and a file with a GPM storing the relations between the society and the cells, indicating which farm each cell belongs. Code below describes an example of how to load the connections between the agents and the cells. In this example, each agent chooses a random cell it owns and changes its cover to pasture.

cs = Society{
    database = "amazonia",
    dbms = "mysql",
    user = "andrade",
    layer = "cells"
}

env = Environment{
    nonFooSociety,
    cs
}

env:loadPlacement{file = "mygpm.gpm"}

forEachAgent(nonFooSociety, function(agent)
        agent:getPlacement():sample().cover = "pasture"
    end)
end)

Multiple Placement

Placement is usually related to the physical location of an agent. However, it is possible that an agent needs to be connected to cells by multiple reasons. One can own one or more cells and have others as target for something, or have a house and a workplace, for instance. Therefore it might be necessary for an Agent to be connected to multiple Cells at the same time, with each connection having an independent semantics. For example, one Agent can be physically in a Cell, live in a house located in a second Cell, and work in a building within a third Cell. The basic way of using placement functions in TerraME allows the modeler to work with one type of placement, but each function that deals with placement has an optional argument that names the relation to be used. For example, function Environment:createPlacement{} has another argument with its name. Code below creates two placements: a renting relation is instantiated without any agent ("void") and a workplace filled with one "random" Cell for each Agent.

env = Environment{cellspace, society}

env:createPlacement{strategy = "random", name = "workplace"}
env:createPlacement{strategy = "void", name = "renting"}

Code below describes how to change relations in these placements. In this sense, calling Agent:enter(), Agent:move(), or Agent:leave() changes the relation of a given placement, keeping the other placements unchanged.

agent:enter(onecell, "renting")      -- choose a house to live
agent:move(anothercell, "workplace") -- change the workplace

The relations created previously are bidirected, which means that TerraME stores the cells connected to an agent within the agent and the agents connected to a cell within the cell. Functions Agent:enter(), Agent:move(), and Agent:leave() control both connections to change the relations properly. It is also possible to create one-sided relations in the case where the relation in the opposite direction is not necessary. For example, it might be necessary to have a set of cells that one agent wants to buy, being useless to know, for any given cell, which agents want to buy them. This kind of situation can always be implemented with bidirected relations, but one can save memory by creating the placement directly from the Society or from the CellularSpace, instead of from the Environment. Internally, TerraME uses groups and trajectories to store these relations. Each placement has an index, which must be a name different from any other attribute of the agents and cells it is going to be related. As default, placement relations are stored in a variable called "placement", with object agent.cells (cell.agents) storing the same value of agent.placement.cells (cell.placement.agents) to allow using forEachCell() (forEachAgent()) directly. Other placement relations do not have this facility, requiring to be traversed manually, as presented in code below.

myPlacementFunction = function(cell)
   -- ...
end)

forEachCell(agent, myPlacementFunction)
forEachCell(agent.placement, myPlacementFunction) -- same as previous
forEachCell(agent.renting, myPlacementFunction)

Placement relations are stored as Groups and Trajectories. The modeler can manipulate them directly as well as use the three basic placement functions (enter, leave, and move). Code below shows examples of both strategies, which have a small but important difference. Strategy (a) adds a new Cell to the Agent, while (b) does the same and adds the Agent to the new Cell. Because of that, (a) is recommended for one-sided relations, while (b) can only be used to handle symmetric relations. In this sense, code below cannot be used to create the relations manipulated by code above because renting is a one-sided relation.

forEachAgent(society, function(agent)
    for i = 1, 10 do
        agent.workplace:add(cellspace:sample())       -- (a)
        agent:enter(cellspace:sample(), "workplace")  -- (b)
    end
end

Indirect Relations

The common way to store relations in TerraME is by using a direct connection between objects. This strategy was used in all the previous examples. Although simple, this way of working with relations has two limitations. First, storing relations explicitly requires memory, which can be unfeasible when working with many agents. Second, there may exist relations that depend upon other relations, also called indirect (Torrens and Benenson, 2005). Indirect relations stored as direct connections must be recomputed every time step, even if they are not used by any entity of the model. Sometimes it is preferable to spend more processing time to save memory, being acceptable to need more time to return the relations of a given entity as long as the adopted strategy saves memory. To overcome this hurdle, TerraME allows the modeler to create relations that are computed dynamically and do not require memory to be stored explicitly along the simulation. Take for instance a SocialNetwork where a given Agent is connected to each other that belongs to the neighbor Cells of the Agent's current Cell. It is never efficient to store this relation explicitly as long as the Agents relocate frequently. Code below describes how to create an indirect relation to compute such relation. Function Agent:addSocialNetwork() can get social networks or functions that return social networks as argument. Although they are quite different from static relations, once the criteria that creates them is established, they can be used transparently by the modeler, as if they were static relations, for instance to execute forEachConnection().

runfunction = function(agent)
    local rs = SocialNetwork()
    forEachNeighbor(agent:getCell(), function(neigh)
         forEachAgent(neigh, function(agentwithin)
             rs:add(agentwithin)
         end)
     end)
     return rs
end

agent:addSocialNetwork(runfunction, "neighborhood")

forEachConnection(agent, "neighborhood", function(other)
    -- ...
end)

Some strategies to create indirect relations are available in TerraME by means of Society:createSocialNetwork(). For example, the code below describes how to apply the indirect relation of Code 28 to a whole society using a Moore neighborhood. Note that this neighborhood needs to be generated by the modeler in the cellular space where the agents have placement relations to ensure that the indirect relations will work properly.

soc:createSocialNetwork{
    neighbor = "moore"
    name = "byneighbor"
}

agent = soc:sample()

forEachConnection(agent, "byneighbor", function(other)
    -- ...
end)

References

N. Gilbert (2007). Agent-Based Models (Quantitative Applications in the Social Sciences). 153. SAGE Publications.

N. Gilbert and K. G. Troitzsch (2005). Simulation for the Social Scientist. Open University Press.

P. Torrens and I. Benenson (2005). Geographical automata systems. International Journal of Geographical Information Science (IJGIS), v. 19, n. 4, p. 385–412.