Example #1
0
    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)
        return
Example #2
0
    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)
        return
Example #3
0
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)
        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 isn't, `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):
        '''
        Return the subgroup generated by `gens` using the
        Reidemeister-Schreier algorithm

        '''
        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")
        g, rels = reidemeister_presentation(self, gens, C=C)
        g = FpGroup(g[0].group, rels)
        return g

    def coset_enumeration(self, H, strategy="relator_based", max_cosets=None):
        """
        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.

        """
        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)
        else:
            C = coset_enumeration_c(self, H, max_cosets=max_cosets)
        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"):
        """
        Return the mathematical coset table of ``self`` in ``H``.

        """
        if not H:
            if self._coset_table != None:
                if not self._is_standardized:
                    self.standardize_coset_table()
            else:
                C = self.coset_enumeration([], strategy)
                self._coset_table = C
                self.standardize_coset_table()
            return self._coset_table.table
        else:
            C = self.coset_enumeration(H, strategy)
            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.free_groups 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

        """
        from sympy import S, gcd
        if self._order != None:
            return self._order
        if self._coset_table != None:
            self._order = len(self._coset_table.table)
        elif len(self.generators) == 1:
            self._order = 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

        '''
        # Abelianisation test: check is the abelianisation is infinite
        abelian_rels = []
        from sympy.polys.solvers import RawMatrix as Matrix
        from sympy.polys.domains import ZZ
        from sympy.matrices.normalforms import invariant_factors
        for rel in self.relators:
            abelian_rels.append([rel.exponent_sum(g) for g in self.generators])
        m = Matrix(abelian_rels)
        setattr(m, "ring", ZZ)
        if 0 in invariant_factors(m):
            return True
        else:
            return None

    def _finite_index_subgroup(self, s=[]):
        '''
        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:]
        m = 200
        C = None
        while not C and (m / 2 < CosetTable.coset_table_max_limit):
            m = min(m, CosetTable.coset_table_max_limit)
            try:
                C = self.coset_enumeration(half1, max_cosets=m)
                half = half1
            except ValueError:
                pass
            if not C:
                try:
                    C = self.coset_enumeration(half2, max_cosets=m)
                    half = half2
                except ValueError:
                    m *= 2
                    continue
        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.free_groups 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__
Example #4
0
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 isn't, `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, FpSubgroup)
        >>> from sympy.combinatorics.free_groups 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.free_groups 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

        """
        from sympy import S, gcd
        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 any([g not in used_gens for g in self.generators]):
            return True
        # Abelianisation test: check is the abelianisation is infinite
        abelian_rels = []
        from sympy.polys.solvers import RawMatrix as Matrix
        from sympy.polys.domains import ZZ
        from sympy.matrices.normalforms import invariant_factors
        for rel in self.relators:
            abelian_rels.append([rel.exponent_sum(g) for g in self.generators])
        m = Matrix(abelian_rels)
        setattr(m, "ring", ZZ)
        if 0 in invariant_factors(m):
            return True
        else:
            return None


    def _finite_index_subgroup(self, s=[]):
        '''
        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.free_groups 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, PermutationGroup
        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()
Example #5
0
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)
        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 isn't, `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):
        '''
        Return the subgroup generated by `gens` using the
        Reidemeister-Schreier algorithm

        '''
        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")
        g, rels = reidemeister_presentation(self, gens, C=C)
        g = FpGroup(g[0].group, rels)
        return g

    def coset_enumeration(self, H, strategy="relator_based", max_cosets=None):
        """
        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.

        """
        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)
        else:
            C = coset_enumeration_c(self, H, max_cosets=max_cosets)
        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"):
        """
        Return the mathematical coset table of ``self`` in ``H``.

        """
        if not H:
            if self._coset_table != None:
                if not self._is_standardized:
                    self.standardize_coset_table()
            else:
                C = self.coset_enumeration([], strategy)
                self._coset_table = C
                self.standardize_coset_table()
            return self._coset_table.table
        else:
            C = self.coset_enumeration(H, strategy)
            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.free_groups 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

        """
        from sympy import S, gcd
        if self._order != None:
            return self._order
        if self._coset_table != None:
            self._order = len(self._coset_table.table)
        elif len(self.generators) == 1:
            self._order = 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

        '''
        # Abelianisation test: check is the abelianisation is infinite
        abelian_rels = []
        from sympy.polys.solvers import RawMatrix as Matrix
        from sympy.polys.domains import ZZ
        from sympy.matrices.normalforms import invariant_factors
        for rel in self.relators:
            abelian_rels.append([rel.exponent_sum(g) for g in self.generators])
        m = Matrix(abelian_rels)
        setattr(m, "ring", ZZ)
        if 0 in invariant_factors(m):
            return True
        else:
            return None


    def _finite_index_subgroup(self, s=[]):
        '''
        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_element()
                    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:]
        m = 200
        C = None
        while not C and (m/2 < CosetTable.coset_table_max_limit):
            m = min(m, CosetTable.coset_table_max_limit)
            try:
                C = self.coset_enumeration(half1, max_cosets=m)
                half = half1
            except ValueError:
                pass
            if not C:
                try:
                    C = self.coset_enumeration(half2, max_cosets=m)
                    half = half2
                except ValueError:
                    m *= 2
                    continue
        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_element(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.free_groups 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__
Example #6
0
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 isn't, `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, FpSubgroup)
        >>> from sympy.combinatorics.free_groups 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.free_groups 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

        """
        from sympy import S, gcd
        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 any([g not in used_gens for g in self.generators]):
            return True
        # Abelianisation test: check is the abelianisation is infinite
        abelian_rels = []
        from sympy.polys.solvers import RawMatrix as Matrix
        from sympy.polys.domains import ZZ
        from sympy.matrices.normalforms import invariant_factors
        for rel in self.relators:
            abelian_rels.append([rel.exponent_sum(g) for g in self.generators])
        m = Matrix(abelian_rels)
        setattr(m, "ring", ZZ)
        if 0 in invariant_factors(m):
            return True
        else:
            return None


    def _finite_index_subgroup(self, s=[]):
        '''
        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.free_groups 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, PermutationGroup
        from sympy.combinatorics.homomorphisms import homomorphism
        if self.order() == 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