def test_create_edge_label(self) -> None: g = Graph() v1 = g.add_vertex(1) v2 = g.add_vertex(2) assert (edge_module.create_edge_label( v1, v1, is_directed=g.is_directed()) == "(1, 1)" ), "loop edge label should be (1, 1)" assert (edge_module.create_edge_label( v1, v2, is_directed=g.is_directed()) == "(1, 2)" ), "edge label should be (1, 2)" assert (edge_module.create_edge_label( v2, v1, is_directed=g.is_directed()) == "(1, 2)" ), "edge label should be (1, 2)" assert (edge_module.create_edge_label( 5, 9, is_directed=g.is_directed()) == "(5, 9)" ), "edge label should be (5, 9)" assert (edge_module.create_edge_label( "t", "s", is_directed=g.is_directed()) == "(s, t)" ), "edge label should be (s, t)" g2 = DiGraph() v3 = g2.add_vertex(3) v4 = g2.add_vertex(4) assert (edge_module.create_edge_label( v3, v4, is_directed=g2.is_directed()) == "(3, 4)" ), "edge label should be (3, 4)" assert (edge_module.create_edge_label( v4, v3, is_directed=g2.is_directed()) == "(4, 3)" ), "edge label should be (4, 3)" assert (edge_module.create_edge_label( "t", "s", is_directed=g2.is_directed()) == "(t, s)" ), "edge label should be (t, s)"
def _remove_duplicate_undirected_edges( source_vertex_label: str, edge_tuples: List[T], edge_label_to_source: Dict[str, str]) -> List[T]: """For undirected graphs, adjacency lists generally repeat edge entries for each endpoint. For example, edges (1, 2), (1, 3) would appear as: 1 2 3 2 1 3 1 This function removes duplicates, where a duplicate is defined as an edge with the same edge label (as defined by the function :func:`create_label <vertizee.classes.edge.create_label>`) that maps to a different source vertex. Source vertices are defined by the first column of an adjacency list file. """ cnt: Counter[Any] = collections.Counter() for t in edge_tuples: cnt[t] += 1 unique_edge_tuples = [] for t in cnt: edge_label = edge_module.create_edge_label(t[0], t[1], is_directed=False) if edge_label not in edge_label_to_source: edge_label_to_source[edge_label] = source_vertex_label if edge_label_to_source[edge_label] == source_vertex_label: for _ in range(cnt[t]): unique_edge_tuples.append(t) return unique_edge_tuples
def has_edge(self, vertex1: VertexType, vertex2: VertexType) -> bool: """Returns True if the tree contains the edge. Instead of using this method, it is also possible to use the ``in`` operator: >>> if ("s", "t") in tree: or with objects: >>> edge_st = graph.add_edge("s", "t") >>> if edge_st in tree: Args: vertex1: The first endpoint of the edge. vertex2: The second endpoint of the edge. Returns: bool: True if there is a matching edge in the tree, otherwise False. See Also: :mod:`VertexType <vertizee.classes.vertex>` """ label = edge_module.create_edge_label(vertex1, vertex2, self.is_directed()) return label in self._edges
def get_edge(self, vertex1: "VertexType", vertex2: "VertexType") -> E_co: """Returns the :term:`edge` specified by the vertices, or None if no such edge exists. Args: vertex1: The first vertex (the :term:`tail` in :term:`directed graphs <directed graph>`). vertex2: The second vertex (the :term:`head` in directed graphs). Returns: EdgeBase[V]: The specified edge. Raises: KeyError: If the tree does not contain an edge with the specified vertex endpoints. """ edge_label = edge_module.create_edge_label(vertex1, vertex2, self.is_directed()) return self._edges[edge_label]
def bfs_labeled_edge_traversal( graph: GraphBase[V_co, E_co], source: Optional[VertexType] = None, depth_limit: Optional[float] = None, reverse_graph: bool = False, ) -> Iterator[Tuple[V_co, V_co, str, str, float]]: """Iterates over the labeled :term:`edges <edge>` of a breadth-first search traversal. Running time: :math:`O(m + n)` Note: If ``source`` is specified, then the traversal only includes the graph :term:`component <connected component>` containing the ``source`` vertex. For :term:`directed graphs <digraph>`, setting ``reverse_graph`` to True will generate vertices as if the graph were :term:`reversed <reverse>`. Args: graph: The graph to search. source: The source vertex from which to discover reachable vertices. depth_limit: Optional; The depth limit of the search. Defaults to None (no limit). reverse_graph: Optional; For directed graphs, setting to True will yield a traversal as if the graph were reversed (i.e. the :term:`reverse graph <reverse>`). Defaults to False. Yields: Tuple[Vertex, Vertex, str, str, int]: An iterator over tuples of the form ``(parent, child, label, search_direction, depth)`` where ``(parent, child)`` is the edge being explored in the breadth-first search. The ``label`` is one of the strings: 1. "tree_root" - :math:`(u, u)`, where :math:`u` is the root vertex of a BFS tree. 2. "tree_edge" - edge :math:`(u, v)` is a tree edge if :math:`v` was first discovered by exploring edge :math:`(u, v)`. 3. "back_edge" - back edge :math:`(u, v)` connects vertex :math:`u` to ancestor :math:`v` in a breadth-first tree. Per *Introduction to Algorithms*, self loops are considered back edges. :cite:`2009:clrs` 4. "forward_edge" - non-tree edges :math:`(u, v)` connecting a vertex :math:`u` to a descendant :math:`v` in a breadth-first tree. 5. "cross_edge" - All other edges, which may go between vertices in the same breadth-first tree as long as one vertex is not an ancestor of the other, or they go between vertices in different breadth-first trees. The ``search_direction`` is the direction of traversal and is one of the strings: 1. "preorder" - the traversal discovered new vertex `child` in the BFS. 2. "postorder" - the traversal finished visiting vertex `child` in the BFS. 3. "already_discovered" - the traversal found a non-tree edge connecting to a vertex that was already discovered. The ``depth`` is the count of edges between ``child`` and the root vertex in its breadth-first search tree. If the edge :math:`(parent, child)` is not a tree edge (or the tree root), the ``depth`` defaults to infinity. Example: The labels reveal the complete transcript of the breadth-first search algorithm. >>> from pprint import pprint >>> import vertizee as vz >>> from vertizee.algorithms import search >>> g = vz.DiGraph([(0, 1), (1, 2), (2, 1)]) >>> pprint(list(search.bfs_labeled_edge_traversal(g, source=0))) [(0, 0, 'tree_root', 'preorder', 0), (0, 1, 'tree_edge', 'preorder', 1), (0, 0, 'tree_root', 'postorder', 0), (1, 2, 'tree_edge', 'preorder', 2), (0, 1, 'tree_edge', 'postorder', 1), (2, 1, 'back_edge', 'already_discovered', inf), (1, 2, 'tree_edge', 'postorder', 2)] See Also: * :class:`Direction <vertizee.algorithms.algo_utils.search_utils.Direction>` * :class:`Label <vertizee.algorithms.algo_utils.search_utils.Label>` * :class:`SearchResults <vertizee.algorithms.algo_utils.search_utils.SearchResults>` Note: This function uses ideas from the NetworkX function: `networkx.algorithms.traversal.breadth_first_search.generic_bfs_edges <https://github.com/networkx/networkx/blob/master/networkx/algorithms/traversal/breadth_first_search.py>`_ :cite:`2008:hss` The NetworkX function was in turn adapted from David Eppstein's breadth-first search function in `PADS`. :cite:`2015:eppstein` The edge labeling of this function is based on the treatment in *Introduction to Algorithms*. :cite:`2009:clrs` The feature to allow depth limits is based on Korf. :cite:`1985:korf` """ if len(graph) == 0: raise exception.Unfeasible("search is undefined for an empty graph") classified_edges: Set[str] = set() """The set of edges that have been classified so far by the breadth-first search into one of: tree edge, back edge, cross edge, or forward edge.""" predecessor: Dict[V_co, Optional[V_co]] = collections.defaultdict(lambda: None) """The predecessor is the parent vertex in the BFS tree. Root vertices have predecessor None. In addition, if a source vertex is specified, unreachable vertices also have predecessor None. """ search_trees: UnionFind[V_co] = UnionFind() """UnionFind data structure, where each disjoint set contains the vertices from a breadth-first search tree.""" seen: Set[V_co] = set() """A set of the vertices discovered so far during a breadth-first search.""" vertex_depth: Dict[V_co, float] = collections.defaultdict(lambda: INFINITY) vertices: Union[ValuesView[V_co], Set[V_co]] if source is None: vertices = graph.vertices() else: vertices = {graph[source]} if depth_limit is None: depth_limit = INFINITY for vertex in vertices: if vertex in seen: continue depth_now = 0 search_trees.make_set(vertex) # New BFS tree root. seen.add(vertex) vertex_depth[vertex] = depth_now children = get_adjacent_to_child(child=vertex, parent=None, reverse_graph=reverse_graph) queue: Deque[VertexSearchState[V_co]] = collections.deque() queue.append(VertexSearchState(vertex, children, depth_now)) yield vertex, vertex, Label.TREE_ROOT, Direction.PREORDER, depth_now # Explore the bread-first search tree rooted at `vertex`. while queue: parent_state: VertexSearchState[V_co] = queue.popleft() parent = parent_state.parent for child in parent_state.children: edge_label = edge_module.create_edge_label( parent, child, graph.is_directed()) if child not in seen: # Discovered new vertex? seen.add(child) if parent_state.depth is not None: depth_now = parent_state.depth + 1 vertex_depth[child] = depth_now predecessor[child] = parent search_trees.make_set(child) search_trees.union(parent, child) classified_edges.add(edge_label) yield parent, child, Label.TREE_EDGE, Direction.PREORDER, depth_now grandchildren = get_adjacent_to_child( child=child, parent=parent, reverse_graph=reverse_graph) if depth_now < (depth_limit - 1): queue.append( VertexSearchState(child, grandchildren, depth_now)) elif edge_label not in classified_edges: classified_edges.add(edge_label) classification = _classify_edge(parent, child, vertex_depth, search_trees) yield parent, child, classification, Direction.ALREADY_DISCOVERED, INFINITY if predecessor[parent]: yield ( cast(V_co, predecessor[parent]), parent, Label.TREE_EDGE, Direction.POSTORDER, vertex_depth[parent], ) else: yield parent, parent, Label.TREE_ROOT, Direction.POSTORDER, vertex_depth[ parent]
def dfs_labeled_edge_traversal( graph: GraphBase[V_co, E_co], source: Optional[VertexType] = None, depth_limit: Optional[int] = None, reverse_graph: bool = False, ) -> Iterator[Tuple[V_co, V_co, str, str]]: """Iterates over the labeled edges of a depth-first search traversal. Running time: :math:`O(m + n)` Note: If ``source`` is specified, then the traversal only includes the graph :term:`component <connected component>` containing the ``source`` vertex. Args: graph: The graph to search. source: Optional; The source vertex from which to begin the search. When ``source`` is specified, only the component reachable from the source is searched. Defaults to None. depth_limit: Optional; The depth limit of the search. Defaults to None (no limit). reverse_graph: Optional; For directed graphs, setting to True will yield a traversal as if the graph were reversed (i.e. the :term:`reverse graph <reverse>`). Defaults to False. Yields: Tuple[Vertex, Vertex, str, str]: An iterator over tuples of the form ``(parent, child, label, search_direction)`` where ``(parent, child)`` is the edge being explored in the depth-first search. The ``child`` vertex is found by iterating over the parent's adjacency list. The ``label`` is one of the strings: 1. "tree_root" - :math:`(u, u)`, where :math:`u` is the root vertex of a DFS tree. 2. "tree_edge" - edge :math:`(u, v)` is a tree edge if :math:`v` was first discovered by exploring edge :math:`(u, v)`. 3. "back_edge" - back edge :math:`(u, v)` connects vertex :math:`u` to ancestor :math:`v` in a depth-first tree. Per *Introduction to Algorithms* :cite:`2009:clrs`, :term:`self loops <loop>` are considered back edges. 4. "forward_edge" - non-tree edges :math:`(u, v)` connecting a vertex :math:`u` to a descendant :math:`v` in a depth-first tree. 5. "cross_edge" - All other edges, which may go between vertices in the same depth-first tree as long as one vertex is not an ancestor of the other, or they go between vertices in different depth-first trees. In an undirected graph, every edge is either a tree edge or a back edge. The ``search_direction`` is the direction of traversal and is one of the strings: 1. "preorder" - the traversal discovered new vertex `child` in the DFS. 2. "postorder" - the traversal finished visiting vertex `child` in the DFS. 3. "already_discovered" - the traversal found a non-tree edge connecting to a vertex that was already discovered. Example: The labels reveal the complete transcript of the depth-first search algorithm. >>> from pprint import pprint >>> import vertizee as vz >>> from vertizee.algorithms import dfs_labeled_edge_traversal >>> g = vz.DiGraph([(0, 1), (1, 2), (2, 1)]) >>> pprint(list(dfs_labeled_edge_traversal(g, source=0))) [(0, 0, 'tree_root', 'preorder'), (0, 1, 'tree_edge', 'preorder'), (1, 2, 'tree_edge', 'preorder'), (2, 1, 'back_edge', 'already_discovered'), (1, 2, 'tree_edge', 'postorder'), (0, 1, 'tree_edge', 'postorder'), (0, 0, 'tree_root', 'postorder')] See Also: * :class:`Direction <vertizee.algorithms.algo_utils.search_utils.Direction>` * :class:`Label <vertizee.algorithms.algo_utils.search_utils.Label>` * :class:`SearchResults <vertizee.algorithms.algo_utils.search_utils.SearchResults>` Note: This function is adapted from the NetworkX function: `networkx.algorithms.traversal.depth_first_search.dfs_labeled_edges <https://github.com/networkx/networkx/blob/master/networkx/algorithms/traversal/depth_first_search.py>`_ :cite:`2008:hss` The NetworkX function was in turn adapted from David Eppstein's depth-first search function in `PADS`. :cite:`2015:eppstein` The edge labeling of this function is based on the treatment in *Introduction to Algorithms*. :cite:`2009:clrs` The feature to allow depth limits is based on Korf. :cite:`1985:korf` """ if len(graph) == 0: raise exception.Unfeasible("search is undefined for an empty graph") classified_edges: Set[str] = set() """The set of edges that have been classified so far by the depth-first search into one of: tree edge, back edge, cross edge, or forward edge.""" vertex_color: VertexDict[str] = VertexDict() """A mapping from vertices to their color (white, gray, black) indicating the status of each vertex in the search process (i.e. undiscovered, in the process of being visited, or visit finished).""" vertex_discovery_order: VertexDict[int] = VertexDict() """A mapping from vertices to the order in which they were discovered by the depth-first search.""" for vertex in graph.vertices(): vertex_color[vertex] = WHITE if source is None: vertices: Union[ValuesView[V_co], Set[Any]] = graph.vertices() else: s: V_co = graph[source] vertices = {s} for vertex in vertices: vertex = cast( V_co, vertex) # Without cast, Pylance assumes type Union[V_co, Any] if vertex_color[vertex] != WHITE: # Already discovered? continue vertex_color[vertex] = GRAY # Mark discovered. vertex_discovery_order[vertex] = len(vertex_discovery_order) children = get_adjacent_to_child(child=vertex, parent=None, reverse_graph=reverse_graph) stack = [VertexSearchState(vertex, children, depth_limit)] yield vertex, vertex, Label.TREE_ROOT, Direction.PREORDER # Explore the depth-first search tree rooted at `vertex`. while stack: parent = stack[-1].parent children = stack[-1].children depth_now = stack[-1].depth try: child = next(children) except StopIteration: stack_frame = stack.pop() child = stack_frame.parent vertex_color[child] = BLACK # Finished visiting child. if stack: parent = stack[-1].parent yield parent, child, Label.TREE_EDGE, Direction.POSTORDER else: yield child, child, Label.TREE_ROOT, Direction.POSTORDER continue edge_label = edge_module.create_edge_label(parent, child, graph.is_directed()) if vertex_color[child] == WHITE: # Discovered new vertex? vertex_color[ child] = GRAY # Mark discovered and in the process of being visited. vertex_discovery_order[child] = len(vertex_discovery_order) classified_edges.add(edge_label) yield parent, child, Label.TREE_EDGE, Direction.PREORDER grandchildren = get_adjacent_to_child( child=child, parent=parent, reverse_graph=reverse_graph) if depth_now is None: stack.append( VertexSearchState(child, grandchildren, depth_now)) elif depth_now > 1: stack.append( VertexSearchState(child, grandchildren, depth_now - 1)) elif vertex_color[ child] == GRAY: # In the process of being visited? if edge_label not in classified_edges: classified_edges.add(edge_label) yield parent, child, Label.BACK_EDGE, Direction.ALREADY_DISCOVERED elif vertex_color[child] == BLACK: # Finished being visited? if edge_label not in classified_edges: classified_edges.add(edge_label) if vertex_discovery_order[parent] < vertex_discovery_order[ child]: yield parent, child, Label.FORWARD_EDGE, Direction.ALREADY_DISCOVERED else: yield parent, child, Label.CROSS_EDGE, Direction.ALREADY_DISCOVERED else: raise exception.AlgorithmError( f"vertex color '{vertex_color[child]}' of vertex '{child}' not recognized" )