|
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),) |
|
|
|
""" |
|
|
|
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. If not exists, (-1,-1) is returned. |
|
|
|
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) |
|
>>> w = x1**8 |
|
>>> collector.subword_index(word, w) |
|
(-1, -1) |
|
|
|
""" |
|
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 |
|
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 |
|
|