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
base: master
Are you sure you want to change the base?
Add quotient methods #14981
Changes from 9 commits
8c78128
a3c49fb
5a20fd6
893d661
d6b1f30
b1ddd01
aff6da5
4aef3d0
146ba22
e5297e4
a4defd7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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. | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Examples with using |
||
|
||
>>> 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are specifically choosing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You still don't have an example with |
||
|
||
''' | ||
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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This error could go into the previous
|
||
parent_group = G | ||
|
||
free_group = parent_group.free_group | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Still the case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But, it has to be defined before the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. Then, perhaps, this line should move into the definition of |
||
|
||
if not isinstance(parent_group, FpGroup): | ||
raise ValueError("The parent group must be an instance" | ||
"of FpGroup") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or |
||
|
||
_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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't work when both
Then What you should really do, is find a presentation of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Wouldn't that be a problem if the computed quotients don't belong to the same |
||
|
||
# Return the quotient group with presentation | ||
# <G.generators|q_realtors> | ||
if homomorphism and T: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should really return a homomorphism whenever There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But, There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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): | ||
''' | ||
|
@@ -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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 " | ||
|
@@ -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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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)): | ||
|
@@ -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)) | ||
|
||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you make your keywords
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 | ||
|
@@ -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 | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo with the comma and space: |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
||
""" | ||
|
@@ -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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mabe, it would be good to have some |
||
|
||
F, x, y = free_group("x, y") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
@@ -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") | ||
|
There was a problem hiding this comment.
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 andG
otherwise) whose image is isomorphic toG
. As before,G
can be generated by any set of elements, not just generators ofparent_group
. I also think the user should choose whether or not they want a homomorphism, like insubgroup
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This still stands. It shouldn't say
when G is generated by a proper subset of the generators of the parent_group
There was a problem hiding this comment.
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 whenhomomorphism=True
, and no other conditions matterThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is still relevant