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

Add quotient methods #14981

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
131 changes: 130 additions & 1 deletion sympy/combinatorics/fp_groups.py
Expand Up @@ -514,6 +514,134 @@ def elements(self):
P, T = self._to_perm_group()
return T.invert(P._elements)

def subgroup_quotient(G, H, parent_group=None, homomorphism=False):
'''
Compute the quotient group G/H.
The quotient group is computed on a new FreeGroup when
`G` is a list of the elements of parent_group.

Arguments
=========
G -- A list of `FreeGroupElement`s or an `FpGroup`.
H -- A list of `FreeGroupElement`s or an `FpGroup`.
parent_group -- A group specified when `G` and `H` are given by a list of generators.
homomorphism -- Return a homomorphism whenever `homomorphism` = True

Returns
=======
* When `homomorphism = True`, quotient group along with the homomorphism
from the quotient to the parent_group whose image is isomorphic to G
when G is generated any set of elements of the parent_group
* Only the quotient group in all other cases.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should say "along with a homomorphism from the quotient to the parent group (which is parent_group if specified and G otherwise) whose image is isomorphic to G. As before, G can be generated by any set of elements, not just generators of parent_group. I also think the user should choose whether or not they want a homomorphism, like in subgroup

Copy link
Contributor

Choose a reason for hiding this comment

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

As before, G can be generated by any set of elements, not just generators of parent_group

This still stands. It shouldn't say when G is generated by a proper subset of the generators of the parent_group

Copy link
Contributor

Choose a reason for hiding this comment

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

G is always generated by some elements of the parent group, so there is no need to say anything about it at all. The homomorphism is returned when homomorphism=True, and no other conditions matter

Copy link
Contributor

Choose a reason for hiding this comment

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

This is still relevant


Examples
========
>>> from sympy.combinatorics.fp_groups import FpGroup
>>> from sympy.combinatorics.free_groups import free_group
>>> from sympy.combinatorics.fp_groups import subgroup_quotient

>>> F, x, y = free_group("x, y")
>>> f = FpGroup(F, [x**2, y**3, (x*y)**4])
>>> T = subgroup_quotient(f, [x, y])
>>> T.order() == 1
True

>>> T = subgroup_quotient(f, [x*y**2*x*y, y**2*x*y*x, y**-1])
>>> H = f.subgroup([x*y**2*x*y, y**2*x*y*x, y**-1])
>>> T.order() == f.order()/H.order()
True
Copy link
Contributor

Choose a reason for hiding this comment

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

Examples with using parent_group keyword and getting a homomorphism would be useful


>>> G = [x, y]
>>> H = [x*y**2*x*y, y**2*x*y*x, x*y*x]
>>> T = subgroup_quotient(G, H, parent_group=f)
>>> G = f.subgroup(G)
>>> H = f.subgroup(H)
>>> T.order() == G.order()/H.order()
True
Copy link
Contributor

Choose a reason for hiding this comment

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

You are specifically choosing G to be the same a f here so you can just not compute G as a subgroup and simply use f.order()/H.order(). But generally, as I've said, we should have a test where G is a proper subgroup of the parent group - though it's enough to just do it in the tests. However, it would be good to have an actual example with homomorphism=True, to demonstrate how that feature works

Copy link
Contributor

Choose a reason for hiding this comment

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

You still don't have an example with homomorphism=True


'''
def _get_pres(F, parent_group=None):
if isinstance(F, list) and parent_group:
K, T = parent_group.subgroup(F, homomorphism=True)
f_gens = T(K.generators)
f_rels = T(K.relators)
else:
f_gens = F.generators
f_rels = F.relators
return f_gens, f_rels

def _check(F):
if ((isinstance(F, list) and not all(elem in free_group for elem in F))
or (isinstance(F, FpGroup) and not (F.free_group == free_group))):
raise ValueError("The group elements must belong to the parent group")

# If no parent group is specified,
# G is set to the parent `FpGroup`
if not parent_group:
if isinstance(G, list):
raise ValueError("The parent_group must be"
"defined when the group is a list")
Copy link
Contributor

Choose a reason for hiding this comment

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

This error could go into the previous if statement. E.g.

if not fp_group:
   if isinstance(G, list):
      ...
   else:
       ...

parent_group = G

free_group = parent_group.free_group
Copy link
Contributor

Choose a reason for hiding this comment

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

We don't need this statement - we always use the free group of G

Copy link
Contributor

Choose a reason for hiding this comment

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

Still the case

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But, it has to be defined before the _check function is called.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see. Then, perhaps, this line should move into the definition of _check since it's the only reason we have it?


if not isinstance(parent_group, FpGroup):
raise ValueError("The parent group must be an instance"
"of FpGroup")
Copy link
Contributor

Choose a reason for hiding this comment

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

or FreeGroup. I can't recall of the top of my head if FreeGroups count as FpGroups in SymPy


_check(G)
_check(H)

h_gens, h_rels = _get_pres(H, parent_group=parent_group)
T = None
# return the `FpGroup` on a new `free_group`
if isinstance(G, list):
G, T = parent_group.subgroup(G, homomorphism=True)
h_gens = T.invert(h_gens)
h_rels = T.invert(h_rels)
free_group = G.free_group

g_gens, g_rels = _get_pres(G)

q_relators = list(g_rels) + list(h_gens)
q_group = FpGroup(free_group, q_relators)
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't work when both G and H are proper subgroups of fp_group. For example:

>>> F, a, b, c = free_group("a b c")
>>> G = [a,b]
>>> H = [a]

Then G/H should be isomorphic to a free group on a so it should be <a, b, c | b, c> but q_group will be <a, b, c | b> which is a free group on 2 generators.

What you should really do, is find a presentation of G along with an injection T_G into fp_group, translate the generators of H into elements of this new presentation (btw, this would also let you check if H is a subgroup of G which you should do), then return q_group on the free group of the presentation of G. Does this make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

then return q_group on the free group of the presentation of G

Wouldn't that be a problem if the computed quotients don't belong to the same FreeGroup as that of the FpGroup?


# Return the quotient group with presentation
# <G.generators|q_realtors>
if homomorphism and T:
Copy link
Contributor

Choose a reason for hiding this comment

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

We should really return a homomorphism whenever homomorphism is true, whether or not G is specified as a list but you can define it easily in that case, perhaps by adding an else to if isinstance(G, list) above

Copy link
Contributor

Choose a reason for hiding this comment

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

This is still relevant. The only condition for returning a homomorphism should be homomorphism == True - whether or not T is defined shouldn't matter

Copy link
Contributor Author

Choose a reason for hiding this comment

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

But, T is defined only when G is a list and we need a homomorphism only when the quotient group is defined on another free_group, right?

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that would lead to some strange behaviour. It's true that the homomorphism will be trivial when G == parent_group, but if someone said subgroup_quotient(..., homomorphism=True), then they'd expect to get back a homomorphism. I think it would be strange to be able to say homomorphism=True but not get anything back. We might as well return an identity homomorphism when T isn't defined

T.domain = q_group
return q_group, T
return q_group

def maximal_abelian_quotient(G):
'''
Compute the maximal abelian quotient of an FpGroup.
The quotient group G/[G,G] will be the largest
abelain quotient of `G`.
Here, [G, G] is the commutator subgroup.

Examples
========
>>> from sympy.combinatorics.fp_groups import FpGroup
>>> from sympy.combinatorics.free_groups import free_group
>>> from sympy.combinatorics.fp_groups import maximal_abelian_quotient
>>> F, x, y = free_group("x, y")
>>> f = FpGroup(F, [x**2, y**3, (x*y)**4])
>>> T = maximal_abelian_quotient(f)
>>> T.order()
2
>>> T.is_abelian
True

See Also
========
subgroup_quotient

'''
if not isinstance(G, FpGroup):
raise ValueError("The group must be finitely presented")

return subgroup_quotient(G, G.derived_subgroup())

class FpSubgroup(DefaultPrinting):
'''
Expand Down Expand Up @@ -1294,7 +1422,8 @@ def reidemeister_presentation(fp_grp, H, C=None, homomorphism=False):
define_schreier_generators(C, homomorphism=homomorphism)
reidemeister_relators(C)
gens, rels = C._schreier_generators, C._reidemeister_relators
gens, rels = simplify_presentation(gens, rels, change_gens=True)
if gens:
gens, rels = simplify_presentation(gens, rels, change_gens=True)

C.schreier_generators = tuple(gens)
C.reidemeister_relators = tuple(rels)
Expand Down
108 changes: 76 additions & 32 deletions sympy/combinatorics/homomorphisms.py
Expand Up @@ -352,7 +352,7 @@ def _image(r):
# truth of equality otherwise
success = codomain.make_confluent()
s = codomain.equals(_image(r), identity)
if s in None and not success:
if s is None and not success:
raise RuntimeError("Can't determine if the images "
"define a homomorphism. Try increasing "
"the maximum number of rewriting rules "
Expand Down Expand Up @@ -421,52 +421,58 @@ def block_homomorphism(group, blocks):
H = GroupHomomorphism(group, codomain, images)
return H

def group_isomorphism(G, H, isomorphism=True):
def find_homomorphism(G, H, injective=False, surjective=False, compute=True, all=False):
'''
Compute an isomorphism between 2 given groups.
Compute a homomorphism with required properties between 2 given groups.
An isomorphism is computed when both injective and surjective are set to True.
Copy link
Contributor

Choose a reason for hiding this comment

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

"homomorphism" is shorter than "isomorphism/epimorphism/monomorphism"


Arguments:
G (a finite `FpGroup` or a `PermutationGroup`) -- First group
H (a finite `FpGroup` or a `PermutationGroup`) -- Second group
isomorphism (boolean) -- This is used to avoid the computation of homomorphism
when the user only wants to check if there exists
an isomorphism between the groups.
injective (boolean) -- compute a monomorphism, if possible
surjective (boolean) -- compute an epimorphism, if possible
compute (boolean) -- When set to False, check the existence of a homomorphism,
avoiding computation where possible.
all (boolean) -- compute all possible homomorphisms with specified properties.

Returns:
If isomorphism = False -- Returns a boolean.
If isomorphism = True -- Returns a boolean and an isomorphism between `G` and `H`.
If compute = False -- Return a boolean.
If compute = True -- Return a boolean and a homomorphism with required properties
between `G` and `H`.
Copy link
Contributor

Choose a reason for hiding this comment

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

"homomorphism" instead of "isomorphism/epimorphism/monomorphism"

If all = True -- Return all possible specified homomorphisms as a list.

Summary:
Uses the approach suggested by Robert Tarjan to compute the isomorphism between two groups.
Uses the approach suggested by Robert Tarjan to compute a homomorphism between two groups.
First, the generators of `G` are mapped to the elements of `H` and
we check if the mapping induces an isomorphism.
we check if the mapping induces a homomorphism with required properties.

Examples
========

>>> from sympy.combinatorics import Permutation
>>> from sympy.combinatorics.perm_groups import PermutationGroup
>>> from sympy.combinatorics.free_groups import free_group
>>> from sympy.combinatorics.fp_groups import FpGroup
>>> from sympy.combinatorics.homomorphisms import homomorphism, group_isomorphism
>>> from sympy.combinatorics.homomorphisms import find_homomorphism, group_isomorphism
>>> from sympy.combinatorics.named_groups import DihedralGroup, AlternatingGroup

>>> D = DihedralGroup(8)
>>> p = Permutation(0, 1, 2, 3, 4, 5, 6, 7)
>>> P = PermutationGroup(p)
>>> group_isomorphism(D, P)
>>> find_homomorphism(D, P, injective=True, surjective=True)
(False, None)

>>> F, a, b = free_group("a, b")
>>> G = FpGroup(F, [a**3, b**3, (a*b)**2])
>>> H = AlternatingGroup(4)
>>> (check, T) = group_isomorphism(G, H)
>>> check
>>> list_hom = find_homomorphism(G, H, surjective=True, all=True)
>>> all(elem.is_surjective() for elem in list_hom)
True
>>> T(b*a*b**-1*a**-1*b**-1)
(0 2 3)

'''
if all:
# Compute the list of all possible isomorphisms/epimorphisms/monomorphisms.
list_hom = []

if not isinstance(G, (PermutationGroup, FpGroup)):
raise TypeError("The group must be a PermutationGroup or an FpGroup")
if not isinstance(H, (PermutationGroup, FpGroup)):
Expand All @@ -478,7 +484,7 @@ def group_isomorphism(G, H, isomorphism=True):
# Two infinite FpGroups with the same generators are isomorphic
# when the relators are same but are ordered differently.
if G.generators == H.generators and (G.relators).sort() == (H.relators).sort():
if not isomorphism:
if not compute:
return True
return (True, homomorphism(G, H, G.generators, H.generators))

Expand All @@ -495,12 +501,18 @@ def group_isomorphism(G, H, isomorphism=True):
raise NotImplementedError("Isomorphism methods are not implemented for infinite groups.")
_H, h_isomorphism = H._to_perm_group()

if (g_order != h_order) or (G.is_abelian != H.is_abelian):
if not isomorphism:
return False
return (False, None)

if not isomorphism:
if injective:
if (h_order % g_order != 0) or not G.is_abelian and H.is_abelian:
if not compute:
return False
return (False, None)
if surjective:
if (g_order % h_order != 0) or G.is_abelian and not H.is_abelian:
if not compute:
return False
return (False, None)
Copy link
Contributor

Choose a reason for hiding this comment

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

If you make your keywords injective and surjective, than you'll need an if statement for each of them and G.is_abelian != H.is_abelian could be included as well, though split into parts like this:

if injective:
    if (order stuff) or not G.is_abelian and H.is_abelian:
        #false
if surjective:
    if (order stuff) or G.is_abelian and not H.is_abelian:
        #false

That's because you can't inject a non-abelian group into an abelian one, or have a surjection from an abelian group to a non-abelian one


if (injective and surjective) and not compute:
# Two groups of the same cyclic numbered order
# are isomorphic to each other.
n = g_order
Expand All @@ -512,21 +524,53 @@ def group_isomorphism(G, H, isomorphism=True):
for subset in itertools.permutations(_H, len(gens)):
images = list(subset)
images.extend([_H.identity]*(len(G.generators)-len(images)))
_images = dict(zip(gens,images))
_images = dict(zip(gens, images))
if _check_homomorphism(G, _H, _images):
if isinstance(H, FpGroup):
images = h_isomorphism.invert(images)
T = homomorphism(G, H, G.generators, images, check=False)
if T.is_isomorphism():
# It is a valid isomorphism
if not isomorphism:
return True
return (True, T)
if (injective == T.is_injective()) and (surjective == T.is_surjective()):
if not all:
if not compute:
return True
return True, T
list_hom.append(T)

if all:
return list_hom

if not isomorphism:
if not compute:
return False
return (False, None)

def group_isomorphism(G, H, all=False):
'''
Compute an isomorphism (if possible) between 2 groups.

Arguments:
G (a finite `FpGroup` or a `PermutationGroup`) -- First group
H (a finite `FpGroup` or a `PermutationGroup`) -- Second group
all (boolean) -- compute all possible isomorphisms when set to True.

Examples
========
>>> from sympy.combinatorics.free_groups import free_group
>>> from sympy.combinatorics.fp_groups import FpGroup
>>> from sympy.combinatorics.homomorphisms import group_isomorphism
>>> from sympy.combinatorics.named_groups import AlternatingGroup

>>> F, a, b = free_group("a, b")
>>> G = FpGroup(F, [a**3, b**3, (a*b)**2])
>>> H = AlternatingGroup(4)
>>> (check, T) = group_isomorphism(G, H)
>>> check
True
>>> T(b*a*b**-1*a**-1*b**-1)
(0 2 3)

'''
return find_homomorphism(G, H, injective=True, surjective=True, all=all)

def is_isomorphic(G, H):
'''
Check if the groups are isomorphic to each other
Expand All @@ -537,4 +581,4 @@ def is_isomorphic(G, H):

Returns -- boolean
'''
return group_isomorphism(G, H, isomorphism=False)
return find_homomorphism(G, H, injective=True, surjective=True ,compute=False)
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo with the comma and space: True, compute=

28 changes: 27 additions & 1 deletion sympy/combinatorics/tests/test_fp_groups.py
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
from sympy import S
from sympy.combinatorics.fp_groups import (FpGroup, low_index_subgroups,
reidemeister_presentation, FpSubgroup)
reidemeister_presentation, FpSubgroup,
subgroup_quotient, maximal_abelian_quotient)
from sympy.combinatorics.free_groups import free_group

"""
Expand Down Expand Up @@ -145,6 +146,30 @@ def test_subgroup_presentations():
assert str(gens) == "(b_1, c_3)"
assert len(rels) == 18

def test_subgroup_quotient():
F, x, y = free_group("x, y")
f = FpGroup(F, [x**2, y**3, (x*y)**4])
T = subgroup_quotient(f, [x, y])
H = f.subgroup([x, y])
assert T.order() == f.order()/H.order()

T = subgroup_quotient(f, [x*y**2*x*y, y**2*x*y*x, y**-1])
H = f.subgroup([x*y**2*x*y, y**2*x*y*x, y**-1])
assert T.order() == f.order()/H.order()

G = [x, y]
H = [x*y**2*x*y, y**2*x*y*x, y**-1]
K, T = subgroup_quotient(G, H, parent_group=f, homomorphism=True)
assert T.domain == K
assert T(K.generators) == list(f.generators)
G = f.subgroup(G)
H = f.subgroup(H)
assert K.order() == G.order()/H.order()
Copy link
Contributor

Choose a reason for hiding this comment

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

Mabe, it would be good to have some assert statements for T here, just to see that it is properly defined.


F, x, y = free_group("x, y")
Copy link
Contributor

Choose a reason for hiding this comment

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

This is already defined

T = maximal_abelian_quotient(f)
assert T.is_abelian
assert T.order() == 2

def test_order():
from sympy import S
Expand Down Expand Up @@ -193,6 +218,7 @@ def _test_subgroup(K, T, S):
S = FpSubgroup(f, H)
_test_subgroup(K, T, S)


def test_permutation_methods():
from sympy.combinatorics.fp_groups import FpSubgroup
F, x, y = free_group("x, y")
Expand Down