-
Notifications
You must be signed in to change notification settings - Fork 3
Comprehensive example
The full code of this example is included in this project. The NetLogo model is here. The Java code is here.
In this tutorial we will work with another NetLogo sample model, AIDS. It simulates the spread of HIV in a population. We are going to extract and analyze three graphs from this model.
-
Couple graph. The nodes of this graph are the people in our population. An edge appears between two individuals when they are in couple and disappears when they break up. We will display this graph in a viewer. The positions and the colors of the nodes will be the same as in the NetLogo simulation.
-
Infection graph. This is a "who infected who" graph. At the beginning it will contain only several isolated nodes, the initially infected individuals. When someone is infected, we will add a new node in the graph together with an arc from the person who infected him. We will display this graph with automatic layout. The initial infection sources will be colored in red and the remaining nodes in black.
-
Cumulative graph. This one is like the couple graph, but edges do not disappear when people break up. We will color the nodes of this graph as in the NetLogo simulation, but we will use an automatic layout. This graph is very important and we will come back to it later.
We will use three different senders, one for each graph. In the following procedure we clear all previously created senders, create our three senders, send 'graph cleared' events using each of them and setup style sheets and other UI sugar for each graph.
to setup-senders
gs:clear-senders
(foreach (list "couple" "infection" "cumulative") (list 2001 2002 2003) [
gs:add-sender ?1 "localhost" ?2
gs:clear ?1
gs:add-attribute ?1 "ui.title" word ?1 " graph"
gs:add-attribute ?1 "ui.antialias" true
gs:add-attribute ?1 "ui.stylesheet" "node {size: 8px;} edge {fill-color: grey;}"
])
end
We will call this procedure at the beginning of setup
to setup
setup-senders
clear-all
...
end
Now let us take care of the nodes of our graphs. People are created in a procedure called setup-people
. As you already guess, we have to make them send 'node added' events just after their creation:
to setup-people
crt initial-people [
gs:add "couple"
gs:add "cumulative"
...
set infected? (who < initial-people * 0.025)
if infected? [
gs:add "infection"
set infection-length random-float symptoms-show
]
...
]
end
Note that we add all the people to the couple graph and to the cumulative graph, but only the initially infected people to the infection graph. We will add other nodes to the last graph dynamically, when new people are infected. To do this, we have to slightly modify the infect
procedure:
to infect
if coupled? and infected? and not known? and [not infected?] of partner [
if random-float 11 > condom-use or random-float 11 > ([condom-use] of partner) [
if random-float 100 < infection-chance [
ask partner [
set infected? true
gs:add "infection"
]
]
]
]
end
Now the nodes are added correctly to the three graphs, but we also have to send some of their attributes. To color them, we will use the "ui.style"
node attribute. Moreover, we need to send the coordinates of the people to the couple graph because we want its vertices to have the same positions as in the NetLogo simulation. We revisit the setup-people
procedure and change it as follows:
to setup-people
crt initial-people [
gs:add "couple"
gs:add "cumulative"
setxy random-xcor random-ycor
gs:add-attribute "couple" "xy" list xcor ycor
...
set infected? (who < initial-people * 0.025)
if infected? [
gs:add "infection"
gs:add-attribute "infection" "ui.style" "fill-color: red;"
set infection-length random-float symptoms-show
]
...
]
end
The colors are assigned in the procedure assign-color
which is called on each step. We change it as follows:
to assign-color
ifelse not infected? [
set color green
gs:add-attribute "couple" "ui.style" "fill-color: green;"
gs:add-attribute "cumulative" "ui.style" "fill-color: green;"
][
ifelse known? [
set color red
gs:add-attribute "couple" "ui.style" "fill-color: red;"
gs:add-attribute "cumulative" "ui.style" "fill-color: red;"
][
set color blue
gs:add-attribute "couple" "ui.style" "fill-color: blue;"
gs:add-attribute "cumulative" "ui.style" "fill-color: blue;"
]
]
end
There is one more thing that we have to add in the procedure move
to update the node positions in the couple graph:
to move
rt random-float 360
fd 1
gs:add-attribute "couple" "xy" list xcor ycor
end
Now the nodes and their attributes are correctly sent to the external application, but what about the edges of our graphs? Unlike our introduction example, this model does not use links. The trick is to create them temporarily just to make them send 'edge added' events and kill them as soon. People decide to couple in the couple
procedure which will be modified as follows:
to couple
let potential-partner one-of (turtles-at -1 0)
with [not coupled? and shape = "person lefty"]
if potential-partner != nobody [
if random-float 10.0 < [coupling-tendency] of potential-partner [
set partner potential-partner
create-link-with partner [
gs:add "couple"
gs:add "cumulative"
die
]
...
]
]
end
When a couple breaks up (this happens in the uncouple
procedure) we have to remove the edge from the couple graph, but not from the cumulative graph:
to uncouple ;; turtle procedure
if coupled? and (shape = "person righty") [
if (couple-length > commitment) or ([couple-length] of partner) > ([commitment] of partner) [
create-link-with partner [
gs:remove "couple"
die
]
...
]
]
end
And finally, when we add a node to the infection graph, we have to add it together with the arc from the infecting to the infected person:
to infect
if coupled? and infected? and not known? and [not infected?] of partner [
if random-float 11 > condom-use or random-float 11 > ([condom-use] of partner) [
if random-float 100 < infection-chance [
ask partner [
set infected? true
gs:add "infection"
]
create-link-to partner [
gs:add "infection"
die
]
]
]
]
end
In our hands-on example we showed how to use GraphStream to display a graph produced by a NetLogo model. We used a NetStream receiver, a graph and a viewer. Actually, the middle element of our chain, the graph, was not necessary. The viewer maintains its own internal graph and can receive events directly from NetLogo. Moreover, it runs in the swing thread and handles the proxy pipe pumping. If we only want to display a graph produced by Netlogo, we can do it more efficiently using the following class:
public class SimpleNetStreamViewer extends Viewer {
public SimpleNetStreamViewer(NetStreamReceiver receiver, boolean autoLayout) {
super(receiver.getDefaultStream());
addDefaultView(true);
if (autoLayout)
enableAutoLayout();
}
}
To start the viewers, we have just to instantiate objects of this type:
public static void main(String[] args) {
// couple graph viewer
new SimpleNetStreamViewer(new NetStreamReceiver(2001), false);
// infection graph viewer
new SimpleNetStreamViewer(new NetStreamReceiver(2002), true);
// cumulative graph viewer
new SimpleNetStreamViewer(new NetStreamReceiver(2003), true);
}
It is interesting to know if the population in our NetLogo simulation lives in a "small world". In other words, if we take two persons A and B, is there a short chain of type "A made love with someone who made love with someone ... who made love with B". The cumulative graph can help us answer this question. Its diameter is the maximum of the lengths of the shortest paths between each pair of nodes A and B. The degrees of the nodes of the cumulative graph are another source of interesting information. The degree of a person is the number of persons with whom that persons was in couple.
It is possible to compute these measures directly in the NetLogo simulation, but it is not straightforward. On the other hand, GraphStream is a tool designed to analyze dynamic graphs. So why not to use it? Our plan is to map the events coming from NetLogo to a graph, compute its diameter and node degrees and send the results back to NetLogo which will plot them.
We will create a graph and plug the proxy pipe of our receiver on it. In this way the graph will receive the events coming from Netlogo and we can use it to compute its diameter and degrees. But how often to send the results? It is reasonable to do this once once per step of the Netlogo simulation. At each step the simulation will send us a 'step begins' event, we will receive it, compute the results and send them back to NetLogo. To do all these things, we will use the following class:
public class CumulativeGraphAnalyser extends SinkAdapter{
private NetStreamSender sender;
private Graph graph;
public CumulativeGraphAnalyser(NetStreamReceiver receiver, NetStreamSender sender) {
this.sender = sender;
graph = new SingleGraph("cumulative graph", false, false);
ProxyPipe pipe = receiver.getDefaultStream();
pipe.addElementSink(graph);
pipe.addElementSink(this);
}
@Override
public void stepBegins(String sourceId, long timeId, double step) {
// compute and send results here
}
}
We will instantiate an object of this class like this:
public static void main(String[] args) {
...
// cumulative graph viewer
NetStreamReceiver receiver = new NetStreamReceiver(2003);
new SimpleNetStreamViewer(receiver, true);
new CumulativeGraphAnalyser(receiver, new NetStreamSender(3001));
}
Note that the proxy pipe of our receiver will be automatically pumped by the viewer. We create a graph and plug it to the pipe, so it will automatically receive the events coming from NetLogo. We do not need the attribute events, that is why we declare it as an element sink. Note that our CumulativeGraphAnalyser
is also a sink and we plug it to the pipe in the constructor. When a 'step begins' event coming from NetLogo is pumped from the pipe, the overridden stepBegins
method will be executed automatically. In this method we will use the sender to return the computed results to NetLogo.
The last thing to notice is that in the constructor of the graph we switch the strict checking off. The reason is that in our model (as IRL) two persons can meet, couple, break up, then meet again and re-fall in love. This means that the 'edge added' event can be sent more than once for the same edge. By default, GraphStream throws an exception in such situations, but when the strict checking is off, the second 'edge added' event will be silently ignored.
The remaining part is very easy. To send events we need a sourceId
as unique as possible and an increasing timeId
counter.
public class CumulativeGraphAnalyser extends SinkAdapter{
...
private String mySourceId;
private long myTimeId;
public CumulativeGraphAnalyser(NetStreamReceiver receiver, NetStreamSender sender) {
...
mySourceId = toString();
myTimeId = 0;
}
@Override
public void stepBegins(String sourceId, long timeId, double step) {
// diameter
double diameter = 0;
if (Toolkit.isConnected(graph))
diameter = Toolkit.diameter(graph);
sender.graphAttributeAdded(mySourceId, myTimeId++, "diameter", diameter);
// degrees
for (Node node : graph)
sender.nodeAttributeAdded(mySourceId, myTimeId++, node.getId(), "degree", node.getDegree());
}
}
We send the diameter as graph attribute. If the graph is not connected we send 0 instead. The degree of each node is sent as node attribute.
Welcome back to the NetLogo side. We don't have cookies, but we do have some events to receive. We start by setting up our receiver:
to setup-receiver
gs:clear-receivers
gs:add-receiver "cumulative" "localhost" 3001
end
It is pure coincidence that our receiver has the same identifier as one of the senders. We cannot have two receivers or two senders with the same identifier, but it is OK to have a sender and a receiver with the same identifier. However, they are completely independent.
The receivers can filter the values they receive by attribute. If we plan to collect only the diameter and we do not care about the degrees, we can (and even must) use
(gs:add-receiver "cumulative" "localhost" 3001 "diameter")
We will not call the setup-receiver
procedure from setup
as we did for setup-senders
. Instead we will associate it to a separate "setup receiver" button. Remember that the receiver must be running before a sender can connect to it. Since we have senders and receivers in both NetLogo and the external application, we cannot start up the communication in two steps and we need at least three:
- Click the "setup receiver" button to start our receiver.
- Start the external application. Its sender can connect to our receiver and its receivers will be started.
- Click the "setup" button. Our senders can connect to the external application's receivers.
The first two steps need to be be executed only once, then we can run multiple "setup-and-go" cycles.
Instead of creating our receiver in setup
we will just flush it there. This will delete all the non collected values received during the previous simulation.
to setup
...
gs:flush "cumulative"
reset-ticks
end
To store the diameter values we will use a global variable:
globals [
...
diameters
]
...
to setup-globals
...
set diameters []
end
The degrees will be stored in turtle variables:
turtles-own [
...
degree
]
To collect the values received we will use the following procedure:
to get-attributes
set diameters gs:get-attribute "cumulative" "diameter"
ask turtles [
let tmp gs:get-attribute "cumulative" "degree"
if not empty? tmp [
set degree last tmp
]
]
end
Note that gs:get-attribute
reports a list. Our simulation and the external application are asynchronous and there is no guarantee that we will collect a single value at each call. It is possible to get an empty list during nine calls and to retrieve a list of ten values on the tenth call. We keep the whole list in the global variable and only the last value received in the turtle variables. We will explain why we treat them differently in a while.
We will call this procedure from go
without forgetting to trigger the external application sends by sending it a 'step' event:
to go
...
gs:step "cumulative" ticks
get-attributes
tick
end
Like this at each call of go
we update the global variable diameters
and the turtle variables degree
with the values sent by the external application. Now let us use these values.
To display the evolution of the diameter of our graph, we create a plot. This plot has a single default pen with the following pen update command:
foreach diameters [plot ?]
Now it should be clear why our global variable keeps the whole list and not only the last value received. Our plot could be a couple of ticks behind our simulation, but at least we don't lose values.
We can do several interesting things with the degrees. We can display the average degree in a monitor. Its reporter is:
mean [degree] of turtles
We can also plot the degree distribution of the nodes. To do that, we create a plot with a single pen, set the pen's mode to "bar" and its update commands to:
set-plot-x-range 0 max [degree] of turtles + 1
set-plot-y-range 0 1
histogram [degree] of turtles
The final result looks like this:
We have already mentioned that the NetLogo simulation and the external application are asynchronous. Our diameter plot may be several ticks behind the simulation. Concerning the degrees, some of them may be recently updated and other may still have the values from previous steps. So the average degree and the degree distribution we compute do not correspond exactly to the actual state of our system. For our simulation this is not fatal because we use the received values just to display them. But some applications may use the values received from outside to update the state of the agents. In such cases the delay can be a real problem. Fortunately, there is a simple way to synchronize the simulation with the external application.
We already send a 'step' event to the external application in order to trigger the computing and sending the results. If, after doing that, the external application sends us back a 'step' event and we wait to receive it, we can be sure that we already have all the computed results.
We have to add a single instruction to our external application:
public void stepBegins(String sourceId, long timeId, double step) {
// compute and send the diameter and the degrees
...
// sync
sender.stepBegins(mySourceId, myTimeId++, step);
}
On the NetLogo side, there is also a single instruction to add:
to go
...
gs:step "cumulative" ticks
;; sync
while [gs:wait-step "cumulative" != ticks][]
get-attributes
tick
end
Unlike the gs:get-attribute
reporter, gs:wait-step
is blocking. If no 'step' event is received, it does not return immediately. Our simulation remains blocked on this call until the external application sends a 'step' event.
The advantage of the synchronization is that we are sure that we use fresh results. The drawback is that there is less parallelism. With our current setup, at each moment there is one of the applications working and the other waiting. Well, not really, because our external application does not wait for the 'step' event to compute the degrees of the nodes for example. Our graph updates the degrees of its nodes after each received 'edge added' event. In this way when the 'step' event is received, all we have to do is to send these degrees. This is not the case with the diameter, but some dynamic graph algorithms can be efficient even with synchronization. Such algorithms update their result at each event received and when this result is needed, they can return it immediately.