Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Disjoint direct product decomposition of a permutation group #38371

Merged
merged 7 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/doc/en/reference/references/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1658,6 +1658,11 @@ REFERENCES:
.. [CIA] CIA Factbook 09
https://www.cia.gov/library/publications/the-world-factbook/

.. [CJ2022] \M. Chang, C. Jefferson, *Disjoint direct product decomposition
of permutation groups*, Journal of Symbolic Computation (2022),
Volume 108, pages 1-16. :doi:`10.1016/j.jsc.2021.04.003`.
Preprint: :arxiv:`2004.11618v3`.

.. [CK1986] \R. Calderbank, W.M. Kantor,
*The geometry of two-weight codes*,
Bull. London Math. Soc. 18(1986) 97-122.
Expand Down
83 changes: 83 additions & 0 deletions src/sage/groups/perm_gps/permgroup.py
Original file line number Diff line number Diff line change
Expand Up @@ -1581,6 +1581,89 @@ def smallest_moved_point(self):
p = self._libgap_().SmallestMovedPoint()
return self._domain_from_gap[Integer(p)]

@cached_method
def disjoint_direct_product_decomposition(self):
r"""
Returns the finest partition of the underlying set such that ``self``
Newtech66 marked this conversation as resolved.
Show resolved Hide resolved
is isomorphic to the direct product of the projections of ``self``
onto each part of the partition. Each part is a union of orbits
of ``self``.

The algorithm is from [CJ2022]_, which runs in time polynomial in
`n \cdot |X|`, where `n` is the degree of the group and `|X|` is
the size of a generating set, see Theorem 4.5.

EXAMPLES:

The example from the original paper::

Newtech66 marked this conversation as resolved.
Show resolved Hide resolved
sage: H = PermutationGroup([[(1,2,3),(7,9,8),(10,12,11)],[(4,5,6),(7,8,9),(10,11,12)],[(5,6),(8,9),(11,12)],[(7,8,9),(10,11,12)]])
sage: S = H.disjoint_direct_product_decomposition(); S
{{1, 2, 3}, {4, 5, 6, 7, 8, 9, 10, 11, 12}}
sage: A = libgap.Stabilizer(H, list(S[0]), libgap.OnTuples); A
Group([ (7,8,9)(10,11,12), (5,6)(8,9)(11,12), (4,5,6)(7,8,9)(10,11,12) ])
sage: B = libgap.Stabilizer(H, list(S[1]), libgap.OnTuples); B
Group([ (1,2,3) ])
sage: T = PermutationGroup(gap_group=libgap.DirectProduct(A,B))
sage: T.is_isomorphic(H)
True
sage: PermutationGroup(PermutationGroup(gap_group=A).gens(),domain=list(S[1])).disjoint_direct_product_decomposition()
{{4, 5, 6, 7, 8, 9, 10, 11, 12}}
sage: PermutationGroup(PermutationGroup(gap_group=B).gens(),domain=list(S[0])).disjoint_direct_product_decomposition()
{{1, 2, 3}}

Counting the number of "connected" permutation groups of degree `n`::

sage: seq = [sum(1 for G in SymmetricGroup(n).conjugacy_classes_subgroups() if len(G.disjoint_direct_product_decomposition()) == 1) for n in range(1,8)]; seq
[1, 1, 2, 6, 6, 27, 20]
sage: oeis(seq) # optional -- internet
0: A005226: Number of atomic species of degree n; also number of connected permutation groups of degree n.
"""
Newtech66 marked this conversation as resolved.
Show resolved Hide resolved
from sage.combinat.set_partition import SetPartition
from sage.sets.disjoint_set import DisjointSet
H = self._libgap_()
if self.is_trivial():
return SetPartition(DisjointSet(self.domain()))
if libgap.NrMovedPoints(H) == self.degree() and libgap.IsTransitive(H):
return SetPartition([self.domain()])
O = libgap.Orbits(H)
Newtech66 marked this conversation as resolved.
Show resolved Hide resolved
k = len(O)
OrbitMapping = dict()
for i in range(k):
for x in O[i]:
OrbitMapping[x] = i
C = libgap.StabChain(H, libgap.Concatenation(O))
X = libgap.StrongGeneratorsStabChain(C)
P = DisjointSet(k)
R = libgap.List([])
identity = libgap.Identity(H)
for i in range(k-1):
libgap.Append(R, O[i])
Xp = libgap.List([])
while True:
try:
if libgap.IsSubset(O[i], C['orbit']):
C = C['stabilizer']
else:
break
except ValueError: #this should catch a GAPError but I don't know how to make it work
break
for x in X:
xs = libgap.SiftedPermutation(C, x)
if xs != identity:
cj = OrbitMapping[libgap.SmallestMovedPoint(libgap.RestrictedPerm(x, R))]
libgap.Add(Xp, xs)
if libgap.RestrictedPerm(xs, O[i+1]) != identity:
P.union(i+1, cj)
else:
libgap.Add(Xp, x)
X = Xp
return SetPartition([
[self._domain_from_gap[Integer(x)]
for i in part
for x in O[i]] for part in P] +
[[x] for x in self.fixed_points()])

def representative_action(self, x, y):
r"""
Return an element of ``self`` that maps `x` to `y` if it exists.
Expand Down
Loading