|
"""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) |
|
|
|
|
|
@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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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}) |
|
|
|
|
|
self._coset_table = None |
|
|
|
|
|
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 |
|
|
|
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 |
|
|
|
""" |
|
|
|
|
|
|
|
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__ |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
self.C = None |
|
self.normal = normal |
|
|
|
def __contains__(self, g): |
|
|
|
if isinstance(self.parent, FreeGroup): |
|
if self._min_words is None: |
|
|
|
|
|
|
|
|
|
|
|
|
|
def _process(w): |
|
|
|
|
|
|
|
|
|
|
|
|
|
p, r = w.cyclic_reduction(removed=True) |
|
if not r.is_identity: |
|
return [(r, p)] |
|
else: |
|
return [w, w**-1] |
|
|
|
|
|
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 == w2 or (not isinstance(w1, tuple) |
|
and w1**-1 == w2): |
|
continue |
|
|
|
|
|
|
|
|
|
if isinstance(w1, tuple): |
|
|
|
s1, s2 = w1[0][0], w1[0][0]**-1 |
|
else: |
|
s1, s2 = w1[0], w1[len(w1)-1] |
|
|
|
if isinstance(w2, tuple): |
|
|
|
r1, r2 = w2[0][0], w2[0][0]**-1 |
|
else: |
|
r1, r2 = w2[0], w2[len(w1)-1] |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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): |
|
|
|
|
|
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)] != [] |
|
|
|
|
|
|
|
known = {} |
|
|
|
def _word_break(w): |
|
|
|
|
|
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 |
|
|
|
|
|
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__ |
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
len_short_rel = 5 |
|
|
|
|
|
R2 = {rel for rel in R if len(rel) > len_short_rel} |
|
|
|
|
|
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(): |
|
|
|
|
|
for w, alpha in product(R2, C.omega): |
|
if not C.scan_check(alpha, w): |
|
return |
|
|
|
S.append(C) |
|
else: |
|
|
|
for alpha, x in product(range(len(C.table)), C.A): |
|
if C.table[alpha][A_dict[x]] is None: |
|
|
|
undefined_coset, undefined_gen = alpha, x |
|
break |
|
|
|
|
|
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 = -1 |
|
|
|
nu = [None]*n |
|
|
|
mu = [None]*n |
|
|
|
|
|
next_alpha = False |
|
|
|
|
|
for alpha in range(1, n): |
|
|
|
for beta in range(lamda+1): |
|
nu[mu[beta]] = None |
|
|
|
|
|
|
|
|
|
for w in Y: |
|
|
|
|
|
if C.table[alpha][C.A_dict[w]] != alpha: |
|
|
|
next_alpha = True |
|
break |
|
if next_alpha: |
|
next_alpha = False |
|
continue |
|
|
|
mu[0] = alpha |
|
nu[alpha] = 0 |
|
|
|
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 gamma is None or delta is None: |
|
|
|
next_alpha = True |
|
break |
|
if nu[delta] is None: |
|
|
|
lamda += 1 |
|
nu[delta] = lamda |
|
mu[lamda] = delta |
|
if nu[delta] < gamma: |
|
return False |
|
if nu[delta] > gamma: |
|
|
|
next_alpha = True |
|
break |
|
if next_alpha: |
|
next_alpha = False |
|
break |
|
return True |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
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): |
|
""" |
|
Simplifies a set of relators. 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 _simplify_relators |
|
>>> F, x, y = free_group("x, y") |
|
>>> w1 = [x**2*y**4, x**3] |
|
>>> _simplify_relators(w1) |
|
[x**3, x**-1*y**4] |
|
|
|
>>> w2 = [x**2*y**-4*x**5, x**3, x**2*y**8, y**5] |
|
>>> _simplify_relators(w2) |
|
[x**-1*y**-2, x**-1*y*x**-1, x**3, y**5] |
|
|
|
>>> w3 = [x**6*y**4, x**4] |
|
>>> _simplify_relators(w3) |
|
[x**4, x**2*y**4] |
|
|
|
>>> w4 = [x**2, x**5, y**3] |
|
>>> _simplify_relators(w4) |
|
[x, y**3] |
|
|
|
""" |
|
rels = rels[:] |
|
|
|
if not rels: |
|
return [] |
|
|
|
identity = rels[0].group.identity |
|
|
|
|
|
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 = list(exps.values()) |
|
|
|
|
|
for i, rel in enumerate(rels): |
|
if rel in one_syllables_words: |
|
continue |
|
rel = rel.eliminate_words(one_syllables_words, _all = True) |
|
|
|
|
|
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] |
|
|
|
rels += one_syllables_words |
|
rels = list(set(rels)) |
|
rels.sort() |
|
|
|
|
|
try: |
|
rels.remove(identity) |
|
except ValueError: |
|
pass |
|
return rels |
|
|
|
|
|
def elimination_technique_1(gens, rels, identity): |
|
rels = rels[:] |
|
|
|
|
|
rels.sort() |
|
gens = gens[:] |
|
redundant_gens = {} |
|
redundant_rels = [] |
|
used_gens = set() |
|
|
|
|
|
for rel in rels: |
|
|
|
|
|
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] |
|
|
|
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 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 = {} |
|
|
|
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 |
|
|
|
for i, j in product(range(len(C.P)), range(len(C.A))): |
|
|
|
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} |
|
|
|
|
|
rels = list(filter(lambda rel: rel not in order_1_gens, rels)) |
|
|
|
|
|
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)] |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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] |
|
rels = [rel.eliminate_word(gen, rep_by) for rel in rels] |
|
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 = [C._schreier_gen_elem[str(gen)] for gen in gens] |
|
return C.schreier_generators, C.reidemeister_relators, _gens |
|
|
|
return C.schreier_generators, C.reidemeister_relators |
|
|
|
|
|
FpGroupElement = FreeGroupElement |
|
|