From dc7c2f3d926dab31f08ed87063d1cfcc80b95a79 Mon Sep 17 00:00:00 2001 From: Newtech66 Date: Mon, 15 Jul 2024 15:00:26 +0530 Subject: [PATCH 1/7] Initial commit --- src/sage/groups/perm_gps/permgroup.py | 68 +++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/src/sage/groups/perm_gps/permgroup.py b/src/sage/groups/perm_gps/permgroup.py index 2d265216519..962058f57d1 100644 --- a/src/sage/groups/perm_gps/permgroup.py +++ b/src/sage/groups/perm_gps/permgroup.py @@ -1581,6 +1581,74 @@ 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 `self.domain()` such that `self` + 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`. + + EXAMPLES:: + 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 + """ + 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) + 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 + final_partition = DisjointSet(self.domain()) + for part in P: + grp = [self._domain_from_gap[Integer(x)] + for i in part + for x in O[i]] + for i in range(1,len(grp)): + final_partition.union(grp[0],grp[i]) + return SetPartition(final_partition) + def representative_action(self, x, y): r""" Return an element of ``self`` that maps `x` to `y` if it exists. From e15b61bb6a191b48353996e977a37ea8076e8854 Mon Sep 17 00:00:00 2001 From: Newtech66 Date: Mon, 15 Jul 2024 17:41:38 +0530 Subject: [PATCH 2/7] Added citation and minor fixes --- src/doc/en/reference/references/index.rst | 5 +++ src/sage/groups/perm_gps/permgroup.py | 37 +++++++++++++---------- 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index b7725c7848b..93e142baf93 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -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. diff --git a/src/sage/groups/perm_gps/permgroup.py b/src/sage/groups/perm_gps/permgroup.py index 962058f57d1..1006bf7643b 100644 --- a/src/sage/groups/perm_gps/permgroup.py +++ b/src/sage/groups/perm_gps/permgroup.py @@ -1584,12 +1584,17 @@ def smallest_moved_point(self): @cached_method def disjoint_direct_product_decomposition(self): r""" - Returns the finest partition of `self.domain()` such that `self` - is isomorphic to the direct product of the projections of `self` + Returns the finest partition of the underlying set such that ``self`` + 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`. + of ``self``. + + The algorithm is from [CJ2022]_, which runs in time polynomial in + `n \cdot |G|`, where `n` is the degree of the group and `|G|` is + the size of a generating set, see Theorem 4.5. EXAMPLES:: + 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}} @@ -1598,7 +1603,7 @@ def disjoint_direct_product_decomposition(self): 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) + sage: T.is_isomorphic(H) True """ from sage.combinat.set_partition import SetPartition @@ -1613,40 +1618,40 @@ def disjoint_direct_product_decomposition(self): OrbitMapping = dict() for i in range(k): for x in O[i]: - OrbitMapping[x]=i - C = libgap.StabChain(H,libgap.Concatenation(O)) + 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]) + libgap.Append(R, O[i]) Xp = libgap.List([]) while True: try: - if libgap.IsSubset(O[i],C['orbit']): + 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) + 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) + 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) + libgap.Add(Xp, x) X = Xp final_partition = DisjointSet(self.domain()) for part in P: grp = [self._domain_from_gap[Integer(x)] for i in part for x in O[i]] - for i in range(1,len(grp)): - final_partition.union(grp[0],grp[i]) + for i in range(1, len(grp)): + final_partition.union(grp[0], grp[i]) return SetPartition(final_partition) def representative_action(self, x, y): From badcf18eb4feb45c7e97dee633cac6559446baba Mon Sep 17 00:00:00 2001 From: Newtech66 Date: Mon, 15 Jul 2024 18:03:26 +0530 Subject: [PATCH 3/7] Add finest partition check to doctest --- src/sage/groups/perm_gps/permgroup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sage/groups/perm_gps/permgroup.py b/src/sage/groups/perm_gps/permgroup.py index 1006bf7643b..9356e996cbb 100644 --- a/src/sage/groups/perm_gps/permgroup.py +++ b/src/sage/groups/perm_gps/permgroup.py @@ -1605,6 +1605,10 @@ def disjoint_direct_product_decomposition(self): 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}} """ from sage.combinat.set_partition import SetPartition from sage.sets.disjoint_set import DisjointSet From 6512c4615aedb9d0ecb6652e2e19e89392d043d0 Mon Sep 17 00:00:00 2001 From: Newtech66 Date: Mon, 15 Jul 2024 20:14:58 +0530 Subject: [PATCH 4/7] Add new example to doctest --- src/sage/groups/perm_gps/permgroup.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/sage/groups/perm_gps/permgroup.py b/src/sage/groups/perm_gps/permgroup.py index 9356e996cbb..cd9d08ec5de 100644 --- a/src/sage/groups/perm_gps/permgroup.py +++ b/src/sage/groups/perm_gps/permgroup.py @@ -1590,11 +1590,13 @@ def disjoint_direct_product_decomposition(self): of ``self``. The algorithm is from [CJ2022]_, which runs in time polynomial in - `n \cdot |G|`, where `n` is the degree of the group and `|G|` is + `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:: + 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}} @@ -1609,6 +1611,14 @@ def disjoint_direct_product_decomposition(self): {{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 subgroups:: + + sage: # optional -- internet + 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) + 0: A005226: Number of atomic species of degree n; also number of connected permutation groups of degree n. """ from sage.combinat.set_partition import SetPartition from sage.sets.disjoint_set import DisjointSet From 449efdc12e6482c50d76811dcb2ac3a7fff81a6f Mon Sep 17 00:00:00 2001 From: Newtech66 Date: Tue, 16 Jul 2024 14:36:22 +0530 Subject: [PATCH 5/7] Minor improvements --- src/sage/groups/perm_gps/permgroup.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/sage/groups/perm_gps/permgroup.py b/src/sage/groups/perm_gps/permgroup.py index cd9d08ec5de..7a1c4db04a0 100644 --- a/src/sage/groups/perm_gps/permgroup.py +++ b/src/sage/groups/perm_gps/permgroup.py @@ -1593,16 +1593,16 @@ def disjoint_direct_product_decomposition(self): `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:: + EXAMPLES: The example from the original paper:: 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 + 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 + 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 + 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) @@ -1614,10 +1614,9 @@ def disjoint_direct_product_decomposition(self): Counting the number of connected subgroups:: - sage: # optional -- internet 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) + sage: oeis(seq) # optional -- internet 0: A005226: Number of atomic species of degree n; also number of connected permutation groups of degree n. """ from sage.combinat.set_partition import SetPartition @@ -1659,14 +1658,11 @@ def disjoint_direct_product_decomposition(self): else: libgap.Add(Xp, x) X = Xp - final_partition = DisjointSet(self.domain()) - for part in P: - grp = [self._domain_from_gap[Integer(x)] - for i in part - for x in O[i]] - for i in range(1, len(grp)): - final_partition.union(grp[0], grp[i]) - return SetPartition(final_partition) + 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""" From 941cb98d50ad05cf5b570ccf679e421be12e30e9 Mon Sep 17 00:00:00 2001 From: Newtech66 Date: Tue, 16 Jul 2024 14:44:30 +0530 Subject: [PATCH 6/7] Minor fix --- src/sage/groups/perm_gps/permgroup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/sage/groups/perm_gps/permgroup.py b/src/sage/groups/perm_gps/permgroup.py index 7a1c4db04a0..87540379001 100644 --- a/src/sage/groups/perm_gps/permgroup.py +++ b/src/sage/groups/perm_gps/permgroup.py @@ -1612,9 +1612,9 @@ def disjoint_direct_product_decomposition(self): sage: PermutationGroup(PermutationGroup(gap_group=B).gens(),domain=list(S[0])).disjoint_direct_product_decomposition() {{1, 2, 3}} - Counting the number of connected subgroups:: + 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 + 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. From 2d8e920602cd80250987c363c971ebeaa041d53b Mon Sep 17 00:00:00 2001 From: Newtech66 Date: Wed, 17 Jul 2024 02:06:31 +0530 Subject: [PATCH 7/7] More minor fixes --- src/sage/groups/perm_gps/permgroup.py | 31 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/sage/groups/perm_gps/permgroup.py b/src/sage/groups/perm_gps/permgroup.py index 87540379001..d97aad93b03 100644 --- a/src/sage/groups/perm_gps/permgroup.py +++ b/src/sage/groups/perm_gps/permgroup.py @@ -1584,7 +1584,7 @@ def smallest_moved_point(self): @cached_method def disjoint_direct_product_decomposition(self): r""" - Returns the finest partition of the underlying set such that ``self`` + Return the finest partition of the underlying set such that ``self`` 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``. @@ -1612,6 +1612,13 @@ def disjoint_direct_product_decomposition(self): sage: PermutationGroup(PermutationGroup(gap_group=B).gens(),domain=list(S[0])).disjoint_direct_product_decomposition() {{1, 2, 3}} + An example with a different domain:: + + sage: PermutationGroup([[('a','c','d'),('b','e')]]).disjoint_direct_product_decomposition() + {{'a', 'c', 'd'}, {'b', 'e'}} + sage: PermutationGroup([[('a','c','d','b','e')]]).disjoint_direct_product_decomposition() + {{'a', 'b', 'c', 'd', 'e'}} + 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 @@ -1622,22 +1629,22 @@ def disjoint_direct_product_decomposition(self): 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) - k = len(O) + # sort each orbit and order list by smallest element of each orbit + O = libgap.List([libgap.ShallowCopy(orbit) for orbit in libgap.Orbits(H)]) + for orbit in O: + libgap.Sort(orbit) + O.Sort() + num_orbits = len(O) OrbitMapping = dict() - for i in range(k): + for i in range(num_orbits): for x in O[i]: OrbitMapping[x] = i C = libgap.StabChain(H, libgap.Concatenation(O)) X = libgap.StrongGeneratorsStabChain(C) - P = DisjointSet(k) + P = DisjointSet(num_orbits) R = libgap.List([]) identity = libgap.Identity(H) - for i in range(k-1): + for i in range(num_orbits-1): libgap.Append(R, O[i]) Xp = libgap.List([]) while True: @@ -1646,14 +1653,14 @@ def disjoint_direct_product_decomposition(self): C = C['stabilizer'] else: break - except ValueError: #this should catch a GAPError but I don't know how to make it work + except ValueError: 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: + cj = OrbitMapping[libgap.SmallestMovedPoint(libgap.RestrictedPerm(x, R))] P.union(i+1, cj) else: libgap.Add(Xp, x)