from sympy.ntheory.primetest import isprime from sympy.combinatorics.perm_groups import PermutationGroup from sympy.printing.defaults import DefaultPrinting from sympy.combinatorics.free_groups import free_group class PolycyclicGroup(DefaultPrinting): is_group = True is_solvable = True def __init__(self, pc_sequence, pc_series, relative_order, collector=None): """ Parameters ========== pc_sequence : list A sequence of elements whose classes generate the cyclic factor groups of pc_series. pc_series : list A subnormal sequence of subgroups where each factor group is cyclic. relative_order : list The orders of factor groups of pc_series. collector : Collector By default, it is None. Collector class provides the polycyclic presentation with various other functionalities. """ self.pcgs = pc_sequence self.pc_series = pc_series self.relative_order = relative_order self.collector = Collector(self.pcgs, pc_series, relative_order) if not collector else collector def is_prime_order(self): return all(isprime(order) for order in self.relative_order) def length(self): return len(self.pcgs) class Collector(DefaultPrinting): """ References ========== .. [1] Holt, D., Eick, B., O'Brien, E. "Handbook of Computational Group Theory" Section 8.1.3 """ def __init__(self, pcgs, pc_series, relative_order, free_group_=None, pc_presentation=None): """ Most of the parameters for the Collector class are the same as for PolycyclicGroup. Others are described below. Parameters ========== free_group_ : tuple free_group_ provides the mapping of polycyclic generating sequence with the free group elements. pc_presentation : dict Provides the presentation of polycyclic groups with the help of power and conjugate relators. See Also ======== PolycyclicGroup """ self.pcgs = pcgs self.pc_series = pc_series self.relative_order = relative_order self.free_group = free_group('x:{}'.format(len(pcgs)))[0] if not free_group_ else free_group_ self.index = {s: i for i, s in enumerate(self.free_group.symbols)} self.pc_presentation = self.pc_relators() def minimal_uncollected_subword(self, word): r""" Returns the minimal uncollected subwords. Explanation =========== A word ``v`` defined on generators in ``X`` is a minimal uncollected subword of the word ``w`` if ``v`` is a subword of ``w`` and it has one of the following form * `v = {x_{i+1}}^{a_j}x_i` * `v = {x_{i+1}}^{a_j}{x_i}^{-1}` * `v = {x_i}^{a_j}` for `a_j` not in `\{1, \ldots, s-1\}`. Where, ``s`` is the power exponent of the corresponding generator. Examples ======== >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> from sympy.combinatorics import free_group >>> G = SymmetricGroup(4) >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> F, x1, x2 = free_group("x1, x2") >>> word = x2**2*x1**7 >>> collector.minimal_uncollected_subword(word) ((x2, 2),) """ # To handle the case word = if not word: return None array = word.array_form re = self.relative_order index = self.index for i in range(len(array)): s1, e1 = array[i] if re[index[s1]] and (e1 < 0 or e1 > re[index[s1]]-1): return ((s1, e1), ) for i in range(len(array)-1): s1, e1 = array[i] s2, e2 = array[i+1] if index[s1] > index[s2]: e = 1 if e2 > 0 else -1 return ((s1, e1), (s2, e)) return None def relations(self): """ Separates the given relators of pc presentation in power and conjugate relations. Returns ======= (power_rel, conj_rel) Separates pc presentation into power and conjugate relations. Examples ======== >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> G = SymmetricGroup(3) >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> power_rel, conj_rel = collector.relations() >>> power_rel {x0**2: (), x1**3: ()} >>> conj_rel {x0**-1*x1*x0: x1**2} See Also ======== pc_relators """ power_relators = {} conjugate_relators = {} for key, value in self.pc_presentation.items(): if len(key.array_form) == 1: power_relators[key] = value else: conjugate_relators[key] = value return power_relators, conjugate_relators def subword_index(self, word, w): """ Returns the start and ending index of a given subword in a word. Parameters ========== word : FreeGroupElement word defined on free group elements for a polycyclic group. w : FreeGroupElement subword of a given word, whose starting and ending index to be computed. Returns ======= (i, j) A tuple containing starting and ending index of ``w`` in the given word. Examples ======== >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> from sympy.combinatorics import free_group >>> G = SymmetricGroup(4) >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> F, x1, x2 = free_group("x1, x2") >>> word = x2**2*x1**7 >>> w = x2**2*x1 >>> collector.subword_index(word, w) (0, 3) >>> w = x1**7 >>> collector.subword_index(word, w) (2, 9) """ low = -1 high = -1 for i in range(len(word)-len(w)+1): if word.subword(i, i+len(w)) == w: low = i high = i+len(w) break if low == high == -1: return -1, -1 return low, high def map_relation(self, w): """ Return a conjugate relation. Explanation =========== Given a word formed by two free group elements, the corresponding conjugate relation with those free group elements is formed and mapped with the collected word in the polycyclic presentation. Examples ======== >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> from sympy.combinatorics import free_group >>> G = SymmetricGroup(3) >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> F, x0, x1 = free_group("x0, x1") >>> w = x1*x0 >>> collector.map_relation(w) x1**2 See Also ======== pc_presentation """ array = w.array_form s1 = array[0][0] s2 = array[1][0] key = ((s2, -1), (s1, 1), (s2, 1)) key = self.free_group.dtype(key) return self.pc_presentation[key] def collected_word(self, word): r""" Return the collected form of a word. Explanation =========== A word ``w`` is called collected, if `w = {x_{i_1}}^{a_1} * \ldots * {x_{i_r}}^{a_r}` with `i_1 < i_2< \ldots < i_r` and `a_j` is in `\{1, \ldots, {s_j}-1\}`. Otherwise w is uncollected. Parameters ========== word : FreeGroupElement An uncollected word. Returns ======= word A collected word of form `w = {x_{i_1}}^{a_1}, \ldots, {x_{i_r}}^{a_r}` with `i_1, i_2, \ldots, i_r` and `a_j \in \{1, \ldots, {s_j}-1\}`. Examples ======== >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> from sympy.combinatorics.perm_groups import PermutationGroup >>> from sympy.combinatorics import free_group >>> G = SymmetricGroup(4) >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> F, x0, x1, x2, x3 = free_group("x0, x1, x2, x3") >>> word = x3*x2*x1*x0 >>> collected_word = collector.collected_word(word) >>> free_to_perm = {} >>> free_group = collector.free_group >>> for sym, gen in zip(free_group.symbols, collector.pcgs): ... free_to_perm[sym] = gen >>> G1 = PermutationGroup() >>> for w in word: ... sym = w[0] ... perm = free_to_perm[sym] ... G1 = PermutationGroup([perm] + G1.generators) >>> G2 = PermutationGroup() >>> for w in collected_word: ... sym = w[0] ... perm = free_to_perm[sym] ... G2 = PermutationGroup([perm] + G2.generators) The two are not identical, but they are equivalent: >>> G1.equals(G2), G1 == G2 (True, False) See Also ======== minimal_uncollected_subword """ free_group = self.free_group while True: w = self.minimal_uncollected_subword(word) if not w: break low, high = self.subword_index(word, free_group.dtype(w)) if low == -1: continue s1, e1 = w[0] if len(w) == 1: re = self.relative_order[self.index[s1]] q = e1 // re r = e1-q*re key = ((w[0][0], re), ) key = free_group.dtype(key) if self.pc_presentation[key]: presentation = self.pc_presentation[key].array_form sym, exp = presentation[0] word_ = ((w[0][0], r), (sym, q*exp)) word_ = free_group.dtype(word_) else: if r != 0: word_ = ((w[0][0], r), ) word_ = free_group.dtype(word_) else: word_ = None word = word.eliminate_word(free_group.dtype(w), word_) if len(w) == 2 and w[1][1] > 0: s2, e2 = w[1] s2 = ((s2, 1), ) s2 = free_group.dtype(s2) word_ = self.map_relation(free_group.dtype(w)) word_ = s2*word_**e1 word_ = free_group.dtype(word_) word = word.substituted_word(low, high, word_) elif len(w) == 2 and w[1][1] < 0: s2, e2 = w[1] s2 = ((s2, 1), ) s2 = free_group.dtype(s2) word_ = self.map_relation(free_group.dtype(w)) word_ = s2**-1*word_**e1 word_ = free_group.dtype(word_) word = word.substituted_word(low, high, word_) return word def pc_relators(self): r""" Return the polycyclic presentation. Explanation =========== There are two types of relations used in polycyclic presentation. * Power relations : Power relators are of the form `x_i^{re_i}`, where `i \in \{0, \ldots, \mathrm{len(pcgs)}\}`, ``x`` represents polycyclic generator and ``re`` is the corresponding relative order. * Conjugate relations : Conjugate relators are of the form `x_j^-1x_ix_j`, where `j < i \in \{0, \ldots, \mathrm{len(pcgs)}\}`. Returns ======= A dictionary with power and conjugate relations as key and their collected form as corresponding values. Notes ===== Identity Permutation is mapped with empty ``()``. Examples ======== >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> from sympy.combinatorics.permutations import Permutation >>> S = SymmetricGroup(49).sylow_subgroup(7) >>> der = S.derived_series() >>> G = der[len(der)-2] >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> pcgs = PcGroup.pcgs >>> len(pcgs) 6 >>> free_group = collector.free_group >>> pc_resentation = collector.pc_presentation >>> free_to_perm = {} >>> for s, g in zip(free_group.symbols, pcgs): ... free_to_perm[s] = g >>> for k, v in pc_resentation.items(): ... k_array = k.array_form ... if v != (): ... v_array = v.array_form ... lhs = Permutation() ... for gen in k_array: ... s = gen[0] ... e = gen[1] ... lhs = lhs*free_to_perm[s]**e ... if v == (): ... assert lhs.is_identity ... continue ... rhs = Permutation() ... for gen in v_array: ... s = gen[0] ... e = gen[1] ... rhs = rhs*free_to_perm[s]**e ... assert lhs == rhs """ free_group = self.free_group rel_order = self.relative_order pc_relators = {} perm_to_free = {} pcgs = self.pcgs for gen, s in zip(pcgs, free_group.generators): perm_to_free[gen**-1] = s**-1 perm_to_free[gen] = s pcgs = pcgs[::-1] series = self.pc_series[::-1] rel_order = rel_order[::-1] collected_gens = [] for i, gen in enumerate(pcgs): re = rel_order[i] relation = perm_to_free[gen]**re G = series[i] l = G.generator_product(gen**re, original = True) l.reverse() word = free_group.identity for g in l: word = word*perm_to_free[g] word = self.collected_word(word) pc_relators[relation] = word if word else () self.pc_presentation = pc_relators collected_gens.append(gen) if len(collected_gens) > 1: conj = collected_gens[len(collected_gens)-1] conjugator = perm_to_free[conj] for j in range(len(collected_gens)-1): conjugated = perm_to_free[collected_gens[j]] relation = conjugator**-1*conjugated*conjugator gens = conj**-1*collected_gens[j]*conj l = G.generator_product(gens, original = True) l.reverse() word = free_group.identity for g in l: word = word*perm_to_free[g] word = self.collected_word(word) pc_relators[relation] = word if word else () self.pc_presentation = pc_relators return pc_relators def exponent_vector(self, element): r""" Return the exponent vector of length equal to the length of polycyclic generating sequence. Explanation =========== For a given generator/element ``g`` of the polycyclic group, it can be represented as `g = {x_1}^{e_1}, \ldots, {x_n}^{e_n}`, where `x_i` represents polycyclic generators and ``n`` is the number of generators in the free_group equal to the length of pcgs. Parameters ========== element : Permutation Generator of a polycyclic group. Examples ======== >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> from sympy.combinatorics.permutations import Permutation >>> G = SymmetricGroup(4) >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> pcgs = PcGroup.pcgs >>> collector.exponent_vector(G[0]) [1, 0, 0, 0] >>> exp = collector.exponent_vector(G[1]) >>> g = Permutation() >>> for i in range(len(exp)): ... g = g*pcgs[i]**exp[i] if exp[i] else g >>> assert g == G[1] References ========== .. [1] Holt, D., Eick, B., O'Brien, E. "Handbook of Computational Group Theory" Section 8.1.1, Definition 8.4 """ free_group = self.free_group G = PermutationGroup() for g in self.pcgs: G = PermutationGroup([g] + G.generators) gens = G.generator_product(element, original = True) gens.reverse() perm_to_free = {} for sym, g in zip(free_group.generators, self.pcgs): perm_to_free[g**-1] = sym**-1 perm_to_free[g] = sym w = free_group.identity for g in gens: w = w*perm_to_free[g] word = self.collected_word(w) index = self.index exp_vector = [0]*len(free_group) word = word.array_form for t in word: exp_vector[index[t[0]]] = t[1] return exp_vector def depth(self, element): r""" Return the depth of a given element. Explanation =========== The depth of a given element ``g`` is defined by `\mathrm{dep}[g] = i` if `e_1 = e_2 = \ldots = e_{i-1} = 0` and `e_i != 0`, where ``e`` represents the exponent-vector. Examples ======== >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> G = SymmetricGroup(3) >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> collector.depth(G[0]) 2 >>> collector.depth(G[1]) 1 References ========== .. [1] Holt, D., Eick, B., O'Brien, E. "Handbook of Computational Group Theory" Section 8.1.1, Definition 8.5 """ exp_vector = self.exponent_vector(element) return next((i+1 for i, x in enumerate(exp_vector) if x), len(self.pcgs)+1) def leading_exponent(self, element): r""" Return the leading non-zero exponent. Explanation =========== The leading exponent for a given element `g` is defined by `\mathrm{leading\_exponent}[g]` `= e_i`, if `\mathrm{depth}[g] = i`. Examples ======== >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> G = SymmetricGroup(3) >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> collector.leading_exponent(G[1]) 1 """ exp_vector = self.exponent_vector(element) depth = self.depth(element) if depth != len(self.pcgs)+1: return exp_vector[depth-1] return None def _sift(self, z, g): h = g d = self.depth(h) while d < len(self.pcgs) and z[d-1] != 1: k = z[d-1] e = self.leading_exponent(h)*(self.leading_exponent(k))**-1 e = e % self.relative_order[d-1] h = k**-e*h d = self.depth(h) return h def induced_pcgs(self, gens): """ Parameters ========== gens : list A list of generators on which polycyclic subgroup is to be defined. Examples ======== >>> from sympy.combinatorics.named_groups import SymmetricGroup >>> S = SymmetricGroup(8) >>> G = S.sylow_subgroup(2) >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> gens = [G[0], G[1]] >>> ipcgs = collector.induced_pcgs(gens) >>> [gen.order() for gen in ipcgs] [2, 2, 2] >>> G = S.sylow_subgroup(3) >>> PcGroup = G.polycyclic_group() >>> collector = PcGroup.collector >>> gens = [G[0], G[1]] >>> ipcgs = collector.induced_pcgs(gens) >>> [gen.order() for gen in ipcgs] [3] """ z = [1]*len(self.pcgs) G = gens while G: g = G.pop(0) h = self._sift(z, g) d = self.depth(h) if d < len(self.pcgs): for gen in z: if gen != 1: G.append(h**-1*gen**-1*h*gen) z[d-1] = h; z = [gen for gen in z if gen != 1] return z def constructive_membership_test(self, ipcgs, g): """ Return the exponent vector for induced pcgs. """ e = [0]*len(ipcgs) h = g d = self.depth(h) for i, gen in enumerate(ipcgs): while self.depth(gen) == d: f = self.leading_exponent(h)*self.leading_exponent(gen) f = f % self.relative_order[d-1] h = gen**(-f)*h e[i] = f d = self.depth(h) if h == 1: return e return False