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