Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Corrected word_problem parsing and added algorithm option #39820

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
268 changes: 209 additions & 59 deletions src/sage/groups/perm_gps/permgroup_element.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ Check that :issue:`13569` is fixed::
# ****************************************************************************

import copy
import warnings

from libc.stdlib cimport qsort

Expand Down Expand Up @@ -1991,102 +1992,251 @@ cdef class PermutationGroupElement(MultiplicativeGroupElement):
entries[i, self.perm[i]] = 1
return M(entries)

def word_problem(self, words, display=True, as_list=False):
def word_problem(self, words, display=True, as_list=False, algorithm='syllable'):
r"""
Try to solve the word problem for ``self``.

INPUT:

- ``words`` -- list of elements of the ambient group, generating
a subgroup

- ``display`` -- boolean (default: ``True``); whether to display
additional information

- ``as_list`` -- boolean (default: ``False``); whether to return
the result as a list of pairs (generator, exponent)
- ``words`` -- a list of elements of the ambient group, generating
a subgroup `H`.
- ``display`` -- boolean (default ``True``); whether to display
additional information (the raw GAP word and the list form).
- ``as_list`` -- boolean (default ``False``); whether to return
the result as a list. See Output section for format details.
- ``algorithm`` -- string (default ``'syllable'``); the algorithm to use:
- ``'syllable'``: (Recommended) Uses GAP's syllable functions
for robust parsing. Returns indices in list format.
- ``'legacy'``: Uses the old, broken string parsing method.
Preserved for backward compatibility testing but is deprecated
and will be removed. Returns strings in list format.

OUTPUT:

- a pair of strings, both representing the same word
If `g = self` is in the subgroup `H` generated by ``words``, this
function returns an expression for `g` as a word in the elements
of ``words`` and their inverses.

or
- If ``as_list=False`` (default): Returns a pair of strings `(l1, l2)`.
- `l1`: The word in terms of abstract generators `x1, x2, ...`.
- `l2`: The word with the actual generator strings substituted.

- a list of pairs representing the word, each pair being
(generator as a string, exponent as an integer)
- If ``as_list=True``:
- If ``algorithm='syllable'`` (default): Returns a list of pairs
`[generator_index, exponent]`, where `generator_index` is
the 1-based index into the input ``words`` list.
- If ``algorithm='legacy'``: Returns a list of pairs
`[generator_string, exponent]`. This algorithm is deprecated.

Let `G` be the ambient permutation group, containing the given
element `g`. Let `H` be the subgroup of `G` generated by the list
``words`` of elements of `G`. If `g` is in `H`, this function
returns an expression for `g` as a word in the elements of
``words`` and their inverses.

This function does not solve the word problem in Sage. Rather it
pushes it over to GAP, which has optimized algorithms for the word
problem. Essentially, this function is a wrapper for the GAP
functions ``EpimorphismFromFreeGroup`` and
This function uses GAP's ``EpimorphismFromFreeGroup`` and
``PreImagesRepresentative``.

EXAMPLES::

sage: G = PermutationGroup([[(1,2,3),(4,5)],[(3,4)]], canonicalize=False)
sage: g1, g2 = G.gens()
sage: h = g1^2*g2*g1
sage: h.word_problem([g1,g2], False)
sage: h.word_problem([g1,g2], display=False, as_list=False) # Default algorithm
('x1^2*x2^-1*x1', '(1,2,3)(4,5)^2*(3,4)^-1*(1,2,3)(4,5)')

sage: h.word_problem([g1,g2])
x1^2*x2^-1*x1
[['(1,2,3)(4,5)', 2], ['(3,4)', -1], ['(1,2,3)(4,5)', 1]]
Displaying output (using the default 'syllable' algorithm)::

sage: h.word_problem([g1, g2])
x1^2*x2^-1*x1
[[1, 2], [2, -1], [1, 1]]
('x1^2*x2^-1*x1', '(1,2,3)(4,5)^2*(3,4)^-1*(1,2,3)(4,5)')

sage: h.word_problem([g1,g2], False, as_list=True)
Getting the list output (default algorithm, uses indices)::

sage: h.word_problem([g1, g2], display=False, as_list=True)
[[1, 2], [2, -1], [1, 1]]

Using the legacy algorithm (deprecated, returns strings in list)::

sage: h.word_problem([g1, g2], display=False, as_list=True, algorithm='legacy')
doctest:warning...:
DeprecationWarning: The 'legacy' algorithm for word_problem is deprecated...
[['(1,2,3)(4,5)', 2], ['(3,4)', -1], ['(1,2,3)(4,5)', 1]]

TESTS:
Fixing :issue:`36419` (parentheses issue)::

sage: P = PermutationGroup([[(1, 2)], [(1, 2, 3, 4)]])
sage: p = P([(1, 4)])
sage: p.word_problem(P.gens(), display=False, as_list=True) # Default algorithm
[[1, -1], [2, -2], [1, -1], [2, -1], [1, -1], [2, 1]]
sage: x1, x2 = P.gens()
sage: word_new = x1^-1*x2^-1*(x2^-1*x1^-1)^2*x2
sage: word_new == p
True

Compare with legacy algorithm (shows the parsing error)::

sage: p.word_problem(P.gens(), display=True, as_list=True, algorithm='legacy')
x2^-1*(x2^-1*x1^-1)^2*x2*x1^-1*x2^-2*x1^-1*x2
[['(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]]
[['(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]]

Fixing :issue:`36419` (x10 vs x1 issue)::

sage: P = PermutationGroup([[(1, 2)]] * 9 + [[(3, 4, 5)]], canonicalize=False)
sage: p = P([(3, 4, 5)])
sage: gens = P.gens()
sage: p.word_problem(gens, display=False, as_list=False) # Default algorithm
('x10', '(3,4,5)')
sage: p.word_problem(gens, display=False, as_list=True) # Default algorithm
[[10, 1]]

Compare with legacy algorithm (shows the replacement error)::

sage: p.word_problem(gens, display=False, as_list=False, algorithm='legacy')
('x10', '(1,2)0')

Compare with legacy algorithm (shows the replacement error)::

sage: p.word_problem(gens, display=False, as_list=True, algorithm='legacy')
[['(1,2)0', 1]]

Check for :issue:`28556`::

sage: G = SymmetricGroup(6)
sage: g = G('(1,2,3)')
sage: g.word_problem([g], False)
sage: g.word_problem([g], display=False, as_list=False) # Default algorithm
('x1', '(1,2,3)')
sage: g.word_problem([g], display=False, as_list=True) # Default algorithm
[[1, 1]]
"""
if not self._parent._has_natural_domain():
raise NotImplementedError

def convert_back(string):
L = copy.copy(string)
for i, w_i in enumerate(words):
L = L.replace("x" + str(i + 1), str(w_i))
return L

g = words[0].parent()(self)
H = libgap.Group(words)
ans = H.EpimorphismFromFreeGroup().PreImagesRepresentative(g)
raise NotImplementedError("Word problem only implemented for groups acting on {1, ..., n}")

l1 = str(ans)
l2 = convert_back(l1)
if not words:
if self == self.parent().one():
if as_list: return []
else: return ('', '')
else:
raise ValueError("Cannot express non-identity element with zero generators")

if display or as_list:
l3 = l1.split("*")
l4 = []
for m in l3: # parsing the word for display
m_split = m.split("^")
if len(m_split) == 2:
l4.append([m_split[0], int(m_split[1])])
# Ensure self is in the parent group of the generators
# Coerce self into the parent group of the first generator
# This handles cases where self might be from a different but compatible group (e.g., S_n)
try:
g = words[0].parent()(self)
except (TypeError, ValueError) as e:
raise ValueError(f"Element {self} cannot be coerced into the parent group of the generators: {e}")

# --- Legacy Algorithm ---
if algorithm == 'legacy':
warnings.warn(
"The 'legacy' algorithm for word_problem is deprecated due to known bugs "
"in string parsing and will be removed in a future Sage version. "
"Use the default 'syllable' algorithm instead.",
DeprecationWarning, stacklevel=2)

# Original flawed convert_back function (only used for legacy)
def convert_back_legacy(string_in):
L = copy.copy(string_in) # Copy needed here? Strings immutable. Keep for safety.
for i, w_i in enumerate(words):
# This replace is the source of the x10 vs x1 bug
L = L.replace("x" + str(i + 1), str(w_i))
return L

H_legacy = libgap.Group(words)
try:
ans_legacy = H_legacy.EpimorphismFromFreeGroup().PreImagesRepresentative(g)
except RuntimeError as e:
# Check if element is not in subgroup
if "is not in" in str(e) and "generated by" in str(e):
raise ValueError(f"Element {g} is not in the subgroup generated by the given words.")
else:
raise # Re-raise other GAP errors

l1_legacy = str(ans_legacy)
l2_legacy = convert_back_legacy(l1_legacy)

if display or as_list:
l3_legacy = l1_legacy.split("*")
l4_legacy = []
# Original flawed parsing logic
for m in l3_legacy:
m_split = m.split("^")
# This parsing fails on nested parentheses like (x1*x2)^2
if len(m_split) == 2:
# Need to handle potential errors if exponent isn't int
try:
exponent = int(m_split[1])
except ValueError:
# Handle cases where parsing might fail unexpectedly
# This part of legacy is inherently broken for complex cases
exponent = 1 # Or raise error? Legacy behavior unclear.
l4_legacy.append([m_split[0], exponent])
else:
l4_legacy.append([m_split[0], 1])
# Apply flawed conversion
l5_legacy = [[convert_back_legacy(w), e] for w, e in l4_legacy]

if display:
print(l1_legacy)
print(l5_legacy)

if as_list:
return l5_legacy
else:
return l1_legacy, l2_legacy

# --- Syllable Algorithm (Default) ---
elif algorithm == 'syllable':
H = libgap.Group(words)
try:
ans = H.EpimorphismFromFreeGroup().PreImagesRepresentative(g)
except RuntimeError as e:
# Check if element is not in subgroup
if "is not in" in str(e) and "generated by" in str(e):
raise ValueError(f"Element {g} is not in the subgroup generated by the given words.")
else:
raise # Re-raise other GAP errors

l1 = str(ans) # Get the raw GAP string representation (x1*x2^...)

# Build the list representation using syllable functions
num_syllables = ans.NumberSyllables()
syllable_list_indices = [] # List of [index, exponent]
if num_syllables > 0 : # Handle identity element case
for i in range(1, num_syllables + 1):
# GeneratorSyllable returns 1-based index
generator_index = int(ans.GeneratorSyllable(i))
exponent = int(ans.ExponentSyllable(i))
syllable_list_indices.append([generator_index, exponent])

# Build the substituted string representation (l2) correctly
l2_parts = []
for idx, exp in syllable_list_indices:
gen_str = str(words[idx-1]) # Get generator string (use idx-1 for 0-based list)
if exp == 1:
l2_parts.append(gen_str)
else:
l4.append([m_split[0], 1])
l5 = [[convert_back(w), e] for w, e in l4]

if display:
print(l1)
print(l5)
# Handle potential need for parentheses around generator string if it contains spaces etc.
# For permutations, str() usually produces cycle notation without spaces,
# so parentheses might not be strictly needed, but safer? Let's omit for now.
l2_parts.append(f"{gen_str}^{exp}")
l2 = "*".join(l2_parts)

if display:
print(l1)
print(syllable_list_indices) # Display the new index-based list

if as_list:
return syllable_list_indices
else:
return l1, l2

if as_list:
return l5
else:
return l1, l2
raise ValueError(f"Unknown algorithm specified: '{algorithm}'. Choose 'syllable' or 'legacy'.")


cdef class SymmetricGroupElement(PermutationGroupElement):
Expand Down