Beispiel #1
0
    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)"
Beispiel #2
0
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
Beispiel #3
0
    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
Beispiel #4
0
    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]
Beispiel #5
0
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"
                )