Skip to content

Commit e5546e9

Browse files
author
Release Manager
committed
sagemathgh-38762: Added a Tutte Embedding Layout for Graphs <!-- ^ Please provide a concise and informative title. --> <!-- ^ Don't put issue numbers in the title, do this in the PR description below. --> <!-- ^ For example, instead of "Fixes sagemath#12345" use "Introduce new method to calculate 1 + 2". --> <!-- v Describe your changes below in detail. --> <!-- v Why is this change required? What problem does it solve? --> <!-- v If this PR resolves an open issue, please link to it here. For example, "Fixes sagemath#12345". --> Adds enhancement requested in sagemath#38410. For 3-connected and planar graphs (https://en.wikipedia.org/wiki/Polyhedral_graph), there exists an embedding into the plane called the Tutte embedding. In the Tutte embedding, an outer face is set, and all other vertices are placed inside the outer face such that they are at the mean position of their neighbors. (https://en.wikipedia.org/wiki/Tutte_embedding) ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x ] The title is concise and informative. - [x ] The description explains in detail what this PR is about. - [x ] I have linked a relevant issue or discussion. - [x] I have created tests covering the changes. - [ ] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - sagemath#12345: short description why this is a dependency --> <!-- - sagemath#34567: ... --> URL: sagemath#38762 Reported by: Niccolo Turillo Reviewer(s): David Coudert, Niccolo Turillo
2 parents 5bb357a + 87f1e8e commit e5546e9

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

src/sage/graphs/generic_graph.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21010,6 +21010,8 @@ def layout(self, layout=None, pos=None, dim=2, save_pos=False, **options):
2101021010
....: print("option {} : {}".format(key, value))
2101121011
option by_component : Whether to do the spring layout by connected component -- boolean.
2101221012
option dim : The dimension of the layout -- 2 or 3.
21013+
option external_face : A list of the vertices of the external face of the graph, used for Tutte embedding layout.
21014+
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.
2101321015
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'``.
2101421016
option heights : A dictionary mapping heights to the list of vertices at this height.
2101521017
option iterations : The number of times to execute the spring layout algorithm.
@@ -21641,6 +21643,94 @@ def layout_graphviz(self, dim=2, prog='dot', **options):
2164121643

2164221644
return {key_to_vertex[key]: pos for key, pos in positions.items()}
2164321645

21646+
def layout_tutte(self, external_face=None, external_face_pos=None, **options):
21647+
r"""
21648+
Compute graph layout based on a Tutte embedding.
21649+
21650+
The graph must be 3-connected and planar.
21651+
21652+
INPUT:
21653+
21654+
- ``external_face`` -- list (default: ``None``); the external face to
21655+
be made a polygon
21656+
21657+
- ``external_face_pos`` -- dictionary (default: ``None``); the positions
21658+
of the vertices of the external face. If ``None``, will automatically
21659+
generate a unit sided regular polygon.
21660+
21661+
- ``**options`` -- other parameters not used here
21662+
21663+
OUTPUT: a dictionary mapping vertices to positions
21664+
21665+
EXAMPLES::
21666+
21667+
sage: g = graphs.WheelGraph(n=7)
21668+
sage: g.plot(layout='tutte', external_face=[0,1,2]) # needs sage.plot
21669+
Graphics object consisting of 20 graphics primitives
21670+
sage: g = graphs.CubeGraph(n=3, embedding=2)
21671+
sage: g.plot(layout='tutte', external_face=['101','111','001',
21672+
....: '011'], external_face_pos={'101':(1,0), '111':(0,0),
21673+
....: '001':(2,1), '011':(-1,1)}) # needs sage.plot
21674+
Graphics object consisting of 21 graphics primitives
21675+
sage: g = graphs.CompleteGraph(n=5)
21676+
sage: g.plot(layout='tutte', external_face=[0,1,2])
21677+
Traceback (most recent call last):
21678+
...
21679+
ValueError: graph must be planar
21680+
sage: g = graphs.CycleGraph(n=10)
21681+
sage: g.layout(layout='tutte', external_face=[0,1,2,3,4,5,6,7,8,9])
21682+
Traceback (most recent call last):
21683+
...
21684+
ValueError: graph must be 3-connected
21685+
"""
21686+
from sage.matrix.constructor import zero_matrix
21687+
from sage.rings.real_mpfr import RR
21688+
21689+
if (external_face is not None) and (len(external_face) < 3):
21690+
raise ValueError("external face must have at least 3 vertices")
21691+
21692+
if not self.is_planar(set_embedding=True):
21693+
raise ValueError("graph must be planar")
21694+
21695+
if not self.vertex_connectivity(k=3):
21696+
raise ValueError("graph must be 3-connected")
21697+
21698+
faces_edges = self.faces()
21699+
faces_vertices = [[edge[0] for edge in face] for face in faces_edges]
21700+
if external_face is None:
21701+
external_face = faces_vertices[0]
21702+
else:
21703+
# Check that external_face is a face and order it correctly
21704+
matching_face = next((f for f in faces_vertices if sorted(external_face) == sorted(f)), None)
21705+
if matching_face is None:
21706+
raise ValueError("external face must be a face of the graph")
21707+
external_face = matching_face
21708+
21709+
if external_face_pos is None:
21710+
pos = self._circle_embedding(external_face, return_dict=True)
21711+
else:
21712+
pos = external_face_pos.copy()
21713+
21714+
n = self.order()
21715+
M = zero_matrix(RR, n, n)
21716+
b = zero_matrix(RR, n, 2)
21717+
21718+
vertices_to_indices = {v: i for i, v in enumerate(self)}
21719+
for i, v in enumerate(self):
21720+
if v in pos:
21721+
M[i, i] = 1
21722+
b[i, 0] = pos[v][0]
21723+
b[i, 1] = pos[v][1]
21724+
else:
21725+
nv = self.neighbors(v)
21726+
for u in nv:
21727+
j = vertices_to_indices[u]
21728+
M[i, j] = -1
21729+
M[i, i] = len(nv)
21730+
21731+
sol = M.pseudoinverse()*b
21732+
return dict(zip(self, sol))
21733+
2164421734
def _layout_bounding_box(self, pos):
2164521735
"""
2164621736
Return a bounding box around the specified positions.
@@ -21960,6 +22050,9 @@ def plot(self, **options):
2196022050
selected at random. Then the tree will be plotted in levels,
2196122051
depending on minimum distance for the root.
2196222052

22053+
- ``'tutte'`` -- uses the Tutte embedding algorithm. The graph must be
22054+
a 3-connected, planar graph.
22055+
2196322056
- ``vertex_labels`` -- boolean (default: ``True``); whether to print
2196422057
vertex labels
2196522058

@@ -22026,6 +22119,13 @@ def plot(self, **options):
2202622119
appear on the bottom (resp., top) and the tree will grow upwards
2202722120
(resp. downwards). Ignored unless ``layout='tree'``.
2202822121

22122+
- ``external_face`` -- list of vertices (default: ``None``); the external face to be made a
22123+
in the Tutte layout. Ignored unless ``layout='tutte''``.
22124+
22125+
- ``external_face_pos`` -- dictionary (default: ``None``). If specified,
22126+
used as the positions for the external face in the Tutte layout.
22127+
Ignored unless ``layout='tutte'``.
22128+
2202922129
- ``save_pos`` -- boolean (default: ``False``); save position computed
2203022130
during plotting
2203122131

src/sage/graphs/graph_plot.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,13 @@
160160
'tree_orientation':
161161
'The direction of tree branches -- \'up\', \'down\', '
162162
'\'left\' or \'right\'.',
163+
'external_face':
164+
'A list of the vertices of the external face of the graph, '
165+
'used for Tutte embedding layout.',
166+
'external_face_pos':
167+
'A dictionary specifying the positions of the external face of the '
168+
'graph, used for Tutte embedding layout. If none specified, the'
169+
'external face is a regular polygon.',
163170
'save_pos':
164171
'Whether or not to save the computed position for the graph.',
165172
'dim':

0 commit comments

Comments
 (0)