Animations with graph-tool

The drawing capabilities of graph-tool (see draw module) can be harnessed to perform animations in a straightforward manner. Here we show some examples which uses GTK+ to display animations in an interactive_window, as well as offscreen to a file. The idea is to easily generate visualisations which can be used in presentations, and embedded in websites.

Simple interactive animations

The graph_draw() function can optionally return and be passed a GTK+ window object where drawing can be updated. In this way, simple animations can be done very easily, specially when done in an interactive shell. For example, the following will simulate from an Ising model (see IsingGlauberState) and show the states in an interactive window:

>>> g = gt.collection.data["football"]
>>> state = gt.IsingGlauberState(g, beta=1.5/10)
>>> win = None
>>> for i in range(100):
...     ret = state.iterate_sync(niter=100)
...     win = gt.graph_draw(g, g.vp.pos, vertex_fill_color=state.get_state(),
...                         vcmap=matplotlib.cm.bone_r, window=win, return_window=True,
...                         main=False)

Although sufficient for many simple animations, the above method is not the most efficient, as it requires a certain amount of redundant processing for each drawing. The examples below show more efficient approaches that can be more suitable in some more elaborate scenarios.

SIRS epidemics

Here we implement a simple SIRS epidemics on a network, and we construct an animation showing the time evolution. Nodes which are susceptible (S) are shown in white, whereas infected (I) nodes are shown in black. Recovered (R) nodes are removed from the layout, since they cannot propagate the outbreak.

The script which performs the animation is called animation_sirs.py and is shown below.

  1#! /usr/bin/env python
  2# -*- coding: utf-8 -*-
  3
  4# This simple example on how to do animations using graph-tool. Here we do a
  5# simple simulation of an S->I->R->S epidemic model, where each vertex can be in
  6# one of the following states: Susceptible (S), infected (I), recovered (R). A
  7# vertex in the S state becomes infected either spontaneously with a probability
  8# 'x' or because a neighbor is infected. An infected node becomes recovered
  9# with probability 'r', and a recovered vertex becomes again susceptible with
 10# probability 's'.
 11
 12# DISCLAIMER: The following code is definitely not the most efficient approach
 13# if you want to simulate this dynamics for very large networks, and/or for very
 14# long times. The main purpose is simply to highlight the animation capabilities
 15# of graph-tool.
 16
 17from graph_tool.all import *
 18from numpy.random import *
 19import sys, os, os.path
 20
 21seed(42)
 22seed_rng(42)
 23
 24# We need some Gtk and gobject functions
 25from gi.repository import Gtk, Gdk, GdkPixbuf, GObject, GLib
 26
 27# We will use the network of network scientists, and filter out the largest
 28# component
 29g = collection.data["netscience"]
 30g = GraphView(g, vfilt=label_largest_component(g), directed=False)
 31g = Graph(g, prune=True)
 32
 33pos = g.vp["pos"]  # layout positions
 34
 35# We will filter out vertices which are in the "Recovered" state, by masking
 36# them using a property map.
 37removed = g.new_vertex_property("bool")
 38
 39# SIRS dynamics parameters:
 40
 41x = 0.001    # spontaneous outbreak probability
 42r = 0.1      # I->R probability
 43s = 0.01     # R->S probability
 44
 45# (Note that the S->I transition happens simultaneously for every vertex with a
 46#  probability equal to the fraction of non-recovered neighbors which are
 47#  infected.)
 48
 49# The states would usually be represented with simple integers, but here we will
 50# use directly the color of the vertices in (R,G,B,A) format.
 51
 52S = [1, 1, 1, 1]           # White color
 53I = [0, 0, 0, 1]           # Black color
 54R = [0.5, 0.5, 0.5, 1.]    # Grey color (will not actually be drawn)
 55
 56# Initialize all vertices to the S state
 57state = g.new_vertex_property("vector<double>")
 58for v in g.vertices():
 59    state[v] = S
 60
 61# Newly infected nodes will be highlighted in red
 62newly_infected = g.new_vertex_property("bool")
 63
 64# If True, the frames will be dumped to disk as images.
 65offscreen = sys.argv[1] == "offscreen" if len(sys.argv) > 1 else False
 66max_count = 500
 67if offscreen and not os.path.exists("./frames"):
 68    os.mkdir("./frames")
 69
 70# This creates a GTK+ window with the initial graph layout
 71if not offscreen:
 72    win = GraphWindow(g, pos, geometry=(500, 400),
 73                      edge_color=[0.6, 0.6, 0.6, 1],
 74                      vertex_fill_color=state,
 75                      vertex_halo=newly_infected,
 76                      vertex_halo_color=[0.8, 0, 0, 0.6])
 77else:
 78    count = 0
 79    win = Gtk.OffscreenWindow()
 80    win.set_default_size(500, 400)
 81    win.graph = GraphWidget(g, pos,
 82                            edge_color=[0.6, 0.6, 0.6, 1],
 83                            vertex_fill_color=state,
 84                            vertex_halo=newly_infected,
 85                            vertex_halo_color=[0.8, 0, 0, 0.6])
 86    win.add(win.graph)
 87
 88
 89# This function will be called repeatedly by the GTK+ main loop, and we use it
 90# to update the state according to the SIRS dynamics.
 91def update_state():
 92    newly_infected.a = False
 93    removed.a = False
 94
 95    # visit the nodes in random order
 96    vs = list(g.vertices())
 97    shuffle(vs)
 98    for v in vs:
 99        if state[v] == I:
100            if random() < r:
101                state[v] = R
102        elif state[v] == S:
103            if random() < x:
104                state[v] = I
105            else:
106                ns = list(v.out_neighbors())
107                if len(ns) > 0:
108                    w = ns[randint(0, len(ns))]  # choose a random neighbor
109                    if state[w] == I:
110                        state[v] = I
111                        newly_infected[v] = True
112        elif random() < s:
113            state[v] = S
114        if state[v] == R:
115            removed[v] = True
116
117    # Filter out the recovered vertices
118    g.set_vertex_filter(removed, inverted=True)
119
120    # The following will force the re-drawing of the graph, and issue a
121    # re-drawing of the GTK window.
122    win.graph.regenerate_surface()
123    win.graph.queue_draw()
124
125    # if doing an offscreen animation, dump frame to disk
126    if offscreen:
127        global count
128        pixbuf = win.get_pixbuf()
129        pixbuf.savev(r'./frames/sirs%06d.png' % count, 'png', [], [])
130        if count > max_count:
131            sys.exit(0)
132        count += 1
133
134    # We need to return True so that the main loop will call this function more
135    # than once.
136    return True
137
138
139# Bind the function above as an 'idle' callback.
140cid = GLib.idle_add(update_state)
141
142# We will give the user the ability to stop the program by closing the window.
143win.connect("delete_event", Gtk.main_quit)
144
145# Actually show the window, and start the main loop.
146win.show_all()
147Gtk.main()

If called without arguments, the script will show the animation inside an interactive_window. If the parameter offscreen is passed, individual frames will be saved in the frames directory:

$ ./animation_sirs.py offscreen

These frames can be combined and encoded into the appropriate format. Here we use the mencoder tool from mplayer to combine all the frames into a single file with YUY format, and then we encode this with the WebM format, using vpxenc, so that it can be embedded in a website.

$ mencoder mf://frames/sirs*.png -mf w=500:h=400:type=png -ovc raw -of rawvideo -vf format=i420 -nosound -o sirs.yuy
$ vpxenc sirs.yuy -o sirs.webm -w 500 -h 400 --fps=25/1 --target-bitrate=1000 --good --threads=4

The resulting animation can be downloaded here, or played below if your browser supports WebM.

This type of animation can be extended or customized in many ways, by dynamically modifying the various drawing parameters and vertex/edge properties. For instance, one might want to represent the susceptible state as either susceptible or susceptible-fear, depending on whether a neighbor is infected, and the infected state as zombie. Properly modifying the script above would lead to the following movie:

The modified script can be downloaded here.

Dynamic layout

The graph layout can also be updated during an animation. As an illustration, here we consider a very simplistic model for spatial segregation, where the edges of the graph are repeatedly and randomly rewired, as long as the new edge has a shorter euclidean distance.

The script which performs the animation is called animation_dancing.py and is shown below.

  1#! /usr/bin/env python
  2# -*- coding: utf-8 -*-
  3
  4# This simple example on how to do animations using graph-tool, where the layout
  5# changes dynamically. We start with some network, and randomly rewire its
  6# edges, and update the layout dynamically, where edges are rewired only if
  7# their euclidean distance is reduced. It is thus a very simplistic model for
  8# spatial segregation.
  9
 10from graph_tool.all import *
 11from numpy.random import *
 12from numpy.linalg import norm
 13import sys, os, os.path
 14
 15seed(42)
 16seed_rng(42)
 17
 18# We need some Gtk and gobject functions
 19from gi.repository import Gtk, Gdk, GdkPixbuf, GObject, GLib
 20
 21# We will generate a small random network
 22g = random_graph(150, lambda: 1 + poisson(5), directed=False)
 23
 24# Parameters for the layout update
 25
 26step = 0.005       # move step
 27K = 0.5            # preferred edge length
 28
 29pos = sfdp_layout(g, K=K)  # initial layout positions
 30
 31# If True, the frames will be dumped to disk as images.
 32offscreen = sys.argv[1] == "offscreen" if len(sys.argv) > 1 else False
 33max_count = 5000
 34if offscreen and not os.path.exists("./frames"):
 35    os.mkdir("./frames")
 36
 37# This creates a GTK+ window with the initial graph layout
 38if not offscreen:
 39    win = GraphWindow(g, pos, geometry=(500, 400))
 40else:
 41    win = Gtk.OffscreenWindow()
 42    win.set_default_size(500, 400)
 43    win.graph = GraphWidget(g, pos)
 44    win.add(win.graph)
 45
 46# list of edges
 47edges = list(g.edges())
 48
 49count = 0
 50
 51# This function will be called repeatedly by the GTK+ main loop, and we use it
 52# to update the vertex layout and perform the rewiring.
 53def update_state():
 54    global count
 55
 56    # Perform one iteration of the layout step, starting from the previous positions
 57    sfdp_layout(g, pos=pos, K=K, init_step=step, max_iter=1)
 58
 59    for i in range(100):
 60        # get a chosen edge, and swap one of its end points for a random vertex,
 61        # if it is closer
 62        i = randint(0, len(edges))
 63        e = list(edges[i])
 64        shuffle(e)
 65        s1, t1 = e
 66
 67        t2 = g.vertex(randint(0, g.num_vertices()))
 68
 69        if (norm(pos[s1].a - pos[t2].a) <= norm(pos[s1].a - pos[t1].a) and
 70            s1 != t2 and                      # no self-loops
 71            t1.out_degree() > 1 and           # no isolated vertices
 72            t2 not in s1.out_neighbors()):    # no parallel edges
 73
 74            g.remove_edge(edges[i])
 75            edges[i] = g.add_edge(s1, t2)
 76
 77
 78    # The movement of the vertices may cause them to leave the display area. The
 79    # following function rescales the layout to fit the window to avoid this.
 80    if count > 0 and count % 1000 == 0:
 81        win.graph.fit_to_window(ink=True)
 82
 83    count += 1
 84
 85    # The following will force the re-drawing of the graph, and issue a
 86    # re-drawing of the GTK window.
 87    win.graph.regenerate_surface()
 88    win.graph.queue_draw()
 89
 90    # if doing an offscreen animation, dump frame to disk
 91    if offscreen:
 92        pixbuf = win.get_pixbuf()
 93        pixbuf.savev(r'./frames/dancing%06d.png' % count, 'png', [], [])
 94        if count > max_count:
 95            sys.exit(0)
 96
 97    # We need to return True so that the main loop will call this function more
 98    # than once.
 99    return True
100
101
102# Bind the function above as an 'idle' callback.
103cid = GLib.idle_add(update_state)
104
105# We will give the user the ability to stop the program by closing the window.
106win.connect("delete_event", Gtk.main_quit)
107
108# Actually show the window, and start the main loop.
109win.show_all()
110Gtk.main()

This example works like the SIRS example above, and if we pass the offscreen parameter, the frames will be dumped to disk, otherwise the animation is displayed inside an interactive_window.

$ ./animation_dancing.py offscreen

Also like the previous example, we can encode the animation with the WebM format:

$ mencoder mf://frames/dancing*.png -mf w=500:h=400:type=png -ovc raw -of rawvideo -vf format=i420 -nosound -o dancing.yuy
$ vpxenc dancing.yuy -o dancing.webm -w 500 -h 400 --fps=100/1 --target-bitrate=5000 --good --threads=16

The resulting animation can be downloaded here, or played below if your browser supports WebM.

Interactive visualizations

Here we show an example of interactive visualization where the BFS tree of the currently selected vertex is highlighted with a different color.

The script which performs the visualization is called interactive_bst.py and is shown below. When called, it will open an interactive window.

 1#! /usr/bin/env python
 2# -*- coding: utf-8 -*-
 3
 4# This simple example on how to interactive animations using graph-tool. Here we
 5# show the BFS tree of length 3 for the currently selected vertex.
 6
 7from graph_tool.all import *
 8
 9# We need some Gtk functions
10from gi.repository import Gtk, Gdk
11import sys
12
13offscreen = sys.argv[1] == "offscreen" if len(sys.argv) > 1 else False
14
15# We will use the network of network scientists, and filter out the largest
16# component
17g = collection.data["netscience"]
18g = GraphView(g, vfilt=label_largest_component(g), directed=False)
19g = Graph(g, prune=True)
20
21pos = g.vp["pos"]  # layout positions
22
23ecolor = g.new_edge_property("vector<double>")
24for e in g.edges():
25    ecolor[e] = [0.6, 0.6, 0.6, 1]
26vcolor = g.new_vertex_property("vector<double>")
27for v in g.vertices():
28    vcolor[v] = [0.6, 0.6, 0.6, 1]
29
30win = GraphWindow(g, pos, geometry=(500, 400),
31                  edge_color=ecolor,
32                  vertex_fill_color=vcolor)
33
34orange = [0.807843137254902, 0.3607843137254902, 0.0, 1.0]
35old_src = None
36count = 0
37def update_bfs(widget, event):
38    global old_src, g, count, win
39    src = widget.picked
40    if src is None:
41        return True
42    if isinstance(src, PropertyMap):
43        src = [v for v in g.vertices() if src[v]]
44        if len(src) == 0:
45            return True
46        src = src[0]
47    if src == old_src:
48        return True
49    old_src = src
50    pred = shortest_distance(g, src, max_dist=3, pred_map=True)[1]
51    for e in g.edges():
52        ecolor[e] = [0.6, 0.6, 0.6, 1]
53    for v in g.vertices():
54        vcolor[v] = [0.6, 0.6, 0.6, 1]
55    for v in g.vertices():
56        w = g.vertex(pred[v])
57        if w < g.num_vertices():
58            e = g.edge(w, v)
59            if e is not None:
60                ecolor[e] = orange
61                vcolor[v] = vcolor[w] = orange
62    widget.regenerate_surface()
63    widget.queue_draw()
64
65    if offscreen:
66        window = widget.get_window()
67        pixbuf = Gdk.pixbuf_get_from_window(window, 0, 0, 500, 400)
68        pixbuf.savev(r'./frames/bfs%06d.png' % count, 'png', [], [])
69        count += 1
70
71# Bind the function above as a montion notify handler
72win.graph.connect("motion_notify_event", update_bfs)
73
74# We will give the user the ability to stop the program by closing the window.
75win.connect("delete_event", Gtk.main_quit)
76
77# Actually show the window, and start the main loop.
78win.show_all()
79Gtk.main()
$ ./interactive_bst.py offscreen

The above script is interactive, i.e. it expects a reaction from the user. But for the purpose of this demo, it also saves the frames to a file, so we can encode the animation with the WebM format:

$ mencoder mf://frames/bfs*.png -mf w=type=png -ovc raw -of rawvideo -vf format=i420,scale=500:400 -nosound -o bfs.yuy
$ vpxenc bfs.yuy -o bfs.webm -w 500 -h 400 --fps=5/1 --target-bitrate=5000 --good --threads=16

The resulting animation can be downloaded here, or played below if your browser supports WebM.