import pytest import networkx as nx from networkx.algorithms.planarity import ( check_planarity_recursive, get_counterexample, get_counterexample_recursive, ) class TestLRPlanarity: """Nose Unit tests for the :mod:`networkx.algorithms.planarity` module. Tests three things: 1. Check that the result is correct (returns planar if and only if the graph is actually planar) 2. In case a counter example is returned: Check if it is correct 3. In case an embedding is returned: Check if its actually an embedding """ @staticmethod def check_graph(G, is_planar=None): """Raises an exception if the lr_planarity check returns a wrong result Parameters ---------- G : NetworkX graph is_planar : bool The expected result of the planarity check. If set to None only counter example or embedding are verified. """ # obtain results of planarity check is_planar_lr, result = nx.check_planarity(G, True) is_planar_lr_rec, result_rec = check_planarity_recursive(G, True) if is_planar is not None: # set a message for the assert if is_planar: msg = "Wrong planarity check result. Should be planar." else: msg = "Wrong planarity check result. Should be non-planar." # check if the result is as expected assert is_planar == is_planar_lr, msg assert is_planar == is_planar_lr_rec, msg if is_planar_lr: # check embedding check_embedding(G, result) check_embedding(G, result_rec) else: # check counter example check_counterexample(G, result) check_counterexample(G, result_rec) def test_simple_planar_graph(self): e = [ (1, 2), (2, 3), (3, 4), (4, 6), (6, 7), (7, 1), (1, 5), (5, 2), (2, 4), (4, 5), (5, 7), ] self.check_graph(nx.Graph(e), is_planar=True) def test_planar_with_selfloop(self): e = [ (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (1, 2), (1, 3), (1, 5), (2, 5), (2, 4), (3, 4), (3, 5), (4, 5), ] self.check_graph(nx.Graph(e), is_planar=True) def test_k3_3(self): self.check_graph(nx.complete_bipartite_graph(3, 3), is_planar=False) def test_k5(self): self.check_graph(nx.complete_graph(5), is_planar=False) def test_multiple_components_planar(self): e = [(1, 2), (2, 3), (3, 1), (4, 5), (5, 6), (6, 4)] self.check_graph(nx.Graph(e), is_planar=True) def test_multiple_components_non_planar(self): G = nx.complete_graph(5) # add another planar component to the non planar component # G stays non planar G.add_edges_from([(6, 7), (7, 8), (8, 6)]) self.check_graph(G, is_planar=False) def test_non_planar_with_selfloop(self): G = nx.complete_graph(5) # add self loops for i in range(5): G.add_edge(i, i) self.check_graph(G, is_planar=False) def test_non_planar1(self): # tests a graph that has no subgraph directly isomorph to K5 or K3_3 e = [ (1, 5), (1, 6), (1, 7), (2, 6), (2, 3), (3, 5), (3, 7), (4, 5), (4, 6), (4, 7), ] self.check_graph(nx.Graph(e), is_planar=False) def test_loop(self): # test a graph with a selfloop e = [(1, 2), (2, 2)] G = nx.Graph(e) self.check_graph(G, is_planar=True) def test_comp(self): # test multiple component graph e = [(1, 2), (3, 4)] G = nx.Graph(e) G.remove_edge(1, 2) self.check_graph(G, is_planar=True) def test_goldner_harary(self): # test goldner-harary graph (a maximal planar graph) e = [ (1, 2), (1, 3), (1, 4), (1, 5), (1, 7), (1, 8), (1, 10), (1, 11), (2, 3), (2, 4), (2, 6), (2, 7), (2, 9), (2, 10), (2, 11), (3, 4), (4, 5), (4, 6), (4, 7), (5, 7), (6, 7), (7, 8), (7, 9), (7, 10), (8, 10), (9, 10), (10, 11), ] G = nx.Graph(e) self.check_graph(G, is_planar=True) def test_planar_multigraph(self): G = nx.MultiGraph([(1, 2), (1, 2), (1, 2), (1, 2), (2, 3), (3, 1)]) self.check_graph(G, is_planar=True) def test_non_planar_multigraph(self): G = nx.MultiGraph(nx.complete_graph(5)) G.add_edges_from([(1, 2)] * 5) self.check_graph(G, is_planar=False) def test_planar_digraph(self): G = nx.DiGraph([(1, 2), (2, 3), (2, 4), (4, 1), (4, 2), (1, 4), (3, 2)]) self.check_graph(G, is_planar=True) def test_non_planar_digraph(self): G = nx.DiGraph(nx.complete_graph(5)) G.remove_edge(1, 2) G.remove_edge(4, 1) self.check_graph(G, is_planar=False) def test_single_component(self): # Test a graph with only a single node G = nx.Graph() G.add_node(1) self.check_graph(G, is_planar=True) def test_graph1(self): G = nx.Graph( [ (3, 10), (2, 13), (1, 13), (7, 11), (0, 8), (8, 13), (0, 2), (0, 7), (0, 10), (1, 7), ] ) self.check_graph(G, is_planar=True) def test_graph2(self): G = nx.Graph( [ (1, 2), (4, 13), (0, 13), (4, 5), (7, 10), (1, 7), (0, 3), (2, 6), (5, 6), (7, 13), (4, 8), (0, 8), (0, 9), (2, 13), (6, 7), (3, 6), (2, 8), ] ) self.check_graph(G, is_planar=False) def test_graph3(self): G = nx.Graph( [ (0, 7), (3, 11), (3, 4), (8, 9), (4, 11), (1, 7), (1, 13), (1, 11), (3, 5), (5, 7), (1, 3), (0, 4), (5, 11), (5, 13), ] ) self.check_graph(G, is_planar=False) def test_counterexample_planar(self): with pytest.raises(nx.NetworkXException): # Try to get a counterexample of a planar graph G = nx.Graph() G.add_node(1) get_counterexample(G) def test_counterexample_planar_recursive(self): with pytest.raises(nx.NetworkXException): # Try to get a counterexample of a planar graph G = nx.Graph() G.add_node(1) get_counterexample_recursive(G) def test_edge_removal_from_planar_embedding(self): # PlanarEmbedding.check_structure() must succeed after edge removal edges = ((0, 1), (1, 2), (2, 3), (3, 4), (4, 0), (0, 2), (0, 3)) G = nx.Graph(edges) cert, P = nx.check_planarity(G) assert cert is True P.remove_edge(0, 2) self.check_graph(P, is_planar=True) P.add_half_edge_ccw(1, 3, 2) P.add_half_edge_cw(3, 1, 2) self.check_graph(P, is_planar=True) P.remove_edges_from(((0, 3), (1, 3))) self.check_graph(P, is_planar=True) def check_embedding(G, embedding): """Raises an exception if the combinatorial embedding is not correct Parameters ---------- G : NetworkX graph embedding : a dict mapping nodes to a list of edges This specifies the ordering of the outgoing edges from a node for a combinatorial embedding Notes ----- Checks the following things: - The type of the embedding is correct - The nodes and edges match the original graph - Every half edge has its matching opposite half edge - No intersections of edges (checked by Euler's formula) """ if not isinstance(embedding, nx.PlanarEmbedding): raise nx.NetworkXException("Bad embedding. Not of type nx.PlanarEmbedding") # Check structure embedding.check_structure() # Check that graphs are equivalent assert set(G.nodes) == set( embedding.nodes ), "Bad embedding. Nodes don't match the original graph." # Check that the edges are equal g_edges = set() for edge in G.edges: if edge[0] != edge[1]: g_edges.add((edge[0], edge[1])) g_edges.add((edge[1], edge[0])) assert g_edges == set( embedding.edges ), "Bad embedding. Edges don't match the original graph." def check_counterexample(G, sub_graph): """Raises an exception if the counterexample is wrong. Parameters ---------- G : NetworkX graph subdivision_nodes : set A set of nodes inducing a subgraph as a counterexample """ # 1. Create the sub graph sub_graph = nx.Graph(sub_graph) # 2. Remove self loops for u in sub_graph: if sub_graph.has_edge(u, u): sub_graph.remove_edge(u, u) # keep track of nodes we might need to contract contract = list(sub_graph) # 3. Contract Edges while len(contract) > 0: contract_node = contract.pop() if contract_node not in sub_graph: # Node was already contracted continue degree = sub_graph.degree[contract_node] # Check if we can remove the node if degree == 2: # Get the two neighbors neighbors = iter(sub_graph[contract_node]) u = next(neighbors) v = next(neighbors) # Save nodes for later contract.append(u) contract.append(v) # Contract edge sub_graph.remove_node(contract_node) sub_graph.add_edge(u, v) # 4. Check for isomorphism with K5 or K3_3 graphs if len(sub_graph) == 5: if not nx.is_isomorphic(nx.complete_graph(5), sub_graph): raise nx.NetworkXException("Bad counter example.") elif len(sub_graph) == 6: if not nx.is_isomorphic(nx.complete_bipartite_graph(3, 3), sub_graph): raise nx.NetworkXException("Bad counter example.") else: raise nx.NetworkXException("Bad counter example.") class TestPlanarEmbeddingClass: def test_add_half_edge(self): embedding = nx.PlanarEmbedding() embedding.add_half_edge(0, 1) with pytest.raises( nx.NetworkXException, match="Invalid clockwise reference node." ): embedding.add_half_edge(0, 2, cw=3) with pytest.raises( nx.NetworkXException, match="Invalid counterclockwise reference node." ): embedding.add_half_edge(0, 2, ccw=3) with pytest.raises( nx.NetworkXException, match="Only one of cw/ccw can be specified." ): embedding.add_half_edge(0, 2, cw=1, ccw=1) with pytest.raises( nx.NetworkXException, match=( r"Node already has out-half-edge\(s\), either" " cw or ccw reference node required." ), ): embedding.add_half_edge(0, 2) # these should work embedding.add_half_edge(0, 2, cw=1) embedding.add_half_edge(0, 3, ccw=1) assert sorted(embedding.edges(data=True)) == [ (0, 1, {"ccw": 2, "cw": 3}), (0, 2, {"cw": 1, "ccw": 3}), (0, 3, {"cw": 2, "ccw": 1}), ] def test_get_data(self): embedding = self.get_star_embedding(4) data = embedding.get_data() data_cmp = {0: [3, 2, 1], 1: [0], 2: [0], 3: [0]} assert data == data_cmp def test_edge_removal(self): embedding = nx.PlanarEmbedding() embedding.set_data( { 1: [2, 5, 7], 2: [1, 3, 4, 5], 3: [2, 4], 4: [3, 6, 5, 2], 5: [7, 1, 2, 4], 6: [4, 7], 7: [6, 1, 5], } ) # remove_edges_from() calls remove_edge(), so both are tested here embedding.remove_edges_from(((5, 4), (1, 5))) embedding.check_structure() embedding_expected = nx.PlanarEmbedding() embedding_expected.set_data( { 1: [2, 7], 2: [1, 3, 4, 5], 3: [2, 4], 4: [3, 6, 2], 5: [7, 2], 6: [4, 7], 7: [6, 1, 5], } ) assert nx.utils.graphs_equal(embedding, embedding_expected) def test_missing_edge_orientation(self): embedding = nx.PlanarEmbedding({1: {2: {}}, 2: {1: {}}}) with pytest.raises(nx.NetworkXException): # Invalid structure because the orientation of the edge was not set embedding.check_structure() def test_invalid_edge_orientation(self): embedding = nx.PlanarEmbedding( { 1: {2: {"cw": 2, "ccw": 2}}, 2: {1: {"cw": 1, "ccw": 1}}, 1: {3: {}}, 3: {1: {}}, } ) with pytest.raises(nx.NetworkXException): embedding.check_structure() def test_missing_half_edge(self): embedding = nx.PlanarEmbedding() embedding.add_half_edge(1, 2) with pytest.raises(nx.NetworkXException): # Invalid structure because other half edge is missing embedding.check_structure() def test_not_fulfilling_euler_formula(self): embedding = nx.PlanarEmbedding() for i in range(5): ref = None for j in range(5): if i != j: embedding.add_half_edge(i, j, cw=ref) ref = j with pytest.raises(nx.NetworkXException): embedding.check_structure() def test_missing_reference(self): embedding = nx.PlanarEmbedding() with pytest.raises(nx.NetworkXException, match="Invalid reference node."): embedding.add_half_edge(1, 2, ccw=3) def test_connect_components(self): embedding = nx.PlanarEmbedding() embedding.connect_components(1, 2) def test_successful_face_traversal(self): embedding = nx.PlanarEmbedding() embedding.add_half_edge(1, 2) embedding.add_half_edge(2, 1) face = embedding.traverse_face(1, 2) assert face == [1, 2] def test_unsuccessful_face_traversal(self): embedding = nx.PlanarEmbedding( {1: {2: {"cw": 3, "ccw": 2}}, 2: {1: {"cw": 3, "ccw": 1}}} ) with pytest.raises(nx.NetworkXException): embedding.traverse_face(1, 2) def test_forbidden_methods(self): embedding = nx.PlanarEmbedding() embedding.add_node(42) # no exception embedding.add_nodes_from([(23, 24)]) # no exception with pytest.raises(NotImplementedError): embedding.add_edge(1, 3) with pytest.raises(NotImplementedError): embedding.add_edges_from([(0, 2), (1, 4)]) with pytest.raises(NotImplementedError): embedding.add_weighted_edges_from([(0, 2, 350), (1, 4, 125)]) @staticmethod def get_star_embedding(n): embedding = nx.PlanarEmbedding() ref = None for i in range(1, n): embedding.add_half_edge(0, i, cw=ref) ref = i embedding.add_half_edge(i, 0) return embedding