342 lines
12 KiB
Python
342 lines
12 KiB
Python
|
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
|