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(),
... 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 or , depending on
whether a neighbor is infected, and the infected state as .
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.