planar_drawing.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464
  1. from collections import defaultdict
  2. import networkx as nx
  3. __all__ = ["combinatorial_embedding_to_pos"]
  4. def combinatorial_embedding_to_pos(embedding, fully_triangulate=False):
  5. """Assigns every node a (x, y) position based on the given embedding
  6. The algorithm iteratively inserts nodes of the input graph in a certain
  7. order and rearranges previously inserted nodes so that the planar drawing
  8. stays valid. This is done efficiently by only maintaining relative
  9. positions during the node placements and calculating the absolute positions
  10. at the end. For more information see [1]_.
  11. Parameters
  12. ----------
  13. embedding : nx.PlanarEmbedding
  14. This defines the order of the edges
  15. fully_triangulate : bool
  16. If set to True the algorithm adds edges to a copy of the input
  17. embedding and makes it chordal.
  18. Returns
  19. -------
  20. pos : dict
  21. Maps each node to a tuple that defines the (x, y) position
  22. References
  23. ----------
  24. .. [1] M. Chrobak and T.H. Payne:
  25. A Linear-time Algorithm for Drawing a Planar Graph on a Grid 1989
  26. http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.6677
  27. """
  28. if len(embedding.nodes()) < 4:
  29. # Position the node in any triangle
  30. default_positions = [(0, 0), (2, 0), (1, 1)]
  31. pos = {}
  32. for i, v in enumerate(embedding.nodes()):
  33. pos[v] = default_positions[i]
  34. return pos
  35. embedding, outer_face = triangulate_embedding(embedding, fully_triangulate)
  36. # The following dicts map a node to another node
  37. # If a node is not in the key set it means that the node is not yet in G_k
  38. # If a node maps to None then the corresponding subtree does not exist
  39. left_t_child = {}
  40. right_t_child = {}
  41. # The following dicts map a node to an integer
  42. delta_x = {}
  43. y_coordinate = {}
  44. node_list = get_canonical_ordering(embedding, outer_face)
  45. # 1. Phase: Compute relative positions
  46. # Initialization
  47. v1, v2, v3 = node_list[0][0], node_list[1][0], node_list[2][0]
  48. delta_x[v1] = 0
  49. y_coordinate[v1] = 0
  50. right_t_child[v1] = v3
  51. left_t_child[v1] = None
  52. delta_x[v2] = 1
  53. y_coordinate[v2] = 0
  54. right_t_child[v2] = None
  55. left_t_child[v2] = None
  56. delta_x[v3] = 1
  57. y_coordinate[v3] = 1
  58. right_t_child[v3] = v2
  59. left_t_child[v3] = None
  60. for k in range(3, len(node_list)):
  61. vk, contour_neighbors = node_list[k]
  62. wp = contour_neighbors[0]
  63. wp1 = contour_neighbors[1]
  64. wq = contour_neighbors[-1]
  65. wq1 = contour_neighbors[-2]
  66. adds_mult_tri = len(contour_neighbors) > 2
  67. # Stretch gaps:
  68. delta_x[wp1] += 1
  69. delta_x[wq] += 1
  70. delta_x_wp_wq = sum(delta_x[x] for x in contour_neighbors[1:])
  71. # Adjust offsets
  72. delta_x[vk] = (-y_coordinate[wp] + delta_x_wp_wq + y_coordinate[wq]) // 2
  73. y_coordinate[vk] = (y_coordinate[wp] + delta_x_wp_wq + y_coordinate[wq]) // 2
  74. delta_x[wq] = delta_x_wp_wq - delta_x[vk]
  75. if adds_mult_tri:
  76. delta_x[wp1] -= delta_x[vk]
  77. # Install v_k:
  78. right_t_child[wp] = vk
  79. right_t_child[vk] = wq
  80. if adds_mult_tri:
  81. left_t_child[vk] = wp1
  82. right_t_child[wq1] = None
  83. else:
  84. left_t_child[vk] = None
  85. # 2. Phase: Set absolute positions
  86. pos = {}
  87. pos[v1] = (0, y_coordinate[v1])
  88. remaining_nodes = [v1]
  89. while remaining_nodes:
  90. parent_node = remaining_nodes.pop()
  91. # Calculate position for left child
  92. set_position(
  93. parent_node, left_t_child, remaining_nodes, delta_x, y_coordinate, pos
  94. )
  95. # Calculate position for right child
  96. set_position(
  97. parent_node, right_t_child, remaining_nodes, delta_x, y_coordinate, pos
  98. )
  99. return pos
  100. def set_position(parent, tree, remaining_nodes, delta_x, y_coordinate, pos):
  101. """Helper method to calculate the absolute position of nodes."""
  102. child = tree[parent]
  103. parent_node_x = pos[parent][0]
  104. if child is not None:
  105. # Calculate pos of child
  106. child_x = parent_node_x + delta_x[child]
  107. pos[child] = (child_x, y_coordinate[child])
  108. # Remember to calculate pos of its children
  109. remaining_nodes.append(child)
  110. def get_canonical_ordering(embedding, outer_face):
  111. """Returns a canonical ordering of the nodes
  112. The canonical ordering of nodes (v1, ..., vn) must fulfill the following
  113. conditions:
  114. (See Lemma 1 in [2]_)
  115. - For the subgraph G_k of the input graph induced by v1, ..., vk it holds:
  116. - 2-connected
  117. - internally triangulated
  118. - the edge (v1, v2) is part of the outer face
  119. - For a node v(k+1) the following holds:
  120. - The node v(k+1) is part of the outer face of G_k
  121. - It has at least two neighbors in G_k
  122. - All neighbors of v(k+1) in G_k lie consecutively on the outer face of
  123. G_k (excluding the edge (v1, v2)).
  124. The algorithm used here starts with G_n (containing all nodes). It first
  125. selects the nodes v1 and v2. And then tries to find the order of the other
  126. nodes by checking which node can be removed in order to fulfill the
  127. conditions mentioned above. This is done by calculating the number of
  128. chords of nodes on the outer face. For more information see [1]_.
  129. Parameters
  130. ----------
  131. embedding : nx.PlanarEmbedding
  132. The embedding must be triangulated
  133. outer_face : list
  134. The nodes on the outer face of the graph
  135. Returns
  136. -------
  137. ordering : list
  138. A list of tuples `(vk, wp_wq)`. Here `vk` is the node at this position
  139. in the canonical ordering. The element `wp_wq` is a list of nodes that
  140. make up the outer face of G_k.
  141. References
  142. ----------
  143. .. [1] Steven Chaplick.
  144. Canonical Orders of Planar Graphs and (some of) Their Applications 2015
  145. https://wuecampus2.uni-wuerzburg.de/moodle/pluginfile.php/545727/mod_resource/content/0/vg-ss15-vl03-canonical-orders-druckversion.pdf
  146. .. [2] M. Chrobak and T.H. Payne:
  147. A Linear-time Algorithm for Drawing a Planar Graph on a Grid 1989
  148. http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.51.6677
  149. """
  150. v1 = outer_face[0]
  151. v2 = outer_face[1]
  152. chords = defaultdict(int) # Maps nodes to the number of their chords
  153. marked_nodes = set()
  154. ready_to_pick = set(outer_face)
  155. # Initialize outer_face_ccw_nbr (do not include v1 -> v2)
  156. outer_face_ccw_nbr = {}
  157. prev_nbr = v2
  158. for idx in range(2, len(outer_face)):
  159. outer_face_ccw_nbr[prev_nbr] = outer_face[idx]
  160. prev_nbr = outer_face[idx]
  161. outer_face_ccw_nbr[prev_nbr] = v1
  162. # Initialize outer_face_cw_nbr (do not include v2 -> v1)
  163. outer_face_cw_nbr = {}
  164. prev_nbr = v1
  165. for idx in range(len(outer_face) - 1, 0, -1):
  166. outer_face_cw_nbr[prev_nbr] = outer_face[idx]
  167. prev_nbr = outer_face[idx]
  168. def is_outer_face_nbr(x, y):
  169. if x not in outer_face_ccw_nbr:
  170. return outer_face_cw_nbr[x] == y
  171. if x not in outer_face_cw_nbr:
  172. return outer_face_ccw_nbr[x] == y
  173. return outer_face_ccw_nbr[x] == y or outer_face_cw_nbr[x] == y
  174. def is_on_outer_face(x):
  175. return x not in marked_nodes and (x in outer_face_ccw_nbr or x == v1)
  176. # Initialize number of chords
  177. for v in outer_face:
  178. for nbr in embedding.neighbors_cw_order(v):
  179. if is_on_outer_face(nbr) and not is_outer_face_nbr(v, nbr):
  180. chords[v] += 1
  181. ready_to_pick.discard(v)
  182. # Initialize canonical_ordering
  183. canonical_ordering = [None] * len(embedding.nodes())
  184. canonical_ordering[0] = (v1, [])
  185. canonical_ordering[1] = (v2, [])
  186. ready_to_pick.discard(v1)
  187. ready_to_pick.discard(v2)
  188. for k in range(len(embedding.nodes()) - 1, 1, -1):
  189. # 1. Pick v from ready_to_pick
  190. v = ready_to_pick.pop()
  191. marked_nodes.add(v)
  192. # v has exactly two neighbors on the outer face (wp and wq)
  193. wp = None
  194. wq = None
  195. # Iterate over neighbors of v to find wp and wq
  196. nbr_iterator = iter(embedding.neighbors_cw_order(v))
  197. while True:
  198. nbr = next(nbr_iterator)
  199. if nbr in marked_nodes:
  200. # Only consider nodes that are not yet removed
  201. continue
  202. if is_on_outer_face(nbr):
  203. # nbr is either wp or wq
  204. if nbr == v1:
  205. wp = v1
  206. elif nbr == v2:
  207. wq = v2
  208. else:
  209. if outer_face_cw_nbr[nbr] == v:
  210. # nbr is wp
  211. wp = nbr
  212. else:
  213. # nbr is wq
  214. wq = nbr
  215. if wp is not None and wq is not None:
  216. # We don't need to iterate any further
  217. break
  218. # Obtain new nodes on outer face (neighbors of v from wp to wq)
  219. wp_wq = [wp]
  220. nbr = wp
  221. while nbr != wq:
  222. # Get next neighbor (clockwise on the outer face)
  223. next_nbr = embedding[v][nbr]["ccw"]
  224. wp_wq.append(next_nbr)
  225. # Update outer face
  226. outer_face_cw_nbr[nbr] = next_nbr
  227. outer_face_ccw_nbr[next_nbr] = nbr
  228. # Move to next neighbor of v
  229. nbr = next_nbr
  230. if len(wp_wq) == 2:
  231. # There was a chord between wp and wq, decrease number of chords
  232. chords[wp] -= 1
  233. if chords[wp] == 0:
  234. ready_to_pick.add(wp)
  235. chords[wq] -= 1
  236. if chords[wq] == 0:
  237. ready_to_pick.add(wq)
  238. else:
  239. # Update all chords involving w_(p+1) to w_(q-1)
  240. new_face_nodes = set(wp_wq[1:-1])
  241. for w in new_face_nodes:
  242. # If we do not find a chord for w later we can pick it next
  243. ready_to_pick.add(w)
  244. for nbr in embedding.neighbors_cw_order(w):
  245. if is_on_outer_face(nbr) and not is_outer_face_nbr(w, nbr):
  246. # There is a chord involving w
  247. chords[w] += 1
  248. ready_to_pick.discard(w)
  249. if nbr not in new_face_nodes:
  250. # Also increase chord for the neighbor
  251. # We only iterator over new_face_nodes
  252. chords[nbr] += 1
  253. ready_to_pick.discard(nbr)
  254. # Set the canonical ordering node and the list of contour neighbors
  255. canonical_ordering[k] = (v, wp_wq)
  256. return canonical_ordering
  257. def triangulate_face(embedding, v1, v2):
  258. """Triangulates the face given by half edge (v, w)
  259. Parameters
  260. ----------
  261. embedding : nx.PlanarEmbedding
  262. v1 : node
  263. The half-edge (v1, v2) belongs to the face that gets triangulated
  264. v2 : node
  265. """
  266. _, v3 = embedding.next_face_half_edge(v1, v2)
  267. _, v4 = embedding.next_face_half_edge(v2, v3)
  268. if v1 in (v2, v3):
  269. # The component has less than 3 nodes
  270. return
  271. while v1 != v4:
  272. # Add edge if not already present on other side
  273. if embedding.has_edge(v1, v3):
  274. # Cannot triangulate at this position
  275. v1, v2, v3 = v2, v3, v4
  276. else:
  277. # Add edge for triangulation
  278. embedding.add_half_edge_cw(v1, v3, v2)
  279. embedding.add_half_edge_ccw(v3, v1, v2)
  280. v1, v2, v3 = v1, v3, v4
  281. # Get next node
  282. _, v4 = embedding.next_face_half_edge(v2, v3)
  283. def triangulate_embedding(embedding, fully_triangulate=True):
  284. """Triangulates the embedding.
  285. Traverses faces of the embedding and adds edges to a copy of the
  286. embedding to triangulate it.
  287. The method also ensures that the resulting graph is 2-connected by adding
  288. edges if the same vertex is contained twice on a path around a face.
  289. Parameters
  290. ----------
  291. embedding : nx.PlanarEmbedding
  292. The input graph must contain at least 3 nodes.
  293. fully_triangulate : bool
  294. If set to False the face with the most nodes is chooses as outer face.
  295. This outer face does not get triangulated.
  296. Returns
  297. -------
  298. (embedding, outer_face) : (nx.PlanarEmbedding, list) tuple
  299. The element `embedding` is a new embedding containing all edges from
  300. the input embedding and the additional edges to triangulate the graph.
  301. The element `outer_face` is a list of nodes that lie on the outer face.
  302. If the graph is fully triangulated these are three arbitrary connected
  303. nodes.
  304. """
  305. if len(embedding.nodes) <= 1:
  306. return embedding, list(embedding.nodes)
  307. embedding = nx.PlanarEmbedding(embedding)
  308. # Get a list with a node for each connected component
  309. component_nodes = [next(iter(x)) for x in nx.connected_components(embedding)]
  310. # 1. Make graph a single component (add edge between components)
  311. for i in range(len(component_nodes) - 1):
  312. v1 = component_nodes[i]
  313. v2 = component_nodes[i + 1]
  314. embedding.connect_components(v1, v2)
  315. # 2. Calculate faces, ensure 2-connectedness and determine outer face
  316. outer_face = [] # A face with the most number of nodes
  317. face_list = []
  318. edges_visited = set() # Used to keep track of already visited faces
  319. for v in embedding.nodes():
  320. for w in embedding.neighbors_cw_order(v):
  321. new_face = make_bi_connected(embedding, v, w, edges_visited)
  322. if new_face:
  323. # Found a new face
  324. face_list.append(new_face)
  325. if len(new_face) > len(outer_face):
  326. # The face is a candidate to be the outer face
  327. outer_face = new_face
  328. # 3. Triangulate (internal) faces
  329. for face in face_list:
  330. if face is not outer_face or fully_triangulate:
  331. # Triangulate this face
  332. triangulate_face(embedding, face[0], face[1])
  333. if fully_triangulate:
  334. v1 = outer_face[0]
  335. v2 = outer_face[1]
  336. v3 = embedding[v2][v1]["ccw"]
  337. outer_face = [v1, v2, v3]
  338. return embedding, outer_face
  339. def make_bi_connected(embedding, starting_node, outgoing_node, edges_counted):
  340. """Triangulate a face and make it 2-connected
  341. This method also adds all edges on the face to `edges_counted`.
  342. Parameters
  343. ----------
  344. embedding: nx.PlanarEmbedding
  345. The embedding that defines the faces
  346. starting_node : node
  347. A node on the face
  348. outgoing_node : node
  349. A node such that the half edge (starting_node, outgoing_node) belongs
  350. to the face
  351. edges_counted: set
  352. Set of all half-edges that belong to a face that have been visited
  353. Returns
  354. -------
  355. face_nodes: list
  356. A list of all nodes at the border of this face
  357. """
  358. # Check if the face has already been calculated
  359. if (starting_node, outgoing_node) in edges_counted:
  360. # This face was already counted
  361. return []
  362. edges_counted.add((starting_node, outgoing_node))
  363. # Add all edges to edges_counted which have this face to their left
  364. v1 = starting_node
  365. v2 = outgoing_node
  366. face_list = [starting_node] # List of nodes around the face
  367. face_set = set(face_list) # Set for faster queries
  368. _, v3 = embedding.next_face_half_edge(v1, v2)
  369. # Move the nodes v1, v2, v3 around the face:
  370. while v2 != starting_node or v3 != outgoing_node:
  371. if v1 == v2:
  372. raise nx.NetworkXException("Invalid half-edge")
  373. # cycle is not completed yet
  374. if v2 in face_set:
  375. # v2 encountered twice: Add edge to ensure 2-connectedness
  376. embedding.add_half_edge_cw(v1, v3, v2)
  377. embedding.add_half_edge_ccw(v3, v1, v2)
  378. edges_counted.add((v2, v3))
  379. edges_counted.add((v3, v1))
  380. v2 = v1
  381. else:
  382. face_set.add(v2)
  383. face_list.append(v2)
  384. # set next edge
  385. v1 = v2
  386. v2, v3 = embedding.next_face_half_edge(v2, v3)
  387. # remember that this edge has been counted
  388. edges_counted.add((v1, v2))
  389. return face_list