Skip to content

Comprehensive example

sbalev edited this page Apr 25, 2012 · 25 revisions

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.

three graphs

Sending events (NetLogo -> GraphStream)

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 some 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

Receiving events (GraphStream <- NetLogo)

In our introduction example we explained 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);
}

Sending events (GraphStream -> Netlogo)

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 we will be able 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 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 in real life by the way) 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.

Receiving events (NetLogo <- GraphStream)

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 attribute values they receive. 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, 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 have to start up the communication in three steps:

  • 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 receivers can connect to the external application's senders.

The first two steps need to be be executed only once, then we can run multiple "setup-and-go" simulations.

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

Synchronization

TO BE CONTINUED ...

Clone this wiki locally