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 | |