diff --git a/src/sage/graphs/graph.py b/src/sage/graphs/graph.py index 3b4fa52e30c..711ea03afc3 100644 --- a/src/sage/graphs/graph.py +++ b/src/sage/graphs/graph.py @@ -9690,7 +9690,10 @@ def bipartite_double(self, extended=False): from sage.graphs.tutte_polynomial import tutte_polynomial from sage.graphs.lovasz_theta import lovasz_theta from sage.graphs.partial_cube import is_partial_cube - from sage.graphs.orientations import strong_orientations_iterator, random_orientation, acyclic_orientations + from sage.graphs.orientations import orient + from sage.graphs.orientations import strong_orientations_iterator + from sage.graphs.orientations import random_orientation + from sage.graphs.orientations import acyclic_orientations from sage.graphs.connectivity import bridges, cleave, spqr_tree from sage.graphs.connectivity import is_triconnected from sage.graphs.comparability import is_comparability @@ -9740,6 +9743,7 @@ def bipartite_double(self, extended=False): "is_permutation" : "Graph properties", "tutte_polynomial" : "Algorithmically hard stuff", "lovasz_theta" : "Leftovers", + "orient" : "Connectivity, orientations, trees", "strong_orientations_iterator" : "Connectivity, orientations, trees", "random_orientation" : "Connectivity, orientations, trees", "acyclic_orientations" : "Connectivity, orientations, trees", diff --git a/src/sage/graphs/orientations.py b/src/sage/graphs/orientations.py index aecf253b083..184785766da 100644 --- a/src/sage/graphs/orientations.py +++ b/src/sage/graphs/orientations.py @@ -12,6 +12,8 @@ :widths: 30, 70 :delim: | + :meth:`orient` | Return an oriented version of `G` according the input function `f`. + :meth:`acyclic_orientations` | Return an iterator over all acyclic orientations of an undirected graph `G`. :meth:`strong_orientations_iterator` | Return an iterator over all strong orientations of a graph `G` :meth:`random_orientation` | Return a random orientation of a graph `G` @@ -28,7 +30,7 @@ # **************************************************************************** # Copyright (C) 2017 Kolja Knauer # 2017 Petru Valicov -# 2017-2023 David Coudert +# 2017-2024 David Coudert # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -41,6 +43,183 @@ from sage.graphs.digraph import DiGraph +def orient(G, f, weighted=None, data_structure=None, sparse=None, + immutable=None, hash_labels=None): + r""" + Return an oriented version of `G` according the input function `f`. + + INPUT: + + - ``G`` -- an undirected graph + + - ``f`` -- a function that inputs an edge and outputs an orientation of this + edge + + - ``weighted`` -- boolean (default: ``None``); weightedness for the oriented + digraph. By default (``None``), the graph and its orientation will behave + the same. + + - ``sparse`` -- boolean (default: ``None``); ``sparse=True`` is an alias for + ``data_structure="sparse"``, and ``sparse=False`` is an alias for + ``data_structure="dense"``. Only used when ``data_structure=None``. + + - ``data_structure`` -- string (default: ``None``); one of ``'sparse'``, + ``'static_sparse'``, or ``'dense'``. See the documentation of + :class:`DiGraph`. + + - ``immutable`` -- boolean (default: ``None``); whether to create a + mutable/immutable digraph. Only used when ``data_structure=None``. + + * ``immutable=None`` (default) means that the graph and its orientation + will behave the same way. + + * ``immutable=True`` is a shortcut for ``data_structure='static_sparse'`` + + * ``immutable=False`` means that the created digraph is mutable. When used + to orient an immutable graph, the data structure used is ``'sparse'`` + unless anything else is specified. + + - ``hash_labels`` -- boolean (default: ``None``); whether to include edge + labels during hashing of the oriented digraph. This parameter defaults to + ``True`` if the graph is weighted. This parameter is ignored when + parameter ``immutable`` is not ``True``. Beware that trying to hash + unhashable labels will raise an error. + + OUTPUT: a :class:`DiGraph` object + + .. NOTE:: + + This method behaves similarly to method + :meth:`~sage.graphs.generic_graph.GenericGraph.copy`. That is, the + returned digraph uses the same data structure by default, unless the + user asks to use another data structure, and the attributes of the input + graph are copied. + + EXAMPLES:: + + sage: G = graphs.CycleGraph(4); G + Cycle graph: Graph on 4 vertices + sage: D = G.orient(lambda e:e if e[0] < e[1] else (e[1], e[0], e[2])); D + Orientation of Cycle graph: Digraph on 4 vertices + sage: sorted(D.edges(labels=False)) + [(0, 1), (0, 3), (1, 2), (2, 3)] + + TESTS: + + We make sure that one can get an immutable orientation by providing the + ``data_structure`` optional argument:: + + sage: def foo(e): + ....: return e if e[0] < e[1] else (e[1], e[0], e[2]) + sage: G = graphs.CycleGraph(4) + sage: D = G.orient(foo, data_structure='static_sparse') + sage: D.is_immutable() + True + sage: D = G.orient(foo, immutable=True) + sage: D.is_immutable() + True + + Bad input:: + + sage: G.orient(foo, data_structure='sparse', sparse=False) + Traceback (most recent call last): + ... + ValueError: you cannot define 'immutable' or 'sparse' when 'data_structure' has a value + sage: G.orient(foo, data_structure='sparse', immutable=True) + Traceback (most recent call last): + ... + ValueError: you cannot define 'immutable' or 'sparse' when 'data_structure' has a value + sage: G.orient(foo, immutable=True, sparse=False) + Traceback (most recent call last): + ... + ValueError: there is no dense immutable backend at the moment + + Which backend? :: + + sage: G.orient(foo, data_structure='sparse')._backend + + sage: G.orient(foo, data_structure='dense')._backend + + sage: G.orient(foo, data_structure='static_sparse')._backend + + sage: G.orient(foo, immutable=True)._backend + + sage: G.orient(foo, immutable=True, sparse=True)._backend + + sage: G.orient(foo, immutable=False, sparse=True)._backend + + sage: G.orient(foo, immutable=False, sparse=False)._backend + + sage: G.orient(foo, data_structure=None, immutable=None, sparse=True)._backend + + sage: G.orient(foo, data_structure=None, immutable=None, sparse=False)._backend + + sage: G.orient(foo, data_structure=None, immutable=None, sparse=None)._backend + + sage: H = Graph(data_structure='dense') + sage: H.orient(foo, data_structure=None, immutable=None, sparse=None)._backend + + """ + # Which data structure should be used ? + if data_structure is not None: + # data_structure is already defined so there is nothing left to do + # here. Did the user try to define too much ? + if immutable is not None or sparse is not None: + raise ValueError("you cannot define 'immutable' or 'sparse' " + "when 'data_structure' has a value") + # At this point, data_structure is None. + elif immutable is True: + data_structure = 'static_sparse' + if sparse is False: + raise ValueError("there is no dense immutable backend at the moment") + elif immutable is False: + # If the user requests a mutable digraph and input is immutable, we + # choose the 'sparse' cgraph backend. Unless the user explicitly + # asked for something different. + if G.is_immutable(): + data_structure = 'dense' if sparse is False else 'sparse' + elif sparse is True: + data_structure = "sparse" + elif sparse is False: + data_structure = "dense" + + if data_structure is None: + from sage.graphs.base.dense_graph import DenseGraphBackend + if isinstance(G._backend, DenseGraphBackend): + data_structure = "dense" + else: + data_structure = "sparse" + + if weighted is None: + weighted = G.weighted() + + edges = (f(e) for e in G.edge_iterator()) + D = DiGraph([G, edges], format='vertices_and_edges', + data_structure=data_structure, + loops=G.allows_loops(), + multiedges=G.allows_multiple_edges(), + name=f"Orientation of {G.name()}", + pos=copy(G._pos), weighted=weighted, + hash_labels=hash_labels) + + attributes_to_copy = ('_assoc', '_embedding') + for attr in attributes_to_copy: + if hasattr(G, attr): + copy_attr = {} + old_attr = getattr(G, attr) + if isinstance(old_attr, dict): + for v, value in old_attr.items(): + try: + copy_attr[v] = value.copy() + except AttributeError: + copy_attr[v] = copy(value) + setattr(D, attr, copy_attr) + else: + setattr(D, attr, copy(old_attr)) + + return D + + def acyclic_orientations(G): r""" Return an iterator over all acyclic orientations of an undirected graph `G`.