Repository: sagemath/sage

Issue:
Implement a function to generate the acyclic orientations of an undirected graph
### Problem Description

In graph theory, an acyclic orientation of an undirected graph is an assignment of a direction to each edge (an orientation) that does not form any directed cycle and therefore makes it into a directed acyclic graph. Every graph has an acyclic orientation. Currently, SageMath lacks a built-in functionality to find acyclic orientations of undirected graphs. The absence of this feature limits users who require such computations for graph-related research and applications.

### Proposed Solution

We will be referring to a research paper by Matthew B. Squire [https://www.sciencedirect.com/science/article/abs/pii/S0196677497908919](url)

The proposed solution involves introducing a new function to address the current lack of a dedicated feature for computing acyclic orientations in SageMath. 
The function would take an undirected graph as input and implement a multi-step process: 
- first, it would reorder the vertices using a greedy approach based on degree centrality, ensuring efficient computation. 
- Then, the edges would be labeled according to the reordering of vertices. 
- Following this, the function would utilize a recursive algorithm to explore subsets of the graph's edges and generate acyclic orientations by considering a partially ordered set. 

The proposed solution encapsulates the essential logic demonstrated in the provided Python code, offering a seamless and integrated way for users to obtain acyclic orientations within the SageMath environment.

### Alternatives Considered

**Manual Implementation:** Users can manually implement the logic provided in the example code, but a built-in function would simplify the process and promote code reusability.

### Additional Information

_No response_

### Is there an existing issue for this?

- [X] I have searched the existing issues for a bug report that matches the one I want to file, without success.


Discussion:
you should compare the timing with `self.tutte_polynomial().subs(x=2, y=0)`.

For information, there is an open pull request on this question: #35075
See also #37272 for the generation of non-isomorphic acyclic orientations using `nauty_directg`.
Sure! I will mention these issues when I submit a PR and we can close both issues then?

Resolving patch:
diff --git a/src/sage/graphs/graph.py b/src/sage/graphs/graph.py
--- a/src/sage/graphs/graph.py
+++ b/src/sage/graphs/graph.py
@@ -10134,7 +10134,7 @@ 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
+    from sage.graphs.orientations import strong_orientations_iterator, random_orientation, 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
@@ -10180,6 +10180,7 @@ def bipartite_double(self, extended=False):
     "lovasz_theta"              : "Leftovers",
     "strong_orientations_iterator" : "Connectivity, orientations, trees",
     "random_orientation"        : "Connectivity, orientations, trees",
+    "acyclic_orientations"      : "Connectivity, orientations, trees",
     "bridges"                   : "Connectivity, orientations, trees",
     "cleave"                    : "Connectivity, orientations, trees",
     "spqr_tree"                 : "Connectivity, orientations, trees",
diff --git a/src/sage/graphs/orientations.py b/src/sage/graphs/orientations.py
--- a/src/sage/graphs/orientations.py
+++ b/src/sage/graphs/orientations.py
@@ -41,6 +41,263 @@
 from sage.graphs.digraph import DiGraph
 
 
+def acyclic_orientations(G):
+    r"""
+    Return an iterator over all acyclic orientations of an undirected graph `G`.
+
+    ALGORITHM:
+
+    The algorithm is based on [Sq1998]_.
+    It presents an efficient algorithm for listing the acyclic orientations of a
+    graph. The algorithm is shown to require O(n) time per acyclic orientation
+    generated, making it the most efficient known algorithm for generating acyclic
+    orientations.
+
+    The function uses a recursive approach to generate acyclic orientations of the
+    graph. It reorders the vertices and edges of the graph, creating a new graph
+    with updated labels. Then, it iteratively generates acyclic orientations by
+    considering subsets of edges and checking whether they form upsets in a
+    corresponding poset.
+
+    INPUT:
+
+    - ``G`` -- an undirected graph.
+
+    OUTPUT:
+
+    - An iterator over all acyclic orientations of the input graph.
+
+    .. NOTE::
+
+        The function assumes that the input graph is undirected and the edges are unlabelled.
+
+    EXAMPLES:
+
+    To count number acyclic orientations for a graph::
+
+        sage: g = Graph([(0, 3), (0, 4), (3, 4), (1, 3), (1, 2), (2, 3), (2, 4)])
+        sage: it = g.acyclic_orientations()
+        sage: len(list(it))
+        54
+
+    Test for arbitary vertex labels::
+
+        sage: g_str = Graph([('abc', 'def'), ('ghi', 'def'), ('xyz', 'abc'), ('xyz', 'uvw'), ('uvw', 'abc'), ('uvw', 'ghi')])
+        sage: it = g_str.acyclic_orientations()
+        sage: len(list(it))
+        42
+
+    TESTS:
+
+    To count the number of acyclic orientations for a graph with 0 vertices::
+
+        sage: list(Graph().acyclic_orientations())
+        []
+
+    To count the number of acyclic orientations for a graph with 1 vertex::
+
+        sage: list(Graph(1).acyclic_orientations())
+        []
+
+    To count the number of acyclic orientations for a graph with 2 vertices::
+
+        sage: list(Graph(2).acyclic_orientations())
+        []
+
+    Acyclic orientations of a complete graph::
+
+        sage: g = graphs.CompleteGraph(5)
+        sage: it = g.acyclic_orientations()
+        sage: len(list(it))
+        120
+
+    Graph with one edge::
+
+        sage: list(Graph([(0, 1)]).acyclic_orientations())
+        [Digraph on 2 vertices, Digraph on 2 vertices]
+
+    Graph with two edges::
+
+        sage: len(list(Graph([(0, 1), (1, 2)]).acyclic_orientations()))
+        4
+
+    Cycle graph::
+
+        sage: len(list(Graph([(0, 1), (1, 2), (2, 0)]).acyclic_orientations()))
+        6
+
+    """
+    if not G.size():
+        # A graph without edge cannot be oriented
+        return
+
+    from sage.rings.infinity import Infinity
+    from sage.combinat.subset import Subsets
+
+    def reorder_vertices(G):
+        n = G.order()
+        ko = n
+        k = n
+        G_copy = G.copy()
+        vertex_labels = {v: None for v in G_copy.vertices()}
+
+        while G_copy.size() > 0:
+            min_val = float('inf')
+            uv = None
+            for u, v, _ in G_copy.edges():
+                du = G_copy.degree(u)
+                dv = G_copy.degree(v)
+                val = (du + dv) / (du * dv)
+                if val < min_val:
+                    min_val = val
+                    uv = (u, v)
+
+            if uv:
+                u, v = uv
+                vertex_labels[u] = ko
+                vertex_labels[v] = ko - 1
+                G_copy.delete_vertex(u)
+                G_copy.delete_vertex(v)
+                ko -= 2
+
+            if G_copy.size() == 0:
+                break
+
+        for vertex, label in vertex_labels.items():
+            if label is None:
+                vertex_labels[vertex] = ko
+                ko -= 1
+
+        return vertex_labels
+
+    def order_edges(G, vertex_labels):
+        n = len(vertex_labels)
+        m = 1
+        edge_labels = {}
+
+        for j in range(2, n + 1):
+            for i in range(1, j):
+                if G.has_edge(i, j):
+                    edge_labels[(i, j)] = m
+                    m += 1
+
+        return edge_labels
+
+    def is_upset_of_poset(Poset, subset, keys):
+        for (u, v) in subset:
+            for (w, x) in keys:
+                if (Poset[(u, v), (w, x)] == 1 and (w, x) not in subset):
+                    return False
+        return True
+
+    def generate_orientations(globO, starting_of_Ek, m, k, keys):
+        # Creating a poset
+        Poset = {}
+        for i in range(starting_of_Ek, m - 1):
+            for j in range(starting_of_Ek, m - 1):
+                u, v = keys[i]
+                w, x = keys[j]
+                Poset[(u, v), (w, x)] = 0
+
+        # Create a new graph to determine reachable vertices
+        new_G = DiGraph()
+
+        # Process vertices up to starting_of_Ek
+        new_G.add_edges([(v, u) if globO[(u, v)] == 1 else (u, v) for u, v in keys[:starting_of_Ek]])
+
+        # Process vertices starting from starting_of_Ek
+        new_G.add_vertices([u for u, _ in keys[starting_of_Ek:]] + [v for _, v in keys[starting_of_Ek:]])
+
+        if (globO[(k-1, k)] == 1):
+            new_G.add_edge(k, k - 1)
+        else:
+            new_G.add_edge(k-1, k)
+
+        for i in range(starting_of_Ek, m - 1):
+            for j in range(starting_of_Ek, m - 1):
+                u, v = keys[i]
+                w, x = keys[j]
+                # w should be reachable from u and v should be reachable from x
+                if w in new_G.depth_first_search(u) and v in new_G.depth_first_search(x):
+                    Poset[(u, v), (w, x)] = 1
+
+        # For each subset of the base set of E_k, check if it is an upset or not
+        upsets = []
+        for subset in Subsets(keys[starting_of_Ek:m-1]):
+            if (is_upset_of_poset(Poset, subset, keys[starting_of_Ek:m-1])):
+                upsets.append(list(subset))
+
+        for upset in upsets:
+            for i in range(starting_of_Ek, m - 1):
+                u, v = keys[i]
+                if (u, v) in upset:
+                    globO[(u, v)] = 1
+                else:
+                    globO[(u, v)] = 0
+
+            yield globO.copy()
+
+    def helper(G, globO, m, k):
+        keys = list(globO.keys())
+        keys = keys[0:m]
+
+        if m <= 0:
+            yield {}
+            return
+
+        starting_of_Ek = 0
+        for (u, v) in keys:
+            if u >= k - 1 or v >= k - 1:
+                break
+            else:
+                starting_of_Ek += 1
+
+        # s is the size of E_k
+        s = m - 1 - starting_of_Ek
+
+        # Recursively generate acyclic orientations
+        orientations_G_small = helper(G, globO, starting_of_Ek, k - 2)
+
+        # For each orientation of G_k-2, yield acyclic orientations
+        for alpha in orientations_G_small:
+            for (u, v) in alpha:
+                globO[(u, v)] = alpha[(u, v)]
+
+            # Orienting H_k as 1
+            globO[(k-1, k)] = 1
+            yield from generate_orientations(globO, starting_of_Ek, m, k, keys)
+
+            # Orienting H_k as 0
+            globO[(k-1, k)] = 0
+            yield from generate_orientations(globO, starting_of_Ek, m, k, keys)
+
+    # Reorder vertices based on the logic in reorder_vertices function
+    vertex_labels = reorder_vertices(G)
+
+    # Create a new graph with updated vertex labels using SageMath, Assuming the graph edges are unlabelled
+    new_G = G.relabel(perm=vertex_labels, inplace=False)
+
+    G = new_G
+
+    # Order the edges based on the logic in order_edges function
+    edge_labels = order_edges(G, vertex_labels)
+
+    # Create globO array
+    globO = {uv: 0 for uv in edge_labels}
+
+    m = len(edge_labels)
+    k = len(vertex_labels)
+    orientations = helper(G, globO, m, k)
+
+    # Create a mapping between original and new vertex labels
+    reverse_vertex_labels = {label: vertex for vertex, label in vertex_labels.items()}
+
+    # Iterate over acyclic orientations and create relabeled graphs
+    for orientation in orientations:
+        relabeled_graph = DiGraph([(reverse_vertex_labels[u], reverse_vertex_labels[v], label) for (u, v), label in orientation.items()])
+        yield relabeled_graph
+
+
 def strong_orientations_iterator(G):
     r"""
     Return an iterator over all strong orientations of a graph `G`.


Test patch: