-
Notifications
You must be signed in to change notification settings - Fork 13
Pedro R. Andrade
-
General
- Why can I choose different names for the arguments of second-order functions?
- What does underscore (
_
) mean in the argument of second-order functions? - Why doesn't
forEachElement
execute in the same order when using named tables? - When to use
.
and when to use:
? - Why some functions are called with
()
while others use{}
? - Why doesn't
!=
work in Lua?
-
Cell
,CellularSpace
,Neighborhood
, andMap
- How to set (x, y) locations of
Cells
read from external sources? - Why it is not possible to remove an attribute of a
Cell
when I use an instance? - My
Map
is upside down. How to fix it? - How does transparency work?
- Can I create other types of
Neighborhoods
? - Is it possible to have more than one
Neighborhood
perCellularSpace
?
- How to set (x, y) locations of
-
Random
-
Agent
,Society
, andSocialNetwork
- How to define a maximum number of
Agents
in eachCell
? - How to kill agents within the same
Society
? - How to create a
SocialNetwork
from scratch? - How to add and remove connections along the simulation?
- Can an
Agent
have multipleSocialNetworks
? - How to recompute connections?
- Is it possible to have other functions to receive messages besides
on_message()
? - Is it possible to have different synchronization times for
Societies
?
- How to define a maximum number of
-
Timer
andEvent
Second order functions, such as forEachElement
or forEachNeighborhood
, are
the way to apply a piece of code over each object of a given set. For example,
forEachNeighborhood
is applied over each neighborhood of a given Cell
.
It is up to TerraME to call such functions internally and set its arguments.
Such functions are always called with some arguments in the same order.
As the applied function is user defined, the user is responsible
to define the name of the arguments. For example, forEachOrderedElement
calls
a second-order function passing the arguments (1) name of the attribute,
(2) value of the attribute, and (3) type of the attribute. In the code below,
a
will get the name of the attribute, b
will get the value, and c
will get
the attribute's type.
mytable = {
value1 = 2,
value2 = 5,
value5 = 7
}
forEachOrderedElement(mytable, function(a, b, c)
print(a, b, c)
end)
It will produce the following output:
value1 2 number
value2 5 number
value5 7 number
If the code exchange the names of the first and second arguments, b
will get
the name of the attribute and a
will get its value.
forEachOrderedElement(mytable, function(b, a, c)
print(a, b, c)
end)
We will have a slightly different output:
2 value1 number
5 value2 number
7 value5 number
Of course, it is recommended to use names that better represent the values of the arguments instead of a
, b
, and c
.
As TerraME always calls second order functions with the same arguments, sometimes some of the
arguments are not necessary. If the unnecessary arguments are the last ones, it is possible to
ignore them as arguments. For example, a call to forEachElement
that uses the three available
arguments is like:
forEachElement(mytable, function(name, value, type) ... end)
If one only needs only name
and value
, it is possible to declare the second order function
with only two arguments:
forEachElement(mytable, function(name, value) ... end)
However, if the user needs only value
, but not name
, it is necessary to declare
a name for the argument name
beause the value is the second argument.
To indicate that we do not need the first argument, we
usually use _
as name for a given argument.
forEachElement(mytable, function(_, value) ... end)
Lua does not guarantee that a named-table will be trasversed in the same way when executed twice. For example, the code below prints the names and values of a given table:
mytable = {
value1 = 2,
value2 = 5,
value5 = 7
}
forEachElement(mytable, function(name, value)
print(name, value)
end)
It might produce an output like:
value2 5
value5 7
value1 2
or:
value5 7
value2 5
value1 2
If you need to guarantee that the table will be traversed always in the same order, use
forEachOrderedElement
instead.
Basically, operator .
is used to access attributes from a table or from an object of a type defined by TerraME (Cell
, CellularSpace
,
Agent
, etc.). Operator :
is used to call a function from such object.
agent = Agent{
age = 10,
execute = function(self)
self.age = self.age + 1
end
}
print(agent.age)
print(agent.execute) -- it does not execute the function
print(agent.age) -- still 10
It is possible to call execute
using .
. In this case, it would be necessary to set the agent itself as argument of the function
agent.execute(agent)
print(agent.age) -- 11
The code above can easily create errors in the code, for example when one forgets to set the argument (agent.execute()
),
or when the argument is not the agent itself (e.g. agent.execute(agnt)
).
Operator :
is a syntax sugar to avoid such problems, because it adds the agent itself to the first argument of the
function:
agent:execute()
print(agent.age) -- 12
When the function to be called has more than one argument, as the first is hidden, the second argument in the declaration of the function becomes the first one in the function call, and so on. For example:
agent = Agent{
age = 10,
getOld = function(self, years)
self.age = self.age + years
end
}
print(agent.age)
agent:getOld(5) -- 5 years
print(agent.age) -- 15
Functions called with ()
get a set of arguments in a given order. Each argument has a specific meaning and
they are passed as simple values. For example:
agent:move(cell)
On the other hand, functions called with {}
get a single argument: a table with named attributes.
The arguments can be described in any order and the names have a specific meaning to the function.
For example:
agent:message{receiver = otheragent, content = "hello"}
Usually functions with a small number of arguments (up to three) that can be ordered from the most common
(first argument) to the least common (last one) are called with ()
.
Those with more (or a user-defined number of) arguments without a straight order usually use {}
.
Unlike other languages, Lua does not have a !=
operator. The operator that verifies if two values
are different is named ~=
.
When TerraME reads a CellularSpace
, if it finds attributes col
and row
it uses them to represent
the spacial location of the objects. Supposing that the data has attributes mycol
and mylin
with this
information, it is possible to set argument xy
to use them.
mycs = CellularSpace{
file = "myfile.shp",
xy = {"mylin", "mycol"}
}
When one uses an instance
to create a CellularSpace
, every Cell
will have the
attributes of the instance
as default. It means that those attributes do not need
to be declared in the Cells
explicitly. When a given Cell
does not have a given
attribute value or function, Lua automatically searches for the specific attribute
or function in the instance
. This way, when one sets a given attribute of a Cell
to nil
, it restarts the value to the one defined in the instance
. For example, in
the code below, the last line will show 2
.
cell = Cell{value = 2}
cs = CellularSpace{
xdim = 2,
instance = cell
}
sample = cs:sample()
print(sample.value)
sample.value = nil
print(sample.value) -- still 2
In TerraME, as default the (0, 0) point of a CellularSpace
is located on the bottom left. If the Map
is
upside down, the (0, 0) point should be the top left. To invert a Map
, it is necessary to indicate this
when one reads the CellularSpace
, using argument zero
. The example below reads a map inverting the y
axis.
amazonia = CellularSpace{
file = filePath("amazonia.shp"),
zero = "top"
}
See the documentation of CellularSpace
for more information,
specially when one uses argument xy
.
Every value that does not belong to the interval [min
, max
] of a Map
, or is not
described in the values
, is drawn as transparent. This will be changed in a future
version of TerraME to give more flexibility to the modeler.
Neighborhoods are created using createNeighborhood
.
As default, when this function is called without any argument, TerraME creates a Moore (3x3) neighborhood. For other available options,
see the documentation in the link above.
Yes, one Cell
can have more than one Neighborhod
.
CellularSpace:createNeighborhoodi()
has an argument
name
, whose default value is "1"
. If you call CellularSpace:createNeighborhood()
twice, with different names, both neighborhoods are stored
in the Cell
. For example:
cs = CellularSpace{
xdim = 10
}
cs:createNeighborhood{
name = "moore"
}
cs:createNeighborhood{
strategy = "vonneumann",
name = "vonneumann",
self = true
}
To trasverse such neighborhoods, it is necessary to set their name in the second argument of forEachNeighborhood()
.
In this case, the second order function will be the third argument instead of second.
forEachNeighbor(cs:sample(), "moore", function(neigh)
neigh.state = "alive"
end)
Objects of type Random
can generate random numbers by calling
sample()
.
When a Random
object belongs to a Cell
(Agent
) that is used as instance
to a CellularSpace
(Society
), it is not necessary to call sample()
explicitly. TerraME calls it internally to initialize the attribute values of
the Cells
(Agents
) of the CellularSpace
(Society
).
When creating a placement using
Environment:createPlacement()
,
it is possible to define a maximum number of Agents
per Cell
using argument max
, whose
default is one.
After that, TerraME allows a Cell
to have any amount of Agents
. However, as the common strategy in
the literature is to have at most one Agent
per Cell
, functions walkToEmpty()
, walkIfEmpty()
,
and emptyNeighbor()
will
implement relocation considering a Cell
full if there is a single Agent
within it.
If the maximum is greater than one, it is up to the modeller to control whether a Cell
is full or not.
There is no problem on calling Agent:die()
to agents belonging to another Society
.
However, it is not recommended to kill another Agent
that share the same Sociegy
, because it is usually scheduled to execute in the same Event
of the killer.
In this case, it is better to have a third entity to work as a mediator.
Code below shows one example where a mediator called master
manages the Agents
that need to be removed from the simulation.
It gets messages with the target agents and stores them into a Group
.
After the Society
executes in a given time step, master
removes the agents from its list, ensuring that each Agent
will have the opportunity to be executed in each time step.
master = Agent{
to_kill = Group{build = false},
on_message = function(self, message)
self.to_kill:add(message.kill)
end,
execute = function(self)
forEachAgent(self.to_kill, function(agent)
agent:die()
end)
self.to_kill:clean()
end
}
predator = Agent{
energy = 40,
execute = function(self)
forEachAgent(self:getCell(), function(other)
if other ~= self and math.random() < 0.1 then
self.energy = self.energy + other.energy / 2
self:message{target = master, kill = other}
end
end)
-- ...
end
}
predators = Society{
instance = predator,
quantity = 50
}
t = Timer{
Event{action = predators},
Event{action = master}
}
It is possible to create a SocialNetwork
from scratch in TerraME.
Code below creates a SocialNetwork
with two agents, john
and mary
.
When the SocialNetwork
is added to myself
through Agent:addSocialNetwork()
, it connects myself
to john
and mary
.
Note that this code does not establish any connection between john
and mary
.
soc = Society{...}
john = soc:sample() -- get a random agent from the society
mary = soc:sample()
myself = soc:sample()
friends = SocialNetwork()
friends:add(john)
friends:add(mary)
myself:addSocialNetwork(friends) -- adding two connections to myself
print(#myself:getSocialNetwork()) -- amount of agents in the social network
It is possible to add()
and remove()
agents from a SocialNetwork
after creating it. For example, given an agent
, it can add a
friend
to its network and remove a nomorefriend
one:
agent:getSocialNetwork():add(friend)
agent:getSocialNetwork():remove(nomorefriend)
If the Agents
have more than one SocialNetwork
, it is possible to indicate which one will be used:
agent:getSocialNetwork("friends"):add(friend)
agent:getSocialNetwork("friends"):remove(nomorefriend)
agent:getSocialNetwork("nomorefriends"):add(nomorefriend)
Each Agent
can have one or more social networks attached to it.
They are indexed as strings, having an implicit value of "1"
if not specified when calling Agent:addSocialNetwork()
.
Therefore, whenever the modeler wants to have more than one social network per agent, it is necessary to use explicit indexes.
Every function of TerraME that uses social networks has an optional argument to describe which index will be used, such as the second parameter of Agent:addSocialNetwork()
, as shown in code below.
Note that forEachConnection()
has a construction that takes three arguments instead of two, shifting the function to third argument in order to add the index before it.
Using this strategy one can choose from a set of SocialNetworks, representing, for instance, family, friends, and colleagues.
family = SocialNetwork()
family:add(Jonh) -- Jonh is an Agent
family:add(Mary) -- Mary too
family:add(Anekee) -- Anekee too
myself:addSocialNetwork(family, "family") -- myself is also an Agent
friends = SocialNetwork()
friends:add(Mark)
friends:add(Richard)
myself:addSocialNetwork(friends, "friends")
forEachConnection(myself, "family", function(familyMember)
-- ... do something with my family
end)
forEachConnection(myself, "friends", function(friend)
-- ... do something with my friends
end)
Connections can be recomputed by calling
Society:createSocialNetwork()
again, or by setting argument inmemory = false
when calling such function for the first time. When this
argument is used, every time the SocialNetwork
is required, TerraME recomputes it.
Messages can have an argument called subject
.
It is a string that indicates which function of the receiver will be called whenever the message arrives.
It facilitates implementing a model where there are different behaviors for different subjects of messages.
In TerraME, a message with subject "x"
will be received by a function called on_x()
.
As the default way of receiving messages is through "on_message"
, the default subject of a message is "message"
.
Code below presents one example of two subjects, "hello"
and "goodbye"
, each one with a different behavior associated.
citizen = Agent{
-- ...
on_hello = function(self, message)
self:addConnection(message.sender)
end,
on_goodbye = function(self, message)
self:removeConnection(message.sender)
end
}
-- jonh, mary, and joane are citizens
john:message{subject = "hello", receiver = mary}
john:message{subject = "goodbye", receiver = joann}
Society:synchronize()
has one optional argument to depict the temporal interval between the last synchronization and the current one.
The default value is the last synchronization time plus one.
Code below uses different synchronization points.
paul:message{receiver = anne, delay = 1, content = "greetings"}
paul:message{receiver = anne, delay = 2, content = "takethis", x = 3}
paul:message{receiver = anne, delay = 2.5, content = "farewell"}
paul:message{receiver = anne, delay = 5, content = "whereareyou"}
society:synchronize() -- greetings (until time 1)
society:synchronize(1.6) -- takethis, farewell (until time 2.6)
society:synchronize() -- nothing happens (until time 3.6)
society:synchronize(4) -- whereareyou (until time 7.6)
Timer
is a queue of Events
ordered by the time each of them will be executed.
At each simulation step, a Timer
gets the next Event
, updates its internal clock to the registered time for the Event
, and then executes its action
.
After that, the Event
will be scheduled to occur again in the current time plus its period
.
When there are two Events
with the same time and priority, the one added first to such time will be executed first. When a Timer
is created,
Events
are added to the Timer
in the same order they are declared. See the script below:
timer = Timer{
Event{start = 2, period = 2, priority = "high", action = function()
print("first")
end},
Event{action = function()
print("second")
end},
Event{start = 2, period = 3, priority = "low", action = function()
print("third")
end},
Event{period = 2, action = function()
print("fourth")
end}
}
timer:run(4)
It will produce the following output:
second (end of time 1)
first
fourth
second
third (end of time 2)
second (end of time 3)
first
fourth
second (end of time 4).
Usually a Timer
stops in the time passed as argument to run()
.
However, it might stop if it does not contain any Event
.
If the action
of an Event
returns false
, it will not be added to the Timer
again.
For example, the code below should run until time ten, but as the only event returns false
when the simulation time is greater than one, timer
will stop in time two.
Note that the action
needs to get the Event
itself as argument to access the current simulation time through getTime()
.
timer = Timer{
Event{action = function(self)
print("Rained")
if self:getTime() > 1 then
return false
end
end}
}
timer:run(10)
Just call Timer:clear()
. As it is usually called from
an action
of an Event
, to have access to the Timer
itself use the second argument of action
(usually ignored). Note that, if it is called within an action
of an Event
, it must return false
,
otherwise it will be inserted again to the Timer
. For example:
mytimer = Timer{
Event{action = function(_, timer)
if #society == 0 then
-- mytimer might not be available here as it was not created yet
timer:clear()
return false
end
end}
}
When an Event
is executed, it is removed from the Timer
and then its action
is called. In the end of the action
,
the Event
is usually added to the Timer
to be executed in the future. If the the action should not be executed again, it
can simply return false
.
It is possible to create an Event
within the action of another Event
to add a delayed behavior.
For example, the following code simulates the behavior of a person while using a gas shower.
Each time step the person verifies whether it isCold()
. If true, it will open the faucet
a little bit more in order to getWarm()
. However, the gas shower takes some time to
getWarm()
, which we represent by a delayed Event
.
Timer{
Event{action = function(event, timer)
if isCold() then
-- open the gas shower a little bit more, which
-- increases the temperature of the shower in the future
timer:add{start = event:getTime() + 5, action = function()
getWarm()
return false
end}
end
end}
}
The person in the code above will open the faucet a couple of times before the water gets warm, which in turn will get warmer than the person desired after he stops opening the faucet.
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.