test_planarity.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import pytest
  2. import networkx as nx
  3. from networkx.algorithms.planarity import (
  4. check_planarity_recursive,
  5. get_counterexample,
  6. get_counterexample_recursive,
  7. )
  8. class TestLRPlanarity:
  9. """Nose Unit tests for the :mod:`networkx.algorithms.planarity` module.
  10. Tests three things:
  11. 1. Check that the result is correct
  12. (returns planar if and only if the graph is actually planar)
  13. 2. In case a counter example is returned: Check if it is correct
  14. 3. In case an embedding is returned: Check if its actually an embedding
  15. """
  16. @staticmethod
  17. def check_graph(G, is_planar=None):
  18. """Raises an exception if the lr_planarity check returns a wrong result
  19. Parameters
  20. ----------
  21. G : NetworkX graph
  22. is_planar : bool
  23. The expected result of the planarity check.
  24. If set to None only counter example or embedding are verified.
  25. """
  26. # obtain results of planarity check
  27. is_planar_lr, result = nx.check_planarity(G, True)
  28. is_planar_lr_rec, result_rec = check_planarity_recursive(G, True)
  29. if is_planar is not None:
  30. # set a message for the assert
  31. if is_planar:
  32. msg = "Wrong planarity check result. Should be planar."
  33. else:
  34. msg = "Wrong planarity check result. Should be non-planar."
  35. # check if the result is as expected
  36. assert is_planar == is_planar_lr, msg
  37. assert is_planar == is_planar_lr_rec, msg
  38. if is_planar_lr:
  39. # check embedding
  40. check_embedding(G, result)
  41. check_embedding(G, result_rec)
  42. else:
  43. # check counter example
  44. check_counterexample(G, result)
  45. check_counterexample(G, result_rec)
  46. def test_simple_planar_graph(self):
  47. e = [
  48. (1, 2),
  49. (2, 3),
  50. (3, 4),
  51. (4, 6),
  52. (6, 7),
  53. (7, 1),
  54. (1, 5),
  55. (5, 2),
  56. (2, 4),
  57. (4, 5),
  58. (5, 7),
  59. ]
  60. self.check_graph(nx.Graph(e), is_planar=True)
  61. def test_planar_with_selfloop(self):
  62. e = [
  63. (1, 1),
  64. (2, 2),
  65. (3, 3),
  66. (4, 4),
  67. (5, 5),
  68. (1, 2),
  69. (1, 3),
  70. (1, 5),
  71. (2, 5),
  72. (2, 4),
  73. (3, 4),
  74. (3, 5),
  75. (4, 5),
  76. ]
  77. self.check_graph(nx.Graph(e), is_planar=True)
  78. def test_k3_3(self):
  79. self.check_graph(nx.complete_bipartite_graph(3, 3), is_planar=False)
  80. def test_k5(self):
  81. self.check_graph(nx.complete_graph(5), is_planar=False)
  82. def test_multiple_components_planar(self):
  83. e = [(1, 2), (2, 3), (3, 1), (4, 5), (5, 6), (6, 4)]
  84. self.check_graph(nx.Graph(e), is_planar=True)
  85. def test_multiple_components_non_planar(self):
  86. G = nx.complete_graph(5)
  87. # add another planar component to the non planar component
  88. # G stays non planar
  89. G.add_edges_from([(6, 7), (7, 8), (8, 6)])
  90. self.check_graph(G, is_planar=False)
  91. def test_non_planar_with_selfloop(self):
  92. G = nx.complete_graph(5)
  93. # add self loops
  94. for i in range(5):
  95. G.add_edge(i, i)
  96. self.check_graph(G, is_planar=False)
  97. def test_non_planar1(self):
  98. # tests a graph that has no subgraph directly isomorph to K5 or K3_3
  99. e = [
  100. (1, 5),
  101. (1, 6),
  102. (1, 7),
  103. (2, 6),
  104. (2, 3),
  105. (3, 5),
  106. (3, 7),
  107. (4, 5),
  108. (4, 6),
  109. (4, 7),
  110. ]
  111. self.check_graph(nx.Graph(e), is_planar=False)
  112. def test_loop(self):
  113. # test a graph with a selfloop
  114. e = [(1, 2), (2, 2)]
  115. G = nx.Graph(e)
  116. self.check_graph(G, is_planar=True)
  117. def test_comp(self):
  118. # test multiple component graph
  119. e = [(1, 2), (3, 4)]
  120. G = nx.Graph(e)
  121. G.remove_edge(1, 2)
  122. self.check_graph(G, is_planar=True)
  123. def test_goldner_harary(self):
  124. # test goldner-harary graph (a maximal planar graph)
  125. e = [
  126. (1, 2),
  127. (1, 3),
  128. (1, 4),
  129. (1, 5),
  130. (1, 7),
  131. (1, 8),
  132. (1, 10),
  133. (1, 11),
  134. (2, 3),
  135. (2, 4),
  136. (2, 6),
  137. (2, 7),
  138. (2, 9),
  139. (2, 10),
  140. (2, 11),
  141. (3, 4),
  142. (4, 5),
  143. (4, 6),
  144. (4, 7),
  145. (5, 7),
  146. (6, 7),
  147. (7, 8),
  148. (7, 9),
  149. (7, 10),
  150. (8, 10),
  151. (9, 10),
  152. (10, 11),
  153. ]
  154. G = nx.Graph(e)
  155. self.check_graph(G, is_planar=True)
  156. def test_planar_multigraph(self):
  157. G = nx.MultiGraph([(1, 2), (1, 2), (1, 2), (1, 2), (2, 3), (3, 1)])
  158. self.check_graph(G, is_planar=True)
  159. def test_non_planar_multigraph(self):
  160. G = nx.MultiGraph(nx.complete_graph(5))
  161. G.add_edges_from([(1, 2)] * 5)
  162. self.check_graph(G, is_planar=False)
  163. def test_planar_digraph(self):
  164. G = nx.DiGraph([(1, 2), (2, 3), (2, 4), (4, 1), (4, 2), (1, 4), (3, 2)])
  165. self.check_graph(G, is_planar=True)
  166. def test_non_planar_digraph(self):
  167. G = nx.DiGraph(nx.complete_graph(5))
  168. G.remove_edge(1, 2)
  169. G.remove_edge(4, 1)
  170. self.check_graph(G, is_planar=False)
  171. def test_single_component(self):
  172. # Test a graph with only a single node
  173. G = nx.Graph()
  174. G.add_node(1)
  175. self.check_graph(G, is_planar=True)
  176. def test_graph1(self):
  177. G = nx.Graph(
  178. [
  179. (3, 10),
  180. (2, 13),
  181. (1, 13),
  182. (7, 11),
  183. (0, 8),
  184. (8, 13),
  185. (0, 2),
  186. (0, 7),
  187. (0, 10),
  188. (1, 7),
  189. ]
  190. )
  191. self.check_graph(G, is_planar=True)
  192. def test_graph2(self):
  193. G = nx.Graph(
  194. [
  195. (1, 2),
  196. (4, 13),
  197. (0, 13),
  198. (4, 5),
  199. (7, 10),
  200. (1, 7),
  201. (0, 3),
  202. (2, 6),
  203. (5, 6),
  204. (7, 13),
  205. (4, 8),
  206. (0, 8),
  207. (0, 9),
  208. (2, 13),
  209. (6, 7),
  210. (3, 6),
  211. (2, 8),
  212. ]
  213. )
  214. self.check_graph(G, is_planar=False)
  215. def test_graph3(self):
  216. G = nx.Graph(
  217. [
  218. (0, 7),
  219. (3, 11),
  220. (3, 4),
  221. (8, 9),
  222. (4, 11),
  223. (1, 7),
  224. (1, 13),
  225. (1, 11),
  226. (3, 5),
  227. (5, 7),
  228. (1, 3),
  229. (0, 4),
  230. (5, 11),
  231. (5, 13),
  232. ]
  233. )
  234. self.check_graph(G, is_planar=False)
  235. def test_counterexample_planar(self):
  236. with pytest.raises(nx.NetworkXException):
  237. # Try to get a counterexample of a planar graph
  238. G = nx.Graph()
  239. G.add_node(1)
  240. get_counterexample(G)
  241. def test_counterexample_planar_recursive(self):
  242. with pytest.raises(nx.NetworkXException):
  243. # Try to get a counterexample of a planar graph
  244. G = nx.Graph()
  245. G.add_node(1)
  246. get_counterexample_recursive(G)
  247. def check_embedding(G, embedding):
  248. """Raises an exception if the combinatorial embedding is not correct
  249. Parameters
  250. ----------
  251. G : NetworkX graph
  252. embedding : a dict mapping nodes to a list of edges
  253. This specifies the ordering of the outgoing edges from a node for
  254. a combinatorial embedding
  255. Notes
  256. -----
  257. Checks the following things:
  258. - The type of the embedding is correct
  259. - The nodes and edges match the original graph
  260. - Every half edge has its matching opposite half edge
  261. - No intersections of edges (checked by Euler's formula)
  262. """
  263. if not isinstance(embedding, nx.PlanarEmbedding):
  264. raise nx.NetworkXException("Bad embedding. Not of type nx.PlanarEmbedding")
  265. # Check structure
  266. embedding.check_structure()
  267. # Check that graphs are equivalent
  268. assert set(G.nodes) == set(
  269. embedding.nodes
  270. ), "Bad embedding. Nodes don't match the original graph."
  271. # Check that the edges are equal
  272. g_edges = set()
  273. for edge in G.edges:
  274. if edge[0] != edge[1]:
  275. g_edges.add((edge[0], edge[1]))
  276. g_edges.add((edge[1], edge[0]))
  277. assert g_edges == set(
  278. embedding.edges
  279. ), "Bad embedding. Edges don't match the original graph."
  280. def check_counterexample(G, sub_graph):
  281. """Raises an exception if the counterexample is wrong.
  282. Parameters
  283. ----------
  284. G : NetworkX graph
  285. subdivision_nodes : set
  286. A set of nodes inducing a subgraph as a counterexample
  287. """
  288. # 1. Create the sub graph
  289. sub_graph = nx.Graph(sub_graph)
  290. # 2. Remove self loops
  291. for u in sub_graph:
  292. if sub_graph.has_edge(u, u):
  293. sub_graph.remove_edge(u, u)
  294. # keep track of nodes we might need to contract
  295. contract = list(sub_graph)
  296. # 3. Contract Edges
  297. while len(contract) > 0:
  298. contract_node = contract.pop()
  299. if contract_node not in sub_graph:
  300. # Node was already contracted
  301. continue
  302. degree = sub_graph.degree[contract_node]
  303. # Check if we can remove the node
  304. if degree == 2:
  305. # Get the two neighbors
  306. neighbors = iter(sub_graph[contract_node])
  307. u = next(neighbors)
  308. v = next(neighbors)
  309. # Save nodes for later
  310. contract.append(u)
  311. contract.append(v)
  312. # Contract edge
  313. sub_graph.remove_node(contract_node)
  314. sub_graph.add_edge(u, v)
  315. # 4. Check for isomorphism with K5 or K3_3 graphs
  316. if len(sub_graph) == 5:
  317. if not nx.is_isomorphic(nx.complete_graph(5), sub_graph):
  318. raise nx.NetworkXException("Bad counter example.")
  319. elif len(sub_graph) == 6:
  320. if not nx.is_isomorphic(nx.complete_bipartite_graph(3, 3), sub_graph):
  321. raise nx.NetworkXException("Bad counter example.")
  322. else:
  323. raise nx.NetworkXException("Bad counter example.")
  324. class TestPlanarEmbeddingClass:
  325. def test_get_data(self):
  326. embedding = self.get_star_embedding(3)
  327. data = embedding.get_data()
  328. data_cmp = {0: [2, 1], 1: [0], 2: [0]}
  329. assert data == data_cmp
  330. def test_missing_edge_orientation(self):
  331. with pytest.raises(nx.NetworkXException):
  332. embedding = nx.PlanarEmbedding()
  333. embedding.add_edge(1, 2)
  334. embedding.add_edge(2, 1)
  335. # Invalid structure because the orientation of the edge was not set
  336. embedding.check_structure()
  337. def test_invalid_edge_orientation(self):
  338. with pytest.raises(nx.NetworkXException):
  339. embedding = nx.PlanarEmbedding()
  340. embedding.add_half_edge_first(1, 2)
  341. embedding.add_half_edge_first(2, 1)
  342. embedding.add_edge(1, 3)
  343. embedding.check_structure()
  344. def test_missing_half_edge(self):
  345. with pytest.raises(nx.NetworkXException):
  346. embedding = nx.PlanarEmbedding()
  347. embedding.add_half_edge_first(1, 2)
  348. # Invalid structure because other half edge is missing
  349. embedding.check_structure()
  350. def test_not_fulfilling_euler_formula(self):
  351. with pytest.raises(nx.NetworkXException):
  352. embedding = nx.PlanarEmbedding()
  353. for i in range(5):
  354. for j in range(5):
  355. if i != j:
  356. embedding.add_half_edge_first(i, j)
  357. embedding.check_structure()
  358. def test_missing_reference(self):
  359. with pytest.raises(nx.NetworkXException):
  360. embedding = nx.PlanarEmbedding()
  361. embedding.add_half_edge_cw(1, 2, 3)
  362. def test_connect_components(self):
  363. embedding = nx.PlanarEmbedding()
  364. embedding.connect_components(1, 2)
  365. def test_successful_face_traversal(self):
  366. embedding = nx.PlanarEmbedding()
  367. embedding.add_half_edge_first(1, 2)
  368. embedding.add_half_edge_first(2, 1)
  369. face = embedding.traverse_face(1, 2)
  370. assert face == [1, 2]
  371. def test_unsuccessful_face_traversal(self):
  372. with pytest.raises(nx.NetworkXException):
  373. embedding = nx.PlanarEmbedding()
  374. embedding.add_edge(1, 2, ccw=2, cw=3)
  375. embedding.add_edge(2, 1, ccw=1, cw=3)
  376. embedding.traverse_face(1, 2)
  377. @staticmethod
  378. def get_star_embedding(n):
  379. embedding = nx.PlanarEmbedding()
  380. for i in range(1, n):
  381. embedding.add_half_edge_first(0, i)
  382. embedding.add_half_edge_first(i, 0)
  383. return embedding