| | from math import factorial as _factorial, log, prod |
| | from itertools import chain, product |
| |
|
| |
|
| | from sympy.combinatorics import Permutation |
| | from sympy.combinatorics.permutations import (_af_commutes_with, _af_invert, |
| | _af_rmul, _af_rmuln, _af_pow, Cycle) |
| | from sympy.combinatorics.util import (_check_cycles_alt_sym, |
| | _distribute_gens_by_base, _orbits_transversals_from_bsgs, |
| | _handle_precomputed_bsgs, _base_ordering, _strong_gens_from_distr, |
| | _strip, _strip_af) |
| | from sympy.core import Basic |
| | from sympy.core.random import _randrange, randrange, choice |
| | from sympy.core.symbol import Symbol |
| | from sympy.core.sympify import _sympify |
| | from sympy.functions.combinatorial.factorials import factorial |
| | from sympy.ntheory import primefactors, sieve |
| | from sympy.ntheory.factor_ import (factorint, multiplicity) |
| | from sympy.ntheory.primetest import isprime |
| | from sympy.utilities.iterables import has_variety, is_sequence, uniq |
| |
|
| | rmul = Permutation.rmul_with_af |
| | _af_new = Permutation._af_new |
| |
|
| |
|
| | class PermutationGroup(Basic): |
| | r"""The class defining a Permutation group. |
| | |
| | Explanation |
| | =========== |
| | |
| | ``PermutationGroup([p1, p2, ..., pn])`` returns the permutation group |
| | generated by the list of permutations. This group can be supplied |
| | to Polyhedron if one desires to decorate the elements to which the |
| | indices of the permutation refer. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> from sympy.combinatorics import Polyhedron |
| | |
| | The permutations corresponding to motion of the front, right and |
| | bottom face of a $2 \times 2$ Rubik's cube are defined: |
| | |
| | >>> F = Permutation(2, 19, 21, 8)(3, 17, 20, 10)(4, 6, 7, 5) |
| | >>> R = Permutation(1, 5, 21, 14)(3, 7, 23, 12)(8, 10, 11, 9) |
| | >>> D = Permutation(6, 18, 14, 10)(7, 19, 15, 11)(20, 22, 23, 21) |
| | |
| | These are passed as permutations to PermutationGroup: |
| | |
| | >>> G = PermutationGroup(F, R, D) |
| | >>> G.order() |
| | 3674160 |
| | |
| | The group can be supplied to a Polyhedron in order to track the |
| | objects being moved. An example involving the $2 \times 2$ Rubik's cube is |
| | given there, but here is a simple demonstration: |
| | |
| | >>> a = Permutation(2, 1) |
| | >>> b = Permutation(1, 0) |
| | >>> G = PermutationGroup(a, b) |
| | >>> P = Polyhedron(list('ABC'), pgroup=G) |
| | >>> P.corners |
| | (A, B, C) |
| | >>> P.rotate(0) # apply permutation 0 |
| | >>> P.corners |
| | (A, C, B) |
| | >>> P.reset() |
| | >>> P.corners |
| | (A, B, C) |
| | |
| | Or one can make a permutation as a product of selected permutations |
| | and apply them to an iterable directly: |
| | |
| | >>> P10 = G.make_perm([0, 1]) |
| | >>> P10('ABC') |
| | ['C', 'A', 'B'] |
| | |
| | See Also |
| | ======== |
| | |
| | sympy.combinatorics.polyhedron.Polyhedron, |
| | sympy.combinatorics.permutations.Permutation |
| | |
| | References |
| | ========== |
| | |
| | .. [1] Holt, D., Eick, B., O'Brien, E. |
| | "Handbook of Computational Group Theory" |
| | |
| | .. [2] Seress, A. |
| | "Permutation Group Algorithms" |
| | |
| | .. [3] https://en.wikipedia.org/wiki/Schreier_vector |
| | |
| | .. [4] https://en.wikipedia.org/wiki/Nielsen_transformation#Product_replacement_algorithm |
| | |
| | .. [5] Frank Celler, Charles R.Leedham-Green, Scott H.Murray, |
| | Alice C.Niemeyer, and E.A.O'Brien. "Generating Random |
| | Elements of a Finite Group" |
| | |
| | .. [6] https://en.wikipedia.org/wiki/Block_%28permutation_group_theory%29 |
| | |
| | .. [7] https://algorithmist.com/wiki/Union_find |
| | |
| | .. [8] https://en.wikipedia.org/wiki/Multiply_transitive_group#Multiply_transitive_groups |
| | |
| | .. [9] https://en.wikipedia.org/wiki/Center_%28group_theory%29 |
| | |
| | .. [10] https://en.wikipedia.org/wiki/Centralizer_and_normalizer |
| | |
| | .. [11] https://groupprops.subwiki.org/wiki/Derived_subgroup |
| | |
| | .. [12] https://en.wikipedia.org/wiki/Nilpotent_group |
| | |
| | .. [13] https://www.math.colostate.edu/~hulpke/CGT/cgtnotes.pdf |
| | |
| | .. [14] https://docs.gap-system.org/doc/ref/manual.pdf |
| | |
| | """ |
| | is_group = True |
| |
|
| | def __new__(cls, *args, dups=True, **kwargs): |
| | """The default constructor. Accepts Cycle and Permutation forms. |
| | Removes duplicates unless ``dups`` keyword is ``False``. |
| | """ |
| | if not args: |
| | args = [Permutation()] |
| | else: |
| | args = list(args[0] if is_sequence(args[0]) else args) |
| | if not args: |
| | args = [Permutation()] |
| | if any(isinstance(a, Cycle) for a in args): |
| | args = [Permutation(a) for a in args] |
| | if has_variety(a.size for a in args): |
| | degree = kwargs.pop('degree', None) |
| | if degree is None: |
| | degree = max(a.size for a in args) |
| | for i in range(len(args)): |
| | if args[i].size != degree: |
| | args[i] = Permutation(args[i], size=degree) |
| | if dups: |
| | args = list(uniq([_af_new(list(a)) for a in args])) |
| | if len(args) > 1: |
| | args = [g for g in args if not g.is_identity] |
| | return Basic.__new__(cls, *args, **kwargs) |
| |
|
| | def __init__(self, *args, **kwargs): |
| | self._generators = list(self.args) |
| | self._order = None |
| | self._elements = [] |
| | self._center = None |
| | self._is_abelian = None |
| | self._is_transitive = None |
| | self._is_sym = None |
| | self._is_alt = None |
| | self._is_primitive = None |
| | self._is_nilpotent = None |
| | self._is_solvable = None |
| | self._is_trivial = None |
| | self._transitivity_degree = None |
| | self._max_div = None |
| | self._is_perfect = None |
| | self._is_cyclic = None |
| | self._is_dihedral = None |
| | self._r = len(self._generators) |
| | self._degree = self._generators[0].size |
| |
|
| | |
| | self._base = [] |
| | self._strong_gens = [] |
| | self._strong_gens_slp = [] |
| | self._basic_orbits = [] |
| | self._transversals = [] |
| | self._transversal_slp = [] |
| |
|
| | |
| | self._random_gens = [] |
| |
|
| | |
| | self._fp_presentation = None |
| |
|
| | def __getitem__(self, i): |
| | return self._generators[i] |
| |
|
| | def __contains__(self, i): |
| | """Return ``True`` if *i* is contained in PermutationGroup. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> p = Permutation(1, 2, 3) |
| | >>> Permutation(3) in PermutationGroup(p) |
| | True |
| | |
| | """ |
| | if not isinstance(i, Permutation): |
| | raise TypeError("A PermutationGroup contains only Permutations as " |
| | "elements, not elements of type %s" % type(i)) |
| | return self.contains(i) |
| |
|
| | def __len__(self): |
| | return len(self._generators) |
| |
|
| | def equals(self, other): |
| | """Return ``True`` if PermutationGroup generated by elements in the |
| | group are same i.e they represent the same PermutationGroup. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> p = Permutation(0, 1, 2, 3, 4, 5) |
| | >>> G = PermutationGroup([p, p**2]) |
| | >>> H = PermutationGroup([p**2, p]) |
| | >>> G.generators == H.generators |
| | False |
| | >>> G.equals(H) |
| | True |
| | |
| | """ |
| | if not isinstance(other, PermutationGroup): |
| | return False |
| |
|
| | set_self_gens = set(self.generators) |
| | set_other_gens = set(other.generators) |
| |
|
| | |
| | |
| | |
| | if set_self_gens == set_other_gens: |
| | return True |
| |
|
| | |
| | |
| | for gen1 in set_self_gens: |
| | if not other.contains(gen1): |
| | return False |
| | for gen2 in set_other_gens: |
| | if not self.contains(gen2): |
| | return False |
| | return True |
| |
|
| | def __mul__(self, other): |
| | """ |
| | Return the direct product of two permutation groups as a permutation |
| | group. |
| | |
| | Explanation |
| | =========== |
| | |
| | This implementation realizes the direct product by shifting the index |
| | set for the generators of the second group: so if we have ``G`` acting |
| | on ``n1`` points and ``H`` acting on ``n2`` points, ``G*H`` acts on |
| | ``n1 + n2`` points. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import CyclicGroup |
| | >>> G = CyclicGroup(5) |
| | >>> H = G*G |
| | >>> H |
| | PermutationGroup([ |
| | (9)(0 1 2 3 4), |
| | (5 6 7 8 9)]) |
| | >>> H.order() |
| | 25 |
| | |
| | """ |
| | if isinstance(other, Permutation): |
| | return Coset(other, self, dir='+') |
| | gens1 = [perm._array_form for perm in self.generators] |
| | gens2 = [perm._array_form for perm in other.generators] |
| | n1 = self._degree |
| | n2 = other._degree |
| | start = list(range(n1)) |
| | end = list(range(n1, n1 + n2)) |
| | for i in range(len(gens2)): |
| | gens2[i] = [x + n1 for x in gens2[i]] |
| | gens2 = [start + gen for gen in gens2] |
| | gens1 = [gen + end for gen in gens1] |
| | together = gens1 + gens2 |
| | gens = [_af_new(x) for x in together] |
| | return PermutationGroup(gens) |
| |
|
| | def _random_pr_init(self, r, n, _random_prec_n=None): |
| | r"""Initialize random generators for the product replacement algorithm. |
| | |
| | Explanation |
| | =========== |
| | |
| | The implementation uses a modification of the original product |
| | replacement algorithm due to Leedham-Green, as described in [1], |
| | pp. 69-71; also, see [2], pp. 27-29 for a detailed theoretical |
| | analysis of the original product replacement algorithm, and [4]. |
| | |
| | The product replacement algorithm is used for producing random, |
| | uniformly distributed elements of a group `G` with a set of generators |
| | `S`. For the initialization ``_random_pr_init``, a list ``R`` of |
| | `\max\{r, |S|\}` group generators is created as the attribute |
| | ``G._random_gens``, repeating elements of `S` if necessary, and the |
| | identity element of `G` is appended to ``R`` - we shall refer to this |
| | last element as the accumulator. Then the function ``random_pr()`` |
| | is called ``n`` times, randomizing the list ``R`` while preserving |
| | the generation of `G` by ``R``. The function ``random_pr()`` itself |
| | takes two random elements ``g, h`` among all elements of ``R`` but |
| | the accumulator and replaces ``g`` with a randomly chosen element |
| | from `\{gh, g(~h), hg, (~h)g\}`. Then the accumulator is multiplied |
| | by whatever ``g`` was replaced by. The new value of the accumulator is |
| | then returned by ``random_pr()``. |
| | |
| | The elements returned will eventually (for ``n`` large enough) become |
| | uniformly distributed across `G` ([5]). For practical purposes however, |
| | the values ``n = 50, r = 11`` are suggested in [1]. |
| | |
| | Notes |
| | ===== |
| | |
| | THIS FUNCTION HAS SIDE EFFECTS: it changes the attribute |
| | self._random_gens |
| | |
| | See Also |
| | ======== |
| | |
| | random_pr |
| | |
| | """ |
| | deg = self.degree |
| | random_gens = [x._array_form for x in self.generators] |
| | k = len(random_gens) |
| | if k < r: |
| | for i in range(k, r): |
| | random_gens.append(random_gens[i - k]) |
| | acc = list(range(deg)) |
| | random_gens.append(acc) |
| | self._random_gens = random_gens |
| |
|
| | |
| | if _random_prec_n is None: |
| | for i in range(n): |
| | self.random_pr() |
| | else: |
| | for i in range(n): |
| | self.random_pr(_random_prec=_random_prec_n[i]) |
| |
|
| | def _union_find_merge(self, first, second, ranks, parents, not_rep): |
| | """Merges two classes in a union-find data structure. |
| | |
| | Explanation |
| | =========== |
| | |
| | Used in the implementation of Atkinson's algorithm as suggested in [1], |
| | pp. 83-87. The class merging process uses union by rank as an |
| | optimization. ([7]) |
| | |
| | Notes |
| | ===== |
| | |
| | THIS FUNCTION HAS SIDE EFFECTS: the list of class representatives, |
| | ``parents``, the list of class sizes, ``ranks``, and the list of |
| | elements that are not representatives, ``not_rep``, are changed due to |
| | class merging. |
| | |
| | See Also |
| | ======== |
| | |
| | minimal_block, _union_find_rep |
| | |
| | References |
| | ========== |
| | |
| | .. [1] Holt, D., Eick, B., O'Brien, E. |
| | "Handbook of computational group theory" |
| | |
| | .. [7] https://algorithmist.com/wiki/Union_find |
| | |
| | """ |
| | rep_first = self._union_find_rep(first, parents) |
| | rep_second = self._union_find_rep(second, parents) |
| | if rep_first != rep_second: |
| | |
| | if ranks[rep_first] >= ranks[rep_second]: |
| | new_1, new_2 = rep_first, rep_second |
| | else: |
| | new_1, new_2 = rep_second, rep_first |
| | total_rank = ranks[new_1] + ranks[new_2] |
| | if total_rank > self.max_div: |
| | return -1 |
| | parents[new_2] = new_1 |
| | ranks[new_1] = total_rank |
| | not_rep.append(new_2) |
| | return 1 |
| | return 0 |
| |
|
| | def _union_find_rep(self, num, parents): |
| | """Find representative of a class in a union-find data structure. |
| | |
| | Explanation |
| | =========== |
| | |
| | Used in the implementation of Atkinson's algorithm as suggested in [1], |
| | pp. 83-87. After the representative of the class to which ``num`` |
| | belongs is found, path compression is performed as an optimization |
| | ([7]). |
| | |
| | Notes |
| | ===== |
| | |
| | THIS FUNCTION HAS SIDE EFFECTS: the list of class representatives, |
| | ``parents``, is altered due to path compression. |
| | |
| | See Also |
| | ======== |
| | |
| | minimal_block, _union_find_merge |
| | |
| | References |
| | ========== |
| | |
| | .. [1] Holt, D., Eick, B., O'Brien, E. |
| | "Handbook of computational group theory" |
| | |
| | .. [7] https://algorithmist.com/wiki/Union_find |
| | |
| | """ |
| | rep, parent = num, parents[num] |
| | while parent != rep: |
| | rep = parent |
| | parent = parents[rep] |
| | |
| | temp, parent = num, parents[num] |
| | while parent != rep: |
| | parents[temp] = rep |
| | temp = parent |
| | parent = parents[temp] |
| | return rep |
| |
|
| | @property |
| | def base(self): |
| | r"""Return a base from the Schreier-Sims algorithm. |
| | |
| | Explanation |
| | =========== |
| | |
| | For a permutation group `G`, a base is a sequence of points |
| | `B = (b_1, b_2, \dots, b_k)` such that no element of `G` apart |
| | from the identity fixes all the points in `B`. The concepts of |
| | a base and strong generating set and their applications are |
| | discussed in depth in [1], pp. 87-89 and [2], pp. 55-57. |
| | |
| | An alternative way to think of `B` is that it gives the |
| | indices of the stabilizer cosets that contain more than the |
| | identity permutation. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> G = PermutationGroup([Permutation(0, 1, 3)(2, 4)]) |
| | >>> G.base |
| | [0, 2] |
| | |
| | See Also |
| | ======== |
| | |
| | strong_gens, basic_transversals, basic_orbits, basic_stabilizers |
| | |
| | """ |
| | if self._base == []: |
| | self.schreier_sims() |
| | return self._base |
| |
|
| | def baseswap(self, base, strong_gens, pos, randomized=False, |
| | transversals=None, basic_orbits=None, strong_gens_distr=None): |
| | r"""Swap two consecutive base points in base and strong generating set. |
| | |
| | Explanation |
| | =========== |
| | |
| | If a base for a group `G` is given by `(b_1, b_2, \dots, b_k)`, this |
| | function returns a base `(b_1, b_2, \dots, b_{i+1}, b_i, \dots, b_k)`, |
| | where `i` is given by ``pos``, and a strong generating set relative |
| | to that base. The original base and strong generating set are not |
| | modified. |
| | |
| | The randomized version (default) is of Las Vegas type. |
| | |
| | Parameters |
| | ========== |
| | |
| | base, strong_gens |
| | The base and strong generating set. |
| | pos |
| | The position at which swapping is performed. |
| | randomized |
| | A switch between randomized and deterministic version. |
| | transversals |
| | The transversals for the basic orbits, if known. |
| | basic_orbits |
| | The basic orbits, if known. |
| | strong_gens_distr |
| | The strong generators distributed by basic stabilizers, if known. |
| | |
| | Returns |
| | ======= |
| | |
| | (base, strong_gens) |
| | ``base`` is the new base, and ``strong_gens`` is a generating set |
| | relative to it. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import SymmetricGroup |
| | >>> from sympy.combinatorics.testutil import _verify_bsgs |
| | >>> from sympy.combinatorics.perm_groups import PermutationGroup |
| | >>> S = SymmetricGroup(4) |
| | >>> S.schreier_sims() |
| | >>> S.base |
| | [0, 1, 2] |
| | >>> base, gens = S.baseswap(S.base, S.strong_gens, 1, randomized=False) |
| | >>> base, gens |
| | ([0, 2, 1], |
| | [(0 1 2 3), (3)(0 1), (1 3 2), |
| | (2 3), (1 3)]) |
| | |
| | check that base, gens is a BSGS |
| | |
| | >>> S1 = PermutationGroup(gens) |
| | >>> _verify_bsgs(S1, base, gens) |
| | True |
| | |
| | See Also |
| | ======== |
| | |
| | schreier_sims |
| | |
| | Notes |
| | ===== |
| | |
| | The deterministic version of the algorithm is discussed in |
| | [1], pp. 102-103; the randomized version is discussed in [1], p.103, and |
| | [2], p.98. It is of Las Vegas type. |
| | Notice that [1] contains a mistake in the pseudocode and |
| | discussion of BASESWAP: on line 3 of the pseudocode, |
| | `|\beta_{i+1}^{\left\langle T\right\rangle}|` should be replaced by |
| | `|\beta_{i}^{\left\langle T\right\rangle}|`, and the same for the |
| | discussion of the algorithm. |
| | |
| | """ |
| | |
| | |
| | transversals, basic_orbits, strong_gens_distr = \ |
| | _handle_precomputed_bsgs(base, strong_gens, transversals, |
| | basic_orbits, strong_gens_distr) |
| | base_len = len(base) |
| | degree = self.degree |
| | |
| | |
| | size = len(basic_orbits[pos])*len(basic_orbits[pos + 1]) \ |
| | //len(_orbit(degree, strong_gens_distr[pos], base[pos + 1])) |
| | |
| | if pos + 2 > base_len - 1: |
| | T = [] |
| | else: |
| | T = strong_gens_distr[pos + 2][:] |
| | |
| | if randomized is True: |
| | stab_pos = PermutationGroup(strong_gens_distr[pos]) |
| | schreier_vector = stab_pos.schreier_vector(base[pos + 1]) |
| | |
| | while len(_orbit(degree, T, base[pos])) != size: |
| | new = stab_pos.random_stab(base[pos + 1], |
| | schreier_vector=schreier_vector) |
| | T.append(new) |
| | |
| | else: |
| | Gamma = set(basic_orbits[pos]) |
| | Gamma.remove(base[pos]) |
| | if base[pos + 1] in Gamma: |
| | Gamma.remove(base[pos + 1]) |
| | |
| | |
| | while len(_orbit(degree, T, base[pos])) != size: |
| | gamma = next(iter(Gamma)) |
| | x = transversals[pos][gamma] |
| | temp = x._array_form.index(base[pos + 1]) |
| | if temp not in basic_orbits[pos + 1]: |
| | Gamma = Gamma - _orbit(degree, T, gamma) |
| | else: |
| | y = transversals[pos + 1][temp] |
| | el = rmul(x, y) |
| | if el(base[pos]) not in _orbit(degree, T, base[pos]): |
| | T.append(el) |
| | Gamma = Gamma - _orbit(degree, T, base[pos]) |
| | |
| | strong_gens_new_distr = strong_gens_distr[:] |
| | strong_gens_new_distr[pos + 1] = T |
| | base_new = base[:] |
| | base_new[pos], base_new[pos + 1] = base_new[pos + 1], base_new[pos] |
| | strong_gens_new = _strong_gens_from_distr(strong_gens_new_distr) |
| | for gen in T: |
| | if gen not in strong_gens_new: |
| | strong_gens_new.append(gen) |
| | return base_new, strong_gens_new |
| |
|
| | @property |
| | def basic_orbits(self): |
| | r""" |
| | Return the basic orbits relative to a base and strong generating set. |
| | |
| | Explanation |
| | =========== |
| | |
| | If `(b_1, b_2, \dots, b_k)` is a base for a group `G`, and |
| | `G^{(i)} = G_{b_1, b_2, \dots, b_{i-1}}` is the ``i``-th basic stabilizer |
| | (so that `G^{(1)} = G`), the ``i``-th basic orbit relative to this base |
| | is the orbit of `b_i` under `G^{(i)}`. See [1], pp. 87-89 for more |
| | information. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import SymmetricGroup |
| | >>> S = SymmetricGroup(4) |
| | >>> S.basic_orbits |
| | [[0, 1, 2, 3], [1, 2, 3], [2, 3]] |
| | |
| | See Also |
| | ======== |
| | |
| | base, strong_gens, basic_transversals, basic_stabilizers |
| | |
| | """ |
| | if self._basic_orbits == []: |
| | self.schreier_sims() |
| | return self._basic_orbits |
| |
|
| | @property |
| | def basic_stabilizers(self): |
| | r""" |
| | Return a chain of stabilizers relative to a base and strong generating |
| | set. |
| | |
| | Explanation |
| | =========== |
| | |
| | The ``i``-th basic stabilizer `G^{(i)}` relative to a base |
| | `(b_1, b_2, \dots, b_k)` is `G_{b_1, b_2, \dots, b_{i-1}}`. For more |
| | information, see [1], pp. 87-89. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import AlternatingGroup |
| | >>> A = AlternatingGroup(4) |
| | >>> A.schreier_sims() |
| | >>> A.base |
| | [0, 1] |
| | >>> for g in A.basic_stabilizers: |
| | ... print(g) |
| | ... |
| | PermutationGroup([ |
| | (3)(0 1 2), |
| | (1 2 3)]) |
| | PermutationGroup([ |
| | (1 2 3)]) |
| | |
| | See Also |
| | ======== |
| | |
| | base, strong_gens, basic_orbits, basic_transversals |
| | |
| | """ |
| |
|
| | if self._transversals == []: |
| | self.schreier_sims() |
| | strong_gens = self._strong_gens |
| | base = self._base |
| | if not base: |
| | return [] |
| | strong_gens_distr = _distribute_gens_by_base(base, strong_gens) |
| | basic_stabilizers = [] |
| | for gens in strong_gens_distr: |
| | basic_stabilizers.append(PermutationGroup(gens)) |
| | return basic_stabilizers |
| |
|
| | @property |
| | def basic_transversals(self): |
| | """ |
| | Return basic transversals relative to a base and strong generating set. |
| | |
| | Explanation |
| | =========== |
| | |
| | The basic transversals are transversals of the basic orbits. They |
| | are provided as a list of dictionaries, each dictionary having |
| | keys - the elements of one of the basic orbits, and values - the |
| | corresponding transversal elements. See [1], pp. 87-89 for more |
| | information. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import AlternatingGroup |
| | >>> A = AlternatingGroup(4) |
| | >>> A.basic_transversals |
| | [{0: (3), 1: (3)(0 1 2), 2: (3)(0 2 1), 3: (0 3 1)}, {1: (3), 2: (1 2 3), 3: (1 3 2)}] |
| | |
| | See Also |
| | ======== |
| | |
| | strong_gens, base, basic_orbits, basic_stabilizers |
| | |
| | """ |
| |
|
| | if self._transversals == []: |
| | self.schreier_sims() |
| | return self._transversals |
| |
|
| | def composition_series(self): |
| | r""" |
| | Return the composition series for a group as a list |
| | of permutation groups. |
| | |
| | Explanation |
| | =========== |
| | |
| | The composition series for a group `G` is defined as a |
| | subnormal series `G = H_0 > H_1 > H_2 \ldots` A composition |
| | series is a subnormal series such that each factor group |
| | `H(i+1) / H(i)` is simple. |
| | A subnormal series is a composition series only if it is of |
| | maximum length. |
| | |
| | The algorithm works as follows: |
| | Starting with the derived series the idea is to fill |
| | the gap between `G = der[i]` and `H = der[i+1]` for each |
| | `i` independently. Since, all subgroups of the abelian group |
| | `G/H` are normal so, first step is to take the generators |
| | `g` of `G` and add them to generators of `H` one by one. |
| | |
| | The factor groups formed are not simple in general. Each |
| | group is obtained from the previous one by adding one |
| | generator `g`, if the previous group is denoted by `H` |
| | then the next group `K` is generated by `g` and `H`. |
| | The factor group `K/H` is cyclic and it's order is |
| | `K.order()//G.order()`. The series is then extended between |
| | `K` and `H` by groups generated by powers of `g` and `H`. |
| | The series formed is then prepended to the already existing |
| | series. |
| | |
| | Examples |
| | ======== |
| | >>> from sympy.combinatorics.named_groups import SymmetricGroup |
| | >>> from sympy.combinatorics.named_groups import CyclicGroup |
| | >>> S = SymmetricGroup(12) |
| | >>> G = S.sylow_subgroup(2) |
| | >>> C = G.composition_series() |
| | >>> [H.order() for H in C] |
| | [1024, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1] |
| | >>> G = S.sylow_subgroup(3) |
| | >>> C = G.composition_series() |
| | >>> [H.order() for H in C] |
| | [243, 81, 27, 9, 3, 1] |
| | >>> G = CyclicGroup(12) |
| | >>> C = G.composition_series() |
| | >>> [H.order() for H in C] |
| | [12, 6, 3, 1] |
| | |
| | """ |
| | der = self.derived_series() |
| | if not all(g.is_identity for g in der[-1].generators): |
| | raise NotImplementedError('Group should be solvable') |
| | series = [] |
| |
|
| | for i in range(len(der)-1): |
| | H = der[i+1] |
| | up_seg = [] |
| | for g in der[i].generators: |
| | K = PermutationGroup([g] + H.generators) |
| | order = K.order() // H.order() |
| | down_seg = [] |
| | for p, e in factorint(order).items(): |
| | for _ in range(e): |
| | down_seg.append(PermutationGroup([g] + H.generators)) |
| | g = g**p |
| | up_seg = down_seg + up_seg |
| | H = K |
| | up_seg[0] = der[i] |
| | series.extend(up_seg) |
| | series.append(der[-1]) |
| | return series |
| |
|
| | def coset_transversal(self, H): |
| | """Return a transversal of the right cosets of self by its subgroup H |
| | using the second method described in [1], Subsection 4.6.7 |
| | |
| | """ |
| |
|
| | if not H.is_subgroup(self): |
| | raise ValueError("The argument must be a subgroup") |
| |
|
| | if H.order() == 1: |
| | return self.elements |
| |
|
| | self._schreier_sims(base=H.base) |
| |
|
| | base = self.base |
| | base_ordering = _base_ordering(base, self.degree) |
| | identity = Permutation(self.degree - 1) |
| |
|
| | transversals = self.basic_transversals[:] |
| | |
| | |
| | |
| | for l, t in enumerate(transversals): |
| | transversals[l] = sorted(t.values(), |
| | key = lambda x: base_ordering[base[l]^x]) |
| |
|
| | orbits = H.basic_orbits |
| | h_stabs = H.basic_stabilizers |
| | g_stabs = self.basic_stabilizers |
| |
|
| | indices = [x.order()//y.order() for x, y in zip(g_stabs, h_stabs)] |
| |
|
| | |
| | |
| | |
| | |
| | if len(g_stabs) > len(h_stabs): |
| | T = g_stabs[len(h_stabs)].elements |
| | else: |
| | T = [identity] |
| | l = len(h_stabs)-1 |
| | t_len = len(T) |
| | while l > -1: |
| | T_next = [] |
| | for u in transversals[l]: |
| | if u == identity: |
| | continue |
| | b = base_ordering[base[l]^u] |
| | for t in T: |
| | p = t*u |
| | if all(base_ordering[h^p] >= b for h in orbits[l]): |
| | T_next.append(p) |
| | if t_len + len(T_next) == indices[l]: |
| | break |
| | if t_len + len(T_next) == indices[l]: |
| | break |
| | T += T_next |
| | t_len += len(T_next) |
| | l -= 1 |
| | T.remove(identity) |
| | T = [identity] + T |
| | return T |
| |
|
| | def _coset_representative(self, g, H): |
| | """Return the representative of Hg from the transversal that |
| | would be computed by ``self.coset_transversal(H)``. |
| | |
| | """ |
| | if H.order() == 1: |
| | return g |
| | |
| | if not(self.base[:len(H.base)] == H.base): |
| | self._schreier_sims(base=H.base) |
| | orbits = H.basic_orbits[:] |
| | h_transversals = [list(_.values()) for _ in H.basic_transversals] |
| | transversals = [list(_.values()) for _ in self.basic_transversals] |
| | base = self.base |
| | base_ordering = _base_ordering(base, self.degree) |
| | def step(l, x): |
| | gamma = min(orbits[l], key = lambda y: base_ordering[y^x]) |
| | i = [base[l]^h for h in h_transversals[l]].index(gamma) |
| | x = h_transversals[l][i]*x |
| | if l < len(orbits)-1: |
| | for u in transversals[l]: |
| | if base[l]^u == base[l]^x: |
| | break |
| | x = step(l+1, x*u**-1)*u |
| | return x |
| | return step(0, g) |
| |
|
| | def coset_table(self, H): |
| | """Return the standardised (right) coset table of self in H as |
| | a list of lists. |
| | """ |
| | |
| | |
| | |
| |
|
| | if not H.is_subgroup(self): |
| | raise ValueError("The argument must be a subgroup") |
| | T = self.coset_transversal(H) |
| | n = len(T) |
| |
|
| | A = list(chain.from_iterable((gen, gen**-1) |
| | for gen in self.generators)) |
| |
|
| | table = [] |
| | for i in range(n): |
| | row = [self._coset_representative(T[i]*x, H) for x in A] |
| | row = [T.index(r) for r in row] |
| | table.append(row) |
| |
|
| | |
| | |
| | |
| | A = range(len(A)) |
| | gamma = 1 |
| | for alpha, a in product(range(n), A): |
| | beta = table[alpha][a] |
| | if beta >= gamma: |
| | if beta > gamma: |
| | for x in A: |
| | z = table[gamma][x] |
| | table[gamma][x] = table[beta][x] |
| | table[beta][x] = z |
| | for i in range(n): |
| | if table[i][x] == beta: |
| | table[i][x] = gamma |
| | elif table[i][x] == gamma: |
| | table[i][x] = beta |
| | gamma += 1 |
| | if gamma >= n-1: |
| | return table |
| |
|
| | def center(self): |
| | r""" |
| | Return the center of a permutation group. |
| | |
| | Explanation |
| | =========== |
| | |
| | The center for a group `G` is defined as |
| | `Z(G) = \{z\in G | \forall g\in G, zg = gz \}`, |
| | the set of elements of `G` that commute with all elements of `G`. |
| | It is equal to the centralizer of `G` inside `G`, and is naturally a |
| | subgroup of `G` ([9]). |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> D = DihedralGroup(4) |
| | >>> G = D.center() |
| | >>> G.order() |
| | 2 |
| | |
| | See Also |
| | ======== |
| | |
| | centralizer |
| | |
| | Notes |
| | ===== |
| | |
| | This is a naive implementation that is a straightforward application |
| | of ``.centralizer()`` |
| | |
| | """ |
| | if not self._center: |
| | self._center = self.centralizer(self) |
| | return self._center |
| |
|
| | def centralizer(self, other): |
| | r""" |
| | Return the centralizer of a group/set/element. |
| | |
| | Explanation |
| | =========== |
| | |
| | The centralizer of a set of permutations ``S`` inside |
| | a group ``G`` is the set of elements of ``G`` that commute with all |
| | elements of ``S``:: |
| | |
| | `C_G(S) = \{ g \in G | gs = sg \forall s \in S\}` ([10]) |
| | |
| | Usually, ``S`` is a subset of ``G``, but if ``G`` is a proper subgroup of |
| | the full symmetric group, we allow for ``S`` to have elements outside |
| | ``G``. |
| | |
| | It is naturally a subgroup of ``G``; the centralizer of a permutation |
| | group is equal to the centralizer of any set of generators for that |
| | group, since any element commuting with the generators commutes with |
| | any product of the generators. |
| | |
| | Parameters |
| | ========== |
| | |
| | other |
| | a permutation group/list of permutations/single permutation |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import (SymmetricGroup, |
| | ... CyclicGroup) |
| | >>> S = SymmetricGroup(6) |
| | >>> C = CyclicGroup(6) |
| | >>> H = S.centralizer(C) |
| | >>> H.is_subgroup(C) |
| | True |
| | |
| | See Also |
| | ======== |
| | |
| | subgroup_search |
| | |
| | Notes |
| | ===== |
| | |
| | The implementation is an application of ``.subgroup_search()`` with |
| | tests using a specific base for the group ``G``. |
| | |
| | """ |
| | if hasattr(other, 'generators'): |
| | if other.is_trivial or self.is_trivial: |
| | return self |
| | degree = self.degree |
| | identity = _af_new(list(range(degree))) |
| | orbits = other.orbits() |
| | num_orbits = len(orbits) |
| | orbits.sort(key=lambda x: -len(x)) |
| | long_base = [] |
| | orbit_reps = [None]*num_orbits |
| | orbit_reps_indices = [None]*num_orbits |
| | orbit_descr = [None]*degree |
| | for i in range(num_orbits): |
| | orbit = list(orbits[i]) |
| | orbit_reps[i] = orbit[0] |
| | orbit_reps_indices[i] = len(long_base) |
| | for point in orbit: |
| | orbit_descr[point] = i |
| | long_base = long_base + orbit |
| | base, strong_gens = self.schreier_sims_incremental(base=long_base) |
| | strong_gens_distr = _distribute_gens_by_base(base, strong_gens) |
| | i = 0 |
| | for i in range(len(base)): |
| | if strong_gens_distr[i] == [identity]: |
| | break |
| | base = base[:i] |
| | base_len = i |
| | for j in range(num_orbits): |
| | if base[base_len - 1] in orbits[j]: |
| | break |
| | rel_orbits = orbits[: j + 1] |
| | num_rel_orbits = len(rel_orbits) |
| | transversals = [None]*num_rel_orbits |
| | for j in range(num_rel_orbits): |
| | rep = orbit_reps[j] |
| | transversals[j] = dict( |
| | other.orbit_transversal(rep, pairs=True)) |
| | trivial_test = lambda x: True |
| | tests = [None]*base_len |
| | for l in range(base_len): |
| | if base[l] in orbit_reps: |
| | tests[l] = trivial_test |
| | else: |
| | def test(computed_words, l=l): |
| | g = computed_words[l] |
| | rep_orb_index = orbit_descr[base[l]] |
| | rep = orbit_reps[rep_orb_index] |
| | im = g._array_form[base[l]] |
| | im_rep = g._array_form[rep] |
| | tr_el = transversals[rep_orb_index][base[l]] |
| | |
| | |
| | |
| | |
| | return im == tr_el._array_form[im_rep] |
| | tests[l] = test |
| |
|
| | def prop(g): |
| | return [rmul(g, gen) for gen in other.generators] == \ |
| | [rmul(gen, g) for gen in other.generators] |
| | return self.subgroup_search(prop, base=base, |
| | strong_gens=strong_gens, tests=tests) |
| | elif hasattr(other, '__getitem__'): |
| | gens = list(other) |
| | return self.centralizer(PermutationGroup(gens)) |
| | elif hasattr(other, 'array_form'): |
| | return self.centralizer(PermutationGroup([other])) |
| |
|
| | def commutator(self, G, H): |
| | """ |
| | Return the commutator of two subgroups. |
| | |
| | Explanation |
| | =========== |
| | |
| | For a permutation group ``K`` and subgroups ``G``, ``H``, the |
| | commutator of ``G`` and ``H`` is defined as the group generated |
| | by all the commutators `[g, h] = hgh^{-1}g^{-1}` for ``g`` in ``G`` and |
| | ``h`` in ``H``. It is naturally a subgroup of ``K`` ([1], p.27). |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import (SymmetricGroup, |
| | ... AlternatingGroup) |
| | >>> S = SymmetricGroup(5) |
| | >>> A = AlternatingGroup(5) |
| | >>> G = S.commutator(S, A) |
| | >>> G.is_subgroup(A) |
| | True |
| | |
| | See Also |
| | ======== |
| | |
| | derived_subgroup |
| | |
| | Notes |
| | ===== |
| | |
| | The commutator of two subgroups `H, G` is equal to the normal closure |
| | of the commutators of all the generators, i.e. `hgh^{-1}g^{-1}` for `h` |
| | a generator of `H` and `g` a generator of `G` ([1], p.28) |
| | |
| | """ |
| | ggens = G.generators |
| | hgens = H.generators |
| | commutators = [] |
| | for ggen in ggens: |
| | for hgen in hgens: |
| | commutator = rmul(hgen, ggen, ~hgen, ~ggen) |
| | if commutator not in commutators: |
| | commutators.append(commutator) |
| | res = self.normal_closure(commutators) |
| | return res |
| |
|
| | def coset_factor(self, g, factor_index=False): |
| | """Return ``G``'s (self's) coset factorization of ``g`` |
| | |
| | Explanation |
| | =========== |
| | |
| | If ``g`` is an element of ``G`` then it can be written as the product |
| | of permutations drawn from the Schreier-Sims coset decomposition, |
| | |
| | The permutations returned in ``f`` are those for which |
| | the product gives ``g``: ``g = f[n]*...f[1]*f[0]`` where ``n = len(B)`` |
| | and ``B = G.base``. f[i] is one of the permutations in |
| | ``self._basic_orbits[i]``. |
| | |
| | If factor_index==True, |
| | returns a tuple ``[b[0],..,b[n]]``, where ``b[i]`` |
| | belongs to ``self._basic_orbits[i]`` |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation(0, 1, 3, 7, 6, 4)(2, 5) |
| | >>> b = Permutation(0, 1, 3, 2)(4, 5, 7, 6) |
| | >>> G = PermutationGroup([a, b]) |
| | |
| | Define g: |
| | |
| | >>> g = Permutation(7)(1, 2, 4)(3, 6, 5) |
| | |
| | Confirm that it is an element of G: |
| | |
| | >>> G.contains(g) |
| | True |
| | |
| | Thus, it can be written as a product of factors (up to |
| | 3) drawn from u. See below that a factor from u1 and u2 |
| | and the Identity permutation have been used: |
| | |
| | >>> f = G.coset_factor(g) |
| | >>> f[2]*f[1]*f[0] == g |
| | True |
| | >>> f1 = G.coset_factor(g, True); f1 |
| | [0, 4, 4] |
| | >>> tr = G.basic_transversals |
| | >>> f[0] == tr[0][f1[0]] |
| | True |
| | |
| | If g is not an element of G then [] is returned: |
| | |
| | >>> c = Permutation(5, 6, 7) |
| | >>> G.coset_factor(c) |
| | [] |
| | |
| | See Also |
| | ======== |
| | |
| | sympy.combinatorics.util._strip |
| | |
| | """ |
| | if isinstance(g, (Cycle, Permutation)): |
| | g = g.list() |
| | if len(g) != self._degree: |
| | |
| | |
| | |
| | raise ValueError('g should be the same size as permutations of G') |
| | I = list(range(self._degree)) |
| | basic_orbits = self.basic_orbits |
| | transversals = self._transversals |
| | factors = [] |
| | base = self.base |
| | h = g |
| | for i in range(len(base)): |
| | beta = h[base[i]] |
| | if beta == base[i]: |
| | factors.append(beta) |
| | continue |
| | if beta not in basic_orbits[i]: |
| | return [] |
| | u = transversals[i][beta]._array_form |
| | h = _af_rmul(_af_invert(u), h) |
| | factors.append(beta) |
| | if h != I: |
| | return [] |
| | if factor_index: |
| | return factors |
| | tr = self.basic_transversals |
| | factors = [tr[i][factors[i]] for i in range(len(base))] |
| | return factors |
| |
|
| | def generator_product(self, g, original=False): |
| | r''' |
| | Return a list of strong generators `[s1, \dots, sn]` |
| | s.t `g = sn \times \dots \times s1`. If ``original=True``, make the |
| | list contain only the original group generators |
| | |
| | ''' |
| | product = [] |
| | if g.is_identity: |
| | return [] |
| | if g in self.strong_gens: |
| | if not original or g in self.generators: |
| | return [g] |
| | else: |
| | slp = self._strong_gens_slp[g] |
| | for s in slp: |
| | product.extend(self.generator_product(s, original=True)) |
| | return product |
| | elif g**-1 in self.strong_gens: |
| | g = g**-1 |
| | if not original or g in self.generators: |
| | return [g**-1] |
| | else: |
| | slp = self._strong_gens_slp[g] |
| | for s in slp: |
| | product.extend(self.generator_product(s, original=True)) |
| | l = len(product) |
| | product = [product[l-i-1]**-1 for i in range(l)] |
| | return product |
| |
|
| | f = self.coset_factor(g, True) |
| | for i, j in enumerate(f): |
| | slp = self._transversal_slp[i][j] |
| | for s in slp: |
| | if not original: |
| | product.append(self.strong_gens[s]) |
| | else: |
| | s = self.strong_gens[s] |
| | product.extend(self.generator_product(s, original=True)) |
| | return product |
| |
|
| | def coset_rank(self, g): |
| | """rank using Schreier-Sims representation. |
| | |
| | Explanation |
| | =========== |
| | |
| | The coset rank of ``g`` is the ordering number in which |
| | it appears in the lexicographic listing according to the |
| | coset decomposition |
| | |
| | The ordering is the same as in G.generate(method='coset'). |
| | If ``g`` does not belong to the group it returns None. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation(0, 1, 3, 7, 6, 4)(2, 5) |
| | >>> b = Permutation(0, 1, 3, 2)(4, 5, 7, 6) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> c = Permutation(7)(2, 4)(3, 5) |
| | >>> G.coset_rank(c) |
| | 16 |
| | >>> G.coset_unrank(16) |
| | (7)(2 4)(3 5) |
| | |
| | See Also |
| | ======== |
| | |
| | coset_factor |
| | |
| | """ |
| | factors = self.coset_factor(g, True) |
| | if not factors: |
| | return None |
| | rank = 0 |
| | b = 1 |
| | transversals = self._transversals |
| | base = self._base |
| | basic_orbits = self._basic_orbits |
| | for i in range(len(base)): |
| | k = factors[i] |
| | j = basic_orbits[i].index(k) |
| | rank += b*j |
| | b = b*len(transversals[i]) |
| | return rank |
| |
|
| | def coset_unrank(self, rank, af=False): |
| | """unrank using Schreier-Sims representation |
| | |
| | coset_unrank is the inverse operation of coset_rank |
| | if 0 <= rank < order; otherwise it returns None. |
| | |
| | """ |
| | if rank < 0 or rank >= self.order(): |
| | return None |
| | base = self.base |
| | transversals = self.basic_transversals |
| | basic_orbits = self.basic_orbits |
| | m = len(base) |
| | v = [0]*m |
| | for i in range(m): |
| | rank, c = divmod(rank, len(transversals[i])) |
| | v[i] = basic_orbits[i][c] |
| | a = [transversals[i][v[i]]._array_form for i in range(m)] |
| | h = _af_rmuln(*a) |
| | if af: |
| | return h |
| | else: |
| | return _af_new(h) |
| |
|
| | @property |
| | def degree(self): |
| | """Returns the size of the permutations in the group. |
| | |
| | Explanation |
| | =========== |
| | |
| | The number of permutations comprising the group is given by |
| | ``len(group)``; the number of permutations that can be generated |
| | by the group is given by ``group.order()``. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([1, 0, 2]) |
| | >>> G = PermutationGroup([a]) |
| | >>> G.degree |
| | 3 |
| | >>> len(G) |
| | 1 |
| | >>> G.order() |
| | 2 |
| | >>> list(G.generate()) |
| | [(2), (2)(0 1)] |
| | |
| | See Also |
| | ======== |
| | |
| | order |
| | """ |
| | return self._degree |
| |
|
| | @property |
| | def identity(self): |
| | ''' |
| | Return the identity element of the permutation group. |
| | |
| | ''' |
| | return _af_new(list(range(self.degree))) |
| |
|
| | @property |
| | def elements(self): |
| | """Returns all the elements of the permutation group as a list |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> p = PermutationGroup(Permutation(1, 3), Permutation(1, 2)) |
| | >>> p.elements |
| | [(3), (3)(1 2), (1 3), (2 3), (1 2 3), (1 3 2)] |
| | |
| | """ |
| | if not self._elements: |
| | self._elements = list(self.generate()) |
| |
|
| | return self._elements |
| |
|
| | def derived_series(self): |
| | r"""Return the derived series for the group. |
| | |
| | Explanation |
| | =========== |
| | |
| | The derived series for a group `G` is defined as |
| | `G = G_0 > G_1 > G_2 > \ldots` where `G_i = [G_{i-1}, G_{i-1}]`, |
| | i.e. `G_i` is the derived subgroup of `G_{i-1}`, for |
| | `i\in\mathbb{N}`. When we have `G_k = G_{k-1}` for some |
| | `k\in\mathbb{N}`, the series terminates. |
| | |
| | Returns |
| | ======= |
| | |
| | A list of permutation groups containing the members of the derived |
| | series in the order `G = G_0, G_1, G_2, \ldots`. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import (SymmetricGroup, |
| | ... AlternatingGroup, DihedralGroup) |
| | >>> A = AlternatingGroup(5) |
| | >>> len(A.derived_series()) |
| | 1 |
| | >>> S = SymmetricGroup(4) |
| | >>> len(S.derived_series()) |
| | 4 |
| | >>> S.derived_series()[1].is_subgroup(AlternatingGroup(4)) |
| | True |
| | >>> S.derived_series()[2].is_subgroup(DihedralGroup(2)) |
| | True |
| | |
| | See Also |
| | ======== |
| | |
| | derived_subgroup |
| | |
| | """ |
| | res = [self] |
| | current = self |
| | nxt = self.derived_subgroup() |
| | while not current.is_subgroup(nxt): |
| | res.append(nxt) |
| | current = nxt |
| | nxt = nxt.derived_subgroup() |
| | return res |
| |
|
| | def derived_subgroup(self): |
| | r"""Compute the derived subgroup. |
| | |
| | Explanation |
| | =========== |
| | |
| | The derived subgroup, or commutator subgroup is the subgroup generated |
| | by all commutators `[g, h] = hgh^{-1}g^{-1}` for `g, h\in G` ; it is |
| | equal to the normal closure of the set of commutators of the generators |
| | ([1], p.28, [11]). |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([1, 0, 2, 4, 3]) |
| | >>> b = Permutation([0, 1, 3, 2, 4]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> C = G.derived_subgroup() |
| | >>> list(C.generate(af=True)) |
| | [[0, 1, 2, 3, 4], [0, 1, 3, 4, 2], [0, 1, 4, 2, 3]] |
| | |
| | See Also |
| | ======== |
| | |
| | derived_series |
| | |
| | """ |
| | r = self._r |
| | gens = [p._array_form for p in self.generators] |
| | set_commutators = set() |
| | degree = self._degree |
| | rng = list(range(degree)) |
| | for i in range(r): |
| | for j in range(r): |
| | p1 = gens[i] |
| | p2 = gens[j] |
| | c = list(range(degree)) |
| | for k in rng: |
| | c[p2[p1[k]]] = p1[p2[k]] |
| | ct = tuple(c) |
| | if ct not in set_commutators: |
| | set_commutators.add(ct) |
| | cms = [_af_new(p) for p in set_commutators] |
| | G2 = self.normal_closure(cms) |
| | return G2 |
| |
|
| | def generate(self, method="coset", af=False): |
| | """Return iterator to generate the elements of the group. |
| | |
| | Explanation |
| | =========== |
| | |
| | Iteration is done with one of these methods:: |
| | |
| | method='coset' using the Schreier-Sims coset representation |
| | method='dimino' using the Dimino method |
| | |
| | If ``af = True`` it yields the array form of the permutations |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import PermutationGroup |
| | >>> from sympy.combinatorics.polyhedron import tetrahedron |
| | |
| | The permutation group given in the tetrahedron object is also |
| | true groups: |
| | |
| | >>> G = tetrahedron.pgroup |
| | >>> G.is_group |
| | True |
| | |
| | Also the group generated by the permutations in the tetrahedron |
| | pgroup -- even the first two -- is a proper group: |
| | |
| | >>> H = PermutationGroup(G[0], G[1]) |
| | >>> J = PermutationGroup(list(H.generate())); J |
| | PermutationGroup([ |
| | (0 1)(2 3), |
| | (1 2 3), |
| | (1 3 2), |
| | (0 3 1), |
| | (0 2 3), |
| | (0 3)(1 2), |
| | (0 1 3), |
| | (3)(0 2 1), |
| | (0 3 2), |
| | (3)(0 1 2), |
| | (0 2)(1 3)]) |
| | >>> _.is_group |
| | True |
| | """ |
| | if method == "coset": |
| | return self.generate_schreier_sims(af) |
| | elif method == "dimino": |
| | return self.generate_dimino(af) |
| | else: |
| | raise NotImplementedError('No generation defined for %s' % method) |
| |
|
| | def generate_dimino(self, af=False): |
| | """Yield group elements using Dimino's algorithm. |
| | |
| | If ``af == True`` it yields the array form of the permutations. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([0, 2, 1, 3]) |
| | >>> b = Permutation([0, 2, 3, 1]) |
| | >>> g = PermutationGroup([a, b]) |
| | >>> list(g.generate_dimino(af=True)) |
| | [[0, 1, 2, 3], [0, 2, 1, 3], [0, 2, 3, 1], |
| | [0, 1, 3, 2], [0, 3, 2, 1], [0, 3, 1, 2]] |
| | |
| | References |
| | ========== |
| | |
| | .. [1] The Implementation of Various Algorithms for Permutation Groups in |
| | the Computer Algebra System: AXIOM, N.J. Doye, M.Sc. Thesis |
| | |
| | """ |
| | idn = list(range(self.degree)) |
| | order = 0 |
| | element_list = [idn] |
| | set_element_list = {tuple(idn)} |
| | if af: |
| | yield idn |
| | else: |
| | yield _af_new(idn) |
| | gens = [p._array_form for p in self.generators] |
| |
|
| | for i in range(len(gens)): |
| | |
| | D = element_list.copy() |
| | N = [idn] |
| | while N: |
| | A = N |
| | N = [] |
| | for a in A: |
| | for g in gens[:i + 1]: |
| | ag = _af_rmul(a, g) |
| | if tuple(ag) not in set_element_list: |
| | |
| | for d in D: |
| | order += 1 |
| | ap = _af_rmul(d, ag) |
| | if af: |
| | yield ap |
| | else: |
| | p = _af_new(ap) |
| | yield p |
| | element_list.append(ap) |
| | set_element_list.add(tuple(ap)) |
| | N.append(ap) |
| | self._order = len(element_list) |
| |
|
| | def generate_schreier_sims(self, af=False): |
| | """Yield group elements using the Schreier-Sims representation |
| | in coset_rank order |
| | |
| | If ``af = True`` it yields the array form of the permutations |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([0, 2, 1, 3]) |
| | >>> b = Permutation([0, 2, 3, 1]) |
| | >>> g = PermutationGroup([a, b]) |
| | >>> list(g.generate_schreier_sims(af=True)) |
| | [[0, 1, 2, 3], [0, 2, 1, 3], [0, 3, 2, 1], |
| | [0, 1, 3, 2], [0, 2, 3, 1], [0, 3, 1, 2]] |
| | """ |
| |
|
| | n = self._degree |
| | u = self.basic_transversals |
| | basic_orbits = self._basic_orbits |
| | if len(u) == 0: |
| | for x in self.generators: |
| | if af: |
| | yield x._array_form |
| | else: |
| | yield x |
| | return |
| | if len(u) == 1: |
| | for i in basic_orbits[0]: |
| | if af: |
| | yield u[0][i]._array_form |
| | else: |
| | yield u[0][i] |
| | return |
| |
|
| | u = list(reversed(u)) |
| | basic_orbits = basic_orbits[::-1] |
| | |
| | stg = [list(range(n))] |
| | posmax = [len(x) for x in u] |
| | n1 = len(posmax) - 1 |
| | pos = [0]*n1 |
| | h = 0 |
| | while 1: |
| | |
| | if pos[h] >= posmax[h]: |
| | if h == 0: |
| | return |
| | pos[h] = 0 |
| | h -= 1 |
| | stg.pop() |
| | continue |
| | p = _af_rmul(u[h][basic_orbits[h][pos[h]]]._array_form, stg[-1]) |
| | pos[h] += 1 |
| | stg.append(p) |
| | h += 1 |
| | if h == n1: |
| | if af: |
| | for i in basic_orbits[-1]: |
| | p = _af_rmul(u[-1][i]._array_form, stg[-1]) |
| | yield p |
| | else: |
| | for i in basic_orbits[-1]: |
| | p = _af_rmul(u[-1][i]._array_form, stg[-1]) |
| | p1 = _af_new(p) |
| | yield p1 |
| | stg.pop() |
| | h -= 1 |
| |
|
| | @property |
| | def generators(self): |
| | """Returns the generators of the group. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([0, 2, 1]) |
| | >>> b = Permutation([1, 0, 2]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.generators |
| | [(1 2), (2)(0 1)] |
| | |
| | """ |
| | return self._generators |
| |
|
| | def contains(self, g, strict=True): |
| | """Test if permutation ``g`` belong to self, ``G``. |
| | |
| | Explanation |
| | =========== |
| | |
| | If ``g`` is an element of ``G`` it can be written as a product |
| | of factors drawn from the cosets of ``G``'s stabilizers. To see |
| | if ``g`` is one of the actual generators defining the group use |
| | ``G.has(g)``. |
| | |
| | If ``strict`` is not ``True``, ``g`` will be resized, if necessary, |
| | to match the size of permutations in ``self``. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | |
| | >>> a = Permutation(1, 2) |
| | >>> b = Permutation(2, 3, 1) |
| | >>> G = PermutationGroup(a, b, degree=5) |
| | >>> G.contains(G[0]) # trivial check |
| | True |
| | >>> elem = Permutation([[2, 3]], size=5) |
| | >>> G.contains(elem) |
| | True |
| | >>> G.contains(Permutation(4)(0, 1, 2, 3)) |
| | False |
| | |
| | If strict is False, a permutation will be resized, if |
| | necessary: |
| | |
| | >>> H = PermutationGroup(Permutation(5)) |
| | >>> H.contains(Permutation(3)) |
| | False |
| | >>> H.contains(Permutation(3), strict=False) |
| | True |
| | |
| | To test if a given permutation is present in the group: |
| | |
| | >>> elem in G.generators |
| | False |
| | >>> G.has(elem) |
| | False |
| | |
| | See Also |
| | ======== |
| | |
| | coset_factor, sympy.core.basic.Basic.has, __contains__ |
| | |
| | """ |
| | if not isinstance(g, Permutation): |
| | return False |
| | if g.size != self.degree: |
| | if strict: |
| | return False |
| | g = Permutation(g, size=self.degree) |
| | if g in self.generators: |
| | return True |
| | return bool(self.coset_factor(g.array_form, True)) |
| |
|
| | @property |
| | def is_perfect(self): |
| | """Return ``True`` if the group is perfect. |
| | A group is perfect if it equals to its derived subgroup. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation(1,2,3)(4,5) |
| | >>> b = Permutation(1,2,3,4,5) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.is_perfect |
| | False |
| | |
| | """ |
| | if self._is_perfect is None: |
| | self._is_perfect = self.equals(self.derived_subgroup()) |
| | return self._is_perfect |
| |
|
| | @property |
| | def is_abelian(self): |
| | """Test if the group is Abelian. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([0, 2, 1]) |
| | >>> b = Permutation([1, 0, 2]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.is_abelian |
| | False |
| | >>> a = Permutation([0, 2, 1]) |
| | >>> G = PermutationGroup([a]) |
| | >>> G.is_abelian |
| | True |
| | |
| | """ |
| | if self._is_abelian is not None: |
| | return self._is_abelian |
| |
|
| | self._is_abelian = True |
| | gens = [p._array_form for p in self.generators] |
| | for x in gens: |
| | for y in gens: |
| | if y <= x: |
| | continue |
| | if not _af_commutes_with(x, y): |
| | self._is_abelian = False |
| | return False |
| | return True |
| |
|
| | def abelian_invariants(self): |
| | """ |
| | Returns the abelian invariants for the given group. |
| | Let ``G`` be a nontrivial finite abelian group. Then G is isomorphic to |
| | the direct product of finitely many nontrivial cyclic groups of |
| | prime-power order. |
| | |
| | Explanation |
| | =========== |
| | |
| | The prime-powers that occur as the orders of the factors are uniquely |
| | determined by G. More precisely, the primes that occur in the orders of the |
| | factors in any such decomposition of ``G`` are exactly the primes that divide |
| | ``|G|`` and for any such prime ``p``, if the orders of the factors that are |
| | p-groups in one such decomposition of ``G`` are ``p^{t_1} >= p^{t_2} >= ... p^{t_r}``, |
| | then the orders of the factors that are p-groups in any such decomposition of ``G`` |
| | are ``p^{t_1} >= p^{t_2} >= ... p^{t_r}``. |
| | |
| | The uniquely determined integers ``p^{t_1} >= p^{t_2} >= ... p^{t_r}``, taken |
| | for all primes that divide ``|G|`` are called the invariants of the nontrivial |
| | group ``G`` as suggested in ([14], p. 542). |
| | |
| | Notes |
| | ===== |
| | |
| | We adopt the convention that the invariants of a trivial group are []. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([0, 2, 1]) |
| | >>> b = Permutation([1, 0, 2]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.abelian_invariants() |
| | [2] |
| | >>> from sympy.combinatorics import CyclicGroup |
| | >>> G = CyclicGroup(7) |
| | >>> G.abelian_invariants() |
| | [7] |
| | |
| | """ |
| | if self.is_trivial: |
| | return [] |
| | gns = self.generators |
| | inv = [] |
| | G = self |
| | H = G.derived_subgroup() |
| | Hgens = H.generators |
| | for p in primefactors(G.order()): |
| | ranks = [] |
| | while True: |
| | pows = [] |
| | for g in gns: |
| | elm = g**p |
| | if not H.contains(elm): |
| | pows.append(elm) |
| | K = PermutationGroup(Hgens + pows) if pows else H |
| | r = G.order()//K.order() |
| | G = K |
| | gns = pows |
| | if r == 1: |
| | break |
| | ranks.append(multiplicity(p, r)) |
| |
|
| | if ranks: |
| | pows = [1]*ranks[0] |
| | for i in ranks: |
| | for j in range(i): |
| | pows[j] = pows[j]*p |
| | inv.extend(pows) |
| | inv.sort() |
| | return inv |
| |
|
| | def is_elementary(self, p): |
| | """Return ``True`` if the group is elementary abelian. An elementary |
| | abelian group is a finite abelian group, where every nontrivial |
| | element has order `p`, where `p` is a prime. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([0, 2, 1]) |
| | >>> G = PermutationGroup([a]) |
| | >>> G.is_elementary(2) |
| | True |
| | >>> a = Permutation([0, 2, 1, 3]) |
| | >>> b = Permutation([3, 1, 2, 0]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.is_elementary(2) |
| | True |
| | >>> G.is_elementary(3) |
| | False |
| | |
| | """ |
| | return self.is_abelian and all(g.order() == p for g in self.generators) |
| |
|
| | def _eval_is_alt_sym_naive(self, only_sym=False, only_alt=False): |
| | """A naive test using the group order.""" |
| | if only_sym and only_alt: |
| | raise ValueError( |
| | "Both {} and {} cannot be set to True" |
| | .format(only_sym, only_alt)) |
| |
|
| | n = self.degree |
| | sym_order = _factorial(n) |
| | order = self.order() |
| |
|
| | if order == sym_order: |
| | self._is_sym = True |
| | self._is_alt = False |
| | return not only_alt |
| |
|
| | if 2*order == sym_order: |
| | self._is_sym = False |
| | self._is_alt = True |
| | return not only_sym |
| |
|
| | return False |
| |
|
| | def _eval_is_alt_sym_monte_carlo(self, eps=0.05, perms=None): |
| | """A test using monte-carlo algorithm. |
| | |
| | Parameters |
| | ========== |
| | |
| | eps : float, optional |
| | The criterion for the incorrect ``False`` return. |
| | |
| | perms : list[Permutation], optional |
| | If explicitly given, it tests over the given candidates |
| | for testing. |
| | |
| | If ``None``, it randomly computes ``N_eps`` and chooses |
| | ``N_eps`` sample of the permutation from the group. |
| | |
| | See Also |
| | ======== |
| | |
| | _check_cycles_alt_sym |
| | """ |
| | if perms is None: |
| | n = self.degree |
| | if n < 17: |
| | c_n = 0.34 |
| | else: |
| | c_n = 0.57 |
| | d_n = (c_n*log(2))/log(n) |
| | N_eps = int(-log(eps)/d_n) |
| |
|
| | perms = (self.random_pr() for i in range(N_eps)) |
| | return self._eval_is_alt_sym_monte_carlo(perms=perms) |
| |
|
| | for perm in perms: |
| | if _check_cycles_alt_sym(perm): |
| | return True |
| | return False |
| |
|
| | def is_alt_sym(self, eps=0.05, _random_prec=None): |
| | r"""Monte Carlo test for the symmetric/alternating group for degrees |
| | >= 8. |
| | |
| | Explanation |
| | =========== |
| | |
| | More specifically, it is one-sided Monte Carlo with the |
| | answer True (i.e., G is symmetric/alternating) guaranteed to be |
| | correct, and the answer False being incorrect with probability eps. |
| | |
| | For degree < 8, the order of the group is checked so the test |
| | is deterministic. |
| | |
| | Notes |
| | ===== |
| | |
| | The algorithm itself uses some nontrivial results from group theory and |
| | number theory: |
| | 1) If a transitive group ``G`` of degree ``n`` contains an element |
| | with a cycle of length ``n/2 < p < n-2`` for ``p`` a prime, ``G`` is the |
| | symmetric or alternating group ([1], pp. 81-82) |
| | 2) The proportion of elements in the symmetric/alternating group having |
| | the property described in 1) is approximately `\log(2)/\log(n)` |
| | ([1], p.82; [2], pp. 226-227). |
| | The helper function ``_check_cycles_alt_sym`` is used to |
| | go over the cycles in a permutation and look for ones satisfying 1). |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> D = DihedralGroup(10) |
| | >>> D.is_alt_sym() |
| | False |
| | |
| | See Also |
| | ======== |
| | |
| | _check_cycles_alt_sym |
| | |
| | """ |
| | if _random_prec is not None: |
| | N_eps = _random_prec['N_eps'] |
| | perms= (_random_prec[i] for i in range(N_eps)) |
| | return self._eval_is_alt_sym_monte_carlo(perms=perms) |
| |
|
| | if self._is_sym or self._is_alt: |
| | return True |
| | if self._is_sym is False and self._is_alt is False: |
| | return False |
| |
|
| | n = self.degree |
| | if n < 8: |
| | return self._eval_is_alt_sym_naive() |
| | elif self.is_transitive(): |
| | return self._eval_is_alt_sym_monte_carlo(eps=eps) |
| |
|
| | self._is_sym, self._is_alt = False, False |
| | return False |
| |
|
| | @property |
| | def is_nilpotent(self): |
| | """Test if the group is nilpotent. |
| | |
| | Explanation |
| | =========== |
| | |
| | A group `G` is nilpotent if it has a central series of finite length. |
| | Alternatively, `G` is nilpotent if its lower central series terminates |
| | with the trivial group. Every nilpotent group is also solvable |
| | ([1], p.29, [12]). |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import (SymmetricGroup, |
| | ... CyclicGroup) |
| | >>> C = CyclicGroup(6) |
| | >>> C.is_nilpotent |
| | True |
| | >>> S = SymmetricGroup(5) |
| | >>> S.is_nilpotent |
| | False |
| | |
| | See Also |
| | ======== |
| | |
| | lower_central_series, is_solvable |
| | |
| | """ |
| | if self._is_nilpotent is None: |
| | lcs = self.lower_central_series() |
| | terminator = lcs[len(lcs) - 1] |
| | gens = terminator.generators |
| | degree = self.degree |
| | identity = _af_new(list(range(degree))) |
| | if all(g == identity for g in gens): |
| | self._is_solvable = True |
| | self._is_nilpotent = True |
| | return True |
| | else: |
| | self._is_nilpotent = False |
| | return False |
| | else: |
| | return self._is_nilpotent |
| |
|
| | def is_normal(self, gr, strict=True): |
| | """Test if ``G=self`` is a normal subgroup of ``gr``. |
| | |
| | Explanation |
| | =========== |
| | |
| | G is normal in gr if |
| | for each g2 in G, g1 in gr, ``g = g1*g2*g1**-1`` belongs to G |
| | It is sufficient to check this for each g1 in gr.generators and |
| | g2 in G.generators. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([1, 2, 0]) |
| | >>> b = Permutation([1, 0, 2]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G1 = PermutationGroup([a, Permutation([2, 0, 1])]) |
| | >>> G1.is_normal(G) |
| | True |
| | |
| | """ |
| | if not self.is_subgroup(gr, strict=strict): |
| | return False |
| | d_self = self.degree |
| | d_gr = gr.degree |
| | if self.is_trivial and (d_self == d_gr or not strict): |
| | return True |
| | if self._is_abelian: |
| | return True |
| | new_self = self.copy() |
| | if not strict and d_self != d_gr: |
| | if d_self < d_gr: |
| | new_self = PermGroup(new_self.generators + [Permutation(d_gr - 1)]) |
| | else: |
| | gr = PermGroup(gr.generators + [Permutation(d_self - 1)]) |
| | gens2 = [p._array_form for p in new_self.generators] |
| | gens1 = [p._array_form for p in gr.generators] |
| | for g1 in gens1: |
| | for g2 in gens2: |
| | p = _af_rmuln(g1, g2, _af_invert(g1)) |
| | if not new_self.coset_factor(p, True): |
| | return False |
| | return True |
| |
|
| | def is_primitive(self, randomized=True): |
| | r"""Test if a group is primitive. |
| | |
| | Explanation |
| | =========== |
| | |
| | A permutation group ``G`` acting on a set ``S`` is called primitive if |
| | ``S`` contains no nontrivial block under the action of ``G`` |
| | (a block is nontrivial if its cardinality is more than ``1``). |
| | |
| | Notes |
| | ===== |
| | |
| | The algorithm is described in [1], p.83, and uses the function |
| | minimal_block to search for blocks of the form `\{0, k\}` for ``k`` |
| | ranging over representatives for the orbits of `G_0`, the stabilizer of |
| | ``0``. This algorithm has complexity `O(n^2)` where ``n`` is the degree |
| | of the group, and will perform badly if `G_0` is small. |
| | |
| | There are two implementations offered: one finds `G_0` |
| | deterministically using the function ``stabilizer``, and the other |
| | (default) produces random elements of `G_0` using ``random_stab``, |
| | hoping that they generate a subgroup of `G_0` with not too many more |
| | orbits than `G_0` (this is suggested in [1], p.83). Behavior is changed |
| | by the ``randomized`` flag. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> D = DihedralGroup(10) |
| | >>> D.is_primitive() |
| | False |
| | |
| | See Also |
| | ======== |
| | |
| | minimal_block, random_stab |
| | |
| | """ |
| | if self._is_primitive is not None: |
| | return self._is_primitive |
| |
|
| | if self.is_transitive() is False: |
| | return False |
| |
|
| | if randomized: |
| | random_stab_gens = [] |
| | v = self.schreier_vector(0) |
| | for _ in range(len(self)): |
| | random_stab_gens.append(self.random_stab(0, v)) |
| | stab = PermutationGroup(random_stab_gens) |
| | else: |
| | stab = self.stabilizer(0) |
| | orbits = stab.orbits() |
| | for orb in orbits: |
| | x = orb.pop() |
| | if x != 0 and any(e != 0 for e in self.minimal_block([0, x])): |
| | self._is_primitive = False |
| | return False |
| | self._is_primitive = True |
| | return True |
| |
|
| | def minimal_blocks(self, randomized=True): |
| | ''' |
| | For a transitive group, return the list of all minimal |
| | block systems. If a group is intransitive, return `False`. |
| | |
| | Examples |
| | ======== |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> DihedralGroup(6).minimal_blocks() |
| | [[0, 1, 0, 1, 0, 1], [0, 1, 2, 0, 1, 2]] |
| | >>> G = PermutationGroup(Permutation(1,2,5)) |
| | >>> G.minimal_blocks() |
| | False |
| | |
| | See Also |
| | ======== |
| | |
| | minimal_block, is_transitive, is_primitive |
| | |
| | ''' |
| | def _number_blocks(blocks): |
| | |
| | |
| | |
| | |
| | n = len(blocks) |
| | appeared = {} |
| | m = 0 |
| | b = [None]*n |
| | for i in range(n): |
| | if blocks[i] not in appeared: |
| | appeared[blocks[i]] = m |
| | b[i] = m |
| | m += 1 |
| | else: |
| | b[i] = appeared[blocks[i]] |
| | return tuple(b), m |
| |
|
| | if not self.is_transitive(): |
| | return False |
| | blocks = [] |
| | num_blocks = [] |
| | rep_blocks = [] |
| | if randomized: |
| | random_stab_gens = [] |
| | v = self.schreier_vector(0) |
| | for i in range(len(self)): |
| | random_stab_gens.append(self.random_stab(0, v)) |
| | stab = PermutationGroup(random_stab_gens) |
| | else: |
| | stab = self.stabilizer(0) |
| | orbits = stab.orbits() |
| | for orb in orbits: |
| | x = orb.pop() |
| | if x != 0: |
| | block = self.minimal_block([0, x]) |
| | num_block, _ = _number_blocks(block) |
| | |
| | rep = {j for j in range(self.degree) if num_block[j] == 0} |
| | |
| | |
| | minimal = True |
| | blocks_remove_mask = [False] * len(blocks) |
| | for i, r in enumerate(rep_blocks): |
| | if len(r) > len(rep) and rep.issubset(r): |
| | |
| | blocks_remove_mask[i] = True |
| | elif len(r) < len(rep) and r.issubset(rep): |
| | |
| | minimal = False |
| | break |
| | |
| | blocks = [b for i, b in enumerate(blocks) if not blocks_remove_mask[i]] |
| | num_blocks = [n for i, n in enumerate(num_blocks) if not blocks_remove_mask[i]] |
| | rep_blocks = [r for i, r in enumerate(rep_blocks) if not blocks_remove_mask[i]] |
| |
|
| | if minimal and num_block not in num_blocks: |
| | blocks.append(block) |
| | num_blocks.append(num_block) |
| | rep_blocks.append(rep) |
| | return blocks |
| |
|
| | @property |
| | def is_solvable(self): |
| | """Test if the group is solvable. |
| | |
| | ``G`` is solvable if its derived series terminates with the trivial |
| | group ([1], p.29). |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import SymmetricGroup |
| | >>> S = SymmetricGroup(3) |
| | >>> S.is_solvable |
| | True |
| | |
| | See Also |
| | ======== |
| | |
| | is_nilpotent, derived_series |
| | |
| | """ |
| | if self._is_solvable is None: |
| | if self.order() % 2 != 0: |
| | return True |
| | ds = self.derived_series() |
| | terminator = ds[len(ds) - 1] |
| | gens = terminator.generators |
| | degree = self.degree |
| | identity = _af_new(list(range(degree))) |
| | if all(g == identity for g in gens): |
| | self._is_solvable = True |
| | return True |
| | else: |
| | self._is_solvable = False |
| | return False |
| | else: |
| | return self._is_solvable |
| |
|
| | def is_subgroup(self, G, strict=True): |
| | """Return ``True`` if all elements of ``self`` belong to ``G``. |
| | |
| | If ``strict`` is ``False`` then if ``self``'s degree is smaller |
| | than ``G``'s, the elements will be resized to have the same degree. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> from sympy.combinatorics import SymmetricGroup, CyclicGroup |
| | |
| | Testing is strict by default: the degree of each group must be the |
| | same: |
| | |
| | >>> p = Permutation(0, 1, 2, 3, 4, 5) |
| | >>> G1 = PermutationGroup([Permutation(0, 1, 2), Permutation(0, 1)]) |
| | >>> G2 = PermutationGroup([Permutation(0, 2), Permutation(0, 1, 2)]) |
| | >>> G3 = PermutationGroup([p, p**2]) |
| | >>> assert G1.order() == G2.order() == G3.order() == 6 |
| | >>> G1.is_subgroup(G2) |
| | True |
| | >>> G1.is_subgroup(G3) |
| | False |
| | >>> G3.is_subgroup(PermutationGroup(G3[1])) |
| | False |
| | >>> G3.is_subgroup(PermutationGroup(G3[0])) |
| | True |
| | |
| | To ignore the size, set ``strict`` to ``False``: |
| | |
| | >>> S3 = SymmetricGroup(3) |
| | >>> S5 = SymmetricGroup(5) |
| | >>> S3.is_subgroup(S5, strict=False) |
| | True |
| | >>> C7 = CyclicGroup(7) |
| | >>> G = S5*C7 |
| | >>> S5.is_subgroup(G, False) |
| | True |
| | >>> C7.is_subgroup(G, 0) |
| | False |
| | |
| | """ |
| | if isinstance(G, SymmetricPermutationGroup): |
| | if self.degree != G.degree: |
| | return False |
| | return True |
| | if not isinstance(G, PermutationGroup): |
| | return False |
| | if self == G or self.generators[0]==Permutation(): |
| | return True |
| | if G.order() % self.order() != 0: |
| | return False |
| | if self.degree == G.degree or \ |
| | (self.degree < G.degree and not strict): |
| | gens = self.generators |
| | else: |
| | return False |
| | return all(G.contains(g, strict=strict) for g in gens) |
| |
|
| | @property |
| | def is_polycyclic(self): |
| | """Return ``True`` if a group is polycyclic. A group is polycyclic if |
| | it has a subnormal series with cyclic factors. For finite groups, |
| | this is the same as if the group is solvable. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([0, 2, 1, 3]) |
| | >>> b = Permutation([2, 0, 1, 3]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.is_polycyclic |
| | True |
| | |
| | """ |
| | return self.is_solvable |
| |
|
| | def is_transitive(self, strict=True): |
| | """Test if the group is transitive. |
| | |
| | Explanation |
| | =========== |
| | |
| | A group is transitive if it has a single orbit. |
| | |
| | If ``strict`` is ``False`` the group is transitive if it has |
| | a single orbit of length different from 1. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([0, 2, 1, 3]) |
| | >>> b = Permutation([2, 0, 1, 3]) |
| | >>> G1 = PermutationGroup([a, b]) |
| | >>> G1.is_transitive() |
| | False |
| | >>> G1.is_transitive(strict=False) |
| | True |
| | >>> c = Permutation([2, 3, 0, 1]) |
| | >>> G2 = PermutationGroup([a, c]) |
| | >>> G2.is_transitive() |
| | True |
| | >>> d = Permutation([1, 0, 2, 3]) |
| | >>> e = Permutation([0, 1, 3, 2]) |
| | >>> G3 = PermutationGroup([d, e]) |
| | >>> G3.is_transitive() or G3.is_transitive(strict=False) |
| | False |
| | |
| | """ |
| | if self._is_transitive: |
| | return self._is_transitive |
| | if strict: |
| | if self._is_transitive is not None: |
| | return self._is_transitive |
| |
|
| | ans = len(self.orbit(0)) == self.degree |
| | self._is_transitive = ans |
| | return ans |
| |
|
| | got_orb = False |
| | for x in self.orbits(): |
| | if len(x) > 1: |
| | if got_orb: |
| | return False |
| | got_orb = True |
| | return got_orb |
| |
|
| | @property |
| | def is_trivial(self): |
| | """Test if the group is the trivial group. |
| | |
| | This is true if the group contains only the identity permutation. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> G = PermutationGroup([Permutation([0, 1, 2])]) |
| | >>> G.is_trivial |
| | True |
| | |
| | """ |
| | if self._is_trivial is None: |
| | self._is_trivial = len(self) == 1 and self[0].is_Identity |
| | return self._is_trivial |
| |
|
| | def lower_central_series(self): |
| | r"""Return the lower central series for the group. |
| | |
| | The lower central series for a group `G` is the series |
| | `G = G_0 > G_1 > G_2 > \ldots` where |
| | `G_k = [G, G_{k-1}]`, i.e. every term after the first is equal to the |
| | commutator of `G` and the previous term in `G1` ([1], p.29). |
| | |
| | Returns |
| | ======= |
| | |
| | A list of permutation groups in the order `G = G_0, G_1, G_2, \ldots` |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import (AlternatingGroup, |
| | ... DihedralGroup) |
| | >>> A = AlternatingGroup(4) |
| | >>> len(A.lower_central_series()) |
| | 2 |
| | >>> A.lower_central_series()[1].is_subgroup(DihedralGroup(2)) |
| | True |
| | |
| | See Also |
| | ======== |
| | |
| | commutator, derived_series |
| | |
| | """ |
| | res = [self] |
| | current = self |
| | nxt = self.commutator(self, current) |
| | while not current.is_subgroup(nxt): |
| | res.append(nxt) |
| | current = nxt |
| | nxt = self.commutator(self, current) |
| | return res |
| |
|
| | @property |
| | def max_div(self): |
| | """Maximum proper divisor of the degree of a permutation group. |
| | |
| | Explanation |
| | =========== |
| | |
| | Obviously, this is the degree divided by its minimal proper divisor |
| | (larger than ``1``, if one exists). As it is guaranteed to be prime, |
| | the ``sieve`` from ``sympy.ntheory`` is used. |
| | This function is also used as an optimization tool for the functions |
| | ``minimal_block`` and ``_union_find_merge``. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> G = PermutationGroup([Permutation([0, 2, 1, 3])]) |
| | >>> G.max_div |
| | 2 |
| | |
| | See Also |
| | ======== |
| | |
| | minimal_block, _union_find_merge |
| | |
| | """ |
| | if self._max_div is not None: |
| | return self._max_div |
| | n = self.degree |
| | if n == 1: |
| | return 1 |
| | for x in sieve: |
| | if n % x == 0: |
| | d = n//x |
| | self._max_div = d |
| | return d |
| |
|
| | def minimal_block(self, points): |
| | r"""For a transitive group, finds the block system generated by |
| | ``points``. |
| | |
| | Explanation |
| | =========== |
| | |
| | If a group ``G`` acts on a set ``S``, a nonempty subset ``B`` of ``S`` |
| | is called a block under the action of ``G`` if for all ``g`` in ``G`` |
| | we have ``gB = B`` (``g`` fixes ``B``) or ``gB`` and ``B`` have no |
| | common points (``g`` moves ``B`` entirely). ([1], p.23; [6]). |
| | |
| | The distinct translates ``gB`` of a block ``B`` for ``g`` in ``G`` |
| | partition the set ``S`` and this set of translates is known as a block |
| | system. Moreover, we obviously have that all blocks in the partition |
| | have the same size, hence the block size divides ``|S|`` ([1], p.23). |
| | A ``G``-congruence is an equivalence relation ``~`` on the set ``S`` |
| | such that ``a ~ b`` implies ``g(a) ~ g(b)`` for all ``g`` in ``G``. |
| | For a transitive group, the equivalence classes of a ``G``-congruence |
| | and the blocks of a block system are the same thing ([1], p.23). |
| | |
| | The algorithm below checks the group for transitivity, and then finds |
| | the ``G``-congruence generated by the pairs ``(p_0, p_1), (p_0, p_2), |
| | ..., (p_0,p_{k-1})`` which is the same as finding the maximal block |
| | system (i.e., the one with minimum block size) such that |
| | ``p_0, ..., p_{k-1}`` are in the same block ([1], p.83). |
| | |
| | It is an implementation of Atkinson's algorithm, as suggested in [1], |
| | and manipulates an equivalence relation on the set ``S`` using a |
| | union-find data structure. The running time is just above |
| | `O(|points||S|)`. ([1], pp. 83-87; [7]). |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> D = DihedralGroup(10) |
| | >>> D.minimal_block([0, 5]) |
| | [0, 1, 2, 3, 4, 0, 1, 2, 3, 4] |
| | >>> D.minimal_block([0, 1]) |
| | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0] |
| | |
| | See Also |
| | ======== |
| | |
| | _union_find_rep, _union_find_merge, is_transitive, is_primitive |
| | |
| | """ |
| | if not self.is_transitive(): |
| | return False |
| | n = self.degree |
| | gens = self.generators |
| | |
| | parents = list(range(n)) |
| | ranks = [1]*n |
| | not_rep = [] |
| | k = len(points) |
| | |
| | if k > self.max_div: |
| | return [0]*n |
| | for i in range(k - 1): |
| | parents[points[i + 1]] = points[0] |
| | not_rep.append(points[i + 1]) |
| | ranks[points[0]] = k |
| | i = 0 |
| | len_not_rep = k - 1 |
| | while i < len_not_rep: |
| | gamma = not_rep[i] |
| | i += 1 |
| | for gen in gens: |
| | |
| | |
| | delta = self._union_find_rep(gamma, parents) |
| | |
| | |
| | temp = self._union_find_merge(gen(gamma), gen(delta), ranks, |
| | parents, not_rep) |
| | if temp == -1: |
| | return [0]*n |
| | len_not_rep += temp |
| | for i in range(n): |
| | |
| | |
| | self._union_find_rep(i, parents) |
| |
|
| | |
| | new_reps = {} |
| | return [new_reps.setdefault(r, i) for i, r in enumerate(parents)] |
| |
|
| | def conjugacy_class(self, x): |
| | r"""Return the conjugacy class of an element in the group. |
| | |
| | Explanation |
| | =========== |
| | |
| | The conjugacy class of an element ``g`` in a group ``G`` is the set of |
| | elements ``x`` in ``G`` that are conjugate with ``g``, i.e. for which |
| | |
| | ``g = xax^{-1}`` |
| | |
| | for some ``a`` in ``G``. |
| | |
| | Note that conjugacy is an equivalence relation, and therefore that |
| | conjugacy classes are partitions of ``G``. For a list of all the |
| | conjugacy classes of the group, use the conjugacy_classes() method. |
| | |
| | In a permutation group, each conjugacy class corresponds to a particular |
| | `cycle structure': for example, in ``S_3``, the conjugacy classes are: |
| | |
| | * the identity class, ``{()}`` |
| | * all transpositions, ``{(1 2), (1 3), (2 3)}`` |
| | * all 3-cycles, ``{(1 2 3), (1 3 2)}`` |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, SymmetricGroup |
| | >>> S3 = SymmetricGroup(3) |
| | >>> S3.conjugacy_class(Permutation(0, 1, 2)) |
| | {(0 1 2), (0 2 1)} |
| | |
| | Notes |
| | ===== |
| | |
| | This procedure computes the conjugacy class directly by finding the |
| | orbit of the element under conjugation in G. This algorithm is only |
| | feasible for permutation groups of relatively small order, but is like |
| | the orbit() function itself in that respect. |
| | """ |
| | |
| | |
| | new_class = {x} |
| | last_iteration = new_class |
| |
|
| | while len(last_iteration) > 0: |
| | this_iteration = set() |
| |
|
| | for y in last_iteration: |
| | for s in self.generators: |
| | conjugated = s * y * (~s) |
| | if conjugated not in new_class: |
| | this_iteration.add(conjugated) |
| |
|
| | new_class.update(last_iteration) |
| | last_iteration = this_iteration |
| |
|
| | return new_class |
| |
|
| |
|
| | def conjugacy_classes(self): |
| | r"""Return the conjugacy classes of the group. |
| | |
| | Explanation |
| | =========== |
| | |
| | As described in the documentation for the .conjugacy_class() function, |
| | conjugacy is an equivalence relation on a group G which partitions the |
| | set of elements. This method returns a list of all these conjugacy |
| | classes of G. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import SymmetricGroup |
| | >>> SymmetricGroup(3).conjugacy_classes() |
| | [{(2)}, {(0 1 2), (0 2 1)}, {(0 2), (1 2), (2)(0 1)}] |
| | |
| | """ |
| | identity = _af_new(list(range(self.degree))) |
| | known_elements = {identity} |
| | classes = [known_elements.copy()] |
| |
|
| | for x in self.generate(): |
| | if x not in known_elements: |
| | new_class = self.conjugacy_class(x) |
| | classes.append(new_class) |
| | known_elements.update(new_class) |
| |
|
| | return classes |
| |
|
| | def normal_closure(self, other, k=10): |
| | r"""Return the normal closure of a subgroup/set of permutations. |
| | |
| | Explanation |
| | =========== |
| | |
| | If ``S`` is a subset of a group ``G``, the normal closure of ``A`` in ``G`` |
| | is defined as the intersection of all normal subgroups of ``G`` that |
| | contain ``A`` ([1], p.14). Alternatively, it is the group generated by |
| | the conjugates ``x^{-1}yx`` for ``x`` a generator of ``G`` and ``y`` a |
| | generator of the subgroup ``\left\langle S\right\rangle`` generated by |
| | ``S`` (for some chosen generating set for ``\left\langle S\right\rangle``) |
| | ([1], p.73). |
| | |
| | Parameters |
| | ========== |
| | |
| | other |
| | a subgroup/list of permutations/single permutation |
| | k |
| | an implementation-specific parameter that determines the number |
| | of conjugates that are adjoined to ``other`` at once |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import (SymmetricGroup, |
| | ... CyclicGroup, AlternatingGroup) |
| | >>> S = SymmetricGroup(5) |
| | >>> C = CyclicGroup(5) |
| | >>> G = S.normal_closure(C) |
| | >>> G.order() |
| | 60 |
| | >>> G.is_subgroup(AlternatingGroup(5)) |
| | True |
| | |
| | See Also |
| | ======== |
| | |
| | commutator, derived_subgroup, random_pr |
| | |
| | Notes |
| | ===== |
| | |
| | The algorithm is described in [1], pp. 73-74; it makes use of the |
| | generation of random elements for permutation groups by the product |
| | replacement algorithm. |
| | |
| | """ |
| | if hasattr(other, 'generators'): |
| | degree = self.degree |
| | identity = _af_new(list(range(degree))) |
| |
|
| | if all(g == identity for g in other.generators): |
| | return other |
| | Z = PermutationGroup(other.generators[:]) |
| | base, strong_gens = Z.schreier_sims_incremental() |
| | strong_gens_distr = _distribute_gens_by_base(base, strong_gens) |
| | basic_orbits, basic_transversals = \ |
| | _orbits_transversals_from_bsgs(base, strong_gens_distr) |
| |
|
| | self._random_pr_init(r=10, n=20) |
| |
|
| | _loop = True |
| | while _loop: |
| | Z._random_pr_init(r=10, n=10) |
| | for _ in range(k): |
| | g = self.random_pr() |
| | h = Z.random_pr() |
| | conj = h^g |
| | res = _strip(conj, base, basic_orbits, basic_transversals) |
| | if res[0] != identity or res[1] != len(base) + 1: |
| | gens = Z.generators |
| | gens.append(conj) |
| | Z = PermutationGroup(gens) |
| | strong_gens.append(conj) |
| | temp_base, temp_strong_gens = \ |
| | Z.schreier_sims_incremental(base, strong_gens) |
| | base, strong_gens = temp_base, temp_strong_gens |
| | strong_gens_distr = \ |
| | _distribute_gens_by_base(base, strong_gens) |
| | basic_orbits, basic_transversals = \ |
| | _orbits_transversals_from_bsgs(base, |
| | strong_gens_distr) |
| | _loop = False |
| | for g in self.generators: |
| | for h in Z.generators: |
| | conj = h^g |
| | res = _strip(conj, base, basic_orbits, |
| | basic_transversals) |
| | if res[0] != identity or res[1] != len(base) + 1: |
| | _loop = True |
| | break |
| | if _loop: |
| | break |
| | return Z |
| | elif hasattr(other, '__getitem__'): |
| | return self.normal_closure(PermutationGroup(other)) |
| | elif hasattr(other, 'array_form'): |
| | return self.normal_closure(PermutationGroup([other])) |
| |
|
| | def orbit(self, alpha, action='tuples'): |
| | r"""Compute the orbit of alpha `\{g(\alpha) | g \in G\}` as a set. |
| | |
| | Explanation |
| | =========== |
| | |
| | The time complexity of the algorithm used here is `O(|Orb|*r)` where |
| | `|Orb|` is the size of the orbit and ``r`` is the number of generators of |
| | the group. For a more detailed analysis, see [1], p.78, [2], pp. 19-21. |
| | Here alpha can be a single point, or a list of points. |
| | |
| | If alpha is a single point, the ordinary orbit is computed. |
| | if alpha is a list of points, there are three available options: |
| | |
| | 'union' - computes the union of the orbits of the points in the list |
| | 'tuples' - computes the orbit of the list interpreted as an ordered |
| | tuple under the group action ( i.e., g((1,2,3)) = (g(1), g(2), g(3)) ) |
| | 'sets' - computes the orbit of the list interpreted as a sets |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([1, 2, 0, 4, 5, 6, 3]) |
| | >>> G = PermutationGroup([a]) |
| | >>> G.orbit(0) |
| | {0, 1, 2} |
| | >>> G.orbit([0, 4], 'union') |
| | {0, 1, 2, 3, 4, 5, 6} |
| | |
| | See Also |
| | ======== |
| | |
| | orbit_transversal |
| | |
| | """ |
| | return _orbit(self.degree, self.generators, alpha, action) |
| |
|
| | def orbit_rep(self, alpha, beta, schreier_vector=None): |
| | """Return a group element which sends ``alpha`` to ``beta``. |
| | |
| | Explanation |
| | =========== |
| | |
| | If ``beta`` is not in the orbit of ``alpha``, the function returns |
| | ``False``. This implementation makes use of the schreier vector. |
| | For a proof of correctness, see [1], p.80 |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import AlternatingGroup |
| | >>> G = AlternatingGroup(5) |
| | >>> G.orbit_rep(0, 4) |
| | (0 4 1 2 3) |
| | |
| | See Also |
| | ======== |
| | |
| | schreier_vector |
| | |
| | """ |
| | if schreier_vector is None: |
| | schreier_vector = self.schreier_vector(alpha) |
| | if schreier_vector[beta] is None: |
| | return False |
| | k = schreier_vector[beta] |
| | gens = [x._array_form for x in self.generators] |
| | a = [] |
| | while k != -1: |
| | a.append(gens[k]) |
| | beta = gens[k].index(beta) |
| | k = schreier_vector[beta] |
| | if a: |
| | return _af_new(_af_rmuln(*a)) |
| | else: |
| | return _af_new(list(range(self._degree))) |
| |
|
| | def orbit_transversal(self, alpha, pairs=False): |
| | r"""Computes a transversal for the orbit of ``alpha`` as a set. |
| | |
| | Explanation |
| | =========== |
| | |
| | For a permutation group `G`, a transversal for the orbit |
| | `Orb = \{g(\alpha) | g \in G\}` is a set |
| | `\{g_\beta | g_\beta(\alpha) = \beta\}` for `\beta \in Orb`. |
| | Note that there may be more than one possible transversal. |
| | If ``pairs`` is set to ``True``, it returns the list of pairs |
| | `(\beta, g_\beta)`. For a proof of correctness, see [1], p.79 |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> G = DihedralGroup(6) |
| | >>> G.orbit_transversal(0) |
| | [(5), (0 1 2 3 4 5), (0 5)(1 4)(2 3), (0 2 4)(1 3 5), (5)(0 4)(1 3), (0 3)(1 4)(2 5)] |
| | |
| | See Also |
| | ======== |
| | |
| | orbit |
| | |
| | """ |
| | return _orbit_transversal(self._degree, self.generators, alpha, pairs) |
| |
|
| | def orbits(self, rep=False): |
| | """Return the orbits of ``self``, ordered according to lowest element |
| | in each orbit. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation(1, 5)(2, 3)(4, 0, 6) |
| | >>> b = Permutation(1, 5)(3, 4)(2, 6, 0) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.orbits() |
| | [{0, 2, 3, 4, 6}, {1, 5}] |
| | """ |
| | return _orbits(self._degree, self._generators) |
| |
|
| | def order(self): |
| | """Return the order of the group: the number of permutations that |
| | can be generated from elements of the group. |
| | |
| | The number of permutations comprising the group is given by |
| | ``len(group)``; the length of each permutation in the group is |
| | given by ``group.size``. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | |
| | >>> a = Permutation([1, 0, 2]) |
| | >>> G = PermutationGroup([a]) |
| | >>> G.degree |
| | 3 |
| | >>> len(G) |
| | 1 |
| | >>> G.order() |
| | 2 |
| | >>> list(G.generate()) |
| | [(2), (2)(0 1)] |
| | |
| | >>> a = Permutation([0, 2, 1]) |
| | >>> b = Permutation([1, 0, 2]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.order() |
| | 6 |
| | |
| | See Also |
| | ======== |
| | |
| | degree |
| | |
| | """ |
| | if self._order is not None: |
| | return self._order |
| | if self._is_sym: |
| | n = self._degree |
| | self._order = factorial(n) |
| | return self._order |
| | if self._is_alt: |
| | n = self._degree |
| | self._order = factorial(n)/2 |
| | return self._order |
| |
|
| | m = prod([len(x) for x in self.basic_transversals]) |
| | self._order = m |
| | return m |
| |
|
| | def index(self, H): |
| | """ |
| | Returns the index of a permutation group. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation(1,2,3) |
| | >>> b =Permutation(3) |
| | >>> G = PermutationGroup([a]) |
| | >>> H = PermutationGroup([b]) |
| | >>> G.index(H) |
| | 3 |
| | |
| | """ |
| | if H.is_subgroup(self): |
| | return self.order()//H.order() |
| |
|
| | @property |
| | def is_symmetric(self): |
| | """Return ``True`` if the group is symmetric. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import SymmetricGroup |
| | >>> g = SymmetricGroup(5) |
| | >>> g.is_symmetric |
| | True |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> g = PermutationGroup( |
| | ... Permutation(0, 1, 2, 3, 4), |
| | ... Permutation(2, 3)) |
| | >>> g.is_symmetric |
| | True |
| | |
| | Notes |
| | ===== |
| | |
| | This uses a naive test involving the computation of the full |
| | group order. |
| | If you need more quicker taxonomy for large groups, you can use |
| | :meth:`PermutationGroup.is_alt_sym`. |
| | However, :meth:`PermutationGroup.is_alt_sym` may not be accurate |
| | and is not able to distinguish between an alternating group and |
| | a symmetric group. |
| | |
| | See Also |
| | ======== |
| | |
| | is_alt_sym |
| | """ |
| | _is_sym = self._is_sym |
| | if _is_sym is not None: |
| | return _is_sym |
| |
|
| | n = self.degree |
| | if n >= 8: |
| | if self.is_transitive(): |
| | _is_alt_sym = self._eval_is_alt_sym_monte_carlo() |
| | if _is_alt_sym: |
| | if any(g.is_odd for g in self.generators): |
| | self._is_sym, self._is_alt = True, False |
| | return True |
| |
|
| | self._is_sym, self._is_alt = False, True |
| | return False |
| |
|
| | return self._eval_is_alt_sym_naive(only_sym=True) |
| |
|
| | self._is_sym, self._is_alt = False, False |
| | return False |
| |
|
| | return self._eval_is_alt_sym_naive(only_sym=True) |
| |
|
| |
|
| | @property |
| | def is_alternating(self): |
| | """Return ``True`` if the group is alternating. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import AlternatingGroup |
| | >>> g = AlternatingGroup(5) |
| | >>> g.is_alternating |
| | True |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> g = PermutationGroup( |
| | ... Permutation(0, 1, 2, 3, 4), |
| | ... Permutation(2, 3, 4)) |
| | >>> g.is_alternating |
| | True |
| | |
| | Notes |
| | ===== |
| | |
| | This uses a naive test involving the computation of the full |
| | group order. |
| | If you need more quicker taxonomy for large groups, you can use |
| | :meth:`PermutationGroup.is_alt_sym`. |
| | However, :meth:`PermutationGroup.is_alt_sym` may not be accurate |
| | and is not able to distinguish between an alternating group and |
| | a symmetric group. |
| | |
| | See Also |
| | ======== |
| | |
| | is_alt_sym |
| | """ |
| | _is_alt = self._is_alt |
| | if _is_alt is not None: |
| | return _is_alt |
| |
|
| | n = self.degree |
| | if n >= 8: |
| | if self.is_transitive(): |
| | _is_alt_sym = self._eval_is_alt_sym_monte_carlo() |
| | if _is_alt_sym: |
| | if all(g.is_even for g in self.generators): |
| | self._is_sym, self._is_alt = False, True |
| | return True |
| |
|
| | self._is_sym, self._is_alt = True, False |
| | return False |
| |
|
| | return self._eval_is_alt_sym_naive(only_alt=True) |
| |
|
| | self._is_sym, self._is_alt = False, False |
| | return False |
| |
|
| | return self._eval_is_alt_sym_naive(only_alt=True) |
| |
|
| | @classmethod |
| | def _distinct_primes_lemma(cls, primes): |
| | """Subroutine to test if there is only one cyclic group for the |
| | order.""" |
| | primes = sorted(primes) |
| | l = len(primes) |
| | for i in range(l): |
| | for j in range(i+1, l): |
| | if primes[j] % primes[i] == 1: |
| | return None |
| | return True |
| |
|
| | @property |
| | def is_cyclic(self): |
| | r""" |
| | Return ``True`` if the group is Cyclic. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import AbelianGroup |
| | >>> G = AbelianGroup(3, 4) |
| | >>> G.is_cyclic |
| | True |
| | >>> G = AbelianGroup(4, 4) |
| | >>> G.is_cyclic |
| | False |
| | |
| | Notes |
| | ===== |
| | |
| | If the order of a group $n$ can be factored into the distinct |
| | primes $p_1, p_2, \dots , p_s$ and if |
| | |
| | .. math:: |
| | \forall i, j \in \{1, 2, \dots, s \}: |
| | p_i \not \equiv 1 \pmod {p_j} |
| | |
| | holds true, there is only one group of the order $n$ which |
| | is a cyclic group [1]_. This is a generalization of the lemma |
| | that the group of order $15, 35, \dots$ are cyclic. |
| | |
| | And also, these additional lemmas can be used to test if a |
| | group is cyclic if the order of the group is already found. |
| | |
| | - If the group is abelian and the order of the group is |
| | square-free, the group is cyclic. |
| | - If the order of the group is less than $6$ and is not $4$, the |
| | group is cyclic. |
| | - If the order of the group is prime, the group is cyclic. |
| | |
| | References |
| | ========== |
| | |
| | .. [1] 1978: John S. Rose: A Course on Group Theory, |
| | Introduction to Finite Group Theory: 1.4 |
| | """ |
| | if self._is_cyclic is not None: |
| | return self._is_cyclic |
| |
|
| | if len(self.generators) == 1: |
| | self._is_cyclic = True |
| | self._is_abelian = True |
| | return True |
| |
|
| | if self._is_abelian is False: |
| | self._is_cyclic = False |
| | return False |
| |
|
| | order = self.order() |
| |
|
| | if order < 6: |
| | self._is_abelian = True |
| | if order != 4: |
| | self._is_cyclic = True |
| | return True |
| |
|
| | factors = factorint(order) |
| | if all(v == 1 for v in factors.values()): |
| | if self._is_abelian: |
| | self._is_cyclic = True |
| | return True |
| |
|
| | primes = list(factors.keys()) |
| | if PermutationGroup._distinct_primes_lemma(primes) is True: |
| | self._is_cyclic = True |
| | self._is_abelian = True |
| | return True |
| |
|
| | if not self.is_abelian: |
| | self._is_cyclic = False |
| | return False |
| |
|
| | self._is_cyclic = all( |
| | any(g**(order//p) != self.identity for g in self.generators) |
| | for p, e in factors.items() if e > 1 |
| | ) |
| | return self._is_cyclic |
| |
|
| | @property |
| | def is_dihedral(self): |
| | r""" |
| | Return ``True`` if the group is dihedral. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.perm_groups import PermutationGroup |
| | >>> from sympy.combinatorics.permutations import Permutation |
| | >>> from sympy.combinatorics.named_groups import SymmetricGroup, CyclicGroup |
| | >>> G = PermutationGroup(Permutation(1, 6)(2, 5)(3, 4), Permutation(0, 1, 2, 3, 4, 5, 6)) |
| | >>> G.is_dihedral |
| | True |
| | >>> G = SymmetricGroup(3) |
| | >>> G.is_dihedral |
| | True |
| | >>> G = CyclicGroup(6) |
| | >>> G.is_dihedral |
| | False |
| | |
| | References |
| | ========== |
| | |
| | .. [Di1] https://math.stackexchange.com/questions/827230/given-a-cayley-table-is-there-an-algorithm-to-determine-if-it-is-a-dihedral-gro/827273#827273 |
| | .. [Di2] https://kconrad.math.uconn.edu/blurbs/grouptheory/dihedral.pdf |
| | .. [Di3] https://kconrad.math.uconn.edu/blurbs/grouptheory/dihedral2.pdf |
| | .. [Di4] https://en.wikipedia.org/wiki/Dihedral_group |
| | """ |
| | if self._is_dihedral is not None: |
| | return self._is_dihedral |
| |
|
| | order = self.order() |
| |
|
| | if order % 2 == 1: |
| | self._is_dihedral = False |
| | return False |
| | if order == 2: |
| | self._is_dihedral = True |
| | return True |
| | if order == 4: |
| | |
| | self._is_dihedral = not self.is_cyclic |
| | return self._is_dihedral |
| | if self.is_abelian: |
| | |
| | self._is_dihedral = False |
| | return False |
| |
|
| | |
| | n = order // 2 |
| |
|
| | |
| | gens = self.generators |
| | if len(gens) == 2: |
| | x, y = gens |
| | a, b = x.order(), y.order() |
| | |
| | if a < b: |
| | x, y, a, b = y, x, b, a |
| | |
| | if a == 2 == b: |
| | self._is_dihedral = True |
| | return True |
| | |
| | if a == n and b == 2 and y*x*y == ~x: |
| | self._is_dihedral = True |
| | return True |
| |
|
| | |
| | |
| | order_2, order_n = [], [] |
| | for p in self.elements: |
| | k = p.order() |
| | if k == 2: |
| | order_2.append(p) |
| | elif k == n: |
| | order_n.append(p) |
| |
|
| | if len(order_2) != n + 1 - (n % 2): |
| | self._is_dihedral = False |
| | return False |
| |
|
| | if not order_n: |
| | self._is_dihedral = False |
| | return False |
| |
|
| | x = order_n[0] |
| | |
| | |
| | y = order_2[0] |
| | if n % 2 == 0 and y == x**(n//2): |
| | y = order_2[1] |
| |
|
| | self._is_dihedral = (y*x*y == ~x) |
| | return self._is_dihedral |
| |
|
| | def pointwise_stabilizer(self, points, incremental=True): |
| | r"""Return the pointwise stabilizer for a set of points. |
| | |
| | Explanation |
| | =========== |
| | |
| | For a permutation group `G` and a set of points |
| | `\{p_1, p_2,\ldots, p_k\}`, the pointwise stabilizer of |
| | `p_1, p_2, \ldots, p_k` is defined as |
| | `G_{p_1,\ldots, p_k} = |
| | \{g\in G | g(p_i) = p_i \forall i\in\{1, 2,\ldots,k\}\}` ([1],p20). |
| | It is a subgroup of `G`. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import SymmetricGroup |
| | >>> S = SymmetricGroup(7) |
| | >>> Stab = S.pointwise_stabilizer([2, 3, 5]) |
| | >>> Stab.is_subgroup(S.stabilizer(2).stabilizer(3).stabilizer(5)) |
| | True |
| | |
| | See Also |
| | ======== |
| | |
| | stabilizer, schreier_sims_incremental |
| | |
| | Notes |
| | ===== |
| | |
| | When incremental == True, |
| | rather than the obvious implementation using successive calls to |
| | ``.stabilizer()``, this uses the incremental Schreier-Sims algorithm |
| | to obtain a base with starting segment - the given points. |
| | |
| | """ |
| | if incremental: |
| | base, strong_gens = self.schreier_sims_incremental(base=points) |
| | stab_gens = [] |
| | degree = self.degree |
| | for gen in strong_gens: |
| | if [gen(point) for point in points] == points: |
| | stab_gens.append(gen) |
| | if not stab_gens: |
| | stab_gens = _af_new(list(range(degree))) |
| | return PermutationGroup(stab_gens) |
| | else: |
| | gens = self._generators |
| | degree = self.degree |
| | for x in points: |
| | gens = _stabilizer(degree, gens, x) |
| | return PermutationGroup(gens) |
| |
|
| | def make_perm(self, n, seed=None): |
| | """ |
| | Multiply ``n`` randomly selected permutations from |
| | pgroup together, starting with the identity |
| | permutation. If ``n`` is a list of integers, those |
| | integers will be used to select the permutations and they |
| | will be applied in L to R order: make_perm((A, B, C)) will |
| | give CBA(I) where I is the identity permutation. |
| | |
| | ``seed`` is used to set the seed for the random selection |
| | of permutations from pgroup. If this is a list of integers, |
| | the corresponding permutations from pgroup will be selected |
| | in the order give. This is mainly used for testing purposes. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a, b = [Permutation([1, 0, 3, 2]), Permutation([1, 3, 0, 2])] |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.make_perm(1, [0]) |
| | (0 1)(2 3) |
| | >>> G.make_perm(3, [0, 1, 0]) |
| | (0 2 3 1) |
| | >>> G.make_perm([0, 1, 0]) |
| | (0 2 3 1) |
| | |
| | See Also |
| | ======== |
| | |
| | random |
| | """ |
| | if is_sequence(n): |
| | if seed is not None: |
| | raise ValueError('If n is a sequence, seed should be None') |
| | n, seed = len(n), n |
| | else: |
| | try: |
| | n = int(n) |
| | except TypeError: |
| | raise ValueError('n must be an integer or a sequence.') |
| | randomrange = _randrange(seed) |
| |
|
| | |
| | result = Permutation(list(range(self.degree))) |
| | m = len(self) |
| | for _ in range(n): |
| | p = self[randomrange(m)] |
| | result = rmul(result, p) |
| | return result |
| |
|
| | def random(self, af=False): |
| | """Return a random group element |
| | """ |
| | rank = randrange(self.order()) |
| | return self.coset_unrank(rank, af) |
| |
|
| | def random_pr(self, gen_count=11, iterations=50, _random_prec=None): |
| | """Return a random group element using product replacement. |
| | |
| | Explanation |
| | =========== |
| | |
| | For the details of the product replacement algorithm, see |
| | ``_random_pr_init`` In ``random_pr`` the actual 'product replacement' |
| | is performed. Notice that if the attribute ``_random_gens`` |
| | is empty, it needs to be initialized by ``_random_pr_init``. |
| | |
| | See Also |
| | ======== |
| | |
| | _random_pr_init |
| | |
| | """ |
| | if self._random_gens == []: |
| | self._random_pr_init(gen_count, iterations) |
| | random_gens = self._random_gens |
| | r = len(random_gens) - 1 |
| |
|
| | |
| | if _random_prec is None: |
| | s = randrange(r) |
| | t = randrange(r - 1) |
| | if t == s: |
| | t = r - 1 |
| | x = choice([1, 2]) |
| | e = choice([-1, 1]) |
| | else: |
| | s = _random_prec['s'] |
| | t = _random_prec['t'] |
| | if t == s: |
| | t = r - 1 |
| | x = _random_prec['x'] |
| | e = _random_prec['e'] |
| |
|
| | if x == 1: |
| | random_gens[s] = _af_rmul(random_gens[s], _af_pow(random_gens[t], e)) |
| | random_gens[r] = _af_rmul(random_gens[r], random_gens[s]) |
| | else: |
| | random_gens[s] = _af_rmul(_af_pow(random_gens[t], e), random_gens[s]) |
| | random_gens[r] = _af_rmul(random_gens[s], random_gens[r]) |
| | return _af_new(random_gens[r]) |
| |
|
| | def random_stab(self, alpha, schreier_vector=None, _random_prec=None): |
| | """Random element from the stabilizer of ``alpha``. |
| | |
| | The schreier vector for ``alpha`` is an optional argument used |
| | for speeding up repeated calls. The algorithm is described in [1], p.81 |
| | |
| | See Also |
| | ======== |
| | |
| | random_pr, orbit_rep |
| | |
| | """ |
| | if schreier_vector is None: |
| | schreier_vector = self.schreier_vector(alpha) |
| | if _random_prec is None: |
| | rand = self.random_pr() |
| | else: |
| | rand = _random_prec['rand'] |
| | beta = rand(alpha) |
| | h = self.orbit_rep(alpha, beta, schreier_vector) |
| | return rmul(~h, rand) |
| |
|
| | def schreier_sims(self): |
| | """Schreier-Sims algorithm. |
| | |
| | Explanation |
| | =========== |
| | |
| | It computes the generators of the chain of stabilizers |
| | `G > G_{b_1} > .. > G_{b1,..,b_r} > 1` |
| | in which `G_{b_1,..,b_i}` stabilizes `b_1,..,b_i`, |
| | and the corresponding ``s`` cosets. |
| | An element of the group can be written as the product |
| | `h_1*..*h_s`. |
| | |
| | We use the incremental Schreier-Sims algorithm. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([0, 2, 1]) |
| | >>> b = Permutation([1, 0, 2]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.schreier_sims() |
| | >>> G.basic_transversals |
| | [{0: (2)(0 1), 1: (2), 2: (1 2)}, |
| | {0: (2), 2: (0 2)}] |
| | """ |
| | if self._transversals: |
| | return |
| | self._schreier_sims() |
| | return |
| |
|
| | def _schreier_sims(self, base=None): |
| | schreier = self.schreier_sims_incremental(base=base, slp_dict=True) |
| | base, strong_gens = schreier[:2] |
| | self._base = base |
| | self._strong_gens = strong_gens |
| | self._strong_gens_slp = schreier[2] |
| | if not base: |
| | self._transversals = [] |
| | self._basic_orbits = [] |
| | return |
| |
|
| | strong_gens_distr = _distribute_gens_by_base(base, strong_gens) |
| | basic_orbits, transversals, slps = _orbits_transversals_from_bsgs(base,\ |
| | strong_gens_distr, slp=True) |
| |
|
| | |
| | for i, slp in enumerate(slps): |
| | gens = strong_gens_distr[i] |
| | for k in slp: |
| | slp[k] = [strong_gens.index(gens[s]) for s in slp[k]] |
| |
|
| | self._transversals = transversals |
| | self._basic_orbits = [sorted(x) for x in basic_orbits] |
| | self._transversal_slp = slps |
| |
|
| | def schreier_sims_incremental(self, base=None, gens=None, slp_dict=False): |
| | """Extend a sequence of points and generating set to a base and strong |
| | generating set. |
| | |
| | Parameters |
| | ========== |
| | |
| | base |
| | The sequence of points to be extended to a base. Optional |
| | parameter with default value ``[]``. |
| | gens |
| | The generating set to be extended to a strong generating set |
| | relative to the base obtained. Optional parameter with default |
| | value ``self.generators``. |
| | |
| | slp_dict |
| | If `True`, return a dictionary `{g: gens}` for each strong |
| | generator `g` where `gens` is a list of strong generators |
| | coming before `g` in `strong_gens`, such that the product |
| | of the elements of `gens` is equal to `g`. |
| | |
| | Returns |
| | ======= |
| | |
| | (base, strong_gens) |
| | ``base`` is the base obtained, and ``strong_gens`` is the strong |
| | generating set relative to it. The original parameters ``base``, |
| | ``gens`` remain unchanged. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import AlternatingGroup |
| | >>> from sympy.combinatorics.testutil import _verify_bsgs |
| | >>> A = AlternatingGroup(7) |
| | >>> base = [2, 3] |
| | >>> seq = [2, 3] |
| | >>> base, strong_gens = A.schreier_sims_incremental(base=seq) |
| | >>> _verify_bsgs(A, base, strong_gens) |
| | True |
| | >>> base[:2] |
| | [2, 3] |
| | |
| | Notes |
| | ===== |
| | |
| | This version of the Schreier-Sims algorithm runs in polynomial time. |
| | There are certain assumptions in the implementation - if the trivial |
| | group is provided, ``base`` and ``gens`` are returned immediately, |
| | as any sequence of points is a base for the trivial group. If the |
| | identity is present in the generators ``gens``, it is removed as |
| | it is a redundant generator. |
| | The implementation is described in [1], pp. 90-93. |
| | |
| | See Also |
| | ======== |
| | |
| | schreier_sims, schreier_sims_random |
| | |
| | """ |
| | if base is None: |
| | base = [] |
| | if gens is None: |
| | gens = self.generators[:] |
| | degree = self.degree |
| | id_af = list(range(degree)) |
| | |
| | if len(gens) == 1 and gens[0].is_Identity: |
| | if slp_dict: |
| | return base, gens, {gens[0]: [gens[0]]} |
| | return base, gens |
| | |
| | _base, _gens = base[:], gens[:] |
| | |
| | _gens = [x for x in _gens if not x.is_Identity] |
| | |
| | for gen in _gens: |
| | if all(x == gen._array_form[x] for x in _base): |
| | for new in id_af: |
| | if gen._array_form[new] != new: |
| | break |
| | else: |
| | assert None |
| | _base.append(new) |
| | |
| | strong_gens_distr = _distribute_gens_by_base(_base, _gens) |
| | strong_gens_slp = [] |
| | |
| | orbs = {} |
| | transversals = {} |
| | slps = {} |
| | base_len = len(_base) |
| | for i in range(base_len): |
| | transversals[i], slps[i] = _orbit_transversal(degree, strong_gens_distr[i], |
| | _base[i], pairs=True, af=True, slp=True) |
| | transversals[i] = dict(transversals[i]) |
| | orbs[i] = list(transversals[i].keys()) |
| | |
| | |
| | i = base_len - 1 |
| | while i >= 0: |
| | |
| | |
| | continue_i = False |
| | |
| | db = {} |
| | for beta, u_beta in list(transversals[i].items()): |
| | for j, gen in enumerate(strong_gens_distr[i]): |
| | gb = gen._array_form[beta] |
| | u1 = transversals[i][gb] |
| | g1 = _af_rmul(gen._array_form, u_beta) |
| | slp = [(i, g) for g in slps[i][beta]] |
| | slp = [(i, j)] + slp |
| | if g1 != u1: |
| | |
| | |
| | y = True |
| | try: |
| | u1_inv = db[gb] |
| | except KeyError: |
| | u1_inv = db[gb] = _af_invert(u1) |
| | schreier_gen = _af_rmul(u1_inv, g1) |
| | u1_inv_slp = slps[i][gb][:] |
| | u1_inv_slp.reverse() |
| | u1_inv_slp = [(i, (g,)) for g in u1_inv_slp] |
| | slp = u1_inv_slp + slp |
| | h, j, slp = _strip_af(schreier_gen, _base, orbs, transversals, i, slp=slp, slps=slps) |
| | if j <= base_len: |
| | |
| | y = False |
| | elif h: |
| | |
| | y = False |
| | moved = 0 |
| | while h[moved] == moved: |
| | moved += 1 |
| | _base.append(moved) |
| | base_len += 1 |
| | strong_gens_distr.append([]) |
| | if y is False: |
| | |
| | |
| | h = _af_new(h) |
| | strong_gens_slp.append((h, slp)) |
| | for l in range(i + 1, j): |
| | strong_gens_distr[l].append(h) |
| | transversals[l], slps[l] =\ |
| | _orbit_transversal(degree, strong_gens_distr[l], |
| | _base[l], pairs=True, af=True, slp=True) |
| | transversals[l] = dict(transversals[l]) |
| | orbs[l] = list(transversals[l].keys()) |
| | i = j - 1 |
| | |
| | continue_i = True |
| | if continue_i is True: |
| | break |
| | if continue_i is True: |
| | break |
| | if continue_i is True: |
| | continue |
| | i -= 1 |
| |
|
| | strong_gens = _gens[:] |
| |
|
| | if slp_dict: |
| | |
| | |
| | |
| | for k, slp in strong_gens_slp: |
| | strong_gens.append(k) |
| | for i in range(len(slp)): |
| | s = slp[i] |
| | if isinstance(s[1], tuple): |
| | slp[i] = strong_gens_distr[s[0]][s[1][0]]**-1 |
| | else: |
| | slp[i] = strong_gens_distr[s[0]][s[1]] |
| | strong_gens_slp = dict(strong_gens_slp) |
| | |
| | for g in _gens: |
| | strong_gens_slp[g] = [g] |
| | return (_base, strong_gens, strong_gens_slp) |
| |
|
| | strong_gens.extend([k for k, _ in strong_gens_slp]) |
| | return _base, strong_gens |
| |
|
| | def schreier_sims_random(self, base=None, gens=None, consec_succ=10, |
| | _random_prec=None): |
| | r"""Randomized Schreier-Sims algorithm. |
| | |
| | Explanation |
| | =========== |
| | |
| | The randomized Schreier-Sims algorithm takes the sequence ``base`` |
| | and the generating set ``gens``, and extends ``base`` to a base, and |
| | ``gens`` to a strong generating set relative to that base with |
| | probability of a wrong answer at most `2^{-consec\_succ}`, |
| | provided the random generators are sufficiently random. |
| | |
| | Parameters |
| | ========== |
| | |
| | base |
| | The sequence to be extended to a base. |
| | gens |
| | The generating set to be extended to a strong generating set. |
| | consec_succ |
| | The parameter defining the probability of a wrong answer. |
| | _random_prec |
| | An internal parameter used for testing purposes. |
| | |
| | Returns |
| | ======= |
| | |
| | (base, strong_gens) |
| | ``base`` is the base and ``strong_gens`` is the strong generating |
| | set relative to it. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.testutil import _verify_bsgs |
| | >>> from sympy.combinatorics.named_groups import SymmetricGroup |
| | >>> S = SymmetricGroup(5) |
| | >>> base, strong_gens = S.schreier_sims_random(consec_succ=5) |
| | >>> _verify_bsgs(S, base, strong_gens) #doctest: +SKIP |
| | True |
| | |
| | Notes |
| | ===== |
| | |
| | The algorithm is described in detail in [1], pp. 97-98. It extends |
| | the orbits ``orbs`` and the permutation groups ``stabs`` to |
| | basic orbits and basic stabilizers for the base and strong generating |
| | set produced in the end. |
| | The idea of the extension process |
| | is to "sift" random group elements through the stabilizer chain |
| | and amend the stabilizers/orbits along the way when a sift |
| | is not successful. |
| | The helper function ``_strip`` is used to attempt |
| | to decompose a random group element according to the current |
| | state of the stabilizer chain and report whether the element was |
| | fully decomposed (successful sift) or not (unsuccessful sift). In |
| | the latter case, the level at which the sift failed is reported and |
| | used to amend ``stabs``, ``base``, ``gens`` and ``orbs`` accordingly. |
| | The halting condition is for ``consec_succ`` consecutive successful |
| | sifts to pass. This makes sure that the current ``base`` and ``gens`` |
| | form a BSGS with probability at least `1 - 1/\text{consec\_succ}`. |
| | |
| | See Also |
| | ======== |
| | |
| | schreier_sims |
| | |
| | """ |
| | if base is None: |
| | base = [] |
| | if gens is None: |
| | gens = self.generators |
| | base_len = len(base) |
| | n = self.degree |
| | |
| | for gen in gens: |
| | if all(gen(x) == x for x in base): |
| | new = 0 |
| | while gen._array_form[new] == new: |
| | new += 1 |
| | base.append(new) |
| | base_len += 1 |
| | |
| | strong_gens_distr = _distribute_gens_by_base(base, gens) |
| | |
| | transversals = {} |
| | orbs = {} |
| | for i in range(base_len): |
| | transversals[i] = dict(_orbit_transversal(n, strong_gens_distr[i], |
| | base[i], pairs=True)) |
| | orbs[i] = list(transversals[i].keys()) |
| | |
| | c = 0 |
| | |
| | |
| | while c < consec_succ: |
| | if _random_prec is None: |
| | g = self.random_pr() |
| | else: |
| | g = _random_prec['g'].pop() |
| | h, j = _strip(g, base, orbs, transversals) |
| | y = True |
| | |
| | if j <= base_len: |
| | y = False |
| | elif not h.is_Identity: |
| | y = False |
| | moved = 0 |
| | while h(moved) == moved: |
| | moved += 1 |
| | base.append(moved) |
| | base_len += 1 |
| | strong_gens_distr.append([]) |
| | |
| | |
| | if y is False: |
| | for l in range(1, j): |
| | strong_gens_distr[l].append(h) |
| | transversals[l] = dict(_orbit_transversal(n, |
| | strong_gens_distr[l], base[l], pairs=True)) |
| | orbs[l] = list(transversals[l].keys()) |
| | c = 0 |
| | else: |
| | c += 1 |
| | |
| | strong_gens = strong_gens_distr[0][:] |
| | for gen in strong_gens_distr[1]: |
| | if gen not in strong_gens: |
| | strong_gens.append(gen) |
| | return base, strong_gens |
| |
|
| | def schreier_vector(self, alpha): |
| | """Computes the schreier vector for ``alpha``. |
| | |
| | Explanation |
| | =========== |
| | |
| | The Schreier vector efficiently stores information |
| | about the orbit of ``alpha``. It can later be used to quickly obtain |
| | elements of the group that send ``alpha`` to a particular element |
| | in the orbit. Notice that the Schreier vector depends on the order |
| | in which the group generators are listed. For a definition, see [3]. |
| | Since list indices start from zero, we adopt the convention to use |
| | "None" instead of 0 to signify that an element does not belong |
| | to the orbit. |
| | For the algorithm and its correctness, see [2], pp.78-80. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([2, 4, 6, 3, 1, 5, 0]) |
| | >>> b = Permutation([0, 1, 3, 5, 4, 6, 2]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.schreier_vector(0) |
| | [-1, None, 0, 1, None, 1, 0] |
| | |
| | See Also |
| | ======== |
| | |
| | orbit |
| | |
| | """ |
| | n = self.degree |
| | v = [None]*n |
| | v[alpha] = -1 |
| | orb = [alpha] |
| | used = [False]*n |
| | used[alpha] = True |
| | gens = self.generators |
| | r = len(gens) |
| | for b in orb: |
| | for i in range(r): |
| | temp = gens[i]._array_form[b] |
| | if used[temp] is False: |
| | orb.append(temp) |
| | used[temp] = True |
| | v[temp] = i |
| | return v |
| |
|
| | def stabilizer(self, alpha): |
| | r"""Return the stabilizer subgroup of ``alpha``. |
| | |
| | Explanation |
| | =========== |
| | |
| | The stabilizer of `\alpha` is the group `G_\alpha = |
| | \{g \in G | g(\alpha) = \alpha\}`. |
| | For a proof of correctness, see [1], p.79. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> G = DihedralGroup(6) |
| | >>> G.stabilizer(5) |
| | PermutationGroup([ |
| | (5)(0 4)(1 3)]) |
| | |
| | See Also |
| | ======== |
| | |
| | orbit |
| | |
| | """ |
| | return PermGroup(_stabilizer(self._degree, self._generators, alpha)) |
| |
|
| | @property |
| | def strong_gens(self): |
| | r"""Return a strong generating set from the Schreier-Sims algorithm. |
| | |
| | Explanation |
| | =========== |
| | |
| | A generating set `S = \{g_1, g_2, \dots, g_t\}` for a permutation group |
| | `G` is a strong generating set relative to the sequence of points |
| | (referred to as a "base") `(b_1, b_2, \dots, b_k)` if, for |
| | `1 \leq i \leq k` we have that the intersection of the pointwise |
| | stabilizer `G^{(i+1)} := G_{b_1, b_2, \dots, b_i}` with `S` generates |
| | the pointwise stabilizer `G^{(i+1)}`. The concepts of a base and |
| | strong generating set and their applications are discussed in depth |
| | in [1], pp. 87-89 and [2], pp. 55-57. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> D = DihedralGroup(4) |
| | >>> D.strong_gens |
| | [(0 1 2 3), (0 3)(1 2), (1 3)] |
| | >>> D.base |
| | [0, 1] |
| | |
| | See Also |
| | ======== |
| | |
| | base, basic_transversals, basic_orbits, basic_stabilizers |
| | |
| | """ |
| | if self._strong_gens == []: |
| | self.schreier_sims() |
| | return self._strong_gens |
| |
|
| | def subgroup(self, gens): |
| | """ |
| | Return the subgroup generated by `gens` which is a list of |
| | elements of the group |
| | """ |
| |
|
| | if not all(g in self for g in gens): |
| | raise ValueError("The group does not contain the supplied generators") |
| |
|
| | G = PermutationGroup(gens) |
| | return G |
| |
|
| | def subgroup_search(self, prop, base=None, strong_gens=None, tests=None, |
| | init_subgroup=None): |
| | """Find the subgroup of all elements satisfying the property ``prop``. |
| | |
| | Explanation |
| | =========== |
| | |
| | This is done by a depth-first search with respect to base images that |
| | uses several tests to prune the search tree. |
| | |
| | Parameters |
| | ========== |
| | |
| | prop |
| | The property to be used. Has to be callable on group elements |
| | and always return ``True`` or ``False``. It is assumed that |
| | all group elements satisfying ``prop`` indeed form a subgroup. |
| | base |
| | A base for the supergroup. |
| | strong_gens |
| | A strong generating set for the supergroup. |
| | tests |
| | A list of callables of length equal to the length of ``base``. |
| | These are used to rule out group elements by partial base images, |
| | so that ``tests[l](g)`` returns False if the element ``g`` is known |
| | not to satisfy prop base on where g sends the first ``l + 1`` base |
| | points. |
| | init_subgroup |
| | if a subgroup of the sought group is |
| | known in advance, it can be passed to the function as this |
| | parameter. |
| | |
| | Returns |
| | ======= |
| | |
| | res |
| | The subgroup of all elements satisfying ``prop``. The generating |
| | set for this group is guaranteed to be a strong generating set |
| | relative to the base ``base``. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import (SymmetricGroup, |
| | ... AlternatingGroup) |
| | >>> from sympy.combinatorics.testutil import _verify_bsgs |
| | >>> S = SymmetricGroup(7) |
| | >>> prop_even = lambda x: x.is_even |
| | >>> base, strong_gens = S.schreier_sims_incremental() |
| | >>> G = S.subgroup_search(prop_even, base=base, strong_gens=strong_gens) |
| | >>> G.is_subgroup(AlternatingGroup(7)) |
| | True |
| | >>> _verify_bsgs(G, base, G.generators) |
| | True |
| | |
| | Notes |
| | ===== |
| | |
| | This function is extremely lengthy and complicated and will require |
| | some careful attention. The implementation is described in |
| | [1], pp. 114-117, and the comments for the code here follow the lines |
| | of the pseudocode in the book for clarity. |
| | |
| | The complexity is exponential in general, since the search process by |
| | itself visits all members of the supergroup. However, there are a lot |
| | of tests which are used to prune the search tree, and users can define |
| | their own tests via the ``tests`` parameter, so in practice, and for |
| | some computations, it's not terrible. |
| | |
| | A crucial part in the procedure is the frequent base change performed |
| | (this is line 11 in the pseudocode) in order to obtain a new basic |
| | stabilizer. The book mentiones that this can be done by using |
| | ``.baseswap(...)``, however the current implementation uses a more |
| | straightforward way to find the next basic stabilizer - calling the |
| | function ``.stabilizer(...)`` on the previous basic stabilizer. |
| | |
| | """ |
| | |
| | def get_reps(orbits): |
| | |
| | return [min(orbit, key = lambda x: base_ordering[x]) \ |
| | for orbit in orbits] |
| |
|
| | def update_nu(l): |
| | temp_index = len(basic_orbits[l]) + 1 -\ |
| | len(res_basic_orbits_init_base[l]) |
| | |
| | if temp_index >= len(sorted_orbits[l]): |
| | nu[l] = base_ordering[degree] |
| | else: |
| | nu[l] = sorted_orbits[l][temp_index] |
| |
|
| | if base is None: |
| | base, strong_gens = self.schreier_sims_incremental() |
| | base_len = len(base) |
| | degree = self.degree |
| | identity = _af_new(list(range(degree))) |
| | base_ordering = _base_ordering(base, degree) |
| | |
| | base_ordering.append(degree) |
| | |
| | base_ordering.append(-1) |
| | |
| | strong_gens_distr = _distribute_gens_by_base(base, strong_gens) |
| | basic_orbits, transversals = _orbits_transversals_from_bsgs(base, |
| | strong_gens_distr) |
| | |
| | if init_subgroup is None: |
| | init_subgroup = PermutationGroup([identity]) |
| | if tests is None: |
| | trivial_test = lambda x: True |
| | tests = [] |
| | for i in range(base_len): |
| | tests.append(trivial_test) |
| | |
| | res = init_subgroup |
| | f = base_len - 1 |
| | l = base_len - 1 |
| | |
| | res_base = base[:] |
| | |
| | res_base, res_strong_gens = res.schreier_sims_incremental( |
| | base=res_base) |
| | res_strong_gens_distr = _distribute_gens_by_base(res_base, |
| | res_strong_gens) |
| | res_generators = res.generators |
| | res_basic_orbits_init_base = \ |
| | [_orbit(degree, res_strong_gens_distr[i], res_base[i])\ |
| | for i in range(base_len)] |
| | |
| | orbit_reps = [None]*base_len |
| | |
| | orbits = _orbits(degree, res_strong_gens_distr[f]) |
| | orbit_reps[f] = get_reps(orbits) |
| | |
| | |
| | orbit_reps[f].remove(base[f]) |
| | |
| | c = [0]*base_len |
| | u = [identity]*base_len |
| | sorted_orbits = [None]*base_len |
| | for i in range(base_len): |
| | sorted_orbits[i] = basic_orbits[i][:] |
| | sorted_orbits[i].sort(key=lambda point: base_ordering[point]) |
| | |
| | mu = [None]*base_len |
| | nu = [None]*base_len |
| | |
| | mu[l] = degree + 1 |
| | update_nu(l) |
| | |
| | computed_words = [identity]*base_len |
| | |
| | while True: |
| | |
| | while l < base_len - 1 and \ |
| | computed_words[l](base[l]) in orbit_reps[l] and \ |
| | base_ordering[mu[l]] < \ |
| | base_ordering[computed_words[l](base[l])] < \ |
| | base_ordering[nu[l]] and \ |
| | tests[l](computed_words): |
| | |
| | new_point = computed_words[l](base[l]) |
| | res_base[l] = new_point |
| | new_stab_gens = _stabilizer(degree, res_strong_gens_distr[l], |
| | new_point) |
| | res_strong_gens_distr[l + 1] = new_stab_gens |
| | |
| | |
| | orbits = _orbits(degree, new_stab_gens) |
| | orbit_reps[l + 1] = get_reps(orbits) |
| | |
| | l += 1 |
| | temp_orbit = [computed_words[l - 1](point) for point |
| | in basic_orbits[l]] |
| | temp_orbit.sort(key=lambda point: base_ordering[point]) |
| | sorted_orbits[l] = temp_orbit |
| | |
| | new_mu = degree + 1 |
| | for i in range(l): |
| | if base[l] in res_basic_orbits_init_base[i]: |
| | candidate = computed_words[i](base[i]) |
| | if base_ordering[candidate] > base_ordering[new_mu]: |
| | new_mu = candidate |
| | mu[l] = new_mu |
| | update_nu(l) |
| | |
| | c[l] = 0 |
| | temp_point = sorted_orbits[l][c[l]] |
| | gamma = computed_words[l - 1]._array_form.index(temp_point) |
| | u[l] = transversals[l][gamma] |
| | |
| | computed_words[l] = rmul(computed_words[l - 1], u[l]) |
| | |
| | g = computed_words[l] |
| | temp_point = g(base[l]) |
| | if l == base_len - 1 and \ |
| | base_ordering[mu[l]] < \ |
| | base_ordering[temp_point] < base_ordering[nu[l]] and \ |
| | temp_point in orbit_reps[l] and \ |
| | tests[l](computed_words) and \ |
| | prop(g): |
| | |
| | res_generators.append(g) |
| | res_base = base[:] |
| | |
| | res_strong_gens.append(g) |
| | res_strong_gens_distr = _distribute_gens_by_base(res_base, |
| | res_strong_gens) |
| | res_basic_orbits_init_base = \ |
| | [_orbit(degree, res_strong_gens_distr[i], res_base[i]) \ |
| | for i in range(base_len)] |
| | |
| | |
| | orbit_reps[f] = get_reps(orbits) |
| | l = f |
| | |
| | |
| | while l >= 0 and c[l] == len(basic_orbits[l]) - 1: |
| | l = l - 1 |
| | |
| | if l == -1: |
| | return PermutationGroup(res_generators) |
| | |
| | if l < f: |
| | |
| | f = l |
| | c[l] = 0 |
| | |
| | temp_orbits = _orbits(degree, res_strong_gens_distr[f]) |
| | orbit_reps[f] = get_reps(temp_orbits) |
| | |
| | mu[l] = degree + 1 |
| | temp_index = len(basic_orbits[l]) + 1 - \ |
| | len(res_basic_orbits_init_base[l]) |
| | if temp_index >= len(sorted_orbits[l]): |
| | nu[l] = base_ordering[degree] |
| | else: |
| | nu[l] = sorted_orbits[l][temp_index] |
| | |
| | |
| | c[l] += 1 |
| | if l == 0: |
| | gamma = sorted_orbits[l][c[l]] |
| | else: |
| | gamma = computed_words[l - 1]._array_form.index(sorted_orbits[l][c[l]]) |
| |
|
| | u[l] = transversals[l][gamma] |
| | if l == 0: |
| | computed_words[l] = u[l] |
| | else: |
| | computed_words[l] = rmul(computed_words[l - 1], u[l]) |
| |
|
| | @property |
| | def transitivity_degree(self): |
| | r"""Compute the degree of transitivity of the group. |
| | |
| | Explanation |
| | =========== |
| | |
| | A permutation group `G` acting on `\Omega = \{0, 1, \dots, n-1\}` is |
| | ``k``-fold transitive, if, for any `k` points |
| | `(a_1, a_2, \dots, a_k) \in \Omega` and any `k` points |
| | `(b_1, b_2, \dots, b_k) \in \Omega` there exists `g \in G` such that |
| | `g(a_1) = b_1, g(a_2) = b_2, \dots, g(a_k) = b_k` |
| | The degree of transitivity of `G` is the maximum ``k`` such that |
| | `G` is ``k``-fold transitive. ([8]) |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> a = Permutation([1, 2, 0]) |
| | >>> b = Permutation([1, 0, 2]) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> G.transitivity_degree |
| | 3 |
| | |
| | See Also |
| | ======== |
| | |
| | is_transitive, orbit |
| | |
| | """ |
| | if self._transitivity_degree is None: |
| | n = self.degree |
| | G = self |
| | |
| | |
| | |
| | |
| | |
| | |
| | for i in range(n): |
| | orb = G.orbit(i) |
| | if len(orb) != n - i: |
| | self._transitivity_degree = i |
| | return i |
| | G = G.stabilizer(i) |
| | self._transitivity_degree = n |
| | return n |
| | else: |
| | return self._transitivity_degree |
| |
|
| | def _p_elements_group(self, p): |
| | ''' |
| | For an abelian p-group, return the subgroup consisting of |
| | all elements of order p (and the identity) |
| | |
| | ''' |
| | gens = self.generators[:] |
| | gens = sorted(gens, key=lambda x: x.order(), reverse=True) |
| | gens_p = [g**(g.order()/p) for g in gens] |
| | gens_r = [] |
| | for i in range(len(gens)): |
| | x = gens[i] |
| | x_order = x.order() |
| | |
| | x_p = x**(x_order/p) |
| | if i > 0: |
| | P = PermutationGroup(gens_p[:i]) |
| | else: |
| | P = PermutationGroup(self.identity) |
| | if x**(x_order/p) not in P: |
| | gens_r.append(x**(x_order/p)) |
| | else: |
| | |
| | |
| | g = P.generator_product(x_p, original=True) |
| | for s in g: |
| | x = x*s**-1 |
| | x_order = x_order/p |
| | |
| | del gens[i] |
| | del gens_p[i] |
| | j = i - 1 |
| | while j < len(gens) and gens[j].order() >= x_order: |
| | j += 1 |
| | gens = gens[:j] + [x] + gens[j:] |
| | gens_p = gens_p[:j] + [x] + gens_p[j:] |
| | return PermutationGroup(gens_r) |
| |
|
| | def _sylow_alt_sym(self, p): |
| | ''' |
| | Return a p-Sylow subgroup of a symmetric or an |
| | alternating group. |
| | |
| | Explanation |
| | =========== |
| | |
| | The algorithm for this is hinted at in [1], Chapter 4, |
| | Exercise 4. |
| | |
| | For Sym(n) with n = p^i, the idea is as follows. Partition |
| | the interval [0..n-1] into p equal parts, each of length p^(i-1): |
| | [0..p^(i-1)-1], [p^(i-1)..2*p^(i-1)-1]...[(p-1)*p^(i-1)..p^i-1]. |
| | Find a p-Sylow subgroup of Sym(p^(i-1)) (treated as a subgroup |
| | of ``self``) acting on each of the parts. Call the subgroups |
| | P_1, P_2...P_p. The generators for the subgroups P_2...P_p |
| | can be obtained from those of P_1 by applying a "shifting" |
| | permutation to them, that is, a permutation mapping [0..p^(i-1)-1] |
| | to the second part (the other parts are obtained by using the shift |
| | multiple times). The union of this permutation and the generators |
| | of P_1 is a p-Sylow subgroup of ``self``. |
| | |
| | For n not equal to a power of p, partition |
| | [0..n-1] in accordance with how n would be written in base p. |
| | E.g. for p=2 and n=11, 11 = 2^3 + 2^2 + 1 so the partition |
| | is [[0..7], [8..9], {10}]. To generate a p-Sylow subgroup, |
| | take the union of the generators for each of the parts. |
| | For the above example, {(0 1), (0 2)(1 3), (0 4), (1 5)(2 7)} |
| | from the first part, {(8 9)} from the second part and |
| | nothing from the third. This gives 4 generators in total, and |
| | the subgroup they generate is p-Sylow. |
| | |
| | Alternating groups are treated the same except when p=2. In this |
| | case, (0 1)(s s+1) should be added for an appropriate s (the start |
| | of a part) for each part in the partitions. |
| | |
| | See Also |
| | ======== |
| | |
| | sylow_subgroup, is_alt_sym |
| | |
| | ''' |
| | n = self.degree |
| | gens = [] |
| | identity = Permutation(n-1) |
| | |
| | |
| | alt = p == 2 and all(g.is_even for g in self.generators) |
| |
|
| | |
| | coeffs = [] |
| | m = n |
| | while m > 0: |
| | coeffs.append(m % p) |
| | m = m // p |
| |
|
| | power = len(coeffs)-1 |
| | |
| | |
| | |
| | for i in range(1, power+1): |
| | if i == 1 and alt: |
| | |
| | continue |
| | gen = Permutation([(j + p**(i-1)) % p**i for j in range(p**i)]) |
| | gens.append(identity*gen) |
| | if alt: |
| | gen = Permutation(0, 1)*gen*Permutation(0, 1)*gen |
| | gens.append(gen) |
| |
|
| | |
| | |
| | start = 0 |
| |
|
| | while power > 0: |
| | a = coeffs[power] |
| |
|
| | |
| | |
| | for _ in range(a): |
| | shift = Permutation() |
| | if start > 0: |
| | for i in range(p**power): |
| | shift = shift(i, start + i) |
| |
|
| | if alt: |
| | gen = Permutation(0, 1)*shift*Permutation(0, 1)*shift |
| | gens.append(gen) |
| | j = 2*(power - 1) |
| | else: |
| | j = power |
| |
|
| | for i, gen in enumerate(gens[:j]): |
| | if alt and i % 2 == 1: |
| | continue |
| | |
| | |
| | gen = shift*gen*shift |
| | gens.append(gen) |
| |
|
| | start += p**power |
| | power = power-1 |
| |
|
| | return gens |
| |
|
| | def sylow_subgroup(self, p): |
| | ''' |
| | Return a p-Sylow subgroup of the group. |
| | |
| | The algorithm is described in [1], Chapter 4, Section 7 |
| | |
| | Examples |
| | ======== |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> from sympy.combinatorics.named_groups import SymmetricGroup |
| | >>> from sympy.combinatorics.named_groups import AlternatingGroup |
| | |
| | >>> D = DihedralGroup(6) |
| | >>> S = D.sylow_subgroup(2) |
| | >>> S.order() |
| | 4 |
| | >>> G = SymmetricGroup(6) |
| | >>> S = G.sylow_subgroup(5) |
| | >>> S.order() |
| | 5 |
| | |
| | >>> G1 = AlternatingGroup(3) |
| | >>> G2 = AlternatingGroup(5) |
| | >>> G3 = AlternatingGroup(9) |
| | |
| | >>> S1 = G1.sylow_subgroup(3) |
| | >>> S2 = G2.sylow_subgroup(3) |
| | >>> S3 = G3.sylow_subgroup(3) |
| | |
| | >>> len1 = len(S1.lower_central_series()) |
| | >>> len2 = len(S2.lower_central_series()) |
| | >>> len3 = len(S3.lower_central_series()) |
| | |
| | >>> len1 == len2 |
| | True |
| | >>> len1 < len3 |
| | True |
| | |
| | ''' |
| | from sympy.combinatorics.homomorphisms import ( |
| | orbit_homomorphism, block_homomorphism) |
| |
|
| | if not isprime(p): |
| | raise ValueError("p must be a prime") |
| |
|
| | def is_p_group(G): |
| | |
| | |
| | m = G.order() |
| | n = 0 |
| | while m % p == 0: |
| | m = m/p |
| | n += 1 |
| | if m == 1: |
| | return True, n |
| | return False, n |
| |
|
| | def _sylow_reduce(mu, nu): |
| | |
| | |
| | |
| | Q = mu.image().sylow_subgroup(p) |
| | Q = mu.invert_subgroup(Q) |
| | nu = nu.restrict_to(Q) |
| | R = nu.image().sylow_subgroup(p) |
| | return nu.invert_subgroup(R) |
| |
|
| | order = self.order() |
| | if order % p != 0: |
| | return PermutationGroup([self.identity]) |
| | p_group, n = is_p_group(self) |
| | if p_group: |
| | return self |
| |
|
| | if self.is_alt_sym(): |
| | return PermutationGroup(self._sylow_alt_sym(p)) |
| |
|
| | |
| | |
| | |
| | orbits = self.orbits() |
| | non_p_orbits = [o for o in orbits if len(o) % p != 0 and len(o) != 1] |
| | if non_p_orbits: |
| | G = self.stabilizer(list(non_p_orbits[0]).pop()) |
| | return G.sylow_subgroup(p) |
| |
|
| | if not self.is_transitive(): |
| | |
| | orbits = sorted(orbits, key=len) |
| | omega1 = orbits.pop() |
| | omega2 = orbits[0].union(*orbits) |
| | mu = orbit_homomorphism(self, omega1) |
| | nu = orbit_homomorphism(self, omega2) |
| | return _sylow_reduce(mu, nu) |
| |
|
| | blocks = self.minimal_blocks() |
| | if len(blocks) > 1: |
| | |
| | mu = block_homomorphism(self, blocks[0]) |
| | nu = block_homomorphism(self, blocks[1]) |
| | return _sylow_reduce(mu, nu) |
| | elif len(blocks) == 1: |
| | block = list(blocks)[0] |
| | if any(e != 0 for e in block): |
| | |
| | mu = block_homomorphism(self, block) |
| | if not is_p_group(mu.image())[0]: |
| | S = mu.image().sylow_subgroup(p) |
| | return mu.invert_subgroup(S).sylow_subgroup(p) |
| |
|
| | |
| | g = self.random() |
| | g_order = g.order() |
| | while g_order % p != 0 or g_order == 0: |
| | g = self.random() |
| | g_order = g.order() |
| | g = g**(g_order // p) |
| | if order % p**2 != 0: |
| | return PermutationGroup(g) |
| |
|
| | C = self.centralizer(g) |
| | while C.order() % p**n != 0: |
| | S = C.sylow_subgroup(p) |
| | s_order = S.order() |
| | Z = S.center() |
| | P = Z._p_elements_group(p) |
| | h = P.random() |
| | C_h = self.centralizer(h) |
| | while C_h.order() % p*s_order != 0: |
| | h = P.random() |
| | C_h = self.centralizer(h) |
| | C = C_h |
| |
|
| | return C.sylow_subgroup(p) |
| |
|
| | def _block_verify(self, L, alpha): |
| | delta = sorted(self.orbit(alpha)) |
| | |
| | |
| | p = [-1]*len(delta) |
| | blocks = [-1]*len(delta) |
| |
|
| | B = [[]] |
| | u = [0]*len(delta) |
| |
|
| | t = L.orbit_transversal(alpha, pairs=True) |
| | for a, beta in t: |
| | B[0].append(a) |
| | i_a = delta.index(a) |
| | p[i_a] = 0 |
| | blocks[i_a] = alpha |
| | u[i_a] = beta |
| |
|
| | rho = 0 |
| | m = 0 |
| |
|
| | while rho <= m: |
| | beta = B[rho][0] |
| | for g in self.generators: |
| | d = beta^g |
| | i_d = delta.index(d) |
| | sigma = p[i_d] |
| | if sigma < 0: |
| | |
| | m += 1 |
| | sigma = m |
| | u[i_d] = u[delta.index(beta)]*g |
| | p[i_d] = sigma |
| | rep = d |
| | blocks[i_d] = rep |
| | newb = [rep] |
| | for gamma in B[rho][1:]: |
| | i_gamma = delta.index(gamma) |
| | d = gamma^g |
| | i_d = delta.index(d) |
| | if p[i_d] < 0: |
| | u[i_d] = u[i_gamma]*g |
| | p[i_d] = sigma |
| | blocks[i_d] = rep |
| | newb.append(d) |
| | else: |
| | |
| | s = u[i_gamma]*g*u[i_d]**(-1) |
| | return False, s |
| |
|
| | B.append(newb) |
| | else: |
| | for h in B[rho][1:]: |
| | if h^g not in B[sigma]: |
| | |
| | s = u[delta.index(beta)]*g*u[i_d]**(-1) |
| | return False, s |
| | rho += 1 |
| |
|
| | return True, blocks |
| |
|
| | def _verify(H, K, phi, z, alpha): |
| | ''' |
| | Return a list of relators ``rels`` in generators ``gens`_h` that |
| | are mapped to ``H.generators`` by ``phi`` so that given a finite |
| | presentation <gens_k | rels_k> of ``K`` on a subset of ``gens_h`` |
| | <gens_h | rels_k + rels> is a finite presentation of ``H``. |
| | |
| | Explanation |
| | =========== |
| | |
| | ``H`` should be generated by the union of ``K.generators`` and ``z`` |
| | (a single generator), and ``H.stabilizer(alpha) == K``; ``phi`` is a |
| | canonical injection from a free group into a permutation group |
| | containing ``H``. |
| | |
| | The algorithm is described in [1], Chapter 6. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import free_group, Permutation, PermutationGroup |
| | >>> from sympy.combinatorics.homomorphisms import homomorphism |
| | >>> from sympy.combinatorics.fp_groups import FpGroup |
| | |
| | >>> H = PermutationGroup(Permutation(0, 2), Permutation (1, 5)) |
| | >>> K = PermutationGroup(Permutation(5)(0, 2)) |
| | >>> F = free_group("x_0 x_1")[0] |
| | >>> gens = F.generators |
| | >>> phi = homomorphism(F, H, F.generators, H.generators) |
| | >>> rels_k = [gens[0]**2] # relators for presentation of K |
| | >>> z= Permutation(1, 5) |
| | >>> check, rels_h = H._verify(K, phi, z, 1) |
| | >>> check |
| | True |
| | >>> rels = rels_k + rels_h |
| | >>> G = FpGroup(F, rels) # presentation of H |
| | >>> G.order() == H.order() |
| | True |
| | |
| | See also |
| | ======== |
| | |
| | strong_presentation, presentation, stabilizer |
| | |
| | ''' |
| |
|
| | orbit = H.orbit(alpha) |
| | beta = alpha^(z**-1) |
| |
|
| | K_beta = K.stabilizer(beta) |
| |
|
| | |
| | gammas = [alpha, beta] |
| | orbits = list({tuple(K_beta.orbit(o)) for o in orbit}) |
| | orbit_reps = [orb[0] for orb in orbits] |
| | for rep in orbit_reps: |
| | if rep not in gammas: |
| | gammas.append(rep) |
| |
|
| | |
| | betas = [alpha, beta] |
| | transversal = {alpha: phi.invert(H.identity), beta: phi.invert(z**-1)} |
| |
|
| | for s, g in K.orbit_transversal(beta, pairs=True): |
| | if s not in transversal: |
| | transversal[s] = transversal[beta]*phi.invert(g) |
| |
|
| |
|
| | union = K.orbit(alpha).union(K.orbit(beta)) |
| | while (len(union) < len(orbit)): |
| | for gamma in gammas: |
| | if gamma in union: |
| | r = gamma^z |
| | if r not in union: |
| | betas.append(r) |
| | transversal[r] = transversal[gamma]*phi.invert(z) |
| | for s, g in K.orbit_transversal(r, pairs=True): |
| | if s not in transversal: |
| | transversal[s] = transversal[r]*phi.invert(g) |
| | union = union.union(K.orbit(r)) |
| | break |
| |
|
| | |
| | rels = [] |
| |
|
| | for b in betas: |
| | k_gens = K.stabilizer(b).generators |
| | for y in k_gens: |
| | new_rel = transversal[b] |
| | gens = K.generator_product(y, original=True) |
| | for g in gens[::-1]: |
| | new_rel = new_rel*phi.invert(g) |
| | new_rel = new_rel*transversal[b]**-1 |
| |
|
| | perm = phi(new_rel) |
| | try: |
| | gens = K.generator_product(perm, original=True) |
| | except ValueError: |
| | return False, perm |
| | for g in gens: |
| | new_rel = new_rel*phi.invert(g)**-1 |
| | if new_rel not in rels: |
| | rels.append(new_rel) |
| |
|
| | for gamma in gammas: |
| | new_rel = transversal[gamma]*phi.invert(z)*transversal[gamma^z]**-1 |
| | perm = phi(new_rel) |
| | try: |
| | gens = K.generator_product(perm, original=True) |
| | except ValueError: |
| | return False, perm |
| | for g in gens: |
| | new_rel = new_rel*phi.invert(g)**-1 |
| | if new_rel not in rels: |
| | rels.append(new_rel) |
| |
|
| | return True, rels |
| |
|
| | def strong_presentation(self): |
| | ''' |
| | Return a strong finite presentation of group. The generators |
| | of the returned group are in the same order as the strong |
| | generators of group. |
| | |
| | The algorithm is based on Sims' Verify algorithm described |
| | in [1], Chapter 6. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> P = DihedralGroup(4) |
| | >>> G = P.strong_presentation() |
| | >>> P.order() == G.order() |
| | True |
| | |
| | See Also |
| | ======== |
| | |
| | presentation, _verify |
| | |
| | ''' |
| | from sympy.combinatorics.fp_groups import (FpGroup, |
| | simplify_presentation) |
| | from sympy.combinatorics.free_groups import free_group |
| | from sympy.combinatorics.homomorphisms import (block_homomorphism, |
| | homomorphism, GroupHomomorphism) |
| |
|
| | strong_gens = self.strong_gens[:] |
| | stabs = self.basic_stabilizers[:] |
| | base = self.base[:] |
| |
|
| | |
| | |
| | gen_syms = [('x_%d'%i) for i in range(len(strong_gens))] |
| | F = free_group(', '.join(gen_syms))[0] |
| | phi = homomorphism(F, self, F.generators, strong_gens) |
| |
|
| | H = PermutationGroup(self.identity) |
| | while stabs: |
| | alpha = base.pop() |
| | K = H |
| | H = stabs.pop() |
| | new_gens = [g for g in H.generators if g not in K] |
| |
|
| | if K.order() == 1: |
| | z = new_gens.pop() |
| | rels = [F.generators[-1]**z.order()] |
| | intermediate_gens = [z] |
| | K = PermutationGroup(intermediate_gens) |
| |
|
| | |
| | while new_gens: |
| | z = new_gens.pop() |
| | intermediate_gens = [z] + intermediate_gens |
| | K_s = PermutationGroup(intermediate_gens) |
| | orbit = K_s.orbit(alpha) |
| | orbit_k = K.orbit(alpha) |
| |
|
| | |
| | if orbit_k == orbit: |
| | if z in K: |
| | rel = phi.invert(z) |
| | perm = z |
| | else: |
| | t = K.orbit_rep(alpha, alpha^z) |
| | rel = phi.invert(z)*phi.invert(t)**-1 |
| | perm = z*t**-1 |
| | for g in K.generator_product(perm, original=True): |
| | rel = rel*phi.invert(g)**-1 |
| | new_rels = [rel] |
| | elif len(orbit_k) == 1: |
| | |
| | |
| | |
| | |
| | |
| | |
| | success, new_rels = K_s._verify(K, phi, z, alpha) |
| | else: |
| | |
| | |
| | check, block = K_s._block_verify(K, alpha) |
| | if check: |
| | |
| | |
| | |
| | |
| | t = block_homomorphism(K_s, block) |
| | m = t.codomain.degree |
| | d = K_s.degree |
| |
|
| | |
| | |
| | |
| | |
| | p = Permutation() |
| | for i in range(m): |
| | p *= Permutation(i, i+d) |
| |
|
| | t_img = t.images |
| | |
| | |
| | images = {g: g*p*t_img[g]*p for g in t_img} |
| | for g in self.strong_gens[:-len(K_s.generators)]: |
| | images[g] = g |
| | K_s_act = PermutationGroup(list(images.values())) |
| | f = GroupHomomorphism(self, K_s_act, images) |
| |
|
| | K_act = PermutationGroup([f(g) for g in K.generators]) |
| | success, new_rels = K_s_act._verify(K_act, f.compose(phi), f(z), d) |
| |
|
| | for n in new_rels: |
| | if n not in rels: |
| | rels.append(n) |
| | K = K_s |
| |
|
| | group = FpGroup(F, rels) |
| | return simplify_presentation(group) |
| |
|
| | def presentation(self, eliminate_gens=True): |
| | ''' |
| | Return an `FpGroup` presentation of the group. |
| | |
| | The algorithm is described in [1], Chapter 6.1. |
| | |
| | ''' |
| | from sympy.combinatorics.fp_groups import (FpGroup, |
| | simplify_presentation) |
| | from sympy.combinatorics.coset_table import CosetTable |
| | from sympy.combinatorics.free_groups import free_group |
| | from sympy.combinatorics.homomorphisms import homomorphism |
| |
|
| | if self._fp_presentation: |
| | return self._fp_presentation |
| |
|
| | def _factor_group_by_rels(G, rels): |
| | if isinstance(G, FpGroup): |
| | rels.extend(G.relators) |
| | return FpGroup(G.free_group, list(set(rels))) |
| | return FpGroup(G, rels) |
| |
|
| | gens = self.generators |
| | len_g = len(gens) |
| |
|
| | if len_g == 1: |
| | order = gens[0].order() |
| | |
| | if order == 1: |
| | return free_group([])[0] |
| | F, x = free_group('x') |
| | return FpGroup(F, [x**order]) |
| |
|
| | if self.order() > 20: |
| | half_gens = self.generators[0:(len_g+1)//2] |
| | else: |
| | half_gens = [] |
| | H = PermutationGroup(half_gens) |
| | H_p = H.presentation() |
| |
|
| | len_h = len(H_p.generators) |
| |
|
| | C = self.coset_table(H) |
| | n = len(C) |
| |
|
| | gen_syms = [('x_%d'%i) for i in range(len(gens))] |
| | F = free_group(', '.join(gen_syms))[0] |
| |
|
| | |
| | images = [F.generators[i] for i in range(len_h)] |
| | R = homomorphism(H_p, F, H_p.generators, images, check=False) |
| |
|
| | |
| | rels = R(H_p.relators) |
| | G_p = FpGroup(F, rels) |
| |
|
| | |
| | T = homomorphism(G_p, self, G_p.generators, gens) |
| |
|
| | C_p = CosetTable(G_p, []) |
| |
|
| | C_p.table = [[None]*(2*len_g) for i in range(n)] |
| |
|
| | |
| | transversal = [None]*n |
| | transversal[0] = G_p.identity |
| |
|
| | |
| | for i in range(2*len_h): |
| | C_p.table[0][i] = 0 |
| |
|
| | gamma = 1 |
| | for alpha, x in product(range(n), range(2*len_g)): |
| | beta = C[alpha][x] |
| | if beta == gamma: |
| | gen = G_p.generators[x//2]**((-1)**(x % 2)) |
| | transversal[beta] = transversal[alpha]*gen |
| | C_p.table[alpha][x] = beta |
| | C_p.table[beta][x + (-1)**(x % 2)] = alpha |
| | gamma += 1 |
| | if gamma == n: |
| | break |
| |
|
| | C_p.p = list(range(n)) |
| | beta = x = 0 |
| |
|
| | while not C_p.is_complete(): |
| | |
| | while C_p.table[beta][x] == C[beta][x]: |
| | x = (x + 1) % (2*len_g) |
| | if x == 0: |
| | beta = (beta + 1) % n |
| |
|
| | |
| | gen = G_p.generators[x//2]**((-1)**(x % 2)) |
| | new_rel = transversal[beta]*gen*transversal[C[beta][x]]**-1 |
| | perm = T(new_rel) |
| | nxt = G_p.identity |
| | for s in H.generator_product(perm, original=True): |
| | nxt = nxt*T.invert(s)**-1 |
| | new_rel = new_rel*nxt |
| |
|
| | |
| | G_p = _factor_group_by_rels(G_p, [new_rel]) |
| | C_p.scan_and_fill(0, new_rel) |
| | C_p = G_p.coset_enumeration([], strategy="coset_table", |
| | draft=C_p, max_cosets=n, incomplete=True) |
| |
|
| | self._fp_presentation = simplify_presentation(G_p) |
| | return self._fp_presentation |
| |
|
| | def polycyclic_group(self): |
| | """ |
| | Return the PolycyclicGroup instance with below parameters: |
| | |
| | Explanation |
| | =========== |
| | |
| | * pc_sequence : Polycyclic sequence is formed by collecting all |
| | the missing generators between the adjacent groups in the |
| | derived series of given permutation group. |
| | |
| | * pc_series : Polycyclic series is formed by adding all the missing |
| | generators of ``der[i+1]`` in ``der[i]``, where ``der`` represents |
| | the derived series. |
| | |
| | * relative_order : A list, computed by the ratio of adjacent groups in |
| | pc_series. |
| | |
| | """ |
| | from sympy.combinatorics.pc_groups import PolycyclicGroup |
| | if not self.is_polycyclic: |
| | raise ValueError("The group must be solvable") |
| |
|
| | der = self.derived_series() |
| | pc_series = [] |
| | pc_sequence = [] |
| | relative_order = [] |
| | pc_series.append(der[-1]) |
| | der.reverse() |
| |
|
| | for i in range(len(der)-1): |
| | H = der[i] |
| | for g in der[i+1].generators: |
| | if g not in H: |
| | H = PermutationGroup([g] + H.generators) |
| | pc_series.insert(0, H) |
| | pc_sequence.insert(0, g) |
| |
|
| | G1 = pc_series[0].order() |
| | G2 = pc_series[1].order() |
| | relative_order.insert(0, G1 // G2) |
| |
|
| | return PolycyclicGroup(pc_sequence, pc_series, relative_order, collector=None) |
| |
|
| |
|
| | def _orbit(degree, generators, alpha, action='tuples'): |
| | r"""Compute the orbit of alpha `\{g(\alpha) | g \in G\}` as a set. |
| | |
| | Explanation |
| | =========== |
| | |
| | The time complexity of the algorithm used here is `O(|Orb|*r)` where |
| | `|Orb|` is the size of the orbit and ``r`` is the number of generators of |
| | the group. For a more detailed analysis, see [1], p.78, [2], pp. 19-21. |
| | Here alpha can be a single point, or a list of points. |
| | |
| | If alpha is a single point, the ordinary orbit is computed. |
| | if alpha is a list of points, there are three available options: |
| | |
| | 'union' - computes the union of the orbits of the points in the list |
| | 'tuples' - computes the orbit of the list interpreted as an ordered |
| | tuple under the group action ( i.e., g((1, 2, 3)) = (g(1), g(2), g(3)) ) |
| | 'sets' - computes the orbit of the list interpreted as a sets |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup |
| | >>> from sympy.combinatorics.perm_groups import _orbit |
| | >>> a = Permutation([1, 2, 0, 4, 5, 6, 3]) |
| | >>> G = PermutationGroup([a]) |
| | >>> _orbit(G.degree, G.generators, 0) |
| | {0, 1, 2} |
| | >>> _orbit(G.degree, G.generators, [0, 4], 'union') |
| | {0, 1, 2, 3, 4, 5, 6} |
| | |
| | See Also |
| | ======== |
| | |
| | orbit, orbit_transversal |
| | |
| | """ |
| | if not hasattr(alpha, '__getitem__'): |
| | alpha = [alpha] |
| |
|
| | gens = [x._array_form for x in generators] |
| | if len(alpha) == 1 or action == 'union': |
| | orb = alpha |
| | used = [False]*degree |
| | for el in alpha: |
| | used[el] = True |
| | for b in orb: |
| | for gen in gens: |
| | temp = gen[b] |
| | if used[temp] == False: |
| | orb.append(temp) |
| | used[temp] = True |
| | return set(orb) |
| | elif action == 'tuples': |
| | alpha = tuple(alpha) |
| | orb = [alpha] |
| | used = {alpha} |
| | for b in orb: |
| | for gen in gens: |
| | temp = tuple([gen[x] for x in b]) |
| | if temp not in used: |
| | orb.append(temp) |
| | used.add(temp) |
| | return set(orb) |
| | elif action == 'sets': |
| | alpha = frozenset(alpha) |
| | orb = [alpha] |
| | used = {alpha} |
| | for b in orb: |
| | for gen in gens: |
| | temp = frozenset([gen[x] for x in b]) |
| | if temp not in used: |
| | orb.append(temp) |
| | used.add(temp) |
| | return {tuple(x) for x in orb} |
| |
|
| |
|
| | def _orbits(degree, generators): |
| | """Compute the orbits of G. |
| | |
| | If ``rep=False`` it returns a list of sets else it returns a list of |
| | representatives of the orbits |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation |
| | >>> from sympy.combinatorics.perm_groups import _orbits |
| | >>> a = Permutation([0, 2, 1]) |
| | >>> b = Permutation([1, 0, 2]) |
| | >>> _orbits(a.size, [a, b]) |
| | [{0, 1, 2}] |
| | """ |
| |
|
| | orbs = [] |
| | sorted_I = list(range(degree)) |
| | I = set(sorted_I) |
| | while I: |
| | i = sorted_I[0] |
| | orb = _orbit(degree, generators, i) |
| | orbs.append(orb) |
| | |
| | I -= orb |
| | sorted_I = [i for i in sorted_I if i not in orb] |
| | return orbs |
| |
|
| |
|
| | def _orbit_transversal(degree, generators, alpha, pairs, af=False, slp=False): |
| | r"""Computes a transversal for the orbit of ``alpha`` as a set. |
| | |
| | Explanation |
| | =========== |
| | |
| | generators generators of the group ``G`` |
| | |
| | For a permutation group ``G``, a transversal for the orbit |
| | `Orb = \{g(\alpha) | g \in G\}` is a set |
| | `\{g_\beta | g_\beta(\alpha) = \beta\}` for `\beta \in Orb`. |
| | Note that there may be more than one possible transversal. |
| | If ``pairs`` is set to ``True``, it returns the list of pairs |
| | `(\beta, g_\beta)`. For a proof of correctness, see [1], p.79 |
| | |
| | if ``af`` is ``True``, the transversal elements are given in |
| | array form. |
| | |
| | If `slp` is `True`, a dictionary `{beta: slp_beta}` is returned |
| | for `\beta \in Orb` where `slp_beta` is a list of indices of the |
| | generators in `generators` s.t. if `slp_beta = [i_1 \dots i_n]` |
| | `g_\beta = generators[i_n] \times \dots \times generators[i_1]`. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> from sympy.combinatorics.perm_groups import _orbit_transversal |
| | >>> G = DihedralGroup(6) |
| | >>> _orbit_transversal(G.degree, G.generators, 0, False) |
| | [(5), (0 1 2 3 4 5), (0 5)(1 4)(2 3), (0 2 4)(1 3 5), (5)(0 4)(1 3), (0 3)(1 4)(2 5)] |
| | """ |
| |
|
| | tr = [(alpha, list(range(degree)))] |
| | slp_dict = {alpha: []} |
| | used = [False]*degree |
| | used[alpha] = True |
| | gens = [x._array_form for x in generators] |
| | for x, px in tr: |
| | px_slp = slp_dict[x] |
| | for gen in gens: |
| | temp = gen[x] |
| | if used[temp] == False: |
| | slp_dict[temp] = [gens.index(gen)] + px_slp |
| | tr.append((temp, _af_rmul(gen, px))) |
| | used[temp] = True |
| | if pairs: |
| | if not af: |
| | tr = [(x, _af_new(y)) for x, y in tr] |
| | if not slp: |
| | return tr |
| | return tr, slp_dict |
| |
|
| | if af: |
| | tr = [y for _, y in tr] |
| | if not slp: |
| | return tr |
| | return tr, slp_dict |
| |
|
| | tr = [_af_new(y) for _, y in tr] |
| | if not slp: |
| | return tr |
| | return tr, slp_dict |
| |
|
| |
|
| | def _stabilizer(degree, generators, alpha): |
| | r"""Return the stabilizer subgroup of ``alpha``. |
| | |
| | Explanation |
| | =========== |
| | |
| | The stabilizer of `\alpha` is the group `G_\alpha = |
| | \{g \in G | g(\alpha) = \alpha\}`. |
| | For a proof of correctness, see [1], p.79. |
| | |
| | degree : degree of G |
| | generators : generators of G |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics.perm_groups import _stabilizer |
| | >>> from sympy.combinatorics.named_groups import DihedralGroup |
| | >>> G = DihedralGroup(6) |
| | >>> _stabilizer(G.degree, G.generators, 5) |
| | [(5)(0 4)(1 3), (5)] |
| | |
| | See Also |
| | ======== |
| | |
| | orbit |
| | |
| | """ |
| | orb = [alpha] |
| | table = {alpha: list(range(degree))} |
| | table_inv = {alpha: list(range(degree))} |
| | used = [False]*degree |
| | used[alpha] = True |
| | gens = [x._array_form for x in generators] |
| | stab_gens = [] |
| | for b in orb: |
| | for gen in gens: |
| | temp = gen[b] |
| | if used[temp] is False: |
| | gen_temp = _af_rmul(gen, table[b]) |
| | orb.append(temp) |
| | table[temp] = gen_temp |
| | table_inv[temp] = _af_invert(gen_temp) |
| | used[temp] = True |
| | else: |
| | schreier_gen = _af_rmuln(table_inv[temp], gen, table[b]) |
| | if schreier_gen not in stab_gens: |
| | stab_gens.append(schreier_gen) |
| | return [_af_new(x) for x in stab_gens] |
| |
|
| |
|
| | PermGroup = PermutationGroup |
| |
|
| |
|
| | class SymmetricPermutationGroup(Basic): |
| | """ |
| | The class defining the lazy form of SymmetricGroup. |
| | |
| | deg : int |
| | |
| | """ |
| | def __new__(cls, deg): |
| | deg = _sympify(deg) |
| | obj = Basic.__new__(cls, deg) |
| | return obj |
| |
|
| | def __init__(self, *args, **kwargs): |
| | self._deg = self.args[0] |
| | self._order = None |
| |
|
| | def __contains__(self, i): |
| | """Return ``True`` if *i* is contained in SymmetricPermutationGroup. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, SymmetricPermutationGroup |
| | >>> G = SymmetricPermutationGroup(4) |
| | >>> Permutation(1, 2, 3) in G |
| | True |
| | |
| | """ |
| | if not isinstance(i, Permutation): |
| | raise TypeError("A SymmetricPermutationGroup contains only Permutations as " |
| | "elements, not elements of type %s" % type(i)) |
| | return i.size == self.degree |
| |
|
| | def order(self): |
| | """ |
| | Return the order of the SymmetricPermutationGroup. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import SymmetricPermutationGroup |
| | >>> G = SymmetricPermutationGroup(4) |
| | >>> G.order() |
| | 24 |
| | """ |
| | if self._order is not None: |
| | return self._order |
| | n = self._deg |
| | self._order = factorial(n) |
| | return self._order |
| |
|
| | @property |
| | def degree(self): |
| | """ |
| | Return the degree of the SymmetricPermutationGroup. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import SymmetricPermutationGroup |
| | >>> G = SymmetricPermutationGroup(4) |
| | >>> G.degree |
| | 4 |
| | |
| | """ |
| | return self._deg |
| |
|
| | @property |
| | def identity(self): |
| | ''' |
| | Return the identity element of the SymmetricPermutationGroup. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import SymmetricPermutationGroup |
| | >>> G = SymmetricPermutationGroup(4) |
| | >>> G.identity() |
| | (3) |
| | |
| | ''' |
| | return _af_new(list(range(self._deg))) |
| |
|
| |
|
| | class Coset(Basic): |
| | """A left coset of a permutation group with respect to an element. |
| | |
| | Parameters |
| | ========== |
| | |
| | g : Permutation |
| | |
| | H : PermutationGroup |
| | |
| | dir : "+" or "-", If not specified by default it will be "+" |
| | here ``dir`` specified the type of coset "+" represent the |
| | right coset and "-" represent the left coset. |
| | |
| | G : PermutationGroup, optional |
| | The group which contains *H* as its subgroup and *g* as its |
| | element. |
| | |
| | If not specified, it would automatically become a symmetric |
| | group ``SymmetricPermutationGroup(g.size)`` and |
| | ``SymmetricPermutationGroup(H.degree)`` if ``g.size`` and ``H.degree`` |
| | are matching.``SymmetricPermutationGroup`` is a lazy form of SymmetricGroup |
| | used for representation purpose. |
| | |
| | """ |
| |
|
| | def __new__(cls, g, H, G=None, dir="+"): |
| | g = _sympify(g) |
| | if not isinstance(g, Permutation): |
| | raise NotImplementedError |
| |
|
| | H = _sympify(H) |
| | if not isinstance(H, PermutationGroup): |
| | raise NotImplementedError |
| |
|
| | if G is not None: |
| | G = _sympify(G) |
| | if not isinstance(G, (PermutationGroup, SymmetricPermutationGroup)): |
| | raise NotImplementedError |
| | if not H.is_subgroup(G): |
| | raise ValueError("{} must be a subgroup of {}.".format(H, G)) |
| | if g not in G: |
| | raise ValueError("{} must be an element of {}.".format(g, G)) |
| | else: |
| | g_size = g.size |
| | h_degree = H.degree |
| | if g_size != h_degree: |
| | raise ValueError( |
| | "The size of the permutation {} and the degree of " |
| | "the permutation group {} should be matching " |
| | .format(g, H)) |
| | G = SymmetricPermutationGroup(g.size) |
| |
|
| | if isinstance(dir, str): |
| | dir = Symbol(dir) |
| | elif not isinstance(dir, Symbol): |
| | raise TypeError("dir must be of type basestring or " |
| | "Symbol, not %s" % type(dir)) |
| | if str(dir) not in ('+', '-'): |
| | raise ValueError("dir must be one of '+' or '-' not %s" % dir) |
| | obj = Basic.__new__(cls, g, H, G, dir) |
| | return obj |
| |
|
| | def __init__(self, *args, **kwargs): |
| | self._dir = self.args[3] |
| |
|
| | @property |
| | def is_left_coset(self): |
| | """ |
| | Check if the coset is left coset that is ``gH``. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup, Coset |
| | >>> a = Permutation(1, 2) |
| | >>> b = Permutation(0, 1) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> cst = Coset(a, G, dir="-") |
| | >>> cst.is_left_coset |
| | True |
| | |
| | """ |
| | return str(self._dir) == '-' |
| |
|
| | @property |
| | def is_right_coset(self): |
| | """ |
| | Check if the coset is right coset that is ``Hg``. |
| | |
| | Examples |
| | ======== |
| | |
| | >>> from sympy.combinatorics import Permutation, PermutationGroup, Coset |
| | >>> a = Permutation(1, 2) |
| | >>> b = Permutation(0, 1) |
| | >>> G = PermutationGroup([a, b]) |
| | >>> cst = Coset(a, G, dir="+") |
| | >>> cst.is_right_coset |
| | True |
| | |
| | """ |
| | return str(self._dir) == '+' |
| |
|
| | def as_list(self): |
| | """ |
| | Return all the elements of coset in the form of list. |
| | """ |
| | g = self.args[0] |
| | H = self.args[1] |
| | cst = [] |
| | if str(self._dir) == '+': |
| | for h in H.elements: |
| | cst.append(h*g) |
| | else: |
| | for h in H.elements: |
| | cst.append(g*h) |
| | return cst |
| |
|