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())
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})
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())
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})
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})
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})
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)
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
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
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})
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)
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.
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))