From bea87dba7d9138a6612e15f6576471e61bfe1781 Mon Sep 17 00:00:00 2001 From: Nicco Date: Thu, 3 Oct 2024 12:28:42 -0400 Subject: [PATCH 1/4] Added new layout for graphs based on a Tutte embedding --- src/sage/graphs/generic_graph.py | 83 ++++++++++++++++++++++++++++++++ src/sage/graphs/graph_plot.py | 5 ++ 2 files changed, 88 insertions(+) diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py index 6ed8446ae1c..78ace0fb843 100644 --- a/src/sage/graphs/generic_graph.py +++ b/src/sage/graphs/generic_graph.py @@ -20393,6 +20393,8 @@ def layout(self, layout=None, pos=None, dim=2, save_pos=False, **options): option spring : Use spring layout to finalize the current layout. option tree_orientation : The direction of tree branches -- 'up', 'down', 'left' or 'right'. option tree_root : A vertex designation for drawing trees. A vertex of the tree to be used as the root for the ``layout='tree'`` option. If no root is specified, then one is chosen close to the center of the tree. Ignored unless ``layout='tree'``. + option external_face : A list of vertices to be the external face in the Tutte layout. Ignored unless ``layout='tutte'``. + option external_face_pos : A dictionary of positions for the external face in the Tutte layout. If none are specified, the external face is a regular polygon. Ignored unless ``layout='tutte'`` Some of them only apply to certain layout algorithms. For details, see :meth:`.layout_acyclic`, :meth:`.layout_planar`, @@ -21014,6 +21016,77 @@ def layout_graphviz(self, dim=2, prog='dot', **options): positions = dot2tex.dot2tex(self.graphviz_string(**options), format='positions', prog=prog) return {key_to_vertex[key]: pos for key, pos in positions.items()} + + def layout_tutte(self, external_face, external_face_pos=None, **options): + """ + + Compute graph layout based on a Tutte embedding. + + The graph must be 3-connected and planar. + + INPUT: + + - ``external_face`` -- list; the external face to be made a polygon + + - ``external_face_pos`` -- list (default: ``None``); the positions of the vertices + of the external face. If ``None``, will automatically generate a unit sided regular polygon. + + - ``**options`` -- other parameters not used here + + """ + from sage.graphs.graph import Graph + from sage.matrix.constructor import zero_matrix + from sage.rings.real_mpfr import RR + + if (len(external_face) < 3): + raise ValueError("External face must have at least 3 vertices") + + G = Graph(self) + if (not G.is_planar()): + raise ValueError("Graph must be planar") + C = G.subgraph(vertices=external_face) + if (not C.is_cycle()): + raise ValueError("External face must be a cycle") + external_face_ordered = C.depth_first_search(start=external_face[0]) + + from sage.graphs.connectivity import vertex_connectivity + if (vertex_connectivity(G, k=4)): + raise ValueError("Graph must be 3-connected") + + from math import sin, cos, pi + pos = dict() + + if external_face_pos is None: + l = len(external_face) + a0 = pi/l+pi/2 + for i, vertex in enumerate(external_face_ordered): + ai = a0+pi*2*i/l + pos[vertex] = (cos(ai),sin(ai)) + else: + for v,p in external_face_pos.items(): + pos[v] = p + + V = self.vertices() + n = len(V) + M = zero_matrix(RR,n,n) + b = zero_matrix(RR,n,2) + + for i in range(n): + v = V[i] + if v in pos: + M[i,i] = 1 + b[i,0] = pos[v][0] + b[i,1] = pos[v][1] + else: + nv = G.neighbors(v) + for u in nv: + j = V.index(u) + M[i,j] = -1 + M[i,i] = len(nv) + + sol = M.pseudoinverse()*b + return {V[i]:sol[i] for i in range(n)} + def _layout_bounding_box(self, pos): """ @@ -21333,6 +21406,9 @@ def plot(self, **options): of the tree using the keyword tree_root, otherwise a root will be selected at random. Then the tree will be plotted in levels, depending on minimum distance for the root. + + - ``'tutte'`` -- uses the Tutte embedding algorithm. The graph must be + a 3-connected, planar graph. - ``vertex_labels`` -- boolean (default: ``True``); whether to print vertex labels @@ -21399,6 +21475,13 @@ def plot(self, **options): "down". If "up" (resp., "down"), then the root of the tree will appear on the bottom (resp., top) and the tree will grow upwards (resp. downwards). Ignored unless ``layout='tree'``. + + - ``external_face`` -- list of vertices; the external face to be made a + in the Tutte layout. Ignored unless ``layout='tutte''``. + + - ``external_face_pos`` -- dictionary (default: ``None``). If specified, + used as the positions for the external face in the Tutte layout. Ignored + unless ``layout='tutte'``. - ``save_pos`` -- boolean (default: ``False``); save position computed during plotting diff --git a/src/sage/graphs/graph_plot.py b/src/sage/graphs/graph_plot.py index c97a5892efe..e77a44e6c17 100644 --- a/src/sage/graphs/graph_plot.py +++ b/src/sage/graphs/graph_plot.py @@ -160,6 +160,11 @@ 'tree_orientation': 'The direction of tree branches -- \'up\', \'down\', ' '\'left\' or \'right\'.', + 'external_face': + 'The external face of the graph, used for Tutte embedding layout', + 'external_face_pos': + 'The position of the external face of the graph, used for Tutte embedding' + 'layout. If none specified, the external face is a regular polygon.', 'save_pos': 'Whether or not to save the computed position for the graph.', 'dim': From 43ae7d20bc97b06e97e6aa83914ec215c68e5c1f Mon Sep 17 00:00:00 2001 From: Nicco Date: Thu, 3 Oct 2024 12:51:12 -0400 Subject: [PATCH 2/4] Added OUTPUT and EXAMPLES --- src/sage/graphs/generic_graph.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py index 78ace0fb843..b87dcaeaf89 100644 --- a/src/sage/graphs/generic_graph.py +++ b/src/sage/graphs/generic_graph.py @@ -21033,6 +21033,23 @@ def layout_tutte(self, external_face, external_face_pos=None, **options): - ``**options`` -- other parameters not used here + OUTPUT: a dictionary mapping vertices to positions + + EXAMPLES:: + + sage: g = graphs.WheelGraph(n=7) + sage: g.layout_tutte(external_face=[1, 2, 3, 4, 5, 6]) + {1: (-0.499999999999999, 0.866025403784438), + 2: (0.500000000000000, 0.866025403784439), + 3: (1.00000000000000, 8.85026869717109e-17), + 4: (0.500000000000000, -0.866025403784439), + 5: (-0.500000000000001, -0.866025403784438), + 6: (-1.00000000000000, 4.55896726715916e-16), + 0: (-5.55111512312578e-17, 1.86944233679563e-16)} + sage: g = graphs.CubeGraph(n=3, embedding=2) + sage: g.plot(layout='tutte', external_face=['101','111','001','011'], external_face_pos={'101':(1,0), '111':(0,0), '001':(2,1), '011':(-1,1)}) + Launched png viewer for Graphics object consisting of 21 graphics primitives + """ from sage.graphs.graph import Graph from sage.matrix.constructor import zero_matrix From 300f3b58a544d242f6cb3264bd4c6fe93307bab4 Mon Sep 17 00:00:00 2001 From: Nicco Date: Sat, 5 Oct 2024 11:30:37 -0400 Subject: [PATCH 3/4] Removed unnecessary graph duplication, fixed 3-connectivity check, optimized unnecessary call to .index() --- src/sage/graphs/generic_graph.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py index b87dcaeaf89..ccbb125b4f4 100644 --- a/src/sage/graphs/generic_graph.py +++ b/src/sage/graphs/generic_graph.py @@ -21058,16 +21058,16 @@ def layout_tutte(self, external_face, external_face_pos=None, **options): if (len(external_face) < 3): raise ValueError("External face must have at least 3 vertices") - G = Graph(self) - if (not G.is_planar()): + if (not self.is_planar()): raise ValueError("Graph must be planar") - C = G.subgraph(vertices=external_face) - if (not C.is_cycle()): + + C = self.subgraph(vertices=external_face) + if (not C.is_cycle(directed_cycle=False)): raise ValueError("External face must be a cycle") - external_face_ordered = C.depth_first_search(start=external_face[0]) + external_face_ordered = C.depth_first_search(start=external_face[0], ignore_direction=False) from sage.graphs.connectivity import vertex_connectivity - if (vertex_connectivity(G, k=4)): + if (not vertex_connectivity(self, k=3)): raise ValueError("Graph must be 3-connected") from math import sin, cos, pi @@ -21088,6 +21088,7 @@ def layout_tutte(self, external_face, external_face_pos=None, **options): M = zero_matrix(RR,n,n) b = zero_matrix(RR,n,2) + vertices_to_indices = {v:i for i,v in enumerate(V)} for i in range(n): v = V[i] if v in pos: @@ -21095,9 +21096,9 @@ def layout_tutte(self, external_face, external_face_pos=None, **options): b[i,0] = pos[v][0] b[i,1] = pos[v][1] else: - nv = G.neighbors(v) + nv = self.neighbors(v) for u in nv: - j = V.index(u) + j = vertices_to_indices[u] M[i,j] = -1 M[i,i] = len(nv) @@ -21425,7 +21426,7 @@ def plot(self, **options): depending on minimum distance for the root. - ``'tutte'`` -- uses the Tutte embedding algorithm. The graph must be - a 3-connected, planar graph. + a 3-connected, planar graph. - ``vertex_labels`` -- boolean (default: ``True``); whether to print vertex labels From ff6372d698dd3f92e01f25c15904c584d5ca7677 Mon Sep 17 00:00:00 2001 From: Nicco Date: Sat, 5 Oct 2024 14:21:08 -0400 Subject: [PATCH 4/4] Fixed some coding style inconsistencies and added more examples --- src/sage/graphs/generic_graph.py | 70 ++++++++++++++++---------------- src/sage/graphs/graph_plot.py | 8 ++-- 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py index c829966d173..97aa7b06992 100644 --- a/src/sage/graphs/generic_graph.py +++ b/src/sage/graphs/generic_graph.py @@ -20384,6 +20384,8 @@ def layout(self, layout=None, pos=None, dim=2, save_pos=False, **options): ....: print("option {} : {}".format(key, value)) option by_component : Whether to do the spring layout by connected component -- boolean. option dim : The dimension of the layout -- 2 or 3. + option external_face : A list of the vertices of the external face of the graph, used for Tutte embedding layout. + option external_face_pos : A dictionary specifying the positions of the external face of the graph, used for Tutte embedding layout. If none specified, theexternal face is a regular polygon. option forest_roots : An iterable specifying which vertices to use as roots for the ``layout='forest'`` option. If no root is specified for a tree, then one is chosen close to the center of the tree. Ignored unless ``layout='forest'``. option heights : A dictionary mapping heights to the list of vertices at this height. option iterations : The number of times to execute the spring layout algorithm. @@ -20393,8 +20395,6 @@ def layout(self, layout=None, pos=None, dim=2, save_pos=False, **options): option spring : Use spring layout to finalize the current layout. option tree_orientation : The direction of tree branches -- 'up', 'down', 'left' or 'right'. option tree_root : A vertex designation for drawing trees. A vertex of the tree to be used as the root for the ``layout='tree'`` option. If no root is specified, then one is chosen close to the center of the tree. Ignored unless ``layout='tree'``. - option external_face : A list of vertices to be the external face in the Tutte layout. Ignored unless ``layout='tutte'``. - option external_face_pos : A dictionary of positions for the external face in the Tutte layout. If none are specified, the external face is a regular polygon. Ignored unless ``layout='tutte'`` Some of them only apply to certain layout algorithms. For details, see :meth:`.layout_acyclic`, :meth:`.layout_planar`, @@ -21018,8 +21018,7 @@ def layout_graphviz(self, dim=2, prog='dot', **options): return {key_to_vertex[key]: pos for key, pos in positions.items()} def layout_tutte(self, external_face, external_face_pos=None, **options): - """ - + r""" Compute graph layout based on a Tutte embedding. The graph must be 3-connected and planar. @@ -21028,7 +21027,7 @@ def layout_tutte(self, external_face, external_face_pos=None, **options): - ``external_face`` -- list; the external face to be made a polygon - - ``external_face_pos`` -- list (default: ``None``); the positions of the vertices + - ``external_face_pos`` -- dictionary (default: ``None``); the positions of the vertices of the external face. If ``None``, will automatically generate a unit sided regular polygon. - ``**options`` -- other parameters not used here @@ -21036,26 +21035,27 @@ def layout_tutte(self, external_face, external_face_pos=None, **options): OUTPUT: a dictionary mapping vertices to positions EXAMPLES:: - sage: g = graphs.WheelGraph(n=7) - sage: g.layout_tutte(external_face=[1, 2, 3, 4, 5, 6]) - {1: (-0.499999999999999, 0.866025403784438), - 2: (0.500000000000000, 0.866025403784439), - 3: (1.00000000000000, 8.85026869717109e-17), - 4: (0.500000000000000, -0.866025403784439), - 5: (-0.500000000000001, -0.866025403784438), - 6: (-1.00000000000000, 4.55896726715916e-16), - 0: (-5.55111512312578e-17, 1.86944233679563e-16)} + sage: g.plot(layout='tutte', external_face=[0,1,2]) # needs sage.plot + Graphics object consisting of 20 graphics primitives sage: g = graphs.CubeGraph(n=3, embedding=2) - sage: g.plot(layout='tutte', external_face=['101','111','001','011'], external_face_pos={'101':(1,0), '111':(0,0), '001':(2,1), '011':(-1,1)}) - Launched png viewer for Graphics object consisting of 21 graphics primitives - + sage: g.plot(layout='tutte', external_face=['101','111','001','011'], external_face_pos={'101':(1,0), '111':(0,0), '001':(2,1), '011':(-1,1)}) # needs sage.plot + Graphics object consisting of 21 graphics primitives + sage: g = graphs.CompleteGraph(n=5) + sage: g.plot(layout='tutte', external_face=[0,1,2]) + Traceback (most recent call last): + ... + ValueError: Graph must be planar + sage: g = graphs.CycleGraph(n=10) + sage: g.layout(layout='tutte', external_face=[0,1,2,3,4,5,6,7,8,9]) + Traceback (most recent call last): + ... + ValueError: Graph must be 3-connected """ - from sage.graphs.graph import Graph from sage.matrix.constructor import zero_matrix from sage.rings.real_mpfr import RR - if (len(external_face) < 3): + if len(external_face) < 3: raise ValueError("External face must have at least 3 vertices") if (not self.is_planar()): @@ -21074,33 +21074,33 @@ def layout_tutte(self, external_face, external_face_pos=None, **options): pos = dict() if external_face_pos is None: - l = len(external_face) - a0 = pi/l+pi/2 + external_face_length = len(external_face) + a0 = pi/external_face_length + pi/2 for i, vertex in enumerate(external_face_ordered): - ai = a0+pi*2*i/l - pos[vertex] = (cos(ai),sin(ai)) + ai = a0 + pi*2*i/external_face_length + pos[vertex] = (cos(ai), sin(ai)) else: - for v,p in external_face_pos.items(): + for v, p in external_face_pos.items(): pos[v] = p V = self.vertices() n = len(V) - M = zero_matrix(RR,n,n) - b = zero_matrix(RR,n,2) + M = zero_matrix(RR, n, n) + b = zero_matrix(RR, n, 2) - vertices_to_indices = {v:i for i,v in enumerate(V)} + vertices_to_indices = {v:I for I, v in enumerate(V)} for i in range(n): v = V[i] if v in pos: - M[i,i] = 1 - b[i,0] = pos[v][0] - b[i,1] = pos[v][1] + M[i, i] = 1 + b[i, 0] = pos[v][0] + b[i, 1] = pos[v][1] else: nv = self.neighbors(v) for u in nv: j = vertices_to_indices[u] - M[i,j] = -1 - M[i,i] = len(nv) + M[i, j] = -1 + M[i, i] = len(nv) sol = M.pseudoinverse()*b return {V[i]:sol[i] for i in range(n)} @@ -21495,11 +21495,11 @@ def plot(self, **options): (resp. downwards). Ignored unless ``layout='tree'``. - ``external_face`` -- list of vertices; the external face to be made a - in the Tutte layout. Ignored unless ``layout='tutte''``. + in the Tutte layout. Ignored unless ``layout='tutte''``. - ``external_face_pos`` -- dictionary (default: ``None``). If specified, - used as the positions for the external face in the Tutte layout. Ignored - unless ``layout='tutte'``. + used as the positions for the external face in the Tutte layout. Ignored + unless ``layout='tutte'``. - ``save_pos`` -- boolean (default: ``False``); save position computed during plotting diff --git a/src/sage/graphs/graph_plot.py b/src/sage/graphs/graph_plot.py index af7f021bcbb..80c7186e59f 100644 --- a/src/sage/graphs/graph_plot.py +++ b/src/sage/graphs/graph_plot.py @@ -161,10 +161,12 @@ 'The direction of tree branches -- \'up\', \'down\', ' '\'left\' or \'right\'.', 'external_face': - 'The external face of the graph, used for Tutte embedding layout', + 'A list of the vertices of the external face of the graph, ' + 'used for Tutte embedding layout.', 'external_face_pos': - 'The position of the external face of the graph, used for Tutte embedding' - 'layout. If none specified, the external face is a regular polygon.', + 'A dictionary specifying the positions of the external face of the ' + 'graph, used for Tutte embedding layout. If none specified, the' + 'external face is a regular polygon.', 'save_pos': 'Whether or not to save the computed position for the graph.', 'dim':