diagram_drawing.py 93 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591
  1. r"""
  2. This module contains the functionality to arrange the nodes of a
  3. diagram on an abstract grid, and then to produce a graphical
  4. representation of the grid.
  5. The currently supported back-ends are Xy-pic [Xypic].
  6. Layout Algorithm
  7. ================
  8. This section provides an overview of the algorithms implemented in
  9. :class:`DiagramGrid` to lay out diagrams.
  10. The first step of the algorithm is the removal composite and identity
  11. morphisms which do not have properties in the supplied diagram. The
  12. premises and conclusions of the diagram are then merged.
  13. The generic layout algorithm begins with the construction of the
  14. "skeleton" of the diagram. The skeleton is an undirected graph which
  15. has the objects of the diagram as vertices and has an (undirected)
  16. edge between each pair of objects between which there exist morphisms.
  17. The direction of the morphisms does not matter at this stage. The
  18. skeleton also includes an edge between each pair of vertices `A` and
  19. `C` such that there exists an object `B` which is connected via
  20. a morphism to `A`, and via a morphism to `C`.
  21. The skeleton constructed in this way has the property that every
  22. object is a vertex of a triangle formed by three edges of the
  23. skeleton. This property lies at the base of the generic layout
  24. algorithm.
  25. After the skeleton has been constructed, the algorithm lists all
  26. triangles which can be formed. Note that some triangles will not have
  27. all edges corresponding to morphisms which will actually be drawn.
  28. Triangles which have only one edge or less which will actually be
  29. drawn are immediately discarded.
  30. The list of triangles is sorted according to the number of edges which
  31. correspond to morphisms, then the triangle with the least number of such
  32. edges is selected. One of such edges is picked and the corresponding
  33. objects are placed horizontally, on a grid. This edge is recorded to
  34. be in the fringe. The algorithm then finds a "welding" of a triangle
  35. to the fringe. A welding is an edge in the fringe where a triangle
  36. could be attached. If the algorithm succeeds in finding such a
  37. welding, it adds to the grid that vertex of the triangle which was not
  38. yet included in any edge in the fringe and records the two new edges in
  39. the fringe. This process continues iteratively until all objects of
  40. the diagram has been placed or until no more weldings can be found.
  41. An edge is only removed from the fringe when a welding to this edge
  42. has been found, and there is no room around this edge to place
  43. another vertex.
  44. When no more weldings can be found, but there are still triangles
  45. left, the algorithm searches for a possibility of attaching one of the
  46. remaining triangles to the existing structure by a vertex. If such a
  47. possibility is found, the corresponding edge of the found triangle is
  48. placed in the found space and the iterative process of welding
  49. triangles restarts.
  50. When logical groups are supplied, each of these groups is laid out
  51. independently. Then a diagram is constructed in which groups are
  52. objects and any two logical groups between which there exist morphisms
  53. are connected via a morphism. This diagram is laid out. Finally,
  54. the grid which includes all objects of the initial diagram is
  55. constructed by replacing the cells which contain logical groups with
  56. the corresponding laid out grids, and by correspondingly expanding the
  57. rows and columns.
  58. The sequential layout algorithm begins by constructing the
  59. underlying undirected graph defined by the morphisms obtained after
  60. simplifying premises and conclusions and merging them (see above).
  61. The vertex with the minimal degree is then picked up and depth-first
  62. search is started from it. All objects which are located at distance
  63. `n` from the root in the depth-first search tree, are positioned in
  64. the `n`-th column of the resulting grid. The sequential layout will
  65. therefore attempt to lay the objects out along a line.
  66. References
  67. ==========
  68. .. [Xypic] https://xy-pic.sourceforge.net/
  69. """
  70. from sympy.categories import (CompositeMorphism, IdentityMorphism,
  71. NamedMorphism, Diagram)
  72. from sympy.core import Dict, Symbol, default_sort_key
  73. from sympy.printing.latex import latex
  74. from sympy.sets import FiniteSet
  75. from sympy.utilities.iterables import iterable
  76. from sympy.utilities.decorator import doctest_depends_on
  77. from itertools import chain
  78. __doctest_requires__ = {('preview_diagram',): 'pyglet'}
  79. class _GrowableGrid:
  80. """
  81. Holds a growable grid of objects.
  82. Explanation
  83. ===========
  84. It is possible to append or prepend a row or a column to the grid
  85. using the corresponding methods. Prepending rows or columns has
  86. the effect of changing the coordinates of the already existing
  87. elements.
  88. This class currently represents a naive implementation of the
  89. functionality with little attempt at optimisation.
  90. """
  91. def __init__(self, width, height):
  92. self._width = width
  93. self._height = height
  94. self._array = [[None for j in range(width)] for i in range(height)]
  95. @property
  96. def width(self):
  97. return self._width
  98. @property
  99. def height(self):
  100. return self._height
  101. def __getitem__(self, i_j):
  102. """
  103. Returns the element located at in the i-th line and j-th
  104. column.
  105. """
  106. i, j = i_j
  107. return self._array[i][j]
  108. def __setitem__(self, i_j, newvalue):
  109. """
  110. Sets the element located at in the i-th line and j-th
  111. column.
  112. """
  113. i, j = i_j
  114. self._array[i][j] = newvalue
  115. def append_row(self):
  116. """
  117. Appends an empty row to the grid.
  118. """
  119. self._height += 1
  120. self._array.append([None for j in range(self._width)])
  121. def append_column(self):
  122. """
  123. Appends an empty column to the grid.
  124. """
  125. self._width += 1
  126. for i in range(self._height):
  127. self._array[i].append(None)
  128. def prepend_row(self):
  129. """
  130. Prepends the grid with an empty row.
  131. """
  132. self._height += 1
  133. self._array.insert(0, [None for j in range(self._width)])
  134. def prepend_column(self):
  135. """
  136. Prepends the grid with an empty column.
  137. """
  138. self._width += 1
  139. for i in range(self._height):
  140. self._array[i].insert(0, None)
  141. class DiagramGrid:
  142. r"""
  143. Constructs and holds the fitting of the diagram into a grid.
  144. Explanation
  145. ===========
  146. The mission of this class is to analyse the structure of the
  147. supplied diagram and to place its objects on a grid such that,
  148. when the objects and the morphisms are actually drawn, the diagram
  149. would be "readable", in the sense that there will not be many
  150. intersections of moprhisms. This class does not perform any
  151. actual drawing. It does strive nevertheless to offer sufficient
  152. metadata to draw a diagram.
  153. Consider the following simple diagram.
  154. >>> from sympy.categories import Object, NamedMorphism
  155. >>> from sympy.categories import Diagram, DiagramGrid
  156. >>> from sympy import pprint
  157. >>> A = Object("A")
  158. >>> B = Object("B")
  159. >>> C = Object("C")
  160. >>> f = NamedMorphism(A, B, "f")
  161. >>> g = NamedMorphism(B, C, "g")
  162. >>> diagram = Diagram([f, g])
  163. The simplest way to have a diagram laid out is the following:
  164. >>> grid = DiagramGrid(diagram)
  165. >>> (grid.width, grid.height)
  166. (2, 2)
  167. >>> pprint(grid)
  168. A B
  169. <BLANKLINE>
  170. C
  171. Sometimes one sees the diagram as consisting of logical groups.
  172. One can advise ``DiagramGrid`` as to such groups by employing the
  173. ``groups`` keyword argument.
  174. Consider the following diagram:
  175. >>> D = Object("D")
  176. >>> f = NamedMorphism(A, B, "f")
  177. >>> g = NamedMorphism(B, C, "g")
  178. >>> h = NamedMorphism(D, A, "h")
  179. >>> k = NamedMorphism(D, B, "k")
  180. >>> diagram = Diagram([f, g, h, k])
  181. Lay it out with generic layout:
  182. >>> grid = DiagramGrid(diagram)
  183. >>> pprint(grid)
  184. A B D
  185. <BLANKLINE>
  186. C
  187. Now, we can group the objects `A` and `D` to have them near one
  188. another:
  189. >>> grid = DiagramGrid(diagram, groups=[[A, D], B, C])
  190. >>> pprint(grid)
  191. B C
  192. <BLANKLINE>
  193. A D
  194. Note how the positioning of the other objects changes.
  195. Further indications can be supplied to the constructor of
  196. :class:`DiagramGrid` using keyword arguments. The currently
  197. supported hints are explained in the following paragraphs.
  198. :class:`DiagramGrid` does not automatically guess which layout
  199. would suit the supplied diagram better. Consider, for example,
  200. the following linear diagram:
  201. >>> E = Object("E")
  202. >>> f = NamedMorphism(A, B, "f")
  203. >>> g = NamedMorphism(B, C, "g")
  204. >>> h = NamedMorphism(C, D, "h")
  205. >>> i = NamedMorphism(D, E, "i")
  206. >>> diagram = Diagram([f, g, h, i])
  207. When laid out with the generic layout, it does not get to look
  208. linear:
  209. >>> grid = DiagramGrid(diagram)
  210. >>> pprint(grid)
  211. A B
  212. <BLANKLINE>
  213. C D
  214. <BLANKLINE>
  215. E
  216. To get it laid out in a line, use ``layout="sequential"``:
  217. >>> grid = DiagramGrid(diagram, layout="sequential")
  218. >>> pprint(grid)
  219. A B C D E
  220. One may sometimes need to transpose the resulting layout. While
  221. this can always be done by hand, :class:`DiagramGrid` provides a
  222. hint for that purpose:
  223. >>> grid = DiagramGrid(diagram, layout="sequential", transpose=True)
  224. >>> pprint(grid)
  225. A
  226. <BLANKLINE>
  227. B
  228. <BLANKLINE>
  229. C
  230. <BLANKLINE>
  231. D
  232. <BLANKLINE>
  233. E
  234. Separate hints can also be provided for each group. For an
  235. example, refer to ``tests/test_drawing.py``, and see the different
  236. ways in which the five lemma [FiveLemma] can be laid out.
  237. See Also
  238. ========
  239. Diagram
  240. References
  241. ==========
  242. .. [FiveLemma] https://en.wikipedia.org/wiki/Five_lemma
  243. """
  244. @staticmethod
  245. def _simplify_morphisms(morphisms):
  246. """
  247. Given a dictionary mapping morphisms to their properties,
  248. returns a new dictionary in which there are no morphisms which
  249. do not have properties, and which are compositions of other
  250. morphisms included in the dictionary. Identities are dropped
  251. as well.
  252. """
  253. newmorphisms = {}
  254. for morphism, props in morphisms.items():
  255. if isinstance(morphism, CompositeMorphism) and not props:
  256. continue
  257. elif isinstance(morphism, IdentityMorphism):
  258. continue
  259. else:
  260. newmorphisms[morphism] = props
  261. return newmorphisms
  262. @staticmethod
  263. def _merge_premises_conclusions(premises, conclusions):
  264. """
  265. Given two dictionaries of morphisms and their properties,
  266. produces a single dictionary which includes elements from both
  267. dictionaries. If a morphism has some properties in premises
  268. and also in conclusions, the properties in conclusions take
  269. priority.
  270. """
  271. return dict(chain(premises.items(), conclusions.items()))
  272. @staticmethod
  273. def _juxtapose_edges(edge1, edge2):
  274. """
  275. If ``edge1`` and ``edge2`` have precisely one common endpoint,
  276. returns an edge which would form a triangle with ``edge1`` and
  277. ``edge2``.
  278. If ``edge1`` and ``edge2`` do not have a common endpoint,
  279. returns ``None``.
  280. If ``edge1`` and ``edge`` are the same edge, returns ``None``.
  281. """
  282. intersection = edge1 & edge2
  283. if len(intersection) != 1:
  284. # The edges either have no common points or are equal.
  285. return None
  286. # The edges have a common endpoint. Extract the different
  287. # endpoints and set up the new edge.
  288. return (edge1 - intersection) | (edge2 - intersection)
  289. @staticmethod
  290. def _add_edge_append(dictionary, edge, elem):
  291. """
  292. If ``edge`` is not in ``dictionary``, adds ``edge`` to the
  293. dictionary and sets its value to ``[elem]``. Otherwise
  294. appends ``elem`` to the value of existing entry.
  295. Note that edges are undirected, thus `(A, B) = (B, A)`.
  296. """
  297. if edge in dictionary:
  298. dictionary[edge].append(elem)
  299. else:
  300. dictionary[edge] = [elem]
  301. @staticmethod
  302. def _build_skeleton(morphisms):
  303. """
  304. Creates a dictionary which maps edges to corresponding
  305. morphisms. Thus for a morphism `f:A\rightarrow B`, the edge
  306. `(A, B)` will be associated with `f`. This function also adds
  307. to the list those edges which are formed by juxtaposition of
  308. two edges already in the list. These new edges are not
  309. associated with any morphism and are only added to assure that
  310. the diagram can be decomposed into triangles.
  311. """
  312. edges = {}
  313. # Create edges for morphisms.
  314. for morphism in morphisms:
  315. DiagramGrid._add_edge_append(
  316. edges, frozenset([morphism.domain, morphism.codomain]), morphism)
  317. # Create new edges by juxtaposing existing edges.
  318. edges1 = dict(edges)
  319. for w in edges1:
  320. for v in edges1:
  321. wv = DiagramGrid._juxtapose_edges(w, v)
  322. if wv and wv not in edges:
  323. edges[wv] = []
  324. return edges
  325. @staticmethod
  326. def _list_triangles(edges):
  327. """
  328. Builds the set of triangles formed by the supplied edges. The
  329. triangles are arbitrary and need not be commutative. A
  330. triangle is a set that contains all three of its sides.
  331. """
  332. triangles = set()
  333. for w in edges:
  334. for v in edges:
  335. wv = DiagramGrid._juxtapose_edges(w, v)
  336. if wv and wv in edges:
  337. triangles.add(frozenset([w, v, wv]))
  338. return triangles
  339. @staticmethod
  340. def _drop_redundant_triangles(triangles, skeleton):
  341. """
  342. Returns a list which contains only those triangles who have
  343. morphisms associated with at least two edges.
  344. """
  345. return [tri for tri in triangles
  346. if len([e for e in tri if skeleton[e]]) >= 2]
  347. @staticmethod
  348. def _morphism_length(morphism):
  349. """
  350. Returns the length of a morphism. The length of a morphism is
  351. the number of components it consists of. A non-composite
  352. morphism is of length 1.
  353. """
  354. if isinstance(morphism, CompositeMorphism):
  355. return len(morphism.components)
  356. else:
  357. return 1
  358. @staticmethod
  359. def _compute_triangle_min_sizes(triangles, edges):
  360. r"""
  361. Returns a dictionary mapping triangles to their minimal sizes.
  362. The minimal size of a triangle is the sum of maximal lengths
  363. of morphisms associated to the sides of the triangle. The
  364. length of a morphism is the number of components it consists
  365. of. A non-composite morphism is of length 1.
  366. Sorting triangles by this metric attempts to address two
  367. aspects of layout. For triangles with only simple morphisms
  368. in the edge, this assures that triangles with all three edges
  369. visible will get typeset after triangles with less visible
  370. edges, which sometimes minimizes the necessity in diagonal
  371. arrows. For triangles with composite morphisms in the edges,
  372. this assures that objects connected with shorter morphisms
  373. will be laid out first, resulting the visual proximity of
  374. those objects which are connected by shorter morphisms.
  375. """
  376. triangle_sizes = {}
  377. for triangle in triangles:
  378. size = 0
  379. for e in triangle:
  380. morphisms = edges[e]
  381. if morphisms:
  382. size += max(DiagramGrid._morphism_length(m)
  383. for m in morphisms)
  384. triangle_sizes[triangle] = size
  385. return triangle_sizes
  386. @staticmethod
  387. def _triangle_objects(triangle):
  388. """
  389. Given a triangle, returns the objects included in it.
  390. """
  391. # A triangle is a frozenset of three two-element frozensets
  392. # (the edges). This chains the three edges together and
  393. # creates a frozenset from the iterator, thus producing a
  394. # frozenset of objects of the triangle.
  395. return frozenset(chain(*tuple(triangle)))
  396. @staticmethod
  397. def _other_vertex(triangle, edge):
  398. """
  399. Given a triangle and an edge of it, returns the vertex which
  400. opposes the edge.
  401. """
  402. # This gets the set of objects of the triangle and then
  403. # subtracts the set of objects employed in ``edge`` to get the
  404. # vertex opposite to ``edge``.
  405. return list(DiagramGrid._triangle_objects(triangle) - set(edge))[0]
  406. @staticmethod
  407. def _empty_point(pt, grid):
  408. """
  409. Checks if the cell at coordinates ``pt`` is either empty or
  410. out of the bounds of the grid.
  411. """
  412. if (pt[0] < 0) or (pt[1] < 0) or \
  413. (pt[0] >= grid.height) or (pt[1] >= grid.width):
  414. return True
  415. return grid[pt] is None
  416. @staticmethod
  417. def _put_object(coords, obj, grid, fringe):
  418. """
  419. Places an object at the coordinate ``cords`` in ``grid``,
  420. growing the grid and updating ``fringe``, if necessary.
  421. Returns (0, 0) if no row or column has been prepended, (1, 0)
  422. if a row was prepended, (0, 1) if a column was prepended and
  423. (1, 1) if both a column and a row were prepended.
  424. """
  425. (i, j) = coords
  426. offset = (0, 0)
  427. if i == -1:
  428. grid.prepend_row()
  429. i = 0
  430. offset = (1, 0)
  431. for k in range(len(fringe)):
  432. ((i1, j1), (i2, j2)) = fringe[k]
  433. fringe[k] = ((i1 + 1, j1), (i2 + 1, j2))
  434. elif i == grid.height:
  435. grid.append_row()
  436. if j == -1:
  437. j = 0
  438. offset = (offset[0], 1)
  439. grid.prepend_column()
  440. for k in range(len(fringe)):
  441. ((i1, j1), (i2, j2)) = fringe[k]
  442. fringe[k] = ((i1, j1 + 1), (i2, j2 + 1))
  443. elif j == grid.width:
  444. grid.append_column()
  445. grid[i, j] = obj
  446. return offset
  447. @staticmethod
  448. def _choose_target_cell(pt1, pt2, edge, obj, skeleton, grid):
  449. """
  450. Given two points, ``pt1`` and ``pt2``, and the welding edge
  451. ``edge``, chooses one of the two points to place the opposing
  452. vertex ``obj`` of the triangle. If neither of this points
  453. fits, returns ``None``.
  454. """
  455. pt1_empty = DiagramGrid._empty_point(pt1, grid)
  456. pt2_empty = DiagramGrid._empty_point(pt2, grid)
  457. if pt1_empty and pt2_empty:
  458. # Both cells are empty. Of these two, choose that cell
  459. # which will assure that a visible edge of the triangle
  460. # will be drawn perpendicularly to the current welding
  461. # edge.
  462. A = grid[edge[0]]
  463. if skeleton.get(frozenset([A, obj])):
  464. return pt1
  465. else:
  466. return pt2
  467. if pt1_empty:
  468. return pt1
  469. elif pt2_empty:
  470. return pt2
  471. else:
  472. return None
  473. @staticmethod
  474. def _find_triangle_to_weld(triangles, fringe, grid):
  475. """
  476. Finds, if possible, a triangle and an edge in the ``fringe`` to
  477. which the triangle could be attached. Returns the tuple
  478. containing the triangle and the index of the corresponding
  479. edge in the ``fringe``.
  480. This function relies on the fact that objects are unique in
  481. the diagram.
  482. """
  483. for triangle in triangles:
  484. for (a, b) in fringe:
  485. if frozenset([grid[a], grid[b]]) in triangle:
  486. return (triangle, (a, b))
  487. return None
  488. @staticmethod
  489. def _weld_triangle(tri, welding_edge, fringe, grid, skeleton):
  490. """
  491. If possible, welds the triangle ``tri`` to ``fringe`` and
  492. returns ``False``. If this method encounters a degenerate
  493. situation in the fringe and corrects it such that a restart of
  494. the search is required, it returns ``True`` (which means that
  495. a restart in finding triangle weldings is required).
  496. A degenerate situation is a situation when an edge listed in
  497. the fringe does not belong to the visual boundary of the
  498. diagram.
  499. """
  500. a, b = welding_edge
  501. target_cell = None
  502. obj = DiagramGrid._other_vertex(tri, (grid[a], grid[b]))
  503. # We now have a triangle and an edge where it can be welded to
  504. # the fringe. Decide where to place the other vertex of the
  505. # triangle and check for degenerate situations en route.
  506. if (abs(a[0] - b[0]) == 1) and (abs(a[1] - b[1]) == 1):
  507. # A diagonal edge.
  508. target_cell = (a[0], b[1])
  509. if grid[target_cell]:
  510. # That cell is already occupied.
  511. target_cell = (b[0], a[1])
  512. if grid[target_cell]:
  513. # Degenerate situation, this edge is not
  514. # on the actual fringe. Correct the
  515. # fringe and go on.
  516. fringe.remove((a, b))
  517. return True
  518. elif a[0] == b[0]:
  519. # A horizontal edge. We first attempt to build the
  520. # triangle in the downward direction.
  521. down_left = a[0] + 1, a[1]
  522. down_right = a[0] + 1, b[1]
  523. target_cell = DiagramGrid._choose_target_cell(
  524. down_left, down_right, (a, b), obj, skeleton, grid)
  525. if not target_cell:
  526. # No room below this edge. Check above.
  527. up_left = a[0] - 1, a[1]
  528. up_right = a[0] - 1, b[1]
  529. target_cell = DiagramGrid._choose_target_cell(
  530. up_left, up_right, (a, b), obj, skeleton, grid)
  531. if not target_cell:
  532. # This edge is not in the fringe, remove it
  533. # and restart.
  534. fringe.remove((a, b))
  535. return True
  536. elif a[1] == b[1]:
  537. # A vertical edge. We will attempt to place the other
  538. # vertex of the triangle to the right of this edge.
  539. right_up = a[0], a[1] + 1
  540. right_down = b[0], a[1] + 1
  541. target_cell = DiagramGrid._choose_target_cell(
  542. right_up, right_down, (a, b), obj, skeleton, grid)
  543. if not target_cell:
  544. # No room to the left. See what's to the right.
  545. left_up = a[0], a[1] - 1
  546. left_down = b[0], a[1] - 1
  547. target_cell = DiagramGrid._choose_target_cell(
  548. left_up, left_down, (a, b), obj, skeleton, grid)
  549. if not target_cell:
  550. # This edge is not in the fringe, remove it
  551. # and restart.
  552. fringe.remove((a, b))
  553. return True
  554. # We now know where to place the other vertex of the
  555. # triangle.
  556. offset = DiagramGrid._put_object(target_cell, obj, grid, fringe)
  557. # Take care of the displacement of coordinates if a row or
  558. # a column was prepended.
  559. target_cell = (target_cell[0] + offset[0],
  560. target_cell[1] + offset[1])
  561. a = (a[0] + offset[0], a[1] + offset[1])
  562. b = (b[0] + offset[0], b[1] + offset[1])
  563. fringe.extend([(a, target_cell), (b, target_cell)])
  564. # No restart is required.
  565. return False
  566. @staticmethod
  567. def _triangle_key(tri, triangle_sizes):
  568. """
  569. Returns a key for the supplied triangle. It should be the
  570. same independently of the hash randomisation.
  571. """
  572. objects = sorted(
  573. DiagramGrid._triangle_objects(tri), key=default_sort_key)
  574. return (triangle_sizes[tri], default_sort_key(objects))
  575. @staticmethod
  576. def _pick_root_edge(tri, skeleton):
  577. """
  578. For a given triangle always picks the same root edge. The
  579. root edge is the edge that will be placed first on the grid.
  580. """
  581. candidates = [sorted(e, key=default_sort_key)
  582. for e in tri if skeleton[e]]
  583. sorted_candidates = sorted(candidates, key=default_sort_key)
  584. # Don't forget to assure the proper ordering of the vertices
  585. # in this edge.
  586. return tuple(sorted(sorted_candidates[0], key=default_sort_key))
  587. @staticmethod
  588. def _drop_irrelevant_triangles(triangles, placed_objects):
  589. """
  590. Returns only those triangles whose set of objects is not
  591. completely included in ``placed_objects``.
  592. """
  593. return [tri for tri in triangles if not placed_objects.issuperset(
  594. DiagramGrid._triangle_objects(tri))]
  595. @staticmethod
  596. def _grow_pseudopod(triangles, fringe, grid, skeleton, placed_objects):
  597. """
  598. Starting from an object in the existing structure on the ``grid``,
  599. adds an edge to which a triangle from ``triangles`` could be
  600. welded. If this method has found a way to do so, it returns
  601. the object it has just added.
  602. This method should be applied when ``_weld_triangle`` cannot
  603. find weldings any more.
  604. """
  605. for i in range(grid.height):
  606. for j in range(grid.width):
  607. obj = grid[i, j]
  608. if not obj:
  609. continue
  610. # Here we need to choose a triangle which has only
  611. # ``obj`` in common with the existing structure. The
  612. # situations when this is not possible should be
  613. # handled elsewhere.
  614. def good_triangle(tri):
  615. objs = DiagramGrid._triangle_objects(tri)
  616. return obj in objs and \
  617. placed_objects & (objs - {obj}) == set()
  618. tris = [tri for tri in triangles if good_triangle(tri)]
  619. if not tris:
  620. # This object is not interesting.
  621. continue
  622. # Pick the "simplest" of the triangles which could be
  623. # attached. Remember that the list of triangles is
  624. # sorted according to their "simplicity" (see
  625. # _compute_triangle_min_sizes for the metric).
  626. #
  627. # Note that ``tris`` are sequentially built from
  628. # ``triangles``, so we don't have to worry about hash
  629. # randomisation.
  630. tri = tris[0]
  631. # We have found a triangle which could be attached to
  632. # the existing structure by a vertex.
  633. candidates = sorted([e for e in tri if skeleton[e]],
  634. key=lambda e: FiniteSet(*e).sort_key())
  635. edges = [e for e in candidates if obj in e]
  636. # Note that a meaningful edge (i.e., and edge that is
  637. # associated with a morphism) containing ``obj``
  638. # always exists. That's because all triangles are
  639. # guaranteed to have at least two meaningful edges.
  640. # See _drop_redundant_triangles.
  641. # Get the object at the other end of the edge.
  642. edge = edges[0]
  643. other_obj = tuple(edge - frozenset([obj]))[0]
  644. # Now check for free directions. When checking for
  645. # free directions, prefer the horizontal and vertical
  646. # directions.
  647. neighbours = [(i - 1, j), (i, j + 1), (i + 1, j), (i, j - 1),
  648. (i - 1, j - 1), (i - 1, j + 1), (i + 1, j - 1), (i + 1, j + 1)]
  649. for pt in neighbours:
  650. if DiagramGrid._empty_point(pt, grid):
  651. # We have a found a place to grow the
  652. # pseudopod into.
  653. offset = DiagramGrid._put_object(
  654. pt, other_obj, grid, fringe)
  655. i += offset[0]
  656. j += offset[1]
  657. pt = (pt[0] + offset[0], pt[1] + offset[1])
  658. fringe.append(((i, j), pt))
  659. return other_obj
  660. # This diagram is actually cooler that I can handle. Fail cowardly.
  661. return None
  662. @staticmethod
  663. def _handle_groups(diagram, groups, merged_morphisms, hints):
  664. """
  665. Given the slightly preprocessed morphisms of the diagram,
  666. produces a grid laid out according to ``groups``.
  667. If a group has hints, it is laid out with those hints only,
  668. without any influence from ``hints``. Otherwise, it is laid
  669. out with ``hints``.
  670. """
  671. def lay_out_group(group, local_hints):
  672. """
  673. If ``group`` is a set of objects, uses a ``DiagramGrid``
  674. to lay it out and returns the grid. Otherwise returns the
  675. object (i.e., ``group``). If ``local_hints`` is not
  676. empty, it is supplied to ``DiagramGrid`` as the dictionary
  677. of hints. Otherwise, the ``hints`` argument of
  678. ``_handle_groups`` is used.
  679. """
  680. if isinstance(group, FiniteSet):
  681. # Set up the corresponding object-to-group
  682. # mappings.
  683. for obj in group:
  684. obj_groups[obj] = group
  685. # Lay out the current group.
  686. if local_hints:
  687. groups_grids[group] = DiagramGrid(
  688. diagram.subdiagram_from_objects(group), **local_hints)
  689. else:
  690. groups_grids[group] = DiagramGrid(
  691. diagram.subdiagram_from_objects(group), **hints)
  692. else:
  693. obj_groups[group] = group
  694. def group_to_finiteset(group):
  695. """
  696. Converts ``group`` to a :class:``FiniteSet`` if it is an
  697. iterable.
  698. """
  699. if iterable(group):
  700. return FiniteSet(*group)
  701. else:
  702. return group
  703. obj_groups = {}
  704. groups_grids = {}
  705. # We would like to support various containers to represent
  706. # groups. To achieve that, before laying each group out, it
  707. # should be converted to a FiniteSet, because that is what the
  708. # following code expects.
  709. if isinstance(groups, (dict, Dict)):
  710. finiteset_groups = {}
  711. for group, local_hints in groups.items():
  712. finiteset_group = group_to_finiteset(group)
  713. finiteset_groups[finiteset_group] = local_hints
  714. lay_out_group(group, local_hints)
  715. groups = finiteset_groups
  716. else:
  717. finiteset_groups = []
  718. for group in groups:
  719. finiteset_group = group_to_finiteset(group)
  720. finiteset_groups.append(finiteset_group)
  721. lay_out_group(finiteset_group, None)
  722. groups = finiteset_groups
  723. new_morphisms = []
  724. for morphism in merged_morphisms:
  725. dom = obj_groups[morphism.domain]
  726. cod = obj_groups[morphism.codomain]
  727. # Note that we are not really interested in morphisms
  728. # which do not employ two different groups, because
  729. # these do not influence the layout.
  730. if dom != cod:
  731. # These are essentially unnamed morphisms; they are
  732. # not going to mess in the final layout. By giving
  733. # them the same names, we avoid unnecessary
  734. # duplicates.
  735. new_morphisms.append(NamedMorphism(dom, cod, "dummy"))
  736. # Lay out the new diagram. Since these are dummy morphisms,
  737. # properties and conclusions are irrelevant.
  738. top_grid = DiagramGrid(Diagram(new_morphisms))
  739. # We now have to substitute the groups with the corresponding
  740. # grids, laid out at the beginning of this function. Compute
  741. # the size of each row and column in the grid, so that all
  742. # nested grids fit.
  743. def group_size(group):
  744. """
  745. For the supplied group (or object, eventually), returns
  746. the size of the cell that will hold this group (object).
  747. """
  748. if group in groups_grids:
  749. grid = groups_grids[group]
  750. return (grid.height, grid.width)
  751. else:
  752. return (1, 1)
  753. row_heights = [max(group_size(top_grid[i, j])[0]
  754. for j in range(top_grid.width))
  755. for i in range(top_grid.height)]
  756. column_widths = [max(group_size(top_grid[i, j])[1]
  757. for i in range(top_grid.height))
  758. for j in range(top_grid.width)]
  759. grid = _GrowableGrid(sum(column_widths), sum(row_heights))
  760. real_row = 0
  761. real_column = 0
  762. for logical_row in range(top_grid.height):
  763. for logical_column in range(top_grid.width):
  764. obj = top_grid[logical_row, logical_column]
  765. if obj in groups_grids:
  766. # This is a group. Copy the corresponding grid in
  767. # place.
  768. local_grid = groups_grids[obj]
  769. for i in range(local_grid.height):
  770. for j in range(local_grid.width):
  771. grid[real_row + i,
  772. real_column + j] = local_grid[i, j]
  773. else:
  774. # This is an object. Just put it there.
  775. grid[real_row, real_column] = obj
  776. real_column += column_widths[logical_column]
  777. real_column = 0
  778. real_row += row_heights[logical_row]
  779. return grid
  780. @staticmethod
  781. def _generic_layout(diagram, merged_morphisms):
  782. """
  783. Produces the generic layout for the supplied diagram.
  784. """
  785. all_objects = set(diagram.objects)
  786. if len(all_objects) == 1:
  787. # There only one object in the diagram, just put in on 1x1
  788. # grid.
  789. grid = _GrowableGrid(1, 1)
  790. grid[0, 0] = tuple(all_objects)[0]
  791. return grid
  792. skeleton = DiagramGrid._build_skeleton(merged_morphisms)
  793. grid = _GrowableGrid(2, 1)
  794. if len(skeleton) == 1:
  795. # This diagram contains only one morphism. Draw it
  796. # horizontally.
  797. objects = sorted(all_objects, key=default_sort_key)
  798. grid[0, 0] = objects[0]
  799. grid[0, 1] = objects[1]
  800. return grid
  801. triangles = DiagramGrid._list_triangles(skeleton)
  802. triangles = DiagramGrid._drop_redundant_triangles(triangles, skeleton)
  803. triangle_sizes = DiagramGrid._compute_triangle_min_sizes(
  804. triangles, skeleton)
  805. triangles = sorted(triangles, key=lambda tri:
  806. DiagramGrid._triangle_key(tri, triangle_sizes))
  807. # Place the first edge on the grid.
  808. root_edge = DiagramGrid._pick_root_edge(triangles[0], skeleton)
  809. grid[0, 0], grid[0, 1] = root_edge
  810. fringe = [((0, 0), (0, 1))]
  811. # Record which objects we now have on the grid.
  812. placed_objects = set(root_edge)
  813. while placed_objects != all_objects:
  814. welding = DiagramGrid._find_triangle_to_weld(
  815. triangles, fringe, grid)
  816. if welding:
  817. (triangle, welding_edge) = welding
  818. restart_required = DiagramGrid._weld_triangle(
  819. triangle, welding_edge, fringe, grid, skeleton)
  820. if restart_required:
  821. continue
  822. placed_objects.update(
  823. DiagramGrid._triangle_objects(triangle))
  824. else:
  825. # No more weldings found. Try to attach triangles by
  826. # vertices.
  827. new_obj = DiagramGrid._grow_pseudopod(
  828. triangles, fringe, grid, skeleton, placed_objects)
  829. if not new_obj:
  830. # No more triangles can be attached, not even by
  831. # the edge. We will set up a new diagram out of
  832. # what has been left, laid it out independently,
  833. # and then attach it to this one.
  834. remaining_objects = all_objects - placed_objects
  835. remaining_diagram = diagram.subdiagram_from_objects(
  836. FiniteSet(*remaining_objects))
  837. remaining_grid = DiagramGrid(remaining_diagram)
  838. # Now, let's glue ``remaining_grid`` to ``grid``.
  839. final_width = grid.width + remaining_grid.width
  840. final_height = max(grid.height, remaining_grid.height)
  841. final_grid = _GrowableGrid(final_width, final_height)
  842. for i in range(grid.width):
  843. for j in range(grid.height):
  844. final_grid[i, j] = grid[i, j]
  845. start_j = grid.width
  846. for i in range(remaining_grid.height):
  847. for j in range(remaining_grid.width):
  848. final_grid[i, start_j + j] = remaining_grid[i, j]
  849. return final_grid
  850. placed_objects.add(new_obj)
  851. triangles = DiagramGrid._drop_irrelevant_triangles(
  852. triangles, placed_objects)
  853. return grid
  854. @staticmethod
  855. def _get_undirected_graph(objects, merged_morphisms):
  856. """
  857. Given the objects and the relevant morphisms of a diagram,
  858. returns the adjacency lists of the underlying undirected
  859. graph.
  860. """
  861. adjlists = {}
  862. for obj in objects:
  863. adjlists[obj] = []
  864. for morphism in merged_morphisms:
  865. adjlists[morphism.domain].append(morphism.codomain)
  866. adjlists[morphism.codomain].append(morphism.domain)
  867. # Assure that the objects in the adjacency list are always in
  868. # the same order.
  869. for obj in adjlists.keys():
  870. adjlists[obj].sort(key=default_sort_key)
  871. return adjlists
  872. @staticmethod
  873. def _sequential_layout(diagram, merged_morphisms):
  874. r"""
  875. Lays out the diagram in "sequential" layout. This method
  876. will attempt to produce a result as close to a line as
  877. possible. For linear diagrams, the result will actually be a
  878. line.
  879. """
  880. objects = diagram.objects
  881. sorted_objects = sorted(objects, key=default_sort_key)
  882. # Set up the adjacency lists of the underlying undirected
  883. # graph of ``merged_morphisms``.
  884. adjlists = DiagramGrid._get_undirected_graph(objects, merged_morphisms)
  885. # Find an object with the minimal degree. This is going to be
  886. # the root.
  887. root = sorted_objects[0]
  888. mindegree = len(adjlists[root])
  889. for obj in sorted_objects:
  890. current_degree = len(adjlists[obj])
  891. if current_degree < mindegree:
  892. root = obj
  893. mindegree = current_degree
  894. grid = _GrowableGrid(1, 1)
  895. grid[0, 0] = root
  896. placed_objects = {root}
  897. def place_objects(pt, placed_objects):
  898. """
  899. Does depth-first search in the underlying graph of the
  900. diagram and places the objects en route.
  901. """
  902. # We will start placing new objects from here.
  903. new_pt = (pt[0], pt[1] + 1)
  904. for adjacent_obj in adjlists[grid[pt]]:
  905. if adjacent_obj in placed_objects:
  906. # This object has already been placed.
  907. continue
  908. DiagramGrid._put_object(new_pt, adjacent_obj, grid, [])
  909. placed_objects.add(adjacent_obj)
  910. placed_objects.update(place_objects(new_pt, placed_objects))
  911. new_pt = (new_pt[0] + 1, new_pt[1])
  912. return placed_objects
  913. place_objects((0, 0), placed_objects)
  914. return grid
  915. @staticmethod
  916. def _drop_inessential_morphisms(merged_morphisms):
  917. r"""
  918. Removes those morphisms which should appear in the diagram,
  919. but which have no relevance to object layout.
  920. Currently this removes "loop" morphisms: the non-identity
  921. morphisms with the same domains and codomains.
  922. """
  923. morphisms = [m for m in merged_morphisms if m.domain != m.codomain]
  924. return morphisms
  925. @staticmethod
  926. def _get_connected_components(objects, merged_morphisms):
  927. """
  928. Given a container of morphisms, returns a list of connected
  929. components formed by these morphisms. A connected component
  930. is represented by a diagram consisting of the corresponding
  931. morphisms.
  932. """
  933. component_index = {}
  934. for o in objects:
  935. component_index[o] = None
  936. # Get the underlying undirected graph of the diagram.
  937. adjlist = DiagramGrid._get_undirected_graph(objects, merged_morphisms)
  938. def traverse_component(object, current_index):
  939. """
  940. Does a depth-first search traversal of the component
  941. containing ``object``.
  942. """
  943. component_index[object] = current_index
  944. for o in adjlist[object]:
  945. if component_index[o] is None:
  946. traverse_component(o, current_index)
  947. # Traverse all components.
  948. current_index = 0
  949. for o in adjlist:
  950. if component_index[o] is None:
  951. traverse_component(o, current_index)
  952. current_index += 1
  953. # List the objects of the components.
  954. component_objects = [[] for i in range(current_index)]
  955. for o, idx in component_index.items():
  956. component_objects[idx].append(o)
  957. # Finally, list the morphisms belonging to each component.
  958. #
  959. # Note: If some objects are isolated, they will not get any
  960. # morphisms at this stage, and since the layout algorithm
  961. # relies, we are essentially going to lose this object.
  962. # Therefore, check if there are isolated objects and, for each
  963. # of them, provide the trivial identity morphism. It will get
  964. # discarded later, but the object will be there.
  965. component_morphisms = []
  966. for component in component_objects:
  967. current_morphisms = {}
  968. for m in merged_morphisms:
  969. if (m.domain in component) and (m.codomain in component):
  970. current_morphisms[m] = merged_morphisms[m]
  971. if len(component) == 1:
  972. # Let's add an identity morphism, for the sake of
  973. # surely having morphisms in this component.
  974. current_morphisms[IdentityMorphism(component[0])] = FiniteSet()
  975. component_morphisms.append(Diagram(current_morphisms))
  976. return component_morphisms
  977. def __init__(self, diagram, groups=None, **hints):
  978. premises = DiagramGrid._simplify_morphisms(diagram.premises)
  979. conclusions = DiagramGrid._simplify_morphisms(diagram.conclusions)
  980. all_merged_morphisms = DiagramGrid._merge_premises_conclusions(
  981. premises, conclusions)
  982. merged_morphisms = DiagramGrid._drop_inessential_morphisms(
  983. all_merged_morphisms)
  984. # Store the merged morphisms for later use.
  985. self._morphisms = all_merged_morphisms
  986. components = DiagramGrid._get_connected_components(
  987. diagram.objects, all_merged_morphisms)
  988. if groups and (groups != diagram.objects):
  989. # Lay out the diagram according to the groups.
  990. self._grid = DiagramGrid._handle_groups(
  991. diagram, groups, merged_morphisms, hints)
  992. elif len(components) > 1:
  993. # Note that we check for connectedness _before_ checking
  994. # the layout hints because the layout strategies don't
  995. # know how to deal with disconnected diagrams.
  996. # The diagram is disconnected. Lay out the components
  997. # independently.
  998. grids = []
  999. # Sort the components to eventually get the grids arranged
  1000. # in a fixed, hash-independent order.
  1001. components = sorted(components, key=default_sort_key)
  1002. for component in components:
  1003. grid = DiagramGrid(component, **hints)
  1004. grids.append(grid)
  1005. # Throw the grids together, in a line.
  1006. total_width = sum(g.width for g in grids)
  1007. total_height = max(g.height for g in grids)
  1008. grid = _GrowableGrid(total_width, total_height)
  1009. start_j = 0
  1010. for g in grids:
  1011. for i in range(g.height):
  1012. for j in range(g.width):
  1013. grid[i, start_j + j] = g[i, j]
  1014. start_j += g.width
  1015. self._grid = grid
  1016. elif "layout" in hints:
  1017. if hints["layout"] == "sequential":
  1018. self._grid = DiagramGrid._sequential_layout(
  1019. diagram, merged_morphisms)
  1020. else:
  1021. self._grid = DiagramGrid._generic_layout(diagram, merged_morphisms)
  1022. if hints.get("transpose"):
  1023. # Transpose the resulting grid.
  1024. grid = _GrowableGrid(self._grid.height, self._grid.width)
  1025. for i in range(self._grid.height):
  1026. for j in range(self._grid.width):
  1027. grid[j, i] = self._grid[i, j]
  1028. self._grid = grid
  1029. @property
  1030. def width(self):
  1031. """
  1032. Returns the number of columns in this diagram layout.
  1033. Examples
  1034. ========
  1035. >>> from sympy.categories import Object, NamedMorphism
  1036. >>> from sympy.categories import Diagram, DiagramGrid
  1037. >>> A = Object("A")
  1038. >>> B = Object("B")
  1039. >>> C = Object("C")
  1040. >>> f = NamedMorphism(A, B, "f")
  1041. >>> g = NamedMorphism(B, C, "g")
  1042. >>> diagram = Diagram([f, g])
  1043. >>> grid = DiagramGrid(diagram)
  1044. >>> grid.width
  1045. 2
  1046. """
  1047. return self._grid.width
  1048. @property
  1049. def height(self):
  1050. """
  1051. Returns the number of rows in this diagram layout.
  1052. Examples
  1053. ========
  1054. >>> from sympy.categories import Object, NamedMorphism
  1055. >>> from sympy.categories import Diagram, DiagramGrid
  1056. >>> A = Object("A")
  1057. >>> B = Object("B")
  1058. >>> C = Object("C")
  1059. >>> f = NamedMorphism(A, B, "f")
  1060. >>> g = NamedMorphism(B, C, "g")
  1061. >>> diagram = Diagram([f, g])
  1062. >>> grid = DiagramGrid(diagram)
  1063. >>> grid.height
  1064. 2
  1065. """
  1066. return self._grid.height
  1067. def __getitem__(self, i_j):
  1068. """
  1069. Returns the object placed in the row ``i`` and column ``j``.
  1070. The indices are 0-based.
  1071. Examples
  1072. ========
  1073. >>> from sympy.categories import Object, NamedMorphism
  1074. >>> from sympy.categories import Diagram, DiagramGrid
  1075. >>> A = Object("A")
  1076. >>> B = Object("B")
  1077. >>> C = Object("C")
  1078. >>> f = NamedMorphism(A, B, "f")
  1079. >>> g = NamedMorphism(B, C, "g")
  1080. >>> diagram = Diagram([f, g])
  1081. >>> grid = DiagramGrid(diagram)
  1082. >>> (grid[0, 0], grid[0, 1])
  1083. (Object("A"), Object("B"))
  1084. >>> (grid[1, 0], grid[1, 1])
  1085. (None, Object("C"))
  1086. """
  1087. i, j = i_j
  1088. return self._grid[i, j]
  1089. @property
  1090. def morphisms(self):
  1091. """
  1092. Returns those morphisms (and their properties) which are
  1093. sufficiently meaningful to be drawn.
  1094. Examples
  1095. ========
  1096. >>> from sympy.categories import Object, NamedMorphism
  1097. >>> from sympy.categories import Diagram, DiagramGrid
  1098. >>> A = Object("A")
  1099. >>> B = Object("B")
  1100. >>> C = Object("C")
  1101. >>> f = NamedMorphism(A, B, "f")
  1102. >>> g = NamedMorphism(B, C, "g")
  1103. >>> diagram = Diagram([f, g])
  1104. >>> grid = DiagramGrid(diagram)
  1105. >>> grid.morphisms
  1106. {NamedMorphism(Object("A"), Object("B"), "f"): EmptySet,
  1107. NamedMorphism(Object("B"), Object("C"), "g"): EmptySet}
  1108. """
  1109. return self._morphisms
  1110. def __str__(self):
  1111. """
  1112. Produces a string representation of this class.
  1113. This method returns a string representation of the underlying
  1114. list of lists of objects.
  1115. Examples
  1116. ========
  1117. >>> from sympy.categories import Object, NamedMorphism
  1118. >>> from sympy.categories import Diagram, DiagramGrid
  1119. >>> A = Object("A")
  1120. >>> B = Object("B")
  1121. >>> C = Object("C")
  1122. >>> f = NamedMorphism(A, B, "f")
  1123. >>> g = NamedMorphism(B, C, "g")
  1124. >>> diagram = Diagram([f, g])
  1125. >>> grid = DiagramGrid(diagram)
  1126. >>> print(grid)
  1127. [[Object("A"), Object("B")],
  1128. [None, Object("C")]]
  1129. """
  1130. return repr(self._grid._array)
  1131. class ArrowStringDescription:
  1132. r"""
  1133. Stores the information necessary for producing an Xy-pic
  1134. description of an arrow.
  1135. The principal goal of this class is to abstract away the string
  1136. representation of an arrow and to also provide the functionality
  1137. to produce the actual Xy-pic string.
  1138. ``unit`` sets the unit which will be used to specify the amount of
  1139. curving and other distances. ``horizontal_direction`` should be a
  1140. string of ``"r"`` or ``"l"`` specifying the horizontal offset of the
  1141. target cell of the arrow relatively to the current one.
  1142. ``vertical_direction`` should specify the vertical offset using a
  1143. series of either ``"d"`` or ``"u"``. ``label_position`` should be
  1144. either ``"^"``, ``"_"``, or ``"|"`` to specify that the label should
  1145. be positioned above the arrow, below the arrow or just over the arrow,
  1146. in a break. Note that the notions "above" and "below" are relative
  1147. to arrow direction. ``label`` stores the morphism label.
  1148. This works as follows (disregard the yet unexplained arguments):
  1149. >>> from sympy.categories.diagram_drawing import ArrowStringDescription
  1150. >>> astr = ArrowStringDescription(
  1151. ... unit="mm", curving=None, curving_amount=None,
  1152. ... looping_start=None, looping_end=None, horizontal_direction="d",
  1153. ... vertical_direction="r", label_position="_", label="f")
  1154. >>> print(str(astr))
  1155. \ar[dr]_{f}
  1156. ``curving`` should be one of ``"^"``, ``"_"`` to specify in which
  1157. direction the arrow is going to curve. ``curving_amount`` is a number
  1158. describing how many ``unit``'s the morphism is going to curve:
  1159. >>> astr = ArrowStringDescription(
  1160. ... unit="mm", curving="^", curving_amount=12,
  1161. ... looping_start=None, looping_end=None, horizontal_direction="d",
  1162. ... vertical_direction="r", label_position="_", label="f")
  1163. >>> print(str(astr))
  1164. \ar@/^12mm/[dr]_{f}
  1165. ``looping_start`` and ``looping_end`` are currently only used for
  1166. loop morphisms, those which have the same domain and codomain.
  1167. These two attributes should store a valid Xy-pic direction and
  1168. specify, correspondingly, the direction the arrow gets out into
  1169. and the direction the arrow gets back from:
  1170. >>> astr = ArrowStringDescription(
  1171. ... unit="mm", curving=None, curving_amount=None,
  1172. ... looping_start="u", looping_end="l", horizontal_direction="",
  1173. ... vertical_direction="", label_position="_", label="f")
  1174. >>> print(str(astr))
  1175. \ar@(u,l)[]_{f}
  1176. ``label_displacement`` controls how far the arrow label is from
  1177. the ends of the arrow. For example, to position the arrow label
  1178. near the arrow head, use ">":
  1179. >>> astr = ArrowStringDescription(
  1180. ... unit="mm", curving="^", curving_amount=12,
  1181. ... looping_start=None, looping_end=None, horizontal_direction="d",
  1182. ... vertical_direction="r", label_position="_", label="f")
  1183. >>> astr.label_displacement = ">"
  1184. >>> print(str(astr))
  1185. \ar@/^12mm/[dr]_>{f}
  1186. Finally, ``arrow_style`` is used to specify the arrow style. To
  1187. get a dashed arrow, for example, use "{-->}" as arrow style:
  1188. >>> astr = ArrowStringDescription(
  1189. ... unit="mm", curving="^", curving_amount=12,
  1190. ... looping_start=None, looping_end=None, horizontal_direction="d",
  1191. ... vertical_direction="r", label_position="_", label="f")
  1192. >>> astr.arrow_style = "{-->}"
  1193. >>> print(str(astr))
  1194. \ar@/^12mm/@{-->}[dr]_{f}
  1195. Notes
  1196. =====
  1197. Instances of :class:`ArrowStringDescription` will be constructed
  1198. by :class:`XypicDiagramDrawer` and provided for further use in
  1199. formatters. The user is not expected to construct instances of
  1200. :class:`ArrowStringDescription` themselves.
  1201. To be able to properly utilise this class, the reader is encouraged
  1202. to checkout the Xy-pic user guide, available at [Xypic].
  1203. See Also
  1204. ========
  1205. XypicDiagramDrawer
  1206. References
  1207. ==========
  1208. .. [Xypic] https://xy-pic.sourceforge.net/
  1209. """
  1210. def __init__(self, unit, curving, curving_amount, looping_start,
  1211. looping_end, horizontal_direction, vertical_direction,
  1212. label_position, label):
  1213. self.unit = unit
  1214. self.curving = curving
  1215. self.curving_amount = curving_amount
  1216. self.looping_start = looping_start
  1217. self.looping_end = looping_end
  1218. self.horizontal_direction = horizontal_direction
  1219. self.vertical_direction = vertical_direction
  1220. self.label_position = label_position
  1221. self.label = label
  1222. self.label_displacement = ""
  1223. self.arrow_style = ""
  1224. # This flag shows that the position of the label of this
  1225. # morphism was set while typesetting a curved morphism and
  1226. # should not be modified later.
  1227. self.forced_label_position = False
  1228. def __str__(self):
  1229. if self.curving:
  1230. curving_str = "@/%s%d%s/" % (self.curving, self.curving_amount,
  1231. self.unit)
  1232. else:
  1233. curving_str = ""
  1234. if self.looping_start and self.looping_end:
  1235. looping_str = "@(%s,%s)" % (self.looping_start, self.looping_end)
  1236. else:
  1237. looping_str = ""
  1238. if self.arrow_style:
  1239. style_str = "@" + self.arrow_style
  1240. else:
  1241. style_str = ""
  1242. return "\\ar%s%s%s[%s%s]%s%s{%s}" % \
  1243. (curving_str, looping_str, style_str, self.horizontal_direction,
  1244. self.vertical_direction, self.label_position,
  1245. self.label_displacement, self.label)
  1246. class XypicDiagramDrawer:
  1247. r"""
  1248. Given a :class:`~.Diagram` and the corresponding
  1249. :class:`DiagramGrid`, produces the Xy-pic representation of the
  1250. diagram.
  1251. The most important method in this class is ``draw``. Consider the
  1252. following triangle diagram:
  1253. >>> from sympy.categories import Object, NamedMorphism, Diagram
  1254. >>> from sympy.categories import DiagramGrid, XypicDiagramDrawer
  1255. >>> A = Object("A")
  1256. >>> B = Object("B")
  1257. >>> C = Object("C")
  1258. >>> f = NamedMorphism(A, B, "f")
  1259. >>> g = NamedMorphism(B, C, "g")
  1260. >>> diagram = Diagram([f, g], {g * f: "unique"})
  1261. To draw this diagram, its objects need to be laid out with a
  1262. :class:`DiagramGrid`::
  1263. >>> grid = DiagramGrid(diagram)
  1264. Finally, the drawing:
  1265. >>> drawer = XypicDiagramDrawer()
  1266. >>> print(drawer.draw(diagram, grid))
  1267. \xymatrix{
  1268. A \ar[d]_{g\circ f} \ar[r]^{f} & B \ar[ld]^{g} \\
  1269. C &
  1270. }
  1271. For further details see the docstring of this method.
  1272. To control the appearance of the arrows, formatters are used. The
  1273. dictionary ``arrow_formatters`` maps morphisms to formatter
  1274. functions. A formatter is accepts an
  1275. :class:`ArrowStringDescription` and is allowed to modify any of
  1276. the arrow properties exposed thereby. For example, to have all
  1277. morphisms with the property ``unique`` appear as dashed arrows,
  1278. and to have their names prepended with `\exists !`, the following
  1279. should be done:
  1280. >>> def formatter(astr):
  1281. ... astr.label = r"\exists !" + astr.label
  1282. ... astr.arrow_style = "{-->}"
  1283. >>> drawer.arrow_formatters["unique"] = formatter
  1284. >>> print(drawer.draw(diagram, grid))
  1285. \xymatrix{
  1286. A \ar@{-->}[d]_{\exists !g\circ f} \ar[r]^{f} & B \ar[ld]^{g} \\
  1287. C &
  1288. }
  1289. To modify the appearance of all arrows in the diagram, set
  1290. ``default_arrow_formatter``. For example, to place all morphism
  1291. labels a little bit farther from the arrow head so that they look
  1292. more centred, do as follows:
  1293. >>> def default_formatter(astr):
  1294. ... astr.label_displacement = "(0.45)"
  1295. >>> drawer.default_arrow_formatter = default_formatter
  1296. >>> print(drawer.draw(diagram, grid))
  1297. \xymatrix{
  1298. A \ar@{-->}[d]_(0.45){\exists !g\circ f} \ar[r]^(0.45){f} & B \ar[ld]^(0.45){g} \\
  1299. C &
  1300. }
  1301. In some diagrams some morphisms are drawn as curved arrows.
  1302. Consider the following diagram:
  1303. >>> D = Object("D")
  1304. >>> E = Object("E")
  1305. >>> h = NamedMorphism(D, A, "h")
  1306. >>> k = NamedMorphism(D, B, "k")
  1307. >>> diagram = Diagram([f, g, h, k])
  1308. >>> grid = DiagramGrid(diagram)
  1309. >>> drawer = XypicDiagramDrawer()
  1310. >>> print(drawer.draw(diagram, grid))
  1311. \xymatrix{
  1312. A \ar[r]_{f} & B \ar[d]^{g} & D \ar[l]^{k} \ar@/_3mm/[ll]_{h} \\
  1313. & C &
  1314. }
  1315. To control how far the morphisms are curved by default, one can
  1316. use the ``unit`` and ``default_curving_amount`` attributes:
  1317. >>> drawer.unit = "cm"
  1318. >>> drawer.default_curving_amount = 1
  1319. >>> print(drawer.draw(diagram, grid))
  1320. \xymatrix{
  1321. A \ar[r]_{f} & B \ar[d]^{g} & D \ar[l]^{k} \ar@/_1cm/[ll]_{h} \\
  1322. & C &
  1323. }
  1324. In some diagrams, there are multiple curved morphisms between the
  1325. same two objects. To control by how much the curving changes
  1326. between two such successive morphisms, use
  1327. ``default_curving_step``:
  1328. >>> drawer.default_curving_step = 1
  1329. >>> h1 = NamedMorphism(A, D, "h1")
  1330. >>> diagram = Diagram([f, g, h, k, h1])
  1331. >>> grid = DiagramGrid(diagram)
  1332. >>> print(drawer.draw(diagram, grid))
  1333. \xymatrix{
  1334. A \ar[r]_{f} \ar@/^1cm/[rr]^{h_{1}} & B \ar[d]^{g} & D \ar[l]^{k} \ar@/_2cm/[ll]_{h} \\
  1335. & C &
  1336. }
  1337. The default value of ``default_curving_step`` is 4 units.
  1338. See Also
  1339. ========
  1340. draw, ArrowStringDescription
  1341. """
  1342. def __init__(self):
  1343. self.unit = "mm"
  1344. self.default_curving_amount = 3
  1345. self.default_curving_step = 4
  1346. # This dictionary maps properties to the corresponding arrow
  1347. # formatters.
  1348. self.arrow_formatters = {}
  1349. # This is the default arrow formatter which will be applied to
  1350. # each arrow independently of its properties.
  1351. self.default_arrow_formatter = None
  1352. @staticmethod
  1353. def _process_loop_morphism(i, j, grid, morphisms_str_info, object_coords):
  1354. """
  1355. Produces the information required for constructing the string
  1356. representation of a loop morphism. This function is invoked
  1357. from ``_process_morphism``.
  1358. See Also
  1359. ========
  1360. _process_morphism
  1361. """
  1362. curving = ""
  1363. label_pos = "^"
  1364. looping_start = ""
  1365. looping_end = ""
  1366. # This is a loop morphism. Count how many morphisms stick
  1367. # in each of the four quadrants. Note that straight
  1368. # vertical and horizontal morphisms count in two quadrants
  1369. # at the same time (i.e., a morphism going up counts both
  1370. # in the first and the second quadrants).
  1371. # The usual numbering (counterclockwise) of quadrants
  1372. # applies.
  1373. quadrant = [0, 0, 0, 0]
  1374. obj = grid[i, j]
  1375. for m, m_str_info in morphisms_str_info.items():
  1376. if (m.domain == obj) and (m.codomain == obj):
  1377. # That's another loop morphism. Check how it
  1378. # loops and mark the corresponding quadrants as
  1379. # busy.
  1380. (l_s, l_e) = (m_str_info.looping_start, m_str_info.looping_end)
  1381. if (l_s, l_e) == ("r", "u"):
  1382. quadrant[0] += 1
  1383. elif (l_s, l_e) == ("u", "l"):
  1384. quadrant[1] += 1
  1385. elif (l_s, l_e) == ("l", "d"):
  1386. quadrant[2] += 1
  1387. elif (l_s, l_e) == ("d", "r"):
  1388. quadrant[3] += 1
  1389. continue
  1390. if m.domain == obj:
  1391. (end_i, end_j) = object_coords[m.codomain]
  1392. goes_out = True
  1393. elif m.codomain == obj:
  1394. (end_i, end_j) = object_coords[m.domain]
  1395. goes_out = False
  1396. else:
  1397. continue
  1398. d_i = end_i - i
  1399. d_j = end_j - j
  1400. m_curving = m_str_info.curving
  1401. if (d_i != 0) and (d_j != 0):
  1402. # This is really a diagonal morphism. Detect the
  1403. # quadrant.
  1404. if (d_i > 0) and (d_j > 0):
  1405. quadrant[0] += 1
  1406. elif (d_i > 0) and (d_j < 0):
  1407. quadrant[1] += 1
  1408. elif (d_i < 0) and (d_j < 0):
  1409. quadrant[2] += 1
  1410. elif (d_i < 0) and (d_j > 0):
  1411. quadrant[3] += 1
  1412. elif d_i == 0:
  1413. # Knowing where the other end of the morphism is
  1414. # and which way it goes, we now have to decide
  1415. # which quadrant is now the upper one and which is
  1416. # the lower one.
  1417. if d_j > 0:
  1418. if goes_out:
  1419. upper_quadrant = 0
  1420. lower_quadrant = 3
  1421. else:
  1422. upper_quadrant = 3
  1423. lower_quadrant = 0
  1424. else:
  1425. if goes_out:
  1426. upper_quadrant = 2
  1427. lower_quadrant = 1
  1428. else:
  1429. upper_quadrant = 1
  1430. lower_quadrant = 2
  1431. if m_curving:
  1432. if m_curving == "^":
  1433. quadrant[upper_quadrant] += 1
  1434. elif m_curving == "_":
  1435. quadrant[lower_quadrant] += 1
  1436. else:
  1437. # This morphism counts in both upper and lower
  1438. # quadrants.
  1439. quadrant[upper_quadrant] += 1
  1440. quadrant[lower_quadrant] += 1
  1441. elif d_j == 0:
  1442. # Knowing where the other end of the morphism is
  1443. # and which way it goes, we now have to decide
  1444. # which quadrant is now the left one and which is
  1445. # the right one.
  1446. if d_i < 0:
  1447. if goes_out:
  1448. left_quadrant = 1
  1449. right_quadrant = 0
  1450. else:
  1451. left_quadrant = 0
  1452. right_quadrant = 1
  1453. else:
  1454. if goes_out:
  1455. left_quadrant = 3
  1456. right_quadrant = 2
  1457. else:
  1458. left_quadrant = 2
  1459. right_quadrant = 3
  1460. if m_curving:
  1461. if m_curving == "^":
  1462. quadrant[left_quadrant] += 1
  1463. elif m_curving == "_":
  1464. quadrant[right_quadrant] += 1
  1465. else:
  1466. # This morphism counts in both upper and lower
  1467. # quadrants.
  1468. quadrant[left_quadrant] += 1
  1469. quadrant[right_quadrant] += 1
  1470. # Pick the freest quadrant to curve our morphism into.
  1471. freest_quadrant = 0
  1472. for i in range(4):
  1473. if quadrant[i] < quadrant[freest_quadrant]:
  1474. freest_quadrant = i
  1475. # Now set up proper looping.
  1476. (looping_start, looping_end) = [("r", "u"), ("u", "l"), ("l", "d"),
  1477. ("d", "r")][freest_quadrant]
  1478. return (curving, label_pos, looping_start, looping_end)
  1479. @staticmethod
  1480. def _process_horizontal_morphism(i, j, target_j, grid, morphisms_str_info,
  1481. object_coords):
  1482. """
  1483. Produces the information required for constructing the string
  1484. representation of a horizontal morphism. This function is
  1485. invoked from ``_process_morphism``.
  1486. See Also
  1487. ========
  1488. _process_morphism
  1489. """
  1490. # The arrow is horizontal. Check if it goes from left to
  1491. # right (``backwards == False``) or from right to left
  1492. # (``backwards == True``).
  1493. backwards = False
  1494. start = j
  1495. end = target_j
  1496. if end < start:
  1497. (start, end) = (end, start)
  1498. backwards = True
  1499. # Let's see which objects are there between ``start`` and
  1500. # ``end``, and then count how many morphisms stick out
  1501. # upwards, and how many stick out downwards.
  1502. #
  1503. # For example, consider the situation:
  1504. #
  1505. # B1 C1
  1506. # | |
  1507. # A--B--C--D
  1508. # |
  1509. # B2
  1510. #
  1511. # Between the objects `A` and `D` there are two objects:
  1512. # `B` and `C`. Further, there are two morphisms which
  1513. # stick out upward (the ones between `B1` and `B` and
  1514. # between `C` and `C1`) and one morphism which sticks out
  1515. # downward (the one between `B and `B2`).
  1516. #
  1517. # We need this information to decide how to curve the
  1518. # arrow between `A` and `D`. First of all, since there
  1519. # are two objects between `A` and `D``, we must curve the
  1520. # arrow. Then, we will have it curve downward, because
  1521. # there is more space (less morphisms stick out downward
  1522. # than upward).
  1523. up = []
  1524. down = []
  1525. straight_horizontal = []
  1526. for k in range(start + 1, end):
  1527. obj = grid[i, k]
  1528. if not obj:
  1529. continue
  1530. for m in morphisms_str_info:
  1531. if m.domain == obj:
  1532. (end_i, end_j) = object_coords[m.codomain]
  1533. elif m.codomain == obj:
  1534. (end_i, end_j) = object_coords[m.domain]
  1535. else:
  1536. continue
  1537. if end_i > i:
  1538. down.append(m)
  1539. elif end_i < i:
  1540. up.append(m)
  1541. elif not morphisms_str_info[m].curving:
  1542. # This is a straight horizontal morphism,
  1543. # because it has no curving.
  1544. straight_horizontal.append(m)
  1545. if len(up) < len(down):
  1546. # More morphisms stick out downward than upward, let's
  1547. # curve the morphism up.
  1548. if backwards:
  1549. curving = "_"
  1550. label_pos = "_"
  1551. else:
  1552. curving = "^"
  1553. label_pos = "^"
  1554. # Assure that the straight horizontal morphisms have
  1555. # their labels on the lower side of the arrow.
  1556. for m in straight_horizontal:
  1557. (i1, j1) = object_coords[m.domain]
  1558. (i2, j2) = object_coords[m.codomain]
  1559. m_str_info = morphisms_str_info[m]
  1560. if j1 < j2:
  1561. m_str_info.label_position = "_"
  1562. else:
  1563. m_str_info.label_position = "^"
  1564. # Don't allow any further modifications of the
  1565. # position of this label.
  1566. m_str_info.forced_label_position = True
  1567. else:
  1568. # More morphisms stick out downward than upward, let's
  1569. # curve the morphism up.
  1570. if backwards:
  1571. curving = "^"
  1572. label_pos = "^"
  1573. else:
  1574. curving = "_"
  1575. label_pos = "_"
  1576. # Assure that the straight horizontal morphisms have
  1577. # their labels on the upper side of the arrow.
  1578. for m in straight_horizontal:
  1579. (i1, j1) = object_coords[m.domain]
  1580. (i2, j2) = object_coords[m.codomain]
  1581. m_str_info = morphisms_str_info[m]
  1582. if j1 < j2:
  1583. m_str_info.label_position = "^"
  1584. else:
  1585. m_str_info.label_position = "_"
  1586. # Don't allow any further modifications of the
  1587. # position of this label.
  1588. m_str_info.forced_label_position = True
  1589. return (curving, label_pos)
  1590. @staticmethod
  1591. def _process_vertical_morphism(i, j, target_i, grid, morphisms_str_info,
  1592. object_coords):
  1593. """
  1594. Produces the information required for constructing the string
  1595. representation of a vertical morphism. This function is
  1596. invoked from ``_process_morphism``.
  1597. See Also
  1598. ========
  1599. _process_morphism
  1600. """
  1601. # This arrow is vertical. Check if it goes from top to
  1602. # bottom (``backwards == False``) or from bottom to top
  1603. # (``backwards == True``).
  1604. backwards = False
  1605. start = i
  1606. end = target_i
  1607. if end < start:
  1608. (start, end) = (end, start)
  1609. backwards = True
  1610. # Let's see which objects are there between ``start`` and
  1611. # ``end``, and then count how many morphisms stick out to
  1612. # the left, and how many stick out to the right.
  1613. #
  1614. # See the corresponding comment in the previous branch of
  1615. # this if-statement for more details.
  1616. left = []
  1617. right = []
  1618. straight_vertical = []
  1619. for k in range(start + 1, end):
  1620. obj = grid[k, j]
  1621. if not obj:
  1622. continue
  1623. for m in morphisms_str_info:
  1624. if m.domain == obj:
  1625. (end_i, end_j) = object_coords[m.codomain]
  1626. elif m.codomain == obj:
  1627. (end_i, end_j) = object_coords[m.domain]
  1628. else:
  1629. continue
  1630. if end_j > j:
  1631. right.append(m)
  1632. elif end_j < j:
  1633. left.append(m)
  1634. elif not morphisms_str_info[m].curving:
  1635. # This is a straight vertical morphism,
  1636. # because it has no curving.
  1637. straight_vertical.append(m)
  1638. if len(left) < len(right):
  1639. # More morphisms stick out to the left than to the
  1640. # right, let's curve the morphism to the right.
  1641. if backwards:
  1642. curving = "^"
  1643. label_pos = "^"
  1644. else:
  1645. curving = "_"
  1646. label_pos = "_"
  1647. # Assure that the straight vertical morphisms have
  1648. # their labels on the left side of the arrow.
  1649. for m in straight_vertical:
  1650. (i1, j1) = object_coords[m.domain]
  1651. (i2, j2) = object_coords[m.codomain]
  1652. m_str_info = morphisms_str_info[m]
  1653. if i1 < i2:
  1654. m_str_info.label_position = "^"
  1655. else:
  1656. m_str_info.label_position = "_"
  1657. # Don't allow any further modifications of the
  1658. # position of this label.
  1659. m_str_info.forced_label_position = True
  1660. else:
  1661. # More morphisms stick out to the right than to the
  1662. # left, let's curve the morphism to the left.
  1663. if backwards:
  1664. curving = "_"
  1665. label_pos = "_"
  1666. else:
  1667. curving = "^"
  1668. label_pos = "^"
  1669. # Assure that the straight vertical morphisms have
  1670. # their labels on the right side of the arrow.
  1671. for m in straight_vertical:
  1672. (i1, j1) = object_coords[m.domain]
  1673. (i2, j2) = object_coords[m.codomain]
  1674. m_str_info = morphisms_str_info[m]
  1675. if i1 < i2:
  1676. m_str_info.label_position = "_"
  1677. else:
  1678. m_str_info.label_position = "^"
  1679. # Don't allow any further modifications of the
  1680. # position of this label.
  1681. m_str_info.forced_label_position = True
  1682. return (curving, label_pos)
  1683. def _process_morphism(self, diagram, grid, morphism, object_coords,
  1684. morphisms, morphisms_str_info):
  1685. """
  1686. Given the required information, produces the string
  1687. representation of ``morphism``.
  1688. """
  1689. def repeat_string_cond(times, str_gt, str_lt):
  1690. """
  1691. If ``times > 0``, repeats ``str_gt`` ``times`` times.
  1692. Otherwise, repeats ``str_lt`` ``-times`` times.
  1693. """
  1694. if times > 0:
  1695. return str_gt * times
  1696. else:
  1697. return str_lt * (-times)
  1698. def count_morphisms_undirected(A, B):
  1699. """
  1700. Counts how many processed morphisms there are between the
  1701. two supplied objects.
  1702. """
  1703. return len([m for m in morphisms_str_info
  1704. if {m.domain, m.codomain} == {A, B}])
  1705. def count_morphisms_filtered(dom, cod, curving):
  1706. """
  1707. Counts the processed morphisms which go out of ``dom``
  1708. into ``cod`` with curving ``curving``.
  1709. """
  1710. return len([m for m, m_str_info in morphisms_str_info.items()
  1711. if (m.domain, m.codomain) == (dom, cod) and
  1712. (m_str_info.curving == curving)])
  1713. (i, j) = object_coords[morphism.domain]
  1714. (target_i, target_j) = object_coords[morphism.codomain]
  1715. # We now need to determine the direction of
  1716. # the arrow.
  1717. delta_i = target_i - i
  1718. delta_j = target_j - j
  1719. vertical_direction = repeat_string_cond(delta_i,
  1720. "d", "u")
  1721. horizontal_direction = repeat_string_cond(delta_j,
  1722. "r", "l")
  1723. curving = ""
  1724. label_pos = "^"
  1725. looping_start = ""
  1726. looping_end = ""
  1727. if (delta_i == 0) and (delta_j == 0):
  1728. # This is a loop morphism.
  1729. (curving, label_pos, looping_start,
  1730. looping_end) = XypicDiagramDrawer._process_loop_morphism(
  1731. i, j, grid, morphisms_str_info, object_coords)
  1732. elif (delta_i == 0) and (abs(j - target_j) > 1):
  1733. # This is a horizontal morphism.
  1734. (curving, label_pos) = XypicDiagramDrawer._process_horizontal_morphism(
  1735. i, j, target_j, grid, morphisms_str_info, object_coords)
  1736. elif (delta_j == 0) and (abs(i - target_i) > 1):
  1737. # This is a vertical morphism.
  1738. (curving, label_pos) = XypicDiagramDrawer._process_vertical_morphism(
  1739. i, j, target_i, grid, morphisms_str_info, object_coords)
  1740. count = count_morphisms_undirected(morphism.domain, morphism.codomain)
  1741. curving_amount = ""
  1742. if curving:
  1743. # This morphisms should be curved anyway.
  1744. curving_amount = self.default_curving_amount + count * \
  1745. self.default_curving_step
  1746. elif count:
  1747. # There are no objects between the domain and codomain of
  1748. # the current morphism, but this is not there already are
  1749. # some morphisms with the same domain and codomain, so we
  1750. # have to curve this one.
  1751. curving = "^"
  1752. filtered_morphisms = count_morphisms_filtered(
  1753. morphism.domain, morphism.codomain, curving)
  1754. curving_amount = self.default_curving_amount + \
  1755. filtered_morphisms * \
  1756. self.default_curving_step
  1757. # Let's now get the name of the morphism.
  1758. morphism_name = ""
  1759. if isinstance(morphism, IdentityMorphism):
  1760. morphism_name = "id_{%s}" + latex(grid[i, j])
  1761. elif isinstance(morphism, CompositeMorphism):
  1762. component_names = [latex(Symbol(component.name)) for
  1763. component in morphism.components]
  1764. component_names.reverse()
  1765. morphism_name = "\\circ ".join(component_names)
  1766. elif isinstance(morphism, NamedMorphism):
  1767. morphism_name = latex(Symbol(morphism.name))
  1768. return ArrowStringDescription(
  1769. self.unit, curving, curving_amount, looping_start,
  1770. looping_end, horizontal_direction, vertical_direction,
  1771. label_pos, morphism_name)
  1772. @staticmethod
  1773. def _check_free_space_horizontal(dom_i, dom_j, cod_j, grid):
  1774. """
  1775. For a horizontal morphism, checks whether there is free space
  1776. (i.e., space not occupied by any objects) above the morphism
  1777. or below it.
  1778. """
  1779. if dom_j < cod_j:
  1780. (start, end) = (dom_j, cod_j)
  1781. backwards = False
  1782. else:
  1783. (start, end) = (cod_j, dom_j)
  1784. backwards = True
  1785. # Check for free space above.
  1786. if dom_i == 0:
  1787. free_up = True
  1788. else:
  1789. free_up = all(grid[dom_i - 1, j] for j in
  1790. range(start, end + 1))
  1791. # Check for free space below.
  1792. if dom_i == grid.height - 1:
  1793. free_down = True
  1794. else:
  1795. free_down = not any(grid[dom_i + 1, j] for j in
  1796. range(start, end + 1))
  1797. return (free_up, free_down, backwards)
  1798. @staticmethod
  1799. def _check_free_space_vertical(dom_i, cod_i, dom_j, grid):
  1800. """
  1801. For a vertical morphism, checks whether there is free space
  1802. (i.e., space not occupied by any objects) to the left of the
  1803. morphism or to the right of it.
  1804. """
  1805. if dom_i < cod_i:
  1806. (start, end) = (dom_i, cod_i)
  1807. backwards = False
  1808. else:
  1809. (start, end) = (cod_i, dom_i)
  1810. backwards = True
  1811. # Check if there's space to the left.
  1812. if dom_j == 0:
  1813. free_left = True
  1814. else:
  1815. free_left = not any(grid[i, dom_j - 1] for i in
  1816. range(start, end + 1))
  1817. if dom_j == grid.width - 1:
  1818. free_right = True
  1819. else:
  1820. free_right = not any(grid[i, dom_j + 1] for i in
  1821. range(start, end + 1))
  1822. return (free_left, free_right, backwards)
  1823. @staticmethod
  1824. def _check_free_space_diagonal(dom_i, cod_i, dom_j, cod_j, grid):
  1825. """
  1826. For a diagonal morphism, checks whether there is free space
  1827. (i.e., space not occupied by any objects) above the morphism
  1828. or below it.
  1829. """
  1830. def abs_xrange(start, end):
  1831. if start < end:
  1832. return range(start, end + 1)
  1833. else:
  1834. return range(end, start + 1)
  1835. if dom_i < cod_i and dom_j < cod_j:
  1836. # This morphism goes from top-left to
  1837. # bottom-right.
  1838. (start_i, start_j) = (dom_i, dom_j)
  1839. (end_i, end_j) = (cod_i, cod_j)
  1840. backwards = False
  1841. elif dom_i > cod_i and dom_j > cod_j:
  1842. # This morphism goes from bottom-right to
  1843. # top-left.
  1844. (start_i, start_j) = (cod_i, cod_j)
  1845. (end_i, end_j) = (dom_i, dom_j)
  1846. backwards = True
  1847. if dom_i < cod_i and dom_j > cod_j:
  1848. # This morphism goes from top-right to
  1849. # bottom-left.
  1850. (start_i, start_j) = (dom_i, dom_j)
  1851. (end_i, end_j) = (cod_i, cod_j)
  1852. backwards = True
  1853. elif dom_i > cod_i and dom_j < cod_j:
  1854. # This morphism goes from bottom-left to
  1855. # top-right.
  1856. (start_i, start_j) = (cod_i, cod_j)
  1857. (end_i, end_j) = (dom_i, dom_j)
  1858. backwards = False
  1859. # This is an attempt at a fast and furious strategy to
  1860. # decide where there is free space on the two sides of
  1861. # a diagonal morphism. For a diagonal morphism
  1862. # starting at ``(start_i, start_j)`` and ending at
  1863. # ``(end_i, end_j)`` the rectangle defined by these
  1864. # two points is considered. The slope of the diagonal
  1865. # ``alpha`` is then computed. Then, for every cell
  1866. # ``(i, j)`` within the rectangle, the slope
  1867. # ``alpha1`` of the line through ``(start_i,
  1868. # start_j)`` and ``(i, j)`` is considered. If
  1869. # ``alpha1`` is between 0 and ``alpha``, the point
  1870. # ``(i, j)`` is above the diagonal, if ``alpha1`` is
  1871. # between ``alpha`` and infinity, the point is below
  1872. # the diagonal. Also note that, with some beforehand
  1873. # precautions, this trick works for both the main and
  1874. # the secondary diagonals of the rectangle.
  1875. # I have considered the possibility to only follow the
  1876. # shorter diagonals immediately above and below the
  1877. # main (or secondary) diagonal. This, however,
  1878. # wouldn't have resulted in much performance gain or
  1879. # better detection of outer edges, because of
  1880. # relatively small sizes of diagram grids, while the
  1881. # code would have become harder to understand.
  1882. alpha = float(end_i - start_i)/(end_j - start_j)
  1883. free_up = True
  1884. free_down = True
  1885. for i in abs_xrange(start_i, end_i):
  1886. if not free_up and not free_down:
  1887. break
  1888. for j in abs_xrange(start_j, end_j):
  1889. if not free_up and not free_down:
  1890. break
  1891. if (i, j) == (start_i, start_j):
  1892. continue
  1893. if j == start_j:
  1894. alpha1 = "inf"
  1895. else:
  1896. alpha1 = float(i - start_i)/(j - start_j)
  1897. if grid[i, j]:
  1898. if (alpha1 == "inf") or (abs(alpha1) > abs(alpha)):
  1899. free_down = False
  1900. elif abs(alpha1) < abs(alpha):
  1901. free_up = False
  1902. return (free_up, free_down, backwards)
  1903. def _push_labels_out(self, morphisms_str_info, grid, object_coords):
  1904. """
  1905. For all straight morphisms which form the visual boundary of
  1906. the laid out diagram, puts their labels on their outer sides.
  1907. """
  1908. def set_label_position(free1, free2, pos1, pos2, backwards, m_str_info):
  1909. """
  1910. Given the information about room available to one side and
  1911. to the other side of a morphism (``free1`` and ``free2``),
  1912. sets the position of the morphism label in such a way that
  1913. it is on the freer side. This latter operations involves
  1914. choice between ``pos1`` and ``pos2``, taking ``backwards``
  1915. in consideration.
  1916. Thus this function will do nothing if either both ``free1
  1917. == True`` and ``free2 == True`` or both ``free1 == False``
  1918. and ``free2 == False``. In either case, choosing one side
  1919. over the other presents no advantage.
  1920. """
  1921. if backwards:
  1922. (pos1, pos2) = (pos2, pos1)
  1923. if free1 and not free2:
  1924. m_str_info.label_position = pos1
  1925. elif free2 and not free1:
  1926. m_str_info.label_position = pos2
  1927. for m, m_str_info in morphisms_str_info.items():
  1928. if m_str_info.curving or m_str_info.forced_label_position:
  1929. # This is either a curved morphism, and curved
  1930. # morphisms have other magic, or the position of this
  1931. # label has already been fixed.
  1932. continue
  1933. if m.domain == m.codomain:
  1934. # This is a loop morphism, their labels, again have a
  1935. # different magic.
  1936. continue
  1937. (dom_i, dom_j) = object_coords[m.domain]
  1938. (cod_i, cod_j) = object_coords[m.codomain]
  1939. if dom_i == cod_i:
  1940. # Horizontal morphism.
  1941. (free_up, free_down,
  1942. backwards) = XypicDiagramDrawer._check_free_space_horizontal(
  1943. dom_i, dom_j, cod_j, grid)
  1944. set_label_position(free_up, free_down, "^", "_",
  1945. backwards, m_str_info)
  1946. elif dom_j == cod_j:
  1947. # Vertical morphism.
  1948. (free_left, free_right,
  1949. backwards) = XypicDiagramDrawer._check_free_space_vertical(
  1950. dom_i, cod_i, dom_j, grid)
  1951. set_label_position(free_left, free_right, "_", "^",
  1952. backwards, m_str_info)
  1953. else:
  1954. # A diagonal morphism.
  1955. (free_up, free_down,
  1956. backwards) = XypicDiagramDrawer._check_free_space_diagonal(
  1957. dom_i, cod_i, dom_j, cod_j, grid)
  1958. set_label_position(free_up, free_down, "^", "_",
  1959. backwards, m_str_info)
  1960. @staticmethod
  1961. def _morphism_sort_key(morphism, object_coords):
  1962. """
  1963. Provides a morphism sorting key such that horizontal or
  1964. vertical morphisms between neighbouring objects come
  1965. first, then horizontal or vertical morphisms between more
  1966. far away objects, and finally, all other morphisms.
  1967. """
  1968. (i, j) = object_coords[morphism.domain]
  1969. (target_i, target_j) = object_coords[morphism.codomain]
  1970. if morphism.domain == morphism.codomain:
  1971. # Loop morphisms should get after diagonal morphisms
  1972. # so that the proper direction in which to curve the
  1973. # loop can be determined.
  1974. return (3, 0, default_sort_key(morphism))
  1975. if target_i == i:
  1976. return (1, abs(target_j - j), default_sort_key(morphism))
  1977. if target_j == j:
  1978. return (1, abs(target_i - i), default_sort_key(morphism))
  1979. # Diagonal morphism.
  1980. return (2, 0, default_sort_key(morphism))
  1981. @staticmethod
  1982. def _build_xypic_string(diagram, grid, morphisms,
  1983. morphisms_str_info, diagram_format):
  1984. """
  1985. Given a collection of :class:`ArrowStringDescription`
  1986. describing the morphisms of a diagram and the object layout
  1987. information of a diagram, produces the final Xy-pic picture.
  1988. """
  1989. # Build the mapping between objects and morphisms which have
  1990. # them as domains.
  1991. object_morphisms = {}
  1992. for obj in diagram.objects:
  1993. object_morphisms[obj] = []
  1994. for morphism in morphisms:
  1995. object_morphisms[morphism.domain].append(morphism)
  1996. result = "\\xymatrix%s{\n" % diagram_format
  1997. for i in range(grid.height):
  1998. for j in range(grid.width):
  1999. obj = grid[i, j]
  2000. if obj:
  2001. result += latex(obj) + " "
  2002. morphisms_to_draw = object_morphisms[obj]
  2003. for morphism in morphisms_to_draw:
  2004. result += str(morphisms_str_info[morphism]) + " "
  2005. # Don't put the & after the last column.
  2006. if j < grid.width - 1:
  2007. result += "& "
  2008. # Don't put the line break after the last row.
  2009. if i < grid.height - 1:
  2010. result += "\\\\"
  2011. result += "\n"
  2012. result += "}\n"
  2013. return result
  2014. def draw(self, diagram, grid, masked=None, diagram_format=""):
  2015. r"""
  2016. Returns the Xy-pic representation of ``diagram`` laid out in
  2017. ``grid``.
  2018. Consider the following simple triangle diagram.
  2019. >>> from sympy.categories import Object, NamedMorphism, Diagram
  2020. >>> from sympy.categories import DiagramGrid, XypicDiagramDrawer
  2021. >>> A = Object("A")
  2022. >>> B = Object("B")
  2023. >>> C = Object("C")
  2024. >>> f = NamedMorphism(A, B, "f")
  2025. >>> g = NamedMorphism(B, C, "g")
  2026. >>> diagram = Diagram([f, g], {g * f: "unique"})
  2027. To draw this diagram, its objects need to be laid out with a
  2028. :class:`DiagramGrid`::
  2029. >>> grid = DiagramGrid(diagram)
  2030. Finally, the drawing:
  2031. >>> drawer = XypicDiagramDrawer()
  2032. >>> print(drawer.draw(diagram, grid))
  2033. \xymatrix{
  2034. A \ar[d]_{g\circ f} \ar[r]^{f} & B \ar[ld]^{g} \\
  2035. C &
  2036. }
  2037. The argument ``masked`` can be used to skip morphisms in the
  2038. presentation of the diagram:
  2039. >>> print(drawer.draw(diagram, grid, masked=[g * f]))
  2040. \xymatrix{
  2041. A \ar[r]^{f} & B \ar[ld]^{g} \\
  2042. C &
  2043. }
  2044. Finally, the ``diagram_format`` argument can be used to
  2045. specify the format string of the diagram. For example, to
  2046. increase the spacing by 1 cm, proceeding as follows:
  2047. >>> print(drawer.draw(diagram, grid, diagram_format="@+1cm"))
  2048. \xymatrix@+1cm{
  2049. A \ar[d]_{g\circ f} \ar[r]^{f} & B \ar[ld]^{g} \\
  2050. C &
  2051. }
  2052. """
  2053. # This method works in several steps. It starts by removing
  2054. # the masked morphisms, if necessary, and then maps objects to
  2055. # their positions in the grid (coordinate tuples). Remember
  2056. # that objects are unique in ``Diagram`` and in the layout
  2057. # produced by ``DiagramGrid``, so every object is mapped to a
  2058. # single coordinate pair.
  2059. #
  2060. # The next step is the central step and is concerned with
  2061. # analysing the morphisms of the diagram and deciding how to
  2062. # draw them. For example, how to curve the arrows is decided
  2063. # at this step. The bulk of the analysis is implemented in
  2064. # ``_process_morphism``, to the result of which the
  2065. # appropriate formatters are applied.
  2066. #
  2067. # The result of the previous step is a list of
  2068. # ``ArrowStringDescription``. After the analysis and
  2069. # application of formatters, some extra logic tries to assure
  2070. # better positioning of morphism labels (for example, an
  2071. # attempt is made to avoid the situations when arrows cross
  2072. # labels). This functionality constitutes the next step and
  2073. # is implemented in ``_push_labels_out``. Note that label
  2074. # positions which have been set via a formatter are not
  2075. # affected in this step.
  2076. #
  2077. # Finally, at the closing step, the array of
  2078. # ``ArrowStringDescription`` and the layout information
  2079. # incorporated in ``DiagramGrid`` are combined to produce the
  2080. # resulting Xy-pic picture. This part of code lies in
  2081. # ``_build_xypic_string``.
  2082. if not masked:
  2083. morphisms_props = grid.morphisms
  2084. else:
  2085. morphisms_props = {}
  2086. for m, props in grid.morphisms.items():
  2087. if m in masked:
  2088. continue
  2089. morphisms_props[m] = props
  2090. # Build the mapping between objects and their position in the
  2091. # grid.
  2092. object_coords = {}
  2093. for i in range(grid.height):
  2094. for j in range(grid.width):
  2095. if grid[i, j]:
  2096. object_coords[grid[i, j]] = (i, j)
  2097. morphisms = sorted(morphisms_props,
  2098. key=lambda m: XypicDiagramDrawer._morphism_sort_key(
  2099. m, object_coords))
  2100. # Build the tuples defining the string representations of
  2101. # morphisms.
  2102. morphisms_str_info = {}
  2103. for morphism in morphisms:
  2104. string_description = self._process_morphism(
  2105. diagram, grid, morphism, object_coords, morphisms,
  2106. morphisms_str_info)
  2107. if self.default_arrow_formatter:
  2108. self.default_arrow_formatter(string_description)
  2109. for prop in morphisms_props[morphism]:
  2110. # prop is a Symbol. TODO: Find out why.
  2111. if prop.name in self.arrow_formatters:
  2112. formatter = self.arrow_formatters[prop.name]
  2113. formatter(string_description)
  2114. morphisms_str_info[morphism] = string_description
  2115. # Reposition the labels a bit.
  2116. self._push_labels_out(morphisms_str_info, grid, object_coords)
  2117. return XypicDiagramDrawer._build_xypic_string(
  2118. diagram, grid, morphisms, morphisms_str_info, diagram_format)
  2119. def xypic_draw_diagram(diagram, masked=None, diagram_format="",
  2120. groups=None, **hints):
  2121. r"""
  2122. Provides a shortcut combining :class:`DiagramGrid` and
  2123. :class:`XypicDiagramDrawer`. Returns an Xy-pic presentation of
  2124. ``diagram``. The argument ``masked`` is a list of morphisms which
  2125. will be not be drawn. The argument ``diagram_format`` is the
  2126. format string inserted after "\xymatrix". ``groups`` should be a
  2127. set of logical groups. The ``hints`` will be passed directly to
  2128. the constructor of :class:`DiagramGrid`.
  2129. For more information about the arguments, see the docstrings of
  2130. :class:`DiagramGrid` and ``XypicDiagramDrawer.draw``.
  2131. Examples
  2132. ========
  2133. >>> from sympy.categories import Object, NamedMorphism, Diagram
  2134. >>> from sympy.categories import xypic_draw_diagram
  2135. >>> A = Object("A")
  2136. >>> B = Object("B")
  2137. >>> C = Object("C")
  2138. >>> f = NamedMorphism(A, B, "f")
  2139. >>> g = NamedMorphism(B, C, "g")
  2140. >>> diagram = Diagram([f, g], {g * f: "unique"})
  2141. >>> print(xypic_draw_diagram(diagram))
  2142. \xymatrix{
  2143. A \ar[d]_{g\circ f} \ar[r]^{f} & B \ar[ld]^{g} \\
  2144. C &
  2145. }
  2146. See Also
  2147. ========
  2148. XypicDiagramDrawer, DiagramGrid
  2149. """
  2150. grid = DiagramGrid(diagram, groups, **hints)
  2151. drawer = XypicDiagramDrawer()
  2152. return drawer.draw(diagram, grid, masked, diagram_format)
  2153. @doctest_depends_on(exe=('latex', 'dvipng'), modules=('pyglet',))
  2154. def preview_diagram(diagram, masked=None, diagram_format="", groups=None,
  2155. output='png', viewer=None, euler=True, **hints):
  2156. """
  2157. Combines the functionality of ``xypic_draw_diagram`` and
  2158. ``sympy.printing.preview``. The arguments ``masked``,
  2159. ``diagram_format``, ``groups``, and ``hints`` are passed to
  2160. ``xypic_draw_diagram``, while ``output``, ``viewer, and ``euler``
  2161. are passed to ``preview``.
  2162. Examples
  2163. ========
  2164. >>> from sympy.categories import Object, NamedMorphism, Diagram
  2165. >>> from sympy.categories import preview_diagram
  2166. >>> A = Object("A")
  2167. >>> B = Object("B")
  2168. >>> C = Object("C")
  2169. >>> f = NamedMorphism(A, B, "f")
  2170. >>> g = NamedMorphism(B, C, "g")
  2171. >>> d = Diagram([f, g], {g * f: "unique"})
  2172. >>> preview_diagram(d)
  2173. See Also
  2174. ========
  2175. XypicDiagramDrawer
  2176. """
  2177. from sympy.printing import preview
  2178. latex_output = xypic_draw_diagram(diagram, masked, diagram_format,
  2179. groups, **hints)
  2180. preview(latex_output, output, viewer, euler, ("xypic",))