test_planar_drawing.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import math
  2. import pytest
  3. import networkx as nx
  4. from networkx.algorithms.planar_drawing import triangulate_embedding
  5. def test_graph1():
  6. embedding_data = {0: [1, 2, 3], 1: [2, 0], 2: [3, 0, 1], 3: [2, 0]}
  7. check_embedding_data(embedding_data)
  8. def test_graph2():
  9. embedding_data = {
  10. 0: [8, 6],
  11. 1: [2, 6, 9],
  12. 2: [8, 1, 7, 9, 6, 4],
  13. 3: [9],
  14. 4: [2],
  15. 5: [6, 8],
  16. 6: [9, 1, 0, 5, 2],
  17. 7: [9, 2],
  18. 8: [0, 2, 5],
  19. 9: [1, 6, 2, 7, 3],
  20. }
  21. check_embedding_data(embedding_data)
  22. def test_circle_graph():
  23. embedding_data = {
  24. 0: [1, 9],
  25. 1: [0, 2],
  26. 2: [1, 3],
  27. 3: [2, 4],
  28. 4: [3, 5],
  29. 5: [4, 6],
  30. 6: [5, 7],
  31. 7: [6, 8],
  32. 8: [7, 9],
  33. 9: [8, 0],
  34. }
  35. check_embedding_data(embedding_data)
  36. def test_grid_graph():
  37. embedding_data = {
  38. (0, 1): [(0, 0), (1, 1), (0, 2)],
  39. (1, 2): [(1, 1), (2, 2), (0, 2)],
  40. (0, 0): [(0, 1), (1, 0)],
  41. (2, 1): [(2, 0), (2, 2), (1, 1)],
  42. (1, 1): [(2, 1), (1, 2), (0, 1), (1, 0)],
  43. (2, 0): [(1, 0), (2, 1)],
  44. (2, 2): [(1, 2), (2, 1)],
  45. (1, 0): [(0, 0), (2, 0), (1, 1)],
  46. (0, 2): [(1, 2), (0, 1)],
  47. }
  48. check_embedding_data(embedding_data)
  49. def test_one_node_graph():
  50. embedding_data = {0: []}
  51. check_embedding_data(embedding_data)
  52. def test_two_node_graph():
  53. embedding_data = {0: [1], 1: [0]}
  54. check_embedding_data(embedding_data)
  55. def test_three_node_graph():
  56. embedding_data = {0: [1, 2], 1: [0, 2], 2: [0, 1]}
  57. check_embedding_data(embedding_data)
  58. def test_multiple_component_graph1():
  59. embedding_data = {0: [], 1: []}
  60. check_embedding_data(embedding_data)
  61. def test_multiple_component_graph2():
  62. embedding_data = {0: [1, 2], 1: [0, 2], 2: [0, 1], 3: [4, 5], 4: [3, 5], 5: [3, 4]}
  63. check_embedding_data(embedding_data)
  64. def test_invalid_half_edge():
  65. with pytest.raises(nx.NetworkXException):
  66. embedding_data = {1: [2, 3, 4], 2: [1, 3, 4], 3: [1, 2, 4], 4: [1, 2, 3]}
  67. embedding = nx.PlanarEmbedding()
  68. embedding.set_data(embedding_data)
  69. nx.combinatorial_embedding_to_pos(embedding)
  70. def test_triangulate_embedding1():
  71. embedding = nx.PlanarEmbedding()
  72. embedding.add_node(1)
  73. expected_embedding = {1: []}
  74. check_triangulation(embedding, expected_embedding)
  75. def test_triangulate_embedding2():
  76. embedding = nx.PlanarEmbedding()
  77. embedding.connect_components(1, 2)
  78. expected_embedding = {1: [2], 2: [1]}
  79. check_triangulation(embedding, expected_embedding)
  80. def check_triangulation(embedding, expected_embedding):
  81. res_embedding, _ = triangulate_embedding(embedding, True)
  82. assert (
  83. res_embedding.get_data() == expected_embedding
  84. ), "Expected embedding incorrect"
  85. res_embedding, _ = triangulate_embedding(embedding, False)
  86. assert (
  87. res_embedding.get_data() == expected_embedding
  88. ), "Expected embedding incorrect"
  89. def check_embedding_data(embedding_data):
  90. """Checks that the planar embedding of the input is correct"""
  91. embedding = nx.PlanarEmbedding()
  92. embedding.set_data(embedding_data)
  93. pos_fully = nx.combinatorial_embedding_to_pos(embedding, False)
  94. msg = "Planar drawing does not conform to the embedding (fully " "triangulation)"
  95. assert planar_drawing_conforms_to_embedding(embedding, pos_fully), msg
  96. check_edge_intersections(embedding, pos_fully)
  97. pos_internally = nx.combinatorial_embedding_to_pos(embedding, True)
  98. msg = "Planar drawing does not conform to the embedding (internal " "triangulation)"
  99. assert planar_drawing_conforms_to_embedding(embedding, pos_internally), msg
  100. check_edge_intersections(embedding, pos_internally)
  101. def is_close(a, b, rel_tol=1e-09, abs_tol=0.0):
  102. # Check if float numbers are basically equal, for python >=3.5 there is
  103. # function for that in the standard library
  104. return abs(a - b) <= max(rel_tol * max(abs(a), abs(b)), abs_tol)
  105. def point_in_between(a, b, p):
  106. # checks if p is on the line between a and b
  107. x1, y1 = a
  108. x2, y2 = b
  109. px, py = p
  110. dist_1_2 = math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
  111. dist_1_p = math.sqrt((x1 - px) ** 2 + (y1 - py) ** 2)
  112. dist_2_p = math.sqrt((x2 - px) ** 2 + (y2 - py) ** 2)
  113. return is_close(dist_1_p + dist_2_p, dist_1_2)
  114. def check_edge_intersections(G, pos):
  115. """Check all edges in G for intersections.
  116. Raises an exception if an intersection is found.
  117. Parameters
  118. ----------
  119. G : NetworkX graph
  120. pos : dict
  121. Maps every node to a tuple (x, y) representing its position
  122. """
  123. for a, b in G.edges():
  124. for c, d in G.edges():
  125. # Check if end points are different
  126. if a != c and b != d and b != c and a != d:
  127. x1, y1 = pos[a]
  128. x2, y2 = pos[b]
  129. x3, y3 = pos[c]
  130. x4, y4 = pos[d]
  131. determinant = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
  132. if determinant != 0: # the lines are not parallel
  133. # calculate intersection point, see:
  134. # https://en.wikipedia.org/wiki/Line%E2%80%93line_intersection
  135. px = (x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (
  136. x3 * y4 - y3 * x4
  137. ) / determinant
  138. py = (x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (
  139. x3 * y4 - y3 * x4
  140. ) / determinant
  141. # Check if intersection lies between the points
  142. if point_in_between(pos[a], pos[b], (px, py)) and point_in_between(
  143. pos[c], pos[d], (px, py)
  144. ):
  145. msg = f"There is an intersection at {px},{py}"
  146. raise nx.NetworkXException(msg)
  147. # Check overlap
  148. msg = "A node lies on a edge connecting two other nodes"
  149. if (
  150. point_in_between(pos[a], pos[b], pos[c])
  151. or point_in_between(pos[a], pos[b], pos[d])
  152. or point_in_between(pos[c], pos[d], pos[a])
  153. or point_in_between(pos[c], pos[d], pos[b])
  154. ):
  155. raise nx.NetworkXException(msg)
  156. # No edge intersection found
  157. class Vector:
  158. """Compare vectors by their angle without loss of precision
  159. All vectors in direction [0, 1] are the smallest.
  160. The vectors grow in clockwise direction.
  161. """
  162. __slots__ = ["x", "y", "node", "quadrant"]
  163. def __init__(self, x, y, node):
  164. self.x = x
  165. self.y = y
  166. self.node = node
  167. if self.x >= 0 and self.y > 0:
  168. self.quadrant = 1
  169. elif self.x > 0 and self.y <= 0:
  170. self.quadrant = 2
  171. elif self.x <= 0 and self.y < 0:
  172. self.quadrant = 3
  173. else:
  174. self.quadrant = 4
  175. def __eq__(self, other):
  176. return self.quadrant == other.quadrant and self.x * other.y == self.y * other.x
  177. def __lt__(self, other):
  178. if self.quadrant < other.quadrant:
  179. return True
  180. elif self.quadrant > other.quadrant:
  181. return False
  182. else:
  183. return self.x * other.y < self.y * other.x
  184. def __ne__(self, other):
  185. return self != other
  186. def __le__(self, other):
  187. return not other < self
  188. def __gt__(self, other):
  189. return other < self
  190. def __ge__(self, other):
  191. return not self < other
  192. def planar_drawing_conforms_to_embedding(embedding, pos):
  193. """Checks if pos conforms to the planar embedding
  194. Returns true iff the neighbors are actually oriented in the orientation
  195. specified of the embedding
  196. """
  197. for v in embedding:
  198. nbr_vectors = []
  199. v_pos = pos[v]
  200. for nbr in embedding[v]:
  201. new_vector = Vector(pos[nbr][0] - v_pos[0], pos[nbr][1] - v_pos[1], nbr)
  202. nbr_vectors.append(new_vector)
  203. # Sort neighbors according to their phi angle
  204. nbr_vectors.sort()
  205. for idx, nbr_vector in enumerate(nbr_vectors):
  206. cw_vector = nbr_vectors[(idx + 1) % len(nbr_vectors)]
  207. ccw_vector = nbr_vectors[idx - 1]
  208. if (
  209. embedding[v][nbr_vector.node]["cw"] != cw_vector.node
  210. or embedding[v][nbr_vector.node]["ccw"] != ccw_vector.node
  211. ):
  212. return False
  213. if cw_vector.node != nbr_vector.node and cw_vector == nbr_vector:
  214. # Lines overlap
  215. return False
  216. if ccw_vector.node != nbr_vector.node and ccw_vector == nbr_vector:
  217. # Lines overlap
  218. return False
  219. return True