From 953c07a85012cdee8beda46bd4da30cfb182ca18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Fri, 7 Jul 2023 20:36:19 +0200 Subject: [PATCH 1/9] fully commutative test --- src/sage/categories/coxeter_groups.py | 43 ++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/src/sage/categories/coxeter_groups.py b/src/sage/categories/coxeter_groups.py index 4cb0b21d7a1..122f8c9f711 100644 --- a/src/sage/categories/coxeter_groups.py +++ b/src/sage/categories/coxeter_groups.py @@ -1493,7 +1493,7 @@ def descents(self, side='right', index_set=None, positive=False): return [i for i in index_set if self.has_descent(i, side=side, positive=positive)] - def is_grassmannian(self, side="right"): + def is_grassmannian(self, side="right") -> bool: """ Return whether ``self`` is Grassmannian. @@ -1529,6 +1529,47 @@ def is_grassmannian(self, side="right"): """ return len(self.descents(side=side)) <= 1 + def is_fully_commutative(self) -> bool: + r""" + Check if ``self`` is a fully-commutative element. + + We use the characterization that + an element `w` in a Coxeter system `(W,S)` is + fully-commutative if and only if for every pair of + generators `s,t \in S` for which `m(s,t)>2`, no reduced + word of `w` contains the 'braid' word `sts...` of length + `m(s,t)` as a contiguous subword. See [Ste1996]_. + + EXAMPLES:: + + sage: W = CoxeterGroup(['A', 3]) + sage: len([1 for w in W if w.is_fully_commutative()] + ?True + sage: W = CoxeterGroup(['B', 3]) + sage: len([1 for w in W if w.is_fully_commutative()] + ?True + """ + matrix = self.parent().coxeter_matrix() + + def contains_long_braid(w): + # This detects 'braid' subwords. + # TODO: optimisation + if len(w) <= 2: + return False + for i in range(len(w) - 2): + a = w[i] + b = w[i + 1] + m = matrix[a, b] + if m > 2 and i + m <= len(w): + ab = [a, b] + if all(wj == ab[j % 2] + for j, wj in enumerate(w[i:i + m])): + return True + return False + + return not any(contains_long_braid(word) + for word in self.reduced_words()) + def reduced_word_reverse_iterator(self): """ Return a reverse iterator on a reduced word for ``self``. From eb3d073ff9eaeb5d663d88089de3e3cdf530c0e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Fri, 7 Jul 2023 21:04:25 +0200 Subject: [PATCH 2/9] method is_fully_commutative --- src/sage/categories/coxeter_groups.py | 105 +++++++++++++++++++------- 1 file changed, 79 insertions(+), 26 deletions(-) diff --git a/src/sage/categories/coxeter_groups.py b/src/sage/categories/coxeter_groups.py index 122f8c9f711..025a331fea3 100644 --- a/src/sage/categories/coxeter_groups.py +++ b/src/sage/categories/coxeter_groups.py @@ -251,6 +251,53 @@ def braid_group_as_finitely_presented_group(self): rels = self.braid_relations() return F / [prod(S[I.index(i)] for i in l) * prod(S[I.index(i)]**-1 for i in reversed(r)) for l, r in rels] + def braid_orbit_iter(self, word): + r""" + Iterate over the braid orbit of a word ``word`` of indices. + + The input word does not need to be a reduced expression of + an element. + + INPUT: + + - ``word``: a list (or iterable) of indices in + ``self.index_set()`` + + OUTPUT: + + all lists that can be obtained from + ``word`` by replacements of braid relations + + EXAMPLES:: + + sage: W = CoxeterGroups().example() + sage: sorted(W.braid_orbit_iter([0, 1, 2, 1])) # optional - sage.combinat sage.groups + [[0, 1, 2, 1], [0, 2, 1, 2], [2, 0, 1, 2]] + """ + word = list(word) + from sage.combinat.root_system.braid_orbit import BraidOrbit + + braid_rels = self.braid_relations() + I = self.index_set() + + from sage.rings.integer_ring import ZZ + be_careful = any(i not in ZZ for i in I) + + if be_careful: + Iinv = {i: j for j, i in enumerate(I)} + word = [Iinv[i] for i in word] + braid_rels = [[[Iinv[i] for i in l], + [Iinv[i] for i in r]] for l, r in braid_rels] + + orb = BraidOrbit(word, braid_rels) + + if be_careful: + for word in orb: + yield [I[i] for i in word] + else: + for I in orb: + yield list(I) + def braid_orbit(self, word): r""" Return the braid orbit of a word ``word`` of indices. @@ -314,26 +361,7 @@ def braid_orbit(self, word): :meth:`.reduced_words` """ - word = list(word) - from sage.combinat.root_system.braid_orbit import BraidOrbit - - braid_rels = self.braid_relations() - I = self.index_set() - - from sage.rings.integer_ring import ZZ - be_careful = any(i not in ZZ for i in I) - - if be_careful: - Iinv = {i: j for j, i in enumerate(I)} - word = [Iinv[i] for i in word] - braid_rels = [[[Iinv[i] for i in l], - [Iinv[i] for i in r]] for l, r in braid_rels] - - orb = BraidOrbit(word, braid_rels) - - if be_careful: - return [[I[i] for i in word] for word in orb] - return [list(I) for I in orb] + return list(self.braid_orbit_iter(word)) def __iter__(self): r""" @@ -1543,11 +1571,11 @@ def is_fully_commutative(self) -> bool: EXAMPLES:: sage: W = CoxeterGroup(['A', 3]) - sage: len([1 for w in W if w.is_fully_commutative()] - ?True + sage: len([1 for w in W if w.is_fully_commutative()]) + 14 sage: W = CoxeterGroup(['B', 3]) - sage: len([1 for w in W if w.is_fully_commutative()] - ?True + sage: len([1 for w in W if w.is_fully_commutative()]) + 24 """ matrix = self.parent().coxeter_matrix() @@ -1568,7 +1596,7 @@ def contains_long_braid(w): return False return not any(contains_long_braid(word) - for word in self.reduced_words()) + for word in self.reduced_words_iter()) def reduced_word_reverse_iterator(self): """ @@ -1630,6 +1658,31 @@ def reduced_word(self): result = list(self.reduced_word_reverse_iterator()) return list(reversed(result)) + def reduced_words_iter(self): + r""" + Iterate over all reduced words for ``self``. + + See :meth:`reduced_word` for the definition of a reduced + word. + + The algorithm uses the Matsumoto property that any two + reduced expressions are related by braid relations, see + Theorem 3.3.1(ii) in [BB2005]_. + + .. SEEALSO:: + + :meth:`braid_orbit_iter` + + EXAMPLES:: + + sage: W = CoxeterGroups().example() + sage: s = W.simple_reflections() + sage: w = s[0] * s[2] + sage: sorted(w.reduced_words_iter()) # optional - sage.combinat + [[0, 2], [2, 0]] + """ + return self.parent().braid_orbit_iter(self.reduced_word()) + def reduced_words(self): r""" Return all reduced words for ``self``. @@ -1689,7 +1742,7 @@ def reduced_words(self): :meth:`.reduced_word`, :meth:`.reduced_word_reverse_iterator`, :meth:`length`, :meth:`reduced_word_graph` """ - return self.parent().braid_orbit(self.reduced_word()) + return list(self.reduced_words_iter()) def support(self): r""" From 782ca36c6e0ef0958dd132d73b4a5957f0427649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Sat, 8 Jul 2023 08:28:38 +0200 Subject: [PATCH 3/9] test for is_FC in cython --- src/sage/combinat/root_system/braid_orbit.pyx | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/sage/combinat/root_system/braid_orbit.pyx b/src/sage/combinat/root_system/braid_orbit.pyx index f3efeb357ce..adc5a6b9875 100644 --- a/src/sage/combinat/root_system/braid_orbit.pyx +++ b/src/sage/combinat/root_system/braid_orbit.pyx @@ -74,6 +74,61 @@ cpdef set BraidOrbit(list word, list rels): return words +cpdef bint is_full_commutative(list word, list rels): + r""" + Check if the braid orbit of ``word`` is using a braid relation. + + INPUT: + + - ``word`` -- list of integers + + - ``rels`` -- list of pairs ``(A, B)``, where ``A`` and ``B`` are + lists of integers the same length + + EXAMPLES:: + + sage: from sage.combinat.root_system.braid_orbit import is_fully_commutative + sage: rels = [[[2, 1, 2], [1, 2, 1]], [[3, 1], [1, 3]], [[3, 2, 3], [2, 3, 2]]] + sage: word = [1,2,1,3,2,1] + sage: is_fully_commutative(word, rels)) + False + sage: word = [1,2,3] + sage: is_fully_commutative(word, rels)) + True + """ + cdef int i, l, rel_l, loop_ind, list_len + cdef tuple left, right, test_word, new_word + cdef list rel + + l = len(word) + cdef set words = set( [tuple(word)] ) + cdef list test_words = [ tuple(word) ] + + rels = rels + [[b, a] for a, b in rels] + rels = [[tuple(a), tuple(b), len(a)] for a, b in rels] + + loop_ind = 0 + list_len = 1 + while loop_ind < list_len: + sig_check() + test_word = test_words[loop_ind] + loop_ind += 1 + for rel in rels: + left = rel[0] + right = rel[1] + rel_l = rel[2] + for i in range(l-rel_l+1): + if pattern_match(test_word, i, left, rel_l): + if rel_l > 2: + return False + new_word = test_word[:i] + right + test_word[i+rel_l:] + if new_word not in words: + words.add(new_word) + test_words.append(new_word) + list_len += 1 + return True + + cdef inline bint pattern_match(tuple L, int i, tuple X, int l): r""" Return ``True`` if ``L[i:i+l] == X``. From bb59a049d10d6a58bc76728a64c7488bdc062445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Sat, 8 Jul 2023 08:55:59 +0200 Subject: [PATCH 4/9] fixes in cython code for FC check --- src/sage/combinat/root_system/braid_orbit.pyx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sage/combinat/root_system/braid_orbit.pyx b/src/sage/combinat/root_system/braid_orbit.pyx index adc5a6b9875..d95bc388f83 100644 --- a/src/sage/combinat/root_system/braid_orbit.pyx +++ b/src/sage/combinat/root_system/braid_orbit.pyx @@ -74,7 +74,7 @@ cpdef set BraidOrbit(list word, list rels): return words -cpdef bint is_full_commutative(list word, list rels): +cpdef bint is_fully_commutative(list word, list rels): r""" Check if the braid orbit of ``word`` is using a braid relation. @@ -90,10 +90,10 @@ cpdef bint is_full_commutative(list word, list rels): sage: from sage.combinat.root_system.braid_orbit import is_fully_commutative sage: rels = [[[2, 1, 2], [1, 2, 1]], [[3, 1], [1, 3]], [[3, 2, 3], [2, 3, 2]]] sage: word = [1,2,1,3,2,1] - sage: is_fully_commutative(word, rels)) + sage: is_fully_commutative(word, rels) False sage: word = [1,2,3] - sage: is_fully_commutative(word, rels)) + sage: is_fully_commutative(word, rels) True """ cdef int i, l, rel_l, loop_ind, list_len From a3d3dbd44a10668234cba2de889cd13be9b2ce7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Sat, 8 Jul 2023 09:02:33 +0200 Subject: [PATCH 5/9] use cython code for fully comm --- src/sage/categories/coxeter_groups.py | 33 ++++++++++++--------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/sage/categories/coxeter_groups.py b/src/sage/categories/coxeter_groups.py index 025a331fea3..7744258b8c8 100644 --- a/src/sage/categories/coxeter_groups.py +++ b/src/sage/categories/coxeter_groups.py @@ -1577,26 +1577,23 @@ def is_fully_commutative(self) -> bool: sage: len([1 for w in W if w.is_fully_commutative()]) 24 """ - matrix = self.parent().coxeter_matrix() + word = self.reduced_word() + from sage.combinat.root_system.braid_orbit import is_fully_commutative as is_fully_comm - def contains_long_braid(w): - # This detects 'braid' subwords. - # TODO: optimisation - if len(w) <= 2: - return False - for i in range(len(w) - 2): - a = w[i] - b = w[i + 1] - m = matrix[a, b] - if m > 2 and i + m <= len(w): - ab = [a, b] - if all(wj == ab[j % 2] - for j, wj in enumerate(w[i:i + m])): - return True - return False + group = self.parent() + braid_rels = group.braid_relations() + I = group.index_set() + + from sage.rings.integer_ring import ZZ + be_careful = any(i not in ZZ for i in I) + + if be_careful: + Iinv = {i: j for j, i in enumerate(I)} + word = [Iinv[i] for i in word] + braid_rels = [[[Iinv[i] for i in l], + [Iinv[i] for i in r]] for l, r in braid_rels] - return not any(contains_long_braid(word) - for word in self.reduced_words_iter()) + return is_fully_comm(word, braid_rels) def reduced_word_reverse_iterator(self): """ From 021835f88c06411b1a67f727500306e9a799e283 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Sat, 8 Jul 2023 09:43:38 +0200 Subject: [PATCH 6/9] more uses of cython code for is_fully_comm --- src/sage/combinat/affine_permutation.py | 27 +++++----- .../combinat/fully_commutative_elements.py | 51 ++++++------------- 2 files changed, 29 insertions(+), 49 deletions(-) diff --git a/src/sage/combinat/affine_permutation.py b/src/sage/combinat/affine_permutation.py index dd3d1b3654b..cbe4ea6fcd0 100644 --- a/src/sage/combinat/affine_permutation.py +++ b/src/sage/combinat/affine_permutation.py @@ -217,7 +217,7 @@ def __call__(self, i): """ return self.value(i) - def is_i_grassmannian(self, i=0, side="right"): + def is_i_grassmannian(self, i=0, side="right") -> bool: r""" Test whether ``self`` is `i`-grassmannian, i.e., either is the identity or has ``i`` as the sole descent. @@ -277,7 +277,7 @@ def lower_covers(self,side="right"): S = self.descents(side) return [self.apply_simple_reflection(i, side) for i in S] - def is_one(self): + def is_one(self) -> bool: r""" Tests whether the affine permutation is the identity. @@ -876,18 +876,21 @@ def to_lehmer_code(self, typ='decreasing', side='right'): code[i] += (b-i) // (self.k+1) + 1 return Composition(code) - def is_fully_commutative(self): + def is_fully_commutative(self) -> bool: r""" - Determine whether ``self`` is fully commutative, i.e., has no - reduced words with a braid. + Determine whether ``self`` is fully commutative. + + This means that it has no reduced word with a braid. + + This uses a specific algorithm. EXAMPLES:: sage: A = AffinePermutationGroup(['A',7,1]) - sage: p=A([3, -1, 0, 6, 5, 4, 10, 9]) + sage: p = A([3, -1, 0, 6, 5, 4, 10, 9]) sage: p.is_fully_commutative() False - sage: q=A([-3, -2, 0, 7, 9, 2, 11, 12]) + sage: q = A([-3, -2, 0, 7, 9, 2, 11, 12]) sage: q.is_fully_commutative() True """ @@ -900,14 +903,12 @@ def is_fully_commutative(self): if c[i] > 0: if firstnonzero is None: firstnonzero = i - if m != -1 and c[i] - (i-m) >= c[m]: + if m != -1 and c[i] - (i - m) >= c[m]: return False m = i - #now check m (the last non-zero) against firstnonzero. - d = self.n-(m-firstnonzero) - if c[firstnonzero]-d >= c[m]: - return False - return True + # now check m (the last non-zero) against first non-zero. + d = self.n - (m - firstnonzero) + return not c[firstnonzero] - d >= c[m] def to_bounded_partition(self, typ='decreasing', side='right'): r""" diff --git a/src/sage/combinat/fully_commutative_elements.py b/src/sage/combinat/fully_commutative_elements.py index 5b338912e6c..660e917884f 100644 --- a/src/sage/combinat/fully_commutative_elements.py +++ b/src/sage/combinat/fully_commutative_elements.py @@ -153,44 +153,23 @@ def is_fully_commutative(self): sage: x = FC.element_class(FC, [1, 2, 1], check=False); x.is_fully_commutative() False """ - matrix = self.parent().coxeter_group().coxeter_matrix() - w = tuple(self) - - # The following function detects 'braid' words. - def contains_long_braid(w): - for i in range(len(w) - 2): - a = w[i] - b = w[i + 1] - m = matrix[a, b] - if m > 2 and i + m <= len(w): - ab_braid = (a, b) * (m // 2) + ((a,) if m % 2 else ()) - if w[i:i + m] == ab_braid: - return True - return False + word = list(self) + from sage.combinat.root_system.braid_orbit import is_fully_commutative as is_fully_comm - # The following function applies a commutation relation on a word. - def commute_once(word, i): - return word[:i] + (word[i + 1], word[i]) + word[i + 2:] + group = self.parent().coxeter_group() + braid_rels = group.braid_relations() + I = group.index_set() - # A word is the reduced word of an FC element iff no sequence of - # commutation relations on it yields a word with a 'braid' word: - if contains_long_braid(w): - return False - else: - l, checked, queue = len(w), {w}, deque([w]) - while queue: - word = queue.pop() - for i in range(l - 1): - a, b = word[i], word[i + 1] - if matrix[a, b] == 2: - new_word = commute_once(word, i) - if new_word not in checked: - if contains_long_braid(new_word): - return False - else: - checked.add(new_word) - queue.appendleft(new_word) - return True + from sage.rings.integer_ring import ZZ + be_careful = any(i not in ZZ for i in I) + + if be_careful: + Iinv = {i: j for j, i in enumerate(I)} + word = [Iinv[i] for i in word] + braid_rels = [[[Iinv[i] for i in l], + [Iinv[i] for i in r]] for l, r in braid_rels] + + return is_fully_comm(word, braid_rels) # Representing FC elements: Heaps def heap(self, **kargs): From 2734c54a56ac81b49f3040c41cf9ef3aa9706d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Tue, 11 Jul 2023 07:31:08 +0200 Subject: [PATCH 7/9] Update src/sage/categories/coxeter_groups.py Co-authored-by: Travis Scrimshaw --- src/sage/categories/coxeter_groups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/categories/coxeter_groups.py b/src/sage/categories/coxeter_groups.py index 7744258b8c8..6432f831205 100644 --- a/src/sage/categories/coxeter_groups.py +++ b/src/sage/categories/coxeter_groups.py @@ -260,7 +260,7 @@ def braid_orbit_iter(self, word): INPUT: - - ``word``: a list (or iterable) of indices in + - ``word`` -- a list (or iterable) of indices in ``self.index_set()`` OUTPUT: From 7b413c712045d8866b19656409610a6314537938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Tue, 11 Jul 2023 07:58:22 +0200 Subject: [PATCH 8/9] add suggested doctest --- src/sage/categories/coxeter_groups.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/sage/categories/coxeter_groups.py b/src/sage/categories/coxeter_groups.py index 6432f831205..6b78cc0dfaa 100644 --- a/src/sage/categories/coxeter_groups.py +++ b/src/sage/categories/coxeter_groups.py @@ -1561,10 +1561,9 @@ def is_fully_commutative(self) -> bool: r""" Check if ``self`` is a fully-commutative element. - We use the characterization that - an element `w` in a Coxeter system `(W,S)` is - fully-commutative if and only if for every pair of - generators `s,t \in S` for which `m(s,t)>2`, no reduced + We use the characterization that an element `w` in a Coxeter + system `(W,S)` is fully-commutative if and only if for every pair + of generators `s,t \in S` for which `m(s,t)>2`, no reduced word of `w` contains the 'braid' word `sts...` of length `m(s,t)` as a contiguous subword. See [Ste1996]_. @@ -1576,6 +1575,12 @@ def is_fully_commutative(self) -> bool: sage: W = CoxeterGroup(['B', 3]) sage: len([1 for w in W if w.is_fully_commutative()]) 24 + + TESTS:: + + sage: W = CoxeterGroup(['A', 2], index_set=['u','v']) + sage: len([1 for w in W if w.is_fully_commutative()]) + 5 """ word = self.reduced_word() from sage.combinat.root_system.braid_orbit import is_fully_commutative as is_fully_comm From dca133a63d42ffa3224ba3f962a2b837f3043197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Chapoton?= Date: Tue, 11 Jul 2023 08:04:00 +0200 Subject: [PATCH 9/9] better doctest --- src/sage/categories/coxeter_groups.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/categories/coxeter_groups.py b/src/sage/categories/coxeter_groups.py index 6b78cc0dfaa..a131c61427e 100644 --- a/src/sage/categories/coxeter_groups.py +++ b/src/sage/categories/coxeter_groups.py @@ -1578,9 +1578,9 @@ def is_fully_commutative(self) -> bool: TESTS:: - sage: W = CoxeterGroup(['A', 2], index_set=['u','v']) + sage: W = CoxeterGroup(matrix(2,2,[1,7,7,1]),index_set='ab') sage: len([1 for w in W if w.is_fully_commutative()]) - 5 + 13 """ word = self.reduced_word() from sage.combinat.root_system.braid_orbit import is_fully_commutative as is_fully_comm