I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,43 @@
from sympy.combinatorics.permutations import Permutation, Cycle
from sympy.combinatorics.prufer import Prufer
from sympy.combinatorics.generators import cyclic, alternating, symmetric, dihedral
from sympy.combinatorics.subsets import Subset
from sympy.combinatorics.partitions import (Partition, IntegerPartition,
RGS_rank, RGS_unrank, RGS_enum)
from sympy.combinatorics.polyhedron import (Polyhedron, tetrahedron, cube,
octahedron, dodecahedron, icosahedron)
from sympy.combinatorics.perm_groups import PermutationGroup, Coset, SymmetricPermutationGroup
from sympy.combinatorics.group_constructs import DirectProduct
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
from sympy.combinatorics.free_groups import free_group
__all__ = [
'Permutation', 'Cycle',
'Prufer',
'cyclic', 'alternating', 'symmetric', 'dihedral',
'Subset',
'Partition', 'IntegerPartition', 'RGS_rank', 'RGS_unrank', 'RGS_enum',
'Polyhedron', 'tetrahedron', 'cube', 'octahedron', 'dodecahedron',
'icosahedron',
'PermutationGroup', 'Coset', 'SymmetricPermutationGroup',
'DirectProduct',
'GrayCode',
'SymmetricGroup', 'DihedralGroup', 'CyclicGroup', 'AlternatingGroup',
'AbelianGroup', 'RubikGroup',
'PolycyclicGroup', 'Collector',
'free_group',
]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,611 @@
r"""
Construct transitive subgroups of symmetric groups, useful in Galois theory.
Besides constructing instances of the :py:class:`~.PermutationGroup` class to
represent the transitive subgroups of $S_n$ for small $n$, this module provides
*names* for these groups.
In some applications, it may be preferable to know the name of a group,
rather than receive an instance of the :py:class:`~.PermutationGroup`
class, and then have to do extra work to determine which group it is, by
checking various properties.
Names are instances of ``Enum`` classes defined in this module. With a name in
hand, the name's ``get_perm_group`` method can then be used to retrieve a
:py:class:`~.PermutationGroup`.
The names used for groups in this module are taken from [1].
References
==========
.. [1] Cohen, H. *A Course in Computational Algebraic Number Theory*.
"""
from collections import defaultdict
from enum import Enum
import itertools
from sympy.combinatorics.named_groups import (
SymmetricGroup, AlternatingGroup, CyclicGroup, DihedralGroup,
set_symmetric_group_properties, set_alternating_group_properties,
)
from sympy.combinatorics.perm_groups import PermutationGroup
from sympy.combinatorics.permutations import Permutation
class S1TransitiveSubgroups(Enum):
"""
Names for the transitive subgroups of S1.
"""
S1 = "S1"
def get_perm_group(self):
return SymmetricGroup(1)
class S2TransitiveSubgroups(Enum):
"""
Names for the transitive subgroups of S2.
"""
S2 = "S2"
def get_perm_group(self):
return SymmetricGroup(2)
class S3TransitiveSubgroups(Enum):
"""
Names for the transitive subgroups of S3.
"""
A3 = "A3"
S3 = "S3"
def get_perm_group(self):
if self == S3TransitiveSubgroups.A3:
return AlternatingGroup(3)
elif self == S3TransitiveSubgroups.S3:
return SymmetricGroup(3)
class S4TransitiveSubgroups(Enum):
"""
Names for the transitive subgroups of S4.
"""
C4 = "C4"
V = "V"
D4 = "D4"
A4 = "A4"
S4 = "S4"
def get_perm_group(self):
if self == S4TransitiveSubgroups.C4:
return CyclicGroup(4)
elif self == S4TransitiveSubgroups.V:
return four_group()
elif self == S4TransitiveSubgroups.D4:
return DihedralGroup(4)
elif self == S4TransitiveSubgroups.A4:
return AlternatingGroup(4)
elif self == S4TransitiveSubgroups.S4:
return SymmetricGroup(4)
class S5TransitiveSubgroups(Enum):
"""
Names for the transitive subgroups of S5.
"""
C5 = "C5"
D5 = "D5"
M20 = "M20"
A5 = "A5"
S5 = "S5"
def get_perm_group(self):
if self == S5TransitiveSubgroups.C5:
return CyclicGroup(5)
elif self == S5TransitiveSubgroups.D5:
return DihedralGroup(5)
elif self == S5TransitiveSubgroups.M20:
return M20()
elif self == S5TransitiveSubgroups.A5:
return AlternatingGroup(5)
elif self == S5TransitiveSubgroups.S5:
return SymmetricGroup(5)
class S6TransitiveSubgroups(Enum):
"""
Names for the transitive subgroups of S6.
"""
C6 = "C6"
S3 = "S3"
D6 = "D6"
A4 = "A4"
G18 = "G18"
A4xC2 = "A4 x C2"
S4m = "S4-"
S4p = "S4+"
G36m = "G36-"
G36p = "G36+"
S4xC2 = "S4 x C2"
PSL2F5 = "PSL2(F5)"
G72 = "G72"
PGL2F5 = "PGL2(F5)"
A6 = "A6"
S6 = "S6"
def get_perm_group(self):
if self == S6TransitiveSubgroups.C6:
return CyclicGroup(6)
elif self == S6TransitiveSubgroups.S3:
return S3_in_S6()
elif self == S6TransitiveSubgroups.D6:
return DihedralGroup(6)
elif self == S6TransitiveSubgroups.A4:
return A4_in_S6()
elif self == S6TransitiveSubgroups.G18:
return G18()
elif self == S6TransitiveSubgroups.A4xC2:
return A4xC2()
elif self == S6TransitiveSubgroups.S4m:
return S4m()
elif self == S6TransitiveSubgroups.S4p:
return S4p()
elif self == S6TransitiveSubgroups.G36m:
return G36m()
elif self == S6TransitiveSubgroups.G36p:
return G36p()
elif self == S6TransitiveSubgroups.S4xC2:
return S4xC2()
elif self == S6TransitiveSubgroups.PSL2F5:
return PSL2F5()
elif self == S6TransitiveSubgroups.G72:
return G72()
elif self == S6TransitiveSubgroups.PGL2F5:
return PGL2F5()
elif self == S6TransitiveSubgroups.A6:
return AlternatingGroup(6)
elif self == S6TransitiveSubgroups.S6:
return SymmetricGroup(6)
def four_group():
"""
Return a representation of the Klein four-group as a transitive subgroup
of S4.
"""
return PermutationGroup(
Permutation(0, 1)(2, 3),
Permutation(0, 2)(1, 3)
)
def M20():
"""
Return a representation of the metacyclic group M20, a transitive subgroup
of S5 that is one of the possible Galois groups for polys of degree 5.
Notes
=====
See [1], Page 323.
"""
G = PermutationGroup(Permutation(0, 1, 2, 3, 4), Permutation(1, 2, 4, 3))
G._degree = 5
G._order = 20
G._is_transitive = True
G._is_sym = False
G._is_alt = False
G._is_cyclic = False
G._is_dihedral = False
return G
def S3_in_S6():
"""
Return a representation of S3 as a transitive subgroup of S6.
Notes
=====
The representation is found by viewing the group as the symmetries of a
triangular prism.
"""
G = PermutationGroup(Permutation(0, 1, 2)(3, 4, 5), Permutation(0, 3)(2, 4)(1, 5))
set_symmetric_group_properties(G, 3, 6)
return G
def A4_in_S6():
"""
Return a representation of A4 as a transitive subgroup of S6.
Notes
=====
This was computed using :py:func:`~.find_transitive_subgroups_of_S6`.
"""
G = PermutationGroup(Permutation(0, 4, 5)(1, 3, 2), Permutation(0, 1, 2)(3, 5, 4))
set_alternating_group_properties(G, 4, 6)
return G
def S4m():
"""
Return a representation of the S4- transitive subgroup of S6.
Notes
=====
This was computed using :py:func:`~.find_transitive_subgroups_of_S6`.
"""
G = PermutationGroup(Permutation(1, 4, 5, 3), Permutation(0, 4)(1, 5)(2, 3))
set_symmetric_group_properties(G, 4, 6)
return G
def S4p():
"""
Return a representation of the S4+ transitive subgroup of S6.
Notes
=====
This was computed using :py:func:`~.find_transitive_subgroups_of_S6`.
"""
G = PermutationGroup(Permutation(0, 2, 4, 1)(3, 5), Permutation(0, 3)(4, 5))
set_symmetric_group_properties(G, 4, 6)
return G
def A4xC2():
"""
Return a representation of the (A4 x C2) transitive subgroup of S6.
Notes
=====
This was computed using :py:func:`~.find_transitive_subgroups_of_S6`.
"""
return PermutationGroup(
Permutation(0, 4, 5)(1, 3, 2), Permutation(0, 1, 2)(3, 5, 4),
Permutation(5)(2, 4))
def S4xC2():
"""
Return a representation of the (S4 x C2) transitive subgroup of S6.
Notes
=====
This was computed using :py:func:`~.find_transitive_subgroups_of_S6`.
"""
return PermutationGroup(
Permutation(1, 4, 5, 3), Permutation(0, 4)(1, 5)(2, 3),
Permutation(1, 4)(3, 5))
def G18():
"""
Return a representation of the group G18, a transitive subgroup of S6
isomorphic to the semidirect product of C3^2 with C2.
Notes
=====
This was computed using :py:func:`~.find_transitive_subgroups_of_S6`.
"""
return PermutationGroup(
Permutation(5)(0, 1, 2), Permutation(3, 4, 5),
Permutation(0, 4)(1, 5)(2, 3))
def G36m():
"""
Return a representation of the group G36-, a transitive subgroup of S6
isomorphic to the semidirect product of C3^2 with C2^2.
Notes
=====
This was computed using :py:func:`~.find_transitive_subgroups_of_S6`.
"""
return PermutationGroup(
Permutation(5)(0, 1, 2), Permutation(3, 4, 5),
Permutation(1, 2)(3, 5), Permutation(0, 4)(1, 5)(2, 3))
def G36p():
"""
Return a representation of the group G36+, a transitive subgroup of S6
isomorphic to the semidirect product of C3^2 with C4.
Notes
=====
This was computed using :py:func:`~.find_transitive_subgroups_of_S6`.
"""
return PermutationGroup(
Permutation(5)(0, 1, 2), Permutation(3, 4, 5),
Permutation(0, 5, 2, 3)(1, 4))
def G72():
"""
Return a representation of the group G72, a transitive subgroup of S6
isomorphic to the semidirect product of C3^2 with D4.
Notes
=====
See [1], Page 325.
"""
return PermutationGroup(
Permutation(5)(0, 1, 2),
Permutation(0, 4, 1, 3)(2, 5), Permutation(0, 3)(1, 4)(2, 5))
def PSL2F5():
r"""
Return a representation of the group $PSL_2(\mathbb{F}_5)$, as a transitive
subgroup of S6, isomorphic to $A_5$.
Notes
=====
This was computed using :py:func:`~.find_transitive_subgroups_of_S6`.
"""
G = PermutationGroup(
Permutation(0, 4, 5)(1, 3, 2), Permutation(0, 4, 3, 1, 5))
set_alternating_group_properties(G, 5, 6)
return G
def PGL2F5():
r"""
Return a representation of the group $PGL_2(\mathbb{F}_5)$, as a transitive
subgroup of S6, isomorphic to $S_5$.
Notes
=====
See [1], Page 325.
"""
G = PermutationGroup(
Permutation(0, 1, 2, 3, 4), Permutation(0, 5)(1, 2)(3, 4))
set_symmetric_group_properties(G, 5, 6)
return G
def find_transitive_subgroups_of_S6(*targets, print_report=False):
r"""
Search for certain transitive subgroups of $S_6$.
The symmetric group $S_6$ has 16 different transitive subgroups, up to
conjugacy. Some are more easily constructed than others. For example, the
dihedral group $D_6$ is immediately found, but it is not at all obvious how
to realize $S_4$ or $S_5$ *transitively* within $S_6$.
In some cases there are well-known constructions that can be used. For
example, $S_5$ is isomorphic to $PGL_2(\mathbb{F}_5)$, which acts in a
natural way on the projective line $P^1(\mathbb{F}_5)$, a set of order 6.
In absence of such special constructions however, we can simply search for
generators. For example, transitive instances of $A_4$ and $S_4$ can be
found within $S_6$ in this way.
Once we are engaged in such searches, it may then be easier (if less
elegant) to find even those groups like $S_5$ that do have special
constructions, by mere search.
This function locates generators for transitive instances in $S_6$ of the
following subgroups:
* $A_4$
* $S_4^-$ ($S_4$ not contained within $A_6$)
* $S_4^+$ ($S_4$ contained within $A_6$)
* $A_4 \times C_2$
* $S_4 \times C_2$
* $G_{18} = C_3^2 \rtimes C_2$
* $G_{36}^- = C_3^2 \rtimes C_2^2$
* $G_{36}^+ = C_3^2 \rtimes C_4$
* $G_{72} = C_3^2 \rtimes D_4$
* $A_5$
* $S_5$
Note: Each of these groups also has a dedicated function in this module
that returns the group immediately, using generators that were found by
this search procedure.
The search procedure serves as a record of how these generators were
found. Also, due to randomness in the generation of the elements of
permutation groups, it can be called again, in order to (probably) get
different generators for the same groups.
Parameters
==========
targets : list of :py:class:`~.S6TransitiveSubgroups` values
The groups you want to find.
print_report : bool (default False)
If True, print to stdout the generators found for each group.
Returns
=======
dict
mapping each name in *targets* to the :py:class:`~.PermutationGroup`
that was found
References
==========
.. [2] https://en.wikipedia.org/wiki/Projective_linear_group#Exceptional_isomorphisms
.. [3] https://en.wikipedia.org/wiki/Automorphisms_of_the_symmetric_and_alternating_groups#PGL%282,5%29
"""
def elts_by_order(G):
"""Sort the elements of a group by their order. """
elts = defaultdict(list)
for g in G.elements:
elts[g.order()].append(g)
return elts
def order_profile(G, name=None):
"""Determine how many elements a group has, of each order. """
elts = elts_by_order(G)
profile = {o:len(e) for o, e in elts.items()}
if name:
print(f'{name}: ' + ' '.join(f'{len(profile[r])}@{r}' for r in sorted(profile.keys())))
return profile
S6 = SymmetricGroup(6)
A6 = AlternatingGroup(6)
S6_by_order = elts_by_order(S6)
def search(existing_gens, needed_gen_orders, order, alt=None, profile=None, anti_profile=None):
"""
Find a transitive subgroup of S6.
Parameters
==========
existing_gens : list of Permutation
Optionally empty list of generators that must be in the group.
needed_gen_orders : list of positive int
Nonempty list of the orders of the additional generators that are
to be found.
order: int
The order of the group being sought.
alt: bool, None
If True, require the group to be contained in A6.
If False, require the group not to be contained in A6.
profile : dict
If given, the group's order profile must equal this.
anti_profile : dict
If given, the group's order profile must *not* equal this.
"""
for gens in itertools.product(*[S6_by_order[n] for n in needed_gen_orders]):
if len(set(gens)) < len(gens):
continue
G = PermutationGroup(existing_gens + list(gens))
if G.order() == order and G.is_transitive():
if alt is not None and G.is_subgroup(A6) != alt:
continue
if profile and order_profile(G) != profile:
continue
if anti_profile and order_profile(G) == anti_profile:
continue
return G
def match_known_group(G, alt=None):
needed = [g.order() for g in G.generators]
return search([], needed, G.order(), alt=alt, profile=order_profile(G))
found = {}
def finish_up(name, G):
found[name] = G
if print_report:
print("=" * 40)
print(f"{name}:")
print(G.generators)
if S6TransitiveSubgroups.A4 in targets or S6TransitiveSubgroups.A4xC2 in targets:
A4_in_S6 = match_known_group(AlternatingGroup(4))
finish_up(S6TransitiveSubgroups.A4, A4_in_S6)
if S6TransitiveSubgroups.S4m in targets or S6TransitiveSubgroups.S4xC2 in targets:
S4m_in_S6 = match_known_group(SymmetricGroup(4), alt=False)
finish_up(S6TransitiveSubgroups.S4m, S4m_in_S6)
if S6TransitiveSubgroups.S4p in targets:
S4p_in_S6 = match_known_group(SymmetricGroup(4), alt=True)
finish_up(S6TransitiveSubgroups.S4p, S4p_in_S6)
if S6TransitiveSubgroups.A4xC2 in targets:
A4xC2_in_S6 = search(A4_in_S6.generators, [2], 24, anti_profile=order_profile(SymmetricGroup(4)))
finish_up(S6TransitiveSubgroups.A4xC2, A4xC2_in_S6)
if S6TransitiveSubgroups.S4xC2 in targets:
S4xC2_in_S6 = search(S4m_in_S6.generators, [2], 48)
finish_up(S6TransitiveSubgroups.S4xC2, S4xC2_in_S6)
# For the normal factor N = C3^2 in any of the G_n subgroups, we take one
# obvious instance of C3^2 in S6:
N_gens = [Permutation(5)(0, 1, 2), Permutation(5)(3, 4, 5)]
if S6TransitiveSubgroups.G18 in targets:
G18_in_S6 = search(N_gens, [2], 18)
finish_up(S6TransitiveSubgroups.G18, G18_in_S6)
if S6TransitiveSubgroups.G36m in targets:
G36m_in_S6 = search(N_gens, [2, 2], 36, alt=False)
finish_up(S6TransitiveSubgroups.G36m, G36m_in_S6)
if S6TransitiveSubgroups.G36p in targets:
G36p_in_S6 = search(N_gens, [4], 36, alt=True)
finish_up(S6TransitiveSubgroups.G36p, G36p_in_S6)
if S6TransitiveSubgroups.G72 in targets:
G72_in_S6 = search(N_gens, [4, 2], 72)
finish_up(S6TransitiveSubgroups.G72, G72_in_S6)
# The PSL2(F5) and PGL2(F5) subgroups are isomorphic to A5 and S5, resp.
if S6TransitiveSubgroups.PSL2F5 in targets:
PSL2F5_in_S6 = match_known_group(AlternatingGroup(5))
finish_up(S6TransitiveSubgroups.PSL2F5, PSL2F5_in_S6)
if S6TransitiveSubgroups.PGL2F5 in targets:
PGL2F5_in_S6 = match_known_group(SymmetricGroup(5))
finish_up(S6TransitiveSubgroups.PGL2F5, PGL2F5_in_S6)
# There is little need to "search" for any of the groups C6, S3, D6, A6,
# or S6, since they all have obvious realizations within S6. However, we
# support them here just in case a random representation is desired.
if S6TransitiveSubgroups.C6 in targets:
C6 = match_known_group(CyclicGroup(6))
finish_up(S6TransitiveSubgroups.C6, C6)
if S6TransitiveSubgroups.S3 in targets:
S3 = match_known_group(SymmetricGroup(3))
finish_up(S6TransitiveSubgroups.S3, S3)
if S6TransitiveSubgroups.D6 in targets:
D6 = match_known_group(DihedralGroup(6))
finish_up(S6TransitiveSubgroups.D6, D6)
if S6TransitiveSubgroups.A6 in targets:
A6 = match_known_group(A6)
finish_up(S6TransitiveSubgroups.A6, A6)
if S6TransitiveSubgroups.S6 in targets:
S6 = match_known_group(S6)
finish_up(S6TransitiveSubgroups.S6, S6)
return found

View File

@ -0,0 +1,302 @@
from sympy.combinatorics.permutations import Permutation
from sympy.core.symbol import symbols
from sympy.matrices import Matrix
from sympy.utilities.iterables import variations, rotate_left
def symmetric(n):
"""
Generates the symmetric group of order n, Sn.
Examples
========
>>> from sympy.combinatorics.generators import symmetric
>>> list(symmetric(3))
[(2), (1 2), (2)(0 1), (0 1 2), (0 2 1), (0 2)]
"""
for perm in variations(range(n), n):
yield Permutation(perm)
def cyclic(n):
"""
Generates the cyclic group of order n, Cn.
Examples
========
>>> from sympy.combinatorics.generators import cyclic
>>> list(cyclic(5))
[(4), (0 1 2 3 4), (0 2 4 1 3),
(0 3 1 4 2), (0 4 3 2 1)]
See Also
========
dihedral
"""
gen = list(range(n))
for i in range(n):
yield Permutation(gen)
gen = rotate_left(gen, 1)
def alternating(n):
"""
Generates the alternating group of order n, An.
Examples
========
>>> from sympy.combinatorics.generators import alternating
>>> list(alternating(3))
[(2), (0 1 2), (0 2 1)]
"""
for perm in variations(range(n), n):
p = Permutation(perm)
if p.is_even:
yield p
def dihedral(n):
"""
Generates the dihedral group of order 2n, Dn.
The result is given as a subgroup of Sn, except for the special cases n=1
(the group S2) and n=2 (the Klein 4-group) where that's not possible
and embeddings in S2 and S4 respectively are given.
Examples
========
>>> from sympy.combinatorics.generators import dihedral
>>> list(dihedral(3))
[(2), (0 2), (0 1 2), (1 2), (0 2 1), (2)(0 1)]
See Also
========
cyclic
"""
if n == 1:
yield Permutation([0, 1])
yield Permutation([1, 0])
elif n == 2:
yield Permutation([0, 1, 2, 3])
yield Permutation([1, 0, 3, 2])
yield Permutation([2, 3, 0, 1])
yield Permutation([3, 2, 1, 0])
else:
gen = list(range(n))
for i in range(n):
yield Permutation(gen)
yield Permutation(gen[::-1])
gen = rotate_left(gen, 1)
def rubik_cube_generators():
"""Return the permutations of the 3x3 Rubik's cube, see
https://www.gap-system.org/Doc/Examples/rubik.html
"""
a = [
[(1, 3, 8, 6), (2, 5, 7, 4), (9, 33, 25, 17), (10, 34, 26, 18),
(11, 35, 27, 19)],
[(9, 11, 16, 14), (10, 13, 15, 12), (1, 17, 41, 40), (4, 20, 44, 37),
(6, 22, 46, 35)],
[(17, 19, 24, 22), (18, 21, 23, 20), (6, 25, 43, 16), (7, 28, 42, 13),
(8, 30, 41, 11)],
[(25, 27, 32, 30), (26, 29, 31, 28), (3, 38, 43, 19), (5, 36, 45, 21),
(8, 33, 48, 24)],
[(33, 35, 40, 38), (34, 37, 39, 36), (3, 9, 46, 32), (2, 12, 47, 29),
(1, 14, 48, 27)],
[(41, 43, 48, 46), (42, 45, 47, 44), (14, 22, 30, 38),
(15, 23, 31, 39), (16, 24, 32, 40)]
]
return [Permutation([[i - 1 for i in xi] for xi in x], size=48) for x in a]
def rubik(n):
"""Return permutations for an nxn Rubik's cube.
Permutations returned are for rotation of each of the slice
from the face up to the last face for each of the 3 sides (in this order):
front, right and bottom. Hence, the first n - 1 permutations are for the
slices from the front.
"""
if n < 2:
raise ValueError('dimension of cube must be > 1')
# 1-based reference to rows and columns in Matrix
def getr(f, i):
return faces[f].col(n - i)
def getl(f, i):
return faces[f].col(i - 1)
def getu(f, i):
return faces[f].row(i - 1)
def getd(f, i):
return faces[f].row(n - i)
def setr(f, i, s):
faces[f][:, n - i] = Matrix(n, 1, s)
def setl(f, i, s):
faces[f][:, i - 1] = Matrix(n, 1, s)
def setu(f, i, s):
faces[f][i - 1, :] = Matrix(1, n, s)
def setd(f, i, s):
faces[f][n - i, :] = Matrix(1, n, s)
# motion of a single face
def cw(F, r=1):
for _ in range(r):
face = faces[F]
rv = []
for c in range(n):
for r in range(n - 1, -1, -1):
rv.append(face[r, c])
faces[F] = Matrix(n, n, rv)
def ccw(F):
cw(F, 3)
# motion of plane i from the F side;
# fcw(0) moves the F face, fcw(1) moves the plane
# just behind the front face, etc...
def fcw(i, r=1):
for _ in range(r):
if i == 0:
cw(F)
i += 1
temp = getr(L, i)
setr(L, i, list(getu(D, i)))
setu(D, i, list(reversed(getl(R, i))))
setl(R, i, list(getd(U, i)))
setd(U, i, list(reversed(temp)))
i -= 1
def fccw(i):
fcw(i, 3)
# motion of the entire cube from the F side
def FCW(r=1):
for _ in range(r):
cw(F)
ccw(B)
cw(U)
t = faces[U]
cw(L)
faces[U] = faces[L]
cw(D)
faces[L] = faces[D]
cw(R)
faces[D] = faces[R]
faces[R] = t
def FCCW():
FCW(3)
# motion of the entire cube from the U side
def UCW(r=1):
for _ in range(r):
cw(U)
ccw(D)
t = faces[F]
faces[F] = faces[R]
faces[R] = faces[B]
faces[B] = faces[L]
faces[L] = t
def UCCW():
UCW(3)
# defining the permutations for the cube
U, F, R, B, L, D = names = symbols('U, F, R, B, L, D')
# the faces are represented by nxn matrices
faces = {}
count = 0
for fi in range(6):
f = []
for a in range(n**2):
f.append(count)
count += 1
faces[names[fi]] = Matrix(n, n, f)
# this will either return the value of the current permutation
# (show != 1) or else append the permutation to the group, g
def perm(show=0):
# add perm to the list of perms
p = []
for f in names:
p.extend(faces[f])
if show:
return p
g.append(Permutation(p))
g = [] # container for the group's permutations
I = list(range(6*n**2)) # the identity permutation used for checking
# define permutations corresponding to cw rotations of the planes
# up TO the last plane from that direction; by not including the
# last plane, the orientation of the cube is maintained.
# F slices
for i in range(n - 1):
fcw(i)
perm()
fccw(i) # restore
assert perm(1) == I
# R slices
# bring R to front
UCW()
for i in range(n - 1):
fcw(i)
# put it back in place
UCCW()
# record
perm()
# restore
# bring face to front
UCW()
fccw(i)
# restore
UCCW()
assert perm(1) == I
# D slices
# bring up bottom
FCW()
UCCW()
FCCW()
for i in range(n - 1):
# turn strip
fcw(i)
# put bottom back on the bottom
FCW()
UCW()
FCCW()
# record
perm()
# restore
# bring up bottom
FCW()
UCCW()
FCCW()
# turn strip
fccw(i)
# put bottom back on the bottom
FCW()
UCW()
FCCW()
assert perm(1) == I
return g

View File

@ -0,0 +1,430 @@
from sympy.core import Basic, Integer
import random
class GrayCode(Basic):
"""
A Gray code is essentially a Hamiltonian walk on
a n-dimensional cube with edge length of one.
The vertices of the cube are represented by vectors
whose values are binary. The Hamilton walk visits
each vertex exactly once. The Gray code for a 3d
cube is ['000','100','110','010','011','111','101',
'001'].
A Gray code solves the problem of sequentially
generating all possible subsets of n objects in such
a way that each subset is obtained from the previous
one by either deleting or adding a single object.
In the above example, 1 indicates that the object is
present, and 0 indicates that its absent.
Gray codes have applications in statistics as well when
we want to compute various statistics related to subsets
in an efficient manner.
Examples
========
>>> from sympy.combinatorics import GrayCode
>>> a = GrayCode(3)
>>> list(a.generate_gray())
['000', '001', '011', '010', '110', '111', '101', '100']
>>> a = GrayCode(4)
>>> list(a.generate_gray())
['0000', '0001', '0011', '0010', '0110', '0111', '0101', '0100', \
'1100', '1101', '1111', '1110', '1010', '1011', '1001', '1000']
References
==========
.. [1] Nijenhuis,A. and Wilf,H.S.(1978).
Combinatorial Algorithms. Academic Press.
.. [2] Knuth, D. (2011). The Art of Computer Programming, Vol 4
Addison Wesley
"""
_skip = False
_current = 0
_rank = None
def __new__(cls, n, *args, **kw_args):
"""
Default constructor.
It takes a single argument ``n`` which gives the dimension of the Gray
code. The starting Gray code string (``start``) or the starting ``rank``
may also be given; the default is to start at rank = 0 ('0...0').
Examples
========
>>> from sympy.combinatorics import GrayCode
>>> a = GrayCode(3)
>>> a
GrayCode(3)
>>> a.n
3
>>> a = GrayCode(3, start='100')
>>> a.current
'100'
>>> a = GrayCode(4, rank=4)
>>> a.current
'0110'
>>> a.rank
4
"""
if n < 1 or int(n) != n:
raise ValueError(
'Gray code dimension must be a positive integer, not %i' % n)
n = Integer(n)
args = (n,) + args
obj = Basic.__new__(cls, *args)
if 'start' in kw_args:
obj._current = kw_args["start"]
if len(obj._current) > n:
raise ValueError('Gray code start has length %i but '
'should not be greater than %i' % (len(obj._current), n))
elif 'rank' in kw_args:
if int(kw_args["rank"]) != kw_args["rank"]:
raise ValueError('Gray code rank must be a positive integer, '
'not %i' % kw_args["rank"])
obj._rank = int(kw_args["rank"]) % obj.selections
obj._current = obj.unrank(n, obj._rank)
return obj
def next(self, delta=1):
"""
Returns the Gray code a distance ``delta`` (default = 1) from the
current value in canonical order.
Examples
========
>>> from sympy.combinatorics import GrayCode
>>> a = GrayCode(3, start='110')
>>> a.next().current
'111'
>>> a.next(-1).current
'010'
"""
return GrayCode(self.n, rank=(self.rank + delta) % self.selections)
@property
def selections(self):
"""
Returns the number of bit vectors in the Gray code.
Examples
========
>>> from sympy.combinatorics import GrayCode
>>> a = GrayCode(3)
>>> a.selections
8
"""
return 2**self.n
@property
def n(self):
"""
Returns the dimension of the Gray code.
Examples
========
>>> from sympy.combinatorics import GrayCode
>>> a = GrayCode(5)
>>> a.n
5
"""
return self.args[0]
def generate_gray(self, **hints):
"""
Generates the sequence of bit vectors of a Gray Code.
Examples
========
>>> from sympy.combinatorics import GrayCode
>>> a = GrayCode(3)
>>> list(a.generate_gray())
['000', '001', '011', '010', '110', '111', '101', '100']
>>> list(a.generate_gray(start='011'))
['011', '010', '110', '111', '101', '100']
>>> list(a.generate_gray(rank=4))
['110', '111', '101', '100']
See Also
========
skip
References
==========
.. [1] Knuth, D. (2011). The Art of Computer Programming,
Vol 4, Addison Wesley
"""
bits = self.n
start = None
if "start" in hints:
start = hints["start"]
elif "rank" in hints:
start = GrayCode.unrank(self.n, hints["rank"])
if start is not None:
self._current = start
current = self.current
graycode_bin = gray_to_bin(current)
if len(graycode_bin) > self.n:
raise ValueError('Gray code start has length %i but should '
'not be greater than %i' % (len(graycode_bin), bits))
self._current = int(current, 2)
graycode_int = int(''.join(graycode_bin), 2)
for i in range(graycode_int, 1 << bits):
if self._skip:
self._skip = False
else:
yield self.current
bbtc = (i ^ (i + 1))
gbtc = (bbtc ^ (bbtc >> 1))
self._current = (self._current ^ gbtc)
self._current = 0
def skip(self):
"""
Skips the bit generation.
Examples
========
>>> from sympy.combinatorics import GrayCode
>>> a = GrayCode(3)
>>> for i in a.generate_gray():
... if i == '010':
... a.skip()
... print(i)
...
000
001
011
010
111
101
100
See Also
========
generate_gray
"""
self._skip = True
@property
def rank(self):
"""
Ranks the Gray code.
A ranking algorithm determines the position (or rank)
of a combinatorial object among all the objects w.r.t.
a given order. For example, the 4 bit binary reflected
Gray code (BRGC) '0101' has a rank of 6 as it appears in
the 6th position in the canonical ordering of the family
of 4 bit Gray codes.
Examples
========
>>> from sympy.combinatorics import GrayCode
>>> a = GrayCode(3)
>>> list(a.generate_gray())
['000', '001', '011', '010', '110', '111', '101', '100']
>>> GrayCode(3, start='100').rank
7
>>> GrayCode(3, rank=7).current
'100'
See Also
========
unrank
References
==========
.. [1] https://web.archive.org/web/20200224064753/http://statweb.stanford.edu/~susan/courses/s208/node12.html
"""
if self._rank is None:
self._rank = int(gray_to_bin(self.current), 2)
return self._rank
@property
def current(self):
"""
Returns the currently referenced Gray code as a bit string.
Examples
========
>>> from sympy.combinatorics import GrayCode
>>> GrayCode(3, start='100').current
'100'
"""
rv = self._current or '0'
if not isinstance(rv, str):
rv = bin(rv)[2:]
return rv.rjust(self.n, '0')
@classmethod
def unrank(self, n, rank):
"""
Unranks an n-bit sized Gray code of rank k. This method exists
so that a derivative GrayCode class can define its own code of
a given rank.
The string here is generated in reverse order to allow for tail-call
optimization.
Examples
========
>>> from sympy.combinatorics import GrayCode
>>> GrayCode(5, rank=3).current
'00010'
>>> GrayCode.unrank(5, 3)
'00010'
See Also
========
rank
"""
def _unrank(k, n):
if n == 1:
return str(k % 2)
m = 2**(n - 1)
if k < m:
return '0' + _unrank(k, n - 1)
return '1' + _unrank(m - (k % m) - 1, n - 1)
return _unrank(rank, n)
def random_bitstring(n):
"""
Generates a random bitlist of length n.
Examples
========
>>> from sympy.combinatorics.graycode import random_bitstring
>>> random_bitstring(3) # doctest: +SKIP
100
"""
return ''.join([random.choice('01') for i in range(n)])
def gray_to_bin(bin_list):
"""
Convert from Gray coding to binary coding.
We assume big endian encoding.
Examples
========
>>> from sympy.combinatorics.graycode import gray_to_bin
>>> gray_to_bin('100')
'111'
See Also
========
bin_to_gray
"""
b = [bin_list[0]]
for i in range(1, len(bin_list)):
b += str(int(b[i - 1] != bin_list[i]))
return ''.join(b)
def bin_to_gray(bin_list):
"""
Convert from binary coding to gray coding.
We assume big endian encoding.
Examples
========
>>> from sympy.combinatorics.graycode import bin_to_gray
>>> bin_to_gray('111')
'100'
See Also
========
gray_to_bin
"""
b = [bin_list[0]]
for i in range(1, len(bin_list)):
b += str(int(bin_list[i]) ^ int(bin_list[i - 1]))
return ''.join(b)
def get_subset_from_bitstring(super_set, bitstring):
"""
Gets the subset defined by the bitstring.
Examples
========
>>> from sympy.combinatorics.graycode import get_subset_from_bitstring
>>> get_subset_from_bitstring(['a', 'b', 'c', 'd'], '0011')
['c', 'd']
>>> get_subset_from_bitstring(['c', 'a', 'c', 'c'], '1100')
['c', 'a']
See Also
========
graycode_subsets
"""
if len(super_set) != len(bitstring):
raise ValueError("The sizes of the lists are not equal")
return [super_set[i] for i, j in enumerate(bitstring)
if bitstring[i] == '1']
def graycode_subsets(gray_code_set):
"""
Generates the subsets as enumerated by a Gray code.
Examples
========
>>> from sympy.combinatorics.graycode import graycode_subsets
>>> list(graycode_subsets(['a', 'b', 'c']))
[[], ['c'], ['b', 'c'], ['b'], ['a', 'b'], ['a', 'b', 'c'], \
['a', 'c'], ['a']]
>>> list(graycode_subsets(['a', 'b', 'c', 'c']))
[[], ['c'], ['c', 'c'], ['c'], ['b', 'c'], ['b', 'c', 'c'], \
['b', 'c'], ['b'], ['a', 'b'], ['a', 'b', 'c'], ['a', 'b', 'c', 'c'], \
['a', 'b', 'c'], ['a', 'c'], ['a', 'c', 'c'], ['a', 'c'], ['a']]
See Also
========
get_subset_from_bitstring
"""
for bitstring in list(GrayCode(len(gray_code_set)).generate_gray()):
yield get_subset_from_bitstring(gray_code_set, bitstring)

View File

@ -0,0 +1,61 @@
from sympy.combinatorics.perm_groups import PermutationGroup
from sympy.combinatorics.permutations import Permutation
from sympy.utilities.iterables import uniq
_af_new = Permutation._af_new
def DirectProduct(*groups):
"""
Returns the direct product of several groups as a permutation group.
Explanation
===========
This is implemented much like the __mul__ procedure for taking the direct
product of two permutation groups, but the idea of shifting the
generators is realized in the case of an arbitrary number of groups.
A call to DirectProduct(G1, G2, ..., Gn) is generally expected to be faster
than a call to G1*G2*...*Gn (and thus the need for this algorithm).
Examples
========
>>> from sympy.combinatorics.group_constructs import DirectProduct
>>> from sympy.combinatorics.named_groups import CyclicGroup
>>> C = CyclicGroup(4)
>>> G = DirectProduct(C, C, C)
>>> G.order()
64
See Also
========
sympy.combinatorics.perm_groups.PermutationGroup.__mul__
"""
degrees = []
gens_count = []
total_degree = 0
total_gens = 0
for group in groups:
current_deg = group.degree
current_num_gens = len(group.generators)
degrees.append(current_deg)
total_degree += current_deg
gens_count.append(current_num_gens)
total_gens += current_num_gens
array_gens = []
for i in range(total_gens):
array_gens.append(list(range(total_degree)))
current_gen = 0
current_deg = 0
for i in range(len(gens_count)):
for j in range(current_gen, current_gen + gens_count[i]):
gen = ((groups[i].generators)[j - current_gen]).array_form
array_gens[j][current_deg:current_deg + degrees[i]] = \
[x + current_deg for x in gen]
current_gen += gens_count[i]
current_deg += degrees[i]
perm_gens = list(uniq([_af_new(list(a)) for a in array_gens]))
return PermutationGroup(perm_gens, dups=False)

View File

@ -0,0 +1,296 @@
from itertools import chain, combinations
from sympy.external.gmpy import gcd
from sympy.ntheory.factor_ import factorint
from sympy.utilities.misc import as_int
def _is_nilpotent_number(factors: dict) -> bool:
""" Check whether `n` is a nilpotent number.
Note that ``factors`` is a prime factorization of `n`.
This is a low-level helper for ``is_nilpotent_number``, for internal use.
"""
for p in factors.keys():
for q, e in factors.items():
# We want to calculate
# any(pow(q, k, p) == 1 for k in range(1, e + 1))
m = 1
for _ in range(e):
m = m*q % p
if m == 1:
return False
return True
def is_nilpotent_number(n) -> bool:
"""
Check whether `n` is a nilpotent number. A number `n` is said to be
nilpotent if and only if every finite group of order `n` is nilpotent.
For more information see [1]_.
Examples
========
>>> from sympy.combinatorics.group_numbers import is_nilpotent_number
>>> from sympy import randprime
>>> is_nilpotent_number(21)
False
>>> is_nilpotent_number(randprime(1, 30)**12)
True
References
==========
.. [1] Pakianathan, J., Shankar, K., Nilpotent Numbers,
The American Mathematical Monthly, 107(7), 631-634.
.. [2] https://oeis.org/A056867
"""
n = as_int(n)
if n <= 0:
raise ValueError("n must be a positive integer, not %i" % n)
return _is_nilpotent_number(factorint(n))
def is_abelian_number(n) -> bool:
"""
Check whether `n` is an abelian number. A number `n` is said to be abelian
if and only if every finite group of order `n` is abelian. For more
information see [1]_.
Examples
========
>>> from sympy.combinatorics.group_numbers import is_abelian_number
>>> from sympy import randprime
>>> is_abelian_number(4)
True
>>> is_abelian_number(randprime(1, 2000)**2)
True
>>> is_abelian_number(60)
False
References
==========
.. [1] Pakianathan, J., Shankar, K., Nilpotent Numbers,
The American Mathematical Monthly, 107(7), 631-634.
.. [2] https://oeis.org/A051532
"""
n = as_int(n)
if n <= 0:
raise ValueError("n must be a positive integer, not %i" % n)
factors = factorint(n)
return all(e < 3 for e in factors.values()) and _is_nilpotent_number(factors)
def is_cyclic_number(n) -> bool:
"""
Check whether `n` is a cyclic number. A number `n` is said to be cyclic
if and only if every finite group of order `n` is cyclic. For more
information see [1]_.
Examples
========
>>> from sympy.combinatorics.group_numbers import is_cyclic_number
>>> from sympy import randprime
>>> is_cyclic_number(15)
True
>>> is_cyclic_number(randprime(1, 2000)**2)
False
>>> is_cyclic_number(4)
False
References
==========
.. [1] Pakianathan, J., Shankar, K., Nilpotent Numbers,
The American Mathematical Monthly, 107(7), 631-634.
.. [2] https://oeis.org/A003277
"""
n = as_int(n)
if n <= 0:
raise ValueError("n must be a positive integer, not %i" % n)
factors = factorint(n)
return all(e == 1 for e in factors.values()) and _is_nilpotent_number(factors)
def _holder_formula(prime_factors):
r""" Number of groups of order `n`.
where `n` is squarefree and its prime factors are ``prime_factors``.
i.e., ``n == math.prod(prime_factors)``
Explanation
===========
When `n` is squarefree, the number of groups of order `n` is expressed by
.. math ::
\sum_{d \mid n} \prod_p \frac{p^{c(p, d)} - 1}{p - 1}
where `n=de`, `p` is the prime factor of `e`,
and `c(p, d)` is the number of prime factors `q` of `d` such that `q \equiv 1 \pmod{p}` [2]_.
The formula is elegant, but can be improved when implemented as an algorithm.
Since `n` is assumed to be squarefree, the divisor `d` of `n` can be identified with the power set of prime factors.
We let `N` be the set of prime factors of `n`.
`F = \{p \in N : \forall q \in N, q \not\equiv 1 \pmod{p} \}, M = N \setminus F`, we have the following.
.. math ::
\sum_{d \in 2^{M}} \prod_{p \in M \setminus d} \frac{p^{c(p, F \cup d)} - 1}{p - 1}
Practically, many prime factors are expected to be members of `F`, thus reducing computation time.
Parameters
==========
prime_factors : set
The set of prime factors of ``n``. where `n` is squarefree.
Returns
=======
int : Number of groups of order ``n``
Examples
========
>>> from sympy.combinatorics.group_numbers import _holder_formula
>>> _holder_formula({2}) # n = 2
1
>>> _holder_formula({2, 3}) # n = 2*3 = 6
2
See Also
========
groups_count
References
==========
.. [1] Otto Holder, Die Gruppen der Ordnungen p^3, pq^2, pqr, p^4,
Math. Ann. 43 pp. 301-412 (1893).
http://dx.doi.org/10.1007/BF01443651
.. [2] John H. Conway, Heiko Dietrich and E.A. O'Brien,
Counting groups: gnus, moas and other exotica
The Mathematical Intelligencer 30, 6-15 (2008)
https://doi.org/10.1007/BF02985731
"""
F = {p for p in prime_factors if all(q % p != 1 for q in prime_factors)}
M = prime_factors - F
s = 0
powerset = chain.from_iterable(combinations(M, r) for r in range(len(M)+1))
for ps in powerset:
ps = set(ps)
prod = 1
for p in M - ps:
c = len([q for q in F | ps if q % p == 1])
prod *= (p**c - 1) // (p - 1)
if not prod:
break
s += prod
return s
def groups_count(n):
r""" Number of groups of order `n`.
In [1]_, ``gnu(n)`` is given, so we follow this notation here as well.
Parameters
==========
n : Integer
``n`` is a positive integer
Returns
=======
int : ``gnu(n)``
Raises
======
ValueError
Number of groups of order ``n`` is unknown or not implemented.
For example, gnu(`2^{11}`) is not yet known.
On the other hand, gnu(12) is known to be 5,
but this has not yet been implemented in this function.
Examples
========
>>> from sympy.combinatorics.group_numbers import groups_count
>>> groups_count(3) # There is only one cyclic group of order 3
1
>>> # There are two groups of order 10: the cyclic group and the dihedral group
>>> groups_count(10)
2
See Also
========
is_cyclic_number
`n` is cyclic iff gnu(n) = 1
References
==========
.. [1] John H. Conway, Heiko Dietrich and E.A. O'Brien,
Counting groups: gnus, moas and other exotica
The Mathematical Intelligencer 30, 6-15 (2008)
https://doi.org/10.1007/BF02985731
.. [2] https://oeis.org/A000001
"""
n = as_int(n)
if n <= 0:
raise ValueError("n must be a positive integer, not %i" % n)
factors = factorint(n)
if len(factors) == 1:
(p, e) = list(factors.items())[0]
if p == 2:
A000679 = [1, 1, 2, 5, 14, 51, 267, 2328, 56092, 10494213, 49487367289]
if e < len(A000679):
return A000679[e]
if p == 3:
A090091 = [1, 1, 2, 5, 15, 67, 504, 9310, 1396077, 5937876645]
if e < len(A090091):
return A090091[e]
if e <= 2: # gnu(p) = 1, gnu(p**2) = 2
return e
if e == 3: # gnu(p**3) = 5
return 5
if e == 4: # if p is an odd prime, gnu(p**4) = 15
return 15
if e == 5: # if p >= 5, gnu(p**5) is expressed by the following equation
return 61 + 2*p + 2*gcd(p-1, 3) + gcd(p-1, 4)
if e == 6: # if p >= 6, gnu(p**6) is expressed by the following equation
return 3*p**2 + 39*p + 344 +\
24*gcd(p-1, 3) + 11*gcd(p-1, 4) + 2*gcd(p-1, 5)
if e == 7: # if p >= 7, gnu(p**7) is expressed by the following equation
if p == 5:
return 34297
return 3*p**5 + 12*p**4 + 44*p**3 + 170*p**2 + 707*p + 2455 +\
(4*p**2 + 44*p + 291)*gcd(p-1, 3) + (p**2 + 19*p + 135)*gcd(p-1, 4) + \
(3*p + 31)*gcd(p-1, 5) + 4*gcd(p-1, 7) + 5*gcd(p-1, 8) + gcd(p-1, 9)
if any(e > 1 for e in factors.values()): # n is not squarefree
# some known values for small n that have more than 1 factor and are not square free (https://oeis.org/A000001)
small = {12: 5, 18: 5, 20: 5, 24: 15, 28: 4, 36: 14, 40: 14, 44: 4, 45: 2, 48: 52,
50: 5, 52: 5, 54: 15, 56: 13, 60: 13, 63: 4, 68: 5, 72: 50, 75: 3, 76: 4,
80: 52, 84: 15, 88: 12, 90: 10, 92: 4}
if n in small:
return small[n]
raise ValueError("Number of groups of order n is unknown or not implemented")
if len(factors) == 2: # n is squarefree semiprime
p, q = list(factors.keys())
if p > q:
p, q = q, p
return 2 if q % p == 1 else 1
return _holder_formula(set(factors.keys()))

View File

@ -0,0 +1,549 @@
import itertools
from sympy.combinatorics.fp_groups import FpGroup, FpSubgroup, simplify_presentation
from sympy.combinatorics.free_groups import FreeGroup
from sympy.combinatorics.perm_groups import PermutationGroup
from sympy.core.intfunc import igcd
from sympy.functions.combinatorial.numbers import totient
from sympy.core.singleton import S
class GroupHomomorphism:
'''
A class representing group homomorphisms. Instantiate using `homomorphism()`.
References
==========
.. [1] Holt, D., Eick, B. and O'Brien, E. (2005). Handbook of computational group theory.
'''
def __init__(self, domain, codomain, images):
self.domain = domain
self.codomain = codomain
self.images = images
self._inverses = None
self._kernel = None
self._image = None
def _invs(self):
'''
Return a dictionary with `{gen: inverse}` where `gen` is a rewriting
generator of `codomain` (e.g. strong generator for permutation groups)
and `inverse` is an element of its preimage
'''
image = self.image()
inverses = {}
for k in list(self.images.keys()):
v = self.images[k]
if not (v in inverses
or v.is_identity):
inverses[v] = k
if isinstance(self.codomain, PermutationGroup):
gens = image.strong_gens
else:
gens = image.generators
for g in gens:
if g in inverses or g.is_identity:
continue
w = self.domain.identity
if isinstance(self.codomain, PermutationGroup):
parts = image._strong_gens_slp[g][::-1]
else:
parts = g
for s in parts:
if s in inverses:
w = w*inverses[s]
else:
w = w*inverses[s**-1]**-1
inverses[g] = w
return inverses
def invert(self, g):
'''
Return an element of the preimage of ``g`` or of each element
of ``g`` if ``g`` is a list.
Explanation
===========
If the codomain is an FpGroup, the inverse for equal
elements might not always be the same unless the FpGroup's
rewriting system is confluent. However, making a system
confluent can be time-consuming. If it's important, try
`self.codomain.make_confluent()` first.
'''
from sympy.combinatorics import Permutation
from sympy.combinatorics.free_groups import FreeGroupElement
if isinstance(g, (Permutation, FreeGroupElement)):
if isinstance(self.codomain, FpGroup):
g = self.codomain.reduce(g)
if self._inverses is None:
self._inverses = self._invs()
image = self.image()
w = self.domain.identity
if isinstance(self.codomain, PermutationGroup):
gens = image.generator_product(g)[::-1]
else:
gens = g
# the following can't be "for s in gens:"
# because that would be equivalent to
# "for s in gens.array_form:" when g is
# a FreeGroupElement. On the other hand,
# when you call gens by index, the generator
# (or inverse) at position i is returned.
for i in range(len(gens)):
s = gens[i]
if s.is_identity:
continue
if s in self._inverses:
w = w*self._inverses[s]
else:
w = w*self._inverses[s**-1]**-1
return w
elif isinstance(g, list):
return [self.invert(e) for e in g]
def kernel(self):
'''
Compute the kernel of `self`.
'''
if self._kernel is None:
self._kernel = self._compute_kernel()
return self._kernel
def _compute_kernel(self):
G = self.domain
G_order = G.order()
if G_order is S.Infinity:
raise NotImplementedError(
"Kernel computation is not implemented for infinite groups")
gens = []
if isinstance(G, PermutationGroup):
K = PermutationGroup(G.identity)
else:
K = FpSubgroup(G, gens, normal=True)
i = self.image().order()
while K.order()*i != G_order:
r = G.random()
k = r*self.invert(self(r))**-1
if k not in K:
gens.append(k)
if isinstance(G, PermutationGroup):
K = PermutationGroup(gens)
else:
K = FpSubgroup(G, gens, normal=True)
return K
def image(self):
'''
Compute the image of `self`.
'''
if self._image is None:
values = list(set(self.images.values()))
if isinstance(self.codomain, PermutationGroup):
self._image = self.codomain.subgroup(values)
else:
self._image = FpSubgroup(self.codomain, values)
return self._image
def _apply(self, elem):
'''
Apply `self` to `elem`.
'''
if elem not in self.domain:
if isinstance(elem, (list, tuple)):
return [self._apply(e) for e in elem]
raise ValueError("The supplied element does not belong to the domain")
if elem.is_identity:
return self.codomain.identity
else:
images = self.images
value = self.codomain.identity
if isinstance(self.domain, PermutationGroup):
gens = self.domain.generator_product(elem, original=True)
for g in gens:
if g in self.images:
value = images[g]*value
else:
value = images[g**-1]**-1*value
else:
i = 0
for _, p in elem.array_form:
if p < 0:
g = elem[i]**-1
else:
g = elem[i]
value = value*images[g]**p
i += abs(p)
return value
def __call__(self, elem):
return self._apply(elem)
def is_injective(self):
'''
Check if the homomorphism is injective
'''
return self.kernel().order() == 1
def is_surjective(self):
'''
Check if the homomorphism is surjective
'''
im = self.image().order()
oth = self.codomain.order()
if im is S.Infinity and oth is S.Infinity:
return None
else:
return im == oth
def is_isomorphism(self):
'''
Check if `self` is an isomorphism.
'''
return self.is_injective() and self.is_surjective()
def is_trivial(self):
'''
Check is `self` is a trivial homomorphism, i.e. all elements
are mapped to the identity.
'''
return self.image().order() == 1
def compose(self, other):
'''
Return the composition of `self` and `other`, i.e.
the homomorphism phi such that for all g in the domain
of `other`, phi(g) = self(other(g))
'''
if not other.image().is_subgroup(self.domain):
raise ValueError("The image of `other` must be a subgroup of "
"the domain of `self`")
images = {g: self(other(g)) for g in other.images}
return GroupHomomorphism(other.domain, self.codomain, images)
def restrict_to(self, H):
'''
Return the restriction of the homomorphism to the subgroup `H`
of the domain.
'''
if not isinstance(H, PermutationGroup) or not H.is_subgroup(self.domain):
raise ValueError("Given H is not a subgroup of the domain")
domain = H
images = {g: self(g) for g in H.generators}
return GroupHomomorphism(domain, self.codomain, images)
def invert_subgroup(self, H):
'''
Return the subgroup of the domain that is the inverse image
of the subgroup ``H`` of the homomorphism image
'''
if not H.is_subgroup(self.image()):
raise ValueError("Given H is not a subgroup of the image")
gens = []
P = PermutationGroup(self.image().identity)
for h in H.generators:
h_i = self.invert(h)
if h_i not in P:
gens.append(h_i)
P = PermutationGroup(gens)
for k in self.kernel().generators:
if k*h_i not in P:
gens.append(k*h_i)
P = PermutationGroup(gens)
return P
def homomorphism(domain, codomain, gens, images=(), check=True):
'''
Create (if possible) a group homomorphism from the group ``domain``
to the group ``codomain`` defined by the images of the domain's
generators ``gens``. ``gens`` and ``images`` can be either lists or tuples
of equal sizes. If ``gens`` is a proper subset of the group's generators,
the unspecified generators will be mapped to the identity. If the
images are not specified, a trivial homomorphism will be created.
If the given images of the generators do not define a homomorphism,
an exception is raised.
If ``check`` is ``False``, do not check whether the given images actually
define a homomorphism.
'''
if not isinstance(domain, (PermutationGroup, FpGroup, FreeGroup)):
raise TypeError("The domain must be a group")
if not isinstance(codomain, (PermutationGroup, FpGroup, FreeGroup)):
raise TypeError("The codomain must be a group")
generators = domain.generators
if not all(g in generators for g in gens):
raise ValueError("The supplied generators must be a subset of the domain's generators")
if not all(g in codomain for g in images):
raise ValueError("The images must be elements of the codomain")
if images and len(images) != len(gens):
raise ValueError("The number of images must be equal to the number of generators")
gens = list(gens)
images = list(images)
images.extend([codomain.identity]*(len(generators)-len(images)))
gens.extend([g for g in generators if g not in gens])
images = dict(zip(gens,images))
if check and not _check_homomorphism(domain, codomain, images):
raise ValueError("The given images do not define a homomorphism")
return GroupHomomorphism(domain, codomain, images)
def _check_homomorphism(domain, codomain, images):
"""
Check that a given mapping of generators to images defines a homomorphism.
Parameters
==========
domain : PermutationGroup, FpGroup, FreeGroup
codomain : PermutationGroup, FpGroup, FreeGroup
images : dict
The set of keys must be equal to domain.generators.
The values must be elements of the codomain.
"""
pres = domain if hasattr(domain, 'relators') else domain.presentation()
rels = pres.relators
gens = pres.generators
symbols = [g.ext_rep[0] for g in gens]
symbols_to_domain_generators = dict(zip(symbols, domain.generators))
identity = codomain.identity
def _image(r):
w = identity
for symbol, power in r.array_form:
g = symbols_to_domain_generators[symbol]
w *= images[g]**power
return w
for r in rels:
if isinstance(codomain, FpGroup):
s = codomain.equals(_image(r), identity)
if s is None:
# only try to make the rewriting system
# confluent when it can't determine the
# truth of equality otherwise
success = codomain.make_confluent()
s = codomain.equals(_image(r), identity)
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 "
"(group._rewriting_system.set_max(new_value); "
"the current value is stored in group._rewriting"
"_system.maxeqns)")
else:
s = _image(r).is_identity
if not s:
return False
return True
def orbit_homomorphism(group, omega):
'''
Return the homomorphism induced by the action of the permutation
group ``group`` on the set ``omega`` that is closed under the action.
'''
from sympy.combinatorics import Permutation
from sympy.combinatorics.named_groups import SymmetricGroup
codomain = SymmetricGroup(len(omega))
identity = codomain.identity
omega = list(omega)
images = {g: identity*Permutation([omega.index(o^g) for o in omega]) for g in group.generators}
group._schreier_sims(base=omega)
H = GroupHomomorphism(group, codomain, images)
if len(group.basic_stabilizers) > len(omega):
H._kernel = group.basic_stabilizers[len(omega)]
else:
H._kernel = PermutationGroup([group.identity])
return H
def block_homomorphism(group, blocks):
'''
Return the homomorphism induced by the action of the permutation
group ``group`` on the block system ``blocks``. The latter should be
of the same form as returned by the ``minimal_block`` method for
permutation groups, namely a list of length ``group.degree`` where
the i-th entry is a representative of the block i belongs to.
'''
from sympy.combinatorics import Permutation
from sympy.combinatorics.named_groups import SymmetricGroup
n = len(blocks)
# number the blocks; m is the total number,
# b is such that b[i] is the number of the block i belongs to,
# p is the list of length m such that p[i] is the representative
# of the i-th block
m = 0
p = []
b = [None]*n
for i in range(n):
if blocks[i] == i:
p.append(i)
b[i] = m
m += 1
for i in range(n):
b[i] = b[blocks[i]]
codomain = SymmetricGroup(m)
# the list corresponding to the identity permutation in codomain
identity = range(m)
images = {g: Permutation([b[p[i]^g] for i in identity]) for g in group.generators}
H = GroupHomomorphism(group, codomain, images)
return H
def group_isomorphism(G, H, isomorphism=True):
'''
Compute an isomorphism between 2 given groups.
Parameters
==========
G : A finite ``FpGroup`` or a ``PermutationGroup``.
First group.
H : A finite ``FpGroup`` or a ``PermutationGroup``
Second group.
isomorphism : bool
This is used to avoid the computation of homomorphism
when the user only wants to check if there exists
an isomorphism between the groups.
Returns
=======
If isomorphism = False -- Returns a boolean.
If isomorphism = True -- Returns a boolean and an isomorphism between `G` and `H`.
Examples
========
>>> from sympy.combinatorics import free_group, Permutation
>>> from sympy.combinatorics.perm_groups import PermutationGroup
>>> from sympy.combinatorics.fp_groups import FpGroup
>>> from sympy.combinatorics.homomorphisms import 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)
(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
True
>>> T(b*a*b**-1*a**-1*b**-1)
(0 2 3)
Notes
=====
Uses the approach suggested by Robert Tarjan to compute the isomorphism between two groups.
First, the generators of ``G`` are mapped to the elements of ``H`` and
we check if the mapping induces an isomorphism.
'''
if not isinstance(G, (PermutationGroup, FpGroup)):
raise TypeError("The group must be a PermutationGroup or an FpGroup")
if not isinstance(H, (PermutationGroup, FpGroup)):
raise TypeError("The group must be a PermutationGroup or an FpGroup")
if isinstance(G, FpGroup) and isinstance(H, FpGroup):
G = simplify_presentation(G)
H = simplify_presentation(H)
# 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:
return True
return (True, homomorphism(G, H, G.generators, H.generators))
# `_H` is the permutation group isomorphic to `H`.
_H = H
g_order = G.order()
h_order = H.order()
if g_order is S.Infinity:
raise NotImplementedError("Isomorphism methods are not implemented for infinite groups.")
if isinstance(H, FpGroup):
if h_order is S.Infinity:
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:
# Two groups of the same cyclic numbered order
# are isomorphic to each other.
n = g_order
if (igcd(n, totient(n))) == 1:
return True
# Match the generators of `G` with subsets of `_H`
gens = list(G.generators)
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))
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 not isomorphism:
return False
return (False, None)
def is_isomorphic(G, H):
'''
Check if the groups are isomorphic to each other
Parameters
==========
G : A finite ``FpGroup`` or a ``PermutationGroup``
First group.
H : A finite ``FpGroup`` or a ``PermutationGroup``
Second group.
Returns
=======
boolean
'''
return group_isomorphism(G, H, isomorphism=False)

View File

@ -0,0 +1,332 @@
from sympy.combinatorics.group_constructs import DirectProduct
from sympy.combinatorics.perm_groups import PermutationGroup
from sympy.combinatorics.permutations import Permutation
_af_new = Permutation._af_new
def AbelianGroup(*cyclic_orders):
"""
Returns the direct product of cyclic groups with the given orders.
Explanation
===========
According to the structure theorem for finite abelian groups ([1]),
every finite abelian group can be written as the direct product of
finitely many cyclic groups.
Examples
========
>>> from sympy.combinatorics.named_groups import AbelianGroup
>>> AbelianGroup(3, 4)
PermutationGroup([
(6)(0 1 2),
(3 4 5 6)])
>>> _.is_group
True
See Also
========
DirectProduct
References
==========
.. [1] https://groupprops.subwiki.org/wiki/Structure_theorem_for_finitely_generated_abelian_groups
"""
groups = []
degree = 0
order = 1
for size in cyclic_orders:
degree += size
order *= size
groups.append(CyclicGroup(size))
G = DirectProduct(*groups)
G._is_abelian = True
G._degree = degree
G._order = order
return G
def AlternatingGroup(n):
"""
Generates the alternating group on ``n`` elements as a permutation group.
Explanation
===========
For ``n > 2``, the generators taken are ``(0 1 2), (0 1 2 ... n-1)`` for
``n`` odd
and ``(0 1 2), (1 2 ... n-1)`` for ``n`` even (See [1], p.31, ex.6.9.).
After the group is generated, some of its basic properties are set.
The cases ``n = 1, 2`` are handled separately.
Examples
========
>>> from sympy.combinatorics.named_groups import AlternatingGroup
>>> G = AlternatingGroup(4)
>>> G.is_group
True
>>> a = list(G.generate_dimino())
>>> len(a)
12
>>> all(perm.is_even for perm in a)
True
See Also
========
SymmetricGroup, CyclicGroup, DihedralGroup
References
==========
.. [1] Armstrong, M. "Groups and Symmetry"
"""
# small cases are special
if n in (1, 2):
return PermutationGroup([Permutation([0])])
a = list(range(n))
a[0], a[1], a[2] = a[1], a[2], a[0]
gen1 = a
if n % 2:
a = list(range(1, n))
a.append(0)
gen2 = a
else:
a = list(range(2, n))
a.append(1)
a.insert(0, 0)
gen2 = a
gens = [gen1, gen2]
if gen1 == gen2:
gens = gens[:1]
G = PermutationGroup([_af_new(a) for a in gens], dups=False)
set_alternating_group_properties(G, n, n)
G._is_alt = True
return G
def set_alternating_group_properties(G, n, degree):
"""Set known properties of an alternating group. """
if n < 4:
G._is_abelian = True
G._is_nilpotent = True
else:
G._is_abelian = False
G._is_nilpotent = False
if n < 5:
G._is_solvable = True
else:
G._is_solvable = False
G._degree = degree
G._is_transitive = True
G._is_dihedral = False
def CyclicGroup(n):
"""
Generates the cyclic group of order ``n`` as a permutation group.
Explanation
===========
The generator taken is the ``n``-cycle ``(0 1 2 ... n-1)``
(in cycle notation). After the group is generated, some of its basic
properties are set.
Examples
========
>>> from sympy.combinatorics.named_groups import CyclicGroup
>>> G = CyclicGroup(6)
>>> G.is_group
True
>>> G.order()
6
>>> list(G.generate_schreier_sims(af=True))
[[0, 1, 2, 3, 4, 5], [1, 2, 3, 4, 5, 0], [2, 3, 4, 5, 0, 1],
[3, 4, 5, 0, 1, 2], [4, 5, 0, 1, 2, 3], [5, 0, 1, 2, 3, 4]]
See Also
========
SymmetricGroup, DihedralGroup, AlternatingGroup
"""
a = list(range(1, n))
a.append(0)
gen = _af_new(a)
G = PermutationGroup([gen])
G._is_abelian = True
G._is_nilpotent = True
G._is_solvable = True
G._degree = n
G._is_transitive = True
G._order = n
G._is_dihedral = (n == 2)
return G
def DihedralGroup(n):
r"""
Generates the dihedral group `D_n` as a permutation group.
Explanation
===========
The dihedral group `D_n` is the group of symmetries of the regular
``n``-gon. The generators taken are the ``n``-cycle ``a = (0 1 2 ... n-1)``
(a rotation of the ``n``-gon) and ``b = (0 n-1)(1 n-2)...``
(a reflection of the ``n``-gon) in cycle rotation. It is easy to see that
these satisfy ``a**n = b**2 = 1`` and ``bab = ~a`` so they indeed generate
`D_n` (See [1]). After the group is generated, some of its basic properties
are set.
Examples
========
>>> from sympy.combinatorics.named_groups import DihedralGroup
>>> G = DihedralGroup(5)
>>> G.is_group
True
>>> a = list(G.generate_dimino())
>>> [perm.cyclic_form for perm in a]
[[], [[0, 1, 2, 3, 4]], [[0, 2, 4, 1, 3]],
[[0, 3, 1, 4, 2]], [[0, 4, 3, 2, 1]], [[0, 4], [1, 3]],
[[1, 4], [2, 3]], [[0, 1], [2, 4]], [[0, 2], [3, 4]],
[[0, 3], [1, 2]]]
See Also
========
SymmetricGroup, CyclicGroup, AlternatingGroup
References
==========
.. [1] https://en.wikipedia.org/wiki/Dihedral_group
"""
# small cases are special
if n == 1:
return PermutationGroup([Permutation([1, 0])])
if n == 2:
return PermutationGroup([Permutation([1, 0, 3, 2]),
Permutation([2, 3, 0, 1]), Permutation([3, 2, 1, 0])])
a = list(range(1, n))
a.append(0)
gen1 = _af_new(a)
a = list(range(n))
a.reverse()
gen2 = _af_new(a)
G = PermutationGroup([gen1, gen2])
# if n is a power of 2, group is nilpotent
if n & (n-1) == 0:
G._is_nilpotent = True
else:
G._is_nilpotent = False
G._is_dihedral = True
G._is_abelian = False
G._is_solvable = True
G._degree = n
G._is_transitive = True
G._order = 2*n
return G
def SymmetricGroup(n):
"""
Generates the symmetric group on ``n`` elements as a permutation group.
Explanation
===========
The generators taken are the ``n``-cycle
``(0 1 2 ... n-1)`` and the transposition ``(0 1)`` (in cycle notation).
(See [1]). After the group is generated, some of its basic properties
are set.
Examples
========
>>> from sympy.combinatorics.named_groups import SymmetricGroup
>>> G = SymmetricGroup(4)
>>> G.is_group
True
>>> G.order()
24
>>> list(G.generate_schreier_sims(af=True))
[[0, 1, 2, 3], [1, 2, 3, 0], [2, 3, 0, 1], [3, 1, 2, 0], [0, 2, 3, 1],
[1, 3, 0, 2], [2, 0, 1, 3], [3, 2, 0, 1], [0, 3, 1, 2], [1, 0, 2, 3],
[2, 1, 3, 0], [3, 0, 1, 2], [0, 1, 3, 2], [1, 2, 0, 3], [2, 3, 1, 0],
[3, 1, 0, 2], [0, 2, 1, 3], [1, 3, 2, 0], [2, 0, 3, 1], [3, 2, 1, 0],
[0, 3, 2, 1], [1, 0, 3, 2], [2, 1, 0, 3], [3, 0, 2, 1]]
See Also
========
CyclicGroup, DihedralGroup, AlternatingGroup
References
==========
.. [1] https://en.wikipedia.org/wiki/Symmetric_group#Generators_and_relations
"""
if n == 1:
G = PermutationGroup([Permutation([0])])
elif n == 2:
G = PermutationGroup([Permutation([1, 0])])
else:
a = list(range(1, n))
a.append(0)
gen1 = _af_new(a)
a = list(range(n))
a[0], a[1] = a[1], a[0]
gen2 = _af_new(a)
G = PermutationGroup([gen1, gen2])
set_symmetric_group_properties(G, n, n)
G._is_sym = True
return G
def set_symmetric_group_properties(G, n, degree):
"""Set known properties of a symmetric group. """
if n < 3:
G._is_abelian = True
G._is_nilpotent = True
else:
G._is_abelian = False
G._is_nilpotent = False
if n < 5:
G._is_solvable = True
else:
G._is_solvable = False
G._degree = degree
G._is_transitive = True
G._is_dihedral = (n in [2, 3]) # cf Landau's func and Stirling's approx
def RubikGroup(n):
"""Return a group of Rubik's cube generators
>>> from sympy.combinatorics.named_groups import RubikGroup
>>> RubikGroup(2).is_group
True
"""
from sympy.combinatorics.generators import rubik
if n <= 1:
raise ValueError("Invalid cube. n has to be greater than 1")
return PermutationGroup(rubik(n))

View File

@ -0,0 +1,745 @@
from sympy.core import Basic, Dict, sympify, Tuple
from sympy.core.numbers import Integer
from sympy.core.sorting import default_sort_key
from sympy.core.sympify import _sympify
from sympy.functions.combinatorial.numbers import bell
from sympy.matrices import zeros
from sympy.sets.sets import FiniteSet, Union
from sympy.utilities.iterables import flatten, group
from sympy.utilities.misc import as_int
from collections import defaultdict
class Partition(FiniteSet):
"""
This class represents an abstract partition.
A partition is a set of disjoint sets whose union equals a given set.
See Also
========
sympy.utilities.iterables.partitions,
sympy.utilities.iterables.multiset_partitions
"""
_rank = None
_partition = None
def __new__(cls, *partition):
"""
Generates a new partition object.
This method also verifies if the arguments passed are
valid and raises a ValueError if they are not.
Examples
========
Creating Partition from Python lists:
>>> from sympy.combinatorics import Partition
>>> a = Partition([1, 2], [3])
>>> a
Partition({3}, {1, 2})
>>> a.partition
[[1, 2], [3]]
>>> len(a)
2
>>> a.members
(1, 2, 3)
Creating Partition from Python sets:
>>> Partition({1, 2, 3}, {4, 5})
Partition({4, 5}, {1, 2, 3})
Creating Partition from SymPy finite sets:
>>> from sympy import FiniteSet
>>> a = FiniteSet(1, 2, 3)
>>> b = FiniteSet(4, 5)
>>> Partition(a, b)
Partition({4, 5}, {1, 2, 3})
"""
args = []
dups = False
for arg in partition:
if isinstance(arg, list):
as_set = set(arg)
if len(as_set) < len(arg):
dups = True
break # error below
arg = as_set
args.append(_sympify(arg))
if not all(isinstance(part, FiniteSet) for part in args):
raise ValueError(
"Each argument to Partition should be " \
"a list, set, or a FiniteSet")
# sort so we have a canonical reference for RGS
U = Union(*args)
if dups or len(U) < sum(len(arg) for arg in args):
raise ValueError("Partition contained duplicate elements.")
obj = FiniteSet.__new__(cls, *args)
obj.members = tuple(U)
obj.size = len(U)
return obj
def sort_key(self, order=None):
"""Return a canonical key that can be used for sorting.
Ordering is based on the size and sorted elements of the partition
and ties are broken with the rank.
Examples
========
>>> from sympy import default_sort_key
>>> from sympy.combinatorics import Partition
>>> from sympy.abc import x
>>> a = Partition([1, 2])
>>> b = Partition([3, 4])
>>> c = Partition([1, x])
>>> d = Partition(list(range(4)))
>>> l = [d, b, a + 1, a, c]
>>> l.sort(key=default_sort_key); l
[Partition({1, 2}), Partition({1}, {2}), Partition({1, x}), Partition({3, 4}), Partition({0, 1, 2, 3})]
"""
if order is None:
members = self.members
else:
members = tuple(sorted(self.members,
key=lambda w: default_sort_key(w, order)))
return tuple(map(default_sort_key, (self.size, members, self.rank)))
@property
def partition(self):
"""Return partition as a sorted list of lists.
Examples
========
>>> from sympy.combinatorics import Partition
>>> Partition([1], [2, 3]).partition
[[1], [2, 3]]
"""
if self._partition is None:
self._partition = sorted([sorted(p, key=default_sort_key)
for p in self.args])
return self._partition
def __add__(self, other):
"""
Return permutation whose rank is ``other`` greater than current rank,
(mod the maximum rank for the set).
Examples
========
>>> from sympy.combinatorics import Partition
>>> a = Partition([1, 2], [3])
>>> a.rank
1
>>> (a + 1).rank
2
>>> (a + 100).rank
1
"""
other = as_int(other)
offset = self.rank + other
result = RGS_unrank((offset) %
RGS_enum(self.size),
self.size)
return Partition.from_rgs(result, self.members)
def __sub__(self, other):
"""
Return permutation whose rank is ``other`` less than current rank,
(mod the maximum rank for the set).
Examples
========
>>> from sympy.combinatorics import Partition
>>> a = Partition([1, 2], [3])
>>> a.rank
1
>>> (a - 1).rank
0
>>> (a - 100).rank
1
"""
return self.__add__(-other)
def __le__(self, other):
"""
Checks if a partition is less than or equal to
the other based on rank.
Examples
========
>>> from sympy.combinatorics import Partition
>>> a = Partition([1, 2], [3, 4, 5])
>>> b = Partition([1], [2, 3], [4], [5])
>>> a.rank, b.rank
(9, 34)
>>> a <= a
True
>>> a <= b
True
"""
return self.sort_key() <= sympify(other).sort_key()
def __lt__(self, other):
"""
Checks if a partition is less than the other.
Examples
========
>>> from sympy.combinatorics import Partition
>>> a = Partition([1, 2], [3, 4, 5])
>>> b = Partition([1], [2, 3], [4], [5])
>>> a.rank, b.rank
(9, 34)
>>> a < b
True
"""
return self.sort_key() < sympify(other).sort_key()
@property
def rank(self):
"""
Gets the rank of a partition.
Examples
========
>>> from sympy.combinatorics import Partition
>>> a = Partition([1, 2], [3], [4, 5])
>>> a.rank
13
"""
if self._rank is not None:
return self._rank
self._rank = RGS_rank(self.RGS)
return self._rank
@property
def RGS(self):
"""
Returns the "restricted growth string" of the partition.
Explanation
===========
The RGS is returned as a list of indices, L, where L[i] indicates
the block in which element i appears. For example, in a partition
of 3 elements (a, b, c) into 2 blocks ([c], [a, b]) the RGS is
[1, 1, 0]: "a" is in block 1, "b" is in block 1 and "c" is in block 0.
Examples
========
>>> from sympy.combinatorics import Partition
>>> a = Partition([1, 2], [3], [4, 5])
>>> a.members
(1, 2, 3, 4, 5)
>>> a.RGS
(0, 0, 1, 2, 2)
>>> a + 1
Partition({3}, {4}, {5}, {1, 2})
>>> _.RGS
(0, 0, 1, 2, 3)
"""
rgs = {}
partition = self.partition
for i, part in enumerate(partition):
for j in part:
rgs[j] = i
return tuple([rgs[i] for i in sorted(
[i for p in partition for i in p], key=default_sort_key)])
@classmethod
def from_rgs(self, rgs, elements):
"""
Creates a set partition from a restricted growth string.
Explanation
===========
The indices given in rgs are assumed to be the index
of the element as given in elements *as provided* (the
elements are not sorted by this routine). Block numbering
starts from 0. If any block was not referenced in ``rgs``
an error will be raised.
Examples
========
>>> from sympy.combinatorics import Partition
>>> Partition.from_rgs([0, 1, 2, 0, 1], list('abcde'))
Partition({c}, {a, d}, {b, e})
>>> Partition.from_rgs([0, 1, 2, 0, 1], list('cbead'))
Partition({e}, {a, c}, {b, d})
>>> a = Partition([1, 4], [2], [3, 5])
>>> Partition.from_rgs(a.RGS, a.members)
Partition({2}, {1, 4}, {3, 5})
"""
if len(rgs) != len(elements):
raise ValueError('mismatch in rgs and element lengths')
max_elem = max(rgs) + 1
partition = [[] for i in range(max_elem)]
j = 0
for i in rgs:
partition[i].append(elements[j])
j += 1
if not all(p for p in partition):
raise ValueError('some blocks of the partition were empty.')
return Partition(*partition)
class IntegerPartition(Basic):
"""
This class represents an integer partition.
Explanation
===========
In number theory and combinatorics, a partition of a positive integer,
``n``, also called an integer partition, is a way of writing ``n`` as a
list of positive integers that sum to n. Two partitions that differ only
in the order of summands are considered to be the same partition; if order
matters then the partitions are referred to as compositions. For example,
4 has five partitions: [4], [3, 1], [2, 2], [2, 1, 1], and [1, 1, 1, 1];
the compositions [1, 2, 1] and [1, 1, 2] are the same as partition
[2, 1, 1].
See Also
========
sympy.utilities.iterables.partitions,
sympy.utilities.iterables.multiset_partitions
References
==========
.. [1] https://en.wikipedia.org/wiki/Partition_%28number_theory%29
"""
_dict = None
_keys = None
def __new__(cls, partition, integer=None):
"""
Generates a new IntegerPartition object from a list or dictionary.
Explanation
===========
The partition can be given as a list of positive integers or a
dictionary of (integer, multiplicity) items. If the partition is
preceded by an integer an error will be raised if the partition
does not sum to that given integer.
Examples
========
>>> from sympy.combinatorics.partitions import IntegerPartition
>>> a = IntegerPartition([5, 4, 3, 1, 1])
>>> a
IntegerPartition(14, (5, 4, 3, 1, 1))
>>> print(a)
[5, 4, 3, 1, 1]
>>> IntegerPartition({1:3, 2:1})
IntegerPartition(5, (2, 1, 1, 1))
If the value that the partition should sum to is given first, a check
will be made to see n error will be raised if there is a discrepancy:
>>> IntegerPartition(10, [5, 4, 3, 1])
Traceback (most recent call last):
...
ValueError: The partition is not valid
"""
if integer is not None:
integer, partition = partition, integer
if isinstance(partition, (dict, Dict)):
_ = []
for k, v in sorted(partition.items(), reverse=True):
if not v:
continue
k, v = as_int(k), as_int(v)
_.extend([k]*v)
partition = tuple(_)
else:
partition = tuple(sorted(map(as_int, partition), reverse=True))
sum_ok = False
if integer is None:
integer = sum(partition)
sum_ok = True
else:
integer = as_int(integer)
if not sum_ok and sum(partition) != integer:
raise ValueError("Partition did not add to %s" % integer)
if any(i < 1 for i in partition):
raise ValueError("All integer summands must be greater than one")
obj = Basic.__new__(cls, Integer(integer), Tuple(*partition))
obj.partition = list(partition)
obj.integer = integer
return obj
def prev_lex(self):
"""Return the previous partition of the integer, n, in lexical order,
wrapping around to [1, ..., 1] if the partition is [n].
Examples
========
>>> from sympy.combinatorics.partitions import IntegerPartition
>>> p = IntegerPartition([4])
>>> print(p.prev_lex())
[3, 1]
>>> p.partition > p.prev_lex().partition
True
"""
d = defaultdict(int)
d.update(self.as_dict())
keys = self._keys
if keys == [1]:
return IntegerPartition({self.integer: 1})
if keys[-1] != 1:
d[keys[-1]] -= 1
if keys[-1] == 2:
d[1] = 2
else:
d[keys[-1] - 1] = d[1] = 1
else:
d[keys[-2]] -= 1
left = d[1] + keys[-2]
new = keys[-2]
d[1] = 0
while left:
new -= 1
if left - new >= 0:
d[new] += left//new
left -= d[new]*new
return IntegerPartition(self.integer, d)
def next_lex(self):
"""Return the next partition of the integer, n, in lexical order,
wrapping around to [n] if the partition is [1, ..., 1].
Examples
========
>>> from sympy.combinatorics.partitions import IntegerPartition
>>> p = IntegerPartition([3, 1])
>>> print(p.next_lex())
[4]
>>> p.partition < p.next_lex().partition
True
"""
d = defaultdict(int)
d.update(self.as_dict())
key = self._keys
a = key[-1]
if a == self.integer:
d.clear()
d[1] = self.integer
elif a == 1:
if d[a] > 1:
d[a + 1] += 1
d[a] -= 2
else:
b = key[-2]
d[b + 1] += 1
d[1] = (d[b] - 1)*b
d[b] = 0
else:
if d[a] > 1:
if len(key) == 1:
d.clear()
d[a + 1] = 1
d[1] = self.integer - a - 1
else:
a1 = a + 1
d[a1] += 1
d[1] = d[a]*a - a1
d[a] = 0
else:
b = key[-2]
b1 = b + 1
d[b1] += 1
need = d[b]*b + d[a]*a - b1
d[a] = d[b] = 0
d[1] = need
return IntegerPartition(self.integer, d)
def as_dict(self):
"""Return the partition as a dictionary whose keys are the
partition integers and the values are the multiplicity of that
integer.
Examples
========
>>> from sympy.combinatorics.partitions import IntegerPartition
>>> IntegerPartition([1]*3 + [2] + [3]*4).as_dict()
{1: 3, 2: 1, 3: 4}
"""
if self._dict is None:
groups = group(self.partition, multiple=False)
self._keys = [g[0] for g in groups]
self._dict = dict(groups)
return self._dict
@property
def conjugate(self):
"""
Computes the conjugate partition of itself.
Examples
========
>>> from sympy.combinatorics.partitions import IntegerPartition
>>> a = IntegerPartition([6, 3, 3, 2, 1])
>>> a.conjugate
[5, 4, 3, 1, 1, 1]
"""
j = 1
temp_arr = list(self.partition) + [0]
k = temp_arr[0]
b = [0]*k
while k > 0:
while k > temp_arr[j]:
b[k - 1] = j
k -= 1
j += 1
return b
def __lt__(self, other):
"""Return True if self is less than other when the partition
is listed from smallest to biggest.
Examples
========
>>> from sympy.combinatorics.partitions import IntegerPartition
>>> a = IntegerPartition([3, 1])
>>> a < a
False
>>> b = a.next_lex()
>>> a < b
True
>>> a == b
False
"""
return list(reversed(self.partition)) < list(reversed(other.partition))
def __le__(self, other):
"""Return True if self is less than other when the partition
is listed from smallest to biggest.
Examples
========
>>> from sympy.combinatorics.partitions import IntegerPartition
>>> a = IntegerPartition([4])
>>> a <= a
True
"""
return list(reversed(self.partition)) <= list(reversed(other.partition))
def as_ferrers(self, char='#'):
"""
Prints the ferrer diagram of a partition.
Examples
========
>>> from sympy.combinatorics.partitions import IntegerPartition
>>> print(IntegerPartition([1, 1, 5]).as_ferrers())
#####
#
#
"""
return "\n".join([char*i for i in self.partition])
def __str__(self):
return str(list(self.partition))
def random_integer_partition(n, seed=None):
"""
Generates a random integer partition summing to ``n`` as a list
of reverse-sorted integers.
Examples
========
>>> from sympy.combinatorics.partitions import random_integer_partition
For the following, a seed is given so a known value can be shown; in
practice, the seed would not be given.
>>> random_integer_partition(100, seed=[1, 1, 12, 1, 2, 1, 85, 1])
[85, 12, 2, 1]
>>> random_integer_partition(10, seed=[1, 2, 3, 1, 5, 1])
[5, 3, 1, 1]
>>> random_integer_partition(1)
[1]
"""
from sympy.core.random import _randint
n = as_int(n)
if n < 1:
raise ValueError('n must be a positive integer')
randint = _randint(seed)
partition = []
while (n > 0):
k = randint(1, n)
mult = randint(1, n//k)
partition.append((k, mult))
n -= k*mult
partition.sort(reverse=True)
partition = flatten([[k]*m for k, m in partition])
return partition
def RGS_generalized(m):
"""
Computes the m + 1 generalized unrestricted growth strings
and returns them as rows in matrix.
Examples
========
>>> from sympy.combinatorics.partitions import RGS_generalized
>>> RGS_generalized(6)
Matrix([
[ 1, 1, 1, 1, 1, 1, 1],
[ 1, 2, 3, 4, 5, 6, 0],
[ 2, 5, 10, 17, 26, 0, 0],
[ 5, 15, 37, 77, 0, 0, 0],
[ 15, 52, 151, 0, 0, 0, 0],
[ 52, 203, 0, 0, 0, 0, 0],
[203, 0, 0, 0, 0, 0, 0]])
"""
d = zeros(m + 1)
for i in range(m + 1):
d[0, i] = 1
for i in range(1, m + 1):
for j in range(m):
if j <= m - i:
d[i, j] = j * d[i - 1, j] + d[i - 1, j + 1]
else:
d[i, j] = 0
return d
def RGS_enum(m):
"""
RGS_enum computes the total number of restricted growth strings
possible for a superset of size m.
Examples
========
>>> from sympy.combinatorics.partitions import RGS_enum
>>> from sympy.combinatorics import Partition
>>> RGS_enum(4)
15
>>> RGS_enum(5)
52
>>> RGS_enum(6)
203
We can check that the enumeration is correct by actually generating
the partitions. Here, the 15 partitions of 4 items are generated:
>>> a = Partition(list(range(4)))
>>> s = set()
>>> for i in range(20):
... s.add(a)
... a += 1
...
>>> assert len(s) == 15
"""
if (m < 1):
return 0
elif (m == 1):
return 1
else:
return bell(m)
def RGS_unrank(rank, m):
"""
Gives the unranked restricted growth string for a given
superset size.
Examples
========
>>> from sympy.combinatorics.partitions import RGS_unrank
>>> RGS_unrank(14, 4)
[0, 1, 2, 3]
>>> RGS_unrank(0, 4)
[0, 0, 0, 0]
"""
if m < 1:
raise ValueError("The superset size must be >= 1")
if rank < 0 or RGS_enum(m) <= rank:
raise ValueError("Invalid arguments")
L = [1] * (m + 1)
j = 1
D = RGS_generalized(m)
for i in range(2, m + 1):
v = D[m - i, j]
cr = j*v
if cr <= rank:
L[i] = j + 1
rank -= cr
j += 1
else:
L[i] = int(rank / v + 1)
rank %= v
return [x - 1 for x in L[1:]]
def RGS_rank(rgs):
"""
Computes the rank of a restricted growth string.
Examples
========
>>> from sympy.combinatorics.partitions import RGS_rank, RGS_unrank
>>> RGS_rank([0, 1, 2, 1, 3])
42
>>> RGS_rank(RGS_unrank(4, 7))
4
"""
rgs_size = len(rgs)
rank = 0
D = RGS_generalized(rgs_size)
for i in range(1, rgs_size):
n = len(rgs[(i + 1):])
m = max(rgs[0:i])
rank += D[n, m + 1] * rgs[i]
return rank

View File

@ -0,0 +1,709 @@
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 = <identity>
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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,435 @@
from sympy.core import Basic
from sympy.core.containers import Tuple
from sympy.tensor.array import Array
from sympy.core.sympify import _sympify
from sympy.utilities.iterables import flatten, iterable
from sympy.utilities.misc import as_int
from collections import defaultdict
class Prufer(Basic):
"""
The Prufer correspondence is an algorithm that describes the
bijection between labeled trees and the Prufer code. A Prufer
code of a labeled tree is unique up to isomorphism and has
a length of n - 2.
Prufer sequences were first used by Heinz Prufer to give a
proof of Cayley's formula.
References
==========
.. [1] https://mathworld.wolfram.com/LabeledTree.html
"""
_prufer_repr = None
_tree_repr = None
_nodes = None
_rank = None
@property
def prufer_repr(self):
"""Returns Prufer sequence for the Prufer object.
This sequence is found by removing the highest numbered vertex,
recording the node it was attached to, and continuing until only
two vertices remain. The Prufer sequence is the list of recorded nodes.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> Prufer([[0, 3], [1, 3], [2, 3], [3, 4], [4, 5]]).prufer_repr
[3, 3, 3, 4]
>>> Prufer([1, 0, 0]).prufer_repr
[1, 0, 0]
See Also
========
to_prufer
"""
if self._prufer_repr is None:
self._prufer_repr = self.to_prufer(self._tree_repr[:], self.nodes)
return self._prufer_repr
@property
def tree_repr(self):
"""Returns the tree representation of the Prufer object.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> Prufer([[0, 3], [1, 3], [2, 3], [3, 4], [4, 5]]).tree_repr
[[0, 3], [1, 3], [2, 3], [3, 4], [4, 5]]
>>> Prufer([1, 0, 0]).tree_repr
[[1, 2], [0, 1], [0, 3], [0, 4]]
See Also
========
to_tree
"""
if self._tree_repr is None:
self._tree_repr = self.to_tree(self._prufer_repr[:])
return self._tree_repr
@property
def nodes(self):
"""Returns the number of nodes in the tree.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> Prufer([[0, 3], [1, 3], [2, 3], [3, 4], [4, 5]]).nodes
6
>>> Prufer([1, 0, 0]).nodes
5
"""
return self._nodes
@property
def rank(self):
"""Returns the rank of the Prufer sequence.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> p = Prufer([[0, 3], [1, 3], [2, 3], [3, 4], [4, 5]])
>>> p.rank
778
>>> p.next(1).rank
779
>>> p.prev().rank
777
See Also
========
prufer_rank, next, prev, size
"""
if self._rank is None:
self._rank = self.prufer_rank()
return self._rank
@property
def size(self):
"""Return the number of possible trees of this Prufer object.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> Prufer([0]*4).size == Prufer([6]*4).size == 1296
True
See Also
========
prufer_rank, rank, next, prev
"""
return self.prev(self.rank).prev().rank + 1
@staticmethod
def to_prufer(tree, n):
"""Return the Prufer sequence for a tree given as a list of edges where
``n`` is the number of nodes in the tree.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> a = Prufer([[0, 1], [0, 2], [0, 3]])
>>> a.prufer_repr
[0, 0]
>>> Prufer.to_prufer([[0, 1], [0, 2], [0, 3]], 4)
[0, 0]
See Also
========
prufer_repr: returns Prufer sequence of a Prufer object.
"""
d = defaultdict(int)
L = []
for edge in tree:
# Increment the value of the corresponding
# node in the degree list as we encounter an
# edge involving it.
d[edge[0]] += 1
d[edge[1]] += 1
for i in range(n - 2):
# find the smallest leaf
for x in range(n):
if d[x] == 1:
break
# find the node it was connected to
y = None
for edge in tree:
if x == edge[0]:
y = edge[1]
elif x == edge[1]:
y = edge[0]
if y is not None:
break
# record and update
L.append(y)
for j in (x, y):
d[j] -= 1
if not d[j]:
d.pop(j)
tree.remove(edge)
return L
@staticmethod
def to_tree(prufer):
"""Return the tree (as a list of edges) of the given Prufer sequence.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> a = Prufer([0, 2], 4)
>>> a.tree_repr
[[0, 1], [0, 2], [2, 3]]
>>> Prufer.to_tree([0, 2])
[[0, 1], [0, 2], [2, 3]]
References
==========
.. [1] https://hamberg.no/erlend/posts/2010-11-06-prufer-sequence-compact-tree-representation.html
See Also
========
tree_repr: returns tree representation of a Prufer object.
"""
tree = []
last = []
n = len(prufer) + 2
d = defaultdict(lambda: 1)
for p in prufer:
d[p] += 1
for i in prufer:
for j in range(n):
# find the smallest leaf (degree = 1)
if d[j] == 1:
break
# (i, j) is the new edge that we append to the tree
# and remove from the degree dictionary
d[i] -= 1
d[j] -= 1
tree.append(sorted([i, j]))
last = [i for i in range(n) if d[i] == 1] or [0, 1]
tree.append(last)
return tree
@staticmethod
def edges(*runs):
"""Return a list of edges and the number of nodes from the given runs
that connect nodes in an integer-labelled tree.
All node numbers will be shifted so that the minimum node is 0. It is
not a problem if edges are repeated in the runs; only unique edges are
returned. There is no assumption made about what the range of the node
labels should be, but all nodes from the smallest through the largest
must be present.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> Prufer.edges([1, 2, 3], [2, 4, 5]) # a T
([[0, 1], [1, 2], [1, 3], [3, 4]], 5)
Duplicate edges are removed:
>>> Prufer.edges([0, 1, 2, 3], [1, 4, 5], [1, 4, 6]) # a K
([[0, 1], [1, 2], [1, 4], [2, 3], [4, 5], [4, 6]], 7)
"""
e = set()
nmin = runs[0][0]
for r in runs:
for i in range(len(r) - 1):
a, b = r[i: i + 2]
if b < a:
a, b = b, a
e.add((a, b))
rv = []
got = set()
nmin = nmax = None
for ei in e:
got.update(ei)
nmin = min(ei[0], nmin) if nmin is not None else ei[0]
nmax = max(ei[1], nmax) if nmax is not None else ei[1]
rv.append(list(ei))
missing = set(range(nmin, nmax + 1)) - got
if missing:
missing = [i + nmin for i in missing]
if len(missing) == 1:
msg = 'Node %s is missing.' % missing.pop()
else:
msg = 'Nodes %s are missing.' % sorted(missing)
raise ValueError(msg)
if nmin != 0:
for i, ei in enumerate(rv):
rv[i] = [n - nmin for n in ei]
nmax -= nmin
return sorted(rv), nmax + 1
def prufer_rank(self):
"""Computes the rank of a Prufer sequence.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> a = Prufer([[0, 1], [0, 2], [0, 3]])
>>> a.prufer_rank()
0
See Also
========
rank, next, prev, size
"""
r = 0
p = 1
for i in range(self.nodes - 3, -1, -1):
r += p*self.prufer_repr[i]
p *= self.nodes
return r
@classmethod
def unrank(self, rank, n):
"""Finds the unranked Prufer sequence.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> Prufer.unrank(0, 4)
Prufer([0, 0])
"""
n, rank = as_int(n), as_int(rank)
L = defaultdict(int)
for i in range(n - 3, -1, -1):
L[i] = rank % n
rank = (rank - L[i])//n
return Prufer([L[i] for i in range(len(L))])
def __new__(cls, *args, **kw_args):
"""The constructor for the Prufer object.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
A Prufer object can be constructed from a list of edges:
>>> a = Prufer([[0, 1], [0, 2], [0, 3]])
>>> a.prufer_repr
[0, 0]
If the number of nodes is given, no checking of the nodes will
be performed; it will be assumed that nodes 0 through n - 1 are
present:
>>> Prufer([[0, 1], [0, 2], [0, 3]], 4)
Prufer([[0, 1], [0, 2], [0, 3]], 4)
A Prufer object can be constructed from a Prufer sequence:
>>> b = Prufer([1, 3])
>>> b.tree_repr
[[0, 1], [1, 3], [2, 3]]
"""
arg0 = Array(args[0]) if args[0] else Tuple()
args = (arg0,) + tuple(_sympify(arg) for arg in args[1:])
ret_obj = Basic.__new__(cls, *args, **kw_args)
args = [list(args[0])]
if args[0] and iterable(args[0][0]):
if not args[0][0]:
raise ValueError(
'Prufer expects at least one edge in the tree.')
if len(args) > 1:
nnodes = args[1]
else:
nodes = set(flatten(args[0]))
nnodes = max(nodes) + 1
if nnodes != len(nodes):
missing = set(range(nnodes)) - nodes
if len(missing) == 1:
msg = 'Node %s is missing.' % missing.pop()
else:
msg = 'Nodes %s are missing.' % sorted(missing)
raise ValueError(msg)
ret_obj._tree_repr = [list(i) for i in args[0]]
ret_obj._nodes = nnodes
else:
ret_obj._prufer_repr = args[0]
ret_obj._nodes = len(ret_obj._prufer_repr) + 2
return ret_obj
def next(self, delta=1):
"""Generates the Prufer sequence that is delta beyond the current one.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> a = Prufer([[0, 1], [0, 2], [0, 3]])
>>> b = a.next(1) # == a.next()
>>> b.tree_repr
[[0, 2], [0, 1], [1, 3]]
>>> b.rank
1
See Also
========
prufer_rank, rank, prev, size
"""
return Prufer.unrank(self.rank + delta, self.nodes)
def prev(self, delta=1):
"""Generates the Prufer sequence that is -delta before the current one.
Examples
========
>>> from sympy.combinatorics.prufer import Prufer
>>> a = Prufer([[0, 1], [1, 2], [2, 3], [1, 4]])
>>> a.rank
36
>>> b = a.prev()
>>> b
Prufer([1, 2, 0])
>>> b.rank
35
See Also
========
prufer_rank, rank, next, size
"""
return Prufer.unrank(self.rank -delta, self.nodes)

View File

@ -0,0 +1,453 @@
from collections import deque
from sympy.combinatorics.rewritingsystem_fsm import StateMachine
class RewritingSystem:
'''
A class implementing rewriting systems for `FpGroup`s.
References
==========
.. [1] Epstein, D., Holt, D. and Rees, S. (1991).
The use of Knuth-Bendix methods to solve the word problem in automatic groups.
Journal of Symbolic Computation, 12(4-5), pp.397-414.
.. [2] GAP's Manual on its KBMAG package
https://www.gap-system.org/Manuals/pkg/kbmag-1.5.3/doc/manual.pdf
'''
def __init__(self, group):
self.group = group
self.alphabet = group.generators
self._is_confluent = None
# these values are taken from [2]
self.maxeqns = 32767 # max rules
self.tidyint = 100 # rules before tidying
# _max_exceeded is True if maxeqns is exceeded
# at any point
self._max_exceeded = False
# Reduction automaton
self.reduction_automaton = None
self._new_rules = {}
# dictionary of reductions
self.rules = {}
self.rules_cache = deque([], 50)
self._init_rules()
# All the transition symbols in the automaton
generators = list(self.alphabet)
generators += [gen**-1 for gen in generators]
# Create a finite state machine as an instance of the StateMachine object
self.reduction_automaton = StateMachine('Reduction automaton for '+ repr(self.group), generators)
self.construct_automaton()
def set_max(self, n):
'''
Set the maximum number of rules that can be defined
'''
if n > self.maxeqns:
self._max_exceeded = False
self.maxeqns = n
return
@property
def is_confluent(self):
'''
Return `True` if the system is confluent
'''
if self._is_confluent is None:
self._is_confluent = self._check_confluence()
return self._is_confluent
def _init_rules(self):
identity = self.group.free_group.identity
for r in self.group.relators:
self.add_rule(r, identity)
self._remove_redundancies()
return
def _add_rule(self, r1, r2):
'''
Add the rule r1 -> r2 with no checking or further
deductions
'''
if len(self.rules) + 1 > self.maxeqns:
self._is_confluent = self._check_confluence()
self._max_exceeded = True
raise RuntimeError("Too many rules were defined.")
self.rules[r1] = r2
# Add the newly added rule to the `new_rules` dictionary.
if self.reduction_automaton:
self._new_rules[r1] = r2
def add_rule(self, w1, w2, check=False):
new_keys = set()
if w1 == w2:
return new_keys
if w1 < w2:
w1, w2 = w2, w1
if (w1, w2) in self.rules_cache:
return new_keys
self.rules_cache.append((w1, w2))
s1, s2 = w1, w2
# The following is the equivalent of checking
# s1 for overlaps with the implicit reductions
# {g*g**-1 -> <identity>} and {g**-1*g -> <identity>}
# for any generator g without installing the
# redundant rules that would result from processing
# the overlaps. See [1], Section 3 for details.
if len(s1) - len(s2) < 3:
if s1 not in self.rules:
new_keys.add(s1)
if not check:
self._add_rule(s1, s2)
if s2**-1 > s1**-1 and s2**-1 not in self.rules:
new_keys.add(s2**-1)
if not check:
self._add_rule(s2**-1, s1**-1)
# overlaps on the right
while len(s1) - len(s2) > -1:
g = s1[len(s1)-1]
s1 = s1.subword(0, len(s1)-1)
s2 = s2*g**-1
if len(s1) - len(s2) < 0:
if s2 not in self.rules:
if not check:
self._add_rule(s2, s1)
new_keys.add(s2)
elif len(s1) - len(s2) < 3:
new = self.add_rule(s1, s2, check)
new_keys.update(new)
# overlaps on the left
while len(w1) - len(w2) > -1:
g = w1[0]
w1 = w1.subword(1, len(w1))
w2 = g**-1*w2
if len(w1) - len(w2) < 0:
if w2 not in self.rules:
if not check:
self._add_rule(w2, w1)
new_keys.add(w2)
elif len(w1) - len(w2) < 3:
new = self.add_rule(w1, w2, check)
new_keys.update(new)
return new_keys
def _remove_redundancies(self, changes=False):
'''
Reduce left- and right-hand sides of reduction rules
and remove redundant equations (i.e. those for which
lhs == rhs). If `changes` is `True`, return a set
containing the removed keys and a set containing the
added keys
'''
removed = set()
added = set()
rules = self.rules.copy()
for r in rules:
v = self.reduce(r, exclude=r)
w = self.reduce(rules[r])
if v != r:
del self.rules[r]
removed.add(r)
if v > w:
added.add(v)
self.rules[v] = w
elif v < w:
added.add(w)
self.rules[w] = v
else:
self.rules[v] = w
if changes:
return removed, added
return
def make_confluent(self, check=False):
'''
Try to make the system confluent using the Knuth-Bendix
completion algorithm
'''
if self._max_exceeded:
return self._is_confluent
lhs = list(self.rules.keys())
def _overlaps(r1, r2):
len1 = len(r1)
len2 = len(r2)
result = []
for j in range(1, len1 + len2):
if (r1.subword(len1 - j, len1 + len2 - j, strict=False)
== r2.subword(j - len1, j, strict=False)):
a = r1.subword(0, len1-j, strict=False)
a = a*r2.subword(0, j-len1, strict=False)
b = r2.subword(j-len1, j, strict=False)
c = r2.subword(j, len2, strict=False)
c = c*r1.subword(len1 + len2 - j, len1, strict=False)
result.append(a*b*c)
return result
def _process_overlap(w, r1, r2, check):
s = w.eliminate_word(r1, self.rules[r1])
s = self.reduce(s)
t = w.eliminate_word(r2, self.rules[r2])
t = self.reduce(t)
if s != t:
if check:
# system not confluent
return [0]
try:
new_keys = self.add_rule(t, s, check)
return new_keys
except RuntimeError:
return False
return
added = 0
i = 0
while i < len(lhs):
r1 = lhs[i]
i += 1
# j could be i+1 to not
# check each pair twice but lhs
# is extended in the loop and the new
# elements have to be checked with the
# preceding ones. there is probably a better way
# to handle this
j = 0
while j < len(lhs):
r2 = lhs[j]
j += 1
if r1 == r2:
continue
overlaps = _overlaps(r1, r2)
overlaps.extend(_overlaps(r1**-1, r2))
if not overlaps:
continue
for w in overlaps:
new_keys = _process_overlap(w, r1, r2, check)
if new_keys:
if check:
return False
lhs.extend(new_keys)
added += len(new_keys)
elif new_keys == False:
# too many rules were added so the process
# couldn't complete
return self._is_confluent
if added > self.tidyint and not check:
# tidy up
r, a = self._remove_redundancies(changes=True)
added = 0
if r:
# reset i since some elements were removed
i = min(lhs.index(s) for s in r)
lhs = [l for l in lhs if l not in r]
lhs.extend(a)
if r1 in r:
# r1 was removed as redundant
break
self._is_confluent = True
if not check:
self._remove_redundancies()
return True
def _check_confluence(self):
return self.make_confluent(check=True)
def reduce(self, word, exclude=None):
'''
Apply reduction rules to `word` excluding the reduction rule
for the lhs equal to `exclude`
'''
rules = {r: self.rules[r] for r in self.rules if r != exclude}
# the following is essentially `eliminate_words()` code from the
# `FreeGroupElement` class, the only difference being the first
# "if" statement
again = True
new = word
while again:
again = False
for r in rules:
prev = new
if rules[r]**-1 > r**-1:
new = new.eliminate_word(r, rules[r], _all=True, inverse=False)
else:
new = new.eliminate_word(r, rules[r], _all=True)
if new != prev:
again = True
return new
def _compute_inverse_rules(self, rules):
'''
Compute the inverse rules for a given set of rules.
The inverse rules are used in the automaton for word reduction.
Arguments:
rules (dictionary): Rules for which the inverse rules are to computed.
Returns:
Dictionary of inverse_rules.
'''
inverse_rules = {}
for r in rules:
rule_key_inverse = r**-1
rule_value_inverse = (rules[r])**-1
if (rule_value_inverse < rule_key_inverse):
inverse_rules[rule_key_inverse] = rule_value_inverse
else:
inverse_rules[rule_value_inverse] = rule_key_inverse
return inverse_rules
def construct_automaton(self):
'''
Construct the automaton based on the set of reduction rules of the system.
Automata Design:
The accept states of the automaton are the proper prefixes of the left hand side of the rules.
The complete left hand side of the rules are the dead states of the automaton.
'''
self._add_to_automaton(self.rules)
def _add_to_automaton(self, rules):
'''
Add new states and transitions to the automaton.
Summary:
States corresponding to the new rules added to the system are computed and added to the automaton.
Transitions in the previously added states are also modified if necessary.
Arguments:
rules (dictionary) -- Dictionary of the newly added rules.
'''
# Automaton variables
automaton_alphabet = []
proper_prefixes = {}
# compute the inverses of all the new rules added
all_rules = rules
inverse_rules = self._compute_inverse_rules(all_rules)
all_rules.update(inverse_rules)
# Keep track of the accept_states.
accept_states = []
for rule in all_rules:
# The symbols present in the new rules are the symbols to be verified at each state.
# computes the automaton_alphabet, as the transitions solely depend upon the new states.
automaton_alphabet += rule.letter_form_elm
# Compute the proper prefixes for every rule.
proper_prefixes[rule] = []
letter_word_array = list(rule.letter_form_elm)
len_letter_word_array = len(letter_word_array)
for i in range (1, len_letter_word_array):
letter_word_array[i] = letter_word_array[i-1]*letter_word_array[i]
# Add accept states.
elem = letter_word_array[i-1]
if elem not in self.reduction_automaton.states:
self.reduction_automaton.add_state(elem, state_type='a')
accept_states.append(elem)
proper_prefixes[rule] = letter_word_array
# Check for overlaps between dead and accept states.
if rule in accept_states:
self.reduction_automaton.states[rule].state_type = 'd'
self.reduction_automaton.states[rule].rh_rule = all_rules[rule]
accept_states.remove(rule)
# Add dead states
if rule not in self.reduction_automaton.states:
self.reduction_automaton.add_state(rule, state_type='d', rh_rule=all_rules[rule])
automaton_alphabet = set(automaton_alphabet)
# Add new transitions for every state.
for state in self.reduction_automaton.states:
current_state_name = state
current_state_type = self.reduction_automaton.states[state].state_type
# Transitions will be modified only when suffixes of the current_state
# belongs to the proper_prefixes of the new rules.
# The rest are ignored if they cannot lead to a dead state after a finite number of transisitons.
if current_state_type == 's':
for letter in automaton_alphabet:
if letter in self.reduction_automaton.states:
self.reduction_automaton.states[state].add_transition(letter, letter)
else:
self.reduction_automaton.states[state].add_transition(letter, current_state_name)
elif current_state_type == 'a':
# Check if the transition to any new state in possible.
for letter in automaton_alphabet:
_next = current_state_name*letter
while len(_next) and _next not in self.reduction_automaton.states:
_next = _next.subword(1, len(_next))
if not len(_next):
_next = 'start'
self.reduction_automaton.states[state].add_transition(letter, _next)
# Add transitions for new states. All symbols used in the automaton are considered here.
# Ignore this if `reduction_automaton.automaton_alphabet` = `automaton_alphabet`.
if len(self.reduction_automaton.automaton_alphabet) != len(automaton_alphabet):
for state in accept_states:
current_state_name = state
for letter in self.reduction_automaton.automaton_alphabet:
_next = current_state_name*letter
while len(_next) and _next not in self.reduction_automaton.states:
_next = _next.subword(1, len(_next))
if not len(_next):
_next = 'start'
self.reduction_automaton.states[state].add_transition(letter, _next)
def reduce_using_automaton(self, word):
'''
Reduce a word using an automaton.
Summary:
All the symbols of the word are stored in an array and are given as the input to the automaton.
If the automaton reaches a dead state that subword is replaced and the automaton is run from the beginning.
The complete word has to be replaced when the word is read and the automaton reaches a dead state.
So, this process is repeated until the word is read completely and the automaton reaches the accept state.
Arguments:
word (instance of FreeGroupElement) -- Word that needs to be reduced.
'''
# Modify the automaton if new rules are found.
if self._new_rules:
self._add_to_automaton(self._new_rules)
self._new_rules = {}
flag = 1
while flag:
flag = 0
current_state = self.reduction_automaton.states['start']
for i, s in enumerate(word.letter_form_elm):
next_state_name = current_state.transitions[s]
next_state = self.reduction_automaton.states[next_state_name]
if next_state.state_type == 'd':
subst = next_state.rh_rule
word = word.substituted_word(i - len(next_state_name) + 1, i+1, subst)
flag = 1
break
current_state = next_state
return word

View File

@ -0,0 +1,60 @@
class State:
'''
A representation of a state managed by a ``StateMachine``.
Attributes:
name (instance of FreeGroupElement or string) -- State name which is also assigned to the Machine.
transisitons (OrderedDict) -- Represents all the transitions of the state object.
state_type (string) -- Denotes the type (accept/start/dead) of the state.
rh_rule (instance of FreeGroupElement) -- right hand rule for dead state.
state_machine (instance of StateMachine object) -- The finite state machine that the state belongs to.
'''
def __init__(self, name, state_machine, state_type=None, rh_rule=None):
self.name = name
self.transitions = {}
self.state_machine = state_machine
self.state_type = state_type[0]
self.rh_rule = rh_rule
def add_transition(self, letter, state):
'''
Add a transition from the current state to a new state.
Keyword Arguments:
letter -- The alphabet element the current state reads to make the state transition.
state -- This will be an instance of the State object which represents a new state after in the transition after the alphabet is read.
'''
self.transitions[letter] = state
class StateMachine:
'''
Representation of a finite state machine the manages the states and the transitions of the automaton.
Attributes:
states (dictionary) -- Collection of all registered `State` objects.
name (str) -- Name of the state machine.
'''
def __init__(self, name, automaton_alphabet):
self.name = name
self.automaton_alphabet = automaton_alphabet
self.states = {} # Contains all the states in the machine.
self.add_state('start', state_type='s')
def add_state(self, state_name, state_type=None, rh_rule=None):
'''
Instantiate a state object and stores it in the 'states' dictionary.
Arguments:
state_name (instance of FreeGroupElement or string) -- name of the new states.
state_type (string) -- Denotes the type (accept/start/dead) of the state added.
rh_rule (instance of FreeGroupElement) -- right hand rule for dead state.
'''
new_state = State(state_name, self, state_type, rh_rule)
self.states[state_name] = new_state
def __repr__(self):
return "%s" % (self.name)

View File

@ -0,0 +1,160 @@
"""
The Schur number S(k) is the largest integer n for which the interval [1,n]
can be partitioned into k sum-free sets.(https://mathworld.wolfram.com/SchurNumber.html)
"""
import math
from sympy.core import S
from sympy.core.basic import Basic
from sympy.core.function import Function
from sympy.core.numbers import Integer
class SchurNumber(Function):
r"""
This function creates a SchurNumber object
which is evaluated for `k \le 5` otherwise only
the lower bound information can be retrieved.
Examples
========
>>> from sympy.combinatorics.schur_number import SchurNumber
Since S(3) = 13, hence the output is a number
>>> SchurNumber(3)
13
We do not know the Schur number for values greater than 5, hence
only the object is returned
>>> SchurNumber(6)
SchurNumber(6)
Now, the lower bound information can be retrieved using lower_bound()
method
>>> SchurNumber(6).lower_bound()
536
"""
@classmethod
def eval(cls, k):
if k.is_Number:
if k is S.Infinity:
return S.Infinity
if k.is_zero:
return S.Zero
if not k.is_integer or k.is_negative:
raise ValueError("k should be a positive integer")
first_known_schur_numbers = {1: 1, 2: 4, 3: 13, 4: 44, 5: 160}
if k <= 5:
return Integer(first_known_schur_numbers[k])
def lower_bound(self):
f_ = self.args[0]
# Improved lower bounds known for S(6) and S(7)
if f_ == 6:
return Integer(536)
if f_ == 7:
return Integer(1680)
# For other cases, use general expression
if f_.is_Integer:
return 3*self.func(f_ - 1).lower_bound() - 1
return (3**f_ - 1)/2
def _schur_subsets_number(n):
if n is S.Infinity:
raise ValueError("Input must be finite")
if n <= 0:
raise ValueError("n must be a non-zero positive integer.")
elif n <= 3:
min_k = 1
else:
min_k = math.ceil(math.log(2*n + 1, 3))
return Integer(min_k)
def schur_partition(n):
"""
This function returns the partition in the minimum number of sum-free subsets
according to the lower bound given by the Schur Number.
Parameters
==========
n: a number
n is the upper limit of the range [1, n] for which we need to find and
return the minimum number of free subsets according to the lower bound
of schur number
Returns
=======
List of lists
List of the minimum number of sum-free subsets
Notes
=====
It is possible for some n to make the partition into less
subsets since the only known Schur numbers are:
S(1) = 1, S(2) = 4, S(3) = 13, S(4) = 44.
e.g for n = 44 the lower bound from the function above is 5 subsets but it has been proven
that can be done with 4 subsets.
Examples
========
For n = 1, 2, 3 the answer is the set itself
>>> from sympy.combinatorics.schur_number import schur_partition
>>> schur_partition(2)
[[1, 2]]
For n > 3, the answer is the minimum number of sum-free subsets:
>>> schur_partition(5)
[[3, 2], [5], [1, 4]]
>>> schur_partition(8)
[[3, 2], [6, 5, 8], [1, 4, 7]]
"""
if isinstance(n, Basic) and not n.is_Number:
raise ValueError("Input value must be a number")
number_of_subsets = _schur_subsets_number(n)
if n == 1:
sum_free_subsets = [[1]]
elif n == 2:
sum_free_subsets = [[1, 2]]
elif n == 3:
sum_free_subsets = [[1, 2, 3]]
else:
sum_free_subsets = [[1, 4], [2, 3]]
while len(sum_free_subsets) < number_of_subsets:
sum_free_subsets = _generate_next_list(sum_free_subsets, n)
missed_elements = [3*k + 1 for k in range(len(sum_free_subsets), (n-1)//3 + 1)]
sum_free_subsets[-1] += missed_elements
return sum_free_subsets
def _generate_next_list(current_list, n):
new_list = []
for item in current_list:
temp_1 = [number*3 for number in item if number*3 <= n]
temp_2 = [number*3 - 1 for number in item if number*3 - 1 <= n]
new_item = temp_1 + temp_2
new_list.append(new_item)
last_list = [3*k + 1 for k in range(len(current_list)+1) if 3*k + 1 <= n]
new_list.append(last_list)
current_list = new_list
return current_list

View File

@ -0,0 +1,619 @@
from itertools import combinations
from sympy.combinatorics.graycode import GrayCode
class Subset():
"""
Represents a basic subset object.
Explanation
===========
We generate subsets using essentially two techniques,
binary enumeration and lexicographic enumeration.
The Subset class takes two arguments, the first one
describes the initial subset to consider and the second
describes the superset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.next_binary().subset
['b']
>>> a.prev_binary().subset
['c']
"""
_rank_binary = None
_rank_lex = None
_rank_graycode = None
_subset = None
_superset = None
def __new__(cls, subset, superset):
"""
Default constructor.
It takes the ``subset`` and its ``superset`` as its parameters.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.subset
['c', 'd']
>>> a.superset
['a', 'b', 'c', 'd']
>>> a.size
2
"""
if len(subset) > len(superset):
raise ValueError('Invalid arguments have been provided. The '
'superset must be larger than the subset.')
for elem in subset:
if elem not in superset:
raise ValueError('The superset provided is invalid as it does '
'not contain the element {}'.format(elem))
obj = object.__new__(cls)
obj._subset = subset
obj._superset = superset
return obj
def __eq__(self, other):
"""Return a boolean indicating whether a == b on the basis of
whether both objects are of the class Subset and if the values
of the subset and superset attributes are the same.
"""
if not isinstance(other, Subset):
return NotImplemented
return self.subset == other.subset and self.superset == other.superset
def iterate_binary(self, k):
"""
This is a helper function. It iterates over the
binary subsets by ``k`` steps. This variable can be
both positive or negative.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.iterate_binary(-2).subset
['d']
>>> a = Subset(['a', 'b', 'c'], ['a', 'b', 'c', 'd'])
>>> a.iterate_binary(2).subset
[]
See Also
========
next_binary, prev_binary
"""
bin_list = Subset.bitlist_from_subset(self.subset, self.superset)
n = (int(''.join(bin_list), 2) + k) % 2**self.superset_size
bits = bin(n)[2:].rjust(self.superset_size, '0')
return Subset.subset_from_bitlist(self.superset, bits)
def next_binary(self):
"""
Generates the next binary ordered subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.next_binary().subset
['b']
>>> a = Subset(['a', 'b', 'c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.next_binary().subset
[]
See Also
========
prev_binary, iterate_binary
"""
return self.iterate_binary(1)
def prev_binary(self):
"""
Generates the previous binary ordered subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset([], ['a', 'b', 'c', 'd'])
>>> a.prev_binary().subset
['a', 'b', 'c', 'd']
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.prev_binary().subset
['c']
See Also
========
next_binary, iterate_binary
"""
return self.iterate_binary(-1)
def next_lexicographic(self):
"""
Generates the next lexicographically ordered subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.next_lexicographic().subset
['d']
>>> a = Subset(['d'], ['a', 'b', 'c', 'd'])
>>> a.next_lexicographic().subset
[]
See Also
========
prev_lexicographic
"""
i = self.superset_size - 1
indices = Subset.subset_indices(self.subset, self.superset)
if i in indices:
if i - 1 in indices:
indices.remove(i - 1)
else:
indices.remove(i)
i = i - 1
while i >= 0 and i not in indices:
i = i - 1
if i >= 0:
indices.remove(i)
indices.append(i+1)
else:
while i not in indices and i >= 0:
i = i - 1
indices.append(i + 1)
ret_set = []
super_set = self.superset
for i in indices:
ret_set.append(super_set[i])
return Subset(ret_set, super_set)
def prev_lexicographic(self):
"""
Generates the previous lexicographically ordered subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset([], ['a', 'b', 'c', 'd'])
>>> a.prev_lexicographic().subset
['d']
>>> a = Subset(['c','d'], ['a', 'b', 'c', 'd'])
>>> a.prev_lexicographic().subset
['c']
See Also
========
next_lexicographic
"""
i = self.superset_size - 1
indices = Subset.subset_indices(self.subset, self.superset)
while i >= 0 and i not in indices:
i = i - 1
if i == 0 or i - 1 in indices:
indices.remove(i)
else:
if i >= 0:
indices.remove(i)
indices.append(i - 1)
indices.append(self.superset_size - 1)
ret_set = []
super_set = self.superset
for i in indices:
ret_set.append(super_set[i])
return Subset(ret_set, super_set)
def iterate_graycode(self, k):
"""
Helper function used for prev_gray and next_gray.
It performs ``k`` step overs to get the respective Gray codes.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset([1, 2, 3], [1, 2, 3, 4])
>>> a.iterate_graycode(3).subset
[1, 4]
>>> a.iterate_graycode(-2).subset
[1, 2, 4]
See Also
========
next_gray, prev_gray
"""
unranked_code = GrayCode.unrank(self.superset_size,
(self.rank_gray + k) % self.cardinality)
return Subset.subset_from_bitlist(self.superset,
unranked_code)
def next_gray(self):
"""
Generates the next Gray code ordered subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset([1, 2, 3], [1, 2, 3, 4])
>>> a.next_gray().subset
[1, 3]
See Also
========
iterate_graycode, prev_gray
"""
return self.iterate_graycode(1)
def prev_gray(self):
"""
Generates the previous Gray code ordered subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset([2, 3, 4], [1, 2, 3, 4, 5])
>>> a.prev_gray().subset
[2, 3, 4, 5]
See Also
========
iterate_graycode, next_gray
"""
return self.iterate_graycode(-1)
@property
def rank_binary(self):
"""
Computes the binary ordered rank.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset([], ['a','b','c','d'])
>>> a.rank_binary
0
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.rank_binary
3
See Also
========
iterate_binary, unrank_binary
"""
if self._rank_binary is None:
self._rank_binary = int("".join(
Subset.bitlist_from_subset(self.subset,
self.superset)), 2)
return self._rank_binary
@property
def rank_lexicographic(self):
"""
Computes the lexicographic ranking of the subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.rank_lexicographic
14
>>> a = Subset([2, 4, 5], [1, 2, 3, 4, 5, 6])
>>> a.rank_lexicographic
43
"""
if self._rank_lex is None:
def _ranklex(self, subset_index, i, n):
if subset_index == [] or i > n:
return 0
if i in subset_index:
subset_index.remove(i)
return 1 + _ranklex(self, subset_index, i + 1, n)
return 2**(n - i - 1) + _ranklex(self, subset_index, i + 1, n)
indices = Subset.subset_indices(self.subset, self.superset)
self._rank_lex = _ranklex(self, indices, 0, self.superset_size)
return self._rank_lex
@property
def rank_gray(self):
"""
Computes the Gray code ranking of the subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c','d'], ['a','b','c','d'])
>>> a.rank_gray
2
>>> a = Subset([2, 4, 5], [1, 2, 3, 4, 5, 6])
>>> a.rank_gray
27
See Also
========
iterate_graycode, unrank_gray
"""
if self._rank_graycode is None:
bits = Subset.bitlist_from_subset(self.subset, self.superset)
self._rank_graycode = GrayCode(len(bits), start=bits).rank
return self._rank_graycode
@property
def subset(self):
"""
Gets the subset represented by the current instance.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.subset
['c', 'd']
See Also
========
superset, size, superset_size, cardinality
"""
return self._subset
@property
def size(self):
"""
Gets the size of the subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.size
2
See Also
========
subset, superset, superset_size, cardinality
"""
return len(self.subset)
@property
def superset(self):
"""
Gets the superset of the subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.superset
['a', 'b', 'c', 'd']
See Also
========
subset, size, superset_size, cardinality
"""
return self._superset
@property
def superset_size(self):
"""
Returns the size of the superset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.superset_size
4
See Also
========
subset, superset, size, cardinality
"""
return len(self.superset)
@property
def cardinality(self):
"""
Returns the number of all possible subsets.
Examples
========
>>> from sympy.combinatorics import Subset
>>> a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
>>> a.cardinality
16
See Also
========
subset, superset, size, superset_size
"""
return 2**(self.superset_size)
@classmethod
def subset_from_bitlist(self, super_set, bitlist):
"""
Gets the subset defined by the bitlist.
Examples
========
>>> from sympy.combinatorics import Subset
>>> Subset.subset_from_bitlist(['a', 'b', 'c', 'd'], '0011').subset
['c', 'd']
See Also
========
bitlist_from_subset
"""
if len(super_set) != len(bitlist):
raise ValueError("The sizes of the lists are not equal")
ret_set = []
for i in range(len(bitlist)):
if bitlist[i] == '1':
ret_set.append(super_set[i])
return Subset(ret_set, super_set)
@classmethod
def bitlist_from_subset(self, subset, superset):
"""
Gets the bitlist corresponding to a subset.
Examples
========
>>> from sympy.combinatorics import Subset
>>> Subset.bitlist_from_subset(['c', 'd'], ['a', 'b', 'c', 'd'])
'0011'
See Also
========
subset_from_bitlist
"""
bitlist = ['0'] * len(superset)
if isinstance(subset, Subset):
subset = subset.subset
for i in Subset.subset_indices(subset, superset):
bitlist[i] = '1'
return ''.join(bitlist)
@classmethod
def unrank_binary(self, rank, superset):
"""
Gets the binary ordered subset of the specified rank.
Examples
========
>>> from sympy.combinatorics import Subset
>>> Subset.unrank_binary(4, ['a', 'b', 'c', 'd']).subset
['b']
See Also
========
iterate_binary, rank_binary
"""
bits = bin(rank)[2:].rjust(len(superset), '0')
return Subset.subset_from_bitlist(superset, bits)
@classmethod
def unrank_gray(self, rank, superset):
"""
Gets the Gray code ordered subset of the specified rank.
Examples
========
>>> from sympy.combinatorics import Subset
>>> Subset.unrank_gray(4, ['a', 'b', 'c']).subset
['a', 'b']
>>> Subset.unrank_gray(0, ['a', 'b', 'c']).subset
[]
See Also
========
iterate_graycode, rank_gray
"""
graycode_bitlist = GrayCode.unrank(len(superset), rank)
return Subset.subset_from_bitlist(superset, graycode_bitlist)
@classmethod
def subset_indices(self, subset, superset):
"""Return indices of subset in superset in a list; the list is empty
if all elements of ``subset`` are not in ``superset``.
Examples
========
>>> from sympy.combinatorics import Subset
>>> superset = [1, 3, 2, 5, 4]
>>> Subset.subset_indices([3, 2, 1], superset)
[1, 2, 0]
>>> Subset.subset_indices([1, 6], superset)
[]
>>> Subset.subset_indices([], superset)
[]
"""
a, b = superset, subset
sb = set(b)
d = {}
for i, ai in enumerate(a):
if ai in sb:
d[ai] = i
sb.remove(ai)
if not sb:
break
else:
return []
return [d[bi] for bi in b]
def ksubsets(superset, k):
"""
Finds the subsets of size ``k`` in lexicographic order.
This uses the itertools generator.
Examples
========
>>> from sympy.combinatorics.subsets import ksubsets
>>> list(ksubsets([1, 2, 3], 2))
[(1, 2), (1, 3), (2, 3)]
>>> list(ksubsets([1, 2, 3, 4, 5], 2))
[(1, 2), (1, 3), (1, 4), (1, 5), (2, 3), (2, 4), \
(2, 5), (3, 4), (3, 5), (4, 5)]
See Also
========
Subset
"""
return combinations(superset, k)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,825 @@
from sympy.combinatorics.fp_groups import FpGroup
from sympy.combinatorics.coset_table import (CosetTable,
coset_enumeration_r, coset_enumeration_c)
from sympy.combinatorics.coset_table import modified_coset_enumeration_r
from sympy.combinatorics.free_groups import free_group
from sympy.testing.pytest import slow
"""
References
==========
[1] Holt, D., Eick, B., O'Brien, E.
"Handbook of Computational Group Theory"
[2] John J. Cannon; Lucien A. Dimino; George Havas; Jane M. Watson
Mathematics of Computation, Vol. 27, No. 123. (Jul., 1973), pp. 463-490.
"Implementation and Analysis of the Todd-Coxeter Algorithm"
"""
def test_scan_1():
# Example 5.1 from [1]
F, x, y = free_group("x, y")
f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y])
c = CosetTable(f, [x])
c.scan_and_fill(0, x)
assert c.table == [[0, 0, None, None]]
assert c.p == [0]
assert c.n == 1
assert c.omega == [0]
c.scan_and_fill(0, x**3)
assert c.table == [[0, 0, None, None]]
assert c.p == [0]
assert c.n == 1
assert c.omega == [0]
c.scan_and_fill(0, y**3)
assert c.table == [[0, 0, 1, 2], [None, None, 2, 0], [None, None, 0, 1]]
assert c.p == [0, 1, 2]
assert c.n == 3
assert c.omega == [0, 1, 2]
c.scan_and_fill(0, x**-1*y**-1*x*y)
assert c.table == [[0, 0, 1, 2], [None, None, 2, 0], [2, 2, 0, 1]]
assert c.p == [0, 1, 2]
assert c.n == 3
assert c.omega == [0, 1, 2]
c.scan_and_fill(1, x**3)
assert c.table == [[0, 0, 1, 2], [3, 4, 2, 0], [2, 2, 0, 1], \
[4, 1, None, None], [1, 3, None, None]]
assert c.p == [0, 1, 2, 3, 4]
assert c.n == 5
assert c.omega == [0, 1, 2, 3, 4]
c.scan_and_fill(1, y**3)
assert c.table == [[0, 0, 1, 2], [3, 4, 2, 0], [2, 2, 0, 1], \
[4, 1, None, None], [1, 3, None, None]]
assert c.p == [0, 1, 2, 3, 4]
assert c.n == 5
assert c.omega == [0, 1, 2, 3, 4]
c.scan_and_fill(1, x**-1*y**-1*x*y)
assert c.table == [[0, 0, 1, 2], [1, 1, 2, 0], [2, 2, 0, 1], \
[None, 1, None, None], [1, 3, None, None]]
assert c.p == [0, 1, 2, 1, 1]
assert c.n == 3
assert c.omega == [0, 1, 2]
# Example 5.2 from [1]
f = FpGroup(F, [x**2, y**3, (x*y)**3])
c = CosetTable(f, [x*y])
c.scan_and_fill(0, x*y)
assert c.table == [[1, None, None, 1], [None, 0, 0, None]]
assert c.p == [0, 1]
assert c.n == 2
assert c.omega == [0, 1]
c.scan_and_fill(0, x**2)
assert c.table == [[1, 1, None, 1], [0, 0, 0, None]]
assert c.p == [0, 1]
assert c.n == 2
assert c.omega == [0, 1]
c.scan_and_fill(0, y**3)
assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [None, None, 1, 0]]
assert c.p == [0, 1, 2]
assert c.n == 3
assert c.omega == [0, 1, 2]
c.scan_and_fill(0, (x*y)**3)
assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [None, None, 1, 0]]
assert c.p == [0, 1, 2]
assert c.n == 3
assert c.omega == [0, 1, 2]
c.scan_and_fill(1, x**2)
assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [None, None, 1, 0]]
assert c.p == [0, 1, 2]
assert c.n == 3
assert c.omega == [0, 1, 2]
c.scan_and_fill(1, y**3)
assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [None, None, 1, 0]]
assert c.p == [0, 1, 2]
assert c.n == 3
assert c.omega == [0, 1, 2]
c.scan_and_fill(1, (x*y)**3)
assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [3, 4, 1, 0], [None, 2, 4, None], [2, None, None, 3]]
assert c.p == [0, 1, 2, 3, 4]
assert c.n == 5
assert c.omega == [0, 1, 2, 3, 4]
c.scan_and_fill(2, x**2)
assert c.table == [[1, 1, 2, 1], [0, 0, 0, 2], [3, 3, 1, 0], [2, 2, 3, 3], [2, None, None, 3]]
assert c.p == [0, 1, 2, 3, 3]
assert c.n == 4
assert c.omega == [0, 1, 2, 3]
@slow
def test_coset_enumeration():
# this test function contains the combined tests for the two strategies
# i.e. HLT and Felsch strategies.
# Example 5.1 from [1]
F, x, y = free_group("x, y")
f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y])
C_r = coset_enumeration_r(f, [x])
C_r.compress(); C_r.standardize()
C_c = coset_enumeration_c(f, [x])
C_c.compress(); C_c.standardize()
table1 = [[0, 0, 1, 2], [1, 1, 2, 0], [2, 2, 0, 1]]
assert C_r.table == table1
assert C_c.table == table1
# E1 from [2] Pg. 474
F, r, s, t = free_group("r, s, t")
E1 = FpGroup(F, [t**-1*r*t*r**-2, r**-1*s*r*s**-2, s**-1*t*s*t**-2])
C_r = coset_enumeration_r(E1, [])
C_r.compress()
C_c = coset_enumeration_c(E1, [])
C_c.compress()
table2 = [[0, 0, 0, 0, 0, 0]]
assert C_r.table == table2
# test for issue #11449
assert C_c.table == table2
# Cox group from [2] Pg. 474
F, a, b = free_group("a, b")
Cox = FpGroup(F, [a**6, b**6, (a*b)**2, (a**2*b**2)**2, (a**3*b**3)**5])
C_r = coset_enumeration_r(Cox, [a])
C_r.compress(); C_r.standardize()
C_c = coset_enumeration_c(Cox, [a])
C_c.compress(); C_c.standardize()
table3 = [[0, 0, 1, 2],
[2, 3, 4, 0],
[5, 1, 0, 6],
[1, 7, 8, 9],
[9, 10, 11, 1],
[12, 2, 9, 13],
[14, 9, 2, 11],
[3, 12, 15, 16],
[16, 17, 18, 3],
[6, 4, 3, 5],
[4, 19, 20, 21],
[21, 22, 6, 4],
[7, 5, 23, 24],
[25, 23, 5, 18],
[19, 6, 22, 26],
[24, 27, 28, 7],
[29, 8, 7, 30],
[8, 31, 32, 33],
[33, 34, 13, 8],
[10, 14, 35, 35],
[35, 36, 37, 10],
[30, 11, 10, 29],
[11, 38, 39, 14],
[13, 39, 38, 12],
[40, 15, 12, 41],
[42, 13, 34, 43],
[44, 35, 14, 45],
[15, 46, 47, 34],
[34, 48, 49, 15],
[50, 16, 21, 51],
[52, 21, 16, 49],
[17, 50, 53, 54],
[54, 55, 56, 17],
[41, 18, 17, 40],
[18, 28, 27, 25],
[26, 20, 19, 19],
[20, 57, 58, 59],
[59, 60, 51, 20],
[22, 52, 61, 23],
[23, 62, 63, 22],
[64, 24, 33, 65],
[48, 33, 24, 61],
[62, 25, 54, 66],
[67, 54, 25, 68],
[57, 26, 59, 69],
[70, 59, 26, 63],
[27, 64, 71, 72],
[72, 73, 68, 27],
[28, 41, 74, 75],
[75, 76, 30, 28],
[31, 29, 77, 78],
[79, 77, 29, 37],
[38, 30, 76, 80],
[78, 81, 82, 31],
[43, 32, 31, 42],
[32, 83, 84, 85],
[85, 86, 65, 32],
[36, 44, 87, 88],
[88, 89, 90, 36],
[45, 37, 36, 44],
[37, 82, 81, 79],
[80, 74, 41, 38],
[39, 42, 91, 92],
[92, 93, 45, 39],
[46, 40, 94, 95],
[96, 94, 40, 56],
[97, 91, 42, 82],
[83, 43, 98, 99],
[100, 98, 43, 47],
[101, 87, 44, 90],
[82, 45, 93, 97],
[95, 102, 103, 46],
[104, 47, 46, 105],
[47, 106, 107, 100],
[61, 108, 109, 48],
[105, 49, 48, 104],
[49, 110, 111, 52],
[51, 111, 110, 50],
[112, 53, 50, 113],
[114, 51, 60, 115],
[116, 61, 52, 117],
[53, 118, 119, 60],
[60, 70, 66, 53],
[55, 67, 120, 121],
[121, 122, 123, 55],
[113, 56, 55, 112],
[56, 103, 102, 96],
[69, 124, 125, 57],
[115, 58, 57, 114],
[58, 126, 127, 128],
[128, 128, 69, 58],
[66, 129, 130, 62],
[117, 63, 62, 116],
[63, 125, 124, 70],
[65, 109, 108, 64],
[131, 71, 64, 132],
[133, 65, 86, 134],
[135, 66, 70, 136],
[68, 130, 129, 67],
[137, 120, 67, 138],
[132, 68, 73, 131],
[139, 69, 128, 140],
[71, 141, 142, 86],
[86, 143, 144, 71],
[145, 72, 75, 146],
[147, 75, 72, 144],
[73, 145, 148, 120],
[120, 149, 150, 73],
[74, 151, 152, 94],
[94, 153, 146, 74],
[76, 147, 154, 77],
[77, 155, 156, 76],
[157, 78, 85, 158],
[143, 85, 78, 154],
[155, 79, 88, 159],
[160, 88, 79, 161],
[151, 80, 92, 162],
[163, 92, 80, 156],
[81, 157, 164, 165],
[165, 166, 161, 81],
[99, 107, 106, 83],
[134, 84, 83, 133],
[84, 167, 168, 169],
[169, 170, 158, 84],
[87, 171, 172, 93],
[93, 163, 159, 87],
[89, 160, 173, 174],
[174, 175, 176, 89],
[90, 90, 89, 101],
[91, 177, 178, 98],
[98, 179, 162, 91],
[180, 95, 100, 181],
[179, 100, 95, 152],
[153, 96, 121, 148],
[182, 121, 96, 183],
[177, 97, 165, 184],
[185, 165, 97, 172],
[186, 99, 169, 187],
[188, 169, 99, 178],
[171, 101, 174, 189],
[190, 174, 101, 176],
[102, 180, 191, 192],
[192, 193, 183, 102],
[103, 113, 194, 195],
[195, 196, 105, 103],
[106, 104, 197, 198],
[199, 197, 104, 109],
[110, 105, 196, 200],
[198, 201, 133, 106],
[107, 186, 202, 203],
[203, 204, 181, 107],
[108, 116, 205, 206],
[206, 207, 132, 108],
[109, 133, 201, 199],
[200, 194, 113, 110],
[111, 114, 208, 209],
[209, 210, 117, 111],
[118, 112, 211, 212],
[213, 211, 112, 123],
[214, 208, 114, 125],
[126, 115, 215, 216],
[217, 215, 115, 119],
[218, 205, 116, 130],
[125, 117, 210, 214],
[212, 219, 220, 118],
[136, 119, 118, 135],
[119, 221, 222, 217],
[122, 182, 223, 224],
[224, 225, 226, 122],
[138, 123, 122, 137],
[123, 220, 219, 213],
[124, 139, 227, 228],
[228, 229, 136, 124],
[216, 222, 221, 126],
[140, 127, 126, 139],
[127, 230, 231, 232],
[232, 233, 140, 127],
[129, 135, 234, 235],
[235, 236, 138, 129],
[130, 132, 207, 218],
[141, 131, 237, 238],
[239, 237, 131, 150],
[167, 134, 240, 241],
[242, 240, 134, 142],
[243, 234, 135, 220],
[221, 136, 229, 244],
[149, 137, 245, 246],
[247, 245, 137, 226],
[220, 138, 236, 243],
[244, 227, 139, 221],
[230, 140, 233, 248],
[238, 249, 250, 141],
[251, 142, 141, 252],
[142, 253, 254, 242],
[154, 255, 256, 143],
[252, 144, 143, 251],
[144, 257, 258, 147],
[146, 258, 257, 145],
[259, 148, 145, 260],
[261, 146, 153, 262],
[263, 154, 147, 264],
[148, 265, 266, 153],
[246, 267, 268, 149],
[260, 150, 149, 259],
[150, 250, 249, 239],
[162, 269, 270, 151],
[262, 152, 151, 261],
[152, 271, 272, 179],
[159, 273, 274, 155],
[264, 156, 155, 263],
[156, 270, 269, 163],
[158, 256, 255, 157],
[275, 164, 157, 276],
[277, 158, 170, 278],
[279, 159, 163, 280],
[161, 274, 273, 160],
[281, 173, 160, 282],
[276, 161, 166, 275],
[283, 162, 179, 284],
[164, 285, 286, 170],
[170, 188, 184, 164],
[166, 185, 189, 173],
[173, 287, 288, 166],
[241, 254, 253, 167],
[278, 168, 167, 277],
[168, 289, 290, 291],
[291, 292, 187, 168],
[189, 293, 294, 171],
[280, 172, 171, 279],
[172, 295, 296, 185],
[175, 190, 297, 297],
[297, 298, 299, 175],
[282, 176, 175, 281],
[176, 294, 293, 190],
[184, 296, 295, 177],
[284, 178, 177, 283],
[178, 300, 301, 188],
[181, 272, 271, 180],
[302, 191, 180, 303],
[304, 181, 204, 305],
[183, 266, 265, 182],
[306, 223, 182, 307],
[303, 183, 193, 302],
[308, 184, 188, 309],
[310, 189, 185, 311],
[187, 301, 300, 186],
[305, 202, 186, 304],
[312, 187, 292, 313],
[314, 297, 190, 315],
[191, 316, 317, 204],
[204, 318, 319, 191],
[320, 192, 195, 321],
[322, 195, 192, 319],
[193, 320, 323, 223],
[223, 324, 325, 193],
[194, 326, 327, 211],
[211, 328, 321, 194],
[196, 322, 329, 197],
[197, 330, 331, 196],
[332, 198, 203, 333],
[318, 203, 198, 329],
[330, 199, 206, 334],
[335, 206, 199, 336],
[326, 200, 209, 337],
[338, 209, 200, 331],
[201, 332, 339, 240],
[240, 340, 336, 201],
[202, 341, 342, 292],
[292, 343, 333, 202],
[205, 344, 345, 210],
[210, 338, 334, 205],
[207, 335, 346, 237],
[237, 347, 348, 207],
[208, 349, 350, 215],
[215, 351, 337, 208],
[352, 212, 217, 353],
[351, 217, 212, 327],
[328, 213, 224, 323],
[354, 224, 213, 355],
[349, 214, 228, 356],
[357, 228, 214, 345],
[358, 216, 232, 359],
[360, 232, 216, 350],
[344, 218, 235, 361],
[362, 235, 218, 348],
[219, 352, 363, 364],
[364, 365, 355, 219],
[222, 358, 366, 367],
[367, 368, 353, 222],
[225, 354, 369, 370],
[370, 371, 372, 225],
[307, 226, 225, 306],
[226, 268, 267, 247],
[227, 373, 374, 233],
[233, 360, 356, 227],
[229, 357, 361, 234],
[234, 375, 376, 229],
[248, 231, 230, 230],
[231, 377, 378, 379],
[379, 380, 359, 231],
[236, 362, 381, 245],
[245, 382, 383, 236],
[384, 238, 242, 385],
[340, 242, 238, 346],
[347, 239, 246, 381],
[386, 246, 239, 387],
[388, 241, 291, 389],
[343, 291, 241, 339],
[375, 243, 364, 390],
[391, 364, 243, 383],
[373, 244, 367, 392],
[393, 367, 244, 376],
[382, 247, 370, 394],
[395, 370, 247, 396],
[377, 248, 379, 397],
[398, 379, 248, 374],
[249, 384, 399, 400],
[400, 401, 387, 249],
[250, 260, 402, 403],
[403, 404, 252, 250],
[253, 251, 405, 406],
[407, 405, 251, 256],
[257, 252, 404, 408],
[406, 409, 277, 253],
[254, 388, 410, 411],
[411, 412, 385, 254],
[255, 263, 413, 414],
[414, 415, 276, 255],
[256, 277, 409, 407],
[408, 402, 260, 257],
[258, 261, 416, 417],
[417, 418, 264, 258],
[265, 259, 419, 420],
[421, 419, 259, 268],
[422, 416, 261, 270],
[271, 262, 423, 424],
[425, 423, 262, 266],
[426, 413, 263, 274],
[270, 264, 418, 422],
[420, 427, 307, 265],
[266, 303, 428, 425],
[267, 386, 429, 430],
[430, 431, 396, 267],
[268, 307, 427, 421],
[269, 283, 432, 433],
[433, 434, 280, 269],
[424, 428, 303, 271],
[272, 304, 435, 436],
[436, 437, 284, 272],
[273, 279, 438, 439],
[439, 440, 282, 273],
[274, 276, 415, 426],
[285, 275, 441, 442],
[443, 441, 275, 288],
[289, 278, 444, 445],
[446, 444, 278, 286],
[447, 438, 279, 294],
[295, 280, 434, 448],
[287, 281, 449, 450],
[451, 449, 281, 299],
[294, 282, 440, 447],
[448, 432, 283, 295],
[300, 284, 437, 452],
[442, 453, 454, 285],
[309, 286, 285, 308],
[286, 455, 456, 446],
[450, 457, 458, 287],
[311, 288, 287, 310],
[288, 454, 453, 443],
[445, 456, 455, 289],
[313, 290, 289, 312],
[290, 459, 460, 461],
[461, 462, 389, 290],
[293, 310, 463, 464],
[464, 465, 315, 293],
[296, 308, 466, 467],
[467, 468, 311, 296],
[298, 314, 469, 470],
[470, 471, 472, 298],
[315, 299, 298, 314],
[299, 458, 457, 451],
[452, 435, 304, 300],
[301, 312, 473, 474],
[474, 475, 309, 301],
[316, 302, 476, 477],
[478, 476, 302, 325],
[341, 305, 479, 480],
[481, 479, 305, 317],
[324, 306, 482, 483],
[484, 482, 306, 372],
[485, 466, 308, 454],
[455, 309, 475, 486],
[487, 463, 310, 458],
[454, 311, 468, 485],
[486, 473, 312, 455],
[459, 313, 488, 489],
[490, 488, 313, 342],
[491, 469, 314, 472],
[458, 315, 465, 487],
[477, 492, 485, 316],
[463, 317, 316, 468],
[317, 487, 493, 481],
[329, 447, 464, 318],
[468, 319, 318, 463],
[319, 467, 448, 322],
[321, 448, 467, 320],
[475, 323, 320, 466],
[432, 321, 328, 437],
[438, 329, 322, 434],
[323, 474, 452, 328],
[483, 494, 486, 324],
[466, 325, 324, 475],
[325, 485, 492, 478],
[337, 422, 433, 326],
[437, 327, 326, 432],
[327, 436, 424, 351],
[334, 426, 439, 330],
[434, 331, 330, 438],
[331, 433, 422, 338],
[333, 464, 447, 332],
[449, 339, 332, 440],
[465, 333, 343, 469],
[413, 334, 338, 418],
[336, 439, 426, 335],
[441, 346, 335, 415],
[440, 336, 340, 449],
[416, 337, 351, 423],
[339, 451, 470, 343],
[346, 443, 450, 340],
[480, 493, 487, 341],
[469, 342, 341, 465],
[342, 491, 495, 490],
[361, 407, 414, 344],
[418, 345, 344, 413],
[345, 417, 408, 357],
[381, 446, 442, 347],
[415, 348, 347, 441],
[348, 414, 407, 362],
[356, 408, 417, 349],
[423, 350, 349, 416],
[350, 425, 420, 360],
[353, 424, 436, 352],
[479, 363, 352, 435],
[428, 353, 368, 476],
[355, 452, 474, 354],
[488, 369, 354, 473],
[435, 355, 365, 479],
[402, 356, 360, 419],
[405, 361, 357, 404],
[359, 420, 425, 358],
[476, 366, 358, 428],
[427, 359, 380, 482],
[444, 381, 362, 409],
[363, 481, 477, 368],
[368, 393, 390, 363],
[365, 391, 394, 369],
[369, 490, 480, 365],
[366, 478, 483, 380],
[380, 398, 392, 366],
[371, 395, 496, 497],
[497, 498, 489, 371],
[473, 372, 371, 488],
[372, 486, 494, 484],
[392, 400, 403, 373],
[419, 374, 373, 402],
[374, 421, 430, 398],
[390, 411, 406, 375],
[404, 376, 375, 405],
[376, 403, 400, 393],
[397, 430, 421, 377],
[482, 378, 377, 427],
[378, 484, 497, 499],
[499, 499, 397, 378],
[394, 461, 445, 382],
[409, 383, 382, 444],
[383, 406, 411, 391],
[385, 450, 443, 384],
[492, 399, 384, 453],
[457, 385, 412, 493],
[387, 442, 446, 386],
[494, 429, 386, 456],
[453, 387, 401, 492],
[389, 470, 451, 388],
[493, 410, 388, 457],
[471, 389, 462, 495],
[412, 390, 393, 399],
[462, 394, 391, 410],
[401, 392, 398, 429],
[396, 445, 461, 395],
[498, 496, 395, 460],
[456, 396, 431, 494],
[431, 397, 499, 496],
[399, 477, 481, 412],
[429, 483, 478, 401],
[410, 480, 490, 462],
[496, 497, 484, 431],
[489, 495, 491, 459],
[495, 460, 459, 471],
[460, 489, 498, 498],
[472, 472, 471, 491]]
assert C_r.table == table3
assert C_c.table == table3
# Group denoted by B2,4 from [2] Pg. 474
F, a, b = free_group("a, b")
B_2_4 = FpGroup(F, [a**4, b**4, (a*b)**4, (a**-1*b)**4, (a**2*b)**4, \
(a*b**2)**4, (a**2*b**2)**4, (a**-1*b*a*b)**4, (a*b**-1*a*b)**4])
C_r = coset_enumeration_r(B_2_4, [a])
C_c = coset_enumeration_c(B_2_4, [a])
index_r = 0
for i in range(len(C_r.p)):
if C_r.p[i] == i:
index_r += 1
assert index_r == 1024
index_c = 0
for i in range(len(C_c.p)):
if C_c.p[i] == i:
index_c += 1
assert index_c == 1024
# trivial Macdonald group G(2,2) from [2] Pg. 480
M = FpGroup(F, [b**-1*a**-1*b*a*b**-1*a*b*a**-2, a**-1*b**-1*a*b*a**-1*b*a*b**-2])
C_r = coset_enumeration_r(M, [a])
C_r.compress(); C_r.standardize()
C_c = coset_enumeration_c(M, [a])
C_c.compress(); C_c.standardize()
table4 = [[0, 0, 0, 0]]
assert C_r.table == table4
assert C_c.table == table4
def test_look_ahead():
# Section 3.2 [Test Example] Example (d) from [2]
F, a, b, c = free_group("a, b, c")
f = FpGroup(F, [a**11, b**5, c**4, (a*c)**3, b**2*c**-1*b**-1*c, a**4*b**-1*a**-1*b])
H = [c, b, c**2]
table0 = [[1, 2, 0, 0, 0, 0],
[3, 0, 4, 5, 6, 7],
[0, 8, 9, 10, 11, 12],
[5, 1, 10, 13, 14, 15],
[16, 5, 16, 1, 17, 18],
[4, 3, 1, 8, 19, 20],
[12, 21, 22, 23, 24, 1],
[25, 26, 27, 28, 1, 24],
[2, 10, 5, 16, 22, 28],
[10, 13, 13, 2, 29, 30]]
CosetTable.max_stack_size = 10
C_c = coset_enumeration_c(f, H)
C_c.compress(); C_c.standardize()
assert C_c.table[: 10] == table0
def test_modified_methods():
'''
Tests for modified coset table methods.
Example 5.7 from [1] Holt, D., Eick, B., O'Brien
"Handbook of Computational Group Theory".
'''
F, x, y = free_group("x, y")
f = FpGroup(F, [x**3, y**5, (x*y)**2])
H = [x*y, x**-1*y**-1*x*y*x]
C = CosetTable(f, H)
C.modified_define(0, x)
identity = C._grp.identity
a_0 = C._grp.generators[0]
a_1 = C._grp.generators[1]
assert C.P == [[identity, None, None, None],
[None, identity, None, None]]
assert C.table == [[1, None, None, None],
[None, 0, None, None]]
C.modified_define(1, x)
assert C.table == [[1, None, None, None],
[2, 0, None, None],
[None, 1, None, None]]
assert C.P == [[identity, None, None, None],
[identity, identity, None, None],
[None, identity, None, None]]
C.modified_scan(0, x**3, C._grp.identity, fill=False)
assert C.P == [[identity, identity, None, None],
[identity, identity, None, None],
[identity, identity, None, None]]
assert C.table == [[1, 2, None, None],
[2, 0, None, None],
[0, 1, None, None]]
C.modified_scan(0, x*y, C._grp.generators[0], fill=False)
assert C.P == [[identity, identity, None, a_0**-1],
[identity, identity, a_0, None],
[identity, identity, None, None]]
assert C.table == [[1, 2, None, 1],
[2, 0, 0, None],
[0, 1, None, None]]
C.modified_define(2, y**-1)
assert C.table == [[1, 2, None, 1],
[2, 0, 0, None],
[0, 1, None, 3],
[None, None, 2, None]]
assert C.P == [[identity, identity, None, a_0**-1],
[identity, identity, a_0, None],
[identity, identity, None, identity],
[None, None, identity, None]]
C.modified_scan(0, x**-1*y**-1*x*y*x, C._grp.generators[1])
assert C.table == [[1, 2, None, 1],
[2, 0, 0, None],
[0, 1, None, 3],
[3, 3, 2, None]]
assert C.P == [[identity, identity, None, a_0**-1],
[identity, identity, a_0, None],
[identity, identity, None, identity],
[a_1, a_1**-1, identity, None]]
C.modified_scan(2, (x*y)**2, C._grp.identity)
assert C.table == [[1, 2, 3, 1],
[2, 0, 0, None],
[0, 1, None, 3],
[3, 3, 2, 0]]
assert C.P == [[identity, identity, a_1**-1, a_0**-1],
[identity, identity, a_0, None],
[identity, identity, None, identity],
[a_1, a_1**-1, identity, a_1]]
C.modified_define(2, y)
assert C.table == [[1, 2, 3, 1],
[2, 0, 0, None],
[0, 1, 4, 3],
[3, 3, 2, 0],
[None, None, None, 2]]
assert C.P == [[identity, identity, a_1**-1, a_0**-1],
[identity, identity, a_0, None],
[identity, identity, identity, identity],
[a_1, a_1**-1, identity, a_1],
[None, None, None, identity]]
C.modified_scan(0, y**5, C._grp.identity)
assert C.table == [[1, 2, 3, 1], [2, 0, 0, 4], [0, 1, 4, 3], [3, 3, 2, 0], [None, None, 1, 2]]
assert C.P == [[identity, identity, a_1**-1, a_0**-1],
[identity, identity, a_0, a_0*a_1**-1],
[identity, identity, identity, identity],
[a_1, a_1**-1, identity, a_1],
[None, None, a_1*a_0**-1, identity]]
C.modified_scan(1, (x*y)**2, C._grp.identity)
assert C.table == [[1, 2, 3, 1],
[2, 0, 0, 4],
[0, 1, 4, 3],
[3, 3, 2, 0],
[4, 4, 1, 2]]
assert C.P == [[identity, identity, a_1**-1, a_0**-1],
[identity, identity, a_0, a_0*a_1**-1],
[identity, identity, identity, identity],
[a_1, a_1**-1, identity, a_1],
[a_0*a_1**-1, a_1*a_0**-1, a_1*a_0**-1, identity]]
# Modified coset enumeration test
f = FpGroup(F, [x**3, y**3, x**-1*y**-1*x*y])
C = coset_enumeration_r(f, [x])
C_m = modified_coset_enumeration_r(f, [x])
assert C_m.table == C.table

View File

@ -0,0 +1,257 @@
from sympy.core.singleton import S
from sympy.combinatorics.fp_groups import (FpGroup, low_index_subgroups,
reidemeister_presentation, FpSubgroup,
simplify_presentation)
from sympy.combinatorics.free_groups import (free_group, FreeGroup)
from sympy.testing.pytest import slow
"""
References
==========
[1] Holt, D., Eick, B., O'Brien, E.
"Handbook of Computational Group Theory"
[2] John J. Cannon; Lucien A. Dimino; George Havas; Jane M. Watson
Mathematics of Computation, Vol. 27, No. 123. (Jul., 1973), pp. 463-490.
"Implementation and Analysis of the Todd-Coxeter Algorithm"
[3] PROC. SECOND INTERNAT. CONF. THEORY OF GROUPS, CANBERRA 1973,
pp. 347-356. "A Reidemeister-Schreier program" by George Havas.
http://staff.itee.uq.edu.au/havas/1973cdhw.pdf
"""
def test_low_index_subgroups():
F, x, y = free_group("x, y")
# Example 5.10 from [1] Pg. 194
f = FpGroup(F, [x**2, y**3, (x*y)**4])
L = low_index_subgroups(f, 4)
t1 = [[[0, 0, 0, 0]],
[[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 3, 3]],
[[0, 0, 1, 2], [2, 2, 2, 0], [1, 1, 0, 1]],
[[1, 1, 0, 0], [0, 0, 1, 1]]]
for i in range(len(t1)):
assert L[i].table == t1[i]
f = FpGroup(F, [x**2, y**3, (x*y)**7])
L = low_index_subgroups(f, 15)
t2 = [[[0, 0, 0, 0]],
[[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5],
[4, 4, 5, 3], [6, 6, 3, 4], [5, 5, 6, 6]],
[[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5],
[6, 6, 5, 3], [5, 5, 3, 4], [4, 4, 6, 6]],
[[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5],
[6, 6, 5, 3], [7, 7, 3, 4], [4, 4, 8, 9], [5, 5, 10, 11],
[11, 11, 9, 6], [9, 9, 6, 8], [12, 12, 11, 7], [8, 8, 7, 10],
[10, 10, 13, 14], [14, 14, 14, 12], [13, 13, 12, 13]],
[[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5],
[6, 6, 5, 3], [7, 7, 3, 4], [4, 4, 8, 9], [5, 5, 10, 11],
[11, 11, 9, 6], [12, 12, 6, 8], [10, 10, 11, 7], [8, 8, 7, 10],
[9, 9, 13, 14], [14, 14, 14, 12], [13, 13, 12, 13]],
[[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5],
[6, 6, 5, 3], [7, 7, 3, 4], [4, 4, 8, 9], [5, 5, 10, 11],
[11, 11, 9, 6], [12, 12, 6, 8], [13, 13, 11, 7], [8, 8, 7, 10],
[9, 9, 12, 12], [10, 10, 13, 13]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 3, 3], [2, 2, 5, 6]
, [7, 7, 6, 4], [8, 8, 4, 5], [5, 5, 8, 9], [6, 6, 9, 7],
[10, 10, 7, 8], [9, 9, 11, 12], [11, 11, 12, 10], [13, 13, 10, 11],
[12, 12, 13, 13]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 3, 3], [2, 2, 5, 6]
, [7, 7, 6, 4], [8, 8, 4, 5], [5, 5, 8, 9], [6, 6, 9, 7],
[10, 10, 7, 8], [9, 9, 11, 12], [13, 13, 12, 10], [12, 12, 10, 11],
[11, 11, 13, 13]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 4, 4]
, [7, 7, 6, 3], [8, 8, 3, 5], [5, 5, 8, 9], [6, 6, 9, 7],
[10, 10, 7, 8], [9, 9, 11, 12], [13, 13, 12, 10], [12, 12, 10, 11],
[11, 11, 13, 13]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8]
, [5, 5, 6, 3], [9, 9, 3, 5], [10, 10, 8, 4], [8, 8, 4, 7],
[6, 6, 10, 11], [7, 7, 11, 9], [12, 12, 9, 10], [11, 11, 13, 14],
[14, 14, 14, 12], [13, 13, 12, 13]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8]
, [6, 6, 6, 3], [5, 5, 3, 5], [8, 8, 8, 4], [7, 7, 4, 7]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8]
, [9, 9, 6, 3], [6, 6, 3, 5], [10, 10, 8, 4], [11, 11, 4, 7],
[5, 5, 10, 12], [7, 7, 12, 9], [8, 8, 11, 11], [13, 13, 9, 10],
[12, 12, 13, 13]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8]
, [9, 9, 6, 3], [6, 6, 3, 5], [10, 10, 8, 4], [11, 11, 4, 7],
[5, 5, 12, 11], [7, 7, 10, 10], [8, 8, 9, 12], [13, 13, 11, 9],
[12, 12, 13, 13]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8]
, [9, 9, 6, 3], [10, 10, 3, 5], [7, 7, 8, 4], [11, 11, 4, 7],
[5, 5, 9, 9], [6, 6, 11, 12], [8, 8, 12, 10], [13, 13, 10, 11],
[12, 12, 13, 13]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8]
, [9, 9, 6, 3], [10, 10, 3, 5], [7, 7, 8, 4], [11, 11, 4, 7],
[5, 5, 12, 11], [6, 6, 10, 10], [8, 8, 9, 12], [13, 13, 11, 9],
[12, 12, 13, 13]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8]
, [9, 9, 6, 3], [10, 10, 3, 5], [11, 11, 8, 4], [12, 12, 4, 7],
[5, 5, 9, 9], [6, 6, 12, 13], [7, 7, 11, 11], [8, 8, 13, 10],
[13, 13, 10, 12]],
[[1, 1, 0, 0], [0, 0, 2, 3], [4, 4, 3, 1], [5, 5, 1, 2], [2, 2, 4, 4]
, [3, 3, 6, 7], [7, 7, 7, 5], [6, 6, 5, 6]]]
for i in range(len(t2)):
assert L[i].table == t2[i]
f = FpGroup(F, [x**2, y**3, (x*y)**7])
L = low_index_subgroups(f, 10, [x])
t3 = [[[0, 0, 0, 0]],
[[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5], [4, 4, 5, 3],
[6, 6, 3, 4], [5, 5, 6, 6]],
[[0, 0, 1, 2], [1, 1, 2, 0], [3, 3, 0, 1], [2, 2, 4, 5], [6, 6, 5, 3],
[5, 5, 3, 4], [4, 4, 6, 6]],
[[0, 0, 1, 2], [3, 3, 2, 0], [4, 4, 0, 1], [1, 1, 5, 6], [2, 2, 7, 8],
[6, 6, 6, 3], [5, 5, 3, 5], [8, 8, 8, 4], [7, 7, 4, 7]]]
for i in range(len(t3)):
assert L[i].table == t3[i]
def test_subgroup_presentations():
F, x, y = free_group("x, y")
f = FpGroup(F, [x**3, y**5, (x*y)**2])
H = [x*y, x**-1*y**-1*x*y*x]
p1 = reidemeister_presentation(f, H)
assert str(p1) == "((y_1, y_2), (y_1**2, y_2**3, y_2*y_1*y_2*y_1*y_2*y_1))"
H = f.subgroup(H)
assert (H.generators, H.relators) == p1
f = FpGroup(F, [x**3, y**3, (x*y)**3])
H = [x*y, x*y**-1]
p2 = reidemeister_presentation(f, H)
assert str(p2) == "((x_0, y_0), (x_0**3, y_0**3, x_0*y_0*x_0*y_0*x_0*y_0))"
f = FpGroup(F, [x**2*y**2, y**-1*x*y*x**-3])
H = [x]
p3 = reidemeister_presentation(f, H)
assert str(p3) == "((x_0,), (x_0**4,))"
f = FpGroup(F, [x**3*y**-3, (x*y)**3, (x*y**-1)**2])
H = [x]
p4 = reidemeister_presentation(f, H)
assert str(p4) == "((x_0,), (x_0**6,))"
# this presentation can be improved, the most simplified form
# of presentation is <a, b | a^11, b^2, (a*b)^3, (a^4*b*a^-5*b)^2>
# See [2] Pg 474 group PSL_2(11)
# This is the group PSL_2(11)
F, a, b, c = free_group("a, b, c")
f = FpGroup(F, [a**11, b**5, c**4, (b*c**2)**2, (a*b*c)**3, (a**4*c**2)**3, b**2*c**-1*b**-1*c, a**4*b**-1*a**-1*b])
H = [a, b, c**2]
gens, rels = reidemeister_presentation(f, H)
assert str(gens) == "(b_1, c_3)"
assert len(rels) == 18
@slow
def test_order():
F, x, y = free_group("x, y")
f = FpGroup(F, [x**4, y**2, x*y*x**-1*y])
assert f.order() == 8
f = FpGroup(F, [x*y*x**-1*y**-1, y**2])
assert f.order() is S.Infinity
F, a, b, c = free_group("a, b, c")
f = FpGroup(F, [a**250, b**2, c*b*c**-1*b, c**4, c**-1*a**-1*c*a, a**-1*b**-1*a*b])
assert f.order() == 2000
F, x = free_group("x")
f = FpGroup(F, [])
assert f.order() is S.Infinity
f = FpGroup(free_group('')[0], [])
assert f.order() == 1
def test_fp_subgroup():
def _test_subgroup(K, T, S):
_gens = T(K.generators)
assert all(elem in S for elem in _gens)
assert T.is_injective()
assert T.image().order() == S.order()
F, x, y = free_group("x, y")
f = FpGroup(F, [x**4, y**2, x*y*x**-1*y])
S = FpSubgroup(f, [x*y])
assert (x*y)**-3 in S
K, T = f.subgroup([x*y], homomorphism=True)
assert T(K.generators) == [y*x**-1]
_test_subgroup(K, T, S)
S = FpSubgroup(f, [x**-1*y*x])
assert x**-1*y**4*x in S
assert x**-1*y**4*x**2 not in S
K, T = f.subgroup([x**-1*y*x], homomorphism=True)
assert T(K.generators[0]**3) == y**3
_test_subgroup(K, T, S)
f = FpGroup(F, [x**3, y**5, (x*y)**2])
H = [x*y, x**-1*y**-1*x*y*x]
K, T = f.subgroup(H, homomorphism=True)
S = FpSubgroup(f, H)
_test_subgroup(K, T, S)
def test_permutation_methods():
F, x, y = free_group("x, y")
# DihedralGroup(8)
G = FpGroup(F, [x**2, y**8, x*y*x**-1*y])
T = G._to_perm_group()[1]
assert T.is_isomorphism()
assert G.center() == [y**4]
# DiheadralGroup(4)
G = FpGroup(F, [x**2, y**4, x*y*x**-1*y])
S = FpSubgroup(G, G.normal_closure([x]))
assert x in S
assert y**-1*x*y in S
# Z_5xZ_4
G = FpGroup(F, [x*y*x**-1*y**-1, y**5, x**4])
assert G.is_abelian
assert G.is_solvable
# AlternatingGroup(5)
G = FpGroup(F, [x**3, y**2, (x*y)**5])
assert not G.is_solvable
# AlternatingGroup(4)
G = FpGroup(F, [x**3, y**2, (x*y)**3])
assert len(G.derived_series()) == 3
S = FpSubgroup(G, G.derived_subgroup())
assert S.order() == 4
def test_simplify_presentation():
# ref #16083
G = simplify_presentation(FpGroup(FreeGroup([]), []))
assert not G.generators
assert not G.relators
# CyclicGroup(3)
# The second generator in <x, y | x^2, x^5, y^3> is trivial due to relators {x^2, x^5}
F, x, y = free_group("x, y")
G = simplify_presentation(FpGroup(F, [x**2, x**5, y**3]))
assert x in G.relators
def test_cyclic():
F, x, y = free_group("x, y")
f = FpGroup(F, [x*y, x**-1*y**-1*x*y*x])
assert f.is_cyclic
f = FpGroup(F, [x*y, x*y**-1])
assert f.is_cyclic
f = FpGroup(F, [x**4, y**2, x*y*x**-1*y])
assert not f.is_cyclic
def test_abelian_invariants():
F, x, y = free_group("x, y")
f = FpGroup(F, [x*y, x**-1*y**-1*x*y*x])
assert f.abelian_invariants() == []
f = FpGroup(F, [x*y, x*y**-1])
assert f.abelian_invariants() == [2]
f = FpGroup(F, [x**4, y**2, x*y*x**-1*y])
assert f.abelian_invariants() == [2, 4]

View File

@ -0,0 +1,221 @@
from sympy.combinatorics.free_groups import free_group, FreeGroup
from sympy.core import Symbol
from sympy.testing.pytest import raises
from sympy.core.numbers import oo
F, x, y, z = free_group("x, y, z")
def test_FreeGroup__init__():
x, y, z = map(Symbol, "xyz")
assert len(FreeGroup("x, y, z").generators) == 3
assert len(FreeGroup(x).generators) == 1
assert len(FreeGroup(("x", "y", "z"))) == 3
assert len(FreeGroup((x, y, z)).generators) == 3
def test_free_group():
G, a, b, c = free_group("a, b, c")
assert F.generators == (x, y, z)
assert x*z**2 in F
assert x in F
assert y*z**-1 in F
assert (y*z)**0 in F
assert a not in F
assert a**0 not in F
assert len(F) == 3
assert str(F) == '<free group on the generators (x, y, z)>'
assert not F == G
assert F.order() is oo
assert F.is_abelian == False
assert F.center() == {F.identity}
(e,) = free_group("")
assert e.order() == 1
assert e.generators == ()
assert e.elements == {e.identity}
assert e.is_abelian == True
def test_FreeGroup__hash__():
assert hash(F)
def test_FreeGroup__eq__():
assert free_group("x, y, z")[0] == free_group("x, y, z")[0]
assert free_group("x, y, z")[0] is free_group("x, y, z")[0]
assert free_group("x, y, z")[0] != free_group("a, x, y")[0]
assert free_group("x, y, z")[0] is not free_group("a, x, y")[0]
assert free_group("x, y")[0] != free_group("x, y, z")[0]
assert free_group("x, y")[0] is not free_group("x, y, z")[0]
assert free_group("x, y, z")[0] != free_group("x, y")[0]
assert free_group("x, y, z")[0] is not free_group("x, y")[0]
def test_FreeGroup__getitem__():
assert F[0:] == FreeGroup("x, y, z")
assert F[1:] == FreeGroup("y, z")
assert F[2:] == FreeGroup("z")
def test_FreeGroupElm__hash__():
assert hash(x*y*z)
def test_FreeGroupElm_copy():
f = x*y*z**3
g = f.copy()
h = x*y*z**7
assert f == g
assert f != h
def test_FreeGroupElm_inverse():
assert x.inverse() == x**-1
assert (x*y).inverse() == y**-1*x**-1
assert (y*x*y**-1).inverse() == y*x**-1*y**-1
assert (y**2*x**-1).inverse() == x*y**-2
def test_FreeGroupElm_type_error():
raises(TypeError, lambda: 2/x)
raises(TypeError, lambda: x**2 + y**2)
raises(TypeError, lambda: x/2)
def test_FreeGroupElm_methods():
assert (x**0).order() == 1
assert (y**2).order() is oo
assert (x**-1*y).commutator(x) == y**-1*x**-1*y*x
assert len(x**2*y**-1) == 3
assert len(x**-1*y**3*z) == 5
def test_FreeGroupElm_eliminate_word():
w = x**5*y*x**2*y**-4*x
assert w.eliminate_word( x, x**2 ) == x**10*y*x**4*y**-4*x**2
w3 = x**2*y**3*x**-1*y
assert w3.eliminate_word(x, x**2) == x**4*y**3*x**-2*y
assert w3.eliminate_word(x, y) == y**5
assert w3.eliminate_word(x, y**4) == y**8
assert w3.eliminate_word(y, x**-1) == x**-3
assert w3.eliminate_word(x, y*z) == y*z*y*z*y**3*z**-1
assert (y**-3).eliminate_word(y, x**-1*z**-1) == z*x*z*x*z*x
#assert w3.eliminate_word(x, y*x) == y*x*y*x**2*y*x*y*x*y*x*z**3
#assert w3.eliminate_word(x, x*y) == x*y*x**2*y*x*y*x*y*x*y*z**3
def test_FreeGroupElm_array_form():
assert (x*z).array_form == ((Symbol('x'), 1), (Symbol('z'), 1))
assert (x**2*z*y*x**-2).array_form == \
((Symbol('x'), 2), (Symbol('z'), 1), (Symbol('y'), 1), (Symbol('x'), -2))
assert (x**-2*y**-1).array_form == ((Symbol('x'), -2), (Symbol('y'), -1))
def test_FreeGroupElm_letter_form():
assert (x**3).letter_form == (Symbol('x'), Symbol('x'), Symbol('x'))
assert (x**2*z**-2*x).letter_form == \
(Symbol('x'), Symbol('x'), -Symbol('z'), -Symbol('z'), Symbol('x'))
def test_FreeGroupElm_ext_rep():
assert (x**2*z**-2*x).ext_rep == \
(Symbol('x'), 2, Symbol('z'), -2, Symbol('x'), 1)
assert (x**-2*y**-1).ext_rep == (Symbol('x'), -2, Symbol('y'), -1)
assert (x*z).ext_rep == (Symbol('x'), 1, Symbol('z'), 1)
def test_FreeGroupElm__mul__pow__():
x1 = x.group.dtype(((Symbol('x'), 1),))
assert x**2 == x1*x
assert (x**2*y*x**-2)**4 == x**2*y**4*x**-2
assert (x**2)**2 == x**4
assert (x**-1)**-1 == x
assert (x**-1)**0 == F.identity
assert (y**2)**-2 == y**-4
assert x**2*x**-1 == x
assert x**2*y**2*y**-1 == x**2*y
assert x*x**-1 == F.identity
assert x/x == F.identity
assert x/x**2 == x**-1
assert (x**2*y)/(x**2*y**-1) == x**2*y**2*x**-2
assert (x**2*y)/(y**-1*x**2) == x**2*y*x**-2*y
assert x*(x**-1*y*z*y**-1) == y*z*y**-1
assert x**2*(x**-2*y**-1*z**2*y) == y**-1*z**2*y
a = F.identity
for n in range(10):
assert a == x**n
assert a**-1 == x**-n
a *= x
def test_FreeGroupElm__len__():
assert len(x**5*y*x**2*y**-4*x) == 13
assert len(x**17) == 17
assert len(y**0) == 0
def test_FreeGroupElm_comparison():
assert not (x*y == y*x)
assert x**0 == y**0
assert x**2 < y**3
assert not x**3 < y**2
assert x*y < x**2*y
assert x**2*y**2 < y**4
assert not y**4 < y**-4
assert not y**4 < x**-4
assert y**-2 < y**2
assert x**2 <= y**2
assert x**2 <= x**2
assert not y*z > z*y
assert x > x**-1
assert not x**2 >= y**2
def test_FreeGroupElm_syllables():
w = x**5*y*x**2*y**-4*x
assert w.number_syllables() == 5
assert w.exponent_syllable(2) == 2
assert w.generator_syllable(3) == Symbol('y')
assert w.sub_syllables(1, 2) == y
assert w.sub_syllables(3, 3) == F.identity
def test_FreeGroup_exponents():
w1 = x**2*y**3
assert w1.exponent_sum(x) == 2
assert w1.exponent_sum(x**-1) == -2
assert w1.generator_count(x) == 2
w2 = x**2*y**4*x**-3
assert w2.exponent_sum(x) == -1
assert w2.generator_count(x) == 5
def test_FreeGroup_generators():
assert (x**2*y**4*z**-1).contains_generators() == {x, y, z}
assert (x**-1*y**3).contains_generators() == {x, y}
def test_FreeGroupElm_words():
w = x**5*y*x**2*y**-4*x
assert w.subword(2, 6) == x**3*y
assert w.subword(3, 2) == F.identity
assert w.subword(6, 10) == x**2*y**-2
assert w.substituted_word(0, 7, y**-1) == y**-1*x*y**-4*x
assert w.substituted_word(0, 7, y**2*x) == y**2*x**2*y**-4*x

View File

@ -0,0 +1,82 @@
"""Test groups defined by the galois module. """
from sympy.combinatorics.galois import (
S4TransitiveSubgroups, S5TransitiveSubgroups, S6TransitiveSubgroups,
find_transitive_subgroups_of_S6,
)
from sympy.combinatorics.homomorphisms import is_isomorphic
from sympy.combinatorics.named_groups import (
SymmetricGroup, AlternatingGroup, CyclicGroup,
)
def test_four_group():
G = S4TransitiveSubgroups.V.get_perm_group()
A4 = AlternatingGroup(4)
assert G.is_subgroup(A4)
assert G.degree == 4
assert G.is_transitive()
assert G.order() == 4
assert not G.is_cyclic
def test_M20():
G = S5TransitiveSubgroups.M20.get_perm_group()
S5 = SymmetricGroup(5)
A5 = AlternatingGroup(5)
assert G.is_subgroup(S5)
assert not G.is_subgroup(A5)
assert G.degree == 5
assert G.is_transitive()
assert G.order() == 20
# Setting this True means that for each of the transitive subgroups of S6,
# we run a test not only on the fixed representation, but also on one freshly
# generated by the search procedure.
INCLUDE_SEARCH_REPS = False
S6_randomized = {}
if INCLUDE_SEARCH_REPS:
S6_randomized = find_transitive_subgroups_of_S6(*list(S6TransitiveSubgroups))
def get_versions_of_S6_subgroup(name):
vers = [name.get_perm_group()]
if INCLUDE_SEARCH_REPS:
vers.append(S6_randomized[name])
return vers
def test_S6_transitive_subgroups():
"""
Test enough characteristics to distinguish all 16 transitive subgroups.
"""
ts = S6TransitiveSubgroups
A6 = AlternatingGroup(6)
for name, alt, order, is_isom, not_isom in [
(ts.C6, False, 6, CyclicGroup(6), None),
(ts.S3, False, 6, SymmetricGroup(3), None),
(ts.D6, False, 12, None, None),
(ts.A4, True, 12, None, None),
(ts.G18, False, 18, None, None),
(ts.A4xC2, False, 24, None, SymmetricGroup(4)),
(ts.S4m, False, 24, SymmetricGroup(4), None),
(ts.S4p, True, 24, None, None),
(ts.G36m, False, 36, None, None),
(ts.G36p, True, 36, None, None),
(ts.S4xC2, False, 48, None, None),
(ts.PSL2F5, True, 60, None, None),
(ts.G72, False, 72, None, None),
(ts.PGL2F5, False, 120, None, None),
(ts.A6, True, 360, None, None),
(ts.S6, False, 720, None, None),
]:
for G in get_versions_of_S6_subgroup(name):
assert G.is_transitive()
assert G.degree == 6
assert G.is_subgroup(A6) is alt
assert G.order() == order
if is_isom:
assert is_isomorphic(G, is_isom)
if not_isom:
assert not is_isomorphic(G, not_isom)

View File

@ -0,0 +1,105 @@
from sympy.combinatorics.generators import symmetric, cyclic, alternating, \
dihedral, rubik
from sympy.combinatorics.permutations import Permutation
from sympy.testing.pytest import raises
def test_generators():
assert list(cyclic(6)) == [
Permutation([0, 1, 2, 3, 4, 5]),
Permutation([1, 2, 3, 4, 5, 0]),
Permutation([2, 3, 4, 5, 0, 1]),
Permutation([3, 4, 5, 0, 1, 2]),
Permutation([4, 5, 0, 1, 2, 3]),
Permutation([5, 0, 1, 2, 3, 4])]
assert list(cyclic(10)) == [
Permutation([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
Permutation([1, 2, 3, 4, 5, 6, 7, 8, 9, 0]),
Permutation([2, 3, 4, 5, 6, 7, 8, 9, 0, 1]),
Permutation([3, 4, 5, 6, 7, 8, 9, 0, 1, 2]),
Permutation([4, 5, 6, 7, 8, 9, 0, 1, 2, 3]),
Permutation([5, 6, 7, 8, 9, 0, 1, 2, 3, 4]),
Permutation([6, 7, 8, 9, 0, 1, 2, 3, 4, 5]),
Permutation([7, 8, 9, 0, 1, 2, 3, 4, 5, 6]),
Permutation([8, 9, 0, 1, 2, 3, 4, 5, 6, 7]),
Permutation([9, 0, 1, 2, 3, 4, 5, 6, 7, 8])]
assert list(alternating(4)) == [
Permutation([0, 1, 2, 3]),
Permutation([0, 2, 3, 1]),
Permutation([0, 3, 1, 2]),
Permutation([1, 0, 3, 2]),
Permutation([1, 2, 0, 3]),
Permutation([1, 3, 2, 0]),
Permutation([2, 0, 1, 3]),
Permutation([2, 1, 3, 0]),
Permutation([2, 3, 0, 1]),
Permutation([3, 0, 2, 1]),
Permutation([3, 1, 0, 2]),
Permutation([3, 2, 1, 0])]
assert list(symmetric(3)) == [
Permutation([0, 1, 2]),
Permutation([0, 2, 1]),
Permutation([1, 0, 2]),
Permutation([1, 2, 0]),
Permutation([2, 0, 1]),
Permutation([2, 1, 0])]
assert list(symmetric(4)) == [
Permutation([0, 1, 2, 3]),
Permutation([0, 1, 3, 2]),
Permutation([0, 2, 1, 3]),
Permutation([0, 2, 3, 1]),
Permutation([0, 3, 1, 2]),
Permutation([0, 3, 2, 1]),
Permutation([1, 0, 2, 3]),
Permutation([1, 0, 3, 2]),
Permutation([1, 2, 0, 3]),
Permutation([1, 2, 3, 0]),
Permutation([1, 3, 0, 2]),
Permutation([1, 3, 2, 0]),
Permutation([2, 0, 1, 3]),
Permutation([2, 0, 3, 1]),
Permutation([2, 1, 0, 3]),
Permutation([2, 1, 3, 0]),
Permutation([2, 3, 0, 1]),
Permutation([2, 3, 1, 0]),
Permutation([3, 0, 1, 2]),
Permutation([3, 0, 2, 1]),
Permutation([3, 1, 0, 2]),
Permutation([3, 1, 2, 0]),
Permutation([3, 2, 0, 1]),
Permutation([3, 2, 1, 0])]
assert list(dihedral(1)) == [
Permutation([0, 1]), Permutation([1, 0])]
assert list(dihedral(2)) == [
Permutation([0, 1, 2, 3]),
Permutation([1, 0, 3, 2]),
Permutation([2, 3, 0, 1]),
Permutation([3, 2, 1, 0])]
assert list(dihedral(3)) == [
Permutation([0, 1, 2]),
Permutation([2, 1, 0]),
Permutation([1, 2, 0]),
Permutation([0, 2, 1]),
Permutation([2, 0, 1]),
Permutation([1, 0, 2])]
assert list(dihedral(5)) == [
Permutation([0, 1, 2, 3, 4]),
Permutation([4, 3, 2, 1, 0]),
Permutation([1, 2, 3, 4, 0]),
Permutation([0, 4, 3, 2, 1]),
Permutation([2, 3, 4, 0, 1]),
Permutation([1, 0, 4, 3, 2]),
Permutation([3, 4, 0, 1, 2]),
Permutation([2, 1, 0, 4, 3]),
Permutation([4, 0, 1, 2, 3]),
Permutation([3, 2, 1, 0, 4])]
raises(ValueError, lambda: rubik(1))

View File

@ -0,0 +1,72 @@
from sympy.combinatorics.graycode import (GrayCode, bin_to_gray,
random_bitstring, get_subset_from_bitstring, graycode_subsets,
gray_to_bin)
from sympy.testing.pytest import raises
def test_graycode():
g = GrayCode(2)
got = []
for i in g.generate_gray():
if i.startswith('0'):
g.skip()
got.append(i)
assert got == '00 11 10'.split()
a = GrayCode(6)
assert a.current == '0'*6
assert a.rank == 0
assert len(list(a.generate_gray())) == 64
codes = ['011001', '011011', '011010',
'011110', '011111', '011101', '011100', '010100', '010101', '010111',
'010110', '010010', '010011', '010001', '010000', '110000', '110001',
'110011', '110010', '110110', '110111', '110101', '110100', '111100',
'111101', '111111', '111110', '111010', '111011', '111001', '111000',
'101000', '101001', '101011', '101010', '101110', '101111', '101101',
'101100', '100100', '100101', '100111', '100110', '100010', '100011',
'100001', '100000']
assert list(a.generate_gray(start='011001')) == codes
assert list(
a.generate_gray(rank=GrayCode(6, start='011001').rank)) == codes
assert a.next().current == '000001'
assert a.next(2).current == '000011'
assert a.next(-1).current == '100000'
a = GrayCode(5, start='10010')
assert a.rank == 28
a = GrayCode(6, start='101000')
assert a.rank == 48
assert GrayCode(6, rank=4).current == '000110'
assert GrayCode(6, rank=4).rank == 4
assert [GrayCode(4, start=s).rank for s in
GrayCode(4).generate_gray()] == [0, 1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15]
a = GrayCode(15, rank=15)
assert a.current == '000000000001000'
assert bin_to_gray('111') == '100'
a = random_bitstring(5)
assert type(a) is str
assert len(a) == 5
assert all(i in ['0', '1'] for i in a)
assert get_subset_from_bitstring(
['a', 'b', 'c', 'd'], '0011') == ['c', 'd']
assert get_subset_from_bitstring('abcd', '1001') == ['a', 'd']
assert list(graycode_subsets(['a', 'b', 'c'])) == \
[[], ['c'], ['b', 'c'], ['b'], ['a', 'b'], ['a', 'b', 'c'],
['a', 'c'], ['a']]
raises(ValueError, lambda: GrayCode(0))
raises(ValueError, lambda: GrayCode(2.2))
raises(ValueError, lambda: GrayCode(2, start=[1, 1, 0]))
raises(ValueError, lambda: GrayCode(2, rank=2.5))
raises(ValueError, lambda: get_subset_from_bitstring(['c', 'a', 'c'], '1100'))
raises(ValueError, lambda: list(GrayCode(3).generate_gray(start="1111")))
def test_live_issue_117():
assert bin_to_gray('0100') == '0110'
assert bin_to_gray('0101') == '0111'
for bits in ('0100', '0101'):
assert gray_to_bin(bin_to_gray(bits)) == bits

View File

@ -0,0 +1,15 @@
from sympy.combinatorics.group_constructs import DirectProduct
from sympy.combinatorics.named_groups import CyclicGroup, DihedralGroup
def test_direct_product_n():
C = CyclicGroup(4)
D = DihedralGroup(4)
G = DirectProduct(C, C, C)
assert G.order() == 64
assert G.degree == 12
assert len(G.orbits()) == 3
assert G.is_abelian is True
H = DirectProduct(D, C)
assert H.order() == 32
assert H.is_abelian is False

View File

@ -0,0 +1,110 @@
from sympy.combinatorics.group_numbers import (is_nilpotent_number,
is_abelian_number, is_cyclic_number, _holder_formula, groups_count)
from sympy.ntheory.factor_ import factorint
from sympy.ntheory.generate import prime
from sympy.testing.pytest import raises
from sympy import randprime
def test_is_nilpotent_number():
assert is_nilpotent_number(21) == False
assert is_nilpotent_number(randprime(1, 30)**12) == True
raises(ValueError, lambda: is_nilpotent_number(-5))
A056867 = [1, 2, 3, 4, 5, 7, 8, 9, 11, 13, 15, 16, 17, 19,
23, 25, 27, 29, 31, 32, 33, 35, 37, 41, 43, 45,
47, 49, 51, 53, 59, 61, 64, 65, 67, 69, 71, 73,
77, 79, 81, 83, 85, 87, 89, 91, 95, 97, 99]
for n in range(1, 100):
assert is_nilpotent_number(n) == (n in A056867)
def test_is_abelian_number():
assert is_abelian_number(4) == True
assert is_abelian_number(randprime(1, 2000)**2) == True
assert is_abelian_number(randprime(1000, 100000)) == True
assert is_abelian_number(60) == False
assert is_abelian_number(24) == False
raises(ValueError, lambda: is_abelian_number(-5))
A051532 = [1, 2, 3, 4, 5, 7, 9, 11, 13, 15, 17, 19, 23, 25,
29, 31, 33, 35, 37, 41, 43, 45, 47, 49, 51, 53,
59, 61, 65, 67, 69, 71, 73, 77, 79, 83, 85, 87,
89, 91, 95, 97, 99]
for n in range(1, 100):
assert is_abelian_number(n) == (n in A051532)
A003277 = [1, 2, 3, 5, 7, 11, 13, 15, 17, 19, 23, 29,
31, 33, 35, 37, 41, 43, 47, 51, 53, 59, 61,
65, 67, 69, 71, 73, 77, 79, 83, 85, 87, 89,
91, 95, 97]
def test_is_cyclic_number():
assert is_cyclic_number(15) == True
assert is_cyclic_number(randprime(1, 2000)**2) == False
assert is_cyclic_number(randprime(1000, 100000)) == True
assert is_cyclic_number(4) == False
raises(ValueError, lambda: is_cyclic_number(-5))
for n in range(1, 100):
assert is_cyclic_number(n) == (n in A003277)
def test_holder_formula():
# semiprime
assert _holder_formula({3, 5}) == 1
assert _holder_formula({5, 11}) == 2
# n in A003277 is always 1
for n in A003277:
assert _holder_formula(set(factorint(n).keys())) == 1
# otherwise
assert _holder_formula({2, 3, 5, 7}) == 12
def test_groups_count():
A000001 = [0, 1, 1, 1, 2, 1, 2, 1, 5, 2, 2, 1, 5, 1,
2, 1, 14, 1, 5, 1, 5, 2, 2, 1, 15, 2, 2,
5, 4, 1, 4, 1, 51, 1, 2, 1, 14, 1, 2, 2,
14, 1, 6, 1, 4, 2, 2, 1, 52, 2, 5, 1, 5,
1, 15, 2, 13, 2, 2, 1, 13, 1, 2, 4, 267,
1, 4, 1, 5, 1, 4, 1, 50, 1, 2, 3, 4, 1,
6, 1, 52, 15, 2, 1, 15, 1, 2, 1, 12, 1,
10, 1, 4, 2]
for n in range(1, len(A000001)):
try:
assert groups_count(n) == A000001[n]
except ValueError:
pass
A000679 = [1, 1, 2, 5, 14, 51, 267, 2328, 56092, 10494213, 49487367289]
for e in range(1, len(A000679)):
assert groups_count(2**e) == A000679[e]
A090091 = [1, 1, 2, 5, 15, 67, 504, 9310, 1396077, 5937876645]
for e in range(1, len(A090091)):
assert groups_count(3**e) == A090091[e]
A090130 = [1, 1, 2, 5, 15, 77, 684, 34297]
for e in range(1, len(A090130)):
assert groups_count(5**e) == A090130[e]
A090140 = [1, 1, 2, 5, 15, 83, 860, 113147]
for e in range(1, len(A090140)):
assert groups_count(7**e) == A090140[e]
A232105 = [51, 67, 77, 83, 87, 97, 101, 107, 111, 125, 131,
145, 149, 155, 159, 173, 183, 193, 203, 207, 217]
for i in range(len(A232105)):
assert groups_count(prime(i+1)**5) == A232105[i]
A232106 = [267, 504, 684, 860, 1192, 1476, 1944, 2264, 2876,
4068, 4540, 6012, 7064, 7664, 8852, 10908, 13136]
for i in range(len(A232106)):
assert groups_count(prime(i+1)**6) == A232106[i]
A232107 = [2328, 9310, 34297, 113147, 750735, 1600573,
5546909, 9380741, 23316851, 71271069, 98488755]
for i in range(len(A232107)):
assert groups_count(prime(i+1)**7) == A232107[i]

View File

@ -0,0 +1,114 @@
from sympy.combinatorics import Permutation
from sympy.combinatorics.perm_groups import PermutationGroup
from sympy.combinatorics.homomorphisms import homomorphism, group_isomorphism, is_isomorphic
from sympy.combinatorics.free_groups import free_group
from sympy.combinatorics.fp_groups import FpGroup
from sympy.combinatorics.named_groups import AlternatingGroup, DihedralGroup, CyclicGroup
from sympy.testing.pytest import raises
def test_homomorphism():
# FpGroup -> PermutationGroup
F, a, b = free_group("a, b")
G = FpGroup(F, [a**3, b**3, (a*b)**2])
c = Permutation(3)(0, 1, 2)
d = Permutation(3)(1, 2, 3)
A = AlternatingGroup(4)
T = homomorphism(G, A, [a, b], [c, d])
assert T(a*b**2*a**-1) == c*d**2*c**-1
assert T.is_isomorphism()
assert T(T.invert(Permutation(3)(0, 2, 3))) == Permutation(3)(0, 2, 3)
T = homomorphism(G, AlternatingGroup(4), G.generators)
assert T.is_trivial()
assert T.kernel().order() == G.order()
E, e = free_group("e")
G = FpGroup(E, [e**8])
P = PermutationGroup([Permutation(0, 1, 2, 3), Permutation(0, 2)])
T = homomorphism(G, P, [e], [Permutation(0, 1, 2, 3)])
assert T.image().order() == 4
assert T(T.invert(Permutation(0, 2)(1, 3))) == Permutation(0, 2)(1, 3)
T = homomorphism(E, AlternatingGroup(4), E.generators, [c])
assert T.invert(c**2) == e**-1 #order(c) == 3 so c**2 == c**-1
# FreeGroup -> FreeGroup
T = homomorphism(F, E, [a], [e])
assert T(a**-2*b**4*a**2).is_identity
# FreeGroup -> FpGroup
G = FpGroup(F, [a*b*a**-1*b**-1])
T = homomorphism(F, G, F.generators, G.generators)
assert T.invert(a**-1*b**-1*a**2) == a*b**-1
# PermutationGroup -> PermutationGroup
D = DihedralGroup(8)
p = Permutation(0, 1, 2, 3, 4, 5, 6, 7)
P = PermutationGroup(p)
T = homomorphism(P, D, [p], [p])
assert T.is_injective()
assert not T.is_isomorphism()
assert T.invert(p**3) == p**3
T2 = homomorphism(F, P, [F.generators[0]], P.generators)
T = T.compose(T2)
assert T.domain == F
assert T.codomain == D
assert T(a*b) == p
D3 = DihedralGroup(3)
T = homomorphism(D3, D3, D3.generators, D3.generators)
assert T.is_isomorphism()
def test_isomorphisms():
F, a, b = free_group("a, b")
E, c, d = free_group("c, d")
# Infinite groups with differently ordered relators.
G = FpGroup(F, [a**2, b**3])
H = FpGroup(F, [b**3, a**2])
assert is_isomorphic(G, H)
# Trivial Case
# FpGroup -> FpGroup
H = FpGroup(F, [a**3, b**3, (a*b)**2])
F, c, d = free_group("c, d")
G = FpGroup(F, [c**3, d**3, (c*d)**2])
check, T = group_isomorphism(G, H)
assert check
assert T(c**3*d**2) == a**3*b**2
# FpGroup -> PermutationGroup
# FpGroup is converted to the equivalent isomorphic group.
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)
assert check
assert T(b*a*b**-1*a**-1*b**-1) == Permutation(0, 2, 3)
assert T(b*a*b*a**-1*b**-1) == Permutation(0, 3, 2)
# PermutationGroup -> PermutationGroup
D = DihedralGroup(8)
p = Permutation(0, 1, 2, 3, 4, 5, 6, 7)
P = PermutationGroup(p)
assert not is_isomorphic(D, P)
A = CyclicGroup(5)
B = CyclicGroup(7)
assert not is_isomorphic(A, B)
# Two groups of the same prime order are isomorphic to each other.
G = FpGroup(F, [a, b**5])
H = CyclicGroup(5)
assert G.order() == H.order()
assert is_isomorphic(G, H)
def test_check_homomorphism():
a = Permutation(1,2,3,4)
b = Permutation(1,3)
G = PermutationGroup([a, b])
raises(ValueError, lambda: homomorphism(G, G, [a], [a]))

View File

@ -0,0 +1,70 @@
from sympy.combinatorics.named_groups import (SymmetricGroup, CyclicGroup,
DihedralGroup, AlternatingGroup,
AbelianGroup, RubikGroup)
from sympy.testing.pytest import raises
def test_SymmetricGroup():
G = SymmetricGroup(5)
elements = list(G.generate())
assert (G.generators[0]).size == 5
assert len(elements) == 120
assert G.is_solvable is False
assert G.is_abelian is False
assert G.is_nilpotent is False
assert G.is_transitive() is True
H = SymmetricGroup(1)
assert H.order() == 1
L = SymmetricGroup(2)
assert L.order() == 2
def test_CyclicGroup():
G = CyclicGroup(10)
elements = list(G.generate())
assert len(elements) == 10
assert (G.derived_subgroup()).order() == 1
assert G.is_abelian is True
assert G.is_solvable is True
assert G.is_nilpotent is True
H = CyclicGroup(1)
assert H.order() == 1
L = CyclicGroup(2)
assert L.order() == 2
def test_DihedralGroup():
G = DihedralGroup(6)
elements = list(G.generate())
assert len(elements) == 12
assert G.is_transitive() is True
assert G.is_abelian is False
assert G.is_solvable is True
assert G.is_nilpotent is False
H = DihedralGroup(1)
assert H.order() == 2
L = DihedralGroup(2)
assert L.order() == 4
assert L.is_abelian is True
assert L.is_nilpotent is True
def test_AlternatingGroup():
G = AlternatingGroup(5)
elements = list(G.generate())
assert len(elements) == 60
assert [perm.is_even for perm in elements] == [True]*60
H = AlternatingGroup(1)
assert H.order() == 1
L = AlternatingGroup(2)
assert L.order() == 1
def test_AbelianGroup():
A = AbelianGroup(3, 3, 3)
assert A.order() == 27
assert A.is_abelian is True
def test_RubikGroup():
raises(ValueError, lambda: RubikGroup(1))

View File

@ -0,0 +1,118 @@
from sympy.core.sorting import ordered, default_sort_key
from sympy.combinatorics.partitions import (Partition, IntegerPartition,
RGS_enum, RGS_unrank, RGS_rank,
random_integer_partition)
from sympy.testing.pytest import raises
from sympy.utilities.iterables import partitions
from sympy.sets.sets import Set, FiniteSet
def test_partition_constructor():
raises(ValueError, lambda: Partition([1, 1, 2]))
raises(ValueError, lambda: Partition([1, 2, 3], [2, 3, 4]))
raises(ValueError, lambda: Partition(1, 2, 3))
raises(ValueError, lambda: Partition(*list(range(3))))
assert Partition([1, 2, 3], [4, 5]) == Partition([4, 5], [1, 2, 3])
assert Partition({1, 2, 3}, {4, 5}) == Partition([1, 2, 3], [4, 5])
a = FiniteSet(1, 2, 3)
b = FiniteSet(4, 5)
assert Partition(a, b) == Partition([1, 2, 3], [4, 5])
assert Partition({a, b}) == Partition(FiniteSet(a, b))
assert Partition({a, b}) != Partition(a, b)
def test_partition():
from sympy.abc import x
a = Partition([1, 2, 3], [4])
b = Partition([1, 2], [3, 4])
c = Partition([x])
l = [a, b, c]
l.sort(key=default_sort_key)
assert l == [c, a, b]
l.sort(key=lambda w: default_sort_key(w, order='rev-lex'))
assert l == [c, a, b]
assert (a == b) is False
assert a <= b
assert (a > b) is False
assert a != b
assert a < b
assert (a + 2).partition == [[1, 2], [3, 4]]
assert (b - 1).partition == [[1, 2, 4], [3]]
assert (a - 1).partition == [[1, 2, 3, 4]]
assert (a + 1).partition == [[1, 2, 4], [3]]
assert (b + 1).partition == [[1, 2], [3], [4]]
assert a.rank == 1
assert b.rank == 3
assert a.RGS == (0, 0, 0, 1)
assert b.RGS == (0, 0, 1, 1)
def test_integer_partition():
# no zeros in partition
raises(ValueError, lambda: IntegerPartition(list(range(3))))
# check fails since 1 + 2 != 100
raises(ValueError, lambda: IntegerPartition(100, list(range(1, 3))))
a = IntegerPartition(8, [1, 3, 4])
b = a.next_lex()
c = IntegerPartition([1, 3, 4])
d = IntegerPartition(8, {1: 3, 3: 1, 2: 1})
assert a == c
assert a.integer == d.integer
assert a.conjugate == [3, 2, 2, 1]
assert (a == b) is False
assert a <= b
assert (a > b) is False
assert a != b
for i in range(1, 11):
next = set()
prev = set()
a = IntegerPartition([i])
ans = {IntegerPartition(p) for p in partitions(i)}
n = len(ans)
for j in range(n):
next.add(a)
a = a.next_lex()
IntegerPartition(i, a.partition) # check it by giving i
for j in range(n):
prev.add(a)
a = a.prev_lex()
IntegerPartition(i, a.partition) # check it by giving i
assert next == ans
assert prev == ans
assert IntegerPartition([1, 2, 3]).as_ferrers() == '###\n##\n#'
assert IntegerPartition([1, 1, 3]).as_ferrers('o') == 'ooo\no\no'
assert str(IntegerPartition([1, 1, 3])) == '[3, 1, 1]'
assert IntegerPartition([1, 1, 3]).partition == [3, 1, 1]
raises(ValueError, lambda: random_integer_partition(-1))
assert random_integer_partition(1) == [1]
assert random_integer_partition(10, seed=[1, 3, 2, 1, 5, 1]
) == [5, 2, 1, 1, 1]
def test_rgs():
raises(ValueError, lambda: RGS_unrank(-1, 3))
raises(ValueError, lambda: RGS_unrank(3, 0))
raises(ValueError, lambda: RGS_unrank(10, 1))
raises(ValueError, lambda: Partition.from_rgs(list(range(3)), list(range(2))))
raises(ValueError, lambda: Partition.from_rgs(list(range(1, 3)), list(range(2))))
assert RGS_enum(-1) == 0
assert RGS_enum(1) == 1
assert RGS_unrank(7, 5) == [0, 0, 1, 0, 2]
assert RGS_unrank(23, 14) == [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 2, 2]
assert RGS_rank(RGS_unrank(40, 100)) == 40
def test_ordered_partition_9608():
a = Partition([1, 2, 3], [4])
b = Partition([1, 2], [3, 4])
assert list(ordered([a,b], Set._infimum_key))

View File

@ -0,0 +1,87 @@
from sympy.combinatorics.permutations import Permutation
from sympy.combinatorics.named_groups import SymmetricGroup, AlternatingGroup, DihedralGroup
from sympy.matrices import Matrix
def test_pc_presentation():
Groups = [SymmetricGroup(3), SymmetricGroup(4), SymmetricGroup(9).sylow_subgroup(3),
SymmetricGroup(9).sylow_subgroup(2), SymmetricGroup(8).sylow_subgroup(2), DihedralGroup(10)]
S = SymmetricGroup(125).sylow_subgroup(5)
G = S.derived_series()[2]
Groups.append(G)
G = SymmetricGroup(25).sylow_subgroup(5)
Groups.append(G)
S = SymmetricGroup(11**2).sylow_subgroup(11)
G = S.derived_series()[2]
Groups.append(G)
for G in Groups:
PcGroup = G.polycyclic_group()
collector = PcGroup.collector
pc_presentation = collector.pc_presentation
pcgs = PcGroup.pcgs
free_group = collector.free_group
free_to_perm = {}
for s, g in zip(free_group.symbols, pcgs):
free_to_perm[s] = g
for k, v in pc_presentation.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
def test_exponent_vector():
Groups = [SymmetricGroup(3), SymmetricGroup(4), SymmetricGroup(9).sylow_subgroup(3),
SymmetricGroup(9).sylow_subgroup(2), SymmetricGroup(8).sylow_subgroup(2)]
for G in Groups:
PcGroup = G.polycyclic_group()
collector = PcGroup.collector
pcgs = PcGroup.pcgs
# free_group = collector.free_group
for gen in G.generators:
exp = collector.exponent_vector(gen)
g = Permutation()
for i in range(len(exp)):
g = g*pcgs[i]**exp[i] if exp[i] else g
assert g == gen
def test_induced_pcgs():
G = [SymmetricGroup(9).sylow_subgroup(3), SymmetricGroup(20).sylow_subgroup(2), AlternatingGroup(4),
DihedralGroup(4), DihedralGroup(10), DihedralGroup(9), SymmetricGroup(3), SymmetricGroup(4)]
for g in G:
PcGroup = g.polycyclic_group()
collector = PcGroup.collector
gens = list(g.generators)
ipcgs = collector.induced_pcgs(gens)
m = []
for i in ipcgs:
m.append(collector.exponent_vector(i))
assert Matrix(m).is_upper

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,564 @@
from itertools import permutations
from copy import copy
from sympy.core.expr import unchanged
from sympy.core.numbers import Integer
from sympy.core.relational import Eq
from sympy.core.symbol import Symbol
from sympy.core.singleton import S
from sympy.combinatorics.permutations import \
Permutation, _af_parity, _af_rmul, _af_rmuln, AppliedPermutation, Cycle
from sympy.printing import sstr, srepr, pretty, latex
from sympy.testing.pytest import raises, warns_deprecated_sympy
rmul = Permutation.rmul
a = Symbol('a', integer=True)
def test_Permutation():
# don't auto fill 0
raises(ValueError, lambda: Permutation([1]))
p = Permutation([0, 1, 2, 3])
# call as bijective
assert [p(i) for i in range(p.size)] == list(p)
# call as operator
assert p(list(range(p.size))) == list(p)
# call as function
assert list(p(1, 2)) == [0, 2, 1, 3]
raises(TypeError, lambda: p(-1))
raises(TypeError, lambda: p(5))
# conversion to list
assert list(p) == list(range(4))
assert p.copy() == p
assert copy(p) == p
assert Permutation(size=4) == Permutation(3)
assert Permutation(Permutation(3), size=5) == Permutation(4)
# cycle form with size
assert Permutation([[1, 2]], size=4) == Permutation([[1, 2], [0], [3]])
# random generation
assert Permutation.random(2) in (Permutation([1, 0]), Permutation([0, 1]))
p = Permutation([2, 5, 1, 6, 3, 0, 4])
q = Permutation([[1], [0, 3, 5, 6, 2, 4]])
assert len({p, p}) == 1
r = Permutation([1, 3, 2, 0, 4, 6, 5])
ans = Permutation(_af_rmuln(*[w.array_form for w in (p, q, r)])).array_form
assert rmul(p, q, r).array_form == ans
# make sure no other permutation of p, q, r could have given
# that answer
for a, b, c in permutations((p, q, r)):
if (a, b, c) == (p, q, r):
continue
assert rmul(a, b, c).array_form != ans
assert p.support() == list(range(7))
assert q.support() == [0, 2, 3, 4, 5, 6]
assert Permutation(p.cyclic_form).array_form == p.array_form
assert p.cardinality == 5040
assert q.cardinality == 5040
assert q.cycles == 2
assert rmul(q, p) == Permutation([4, 6, 1, 2, 5, 3, 0])
assert rmul(p, q) == Permutation([6, 5, 3, 0, 2, 4, 1])
assert _af_rmul(p.array_form, q.array_form) == \
[6, 5, 3, 0, 2, 4, 1]
assert rmul(Permutation([[1, 2, 3], [0, 4]]),
Permutation([[1, 2, 4], [0], [3]])).cyclic_form == \
[[0, 4, 2], [1, 3]]
assert q.array_form == [3, 1, 4, 5, 0, 6, 2]
assert q.cyclic_form == [[0, 3, 5, 6, 2, 4]]
assert q.full_cyclic_form == [[0, 3, 5, 6, 2, 4], [1]]
assert p.cyclic_form == [[0, 2, 1, 5], [3, 6, 4]]
t = p.transpositions()
assert t == [(0, 5), (0, 1), (0, 2), (3, 4), (3, 6)]
assert Permutation.rmul(*[Permutation(Cycle(*ti)) for ti in (t)])
assert Permutation([1, 0]).transpositions() == [(0, 1)]
assert p**13 == p
assert q**0 == Permutation(list(range(q.size)))
assert q**-2 == ~q**2
assert q**2 == Permutation([5, 1, 0, 6, 3, 2, 4])
assert q**3 == q**2*q
assert q**4 == q**2*q**2
a = Permutation(1, 3)
b = Permutation(2, 0, 3)
I = Permutation(3)
assert ~a == a**-1
assert a*~a == I
assert a*b**-1 == a*~b
ans = Permutation(0, 5, 3, 1, 6)(2, 4)
assert (p + q.rank()).rank() == ans.rank()
assert (p + q.rank())._rank == ans.rank()
assert (q + p.rank()).rank() == ans.rank()
raises(TypeError, lambda: p + Permutation(list(range(10))))
assert (p - q.rank()).rank() == Permutation(0, 6, 3, 1, 2, 5, 4).rank()
assert p.rank() - q.rank() < 0 # for coverage: make sure mod is used
assert (q - p.rank()).rank() == Permutation(1, 4, 6, 2)(3, 5).rank()
assert p*q == Permutation(_af_rmuln(*[list(w) for w in (q, p)]))
assert p*Permutation([]) == p
assert Permutation([])*p == p
assert p*Permutation([[0, 1]]) == Permutation([2, 5, 0, 6, 3, 1, 4])
assert Permutation([[0, 1]])*p == Permutation([5, 2, 1, 6, 3, 0, 4])
pq = p ^ q
assert pq == Permutation([5, 6, 0, 4, 1, 2, 3])
assert pq == rmul(q, p, ~q)
qp = q ^ p
assert qp == Permutation([4, 3, 6, 2, 1, 5, 0])
assert qp == rmul(p, q, ~p)
raises(ValueError, lambda: p ^ Permutation([]))
assert p.commutator(q) == Permutation(0, 1, 3, 4, 6, 5, 2)
assert q.commutator(p) == Permutation(0, 2, 5, 6, 4, 3, 1)
assert p.commutator(q) == ~q.commutator(p)
raises(ValueError, lambda: p.commutator(Permutation([])))
assert len(p.atoms()) == 7
assert q.atoms() == {0, 1, 2, 3, 4, 5, 6}
assert p.inversion_vector() == [2, 4, 1, 3, 1, 0]
assert q.inversion_vector() == [3, 1, 2, 2, 0, 1]
assert Permutation.from_inversion_vector(p.inversion_vector()) == p
assert Permutation.from_inversion_vector(q.inversion_vector()).array_form\
== q.array_form
raises(ValueError, lambda: Permutation.from_inversion_vector([0, 2]))
assert Permutation(list(range(500, -1, -1))).inversions() == 125250
s = Permutation([0, 4, 1, 3, 2])
assert s.parity() == 0
_ = s.cyclic_form # needed to create a value for _cyclic_form
assert len(s._cyclic_form) != s.size and s.parity() == 0
assert not s.is_odd
assert s.is_even
assert Permutation([0, 1, 4, 3, 2]).parity() == 1
assert _af_parity([0, 4, 1, 3, 2]) == 0
assert _af_parity([0, 1, 4, 3, 2]) == 1
s = Permutation([0])
assert s.is_Singleton
assert Permutation([]).is_Empty
r = Permutation([3, 2, 1, 0])
assert (r**2).is_Identity
assert rmul(~p, p).is_Identity
assert (~p)**13 == Permutation([5, 2, 0, 4, 6, 1, 3])
assert p.max() == 6
assert p.min() == 0
q = Permutation([[6], [5], [0, 1, 2, 3, 4]])
assert q.max() == 4
assert q.min() == 0
p = Permutation([1, 5, 2, 0, 3, 6, 4])
q = Permutation([[1, 2, 3, 5, 6], [0, 4]])
assert p.ascents() == [0, 3, 4]
assert q.ascents() == [1, 2, 4]
assert r.ascents() == []
assert p.descents() == [1, 2, 5]
assert q.descents() == [0, 3, 5]
assert Permutation(r.descents()).is_Identity
assert p.inversions() == 7
# test the merge-sort with a longer permutation
big = list(p) + list(range(p.max() + 1, p.max() + 130))
assert Permutation(big).inversions() == 7
assert p.signature() == -1
assert q.inversions() == 11
assert q.signature() == -1
assert rmul(p, ~p).inversions() == 0
assert rmul(p, ~p).signature() == 1
assert p.order() == 6
assert q.order() == 10
assert (p**(p.order())).is_Identity
assert p.length() == 6
assert q.length() == 7
assert r.length() == 4
assert p.runs() == [[1, 5], [2], [0, 3, 6], [4]]
assert q.runs() == [[4], [2, 3, 5], [0, 6], [1]]
assert r.runs() == [[3], [2], [1], [0]]
assert p.index() == 8
assert q.index() == 8
assert r.index() == 3
assert p.get_precedence_distance(q) == q.get_precedence_distance(p)
assert p.get_adjacency_distance(q) == p.get_adjacency_distance(q)
assert p.get_positional_distance(q) == p.get_positional_distance(q)
p = Permutation([0, 1, 2, 3])
q = Permutation([3, 2, 1, 0])
assert p.get_precedence_distance(q) == 6
assert p.get_adjacency_distance(q) == 3
assert p.get_positional_distance(q) == 8
p = Permutation([0, 3, 1, 2, 4])
q = Permutation.josephus(4, 5, 2)
assert p.get_adjacency_distance(q) == 3
raises(ValueError, lambda: p.get_adjacency_distance(Permutation([])))
raises(ValueError, lambda: p.get_positional_distance(Permutation([])))
raises(ValueError, lambda: p.get_precedence_distance(Permutation([])))
a = [Permutation.unrank_nonlex(4, i) for i in range(5)]
iden = Permutation([0, 1, 2, 3])
for i in range(5):
for j in range(i + 1, 5):
assert a[i].commutes_with(a[j]) == \
(rmul(a[i], a[j]) == rmul(a[j], a[i]))
if a[i].commutes_with(a[j]):
assert a[i].commutator(a[j]) == iden
assert a[j].commutator(a[i]) == iden
a = Permutation(3)
b = Permutation(0, 6, 3)(1, 2)
assert a.cycle_structure == {1: 4}
assert b.cycle_structure == {2: 1, 3: 1, 1: 2}
# issue 11130
raises(ValueError, lambda: Permutation(3, size=3))
raises(ValueError, lambda: Permutation([1, 2, 0, 3], size=3))
def test_Permutation_subclassing():
# Subclass that adds permutation application on iterables
class CustomPermutation(Permutation):
def __call__(self, *i):
try:
return super().__call__(*i)
except TypeError:
pass
try:
perm_obj = i[0]
return [self._array_form[j] for j in perm_obj]
except TypeError:
raise TypeError('unrecognized argument')
def __eq__(self, other):
if isinstance(other, Permutation):
return self._hashable_content() == other._hashable_content()
else:
return super().__eq__(other)
def __hash__(self):
return super().__hash__()
p = CustomPermutation([1, 2, 3, 0])
q = Permutation([1, 2, 3, 0])
assert p == q
raises(TypeError, lambda: q([1, 2]))
assert [2, 3] == p([1, 2])
assert type(p * q) == CustomPermutation
assert type(q * p) == Permutation # True because q.__mul__(p) is called!
# Run all tests for the Permutation class also on the subclass
def wrapped_test_Permutation():
# Monkeypatch the class definition in the globals
globals()['__Perm'] = globals()['Permutation']
globals()['Permutation'] = CustomPermutation
test_Permutation()
globals()['Permutation'] = globals()['__Perm'] # Restore
del globals()['__Perm']
wrapped_test_Permutation()
def test_josephus():
assert Permutation.josephus(4, 6, 1) == Permutation([3, 1, 0, 2, 5, 4])
assert Permutation.josephus(1, 5, 1).is_Identity
def test_ranking():
assert Permutation.unrank_lex(5, 10).rank() == 10
p = Permutation.unrank_lex(15, 225)
assert p.rank() == 225
p1 = p.next_lex()
assert p1.rank() == 226
assert Permutation.unrank_lex(15, 225).rank() == 225
assert Permutation.unrank_lex(10, 0).is_Identity
p = Permutation.unrank_lex(4, 23)
assert p.rank() == 23
assert p.array_form == [3, 2, 1, 0]
assert p.next_lex() is None
p = Permutation([1, 5, 2, 0, 3, 6, 4])
q = Permutation([[1, 2, 3, 5, 6], [0, 4]])
a = [Permutation.unrank_trotterjohnson(4, i).array_form for i in range(5)]
assert a == [[0, 1, 2, 3], [0, 1, 3, 2], [0, 3, 1, 2], [3, 0, 1,
2], [3, 0, 2, 1] ]
assert [Permutation(pa).rank_trotterjohnson() for pa in a] == list(range(5))
assert Permutation([0, 1, 2, 3]).next_trotterjohnson() == \
Permutation([0, 1, 3, 2])
assert q.rank_trotterjohnson() == 2283
assert p.rank_trotterjohnson() == 3389
assert Permutation([1, 0]).rank_trotterjohnson() == 1
a = Permutation(list(range(3)))
b = a
l = []
tj = []
for i in range(6):
l.append(a)
tj.append(b)
a = a.next_lex()
b = b.next_trotterjohnson()
assert a == b is None
assert {tuple(a) for a in l} == {tuple(a) for a in tj}
p = Permutation([2, 5, 1, 6, 3, 0, 4])
q = Permutation([[6], [5], [0, 1, 2, 3, 4]])
assert p.rank() == 1964
assert q.rank() == 870
assert Permutation([]).rank_nonlex() == 0
prank = p.rank_nonlex()
assert prank == 1600
assert Permutation.unrank_nonlex(7, 1600) == p
qrank = q.rank_nonlex()
assert qrank == 41
assert Permutation.unrank_nonlex(7, 41) == Permutation(q.array_form)
a = [Permutation.unrank_nonlex(4, i).array_form for i in range(24)]
assert a == [
[1, 2, 3, 0], [3, 2, 0, 1], [1, 3, 0, 2], [1, 2, 0, 3], [2, 3, 1, 0],
[2, 0, 3, 1], [3, 0, 1, 2], [2, 0, 1, 3], [1, 3, 2, 0], [3, 0, 2, 1],
[1, 0, 3, 2], [1, 0, 2, 3], [2, 1, 3, 0], [2, 3, 0, 1], [3, 1, 0, 2],
[2, 1, 0, 3], [3, 2, 1, 0], [0, 2, 3, 1], [0, 3, 1, 2], [0, 2, 1, 3],
[3, 1, 2, 0], [0, 3, 2, 1], [0, 1, 3, 2], [0, 1, 2, 3]]
N = 10
p1 = Permutation(a[0])
for i in range(1, N+1):
p1 = p1*Permutation(a[i])
p2 = Permutation.rmul_with_af(*[Permutation(h) for h in a[N::-1]])
assert p1 == p2
ok = []
p = Permutation([1, 0])
for i in range(3):
ok.append(p.array_form)
p = p.next_nonlex()
if p is None:
ok.append(None)
break
assert ok == [[1, 0], [0, 1], None]
assert Permutation([3, 2, 0, 1]).next_nonlex() == Permutation([1, 3, 0, 2])
assert [Permutation(pa).rank_nonlex() for pa in a] == list(range(24))
def test_mul():
a, b = [0, 2, 1, 3], [0, 1, 3, 2]
assert _af_rmul(a, b) == [0, 2, 3, 1]
assert _af_rmuln(a, b, list(range(4))) == [0, 2, 3, 1]
assert rmul(Permutation(a), Permutation(b)).array_form == [0, 2, 3, 1]
a = Permutation([0, 2, 1, 3])
b = (0, 1, 3, 2)
c = (3, 1, 2, 0)
assert Permutation.rmul(a, b, c) == Permutation([1, 2, 3, 0])
assert Permutation.rmul(a, c) == Permutation([3, 2, 1, 0])
raises(TypeError, lambda: Permutation.rmul(b, c))
n = 6
m = 8
a = [Permutation.unrank_nonlex(n, i).array_form for i in range(m)]
h = list(range(n))
for i in range(m):
h = _af_rmul(h, a[i])
h2 = _af_rmuln(*a[:i + 1])
assert h == h2
def test_args():
p = Permutation([(0, 3, 1, 2), (4, 5)])
assert p._cyclic_form is None
assert Permutation(p) == p
assert p.cyclic_form == [[0, 3, 1, 2], [4, 5]]
assert p._array_form == [3, 2, 0, 1, 5, 4]
p = Permutation((0, 3, 1, 2))
assert p._cyclic_form is None
assert p._array_form == [0, 3, 1, 2]
assert Permutation([0]) == Permutation((0, ))
assert Permutation([[0], [1]]) == Permutation(((0, ), (1, ))) == \
Permutation(((0, ), [1]))
assert Permutation([[1, 2]]) == Permutation([0, 2, 1])
assert Permutation([[1], [4, 2]]) == Permutation([0, 1, 4, 3, 2])
assert Permutation([[1], [4, 2]], size=1) == Permutation([0, 1, 4, 3, 2])
assert Permutation(
[[1], [4, 2]], size=6) == Permutation([0, 1, 4, 3, 2, 5])
assert Permutation([[0, 1], [0, 2]]) == Permutation(0, 1, 2)
assert Permutation([], size=3) == Permutation([0, 1, 2])
assert Permutation(3).list(5) == [0, 1, 2, 3, 4]
assert Permutation(3).list(-1) == []
assert Permutation(5)(1, 2).list(-1) == [0, 2, 1]
assert Permutation(5)(1, 2).list() == [0, 2, 1, 3, 4, 5]
raises(ValueError, lambda: Permutation([1, 2], [0]))
# enclosing brackets needed
raises(ValueError, lambda: Permutation([[1, 2], 0]))
# enclosing brackets needed on 0
raises(ValueError, lambda: Permutation([1, 1, 0]))
raises(ValueError, lambda: Permutation([4, 5], size=10)) # where are 0-3?
# but this is ok because cycles imply that only those listed moved
assert Permutation(4, 5) == Permutation([0, 1, 2, 3, 5, 4])
def test_Cycle():
assert str(Cycle()) == '()'
assert Cycle(Cycle(1,2)) == Cycle(1, 2)
assert Cycle(1,2).copy() == Cycle(1,2)
assert list(Cycle(1, 3, 2)) == [0, 3, 1, 2]
assert Cycle(1, 2)(2, 3) == Cycle(1, 3, 2)
assert Cycle(1, 2)(2, 3)(4, 5) == Cycle(1, 3, 2)(4, 5)
assert Permutation(Cycle(1, 2)(2, 1, 0, 3)).cyclic_form, Cycle(0, 2, 1)
raises(ValueError, lambda: Cycle().list())
assert Cycle(1, 2).list() == [0, 2, 1]
assert Cycle(1, 2).list(4) == [0, 2, 1, 3]
assert Cycle(3).list(2) == [0, 1]
assert Cycle(3).list(6) == [0, 1, 2, 3, 4, 5]
assert Permutation(Cycle(1, 2), size=4) == \
Permutation([0, 2, 1, 3])
assert str(Cycle(1, 2)(4, 5)) == '(1 2)(4 5)'
assert str(Cycle(1, 2)) == '(1 2)'
assert Cycle(Permutation(list(range(3)))) == Cycle()
assert Cycle(1, 2).list() == [0, 2, 1]
assert Cycle(1, 2).list(4) == [0, 2, 1, 3]
assert Cycle().size == 0
raises(ValueError, lambda: Cycle((1, 2)))
raises(ValueError, lambda: Cycle(1, 2, 1))
raises(TypeError, lambda: Cycle(1, 2)*{})
raises(ValueError, lambda: Cycle(4)[a])
raises(ValueError, lambda: Cycle(2, -4, 3))
# check round-trip
p = Permutation([[1, 2], [4, 3]], size=5)
assert Permutation(Cycle(p)) == p
def test_from_sequence():
assert Permutation.from_sequence('SymPy') == Permutation(4)(0, 1, 3)
assert Permutation.from_sequence('SymPy', key=lambda x: x.lower()) == \
Permutation(4)(0, 2)(1, 3)
def test_resize():
p = Permutation(0, 1, 2)
assert p.resize(5) == Permutation(0, 1, 2, size=5)
assert p.resize(4) == Permutation(0, 1, 2, size=4)
assert p.resize(3) == p
raises(ValueError, lambda: p.resize(2))
p = Permutation(0, 1, 2)(3, 4)(5, 6)
assert p.resize(3) == Permutation(0, 1, 2)
raises(ValueError, lambda: p.resize(4))
def test_printing_cyclic():
p1 = Permutation([0, 2, 1])
assert repr(p1) == 'Permutation(1, 2)'
assert str(p1) == '(1 2)'
p2 = Permutation()
assert repr(p2) == 'Permutation()'
assert str(p2) == '()'
p3 = Permutation([1, 2, 0, 3])
assert repr(p3) == 'Permutation(3)(0, 1, 2)'
def test_printing_non_cyclic():
p1 = Permutation([0, 1, 2, 3, 4, 5])
assert srepr(p1, perm_cyclic=False) == 'Permutation([], size=6)'
assert sstr(p1, perm_cyclic=False) == 'Permutation([], size=6)'
p2 = Permutation([0, 1, 2])
assert srepr(p2, perm_cyclic=False) == 'Permutation([0, 1, 2])'
assert sstr(p2, perm_cyclic=False) == 'Permutation([0, 1, 2])'
p3 = Permutation([0, 2, 1])
assert srepr(p3, perm_cyclic=False) == 'Permutation([0, 2, 1])'
assert sstr(p3, perm_cyclic=False) == 'Permutation([0, 2, 1])'
p4 = Permutation([0, 1, 3, 2, 4, 5, 6, 7])
assert srepr(p4, perm_cyclic=False) == 'Permutation([0, 1, 3, 2], size=8)'
def test_deprecated_print_cyclic():
p = Permutation(0, 1, 2)
try:
Permutation.print_cyclic = True
with warns_deprecated_sympy():
assert sstr(p) == '(0 1 2)'
with warns_deprecated_sympy():
assert srepr(p) == 'Permutation(0, 1, 2)'
with warns_deprecated_sympy():
assert pretty(p) == '(0 1 2)'
with warns_deprecated_sympy():
assert latex(p) == r'\left( 0\; 1\; 2\right)'
Permutation.print_cyclic = False
with warns_deprecated_sympy():
assert sstr(p) == 'Permutation([1, 2, 0])'
with warns_deprecated_sympy():
assert srepr(p) == 'Permutation([1, 2, 0])'
with warns_deprecated_sympy():
assert pretty(p, use_unicode=False) == '/0 1 2\\\n\\1 2 0/'
with warns_deprecated_sympy():
assert latex(p) == \
r'\begin{pmatrix} 0 & 1 & 2 \\ 1 & 2 & 0 \end{pmatrix}'
finally:
Permutation.print_cyclic = None
def test_permutation_equality():
a = Permutation(0, 1, 2)
b = Permutation(0, 1, 2)
assert Eq(a, b) is S.true
c = Permutation(0, 2, 1)
assert Eq(a, c) is S.false
d = Permutation(0, 1, 2, size=4)
assert unchanged(Eq, a, d)
e = Permutation(0, 2, 1, size=4)
assert unchanged(Eq, a, e)
i = Permutation()
assert unchanged(Eq, i, 0)
assert unchanged(Eq, 0, i)
def test_issue_17661():
c1 = Cycle(1,2)
c2 = Cycle(1,2)
assert c1 == c2
assert repr(c1) == 'Cycle(1, 2)'
assert c1 == c2
def test_permutation_apply():
x = Symbol('x')
p = Permutation(0, 1, 2)
assert p.apply(0) == 1
assert isinstance(p.apply(0), Integer)
assert p.apply(x) == AppliedPermutation(p, x)
assert AppliedPermutation(p, x).subs(x, 0) == 1
x = Symbol('x', integer=False)
raises(NotImplementedError, lambda: p.apply(x))
x = Symbol('x', negative=True)
raises(NotImplementedError, lambda: p.apply(x))
def test_AppliedPermutation():
x = Symbol('x')
p = Permutation(0, 1, 2)
raises(ValueError, lambda: AppliedPermutation((0, 1, 2), x))
assert AppliedPermutation(p, 1, evaluate=True) == 2
assert AppliedPermutation(p, 1, evaluate=False).__class__ == \
AppliedPermutation

View File

@ -0,0 +1,105 @@
from sympy.core.symbol import symbols
from sympy.sets.sets import FiniteSet
from sympy.combinatorics.polyhedron import (Polyhedron,
tetrahedron, cube as square, octahedron, dodecahedron, icosahedron,
cube_faces)
from sympy.combinatorics.permutations import Permutation
from sympy.combinatorics.perm_groups import PermutationGroup
from sympy.testing.pytest import raises
rmul = Permutation.rmul
def test_polyhedron():
raises(ValueError, lambda: Polyhedron(list('ab'),
pgroup=[Permutation([0])]))
pgroup = [Permutation([[0, 7, 2, 5], [6, 1, 4, 3]]),
Permutation([[0, 7, 1, 6], [5, 2, 4, 3]]),
Permutation([[3, 6, 0, 5], [4, 1, 7, 2]]),
Permutation([[7, 4, 5], [1, 3, 0], [2], [6]]),
Permutation([[1, 3, 2], [7, 6, 5], [4], [0]]),
Permutation([[4, 7, 6], [2, 0, 3], [1], [5]]),
Permutation([[1, 2, 0], [4, 5, 6], [3], [7]]),
Permutation([[4, 2], [0, 6], [3, 7], [1, 5]]),
Permutation([[3, 5], [7, 1], [2, 6], [0, 4]]),
Permutation([[2, 5], [1, 6], [0, 4], [3, 7]]),
Permutation([[4, 3], [7, 0], [5, 1], [6, 2]]),
Permutation([[4, 1], [0, 5], [6, 2], [7, 3]]),
Permutation([[7, 2], [3, 6], [0, 4], [1, 5]]),
Permutation([0, 1, 2, 3, 4, 5, 6, 7])]
corners = tuple(symbols('A:H'))
faces = cube_faces
cube = Polyhedron(corners, faces, pgroup)
assert cube.edges == FiniteSet(*(
(0, 1), (6, 7), (1, 2), (5, 6), (0, 3), (2, 3),
(4, 7), (4, 5), (3, 7), (1, 5), (0, 4), (2, 6)))
for i in range(3): # add 180 degree face rotations
cube.rotate(cube.pgroup[i]**2)
assert cube.corners == corners
for i in range(3, 7): # add 240 degree axial corner rotations
cube.rotate(cube.pgroup[i]**2)
assert cube.corners == corners
cube.rotate(1)
raises(ValueError, lambda: cube.rotate(Permutation([0, 1])))
assert cube.corners != corners
assert cube.array_form == [7, 6, 4, 5, 3, 2, 0, 1]
assert cube.cyclic_form == [[0, 7, 1, 6], [2, 4, 3, 5]]
cube.reset()
assert cube.corners == corners
def check(h, size, rpt, target):
assert len(h.faces) + len(h.vertices) - len(h.edges) == 2
assert h.size == size
got = set()
for p in h.pgroup:
# make sure it restores original
P = h.copy()
hit = P.corners
for i in range(rpt):
P.rotate(p)
if P.corners == hit:
break
else:
print('error in permutation', p.array_form)
for i in range(rpt):
P.rotate(p)
got.add(tuple(P.corners))
c = P.corners
f = [[c[i] for i in f] for f in P.faces]
assert h.faces == Polyhedron(c, f).faces
assert len(got) == target
assert PermutationGroup([Permutation(g) for g in got]).is_group
for h, size, rpt, target in zip(
(tetrahedron, square, octahedron, dodecahedron, icosahedron),
(4, 8, 6, 20, 12),
(3, 4, 4, 5, 5),
(12, 24, 24, 60, 60)):
check(h, size, rpt, target)
def test_pgroups():
from sympy.combinatorics.polyhedron import (cube, tetrahedron_faces,
octahedron_faces, dodecahedron_faces, icosahedron_faces)
from sympy.combinatorics.polyhedron import _pgroup_calcs
(tetrahedron2, cube2, octahedron2, dodecahedron2, icosahedron2,
tetrahedron_faces2, cube_faces2, octahedron_faces2,
dodecahedron_faces2, icosahedron_faces2) = _pgroup_calcs()
assert tetrahedron == tetrahedron2
assert cube == cube2
assert octahedron == octahedron2
assert dodecahedron == dodecahedron2
assert icosahedron == icosahedron2
assert sorted(map(sorted, tetrahedron_faces)) == sorted(map(sorted, tetrahedron_faces2))
assert sorted(cube_faces) == sorted(cube_faces2)
assert sorted(octahedron_faces) == sorted(octahedron_faces2)
assert sorted(dodecahedron_faces) == sorted(dodecahedron_faces2)
assert sorted(icosahedron_faces) == sorted(icosahedron_faces2)

View File

@ -0,0 +1,74 @@
from sympy.combinatorics.prufer import Prufer
from sympy.testing.pytest import raises
def test_prufer():
# number of nodes is optional
assert Prufer([[0, 1], [0, 2], [0, 3], [0, 4]], 5).nodes == 5
assert Prufer([[0, 1], [0, 2], [0, 3], [0, 4]]).nodes == 5
a = Prufer([[0, 1], [0, 2], [0, 3], [0, 4]])
assert a.rank == 0
assert a.nodes == 5
assert a.prufer_repr == [0, 0, 0]
a = Prufer([[2, 4], [1, 4], [1, 3], [0, 5], [0, 4]])
assert a.rank == 924
assert a.nodes == 6
assert a.tree_repr == [[2, 4], [1, 4], [1, 3], [0, 5], [0, 4]]
assert a.prufer_repr == [4, 1, 4, 0]
assert Prufer.edges([0, 1, 2, 3], [1, 4, 5], [1, 4, 6]) == \
([[0, 1], [1, 2], [1, 4], [2, 3], [4, 5], [4, 6]], 7)
assert Prufer([0]*4).size == Prufer([6]*4).size == 1296
# accept iterables but convert to list of lists
tree = [(0, 1), (1, 5), (0, 3), (0, 2), (2, 6), (4, 7), (2, 4)]
tree_lists = [list(t) for t in tree]
assert Prufer(tree).tree_repr == tree_lists
assert sorted(Prufer(set(tree)).tree_repr) == sorted(tree_lists)
raises(ValueError, lambda: Prufer([[1, 2], [3, 4]])) # 0 is missing
raises(ValueError, lambda: Prufer([[2, 3], [3, 4]])) # 0, 1 are missing
assert Prufer(*Prufer.edges([1, 2], [3, 4])).prufer_repr == [1, 3]
raises(ValueError, lambda: Prufer.edges(
[1, 3], [3, 4])) # a broken tree but edges doesn't care
raises(ValueError, lambda: Prufer.edges([1, 2], [5, 6]))
raises(ValueError, lambda: Prufer([[]]))
a = Prufer([[0, 1], [0, 2], [0, 3]])
b = a.next()
assert b.tree_repr == [[0, 2], [0, 1], [1, 3]]
assert b.rank == 1
def test_round_trip():
def doit(t, b):
e, n = Prufer.edges(*t)
t = Prufer(e, n)
a = sorted(t.tree_repr)
b = [i - 1 for i in b]
assert t.prufer_repr == b
assert sorted(Prufer(b).tree_repr) == a
assert Prufer.unrank(t.rank, n).prufer_repr == b
doit([[1, 2]], [])
doit([[2, 1, 3]], [1])
doit([[1, 3, 2]], [3])
doit([[1, 2, 3]], [2])
doit([[2, 1, 4], [1, 3]], [1, 1])
doit([[3, 2, 1, 4]], [2, 1])
doit([[3, 2, 1], [2, 4]], [2, 2])
doit([[1, 3, 2, 4]], [3, 2])
doit([[1, 4, 2, 3]], [4, 2])
doit([[3, 1, 4, 2]], [4, 1])
doit([[4, 2, 1, 3]], [1, 2])
doit([[1, 2, 4, 3]], [2, 4])
doit([[1, 3, 4, 2]], [3, 4])
doit([[2, 4, 1], [4, 3]], [4, 4])
doit([[1, 2, 3, 4]], [2, 3])
doit([[2, 3, 1], [3, 4]], [3, 3])
doit([[1, 4, 3, 2]], [4, 3])
doit([[2, 1, 4, 3]], [1, 4])
doit([[2, 1, 3, 4]], [1, 3])
doit([[6, 2, 1, 4], [1, 3, 5, 8], [3, 7]], [1, 2, 1, 3, 3, 5])

View File

@ -0,0 +1,49 @@
from sympy.combinatorics.fp_groups import FpGroup
from sympy.combinatorics.free_groups import free_group
from sympy.testing.pytest import raises
def test_rewriting():
F, a, b = free_group("a, b")
G = FpGroup(F, [a*b*a**-1*b**-1])
a, b = G.generators
R = G._rewriting_system
assert R.is_confluent
assert G.reduce(b**-1*a) == a*b**-1
assert G.reduce(b**3*a**4*b**-2*a) == a**5*b
assert G.equals(b**2*a**-1*b, b**4*a**-1*b**-1)
assert R.reduce_using_automaton(b*a*a**2*b**-1) == a**3
assert R.reduce_using_automaton(b**3*a**4*b**-2*a) == a**5*b
assert R.reduce_using_automaton(b**-1*a) == a*b**-1
G = FpGroup(F, [a**3, b**3, (a*b)**2])
R = G._rewriting_system
R.make_confluent()
# R._is_confluent should be set to True after
# a successful run of make_confluent
assert R.is_confluent
# but also the system should actually be confluent
assert R._check_confluence()
assert G.reduce(b*a**-1*b**-1*a**3*b**4*a**-1*b**-15) == a**-1*b**-1
# check for automaton reduction
assert R.reduce_using_automaton(b*a**-1*b**-1*a**3*b**4*a**-1*b**-15) == a**-1*b**-1
G = FpGroup(F, [a**2, b**3, (a*b)**4])
R = G._rewriting_system
assert G.reduce(a**2*b**-2*a**2*b) == b**-1
assert R.reduce_using_automaton(a**2*b**-2*a**2*b) == b**-1
assert G.reduce(a**3*b**-2*a**2*b) == a**-1*b**-1
assert R.reduce_using_automaton(a**3*b**-2*a**2*b) == a**-1*b**-1
# Check after adding a rule
R.add_rule(a**2, b)
assert R.reduce_using_automaton(a**2*b**-2*a**2*b) == b**-1
assert R.reduce_using_automaton(a**4*b**-2*a**2*b**3) == b
R.set_max(15)
raises(RuntimeError, lambda: R.add_rule(a**-3, b))
R.set_max(20)
R.add_rule(a**-3, b)
assert R.add_rule(a, a) == set()

View File

@ -0,0 +1,55 @@
from sympy.core import S, Rational
from sympy.combinatorics.schur_number import schur_partition, SchurNumber
from sympy.core.random import _randint
from sympy.testing.pytest import raises
from sympy.core.symbol import symbols
def _sum_free_test(subset):
"""
Checks if subset is sum-free(There are no x,y,z in the subset such that
x + y = z)
"""
for i in subset:
for j in subset:
assert (i + j in subset) is False
def test_schur_partition():
raises(ValueError, lambda: schur_partition(S.Infinity))
raises(ValueError, lambda: schur_partition(-1))
raises(ValueError, lambda: schur_partition(0))
assert schur_partition(2) == [[1, 2]]
random_number_generator = _randint(1000)
for _ in range(5):
n = random_number_generator(1, 1000)
result = schur_partition(n)
t = 0
numbers = []
for item in result:
_sum_free_test(item)
"""
Checks if the occurrence of all numbers is exactly one
"""
t += len(item)
for l in item:
assert (l in numbers) is False
numbers.append(l)
assert n == t
x = symbols("x")
raises(ValueError, lambda: schur_partition(x))
def test_schur_number():
first_known_schur_numbers = {1: 1, 2: 4, 3: 13, 4: 44, 5: 160}
for k in first_known_schur_numbers:
assert SchurNumber(k) == first_known_schur_numbers[k]
assert SchurNumber(S.Infinity) == S.Infinity
assert SchurNumber(0) == 0
raises(ValueError, lambda: SchurNumber(0.5))
n = symbols("n")
assert SchurNumber(n).lower_bound() == 3**n/2 - Rational(1, 2)
assert SchurNumber(8).lower_bound() == 5039

View File

@ -0,0 +1,63 @@
from sympy.combinatorics.subsets import Subset, ksubsets
from sympy.testing.pytest import raises
def test_subset():
a = Subset(['c', 'd'], ['a', 'b', 'c', 'd'])
assert a.next_binary() == Subset(['b'], ['a', 'b', 'c', 'd'])
assert a.prev_binary() == Subset(['c'], ['a', 'b', 'c', 'd'])
assert a.next_lexicographic() == Subset(['d'], ['a', 'b', 'c', 'd'])
assert a.prev_lexicographic() == Subset(['c'], ['a', 'b', 'c', 'd'])
assert a.next_gray() == Subset(['c'], ['a', 'b', 'c', 'd'])
assert a.prev_gray() == Subset(['d'], ['a', 'b', 'c', 'd'])
assert a.rank_binary == 3
assert a.rank_lexicographic == 14
assert a.rank_gray == 2
assert a.cardinality == 16
assert a.size == 2
assert Subset.bitlist_from_subset(a, ['a', 'b', 'c', 'd']) == '0011'
a = Subset([2, 5, 7], [1, 2, 3, 4, 5, 6, 7])
assert a.next_binary() == Subset([2, 5, 6], [1, 2, 3, 4, 5, 6, 7])
assert a.prev_binary() == Subset([2, 5], [1, 2, 3, 4, 5, 6, 7])
assert a.next_lexicographic() == Subset([2, 6], [1, 2, 3, 4, 5, 6, 7])
assert a.prev_lexicographic() == Subset([2, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7])
assert a.next_gray() == Subset([2, 5, 6, 7], [1, 2, 3, 4, 5, 6, 7])
assert a.prev_gray() == Subset([2, 5], [1, 2, 3, 4, 5, 6, 7])
assert a.rank_binary == 37
assert a.rank_lexicographic == 93
assert a.rank_gray == 57
assert a.cardinality == 128
superset = ['a', 'b', 'c', 'd']
assert Subset.unrank_binary(4, superset).rank_binary == 4
assert Subset.unrank_gray(10, superset).rank_gray == 10
superset = [1, 2, 3, 4, 5, 6, 7, 8, 9]
assert Subset.unrank_binary(33, superset).rank_binary == 33
assert Subset.unrank_gray(25, superset).rank_gray == 25
a = Subset([], ['a', 'b', 'c', 'd'])
i = 1
while a.subset != Subset(['d'], ['a', 'b', 'c', 'd']).subset:
a = a.next_lexicographic()
i = i + 1
assert i == 16
i = 1
while a.subset != Subset([], ['a', 'b', 'c', 'd']).subset:
a = a.prev_lexicographic()
i = i + 1
assert i == 16
raises(ValueError, lambda: Subset(['a', 'b'], ['a']))
raises(ValueError, lambda: Subset(['a'], ['b', 'c']))
raises(ValueError, lambda: Subset.subset_from_bitlist(['a', 'b'], '010'))
assert Subset(['a'], ['a', 'b']) != Subset(['b'], ['a', 'b'])
assert Subset(['a'], ['a', 'b']) != Subset(['a'], ['a', 'c'])
def test_ksubsets():
assert list(ksubsets([1, 2, 3], 2)) == [(1, 2), (1, 3), (2, 3)]
assert list(ksubsets([1, 2, 3, 4, 5], 2)) == [(1, 2), (1, 3), (1, 4),
(1, 5), (2, 3), (2, 4), (2, 5), (3, 4), (3, 5), (4, 5)]

View File

@ -0,0 +1,560 @@
from sympy.combinatorics.permutations import Permutation, Perm
from sympy.combinatorics.tensor_can import (perm_af_direct_product, dummy_sgs,
riemann_bsgs, get_symmetric_group_sgs, canonicalize, bsgs_direct_product)
from sympy.combinatorics.testutil import canonicalize_naive, graph_certificate
from sympy.testing.pytest import skip, XFAIL
def test_perm_af_direct_product():
gens1 = [[1,0,2,3], [0,1,3,2]]
gens2 = [[1,0]]
assert perm_af_direct_product(gens1, gens2, 0) == [[1, 0, 2, 3, 4, 5], [0, 1, 3, 2, 4, 5], [0, 1, 2, 3, 5, 4]]
gens1 = [[1,0,2,3,5,4], [0,1,3,2,4,5]]
gens2 = [[1,0,2,3]]
assert [[1, 0, 2, 3, 4, 5, 7, 6], [0, 1, 3, 2, 4, 5, 6, 7], [0, 1, 2, 3, 5, 4, 6, 7]]
def test_dummy_sgs():
a = dummy_sgs([1,2], 0, 4)
assert a == [[0,2,1,3,4,5]]
a = dummy_sgs([2,3,4,5], 0, 8)
assert a == [x._array_form for x in [Perm(9)(2,3), Perm(9)(4,5),
Perm(9)(2,4)(3,5)]]
a = dummy_sgs([2,3,4,5], 1, 8)
assert a == [x._array_form for x in [Perm(2,3)(8,9), Perm(4,5)(8,9),
Perm(9)(2,4)(3,5)]]
def test_get_symmetric_group_sgs():
assert get_symmetric_group_sgs(2) == ([0], [Permutation(3)(0,1)])
assert get_symmetric_group_sgs(2, 1) == ([0], [Permutation(0,1)(2,3)])
assert get_symmetric_group_sgs(3) == ([0,1], [Permutation(4)(0,1), Permutation(4)(1,2)])
assert get_symmetric_group_sgs(3, 1) == ([0,1], [Permutation(0,1)(3,4), Permutation(1,2)(3,4)])
assert get_symmetric_group_sgs(4) == ([0,1,2], [Permutation(5)(0,1), Permutation(5)(1,2), Permutation(5)(2,3)])
assert get_symmetric_group_sgs(4, 1) == ([0,1,2], [Permutation(0,1)(4,5), Permutation(1,2)(4,5), Permutation(2,3)(4,5)])
def test_canonicalize_no_slot_sym():
# cases in which there is no slot symmetry after fixing the
# free indices; here and in the following if the symmetry of the
# metric is not specified, it is assumed to be symmetric.
# If it is not specified, tensors are commuting.
# A_d0 * B^d0; g = [1,0, 2,3]; T_c = A^d0*B_d0; can = [0,1,2,3]
base1, gens1 = get_symmetric_group_sgs(1)
dummies = [0, 1]
g = Permutation([1,0,2,3])
can = canonicalize(g, dummies, 0, (base1,gens1,1,0), (base1,gens1,1,0))
assert can == [0,1,2,3]
# equivalently
can = canonicalize(g, dummies, 0, (base1, gens1, 2, None))
assert can == [0,1,2,3]
# with antisymmetric metric; T_c = -A^d0*B_d0; can = [0,1,3,2]
can = canonicalize(g, dummies, 1, (base1,gens1,1,0), (base1,gens1,1,0))
assert can == [0,1,3,2]
# A^a * B^b; ord = [a,b]; g = [0,1,2,3]; can = g
g = Permutation([0,1,2,3])
dummies = []
t0 = t1 = (base1, gens1, 1, 0)
can = canonicalize(g, dummies, 0, t0, t1)
assert can == [0,1,2,3]
# B^b * A^a
g = Permutation([1,0,2,3])
can = canonicalize(g, dummies, 0, t0, t1)
assert can == [1,0,2,3]
# A symmetric
# A^{b}_{d0}*A^{d0, a} order a,b,d0,-d0; T_c = A^{a d0}*A{b}_{d0}
# g = [1,3,2,0,4,5]; can = [0,2,1,3,4,5]
base2, gens2 = get_symmetric_group_sgs(2)
dummies = [2,3]
g = Permutation([1,3,2,0,4,5])
can = canonicalize(g, dummies, 0, (base2, gens2, 2, 0))
assert can == [0, 2, 1, 3, 4, 5]
# with antisymmetric metric
can = canonicalize(g, dummies, 1, (base2, gens2, 2, 0))
assert can == [0, 2, 1, 3, 4, 5]
# A^{a}_{d0}*A^{d0, b}
g = Permutation([0,3,2,1,4,5])
can = canonicalize(g, dummies, 1, (base2, gens2, 2, 0))
assert can == [0, 2, 1, 3, 5, 4]
# A, B symmetric
# A^b_d0*B^{d0,a}; g=[1,3,2,0,4,5]
# T_c = A^{b,d0}*B_{a,d0}; can = [1,2,0,3,4,5]
dummies = [2,3]
g = Permutation([1,3,2,0,4,5])
can = canonicalize(g, dummies, 0, (base2,gens2,1,0), (base2,gens2,1,0))
assert can == [1,2,0,3,4,5]
# same with antisymmetric metric
can = canonicalize(g, dummies, 1, (base2,gens2,1,0), (base2,gens2,1,0))
assert can == [1,2,0,3,5,4]
# A^{d1}_{d0}*B^d0*C_d1 ord=[d0,-d0,d1,-d1]; g = [2,1,0,3,4,5]
# T_c = A^{d0 d1}*B_d0*C_d1; can = [0,2,1,3,4,5]
base1, gens1 = get_symmetric_group_sgs(1)
base2, gens2 = get_symmetric_group_sgs(2)
g = Permutation([2,1,0,3,4,5])
dummies = [0,1,2,3]
t0 = (base2, gens2, 1, 0)
t1 = t2 = (base1, gens1, 1, 0)
can = canonicalize(g, dummies, 0, t0, t1, t2)
assert can == [0, 2, 1, 3, 4, 5]
# A without symmetry
# A^{d1}_{d0}*B^d0*C_d1 ord=[d0,-d0,d1,-d1]; g = [2,1,0,3,4,5]
# T_c = A^{d0 d1}*B_d1*C_d0; can = [0,2,3,1,4,5]
g = Permutation([2,1,0,3,4,5])
dummies = [0,1,2,3]
t0 = ([], [Permutation(list(range(4)))], 1, 0)
can = canonicalize(g, dummies, 0, t0, t1, t2)
assert can == [0,2,3,1,4,5]
# A, B without symmetry
# A^{d1}_{d0}*B_{d1}^{d0}; g = [2,1,3,0,4,5]
# T_c = A^{d0 d1}*B_{d0 d1}; can = [0,2,1,3,4,5]
t0 = t1 = ([], [Permutation(list(range(4)))], 1, 0)
dummies = [0,1,2,3]
g = Permutation([2,1,3,0,4,5])
can = canonicalize(g, dummies, 0, t0, t1)
assert can == [0, 2, 1, 3, 4, 5]
# A_{d0}^{d1}*B_{d1}^{d0}; g = [1,2,3,0,4,5]
# T_c = A^{d0 d1}*B_{d1 d0}; can = [0,2,3,1,4,5]
g = Permutation([1,2,3,0,4,5])
can = canonicalize(g, dummies, 0, t0, t1)
assert can == [0,2,3,1,4,5]
# A, B, C without symmetry
# A^{d1 d0}*B_{a d0}*C_{d1 b} ord=[a,b,d0,-d0,d1,-d1]
# g=[4,2,0,3,5,1,6,7]
# T_c=A^{d0 d1}*B_{a d1}*C_{d0 b}; can = [2,4,0,5,3,1,6,7]
t0 = t1 = t2 = ([], [Permutation(list(range(4)))], 1, 0)
dummies = [2,3,4,5]
g = Permutation([4,2,0,3,5,1,6,7])
can = canonicalize(g, dummies, 0, t0, t1, t2)
assert can == [2,4,0,5,3,1,6,7]
# A symmetric, B and C without symmetry
# A^{d1 d0}*B_{a d0}*C_{d1 b} ord=[a,b,d0,-d0,d1,-d1]
# g=[4,2,0,3,5,1,6,7]
# T_c = A^{d0 d1}*B_{a d0}*C_{d1 b}; can = [2,4,0,3,5,1,6,7]
t0 = (base2,gens2,1,0)
t1 = t2 = ([], [Permutation(list(range(4)))], 1, 0)
dummies = [2,3,4,5]
g = Permutation([4,2,0,3,5,1,6,7])
can = canonicalize(g, dummies, 0, t0, t1, t2)
assert can == [2,4,0,3,5,1,6,7]
# A and C symmetric, B without symmetry
# A^{d1 d0}*B_{a d0}*C_{d1 b} ord=[a,b,d0,-d0,d1,-d1]
# g=[4,2,0,3,5,1,6,7]
# T_c = A^{d0 d1}*B_{a d0}*C_{b d1}; can = [2,4,0,3,1,5,6,7]
t0 = t2 = (base2,gens2,1,0)
t1 = ([], [Permutation(list(range(4)))], 1, 0)
dummies = [2,3,4,5]
g = Permutation([4,2,0,3,5,1,6,7])
can = canonicalize(g, dummies, 0, t0, t1, t2)
assert can == [2,4,0,3,1,5,6,7]
# A symmetric, B without symmetry, C antisymmetric
# A^{d1 d0}*B_{a d0}*C_{d1 b} ord=[a,b,d0,-d0,d1,-d1]
# g=[4,2,0,3,5,1,6,7]
# T_c = -A^{d0 d1}*B_{a d0}*C_{b d1}; can = [2,4,0,3,1,5,7,6]
t0 = (base2,gens2, 1, 0)
t1 = ([], [Permutation(list(range(4)))], 1, 0)
base2a, gens2a = get_symmetric_group_sgs(2, 1)
t2 = (base2a, gens2a, 1, 0)
dummies = [2,3,4,5]
g = Permutation([4,2,0,3,5,1,6,7])
can = canonicalize(g, dummies, 0, t0, t1, t2)
assert can == [2,4,0,3,1,5,7,6]
def test_canonicalize_no_dummies():
base1, gens1 = get_symmetric_group_sgs(1)
base2, gens2 = get_symmetric_group_sgs(2)
base2a, gens2a = get_symmetric_group_sgs(2, 1)
# A commuting
# A^c A^b A^a; ord = [a,b,c]; g = [2,1,0,3,4]
# T_c = A^a A^b A^c; can = list(range(5))
g = Permutation([2,1,0,3,4])
can = canonicalize(g, [], 0, (base1, gens1, 3, 0))
assert can == list(range(5))
# A anticommuting
# A^c A^b A^a; ord = [a,b,c]; g = [2,1,0,3,4]
# T_c = -A^a A^b A^c; can = [0,1,2,4,3]
g = Permutation([2,1,0,3,4])
can = canonicalize(g, [], 0, (base1, gens1, 3, 1))
assert can == [0,1,2,4,3]
# A commuting and symmetric
# A^{b,d}*A^{c,a}; ord = [a,b,c,d]; g = [1,3,2,0,4,5]
# T_c = A^{a c}*A^{b d}; can = [0,2,1,3,4,5]
g = Permutation([1,3,2,0,4,5])
can = canonicalize(g, [], 0, (base2, gens2, 2, 0))
assert can == [0,2,1,3,4,5]
# A anticommuting and symmetric
# A^{b,d}*A^{c,a}; ord = [a,b,c,d]; g = [1,3,2,0,4,5]
# T_c = -A^{a c}*A^{b d}; can = [0,2,1,3,5,4]
g = Permutation([1,3,2,0,4,5])
can = canonicalize(g, [], 0, (base2, gens2, 2, 1))
assert can == [0,2,1,3,5,4]
# A^{c,a}*A^{b,d} ; g = [2,0,1,3,4,5]
# T_c = A^{a c}*A^{b d}; can = [0,2,1,3,4,5]
g = Permutation([2,0,1,3,4,5])
can = canonicalize(g, [], 0, (base2, gens2, 2, 1))
assert can == [0,2,1,3,4,5]
def test_no_metric_symmetry():
# no metric symmetry
# A^d1_d0 * A^d0_d1; ord = [d0,-d0,d1,-d1]; g= [2,1,0,3,4,5]
# T_c = A^d0_d1 * A^d1_d0; can = [0,3,2,1,4,5]
g = Permutation([2,1,0,3,4,5])
can = canonicalize(g, list(range(4)), None, [[], [Permutation(list(range(4)))], 2, 0])
assert can == [0,3,2,1,4,5]
# A^d1_d2 * A^d0_d3 * A^d2_d1 * A^d3_d0
# ord = [d0,-d0,d1,-d1,d2,-d2,d3,-d3]
# 0 1 2 3 4 5 6 7
# g = [2,5,0,7,4,3,6,1,8,9]
# T_c = A^d0_d1 * A^d1_d0 * A^d2_d3 * A^d3_d2
# can = [0,3,2,1,4,7,6,5,8,9]
g = Permutation([2,5,0,7,4,3,6,1,8,9])
#can = canonicalize(g, list(range(8)), 0, [[], [list(range(4))], 4, 0])
#assert can == [0, 2, 3, 1, 4, 6, 7, 5, 8, 9]
can = canonicalize(g, list(range(8)), None, [[], [Permutation(list(range(4)))], 4, 0])
assert can == [0, 3, 2, 1, 4, 7, 6, 5, 8, 9]
# A^d0_d2 * A^d1_d3 * A^d3_d0 * A^d2_d1
# g = [0,5,2,7,6,1,4,3,8,9]
# T_c = A^d0_d1 * A^d1_d2 * A^d2_d3 * A^d3_d0
# can = [0,3,2,5,4,7,6,1,8,9]
g = Permutation([0,5,2,7,6,1,4,3,8,9])
can = canonicalize(g, list(range(8)), None, [[], [Permutation(list(range(4)))], 4, 0])
assert can == [0,3,2,5,4,7,6,1,8,9]
g = Permutation([12,7,10,3,14,13,4,11,6,1,2,9,0,15,8,5,16,17])
can = canonicalize(g, list(range(16)), None, [[], [Permutation(list(range(4)))], 8, 0])
assert can == [0,3,2,5,4,7,6,1,8,11,10,13,12,15,14,9,16,17]
def test_canonical_free():
# t = A^{d0 a1}*A_d0^a0
# ord = [a0,a1,d0,-d0]; g = [2,1,3,0,4,5]; dummies = [[2,3]]
# t_c = A_d0^a0*A^{d0 a1}
# can = [3,0, 2,1, 4,5]
g = Permutation([2,1,3,0,4,5])
dummies = [[2,3]]
can = canonicalize(g, dummies, [None], ([], [Permutation(3)], 2, 0))
assert can == [3,0, 2,1, 4,5]
def test_canonicalize1():
base1, gens1 = get_symmetric_group_sgs(1)
base1a, gens1a = get_symmetric_group_sgs(1, 1)
base2, gens2 = get_symmetric_group_sgs(2)
base3, gens3 = get_symmetric_group_sgs(3)
base2a, gens2a = get_symmetric_group_sgs(2, 1)
base3a, gens3a = get_symmetric_group_sgs(3, 1)
# A_d0*A^d0; ord = [d0,-d0]; g = [1,0,2,3]
# T_c = A^d0*A_d0; can = [0,1,2,3]
g = Permutation([1,0,2,3])
can = canonicalize(g, [0, 1], 0, (base1, gens1, 2, 0))
assert can == list(range(4))
# A commuting
# A_d0*A_d1*A_d2*A^d2*A^d1*A^d0; ord=[d0,-d0,d1,-d1,d2,-d2]
# g = [1,3,5,4,2,0,6,7]
# T_c = A^d0*A_d0*A^d1*A_d1*A^d2*A_d2; can = list(range(8))
g = Permutation([1,3,5,4,2,0,6,7])
can = canonicalize(g, list(range(6)), 0, (base1, gens1, 6, 0))
assert can == list(range(8))
# A anticommuting
# A_d0*A_d1*A_d2*A^d2*A^d1*A^d0; ord=[d0,-d0,d1,-d1,d2,-d2]
# g = [1,3,5,4,2,0,6,7]
# T_c 0; can = 0
g = Permutation([1,3,5,4,2,0,6,7])
can = canonicalize(g, list(range(6)), 0, (base1, gens1, 6, 1))
assert can == 0
can1 = canonicalize_naive(g, list(range(6)), 0, (base1, gens1, 6, 1))
assert can1 == 0
# A commuting symmetric
# A^{d0 b}*A^a_d1*A^d1_d0; ord=[a,b,d0,-d0,d1,-d1]
# g = [2,1,0,5,4,3,6,7]
# T_c = A^{a d0}*A^{b d1}*A_{d0 d1}; can = [0,2,1,4,3,5,6,7]
g = Permutation([2,1,0,5,4,3,6,7])
can = canonicalize(g, list(range(2,6)), 0, (base2, gens2, 3, 0))
assert can == [0,2,1,4,3,5,6,7]
# A, B commuting symmetric
# A^{d0 b}*A^d1_d0*B^a_d1; ord=[a,b,d0,-d0,d1,-d1]
# g = [2,1,4,3,0,5,6,7]
# T_c = A^{b d0}*A_d0^d1*B^a_d1; can = [1,2,3,4,0,5,6,7]
g = Permutation([2,1,4,3,0,5,6,7])
can = canonicalize(g, list(range(2,6)), 0, (base2,gens2,2,0), (base2,gens2,1,0))
assert can == [1,2,3,4,0,5,6,7]
# A commuting symmetric
# A^{d1 d0 b}*A^{a}_{d1 d0}; ord=[a,b, d0,-d0,d1,-d1]
# g = [4,2,1,0,5,3,6,7]
# T_c = A^{a d0 d1}*A^{b}_{d0 d1}; can = [0,2,4,1,3,5,6,7]
g = Permutation([4,2,1,0,5,3,6,7])
can = canonicalize(g, list(range(2,6)), 0, (base3, gens3, 2, 0))
assert can == [0,2,4,1,3,5,6,7]
# A^{d3 d0 d2}*A^a0_{d1 d2}*A^d1_d3^a1*A^{a2 a3}_d0
# ord = [a0,a1,a2,a3,d0,-d0,d1,-d1,d2,-d2,d3,-d3]
# 0 1 2 3 4 5 6 7 8 9 10 11
# g = [10,4,8, 0,7,9, 6,11,1, 2,3,5, 12,13]
# T_c = A^{a0 d0 d1}*A^a1_d0^d2*A^{a2 a3 d3}*A_{d1 d2 d3}
# can = [0,4,6, 1,5,8, 2,3,10, 7,9,11, 12,13]
g = Permutation([10,4,8, 0,7,9, 6,11,1, 2,3,5, 12,13])
can = canonicalize(g, list(range(4,12)), 0, (base3, gens3, 4, 0))
assert can == [0,4,6, 1,5,8, 2,3,10, 7,9,11, 12,13]
# A commuting symmetric, B antisymmetric
# A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3
# ord = [d0,-d0,d1,-d1,d2,-d2,d3,-d3]
# g = [0,2,4,5,7,3,1,6,8,9]
# in this esxample and in the next three,
# renaming dummy indices and using symmetry of A,
# T = A^{d0 d1 d2} * A_{d0 d1 d3} * B_d2^d3
# can = 0
g = Permutation([0,2,4,5,7,3,1,6,8,9])
can = canonicalize(g, list(range(8)), 0, (base3, gens3,2,0), (base2a,gens2a,1,0))
assert can == 0
# A anticommuting symmetric, B anticommuting
# A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3
# T_c = A^{d0 d1 d2} * A_{d0 d1}^d3 * B_{d2 d3}
# can = [0,2,4, 1,3,6, 5,7, 8,9]
can = canonicalize(g, list(range(8)), 0, (base3, gens3,2,1), (base2a,gens2a,1,0))
assert can == [0,2,4, 1,3,6, 5,7, 8,9]
# A anticommuting symmetric, B antisymmetric commuting, antisymmetric metric
# A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3
# T_c = -A^{d0 d1 d2} * A_{d0 d1}^d3 * B_{d2 d3}
# can = [0,2,4, 1,3,6, 5,7, 9,8]
can = canonicalize(g, list(range(8)), 1, (base3, gens3,2,1), (base2a,gens2a,1,0))
assert can == [0,2,4, 1,3,6, 5,7, 9,8]
# A anticommuting symmetric, B anticommuting anticommuting,
# no metric symmetry
# A^{d0 d1 d2} * A_{d2 d3 d1} * B_d0^d3
# T_c = A^{d0 d1 d2} * A_{d0 d1 d3} * B_d2^d3
# can = [0,2,4, 1,3,7, 5,6, 8,9]
can = canonicalize(g, list(range(8)), None, (base3, gens3,2,1), (base2a,gens2a,1,0))
assert can == [0,2,4,1,3,7,5,6,8,9]
# Gamma anticommuting
# Gamma_{mu nu} * gamma^rho * Gamma^{nu mu alpha}
# ord = [alpha, rho, mu,-mu,nu,-nu]
# g = [3,5,1,4,2,0,6,7]
# T_c = -Gamma^{mu nu} * gamma^rho * Gamma_{alpha mu nu}
# can = [2,4,1,0,3,5,7,6]]
g = Permutation([3,5,1,4,2,0,6,7])
t0 = (base2a, gens2a, 1, None)
t1 = (base1, gens1, 1, None)
t2 = (base3a, gens3a, 1, None)
can = canonicalize(g, list(range(2, 6)), 0, t0, t1, t2)
assert can == [2,4,1,0,3,5,7,6]
# Gamma_{mu nu} * Gamma^{gamma beta} * gamma_rho * Gamma^{nu mu alpha}
# ord = [alpha, beta, gamma, -rho, mu,-mu,nu,-nu]
# 0 1 2 3 4 5 6 7
# g = [5,7,2,1,3,6,4,0,8,9]
# T_c = Gamma^{mu nu} * Gamma^{beta gamma} * gamma_rho * Gamma^alpha_{mu nu} # can = [4,6,1,2,3,0,5,7,8,9]
t0 = (base2a, gens2a, 2, None)
g = Permutation([5,7,2,1,3,6,4,0,8,9])
can = canonicalize(g, list(range(4, 8)), 0, t0, t1, t2)
assert can == [4,6,1,2,3,0,5,7,8,9]
# f^a_{b,c} antisymmetric in b,c; A_mu^a no symmetry
# f^c_{d a} * f_{c e b} * A_mu^d * A_nu^a * A^{nu e} * A^{mu b}
# ord = [mu,-mu,nu,-nu,a,-a,b,-b,c,-c,d,-d, e, -e]
# 0 1 2 3 4 5 6 7 8 9 10 11 12 13
# g = [8,11,5, 9,13,7, 1,10, 3,4, 2,12, 0,6, 14,15]
# T_c = -f^{a b c} * f_a^{d e} * A^mu_b * A_{mu d} * A^nu_c * A_{nu e}
# can = [4,6,8, 5,10,12, 0,7, 1,11, 2,9, 3,13, 15,14]
g = Permutation([8,11,5, 9,13,7, 1,10, 3,4, 2,12, 0,6, 14,15])
base_f, gens_f = bsgs_direct_product(base1, gens1, base2a, gens2a)
base_A, gens_A = bsgs_direct_product(base1, gens1, base1, gens1)
t0 = (base_f, gens_f, 2, 0)
t1 = (base_A, gens_A, 4, 0)
can = canonicalize(g, [list(range(4)), list(range(4, 14))], [0, 0], t0, t1)
assert can == [4,6,8, 5,10,12, 0,7, 1,11, 2,9, 3,13, 15,14]
def test_riemann_invariants():
baser, gensr = riemann_bsgs
# R^{d0 d1}_{d1 d0}; ord = [d0,-d0,d1,-d1]; g = [0,2,3,1,4,5]
# T_c = -R^{d0 d1}_{d0 d1}; can = [0,2,1,3,5,4]
g = Permutation([0,2,3,1,4,5])
can = canonicalize(g, list(range(2, 4)), 0, (baser, gensr, 1, 0))
assert can == [0,2,1,3,5,4]
# use a non minimal BSGS
can = canonicalize(g, list(range(2, 4)), 0, ([2, 0], [Permutation([1,0,2,3,5,4]), Permutation([2,3,0,1,4,5])], 1, 0))
assert can == [0,2,1,3,5,4]
"""
The following tests in test_riemann_invariants and in
test_riemann_invariants1 have been checked using xperm.c from XPerm in
in [1] and with an older version contained in [2]
[1] xperm.c part of xPerm written by J. M. Martin-Garcia
http://www.xact.es/index.html
[2] test_xperm.cc in cadabra by Kasper Peeters, http://cadabra.phi-sci.com/
"""
# R_d11^d1_d0^d5 * R^{d6 d4 d0}_d5 * R_{d7 d2 d8 d9} *
# R_{d10 d3 d6 d4} * R^{d2 d7 d11}_d1 * R^{d8 d9 d3 d10}
# ord: contravariant d_k ->2*k, covariant d_k -> 2*k+1
# T_c = R^{d0 d1 d2 d3} * R_{d0 d1}^{d4 d5} * R_{d2 d3}^{d6 d7} *
# R_{d4 d5}^{d8 d9} * R_{d6 d7}^{d10 d11} * R_{d8 d9 d10 d11}
g = Permutation([23,2,1,10,12,8,0,11,15,5,17,19,21,7,13,9,4,14,22,3,16,18,6,20,24,25])
can = canonicalize(g, list(range(24)), 0, (baser, gensr, 6, 0))
assert can == [0,2,4,6,1,3,8,10,5,7,12,14,9,11,16,18,13,15,20,22,17,19,21,23,24,25]
# use a non minimal BSGS
can = canonicalize(g, list(range(24)), 0, ([2, 0], [Permutation([1,0,2,3,5,4]), Permutation([2,3,0,1,4,5])], 6, 0))
assert can == [0,2,4,6,1,3,8,10,5,7,12,14,9,11,16,18,13,15,20,22,17,19,21,23,24,25]
g = Permutation([0,2,5,7,4,6,9,11,8,10,13,15,12,14,17,19,16,18,21,23,20,22,25,27,24,26,29,31,28,30,33,35,32,34,37,39,36,38,1,3,40,41])
can = canonicalize(g, list(range(40)), 0, (baser, gensr, 10, 0))
assert can == [0,2,4,6,1,3,8,10,5,7,12,14,9,11,16,18,13,15,20,22,17,19,24,26,21,23,28,30,25,27,32,34,29,31,36,38,33,35,37,39,40,41]
@XFAIL
def test_riemann_invariants1():
skip('takes too much time')
baser, gensr = riemann_bsgs
g = Permutation([17, 44, 11, 3, 0, 19, 23, 15, 38, 4, 25, 27, 43, 36, 22, 14, 8, 30, 41, 20, 2, 10, 12, 28, 18, 1, 29, 13, 37, 42, 33, 7, 9, 31, 24, 26, 39, 5, 34, 47, 32, 6, 21, 40, 35, 46, 45, 16, 48, 49])
can = canonicalize(g, list(range(48)), 0, (baser, gensr, 12, 0))
assert can == [0, 2, 4, 6, 1, 3, 8, 10, 5, 7, 12, 14, 9, 11, 16, 18, 13, 15, 20, 22, 17, 19, 24, 26, 21, 23, 28, 30, 25, 27, 32, 34, 29, 31, 36, 38, 33, 35, 40, 42, 37, 39, 44, 46, 41, 43, 45, 47, 48, 49]
g = Permutation([0,2,4,6, 7,8,10,12, 14,16,18,20, 19,22,24,26, 5,21,28,30, 32,34,36,38, 40,42,44,46, 13,48,50,52, 15,49,54,56, 17,33,41,58, 9,23,60,62, 29,35,63,64, 3,45,66,68, 25,37,47,57, 11,31,69,70, 27,39,53,72, 1,59,73,74, 55,61,67,76, 43,65,75,78, 51,71,77,79, 80,81])
can = canonicalize(g, list(range(80)), 0, (baser, gensr, 20, 0))
assert can == [0,2,4,6, 1,8,10,12, 3,14,16,18, 5,20,22,24, 7,26,28,30, 9,15,32,34, 11,36,23,38, 13,40,42,44, 17,39,29,46, 19,48,43,50, 21,45,52,54, 25,56,33,58, 27,60,53,62, 31,51,64,66, 35,65,47,68, 37,70,49,72, 41,74,57,76, 55,67,59,78, 61,69,71,75, 63,79,73,77, 80,81]
def test_riemann_products():
baser, gensr = riemann_bsgs
base1, gens1 = get_symmetric_group_sgs(1)
base2, gens2 = get_symmetric_group_sgs(2)
base2a, gens2a = get_symmetric_group_sgs(2, 1)
# R^{a b d0}_d0 = 0
g = Permutation([0,1,2,3,4,5])
can = canonicalize(g, list(range(2,4)), 0, (baser, gensr, 1, 0))
assert can == 0
# R^{d0 b a}_d0 ; ord = [a,b,d0,-d0}; g = [2,1,0,3,4,5]
# T_c = -R^{a d0 b}_d0; can = [0,2,1,3,5,4]
g = Permutation([2,1,0,3,4,5])
can = canonicalize(g, list(range(2, 4)), 0, (baser, gensr, 1, 0))
assert can == [0,2,1,3,5,4]
# R^d1_d2^b_d0 * R^{d0 a}_d1^d2; ord=[a,b,d0,-d0,d1,-d1,d2,-d2]
# g = [4,7,1,3,2,0,5,6,8,9]
# T_c = -R^{a d0 d1 d2}* R^b_{d0 d1 d2}
# can = [0,2,4,6,1,3,5,7,9,8]
g = Permutation([4,7,1,3,2,0,5,6,8,9])
can = canonicalize(g, list(range(2,8)), 0, (baser, gensr, 2, 0))
assert can == [0,2,4,6,1,3,5,7,9,8]
can1 = canonicalize_naive(g, list(range(2,8)), 0, (baser, gensr, 2, 0))
assert can == can1
# A symmetric commuting
# R^{d6 d5}_d2^d1 * R^{d4 d0 d2 d3} * A_{d6 d0} A_{d3 d1} * A_{d4 d5}
# g = [12,10,5,2, 8,0,4,6, 13,1, 7,3, 9,11,14,15]
# T_c = -R^{d0 d1 d2 d3} * R_d0^{d4 d5 d6} * A_{d1 d4}*A_{d2 d5}*A_{d3 d6}
g = Permutation([12,10,5,2,8,0,4,6,13,1,7,3,9,11,14,15])
can = canonicalize(g, list(range(14)), 0, ((baser,gensr,2,0)), (base2,gens2,3,0))
assert can == [0, 2, 4, 6, 1, 8, 10, 12, 3, 9, 5, 11, 7, 13, 15, 14]
# R^{d2 a0 a2 d0} * R^d1_d2^{a1 a3} * R^{a4 a5}_{d0 d1}
# ord = [a0,a1,a2,a3,a4,a5,d0,-d0,d1,-d1,d2,-d2]
# 0 1 2 3 4 5 6 7 8 9 10 11
# can = [0, 6, 2, 8, 1, 3, 7, 10, 4, 5, 9, 11, 12, 13]
# T_c = R^{a0 d0 a2 d1}*R^{a1 a3}_d0^d2*R^{a4 a5}_{d1 d2}
g = Permutation([10,0,2,6,8,11,1,3,4,5,7,9,12,13])
can = canonicalize(g, list(range(6,12)), 0, (baser, gensr, 3, 0))
assert can == [0, 6, 2, 8, 1, 3, 7, 10, 4, 5, 9, 11, 12, 13]
#can1 = canonicalize_naive(g, list(range(6,12)), 0, (baser, gensr, 3, 0))
#assert can == can1
# A^n_{i, j} antisymmetric in i,j
# A_m0^d0_a1 * A_m1^a0_d0; ord = [m0,m1,a0,a1,d0,-d0]
# g = [0,4,3,1,2,5,6,7]
# T_c = -A_{m a1}^d0 * A_m1^a0_d0
# can = [0,3,4,1,2,5,7,6]
base, gens = bsgs_direct_product(base1, gens1, base2a, gens2a)
dummies = list(range(4, 6))
g = Permutation([0,4,3,1,2,5,6,7])
can = canonicalize(g, dummies, 0, (base, gens, 2, 0))
assert can == [0, 3, 4, 1, 2, 5, 7, 6]
# A^n_{i, j} symmetric in i,j
# A^m0_a0^d2 * A^n0_d2^d1 * A^n1_d1^d0 * A_{m0 d0}^a1
# ordering: first the free indices; then first n, then d
# ord=[n0,n1,a0,a1, m0,-m0,d0,-d0,d1,-d1,d2,-d2]
# 0 1 2 3 4 5 6 7 8 9 10 11]
# g = [4,2,10, 0,11,8, 1,9,6, 5,7,3, 12,13]
# if the dummy indices m_i and d_i were separated,
# one gets
# T_c = A^{n0 d0 d1} * A^n1_d0^d2 * A^m0^a0_d1 * A_m0^a1_d2
# can = [0, 6, 8, 1, 7, 10, 4, 2, 9, 5, 3, 11, 12, 13]
# If they are not, so can is
# T_c = A^{n0 m0 d0} A^n1_m0^d1 A^{d2 a0}_d0 A_d2^a1_d1
# can = [0, 4, 6, 1, 5, 8, 10, 2, 7, 11, 3, 9, 12, 13]
# case with single type of indices
base, gens = bsgs_direct_product(base1, gens1, base2, gens2)
dummies = list(range(4, 12))
g = Permutation([4,2,10, 0,11,8, 1,9,6, 5,7,3, 12,13])
can = canonicalize(g, dummies, 0, (base, gens, 4, 0))
assert can == [0, 4, 6, 1, 5, 8, 10, 2, 7, 11, 3, 9, 12, 13]
# case with separated indices
dummies = [list(range(4, 6)), list(range(6,12))]
sym = [0, 0]
can = canonicalize(g, dummies, sym, (base, gens, 4, 0))
assert can == [0, 6, 8, 1, 7, 10, 4, 2, 9, 5, 3, 11, 12, 13]
# case with separated indices with the second type of index
# with antisymmetric metric: there is a sign change
sym = [0, 1]
can = canonicalize(g, dummies, sym, (base, gens, 4, 0))
assert can == [0, 6, 8, 1, 7, 10, 4, 2, 9, 5, 3, 11, 13, 12]
def test_graph_certificate():
# test tensor invariants constructed from random regular graphs;
# checked graph isomorphism with networkx
import random
def randomize_graph(size, g):
p = list(range(size))
random.shuffle(p)
g1a = {}
for k, v in g1.items():
g1a[p[k]] = [p[i] for i in v]
return g1a
g1 = {0: [2, 3, 7], 1: [4, 5, 7], 2: [0, 4, 6], 3: [0, 6, 7], 4: [1, 2, 5], 5: [1, 4, 6], 6: [2, 3, 5], 7: [0, 1, 3]}
g2 = {0: [2, 3, 7], 1: [2, 4, 5], 2: [0, 1, 5], 3: [0, 6, 7], 4: [1, 5, 6], 5: [1, 2, 4], 6: [3, 4, 7], 7: [0, 3, 6]}
c1 = graph_certificate(g1)
c2 = graph_certificate(g2)
assert c1 != c2
g1a = randomize_graph(8, g1)
c1a = graph_certificate(g1a)
assert c1 == c1a
g1 = {0: [8, 1, 9, 7], 1: [0, 9, 3, 4], 2: [3, 4, 6, 7], 3: [1, 2, 5, 6], 4: [8, 1, 2, 5], 5: [9, 3, 4, 7], 6: [8, 2, 3, 7], 7: [0, 2, 5, 6], 8: [0, 9, 4, 6], 9: [8, 0, 5, 1]}
g2 = {0: [1, 2, 5, 6], 1: [0, 9, 5, 7], 2: [0, 4, 6, 7], 3: [8, 9, 6, 7], 4: [8, 2, 6, 7], 5: [0, 9, 8, 1], 6: [0, 2, 3, 4], 7: [1, 2, 3, 4], 8: [9, 3, 4, 5], 9: [8, 1, 3, 5]}
c1 = graph_certificate(g1)
c2 = graph_certificate(g2)
assert c1 != c2
g1a = randomize_graph(10, g1)
c1a = graph_certificate(g1a)
assert c1 == c1a

View File

@ -0,0 +1,55 @@
from sympy.combinatorics.named_groups import SymmetricGroup, AlternatingGroup,\
CyclicGroup
from sympy.combinatorics.testutil import _verify_bsgs, _cmp_perm_lists,\
_naive_list_centralizer, _verify_centralizer,\
_verify_normal_closure
from sympy.combinatorics.permutations import Permutation
from sympy.combinatorics.perm_groups import PermutationGroup
from sympy.core.random import shuffle
def test_cmp_perm_lists():
S = SymmetricGroup(4)
els = list(S.generate_dimino())
other = els[:]
shuffle(other)
assert _cmp_perm_lists(els, other) is True
def test_naive_list_centralizer():
# verified by GAP
S = SymmetricGroup(3)
A = AlternatingGroup(3)
assert _naive_list_centralizer(S, S) == [Permutation([0, 1, 2])]
assert PermutationGroup(_naive_list_centralizer(S, A)).is_subgroup(A)
def test_verify_bsgs():
S = SymmetricGroup(5)
S.schreier_sims()
base = S.base
strong_gens = S.strong_gens
assert _verify_bsgs(S, base, strong_gens) is True
assert _verify_bsgs(S, base[:-1], strong_gens) is False
assert _verify_bsgs(S, base, S.generators) is False
def test_verify_centralizer():
# verified by GAP
S = SymmetricGroup(3)
A = AlternatingGroup(3)
triv = PermutationGroup([Permutation([0, 1, 2])])
assert _verify_centralizer(S, S, centr=triv)
assert _verify_centralizer(S, A, centr=A)
def test_verify_normal_closure():
# verified by GAP
S = SymmetricGroup(3)
A = AlternatingGroup(3)
assert _verify_normal_closure(S, A, closure=A)
S = SymmetricGroup(5)
A = AlternatingGroup(5)
C = CyclicGroup(5)
assert _verify_normal_closure(S, A, closure=A)
assert _verify_normal_closure(S, C, closure=A)

View File

@ -0,0 +1,120 @@
from sympy.combinatorics.named_groups import SymmetricGroup, DihedralGroup,\
AlternatingGroup
from sympy.combinatorics.permutations import Permutation
from sympy.combinatorics.util import _check_cycles_alt_sym, _strip,\
_distribute_gens_by_base, _strong_gens_from_distr,\
_orbits_transversals_from_bsgs, _handle_precomputed_bsgs, _base_ordering,\
_remove_gens
from sympy.combinatorics.testutil import _verify_bsgs
def test_check_cycles_alt_sym():
perm1 = Permutation([[0, 1, 2, 3, 4, 5, 6], [7], [8], [9]])
perm2 = Permutation([[0, 1, 2, 3, 4, 5], [6, 7, 8, 9]])
perm3 = Permutation([[0, 1, 2, 3, 4], [5, 6, 7, 8, 9]])
assert _check_cycles_alt_sym(perm1) is True
assert _check_cycles_alt_sym(perm2) is False
assert _check_cycles_alt_sym(perm3) is False
def test_strip():
D = DihedralGroup(5)
D.schreier_sims()
member = Permutation([4, 0, 1, 2, 3])
not_member1 = Permutation([0, 1, 4, 3, 2])
not_member2 = Permutation([3, 1, 4, 2, 0])
identity = Permutation([0, 1, 2, 3, 4])
res1 = _strip(member, D.base, D.basic_orbits, D.basic_transversals)
res2 = _strip(not_member1, D.base, D.basic_orbits, D.basic_transversals)
res3 = _strip(not_member2, D.base, D.basic_orbits, D.basic_transversals)
assert res1[0] == identity
assert res1[1] == len(D.base) + 1
assert res2[0] == not_member1
assert res2[1] == len(D.base) + 1
assert res3[0] != identity
assert res3[1] == 2
def test_distribute_gens_by_base():
base = [0, 1, 2]
gens = [Permutation([0, 1, 2, 3]), Permutation([0, 1, 3, 2]),
Permutation([0, 2, 3, 1]), Permutation([3, 2, 1, 0])]
assert _distribute_gens_by_base(base, gens) == [gens,
[Permutation([0, 1, 2, 3]),
Permutation([0, 1, 3, 2]),
Permutation([0, 2, 3, 1])],
[Permutation([0, 1, 2, 3]),
Permutation([0, 1, 3, 2])]]
def test_strong_gens_from_distr():
strong_gens_distr = [[Permutation([0, 2, 1]), Permutation([1, 2, 0]),
Permutation([1, 0, 2])], [Permutation([0, 2, 1])]]
assert _strong_gens_from_distr(strong_gens_distr) == \
[Permutation([0, 2, 1]),
Permutation([1, 2, 0]),
Permutation([1, 0, 2])]
def test_orbits_transversals_from_bsgs():
S = SymmetricGroup(4)
S.schreier_sims()
base = S.base
strong_gens = S.strong_gens
strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
result = _orbits_transversals_from_bsgs(base, strong_gens_distr)
orbits = result[0]
transversals = result[1]
base_len = len(base)
for i in range(base_len):
for el in orbits[i]:
assert transversals[i][el](base[i]) == el
for j in range(i):
assert transversals[i][el](base[j]) == base[j]
order = 1
for i in range(base_len):
order *= len(orbits[i])
assert S.order() == order
def test_handle_precomputed_bsgs():
A = AlternatingGroup(5)
A.schreier_sims()
base = A.base
strong_gens = A.strong_gens
result = _handle_precomputed_bsgs(base, strong_gens)
strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
assert strong_gens_distr == result[2]
transversals = result[0]
orbits = result[1]
base_len = len(base)
for i in range(base_len):
for el in orbits[i]:
assert transversals[i][el](base[i]) == el
for j in range(i):
assert transversals[i][el](base[j]) == base[j]
order = 1
for i in range(base_len):
order *= len(orbits[i])
assert A.order() == order
def test_base_ordering():
base = [2, 4, 5]
degree = 7
assert _base_ordering(base, degree) == [3, 4, 0, 5, 1, 2, 6]
def test_remove_gens():
S = SymmetricGroup(10)
base, strong_gens = S.schreier_sims_incremental()
new_gens = _remove_gens(base, strong_gens)
assert _verify_bsgs(S, base, new_gens) is True
A = AlternatingGroup(7)
base, strong_gens = A.schreier_sims_incremental()
new_gens = _remove_gens(base, strong_gens)
assert _verify_bsgs(A, base, new_gens) is True
D = DihedralGroup(2)
base, strong_gens = D.schreier_sims_incremental()
new_gens = _remove_gens(base, strong_gens)
assert _verify_bsgs(D, base, new_gens) is True

View File

@ -0,0 +1,357 @@
from sympy.combinatorics import Permutation
from sympy.combinatorics.util import _distribute_gens_by_base
rmul = Permutation.rmul
def _cmp_perm_lists(first, second):
"""
Compare two lists of permutations as sets.
Explanation
===========
This is used for testing purposes. Since the array form of a
permutation is currently a list, Permutation is not hashable
and cannot be put into a set.
Examples
========
>>> from sympy.combinatorics.permutations import Permutation
>>> from sympy.combinatorics.testutil import _cmp_perm_lists
>>> a = Permutation([0, 2, 3, 4, 1])
>>> b = Permutation([1, 2, 0, 4, 3])
>>> c = Permutation([3, 4, 0, 1, 2])
>>> ls1 = [a, b, c]
>>> ls2 = [b, c, a]
>>> _cmp_perm_lists(ls1, ls2)
True
"""
return {tuple(a) for a in first} == \
{tuple(a) for a in second}
def _naive_list_centralizer(self, other, af=False):
from sympy.combinatorics.perm_groups import PermutationGroup
"""
Return a list of elements for the centralizer of a subgroup/set/element.
Explanation
===========
This is a brute force implementation that goes over all elements of the
group and checks for membership in the centralizer. It is used to
test ``.centralizer()`` from ``sympy.combinatorics.perm_groups``.
Examples
========
>>> from sympy.combinatorics.testutil import _naive_list_centralizer
>>> from sympy.combinatorics.named_groups import DihedralGroup
>>> D = DihedralGroup(4)
>>> _naive_list_centralizer(D, D)
[Permutation([0, 1, 2, 3]), Permutation([2, 3, 0, 1])]
See Also
========
sympy.combinatorics.perm_groups.centralizer
"""
from sympy.combinatorics.permutations import _af_commutes_with
if hasattr(other, 'generators'):
elements = list(self.generate_dimino(af=True))
gens = [x._array_form for x in other.generators]
commutes_with_gens = lambda x: all(_af_commutes_with(x, gen) for gen in gens)
centralizer_list = []
if not af:
for element in elements:
if commutes_with_gens(element):
centralizer_list.append(Permutation._af_new(element))
else:
for element in elements:
if commutes_with_gens(element):
centralizer_list.append(element)
return centralizer_list
elif hasattr(other, 'getitem'):
return _naive_list_centralizer(self, PermutationGroup(other), af)
elif hasattr(other, 'array_form'):
return _naive_list_centralizer(self, PermutationGroup([other]), af)
def _verify_bsgs(group, base, gens):
"""
Verify the correctness of a base and strong generating set.
Explanation
===========
This is a naive implementation using the definition of a base and a strong
generating set relative to it. There are other procedures for
verifying a base and strong generating set, but this one will
serve for more robust testing.
Examples
========
>>> from sympy.combinatorics.named_groups import AlternatingGroup
>>> from sympy.combinatorics.testutil import _verify_bsgs
>>> A = AlternatingGroup(4)
>>> A.schreier_sims()
>>> _verify_bsgs(A, A.base, A.strong_gens)
True
See Also
========
sympy.combinatorics.perm_groups.PermutationGroup.schreier_sims
"""
from sympy.combinatorics.perm_groups import PermutationGroup
strong_gens_distr = _distribute_gens_by_base(base, gens)
current_stabilizer = group
for i in range(len(base)):
candidate = PermutationGroup(strong_gens_distr[i])
if current_stabilizer.order() != candidate.order():
return False
current_stabilizer = current_stabilizer.stabilizer(base[i])
if current_stabilizer.order() != 1:
return False
return True
def _verify_centralizer(group, arg, centr=None):
"""
Verify the centralizer of a group/set/element inside another group.
This is used for testing ``.centralizer()`` from
``sympy.combinatorics.perm_groups``
Examples
========
>>> from sympy.combinatorics.named_groups import (SymmetricGroup,
... AlternatingGroup)
>>> from sympy.combinatorics.perm_groups import PermutationGroup
>>> from sympy.combinatorics.permutations import Permutation
>>> from sympy.combinatorics.testutil import _verify_centralizer
>>> S = SymmetricGroup(5)
>>> A = AlternatingGroup(5)
>>> centr = PermutationGroup([Permutation([0, 1, 2, 3, 4])])
>>> _verify_centralizer(S, A, centr)
True
See Also
========
_naive_list_centralizer,
sympy.combinatorics.perm_groups.PermutationGroup.centralizer,
_cmp_perm_lists
"""
if centr is None:
centr = group.centralizer(arg)
centr_list = list(centr.generate_dimino(af=True))
centr_list_naive = _naive_list_centralizer(group, arg, af=True)
return _cmp_perm_lists(centr_list, centr_list_naive)
def _verify_normal_closure(group, arg, closure=None):
from sympy.combinatorics.perm_groups import PermutationGroup
"""
Verify the normal closure of a subgroup/subset/element in a group.
This is used to test
sympy.combinatorics.perm_groups.PermutationGroup.normal_closure
Examples
========
>>> from sympy.combinatorics.named_groups import (SymmetricGroup,
... AlternatingGroup)
>>> from sympy.combinatorics.testutil import _verify_normal_closure
>>> S = SymmetricGroup(3)
>>> A = AlternatingGroup(3)
>>> _verify_normal_closure(S, A, closure=A)
True
See Also
========
sympy.combinatorics.perm_groups.PermutationGroup.normal_closure
"""
if closure is None:
closure = group.normal_closure(arg)
conjugates = set()
if hasattr(arg, 'generators'):
subgr_gens = arg.generators
elif hasattr(arg, '__getitem__'):
subgr_gens = arg
elif hasattr(arg, 'array_form'):
subgr_gens = [arg]
for el in group.generate_dimino():
conjugates.update(gen ^ el for gen in subgr_gens)
naive_closure = PermutationGroup(list(conjugates))
return closure.is_subgroup(naive_closure)
def canonicalize_naive(g, dummies, sym, *v):
"""
Canonicalize tensor formed by tensors of the different types.
Explanation
===========
sym_i symmetry under exchange of two component tensors of type `i`
None no symmetry
0 commuting
1 anticommuting
Parameters
==========
g : Permutation representing the tensor.
dummies : List of dummy indices.
msym : Symmetry of the metric.
v : A list of (base_i, gens_i, n_i, sym_i) for tensors of type `i`.
base_i, gens_i BSGS for tensors of this type
n_i number of tensors of type `i`
Returns
=======
Returns 0 if the tensor is zero, else returns the array form of
the permutation representing the canonical form of the tensor.
Examples
========
>>> from sympy.combinatorics.testutil import canonicalize_naive
>>> from sympy.combinatorics.tensor_can import get_symmetric_group_sgs
>>> from sympy.combinatorics import Permutation
>>> g = Permutation([1, 3, 2, 0, 4, 5])
>>> base2, gens2 = get_symmetric_group_sgs(2)
>>> canonicalize_naive(g, [2, 3], 0, (base2, gens2, 2, 0))
[0, 2, 1, 3, 4, 5]
"""
from sympy.combinatorics.perm_groups import PermutationGroup
from sympy.combinatorics.tensor_can import gens_products, dummy_sgs
from sympy.combinatorics.permutations import _af_rmul
v1 = []
for i in range(len(v)):
base_i, gens_i, n_i, sym_i = v[i]
v1.append((base_i, gens_i, [[]]*n_i, sym_i))
size, sbase, sgens = gens_products(*v1)
dgens = dummy_sgs(dummies, sym, size-2)
if isinstance(sym, int):
num_types = 1
dummies = [dummies]
sym = [sym]
else:
num_types = len(sym)
dgens = []
for i in range(num_types):
dgens.extend(dummy_sgs(dummies[i], sym[i], size - 2))
S = PermutationGroup(sgens)
D = PermutationGroup([Permutation(x) for x in dgens])
dlist = list(D.generate(af=True))
g = g.array_form
st = set()
for s in S.generate(af=True):
h = _af_rmul(g, s)
for d in dlist:
q = tuple(_af_rmul(d, h))
st.add(q)
a = list(st)
a.sort()
prev = (0,)*size
for h in a:
if h[:-2] == prev[:-2]:
if h[-1] != prev[-1]:
return 0
prev = h
return list(a[0])
def graph_certificate(gr):
"""
Return a certificate for the graph
Parameters
==========
gr : adjacency list
Explanation
===========
The graph is assumed to be unoriented and without
external lines.
Associate to each vertex of the graph a symmetric tensor with
number of indices equal to the degree of the vertex; indices
are contracted when they correspond to the same line of the graph.
The canonical form of the tensor gives a certificate for the graph.
This is not an efficient algorithm to get the certificate of a graph.
Examples
========
>>> from sympy.combinatorics.testutil import graph_certificate
>>> gr1 = {0:[1, 2, 3, 5], 1:[0, 2, 4], 2:[0, 1, 3, 4], 3:[0, 2, 4], 4:[1, 2, 3, 5], 5:[0, 4]}
>>> gr2 = {0:[1, 5], 1:[0, 2, 3, 4], 2:[1, 3, 5], 3:[1, 2, 4, 5], 4:[1, 3, 5], 5:[0, 2, 3, 4]}
>>> c1 = graph_certificate(gr1)
>>> c2 = graph_certificate(gr2)
>>> c1
[0, 2, 4, 6, 1, 8, 10, 12, 3, 14, 16, 18, 5, 9, 15, 7, 11, 17, 13, 19, 20, 21]
>>> c1 == c2
True
"""
from sympy.combinatorics.permutations import _af_invert
from sympy.combinatorics.tensor_can import get_symmetric_group_sgs, canonicalize
items = list(gr.items())
items.sort(key=lambda x: len(x[1]), reverse=True)
pvert = [x[0] for x in items]
pvert = _af_invert(pvert)
# the indices of the tensor are twice the number of lines of the graph
num_indices = 0
for v, neigh in items:
num_indices += len(neigh)
# associate to each vertex its indices; for each line
# between two vertices assign the
# even index to the vertex which comes first in items,
# the odd index to the other vertex
vertices = [[] for i in items]
i = 0
for v, neigh in items:
for v2 in neigh:
if pvert[v] < pvert[v2]:
vertices[pvert[v]].append(i)
vertices[pvert[v2]].append(i+1)
i += 2
g = []
for v in vertices:
g.extend(v)
assert len(g) == num_indices
g += [num_indices, num_indices + 1]
size = num_indices + 2
assert sorted(g) == list(range(size))
g = Permutation(g)
vlen = [0]*(len(vertices[0])+1)
for neigh in vertices:
vlen[len(neigh)] += 1
v = []
for i in range(len(vlen)):
n = vlen[i]
if n:
base, gens = get_symmetric_group_sgs(i)
v.append((base, gens, n, 0))
v.reverse()
dummies = list(range(num_indices))
can = canonicalize(g, dummies, 0, *v)
return can

View File

@ -0,0 +1,532 @@
from sympy.combinatorics.permutations import Permutation, _af_invert, _af_rmul
from sympy.ntheory import isprime
rmul = Permutation.rmul
_af_new = Permutation._af_new
############################################
#
# Utilities for computational group theory
#
############################################
def _base_ordering(base, degree):
r"""
Order `\{0, 1, \dots, n-1\}` so that base points come first and in order.
Parameters
==========
base : the base
degree : the degree of the associated permutation group
Returns
=======
A list ``base_ordering`` such that ``base_ordering[point]`` is the
number of ``point`` in the ordering.
Examples
========
>>> from sympy.combinatorics import SymmetricGroup
>>> from sympy.combinatorics.util import _base_ordering
>>> S = SymmetricGroup(4)
>>> S.schreier_sims()
>>> _base_ordering(S.base, S.degree)
[0, 1, 2, 3]
Notes
=====
This is used in backtrack searches, when we define a relation `\ll` on
the underlying set for a permutation group of degree `n`,
`\{0, 1, \dots, n-1\}`, so that if `(b_1, b_2, \dots, b_k)` is a base we
have `b_i \ll b_j` whenever `i<j` and `b_i \ll a` for all
`i\in\{1,2, \dots, k\}` and `a` is not in the base. The idea is developed
and applied to backtracking algorithms in [1], pp.108-132. The points
that are not in the base are taken in increasing order.
References
==========
.. [1] Holt, D., Eick, B., O'Brien, E.
"Handbook of computational group theory"
"""
base_len = len(base)
ordering = [0]*degree
for i in range(base_len):
ordering[base[i]] = i
current = base_len
for i in range(degree):
if i not in base:
ordering[i] = current
current += 1
return ordering
def _check_cycles_alt_sym(perm):
"""
Checks for cycles of prime length p with n/2 < p < n-2.
Explanation
===========
Here `n` is the degree of the permutation. This is a helper function for
the function is_alt_sym from sympy.combinatorics.perm_groups.
Examples
========
>>> from sympy.combinatorics.util import _check_cycles_alt_sym
>>> from sympy.combinatorics import Permutation
>>> a = Permutation([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [11, 12]])
>>> _check_cycles_alt_sym(a)
False
>>> b = Permutation([[0, 1, 2, 3, 4, 5, 6], [7, 8, 9, 10]])
>>> _check_cycles_alt_sym(b)
True
See Also
========
sympy.combinatorics.perm_groups.PermutationGroup.is_alt_sym
"""
n = perm.size
af = perm.array_form
current_len = 0
total_len = 0
used = set()
for i in range(n//2):
if i not in used and i < n//2 - total_len:
current_len = 1
used.add(i)
j = i
while af[j] != i:
current_len += 1
j = af[j]
used.add(j)
total_len += current_len
if current_len > n//2 and current_len < n - 2 and isprime(current_len):
return True
return False
def _distribute_gens_by_base(base, gens):
r"""
Distribute the group elements ``gens`` by membership in basic stabilizers.
Explanation
===========
Notice that for a base `(b_1, b_2, \dots, b_k)`, the basic stabilizers
are defined as `G^{(i)} = G_{b_1, \dots, b_{i-1}}` for
`i \in\{1, 2, \dots, k\}`.
Parameters
==========
base : a sequence of points in `\{0, 1, \dots, n-1\}`
gens : a list of elements of a permutation group of degree `n`.
Returns
=======
list
List of length `k`, where `k` is the length of *base*. The `i`-th entry
contains those elements in *gens* which fix the first `i` elements of
*base* (so that the `0`-th entry is equal to *gens* itself). If no
element fixes the first `i` elements of *base*, the `i`-th element is
set to a list containing the identity element.
Examples
========
>>> from sympy.combinatorics.named_groups import DihedralGroup
>>> from sympy.combinatorics.util import _distribute_gens_by_base
>>> D = DihedralGroup(3)
>>> D.schreier_sims()
>>> D.strong_gens
[(0 1 2), (0 2), (1 2)]
>>> D.base
[0, 1]
>>> _distribute_gens_by_base(D.base, D.strong_gens)
[[(0 1 2), (0 2), (1 2)],
[(1 2)]]
See Also
========
_strong_gens_from_distr, _orbits_transversals_from_bsgs,
_handle_precomputed_bsgs
"""
base_len = len(base)
degree = gens[0].size
stabs = [[] for _ in range(base_len)]
max_stab_index = 0
for gen in gens:
j = 0
while j < base_len - 1 and gen._array_form[base[j]] == base[j]:
j += 1
if j > max_stab_index:
max_stab_index = j
for k in range(j + 1):
stabs[k].append(gen)
for i in range(max_stab_index + 1, base_len):
stabs[i].append(_af_new(list(range(degree))))
return stabs
def _handle_precomputed_bsgs(base, strong_gens, transversals=None,
basic_orbits=None, strong_gens_distr=None):
"""
Calculate BSGS-related structures from those present.
Explanation
===========
The base and strong generating set must be provided; if any of the
transversals, basic orbits or distributed strong generators are not
provided, they will be calculated from the base and strong generating set.
Parameters
==========
base : the base
strong_gens : the strong generators
transversals : basic transversals
basic_orbits : basic orbits
strong_gens_distr : strong generators distributed by membership in basic stabilizers
Returns
=======
(transversals, basic_orbits, strong_gens_distr)
where *transversals* are the basic transversals, *basic_orbits* are the
basic orbits, and *strong_gens_distr* are the strong generators distributed
by membership in basic stabilizers.
Examples
========
>>> from sympy.combinatorics.named_groups import DihedralGroup
>>> from sympy.combinatorics.util import _handle_precomputed_bsgs
>>> D = DihedralGroup(3)
>>> D.schreier_sims()
>>> _handle_precomputed_bsgs(D.base, D.strong_gens,
... basic_orbits=D.basic_orbits)
([{0: (2), 1: (0 1 2), 2: (0 2)}, {1: (2), 2: (1 2)}], [[0, 1, 2], [1, 2]], [[(0 1 2), (0 2), (1 2)], [(1 2)]])
See Also
========
_orbits_transversals_from_bsgs, _distribute_gens_by_base
"""
if strong_gens_distr is None:
strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
if transversals is None:
if basic_orbits is None:
basic_orbits, transversals = \
_orbits_transversals_from_bsgs(base, strong_gens_distr)
else:
transversals = \
_orbits_transversals_from_bsgs(base, strong_gens_distr,
transversals_only=True)
else:
if basic_orbits is None:
base_len = len(base)
basic_orbits = [None]*base_len
for i in range(base_len):
basic_orbits[i] = list(transversals[i].keys())
return transversals, basic_orbits, strong_gens_distr
def _orbits_transversals_from_bsgs(base, strong_gens_distr,
transversals_only=False, slp=False):
"""
Compute basic orbits and transversals from a base and strong generating set.
Explanation
===========
The generators are provided as distributed across the basic stabilizers.
If the optional argument ``transversals_only`` is set to True, only the
transversals are returned.
Parameters
==========
base : The base.
strong_gens_distr : Strong generators distributed by membership in basic stabilizers.
transversals_only : bool, default: False
A flag switching between returning only the
transversals and both orbits and transversals.
slp : bool, default: False
If ``True``, return a list of dictionaries containing the
generator presentations of the elements of the transversals,
i.e. the list of indices of generators from ``strong_gens_distr[i]``
such that their product is the relevant transversal element.
Examples
========
>>> from sympy.combinatorics import SymmetricGroup
>>> from sympy.combinatorics.util import _distribute_gens_by_base
>>> S = SymmetricGroup(3)
>>> S.schreier_sims()
>>> strong_gens_distr = _distribute_gens_by_base(S.base, S.strong_gens)
>>> (S.base, strong_gens_distr)
([0, 1], [[(0 1 2), (2)(0 1), (1 2)], [(1 2)]])
See Also
========
_distribute_gens_by_base, _handle_precomputed_bsgs
"""
from sympy.combinatorics.perm_groups import _orbit_transversal
base_len = len(base)
degree = strong_gens_distr[0][0].size
transversals = [None]*base_len
slps = [None]*base_len
if transversals_only is False:
basic_orbits = [None]*base_len
for i in range(base_len):
transversals[i], slps[i] = _orbit_transversal(degree, strong_gens_distr[i],
base[i], pairs=True, slp=True)
transversals[i] = dict(transversals[i])
if transversals_only is False:
basic_orbits[i] = list(transversals[i].keys())
if transversals_only:
return transversals
else:
if not slp:
return basic_orbits, transversals
return basic_orbits, transversals, slps
def _remove_gens(base, strong_gens, basic_orbits=None, strong_gens_distr=None):
"""
Remove redundant generators from a strong generating set.
Parameters
==========
base : a base
strong_gens : a strong generating set relative to *base*
basic_orbits : basic orbits
strong_gens_distr : strong generators distributed by membership in basic stabilizers
Returns
=======
A strong generating set with respect to ``base`` which is a subset of
``strong_gens``.
Examples
========
>>> from sympy.combinatorics import SymmetricGroup
>>> from sympy.combinatorics.util import _remove_gens
>>> from sympy.combinatorics.testutil import _verify_bsgs
>>> S = SymmetricGroup(15)
>>> base, strong_gens = S.schreier_sims_incremental()
>>> new_gens = _remove_gens(base, strong_gens)
>>> len(new_gens)
14
>>> _verify_bsgs(S, base, new_gens)
True
Notes
=====
This procedure is outlined in [1],p.95.
References
==========
.. [1] Holt, D., Eick, B., O'Brien, E.
"Handbook of computational group theory"
"""
from sympy.combinatorics.perm_groups import _orbit
base_len = len(base)
degree = strong_gens[0].size
if strong_gens_distr is None:
strong_gens_distr = _distribute_gens_by_base(base, strong_gens)
if basic_orbits is None:
basic_orbits = []
for i in range(base_len):
basic_orbit = _orbit(degree, strong_gens_distr[i], base[i])
basic_orbits.append(basic_orbit)
strong_gens_distr.append([])
res = strong_gens[:]
for i in range(base_len - 1, -1, -1):
gens_copy = strong_gens_distr[i][:]
for gen in strong_gens_distr[i]:
if gen not in strong_gens_distr[i + 1]:
temp_gens = gens_copy[:]
temp_gens.remove(gen)
if temp_gens == []:
continue
temp_orbit = _orbit(degree, temp_gens, base[i])
if temp_orbit == basic_orbits[i]:
gens_copy.remove(gen)
res.remove(gen)
return res
def _strip(g, base, orbits, transversals):
"""
Attempt to decompose a permutation using a (possibly partial) BSGS
structure.
Explanation
===========
This is done by treating the sequence ``base`` as an actual base, and
the orbits ``orbits`` and transversals ``transversals`` as basic orbits and
transversals relative to it.
This process is called "sifting". A sift is unsuccessful when a certain
orbit element is not found or when after the sift the decomposition
does not end with the identity element.
The argument ``transversals`` is a list of dictionaries that provides
transversal elements for the orbits ``orbits``.
Parameters
==========
g : permutation to be decomposed
base : sequence of points
orbits : list
A list in which the ``i``-th entry is an orbit of ``base[i]``
under some subgroup of the pointwise stabilizer of `
`base[0], base[1], ..., base[i - 1]``. The groups themselves are implicit
in this function since the only information we need is encoded in the orbits
and transversals
transversals : list
A list of orbit transversals associated with the orbits *orbits*.
Examples
========
>>> from sympy.combinatorics import Permutation, SymmetricGroup
>>> from sympy.combinatorics.util import _strip
>>> S = SymmetricGroup(5)
>>> S.schreier_sims()
>>> g = Permutation([0, 2, 3, 1, 4])
>>> _strip(g, S.base, S.basic_orbits, S.basic_transversals)
((4), 5)
Notes
=====
The algorithm is described in [1],pp.89-90. The reason for returning
both the current state of the element being decomposed and the level
at which the sifting ends is that they provide important information for
the randomized version of the Schreier-Sims algorithm.
References
==========
.. [1] Holt, D., Eick, B., O'Brien, E."Handbook of computational group theory"
See Also
========
sympy.combinatorics.perm_groups.PermutationGroup.schreier_sims
sympy.combinatorics.perm_groups.PermutationGroup.schreier_sims_random
"""
h = g._array_form
base_len = len(base)
for i in range(base_len):
beta = h[base[i]]
if beta == base[i]:
continue
if beta not in orbits[i]:
return _af_new(h), i + 1
u = transversals[i][beta]._array_form
h = _af_rmul(_af_invert(u), h)
return _af_new(h), base_len + 1
def _strip_af(h, base, orbits, transversals, j, slp=[], slps={}):
"""
optimized _strip, with h, transversals and result in array form
if the stripped elements is the identity, it returns False, base_len + 1
j h[base[i]] == base[i] for i <= j
"""
base_len = len(base)
for i in range(j+1, base_len):
beta = h[base[i]]
if beta == base[i]:
continue
if beta not in orbits[i]:
if not slp:
return h, i + 1
return h, i + 1, slp
u = transversals[i][beta]
if h == u:
if not slp:
return False, base_len + 1
return False, base_len + 1, slp
h = _af_rmul(_af_invert(u), h)
if slp:
u_slp = slps[i][beta][:]
u_slp.reverse()
u_slp = [(i, (g,)) for g in u_slp]
slp = u_slp + slp
if not slp:
return h, base_len + 1
return h, base_len + 1, slp
def _strong_gens_from_distr(strong_gens_distr):
"""
Retrieve strong generating set from generators of basic stabilizers.
This is just the union of the generators of the first and second basic
stabilizers.
Parameters
==========
strong_gens_distr : strong generators distributed by membership in basic stabilizers
Examples
========
>>> from sympy.combinatorics import SymmetricGroup
>>> from sympy.combinatorics.util import (_strong_gens_from_distr,
... _distribute_gens_by_base)
>>> S = SymmetricGroup(3)
>>> S.schreier_sims()
>>> S.strong_gens
[(0 1 2), (2)(0 1), (1 2)]
>>> strong_gens_distr = _distribute_gens_by_base(S.base, S.strong_gens)
>>> _strong_gens_from_distr(strong_gens_distr)
[(0 1 2), (2)(0 1), (1 2)]
See Also
========
_distribute_gens_by_base
"""
if len(strong_gens_distr) == 1:
return strong_gens_distr[0][:]
else:
result = strong_gens_distr[0]
for gen in strong_gens_distr[1]:
if gen not in result:
result.append(gen)
return result