1 """Graphs
2
3 @var _version: Version of this module
4 @type _version: String
5 """
6
7 import Tkinter
8 import random
9 from Utils import member, emptyset, pretty_str_set, scrolled_frame
10 from IO import read_twlib, GraphCanvas
11 from Hypergraphs import Incidence, ReducedGraphicalHypergraph
12
13 _version = '$Id: Graphs.py,v 1.2 2008/10/07 09:09:58 jc Exp $'
17
20
23
26
29
32
35
38
40 """Abstract class containing methods which are appropriate only
41 for graphs where arrows are allowed.
42 """
43
45 """Add an arrow (directed edge) between two
46 existing vertices
47
48 Does not affect any lines from C{frm} to C{to}
49 @param frm: One of the vertices connected by the line
50 @type frm: Any immutable type
51 @param to: The other vertex connected by the line
52 @type to: Any immutable type
53 @raise KeyError: If either vertex does not exist
54 """
55 self._ch[frm].add(to)
56 self._pa[to].add(frm)
57
59 """Add a collection of arrows to the graph
60
61 @param arrows: The arrows to add
62 @type arrows: An iterator over pairs (of vertices).
63 Each pair is a sequence of length 2
64 @raise KeyError: If a vertex does not exist
65 """
66 for arrow in arrows:
67 self.add_arrow(arrow[0],arrow[1])
68
70 """Put an arrow (directed edge) between two
71 existing vertices
72
73 Any pre-existing line between C{frm} and C{to} will be deleted.
74 @param frm: One of the vertices connected by the line
75 @type frm: Immutable type
76 @param to: The other vertex connected by the line
77 @type to: Immutable type
78 @raise KeyError: If either vertex does not exist
79 @raise DirectedCycleError: If C{self} is an ADG and the
80 arrow would create a directed cycle
81 """
82 self.add_arrow(frm,to)
83 self.discard_line(frm,to)
84
86 """Add a collection of arrows to the graph
87
88 Any pre-existing lines will be deleted.
89 @param arrows: The arrows to add
90 @type arrows: An iterator over pairs (of vertices).
91 Each pair is a sequence of length 2
92 @raise KeyError: If a vertex does not exist
93 @raise DirectedCycleError: If C{self} is an ADG and one of the
94 arrows would create a directed cycle
95 """
96 for arrow in arrows:
97 self.put_arrow(arrow[0],arrow[1])
98
100 """Put arrows from all C{parents} to C{child},
101 creating vertices when needed
102
103 Any existing lines between parents and children
104 will be overwritten
105 @param child: The vertex to which all added arrows will point
106 @type child: Immutable type
107 @param parents: The collection of vertices from which there will be
108 arrows pointing to child
109 @type parents: An iterator over immutable types
110 @raise DirectedCycleError: If C{self} is an ADG and one of the
111 arrows would create a directed cycle
112 """
113 existing_vertices = self.vertices()
114 if child not in existing_vertices:
115 self.add_vertex(child)
116 for parent in parents:
117 if parent not in existing_vertices:
118 self.add_vertex(parent)
119 self.put_arrow(parent,child)
120
122 """Remove an arrow between two existing vertices
123
124 @param frm: One of the vertices connected by the possible arrow
125 @type frm: Immutable type
126 @param to: The other vertex connected by the possible arrow
127 @type to: Immutable type
128 @raise KeyError: If either vertex does not exist
129 @raise KeyError: If the arrow is not there
130 """
131 self._pa[to].remove(frm)
132 self._ch[frm].remove(to)
133
136 """Abstract class with method suitable for all types of graph
137
138 @ivar _pa: Maps each vertex to its parents
139 @type _pa: Dictionary
140 @ivar _ch: Maps each vertex to its children
141 @type _ch: Dictionary
142 @ivar _ne: Maps each vertex to its neighbours
143 @type _ne: Dictionary
144 @ivar _vertex_positions: Maps vertices to canvas co-ordinates
145 @type _vertex_positions: Dictionary
146 """
147
148 - def __init__(self,vertices=(),arrows=(),lines=(),vertex_positions=None):
149 """Graph initialisation
150
151 @param vertices: The vertices of the graph
152 @type vertices: An iterator/sequence over immutable types
153 @param arrows: The arrows of the graph
154 @type arrows: An iterator/sequence over pairs (of vertices).
155 Each pair is a sequence of length 2.
156 @param lines: The lines of the graph
157 @type lines: An iterator/sequence over pairs (of vertices).
158 Each pair is a sequence of length 2.
159 @param vertex_positions: A mapping from vertices to canvas
160 co-ordinates
161 @type vertex_positions: Dictionary
162 @raise KeyError: If C{arrows} or C{lines} contains a vertex not included in
163 C{vertices}
164 @raise ExistingVertexError: If C{vertices} repeats a vertex.
165 """
166 if vertex_positions is None:
167 vertex_positions = {}
168 self.reinit(vertices,arrows,lines,vertex_positions)
169
171 """Graphs of different classes are considered equal if they have the same vertices, arrows
172 and lines
173
174 @return: Whether the graphs are equal
175 @rtype: Boolean
176 """
177 return (
178 self._pa == other._pa and
179 self._ne == other._ne
180 )
181
183 """Graphs of different classes are considered equal if they
184 have the same vertices, arrows and lines
185
186 @return: Whether the graphs are not equal
187 @rtype: Boolean
188 """
189 return (
190 self._pa != other._pa or
191 self._ne != other._ne
192 )
193
195 """Formal string representation of a graph
196 """
197 return '%s(%s,%s,%s,%s)' % (
198 self.__class__.__name__,
199 sorted(self.vertexlist()),
200 sorted(self.arrows()),
201 sorted(self.lines()),
202 self._vertex_positions)
203
205 """Pretty string representation of a graph
206
207 Vertices, arrows and edges are presented in
208 sorted order
209 @return: Pretty string representation of a graph
210 @rtype: String
211 """
212 out = 'Vertices:\n%s\n' % sorted(self.vertexlist())
213 arrows = self.arrows()
214 if arrows:
215 out += 'Arrows:\n'
216 for arrow in sorted(arrows):
217 out += '%s -> %s\n' % arrow
218 lines = self.lines()
219 if lines:
220 out += 'Lines:\n'
221 for line in sorted(lines):
222 out += '%s - %s\n' % line
223 return out
224
226 '''Adds a vertex to a graph
227
228 @param vertex: The vertex to add
229 @type vertex: Anything immutable
230 @raise ExistingVertexError: If vertex already exists
231 '''
232 if vertex in self._pa:
233 raise ExistingVertexError
234 for dkt in (self._pa, self._ch, self._ne):
235 dkt[vertex] = set()
236
237
239 """Add a collection of vertices to the graph
240
241 @param vertices: The vertices to add
242 @type vertices: An iterator/sequence over immutable types
243 """
244 for vertex in vertices:
245 self.add_vertex(vertex)
246
248 """Returns an arbitrary vertex in the graph
249
250 @return: A vertex in the graph
251 @rtype: Anything immutable
252 """
253 return self._pa.keys()[0]
254
256 """Return a list of arrows
257
258 @return: A list of arrows
259 @rtype: List of C{(vertex,child)} tuples
260 """
261 arrows = []
262 for vertex, children in self._ch.items():
263 for child in children:
264 arrows.append((vertex,child))
265 return arrows
266
268 """Return an arbitrary child of C{vertex}
269
270 @return: A vertex which is a child of C{vertex} or None
271 if it has no children
272 @rtype: An immutable type (typically a String)
273 """
274 for child in self._ch[vertex]:
275 return child
276
277
279 """Return the children of vertex
280
281 @return: The children of C{vertex}
282 @rtype: Set
283 """
284 return self._ch[vertex]
285
287 """Return a copy a graph
288
289 @return: A copy of the graph
290 @rtype: Same class as C{self}
291 """
292 cp = self.__class__()
293 for attr, val in vars(self).items():
294 if attr == '_vertex_positions':
295 setattr(cp,attr,val.copy())
296 continue
297 setattr(cp,attr,{})
298 for vertex, vertices in val.items():
299 getattr(cp,attr)[vertex] = vertices.copy()
300 return cp
301
303 """Deletes a vertex from a graph
304
305 @param vertex: The vertex to delete
306 @type vertex: Immutable type
307 @raise KeyError: If the vertex does not exist
308 """
309 for dkt in (self._pa, self._ch, self._ne):
310 del dkt[vertex]
311 for vertices in dkt.values():
312 vertices.discard(vertex)
313
314 - def diff(self,other):
316
318 """Discard any arrow between two existing vertices
319
320 If there is no such arrow nothing happens
321 @param frm: One of the vertices connected by the possible arrow
322 @type frm: Immutable type
323 @param to: The other vertex connected by the possible arrow
324 @type to: Immutable type
325 @raise KeyError: If either vertex does not exist
326 """
327 self._pa[to].discard(frm)
328 self._ch[frm].discard(to)
329
331 """Discard the line between two existing vertices
332 (if it exists)
333
334 If there is no such line nothing happens
335 @param frm: One of the vertices connected by the possible arrow
336 @type frm: Immutable type
337 @param to: The other vertex connected by the possible arrow
338 @type to: Immutable type
339 @raise KeyError: If either vertex does not exist
340 """
341 self._ne[to].discard(frm)
342 self._ne[frm].discard(to)
343
345 """Return a list of the edges in the graph
346
347 @return: A list of edges
348 @rtype: List
349 """
350 edges = []
351 for dkt in (self._ne, self._ch):
352 for vertex, others in dkt.items():
353 for other in others:
354 edges.append((vertex,other))
355 return edges
356
358 """Iterates over paths which can be produced by adding
359 one vertex to a path in C{paths}
360
361 Each path is actually a tuple C{path_set,path}
362 where C{path_set} is the set of all vertices in the
363 path, and C{path} is a list representing the path itself
364
365 @param paths: Paths to extend
366 @type paths: List
367 @return: Generator of paths
368 @rtype: Generator iterator
369 """
370 for path_set, path in paths:
371 last_vertex = path[-1]
372 for new_vertex in ((self._ne[last_vertex] |
373 self._ch[last_vertex]) - path_set):
374 new_vertex_list = [new_vertex]
375 yield (path_set | set(new_vertex_list),
376 path + new_vertex_list)
377
378
380 """Set the graph to match the one displayed on a GUI,
381 and perhaps kill the GUI
382
383 @param gui: The GUI
384 @type gui: L{GraphCanvas} object
385 @param win: Window to kill
386 @type win: C{Tkinter} object
387 """
388 vertices, arrows, lines, vertex_positions = gui.graph()
389 try:
390 self.reinit(vertices,arrows,lines,vertex_positions)
391 if win is not None:
392 win.destroy()
393 except AttributeError, msg:
394 print 'Graph contains an illegal edge', msg
395
397 """Edit a graph using a GUI
398
399 @param parent: Parent window for GUI
400 @type parent: Suitable Tkinter widget
401 @param config: Configuration options for the GUI
402 @type config: Various
403 """
404 gui_main = Tkinter.Frame(parent)
405 gui_main.pack()
406 canvas = GraphCanvas(self, gui_main, **config)
407 canvas.pack()
408 bottom = Tkinter.Frame(gui_main)
409 bottom.pack()
410 for (txt,cmd) in (('Reset', canvas.original_state),
411 ('Delete', canvas.zap),
412 ('Save', lambda gui=canvas: self.get_gui_graph_kill(gui)),
413 ('Done', (lambda gui=canvas,win=gui_main:
414 self.get_gui_graph_kill(gui,win))),
415 ('Quit', (lambda gui=canvas,win=parent:
416 self.get_gui_graph_kill(gui,win))),
417 ('Help', self._gui_help)):
418 button = Tkinter.Button(bottom,text=txt,command=cmd)
419 button.bind('<Return>', lambda event: cmd())
420 button.pack(side=Tkinter.LEFT)
421
422
423
424 - def gui_display(self,parent,scrollable=False,**config):
425 """Display a graph using a GUI
426
427 @param parent: Parent window for GUI
428 @type parent: Suitable Tkinter widget
429 @param config: Configuration options for the GUI
430 @type config: Various
431 @return: The GUI
432 @rtype: L{GraphCanvas}
433 """
434 if scrollable:
435 return self.gui_display(scrolled_frame(parent),False,width=10000,height=1000,**config)
436 gui_main = GraphCanvas(self, parent, edit=False, **config)
437 gui_main.pack()
438 return gui_main
439
440
442 """Return whether C{vertex1} is a neighbour of C{vertex2}
443
444 @param vertex1: Vertex
445 @type vertex1: Immutable
446 @param vertex2: Vertex
447 @type vertex2: Immutable
448 @raise KeyError: If C{vertex1} is not a vertex
449 """
450 return vertex2 in self._ne[vertex1]
451
453 """Return whether C{parent} is a parent of C{child}
454
455 @param parent: Potential parent vertex
456 @type parent: Immutable
457 @param child: Potential child vertex
458 @type child: Immutable
459 @return: Whether C{parent} is a parent of C{child}
460 @rtype: Boolean
461 @raise KeyError: If C{child} is not a vertex
462 """
463 return parent in self._pa[child]
464
465
466
468 """Returns an ordered list of lines in the graph
469
470 @return: An ordered list of lines in the graph
471 @rtype: List
472 """
473 lines = []
474 vertices = set(self._ne.keys())
475 while vertices:
476 vertex1 = vertices.pop()
477 for vertex2 in self._ne[vertex1] & vertices:
478 if vertex1 < vertex2:
479 lines.append((vertex1,vertex2))
480 else:
481 lines.append((vertex2,vertex1))
482 return lines
483
498
499
501 """Return the set (not a copy) of neighbours of a vertex
502
503 Neighbours are connected by undirected edges (lines)
504 @return: Neighbours of C{vertex}
505 @rtype: Set
506 """
507 return self._ne[vertex]
508
520
522 """Return an arbitrary parent of vertex
523
524 @return: A vertex which is a parent of C{vertex} or None
525 if it has no parents
526 @rtype: An immutable type (typically a String)
527 """
528 for parent in self._pa[vertex]:
529 return parent
530
532 """Return the number of parents of vertex
533
534 @return: The number of parents of C{vertex}
535 @rtype: Integer
536 @raise KeyError: If C{vertex} is not in the graph.
537 """
538 return len(self._pa[vertex])
539
541 """Iterate over the number of parents of vertices
542
543 (In no particular order)
544 """
545 for ps in self._pa.values():
546 yield len(ps)
547
549 """Return (a copy of) the parents of vertex
550
551 @return: The parents of C{vertex}
552 @rtype: Set
553 @raise KeyError: If C{vertex} is not in the graph.
554 """
555 return self._pa[vertex].copy()
556
558 """Iterate over (copies of) all parent sets
559 """
560 for ps in self._pa.values():
561 yield ps.copy()
562
564 """Iterates over paths starting with C{vertex}
565
566 Each path is actually a tuple C{path_set,path}
567 where C{path_set} is the set of all vertices in the
568 path, and C{path} is a list representing the path itself
569
570 @param vertex: Vertex from which all paths start
571 @type vertex: Immutable
572 @return: Generator of paths
573 @rtype: Generator iterator
574 """
575 path = (set([vertex]),[vertex])
576 yield path
577 paths = [path]
578 while paths:
579 new_paths = []
580 for new_path in self.extend_paths(paths):
581 new_paths.append(new_path)
582 yield new_path
583 paths = new_paths
584
586 '''Adds a vertex to a graph or does nothing if it already exists
587
588 @param vertex: The vertex to add
589 @type vertex: Immutable type
590 '''
591 if vertex not in self._pa:
592 for dkt in (self._pa, self._ch, self._ne):
593 dkt[vertex] = set()
594
595
596 - def reinit(self,vertices,arrows,lines,vertex_positions):
597 """(Re-)initialise a graph
598
599 Pre-existing state will be deleted.
600 @param vertices: The vertices of the graph
601 @type vertices: An iterator/sequence over immutable types
602 @param arrows: The arrows of the graph
603 @type arrows: An iterator/sequence over pairs (of vertices).
604 Each pair is a sequence of length 2.
605 @param lines: The lines of the graph
606 @type lines: An iterator/sequence over pairs (of vertices).
607 Each pair is a sequence of length 2.
608 @param vertex_positions: A mapping from vertices to canvas
609 co-ordinates
610 @type vertex_positions: Dictionary
611 @raise KeyError: If C{arrows} or C{lines} contains a vertex not included in
612 C{vertices}
613 """
614 self._pa = {}
615 self._ch = {}
616 self._ne = {}
617 self.add_vertices(vertices)
618 if arrows:
619 self.add_arrows(arrows)
620 if lines:
621 self.add_lines(lines)
622 self._vertex_positions = vertex_positions
623
625 """Remove a vertex
626
627 @param vertex: Vertex to be removed
628 @type vertex: Immutable
629 @raise KeyError: If C{vertex} is not in the graph
630 """
631 for parent in self._pa[vertex]:
632 self._ch[parent].remove(vertex)
633 for child in self._ch[vertex]:
634 self._pa[child].remove(vertex)
635 for nbr in self._ne[vertex]:
636 self._ne[nbr].remove(vertex)
637 del self._pa[vertex]
638 del self._ch[vertex]
639 del self._ne[vertex]
640 try:
641 del self._vertex_positions[vertex]
642 except KeyError:
643 pass
644
646 """
647 Position each vertex according to the co-ordinates in C{coords}
648
649 @param coords: A dictionary mapping some (maybe all) vertices to a co-ordinate for display
650 @type coords: Dictionary
651 """
652 self._vertex_positions.update(coords)
653
655 """Structural Hamming distance to graph b from C{self}
656
657 +1 for a missing edge (directed or not)
658 +1 for an incorrectly oriented edge
659 +1 for an extra edge (directed or not)
660
661 Only works on simple graphs (those without multiple connections between vertices)
662
663 @param b: Graph to compare to C{self}
664 @type b: L{Graph}
665 @return: Structural Hamming distance to graph b from C{self}
666 @rtype: Int
667 """
668 if self.vertices() != b.vertices():
669 raise ValueError('Graphs must have the same vertex sets')
670
671 d = 0
672 vertices = self._pa.keys()
673 for i, v in enumerate(vertices):
674 for w in vertices[i+1:]:
675 if w in self._pa[v]:
676 if w not in b._pa[v]:
677 d += 1
678 elif w in self._ch[v]:
679 if w not in b._ch[v]:
680 d += 1
681 elif w in self._ne[v]:
682 if w not in b._ne[v]:
683 d += 1
684 elif (w in b._pa[v]) or (w in b._ch[v]) or (w in b._ne[v]):
685 d += 1
686 return d
687
688
689
690
692 """Return whether C{vertices} is a subset of the parents of C{child}
693
694 @param child: Child vertex
695 @type child: Immutable
696 @param vertices: Set of vertices
697 @type vertices: Iterable
698 @return: Whether C{vertices} is a subset of C{child}'s parents
699 @rtype: Boolean
700 @raise KeyError: If C{child} is not a vertex
701 """
702
703 return self._pa[child].issuperset(vertices)
704
705
707 """Return whether C{vertices} is a superset of the parents of C{child}
708
709 @param child: Child vertex
710 @type child: Immutable
711 @param vertices: Set of vertices
712 @type vertices: Iterable
713 @return: Whether C{vertices} is a superset of C{child}'s parents
714 @rtype: Boolean
715 @raise KeyError: If C{child} is not a vertex
716 """
717
718 return self._pa[child].issubset(vertices)
719
721 """
722 Return a dictionary mapping each vertex to a co-ordinate for display
723
724 @return: A dictionary mapping some (maybe all) vertices to a co-ordinate for display
725 @rtype: Dictionary
726 """
727 return self._vertex_positions.copy()
728
730 """Returns a list of the vertices in the graph
731
732 @return: A list of the vertices in the graph
733 @rtype: List
734 """
735 return self._pa.keys()
736
738 """Returns the set of vertices in the graph
739
740 Can be altered without affecting C{self}
741 @return: The set of vertices in the graph
742 @rtype: Set
743 """
744 return set(self._pa)
745
747 """Display help for editing graphs in a top-level window
748 """
749 top = Tkinter.Toplevel()
750 top.title('Graph editing help')
751 Tkinter.Label(top,text=self._edit_help_msg,justify=Tkinter.LEFT).pack()
752
753 _edit_help_msg = """
754 Select an object with the left mouse button.
755 Clicking on a node with the right button will draw a
756 line to the selected node (if any).
757 Clicking on a node with the middle button will draw an
758 arrow to the selected node (if any).
759
760 You will be prevented from saving a graph with an illegal edge
761 (e.g. an arrow in an undirected graph).
762
763 'Reset': Return the canvas to its original state
764 'Delete': Delete the selected object (node or edge)
765 'Save': Save the graph, but do not remove the canvas
766 'Done': Save the graph removing canvas and these buttons, but leaving the parent
767 'Quit': Save the graph and destroy the parent window
768 'Help': Generate this message
769 """
770
774 """Abstract class containing methods which are appropriate only for
775 graphs where both lines and arrows are allowed.
776 """
777
779 """Put an edge (in the mathematical sense)
780
781 @param frm: One of the vertices connected by the edge
782 @type frm: Immutable type
783 @param to: The other vertex connected by the edge
784 @type to: Immutable type
785 @raise KeyError: If either vertex does not exist
786 """
787 if frm in self._ch[to]:
788 self.put_line(frm,to)
789 else:
790 self.put_arrow(frm,to)
791
796 """Abstract class containing methods which are appropriate only for
797 graphs where lines are allowed.
798
799 @cvar _edit_help_msg: Help message for editing graphs
800 @type _edit_help_msg: String
801 """
802
804 """Add a clique with the specified vertices to the graph
805
806 Vertices may include new vertices. All vertices will be connected
807 by undirected edges.
808 @param vertices: Vertices to pairwise connect
809 @type vertices: Iterable
810 """
811 for vertex in vertices:
812 try:
813 self.add_vertex(vertex)
814 except ExistingVertexError:
815 pass
816 self.complete(vertices)
817
818
820 """Add a line (undirected edge) between two
821 existing vertices
822
823 @param frm: One of the vertices connected by the line
824 @type frm: Any immutable type
825 @param to: The other vertex connected by the line
826 @type to: Any immutable type
827 @raise KeyError: If either vertex does not exist
828 """
829 self._ne[frm].add(to)
830 self._ne[to].add(frm)
831
833 """Add a collection of lines to the graph
834
835 @param lines: The lines to add
836 @type lines: An iterator over pairs of vertices.
837 Each pair is a sequence of length 2
838 @raise KeyError: If a vertex does not exist
839 """
840 for line in lines:
841 self.add_line(line[0],line[1])
842
844 '''Pairwise connects all C{vertices} by undirected edges
845
846 Any existing directed edges are removed
847 @param vertices: Vertices to pairwise connect
848 @type vertices: Iterable
849 @raise KeyError: If a vertex does not exist
850 '''
851 if not vertices:
852 return
853 vs = list(vertices)
854 vertex1 = vs.pop()
855 while vs:
856 for vertex2 in vs:
857 self.put_line(vertex1,vertex2)
858 vertex1 = vs.pop()
859
861 """Put a line (undirected edge) between two
862 existing vertices
863
864 Any pre-existing arrow between C{frm} and C{to} in either
865 direction will be deleted.
866 @param frm: One of the vertices connected by the line
867 @type frm: Immutable type
868 @param to: The other vertex connected by the line
869 @type to: Immutable type
870 @raise KeyError: If either vertex does not exist
871 """
872 self._ne[frm].add(to)
873 self._ne[to].add(frm)
874 self.discard_arrow(frm,to)
875 self.discard_arrow(to,frm)
876
878 """Add a collection of lines to the graph
879
880 Any pre-existing arrows in either
881 direction will be deleted.
882 @param lines: The lines to add
883 @type lines: An iterator over pairs of vertices.
884 Each pair is a sequence of length 2
885 @raise KeyError: If a vertex does not exist
886 """
887 for line in lines:
888 self.put_line(line[0],line[1])
889
891 """Remove a line from the graph
892
893 @param frm: One of the vertices connected by the line
894 @type frm: Immutable type
895 @param to: The other vertex connected by the line
896 @type to: Immutable type
897 @raise KeyError: If either vertex does not exist
898 @raise KeyError: If the line is not there
899 """
900 self._ne[frm].remove(to)
901 self._ne[to].remove(frm)
902
903
904 -class Graph(_AbsGraph,_AbsDiGraph,_AbsUGraph,_AbsMixedGraph):
905 """Unrestricted graphs"""
906 pass
907
908
909 -class DiGraph(_AbsGraph,_AbsDiGraph):
910 """Graphs with only directed edges (i.e. arrows)
911
912 A graph with A->B and B<-A is not allowed, since this produces an
913 undirected edge.
914 """
915
916 - def __init__(self,vertices=(),arrows=(),vertex_positions=None):
918
919
921 return '%s(%s,%s,%s)' % (
922 self.__class__.__name__,
923 sorted(self.vertexlist()),
924 sorted(self.arrows()),
925 self._vertex_positions)
926
928 """Returns the set of all ancestors of C{vertex}.
929 @param vertex: A vertex in the graph
930 @type vertex: Immutable
931 @return: ancestors of C{vertex}
932 @rtype: frozenset of vertices
933 """
934 ancestors = set()
935 self._ancestors(vertex,ancestors)
936 return frozenset(ancestors)
937
938 - def connect(self,vertex,vertices):
939 """Iterates over digraphs produced by making C{vertex} a parent
940 to subsets of C{vertices}
941
942 @param vertex: Parent vertex
943 @type vertex: Immutable
944 @param vertices: Potential children
945 @type vertices: Sequence
946 @return: Generator of digraphs
947 @rtype: Generator iterator
948 """
949 if vertices:
950 first = vertices[0]
951 for adg in self.connect(vertex,vertices[1:]):
952 yield adg
953 adg.add_arrow(vertex,first)
954 yield adg
955 adg.remove_arrow(vertex,first)
956 else:
957 yield self
958
959
961 """Returns the set of all descendants of C{vertex}.
962 @param vertex: A vertex in the graph
963 @type vertex: Immutable
964 @return: descendants of C{vertex}
965 @rtype: frozenset of vertices
966 """
967 descendants = set()
968 self._descendants(vertex,descendants)
969 return frozenset(descendants)
970
971 - def dfsvisit(self,vertex,visited,order):
972 """Visit unvisited vertices in depth-first fashion and append
973 them to a topological ordering of vertices
974
975 @param vertex: First vertex to visit
976 @type vertex: Immutable type (typically String)
977 @param visited: Nodes already visited
978 @type visited: Set
979 @param order: Topological ordering of vertices
980 @type order: List
981 """
982 for parent in self._pa[vertex]:
983 if parent not in visited:
984 self.dfsvisit(parent,visited,order)
985 visited.add(vertex)
986 order.append(vertex)
987
988
990 """Enumerates all cycles in a digraph
991 of length at most C{lim}
992
993 Ref::
994
995
996 E{@}article{tarjan:211,
997 author = {Robert Tarjan},
998 title = {Enumeration of the Elementary Circuits of a Directed Graph},
999 publisher = {SIAM},
1000 year = {1973},
1001 journal = {SIAM Journal on Computing},
1002 volume = {2},
1003 number = {3},
1004 pages = {211-216},
1005 keywords = {algorithm; backtracking; circuit; cycle; digraph; graph},
1006 url = {http://link.aip.org/link/?SMJ/2/211/1},
1007 doi = {10.1137/0202017}
1008 }
1009
1010 E{@}TechReport{tarjan72:_enumer,
1011 author = {Robert Tarjan},
1012 title = {Enumeration of the elementary circuits of a directed graph},
1013 institution = {Cornell University},
1014 year = 1972,
1015 number = {72-145},
1016 address = {Ithaca, NY},
1017 month = {September}
1018 }
1019
1020 Time bound is O((V+E)(C+1)) where C is the (potentially large)
1021 number of cycles.
1022
1023 @return: Yields all cycles in the digraph
1024 @rtype: iterator
1025 """
1026 for v in sorted(self._pa):
1027 for cycle in self._tarjan_backtrack(v,v,set(),[],[],lim-1):
1028 yield cycle
1029
1051 for (x_node,y_node) in ordered_edges:
1052 if known(x_node,y_node):
1053 continue
1054 y_parents = self.parents(y_node)
1055 for w_node in essential_graph.parents(x_node):
1056 if w_node not in y_parents:
1057 for y_parent in y_parents:
1058 essential_graph.add_arrow(y_parent,y_node)
1059 break
1060 else:
1061 essential_graph.add_arrow(w_node,y_node)
1062 else:
1063 for z_node in y_parents:
1064 if z_node != x_node and z_node not in self.parents(x_node):
1065 method = essential_graph.add_arrow
1066 break
1067 else:
1068 method = essential_graph.add_line
1069 for y_parent in y_parents:
1070 if not known(y_parent,y_node):
1071 method(y_parent,y_node)
1072 return essential_graph
1073
1074
1075 @classmethod
1076
1078 for v, pa in self._pa.items():
1079 yield v, frozenset(pa)
1080
1081
1086
1088 """Generate digraphs which respect the order of C{vertices}
1089
1090 Generates all digraphs produced by adding arrows to C{self} which
1091 are between vertices in C{vertices} and which respect the order of vertices
1092 in C{vertices}.
1093
1094 @param vertices: Ordered vertices
1095 @type vertices: Tuple
1096 @return: Generator of digraphs which respect the order of C{vertices}
1097 @rtype: Generator iterator
1098 """
1099 if vertices:
1100 rest = vertices[1:]
1101 for adg in self.generate(rest):
1102 for adg2 in adg.connect(vertices[0],rest):
1103 yield adg2
1104 else:
1105 yield self
1106
1108 """Return all minimal cycles involving C{source}.
1109
1110 @param source: Source vertex
1111 @type source: Immutable
1112 @return: Minimal cycles
1113 @rtype: List of tuples of vertices
1114 """
1115 cycles = []
1116 for child in self._ch[source]:
1117 self._minimaldfs(child,source,(),cycles)
1118 return cycles
1119
1120
1122 """Return all minimal (non-direct) paths from C{source} to C{sink}.
1123
1124 A path C{source,v1,v2,.. sink} is minimal if there are no non-consecutive
1125 vertices joined by an edge in C{self}, (with the possible exception of source to sink)
1126 and all vertices are distinct.
1127 (There are no short-cuts provided by edges).
1128 @param source: Source vertex
1129 @type source: Immutable
1130 @param sink: Sink vertex
1131 @type sink: Immutable
1132 @return: Minimal paths
1133 @rtype: List of lists of vertices
1134 """
1135 paths = []
1136 self._minimaldfs(source,sink,(),paths)
1137 return paths
1138
1140 """Return a topological ordering of the vertices
1141
1142 Children come after their parents in the ordering
1143 @return: A topological ordering of the vertices
1144 @rtype: List
1145 """
1146 visited = set()
1147 order = []
1148 for vertex in self.vertexlist():
1149 if vertex not in visited:
1150 self.dfsvisit(vertex,visited,order)
1151 return order
1152
1154 """Add ancestors of C{vertex} not in C{ancestors} to
1155 C{ancestors}
1156 """
1157 for pa in self._pa[vertex] - ancestors:
1158 ancestors.add(pa)
1159 self._ancestors(pa,ancestors)
1160
1168
1169 - def _minimaldfs(self,current,sink,before_current,paths):
1170 """current is last vertex on current path
1171 before_current is all previous vertices
1172 """
1173 for child in self._ch[current]:
1174
1175 for i, v in enumerate(before_current):
1176 if v == child or (child in self._ch[v] and (i>0 or child != sink)):
1177 break
1178 else:
1179 if child == sink:
1180 paths.append(before_current + (current,sink))
1181 else:
1182 self._minimaldfs(child,sink,before_current+(current,),paths)
1183
1184
1185
1186
1187
1189 if len(point_stack) <= lim:
1190 f = False
1191 point_stack.append(v)
1192 marked.add(v)
1193 marked_stack.append(v)
1194 for w in self._ch[v]:
1195
1196 if w < start:
1197 continue
1198 elif w == start:
1199 yield tuple(point_stack+[start])
1200 f = True
1201 elif w not in marked:
1202 g = False
1203 for cycle in self._tarjan_backtrack(start,w,marked,marked_stack,point_stack,lim):
1204 yield cycle
1205 g = True
1206 f = f or g
1207 if f:
1208 while marked_stack[-1] != v:
1209 u = marked_stack.pop()
1210 marked.remove(u)
1211 marked_stack.pop()
1212 marked.remove(v)
1213 point_stack.pop()
1214
1216 """Undirected graphs where each connectivity component is a tree
1217 """
1218 pass
1219
1220 -class ADG(DiGraph):
1221 """Acyclic directed graphs
1222
1223 @ivar _an: Maps each vertex to its ancestors
1224 @type _an: Dictionary
1225 @ivar _de: Maps each vertex to its descendants
1226 @type _de: Dictionary
1227 """
1228
1229
1230 - def __init__(self,vertices=(),arrows=(),vertex_positions=None):
1234
1236 """Return whether C{an} is an ancestor of C{de}
1237
1238 @param an: Potential ancestor
1239 @type an: Immutable
1240 @param de: Potential descendant
1241 @type de: Immutable
1242 @return: Whether C{an} is an ancestor of C{de}
1243 @rtype: Boolean
1244 """
1245 return an in self._an[de]
1246
1248 return frozenset(self._an[vertex])
1249
1251 """Return the subgraph of the vertices in the small ancestral set
1252 of C{vertices}"""
1253
1254 an = set(vertices)
1255 for vertex in vertices:
1256 an |= self._an[vertex]
1257
1258
1259 arr = []
1260 for vertex in an:
1261 for ch in self._ch[vertex] & an:
1262 arr.append((vertex,ch))
1263 for pa in self._pa[vertex] & an:
1264 arr.append((pa,vertex))
1265
1266 return ADG(vertices=an, arrows=arr)
1267
1268
1270 return frozenset(self._de[vertex])
1271
1273 DiGraph.add_vertex(self,vertex)
1274 self._an[vertex] = set()
1275 self._de[vertex] = set()
1276
1278 """Add an arrow (directed edge) between two
1279 existing vertices
1280
1281 @param frm: One of the vertices connected by the line
1282 @type frm: Immutable type
1283 @param to: The other vertex connected by the line
1284 @type to: Immutable type
1285 @raise KeyError: If either vertex does not exist
1286 @raise DirectedCycleError: If the arrow would create a directed cycle
1287 """
1288 if to in self._an[frm]:
1289 raise DirectedCycleError
1290 DiGraph.add_arrow(self,frm,to)
1291
1292
1293 self._an[to].add(frm)
1294 self._an[to].update(self._an[frm])
1295 for de in self._de[to]:
1296 self._an[de].add(frm)
1297 self._an[de].update(self._an[frm])
1298
1299
1300 self._de[frm].add(to)
1301 self._de[frm].update(self._de[to])
1302 for an in self._an[frm]:
1303 self._de[an].add(to)
1304 self._de[an].update(self._de[to])
1305
1306 - def connect(self,vertex,vertices):
1307 """Iterates over all the the ADGs produced by connecting C{vertex} to
1308 C{vertices}
1309
1310 For each vertex in C{vertices}, C{vertex} can be 1)unconnected, 2)a parent
1311 or 3) a child.
1312 Assumes C{vertex} initially not connected to any C{vertices} in C{self}
1313 """
1314 if vertices:
1315 first = vertices[0]
1316 for adg in self.connect(vertex,vertices[1:]):
1317
1318
1319 yield adg
1320
1321
1322 try:
1323 adg.add_arrow(vertex,first)
1324 yield adg
1325 adg.remove_arrow(vertex,first)
1326 except DirectedCycleError:
1327 pass
1328
1329
1330 try:
1331 adg.add_arrow(first,vertex)
1332 yield adg
1333 adg.remove_arrow(first,vertex)
1334 except DirectedCycleError:
1335 pass
1336 else:
1337 yield self
1338
1339 - def connect(self,vertex,vertices):
1340 """Iterates over all the the ADGs produced by connecting C{vertex} to
1341 C{vertices}
1342
1343 For each vertex in C{vertices}, C{vertex} can be 1)unconnected, 2)a parent
1344 or 3) a child.
1345 Assumes C{vertex} initially not connected to any C{vertices} in C{self}
1346 """
1347 if vertices:
1348 first = vertices[0]
1349 for adg in self.connect(vertex,vertices[1:]):
1350
1351
1352 yield adg
1353
1354
1355 try:
1356 adg.add_arrow(vertex,first)
1357 yield adg
1358 adg.remove_arrow(vertex,first)
1359 except DirectedCycleError:
1360 pass
1361
1362
1363 try:
1364 adg.add_arrow(first,vertex)
1365 yield adg
1366 adg.remove_arrow(first,vertex)
1367 except DirectedCycleError:
1368 pass
1369 else:
1370 yield self
1371
1372
1374 """Iterates over all the the ADGs produced by connecting C{vertex} to
1375 C{vertices}
1376
1377 Makes copies to avoid expensive arrow removal
1378
1379 For each vertex in C{vertices}, C{vertex} can be 1)unconnected, 2)a parent
1380 or 3) a child.
1381 Assumes C{vertex} initially not connected to any C{vertices} in C{self}
1382 """
1383 if vertices:
1384 first = vertices[0]
1385 for adg in self.connect2(vertex,vertices[1:]):
1386
1387
1388 yield adg
1389
1390
1391 try:
1392 cp = adg.copy()
1393 cp.add_arrow(vertex,first)
1394 yield cp
1395 except DirectedCycleError:
1396 pass
1397
1398
1399 try:
1400 cp = adg.copy()
1401 cp.add_arrow(first,vertex)
1402 yield cp
1403 except DirectedCycleError:
1404 pass
1405 else:
1406 yield self.copy()
1407
1408
1410 """Return the numbers encoding C{self}
1411
1412 @param inv_atom_ids: Mapping from logical atoms to numbers
1413 @type inv_atom_ids: Dictionary
1414 @param predicate: The predicate symbol used for the relation beween a child
1415 and its parent set
1416 @type predicate: String
1417 @return: The numbers encoding C{self}, not in any particular order
1418 @rtype: List (of positive integers)
1419 @raise KeyError: if a family is missing
1420 """
1421 return [inv_atom_ids[(predicate,child,frozenset(parents))] for child,parents in self._pa.items()]
1422
1423
1425 """
1426 @todo: Needs improving
1427 """
1428 DiGraph.remove_arrow(self,frm,to)
1429
1430
1431 for an in self._an[frm]:
1432 for de in self._de[to]:
1433 if not self._ancestor(an,de):
1434 self._an[de].remove(an)
1435 self._de[an].remove(de)
1436 if not self._ancestor(an,to):
1437 self._an[to].remove(an)
1438 self._de[an].remove(to)
1439 for de in self._de[to]:
1440 if not self._ancestor(frm,de):
1441 self._an[de].remove(frm)
1442 self._de[frm].remove(de)
1443 if not self._ancestor(frm,to):
1444 self._an[to].remove(frm)
1445 self._de[frm].remove(to)
1446
1448
1449
1450 for child in self._ch[vertex].copy():
1451 self.remove_arrow(vertex,child)
1452 for parent in self._pa[vertex].copy():
1453 self.remove_arrow(parent,vertex)
1454 del self._pa[vertex]
1455 del self._ch[vertex]
1456 del self._ne[vertex]
1457 del self._an[vertex]
1458 del self._de[vertex]
1459
1460
1462 """True if C{an} is an ancestor of C{de}
1463
1464 Does check without using the _an or _de dictionaries
1465 So can be used for updating these dictionaries
1466 """
1467 for pa in self._pa[de]:
1468 if pa == an or self._ancestor(an,pa):
1469 return True
1470 return False
1471
1472
1473
1474 -class UGraph(_AbsGraph,_AbsUGraph):
1475 """Graphs with only undirected edges (i.e. lines)
1476 """
1477
1478 - def __init__(self,vertices=(),lines=(),vertex_positions=None):
1480
1481
1483 return '%s(%s,%s)' % (
1484 self.__class__.__name__,
1485 sorted(self.vertexlist()),
1486 sorted(self.lines()))
1487
1489 """Extend a complete subset of the graph to find cliques
1490
1491 This is for Version 1 of Bron and Kerbosch's algorithm. Version 2 is better,
1492 but more complex.
1493
1494 Checks whether C{candidates} includes all neighbours of some element of C{notset}
1495 in which case no clique can be formed::
1496
1497 @Article{bron73:_algor,
1498 author = {Bron, C. and Kerbosch, J.},
1499 title = {Algorithm 457: finding all cliques of an undirected graph},
1500 journal = {Communications of the {ACM}},
1501 year = 1973,
1502 volume = 16,
1503 pages = {575--577}
1504 }
1505
1506 @param compsub: The complete set of vertices to be extended
1507 @type compsub: Set
1508 @param candidates: Candidates available to extend C{compsub},
1509 @type candidates: Set
1510 @param notset: Vertices that have previously served as candidates and which
1511 are now explicitly excluded
1512 @type notset: Set
1513 @param hypergraph: The graphical hypergraph to which any found cliques will be added.
1514 @type hypergraph: L{ReducedGraphicalHypergraph}
1515 """
1516
1517
1518
1519
1520
1521
1522
1523
1524 for notitem in notset:
1525 if candidates <= self._ne[notitem]:
1526 return
1527 while candidates:
1528 candidate = candidates.pop()
1529 compsub.add(candidate)
1530 new_candidates = candidates & self._ne[candidate]
1531 new_notset = notset & self._ne[candidate]
1532 self.extend_compsub(compsub,new_candidates,new_notset,hypergraph)
1533 compsub.remove(candidate)
1534 notset.add(candidate)
1535 if not notset:
1536 hypergraph.add_hyperedge(compsub)
1537
1538 - def twdp(self,up=None,monitor=False):
1539 """Exact treewidth computation from Bodlaender et al
1540 """
1541 v = frozenset(self.vertices())
1542 n = len(v)
1543 if up is None:
1544 up = n - 1
1545 before = [((),-1)]
1546 found = False
1547 for i in range(1,n+1):
1548 now = {}
1549 for s, r in before:
1550 if found:
1551 if r >= up:
1552 continue
1553 elif r > up:
1554 continue
1555 ss = set(s)
1556 for x in v - ss:
1557
1558 todo = set([x])
1559 done = set([x])
1560 while todo:
1561 new = set()
1562 for vertex in todo:
1563 new |= self._ne[vertex]
1564 new -= done
1565 todo = new & ss
1566 done |= new
1567 q = len(done - ss) - 1
1568
1569 r1 = max(r,q)
1570 if found:
1571 if r1 >= up:
1572 continue
1573 elif r1 > up:
1574 continue
1575
1576 key = frozenset(ss|set([x]))
1577 try:
1578 t = now[key][2]
1579 if r1 < t:
1580 now[key] = s,x,r1
1581 except KeyError:
1582 now[key] = s,x,r1
1583 if found:
1584 if n-i-1 < up:
1585 found = (s+((x),), r1)
1586 up = max(r1,n-i-1)
1587 elif n-i-1 <= up:
1588 found = (s+((x),), r1)
1589 up = max(r1,n-i-1)
1590 before = [(s+((x),), r1) for (s,x,r1) in now.values()]
1591 if monitor:
1592 print i, len(before)
1593 return found
1594
1595 - def gui_triangulate(self,parent,elimination_order,
1596 original_colour='black',
1597 eliminating_colour='red',
1598 uneliminated_neighbour_colour='green',
1599 eliminated_colour='grey',
1600 fill_in_colour='blue',
1601 width=400,
1602 height=350,
1603 **config):
1604 """Triangulate the graph using C{elimination_order}
1605 and display the process graphically.
1606
1607 Alters C{self}
1608
1609 @param parent: A widget in which the graph will be displayed
1610 @type parent: A suitable Tkinter object, eg a Frame.
1611 @param original_colour: Colour for uneliminated vertices not
1612 participating in an elimination
1613 @type original_colour: String
1614 @param eliminating_colour: Colour for the vertex being eliminated
1615 @type eliminating_colour: String
1616 @param uneliminated_neighbour_colour: Colour for uneliminated neighbours
1617 of the the vertex being eliminated
1618 @type uneliminated_neighbour_colour: String
1619 @param eliminated_colour: Colour for eliminated vertices
1620 @type eliminated_colour: String
1621 @param fill_in_colour: Colour for fill-in lines
1622 @type fill_in_colour: String
1623 @param elimination_order: The elimination order
1624 @type elimination_order: An iterator (typically a list)
1625 @param config: Configuration options for the GUI
1626 @type config: Various
1627 @return: The triangulated graph
1628 @rtype: Same class as C{self}
1629 """
1630 gc = GraphCanvas(self,parent,edit=False,
1631 colour_user_actions=False,
1632 width=width,height=height,**config)
1633 gc.pack()
1634 bottom = Tkinter.Frame(parent)
1635 bottom.pack()
1636 labels = []
1637 for vertex in elimination_order:
1638 label = Tkinter.Label(bottom,text=vertex)
1639 labels.append(label)
1640 label.pack(side=Tkinter.LEFT)
1641 eliminated = set()
1642 i = [0]
1643 def handler(self=self, elimination_order=elimination_order,
1644 eliminated=eliminated,gc=gc,
1645 original_colour=original_colour,
1646 eliminating_colour=eliminating_colour,
1647 uneliminated_neighbour_colour=uneliminated_neighbour_colour,
1648 eliminated_colour=eliminated_colour,
1649 fill_in_colour=fill_in_colour,i=i,labels=labels
1650 ):
1651 j=i[0]
1652 if j > 0:
1653 labels[j-1].config(fg=eliminated_colour)
1654 try:
1655 labels[j].config(fg=eliminating_colour)
1656 i[0]=j+1
1657 except IndexError:
1658 labels[j-1].config(fg=eliminated_colour)
1659 return self._gui_elim_one(elimination_order,eliminated,gc,
1660 original_colour,eliminating_colour,
1661 uneliminated_neighbour_colour,
1662 eliminated_colour,fill_in_colour)
1663 Tkinter.Button(bottom,text='Next',command=handler).pack()
1664
1665
1668
1669 if choose is None:
1670 def choose(set): return set.pop()
1671
1672 vertices = self.vertices()
1673 sets = []
1674 alpha = {}
1675 alpha_inv = []
1676 cardinality = {}
1677 for vertex in vertices:
1678 cardinality[vertex] = 0
1679 sets.append(set())
1680 alpha_inv.append(None)
1681 sets[0] = vertices
1682 j = [0]
1683 i = [len(vertices)-1]
1684
1685 gc = GraphCanvas(self,parent,edit=False,
1686 colour_user_actions=False,
1687 width=width,height=height,**config)
1688 gc.pack()
1689 bottom = Tkinter.Frame(parent)
1690 bottom.pack()
1691 label = Tkinter.Label(bottom,text=str(alpha_inv))
1692 label.pack(side=Tkinter.LEFT)
1693 def handler(self=self,i=i,j=j,alpha=alpha,alpha_inv=alpha_inv,
1694 cardinality=cardinality,sets=sets,choose=choose):
1695 if not sets[j[0]]:
1696 return
1697 v=self._gui_maxcard_one(i,j,alpha,alpha_inv,cardinality,sets,choose)
1698 gc.vertex_config(v,fill='grey')
1699 label.config(text=str(alpha_inv))
1700 Tkinter.Button(bottom,text='Next',command=handler).pack()
1701
1703 """Return the clique hypergraph of an undirected graph
1704
1705 Uses Bron and Kerbosch algorithm, see L{extend_compsub}
1706 @return: The clique hypergraph of an undirected graph
1707 @rtype: L{ReducedGraphicalHypergraph}
1708 """
1709 cliques = ReducedGraphicalHypergraph()
1710 self.extend_compsub(set([]),self.vertices(),set([]),cliques)
1711 return cliques
1712
1713 clique_hypergraph = hypergraph
1714
1716 """Return whether the graph is triangulated
1717
1718 Uses L{maximum_cardinality_search}, see that method for acknowledgements
1719
1720 @return: Whether the graph is triangulated (aka 'decomposable', 'chordal')
1721 @rtype: Boolean
1722 """
1723 return self.triangulate(
1724 self.maximum_cardinality_search()[0],zero_fillin_check=True)
1725
1726 is_chordal = is_triangulated
1727
1728 is_decomposable = is_triangulated
1729
1731 """Return maximum cardinality search
1732
1733 Ref::
1734
1735 @Article{tarjan84:_simpl,
1736 author = {Robert E. Tarjan and Mihalis Yannakakis},
1737 title = {Simple linear-time algorithms to test chordality of graphs, test acyclicity of hypergraphs, and selectively reduce acyclic hypergraphs},
1738 journal = {{SIAM} Journal of Computing},
1739 year = 1984,
1740 volume = 13,
1741 number = 3,
1742 pages = {566--579},
1743 month = {August}
1744 """
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782 if choose is None:
1783 def choose(set): return set.pop()
1784
1785 vertices = self.vertices()
1786 sets = []
1787 alpha = {}
1788 alpha_inv = []
1789 cardinality = {}
1790 for vertex in vertices:
1791 cardinality[vertex] = 0
1792 sets.append(set())
1793 alpha_inv.append(None)
1794 sets.append(set())
1795 sets[0] = vertices
1796 j = 0
1797
1798 for i in range(len(vertices)-1,-1,-1):
1799 v = choose(sets[j])
1800 alpha[v] = i
1801 alpha_inv[i] = v
1802 for w in self._ne[v].difference(alpha):
1803 card_w = cardinality[w]
1804 sets[card_w].remove(w)
1805 card_w += 1
1806 sets[card_w].add(w)
1807 cardinality[w] = card_w
1808 j += 1
1809 while j >= 0 and sets[j] == emptyset:
1810 j -= 1
1811 return alpha, alpha_inv
1812
1814 """Add a line (undirected edge) to the graph between two
1815 existing vertices
1816
1817 @param frm: One of the vertices connected by the line
1818 @type frm: Immutable type
1819 @param to: The other vertex connected by the line
1820 @type to: Immutable type
1821 @raise KeyError: If either vertex does not exist
1822 """
1823 self._ne[frm].add(to)
1824 self._ne[to].add(frm)
1825
1827 n0 = len(self._ne)
1828 if sorted(self._ne) != range(n0):
1829 raise ValueError("Method only works on graphs with vertices named 0,1,..n")
1830 self.add_vertices(range(n0,n0+n))
1831 for new_edge in random.sample(xrange(n*n),m):
1832 self.put_line(n0 + (new_edge/n), n0 + (new_edge%n))
1833 return self
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1870 if elimination_order is None:
1871 elimination_order = self.maximum_cardinality_search()[0]
1872 eliminated = set()
1873 for vertex in elimination_order:
1874 message = self._ne[vertex] - eliminated
1875 while message:
1876 nbr = message.pop()
1877 for other_nbr in message:
1878 if other_nbr not in self._ne[nbr]:
1879 return False
1880 eliminated.add(vertex)
1881 return self
1882
1884 """Triangulate the graph B{inefficiently!} using C{elimination_order}
1885
1886 Triangulates the graph by eliminating vertices in the order given by
1887 C{elimination_order}. When a vertex is eliminated all its uneliminated
1888 neighbours must be a complete set (all pairwise connected by edges). Extra edges
1889 are added, if necessary, to ensure this is the case.
1890
1891 'Eliminated' vertices are not actually deleted, just marked as 'eliminated',
1892 so you don't end up with an empty graph!
1893
1894 @param elimination_order: The elimination order
1895 @type elimination_order: An iterator (typically a list)
1896 """
1897 eliminated = set()
1898 for vertex in elimination_order:
1899 message = self._ne[vertex] - eliminated
1900 self.complete(message)
1901 eliminated.add(vertex)
1902 return self
1903
1904 - def triangulate(self,elimination_order,zero_fillin_check=False,modify=True):
1905 """Triangulate the graph using C{elimination_order}
1906
1907 Alters C{self} by default
1908
1909 Ref::
1910
1911 @Article{tarjan84:_simpl,
1912 author = {Robert E. Tarjan and Mihalis Yannakakis},
1913 title = {Simple linear-time algorithms to test chordality of graphs, test acyclicity of hypergraphs, and selectively reduce acyclic hypergraphs},
1914 journal = {{SIAM} Journal of Computing},
1915 year = 1984,
1916 volume = 13,
1917 number = 3,
1918 pages = {566--579},
1919 month = {August}
1920
1921 @param elimination_order: The elimination order
1922 @type elimination_order: An iterator (typically a list)
1923 @param zero_fillin_check: If set the method returns whether C{elimination_order} is a zero
1924 fill-in and does not alter C{self}
1925 @type zero_fillin_check: Boolean
1926 @param modify: Whether to add the fill-in edges to C{self}
1927 @type modify: Boolean
1928 @return: (As long as C{zero_fillin_check=False}), the fill-in (added edges) produced by C{elimination_order}
1929 @rtype: (As long as C{zero_fillin_check=False}), the list of tuples, each tuple is an unordered pair of vertices
1930 """
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988 f = {}
1989 fill_in = []
1990 for w in elimination_order:
1991 f[w] = w
1992 earlier_neighbours = self._ne[w].intersection(f)
1993 done = set([w]) | earlier_neighbours
1994 for x in earlier_neighbours:
1995 x = f[x]
1996 while x not in done:
1997 if zero_fillin_check:
1998 return False
1999 done.add(x)
2000 fill_in.append((x,w))
2001 x = f[x]
2002 if f[x] == x:
2003 f[x] = w
2004 if zero_fillin_check:
2005 return True
2006 if modify:
2007 self.put_lines(fill_in)
2008 return fill_in
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033 - def _gui_elim_one(self,elimination_order,eliminated,gc,
2034 original_colour,eliminating_colour,
2035 uneliminated_neighbour_colour,
2036 eliminated_colour,fill_in_colour):
2037 if not elimination_order:
2038 for v in self.vertexlist():
2039 gc.vertex_config(v,fill=eliminated_colour)
2040 return
2041 vertex = elimination_order.pop(0)
2042 message = self._ne[vertex] - eliminated
2043 for v in self.vertexlist():
2044 if v == vertex:
2045 gc.vertex_config(v,fill=eliminating_colour)
2046 elif v in message:
2047 gc.vertex_config(v,fill=uneliminated_neighbour_colour)
2048 elif v in elimination_order:
2049 gc.vertex_config(v,fill=original_colour)
2050 else:
2051 gc.vertex_config(v,fill=eliminated_colour)
2052 msg = message.copy()
2053 while message:
2054 v1 = message.pop()
2055 for v2 in message - self._ne[v1]:
2056 gc.drawarc_vertices(v1,v2)
2057 gc.arc_config(v1,v2,fill=fill_in_colour)
2058 self.complete(msg)
2059 eliminated.add(vertex)
2060
2062 i0=i[0]
2063 j0=j[0]
2064 v = choose(sets[j0])
2065 alpha[v] = i0
2066 alpha_inv[i0] = v
2067 for w in self._ne[v].difference(alpha):
2068 card_w = cardinality[w]
2069 sets[card_w].remove(w)
2070 card_w += 1
2071 sets[card_w].add(w)
2072 cardinality[w] = card_w
2073 j0 += 1
2074 while j0 >= 0 and not sets[j0]:
2075 j0 -= 1
2076 i[0] -= 1
2077 j[0]=j0
2078 return v
2079
2082 """Undirected graphs where each connectivity component is a tree
2083
2084 @ivar _co: Maps each vertex to the set of all other vertices in its connectivity component
2085 @type _co: Dictionary
2086 """
2087
2088 - def __init__(self,vertices=(),lines=(),vertex_positions=None):
2093
2095 """Adds a vertex to a graph
2096
2097 @param vertex: The vertex to add
2098 @type vertex: Anything immutable
2099 @raise ExistingVertexError: If vertex already exists
2100 """
2101 UGraph.add_vertex(self,vertex)
2102 self._co[vertex] = set([vertex])
2103
2105 """Return a list of edges directed towards a root for each tree in the forest
2106
2107 Return a list of directed edges of form C{(frm,to)} where C{to} is
2108 nearer to an arbitrarily chosen root than C{frm} and where an edge closer to the root than another
2109 comes later in the returned list. There is a directed edge for each
2110 undirected edge in each tree in the forest.
2111
2112 @return: Directed edges
2113 @rtype: List
2114 """
2115 edges = []
2116 for tree in self.tree_vertices():
2117 edges.extend(self.ordered_edges_towards_root_tree(member(tree)))
2118 return edges
2119
2120
2122 """Return a list of edges directed towards a root
2123
2124 Return a list of directed edges of form C{(frm,to)} where C{to} is
2125 nearer to C{root} than C{frm} and where an edge closer to the root than another
2126 comes later in the returned list. There is a directed edge for each
2127 undirected edge in the tree containing C{root}.
2128
2129 @param root: Vertex towards which to direct edges
2130 @type root: Anything immutable
2131 @return: Directed edges
2132 @rtype: List
2133 """
2134 return self.ordered_edges_towards_root_tree_cut(root,root)
2135
2137 """Return a list of edges directed towards a root excluding those
2138 directed towards the root's neghbour C{block}.
2139
2140 If there is no C{root,block} edge then no blocking occurs
2141
2142 Return a list of directed edges of form C{(frm,to)} where C{to} is
2143 nearer to C{root} than C{frm} and where an edge closer to C{root} than another
2144 comes later in the returned list. If C{block} neighbours C{root} then
2145 there is a directed edge for each
2146 undirected edge in the tree formed by removing C{block} and all vertices which C{block}
2147 separates from C{root}. If C{block} does not neighbour C{root} then there is a directed edge for each
2148 undirected edge in the entire tree containing C{root}.
2149
2150 @param root: Vertex towards which to direct edges
2151 @type root: Anything immutable
2152 @param block: Vertex which blocks directed edges
2153 @type block: Anything immutable
2154 @return: Directed edges
2155 @rtype: List
2156 """
2157 edges = []
2158 for vertex in self._ne[root]:
2159 if vertex != block:
2160 edges.extend(self.ordered_edges_towards_root_tree_cut(vertex,root))
2161 edges.append((vertex,root))
2162 return edges
2163
2165 """Add a line (undirected edge) to the graph between two
2166 existing vertices
2167
2168 @param frm: One of the vertices connected by the line
2169 @type frm: Immutable type
2170 @param to: The other vertex connected by the line
2171 @type to: Immutable type
2172 @raise KeyError: If either vertex does not exist
2173 @raise CycleError: If adding the line would create a cycle
2174 """
2175 if frm in self._co[to]:
2176 raise CycleError
2177 UGraph.put_line(self,frm,to)
2178 self._co[frm].update(self._co[to])
2179 self._co[to].update(self._co[frm])
2180
2182 """Return the vertices of each tree in the forest
2183
2184 @return: The vertices of each tree in the forest
2185 @rtype: Set of frozensets
2186 """
2187 vertices = set()
2188 for tree in self._co.values():
2189 vertices.add(frozenset(tree))
2190 return vertices
2191
2195
2196 import copy
2197
2198
2202
2205
2207 return copy.deepcopy(self)
2208
2210 self._constrained_order = order
2211
2213
2214 if a == b:
2215 return True
2216
2217 fringe = [a]
2218 while fringe:
2219 next = set()
2220 for a in fringe:
2221 ch = self.children(a)
2222 if b in ch:
2223 return True
2224 next |= ch
2225 fringe = next
2226 return False
2227
2228 @staticmethod
2233
2235
2236
2237
2238 if self._constrained_order is None:
2239 return True
2240
2241
2242
2243 if child not in self._constrained_order or parent not in self._constrained_order:
2244 return True
2245
2246
2247 return self._constrained_order.index(child) > self._constrained_order.index(parent)
2248
2253
2254 - def orient(self, keep_class=False):
2255 """Orient an essential graph into a DAG. The class of self becomes ADG unless C{keep_class}
2256 From E{@}inproceedingsE{lb}meek95,
2257 author = {C. Meek},
2258 title = {Causal inference and causal explanation with background knowledge},
2259 year = {1995},
2260 booktitle = {Proceedings of the Eleventh Conference on Uncertainty in Artificial Intelligence},
2261 editor = {S. Hanks and P. Besnard},
2262 pages = {403--410},
2263 publisher = {Morgan Kaufmann}
2264 }
2265 """
2266 lines = self.lines()
2267 while lines:
2268
2269 edge = lines.pop()
2270
2271
2272 if edge[0] not in self.neighbours(edge[1]):
2273 continue
2274
2275
2276 self.remove_line(edge[0],edge[1])
2277 if self.is_potential_parent(edge[0], edge[1]):
2278 self.add_arrow(edge[0], edge[1])
2279 else:
2280 self.add_arrow(edge[1], edge[0])
2281
2282
2283 self.resolve()
2284
2285 if not keep_class:
2286 self = ADG(vertices=self.vertices(), arrows=self.arrows())
2287
2288 return self
2289
2306
2307
2308
2320
2321
2322
2323
2325
2326
2327
2328 progress = False
2329 for a,b in self.arrows():
2330 new_arrows = set()
2331 for c in self.neighbours(b) - self.adjacents(a):
2332 new_arrows.add((b,c))
2333
2334 for b,c in new_arrows:
2335 progress = True
2336 self.remove_line(b,c)
2337 self.add_arrow(b,c)
2338 return progress
2339
2341
2342
2343
2344
2345 progress = False
2346 for a,b in self.lines():
2347 if self._bounce_cycles(a,b):
2348 progress = True
2349 else:
2350 progress |= self._bounce_cycles(b,a)
2351 return progress
2352
2361
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378 progress = False
2379 for c,b in self.arrows():
2380 done = False
2381 n_c = frozenset([c]) | self.adjacents(c)
2382 p_b_nc = self.parents(b) - n_c
2383 for a in self.neighbours(b) & self.neighbours(c):
2384 for d in p_b_nc & self.neighbours(a):
2385 progress = True
2386 self.remove_line(a,b)
2387 self.add_arrow(a,b)
2388 done = True
2389 break
2390 if done:
2391 break
2392 return progress
2393
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409
2410 progress = False
2411 for d,b in self.arrows():
2412 done = False
2413 n_b = frozenset([b]) | self.adjacents(b)
2414 p_d_nb = self.parents(d) - n_b
2415 for a in self.neighbours(b) & self.adjacents(d):
2416 for c in p_d_nb & self.neighbours(a):
2417 progress = True
2418 self.remove_line(a,b)
2419 self.add_arrow(a,b)
2420 done = True
2421 break
2422 if done:
2423 break
2424 return progress
2425
2427 @staticmethod
2429 madg = adg.copy()
2430 madg.__class__ = MutilatedADG
2431 madg._mutilated_edges = {}
2432 return madg
2433
2437
2438
2439
2441 """Return a copy a graph
2442
2443 @return: A copy of the graph
2444 @rtype: Same class as C{self}
2445 """
2446 cp = self.__class__()
2447 for attr, val in vars(self).items():
2448 if attr == '_vertex_positions':
2449 setattr(cp,attr,val.copy())
2450 continue
2451 setattr(cp,attr,{})
2452 for vertex, vertices in val.items():
2453 if hasattr(vertices,'copy'):
2454 getattr(cp,attr)[vertex] = vertices.copy()
2455 else:
2456 getattr(cp,attr)[vertex] = vertices
2457 return cp
2458
2462
2464 return self._mutilated_edges.iteritems()
2465