Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Polycyclic Group Class #16991

Merged
merged 28 commits into from Jul 26, 2019
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions sympy/combinatorics/__init__.py
Expand Up @@ -11,3 +11,4 @@
from sympy.combinatorics.graycode import GrayCode
from sympy.combinatorics.named_groups import (SymmetricGroup, DihedralGroup,
CyclicGroup, AlternatingGroup, AbelianGroup, RubikGroup)
from sympy.combinatorics.pc_groups import PolycyclicGroup, Collector
290 changes: 290 additions & 0 deletions sympy/combinatorics/pc_groups.py
@@ -0,0 +1,290 @@
from sympy.core import Basic
from sympy import sieve
from sympy.combinatorics.perm_groups import PermutationGroup
from sympy.printing.defaults import DefaultPrinting

class PolycyclicGroup(Basic):

is_group = True
is_solvable = True

def __init__(self, pcgs_):
self.perm_group = PermutationGroup(pcgs_)
self.pc_series = self._pc_series()
self.pcgs = self._compute_pcgs()

def _pc_series(self):
return self.perm_group.composition_series()

def _compute_pcgs(self):
# computes the generating sequence for polycyclic groups.
series = self.pc_series
pcgs = []
for i in range(len(series)-1):
for g in series[i].generators:
if not g in series[i+1]:
pcgs.append(g)
return pcgs

def relative_orders(self):
rel_orders = []
for i in range(len(self.pc_series)-1):
G = self.pc_series[i]
H = self.pc_series[i+1]
rel_orders.append(G.order()//H.order())
return rel_orders

def is_prime_order(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this method needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not used anywhere, this is a separate method for polycyclic groups.

for order in self.relative_orders():
if order not in sieve:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does isprime compare with sieve (like return all(isprime(order) for order in self.relative_order()))?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For, greater length of relative_order isprime seems a good option.

return False
return True

def length(self):
return len(self.pcgs)

def pc_element_exponent(self, element):
series = self.pc_series
pcgs = self.pcgs
exponent = [0]*len(series)
for i in range(len(series)):
exp = 0
if not element in series[i]:
for j in range(len(pcgs)):
element = (pcgs[j]**-1)*element
exp = exp + 1
if element in series[i]:
exponent[i] = exp
break
return exponent


class Collector(DefaultPrinting):

"""
References
==========

.. [1] Holt, D., Eick, B., O'Brien, E.
"Handbook of Computational Group Theory"
Section 8.1.3
"""

def __init__(self, pc_relators, relative_order, group):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could have group=None as optional parameter. Then there could be something like

if group is None:
    group = FreeGroup(<some symbols>)

It the generators are needed somewhere, they could also be added to Collector:

self.gens = group.generators

but maybe that is not necessary.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't if we are providing pc_relators then group must be provided?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be this can be done with the class PolycyclicGroup, then the group parameter can be omitted from its methods.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be this can be done with the class PolycyclicGroup

But that also I think will not be a good choice!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like there are several possibilities. If pc_relators are given (as they are now) then the group can be found from those relators. (They contain elements of the group.) Alternatively, and maybe preferably, pcgs and relative_order could be given as parameters, and then the pc_relators could be derived from them by internal methods of Collector class. (Most of the code is there already, only differently organized.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pc_relators could be derived from them by internal methods of Collector class.

If not given by user then there is only one way to get the whole pc_relators via pc_presentation. Should there be an additional method to Collector to compute pc_relators given user provides group is a parameter to Collector.
Does it makes sense having pcgs as a parameter to Collector(because it should be provided by user) better will be take the group directly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that pc_presentation should be a collector method. That is where it logically belongs. Then a collector would create the presentation internally. A polycyclic group would receive a ready made collector including the presentation as an attribute. A collector can be created from a pcgs alone. A polycyclic series can be derived from it and relative orders as well (though it may sometimes be more efficient to pass them also as parameters if they have been precomputed).

self.pc_relators = pc_relators
self.relative_order = relative_order
self.group = group

def minimal_uncollected_subwords(self, word):
"""
Returns the minimal uncollected subwords.

Examples
========
>>> from sympy.combinatorics.pc_groups import Collector
>>> from sympy.combinatorics.free_groups import free_group

Example 8.7 Pg. 281 from [1]
>>> F, x1, x2 = free_group("x1, x2")
>>> pc_relators = {x1**2 : (), x1*x2*x1**-1 : x2**-1, x1**-1*x2*x1 : x2**-1}
>>> relative_order = {x1: 2, x2: 3}
>>> word = x2**2*x1**7
>>> group = word.group
>>> collector = Collector(pc_relators, relative_order, group)
>>> collector.minimal_uncollected_subwords(word)
{((x1, 7),): 2, ((x2, 2), (x1, 1)): 0}

"""

# To handle the case word = <identity>
if not word:
return {}
group = self.group
index = {s: i+1 for i, s in enumerate(group.symbols)}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why i+1 instead of i?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, we thought of treating index as relative order that's why I kept this, Now this can be changed!

array = word.array_form
re = self.relative_order
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be a list? For instance, [2, 3, 2, 2] for S(4).

Copy link
Member

@jksuom jksuom Jul 1, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe 0 or None could be used to denote a missing power relation ("infinite relative order").

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, we can do that!

uncollected_subwords = {}

for i in range(len(array)-1):
s1, e1 = array[i]
s2, e2 = array[i+1]
s = ((s1, 1), )
s = group.dtype(s)
if e1 > re[s]-1:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about e1 < 0?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a continue in this if e1 > re[s] -1 so, in all other cases the below e2 section of code will work!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about e1 < 0?

I doubt that e1 < 0 will be uncollected because further this will be mapped to power relation and we can't have negative relative order!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is uncollected if the relative order is finite (Definition 8.14 a). Someone may try to collect words with negative exponents.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But, that will lead to error because power-relators are in form of positive exponent and have a look at example 8.7 if negative-exponents should be considered then that example should have been proceeded further.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the lines 7 - 9 in COLLECTED_WORD on pg. 282.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In example 8.7, the relative order of x2 is infinite.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh! that's right!

# case-2: v = x[i]**a
uncollected_subwords[((s1, e1), )] = 2
continue
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why continue. It will suffice to return a single uncollected word. The other uncollected words (if there are any) may not be there after the one word has been handled.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we do not need to handle x1**3 and x1**4*x0(cases like this) at the same time, once the power will be reduced then we can process that!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that it is better to handle all uncollected minimal subwords as soon as we find them. They may cause extra work later if the power relation is more complicated than x1**3 = 1.


if e2 > 0 and index[s1] > index[s2]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would start by checking that e1 is less than relative order and nonnegative.

# case-0: v = x[i]**a*x[i+1], where index[x[i]] > index[x[i+1]]
uncollected_subwords[((s1, e1), (s2, 1))] = 0

elif e2 < 0 and index[s1] > index[s2]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The index condition is the important part. It should be checked first. Then this line and the previous one could be handled together. For example,

if index[s1] > index[s2]:
    e = 1 if e2 > 0 else -1
    return ((s1, e1), (s2, e))

I don't think it would be necessary to created a dict uncollected_subwords when at most one word is returned at a time. Its case can be easily determined afterwards: If the length of the returned tuple is 1, we have a single ((s1, e1),). Otherwise we have ((s1, e1), (s2, e)), and the case can be seen from the sign of e.

# case-1: v = x[i]**a*x[i+1]*-1, where index[x[i]] > index[x[i+1]]
uncollected_subwords[((s1, e1), (s2, -1))] = 1

i = len(array)-1
s1, e1 = array[i]
s = ((s1, 1), )
s = group.dtype(s)
if e1 > re[s]-1:
# case-2: v = x[i]**a
uncollected_subwords[((s1, e1), )] = 2

return uncollected_subwords

def relations(self):
"""
Separates the given relators of pc presentation in power and
conjugate relations.

Examples
========
>>> from sympy.combinatorics.pc_groups import Collector
>>> from sympy.combinatorics.free_groups import free_group
>>> F, x1, x2 = free_group("x1, x2")
>>> pc_relators = {x1**2 : 1, x1*x2*x1**-1 : x2**-1, x1**-1*x2*x1 : x2**-1}
>>> relative_order = {x1: 2, x2: 3}
>>> word = x2**2*x1**7
>>> group = word.group
>>> collector = Collector(pc_relators, relative_order, group)
>>> power_rel, conj_rel = collector.relations()
>>> power_rel
{x1**2: 1}
>>> conj_rel
{x1**-1*x2*x1: x2**-1, x1*x2*x1**-1: x2**-1}

"""
power_relators = {}
conjugate_relators = {}
for key, value in self.pc_relators.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.

Examples
========
>>> from sympy.combinatorics.pc_groups import Collector
>>> from sympy.combinatorics.free_groups import free_group
>>> F, x1, x2 = free_group("x1, x2")
>>> pc_relators = {x1**2 : 1, x1*x2*x1**-1 : x2**-1, x1**-1*x2*x1 : x2**-1}
>>> relative_order = {x1: 2, x2: 3}
>>> word = x2**2*x1**7
>>> group = word.group
>>> collector = Collector(pc_relators, relative_order, group)
>>> 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):
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some kind of explanation is expected in the docstring.

Examples
========
>>> from sympy.combinatorics.pc_groups import Collector
>>> from sympy.combinatorics.free_groups import free_group
>>> F, x0, x1, x2 = free_group("x0, x1, x2")
>>> pc_relators = {x0**-1*x1*x0: x1**2, x1**-1*x2*x1:x2, x0**-1*x2*x0:x2*x1}
>>> relative_order = {x0: 2, x1: 2, x2: 3}
>>> word = x2**2*x1**7
>>> group = word.group
>>> collector = Collector(pc_relators, relative_order, group)
>>> w = x2*x1
>>> collector.map_relation(w)
x2
>>> w = x1*x0
>>> collector.map_relation(w)
x1**2

"""
group = w.group
gens = list(sorted(w.contains_generators()))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is list needed? sorted should return a list.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I should remove this!

key = gens[0]**-1*gens[1]*gens[0]
return self.pc_relators[key]


def collected_word(self, word):
"""
Examples
========
>>> from sympy.combinatorics.pc_groups import Collector
>>> from sympy.combinatorics.free_groups import free_group
>>> F, x1, x2 = free_group("x1, x2")
>>> pc_relators = {x1**2 : (), x1*x2*x1**-1 : x2**-1, x1**-1*x2*x1 : x2**-1}
>>> relative_order = {x1: 2, x2: 3}
>>> word = x2**2*x1**7
>>> group = word.group
>>> collector = Collector(pc_relators, relative_order, group)
>>> collector.collected_word(word)
x1*x2**-2

"""
group = self.group
while True:
uncollected_subwords = self.minimal_uncollected_subwords(word)
if not uncollected_subwords:
break
w = list(uncollected_subwords)[0]
case = uncollected_subwords[w]
w = group.dtype(w)
low, high = self.subword_index(word, w)
if low == -1:
continue
if case == 0:
gens = list(sorted(w.contains_generators()))
array = w.array_form
s, e = array[0]
word_ = self.map_relation(w)
word_ = gens[0]*word_**e
word_ = group.dtype(word_)
word = word.substituted_word(low, high, word_)

elif case == 1:
gens = list(sorted(w.contains_generators()))
array = w.array_form
s, e = array[0]
word_ = self.map_relation(w)
word_ = (gens[0]**-1)*word_**e
word_ = group.dtype(word_)
word = word.substituted_word(low, high, word_)

else:
array = w.array_form
s, e = array[0]
s1 = ((s, 1), )
s = group.dtype(s1)
re = self.relative_order[s]
q = e//re
r = e-q*re
key = w[0]**re
if self.pc_relators[key]:
word_ = ((w[0], r), (self.pc_relators[key], re))
word_ = group.dtype(word_)
else:
if r != 0:
word_ = w[0]**r
else:
word_ = None
word = word.eliminate_word(w, word_)
return word
6 changes: 6 additions & 0 deletions sympy/combinatorics/perm_groups.py
Expand Up @@ -4515,6 +4515,12 @@ def _factor_group_by_rels(G, rels):
G._fp_presentation = simplify_presentation(G_p)
return G._fp_presentation

def polycyclic_group(self):
from sympy.combinatorics.pc_groups import PolycyclicGroup
if not self.is_polycyclic:
raise ValueError("The group must be solvable")
return PolycyclicGroup(self.generators)


def _orbit(degree, generators, alpha, action='tuples'):
r"""Compute the orbit of alpha `\{g(\alpha) | g \in G\}` as a set.
Expand Down
77 changes: 77 additions & 0 deletions sympy/combinatorics/tests/test_pc_groups.py
@@ -0,0 +1,77 @@
from sympy.combinatorics.pc_groups import PolycyclicGroup, Collector
from sympy.combinatorics.permutations import Permutation
from sympy.combinatorics.free_groups import free_group

def test_collected_word():
F, x0, x1, x2, x3 = free_group("x0, x1, x2, x3")

# Polycyclic relators for SymmetricGroup(4)
pc_relators = { x0**2: (), x1**3: (), x2**2: (), x3**2: (),
x0**-1*x1*x0: x1**2, x0**-1*x2*x0: x2*x3,
x0**-1*x3*x0: x3, x1**-1*x2*x1: x3,
x1**-1*x3*x1: x2*x3, x2**-1*x3*x2: x3
}

word = x3*x2*x1*x0
relative_order = {x0: 2, x1: 3, x2: 2, x3: 2}
group = word.group
collector = Collector(pc_relators, relative_order, group)
collected_word_ = collector.collected_word(word)

assert collected_word_ == x0*x1**2*x2*x3

# Polycyclic Generators of SymmetricGroup(4)
x0 = Permutation(0, 1)
x1 = Permutation(0, 1, 2)
x2 = Permutation(0, 2)(1, 3)
x3 = Permutation(0, 1)(2, 3)

word = x3*x2*x1*x0
collected_word_ = x0*x1**2*x2*x3
assert word == collected_word_



F, x0, x1 = free_group("x0, x1")
# polycyclic relators for Symmetricgroup(3)
pc_relators = {x0**2: (), x1**3: (), x0**-1*x1*x0: x1**2}
relative_order = {x0: 2, x1: 3}
group = F
collector = Collector(pc_relators, relative_order, group)

a = Permutation(0, 1) # x0
b = Permutation(0, 1, 2) # x1

word = x1*x0
assert collector.collected_word(word) == x0*x1**2
assert b*a == a*b**2

word = x1*x0**2
assert collector.collected_word(word) == x1
assert b*a**2 == b

word = x1**2*x0
assert collector.collected_word(word) == x0*x1
assert b**2*a == a*b

word = x1**4*x0**6
assert collector.collected_word(word) == x1
assert b**4*a**6 == b

word = x0*x1
# The word is already collected
assert collector.collected_word(word) == x0*x1
assert a*b == a*b

word = x0**2*x1
assert collector.collected_word(word) == x1
assert a**2*b == b

word = x0**2*x1**3
# Handle Identity case
assert collector.collected_word(word) == F.identity
assert a**2*b**3 == Permutation(2)

word = x1**-2*x0
assert collector.collected_word(word) == x0*x1**-4
assert b**-2*a == a*b**-4