| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083 |
- # -*- coding: utf-8 -*-
- """
- .. _layouts:
- layouts.py
- ==========
- Layouts are classes that provide graph drawing algorithms.
- These classes all take a :class:`graph_core` argument. The graph
- topology will never be permanently modified by the drawing algorithm:
- e.g. "dummy" node insertion, edge reversal for making the graph
- acyclic and so on, are all kept inside the layout object.
- """
- # This code is part of Grandalf
- # Copyright (C) 2010-2012 Axel Tillequin (bdcht3@gmail.com)
- # published under the GPLv2 license or EPLv1 license
- import importlib
- from bisect import bisect
- from sys import getrecursionlimit, setrecursionlimit
- from mantis.grandalf.utils import *
- # ------------------------------------------------------------------------------
- class VertexViewer(object):
- """
- The VertexViewer class is used as the default provider of
- Vertex dimensions (w,h) and position (xy).
- In most cases it should be replaced by *view* instances associated
- with a ui widgets library, allowing to get dimensions and
- set position directly on the widget.
- """
- def __init__(self, w=2, h=2, data=None):
- self.w = w
- self.h = h
- self.data = data
- self.xy = None
- def __str__(self, *args, **kwargs):
- return "VertexViewer (xy: %s) w: %s h: %s" % (self.xy, self.w, self.h)
- # ------------------------------------------------------------------------------
- class _sugiyama_vertex_attr(object):
- """
- The sugiyama layout adds new attributes to vertices.
- These attributes are stored in an internal _sugimyama_vertex_attr object.
- Attributes:
- rank (int): the rank number is the index of the layer that
- contains this vertex.
- dummy (0/1): a flag indicating if the vertex is *dummy*
- pos (int): the index of the vertex in the layer
- x (list(float)): the list of computed horizontal coordinates of the vertex
- bar (float): the current *barycenter* of the vertex
- """
- def __init__(self, r=None, d=0):
- self.rank = r
- self.dummy = d
- self.pos = None
- self.x = 0
- self.bar = None
- def __str__(self):
- s = "(%3d,%3d) x=%s" % (self.rank, self.pos, str(self.x))
- if self.dummy:
- s = "[d] %s" % s
- return s
- # def __eq__(self,x):
- # return self.bar == x.bar
- # def __ne__(self,x):
- # return self.bar != x.bar
- # def __lt__(self,x):
- # return self.bar < x.bar
- # def __le__(self,x):
- # return self.bar <= x.bar
- # def __gt__(self,x):
- # return self.bar > x.bar
- # def __ge__(self,x):
- # return self.bar >= x.bar
- # ------------------------------------------------------------------------------
- class DummyVertex(_sugiyama_vertex_attr):
- """
- The DummyVertex class is used by the sugiyama layout to represent
- *long* edges, i.e. edges that span over several ranks.
- For these edges, a DummyVertex is inserted in every inner layer.
- Attributes:
- view (viewclass): since a DummyVertex is acting as a Vertex, it
- must have a view.
- ctrl (list[_sugiyama_attr]): the list of associated dummy vertices
- Methods:
- N(dir): reflect the Vertex method and returns the list of adjacent
- vertices (possibly dummy) in the given direction.
- inner(dir): return True if a neighbor in the given direction is *dummy*.
- """
- def __init__(self, r=None, viewclass=VertexViewer):
- self.view = viewclass()
- self.ctrl = None
- super().__init__(r, d=1)
- def N(self, dir):
- assert dir == +1 or dir == -1
- v = self.ctrl.get(self.rank + dir, None)
- return [v] if v is not None else []
- def inner(self, dir):
- assert dir == +1 or dir == -1
- try:
- return any([x.dummy == 1 for x in self.N(dir)])
- except KeyError:
- return False
- except AttributeError:
- return False
- def __str__(self):
- s = "(%3d,%3d) x=%s" % (self.rank, self.pos, str(self.x))
- if self.dummy:
- s = "[d] %s" % s
- return s
- # ------------------------------------------------------------------------------
- class Layer(list):
- """
- Layer is where Sugiyama layout organises vertices in hierarchical lists.
- The placement of a vertex is done by the Sugiyama class, but it highly relies on
- the *ordering* of vertices in each layer to reduce crossings.
- This ordering depends on the neighbors found in the upper or lower layers.
- Attributes:
- layout (SugiyamaLayout): a reference to the sugiyama layout instance that
- contains this layer
- upper (Layer): a reference to the *upper* layer (rank-1)
- lower (Layer): a reference to the *lower* layer (rank+1)
- ccount (int) : number of crossings detected in this layer
- Methods:
- setup (layout): set initial attributes values from provided layout
- nextlayer(): returns *next* layer in the current layout's direction parameter.
- prevlayer(): returns *previous* layer in the current layout's direction parameter.
- order(): compute *optimal* ordering of vertices within the layer.
- """
- __r = None
- layout = None
- upper = None
- lower = None
- __x = 1.0
- ccount = None
- def __eq__(self, other):
- return super().__eq__(other)
- def __str__(self):
- s = "<Layer %d" % self.__r
- s += ", len=%d" % len(self)
- xc = self.ccount or "?"
- s += ", crossings=%s>" % xc
- return s
- def setup(self, layout):
- self.layout = layout
- r = layout.layers.index(self)
- self.__r = r
- if len(self) > 1:
- self.__x = 1.0 / (len(self) - 1)
- for i, v in enumerate(self):
- assert layout.grx[v].rank == r
- layout.grx[v].pos = i
- layout.grx[v].bar = i * self.__x
- if r > 0:
- self.upper = layout.layers[r - 1]
- if r < len(layout.layers) - 1:
- self.lower = layout.layers[r + 1]
- def nextlayer(self):
- return self.lower if self.layout.dirv == -1 else self.upper
- def prevlayer(self):
- return self.lower if self.layout.dirv == +1 else self.upper
- def order(self):
- sug = self.layout
- sug._edge_inverter()
- c = self._cc()
- if c > 0:
- for v in self:
- sug.grx[v].bar = self._meanvalueattr(v)
- # now resort layers l according to bar value:
- self.sort(key=lambda x: sug.grx[x].bar)
- # reduce & count crossings:
- c = self._ordering_reduce_crossings()
- # assign new position in layer l:
- for i, v in enumerate(self):
- sug.grx[v].pos = i
- sug.grx[v].bar = i * self.__x
- sug._edge_inverter()
- self.ccount = c
- return c
- def _meanvalueattr(self, v):
- """
- find new position of vertex v according to adjacency in prevlayer.
- position is given by the mean value of adjacent positions.
- experiments show that meanvalue heuristic performs better than median.
- """
- sug = self.layout
- if not self.prevlayer():
- return sug.grx[v].bar
- bars = [sug.grx[x].bar for x in self._neighbors(v)]
- return sug.grx[v].bar if len(bars) == 0 else float(sum(bars)) / len(bars)
- def _medianindex(self, v):
- """
- find new position of vertex v according to adjacency in layer l+dir.
- position is given by the median value of adjacent positions.
- median heuristic is proven to achieve at most 3 times the minimum
- of crossings (while barycenter achieve in theory the order of |V|)
- """
- assert self.prevlayer() != None
- N = self._neighbors(v)
- g = self.layout.grx
- pos = [g[x].pos for x in N]
- lp = len(pos)
- if lp == 0:
- return []
- pos.sort()
- pos = pos[:: self.layout.dirh]
- i, j = divmod(lp - 1, 2)
- return [pos[i]] if j == 0 else [pos[i], pos[i + j]]
- def _neighbors(self, v):
- """
- neighbors refer to upper/lower adjacent nodes.
- Note that v.N() provides neighbors of v in the graph, while
- this method provides the Vertex and DummyVertex adjacent to v in the
- upper or lower layer (depending on layout.dirv state).
- """
- assert self.layout.dag
- dirv = self.layout.dirv
- grxv = self.layout.grx[v]
- try: # (cache)
- return grxv.nvs[dirv]
- except AttributeError:
- grxv.nvs = {-1: v.N(-1), +1: v.N(+1)}
- if grxv.dummy:
- return grxv.nvs[dirv]
- # v is real, v.N are graph neigbors but we need layers neighbors
- for d in (-1, +1):
- tr = grxv.rank + d
- for i, x in enumerate(v.N(d)):
- if self.layout.grx[x].rank == tr:
- continue
- e = v.e_with(x)
- dum = self.layout.ctrls[e][tr]
- grxv.nvs[d][i] = dum
- return grxv.nvs[dirv]
- def _crossings(self):
- """
- counts (inefficently but at least accurately) the number of
- crossing edges between layer l and l+dirv.
- P[i][j] counts the number of crossings from j-th edge of vertex i.
- The total count of crossings is the sum of flattened P:
- x = sum(sum(P,[]))
- """
- g = self.layout.grx
- P = []
- for v in self:
- P.append([g[x].pos for x in self._neighbors(v)])
- for i, p in enumerate(P):
- candidates = sum(P[i + 1 :], [])
- for j, e in enumerate(p):
- p[j] = len(filter((lambda nx: nx < e), candidates))
- del candidates
- return P
- def _cc(self):
- """
- implementation of the efficient bilayer cross counting by insert-sort
- (see Barth & Mutzel paper "Simple and Efficient Bilayer Cross Counting")
- """
- g = self.layout.grx
- P = []
- for v in self:
- P.extend(sorted([g[x].pos for x in self._neighbors(v)]))
- # count inversions in P:
- s = []
- count = 0
- for i, p in enumerate(P):
- j = bisect(s, p)
- if j < i:
- count += i - j
- s.insert(j, p)
- return count
- def _ordering_reduce_crossings(self):
- assert self.layout.dag
- g = self.layout.grx
- N = len(self)
- X = 0
- for i, j in zip(range(N - 1), range(1, N)):
- vi = self[i]
- vj = self[j]
- ni = [g[v].bar for v in self._neighbors(vi)]
- Xij = Xji = 0
- for nj in [g[v].bar for v in self._neighbors(vj)]:
- x = len([nx for nx in ni if nx > nj])
- Xij += x
- Xji += len(ni) - x
- if Xji < Xij:
- self[i] = vj
- self[j] = vi
- X += Xji
- else:
- X += Xij
- return X
- # ------------------------------------------------------------------------------
- class SugiyamaLayout(object):
- """
- The Sugiyama layout is the traditional "layered" graph layout called
- *dot* in graphviz. This layout is quite efficient but heavily relies
- on drawing heuristics. Adaptive drawing is limited to
- extending the leaves only, but since the algorithm is quite fast
- redrawing the entire graph (up to about a thousand nodes) gives
- usually good results in less than a second.
- The Sugiyama Layout Class takes as input a core_graph object and implements
- an efficient drawing algorithm based on nodes dimensions provided through
- a user-defined *view* property in each vertex.
- Attributes:
- dirvh (int): the current aligment state
- for alignment policy:
- dirvh=0 -> dirh=+1, dirv=-1: leftmost upper
- dirvh=1 -> dirh=-1, dirv=-1: rightmost upper
- dirvh=2 -> dirh=+1, dirv=+1: leftmost lower
- dirvh=3 -> dirh=-1, dirv=+1: rightmost lower
- order_inter (int): the default number of layer placement iterations
- order_attr (str): set attribute name used for layer ordering
- xspace (int): horizontal space between vertices in a layer
- yspace (int): vertical space between layers
- dw (int): default width of a vertex
- dh (int): default height of a vertex
- g (graph_core): the graph component reference
- layers (list[Layer]): the list of layers
- grx (dict): associate vertex (possibly dummy) with their sugiyama attributes
- ctrls (dict): associate edge with all its vertices (including dummies)
- dag (bool): the current acyclic state
- initdone (bool): True if state is initialized (see init_all).
- """
- def __init__(self, g):
- from mantis.grandalf.utils.geometry import median_wh
- # drawing parameters:
- self.dirvh = 0
- self.order_iter = 8
- self.order_attr = "pos"
- self.xspace = 20
- self.yspace = 20
- self.dw = 10
- self.dh = 10
- # For layered graphs, vertices and edges need to have some additional
- # attributes that make sense only for this kind of layout:
- # update graph struct:
- self.g = g
- self.layers = []
- self.grx = {}
- self.ctrls = {}
- self.dag = False
- for v in self.g.V():
- assert hasattr(v, "view")
- self.grx[v] = _sugiyama_vertex_attr()
- self.dw, self.dh = median_wh([v.view for v in self.g.V()])
- self.initdone = False
- def init_all(self, roots=None, inverted_edges=None, optimize=False):
- """initializes the layout algorithm by computing roots (unless provided),
- inverted edges (unless provided), vertices ranks and creates all dummy
- vertices and layers.
-
- Parameters:
- roots (list[Vertex]): set *root* vertices (layer 0)
- inverted_edges (list[Edge]): set edges to invert to have a DAG.
- optimize (bool): optimize ranking if True (default False)
- """
- if self.initdone:
- return
- # For layered sugiyama algorithm, the input graph must be acyclic,
- # so we must provide a list of root nodes and a list of inverted edges.
- if roots is None:
- roots = [v for v in self.g.sV if len(v.e_in()) == 0]
- if inverted_edges is None:
- _ = self.g.get_scs_with_feedback(roots)
- inverted_edges = [x for x in self.g.sE if x.feedback]
- self.alt_e = inverted_edges
- # assign rank to all vertices:
- self.rank_all(roots, optimize)
- # add dummy vertex/edge for 'long' edges:
- for e in self.g.E():
- self.setdummies(e)
- # precompute some layers values:
- for l in self.layers:
- l.setup(self)
- self.initdone = True
- def draw(self, N=1.5):
- """compute every node coordinates after converging to optimal ordering by N
- rounds, and finally perform the edge routing.
- """
- while N > 0.5:
- for (l, mvmt) in self.ordering_step():
- pass
- N = N - 1
- if N > 0:
- for (l, mvmt) in self.ordering_step(oneway=True):
- pass
- self.setxy()
- self.draw_edges()
- def _edge_inverter(self):
- for e in self.alt_e:
- x, y = e.v
- e.v = (y, x)
- self.dag = not self.dag
- if self.dag:
- for e in self.g.degenerated_edges:
- e.detach()
- self.g.sE.remove(e)
- else:
- for e in self.g.degenerated_edges:
- self.g.add_edge(e)
- @property
- def dirvh(self):
- return self.__dirvh
- @property
- def dirv(self):
- return self.__dirv
- @property
- def dirh(self):
- return self.__dirh
- @dirvh.setter
- def dirvh(self, dirvh):
- assert dirvh in range(4)
- self.__dirvh = dirvh
- self.__dirh, self.__dirv = {0: (1, -1),
- 1: (-1, -1),
- 2: (1, 1),
- 3: (-1, 1)}[dirvh]
- @dirv.setter
- def dirv(self, dirv):
- assert dirv in (-1, +1)
- dirvh = (dirv + 1) + (1 - self.__dirh) // 2
- self.dirvh = dirvh
- @dirh.setter
- def dirh(self, dirh):
- assert dirh in (-1, +1)
- dirvh = (self.__dirv + 1) + (1 - dirh) // 2
- self.dirvh = dirvh
- def rank_all(self, roots, optimize=False):
- """Computes rank of all vertices.
- add provided roots to rank 0 vertices,
- otherwise update ranking from provided roots.
- The initial rank is based on precedence relationships,
- optimal ranking may be derived from network flow (simplex).
- """
- self._edge_inverter()
- r = [x for x in self.g.sV if (len(x.e_in()) == 0 and x not in roots)]
- self._rank_init(roots + r)
- if optimize:
- self._rank_optimize()
- self._edge_inverter()
- def _rank_init(self, unranked):
- """Computes rank of provided unranked list of vertices and all
- their children. A vertex will be asign a rank when all its
- inward edges have been *scanned*. When a vertex is asigned
- a rank, its outward edges are marked *scanned*.
- """
- assert self.dag
- scan = {}
- # set rank of unranked based on its in-edges vertices ranks:
- while len(unranked) > 0:
- l = []
- for v in unranked:
- self.setrank(v)
- # mark out-edges has scan-able:
- for e in v.e_out():
- scan[e] = True
- # check if out-vertices are rank-able:
- for x in v.N(+1):
- if not (False in [scan.get(e, False) for e in x.e_in()]):
- if x not in l:
- l.append(x)
- unranked = l
- def _rank_optimize(self):
- """optimize ranking by pushing long edges toward lower layers as much as possible.
- see other interersting network flow solver to minimize total edge length
- (http://jgaa.info/accepted/2005/EiglspergerSiebenhallerKaufmann2005.9.3.pdf)
- """
- assert self.dag
- for l in reversed(self.layers):
- for v in l:
- gv = self.grx[v]
- for x in v.N(-1):
- if all((self.grx[y].rank >= gv.rank for y in x.N(+1))):
- gx = self.grx[x]
- self.layers[gx.rank].remove(x)
- gx.rank = gv.rank - 1
- self.layers[gv.rank - 1].append(x)
- def setrank(self, v):
- """set rank value for vertex v and add it to the corresponding layer.
- The Layer is created if it is the first vertex with this rank.
- """
- assert self.dag
- r = max([self.grx[x].rank for x in v.N(-1)] + [-1]) + 1
- self.grx[v].rank = r
- # add it to its layer:
- try:
- self.layers[r].append(v)
- except IndexError:
- assert r == len(self.layers)
- self.layers.append(Layer([v]))
- def dummyctrl(self, r, ctrl):
- """creates a DummyVertex at rank r inserted in the ctrl dict
- of the associated edge and layer.
- Arguments:
- r (int): rank value
- ctrl (dict): the edge's control vertices
-
- Returns:
- DummyVertex : the created DummyVertex.
- """
- dv = DummyVertex(r)
- dv.view.w, dv.view.h = self.dw, self.dh
- self.grx[dv] = dv
- dv.ctrl = ctrl
- ctrl[r] = dv
- self.layers[r].append(dv)
- return dv
- def setdummies(self, e):
- """creates and defines all needed dummy vertices for edge e.
- """
- v0, v1 = e.v
- r0, r1 = self.grx[v0].rank, self.grx[v1].rank
- if r0 > r1:
- assert e in self.alt_e
- v0, v1 = v1, v0
- r0, r1 = r1, r0
- if (r1 - r0) > 1:
- # "dummy vertices" are stored in the edge ctrl dict,
- # keyed by their rank in layers.
- ctrl = self.ctrls[e] = {}
- ctrl[r0] = v0
- ctrl[r1] = v1
- for r in range(r0 + 1, r1):
- self.dummyctrl(r, ctrl)
- def draw_step(self):
- """iterator that computes all vertices coordinates and edge routing after
- just one step (one layer after the other from top to bottom to top).
- Purely inefficient ! Use it only for "animation" or debugging purpose.
- """
- ostep = self.ordering_step()
- for s in ostep:
- self.setxy()
- self.draw_edges()
- yield s
- def ordering_step(self, oneway=False):
- """iterator that computes all vertices ordering in their layers
- (one layer after the other from top to bottom, to top again unless
- oneway is True).
- """
- self.dirv = -1
- crossings = 0
- for l in self.layers:
- mvmt = l.order()
- crossings += mvmt
- yield (l, mvmt)
- if oneway or (crossings == 0):
- return
- self.dirv = +1
- while l:
- mvmt = l.order()
- yield (l, mvmt)
- l = l.nextlayer()
- def setxy(self):
- """computes all vertex coordinates (x,y) using
- an algorithm by Brandes & Kopf.
- """
- self._edge_inverter()
- self._detect_alignment_conflicts()
- inf = float("infinity")
- # initialize vertex coordinates attributes:
- for l in self.layers:
- for v in l:
- self.grx[v].root = v
- self.grx[v].align = v
- self.grx[v].sink = v
- self.grx[v].shift = inf
- self.grx[v].X = None
- self.grx[v].x = [0.0] * 4
- curvh = self.dirvh # save current dirvh value
- for dirvh in range(4):
- self.dirvh = dirvh
- self._coord_vertical_alignment()
- self._coord_horizontal_compact()
- self.dirvh = curvh # restore it
- # vertical coordinate assigment of all nodes:
- Y = 0
- for l in self.layers:
- dY = max([v.view.h / 2.0 for v in l])
- for v in l:
- vx = sorted(self.grx[v].x)
- # mean of the 2 medians out of the 4 x-coord computed above:
- avgm = (vx[1] + vx[2]) / 2.0
- # final xy-coordinates :
- v.view.xy = (avgm, Y + dY)
- Y += 2 * dY + self.yspace
- self._edge_inverter()
- def _detect_alignment_conflicts(self):
- """mark conflicts between edges:
- inner edges are edges between dummy nodes
- type 0 is regular crossing regular (or sharing vertex)
- type 1 is inner crossing regular (targeted crossings)
- type 2 is inner crossing inner (avoided by reduce_crossings phase)
- """
- curvh = self.dirvh # save current dirvh value
- self.dirvh = 0
- self.conflicts = []
- for L in self.layers:
- last = len(L) - 1
- prev = L.prevlayer()
- if not prev:
- continue
- k0 = 0
- k1_init = len(prev) - 1
- l = 0
- for l1, v in enumerate(L):
- if not self.grx[v].dummy:
- continue
- if l1 == last or v.inner(-1):
- k1 = k1_init
- if v.inner(-1):
- k1 = self.grx[v.N(-1)[-1]].pos
- for vl in L[l : l1 + 1]:
- for vk in L._neighbors(vl):
- k = self.grx[vk].pos
- if k < k0 or k > k1:
- self.conflicts.append((vk, vl))
- l = l1 + 1
- k0 = k1
- self.dirvh = curvh # restore it
- def _coord_vertical_alignment(self):
- """performs vertical alignment according to current dirvh internal state.
- """
- dirh, dirv = self.dirh, self.dirv
- g = self.grx
- for l in self.layers[::-dirv]:
- if not l.prevlayer():
- continue
- r = None
- for vk in l[::dirh]:
- for m in l._medianindex(vk):
- # take the median node in dirv layer:
- um = l.prevlayer()[m]
- # if vk is "free" align it with um's root
- if g[vk].align is vk:
- if dirv == 1:
- vpair = (vk, um)
- else:
- vpair = (um, vk)
- # if vk<->um link is used for alignment
- if (vpair not in self.conflicts) and (
- (r is None) or (dirh * r < dirh * m)
- ):
- g[um].align = vk
- g[vk].root = g[um].root
- g[vk].align = g[vk].root
- r = m
- def _coord_horizontal_compact(self):
- limit = getrecursionlimit()
- N = len(self.layers) + 10
- if N > limit:
- setrecursionlimit(N)
- dirh, dirv = self.dirh, self.dirv
- g = self.grx
- L = self.layers[::-dirv]
- # recursive placement of blocks:
- for l in L:
- for v in l[::dirh]:
- if g[v].root is v:
- self.__place_block(v)
- setrecursionlimit(limit)
- # mirror all nodes if right-aligned:
- if dirh == -1:
- for l in L:
- for v in l:
- x = g[v].X
- if x:
- g[v].X = -x
- # then assign x-coord of its root:
- inf = float("infinity")
- rb = inf
- for l in L:
- for v in l[::dirh]:
- g[v].x[self.dirvh] = g[g[v].root].X
- rs = g[g[v].root].sink
- s = g[rs].shift
- if s < inf:
- g[v].x[self.dirvh] += dirh * s
- rb = min(rb, g[v].x[self.dirvh])
- # normalize to 0, and reinit root/align/sink/shift/X
- for l in self.layers:
- for v in l:
- # g[v].x[dirvh] -= rb
- g[v].root = g[v].align = g[v].sink = v
- g[v].shift = inf
- g[v].X = None
- # TODO: rewrite in iterative form to avoid recursion limit...
- def __place_block(self, v):
- g = self.grx
- if g[v].X is None:
- # every block is initially placed at x=0
- g[v].X = 0.0
- # place block in which v belongs:
- w = v
- while 1:
- j = g[w].pos - self.dirh # predecessor in rank must be placed
- r = g[w].rank
- if 0 <= j < len(self.layers[r]):
- wprec = self.layers[r][j]
- delta = (
- self.xspace + (wprec.view.w + w.view.w) / 2.0
- ) # abs positive minimum displ.
- # take root and place block:
- u = g[wprec].root
- self.__place_block(u)
- # set sink as sink of prec-block root
- if g[v].sink is v:
- g[v].sink = g[u].sink
- if g[v].sink != g[u].sink:
- s = g[u].sink
- newshift = g[v].X - (g[u].X + delta)
- g[s].shift = min(g[s].shift, newshift)
- else:
- g[v].X = max(g[v].X, (g[u].X + delta))
- # take next node to align in block:
- w = g[w].align
- # quit if self aligned
- if w is v:
- break
- def draw_edges(self):
- """Basic edge routing applied only for edges with dummy points.
- Enhanced edge routing can be performed by using the apropriate
- *route_with_xxx* functions from :ref:routing_ in the edges' view.
- """
- for e in self.g.E():
- if hasattr(e, "view"):
- l = []
- if e in self.ctrls:
- D = self.ctrls[e]
- r0, r1 = self.grx[e.v[0]].rank, self.grx[e.v[1]].rank
- if r0 < r1:
- ranks = range(r0 + 1, r1)
- else:
- ranks = range(r0 - 1, r1, -1)
- l = [D[r].view.xy for r in ranks]
- l.insert(0, e.v[0].view.xy)
- l.append(e.v[1].view.xy)
- try:
- self.route_edge(e, l)
- except AttributeError:
- pass
- e.view.setpath(l)
- # DIRECTED GRAPH WITH CONSTRAINTS LAYOUT
- # ------------------------------------------------------------------------------
- class DigcoLayout(object):
- linalg = importlib.import_module("mantis.grandalf.utils.geometry")
- def __init__(self, g):
- # drawing parameters:
- self.xspace = 10
- self.yspace = 10
- self.dr = 10
- self.debug = False
- self.g = g
- self.levels = []
- for i, v in enumerate(self.g.V()):
- assert hasattr(v, "view")
- v.i = i
- self.dr = max((self.dr, v.view.w, v.view.h))
- # solver parameters:
- self._cg_max_iter = g.order()
- self._cg_tolerance = 1.0e-6
- self._eps = 1.0e-5
- self._cv_max_iter = self._cg_max_iter
- def init_all(self, alpha=0.1, beta=0.01):
- y = None
- if self.g.directed:
- # partition g in hierarchical levels:
- y = self.part_to_levels(alpha, beta)
- # initiate positions (y and random in x):
- self.Z = self._xyinit(y)
- def draw(self, N=None):
- if N is None:
- N = self._cv_max_iter
- self.Z = self._optimize(self.Z, limit=N)
- # set view xy from near-optimal coords matrix:
- for v in self.g.V():
- v.view.xy = (self.Z[v.i][0, 0] * self.dr, self.Z[v.i][0, 1] * self.dr)
- self.draw_edges()
- def draw_step(self):
- for x in range(self._cv_max_iter):
- self.draw(N=1)
- self.draw_edges()
- yield
- # Basic edge routing with segments
- def draw_edges(self):
- for e in self.g.E():
- if hasattr(e, "view"):
- l = [e.v[0].view.xy, e.v[1].view.xy]
- try:
- self.route_edge(e, l)
- except AttributeError:
- pass
- e.view.setpath(l)
- # partition the nodes into levels:
- def part_to_levels(self, alpha, beta):
- opty, err = self.optimal_arrangement()
- ordering = list(zip(opty, self.g.sV))
- eps = alpha * (opty.max() - opty.min()) / (len(opty) - 1)
- eps = max(beta, eps)
- sorted(ordering, reverse=True)
- l = []
- self.levels.append(l)
- for i in range(len(list(ordering)) - 1):
- y, v = ordering[i]
- l.append(v)
- v.level = self.levels.index(l)
- if (y - ordering[i + 1][0]) > eps:
- l = []
- self.levels.append(l)
- y, v = ordering[-1]
- l.append(v)
- v.level = self.levels.index(l)
- return opty
- def optimal_arrangement(self):
- b = self.balance()
- y = DigcoLayout.linalg.rand_ortho1(self.g.order())
- return self._conjugate_gradient_L(y, b)
- # balance vector is assembled in finite-element way...
- # this is faster than computing b[i] for each i.
- def balance(self):
- b = DigcoLayout.linalg.array([0.0] * self.g.order(), dtype=float)
- for e in self.g.E():
- s = e.v[0]
- d = e.v[1]
- q = e.w * (self.yspace + (s.view.h + d.view.h) / 2.0)
- b[s.i] += q
- b[d.i] -= q
- return b
- # We compute the solution Y of L.Y = b by conjugate gradient method
- # (L is semi-definite positive so Y is unique and convergence is O(n))
- # note that only arrays are involved here...
- def _conjugate_gradient_L(self, y, b):
- Lii = self.__Lii_()
- r = b - self.__L_pk(Lii, y)
- p = DigcoLayout.linalg.array(r, copy=True)
- rr = sum(r * r)
- for k in range(self._cg_max_iter):
- try:
- Lp = self.__L_pk(Lii, p)
- alpha = rr / sum(p * Lp)
- y += alpha / p
- r -= alpha * Lp
- newrr = sum(r * r)
- beta = newrr / rr
- rr = newrr
- if rr < self._cg_tolerance:
- break
- p = r + beta * p
- except ZeroDivisionError:
- return (None, rr)
- return (y, rr)
- # _xyinit can use diagonally scaled initial vertices positioning to provide
- # better convergence in constrained stress majorization
- def _xyinit(self, y=None):
- if y is None:
- y = DigcoLayout.linalg.rand_ortho1(self.g.order())
- x = DigcoLayout.linalg.rand_ortho1(self.g.order())
- # translate and normalize:
- x = x - x[0]
- y = y - y[0]
- sfactor = 1.0 / max(list(map(abs, y)) + list(map(abs, x)))
- return DigcoLayout.linalg.matrix(list(zip(x * sfactor, y * sfactor)))
- # provide the diagonal of the Laplacian matrix of g
- # the rest of L (sparse!) is already stored in every edges.
- def __Lii_(self):
- Lii = []
- for v in self.g.V():
- Lii.append(sum([e.w for e in v.e]))
- return DigcoLayout.linalg.array(Lii, dtype=float)
- # we don't compute the L.Pk matrix/vector product here since
- # L is sparse (order of |E| not |V|^2 !) so we let each edge
- # contribute to the resulting L.Pk vector in a FE assembly way...
- def __L_pk(self, Lii, pk):
- y = Lii * pk
- for e in self.g.sE:
- i1 = e.v[0].i
- i2 = e.v[1].i
- y[i1] -= e.w * pk[i2]
- y[i2] -= e.w * pk[i1]
- return y
- # conjugate_gradient with given matrix Lw:
- # it is assumed that b is not a multivector,
- # so _cg_Lw should be called in all directions separately.
- # note that everything is a matrix here, (arrays are row vectors only)
- def _cg_Lw(self, Lw, z, b):
- scal = lambda U, V: float(U.transpose() * V)
- r = b - Lw * z
- p = r.copy()
- rr = scal(r, r)
- for k in range(self._cg_max_iter):
- if rr < self._cg_tolerance:
- break
- Lp = Lw * p
- alpha = rr / scal(p, Lp)
- z = z + alpha * p
- r = r - alpha * Lp
- newrr = scal(r, r)
- beta = newrr / rr
- rr = newrr
- p = r + beta * p
- return (z, rr)
- def __Dij_(self):
- Dji = []
- for v in self.g.V():
- wd = self.g.dijkstra(v)
- Di = [wd[w] for w in self.g.V()]
- Dji.append(Di)
- # at this point D is stored by rows,
- # but anymway it's a symmetric matrix
- return DigcoLayout.linalg.matrix(Dji, dtype=float)
- # returns matrix -L^w
- def __Lij_w_(self):
- self.Dij = self.__Dij_() # we keep D also for L^Z computations
- Lij = self.Dij.copy()
- n = self.g.order()
- for i in range(n):
- d = 0
- for j in range(n):
- if j == i:
- continue
- Lij[i, j] = 1.0 / self.Dij[i, j] ** 2
- d += Lij[i, j]
- Lij[i, i] = -d
- return Lij
- # returns vector -L^Z.Z:
- def __Lij_Z_Z(self, Z):
- n = self.g.order()
- # init:
- lzz = Z.copy() * 0.0 # lzz has dim Z (n x 2)
- liz = DigcoLayout.linalg.matrix([0.0] * n) # liz is a row of L^Z (size n)
- # compute lzz = L^Z.Z while assembling L^Z by row (liz):
- for i in range(n):
- iterk_except_i = (k for k in range(n) if k != i)
- for k in iterk_except_i:
- v = Z[i] - Z[k]
- liz[0, k] = 1.0 / (
- self.Dij[i, k] * DigcoLayout.linalg.sqrt(v * v.transpose())
- )
- liz[0, i] = 0.0 # forced, otherwise next liz.sum() is wrong !
- liz[0, i] = -liz.sum()
- # now that we have the i-th row of L^Z, just dotprod with Z:
- lzz[i] = liz * Z
- return lzz
- def _optimize(self, Z, limit=100):
- Lw = self.__Lij_w_()
- K = self.g.order() * (self.g.order() - 1.0) / 2.0
- stress = float("inf")
- count = 0
- deep = 0
- b = self.__Lij_Z_Z(Z)
- while count < limit:
- if self.debug:
- print("count %d" % count)
- print("Z = ", Z)
- print("b = ", b)
- # find next Z by solving Lw.Z = b in every direction:
- x, xerr = self._cg_Lw(Lw[1:, 1:], Z[1:, 0], b[1:, 0])
- y, yerr = self._cg_Lw(Lw[1:, 1:], Z[1:, 1], b[1:, 1])
- Z[1:, 0] = x
- Z[1:, 1] = y
- if self.debug:
- print(" cg -> ")
- print(Z, xerr, yerr)
- # compute new stress:
- FZ = K - float(x.transpose() * b[1:, 0] + y.transpose() * b[1:, 1])
- # precompute new b:
- b = self.__Lij_Z_Z(Z)
- # update new stress:
- FZ += 2 * float(x.transpose() * b[1:, 0] + y.transpose() * b[1:, 1])
- # test convergence:
- print("stress=%.10f" % FZ)
- if stress == 0.0:
- break
- elif abs((stress - FZ) / stress) < self._eps:
- if deep == 2:
- break
- else:
- deep += 1
- stress = FZ
- count += 1
- return Z
- # ------------------------------------------------------------------------------
- class DwyerLayout(DigcoLayout):
- pass
|