Spaces:
Sleeping
Sleeping
"""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 | |
def fp_group(fr_grp, relators=()): | |
_fp_group = FpGroup(fr_grp, relators) | |
return (_fp_group,) + tuple(_fp_group._generators) | |
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. | |
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 | |
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) | |
def is_abelian(self): | |
''' | |
Check if `self` is abelian. | |
''' | |
return self._perm_property("is_abelian") | |
def is_nilpotent(self): | |
''' | |
Check if `self` is nilpotent. | |
''' | |
return self._perm_property("is_nilpotent") | |
def is_solvable(self): | |
''' | |
Check if `self` is solvable. | |
''' | |
return self._perm_property("is_solvable") | |
def elements(self): | |
''' | |
List the elements of `self`. | |
''' | |
P, T = self._to_perm_group() | |
return T.invert(P.elements) | |
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) | |
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 | |
# build 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 = list(exps.values()) | |
# decrease some of the exponents in relators, making use of the single | |
# syllable relators | |
for i, rel in enumerate(rels): | |
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] | |
rels += one_syllables_words # include one_syllable_words in the list of relators | |
rels = list(set(rels)) # get unique values in rels | |
rels.sort() | |
# remove <identity> entries 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 | |
############################################################################### | |
# 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 | |