Repository: sagemath/sage

Issue:
Fix enumeration/cardinality of `UnorderedTuples`, remove dependency on GAP (for modularization)
That's a gratuitous dependency on a heavy library, which could be rewritten using just `itertools`.

Part of:
- https://github.com/sagemath/sage/issues/29705



Discussion:
I am more than willing to fix this,  but I am less than certain what should be done in the case where the underlying iterable has duplicates.  For example,

   sage: S=[1,1,2,3,4,5]
   sage: t = Tuples(S,2)
   sage: t.cardinality() 
   25
   sage: len(t.list())
   36

My instinct is that `Tuples.__iter__()` is incorrectly implemented (it should ignore the second '1'), but I don't want to break anyone that might depend on the original behavior. 

This does look like a bug to me. @tscrim, @fchapoton, do you agree?

Resolving patch:
diff --git a/src/sage/combinat/tuple.py b/src/sage/combinat/tuple.py
--- a/src/sage/combinat/tuple.py
+++ b/src/sage/combinat/tuple.py
@@ -16,12 +16,12 @@
 #                  https://www.gnu.org/licenses/
 # ****************************************************************************
 
-from sage.libs.gap.libgap import libgap
+from sage.arith.misc import binomial
 from sage.rings.integer_ring import ZZ
 from sage.structure.parent import Parent
 from sage.structure.unique_representation import UniqueRepresentation
 from sage.categories.finite_enumerated_sets import FiniteEnumeratedSets
-
+from itertools import product, combinations_with_replacement
 
 class Tuples(Parent, UniqueRepresentation):
     """
@@ -35,23 +35,23 @@ class Tuples(Parent, UniqueRepresentation):
 
         sage: S = [1,2]
         sage: Tuples(S,3).list()
-        [[1, 1, 1], [2, 1, 1], [1, 2, 1], [2, 2, 1], [1, 1, 2],
-         [2, 1, 2], [1, 2, 2], [2, 2, 2]]
+        [(1, 1, 1), (2, 1, 1), (1, 2, 1), (2, 2, 1), (1, 1, 2),
+         (2, 1, 2), (1, 2, 2), (2, 2, 2)]
         sage: mset = ["s","t","e","i","n"]
         sage: Tuples(mset,2).list()
-        [['s', 's'], ['t', 's'], ['e', 's'], ['i', 's'], ['n', 's'],
-         ['s', 't'], ['t', 't'], ['e', 't'], ['i', 't'], ['n', 't'],
-         ['s', 'e'], ['t', 'e'], ['e', 'e'], ['i', 'e'], ['n', 'e'],
-         ['s', 'i'], ['t', 'i'], ['e', 'i'], ['i', 'i'], ['n', 'i'],
-         ['s', 'n'], ['t', 'n'], ['e', 'n'], ['i', 'n'], ['n', 'n']]
+        [('s', 's'), ('t', 's'), ('e', 's'), ('i', 's'), ('n', 's'),
+         ('s', 't'), ('t', 't'), ('e', 't'), ('i', 't'), ('n', 't'),
+         ('s', 'e'), ('t', 'e'), ('e', 'e'), ('i', 'e'), ('n', 'e'),
+         ('s', 'i'), ('t', 'i'), ('e', 'i'), ('i', 'i'), ('n', 'i'),
+         ('s', 'n'), ('t', 'n'), ('e', 'n'), ('i', 'n'), ('n', 'n')]
 
     ::
 
         sage: K.<a> = GF(4, 'a')                                                        # optional - sage.rings.finite_rings
         sage: mset = [x for x in K if x != 0]                                           # optional - sage.rings.finite_rings
         sage: Tuples(mset,2).list()                                                     # optional - sage.rings.finite_rings
-        [[a, a], [a + 1, a], [1, a], [a, a + 1], [a + 1, a + 1], [1, a + 1],
-         [a, 1], [a + 1, 1], [1, 1]]
+        [(a, a), (a + 1, a), (1, a), (a, a + 1), (a + 1, a + 1), (1, a + 1),
+         (a, 1), (a + 1, 1), (1, 1)]
     """
     @staticmethod
     def __classcall_private__(cls, S, k):
@@ -75,7 +75,7 @@ def __init__(self, S, k):
         """
         self.S = S
         self.k = k
-        self._index_list = [S.index(s) for s in S]
+        self._index_list = list(set(S.index(s) for s in S))
         category = FiniteEnumeratedSets()
         Parent.__init__(self, category=category)
 
@@ -94,33 +94,21 @@ def __iter__(self):
 
             sage: S = [1,2]
             sage: Tuples(S,3).list()
-            [[1, 1, 1], [2, 1, 1], [1, 2, 1], [2, 2, 1], [1, 1, 2],
-             [2, 1, 2], [1, 2, 2], [2, 2, 2]]
+            [(1, 1, 1), (2, 1, 1), (1, 2, 1), (2, 2, 1), (1, 1, 2),
+             (2, 1, 2), (1, 2, 2), (2, 2, 2)]
             sage: mset = ["s","t","e","i","n"]
             sage: Tuples(mset,2).list()
-            [['s', 's'], ['t', 's'], ['e', 's'], ['i', 's'], ['n', 's'],
-             ['s', 't'], ['t', 't'], ['e', 't'], ['i', 't'],
-             ['n', 't'], ['s', 'e'], ['t', 'e'], ['e', 'e'], ['i', 'e'],
-             ['n', 'e'], ['s', 'i'], ['t', 'i'], ['e', 'i'],
-             ['i', 'i'], ['n', 'i'], ['s', 'n'], ['t', 'n'], ['e', 'n'],
-             ['i', 'n'], ['n', 'n']]
-        """
-        S = self.S
-        k = self.k
-        import copy
-        if k <= 0:
-            yield []
-            return
-        if k == 1:
-            for x in S:
-                yield [x]
-            return
-
-        for s in S:
-            for x in Tuples(S, k - 1):
-                y = copy.copy(x)
-                y.append(s)
-                yield y
+            [('s', 's'), ('t', 's'), ('e', 's'), ('i', 's'), ('n', 's'),
+             ('s', 't'), ('t', 't'), ('e', 't'), ('i', 't'), ('n', 't'),
+             ('s', 'e'), ('t', 'e'), ('e', 'e'), ('i', 'e'), ('n', 'e'),
+             ('s', 'i'), ('t', 'i'), ('e', 'i'), ('i', 'i'), ('n', 'i'),
+             ('s', 'n'), ('t', 'n'), ('e', 'n'), ('i', 'n'), ('n', 'n')]
+            sage: Tuples((1,1,2),3).list()
+            [(1, 1, 1), (2, 1, 1), (1, 2, 1), (2, 2, 1), (1, 1, 2),
+             (2, 1, 2), (1, 2, 2), (2, 2, 2)]
+        """
+        for p in product(self._index_list, repeat=self.k):
+            yield tuple(self.S[i] for i in reversed(p))
 
     def cardinality(self):
         """
@@ -133,7 +121,7 @@ def cardinality(self):
             sage: Tuples(S,2).cardinality()                                             # optional - sage.libs.gap
             25
         """
-        return ZZ(libgap.NrTuples(self._index_list, ZZ(self.k)))
+        return ZZ(len(self._index_list)).__pow__(self.k)
 
 
 Tuples_sk = Tuples
@@ -151,10 +139,10 @@ class UnorderedTuples(Parent, UniqueRepresentation):
 
         sage: S = [1,2]
         sage: UnorderedTuples(S,3).list()
-        [[1, 1, 1], [1, 1, 2], [1, 2, 2], [2, 2, 2]]
+        [(1, 1, 1), (1, 1, 2), (1, 2, 2), (2, 2, 2)]
         sage: UnorderedTuples(["a","b","c"],2).list()
-        [['a', 'a'], ['a', 'b'], ['a', 'c'], ['b', 'b'], ['b', 'c'],
-         ['c', 'c']]
+        [('a', 'a'), ('a', 'b'), ('a', 'c'), ('b', 'b'), ('b', 'c'),
+         ('c', 'c')]
     """
     @staticmethod
     def __classcall_private__(cls, S, k):
@@ -178,7 +166,7 @@ def __init__(self, S, k):
         """
         self.S = S
         self.k = k
-        self._index_list = [S.index(s) for s in S]
+        self._index_list = list(set(S.index(s) for s in S))
         category = FiniteEnumeratedSets()
         Parent.__init__(self, category=category)
 
@@ -191,19 +179,21 @@ def __repr__(self):
         """
         return "Unordered tuples of %s of length %s" % (self.S, self.k)
 
-    def list(self):
+    def __iter__(self):
         """
         EXAMPLES::
 
             sage: S = [1,2]
             sage: UnorderedTuples(S,3).list()
-            [[1, 1, 1], [1, 1, 2], [1, 2, 2], [2, 2, 2]]
+            [(1, 1, 1), (1, 1, 2), (1, 2, 2), (2, 2, 2)]
             sage: UnorderedTuples(["a","b","c"],2).list()
-            [['a', 'a'], ['a', 'b'], ['a', 'c'], ['b', 'b'], ['b', 'c'],
-             ['c', 'c']]
+            [('a', 'a'), ('a', 'b'), ('a', 'c'), ('b', 'b'), ('b', 'c'),
+             ('c', 'c')]
+            sage: UnorderedTuples([1,1,2],3).list()
+            [(1, 1, 1), (1, 1, 2), (1, 2, 2), (2, 2, 2)]
         """
-        ans = libgap.UnorderedTuples(self._index_list, ZZ(self.k))
-        return [[self.S[i] for i in l] for l in ans]
+        for ans in combinations_with_replacement(self._index_list, self.k):
+            yield tuple(self.S[i] for i in ans)
 
     def cardinality(self):
         """
@@ -213,7 +203,7 @@ def cardinality(self):
             sage: UnorderedTuples(S,2).cardinality()                                    # optional - sage.libs.gap
             15
         """
-        return ZZ(libgap.NrUnorderedTuples(self._index_list, ZZ(self.k)))
+        return binomial(len(self._index_list) + self.k - 1, self.k)
 
 
 UnorderedTuples_sk = UnorderedTuples
diff --git a/src/sage/schemes/projective/projective_space.py b/src/sage/schemes/projective/projective_space.py
--- a/src/sage/schemes/projective/projective_space.py
+++ b/src/sage/schemes/projective/projective_space.py
@@ -2068,7 +2068,7 @@ def subscheme_from_Chow_form(self, Ch, dim):
         L1 = []
         for t in UnorderedTuples(list(range(n + 1)), dim + 1):
             if all(t[i] < t[i + 1] for i in range(dim)):
-                L1.append(t)
+                L1.append(list(t))
         # create the dual brackets
         L2 = []
         signs = []
@@ -2374,7 +2374,7 @@ def rational_points(self, bound=0):
             for ai in R:
                 P[i] = ai
                 for tup in S[i - 1]:
-                    if gcd([ai] + tup) == 1:
+                    if gcd((ai,) + tup) == 1:
                         for j in range(i):
                             P[j] = tup[j]
                         pts.append(self(P))


Test patch: