1 '''
2 Graphcore contains all the base graph manipulation objects.
3 '''
4 import itertools
5
6 from exc import *
7 import visgraph.pathcore as vg_pathcore
8
10
11 '''
12 The base Graph object implements a simple nodes and edges graph.
13
14 Nodes -
15 Nodes consist of a dicionary of properties about that node and
16 a unique id which identifies the node in the current graph. From
17 API perspective a node is a tuple of (nodeid, nodeprops).
18
19 Edges -
20 Edges are directional sets of a from node-id and to node-id and
21 a piece of arbitrary edge information.
22 '''
23
25 self.wipeGraph()
26 self.nidgen = itertools.count()
27 self.eidgen = itertools.count()
28 self.metadata = {}
29
32
35
37 '''
38 Re-initialize the graph structures and start clean again.
39 '''
40 self.edges = {}
41 self.nodes = {}
42 self.edge_by_from = {}
43 self.edge_by_to = {}
44
46 '''
47 Get the list of edges in the graph. Edges are defined
48 as (eid, fromid, toid, einfo) tuples.
49
50 Example: for eid, fromid, toid, einfo in g.getEdges():
51 '''
52 return list(self.edges.values())
53
55 '''
56 Get the (eid, fromid, toid, einfo) tuple for the specified
57 edge.
58
59 Example: e = g.getEdge(eid)
60 '''
61 return self.edges.get(eid, None)
62
64 '''
65 Get a property about an edge specified by from/to id.
66
67 Example: n = g.getEdgeInfo(eid 'name', 'Default Name')
68 '''
69 edge = self.edges.get(eid)
70 if edge == None:
71 raise EdgeNonExistant(eid)
72 eid, fromid, toid, einfo = edge
73 return einfo.get(key, default)
74
76 '''
77 Set a key/value pair about a given edge by
78 it's from/to id.
79
80 Example: g.setEdgeInfo(eid, 'awesomeness', 99)
81 '''
82 edge = self.edges.get(eid)
83 if edge == None:
84 raise EdgeNonExistant(eid)
85 eid, fromid, toid, einfo = edge
86 einfo[key] = value
87
89 '''
90 Store a piece of information (by key:value) about a given node.
91
92 Example g.setNodeInfo(nid, 'Description', 'My Node Is Awesome!')
93 '''
94 d = self.nodes.get(nodeid)
95 if d == None:
96 raise NodeNonExistant(nodeid)
97 d[key] = value
98
100 '''
101 Retrieve a piece of information about a given node by key.
102
103 Example: descr = g.getNodeInfo(nid, 'Description', default='OH HAI')
104 '''
105 d = self.nodes.get(nodeid)
106 if d == None:
107 raise NodeNonExistant(nodeid)
108 return d.get(key, default)
109
111 '''
112 Get the node info dictionary for the specified node.
113
114 Example: d = g.getNodeProps(nid)
115 '''
116 return self.nodes.get(nodeid)
117
118 - def addNode(self, nodeid=None, ninfo=None, **kwargs):
119 '''
120 Add a Node object to the graph. Returns the nodeid.
121
122 Example: nid = g.addNode()
123 g.addNode('woot', {'height':20, 'width':20})
124
125 NOTE: If nodeid is unspecified, it is considered an 'anonymous'
126 node and will have an ID automagically assigned.
127 '''
128 if nodeid == None:
129 nodeid = self.nidgen.next()
130 p = self.nodes.get(nodeid)
131 if p != None:
132 raise DuplicateNode(nodeid)
133 if ninfo == None:
134 ninfo = {}
135 ninfo.update(kwargs)
136 ninfo['nodeid'] = nodeid
137 self.nodes[nodeid] = ninfo
138 return nodeid
139
146
148 '''
149 Return the dictionary of properties for the specified node id.
150 '''
151 return self.nodes.get(nodeid)
152
154 '''
155 Return a list of (nodeid, nodeinfo) tuples.
156 '''
157 return self.nodes.items()
158
160 '''
161 Check if a given node is present within the graph.
162
163 Example: if g.hasNode('yermom'): print 'woot'
164 '''
165 return self.getNode(nodeid) != None
166
167 - def addEdge(self, fromid, toid, eid=None, einfo=None, **kwargs):
168 '''
169 Add an edge to the graph. Edges are directional.
170
171 Example: g.addEdge(node_1, node_2, einfo={'name':'Woot Edge'})
172 '''
173 if einfo == None:
174 einfo = {}
175 einfo.update(kwargs)
176 if eid == None:
177 eid = self.eidgen.next()
178 e = (eid, fromid, toid, einfo)
179 self.edges[eid] = e
180 xlist = self.edge_by_from.get(fromid)
181 if xlist == None:
182 xlist = []
183 self.edge_by_from[fromid] = xlist
184 ylist = self.edge_by_to.get(toid)
185 if ylist == None:
186 ylist = []
187 self.edge_by_to[toid] = ylist
188 xlist.append(e)
189 ylist.append(e)
190 return eid
191
193 '''
194 Delete an edge from the graph (by eid).
195
196 Example: g.delEdge(eid)
197 '''
198 e = self.edges.pop(eid, None)
199 if e == None:
200 raise EdgeNonExistant(eid)
201
202 eid, fromid, toid, einfo = e
203 self.edge_by_to[toid] = [ x for x in self.edge_by_to[toid] if x[0] != eid ]
204 self.edge_by_from[fromid] = [ x for x in self.edge_by_from[fromid] if x[0] != eid ]
205
207 '''
208 Return a list of edges which originate with us.
209
210 Example: for eid, fromid, toid, einfo in g.getRefsFrom(id)
211 '''
212 ret = self.edge_by_from.get(nodeid)
213 if ret == None:
214 ret = []
215 return ret
216
218 '''
219 Return a list of edges which terminate at us.
220
221 Example: for eid, fromid, toid, einfo in g.getRefsFrom(id)
222 '''
223 ret = self.edge_by_to.get(nodeid)
224 if ret == None:
225 ret = []
226 return ret
227
229 '''
230 Return a list of the subgraph clusters (as graphs) contained
231 within this graph. A subgraph "cluster" is defined as a
232 group of interconnected nodes. This can be used to split
233 out grouped subgraphs from within a larger graph.
234 '''
235 ret = []
236 nodes = self.getNodes()
237 done = {}
238 while len(nodes):
239 nid, ninfo = nodes.pop()
240 if done.get(nid):
241 continue
242
243
244 todo = [ (nid, ninfo), ]
245 g = Graph()
246
247 while len(todo):
248 gnid, gninfo = todo.pop()
249
250 done[gnid] = True
251
252 if not g.getNode(gnid):
253 g.addNode(nodeid=gnid, ninfo=gninfo)
254
255 for eid,n1,n2,einfo in self.getRefsFrom(gnid):
256
257 if not g.getNode(n2):
258 n2info = self.getNodeProps(n2)
259 g.addNode(nodeid=n2, ninfo=n2info)
260 todo.append((n2, n2info))
261
262 if not g.getEdge(eid):
263 g.addEdge(n1, n2, eid=eid, einfo=einfo)
264
265 for eid,n1,n2,einfo in self.getRefsTo(gnid):
266
267 if not g.getNode(n1):
268 n1info = self.getNodeProps(n1)
269 g.addNode(nodeid=n1, ninfo=n1info)
270 todo.append((n1, n1info))
271
272 if not g.getEdge(eid):
273 g.addEdge(n1, n2, eid=eid, einfo=einfo)
274
275 ret.append(g)
276
277 return ret
278
279
281 for p in self.pathSearch(*args, **kwargs):
282 return p
283
284 - def pathSearch(self, fromid, toid=None, edgecb=None, tocb=None):
285 '''
286 Search for the shortest path from one node to another
287 with the option to filter based on edges using
288 edgecb. edgecb should be a function:
289
290 def myedgecb(graph, eid, fromid, toid, depth)
291
292 which returns True if it's OK to traverse this node
293 in the search.
294
295 Additionally, toid may be None and the caller may specify
296 tocb with a function such as:
297
298 def mytocb(graph, nid)
299
300 which must return True on finding the target node
301
302 Returns a list of edge ids...
303 '''
304
305 if toid == None and tocb == None:
306 raise Exception('You must use either toid or tocb!')
307
308 root = vg_pathcore.newPathNode(nid=fromid, eid=None)
309
310 todo = [(root, 0),]
311
312
313 while len(todo):
314
315 pnode,depth = todo.pop()
316 ppnode, pkids, pprops = pnode
317
318 nid = pprops.get('nid')
319 for edge in self.getRefsFrom(nid):
320
321 eid, srcid, dstid, einfo = edge
322
323 if vg_pathcore.isPathLoop(pnode, 'nid', dstid):
324 continue
325
326
327 if edgecb != None:
328 if not edgecb(self, edge, depth):
329 continue
330
331
332 match = False
333 if dstid == toid:
334 match = True
335
336 if tocb and tocb(self, dstid):
337 match = True
338
339 if match:
340
341 m = vg_pathcore.newPathNode(pnode, nid=dstid, eid=eid)
342 path = vg_pathcore.getPathToNode(m)
343
344 ret = []
345 for ppnode, pkids, pprops in path:
346 eid = pprops.get('eid')
347 if eid != None:
348 ret.append(eid)
349
350 yield ret
351
352
353 branch = vg_pathcore.newPathNode(pnode, nid=dstid, eid=eid)
354 todo.append((branch, depth+1))
355
356
357
359 '''
360 Search from the specified node (breadth first) until you
361 find a node where nodecb(graph, nid) == True. See
362 pathSearchFromTo for docs on edgecb...
363 '''
364
366 '''
367 An extension to the graph base which implements the idea of hierarchy
368 keeping a weight relationship based on the added edges.
369 Edges are directional from X to y.
370
371 You must add nodes with the property "rootnode" to know where the
372 hierarchy begins!
373 '''
376
378 '''
379 Get all the node id's in this graph which are weight 0 (meaning
380 they have no parents)...
381 '''
382 ret = []
383 for nid, nprops in self.nodes.items():
384 if nprops.get('rootnode', False):
385 ret.append(nid)
386 return ret
387
389 '''
390 Calculate the node weights for the given nodes in the hierarchical
391 graph based on the added "rootnode" nodes. This will return a dict
392 of { nodeid: weight, } key-pairs.
393
394 # NOTE: This will also set the 'weight' property on the nodes
395 '''
396 weights = {}
397 todo = [ (r, []) for r in self.getRootNodes() ]
398 while len(todo):
399
400 nid, path = todo.pop()
401
402 cweight = weights.get(nid, 0)
403 weights[nid] = max(cweight, len(path))
404
405 path.append(nid)
406
407 for eid, fromid, toid, einfo in self.getRefsFrom(nid):
408 if weights.get(toid, -1) >= len(path):
409 continue
410 if toid not in path:
411 todo.append((toid, list(path)))
412
413 for nid,weight in weights.items():
414 self.setNodeInfo(nid, 'weight', weight)
415
416 return weights
417
419 '''
420 Return the number of non-looped paths through this function.
421
422 NOTE: one small issue, "spiral" functions where the leaf node
423 has a lower weight than a block which must reach it by a
424 looped path will be a few total paths short...
425 '''
426
427 nodes = []
428 leafnodes = []
429
430
431 weights = self.getNodeWeights()
432
433
434
435 for nid,ninfo in self.getNodes():
436
437 nodes.append( (weights.get(nid), nid, ninfo) )
438 ninfo['pathcount'] = 0
439
440
441 xrfcnt = len(self.getRefsFrom(nid))
442 if xrfcnt == 0:
443 leafnodes.append((nid,ninfo))
444
445
446 if ninfo.get('rootnode'):
447 ninfo['pathcount'] = 1
448 continue
449
450
451 nodes.sort()
452
453
454 for weight, nid, ninfo in nodes:
455
456
457
458 for eid, fromid, toid, einfo in self.getRefsFrom(nid):
459 props = self.getNodeProps(toid)
460
461
462 if weights[toid] == weight:
463 continue
464 props['pathcount'] += ninfo['pathcount']
465
466 totcount = 0
467 for nid,ninfo in leafnodes:
468 totcount += ninfo['pathcount']
469
470 return totcount
471