bgneal@1
|
1 """ Code example from Complexity and Computation, a book about
|
bgneal@1
|
2 exploring complexity science with Python. Available free from
|
bgneal@1
|
3
|
bgneal@1
|
4 http://greenteapress.com/complexity
|
bgneal@1
|
5
|
bgneal@1
|
6 Copyright 2011 Allen B. Downey.
|
bgneal@1
|
7 Distributed under the GNU General Public License at gnu.org/licenses/gpl.html.
|
bgneal@1
|
8 """
|
bgneal@1
|
9 import itertools
|
bgneal@35
|
10 from collections import deque, Counter
|
bgneal@1
|
11
|
bgneal@1
|
12
|
bgneal@5
|
13 class GraphError(Exception):
|
bgneal@5
|
14 """Exception for Graph errors"""
|
bgneal@5
|
15 pass
|
bgneal@5
|
16
|
bgneal@5
|
17
|
bgneal@1
|
18 class Vertex(object):
|
bgneal@1
|
19 """A Vertex is a node in a graph."""
|
bgneal@1
|
20
|
bgneal@1
|
21 def __init__(self, label=''):
|
bgneal@1
|
22 self.label = label
|
bgneal@1
|
23
|
bgneal@1
|
24 def __repr__(self):
|
bgneal@1
|
25 """Returns a string representation of this object that can
|
bgneal@1
|
26 be evaluated as a Python expression."""
|
bgneal@1
|
27 return 'Vertex(%s)' % repr(self.label)
|
bgneal@1
|
28
|
bgneal@1
|
29 __str__ = __repr__
|
bgneal@1
|
30 """The str and repr forms of this object are the same."""
|
bgneal@1
|
31
|
bgneal@1
|
32
|
bgneal@1
|
33 class Edge(tuple):
|
bgneal@1
|
34 """An Edge is a list of two vertices."""
|
bgneal@1
|
35
|
bgneal@1
|
36 def __new__(cls, *vs):
|
bgneal@1
|
37 """The Edge constructor takes two vertices."""
|
bgneal@1
|
38 if len(vs) != 2:
|
bgneal@1
|
39 raise ValueError, 'Edges must connect exactly two vertices.'
|
bgneal@1
|
40 return tuple.__new__(cls, vs)
|
bgneal@1
|
41
|
bgneal@1
|
42 def __repr__(self):
|
bgneal@1
|
43 """Return a string representation of this object that can
|
bgneal@1
|
44 be evaluated as a Python expression."""
|
bgneal@1
|
45 return 'Edge(%s, %s)' % (repr(self[0]), repr(self[1]))
|
bgneal@1
|
46
|
bgneal@1
|
47 __str__ = __repr__
|
bgneal@1
|
48 """The str and repr forms of this object are the same."""
|
bgneal@1
|
49
|
bgneal@1
|
50
|
bgneal@1
|
51 class Graph(dict):
|
bgneal@1
|
52 """A Graph is a dictionary of dictionaries. The outer
|
bgneal@1
|
53 dictionary maps from a vertex to an inner dictionary.
|
bgneal@1
|
54 The inner dictionary maps from other vertices to edges.
|
bgneal@1
|
55
|
bgneal@1
|
56 For vertices a and b, graph[a][b] maps
|
bgneal@1
|
57 to the edge that connects a->b, if it exists."""
|
bgneal@1
|
58
|
bgneal@6
|
59 def __init__(self, vs=None, es=None):
|
bgneal@1
|
60 """Creates a new graph.
|
bgneal@1
|
61 vs: list of vertices;
|
bgneal@1
|
62 es: list of edges.
|
bgneal@1
|
63 """
|
bgneal@6
|
64 if vs:
|
bgneal@6
|
65 for v in vs:
|
bgneal@6
|
66 self.add_vertex(v)
|
bgneal@1
|
67
|
bgneal@6
|
68 if es:
|
bgneal@6
|
69 for e in es:
|
bgneal@6
|
70 self.add_edge(e)
|
bgneal@1
|
71
|
bgneal@1
|
72 def add_vertex(self, v):
|
bgneal@1
|
73 """Add a vertex to the graph."""
|
bgneal@1
|
74 self[v] = {}
|
bgneal@1
|
75
|
bgneal@1
|
76 def add_edge(self, e):
|
bgneal@1
|
77 """Adds and edge to the graph by adding an entry in both directions.
|
bgneal@1
|
78
|
bgneal@1
|
79 If there is already an edge connecting these Vertices, the
|
bgneal@1
|
80 new edge replaces it.
|
bgneal@1
|
81 """
|
bgneal@1
|
82 v, w = e
|
bgneal@1
|
83 self[v][w] = e
|
bgneal@1
|
84 self[w][v] = e
|
bgneal@1
|
85
|
bgneal@1
|
86 def get_edge(self, v, w):
|
bgneal@1
|
87 """Returns the edge object that exists between the two vertices v & w,
|
bgneal@1
|
88 or None if no such edge exists.
|
bgneal@1
|
89
|
bgneal@1
|
90 """
|
bgneal@1
|
91 try:
|
bgneal@1
|
92 return self[v][w]
|
bgneal@1
|
93 except KeyError:
|
bgneal@1
|
94 return None
|
bgneal@1
|
95
|
bgneal@1
|
96 def remove_edge(self, e):
|
bgneal@1
|
97 """Removes the edge e from the graph."""
|
bgneal@1
|
98
|
bgneal@4
|
99 v, w = e
|
bgneal@4
|
100 del self[v][w]
|
bgneal@4
|
101 del self[w][v]
|
bgneal@1
|
102
|
bgneal@1
|
103 def vertices(self):
|
bgneal@1
|
104 """Returns a list of the vertices in the graph."""
|
bgneal@1
|
105
|
bgneal@1
|
106 return self.keys()
|
bgneal@1
|
107
|
bgneal@1
|
108 def edges(self):
|
bgneal@1
|
109 """"Returns a list of the edges in the graph."""
|
bgneal@1
|
110
|
bgneal@1
|
111 edge_set = set()
|
bgneal@4
|
112 for d in self.itervalues():
|
bgneal@4
|
113 edge_set.update(d.itervalues())
|
bgneal@1
|
114
|
bgneal@1
|
115 return list(edge_set)
|
bgneal@1
|
116
|
bgneal@1
|
117 def out_vertices(self, v):
|
bgneal@1
|
118 """Returns a list of vertices that are adjacent to the given vertex v.
|
bgneal@1
|
119
|
bgneal@1
|
120 """
|
bgneal@1
|
121 return self[v].keys()
|
bgneal@1
|
122
|
bgneal@1
|
123 def out_edges(self, v):
|
bgneal@1
|
124 """Returns a list of edges connected to a given vertex v."""
|
bgneal@1
|
125
|
bgneal@1
|
126 return self[v].values()
|
bgneal@1
|
127
|
bgneal@5
|
128 def remove_all_edges(self):
|
bgneal@5
|
129 """Removes all edges in the graph."""
|
bgneal@5
|
130 for v in self.iterkeys():
|
bgneal@5
|
131 self[v] = {}
|
bgneal@5
|
132
|
bgneal@1
|
133 def add_all_edges(self):
|
bgneal@1
|
134 """Makes the graph complete by adding edges between all pairs of
|
bgneal@1
|
135 vertices.
|
bgneal@1
|
136
|
bgneal@1
|
137 """
|
bgneal@1
|
138 # Clear all edges first
|
bgneal@5
|
139 self.remove_all_edges()
|
bgneal@1
|
140
|
bgneal@5
|
141 # For each combination of 2 vertices, create an edge between them:
|
bgneal@1
|
142 for v, w in itertools.combinations(self.iterkeys(), 2):
|
bgneal@6
|
143 self.add_edge(Edge(v, w))
|
bgneal@1
|
144
|
bgneal@5
|
145 def add_regular_edges(self, k):
|
bgneal@5
|
146 """Makes the graph regular by making every vertex have k edges.
|
bgneal@5
|
147
|
bgneal@5
|
148 It is not always possible to create a regular graph with a given degree.
|
bgneal@5
|
149 If a graph has n vertices, then a regular graph can be constructed with
|
bgneal@5
|
150 degree k if n >= k + 1 and n * k is even. If these conditions are not
|
bgneal@5
|
151 met a GraphError exception is raised.
|
bgneal@5
|
152
|
bgneal@5
|
153 """
|
bgneal@5
|
154 n = len(self.vertices())
|
bgneal@5
|
155 if n < k + 1:
|
bgneal@5
|
156 raise GraphError("Can't make a regular graph with degree >= number"
|
bgneal@5
|
157 " of vertices")
|
bgneal@5
|
158 if (n * k) % 2 != 0:
|
bgneal@5
|
159 raise GraphError("Can't make a regular graph of degree k and"
|
bgneal@5
|
160 " order n where k * n is odd")
|
bgneal@5
|
161
|
bgneal@5
|
162 # Remove all edges first
|
bgneal@5
|
163 self.remove_all_edges()
|
bgneal@5
|
164
|
bgneal@5
|
165 if k % 2 != 0: # if k is odd
|
bgneal@5
|
166 self._add_regular_edges_even(k - 1)
|
bgneal@5
|
167 self._add_regular_edges_odd()
|
bgneal@5
|
168 else:
|
bgneal@5
|
169 self._add_regular_edges_even(k)
|
bgneal@5
|
170
|
bgneal@5
|
171 def _add_regular_edges_even(self, k):
|
bgneal@5
|
172 """Make a regular graph with degree k. k must be even."""
|
bgneal@5
|
173
|
bgneal@5
|
174 vs = self.vertices()
|
bgneal@5
|
175 vs2 = vs * 2
|
bgneal@5
|
176
|
bgneal@5
|
177 for i, v in enumerate(vs):
|
bgneal@5
|
178 for j in range(1, k / 2 + 1):
|
bgneal@5
|
179 w = vs2[i + j]
|
bgneal@5
|
180 self.add_edge(Edge(v, w))
|
bgneal@5
|
181
|
bgneal@5
|
182 def _add_regular_edges_odd(self):
|
bgneal@5
|
183 """Adds an extra edge across the graph to finish off a regular graph
|
bgneal@5
|
184 with odd degree. The number of vertices must be even.
|
bgneal@5
|
185
|
bgneal@5
|
186 """
|
bgneal@5
|
187 vs = self.vertices()
|
bgneal@5
|
188 vs2 = vs * 2
|
bgneal@5
|
189 n = len(vs)
|
bgneal@5
|
190
|
bgneal@5
|
191 for i in range(n / 2):
|
bgneal@5
|
192 v = vs2[i]
|
bgneal@5
|
193 w = vs2[i + n / 2]
|
bgneal@5
|
194 self.add_edge(Edge(v, w))
|
bgneal@5
|
195
|
bgneal@7
|
196 def bfs(self, start, visit_func=None):
|
bgneal@7
|
197 """Perform a breadth first search starting at node start.
|
bgneal@7
|
198
|
bgneal@7
|
199 The function visit_func, if supplied, is invoked on each node.
|
bgneal@7
|
200
|
bgneal@8
|
201 The set of visited nodes is returned.
|
bgneal@8
|
202
|
bgneal@7
|
203 """
|
bgneal@8
|
204 visited = set()
|
bgneal@7
|
205
|
bgneal@7
|
206 # Create a work queue consisting initially of the starting node
|
bgneal@8
|
207 queue = deque([start])
|
bgneal@7
|
208
|
bgneal@7
|
209 while queue:
|
bgneal@7
|
210 # retrieve first item from the queue
|
bgneal@8
|
211 v = queue.popleft()
|
bgneal@7
|
212
|
bgneal@8
|
213 if v in visited:
|
bgneal@7
|
214 continue # Skip this one if we've seen it before
|
bgneal@7
|
215
|
bgneal@7
|
216 # Mark it as visited and invoke user's function on it
|
bgneal@8
|
217 visited.add(v)
|
bgneal@7
|
218 if visit_func:
|
bgneal@7
|
219 visit_func(v)
|
bgneal@7
|
220
|
bgneal@7
|
221 # Add the adjacent neigbors to the node to the queue
|
bgneal@22
|
222 queue.extend(c for c in self.out_vertices(v) if c not in visited)
|
bgneal@7
|
223
|
bgneal@8
|
224 return visited
|
bgneal@8
|
225
|
bgneal@7
|
226 def is_connected(self):
|
bgneal@7
|
227 """Returns True if the graph is connected (there is a path from every
|
bgneal@7
|
228 node to every other node) and False otherwise.
|
bgneal@7
|
229
|
bgneal@7
|
230 """
|
bgneal@7
|
231 vs = self.vertices()
|
bgneal@7
|
232 if len(vs):
|
bgneal@8
|
233 visited = self.bfs(vs[0])
|
bgneal@7
|
234 # See if all nodes have been visited
|
bgneal@8
|
235 return len(vs) == len(visited)
|
bgneal@7
|
236
|
bgneal@7
|
237 return False # Graph is empty
|
bgneal@7
|
238
|
bgneal@35
|
239 def get_p(self):
|
bgneal@35
|
240 """This method returns a dictionary of probabilities where each key is
|
bgneal@35
|
241 the connectivity k and the value is the probability [0-1] for this
|
bgneal@35
|
242 graph.
|
bgneal@35
|
243
|
bgneal@35
|
244 """
|
bgneal@35
|
245 # First, for each vertex, count up how many neighbors it has
|
bgneal@35
|
246 vs = self.vertices()
|
bgneal@35
|
247
|
bgneal@35
|
248 c = Counter()
|
bgneal@35
|
249 for v in vs:
|
bgneal@35
|
250 n = len(self.out_vertices(v))
|
bgneal@35
|
251 c[n] += 1
|
bgneal@35
|
252
|
bgneal@35
|
253 n = len(vs)
|
bgneal@35
|
254 if n > 0:
|
bgneal@35
|
255 for k in c:
|
bgneal@35
|
256 c[k] = float(c[k]) / n
|
bgneal@35
|
257
|
bgneal@35
|
258 return c
|
bgneal@35
|
259
|
bgneal@1
|
260
|
bgneal@1
|
261 def main(script, *args):
|
bgneal@1
|
262 import pprint
|
bgneal@1
|
263
|
bgneal@1
|
264 v = Vertex('v')
|
bgneal@1
|
265 print v
|
bgneal@1
|
266 w = Vertex('w')
|
bgneal@1
|
267 print w
|
bgneal@1
|
268 e = Edge(v, w)
|
bgneal@1
|
269 print e
|
bgneal@1
|
270 g = Graph([v,w], [e])
|
bgneal@1
|
271 pprint.pprint(g)
|
bgneal@1
|
272
|
bgneal@1
|
273 print "g.get_edge(v, w): ", g.get_edge(v, w)
|
bgneal@1
|
274 x = Vertex('x')
|
bgneal@1
|
275 print "g.get_edge(v, x): ", g.get_edge(v, x)
|
bgneal@1
|
276
|
bgneal@1
|
277 g.remove_edge(e)
|
bgneal@1
|
278 pprint.pprint(g)
|
bgneal@1
|
279
|
bgneal@1
|
280 print "vertices: ", g.vertices()
|
bgneal@1
|
281 print "edges: ", g.edges()
|
bgneal@1
|
282
|
bgneal@1
|
283 g.add_edge(e)
|
bgneal@1
|
284 u = Vertex('u')
|
bgneal@1
|
285 e1 = Edge(u, v)
|
bgneal@1
|
286 e2 = Edge(u, w)
|
bgneal@1
|
287 g.add_vertex(u)
|
bgneal@1
|
288 g.add_edge(e1)
|
bgneal@1
|
289 g.add_edge(e2)
|
bgneal@1
|
290 print "Adding vertex u and edges:"
|
bgneal@1
|
291 pprint.pprint(g)
|
bgneal@1
|
292 print "vertices: ", g.vertices()
|
bgneal@1
|
293 print "edges: ", g.edges()
|
bgneal@1
|
294
|
bgneal@1
|
295 print "Out vertices for v: ", g.out_vertices(v)
|
bgneal@1
|
296 print "Out edges for v: ", g.out_edges(v)
|
bgneal@1
|
297
|
bgneal@1
|
298 x = Vertex('x')
|
bgneal@1
|
299 g.add_vertex(x)
|
bgneal@1
|
300 g.add_all_edges()
|
bgneal@1
|
301 pprint.pprint(g)
|
bgneal@1
|
302
|
bgneal@7
|
303 print "g is connected?", g.is_connected()
|
bgneal@7
|
304 edges = g.out_edges(v)
|
bgneal@7
|
305 for e in edges:
|
bgneal@7
|
306 g.remove_edge(e)
|
bgneal@7
|
307 pprint.pprint(g)
|
bgneal@7
|
308 print "g is connected?", g.is_connected()
|
bgneal@7
|
309
|
bgneal@7
|
310 # Isolate v and check is_connected() again
|
bgneal@7
|
311
|
bgneal@1
|
312
|
bgneal@1
|
313 if __name__ == '__main__':
|
bgneal@1
|
314 import sys
|
bgneal@1
|
315 main(*sys.argv)
|