@@ -109,6 +109,7 @@ Check that :issue:`13569` is fixed::
109
109
# ****************************************************************************
110
110
111
111
import copy
112
+ import warnings
112
113
113
114
from libc.stdlib cimport qsort
114
115
@@ -1991,102 +1992,244 @@ cdef class PermutationGroupElement(MultiplicativeGroupElement):
1991
1992
entries[i, self .perm[i]] = 1
1992
1993
return M(entries)
1993
1994
1994
- def word_problem (self , words , display = True , as_list = False ):
1995
+ def word_problem (self , words , display = True , as_list = False , algorithm = ' syllable ' ):
1995
1996
r """
1996
1997
Try to solve the word problem for ``self``.
1997
1998
1998
1999
INPUT:
1999
2000
2000
- - ``words`` -- list of elements of the ambient group, generating
2001
- a subgroup
2002
-
2003
- - ``display`` -- boolean ( default: ``True``) ; whether to display
2004
- additional information
2005
-
2006
- - ``as_list`` -- boolean ( default: ``False``) ; whether to return
2007
- the result as a list of pairs ( generator, exponent)
2001
+ - ``words`` -- a list of elements of the ambient group, generating
2002
+ a subgroup `H`.
2003
+ - ``display`` -- boolean ( default ``True``) ; whether to display
2004
+ additional information ( the raw GAP word and the list form) .
2005
+ - ``as_list`` -- boolean ( default ``False``) ; whether to return
2006
+ the result as a list. See Output section for format details.
2007
+ - ``algorithm`` -- string ( default ``'syllable'``) ; the algorithm to use:
2008
+ - ``'syllable'``: ( Recommended) Uses GAP's syllable functions
2009
+ for robust parsing. Returns indices in list format.
2010
+ - ``'legacy'``: Uses the old, broken string parsing method.
2011
+ Preserved for backward compatibility testing but is deprecated
2012
+ and will be removed. Returns strings in list format.
2008
2013
2009
2014
OUTPUT:
2010
2015
2011
- - a pair of strings, both representing the same word
2012
-
2013
- or
2014
-
2015
- - a list of pairs representing the word, each pair being
2016
- ( generator as a string, exponent as an integer)
2017
-
2018
- Let `G` be the ambient permutation group, containing the given
2019
- element `g`. Let `H` be the subgroup of `G` generated by the list
2020
- ``words`` of elements of `G`. If `g` is in `H`, this function
2021
- returns an expression for `g` as a word in the elements of
2022
- ``words`` and their inverses.
2023
-
2024
- This function does not solve the word problem in Sage. Rather it
2025
- pushes it over to GAP, which has optimized algorithms for the word
2026
- problem. Essentially, this function is a wrapper for the GAP
2027
- functions ``EpimorphismFromFreeGroup`` and
2016
+ If `g = self` is in the subgroup `H` generated by ``words``, this
2017
+ function returns an expression for `g` as a word in the elements
2018
+ of ``words`` and their inverses.
2019
+
2020
+ - If ``as_list=False`` ( default) : Returns a pair of strings `( l1, l2) `.
2021
+ - `l1`: The word in terms of abstract generators `x1, x2, ... `.
2022
+ - `l2`: The word with the actual generator strings substituted.
2023
+ - If ``as_list=True``:
2024
+ - If ``algorithm='syllable'`` ( default) : Returns a list of pairs
2025
+ `[generator_index, exponent ]`, where `generator_index` is
2026
+ the 1-based index into the input ``words`` list.
2027
+ - If ``algorithm='legacy'``: Returns a list of pairs
2028
+ `[generator_string, exponent ]`. This algorithm is deprecated.
2029
+
2030
+ This function uses GAP's ``EpimorphismFromFreeGroup`` and
2028
2031
``PreImagesRepresentative``.
2029
2032
2030
2033
EXAMPLES::
2031
2034
2032
2035
sage: G = PermutationGroup( [[(1,2,3),(4,5) ],[(3,4) ]], canonicalize=False)
2033
2036
sage: g1, g2 = G. gens( )
2034
2037
sage: h = g1^ 2* g2* g1
2035
- sage: h. word_problem( [g1,g2 ], False)
2038
+ sage: h. word_problem( [g1,g2 ], display= False, as_list=False ) # Default algorithm
2036
2039
( 'x1^ 2* x2^ -1* x1', '( 1,2,3) ( 4,5) ^ 2* ( 3,4) ^ -1* ( 1,2,3) ( 4,5) ')
2037
2040
2038
- sage: h. word_problem( [g1,g2 ])
2039
- x1^ 2* x2^ -1* x1
2040
- [['(1,2,3)(4,5)', 2 ], ['(3,4)', -1 ], ['(1,2,3)(4,5)', 1 ]]
2041
+ # Displaying output ( default algorithm)
2042
+ sage: h. word_problem( [g1, g2 ])
2043
+ x1^ 2* x2^ -1* x1
2044
+ [[1, 2 ], [2, -1 ], [1, 1 ]]
2041
2045
( 'x1^ 2* x2^ -1* x1', '( 1,2,3) ( 4,5) ^ 2* ( 3,4) ^ -1* ( 1,2,3) ( 4,5) ')
2042
2046
2043
- sage: h. word_problem( [g1,g2 ], False, as_list=True)
2047
+ # Getting the list output ( default algorithm, uses indices)
2048
+ sage: h. word_problem( [g1, g2 ], display=False, as_list=True)
2049
+ [[1, 2 ], [2, -1 ], [1, 1 ]]
2050
+
2051
+ # Using the legacy algorithm ( deprecated, returns strings in list)
2052
+ sage: h. word_problem( [g1, g2 ], display=False, as_list=True, algorithm='legacy')
2053
+ doctest:warning... :
2054
+ DeprecationWarning: The 'legacy' algorithm for word_problem is deprecated...
2044
2055
[['(1,2,3)(4,5)', 2 ], ['(3,4)', -1 ], ['(1,2,3)(4,5)', 1 ]]
2045
2056
2046
- TESTS:
2057
+ Fixing :issue:`36419` ( parentheses issue) ::
2058
+
2059
+ sage: P = PermutationGroup( [[(1, 2) ], [(1, 2, 3, 4) ]])
2060
+ sage: p = P( [(1, 4) ])
2061
+ sage: p. word_problem( P. gens( ) , display=True, as_list=True) # Default algorithm
2062
+ x1^ -1* x2^ -1* ( x2^ -1* x1^ -1) ^ 2* x2
2063
+ [[1, -1 ], [2, -2 ], [1, -1 ], [2, -1 ], [1, -1 ], [2, 1 ]]
2064
+ [[1, -1 ], [2, -2 ], [1, -1 ], [2, -1 ], [1, -1 ], [2, 1 ]]
2065
+ sage: x1, x2 = P. gens( )
2066
+ sage: word_new = x1^ -1* x2^ -1* ( x2^ -1* x1^ -1) ^ 2* x2 # The word GAP returned
2067
+ sage: word_new == p
2068
+ True
2069
+
2070
+ # Compare with legacy algorithm ( shows the parsing error)
2071
+ sage: p. word_problem( P. gens( ) , display=True, as_list=True, algorithm='legacy')
2072
+ x2^ -1* ( x2^ -1* x1^ -1) ^ 2* x2* x1^ -1* x2^ -2* x1^ -1* x2
2073
+ [['(1,2,3,4)', -1 ], ['((1,2,3,4)', -1 ], ['(1,2)', 1 ], ['(1,2,3,4)', 1 ], ['(1,2)', -1 ], ['(1,2,3,4)', -2 ], ['(1,2)', -1 ], ['(1,2,3,4)', 1 ]]
2074
+ [['(1,2,3,4)', -1 ],
2075
+ ['((1,2,3,4)', -1 ],
2076
+ ['(1,2)', 1 ],
2077
+ ['(1,2,3,4)', 1 ],
2078
+ ['(1,2)', -1 ],
2079
+ ['(1,2,3,4)', -2 ],
2080
+ ['(1,2)', -1 ],
2081
+ ['(1,2,3,4)', 1 ]]
2082
+
2083
+ Fixing :issue:`36419` ( x10 vs x1 issue) ::
2084
+
2085
+ sage: P = PermutationGroup( [[(1, 2) ]] * 9 + [[(3, 4, 5) ]], canonicalize=False)
2086
+ sage: p = P( [(3, 4, 5) ])
2087
+ sage: gens = P. gens( )
2088
+ sage: p. word_problem( gens, display=False, as_list=False) # Default algorithm
2089
+ ( 'x10', '( 3,4,5) ')
2090
+ sage: p. word_problem( gens, display=False, as_list=True) # Default algorithm
2091
+ [[10, 1 ]]
2092
+
2093
+ # Compare with legacy algorithm ( shows the replacement error)
2094
+ sage: p. word_problem( gens, display=False, as_list=False, algorithm='legacy')
2095
+ ( 'x10', '( 1,2) 0')
2096
+ sage: p. word_problem( gens, display=False, as_list=True, algorithm='legacy')
2097
+ [['(1,2)0', 1 ]]
2047
2098
2048
2099
Check for :issue:`28556`::
2049
2100
2050
2101
sage: G = SymmetricGroup( 6)
2051
2102
sage: g = G( '( 1,2,3) ')
2052
- sage: g. word_problem( [g ], False)
2103
+ sage: g. word_problem( [g ], display= False, as_list=False ) # Default algorithm
2053
2104
( 'x1', '( 1,2,3) ')
2105
+ sage: g. word_problem( [g ], display=False, as_list=True) # Default algorithm
2106
+ [[1, 1 ]]
2054
2107
"""
2055
2108
if not self ._parent._has_natural_domain():
2056
- raise NotImplementedError
2109
+ raise NotImplementedError ( " Word problem only implemented for groups acting on {1, ..., n} " )
2057
2110
2058
- def convert_back (string ):
2059
- L = copy.copy(string)
2060
- for i, w_i in enumerate (words):
2061
- L = L.replace(" x" + str (i + 1 ), str (w_i))
2062
- return L
2063
-
2064
- g = words[0 ].parent()(self )
2065
- H = libgap.Group(words)
2066
- ans = H.EpimorphismFromFreeGroup().PreImagesRepresentative(g)
2067
-
2068
- l1 = str (ans)
2069
- l2 = convert_back(l1)
2111
+ if not words:
2112
+ if self == self .parent().one():
2113
+ if as_list: return []
2114
+ else : return (' ' , ' ' )
2115
+ else :
2116
+ raise ValueError (" Cannot express non-identity element with zero generators" )
2070
2117
2071
- if display or as_list:
2072
- l3 = l1.split(" *" )
2073
- l4 = []
2074
- for m in l3: # parsing the word for display
2075
- m_split = m.split(" ^" )
2076
- if len (m_split) == 2 :
2077
- l4.append([m_split[0 ], int (m_split[1 ])])
2118
+ # Ensure self is in the parent group of the generators
2119
+ # Coerce self into the parent group of the first generator
2120
+ # This handles cases where self might be from a different but compatible group (e.g., S_n)
2121
+ try :
2122
+ g = words[0 ].parent()(self )
2123
+ except (TypeError , ValueError ) as e:
2124
+ raise ValueError (f" Element {self} cannot be coerced into the parent group of the generators: {e}" )
2125
+
2126
+ # --- Legacy Algorithm ---
2127
+ if algorithm == ' legacy' :
2128
+ warnings.warn(
2129
+ " The 'legacy' algorithm for word_problem is deprecated due to known bugs "
2130
+ " in string parsing and will be removed in a future Sage version. "
2131
+ " Use the default 'syllable' algorithm instead." ,
2132
+ DeprecationWarning , stacklevel = 2 )
2133
+
2134
+ # Original flawed convert_back function (only used for legacy)
2135
+ def convert_back_legacy (string_in ):
2136
+ L = copy.copy(string_in) # Copy needed here? Strings immutable. Keep for safety.
2137
+ for i, w_i in enumerate (words):
2138
+ # This replace is the source of the x10 vs x1 bug
2139
+ L = L.replace(" x" + str (i + 1 ), str (w_i))
2140
+ return L
2141
+
2142
+ H_legacy = libgap.Group(words)
2143
+ try :
2144
+ ans_legacy = H_legacy.EpimorphismFromFreeGroup().PreImagesRepresentative(g)
2145
+ except RuntimeError as e:
2146
+ # Check if element is not in subgroup
2147
+ if " is not in" in str (e) and " generated by" in str (e):
2148
+ raise ValueError (f" Element {g} is not in the subgroup generated by the given words." )
2149
+ else :
2150
+ raise # Re-raise other GAP errors
2151
+
2152
+ l1_legacy = str (ans_legacy)
2153
+ l2_legacy = convert_back_legacy(l1_legacy)
2154
+
2155
+ if display or as_list:
2156
+ l3_legacy = l1_legacy.split(" *" )
2157
+ l4_legacy = []
2158
+ # Original flawed parsing logic
2159
+ for m in l3_legacy:
2160
+ m_split = m.split(" ^" )
2161
+ # This parsing fails on nested parentheses like (x1*x2)^2
2162
+ if len (m_split) == 2 :
2163
+ # Need to handle potential errors if exponent isn't int
2164
+ try :
2165
+ exponent = int (m_split[1 ])
2166
+ except ValueError :
2167
+ # Handle cases where parsing might fail unexpectedly
2168
+ # This part of legacy is inherently broken for complex cases
2169
+ exponent = 1 # Or raise error? Legacy behavior unclear.
2170
+ l4_legacy.append([m_split[0 ], exponent])
2171
+ else :
2172
+ l4_legacy.append([m_split[0 ], 1 ])
2173
+ # Apply flawed conversion
2174
+ l5_legacy = [[convert_back_legacy(w), e] for w, e in l4_legacy]
2175
+
2176
+ if display:
2177
+ print (l1_legacy)
2178
+ print (l5_legacy)
2179
+
2180
+ if as_list:
2181
+ return l5_legacy
2182
+ else :
2183
+ return l1_legacy, l2_legacy
2184
+
2185
+ # --- Syllable Algorithm (Default) ---
2186
+ elif algorithm == ' syllable' :
2187
+ H = libgap.Group(words)
2188
+ try :
2189
+ ans = H.EpimorphismFromFreeGroup().PreImagesRepresentative(g)
2190
+ except RuntimeError as e:
2191
+ # Check if element is not in subgroup
2192
+ if " is not in" in str (e) and " generated by" in str (e):
2193
+ raise ValueError (f" Element {g} is not in the subgroup generated by the given words." )
2194
+ else :
2195
+ raise # Re-raise other GAP errors
2196
+
2197
+ l1 = str (ans) # Get the raw GAP string representation (x1*x2^...)
2198
+
2199
+ # Build the list representation using syllable functions
2200
+ num_syllables = ans.NumberSyllables()
2201
+ syllable_list_indices = [] # List of [index, exponent]
2202
+ if num_syllables > 0 : # Handle identity element case
2203
+ for i in range (1 , num_syllables + 1 ):
2204
+ # GeneratorSyllable returns 1-based index
2205
+ generator_index = int (ans.GeneratorSyllable(i))
2206
+ exponent = int (ans.ExponentSyllable(i))
2207
+ syllable_list_indices.append([generator_index, exponent])
2208
+
2209
+ # Build the substituted string representation (l2) correctly
2210
+ l2_parts = []
2211
+ for idx, exp in syllable_list_indices:
2212
+ gen_str = str (words[idx- 1 ]) # Get generator string (use idx-1 for 0-based list)
2213
+ if exp == 1 :
2214
+ l2_parts.append(gen_str)
2078
2215
else :
2079
- l4.append([m_split[0 ], 1 ])
2080
- l5 = [[convert_back(w), e] for w, e in l4]
2081
-
2082
- if display:
2083
- print (l1)
2084
- print (l5)
2216
+ # Handle potential need for parentheses around generator string if it contains spaces etc.
2217
+ # For permutations, str() usually produces cycle notation without spaces,
2218
+ # so parentheses might not be strictly needed, but safer? Let's omit for now.
2219
+ l2_parts.append(f" {gen_str}^{exp}" )
2220
+ l2 = " *" .join(l2_parts)
2221
+
2222
+ if display:
2223
+ print (l1)
2224
+ print (syllable_list_indices) # Display the new index-based list
2225
+
2226
+ if as_list:
2227
+ return syllable_list_indices
2228
+ else :
2229
+ return l1, l2
2085
2230
2086
- if as_list:
2087
- return l5
2088
2231
else :
2089
- return l1, l2
2232
+ raise ValueError (f " Unknown algorithm specified: '{algorithm}'. Choose 'syllable' or 'legacy'. " )
2090
2233
2091
2234
2092
2235
cdef class SymmetricGroupElement(PermutationGroupElement):
0 commit comments