@@ -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,252 @@ 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 ( using the default 'syllable' algorithm) ::
2042
+
2043
+ sage: h. word_problem( [g1, g2 ])
2044
+ x1^ 2* x2^ -1* x1
2045
+ [[1, 2 ], [2, -1 ], [1, 1 ]]
2041
2046
( 'x1^ 2* x2^ -1* x1', '( 1,2,3) ( 4,5) ^ 2* ( 3,4) ^ -1* ( 1,2,3) ( 4,5) ')
2042
2047
2043
- sage: h. word_problem( [g1,g2 ], False, as_list=True)
2048
+ Getting the list output ( default algorithm, uses indices) ::
2049
+
2050
+ sage: h. word_problem( [g1, g2 ], display=False, as_list=True)
2051
+ [[1, 2 ], [2, -1 ], [1, 1 ]]
2052
+
2053
+ Using the legacy algorithm ( deprecated, returns strings in list) ::
2054
+
2055
+ sage: h. word_problem( [g1, g2 ], display=False, as_list=True, algorithm='legacy')
2056
+ doctest:warning... :
2057
+ DeprecationWarning: The 'legacy' algorithm for word_problem is deprecated...
2044
2058
[['(1,2,3)(4,5)', 2 ], ['(3,4)', -1 ], ['(1,2,3)(4,5)', 1 ]]
2045
2059
2046
- TESTS:
2060
+ Fixing :issue:`36419` ( parentheses issue) ::
2061
+
2062
+ sage: P = PermutationGroup( [[(1, 2) ], [(1, 2, 3, 4) ]])
2063
+ sage: p = P( [(1, 4) ])
2064
+ sage: p. word_problem( P. gens( ) , display=True, as_list=True) # Default algorithm
2065
+ x1^ -1* x2^ -1* ( x2^ -1* x1^ -1) ^ 2* x2
2066
+ [[1, -1 ], [2, -2 ], [1, -1 ], [2, -1 ], [1, -1 ], [2, 1 ]]
2067
+ [[1, -1 ], [2, -2 ], [1, -1 ], [2, -1 ], [1, -1 ], [2, 1 ]]
2068
+ sage: x1, x2 = P. gens( )
2069
+ sage: word_new = x1^ -1* x2^ -1* ( x2^ -1* x1^ -1) ^ 2* x2
2070
+ sage: word_new == p
2071
+ True
2072
+
2073
+ Compare with legacy algorithm ( shows the parsing error) ::
2074
+
2075
+ sage: p. word_problem( P. gens( ) , display=True, as_list=True, algorithm='legacy')
2076
+ x2^ -1* ( x2^ -1* x1^ -1) ^ 2* x2* x1^ -1* x2^ -2* x1^ -1* x2
2077
+ [['(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 ]]
2078
+ [['(1,2,3,4)', -1 ],
2079
+ ['((1,2,3,4)', -1 ],
2080
+ ['(1,2)', 1 ],
2081
+ ['(1,2,3,4)', 1 ],
2082
+ ['(1,2)', -1 ],
2083
+ ['(1,2,3,4)', -2 ],
2084
+ ['(1,2)', -1 ],
2085
+ ['(1,2,3,4)', 1 ]]
2086
+
2087
+ Fixing :issue:`36419` ( x10 vs x1 issue) ::
2088
+
2089
+ sage: P = PermutationGroup( [[(1, 2) ]] * 9 + [[(3, 4, 5) ]], canonicalize=False)
2090
+ sage: p = P( [(3, 4, 5) ])
2091
+ sage: gens = P. gens( )
2092
+ sage: p. word_problem( gens, display=False, as_list=False) # Default algorithm
2093
+ ( 'x10', '( 3,4,5) ')
2094
+ sage: p. word_problem( gens, display=False, as_list=True) # Default algorithm
2095
+ [[10, 1 ]]
2096
+
2097
+ Compare with legacy algorithm ( shows the replacement error) ::
2098
+
2099
+ sage: p. word_problem( gens, display=False, as_list=False, algorithm='legacy')
2100
+ ( 'x10', '( 1,2) 0')
2101
+
2102
+ Compare with legacy algorithm ( shows the replacement error) ::
2103
+
2104
+ sage: p. word_problem( gens, display=False, as_list=True, algorithm='legacy')
2105
+ [['(1,2)0', 1 ]]
2047
2106
2048
2107
Check for :issue:`28556`::
2049
2108
2050
2109
sage: G = SymmetricGroup( 6)
2051
2110
sage: g = G( '( 1,2,3) ')
2052
- sage: g. word_problem( [g ], False)
2111
+ sage: g. word_problem( [g ], display= False, as_list=False ) # Default algorithm
2053
2112
( 'x1', '( 1,2,3) ')
2113
+ sage: g. word_problem( [g ], display=False, as_list=True) # Default algorithm
2114
+ [[1, 1 ]]
2054
2115
"""
2055
2116
if not self ._parent._has_natural_domain():
2056
- raise NotImplementedError
2057
-
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
2117
+ raise NotImplementedError (" Word problem only implemented for groups acting on {1, ..., n}" )
2063
2118
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 )
2119
+ if not words:
2120
+ if self == self .parent().one():
2121
+ if as_list: return []
2122
+ else : return ( ' ' , ' ' )
2123
+ else :
2124
+ raise ValueError ( " Cannot express non-identity element with zero generators " )
2070
2125
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 ])])
2126
+ # Ensure self is in the parent group of the generators
2127
+ # Coerce self into the parent group of the first generator
2128
+ # This handles cases where self might be from a different but compatible group (e.g., S_n)
2129
+ try :
2130
+ g = words[0 ].parent()(self )
2131
+ except (TypeError , ValueError ) as e:
2132
+ raise ValueError (f" Element {self} cannot be coerced into the parent group of the generators: {e}" )
2133
+
2134
+ # --- Legacy Algorithm ---
2135
+ if algorithm == ' legacy' :
2136
+ warnings.warn(
2137
+ " The 'legacy' algorithm for word_problem is deprecated due to known bugs "
2138
+ " in string parsing and will be removed in a future Sage version. "
2139
+ " Use the default 'syllable' algorithm instead." ,
2140
+ DeprecationWarning , stacklevel = 2 )
2141
+
2142
+ # Original flawed convert_back function (only used for legacy)
2143
+ def convert_back_legacy (string_in ):
2144
+ L = copy.copy(string_in) # Copy needed here? Strings immutable. Keep for safety.
2145
+ for i, w_i in enumerate (words):
2146
+ # This replace is the source of the x10 vs x1 bug
2147
+ L = L.replace(" x" + str (i + 1 ), str (w_i))
2148
+ return L
2149
+
2150
+ H_legacy = libgap.Group(words)
2151
+ try :
2152
+ ans_legacy = H_legacy.EpimorphismFromFreeGroup().PreImagesRepresentative(g)
2153
+ except RuntimeError as e:
2154
+ # Check if element is not in subgroup
2155
+ if " is not in" in str (e) and " generated by" in str (e):
2156
+ raise ValueError (f" Element {g} is not in the subgroup generated by the given words." )
2157
+ else :
2158
+ raise # Re-raise other GAP errors
2159
+
2160
+ l1_legacy = str (ans_legacy)
2161
+ l2_legacy = convert_back_legacy(l1_legacy)
2162
+
2163
+ if display or as_list:
2164
+ l3_legacy = l1_legacy.split(" *" )
2165
+ l4_legacy = []
2166
+ # Original flawed parsing logic
2167
+ for m in l3_legacy:
2168
+ m_split = m.split(" ^" )
2169
+ # This parsing fails on nested parentheses like (x1*x2)^2
2170
+ if len (m_split) == 2 :
2171
+ # Need to handle potential errors if exponent isn't int
2172
+ try :
2173
+ exponent = int (m_split[1 ])
2174
+ except ValueError :
2175
+ # Handle cases where parsing might fail unexpectedly
2176
+ # This part of legacy is inherently broken for complex cases
2177
+ exponent = 1 # Or raise error? Legacy behavior unclear.
2178
+ l4_legacy.append([m_split[0 ], exponent])
2179
+ else :
2180
+ l4_legacy.append([m_split[0 ], 1 ])
2181
+ # Apply flawed conversion
2182
+ l5_legacy = [[convert_back_legacy(w), e] for w, e in l4_legacy]
2183
+
2184
+ if display:
2185
+ print (l1_legacy)
2186
+ print (l5_legacy)
2187
+
2188
+ if as_list:
2189
+ return l5_legacy
2190
+ else :
2191
+ return l1_legacy, l2_legacy
2192
+
2193
+ # --- Syllable Algorithm (Default) ---
2194
+ elif algorithm == ' syllable' :
2195
+ H = libgap.Group(words)
2196
+ try :
2197
+ ans = H.EpimorphismFromFreeGroup().PreImagesRepresentative(g)
2198
+ except RuntimeError as e:
2199
+ # Check if element is not in subgroup
2200
+ if " is not in" in str (e) and " generated by" in str (e):
2201
+ raise ValueError (f" Element {g} is not in the subgroup generated by the given words." )
2202
+ else :
2203
+ raise # Re-raise other GAP errors
2204
+
2205
+ l1 = str (ans) # Get the raw GAP string representation (x1*x2^...)
2206
+
2207
+ # Build the list representation using syllable functions
2208
+ num_syllables = ans.NumberSyllables()
2209
+ syllable_list_indices = [] # List of [index, exponent]
2210
+ if num_syllables > 0 : # Handle identity element case
2211
+ for i in range (1 , num_syllables + 1 ):
2212
+ # GeneratorSyllable returns 1-based index
2213
+ generator_index = int (ans.GeneratorSyllable(i))
2214
+ exponent = int (ans.ExponentSyllable(i))
2215
+ syllable_list_indices.append([generator_index, exponent])
2216
+
2217
+ # Build the substituted string representation (l2) correctly
2218
+ l2_parts = []
2219
+ for idx, exp in syllable_list_indices:
2220
+ gen_str = str (words[idx- 1 ]) # Get generator string (use idx-1 for 0-based list)
2221
+ if exp == 1 :
2222
+ l2_parts.append(gen_str)
2078
2223
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)
2224
+ # Handle potential need for parentheses around generator string if it contains spaces etc.
2225
+ # For permutations, str() usually produces cycle notation without spaces,
2226
+ # so parentheses might not be strictly needed, but safer? Let's omit for now.
2227
+ l2_parts.append(f" {gen_str}^{exp}" )
2228
+ l2 = " *" .join(l2_parts)
2229
+
2230
+ if display:
2231
+ print (l1)
2232
+ print (syllable_list_indices) # Display the new index-based list
2233
+
2234
+ if as_list:
2235
+ return syllable_list_indices
2236
+ else :
2237
+ return l1, l2
2085
2238
2086
- if as_list:
2087
- return l5
2088
2239
else :
2089
- return l1, l2
2240
+ raise ValueError (f " Unknown algorithm specified: '{algorithm}'. Choose 'syllable' or 'legacy'. " )
2090
2241
2091
2242
2092
2243
cdef class SymmetricGroupElement(PermutationGroupElement):
0 commit comments