Skip to content

Commit 28c4dfe

Browse files
Added dynamic programming approach using chief series
Dynamic Programming Algorithm to compute `minimum_generating_set`
2 parents f374fb8 + e0bbd8e commit 28c4dfe

File tree

1 file changed

+173
-104
lines changed

1 file changed

+173
-104
lines changed

src/sage/groups/libgap_mixin.py

Lines changed: 173 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -969,131 +969,200 @@ def is_isomorphic(self, H):
969969
return self.gap().IsomorphismGroups(H.gap()) != libgap.fail
970970

971971

972-
def minimum_generating_set(G) -> list:
973-
"""
974-
Return a list of the minimum generating set of ``G``.
975-
972+
def minimum_generating_set(G)->list:
973+
r"""
976974
INPUT:
977975
978-
- ``G`` -- a group
976+
- ``G`` -- The group whose minimum generating set we want. This must be converted to a 'gap based' group first if it's not via ``G=G.gap()`` .
979977
980978
OUTPUT:
981979
982-
A list of GAP objects that generate the group.
980+
- minimum generating set of ``G``, which is the the set `g` of elements of `G` of smallest cardinality such that `G` is generated by `g`, i.e. `\braket{g}=G`
983981
984982
.. SEEALSO::
985983
986984
:meth:`sage.categories.groups.Groups.ParentMethods.minimum_generating_set`
987985
986+
988987
ALGORITHM:
989988
990-
We follow the algorithm described in the research paper "A New Algorithm for Finding the Minimum Generating Set of a Group" by John Doe (:doi:`10.1016/j.jalgebra.2023.11.012`).
989+
First we cover the cases when the Chief series is of length 1, that is if `G` is simple. This case is handled by ``libgap.MinimalGeneratingSet`` , so we assume that ``libgap.MinimalGeneratingSet`` doesn't work (it only works for solvable and some other kinds of groups).
990+
So, we are guaranteed to find a chief series of length at least 2, since `G` is not simple. Then we proceed as follows..
991991
992-
The algorithm checks two base cases:
992+
`S := ChiefSeries(G)`
993993
994-
1. If G is a cyclic group, it returns the cyclic generator.
995-
2. If G is a simple group, it returns a combination of two elements that generate G.
994+
`l := len(S)-1` (this is index of last normal subgroup, namely `G_l=\{e\}`)
996995
997-
If the above two cases fail, the algorithm finds the minimal normal subgroup N of G.
998-
It then finds the quotient group G/N and recursively finds the minimum generating set of G/N.
999-
Let S be the minimum generating set of G/N. The algorithm finds representatives g of S in G.
996+
Let `g` be the set of representatives of the minimum generating set of `G/S[1]` . (This can be found using ``libgap.MinimalGeneratingSet`` since `G/S[1]` is simple group)
1000997
1001-
If N is abelian, the algorithm checks if any case of the form g_1, g_2, ..., g_i*s_j, g_i+1, ..., g_lg
1002-
(where lg is the length of g) is able to generate G. If not, it uses the set g_1, g_2, ..., g_lg, s_j
1003-
as the minimum generating set of G.
998+
for k = 2 to 'l':
1004999
1005-
If N is non-abelian, the algorithm checks if any case of the form g_1*n_1, g_2*n_2, ..., g_lg*n_lg
1006-
is able to generate G, where n_1, n_2, ..., n_lg can be any elements of N. If not, it checks if any
1007-
case of the form g_1*n_1, g_2*n_2, ..., g_lg*n_lg, n_lg+1 is able to generate G.
1000+
Compute ``GbyGk`` := `G/S[k]`
10081001
1009-
The algorithm guarantees that one of the above cases will generate G.
1002+
Compute ``GbyGkm1`` := `G/S[k-1]`
10101003
1011-
TESTS:
1004+
Compute ``Gkm1byGk`` := `S[k-1]/S[k]`
10121005
1013-
Test that the resultant list is able to generate the original group::
1006+
`g := lift(g,Gkm1byGk,Gkm1byGk,GbyGk)` . The lift function is discussed in the code.
10141007
1015-
sage: from sage.groups.libgap_mixin import minimum_generating_set
1016-
sage: p = libgap.eval("DirectProduct(AlternatingGroup(5),AlternatingGroup(5))")
1017-
sage: s = minimum_generating_set(p); s
1018-
[(1,2,3,4,5)(8,9,10), (3,4,5)(6,7,8)]
1019-
sage: set(p.AsList()) == set(libgap.GroupByGenerators(s).AsList())
1020-
True
1008+
return `g`
10211009
1022-
Test that elements of resultant list are GAP objects::
1010+
TESTS:
10231011
1024-
sage: from sage.groups.libgap_mixin import minimum_generating_set
1025-
sage: G = PermutationGroup([(1,2,3), (2,3), (4,5)])
1026-
sage: s = minimum_generating_set(G); s
1027-
[(2,3), (1,3,2)(4,5)]
1028-
sage: s[0].parent()
1029-
C library interface to GAP
1012+
1. `A_5^7`
1013+
```
1014+
sage: def A_5_to_n(n):
1015+
....: A5 = AlternatingGroup(5).gap()
1016+
....: G = A5
1017+
....: for i in range(n-1):
1018+
....: G = G.DirectProduct(A5)
1019+
....: return G
1020+
....:
1021+
sage: G = A_5_to_n(7)
1022+
sage: g = minimum_generating_set(G)
1023+
sage: g
1024+
[(1,5,4,3,2)(8,9,10)(12,14,13)(17,19,18)(22,24,23)(27,29,28)(32,34,33),
1025+
(2,4,3)(6,8,10,7,9)(11,14,15,13,12)(16,19)(18,20)(21,25,24,22,23)(26,30,29,28,27)(31,35,34)]
1026+
sage: %timeit g = minimum_generating_set(G)
1027+
900 ms ± 90.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
1028+
```
1029+
2. `Z_3^10`
1030+
```
1031+
sage: def Z_2_to_n(n):
1032+
....: Z_2 = PermutationGroup([(1,2)]).gap()
1033+
....: G = Z_2
1034+
....: for i in range(1,n):
1035+
....: G = G.DirectProduct(Z_2)
1036+
....: return G
1037+
....:
1038+
sage: G = Z_2_to_n(10)
1039+
sage: g = minimum_generating_set(G)
1040+
sage: len(g)
1041+
10
1042+
sage: g
1043+
[(19,20),
1044+
(17,18),
1045+
(15,16),
1046+
(13,14),
1047+
(11,12),
1048+
(9,10),
1049+
(7,8),
1050+
(5,6),
1051+
(3,4),
1052+
(1,2)]
1053+
```
10301054
"""
1031-
if not isinstance(G, GapElement):
1032-
try:
1033-
G = G.gap()
1034-
except (AttributeError, ValueError, TypeError):
1035-
raise NotImplementedError("only implemented for groups that can construct a gap group")
1036-
1037-
if not G.IsFinite().sage():
1038-
raise NotImplementedError("only implemented for finite groups")
1039-
1040-
def is_group_by_gens(group, gens):
1041-
return set(group.AsList()) == set(libgap.GroupByGenerators(gens).AsList())
1042-
1043-
group_elements = G.AsList()
1044-
1045-
if G.IsCyclic().sage():
1046-
for ele in group_elements:
1047-
if is_group_by_gens(G, [ele]):
1048-
return [ele]
1049-
1050-
if G.IsSimple().sage():
1051-
n = len(group_elements)
1052-
for i in range(n):
1053-
for j in range(i+1, n):
1054-
if is_group_by_gens(G, [group_elements[i], group_elements[j]]):
1055-
return [group_elements[i], group_elements[j]]
1056-
1057-
N = G.MinimalNormalSubgroups()[0]
1058-
S = N.SmallGeneratingSet()
1059-
1060-
phi = G.NaturalHomomorphismByNormalSubgroup(N)
1061-
GbyN = phi.ImagesSource()
1062-
GbyN_mingenset = minimum_generating_set(GbyN)
1063-
1064-
g = [phi.PreImagesRepresentative(g) for g in GbyN_mingenset]
1065-
lg = len(g)
1066-
1067-
if N.IsAbelian().sage():
1068-
if is_group_by_gens(G, g):
1069-
return g
1070-
1071-
for i in range(lg):
1072-
for j in range(len(S)):
1073-
temp = g[i]
1074-
g[i] *= S[j]
1075-
if is_group_by_gens(G, g):
1076-
return g
1077-
g[i] = temp
1078-
1079-
return g + [S[0]]
1080-
1081-
# A function to generate some combinations of the generators
1082-
# of the group according to the algorithm. Here it is considered that
1083-
# the first element of the group N is the identity element.
1084-
def gen_combinations(g, N, lg):
1085-
for iter in product(N, repeat=lg):
1086-
yield [g[i] * iter[lg-i-1] for i in range(lg)]
1087-
1088-
N_list = list(N.AsList())
1089-
1090-
for raw_gens in gen_combinations(g, N_list, lg):
1091-
if is_group_by_gens(G, raw_gens):
1092-
return raw_gens
1093-
1094-
for raw_gens in gen_combinations(g, N_list, lg):
1095-
for nl in N_list[1:]:
1096-
if is_group_by_gens(G, raw_gens+[nl]):
1097-
return raw_gens + [nl]
1098-
1099-
assert False, "The code should never reach here"
1055+
assert isinstance(G, GapElement)
1056+
if not G.IsFinite().sage(): raise NotImplementedError("only implemented for finite groups")
1057+
try:return list(libgap.MinimalGeneratingSet(G))
1058+
except:pass
1059+
def lift(G_by_Gim1_mingen_reps , Gim1_by_Gi , G_by_Gi , phi_G_by_Gi , phi_Gim1_by_Gi):
1060+
r"""
1061+
Note that G_k is the same as S[k] in the ``minimum_generating_set`` algorithm and cs[k] in its code.
1062+
1063+
INPUT:
1064+
1065+
- ``G_by_Gim1_mingen_reps`` -- representative elements of the minimum generating set of `G/G_{i-1}`
1066+
1067+
- ``G_by_Gim1`` -- `G / G_{i-1}` Quotient Group
1068+
1069+
- ``Gim1_by_Gi`` -- `G_{i-1} / G_{i}` Quotinet Group
1070+
1071+
- ``phi_G_by_Gi`` -- the homomorphism defining the cosets of `G_i` in `G`.
1072+
1073+
- ``phi_Gim1_by_Gi`` -- the homomorphism defining the cosets of `G_{i}` in `G_{i-1}`.
1074+
1075+
OUTPUT:
1076+
1077+
representative elements of the minimum generating set of `G / G_{i}`.
1078+
1079+
ALGORITHM:
1080+
1081+
The inputs:
1082+
1083+
`g = \{g_1,g_2,\dots g_l\}` is the set of representatives of the (supposed) minimum generating st of `G/G_{i-1}`, what we are calling ``G_by_Gim1_mingen_reps`` in the co
1084+
1085+
`\bold{n} =\{n_1,n_2\dots n_k\}` where `\{n_1 G_i,n_2G_i \dots n_kG_{i}\}` is any generating set of `G_{i-1}/G_i
1086+
1087+
`\bold{N} = \{N_1,N_2\dots N_m\}` where `G_{i-1}/G_i = \{N_1G_i,N_2G_2\dots N_m G_m\}` .
1088+
1089+
We wish to find the representatives of minimum generating set of `G/G_i` , which can be done as follows:
1090+
1091+
if `G_{i-1}/G_i` is abelian :
1092+
1093+
if `\braket{gG_i}= G/G_i` :
1094+
1095+
return `g`
1096+
1097+
for `1 \le p \le l` and `n_j \in \bold{n}` :
1098+
1099+
`g^* = \{g_1,g_2\dots g_{p-1} ,g_p n_j,g_{p_1},\dots\}`
1100+
1101+
if `\braket{g^* G_i} = G/G_i` :
1102+
1103+
return `g^*`
1104+
1105+
else:
1106+
1107+
for any (not necessarily distinct) elements `N_{i_1},N_{i_2}\dots N_{i_t} \in \bold{N}` :
1108+
1109+
`g^* = \{g_1N_{i_1},g_{i_2}N_{i_3}\dots g_{i_t}N_t,g_{t+1}\dots g_l\}`
1110+
1111+
if `\braket{g^*G_i}\; = G/G_i`:
1112+
1113+
return `{g^*}`
1114+
1115+
for any (not necessarily distinct) elements `N_{i_1},N_{i_2}\dots N_{i_t} N_{i_{t+1}} \in \bold{N}` :
1116+
1117+
`g^* = \{g_1N_{i_1},g_{i_2}N_{i_3}\dots g_{i_t}N_t,g_{t+1}\dots g_l\}`
1118+
1119+
if `\braket{g^*G_i}\; = G/G_i`:
1120+
1121+
return `{g^*}`
1122+
1123+
By now, we must have exhausted our search.
1124+
1125+
"""
1126+
def gen_combinations(g,N,l):
1127+
if l==0:
1128+
yield g
1129+
return
1130+
for gm in gen_combinations(g,N,l-1):
1131+
for n in N:
1132+
old = gm[l-1]
1133+
gm[l-1] = old * n
1134+
yield gm
1135+
gm[l-1] = old
1136+
l = len(G_by_Gim1_mingen_reps)
1137+
Gim1_by_Gi_L = list(Gim1_by_Gi.AsList())
1138+
Gim1_by_Gi_elem_reps = [phi_Gim1_by_Gi.PreImagesRepresentative(x) for x in Gim1_by_Gi_L]
1139+
Gim1_by_Gi_gen = list(libgap.SmallGeneratingSet(Gim1_by_Gi))
1140+
Gim1_by_Gi_gen_reps = [phi_Gim1_by_Gi.PreImagesRepresentative(x) for x in Gim1_by_Gi_gen]
1141+
if Gim1_by_Gi.IsAbelian().sage():
1142+
if (G_by_Gi == libgap.GroupByGenerators([phi_G_by_Gi.ImagesRepresentative(x) for x in G_by_Gim1_mingen_reps])): return G_by_Gim1_mingen_reps
1143+
for i in range(l):
1144+
for j in range(len(Gim1_by_Gi_gen_reps)):
1145+
temp = G_by_Gim1_mingen_reps[i]
1146+
G_by_Gim1_mingen_reps[i] = G_by_Gim1_mingen_reps[i]*Gim1_by_Gi_gen_reps[j]
1147+
if (G_by_Gi == libgap.GroupByGenerators([phi_G_by_Gi.ImagesRepresentative(x) for x in G_by_Gim1_mingen_reps])): return G_by_Gim1_mingen_reps
1148+
G_by_Gim1_mingen_reps[i] = temp
1149+
return G_by_Gim1_mingen_reps + [Gim1_by_Gi_gen_reps[0]]
1150+
for raw_gens in gen_combinations(G_by_Gim1_mingen_reps, Gim1_by_Gi_elem_reps, l):
1151+
if (G_by_Gi == libgap.GroupByGenerators([phi_G_by_Gi.ImagesRepresentative(x) for x in raw_gens])): return raw_gens
1152+
for raw_gens in gen_combinations(G_by_Gim1_mingen_reps+[Gim1_by_Gi_elem_reps[0]], Gim1_by_Gi_elem_reps, l+1):
1153+
if (G_by_Gi == libgap.GroupByGenerators([phi_G_by_Gi.ImagesRepresentative(x) for x in raw_gens])): return raw_gens
1154+
cs = G.ChiefSeries()
1155+
l = len(cs)-1
1156+
phi_GbyG1 = G.NaturalHomomorphismByNormalSubgroup(cs[1])
1157+
GbyG1 = phi_GbyG1.ImagesSource()
1158+
mingenset_k_reps = [phi_GbyG1.PreImagesRepresentative(x) for x in list(libgap.SmallGeneratingSet(GbyG1))] # k=1 initially
1159+
for k in range(2,l+1):
1160+
mingenset_km1_reps = mingenset_k_reps
1161+
Gk,Gkm1 = cs[k], cs[k-1]
1162+
phi_GbyGk = G.NaturalHomomorphismByNormalSubgroup(Gk)
1163+
GbyGk = phi_GbyGk.ImagesSource()
1164+
phi_Gkm1byGk = Gkm1.NaturalHomomorphismByNormalSubgroup(Gk)
1165+
Gkm1byGk = phi_Gkm1byGk.ImagesSource()
1166+
mingenset_k_reps = lift(mingenset_km1_reps,Gkm1byGk,GbyGk,phi_GbyGk,phi_Gkm1byGk)
1167+
assert (G == libgap.GroupByGenerators(mingenset_k_reps))
1168+
return mingenset_k_reps

0 commit comments

Comments
 (0)