| import itertools |
| from sympy.combinatorics.fp_groups import FpGroup, FpSubgroup, simplify_presentation |
| from sympy.combinatorics.free_groups import FreeGroup |
| from sympy.combinatorics.perm_groups import PermutationGroup |
| from sympy.core.intfunc import igcd |
| from sympy.functions.combinatorial.numbers import totient |
| from sympy.core.singleton import S |
|
|
| class GroupHomomorphism: |
| ''' |
| A class representing group homomorphisms. Instantiate using `homomorphism()`. |
| |
| References |
| ========== |
| |
| .. [1] Holt, D., Eick, B. and O'Brien, E. (2005). Handbook of computational group theory. |
| |
| ''' |
|
|
| def __init__(self, domain, codomain, images): |
| self.domain = domain |
| self.codomain = codomain |
| self.images = images |
| self._inverses = None |
| self._kernel = None |
| self._image = None |
|
|
| def _invs(self): |
| ''' |
| Return a dictionary with `{gen: inverse}` where `gen` is a rewriting |
| generator of `codomain` (e.g. strong generator for permutation groups) |
| and `inverse` is an element of its preimage |
| |
| ''' |
| image = self.image() |
| inverses = {} |
| for k in list(self.images.keys()): |
| v = self.images[k] |
| if not (v in inverses |
| or v.is_identity): |
| inverses[v] = k |
| if isinstance(self.codomain, PermutationGroup): |
| gens = image.strong_gens |
| else: |
| gens = image.generators |
| for g in gens: |
| if g in inverses or g.is_identity: |
| continue |
| w = self.domain.identity |
| if isinstance(self.codomain, PermutationGroup): |
| parts = image._strong_gens_slp[g][::-1] |
| else: |
| parts = g |
| for s in parts: |
| if s in inverses: |
| w = w*inverses[s] |
| else: |
| w = w*inverses[s**-1]**-1 |
| inverses[g] = w |
|
|
| return inverses |
|
|
| def invert(self, g): |
| ''' |
| Return an element of the preimage of ``g`` or of each element |
| of ``g`` if ``g`` is a list. |
| |
| Explanation |
| =========== |
| |
| If the codomain is an FpGroup, the inverse for equal |
| elements might not always be the same unless the FpGroup's |
| rewriting system is confluent. However, making a system |
| confluent can be time-consuming. If it's important, try |
| `self.codomain.make_confluent()` first. |
| |
| ''' |
| from sympy.combinatorics import Permutation |
| from sympy.combinatorics.free_groups import FreeGroupElement |
| if isinstance(g, (Permutation, FreeGroupElement)): |
| if isinstance(self.codomain, FpGroup): |
| g = self.codomain.reduce(g) |
| if self._inverses is None: |
| self._inverses = self._invs() |
| image = self.image() |
| w = self.domain.identity |
| if isinstance(self.codomain, PermutationGroup): |
| gens = image.generator_product(g)[::-1] |
| else: |
| gens = g |
| |
| |
| |
| |
| |
| |
| for i in range(len(gens)): |
| s = gens[i] |
| if s.is_identity: |
| continue |
| if s in self._inverses: |
| w = w*self._inverses[s] |
| else: |
| w = w*self._inverses[s**-1]**-1 |
| return w |
| elif isinstance(g, list): |
| return [self.invert(e) for e in g] |
|
|
| def kernel(self): |
| ''' |
| Compute the kernel of `self`. |
| |
| ''' |
| if self._kernel is None: |
| self._kernel = self._compute_kernel() |
| return self._kernel |
|
|
| def _compute_kernel(self): |
| G = self.domain |
| G_order = G.order() |
| if G_order is S.Infinity: |
| raise NotImplementedError( |
| "Kernel computation is not implemented for infinite groups") |
| gens = [] |
| if isinstance(G, PermutationGroup): |
| K = PermutationGroup(G.identity) |
| else: |
| K = FpSubgroup(G, gens, normal=True) |
| i = self.image().order() |
| while K.order()*i != G_order: |
| r = G.random() |
| k = r*self.invert(self(r))**-1 |
| if k not in K: |
| gens.append(k) |
| if isinstance(G, PermutationGroup): |
| K = PermutationGroup(gens) |
| else: |
| K = FpSubgroup(G, gens, normal=True) |
| return K |
|
|
| def image(self): |
| ''' |
| Compute the image of `self`. |
| |
| ''' |
| if self._image is None: |
| values = list(set(self.images.values())) |
| if isinstance(self.codomain, PermutationGroup): |
| self._image = self.codomain.subgroup(values) |
| else: |
| self._image = FpSubgroup(self.codomain, values) |
| return self._image |
|
|
| def _apply(self, elem): |
| ''' |
| Apply `self` to `elem`. |
| |
| ''' |
| if elem not in self.domain: |
| if isinstance(elem, (list, tuple)): |
| return [self._apply(e) for e in elem] |
| raise ValueError("The supplied element does not belong to the domain") |
| if elem.is_identity: |
| return self.codomain.identity |
| else: |
| images = self.images |
| value = self.codomain.identity |
| if isinstance(self.domain, PermutationGroup): |
| gens = self.domain.generator_product(elem, original=True) |
| for g in gens: |
| if g in self.images: |
| value = images[g]*value |
| else: |
| value = images[g**-1]**-1*value |
| else: |
| i = 0 |
| for _, p in elem.array_form: |
| if p < 0: |
| g = elem[i]**-1 |
| else: |
| g = elem[i] |
| value = value*images[g]**p |
| i += abs(p) |
| return value |
|
|
| def __call__(self, elem): |
| return self._apply(elem) |
|
|
| def is_injective(self): |
| ''' |
| Check if the homomorphism is injective |
| |
| ''' |
| return self.kernel().order() == 1 |
|
|
| def is_surjective(self): |
| ''' |
| Check if the homomorphism is surjective |
| |
| ''' |
| im = self.image().order() |
| oth = self.codomain.order() |
| if im is S.Infinity and oth is S.Infinity: |
| return None |
| else: |
| return im == oth |
|
|
| def is_isomorphism(self): |
| ''' |
| Check if `self` is an isomorphism. |
| |
| ''' |
| return self.is_injective() and self.is_surjective() |
|
|
| def is_trivial(self): |
| ''' |
| Check is `self` is a trivial homomorphism, i.e. all elements |
| are mapped to the identity. |
| |
| ''' |
| return self.image().order() == 1 |
|
|
| def compose(self, other): |
| ''' |
| Return the composition of `self` and `other`, i.e. |
| the homomorphism phi such that for all g in the domain |
| of `other`, phi(g) = self(other(g)) |
| |
| ''' |
| if not other.image().is_subgroup(self.domain): |
| raise ValueError("The image of `other` must be a subgroup of " |
| "the domain of `self`") |
| images = {g: self(other(g)) for g in other.images} |
| return GroupHomomorphism(other.domain, self.codomain, images) |
|
|
| def restrict_to(self, H): |
| ''' |
| Return the restriction of the homomorphism to the subgroup `H` |
| of the domain. |
| |
| ''' |
| if not isinstance(H, PermutationGroup) or not H.is_subgroup(self.domain): |
| raise ValueError("Given H is not a subgroup of the domain") |
| domain = H |
| images = {g: self(g) for g in H.generators} |
| return GroupHomomorphism(domain, self.codomain, images) |
|
|
| def invert_subgroup(self, H): |
| ''' |
| Return the subgroup of the domain that is the inverse image |
| of the subgroup ``H`` of the homomorphism image |
| |
| ''' |
| if not H.is_subgroup(self.image()): |
| raise ValueError("Given H is not a subgroup of the image") |
| gens = [] |
| P = PermutationGroup(self.image().identity) |
| for h in H.generators: |
| h_i = self.invert(h) |
| if h_i not in P: |
| gens.append(h_i) |
| P = PermutationGroup(gens) |
| for k in self.kernel().generators: |
| if k*h_i not in P: |
| gens.append(k*h_i) |
| P = PermutationGroup(gens) |
| return P |
|
|
| def homomorphism(domain, codomain, gens, images=(), check=True): |
| ''' |
| Create (if possible) a group homomorphism from the group ``domain`` |
| to the group ``codomain`` defined by the images of the domain's |
| generators ``gens``. ``gens`` and ``images`` can be either lists or tuples |
| of equal sizes. If ``gens`` is a proper subset of the group's generators, |
| the unspecified generators will be mapped to the identity. If the |
| images are not specified, a trivial homomorphism will be created. |
| |
| If the given images of the generators do not define a homomorphism, |
| an exception is raised. |
| |
| If ``check`` is ``False``, do not check whether the given images actually |
| define a homomorphism. |
| |
| ''' |
| if not isinstance(domain, (PermutationGroup, FpGroup, FreeGroup)): |
| raise TypeError("The domain must be a group") |
| if not isinstance(codomain, (PermutationGroup, FpGroup, FreeGroup)): |
| raise TypeError("The codomain must be a group") |
|
|
| generators = domain.generators |
| if not all(g in generators for g in gens): |
| raise ValueError("The supplied generators must be a subset of the domain's generators") |
| if not all(g in codomain for g in images): |
| raise ValueError("The images must be elements of the codomain") |
|
|
| if images and len(images) != len(gens): |
| raise ValueError("The number of images must be equal to the number of generators") |
|
|
| gens = list(gens) |
| images = list(images) |
|
|
| images.extend([codomain.identity]*(len(generators)-len(images))) |
| gens.extend([g for g in generators if g not in gens]) |
| images = dict(zip(gens,images)) |
|
|
| if check and not _check_homomorphism(domain, codomain, images): |
| raise ValueError("The given images do not define a homomorphism") |
| return GroupHomomorphism(domain, codomain, images) |
|
|
| def _check_homomorphism(domain, codomain, images): |
| """ |
| Check that a given mapping of generators to images defines a homomorphism. |
| |
| Parameters |
| ========== |
| domain : PermutationGroup, FpGroup, FreeGroup |
| codomain : PermutationGroup, FpGroup, FreeGroup |
| images : dict |
| The set of keys must be equal to domain.generators. |
| The values must be elements of the codomain. |
| |
| """ |
| pres = domain if hasattr(domain, 'relators') else domain.presentation() |
| rels = pres.relators |
| gens = pres.generators |
| symbols = [g.ext_rep[0] for g in gens] |
| symbols_to_domain_generators = dict(zip(symbols, domain.generators)) |
| identity = codomain.identity |
|
|
| def _image(r): |
| w = identity |
| for symbol, power in r.array_form: |
| g = symbols_to_domain_generators[symbol] |
| w *= images[g]**power |
| return w |
|
|
| for r in rels: |
| if isinstance(codomain, FpGroup): |
| s = codomain.equals(_image(r), identity) |
| if s is None: |
| |
| |
| |
| success = codomain.make_confluent() |
| s = codomain.equals(_image(r), identity) |
| 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 " |
| "(group._rewriting_system.set_max(new_value); " |
| "the current value is stored in group._rewriting" |
| "_system.maxeqns)") |
| else: |
| s = _image(r).is_identity |
| if not s: |
| return False |
| return True |
|
|
| def orbit_homomorphism(group, omega): |
| ''' |
| Return the homomorphism induced by the action of the permutation |
| group ``group`` on the set ``omega`` that is closed under the action. |
| |
| ''' |
| from sympy.combinatorics import Permutation |
| from sympy.combinatorics.named_groups import SymmetricGroup |
| codomain = SymmetricGroup(len(omega)) |
| identity = codomain.identity |
| omega = list(omega) |
| images = {g: identity*Permutation([omega.index(o^g) for o in omega]) for g in group.generators} |
| group._schreier_sims(base=omega) |
| H = GroupHomomorphism(group, codomain, images) |
| if len(group.basic_stabilizers) > len(omega): |
| H._kernel = group.basic_stabilizers[len(omega)] |
| else: |
| H._kernel = PermutationGroup([group.identity]) |
| return H |
|
|
| def block_homomorphism(group, blocks): |
| ''' |
| Return the homomorphism induced by the action of the permutation |
| group ``group`` on the block system ``blocks``. The latter should be |
| of the same form as returned by the ``minimal_block`` method for |
| permutation groups, namely a list of length ``group.degree`` where |
| the i-th entry is a representative of the block i belongs to. |
| |
| ''' |
| from sympy.combinatorics import Permutation |
| from sympy.combinatorics.named_groups import SymmetricGroup |
|
|
| n = len(blocks) |
|
|
| |
| |
| |
| |
| m = 0 |
| p = [] |
| b = [None]*n |
| for i in range(n): |
| if blocks[i] == i: |
| p.append(i) |
| b[i] = m |
| m += 1 |
| for i in range(n): |
| b[i] = b[blocks[i]] |
|
|
| codomain = SymmetricGroup(m) |
| |
| identity = range(m) |
| images = {g: Permutation([b[p[i]^g] for i in identity]) for g in group.generators} |
| H = GroupHomomorphism(group, codomain, images) |
| return H |
|
|
| def group_isomorphism(G, H, isomorphism=True): |
| ''' |
| Compute an isomorphism between 2 given groups. |
| |
| Parameters |
| ========== |
| |
| G : A finite ``FpGroup`` or a ``PermutationGroup``. |
| First group. |
| |
| H : A finite ``FpGroup`` or a ``PermutationGroup`` |
| Second group. |
| |
| isomorphism : bool |
| This is used to avoid the computation of homomorphism |
| when the user only wants to check if there exists |
| an isomorphism between the groups. |
| |
| Returns |
| ======= |
| |
| If isomorphism = False -- Returns a boolean. |
| If isomorphism = True -- Returns a boolean and an isomorphism between `G` and `H`. |
| |
| Examples |
| ======== |
| |
| >>> from sympy.combinatorics import free_group, Permutation |
| >>> from sympy.combinatorics.perm_groups import PermutationGroup |
| >>> from sympy.combinatorics.fp_groups import FpGroup |
| >>> from sympy.combinatorics.homomorphisms import 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) |
| (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 |
| True |
| >>> T(b*a*b**-1*a**-1*b**-1) |
| (0 2 3) |
| |
| Notes |
| ===== |
| |
| Uses the approach suggested by Robert Tarjan to compute the isomorphism between two groups. |
| First, the generators of ``G`` are mapped to the elements of ``H`` and |
| we check if the mapping induces an isomorphism. |
| |
| ''' |
| if not isinstance(G, (PermutationGroup, FpGroup)): |
| raise TypeError("The group must be a PermutationGroup or an FpGroup") |
| if not isinstance(H, (PermutationGroup, FpGroup)): |
| raise TypeError("The group must be a PermutationGroup or an FpGroup") |
|
|
| if isinstance(G, FpGroup) and isinstance(H, FpGroup): |
| G = simplify_presentation(G) |
| H = simplify_presentation(H) |
| |
| |
| if G.generators == H.generators and (G.relators).sort() == (H.relators).sort(): |
| if not isomorphism: |
| return True |
| return (True, homomorphism(G, H, G.generators, H.generators)) |
|
|
| |
| _H = H |
| g_order = G.order() |
| h_order = H.order() |
|
|
| if g_order is S.Infinity: |
| raise NotImplementedError("Isomorphism methods are not implemented for infinite groups.") |
|
|
| if isinstance(H, FpGroup): |
| if h_order is S.Infinity: |
| 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: |
| |
| |
| n = g_order |
| if (igcd(n, totient(n))) == 1: |
| return True |
|
|
| |
| gens = list(G.generators) |
| 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)) |
| 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(): |
| |
| if not isomorphism: |
| return True |
| return (True, T) |
|
|
| if not isomorphism: |
| return False |
| return (False, None) |
|
|
| def is_isomorphic(G, H): |
| ''' |
| Check if the groups are isomorphic to each other |
| |
| Parameters |
| ========== |
| |
| G : A finite ``FpGroup`` or a ``PermutationGroup`` |
| First group. |
| |
| H : A finite ``FpGroup`` or a ``PermutationGroup`` |
| Second group. |
| |
| Returns |
| ======= |
| |
| boolean |
| ''' |
| return group_isomorphism(G, H, isomorphism=False) |
|
|