#!/usr/bin/env python
# (c) Mark Janssen, this file is licensed under the GNU General Public License v2 found at <http://www.gnu.org/licenses>
# email: dreamingforward@gmail.com
# github: http://github.com/theProphet/GlassBeadGame
"""Graph class."""
#change a lot of these for loops to use faster map() function (see FAQ and QuickReference)
#also: map/reduce/filter now work with any iterable object (including dictionaries!)
#add persistence
#XXX Use of exceptions for control flow may prevent seeing actual errors. Perhaps catch exception, plus error string and assert string is as expected
from defdict import *
#EdgeBaseType = int
VertexBaseType = dict
GraphBaseType = defdict
_DEBUG = True
_PROFILE = True
NumberType = (int, float, long, complex) #for WVertex validation
#should all non-verb methods (sum_in, in_degree, etc.) be properties??
class VertexCommon(VertexBaseType):
"""Various common vertex methods."""
#Add id property to determine id, given Vertex
#XXX should clear() also remove in_vertices()?
__slots__ = ['_graph', '_id'] #functioning as a sort of "variable declaration" list
EDGEVALUE = 1
def __init__(self, graph, id, init={}):
"""Create a vertex object in graph. Assumes id already in graph.
Will add tails in init as necessary."""
self._graph = graph #graph to which this vertex belongs
self._id = id
super(VertexCommon, self).__init__()
self.update(init)
def update(self, tails, edge_value=EDGEVALUE): #XXX limit tails to list or dict?
"""Add tails in sequence or mapping type to Vertex."""
if isinstance(tails, dict):
for key, value in tails.iteritems(): #derived classes may override setitem so don't use dict.update
self[key] = value
else: #given iterable
for key in tails:
self[key] = edge_value
add = update
def discard(self, tail):
"""Removes tail(s) if present, otherwise does nothing.
>>> g = Graph()
>>> g.add(5, range(5))
>>> print g[5]
{0, 1, 2, 3, 4}
>>> g[5].discard(3) #remove single edge
>>> g[5].discard(90) #discard of non-existent edge ignored
>>> g[5].discard([0, 1, 90]) #discard multiple edges
>>> print g[5]
{2, 4}
"""
try: #single tail removal
del self[tail]
except LookupError: return #ignore non-existent tails
except TypeError, error: #must have been given a tail list
if not isinstance(tail, list): raise TypeError(error)
for t in tail: #XXX inefficient if self is near empty
try:
del self[t]
except LookupError:
if not self: break #good place to check if self is empty yet...
def in_vertices(self): #O(n)
"""Return iterator over the vertices that point to self.
>>> g = Graph()
>>> g.add(1, [2, 3, 4])
>>> g.add(2, [3, 2])
>>> list(g[2].in_vertices()) #XXX arbitrary order
[1, 2]
"""
for head in self._graph.itervalues():
if self._id in head:
yield head._id
def in_degree(self): #O(n)
"""Return number of edges pointing into vertex.
>>> g = Graph()
>>> g.add(1, [2, 3, 4])
>>> g.add(2, [3, 2])
>>> g[1].in_degree(), g[2].in_degree(), g[4].in_degree()
(0, 2, 1)
"""
return len(list(self.in_vertices()))
out_vertices = VertexBaseType.iterkeys
out_degree = VertexBaseType.__len__
def __getitem__(self, tail):
"""Return edge value or False if tail non-existent.
>>> g = Graph(VertexType=WVertex)
>>> g[1][3]
False
>>> g.add(1,3)
>>> g[1][3]
1
"""
return dict.get(self, tail, False)
def __setitem__(self, tail, value):
"""Set edge value. If tail does not exist, it is created and added to graph
if necessary.
>>> g = Graph(VertexType=WVertex)
>>> g[1][2] = 1
>>> g[1][3] += 1 #new tail (3): starts as value False (0), now add 1.
>>> print g
{1: {2: 1, 3: 1}, 2: {}, 3: {}}
"""
super(VertexCommon, self).__setitem__(tail, value)
if tail not in self._graph and tail!=self._id: #XXX ?do this first to preserve invariants in case vertex addition fails
self._graph.add(tail)
def copy(self): raise NotImplementedError
def __str__(self):
"""Return string of tail vertices with edge weight values.
>>> g = Graph(VertexType=WVertex)
>>> g.add(1, [1, 3, 4]); g.add(1, 3, 7)
>>> print g[1]
{1: 1, 3: 7, 4: 1}
"""
if _DEBUG: self._validate()
if not self: return '{}' #nothing to sort
keys = self.keys()
keys.sort()
return '{%s}' % ', '.join(["%r: %r" % (k, self[k]) for k in keys])
def _validate(self):
"""Assert Vertex invariants.
>>> g = Graph()
>>> g.add(1,2)
>>> dict.__setitem__(g[1], 3, 1) #tail value 3 not in graph
>>> g[1]._validate()
Traceback (most recent call last):
AssertionError: Non-existant tail 3 in vertex 1
>>> g.add(3,1)
>>> g[3]._id = 2
>>> g._validate() #should call vertex validates too
Traceback (most recent call last):
AssertionError: _graph[_id] is not self
"""
hash(self._id) #id should be hashable
assert isinstance(self._graph, Graph), "_graph attribute not a Graph"
assert self._graph[self._id] is self, "_graph[_id] is not self"
for t in self:
assert t in self._graph, "Non-existant tail %r in vertex %r" % (t, self._id)
class ReverseEdgeMixin(object): #could be used to make undirected graph?
"""Mixin to allow O(1) access to in_vertices. Inherit instead of or before VertexCommon."""
#XXX need doctests, also investigate slots issue (multiple bases have instance layout conflict)
__slots__ = []
def __init__(self, *args):
self.reverse = dict() #ReverseType()?
super(ReverseEdgeMixin, self).__init__(*args)
def in_vertices(self):
"""Return iterator over the vertices that point to self.
>>> class myvertex(ReverseEdgeMixin, Vertex):
... pass
>>> g = Graph(VertexType=myvertex)
>>> g[1].add([2, 3, 4])
>>> g[2].add([3, 2])
>>> list(g[2].in_vertices()) #XXX arbitrary order
[1, 2]
Can also use:
>>> list(g[2].reverse)
[1, 2]
"""
return self.reverse.iterkeys()
def in_degree(self):
"""Return number of edges pointing into vertex.
>>> class myvertex(ReverseEdgeMixin, Vertex):
... pass
>>> g = Graph(VertexType=myvertex)
>>> g[1].add([2, 3, 4])
>>> g[2].add([3, 2])
>>> g[1].in_degree(), g[2].in_degree(), g[4].in_degree()
(0, 2, 1)
Can also use:
>>> len(g[1].reverse), len(g[2].reverse), len(g[4].reverse)
(0, 2, 1)
"""
return len(self.reverse)
def sum_in(self):
"""Return sum of all edges that point to vertex.
>>> class myvertex(ReverseEdgeMixin, WVertex):
... pass
>>> g = Graph(VertexType=myvertex)
>>> g[1].add([1, 2, 3])
>>> g[4][1] += 3
>>> g[1].sum_in(), g[3].sum_in(), g[4].sum_in()
(4, 1, 0)
"""
return sum(self.reverse.itervalues())
def __setitem__(self, tail, value):
"""Set edge capacity. Will also update other vertex.reverse.
>>> class myvertex(ReverseEdgeMixin, WVertex):
... pass
>>> g = Graph(VertexType=myvertex)
>>> g[1][2] = 3
>>> g[1], g[2].reverse
({2: 3}, {1: 3})
"""
#print tail, value
super(ReverseEdgeMixin, self).__setitem__(tail, value)
value = self[tail] #value may have been modified by other classes
if tail != self._id: #creates tail vertex so check value first
self._graph[tail].reverse[self._id] = value
#print self._graph[tail].reverse
else:
self.reverse[self._id] = value
def __delitem__(self, tail):
"""Removes tail from self and self._id from tail.reverse.
>>> class myvertex(ReverseEdgeMixin, WVertex):
... pass
>>> g = Graph(VertexType=myvertex)
>>> g[1][2] = 3
>>> g[2].reverse
{1: 3}
>>> del g[1][2]
>>> g[2].reverse
{}
"""
if tail in self._graph:
del self._graph[tail].reverse[self._id] #creates tail vertex so check if tail in graph first
super(ReverseEdgeMixin, self).__delitem__(tail)
def clear(self): #XXX should this clear self.reverse also?
"""Removes all tails from self and all references to self._id in tail.reverse
>>> class myvertex(ReverseEdgeMixin, Vertex):
... pass
>>> g = Graph({1: {1: 1, 2: 4, 3: 9}, 2: {3: 8}}, myvertex)
>>> g[1].clear()
>>> g[1].reverse, g[2].reverse, g[3].reverse
({}, {}, {2: True})
"""
g, vid = self._graph, self._id
for tail in self:
del g[tail].reverse[vid]
super(ReverseEdgeMixin, self).clear()
def _validate(self):
super(ReverseEdgeMixin, self)._validate()
for tail in self:
assert self._graph[tail].reverse[self._id] == self[tail]
class WVertex(VertexCommon):
"""WVertex has directed, weighted edges."""
__slots__ = []
def sum_in(self):
"""Return sum of all edges that point to vertex.
>>> g = Graph(VertexType=WVertex)
>>> g.add(1, [1, 2, 3])
>>> g.add(4, 1, 3)
>>> g[1].sum_in(), g[3].sum_in(), g[4].sum_in()
(4, 1, 0)
"""
g, t = self._graph, self._id
sum = 0
for h in self.in_vertices():
sum += g[h][t]
return sum
def sum_out(self):
"""Return sum of all edges that leave from vertex.
>>> g = Graph(VertexType=WVertex)
>>> g.add(1, [1, 2, 3])
>>> g.add(1, 4, 3)
>>> g[1].sum_out(), g[2].sum_out()
(6, 0)
"""
return sum(self.itervalues())
def _validate(self):
super(WVertex, self)._validate() #Vertex._validate(self)
for weight in self.itervalues():
assert isinstance(weight, NumberType)
class Vertex(VertexCommon):
"""Vertex has directed, unweighted, edges."""
__slots__ = []
EDGEVALUE = True #XXX doesn't get used -- Graph[v1][v2] returns 1 instead of True
def __setitem__(self, tail, value):
"""Set edge value. If tail does not exist, it is created and added to graph
if necessary.
>>> g = Graph()
>>> g[1][2] = 3 #edge values ignored on plain Vertex
>>> g[1][2]
True
"""
super(Vertex, self).__setitem__(tail, True)
def __str__(self):
"""Return string of tail vertices in set notation.
>>> g = Graph()
>>> g.add(1, [1, 3, 4])
>>> print g[1]
{1, 3, 4}
"""
if _DEBUG: self._validate()
if not self: return '{}' #nothing to sort
keys = self.keys()
keys.sort()
return '{%s}' % ', '.join(map(repr, keys))
def MERGE_VERTEX(g, h, vert): g[h].update(vert)
class Graph(GraphBaseType):
"""Basic graph class. Graph features (directed, weighted, self-referencing, etc) determined by VertexType."""
#Basic data structure {vertex id: {t1: edge; t2: edge}}
#Add label to __init__ to attach description to graph?
#Perhaps make default vertex type WVertex and change doctests accordingly.
#Graph.fromkeys() override?
__slots__ = ['VertexType']
def __init__(self, init={}, VertexType=Vertex):
"""Create the graph, optionally initializing from another graph.
Optional VertexType parameter can be passed to specify default vertex type.
>>> g = Graph(VertexType=WVertex)
>>> len(g), g
(0, {})
>>> g.add([1, 2, 3], [2, 3], 5)
>>> print g
{1: {2: 5, 3: 5}, 2: {2: 5, 3: 5}, 3: {2: 5, 3: 5}}
>>> g2 = Graph(g, Vertex) #can initialize with other Graph type, will convert to Vertex type
>>> print g2
{1: {2, 3}, 2: {2, 3}, 3: {2, 3}}
"""
self.VertexType = VertexType
super(Graph, self).__init__(init, {}, MERGE_VERTEX)
def update(self, other, default=USE_DEFAULT, collision=MERGE_VERTEX): #XXX could remove this if collision was attribute of defdict
"""Merges one graph with another. All vertices will be convertex to VertexType. Takes union of edge lists.
>>> g1, g2 = Graph(VertexType=Vertex), Graph(VertexType=WVertex)
>>> g1.add(1, [1, 2])
>>> g2.add(3, [2, 3]); g2.add(1, 2, 3); g2.add(1, 4, 2)
>>> g1.update(g2) #XXX weight values get set on plain Vertex.
>>> print g1
{1: {1, 2, 4}, 2: {}, 3: {2, 3}, 4: {}}
>>> g2.add(3, 5) #changes to g2 should not affect g1
>>> g1._validate()
"""
super(Graph, self).update(other, default, collision)
def add(self, head, tail=[], edge_value=VertexCommon.EDGEVALUE):
"""Add the vertices and/or edges.
Parameters can be single vertex or list of vertices.
If no second parameter given, assume vertex addition only.
>>> g = Graph(VertexType=WVertex)
>>> g.add(1) #single vertex addition
>>> g.add(1) #adding existing vertex is ignored
>>> g.add([2, 3, 4]) #multiple vertex addition
>>> g.add([2]) #list containing only one vertex is allowed
>>> print g
{1: {}, 2: {}, 3: {}, 4: {}}
If second parameter given, then edge addition is performed.
Vertices are added as necessary. An optional edge value
is accepted as a third parameter.
>>> g.add(2, 1) #edge from vertex 2 to vertex 1
>>> g.add(1, 5, 100) #edge from 1 to new vertex 5 with weight 100
>>> g.add(1, 5, 90) #adding existing edge, edge value overwritten
>>> g.add(3, 3, 2) #loops are allowed
>>> g.add(3, 3) #edge weight overwritten by default if not specified
>>> print g
{1: {5: 90}, 2: {1: 1}, 3: {3: 1}, 4: {}, 5: {}}
Vertex lists allowed on either parameter for multiple edge addition.
>>> g.clear() #remove all vertices (and edges)
>>> g.add(1, [0, 2]) #add edges (1, 0) and (1, 2)
>>> g.add(1, [1])
>>> print g
{0: {}, 1: {0: 1, 1: 1, 2: 1}, 2: {}}
>>> g.add(range(3), range(3)) #fully-connected 3-vertex graph
>>> print g
{0: {0: 1, 1: 1, 2: 1}, 1: {0: 1, 1: 1, 2: 1}, 2: {0: 1, 1: 1, 2: 1}}
"""
#XXX if no edge_value given, then value should not be overwritten
if not isinstance(tail, list): tail = [tail]
try: #single head addition
self[head].add(tail, edge_value)
except TypeError, error: #multiple head addition
if not isinstance(head, list): raise TypeError(error)
for h in head: #XXX will add same tails multiple times
self[h].add(tail, edge_value)
def discard(self, head, tail=[]):
"""Remove vertices and/or edges. Parameters can be single vertex or list of vertices.
If tail is empty, then vertex deletions are made and any connected edges.
>>> g = Graph()
>>> g.add(range(3), range(4))
>>> g.discard(1) #remove vertex 1
>>> g.discard(10) #discard of non-existent vertex ignored
>>> g.discard([1]) #list with single vertex is fine
>>> g.discard([1, 3]) #discards vertices in list
>>> print g
{0: {0, 2}, 2: {0, 2}}
If tail is non-empty, then only edge deletions are made.
>>> g.discard(0, 2) #discard edge
>>> g.discard(5, 0) #non-existent edge ignored
>>> g.discard(2, [1, 0, 2, 2]) #will discard two actual edges
>>> print g
{0: {0}, 2: {}}
"""
if tail==[]: #vertex deletions
try:
del self[head]
except LookupError: pass #do nothing if given non-existent vertex
except TypeError, error: #given head list
if not isinstance(head, list): raise TypeError(error)
for h in head[:]: #must use copy since removing below
if h in self:
self[h].clear()
super(Graph, self).__delitem__(h) #don't duplicate effort (will discard in_vertices below)
else: head.remove(h) #for faster tail removal in next loop
for h in self.itervalues(): #visit remaining vertices and remove occurances of head items in edge lists
h.discard(head)
else: #edge deletions only
if not isinstance(head, list): head = [head] #quick and dirty to avoid extra code
for h in head:
if h in self:
self[h].discard(tail)
if _DEBUG: self._validate()
def __contains__(self, vid): #XXX probably slows things down for little value?
"""Returns non-zero if v in self. If a list is given, all
items are checked for containment.
>>> g = Graph()
>>> g[1].add([1, 2, 2, 3])
>>> 1 in g and 2 in g
True
>>> [1, 2, 3] in g
True
>>> [1, 4] in g
False
>>> [] in g
True
"""
try:
return dict.__contains__(self, vid)
except TypeError, error: #must have been given list
if not isinstance(vid, list): raise TypeError(error)
for v in vid:
if not dict.__contains__(self, v):
return False
return True
def __getitem__(self, vid): #could just set equal to GraphBaseType.setdefault, but doctest module complains
"""Return value of corresponding key. If key does not exist, create it
with default value.
>>> g = Graph()
>>> g[1].add([1,2])
>>> g[3][1]
False
>>> print g #NOTE: Vertex(3) created!
{1: {1, 2}, 2: {}, 3: {}}
"""
return self.setdefault(vid, {}) #will convert plain {} to VertexType if necessary
def __setitem__(self, vid, value):
"""Set graph[vid] to VertexType(value).
>>> g = Graph(VertexType=WVertex)
>>> g[1] = {} #set g[1] to empty vertex (no out edges)
>>> type(g[1]) is WVertex
True
>>> g[2] = {1: 4, 3: 9} #non-existent vertex values get created automatically
>>> print g #XXX what if VertexType==Vertex -> how to specify no edge value???
{1: {}, 2: {1: 4, 3: 9}, 3: {}}
>>> g._validate()
>>>
"""
if isinstance(value, self.VertexType) and value._id == vid and value._graph == self:
dict.__setitem__(self, vid, value)
else: #convert to VertexType or create copy of VertexType
dict.__setitem__(self, vid, self.VertexType(self, vid, value)) #XXX shallow copy
def __delitem__(self, head):
"""Delete a single vertex and associated edges.
>>> g = Graph()
>>> g.add([1, 2], [1, 2, 3])
>>> del g[2] #will remove vertex 2 and edges [(1, 2), (2, 1), (2, 2), (2, 3)]
>>> print g
{1: {1, 3}, 3: {}}
Raises LookupError if given non-existant vertex.
>>> del g[2]
Traceback (most recent call last):
...
KeyError: 2
"""
dict.__getitem__(self, head).clear() #removes out vertices (bypass key creation with dict.__getitem__)
for v in list(self[head].in_vertices()): #create copy (via list()) since in_vertices contents may change during iteration
del self[v][head]
super(Graph, self).__delitem__(head)
def __str__(self):
"""Return graph in adjacency format.
>>> g = Graph()
>>> g.add(range(3), range(3))
>>> str(g)
'{0: {0, 1, 2}, 1: {0, 1, 2}, 2: {0, 1, 2}}'
>>> g = Graph(VertexType=WVertex)
>>> g.add(range(3), range(3))
>>> str(g)
'{0: {0: 1, 1: 1, 2: 1}, 1: {0: 1, 1: 1, 2: 1}, 2: {0: 1, 1: 1, 2: 1}}'
"""
if _DEBUG: self._validate()
return super(Graph, self).__str__()
#return '{%s}' % ', '.join(map(str, self.itervalues()))
def display(self):
"""Display adjacency list.
>>> g=Graph()
>>> g.add(range(2), range(3))
>>> g.display()
0: {0, 1, 2}
1: {0, 1, 2}
2: {}
"""
if _DEBUG: self._validate()
for vid, v in self.iteritems():
print "%s: %s" % (vid, v)
#alternate syntax for various items
vertices = GraphBaseType.iterkeys
order = GraphBaseType.__len__
def pop(self, key, default): raise NotImplementedError
def popitem(self): raise NotImplementedError
def copy(self): raise NotImplementedError
def _validate(self):
"""Check graph invariants.
>>> g = Graph()
>>> g[1] = {2: 3, 3: 9}
>>> g._validate()
>>> dict.__setitem__(g, 1, {2: 3, 3: 9}) #bypass Graph
>>> g._validate()
Traceback (most recent call last):
AssertionError: vertex type not found on 1
"""
#NOTE: calling this after each add/discard slows things down considerably!
for vid, v in self.iteritems():
assert isinstance(v, self.VertexType), "vertex type not found on " + str(vid)
v._validate()
def gprofile(g, size=100):
import time
print "Profiling (ignoring debug)..."
_DEBUG = 0
for i in [1,2]:
start=time.clock()
g.add(range(size),range(100,size + 100))
finish=time.clock()
print "Add %i, 100-(%i+100); pass %i: %5.2fs" % (size, size, i, (finish-start))
for i in [1,2]:
start=time.clock()
g.discard(range(size + 50), range(100))#, range(1000))
finish=time.clock()
print "Discard (%i+50), 100; pass %i: %5.2fs" % (size, i, (finish-start))
g.clear()
g.add(0)
for i in [1,2]:
start=time.clock()
g[0].update(range(size))
finish=time.clock()
print "Update %i, %i; pass %i: %5.2fs" % (size, size, i, (finish-start))
g.clear()
def _test():
"""Miscellaneous tests.
>>> g = Graph(VertexType=WVertex)
>>> g.add(5) #add vertex with id=5 to graph, default edge value=1
>>> g.add(5, 5) #edges pointing back to self are allowed
>>> g[5][7] = 42 #add single out-edge from vertex 5 to 7 with weight 42
>>> assert 7 in g #vertex 7 automatically added to graph
>>> g[5].add([3, 2, 4]) #add 3 out edges from 5, default weight 1
>>> print g[5] #show out-edges from vertex 5
{2: 1, 3: 1, 4: 1, 5: 1, 7: 42}
>>> g.add(5, 7, 24) #edge values are over-written
>>> g[5][7]
24
>>> g.add(5, 7) #edge value over-written with default if not specified
>>> g[5][7]
1
"""
pass
if __name__ == '__main__':
import doctest
doctest.testmod() #, isprivate=lambda *args: 0)
Diff to Previous Revision
--- revision 3 2015-10-15 15:54:16
+++ revision 4 2015-10-15 21:33:32
@@ -1,6 +1,7 @@
#!/usr/bin/env python
# (c) Mark Janssen, this file is licensed under the GNU General Public License v2 found at <http://www.gnu.org/licenses>
# email: dreamingforward@gmail.com
+# github: http://github.com/theProphet/GlassBeadGame
"""Graph class."""