Example #1
0
 def test_child_non_critical(self):
     # tests D consisting of non-critical path.
     # Source cover is expected to be empty
     D: nx.DiGraph = nx.DiGraph()
     D.add_edge(0, 1)
     cover = source_cover(D, set())
     assert_set_equal(cover, set())
Example #2
0
 def test_tree_leafs_critical(self):
     # tests D to be a tree, only leafs are critical
     # Source cover is expected to be {0} - the root
     D: nx.DiGraph = nx.balanced_tree(2, 5, nx.DiGraph())
     critical: Set = {node for node in D.nodes if D.out_degree(node) == 0}
     cover: Set = source_cover(D, critical)
     assert_set_equal(cover, {0})
Example #3
0
 def test_no_critical(self):
     # tests D consisting of single directed edge
     # Source cover expected to not cover it
     D: nx.DiGraph = nx.DiGraph()
     D.add_node(0)
     cover = source_cover(D, set())
     assert_set_equal(cover, set())
Example #4
0
 def test_child_critical(self):
     # tests D consisting of non-critical 0 and path to critical 4.
     # Source cover is expected to be {0}
     D: nx.DiGraph = nx.path_graph(5, nx.DiGraph())
     D.add_edge(0, 1)
     cover = source_cover(D, {4})
     assert_set_equal(cover, {0})
Example #5
0
 def test_single_critical(self):
     # tests D consisting of single critical vertex
     # Source cover expected to cover it
     D: nx.DiGraph = nx.DiGraph()
     D.add_node(0)
     cover = source_cover(D, {0})
     assert_set_equal(cover, {0})
Example #6
0
 def test_tree_all_critical(self):
     # tests D to be a tree, all vertices are critical
     # Source cover is expected to be the root
     # tests correct functionality of deletion of vertices
     # reachable from a critical vertex
     D: nx.DiGraph = nx.balanced_tree(2, 5, nx.DiGraph())
     cover: Set = source_cover(D, set(D.nodes))
     assert_set_equal(cover, {0})
Example #7
0
 def test_tree_leafs_critical_and_isolated(self):
     # tests D to be a tree, only leafs are critical
     # and set of isolated
     # Source cover is expected to be  root(D1) | isolated
     D1: nx.DiGraph = nx.balanced_tree(2, 5, nx.DiGraph())
     D2: nx.DiGraph = nx.DiGraph()
     D2.add_nodes_from({i for i in range(100)})
     D = nx.compose(D1, D2)
     critical: Set = {node for node in D.nodes if D.in_degree(node) == 0}
     cover: Set = source_cover(D, critical)
     assert_set_equal(cover, critical)
Example #8
0
 def test_two_disjoint_trees_leafs_critical(self):
     # tests two trees, only leafs are critical
     # Source cover is expected to be {0} - the root
     D1: nx.DiGraph = nx.balanced_tree(2, 5, nx.DiGraph())
     D2: nx.DiGraph = nx.balanced_tree(2, 6, nx.DiGraph())
     D = nx.compose(D1, D2)
     critical: Set = {node for node in D.nodes if D.out_degree(node) == 0}
     cover: Set = source_cover(D, critical)
     assert_set_equal(cover,
                      {node
                       for node in D.nodes
                       if D.in_degree(node) == 0})  # Only roots form cover
Example #9
0
 def test_tree_multiple_sources_leafs_critical(self):
     # tests D to be a tree, only leafs are critical
     # There are multiple sources in the graphs, including two
     # covering all the vertices. However, the optimal solution is
     # still of size 1. tests if it correctly does not consider these
     # additional source.
     D: nx.DiGraph = nx.balanced_tree(2, 5, nx.DiGraph())
     D.add_edges_from({(65, 1), (65, 2), (66, 15), (65, 29), (66, 38),
                       (67, 58)})
     critical = {node for node in D.nodes if D.out_degree(node) == 0}
     cover = source_cover(D, critical)
     assert_true(cover == {0} or cover == {65})  # Only two correct options
Example #10
0
 def test_tree_all_critical_decoy(self):
     # tests D to be a tree, all vertices are critical
     # Source cover is expected to be the root
     # tests correct functionality of deletion of vertices
     # reachable from a critical vertex
     # There is also one "decoy" - a non-critical vertex 100
     # that covers two leafs. However, after the deletion,
     # the vertex 100 does not cover any critical vertex
     # any more. Hence, it should not be in the cover.
     D: nx.DiGraph = nx.balanced_tree(2, 5, nx.DiGraph())
     D.add_edges_from({(100, 50), (100, 51)})
     cover: Set = source_cover(D, set(D.nodes) - {100})
     assert_set_equal(cover, {0})
Example #11
0
 def test_not_covering_non_critical(self):
     # Test for unbounded approximation factor as discussed
     # in section 2.1 the thesis. The optimal solution is 1. We use the
     # minimal solution. Expected source cover is of length 1 if it works correctly.
     D: nx.DiGraph = nx.DiGraph()
     D.add_nodes_from([
         "s_1,2", "s_3,4", "s_5,6", "t_1", "t_2,3", "t_4,5", "t_6,7",
         "t_8,9"
     ])
     D.add_edges_from({("s_1,2", "t_1"), ("s_3,4", "t_1"), ("s_5,6", "t_1"),
                       ("s_3,4", "t_2,3"), ("s_3,4", "t_4,5"),
                       ("s_5,6", "t_6,7"), ("s_5,6", "t_8,9")})
     cover = source_cover(D, {"t_1"})
     assert_true(len(cover) == 1)
Example #12
0
 def test_tree_multiple_sources_tree_critical(self):
     # tests D to be a tree, whole tree is critical
     # There are multiple sources in the graphs, including two
     # covering all the vertices. However, the optimal solution is
     # still of size 1. In addition to test_tree_multiple_sources_leafs_critical(),
     # this test also tests correctness of the deletion procedure.
     # If it proceeds correctly, it does not consider the newly added sources
     # as critical sinks, so the solution is still of size 1.
     D: nx.DiGraph = nx.balanced_tree(2, 5, nx.DiGraph())
     critical = set(D.nodes)
     D.add_edges_from({(65, 1), (65, 2), (66, 15), (65, 29), (66, 38),
                       (67, 58)})
     cover = source_cover(D, critical)
     assert_true(
         cover == {0}
     )  # All others are reachable from a critical vertex, so this is only viable option.
Example #13
0
 def test_tree_leafs_non_critical(self):
     # tests D to be a tree, no vertices are critcal
     # Source cover is expected to be empty
     D: nx.DiGraph = nx.balanced_tree(2, 5, nx.DiGraph())
     cover: Set = source_cover(D, set())
     assert_set_equal(cover, set())
def bipartite_matching_augmentation(G: nx.Graph, A: Set, M: Dict = None):
    """Returns a set of edges A such that G(V, E + A) is strongly connected.

        Parameters
        ----------
        G : NetworkX Graph
           A bipartite graph G = (A + B, E), where G can be augmented, that is |A + B| >= 4.
           G must also admit a perfect matching.
        A : Set
            A bipartition of G, where |A| = |A + B| / 2
        M: Dict = None
            A perfect bipartite matching of G, for each edge {a, b} in M holds M[a] = b, M[b] = a.
            If M is not given, it will be computed using eppstein_matching(G, A)

        Returns
        -------
        L : Set
           Set of edges from E(G) - M such that G admits a perfect matching even after a single arbitrary
           edge is removed. Edges are in form of (a, b), where a is from A and b from B

        Raises
        ------
        NetworkX.NotImplemented:
            If G is directed or a multigraph.

        Notes
        -----
        Implementation is based on BINDEWALD, Viktor; HOMMELSHEIM, Felix; MÜHLENTHALER, Moritz; SCHAUDT, Oliver.
        How to Secure Matchings Against Edge Failures. CoRR. 2018, vol. abs/1805.01299. Available from arXiv:
        1805.01299

        References
        ----------
        [1]  BINDEWALD, Viktor; HOMMELSHEIM, Felix; MÜHLENTHALER, Moritz; SCHAUDT, Oliver.
        How to Secure Matchings Against Edge Failures. CoRR. 2018, vol. abs/1805.01299. Available from arXiv:
        1805.01299

        """

    if len(
            A
    ) <= 1:  # Graph consisting of only one vertex at each bipartition cannot be augmented.
        raise bipartite_ghraph_not_augmentable_exception(
            "G cannot be augmented.")

    if M is None:  # User can specify her own matching for speed-up
        M: Dict = nx.algorithms.bipartite.eppstein_matching(G, A)

    D: nx.DiGraph = nx.DiGraph()

    for u in M:  # Construction of D, iterate over all keys in M
        if u in A:  # Add all edges from A to D
            w = M[u]
            D.add_node(u)

            outgoing: int = 0
            for uPrime in G.neighbors(w):  # Construct edges of D
                if uPrime != u:
                    D.add_edge(u, uPrime)

    D_condensation: nx.DiGraph = nx.algorithms.components.condensation(
        D)  # Condensation - acyclic digraph

    X: Set = set(
    )  # A set of vertices of D_condensation corresponding to trivial strong components of D
    isolated: Set = set()  # Set of isolated vertices
    sources: Set = set(
    )  # Set of vertices that are not isolated and have no ingoing arc
    sinks: Set = set(
    )  # Set of vertices that are not isolated and have no outgoing arc

    # We do not use function sources, sinks, isolated for performance reasons
    # Because we would either have to loop twice or check one more condition in the loop
    # if we were to modify the function.
    for vertex in D_condensation.nodes:
        inDegree: int = D_condensation.in_degree(vertex)
        outDegree: int = D_condensation.out_degree(vertex)

        if len(D_condensation.nodes[vertex]['members']) == 1:
            # Each trivial strong component is incident to some critical edge
            X.add(vertex)
        if inDegree == 0 and outDegree == 0:
            # Isolated: neither ingoing nor outgoing arc
            isolated.add(vertex)
        elif inDegree == 0:
            # Source: no ingoing arc and not isolated
            sources.add(vertex)
            # Sink: no outgoing arc and not isolated
        elif outDegree == 0:
            sinks.add(vertex)

    A_0 = D_condensation
    A_1 = D_condensation.reverse(copy=False)

    if len(
            X
    ) == 0:  # If there is no trivial strong component, G admits a perfect matching after edge removal
        return set()

    # Use source_cover to choose ln(n) approximation of choice of sources that cover all sinks in C_0, resp. C_1
    C_0 = source_cover(A_0, X, (sources, sinks, isolated))
    C_1 = source_cover(A_1, X, (sinks, sources, isolated))

    # We now determine vertices that lie either on C_1X paths or XC_2 paths
    # Vertices on C_1X paths are those visited when traveling from C_1 to X on
    # D_condensation and from X to C_1 on D_condensation_reverse
    CX_vertices = set()
    XC_vertices = set()
    D_condensation_reverse = D_condensation.reverse(copy=False)

    # Now, specify arguments for the fast DFS traversal
    def action_on_vertex_CX(current_vertex):
        CX_vertices.add(
            current_vertex)  # We have reached it from C, so add it in
        return True

    def action_on_neighbor_CX(neighbor, current_vertex):
        return neighbor not in CX_vertices

    def action_on_vertex_XC(current_vertex):
        XC_vertices.add(current_vertex)
        return True

    def action_on_neighbor_XC(neighbor, current_vertex):
        return neighbor not in XC_vertices

    for source in C_0:
        # Reachable from C_0 (search for X)
        fast_traversal(D_condensation, source, action_on_vertex_CX,
                       action_on_neighbor_CX)

    for critical in X:
        # Reachable from X (search for C_2)
        fast_traversal(D_condensation, critical, action_on_vertex_CX,
                       action_on_neighbor_CX)
        # Reachable from X (search for C_1)
        fast_traversal(D_condensation_reverse, critical, action_on_vertex_XC,
                       action_on_neighbor_XC)

    for source in C_1:
        # Reachable from C_2 (search for X)
        fast_traversal(D_condensation_reverse, source, action_on_vertex_XC,
                       action_on_neighbor_XC)

    D_hat_vertices = CX_vertices & XC_vertices  # Intersection

    # Marginal case when single vertex cannot be connected to form non-trivial strongly connected component.
    # We need to add another arbitrary vertex, which always exists as |V(D)| is guaranteed to be > 2 and contains
    # at least one trivial strong component.
    if len(D_hat_vertices) == 1:
        vert = next(iter(D_hat_vertices))
        D_hat_vertices.add(next(iter(set(D_condensation.nodes) - {vert})))

    #  Update sources, sinks, isolated as intersection with D_hat_vertices
    sources &= D_hat_vertices
    sinks &= D_hat_vertices
    isolated &= D_hat_vertices

    D_hat = nx.classes.function.induced_subgraph(D_condensation,
                                                 D_hat_vertices)
    L_star: Set = eswaran_tarjan(D_hat,
                                 is_condensation=True,
                                 sourcesSinksIsolated=(sources, sinks,
                                                       isolated))

    # Map vertices from L to vertices of L*
    return set(
        map(
            lambda e:
            (next(iter(D_condensation.nodes[e[1]]['members'])), M[next(
                iter(D_condensation.nodes[e[0]]['members']))]), L_star))