diff --git a/sympy/combinatorics/fp_groups.py b/sympy/combinatorics/fp_groups.py index f7127ab5e345..6a802a73b0f6 100644 --- a/sympy/combinatorics/fp_groups.py +++ b/sympy/combinatorics/fp_groups.py @@ -514,6 +514,137 @@ def elements(self): P, T = self._to_perm_group() return T.invert(P._elements) +def subgroup_quotient(G, H, parent_group=None, homomorphism=False): + ''' + Compute the quotient group G/H. + The quotient group is computed on a new FreeGroup when + `G` is a list of the elements of parent_group. + + Arguments + ========= + G -- A list of `FreeGroupElement`s or an `FpGroup`. + H -- A list of `FreeGroupElement`s or an `FpGroup`. + parent_group -- A group specified when `G` and `H` are given by a list of generators. + homomorphism -- Return a homomorphism whenever `homomorphism` = True + + Returns + ======= + * When `homomorphism = True`, quotient group along with the homomorphism + from the quotient to the parent_group whose image is isomorphic to G. + * Only the quotient group in all other cases. + + Examples + ======== + >>> from sympy.combinatorics.fp_groups import FpGroup + >>> from sympy.combinatorics.free_groups import free_group + >>> from sympy.combinatorics.fp_groups import subgroup_quotient + + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**2, y**3, (x*y)**4]) + >>> T = subgroup_quotient(f, [x, y]) + >>> T.order() == 1 + True + + >>> T = subgroup_quotient(f, [x*y**2*x*y, y**2*x*y*x, y**-1]) + >>> H = f.subgroup([x*y**2*x*y, y**2*x*y*x, y**-1]) + >>> T.order() == f.order()/H.order() + True + + >>> G = [x, y] + >>> H = [x*y**2*x*y, y**2*x*y*x, x*y*x] + >>> K, T = subgroup_quotient(G, H, parent_group=f, homomorphism=True) + >>> G = f.subgroup(G) + >>> H = f.subgroup(H) + >>> K.order() == G.order()/H.order() + True + >>> T(K.generators) == list(f.generators) + True + >>> T.domain == K + True + + ''' + def _get_pres(F, parent_group=None): + if isinstance(F, list) and parent_group: + K, T = parent_group.subgroup(F, homomorphism=True) + f_gens = T(K.generators) + f_rels = T(K.relators) + else: + f_gens = F.generators + f_rels = F.relators + return f_gens, f_rels + + def _check(F): + if ((isinstance(F, list) and not all(elem in free_group for elem in F)) + or (isinstance(F, FpGroup) and not (F.free_group == free_group))): + raise ValueError("The group elements must belong to the parent group") + + # If no parent group is specified, + # G is set to the parent `FpGroup` + if not parent_group: + if isinstance(G, list): + raise ValueError("The parent_group must be" + "defined when the group is a list") + parent_group = G + + free_group = parent_group.free_group + + if not isinstance(parent_group, FpGroup): + raise ValueError("The parent group must be an instance" + "of FpGroup") + + _check(G) + _check(H) + + h_gens, h_rels = _get_pres(H, parent_group=parent_group) + T = None + # return the `FpGroup` on a new `free_group` + if isinstance(G, list): + G, T = parent_group.subgroup(G, homomorphism=True) + h_gens = T.invert(h_gens) + h_rels = T.invert(h_rels) + free_group = G.free_group + + g_gens, g_rels = _get_pres(G) + + q_relators = list(g_rels) + list(h_gens) + q_group = FpGroup(free_group, q_relators) + + # Return the quotient group with presentation + # + if homomorphism and T: + T.domain = q_group + return q_group, T + return q_group + +def maximal_abelian_quotient(G): + ''' + Compute the maximal abelian quotient of an FpGroup. + The quotient group G/[G,G] will be the largest + abelain quotient of `G`. + Here, [G, G] is the commutator subgroup. + + Examples + ======== + >>> from sympy.combinatorics.fp_groups import FpGroup + >>> from sympy.combinatorics.free_groups import free_group + >>> from sympy.combinatorics.fp_groups import maximal_abelian_quotient + >>> F, x, y = free_group("x, y") + >>> f = FpGroup(F, [x**2, y**3, (x*y)**4]) + >>> T = maximal_abelian_quotient(f) + >>> T.order() + 2 + >>> T.is_abelian + True + + See Also + ======== + subgroup_quotient + + ''' + if not isinstance(G, FpGroup): + raise ValueError("The group must be finitely presented") + + return subgroup_quotient(G, G.derived_subgroup()) class FpSubgroup(DefaultPrinting): ''' @@ -1294,7 +1425,8 @@ def reidemeister_presentation(fp_grp, H, C=None, homomorphism=False): 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) + if gens: + gens, rels = simplify_presentation(gens, rels, change_gens=True) C.schreier_generators = tuple(gens) C.reidemeister_relators = tuple(rels) diff --git a/sympy/combinatorics/homomorphisms.py b/sympy/combinatorics/homomorphisms.py index 3ac6a5742f79..fd28e80bbfd0 100644 --- a/sympy/combinatorics/homomorphisms.py +++ b/sympy/combinatorics/homomorphisms.py @@ -352,7 +352,7 @@ def _image(r): # truth of equality otherwise success = codomain.make_confluent() s = codomain.equals(_image(r), identity) - if s in None and not success: + if s is None and not success: raise RuntimeError("Can't determine if the images " "define a homomorphism. Try increasing " "the maximum number of rewriting rules " @@ -421,25 +421,30 @@ def block_homomorphism(group, blocks): H = GroupHomomorphism(group, codomain, images) return H -def group_isomorphism(G, H, isomorphism=True): +def find_homomorphism(G, H, injective=False, surjective=False, compute=True, all=False): ''' - Compute an isomorphism between 2 given groups. + Compute a homomorphism with required properties between 2 given groups. + An isomorphism is computed when both injective and surjective are set to True. Arguments: G (a finite `FpGroup` or a `PermutationGroup`) -- First group H (a finite `FpGroup` or a `PermutationGroup`) -- Second group - isomorphism (boolean) -- This is used to avoid the computation of homomorphism - when the user only wants to check if there exists - an isomorphism between the groups. + injective (boolean) -- compute a monomorphism, if possible + surjective (boolean) -- compute an epimorphism, if possible + compute (boolean) -- When set to False, check the existence of a homomorphism, + avoiding computation where possible. + all (boolean) -- compute all possible homomorphisms with specified properties. Returns: - If isomorphism = False -- Returns a boolean. - If isomorphism = True -- Returns a boolean and an isomorphism between `G` and `H`. + If compute = False -- Return a boolean. + If compute = True -- Return a boolean and a homomorphism with required properties + between `G` and `H`. + If all = True -- Return all possible specified homomorphisms as a list. Summary: - Uses the approach suggested by Robert Tarjan to compute the isomorphism between two groups. + Uses the approach suggested by Robert Tarjan to compute a homomorphism between two groups. First, the generators of `G` are mapped to the elements of `H` and - we check if the mapping induces an isomorphism. + we check if the mapping induces a homomorphism with required properties. Examples ======== @@ -448,25 +453,27 @@ def group_isomorphism(G, H, isomorphism=True): >>> from sympy.combinatorics.perm_groups import PermutationGroup >>> from sympy.combinatorics.free_groups import free_group >>> from sympy.combinatorics.fp_groups import FpGroup - >>> from sympy.combinatorics.homomorphisms import homomorphism, group_isomorphism + >>> from sympy.combinatorics.homomorphisms import find_homomorphism, group_isomorphism >>> from sympy.combinatorics.named_groups import DihedralGroup, AlternatingGroup >>> D = DihedralGroup(8) >>> p = Permutation(0, 1, 2, 3, 4, 5, 6, 7) >>> P = PermutationGroup(p) - >>> group_isomorphism(D, P) + >>> find_homomorphism(D, P, injective=True, surjective=True) (False, None) >>> F, a, b = free_group("a, b") >>> G = FpGroup(F, [a**3, b**3, (a*b)**2]) >>> H = AlternatingGroup(4) - >>> (check, T) = group_isomorphism(G, H) - >>> check + >>> list_hom = find_homomorphism(G, H, surjective=True, all=True) + >>> all(elem.is_surjective() for elem in list_hom) True - >>> T(b*a*b**-1*a**-1*b**-1) - (0 2 3) ''' + if all: + # Compute the list of all possible isomorphisms/epimorphisms/monomorphisms. + list_hom = [] + if not isinstance(G, (PermutationGroup, FpGroup)): raise TypeError("The group must be a PermutationGroup or an FpGroup") if not isinstance(H, (PermutationGroup, FpGroup)): @@ -477,8 +484,10 @@ def group_isomorphism(G, H, isomorphism=True): H = simplify_presentation(H) # Two infinite FpGroups with the same generators are isomorphic # when the relators are same but are ordered differently. - if G.generators == H.generators and (G.relators).sort() == (H.relators).sort(): - if not isomorphism: + (G.relators).sort() + (H.relators).sort() + if G.generators == H.generators and (G.relators) == (H.relators): + if not compute: return True return (True, homomorphism(G, H, G.generators, H.generators)) @@ -495,12 +504,18 @@ def group_isomorphism(G, H, isomorphism=True): raise NotImplementedError("Isomorphism methods are not implemented for infinite groups.") _H, h_isomorphism = H._to_perm_group() - if (g_order != h_order) or (G.is_abelian != H.is_abelian): - if not isomorphism: - return False - return (False, None) - - if not isomorphism: + if injective: + if (h_order % g_order != 0) or (not G.is_abelian and H.is_abelian): + if not compute: + return False + return (False, None) + if surjective: + if (g_order % h_order != 0) or (G.is_abelian and not H.is_abelian): + if not compute: + return False + return (False, None) + + if (injective and surjective) and not compute: # Two groups of the same cyclic numbered order # are isomorphic to each other. n = g_order @@ -512,21 +527,53 @@ def group_isomorphism(G, H, isomorphism=True): for subset in itertools.permutations(_H, len(gens)): images = list(subset) images.extend([_H.identity]*(len(G.generators)-len(images))) - _images = dict(zip(gens,images)) + _images = dict(zip(gens, images)) if _check_homomorphism(G, _H, _images): if isinstance(H, FpGroup): images = h_isomorphism.invert(images) T = homomorphism(G, H, G.generators, images, check=False) - if T.is_isomorphism(): - # It is a valid isomorphism - if not isomorphism: - return True - return (True, T) + if (injective == T.is_injective()) and (surjective == T.is_surjective()): + if not all: + if not compute: + return True + return True, T + list_hom.append(T) + + if all: + return list_hom - if not isomorphism: + if not compute: return False return (False, None) +def group_isomorphism(G, H, all=False): + ''' + Compute an isomorphism (if possible) between 2 groups. + + Arguments: + G (a finite `FpGroup` or a `PermutationGroup`) -- First group + H (a finite `FpGroup` or a `PermutationGroup`) -- Second group + all (boolean) -- compute all possible isomorphisms when set to True. + + Examples + ======== + >>> from sympy.combinatorics.free_groups import free_group + >>> from sympy.combinatorics.fp_groups import FpGroup + >>> from sympy.combinatorics.homomorphisms import group_isomorphism + >>> from sympy.combinatorics.named_groups import AlternatingGroup + + >>> F, a, b = free_group("a, b") + >>> G = FpGroup(F, [a**3, b**3, (a*b)**2]) + >>> H = AlternatingGroup(4) + >>> (check, T) = group_isomorphism(G, H) + >>> check + True + >>> T(b*a*b**-1*a**-1*b**-1) + (0 2 3) + + ''' + return find_homomorphism(G, H, injective=True, surjective=True, all=all) + def is_isomorphic(G, H): ''' Check if the groups are isomorphic to each other @@ -537,4 +584,4 @@ def is_isomorphic(G, H): Returns -- boolean ''' - return group_isomorphism(G, H, isomorphism=False) + return find_homomorphism(G, H, injective=True, surjective=True, compute=False) diff --git a/sympy/combinatorics/tests/test_fp_groups.py b/sympy/combinatorics/tests/test_fp_groups.py index e537b881eefa..34a0dd8c62da 100644 --- a/sympy/combinatorics/tests/test_fp_groups.py +++ b/sympy/combinatorics/tests/test_fp_groups.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- from sympy import S from sympy.combinatorics.fp_groups import (FpGroup, low_index_subgroups, - reidemeister_presentation, FpSubgroup) + reidemeister_presentation, FpSubgroup, + subgroup_quotient, maximal_abelian_quotient) from sympy.combinatorics.free_groups import free_group """ @@ -145,6 +146,27 @@ def test_subgroup_presentations(): assert str(gens) == "(b_1, c_3)" assert len(rels) == 18 +def test_subgroup_quotient(): + F, x, y = free_group("x, y") + f = FpGroup(F, [x**2, y**3, (x*y)**4]) + T = subgroup_quotient(f, [x, y]) + H = f.subgroup([x, y]) + assert T.order() == f.order()/H.order() + + T = subgroup_quotient(f, [x*y**2*x*y, y**2*x*y*x, y**-1]) + H = f.subgroup([x*y**2*x*y, y**2*x*y*x, y**-1]) + assert T.order() == f.order()/H.order() + + G = [x, y**2] + H = [x*y**2*x*y, y**2*x*y*x, y**-1] + K, T = subgroup_quotient(G, H, parent_group=f, homomorphism=True) + G = f.subgroup(G) + H = f.subgroup(H) + assert K.order() == G.order()/H.order() + + T = maximal_abelian_quotient(f) + assert T.is_abelian + assert T.order() == 2 def test_order(): from sympy import S @@ -193,6 +215,7 @@ def _test_subgroup(K, T, S): S = FpSubgroup(f, H) _test_subgroup(K, T, S) + def test_permutation_methods(): from sympy.combinatorics.fp_groups import FpSubgroup F, x, y = free_group("x, y") diff --git a/sympy/combinatorics/tests/test_homomorphisms.py b/sympy/combinatorics/tests/test_homomorphisms.py index bd38902d88d9..017dcd3dd9ce 100644 --- a/sympy/combinatorics/tests/test_homomorphisms.py +++ b/sympy/combinatorics/tests/test_homomorphisms.py @@ -1,6 +1,7 @@ from sympy.combinatorics import Permutation from sympy.combinatorics.perm_groups import PermutationGroup -from sympy.combinatorics.homomorphisms import homomorphism, group_isomorphism, is_isomorphic +from sympy.combinatorics.homomorphisms import homomorphism, find_homomorphism +from sympy.combinatorics.homomorphisms import is_isomorphic, group_isomorphism from sympy.combinatorics.free_groups import free_group from sympy.combinatorics.fp_groups import FpGroup from sympy.combinatorics.named_groups import AlternatingGroup, DihedralGroup, CyclicGroup @@ -56,7 +57,7 @@ def test_homomorphism(): assert T.codomain == D assert T(a*b) == p -def test_isomorphisms(): +def test_find_homomorphism(): F, a, b = free_group("a, b") E, c, d = free_group("c, d") @@ -68,15 +69,13 @@ def test_isomorphisms(): # Trivial Case # FpGroup -> FpGroup H = FpGroup(F, [a**3, b**3, (a*b)**2]) - F, c, d = free_group("c, d") - G = FpGroup(F, [c**3, d**3, (c*d)**2]) + G = FpGroup(E, [c**3, d**3, (c*d)**2]) check, T = group_isomorphism(G, H) assert check T(c**3*d**2) == a**3*b**2 # FpGroup -> PermutationGroup # FpGroup is converted to the equivalent isomorphic group. - F, a, b = free_group("a, b") G = FpGroup(F, [a**3, b**3, (a*b)**2]) H = AlternatingGroup(4) check, T = group_isomorphism(G, H) @@ -84,10 +83,22 @@ def test_isomorphisms(): assert T(b*a*b**-1*a**-1*b**-1) == Permutation(0, 2, 3) assert T(b*a*b*a**-1*b**-1) == Permutation(0, 3, 2) + G = FpGroup(F, [a**2, b**8, a*b*a**-1*b]) + H = FpGroup(F, [a**4, b**2, a*b*a**-1*b]) + check = find_homomorphism(G, H, injective=True, compute=False) + assert not check + + G = FpGroup(F, [a*b*a**-1*b**-1, b**5, a**4]) + H = FpGroup(F, [a**3, b**2, (a*b)**5]) + list_hom = find_homomorphism(G, H, injective=True, all=True) + assert all(elem.is_injective() for elem in list_hom) + # PermutationGroup -> PermutationGroup D = DihedralGroup(8) p = Permutation(0, 1, 2, 3, 4, 5, 6, 7) P = PermutationGroup(p) + list_hom = find_homomorphism(D, P, surjective=True, all=True) + assert all(elem.is_surjective() for elem in list_hom) assert not is_isomorphic(D, P) A = CyclicGroup(5)