layouts.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083
  1. # -*- coding: utf-8 -*-
  2. """
  3. .. _layouts:
  4. layouts.py
  5. ==========
  6. Layouts are classes that provide graph drawing algorithms.
  7. These classes all take a :class:`graph_core` argument. The graph
  8. topology will never be permanently modified by the drawing algorithm:
  9. e.g. "dummy" node insertion, edge reversal for making the graph
  10. acyclic and so on, are all kept inside the layout object.
  11. """
  12. # This code is part of Grandalf
  13. # Copyright (C) 2010-2012 Axel Tillequin (bdcht3@gmail.com)
  14. # published under the GPLv2 license or EPLv1 license
  15. import importlib
  16. from bisect import bisect
  17. from sys import getrecursionlimit, setrecursionlimit
  18. from mantis.grandalf.utils import *
  19. # ------------------------------------------------------------------------------
  20. class VertexViewer(object):
  21. """
  22. The VertexViewer class is used as the default provider of
  23. Vertex dimensions (w,h) and position (xy).
  24. In most cases it should be replaced by *view* instances associated
  25. with a ui widgets library, allowing to get dimensions and
  26. set position directly on the widget.
  27. """
  28. def __init__(self, w=2, h=2, data=None):
  29. self.w = w
  30. self.h = h
  31. self.data = data
  32. self.xy = None
  33. def __str__(self, *args, **kwargs):
  34. return "VertexViewer (xy: %s) w: %s h: %s" % (self.xy, self.w, self.h)
  35. # ------------------------------------------------------------------------------
  36. class _sugiyama_vertex_attr(object):
  37. """
  38. The sugiyama layout adds new attributes to vertices.
  39. These attributes are stored in an internal _sugimyama_vertex_attr object.
  40. Attributes:
  41. rank (int): the rank number is the index of the layer that
  42. contains this vertex.
  43. dummy (0/1): a flag indicating if the vertex is *dummy*
  44. pos (int): the index of the vertex in the layer
  45. x (list(float)): the list of computed horizontal coordinates of the vertex
  46. bar (float): the current *barycenter* of the vertex
  47. """
  48. def __init__(self, r=None, d=0):
  49. self.rank = r
  50. self.dummy = d
  51. self.pos = None
  52. self.x = 0
  53. self.bar = None
  54. def __str__(self):
  55. s = "(%3d,%3d) x=%s" % (self.rank, self.pos, str(self.x))
  56. if self.dummy:
  57. s = "[d] %s" % s
  58. return s
  59. # def __eq__(self,x):
  60. # return self.bar == x.bar
  61. # def __ne__(self,x):
  62. # return self.bar != x.bar
  63. # def __lt__(self,x):
  64. # return self.bar < x.bar
  65. # def __le__(self,x):
  66. # return self.bar <= x.bar
  67. # def __gt__(self,x):
  68. # return self.bar > x.bar
  69. # def __ge__(self,x):
  70. # return self.bar >= x.bar
  71. # ------------------------------------------------------------------------------
  72. class DummyVertex(_sugiyama_vertex_attr):
  73. """
  74. The DummyVertex class is used by the sugiyama layout to represent
  75. *long* edges, i.e. edges that span over several ranks.
  76. For these edges, a DummyVertex is inserted in every inner layer.
  77. Attributes:
  78. view (viewclass): since a DummyVertex is acting as a Vertex, it
  79. must have a view.
  80. ctrl (list[_sugiyama_attr]): the list of associated dummy vertices
  81. Methods:
  82. N(dir): reflect the Vertex method and returns the list of adjacent
  83. vertices (possibly dummy) in the given direction.
  84. inner(dir): return True if a neighbor in the given direction is *dummy*.
  85. """
  86. def __init__(self, r=None, viewclass=VertexViewer):
  87. self.view = viewclass()
  88. self.ctrl = None
  89. super().__init__(r, d=1)
  90. def N(self, dir):
  91. assert dir == +1 or dir == -1
  92. v = self.ctrl.get(self.rank + dir, None)
  93. return [v] if v is not None else []
  94. def inner(self, dir):
  95. assert dir == +1 or dir == -1
  96. try:
  97. return any([x.dummy == 1 for x in self.N(dir)])
  98. except KeyError:
  99. return False
  100. except AttributeError:
  101. return False
  102. def __str__(self):
  103. s = "(%3d,%3d) x=%s" % (self.rank, self.pos, str(self.x))
  104. if self.dummy:
  105. s = "[d] %s" % s
  106. return s
  107. # ------------------------------------------------------------------------------
  108. class Layer(list):
  109. """
  110. Layer is where Sugiyama layout organises vertices in hierarchical lists.
  111. The placement of a vertex is done by the Sugiyama class, but it highly relies on
  112. the *ordering* of vertices in each layer to reduce crossings.
  113. This ordering depends on the neighbors found in the upper or lower layers.
  114. Attributes:
  115. layout (SugiyamaLayout): a reference to the sugiyama layout instance that
  116. contains this layer
  117. upper (Layer): a reference to the *upper* layer (rank-1)
  118. lower (Layer): a reference to the *lower* layer (rank+1)
  119. ccount (int) : number of crossings detected in this layer
  120. Methods:
  121. setup (layout): set initial attributes values from provided layout
  122. nextlayer(): returns *next* layer in the current layout's direction parameter.
  123. prevlayer(): returns *previous* layer in the current layout's direction parameter.
  124. order(): compute *optimal* ordering of vertices within the layer.
  125. """
  126. __r = None
  127. layout = None
  128. upper = None
  129. lower = None
  130. __x = 1.0
  131. ccount = None
  132. def __eq__(self, other):
  133. return super().__eq__(other)
  134. def __str__(self):
  135. s = "<Layer %d" % self.__r
  136. s += ", len=%d" % len(self)
  137. xc = self.ccount or "?"
  138. s += ", crossings=%s>" % xc
  139. return s
  140. def setup(self, layout):
  141. self.layout = layout
  142. r = layout.layers.index(self)
  143. self.__r = r
  144. if len(self) > 1:
  145. self.__x = 1.0 / (len(self) - 1)
  146. for i, v in enumerate(self):
  147. assert layout.grx[v].rank == r
  148. layout.grx[v].pos = i
  149. layout.grx[v].bar = i * self.__x
  150. if r > 0:
  151. self.upper = layout.layers[r - 1]
  152. if r < len(layout.layers) - 1:
  153. self.lower = layout.layers[r + 1]
  154. def nextlayer(self):
  155. return self.lower if self.layout.dirv == -1 else self.upper
  156. def prevlayer(self):
  157. return self.lower if self.layout.dirv == +1 else self.upper
  158. def order(self):
  159. sug = self.layout
  160. sug._edge_inverter()
  161. c = self._cc()
  162. if c > 0:
  163. for v in self:
  164. sug.grx[v].bar = self._meanvalueattr(v)
  165. # now resort layers l according to bar value:
  166. self.sort(key=lambda x: sug.grx[x].bar)
  167. # reduce & count crossings:
  168. c = self._ordering_reduce_crossings()
  169. # assign new position in layer l:
  170. for i, v in enumerate(self):
  171. sug.grx[v].pos = i
  172. sug.grx[v].bar = i * self.__x
  173. sug._edge_inverter()
  174. self.ccount = c
  175. return c
  176. def _meanvalueattr(self, v):
  177. """
  178. find new position of vertex v according to adjacency in prevlayer.
  179. position is given by the mean value of adjacent positions.
  180. experiments show that meanvalue heuristic performs better than median.
  181. """
  182. sug = self.layout
  183. if not self.prevlayer():
  184. return sug.grx[v].bar
  185. bars = [sug.grx[x].bar for x in self._neighbors(v)]
  186. return sug.grx[v].bar if len(bars) == 0 else float(sum(bars)) / len(bars)
  187. def _medianindex(self, v):
  188. """
  189. find new position of vertex v according to adjacency in layer l+dir.
  190. position is given by the median value of adjacent positions.
  191. median heuristic is proven to achieve at most 3 times the minimum
  192. of crossings (while barycenter achieve in theory the order of |V|)
  193. """
  194. assert self.prevlayer() != None
  195. N = self._neighbors(v)
  196. g = self.layout.grx
  197. pos = [g[x].pos for x in N]
  198. lp = len(pos)
  199. if lp == 0:
  200. return []
  201. pos.sort()
  202. pos = pos[:: self.layout.dirh]
  203. i, j = divmod(lp - 1, 2)
  204. return [pos[i]] if j == 0 else [pos[i], pos[i + j]]
  205. def _neighbors(self, v):
  206. """
  207. neighbors refer to upper/lower adjacent nodes.
  208. Note that v.N() provides neighbors of v in the graph, while
  209. this method provides the Vertex and DummyVertex adjacent to v in the
  210. upper or lower layer (depending on layout.dirv state).
  211. """
  212. assert self.layout.dag
  213. dirv = self.layout.dirv
  214. grxv = self.layout.grx[v]
  215. try: # (cache)
  216. return grxv.nvs[dirv]
  217. except AttributeError:
  218. grxv.nvs = {-1: v.N(-1), +1: v.N(+1)}
  219. if grxv.dummy:
  220. return grxv.nvs[dirv]
  221. # v is real, v.N are graph neigbors but we need layers neighbors
  222. for d in (-1, +1):
  223. tr = grxv.rank + d
  224. for i, x in enumerate(v.N(d)):
  225. if self.layout.grx[x].rank == tr:
  226. continue
  227. e = v.e_with(x)
  228. dum = self.layout.ctrls[e][tr]
  229. grxv.nvs[d][i] = dum
  230. return grxv.nvs[dirv]
  231. def _crossings(self):
  232. """
  233. counts (inefficently but at least accurately) the number of
  234. crossing edges between layer l and l+dirv.
  235. P[i][j] counts the number of crossings from j-th edge of vertex i.
  236. The total count of crossings is the sum of flattened P:
  237. x = sum(sum(P,[]))
  238. """
  239. g = self.layout.grx
  240. P = []
  241. for v in self:
  242. P.append([g[x].pos for x in self._neighbors(v)])
  243. for i, p in enumerate(P):
  244. candidates = sum(P[i + 1 :], [])
  245. for j, e in enumerate(p):
  246. p[j] = len(filter((lambda nx: nx < e), candidates))
  247. del candidates
  248. return P
  249. def _cc(self):
  250. """
  251. implementation of the efficient bilayer cross counting by insert-sort
  252. (see Barth & Mutzel paper "Simple and Efficient Bilayer Cross Counting")
  253. """
  254. g = self.layout.grx
  255. P = []
  256. for v in self:
  257. P.extend(sorted([g[x].pos for x in self._neighbors(v)]))
  258. # count inversions in P:
  259. s = []
  260. count = 0
  261. for i, p in enumerate(P):
  262. j = bisect(s, p)
  263. if j < i:
  264. count += i - j
  265. s.insert(j, p)
  266. return count
  267. def _ordering_reduce_crossings(self):
  268. assert self.layout.dag
  269. g = self.layout.grx
  270. N = len(self)
  271. X = 0
  272. for i, j in zip(range(N - 1), range(1, N)):
  273. vi = self[i]
  274. vj = self[j]
  275. ni = [g[v].bar for v in self._neighbors(vi)]
  276. Xij = Xji = 0
  277. for nj in [g[v].bar for v in self._neighbors(vj)]:
  278. x = len([nx for nx in ni if nx > nj])
  279. Xij += x
  280. Xji += len(ni) - x
  281. if Xji < Xij:
  282. self[i] = vj
  283. self[j] = vi
  284. X += Xji
  285. else:
  286. X += Xij
  287. return X
  288. # ------------------------------------------------------------------------------
  289. class SugiyamaLayout(object):
  290. """
  291. The Sugiyama layout is the traditional "layered" graph layout called
  292. *dot* in graphviz. This layout is quite efficient but heavily relies
  293. on drawing heuristics. Adaptive drawing is limited to
  294. extending the leaves only, but since the algorithm is quite fast
  295. redrawing the entire graph (up to about a thousand nodes) gives
  296. usually good results in less than a second.
  297. The Sugiyama Layout Class takes as input a core_graph object and implements
  298. an efficient drawing algorithm based on nodes dimensions provided through
  299. a user-defined *view* property in each vertex.
  300. Attributes:
  301. dirvh (int): the current aligment state
  302. for alignment policy:
  303. dirvh=0 -> dirh=+1, dirv=-1: leftmost upper
  304. dirvh=1 -> dirh=-1, dirv=-1: rightmost upper
  305. dirvh=2 -> dirh=+1, dirv=+1: leftmost lower
  306. dirvh=3 -> dirh=-1, dirv=+1: rightmost lower
  307. order_inter (int): the default number of layer placement iterations
  308. order_attr (str): set attribute name used for layer ordering
  309. xspace (int): horizontal space between vertices in a layer
  310. yspace (int): vertical space between layers
  311. dw (int): default width of a vertex
  312. dh (int): default height of a vertex
  313. g (graph_core): the graph component reference
  314. layers (list[Layer]): the list of layers
  315. grx (dict): associate vertex (possibly dummy) with their sugiyama attributes
  316. ctrls (dict): associate edge with all its vertices (including dummies)
  317. dag (bool): the current acyclic state
  318. initdone (bool): True if state is initialized (see init_all).
  319. """
  320. def __init__(self, g):
  321. from mantis.grandalf.utils.geometry import median_wh
  322. # drawing parameters:
  323. self.dirvh = 0
  324. self.order_iter = 8
  325. self.order_attr = "pos"
  326. self.xspace = 20
  327. self.yspace = 20
  328. self.dw = 10
  329. self.dh = 10
  330. # For layered graphs, vertices and edges need to have some additional
  331. # attributes that make sense only for this kind of layout:
  332. # update graph struct:
  333. self.g = g
  334. self.layers = []
  335. self.grx = {}
  336. self.ctrls = {}
  337. self.dag = False
  338. for v in self.g.V():
  339. assert hasattr(v, "view")
  340. self.grx[v] = _sugiyama_vertex_attr()
  341. self.dw, self.dh = median_wh([v.view for v in self.g.V()])
  342. self.initdone = False
  343. def init_all(self, roots=None, inverted_edges=None, optimize=False):
  344. """initializes the layout algorithm by computing roots (unless provided),
  345. inverted edges (unless provided), vertices ranks and creates all dummy
  346. vertices and layers.
  347. Parameters:
  348. roots (list[Vertex]): set *root* vertices (layer 0)
  349. inverted_edges (list[Edge]): set edges to invert to have a DAG.
  350. optimize (bool): optimize ranking if True (default False)
  351. """
  352. if self.initdone:
  353. return
  354. # For layered sugiyama algorithm, the input graph must be acyclic,
  355. # so we must provide a list of root nodes and a list of inverted edges.
  356. if roots is None:
  357. roots = [v for v in self.g.sV if len(v.e_in()) == 0]
  358. if inverted_edges is None:
  359. _ = self.g.get_scs_with_feedback(roots)
  360. inverted_edges = [x for x in self.g.sE if x.feedback]
  361. self.alt_e = inverted_edges
  362. # assign rank to all vertices:
  363. self.rank_all(roots, optimize)
  364. # add dummy vertex/edge for 'long' edges:
  365. for e in self.g.E():
  366. self.setdummies(e)
  367. # precompute some layers values:
  368. for l in self.layers:
  369. l.setup(self)
  370. self.initdone = True
  371. def draw(self, N=1.5):
  372. """compute every node coordinates after converging to optimal ordering by N
  373. rounds, and finally perform the edge routing.
  374. """
  375. while N > 0.5:
  376. for (l, mvmt) in self.ordering_step():
  377. pass
  378. N = N - 1
  379. if N > 0:
  380. for (l, mvmt) in self.ordering_step(oneway=True):
  381. pass
  382. self.setxy()
  383. self.draw_edges()
  384. def _edge_inverter(self):
  385. for e in self.alt_e:
  386. x, y = e.v
  387. e.v = (y, x)
  388. self.dag = not self.dag
  389. if self.dag:
  390. for e in self.g.degenerated_edges:
  391. e.detach()
  392. self.g.sE.remove(e)
  393. else:
  394. for e in self.g.degenerated_edges:
  395. self.g.add_edge(e)
  396. @property
  397. def dirvh(self):
  398. return self.__dirvh
  399. @property
  400. def dirv(self):
  401. return self.__dirv
  402. @property
  403. def dirh(self):
  404. return self.__dirh
  405. @dirvh.setter
  406. def dirvh(self, dirvh):
  407. assert dirvh in range(4)
  408. self.__dirvh = dirvh
  409. self.__dirh, self.__dirv = {0: (1, -1),
  410. 1: (-1, -1),
  411. 2: (1, 1),
  412. 3: (-1, 1)}[dirvh]
  413. @dirv.setter
  414. def dirv(self, dirv):
  415. assert dirv in (-1, +1)
  416. dirvh = (dirv + 1) + (1 - self.__dirh) // 2
  417. self.dirvh = dirvh
  418. @dirh.setter
  419. def dirh(self, dirh):
  420. assert dirh in (-1, +1)
  421. dirvh = (self.__dirv + 1) + (1 - dirh) // 2
  422. self.dirvh = dirvh
  423. def rank_all(self, roots, optimize=False):
  424. """Computes rank of all vertices.
  425. add provided roots to rank 0 vertices,
  426. otherwise update ranking from provided roots.
  427. The initial rank is based on precedence relationships,
  428. optimal ranking may be derived from network flow (simplex).
  429. """
  430. self._edge_inverter()
  431. r = [x for x in self.g.sV if (len(x.e_in()) == 0 and x not in roots)]
  432. self._rank_init(roots + r)
  433. if optimize:
  434. self._rank_optimize()
  435. self._edge_inverter()
  436. def _rank_init(self, unranked):
  437. """Computes rank of provided unranked list of vertices and all
  438. their children. A vertex will be asign a rank when all its
  439. inward edges have been *scanned*. When a vertex is asigned
  440. a rank, its outward edges are marked *scanned*.
  441. """
  442. assert self.dag
  443. scan = {}
  444. # set rank of unranked based on its in-edges vertices ranks:
  445. while len(unranked) > 0:
  446. l = []
  447. for v in unranked:
  448. self.setrank(v)
  449. # mark out-edges has scan-able:
  450. for e in v.e_out():
  451. scan[e] = True
  452. # check if out-vertices are rank-able:
  453. for x in v.N(+1):
  454. if not (False in [scan.get(e, False) for e in x.e_in()]):
  455. if x not in l:
  456. l.append(x)
  457. unranked = l
  458. def _rank_optimize(self):
  459. """optimize ranking by pushing long edges toward lower layers as much as possible.
  460. see other interersting network flow solver to minimize total edge length
  461. (http://jgaa.info/accepted/2005/EiglspergerSiebenhallerKaufmann2005.9.3.pdf)
  462. """
  463. assert self.dag
  464. for l in reversed(self.layers):
  465. for v in l:
  466. gv = self.grx[v]
  467. for x in v.N(-1):
  468. if all((self.grx[y].rank >= gv.rank for y in x.N(+1))):
  469. gx = self.grx[x]
  470. self.layers[gx.rank].remove(x)
  471. gx.rank = gv.rank - 1
  472. self.layers[gv.rank - 1].append(x)
  473. def setrank(self, v):
  474. """set rank value for vertex v and add it to the corresponding layer.
  475. The Layer is created if it is the first vertex with this rank.
  476. """
  477. assert self.dag
  478. r = max([self.grx[x].rank for x in v.N(-1)] + [-1]) + 1
  479. self.grx[v].rank = r
  480. # add it to its layer:
  481. try:
  482. self.layers[r].append(v)
  483. except IndexError:
  484. assert r == len(self.layers)
  485. self.layers.append(Layer([v]))
  486. def dummyctrl(self, r, ctrl):
  487. """creates a DummyVertex at rank r inserted in the ctrl dict
  488. of the associated edge and layer.
  489. Arguments:
  490. r (int): rank value
  491. ctrl (dict): the edge's control vertices
  492. Returns:
  493. DummyVertex : the created DummyVertex.
  494. """
  495. dv = DummyVertex(r)
  496. dv.view.w, dv.view.h = self.dw, self.dh
  497. self.grx[dv] = dv
  498. dv.ctrl = ctrl
  499. ctrl[r] = dv
  500. self.layers[r].append(dv)
  501. return dv
  502. def setdummies(self, e):
  503. """creates and defines all needed dummy vertices for edge e.
  504. """
  505. v0, v1 = e.v
  506. r0, r1 = self.grx[v0].rank, self.grx[v1].rank
  507. if r0 > r1:
  508. assert e in self.alt_e
  509. v0, v1 = v1, v0
  510. r0, r1 = r1, r0
  511. if (r1 - r0) > 1:
  512. # "dummy vertices" are stored in the edge ctrl dict,
  513. # keyed by their rank in layers.
  514. ctrl = self.ctrls[e] = {}
  515. ctrl[r0] = v0
  516. ctrl[r1] = v1
  517. for r in range(r0 + 1, r1):
  518. self.dummyctrl(r, ctrl)
  519. def draw_step(self):
  520. """iterator that computes all vertices coordinates and edge routing after
  521. just one step (one layer after the other from top to bottom to top).
  522. Purely inefficient ! Use it only for "animation" or debugging purpose.
  523. """
  524. ostep = self.ordering_step()
  525. for s in ostep:
  526. self.setxy()
  527. self.draw_edges()
  528. yield s
  529. def ordering_step(self, oneway=False):
  530. """iterator that computes all vertices ordering in their layers
  531. (one layer after the other from top to bottom, to top again unless
  532. oneway is True).
  533. """
  534. self.dirv = -1
  535. crossings = 0
  536. for l in self.layers:
  537. mvmt = l.order()
  538. crossings += mvmt
  539. yield (l, mvmt)
  540. if oneway or (crossings == 0):
  541. return
  542. self.dirv = +1
  543. while l:
  544. mvmt = l.order()
  545. yield (l, mvmt)
  546. l = l.nextlayer()
  547. def setxy(self):
  548. """computes all vertex coordinates (x,y) using
  549. an algorithm by Brandes & Kopf.
  550. """
  551. self._edge_inverter()
  552. self._detect_alignment_conflicts()
  553. inf = float("infinity")
  554. # initialize vertex coordinates attributes:
  555. for l in self.layers:
  556. for v in l:
  557. self.grx[v].root = v
  558. self.grx[v].align = v
  559. self.grx[v].sink = v
  560. self.grx[v].shift = inf
  561. self.grx[v].X = None
  562. self.grx[v].x = [0.0] * 4
  563. curvh = self.dirvh # save current dirvh value
  564. for dirvh in range(4):
  565. self.dirvh = dirvh
  566. self._coord_vertical_alignment()
  567. self._coord_horizontal_compact()
  568. self.dirvh = curvh # restore it
  569. # vertical coordinate assigment of all nodes:
  570. Y = 0
  571. for l in self.layers:
  572. dY = max([v.view.h / 2.0 for v in l])
  573. for v in l:
  574. vx = sorted(self.grx[v].x)
  575. # mean of the 2 medians out of the 4 x-coord computed above:
  576. avgm = (vx[1] + vx[2]) / 2.0
  577. # final xy-coordinates :
  578. v.view.xy = (avgm, Y + dY)
  579. Y += 2 * dY + self.yspace
  580. self._edge_inverter()
  581. def _detect_alignment_conflicts(self):
  582. """mark conflicts between edges:
  583. inner edges are edges between dummy nodes
  584. type 0 is regular crossing regular (or sharing vertex)
  585. type 1 is inner crossing regular (targeted crossings)
  586. type 2 is inner crossing inner (avoided by reduce_crossings phase)
  587. """
  588. curvh = self.dirvh # save current dirvh value
  589. self.dirvh = 0
  590. self.conflicts = []
  591. for L in self.layers:
  592. last = len(L) - 1
  593. prev = L.prevlayer()
  594. if not prev:
  595. continue
  596. k0 = 0
  597. k1_init = len(prev) - 1
  598. l = 0
  599. for l1, v in enumerate(L):
  600. if not self.grx[v].dummy:
  601. continue
  602. if l1 == last or v.inner(-1):
  603. k1 = k1_init
  604. if v.inner(-1):
  605. k1 = self.grx[v.N(-1)[-1]].pos
  606. for vl in L[l : l1 + 1]:
  607. for vk in L._neighbors(vl):
  608. k = self.grx[vk].pos
  609. if k < k0 or k > k1:
  610. self.conflicts.append((vk, vl))
  611. l = l1 + 1
  612. k0 = k1
  613. self.dirvh = curvh # restore it
  614. def _coord_vertical_alignment(self):
  615. """performs vertical alignment according to current dirvh internal state.
  616. """
  617. dirh, dirv = self.dirh, self.dirv
  618. g = self.grx
  619. for l in self.layers[::-dirv]:
  620. if not l.prevlayer():
  621. continue
  622. r = None
  623. for vk in l[::dirh]:
  624. for m in l._medianindex(vk):
  625. # take the median node in dirv layer:
  626. um = l.prevlayer()[m]
  627. # if vk is "free" align it with um's root
  628. if g[vk].align is vk:
  629. if dirv == 1:
  630. vpair = (vk, um)
  631. else:
  632. vpair = (um, vk)
  633. # if vk<->um link is used for alignment
  634. if (vpair not in self.conflicts) and (
  635. (r is None) or (dirh * r < dirh * m)
  636. ):
  637. g[um].align = vk
  638. g[vk].root = g[um].root
  639. g[vk].align = g[vk].root
  640. r = m
  641. def _coord_horizontal_compact(self):
  642. limit = getrecursionlimit()
  643. N = len(self.layers) + 10
  644. if N > limit:
  645. setrecursionlimit(N)
  646. dirh, dirv = self.dirh, self.dirv
  647. g = self.grx
  648. L = self.layers[::-dirv]
  649. # recursive placement of blocks:
  650. for l in L:
  651. for v in l[::dirh]:
  652. if g[v].root is v:
  653. self.__place_block(v)
  654. setrecursionlimit(limit)
  655. # mirror all nodes if right-aligned:
  656. if dirh == -1:
  657. for l in L:
  658. for v in l:
  659. x = g[v].X
  660. if x:
  661. g[v].X = -x
  662. # then assign x-coord of its root:
  663. inf = float("infinity")
  664. rb = inf
  665. for l in L:
  666. for v in l[::dirh]:
  667. g[v].x[self.dirvh] = g[g[v].root].X
  668. rs = g[g[v].root].sink
  669. s = g[rs].shift
  670. if s < inf:
  671. g[v].x[self.dirvh] += dirh * s
  672. rb = min(rb, g[v].x[self.dirvh])
  673. # normalize to 0, and reinit root/align/sink/shift/X
  674. for l in self.layers:
  675. for v in l:
  676. # g[v].x[dirvh] -= rb
  677. g[v].root = g[v].align = g[v].sink = v
  678. g[v].shift = inf
  679. g[v].X = None
  680. # TODO: rewrite in iterative form to avoid recursion limit...
  681. def __place_block(self, v):
  682. g = self.grx
  683. if g[v].X is None:
  684. # every block is initially placed at x=0
  685. g[v].X = 0.0
  686. # place block in which v belongs:
  687. w = v
  688. while 1:
  689. j = g[w].pos - self.dirh # predecessor in rank must be placed
  690. r = g[w].rank
  691. if 0 <= j < len(self.layers[r]):
  692. wprec = self.layers[r][j]
  693. delta = (
  694. self.xspace + (wprec.view.w + w.view.w) / 2.0
  695. ) # abs positive minimum displ.
  696. # take root and place block:
  697. u = g[wprec].root
  698. self.__place_block(u)
  699. # set sink as sink of prec-block root
  700. if g[v].sink is v:
  701. g[v].sink = g[u].sink
  702. if g[v].sink != g[u].sink:
  703. s = g[u].sink
  704. newshift = g[v].X - (g[u].X + delta)
  705. g[s].shift = min(g[s].shift, newshift)
  706. else:
  707. g[v].X = max(g[v].X, (g[u].X + delta))
  708. # take next node to align in block:
  709. w = g[w].align
  710. # quit if self aligned
  711. if w is v:
  712. break
  713. def draw_edges(self):
  714. """Basic edge routing applied only for edges with dummy points.
  715. Enhanced edge routing can be performed by using the apropriate
  716. *route_with_xxx* functions from :ref:routing_ in the edges' view.
  717. """
  718. for e in self.g.E():
  719. if hasattr(e, "view"):
  720. l = []
  721. if e in self.ctrls:
  722. D = self.ctrls[e]
  723. r0, r1 = self.grx[e.v[0]].rank, self.grx[e.v[1]].rank
  724. if r0 < r1:
  725. ranks = range(r0 + 1, r1)
  726. else:
  727. ranks = range(r0 - 1, r1, -1)
  728. l = [D[r].view.xy for r in ranks]
  729. l.insert(0, e.v[0].view.xy)
  730. l.append(e.v[1].view.xy)
  731. try:
  732. self.route_edge(e, l)
  733. except AttributeError:
  734. pass
  735. e.view.setpath(l)
  736. # DIRECTED GRAPH WITH CONSTRAINTS LAYOUT
  737. # ------------------------------------------------------------------------------
  738. class DigcoLayout(object):
  739. linalg = importlib.import_module("mantis.grandalf.utils.geometry")
  740. def __init__(self, g):
  741. # drawing parameters:
  742. self.xspace = 10
  743. self.yspace = 10
  744. self.dr = 10
  745. self.debug = False
  746. self.g = g
  747. self.levels = []
  748. for i, v in enumerate(self.g.V()):
  749. assert hasattr(v, "view")
  750. v.i = i
  751. self.dr = max((self.dr, v.view.w, v.view.h))
  752. # solver parameters:
  753. self._cg_max_iter = g.order()
  754. self._cg_tolerance = 1.0e-6
  755. self._eps = 1.0e-5
  756. self._cv_max_iter = self._cg_max_iter
  757. def init_all(self, alpha=0.1, beta=0.01):
  758. y = None
  759. if self.g.directed:
  760. # partition g in hierarchical levels:
  761. y = self.part_to_levels(alpha, beta)
  762. # initiate positions (y and random in x):
  763. self.Z = self._xyinit(y)
  764. def draw(self, N=None):
  765. if N is None:
  766. N = self._cv_max_iter
  767. self.Z = self._optimize(self.Z, limit=N)
  768. # set view xy from near-optimal coords matrix:
  769. for v in self.g.V():
  770. v.view.xy = (self.Z[v.i][0, 0] * self.dr, self.Z[v.i][0, 1] * self.dr)
  771. self.draw_edges()
  772. def draw_step(self):
  773. for x in range(self._cv_max_iter):
  774. self.draw(N=1)
  775. self.draw_edges()
  776. yield
  777. # Basic edge routing with segments
  778. def draw_edges(self):
  779. for e in self.g.E():
  780. if hasattr(e, "view"):
  781. l = [e.v[0].view.xy, e.v[1].view.xy]
  782. try:
  783. self.route_edge(e, l)
  784. except AttributeError:
  785. pass
  786. e.view.setpath(l)
  787. # partition the nodes into levels:
  788. def part_to_levels(self, alpha, beta):
  789. opty, err = self.optimal_arrangement()
  790. ordering = list(zip(opty, self.g.sV))
  791. eps = alpha * (opty.max() - opty.min()) / (len(opty) - 1)
  792. eps = max(beta, eps)
  793. sorted(ordering, reverse=True)
  794. l = []
  795. self.levels.append(l)
  796. for i in range(len(list(ordering)) - 1):
  797. y, v = ordering[i]
  798. l.append(v)
  799. v.level = self.levels.index(l)
  800. if (y - ordering[i + 1][0]) > eps:
  801. l = []
  802. self.levels.append(l)
  803. y, v = ordering[-1]
  804. l.append(v)
  805. v.level = self.levels.index(l)
  806. return opty
  807. def optimal_arrangement(self):
  808. b = self.balance()
  809. y = DigcoLayout.linalg.rand_ortho1(self.g.order())
  810. return self._conjugate_gradient_L(y, b)
  811. # balance vector is assembled in finite-element way...
  812. # this is faster than computing b[i] for each i.
  813. def balance(self):
  814. b = DigcoLayout.linalg.array([0.0] * self.g.order(), dtype=float)
  815. for e in self.g.E():
  816. s = e.v[0]
  817. d = e.v[1]
  818. q = e.w * (self.yspace + (s.view.h + d.view.h) / 2.0)
  819. b[s.i] += q
  820. b[d.i] -= q
  821. return b
  822. # We compute the solution Y of L.Y = b by conjugate gradient method
  823. # (L is semi-definite positive so Y is unique and convergence is O(n))
  824. # note that only arrays are involved here...
  825. def _conjugate_gradient_L(self, y, b):
  826. Lii = self.__Lii_()
  827. r = b - self.__L_pk(Lii, y)
  828. p = DigcoLayout.linalg.array(r, copy=True)
  829. rr = sum(r * r)
  830. for k in range(self._cg_max_iter):
  831. try:
  832. Lp = self.__L_pk(Lii, p)
  833. alpha = rr / sum(p * Lp)
  834. y += alpha / p
  835. r -= alpha * Lp
  836. newrr = sum(r * r)
  837. beta = newrr / rr
  838. rr = newrr
  839. if rr < self._cg_tolerance:
  840. break
  841. p = r + beta * p
  842. except ZeroDivisionError:
  843. return (None, rr)
  844. return (y, rr)
  845. # _xyinit can use diagonally scaled initial vertices positioning to provide
  846. # better convergence in constrained stress majorization
  847. def _xyinit(self, y=None):
  848. if y is None:
  849. y = DigcoLayout.linalg.rand_ortho1(self.g.order())
  850. x = DigcoLayout.linalg.rand_ortho1(self.g.order())
  851. # translate and normalize:
  852. x = x - x[0]
  853. y = y - y[0]
  854. sfactor = 1.0 / max(list(map(abs, y)) + list(map(abs, x)))
  855. return DigcoLayout.linalg.matrix(list(zip(x * sfactor, y * sfactor)))
  856. # provide the diagonal of the Laplacian matrix of g
  857. # the rest of L (sparse!) is already stored in every edges.
  858. def __Lii_(self):
  859. Lii = []
  860. for v in self.g.V():
  861. Lii.append(sum([e.w for e in v.e]))
  862. return DigcoLayout.linalg.array(Lii, dtype=float)
  863. # we don't compute the L.Pk matrix/vector product here since
  864. # L is sparse (order of |E| not |V|^2 !) so we let each edge
  865. # contribute to the resulting L.Pk vector in a FE assembly way...
  866. def __L_pk(self, Lii, pk):
  867. y = Lii * pk
  868. for e in self.g.sE:
  869. i1 = e.v[0].i
  870. i2 = e.v[1].i
  871. y[i1] -= e.w * pk[i2]
  872. y[i2] -= e.w * pk[i1]
  873. return y
  874. # conjugate_gradient with given matrix Lw:
  875. # it is assumed that b is not a multivector,
  876. # so _cg_Lw should be called in all directions separately.
  877. # note that everything is a matrix here, (arrays are row vectors only)
  878. def _cg_Lw(self, Lw, z, b):
  879. scal = lambda U, V: float(U.transpose() * V)
  880. r = b - Lw * z
  881. p = r.copy()
  882. rr = scal(r, r)
  883. for k in range(self._cg_max_iter):
  884. if rr < self._cg_tolerance:
  885. break
  886. Lp = Lw * p
  887. alpha = rr / scal(p, Lp)
  888. z = z + alpha * p
  889. r = r - alpha * Lp
  890. newrr = scal(r, r)
  891. beta = newrr / rr
  892. rr = newrr
  893. p = r + beta * p
  894. return (z, rr)
  895. def __Dij_(self):
  896. Dji = []
  897. for v in self.g.V():
  898. wd = self.g.dijkstra(v)
  899. Di = [wd[w] for w in self.g.V()]
  900. Dji.append(Di)
  901. # at this point D is stored by rows,
  902. # but anymway it's a symmetric matrix
  903. return DigcoLayout.linalg.matrix(Dji, dtype=float)
  904. # returns matrix -L^w
  905. def __Lij_w_(self):
  906. self.Dij = self.__Dij_() # we keep D also for L^Z computations
  907. Lij = self.Dij.copy()
  908. n = self.g.order()
  909. for i in range(n):
  910. d = 0
  911. for j in range(n):
  912. if j == i:
  913. continue
  914. Lij[i, j] = 1.0 / self.Dij[i, j] ** 2
  915. d += Lij[i, j]
  916. Lij[i, i] = -d
  917. return Lij
  918. # returns vector -L^Z.Z:
  919. def __Lij_Z_Z(self, Z):
  920. n = self.g.order()
  921. # init:
  922. lzz = Z.copy() * 0.0 # lzz has dim Z (n x 2)
  923. liz = DigcoLayout.linalg.matrix([0.0] * n) # liz is a row of L^Z (size n)
  924. # compute lzz = L^Z.Z while assembling L^Z by row (liz):
  925. for i in range(n):
  926. iterk_except_i = (k for k in range(n) if k != i)
  927. for k in iterk_except_i:
  928. v = Z[i] - Z[k]
  929. liz[0, k] = 1.0 / (
  930. self.Dij[i, k] * DigcoLayout.linalg.sqrt(v * v.transpose())
  931. )
  932. liz[0, i] = 0.0 # forced, otherwise next liz.sum() is wrong !
  933. liz[0, i] = -liz.sum()
  934. # now that we have the i-th row of L^Z, just dotprod with Z:
  935. lzz[i] = liz * Z
  936. return lzz
  937. def _optimize(self, Z, limit=100):
  938. Lw = self.__Lij_w_()
  939. K = self.g.order() * (self.g.order() - 1.0) / 2.0
  940. stress = float("inf")
  941. count = 0
  942. deep = 0
  943. b = self.__Lij_Z_Z(Z)
  944. while count < limit:
  945. if self.debug:
  946. print("count %d" % count)
  947. print("Z = ", Z)
  948. print("b = ", b)
  949. # find next Z by solving Lw.Z = b in every direction:
  950. x, xerr = self._cg_Lw(Lw[1:, 1:], Z[1:, 0], b[1:, 0])
  951. y, yerr = self._cg_Lw(Lw[1:, 1:], Z[1:, 1], b[1:, 1])
  952. Z[1:, 0] = x
  953. Z[1:, 1] = y
  954. if self.debug:
  955. print(" cg -> ")
  956. print(Z, xerr, yerr)
  957. # compute new stress:
  958. FZ = K - float(x.transpose() * b[1:, 0] + y.transpose() * b[1:, 1])
  959. # precompute new b:
  960. b = self.__Lij_Z_Z(Z)
  961. # update new stress:
  962. FZ += 2 * float(x.transpose() * b[1:, 0] + y.transpose() * b[1:, 1])
  963. # test convergence:
  964. print("stress=%.10f" % FZ)
  965. if stress == 0.0:
  966. break
  967. elif abs((stress - FZ) / stress) < self._eps:
  968. if deep == 2:
  969. break
  970. else:
  971. deep += 1
  972. stress = FZ
  973. count += 1
  974. return Z
  975. # ------------------------------------------------------------------------------
  976. class DwyerLayout(DigcoLayout):
  977. pass