1349 lines
47 KiB
Python
1349 lines
47 KiB
Python
|
"""Finitely Presented Groups and its algorithms. """
|
||
|
|
||
|
from sympy.core.singleton import S
|
||
|
from sympy.core.symbol import symbols
|
||
|
from sympy.combinatorics.free_groups import (FreeGroup, FreeGroupElement,
|
||
|
free_group)
|
||
|
from sympy.combinatorics.rewritingsystem import RewritingSystem
|
||
|
from sympy.combinatorics.coset_table import (CosetTable,
|
||
|
coset_enumeration_r,
|
||
|
coset_enumeration_c)
|
||
|
from sympy.combinatorics import PermutationGroup
|
||
|
from sympy.matrices.normalforms import invariant_factors
|
||
|
from sympy.matrices import Matrix
|
||
|
from sympy.polys.polytools import gcd
|
||
|
from sympy.printing.defaults import DefaultPrinting
|
||
|
from sympy.utilities import public
|
||
|
from sympy.utilities.magic import pollute
|
||
|
|
||
|
from itertools import product
|
||
|
|
||
|
|
||
|
@public
|
||
|
def fp_group(fr_grp, relators=()):
|
||
|
_fp_group = FpGroup(fr_grp, relators)
|
||
|
return (_fp_group,) + tuple(_fp_group._generators)
|
||
|
|
||
|
@public
|
||
|
def xfp_group(fr_grp, relators=()):
|
||
|
_fp_group = FpGroup(fr_grp, relators)
|
||
|
return (_fp_group, _fp_group._generators)
|
||
|
|
||
|
# Does not work. Both symbols and pollute are undefined. Never tested.
|
||
|
@public
|
||
|
def vfp_group(fr_grpm, relators):
|
||
|
_fp_group = FpGroup(symbols, relators)
|
||
|
pollute([sym.name for sym in _fp_group.symbols], _fp_group.generators)
|
||
|
return _fp_group
|
||
|
|
||
|
|
||
|
def _parse_relators(rels):
|
||
|
"""Parse the passed relators."""
|
||
|
return rels
|
||
|
|
||
|
|
||
|
###############################################################################
|
||
|
# FINITELY PRESENTED GROUPS #
|
||
|
###############################################################################
|
||
|
|
||
|
|
||
|
class FpGroup(DefaultPrinting):
|
||
|
"""
|
||
|
The FpGroup would take a FreeGroup and a list/tuple of relators, the
|
||
|
relators would be specified in such a way that each of them be equal to the
|
||
|
identity of the provided free group.
|
||
|
|
||
|
"""
|
||
|
is_group = True
|
||
|
is_FpGroup = True
|
||
|
is_PermutationGroup = False
|
||
|
|
||
|
def __init__(self, fr_grp, relators):
|
||
|
relators = _parse_relators(relators)
|
||
|
self.free_group = fr_grp
|
||
|
self.relators = relators
|
||
|
self.generators = self._generators()
|
||
|
self.dtype = type("FpGroupElement", (FpGroupElement,), {"group": self})
|
||
|
|
||
|
# CosetTable instance on identity subgroup
|
||
|
self._coset_table = None
|
||
|
# returns whether coset table on identity subgroup
|
||
|
# has been standardized
|
||
|
self._is_standardized = False
|
||
|
|
||
|
self._order = None
|
||
|
self._center = None
|
||
|
|
||
|
self._rewriting_system = RewritingSystem(self)
|
||
|
self._perm_isomorphism = None
|
||
|
return
|
||
|
|
||
|
def _generators(self):
|
||
|
return self.free_group.generators
|
||
|
|
||
|
def make_confluent(self):
|
||
|
'''
|
||
|
Try to make the group's rewriting system confluent
|
||
|
|
||
|
'''
|
||
|
self._rewriting_system.make_confluent()
|
||
|
return
|
||
|
|
||
|
def reduce(self, word):
|
||
|
'''
|
||
|
Return the reduced form of `word` in `self` according to the group's
|
||
|
rewriting system. If it's confluent, the reduced form is the unique normal
|
||
|
form of the word in the group.
|
||
|
|
||
|
'''
|
||
|
return self._rewriting_system.reduce(word)
|
||
|
|
||
|
def equals(self, word1, word2):
|
||
|
'''
|
||
|
Compare `word1` and `word2` for equality in the group
|
||
|
using the group's rewriting system. If the system is
|
||
|
confluent, the returned answer is necessarily correct.
|
||
|
(If it is not, `False` could be returned in some cases
|
||
|
where in fact `word1 == word2`)
|
||
|
|
||
|
'''
|
||
|
if self.reduce(word1*word2**-1) == self.identity:
|
||
|
return True
|
||
|
elif self._rewriting_system.is_confluent:
|
||
|
return False
|
||
|
return None
|
||
|
|
||
|
@property
|
||
|
def identity(self):
|
||
|
return self.free_group.identity
|
||
|
|
||
|
def __contains__(self, g):
|
||
|
return g in self.free_group
|
||
|
|
||
|
def subgroup(self, gens, C=None, homomorphism=False):
|
||
|
'''
|
||
|
Return the subgroup generated by `gens` using the
|
||
|
Reidemeister-Schreier algorithm
|
||
|
homomorphism -- When set to True, return a dictionary containing the images
|
||
|
of the presentation generators in the original group.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.combinatorics.fp_groups import FpGroup
|
||
|
>>> from sympy.combinatorics import free_group
|
||
|
>>> F, x, y = free_group("x, y")
|
||
|
>>> f = FpGroup(F, [x**3, y**5, (x*y)**2])
|
||
|
>>> H = [x*y, x**-1*y**-1*x*y*x]
|
||
|
>>> K, T = f.subgroup(H, homomorphism=True)
|
||
|
>>> T(K.generators)
|
||
|
[x*y, x**-1*y**2*x**-1]
|
||
|
|
||
|
'''
|
||
|
|
||
|
if not all(isinstance(g, FreeGroupElement) for g in gens):
|
||
|
raise ValueError("Generators must be `FreeGroupElement`s")
|
||
|
if not all(g.group == self.free_group for g in gens):
|
||
|
raise ValueError("Given generators are not members of the group")
|
||
|
if homomorphism:
|
||
|
g, rels, _gens = reidemeister_presentation(self, gens, C=C, homomorphism=True)
|
||
|
else:
|
||
|
g, rels = reidemeister_presentation(self, gens, C=C)
|
||
|
if g:
|
||
|
g = FpGroup(g[0].group, rels)
|
||
|
else:
|
||
|
g = FpGroup(free_group('')[0], [])
|
||
|
if homomorphism:
|
||
|
from sympy.combinatorics.homomorphisms import homomorphism
|
||
|
return g, homomorphism(g, self, g.generators, _gens, check=False)
|
||
|
return g
|
||
|
|
||
|
def coset_enumeration(self, H, strategy="relator_based", max_cosets=None,
|
||
|
draft=None, incomplete=False):
|
||
|
"""
|
||
|
Return an instance of ``coset table``, when Todd-Coxeter algorithm is
|
||
|
run over the ``self`` with ``H`` as subgroup, using ``strategy``
|
||
|
argument as strategy. The returned coset table is compressed but not
|
||
|
standardized.
|
||
|
|
||
|
An instance of `CosetTable` for `fp_grp` can be passed as the keyword
|
||
|
argument `draft` in which case the coset enumeration will start with
|
||
|
that instance and attempt to complete it.
|
||
|
|
||
|
When `incomplete` is `True` and the function is unable to complete for
|
||
|
some reason, the partially complete table will be returned.
|
||
|
|
||
|
"""
|
||
|
if not max_cosets:
|
||
|
max_cosets = CosetTable.coset_table_max_limit
|
||
|
if strategy == 'relator_based':
|
||
|
C = coset_enumeration_r(self, H, max_cosets=max_cosets,
|
||
|
draft=draft, incomplete=incomplete)
|
||
|
else:
|
||
|
C = coset_enumeration_c(self, H, max_cosets=max_cosets,
|
||
|
draft=draft, incomplete=incomplete)
|
||
|
if C.is_complete():
|
||
|
C.compress()
|
||
|
return C
|
||
|
|
||
|
def standardize_coset_table(self):
|
||
|
"""
|
||
|
Standardized the coset table ``self`` and makes the internal variable
|
||
|
``_is_standardized`` equal to ``True``.
|
||
|
|
||
|
"""
|
||
|
self._coset_table.standardize()
|
||
|
self._is_standardized = True
|
||
|
|
||
|
def coset_table(self, H, strategy="relator_based", max_cosets=None,
|
||
|
draft=None, incomplete=False):
|
||
|
"""
|
||
|
Return the mathematical coset table of ``self`` in ``H``.
|
||
|
|
||
|
"""
|
||
|
if not H:
|
||
|
if self._coset_table is not None:
|
||
|
if not self._is_standardized:
|
||
|
self.standardize_coset_table()
|
||
|
else:
|
||
|
C = self.coset_enumeration([], strategy, max_cosets=max_cosets,
|
||
|
draft=draft, incomplete=incomplete)
|
||
|
self._coset_table = C
|
||
|
self.standardize_coset_table()
|
||
|
return self._coset_table.table
|
||
|
else:
|
||
|
C = self.coset_enumeration(H, strategy, max_cosets=max_cosets,
|
||
|
draft=draft, incomplete=incomplete)
|
||
|
C.standardize()
|
||
|
return C.table
|
||
|
|
||
|
def order(self, strategy="relator_based"):
|
||
|
"""
|
||
|
Returns the order of the finitely presented group ``self``. It uses
|
||
|
the coset enumeration with identity group as subgroup, i.e ``H=[]``.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.combinatorics import free_group
|
||
|
>>> from sympy.combinatorics.fp_groups import FpGroup
|
||
|
>>> F, x, y = free_group("x, y")
|
||
|
>>> f = FpGroup(F, [x, y**2])
|
||
|
>>> f.order(strategy="coset_table_based")
|
||
|
2
|
||
|
|
||
|
"""
|
||
|
if self._order is not None:
|
||
|
return self._order
|
||
|
if self._coset_table is not None:
|
||
|
self._order = len(self._coset_table.table)
|
||
|
elif len(self.relators) == 0:
|
||
|
self._order = self.free_group.order()
|
||
|
elif len(self.generators) == 1:
|
||
|
self._order = abs(gcd([r.array_form[0][1] for r in self.relators]))
|
||
|
elif self._is_infinite():
|
||
|
self._order = S.Infinity
|
||
|
else:
|
||
|
gens, C = self._finite_index_subgroup()
|
||
|
if C:
|
||
|
ind = len(C.table)
|
||
|
self._order = ind*self.subgroup(gens, C=C).order()
|
||
|
else:
|
||
|
self._order = self.index([])
|
||
|
return self._order
|
||
|
|
||
|
def _is_infinite(self):
|
||
|
'''
|
||
|
Test if the group is infinite. Return `True` if the test succeeds
|
||
|
and `None` otherwise
|
||
|
|
||
|
'''
|
||
|
used_gens = set()
|
||
|
for r in self.relators:
|
||
|
used_gens.update(r.contains_generators())
|
||
|
if not set(self.generators) <= used_gens:
|
||
|
return True
|
||
|
# Abelianisation test: check is the abelianisation is infinite
|
||
|
abelian_rels = []
|
||
|
for rel in self.relators:
|
||
|
abelian_rels.append([rel.exponent_sum(g) for g in self.generators])
|
||
|
m = Matrix(Matrix(abelian_rels))
|
||
|
if 0 in invariant_factors(m):
|
||
|
return True
|
||
|
else:
|
||
|
return None
|
||
|
|
||
|
|
||
|
def _finite_index_subgroup(self, s=None):
|
||
|
'''
|
||
|
Find the elements of `self` that generate a finite index subgroup
|
||
|
and, if found, return the list of elements and the coset table of `self` by
|
||
|
the subgroup, otherwise return `(None, None)`
|
||
|
|
||
|
'''
|
||
|
gen = self.most_frequent_generator()
|
||
|
rels = list(self.generators)
|
||
|
rels.extend(self.relators)
|
||
|
if not s:
|
||
|
if len(self.generators) == 2:
|
||
|
s = [gen] + [g for g in self.generators if g != gen]
|
||
|
else:
|
||
|
rand = self.free_group.identity
|
||
|
i = 0
|
||
|
while ((rand in rels or rand**-1 in rels or rand.is_identity)
|
||
|
and i<10):
|
||
|
rand = self.random()
|
||
|
i += 1
|
||
|
s = [gen, rand] + [g for g in self.generators if g != gen]
|
||
|
mid = (len(s)+1)//2
|
||
|
half1 = s[:mid]
|
||
|
half2 = s[mid:]
|
||
|
draft1 = None
|
||
|
draft2 = None
|
||
|
m = 200
|
||
|
C = None
|
||
|
while not C and (m/2 < CosetTable.coset_table_max_limit):
|
||
|
m = min(m, CosetTable.coset_table_max_limit)
|
||
|
draft1 = self.coset_enumeration(half1, max_cosets=m,
|
||
|
draft=draft1, incomplete=True)
|
||
|
if draft1.is_complete():
|
||
|
C = draft1
|
||
|
half = half1
|
||
|
else:
|
||
|
draft2 = self.coset_enumeration(half2, max_cosets=m,
|
||
|
draft=draft2, incomplete=True)
|
||
|
if draft2.is_complete():
|
||
|
C = draft2
|
||
|
half = half2
|
||
|
if not C:
|
||
|
m *= 2
|
||
|
if not C:
|
||
|
return None, None
|
||
|
C.compress()
|
||
|
return half, C
|
||
|
|
||
|
def most_frequent_generator(self):
|
||
|
gens = self.generators
|
||
|
rels = self.relators
|
||
|
freqs = [sum([r.generator_count(g) for r in rels]) for g in gens]
|
||
|
return gens[freqs.index(max(freqs))]
|
||
|
|
||
|
def random(self):
|
||
|
import random
|
||
|
r = self.free_group.identity
|
||
|
for i in range(random.randint(2,3)):
|
||
|
r = r*random.choice(self.generators)**random.choice([1,-1])
|
||
|
return r
|
||
|
|
||
|
def index(self, H, strategy="relator_based"):
|
||
|
"""
|
||
|
Return the index of subgroup ``H`` in group ``self``.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.combinatorics import free_group
|
||
|
>>> from sympy.combinatorics.fp_groups import FpGroup
|
||
|
>>> F, x, y = free_group("x, y")
|
||
|
>>> f = FpGroup(F, [x**5, y**4, y*x*y**3*x**3])
|
||
|
>>> f.index([x])
|
||
|
4
|
||
|
|
||
|
"""
|
||
|
# TODO: use |G:H| = |G|/|H| (currently H can't be made into a group)
|
||
|
# when we know |G| and |H|
|
||
|
|
||
|
if H == []:
|
||
|
return self.order()
|
||
|
else:
|
||
|
C = self.coset_enumeration(H, strategy)
|
||
|
return len(C.table)
|
||
|
|
||
|
def __str__(self):
|
||
|
if self.free_group.rank > 30:
|
||
|
str_form = "<fp group with %s generators>" % self.free_group.rank
|
||
|
else:
|
||
|
str_form = "<fp group on the generators %s>" % str(self.generators)
|
||
|
return str_form
|
||
|
|
||
|
__repr__ = __str__
|
||
|
|
||
|
#==============================================================================
|
||
|
# PERMUTATION GROUP METHODS
|
||
|
#==============================================================================
|
||
|
|
||
|
def _to_perm_group(self):
|
||
|
'''
|
||
|
Return an isomorphic permutation group and the isomorphism.
|
||
|
The implementation is dependent on coset enumeration so
|
||
|
will only terminate for finite groups.
|
||
|
|
||
|
'''
|
||
|
from sympy.combinatorics import Permutation
|
||
|
from sympy.combinatorics.homomorphisms import homomorphism
|
||
|
if self.order() is S.Infinity:
|
||
|
raise NotImplementedError("Permutation presentation of infinite "
|
||
|
"groups is not implemented")
|
||
|
if self._perm_isomorphism:
|
||
|
T = self._perm_isomorphism
|
||
|
P = T.image()
|
||
|
else:
|
||
|
C = self.coset_table([])
|
||
|
gens = self.generators
|
||
|
images = [[C[i][2*gens.index(g)] for i in range(len(C))] for g in gens]
|
||
|
images = [Permutation(i) for i in images]
|
||
|
P = PermutationGroup(images)
|
||
|
T = homomorphism(self, P, gens, images, check=False)
|
||
|
self._perm_isomorphism = T
|
||
|
return P, T
|
||
|
|
||
|
def _perm_group_list(self, method_name, *args):
|
||
|
'''
|
||
|
Given the name of a `PermutationGroup` method (returning a subgroup
|
||
|
or a list of subgroups) and (optionally) additional arguments it takes,
|
||
|
return a list or a list of lists containing the generators of this (or
|
||
|
these) subgroups in terms of the generators of `self`.
|
||
|
|
||
|
'''
|
||
|
P, T = self._to_perm_group()
|
||
|
perm_result = getattr(P, method_name)(*args)
|
||
|
single = False
|
||
|
if isinstance(perm_result, PermutationGroup):
|
||
|
perm_result, single = [perm_result], True
|
||
|
result = []
|
||
|
for group in perm_result:
|
||
|
gens = group.generators
|
||
|
result.append(T.invert(gens))
|
||
|
return result[0] if single else result
|
||
|
|
||
|
def derived_series(self):
|
||
|
'''
|
||
|
Return the list of lists containing the generators
|
||
|
of the subgroups in the derived series of `self`.
|
||
|
|
||
|
'''
|
||
|
return self._perm_group_list('derived_series')
|
||
|
|
||
|
def lower_central_series(self):
|
||
|
'''
|
||
|
Return the list of lists containing the generators
|
||
|
of the subgroups in the lower central series of `self`.
|
||
|
|
||
|
'''
|
||
|
return self._perm_group_list('lower_central_series')
|
||
|
|
||
|
def center(self):
|
||
|
'''
|
||
|
Return the list of generators of the center of `self`.
|
||
|
|
||
|
'''
|
||
|
return self._perm_group_list('center')
|
||
|
|
||
|
|
||
|
def derived_subgroup(self):
|
||
|
'''
|
||
|
Return the list of generators of the derived subgroup of `self`.
|
||
|
|
||
|
'''
|
||
|
return self._perm_group_list('derived_subgroup')
|
||
|
|
||
|
|
||
|
def centralizer(self, other):
|
||
|
'''
|
||
|
Return the list of generators of the centralizer of `other`
|
||
|
(a list of elements of `self`) in `self`.
|
||
|
|
||
|
'''
|
||
|
T = self._to_perm_group()[1]
|
||
|
other = T(other)
|
||
|
return self._perm_group_list('centralizer', other)
|
||
|
|
||
|
def normal_closure(self, other):
|
||
|
'''
|
||
|
Return the list of generators of the normal closure of `other`
|
||
|
(a list of elements of `self`) in `self`.
|
||
|
|
||
|
'''
|
||
|
T = self._to_perm_group()[1]
|
||
|
other = T(other)
|
||
|
return self._perm_group_list('normal_closure', other)
|
||
|
|
||
|
def _perm_property(self, attr):
|
||
|
'''
|
||
|
Given an attribute of a `PermutationGroup`, return
|
||
|
its value for a permutation group isomorphic to `self`.
|
||
|
|
||
|
'''
|
||
|
P = self._to_perm_group()[0]
|
||
|
return getattr(P, attr)
|
||
|
|
||
|
@property
|
||
|
def is_abelian(self):
|
||
|
'''
|
||
|
Check if `self` is abelian.
|
||
|
|
||
|
'''
|
||
|
return self._perm_property("is_abelian")
|
||
|
|
||
|
@property
|
||
|
def is_nilpotent(self):
|
||
|
'''
|
||
|
Check if `self` is nilpotent.
|
||
|
|
||
|
'''
|
||
|
return self._perm_property("is_nilpotent")
|
||
|
|
||
|
@property
|
||
|
def is_solvable(self):
|
||
|
'''
|
||
|
Check if `self` is solvable.
|
||
|
|
||
|
'''
|
||
|
return self._perm_property("is_solvable")
|
||
|
|
||
|
@property
|
||
|
def elements(self):
|
||
|
'''
|
||
|
List the elements of `self`.
|
||
|
|
||
|
'''
|
||
|
P, T = self._to_perm_group()
|
||
|
return T.invert(P._elements)
|
||
|
|
||
|
@property
|
||
|
def is_cyclic(self):
|
||
|
"""
|
||
|
Return ``True`` if group is Cyclic.
|
||
|
|
||
|
"""
|
||
|
if len(self.generators) <= 1:
|
||
|
return True
|
||
|
try:
|
||
|
P, T = self._to_perm_group()
|
||
|
except NotImplementedError:
|
||
|
raise NotImplementedError("Check for infinite Cyclic group "
|
||
|
"is not implemented")
|
||
|
return P.is_cyclic
|
||
|
|
||
|
def abelian_invariants(self):
|
||
|
"""
|
||
|
Return Abelian Invariants of a group.
|
||
|
"""
|
||
|
try:
|
||
|
P, T = self._to_perm_group()
|
||
|
except NotImplementedError:
|
||
|
raise NotImplementedError("abelian invariants is not implemented"
|
||
|
"for infinite group")
|
||
|
return P.abelian_invariants()
|
||
|
|
||
|
def composition_series(self):
|
||
|
"""
|
||
|
Return subnormal series of maximum length for a group.
|
||
|
"""
|
||
|
try:
|
||
|
P, T = self._to_perm_group()
|
||
|
except NotImplementedError:
|
||
|
raise NotImplementedError("composition series is not implemented"
|
||
|
"for infinite group")
|
||
|
return P.composition_series()
|
||
|
|
||
|
|
||
|
class FpSubgroup(DefaultPrinting):
|
||
|
'''
|
||
|
The class implementing a subgroup of an FpGroup or a FreeGroup
|
||
|
(only finite index subgroups are supported at this point). This
|
||
|
is to be used if one wishes to check if an element of the original
|
||
|
group belongs to the subgroup
|
||
|
|
||
|
'''
|
||
|
def __init__(self, G, gens, normal=False):
|
||
|
super().__init__()
|
||
|
self.parent = G
|
||
|
self.generators = list({g for g in gens if g != G.identity})
|
||
|
self._min_words = None #for use in __contains__
|
||
|
self.C = None
|
||
|
self.normal = normal
|
||
|
|
||
|
def __contains__(self, g):
|
||
|
|
||
|
if isinstance(self.parent, FreeGroup):
|
||
|
if self._min_words is None:
|
||
|
# make _min_words - a list of subwords such that
|
||
|
# g is in the subgroup if and only if it can be
|
||
|
# partitioned into these subwords. Infinite families of
|
||
|
# subwords are presented by tuples, e.g. (r, w)
|
||
|
# stands for the family of subwords r*w**n*r**-1
|
||
|
|
||
|
def _process(w):
|
||
|
# this is to be used before adding new words
|
||
|
# into _min_words; if the word w is not cyclically
|
||
|
# reduced, it will generate an infinite family of
|
||
|
# subwords so should be written as a tuple;
|
||
|
# if it is, w**-1 should be added to the list
|
||
|
# as well
|
||
|
p, r = w.cyclic_reduction(removed=True)
|
||
|
if not r.is_identity:
|
||
|
return [(r, p)]
|
||
|
else:
|
||
|
return [w, w**-1]
|
||
|
|
||
|
# make the initial list
|
||
|
gens = []
|
||
|
for w in self.generators:
|
||
|
if self.normal:
|
||
|
w = w.cyclic_reduction()
|
||
|
gens.extend(_process(w))
|
||
|
|
||
|
for w1 in gens:
|
||
|
for w2 in gens:
|
||
|
# if w1 and w2 are equal or are inverses, continue
|
||
|
if w1 == w2 or (not isinstance(w1, tuple)
|
||
|
and w1**-1 == w2):
|
||
|
continue
|
||
|
|
||
|
# if the start of one word is the inverse of the
|
||
|
# end of the other, their multiple should be added
|
||
|
# to _min_words because of cancellation
|
||
|
if isinstance(w1, tuple):
|
||
|
# start, end
|
||
|
s1, s2 = w1[0][0], w1[0][0]**-1
|
||
|
else:
|
||
|
s1, s2 = w1[0], w1[len(w1)-1]
|
||
|
|
||
|
if isinstance(w2, tuple):
|
||
|
# start, end
|
||
|
r1, r2 = w2[0][0], w2[0][0]**-1
|
||
|
else:
|
||
|
r1, r2 = w2[0], w2[len(w1)-1]
|
||
|
|
||
|
# p1 and p2 are w1 and w2 or, in case when
|
||
|
# w1 or w2 is an infinite family, a representative
|
||
|
p1, p2 = w1, w2
|
||
|
if isinstance(w1, tuple):
|
||
|
p1 = w1[0]*w1[1]*w1[0]**-1
|
||
|
if isinstance(w2, tuple):
|
||
|
p2 = w2[0]*w2[1]*w2[0]**-1
|
||
|
|
||
|
# add the product of the words to the list is necessary
|
||
|
if r1**-1 == s2 and not (p1*p2).is_identity:
|
||
|
new = _process(p1*p2)
|
||
|
if new not in gens:
|
||
|
gens.extend(new)
|
||
|
|
||
|
if r2**-1 == s1 and not (p2*p1).is_identity:
|
||
|
new = _process(p2*p1)
|
||
|
if new not in gens:
|
||
|
gens.extend(new)
|
||
|
|
||
|
self._min_words = gens
|
||
|
|
||
|
min_words = self._min_words
|
||
|
|
||
|
def _is_subword(w):
|
||
|
# check if w is a word in _min_words or one of
|
||
|
# the infinite families in it
|
||
|
w, r = w.cyclic_reduction(removed=True)
|
||
|
if r.is_identity or self.normal:
|
||
|
return w in min_words
|
||
|
else:
|
||
|
t = [s[1] for s in min_words if isinstance(s, tuple)
|
||
|
and s[0] == r]
|
||
|
return [s for s in t if w.power_of(s)] != []
|
||
|
|
||
|
# store the solution of words for which the result of
|
||
|
# _word_break (below) is known
|
||
|
known = {}
|
||
|
|
||
|
def _word_break(w):
|
||
|
# check if w can be written as a product of words
|
||
|
# in min_words
|
||
|
if len(w) == 0:
|
||
|
return True
|
||
|
i = 0
|
||
|
while i < len(w):
|
||
|
i += 1
|
||
|
prefix = w.subword(0, i)
|
||
|
if not _is_subword(prefix):
|
||
|
continue
|
||
|
rest = w.subword(i, len(w))
|
||
|
if rest not in known:
|
||
|
known[rest] = _word_break(rest)
|
||
|
if known[rest]:
|
||
|
return True
|
||
|
return False
|
||
|
|
||
|
if self.normal:
|
||
|
g = g.cyclic_reduction()
|
||
|
return _word_break(g)
|
||
|
else:
|
||
|
if self.C is None:
|
||
|
C = self.parent.coset_enumeration(self.generators)
|
||
|
self.C = C
|
||
|
i = 0
|
||
|
C = self.C
|
||
|
for j in range(len(g)):
|
||
|
i = C.table[i][C.A_dict[g[j]]]
|
||
|
return i == 0
|
||
|
|
||
|
def order(self):
|
||
|
if not self.generators:
|
||
|
return S.One
|
||
|
if isinstance(self.parent, FreeGroup):
|
||
|
return S.Infinity
|
||
|
if self.C is None:
|
||
|
C = self.parent.coset_enumeration(self.generators)
|
||
|
self.C = C
|
||
|
# This is valid because `len(self.C.table)` (the index of the subgroup)
|
||
|
# will always be finite - otherwise coset enumeration doesn't terminate
|
||
|
return self.parent.order()/len(self.C.table)
|
||
|
|
||
|
def to_FpGroup(self):
|
||
|
if isinstance(self.parent, FreeGroup):
|
||
|
gen_syms = [('x_%d'%i) for i in range(len(self.generators))]
|
||
|
return free_group(', '.join(gen_syms))[0]
|
||
|
return self.parent.subgroup(C=self.C)
|
||
|
|
||
|
def __str__(self):
|
||
|
if len(self.generators) > 30:
|
||
|
str_form = "<fp subgroup with %s generators>" % len(self.generators)
|
||
|
else:
|
||
|
str_form = "<fp subgroup on the generators %s>" % str(self.generators)
|
||
|
return str_form
|
||
|
|
||
|
__repr__ = __str__
|
||
|
|
||
|
|
||
|
###############################################################################
|
||
|
# LOW INDEX SUBGROUPS #
|
||
|
###############################################################################
|
||
|
|
||
|
def low_index_subgroups(G, N, Y=()):
|
||
|
"""
|
||
|
Implements the Low Index Subgroups algorithm, i.e find all subgroups of
|
||
|
``G`` upto a given index ``N``. This implements the method described in
|
||
|
[Sim94]. This procedure involves a backtrack search over incomplete Coset
|
||
|
Tables, rather than over forced coincidences.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
G: An FpGroup < X|R >
|
||
|
N: positive integer, representing the maximum index value for subgroups
|
||
|
Y: (an optional argument) specifying a list of subgroup generators, such
|
||
|
that each of the resulting subgroup contains the subgroup generated by Y.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.combinatorics import free_group
|
||
|
>>> from sympy.combinatorics.fp_groups import FpGroup, low_index_subgroups
|
||
|
>>> F, x, y = free_group("x, y")
|
||
|
>>> f = FpGroup(F, [x**2, y**3, (x*y)**4])
|
||
|
>>> L = low_index_subgroups(f, 4)
|
||
|
>>> for coset_table in L:
|
||
|
... print(coset_table.table)
|
||
|
[[0, 0, 0, 0]]
|
||
|
[[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 3, 3]]
|
||
|
[[0, 0, 1, 2], [2, 2, 2, 0], [1, 1, 0, 1]]
|
||
|
[[1, 1, 0, 0], [0, 0, 1, 1]]
|
||
|
|
||
|
References
|
||
|
==========
|
||
|
|
||
|
.. [1] Holt, D., Eick, B., O'Brien, E.
|
||
|
"Handbook of Computational Group Theory"
|
||
|
Section 5.4
|
||
|
|
||
|
.. [2] Marston Conder and Peter Dobcsanyi
|
||
|
"Applications and Adaptions of the Low Index Subgroups Procedure"
|
||
|
|
||
|
"""
|
||
|
C = CosetTable(G, [])
|
||
|
R = G.relators
|
||
|
# length chosen for the length of the short relators
|
||
|
len_short_rel = 5
|
||
|
# elements of R2 only checked at the last step for complete
|
||
|
# coset tables
|
||
|
R2 = {rel for rel in R if len(rel) > len_short_rel}
|
||
|
# elements of R1 are used in inner parts of the process to prune
|
||
|
# branches of the search tree,
|
||
|
R1 = {rel.identity_cyclic_reduction() for rel in set(R) - R2}
|
||
|
R1_c_list = C.conjugates(R1)
|
||
|
S = []
|
||
|
descendant_subgroups(S, C, R1_c_list, C.A[0], R2, N, Y)
|
||
|
return S
|
||
|
|
||
|
|
||
|
def descendant_subgroups(S, C, R1_c_list, x, R2, N, Y):
|
||
|
A_dict = C.A_dict
|
||
|
A_dict_inv = C.A_dict_inv
|
||
|
if C.is_complete():
|
||
|
# if C is complete then it only needs to test
|
||
|
# whether the relators in R2 are satisfied
|
||
|
for w, alpha in product(R2, C.omega):
|
||
|
if not C.scan_check(alpha, w):
|
||
|
return
|
||
|
# relators in R2 are satisfied, append the table to list
|
||
|
S.append(C)
|
||
|
else:
|
||
|
# find the first undefined entry in Coset Table
|
||
|
for alpha, x in product(range(len(C.table)), C.A):
|
||
|
if C.table[alpha][A_dict[x]] is None:
|
||
|
# this is "x" in pseudo-code (using "y" makes it clear)
|
||
|
undefined_coset, undefined_gen = alpha, x
|
||
|
break
|
||
|
# for filling up the undefine entry we try all possible values
|
||
|
# of beta in Omega or beta = n where beta^(undefined_gen^-1) is undefined
|
||
|
reach = C.omega + [C.n]
|
||
|
for beta in reach:
|
||
|
if beta < N:
|
||
|
if beta == C.n or C.table[beta][A_dict_inv[undefined_gen]] is None:
|
||
|
try_descendant(S, C, R1_c_list, R2, N, undefined_coset, \
|
||
|
undefined_gen, beta, Y)
|
||
|
|
||
|
|
||
|
def try_descendant(S, C, R1_c_list, R2, N, alpha, x, beta, Y):
|
||
|
r"""
|
||
|
Solves the problem of trying out each individual possibility
|
||
|
for `\alpha^x.
|
||
|
|
||
|
"""
|
||
|
D = C.copy()
|
||
|
if beta == D.n and beta < N:
|
||
|
D.table.append([None]*len(D.A))
|
||
|
D.p.append(beta)
|
||
|
D.table[alpha][D.A_dict[x]] = beta
|
||
|
D.table[beta][D.A_dict_inv[x]] = alpha
|
||
|
D.deduction_stack.append((alpha, x))
|
||
|
if not D.process_deductions_check(R1_c_list[D.A_dict[x]], \
|
||
|
R1_c_list[D.A_dict_inv[x]]):
|
||
|
return
|
||
|
for w in Y:
|
||
|
if not D.scan_check(0, w):
|
||
|
return
|
||
|
if first_in_class(D, Y):
|
||
|
descendant_subgroups(S, D, R1_c_list, x, R2, N, Y)
|
||
|
|
||
|
|
||
|
def first_in_class(C, Y=()):
|
||
|
"""
|
||
|
Checks whether the subgroup ``H=G1`` corresponding to the Coset Table
|
||
|
could possibly be the canonical representative of its conjugacy class.
|
||
|
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
C: CosetTable
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
bool: True/False
|
||
|
|
||
|
If this returns False, then no descendant of C can have that property, and
|
||
|
so we can abandon C. If it returns True, then we need to process further
|
||
|
the node of the search tree corresponding to C, and so we call
|
||
|
``descendant_subgroups`` recursively on C.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.combinatorics import free_group
|
||
|
>>> from sympy.combinatorics.fp_groups import FpGroup, CosetTable, first_in_class
|
||
|
>>> F, x, y = free_group("x, y")
|
||
|
>>> f = FpGroup(F, [x**2, y**3, (x*y)**4])
|
||
|
>>> C = CosetTable(f, [])
|
||
|
>>> C.table = [[0, 0, None, None]]
|
||
|
>>> first_in_class(C)
|
||
|
True
|
||
|
>>> C.table = [[1, 1, 1, None], [0, 0, None, 1]]; C.p = [0, 1]
|
||
|
>>> first_in_class(C)
|
||
|
True
|
||
|
>>> C.table = [[1, 1, 2, 1], [0, 0, 0, None], [None, None, None, 0]]
|
||
|
>>> C.p = [0, 1, 2]
|
||
|
>>> first_in_class(C)
|
||
|
False
|
||
|
>>> C.table = [[1, 1, 1, 2], [0, 0, 2, 0], [2, None, 0, 1]]
|
||
|
>>> first_in_class(C)
|
||
|
False
|
||
|
|
||
|
# TODO:: Sims points out in [Sim94] that performance can be improved by
|
||
|
# remembering some of the information computed by ``first_in_class``. If
|
||
|
# the ``continue alpha`` statement is executed at line 14, then the same thing
|
||
|
# will happen for that value of alpha in any descendant of the table C, and so
|
||
|
# the values the values of alpha for which this occurs could profitably be
|
||
|
# stored and passed through to the descendants of C. Of course this would
|
||
|
# make the code more complicated.
|
||
|
|
||
|
# The code below is taken directly from the function on page 208 of [Sim94]
|
||
|
# nu[alpha]
|
||
|
|
||
|
"""
|
||
|
n = C.n
|
||
|
# lamda is the largest numbered point in Omega_c_alpha which is currently defined
|
||
|
lamda = -1
|
||
|
# for alpha in Omega_c, nu[alpha] is the point in Omega_c_alpha corresponding to alpha
|
||
|
nu = [None]*n
|
||
|
# for alpha in Omega_c_alpha, mu[alpha] is the point in Omega_c corresponding to alpha
|
||
|
mu = [None]*n
|
||
|
# mutually nu and mu are the mutually-inverse equivalence maps between
|
||
|
# Omega_c_alpha and Omega_c
|
||
|
next_alpha = False
|
||
|
# For each 0!=alpha in [0 .. nc-1], we start by constructing the equivalent
|
||
|
# standardized coset table C_alpha corresponding to H_alpha
|
||
|
for alpha in range(1, n):
|
||
|
# reset nu to "None" after previous value of alpha
|
||
|
for beta in range(lamda+1):
|
||
|
nu[mu[beta]] = None
|
||
|
# we only want to reject our current table in favour of a preceding
|
||
|
# table in the ordering in which 1 is replaced by alpha, if the subgroup
|
||
|
# G_alpha corresponding to this preceding table definitely contains the
|
||
|
# given subgroup
|
||
|
for w in Y:
|
||
|
# TODO: this should support input of a list of general words
|
||
|
# not just the words which are in "A" (i.e gen and gen^-1)
|
||
|
if C.table[alpha][C.A_dict[w]] != alpha:
|
||
|
# continue with alpha
|
||
|
next_alpha = True
|
||
|
break
|
||
|
if next_alpha:
|
||
|
next_alpha = False
|
||
|
continue
|
||
|
# try alpha as the new point 0 in Omega_C_alpha
|
||
|
mu[0] = alpha
|
||
|
nu[alpha] = 0
|
||
|
# compare corresponding entries in C and C_alpha
|
||
|
lamda = 0
|
||
|
for beta in range(n):
|
||
|
for x in C.A:
|
||
|
gamma = C.table[beta][C.A_dict[x]]
|
||
|
delta = C.table[mu[beta]][C.A_dict[x]]
|
||
|
# if either of the entries is undefined,
|
||
|
# we move with next alpha
|
||
|
if gamma is None or delta is None:
|
||
|
# continue with alpha
|
||
|
next_alpha = True
|
||
|
break
|
||
|
if nu[delta] is None:
|
||
|
# delta becomes the next point in Omega_C_alpha
|
||
|
lamda += 1
|
||
|
nu[delta] = lamda
|
||
|
mu[lamda] = delta
|
||
|
if nu[delta] < gamma:
|
||
|
return False
|
||
|
if nu[delta] > gamma:
|
||
|
# continue with alpha
|
||
|
next_alpha = True
|
||
|
break
|
||
|
if next_alpha:
|
||
|
next_alpha = False
|
||
|
break
|
||
|
return True
|
||
|
|
||
|
#========================================================================
|
||
|
# Simplifying Presentation
|
||
|
#========================================================================
|
||
|
|
||
|
def simplify_presentation(*args, change_gens=False):
|
||
|
'''
|
||
|
For an instance of `FpGroup`, return a simplified isomorphic copy of
|
||
|
the group (e.g. remove redundant generators or relators). Alternatively,
|
||
|
a list of generators and relators can be passed in which case the
|
||
|
simplified lists will be returned.
|
||
|
|
||
|
By default, the generators of the group are unchanged. If you would
|
||
|
like to remove redundant generators, set the keyword argument
|
||
|
`change_gens = True`.
|
||
|
|
||
|
'''
|
||
|
if len(args) == 1:
|
||
|
if not isinstance(args[0], FpGroup):
|
||
|
raise TypeError("The argument must be an instance of FpGroup")
|
||
|
G = args[0]
|
||
|
gens, rels = simplify_presentation(G.generators, G.relators,
|
||
|
change_gens=change_gens)
|
||
|
if gens:
|
||
|
return FpGroup(gens[0].group, rels)
|
||
|
return FpGroup(FreeGroup([]), [])
|
||
|
elif len(args) == 2:
|
||
|
gens, rels = args[0][:], args[1][:]
|
||
|
if not gens:
|
||
|
return gens, rels
|
||
|
identity = gens[0].group.identity
|
||
|
else:
|
||
|
if len(args) == 0:
|
||
|
m = "Not enough arguments"
|
||
|
else:
|
||
|
m = "Too many arguments"
|
||
|
raise RuntimeError(m)
|
||
|
|
||
|
prev_gens = []
|
||
|
prev_rels = []
|
||
|
while not set(prev_rels) == set(rels):
|
||
|
prev_rels = rels
|
||
|
while change_gens and not set(prev_gens) == set(gens):
|
||
|
prev_gens = gens
|
||
|
gens, rels = elimination_technique_1(gens, rels, identity)
|
||
|
rels = _simplify_relators(rels, identity)
|
||
|
|
||
|
if change_gens:
|
||
|
syms = [g.array_form[0][0] for g in gens]
|
||
|
F = free_group(syms)[0]
|
||
|
identity = F.identity
|
||
|
gens = F.generators
|
||
|
subs = dict(zip(syms, gens))
|
||
|
for j, r in enumerate(rels):
|
||
|
a = r.array_form
|
||
|
rel = identity
|
||
|
for sym, p in a:
|
||
|
rel = rel*subs[sym]**p
|
||
|
rels[j] = rel
|
||
|
return gens, rels
|
||
|
|
||
|
def _simplify_relators(rels, identity):
|
||
|
"""Relies upon ``_simplification_technique_1`` for its functioning. """
|
||
|
rels = rels[:]
|
||
|
|
||
|
rels = list(set(_simplification_technique_1(rels)))
|
||
|
rels.sort()
|
||
|
rels = [r.identity_cyclic_reduction() for r in rels]
|
||
|
try:
|
||
|
rels.remove(identity)
|
||
|
except ValueError:
|
||
|
pass
|
||
|
return rels
|
||
|
|
||
|
# Pg 350, section 2.5.1 from [2]
|
||
|
def elimination_technique_1(gens, rels, identity):
|
||
|
rels = rels[:]
|
||
|
# the shorter relators are examined first so that generators selected for
|
||
|
# elimination will have shorter strings as equivalent
|
||
|
rels.sort()
|
||
|
gens = gens[:]
|
||
|
redundant_gens = {}
|
||
|
redundant_rels = []
|
||
|
used_gens = set()
|
||
|
# examine each relator in relator list for any generator occurring exactly
|
||
|
# once
|
||
|
for rel in rels:
|
||
|
# don't look for a redundant generator in a relator which
|
||
|
# depends on previously found ones
|
||
|
contained_gens = rel.contains_generators()
|
||
|
if any(g in contained_gens for g in redundant_gens):
|
||
|
continue
|
||
|
contained_gens = list(contained_gens)
|
||
|
contained_gens.sort(reverse = True)
|
||
|
for gen in contained_gens:
|
||
|
if rel.generator_count(gen) == 1 and gen not in used_gens:
|
||
|
k = rel.exponent_sum(gen)
|
||
|
gen_index = rel.index(gen**k)
|
||
|
bk = rel.subword(gen_index + 1, len(rel))
|
||
|
fw = rel.subword(0, gen_index)
|
||
|
chi = bk*fw
|
||
|
redundant_gens[gen] = chi**(-1*k)
|
||
|
used_gens.update(chi.contains_generators())
|
||
|
redundant_rels.append(rel)
|
||
|
break
|
||
|
rels = [r for r in rels if r not in redundant_rels]
|
||
|
# eliminate the redundant generators from remaining relators
|
||
|
rels = [r.eliminate_words(redundant_gens, _all = True).identity_cyclic_reduction() for r in rels]
|
||
|
rels = list(set(rels))
|
||
|
try:
|
||
|
rels.remove(identity)
|
||
|
except ValueError:
|
||
|
pass
|
||
|
gens = [g for g in gens if g not in redundant_gens]
|
||
|
return gens, rels
|
||
|
|
||
|
def _simplification_technique_1(rels):
|
||
|
"""
|
||
|
All relators are checked to see if they are of the form `gen^n`. If any
|
||
|
such relators are found then all other relators are processed for strings
|
||
|
in the `gen` known order.
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.combinatorics import free_group
|
||
|
>>> from sympy.combinatorics.fp_groups import _simplification_technique_1
|
||
|
>>> F, x, y = free_group("x, y")
|
||
|
>>> w1 = [x**2*y**4, x**3]
|
||
|
>>> _simplification_technique_1(w1)
|
||
|
[x**-1*y**4, x**3]
|
||
|
|
||
|
>>> w2 = [x**2*y**-4*x**5, x**3, x**2*y**8, y**5]
|
||
|
>>> _simplification_technique_1(w2)
|
||
|
[x**-1*y*x**-1, x**3, x**-1*y**-2, y**5]
|
||
|
|
||
|
>>> w3 = [x**6*y**4, x**4]
|
||
|
>>> _simplification_technique_1(w3)
|
||
|
[x**2*y**4, x**4]
|
||
|
|
||
|
"""
|
||
|
rels = rels[:]
|
||
|
# dictionary with "gen: n" where gen^n is one of the relators
|
||
|
exps = {}
|
||
|
for i in range(len(rels)):
|
||
|
rel = rels[i]
|
||
|
if rel.number_syllables() == 1:
|
||
|
g = rel[0]
|
||
|
exp = abs(rel.array_form[0][1])
|
||
|
if rel.array_form[0][1] < 0:
|
||
|
rels[i] = rels[i]**-1
|
||
|
g = g**-1
|
||
|
if g in exps:
|
||
|
exp = gcd(exp, exps[g].array_form[0][1])
|
||
|
exps[g] = g**exp
|
||
|
|
||
|
one_syllables_words = exps.values()
|
||
|
# decrease some of the exponents in relators, making use of the single
|
||
|
# syllable relators
|
||
|
for i in range(len(rels)):
|
||
|
rel = rels[i]
|
||
|
if rel in one_syllables_words:
|
||
|
continue
|
||
|
rel = rel.eliminate_words(one_syllables_words, _all = True)
|
||
|
# if rels[i] contains g**n where abs(n) is greater than half of the power p
|
||
|
# of g in exps, g**n can be replaced by g**(n-p) (or g**(p-n) if n<0)
|
||
|
for g in rel.contains_generators():
|
||
|
if g in exps:
|
||
|
exp = exps[g].array_form[0][1]
|
||
|
max_exp = (exp + 1)//2
|
||
|
rel = rel.eliminate_word(g**(max_exp), g**(max_exp-exp), _all = True)
|
||
|
rel = rel.eliminate_word(g**(-max_exp), g**(-(max_exp-exp)), _all = True)
|
||
|
rels[i] = rel
|
||
|
rels = [r.identity_cyclic_reduction() for r in rels]
|
||
|
return rels
|
||
|
|
||
|
|
||
|
###############################################################################
|
||
|
# SUBGROUP PRESENTATIONS #
|
||
|
###############################################################################
|
||
|
|
||
|
# Pg 175 [1]
|
||
|
def define_schreier_generators(C, homomorphism=False):
|
||
|
'''
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
C -- Coset table.
|
||
|
homomorphism -- When set to True, return a dictionary containing the images
|
||
|
of the presentation generators in the original group.
|
||
|
'''
|
||
|
y = []
|
||
|
gamma = 1
|
||
|
f = C.fp_group
|
||
|
X = f.generators
|
||
|
if homomorphism:
|
||
|
# `_gens` stores the elements of the parent group to
|
||
|
# to which the schreier generators correspond to.
|
||
|
_gens = {}
|
||
|
# compute the schreier Traversal
|
||
|
tau = {}
|
||
|
tau[0] = f.identity
|
||
|
C.P = [[None]*len(C.A) for i in range(C.n)]
|
||
|
for alpha, x in product(C.omega, C.A):
|
||
|
beta = C.table[alpha][C.A_dict[x]]
|
||
|
if beta == gamma:
|
||
|
C.P[alpha][C.A_dict[x]] = "<identity>"
|
||
|
C.P[beta][C.A_dict_inv[x]] = "<identity>"
|
||
|
gamma += 1
|
||
|
if homomorphism:
|
||
|
tau[beta] = tau[alpha]*x
|
||
|
elif x in X and C.P[alpha][C.A_dict[x]] is None:
|
||
|
y_alpha_x = '%s_%s' % (x, alpha)
|
||
|
y.append(y_alpha_x)
|
||
|
C.P[alpha][C.A_dict[x]] = y_alpha_x
|
||
|
if homomorphism:
|
||
|
_gens[y_alpha_x] = tau[alpha]*x*tau[beta]**-1
|
||
|
grp_gens = list(free_group(', '.join(y)))
|
||
|
C._schreier_free_group = grp_gens.pop(0)
|
||
|
C._schreier_generators = grp_gens
|
||
|
if homomorphism:
|
||
|
C._schreier_gen_elem = _gens
|
||
|
# replace all elements of P by, free group elements
|
||
|
for i, j in product(range(len(C.P)), range(len(C.A))):
|
||
|
# if equals "<identity>", replace by identity element
|
||
|
if C.P[i][j] == "<identity>":
|
||
|
C.P[i][j] = C._schreier_free_group.identity
|
||
|
elif isinstance(C.P[i][j], str):
|
||
|
r = C._schreier_generators[y.index(C.P[i][j])]
|
||
|
C.P[i][j] = r
|
||
|
beta = C.table[i][j]
|
||
|
C.P[beta][j + 1] = r**-1
|
||
|
|
||
|
def reidemeister_relators(C):
|
||
|
R = C.fp_group.relators
|
||
|
rels = [rewrite(C, coset, word) for word in R for coset in range(C.n)]
|
||
|
order_1_gens = {i for i in rels if len(i) == 1}
|
||
|
|
||
|
# remove all the order 1 generators from relators
|
||
|
rels = list(filter(lambda rel: rel not in order_1_gens, rels))
|
||
|
|
||
|
# replace order 1 generators by identity element in reidemeister relators
|
||
|
for i in range(len(rels)):
|
||
|
w = rels[i]
|
||
|
w = w.eliminate_words(order_1_gens, _all=True)
|
||
|
rels[i] = w
|
||
|
|
||
|
C._schreier_generators = [i for i in C._schreier_generators
|
||
|
if not (i in order_1_gens or i**-1 in order_1_gens)]
|
||
|
|
||
|
# Tietze transformation 1 i.e TT_1
|
||
|
# remove cyclic conjugate elements from relators
|
||
|
i = 0
|
||
|
while i < len(rels):
|
||
|
w = rels[i]
|
||
|
j = i + 1
|
||
|
while j < len(rels):
|
||
|
if w.is_cyclic_conjugate(rels[j]):
|
||
|
del rels[j]
|
||
|
else:
|
||
|
j += 1
|
||
|
i += 1
|
||
|
|
||
|
C._reidemeister_relators = rels
|
||
|
|
||
|
|
||
|
def rewrite(C, alpha, w):
|
||
|
"""
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
C: CosetTable
|
||
|
alpha: A live coset
|
||
|
w: A word in `A*`
|
||
|
|
||
|
Returns
|
||
|
=======
|
||
|
|
||
|
rho(tau(alpha), w)
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.combinatorics.fp_groups import FpGroup, CosetTable, define_schreier_generators, rewrite
|
||
|
>>> from sympy.combinatorics import free_group
|
||
|
>>> F, x, y = free_group("x, y")
|
||
|
>>> f = FpGroup(F, [x**2, y**3, (x*y)**6])
|
||
|
>>> C = CosetTable(f, [])
|
||
|
>>> C.table = [[1, 1, 2, 3], [0, 0, 4, 5], [4, 4, 3, 0], [5, 5, 0, 2], [2, 2, 5, 1], [3, 3, 1, 4]]
|
||
|
>>> C.p = [0, 1, 2, 3, 4, 5]
|
||
|
>>> define_schreier_generators(C)
|
||
|
>>> rewrite(C, 0, (x*y)**6)
|
||
|
x_4*y_2*x_3*x_1*x_2*y_4*x_5
|
||
|
|
||
|
"""
|
||
|
v = C._schreier_free_group.identity
|
||
|
for i in range(len(w)):
|
||
|
x_i = w[i]
|
||
|
v = v*C.P[alpha][C.A_dict[x_i]]
|
||
|
alpha = C.table[alpha][C.A_dict[x_i]]
|
||
|
return v
|
||
|
|
||
|
# Pg 350, section 2.5.2 from [2]
|
||
|
def elimination_technique_2(C):
|
||
|
"""
|
||
|
This technique eliminates one generator at a time. Heuristically this
|
||
|
seems superior in that we may select for elimination the generator with
|
||
|
shortest equivalent string at each stage.
|
||
|
|
||
|
>>> from sympy.combinatorics import free_group
|
||
|
>>> from sympy.combinatorics.fp_groups import FpGroup, coset_enumeration_r, \
|
||
|
reidemeister_relators, define_schreier_generators, elimination_technique_2
|
||
|
>>> F, x, y = free_group("x, y")
|
||
|
>>> f = FpGroup(F, [x**3, y**5, (x*y)**2]); H = [x*y, x**-1*y**-1*x*y*x]
|
||
|
>>> C = coset_enumeration_r(f, H)
|
||
|
>>> C.compress(); C.standardize()
|
||
|
>>> define_schreier_generators(C)
|
||
|
>>> reidemeister_relators(C)
|
||
|
>>> elimination_technique_2(C)
|
||
|
([y_1, y_2], [y_2**-3, y_2*y_1*y_2*y_1*y_2*y_1, y_1**2])
|
||
|
|
||
|
"""
|
||
|
rels = C._reidemeister_relators
|
||
|
rels.sort(reverse=True)
|
||
|
gens = C._schreier_generators
|
||
|
for i in range(len(gens) - 1, -1, -1):
|
||
|
rel = rels[i]
|
||
|
for j in range(len(gens) - 1, -1, -1):
|
||
|
gen = gens[j]
|
||
|
if rel.generator_count(gen) == 1:
|
||
|
k = rel.exponent_sum(gen)
|
||
|
gen_index = rel.index(gen**k)
|
||
|
bk = rel.subword(gen_index + 1, len(rel))
|
||
|
fw = rel.subword(0, gen_index)
|
||
|
rep_by = (bk*fw)**(-1*k)
|
||
|
del rels[i]; del gens[j]
|
||
|
for l in range(len(rels)):
|
||
|
rels[l] = rels[l].eliminate_word(gen, rep_by)
|
||
|
break
|
||
|
C._reidemeister_relators = rels
|
||
|
C._schreier_generators = gens
|
||
|
return C._schreier_generators, C._reidemeister_relators
|
||
|
|
||
|
def reidemeister_presentation(fp_grp, H, C=None, homomorphism=False):
|
||
|
"""
|
||
|
Parameters
|
||
|
==========
|
||
|
|
||
|
fp_group: A finitely presented group, an instance of FpGroup
|
||
|
H: A subgroup whose presentation is to be found, given as a list
|
||
|
of words in generators of `fp_grp`
|
||
|
homomorphism: When set to True, return a homomorphism from the subgroup
|
||
|
to the parent group
|
||
|
|
||
|
Examples
|
||
|
========
|
||
|
|
||
|
>>> from sympy.combinatorics import free_group
|
||
|
>>> from sympy.combinatorics.fp_groups import FpGroup, reidemeister_presentation
|
||
|
>>> F, x, y = free_group("x, y")
|
||
|
|
||
|
Example 5.6 Pg. 177 from [1]
|
||
|
>>> f = FpGroup(F, [x**3, y**5, (x*y)**2])
|
||
|
>>> H = [x*y, x**-1*y**-1*x*y*x]
|
||
|
>>> reidemeister_presentation(f, H)
|
||
|
((y_1, y_2), (y_1**2, y_2**3, y_2*y_1*y_2*y_1*y_2*y_1))
|
||
|
|
||
|
Example 5.8 Pg. 183 from [1]
|
||
|
>>> f = FpGroup(F, [x**3, y**3, (x*y)**3])
|
||
|
>>> H = [x*y, x*y**-1]
|
||
|
>>> reidemeister_presentation(f, H)
|
||
|
((x_0, y_0), (x_0**3, y_0**3, x_0*y_0*x_0*y_0*x_0*y_0))
|
||
|
|
||
|
Exercises Q2. Pg 187 from [1]
|
||
|
>>> f = FpGroup(F, [x**2*y**2, y**-1*x*y*x**-3])
|
||
|
>>> H = [x]
|
||
|
>>> reidemeister_presentation(f, H)
|
||
|
((x_0,), (x_0**4,))
|
||
|
|
||
|
Example 5.9 Pg. 183 from [1]
|
||
|
>>> f = FpGroup(F, [x**3*y**-3, (x*y)**3, (x*y**-1)**2])
|
||
|
>>> H = [x]
|
||
|
>>> reidemeister_presentation(f, H)
|
||
|
((x_0,), (x_0**6,))
|
||
|
|
||
|
"""
|
||
|
if not C:
|
||
|
C = coset_enumeration_r(fp_grp, H)
|
||
|
C.compress(); C.standardize()
|
||
|
define_schreier_generators(C, homomorphism=homomorphism)
|
||
|
reidemeister_relators(C)
|
||
|
gens, rels = C._schreier_generators, C._reidemeister_relators
|
||
|
gens, rels = simplify_presentation(gens, rels, change_gens=True)
|
||
|
|
||
|
C.schreier_generators = tuple(gens)
|
||
|
C.reidemeister_relators = tuple(rels)
|
||
|
|
||
|
if homomorphism:
|
||
|
_gens = []
|
||
|
for gen in gens:
|
||
|
_gens.append(C._schreier_gen_elem[str(gen)])
|
||
|
return C.schreier_generators, C.reidemeister_relators, _gens
|
||
|
|
||
|
return C.schreier_generators, C.reidemeister_relators
|
||
|
|
||
|
|
||
|
FpGroupElement = FreeGroupElement
|