bgneal@23
|
1 """This module contains the SmallWorldGraph class for 4.4, exercise 4.
|
bgneal@23
|
2
|
bgneal@23
|
3 """
|
bgneal@23
|
4 import itertools
|
bgneal@23
|
5 import random
|
bgneal@23
|
6
|
bgneal@23
|
7 from Graph import Edge
|
bgneal@23
|
8 from RandomGraph import RandomGraph
|
bgneal@23
|
9
|
bgneal@23
|
10
|
bgneal@24
|
11 INFINITY = float('Inf')
|
bgneal@24
|
12
|
bgneal@23
|
13 class SmallWorldGraph(RandomGraph):
|
bgneal@23
|
14 """A small world graph for 4.4, exercise 4 in Think Complexity."""
|
bgneal@23
|
15
|
bgneal@23
|
16 def __init__(self, vs, k, p):
|
bgneal@23
|
17 """Create a small world graph. Parameters:
|
bgneal@23
|
18 vs - a list of vertices
|
bgneal@23
|
19 k - regular graph parameter k: the number of edges between vertices
|
bgneal@23
|
20 p - probability for rewiring.
|
bgneal@23
|
21
|
bgneal@23
|
22 First a regular graph is created from the list of vertices and parameter
|
bgneal@23
|
23 k, which specifies how many edges each vertex should have. Then the
|
bgneal@23
|
24 rewire() method is called with parameter p to rewire the graph according
|
bgneal@23
|
25 to the algorithm by Watts and Strogatz.
|
bgneal@23
|
26
|
bgneal@23
|
27 """
|
bgneal@23
|
28 super(SmallWorldGraph, self).__init__(vs=vs)
|
bgneal@23
|
29 self.add_regular_edges(k)
|
bgneal@23
|
30 self.rewire(p)
|
bgneal@23
|
31
|
bgneal@24
|
32 # give each edge a default length of 1; this is used by the
|
bgneal@24
|
33 # shortest_path_tree() method
|
bgneal@24
|
34 for e in self.edges():
|
bgneal@24
|
35 e.length = 1
|
bgneal@24
|
36
|
bgneal@23
|
37 def rewire(self, p):
|
bgneal@23
|
38 """Assuming the graph is initially a regular graph, rewires the graph
|
bgneal@23
|
39 using the Watts and Strogatz algorithm.
|
bgneal@23
|
40
|
bgneal@23
|
41 """
|
bgneal@23
|
42 # We iterate over the edges in a random order:
|
bgneal@23
|
43 edges = self.edges()
|
bgneal@23
|
44 random.shuffle(edges)
|
bgneal@23
|
45 vertices = self.vertices()
|
bgneal@23
|
46
|
bgneal@23
|
47 for e in edges:
|
bgneal@23
|
48 if random.random() < p:
|
bgneal@23
|
49 v, w = e # remember the 2 vertices this edge connected
|
bgneal@23
|
50 self.remove_edge(e) # remove from graph
|
bgneal@23
|
51
|
bgneal@23
|
52 # pick another vertex to connect to v; duplicate edges are
|
bgneal@23
|
53 # forbidden
|
bgneal@23
|
54 while True:
|
bgneal@23
|
55 x = random.choice(vertices)
|
bgneal@23
|
56 if v is not x and not self.get_edge(v, x):
|
bgneal@23
|
57 self.add_edge(Edge(v, x))
|
bgneal@23
|
58 break
|
bgneal@23
|
59
|
bgneal@23
|
60 def clustering_coefficient(self):
|
bgneal@23
|
61 """Compute the clustering coefficient for this graph as defined by Watts
|
bgneal@23
|
62 and Strogatz.
|
bgneal@23
|
63
|
bgneal@23
|
64 """
|
bgneal@23
|
65 cv = {}
|
bgneal@23
|
66 for v in self:
|
bgneal@23
|
67 # consider a node and its neighbors
|
bgneal@23
|
68 nodes = self.out_vertices(v)
|
bgneal@23
|
69 nodes.append(v)
|
bgneal@23
|
70
|
bgneal@23
|
71 # compute the maximum number of possible edges between these nodes
|
bgneal@23
|
72 # if they were all connected to each other:
|
bgneal@23
|
73 n = len(nodes)
|
bgneal@23
|
74 if n == 1:
|
bgneal@23
|
75 # edge case of only 1 node; handle this case to avoid division
|
bgneal@23
|
76 # by zero in the general case
|
bgneal@23
|
77 cv[v] = 1.0
|
bgneal@23
|
78 continue
|
bgneal@23
|
79
|
bgneal@23
|
80 possible = n * (n - 1) / 2.0
|
bgneal@23
|
81
|
bgneal@23
|
82 # now compute how many edges actually exist between the nodes
|
bgneal@23
|
83 actual = 0
|
bgneal@23
|
84 for x, y in itertools.combinations(nodes, 2):
|
bgneal@23
|
85 if self.get_edge(x, y):
|
bgneal@23
|
86 actual += 1
|
bgneal@23
|
87
|
bgneal@23
|
88 # the fraction of actual / possible is this nodes C sub v value
|
bgneal@23
|
89 cv[v] = actual / possible
|
bgneal@23
|
90
|
bgneal@23
|
91 # The clustering coefficient is the average of all C sub v values
|
bgneal@23
|
92 if len(cv):
|
bgneal@23
|
93 return sum(cv.values()) / float(len(cv))
|
bgneal@23
|
94 return 0.0
|
bgneal@24
|
95
|
bgneal@24
|
96 def shortest_path_tree(self, source, hint=None):
|
bgneal@24
|
97 """Finds the length of the shortest path from the source vertex to all
|
bgneal@24
|
98 other vertices in the graph. This length is stored on the vertices as an
|
bgneal@24
|
99 attribute named 'dist'. The algorithm used is Dijkstra's.
|
bgneal@24
|
100
|
bgneal@24
|
101 hint: if provided, must be a dictionary mapping tuples to already known
|
bgneal@24
|
102 shortest path distances. This can be used to speed up the algorithm.
|
bgneal@24
|
103
|
bgneal@24
|
104 """
|
bgneal@24
|
105 if not hint:
|
bgneal@24
|
106 hint = {}
|
bgneal@24
|
107
|
bgneal@24
|
108 for v in self.vertices():
|
bgneal@24
|
109 v.dist = hint.get((source, v), INFINITY)
|
bgneal@24
|
110 source.dist = 0
|
bgneal@24
|
111
|
bgneal@24
|
112 queue = [v for v in self.vertices() if v.dist < INFINITY]
|
bgneal@24
|
113 sort_flag = True
|
bgneal@24
|
114 while len(queue):
|
bgneal@24
|
115
|
bgneal@24
|
116 if sort_flag:
|
bgneal@24
|
117 queue.sort(key=lambda v: v.dist)
|
bgneal@24
|
118 sort_flag = False
|
bgneal@24
|
119
|
bgneal@24
|
120 v = queue.pop(0)
|
bgneal@24
|
121
|
bgneal@24
|
122 # for each neighbor of v, see if we found a new shortest path
|
bgneal@24
|
123 for w, e in self[v].iteritems():
|
bgneal@24
|
124 d = v.dist + e.length
|
bgneal@24
|
125 if d < w.dist:
|
bgneal@24
|
126 w.dist = d
|
bgneal@24
|
127 queue.append(w)
|
bgneal@24
|
128 sort_flag = True
|
bgneal@24
|
129
|
bgneal@25
|
130 def all_pairs_floyd_warshall(self):
|
bgneal@25
|
131 """Finds the shortest paths between all pairs of vertices using the
|
bgneal@25
|
132 Floyd-Warshall algorithm.
|
bgneal@25
|
133
|
bgneal@25
|
134 http://en.wikipedia.org/wiki/Floyd-Warshall_algorithm
|
bgneal@25
|
135
|
bgneal@25
|
136 """
|
bgneal@25
|
137 vertices = self.vertices()
|
bgneal@25
|
138 dist = {}
|
bgneal@25
|
139 for i in vertices:
|
bgneal@25
|
140 for j in vertices:
|
bgneal@25
|
141 if i is j:
|
bgneal@25
|
142 dist[i, j] = 0.0
|
bgneal@25
|
143 else:
|
bgneal@25
|
144 e = self.get_edge(i, j)
|
bgneal@25
|
145 dist[i, j] = e.length if e else INFINITY
|
bgneal@25
|
146
|
bgneal@25
|
147 for k in vertices:
|
bgneal@25
|
148 for i in vertices:
|
bgneal@25
|
149 for j in vertices:
|
bgneal@25
|
150 d_ik = dist[i, k]
|
bgneal@25
|
151 d_kj = dist[k, j]
|
bgneal@25
|
152 new_cost = d_ik + d_kj
|
bgneal@25
|
153 if new_cost < dist[i, j]:
|
bgneal@25
|
154 dist[i, j] = new_cost
|
bgneal@25
|
155
|
bgneal@25
|
156 return dist
|
bgneal@25
|
157
|
bgneal@24
|
158 def big_l(self):
|
bgneal@24
|
159 """Computes the "big-L" value for the graph as per Watts & Strogatz.
|
bgneal@24
|
160
|
bgneal@24
|
161 big_l() is defined as the number of edges in the shortest path between
|
bgneal@24
|
162 two vertices, averaged over all vertices.
|
bgneal@24
|
163
|
bgneal@25
|
164 Uses the shortest_path_tree() method, called once for every node.
|
bgneal@25
|
165
|
bgneal@24
|
166 """
|
bgneal@24
|
167 d = {}
|
bgneal@24
|
168 for v in self.vertices():
|
bgneal@24
|
169 self.shortest_path_tree(v, d)
|
bgneal@24
|
170 t = [((w, v), w.dist) for w in self.vertices() if v is not w]
|
bgneal@24
|
171 d.update(t)
|
bgneal@24
|
172
|
bgneal@24
|
173 if len(d):
|
bgneal@24
|
174 return sum(d.values()) / float(len(d))
|
bgneal@24
|
175 return 0.0
|
bgneal@25
|
176
|
bgneal@25
|
177 def big_l2(self):
|
bgneal@25
|
178 """Computes the "big-L" value for the graph as per Watts & Strogatz.
|
bgneal@25
|
179
|
bgneal@25
|
180 big_l() is defined as the number of edges in the shortest path between
|
bgneal@25
|
181 two vertices, averaged over all vertices.
|
bgneal@25
|
182
|
bgneal@25
|
183 Uses the all_pairs_floyd_warshall() method.
|
bgneal@25
|
184
|
bgneal@25
|
185 """
|
bgneal@25
|
186 dist = self.all_pairs_floyd_warshall()
|
bgneal@25
|
187 vertices = self.vertices()
|
bgneal@25
|
188 result = [dist[i, j] for i in vertices for j in vertices if i is not j]
|
bgneal@25
|
189
|
bgneal@25
|
190 if len(result):
|
bgneal@25
|
191 return sum(result) / float(len(result))
|
bgneal@25
|
192 return 0.0
|