Skip to content

Commit e61d20c

Browse files
author
Release Manager
committed
gh-38778: add method `orient` to `Graph` As suggested by @maxale in #38758 (comment), we add a method to apply an orientation function to a graph and obtain the corresponding directed graph. ### 📝 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. - [x] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - #12345: short description why this is a dependency --> <!-- - #34567: ... --> URL: #38778 Reported by: David Coudert Reviewer(s): Travis Scrimshaw
2 parents f69dd69 + bb3749d commit e61d20c

File tree

2 files changed

+185
-2
lines changed

2 files changed

+185
-2
lines changed

src/sage/graphs/graph.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9690,7 +9690,10 @@ def bipartite_double(self, extended=False):
96909690
from sage.graphs.tutte_polynomial import tutte_polynomial
96919691
from sage.graphs.lovasz_theta import lovasz_theta
96929692
from sage.graphs.partial_cube import is_partial_cube
9693-
from sage.graphs.orientations import strong_orientations_iterator, random_orientation, acyclic_orientations
9693+
from sage.graphs.orientations import orient
9694+
from sage.graphs.orientations import strong_orientations_iterator
9695+
from sage.graphs.orientations import random_orientation
9696+
from sage.graphs.orientations import acyclic_orientations
96949697
from sage.graphs.connectivity import bridges, cleave, spqr_tree
96959698
from sage.graphs.connectivity import is_triconnected
96969699
from sage.graphs.comparability import is_comparability
@@ -9740,6 +9743,7 @@ def bipartite_double(self, extended=False):
97409743
"is_permutation" : "Graph properties",
97419744
"tutte_polynomial" : "Algorithmically hard stuff",
97429745
"lovasz_theta" : "Leftovers",
9746+
"orient" : "Connectivity, orientations, trees",
97439747
"strong_orientations_iterator" : "Connectivity, orientations, trees",
97449748
"random_orientation" : "Connectivity, orientations, trees",
97459749
"acyclic_orientations" : "Connectivity, orientations, trees",

src/sage/graphs/orientations.py

Lines changed: 180 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
:widths: 30, 70
1313
:delim: |
1414
15+
:meth:`orient` | Return an oriented version of `G` according the input function `f`.
16+
:meth:`acyclic_orientations` | Return an iterator over all acyclic orientations of an undirected graph `G`.
1517
:meth:`strong_orientations_iterator` | Return an iterator over all strong orientations of a graph `G`
1618
:meth:`random_orientation` | Return a random orientation of a graph `G`
1719
@@ -28,7 +30,7 @@
2830
# ****************************************************************************
2931
# Copyright (C) 2017 Kolja Knauer <[email protected]>
3032
# 2017 Petru Valicov <[email protected]>
31-
# 2017-2023 David Coudert <[email protected]>
33+
# 2017-2024 David Coudert <[email protected]>
3234
#
3335
# This program is free software: you can redistribute it and/or modify
3436
# it under the terms of the GNU General Public License as published by
@@ -41,6 +43,183 @@
4143
from sage.graphs.digraph import DiGraph
4244

4345

46+
def orient(G, f, weighted=None, data_structure=None, sparse=None,
47+
immutable=None, hash_labels=None):
48+
r"""
49+
Return an oriented version of `G` according the input function `f`.
50+
51+
INPUT:
52+
53+
- ``G`` -- an undirected graph
54+
55+
- ``f`` -- a function that inputs an edge and outputs an orientation of this
56+
edge
57+
58+
- ``weighted`` -- boolean (default: ``None``); weightedness for the oriented
59+
digraph. By default (``None``), the graph and its orientation will behave
60+
the same.
61+
62+
- ``sparse`` -- boolean (default: ``None``); ``sparse=True`` is an alias for
63+
``data_structure="sparse"``, and ``sparse=False`` is an alias for
64+
``data_structure="dense"``. Only used when ``data_structure=None``.
65+
66+
- ``data_structure`` -- string (default: ``None``); one of ``'sparse'``,
67+
``'static_sparse'``, or ``'dense'``. See the documentation of
68+
:class:`DiGraph`.
69+
70+
- ``immutable`` -- boolean (default: ``None``); whether to create a
71+
mutable/immutable digraph. Only used when ``data_structure=None``.
72+
73+
* ``immutable=None`` (default) means that the graph and its orientation
74+
will behave the same way.
75+
76+
* ``immutable=True`` is a shortcut for ``data_structure='static_sparse'``
77+
78+
* ``immutable=False`` means that the created digraph is mutable. When used
79+
to orient an immutable graph, the data structure used is ``'sparse'``
80+
unless anything else is specified.
81+
82+
- ``hash_labels`` -- boolean (default: ``None``); whether to include edge
83+
labels during hashing of the oriented digraph. This parameter defaults to
84+
``True`` if the graph is weighted. This parameter is ignored when
85+
parameter ``immutable`` is not ``True``. Beware that trying to hash
86+
unhashable labels will raise an error.
87+
88+
OUTPUT: a :class:`DiGraph` object
89+
90+
.. NOTE::
91+
92+
This method behaves similarly to method
93+
:meth:`~sage.graphs.generic_graph.GenericGraph.copy`. That is, the
94+
returned digraph uses the same data structure by default, unless the
95+
user asks to use another data structure, and the attributes of the input
96+
graph are copied.
97+
98+
EXAMPLES::
99+
100+
sage: G = graphs.CycleGraph(4); G
101+
Cycle graph: Graph on 4 vertices
102+
sage: D = G.orient(lambda e:e if e[0] < e[1] else (e[1], e[0], e[2])); D
103+
Orientation of Cycle graph: Digraph on 4 vertices
104+
sage: sorted(D.edges(labels=False))
105+
[(0, 1), (0, 3), (1, 2), (2, 3)]
106+
107+
TESTS:
108+
109+
We make sure that one can get an immutable orientation by providing the
110+
``data_structure`` optional argument::
111+
112+
sage: def foo(e):
113+
....: return e if e[0] < e[1] else (e[1], e[0], e[2])
114+
sage: G = graphs.CycleGraph(4)
115+
sage: D = G.orient(foo, data_structure='static_sparse')
116+
sage: D.is_immutable()
117+
True
118+
sage: D = G.orient(foo, immutable=True)
119+
sage: D.is_immutable()
120+
True
121+
122+
Bad input::
123+
124+
sage: G.orient(foo, data_structure='sparse', sparse=False)
125+
Traceback (most recent call last):
126+
...
127+
ValueError: you cannot define 'immutable' or 'sparse' when 'data_structure' has a value
128+
sage: G.orient(foo, data_structure='sparse', immutable=True)
129+
Traceback (most recent call last):
130+
...
131+
ValueError: you cannot define 'immutable' or 'sparse' when 'data_structure' has a value
132+
sage: G.orient(foo, immutable=True, sparse=False)
133+
Traceback (most recent call last):
134+
...
135+
ValueError: there is no dense immutable backend at the moment
136+
137+
Which backend? ::
138+
139+
sage: G.orient(foo, data_structure='sparse')._backend
140+
<sage.graphs.base.sparse_graph.SparseGraphBackend object at ...>
141+
sage: G.orient(foo, data_structure='dense')._backend
142+
<sage.graphs.base.dense_graph.DenseGraphBackend object at ...>
143+
sage: G.orient(foo, data_structure='static_sparse')._backend
144+
<sage.graphs.base.static_sparse_backend.StaticSparseBackend object at ...>
145+
sage: G.orient(foo, immutable=True)._backend
146+
<sage.graphs.base.static_sparse_backend.StaticSparseBackend object at ...>
147+
sage: G.orient(foo, immutable=True, sparse=True)._backend
148+
<sage.graphs.base.static_sparse_backend.StaticSparseBackend object at ...>
149+
sage: G.orient(foo, immutable=False, sparse=True)._backend
150+
<sage.graphs.base.sparse_graph.SparseGraphBackend object at ...>
151+
sage: G.orient(foo, immutable=False, sparse=False)._backend
152+
<sage.graphs.base.sparse_graph.SparseGraphBackend object at ...>
153+
sage: G.orient(foo, data_structure=None, immutable=None, sparse=True)._backend
154+
<sage.graphs.base.sparse_graph.SparseGraphBackend object at ...>
155+
sage: G.orient(foo, data_structure=None, immutable=None, sparse=False)._backend
156+
<sage.graphs.base.dense_graph.DenseGraphBackend object at ...>
157+
sage: G.orient(foo, data_structure=None, immutable=None, sparse=None)._backend
158+
<sage.graphs.base.sparse_graph.SparseGraphBackend object at ...>
159+
sage: H = Graph(data_structure='dense')
160+
sage: H.orient(foo, data_structure=None, immutable=None, sparse=None)._backend
161+
<sage.graphs.base.dense_graph.DenseGraphBackend object at ...>
162+
"""
163+
# Which data structure should be used ?
164+
if data_structure is not None:
165+
# data_structure is already defined so there is nothing left to do
166+
# here. Did the user try to define too much ?
167+
if immutable is not None or sparse is not None:
168+
raise ValueError("you cannot define 'immutable' or 'sparse' "
169+
"when 'data_structure' has a value")
170+
# At this point, data_structure is None.
171+
elif immutable is True:
172+
data_structure = 'static_sparse'
173+
if sparse is False:
174+
raise ValueError("there is no dense immutable backend at the moment")
175+
elif immutable is False:
176+
# If the user requests a mutable digraph and input is immutable, we
177+
# choose the 'sparse' cgraph backend. Unless the user explicitly
178+
# asked for something different.
179+
if G.is_immutable():
180+
data_structure = 'dense' if sparse is False else 'sparse'
181+
elif sparse is True:
182+
data_structure = "sparse"
183+
elif sparse is False:
184+
data_structure = "dense"
185+
186+
if data_structure is None:
187+
from sage.graphs.base.dense_graph import DenseGraphBackend
188+
if isinstance(G._backend, DenseGraphBackend):
189+
data_structure = "dense"
190+
else:
191+
data_structure = "sparse"
192+
193+
if weighted is None:
194+
weighted = G.weighted()
195+
196+
edges = (f(e) for e in G.edge_iterator())
197+
D = DiGraph([G, edges], format='vertices_and_edges',
198+
data_structure=data_structure,
199+
loops=G.allows_loops(),
200+
multiedges=G.allows_multiple_edges(),
201+
name=f"Orientation of {G.name()}",
202+
pos=copy(G._pos), weighted=weighted,
203+
hash_labels=hash_labels)
204+
205+
attributes_to_copy = ('_assoc', '_embedding')
206+
for attr in attributes_to_copy:
207+
if hasattr(G, attr):
208+
copy_attr = {}
209+
old_attr = getattr(G, attr)
210+
if isinstance(old_attr, dict):
211+
for v, value in old_attr.items():
212+
try:
213+
copy_attr[v] = value.copy()
214+
except AttributeError:
215+
copy_attr[v] = copy(value)
216+
setattr(D, attr, copy_attr)
217+
else:
218+
setattr(D, attr, copy(old_attr))
219+
220+
return D
221+
222+
44223
def acyclic_orientations(G):
45224
r"""
46225
Return an iterator over all acyclic orientations of an undirected graph `G`.

0 commit comments

Comments
 (0)