import pytest import networkx as nx from networkx.algorithms.planarity import get_counterexample from networkx.algorithms.planarity import get_counterexample_recursive from networkx.algorithms.planarity import check_planarity_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.OrderedGraph([ (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.OrderedGraph([ (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.OrderedGraph([ (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 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_get_data(self): embedding = self.get_star_embedding(3) data = embedding.get_data() data_cmp = {0: [2, 1], 1: [0], 2: [0]} assert data == data_cmp def test_missing_edge_orientation(self): with pytest.raises(nx.NetworkXException): embedding = nx.PlanarEmbedding() embedding.add_edge(1, 2) embedding.add_edge(2, 1) # Invalid structure because the orientation of the edge was not set embedding.check_structure() def test_invalid_edge_orientation(self): with pytest.raises(nx.NetworkXException): embedding = nx.PlanarEmbedding() embedding.add_half_edge_first(1, 2) embedding.add_half_edge_first(2, 1) embedding.add_edge(1, 3) embedding.check_structure() def test_missing_half_edge(self): with pytest.raises(nx.NetworkXException): embedding = nx.PlanarEmbedding() embedding.add_half_edge_first(1, 2) # Invalid structure because other half edge is missing embedding.check_structure() def test_not_fulfilling_euler_formula(self): with pytest.raises(nx.NetworkXException): embedding = nx.PlanarEmbedding() for i in range(5): for j in range(5): if i != j: embedding.add_half_edge_first(i, j) embedding.check_structure() def test_missing_reference(self): with pytest.raises(nx.NetworkXException): embedding = nx.PlanarEmbedding() embedding.add_half_edge_cw(1, 2, 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_first(1, 2) embedding.add_half_edge_first(2, 1) face = embedding.traverse_face(1, 2) assert face == [1, 2] def test_unsuccessful_face_traversal(self): with pytest.raises(nx.NetworkXException): embedding = nx.PlanarEmbedding() embedding.add_edge(1, 2, ccw=2, cw=3) embedding.add_edge(2, 1, ccw=1, cw=3) embedding.traverse_face(1, 2) @staticmethod def get_star_embedding(n): embedding = nx.PlanarEmbedding() for i in range(1, n): embedding.add_half_edge_first(0, i) embedding.add_half_edge_first(i, 0) return embedding