bgneal@1: """ Code example from Complexity and Computation, a book about bgneal@1: exploring complexity science with Python. Available free from bgneal@1: bgneal@1: http://greenteapress.com/complexity bgneal@1: bgneal@1: Copyright 2011 Allen B. Downey. bgneal@1: Distributed under the GNU General Public License at gnu.org/licenses/gpl.html. bgneal@1: """ bgneal@36: import heapq bgneal@1: import itertools bgneal@35: from collections import deque, Counter bgneal@1: bgneal@1: bgneal@36: INFINITY = float('Inf') bgneal@36: bgneal@36: bgneal@5: class GraphError(Exception): bgneal@5: """Exception for Graph errors""" bgneal@5: pass bgneal@5: bgneal@5: bgneal@1: class Vertex(object): bgneal@1: """A Vertex is a node in a graph.""" bgneal@1: bgneal@1: def __init__(self, label=''): bgneal@1: self.label = label bgneal@1: bgneal@1: def __repr__(self): bgneal@1: """Returns a string representation of this object that can bgneal@1: be evaluated as a Python expression.""" bgneal@1: return 'Vertex(%s)' % repr(self.label) bgneal@1: bgneal@1: __str__ = __repr__ bgneal@1: """The str and repr forms of this object are the same.""" bgneal@1: bgneal@1: bgneal@1: class Edge(tuple): bgneal@1: """An Edge is a list of two vertices.""" bgneal@1: bgneal@1: def __new__(cls, *vs): bgneal@1: """The Edge constructor takes two vertices.""" bgneal@1: if len(vs) != 2: bgneal@1: raise ValueError, 'Edges must connect exactly two vertices.' bgneal@1: return tuple.__new__(cls, vs) bgneal@1: bgneal@1: def __repr__(self): bgneal@1: """Return a string representation of this object that can bgneal@1: be evaluated as a Python expression.""" bgneal@1: return 'Edge(%s, %s)' % (repr(self[0]), repr(self[1])) bgneal@1: bgneal@1: __str__ = __repr__ bgneal@1: """The str and repr forms of this object are the same.""" bgneal@1: bgneal@1: bgneal@1: class Graph(dict): bgneal@1: """A Graph is a dictionary of dictionaries. The outer bgneal@1: dictionary maps from a vertex to an inner dictionary. bgneal@1: The inner dictionary maps from other vertices to edges. bgneal@1: bgneal@1: For vertices a and b, graph[a][b] maps bgneal@1: to the edge that connects a->b, if it exists.""" bgneal@1: bgneal@6: def __init__(self, vs=None, es=None): bgneal@1: """Creates a new graph. bgneal@1: vs: list of vertices; bgneal@1: es: list of edges. bgneal@1: """ bgneal@6: if vs: bgneal@6: for v in vs: bgneal@6: self.add_vertex(v) bgneal@1: bgneal@6: if es: bgneal@6: for e in es: bgneal@6: self.add_edge(e) bgneal@1: bgneal@36: def set_edge_length(self, n=1): bgneal@36: """Give each edge a length of n; this is used by the bgneal@36: shortest_path_tree() method. bgneal@36: bgneal@36: """ bgneal@36: for e in self.edges(): bgneal@36: e.length = n bgneal@36: bgneal@1: def add_vertex(self, v): bgneal@1: """Add a vertex to the graph.""" bgneal@1: self[v] = {} bgneal@1: bgneal@1: def add_edge(self, e): bgneal@1: """Adds and edge to the graph by adding an entry in both directions. bgneal@1: bgneal@1: If there is already an edge connecting these Vertices, the bgneal@1: new edge replaces it. bgneal@1: """ bgneal@1: v, w = e bgneal@1: self[v][w] = e bgneal@1: self[w][v] = e bgneal@1: bgneal@1: def get_edge(self, v, w): bgneal@1: """Returns the edge object that exists between the two vertices v & w, bgneal@1: or None if no such edge exists. bgneal@1: bgneal@1: """ bgneal@1: try: bgneal@1: return self[v][w] bgneal@1: except KeyError: bgneal@1: return None bgneal@1: bgneal@1: def remove_edge(self, e): bgneal@1: """Removes the edge e from the graph.""" bgneal@1: bgneal@4: v, w = e bgneal@4: del self[v][w] bgneal@4: del self[w][v] bgneal@1: bgneal@1: def vertices(self): bgneal@1: """Returns a list of the vertices in the graph.""" bgneal@1: bgneal@1: return self.keys() bgneal@1: bgneal@1: def edges(self): bgneal@1: """"Returns a list of the edges in the graph.""" bgneal@1: bgneal@1: edge_set = set() bgneal@4: for d in self.itervalues(): bgneal@4: edge_set.update(d.itervalues()) bgneal@1: bgneal@1: return list(edge_set) bgneal@1: bgneal@1: def out_vertices(self, v): bgneal@1: """Returns a list of vertices that are adjacent to the given vertex v. bgneal@1: bgneal@1: """ bgneal@1: return self[v].keys() bgneal@1: bgneal@1: def out_edges(self, v): bgneal@1: """Returns a list of edges connected to a given vertex v.""" bgneal@1: bgneal@1: return self[v].values() bgneal@1: bgneal@5: def remove_all_edges(self): bgneal@5: """Removes all edges in the graph.""" bgneal@5: for v in self.iterkeys(): bgneal@5: self[v] = {} bgneal@5: bgneal@1: def add_all_edges(self): bgneal@1: """Makes the graph complete by adding edges between all pairs of bgneal@1: vertices. bgneal@1: bgneal@1: """ bgneal@1: # Clear all edges first bgneal@5: self.remove_all_edges() bgneal@1: bgneal@5: # For each combination of 2 vertices, create an edge between them: bgneal@1: for v, w in itertools.combinations(self.iterkeys(), 2): bgneal@6: self.add_edge(Edge(v, w)) bgneal@1: bgneal@5: def add_regular_edges(self, k): bgneal@5: """Makes the graph regular by making every vertex have k edges. bgneal@5: bgneal@5: It is not always possible to create a regular graph with a given degree. bgneal@5: If a graph has n vertices, then a regular graph can be constructed with bgneal@5: degree k if n >= k + 1 and n * k is even. If these conditions are not bgneal@5: met a GraphError exception is raised. bgneal@5: bgneal@5: """ bgneal@5: n = len(self.vertices()) bgneal@5: if n < k + 1: bgneal@5: raise GraphError("Can't make a regular graph with degree >= number" bgneal@5: " of vertices") bgneal@5: if (n * k) % 2 != 0: bgneal@5: raise GraphError("Can't make a regular graph of degree k and" bgneal@5: " order n where k * n is odd") bgneal@5: bgneal@5: # Remove all edges first bgneal@5: self.remove_all_edges() bgneal@5: bgneal@5: if k % 2 != 0: # if k is odd bgneal@5: self._add_regular_edges_even(k - 1) bgneal@5: self._add_regular_edges_odd() bgneal@5: else: bgneal@5: self._add_regular_edges_even(k) bgneal@5: bgneal@5: def _add_regular_edges_even(self, k): bgneal@5: """Make a regular graph with degree k. k must be even.""" bgneal@5: bgneal@5: vs = self.vertices() bgneal@5: vs2 = vs * 2 bgneal@5: bgneal@5: for i, v in enumerate(vs): bgneal@5: for j in range(1, k / 2 + 1): bgneal@5: w = vs2[i + j] bgneal@5: self.add_edge(Edge(v, w)) bgneal@5: bgneal@5: def _add_regular_edges_odd(self): bgneal@5: """Adds an extra edge across the graph to finish off a regular graph bgneal@5: with odd degree. The number of vertices must be even. bgneal@5: bgneal@5: """ bgneal@5: vs = self.vertices() bgneal@5: vs2 = vs * 2 bgneal@5: n = len(vs) bgneal@5: bgneal@5: for i in range(n / 2): bgneal@5: v = vs2[i] bgneal@5: w = vs2[i + n / 2] bgneal@5: self.add_edge(Edge(v, w)) bgneal@5: bgneal@7: def bfs(self, start, visit_func=None): bgneal@7: """Perform a breadth first search starting at node start. bgneal@7: bgneal@7: The function visit_func, if supplied, is invoked on each node. bgneal@7: bgneal@8: The set of visited nodes is returned. bgneal@8: bgneal@7: """ bgneal@8: visited = set() bgneal@7: bgneal@7: # Create a work queue consisting initially of the starting node bgneal@8: queue = deque([start]) bgneal@7: bgneal@7: while queue: bgneal@7: # retrieve first item from the queue bgneal@8: v = queue.popleft() bgneal@7: bgneal@8: if v in visited: bgneal@7: continue # Skip this one if we've seen it before bgneal@7: bgneal@7: # Mark it as visited and invoke user's function on it bgneal@8: visited.add(v) bgneal@7: if visit_func: bgneal@7: visit_func(v) bgneal@7: bgneal@7: # Add the adjacent neigbors to the node to the queue bgneal@22: queue.extend(c for c in self.out_vertices(v) if c not in visited) bgneal@7: bgneal@8: return visited bgneal@8: bgneal@7: def is_connected(self): bgneal@7: """Returns True if the graph is connected (there is a path from every bgneal@7: node to every other node) and False otherwise. bgneal@7: bgneal@7: """ bgneal@7: vs = self.vertices() bgneal@7: if len(vs): bgneal@8: visited = self.bfs(vs[0]) bgneal@7: # See if all nodes have been visited bgneal@8: return len(vs) == len(visited) bgneal@7: bgneal@7: return False # Graph is empty bgneal@7: bgneal@35: def get_p(self): bgneal@35: """This method returns a dictionary of probabilities where each key is bgneal@35: the connectivity k and the value is the probability [0-1] for this bgneal@35: graph. bgneal@35: bgneal@35: """ bgneal@35: # First, for each vertex, count up how many neighbors it has bgneal@35: vs = self.vertices() bgneal@35: bgneal@35: c = Counter() bgneal@35: for v in vs: bgneal@35: n = len(self.out_vertices(v)) bgneal@35: c[n] += 1 bgneal@35: bgneal@35: n = len(vs) bgneal@35: if n > 0: bgneal@35: for k in c: bgneal@35: c[k] = float(c[k]) / n bgneal@35: bgneal@35: return c bgneal@35: bgneal@36: def clustering_coefficient(self): bgneal@36: """Compute the clustering coefficient for this graph as defined by Watts bgneal@36: and Strogatz. bgneal@36: bgneal@36: """ bgneal@36: cv = {} bgneal@36: for v in self: bgneal@36: # consider a node and its neighbors bgneal@36: nodes = self.out_vertices(v) bgneal@36: nodes.append(v) bgneal@36: bgneal@36: # compute the maximum number of possible edges between these nodes bgneal@36: # if they were all connected to each other: bgneal@36: n = len(nodes) bgneal@36: if n == 1: bgneal@36: # edge case of only 1 node; handle this case to avoid division bgneal@36: # by zero in the general case bgneal@36: cv[v] = 1.0 bgneal@36: continue bgneal@36: bgneal@36: possible = n * (n - 1) / 2.0 bgneal@36: bgneal@36: # now compute how many edges actually exist between the nodes bgneal@36: actual = 0 bgneal@36: for x, y in itertools.combinations(nodes, 2): bgneal@36: if self.get_edge(x, y): bgneal@36: actual += 1 bgneal@36: bgneal@36: # the fraction of actual / possible is this nodes C sub v value bgneal@36: cv[v] = actual / possible bgneal@36: bgneal@36: # The clustering coefficient is the average of all C sub v values bgneal@36: if len(cv): bgneal@36: return sum(cv.values()) / float(len(cv)) bgneal@36: return 0.0 bgneal@36: bgneal@36: def shortest_path_tree(self, source, hint=None): bgneal@36: """Finds the length of the shortest path from the source vertex to all bgneal@36: other vertices in the graph. This length is stored on the vertices as an bgneal@36: attribute named 'dist'. The algorithm used is Dijkstra's. bgneal@36: bgneal@36: hint: if provided, must be a dictionary mapping tuples to already known bgneal@36: shortest path distances. This can be used to speed up the algorithm. bgneal@36: bgneal@36: """ bgneal@36: if not hint: bgneal@36: hint = {} bgneal@36: bgneal@36: for v in self.vertices(): bgneal@36: v.dist = hint.get((source, v), INFINITY) bgneal@36: source.dist = 0 bgneal@36: bgneal@36: queue = [v for v in self.vertices() if v.dist < INFINITY] bgneal@36: sort_flag = True bgneal@36: while len(queue): bgneal@36: bgneal@36: if sort_flag: bgneal@36: queue.sort(key=lambda v: v.dist) bgneal@36: sort_flag = False bgneal@36: bgneal@36: v = queue.pop(0) bgneal@36: bgneal@36: # for each neighbor of v, see if we found a new shortest path bgneal@36: for w, e in self[v].iteritems(): bgneal@36: d = v.dist + e.length bgneal@36: if d < w.dist: bgneal@36: w.dist = d bgneal@36: queue.append(w) bgneal@36: sort_flag = True bgneal@36: bgneal@36: def shortest_path_tree2(self, source): bgneal@36: """Finds the length of the shortest path from the source vertex to all bgneal@36: other vertices in the graph. This length is stored on the vertices as an bgneal@36: attribute named 'dist'. The algorithm used is Dijkstra's with a Heap bgneal@36: used to sort/store pending nodes to be examined. bgneal@36: bgneal@36: """ bgneal@36: for v in self.vertices(): bgneal@36: v.dist = INFINITY bgneal@36: source.dist = 0 bgneal@36: bgneal@36: queue = [] bgneal@36: heapq.heappush(queue, (0, source)) bgneal@36: while len(queue): bgneal@36: bgneal@36: _, v = heapq.heappop(queue) bgneal@36: bgneal@36: # for each neighbor of v, see if we found a new shortest path bgneal@36: for w, e in self[v].iteritems(): bgneal@36: d = v.dist + e.length bgneal@36: if d < w.dist: bgneal@36: w.dist = d bgneal@36: heapq.heappush(queue, (d, w)) bgneal@36: bgneal@36: def all_pairs_floyd_warshall(self): bgneal@36: """Finds the shortest paths between all pairs of vertices using the bgneal@36: Floyd-Warshall algorithm. bgneal@36: bgneal@36: http://en.wikipedia.org/wiki/Floyd-Warshall_algorithm bgneal@36: bgneal@36: """ bgneal@36: vertices = self.vertices() bgneal@36: dist = {} bgneal@36: for i in vertices: bgneal@36: for j in vertices: bgneal@36: if i is j: bgneal@36: dist[i, j] = 0.0 bgneal@36: else: bgneal@36: e = self.get_edge(i, j) bgneal@36: dist[i, j] = e.length if e else INFINITY bgneal@36: bgneal@36: for k in vertices: bgneal@36: for i in vertices: bgneal@36: for j in vertices: bgneal@36: d_ik = dist[i, k] bgneal@36: d_kj = dist[k, j] bgneal@36: new_cost = d_ik + d_kj bgneal@36: if new_cost < dist[i, j]: bgneal@36: dist[i, j] = new_cost bgneal@36: bgneal@36: return dist bgneal@36: bgneal@36: def big_l(self): bgneal@36: """Computes the "big-L" value for the graph as per Watts & Strogatz. bgneal@36: bgneal@36: L is defined as the number of edges in the shortest path between bgneal@36: two vertices, averaged over all vertices. bgneal@36: bgneal@36: Uses the shortest_path_tree() method, called once for every node. bgneal@36: bgneal@36: """ bgneal@36: d = {} bgneal@36: for v in self.vertices(): bgneal@36: self.shortest_path_tree(v, d) bgneal@36: t = [((w, v), w.dist) for w in self.vertices() if v is not w] bgneal@36: d.update(t) bgneal@36: bgneal@36: if len(d): bgneal@36: return sum(d.values()) / float(len(d)) bgneal@36: return 0.0 bgneal@36: bgneal@36: def big_l2(self): bgneal@36: """Computes the "big-L" value for the graph as per Watts & Strogatz. bgneal@36: bgneal@36: L is defined as the number of edges in the shortest path between bgneal@36: two vertices, averaged over all vertices. bgneal@36: bgneal@36: Uses the all_pairs_floyd_warshall() method. bgneal@36: bgneal@36: """ bgneal@36: dist = self.all_pairs_floyd_warshall() bgneal@36: vertices = self.vertices() bgneal@36: result = [dist[i, j] for i in vertices for j in vertices if i is not j] bgneal@36: bgneal@36: if len(result): bgneal@36: return sum(result) / float(len(result)) bgneal@36: return 0.0 bgneal@36: bgneal@36: def big_l3(self): bgneal@36: """Computes the "big-L" value for the graph as per Watts & Strogatz. bgneal@36: bgneal@36: L is defined as the number of edges in the shortest path between bgneal@36: two vertices, averaged over all vertices. bgneal@36: bgneal@36: Uses the shortest_path_tree2() method, called once for every node. bgneal@36: bgneal@36: """ bgneal@36: d = {} bgneal@36: for v in self.vertices(): bgneal@36: self.shortest_path_tree2(v) bgneal@36: t = [((v, w), w.dist) for w in self.vertices() if v is not w] bgneal@36: d.update(t) bgneal@36: bgneal@36: if len(d): bgneal@36: return sum(d.values()) / float(len(d)) bgneal@36: return 0.0 bgneal@36: bgneal@1: bgneal@1: def main(script, *args): bgneal@1: import pprint bgneal@1: bgneal@1: v = Vertex('v') bgneal@1: print v bgneal@1: w = Vertex('w') bgneal@1: print w bgneal@1: e = Edge(v, w) bgneal@1: print e bgneal@1: g = Graph([v,w], [e]) bgneal@1: pprint.pprint(g) bgneal@1: bgneal@1: print "g.get_edge(v, w): ", g.get_edge(v, w) bgneal@1: x = Vertex('x') bgneal@1: print "g.get_edge(v, x): ", g.get_edge(v, x) bgneal@1: bgneal@1: g.remove_edge(e) bgneal@1: pprint.pprint(g) bgneal@1: bgneal@1: print "vertices: ", g.vertices() bgneal@1: print "edges: ", g.edges() bgneal@1: bgneal@1: g.add_edge(e) bgneal@1: u = Vertex('u') bgneal@1: e1 = Edge(u, v) bgneal@1: e2 = Edge(u, w) bgneal@1: g.add_vertex(u) bgneal@1: g.add_edge(e1) bgneal@1: g.add_edge(e2) bgneal@1: print "Adding vertex u and edges:" bgneal@1: pprint.pprint(g) bgneal@1: print "vertices: ", g.vertices() bgneal@1: print "edges: ", g.edges() bgneal@1: bgneal@1: print "Out vertices for v: ", g.out_vertices(v) bgneal@1: print "Out edges for v: ", g.out_edges(v) bgneal@1: bgneal@1: x = Vertex('x') bgneal@1: g.add_vertex(x) bgneal@1: g.add_all_edges() bgneal@1: pprint.pprint(g) bgneal@1: bgneal@7: print "g is connected?", g.is_connected() bgneal@7: edges = g.out_edges(v) bgneal@7: for e in edges: bgneal@7: g.remove_edge(e) bgneal@7: pprint.pprint(g) bgneal@7: print "g is connected?", g.is_connected() bgneal@7: bgneal@7: # Isolate v and check is_connected() again bgneal@7: bgneal@1: bgneal@1: if __name__ == '__main__': bgneal@1: import sys bgneal@1: main(*sys.argv)