From bfcfee360af72c550017394f090adf027ce318cd Mon Sep 17 00:00:00 2001 From: Simon Brandhorst Date: Sun, 28 Jul 2024 12:10:11 +0200 Subject: [PATCH] Add `smaller_degree_permutation_representation`, lazy double cosets (#3899) Co-authored-by: ThomasBreuer Co-authored-by: Max Horn --- docs/src/Groups/permgroup.md | 1 + src/GAP/wrappers.jl | 8 +- src/Groups/cosets.jl | 120 +++++++++++++++++----------- src/Groups/perm.jl | 22 +++++ src/exports.jl | 1 + test/Groups/Permutations.jl | 7 ++ test/Groups/gsets.jl | 6 +- test/Groups/subgroups_and_cosets.jl | 26 +++++- 8 files changed, 137 insertions(+), 54 deletions(-) diff --git a/docs/src/Groups/permgroup.md b/docs/src/Groups/permgroup.md index 9dab21dd8702..a219870b6ea2 100644 --- a/docs/src/Groups/permgroup.md +++ b/docs/src/Groups/permgroup.md @@ -39,6 +39,7 @@ In OSCAR, every permutation group has a degree `n`, that corresponds to the size ```@docs degree(x::PermGroup) +smaller_degree_permutation_representation(G::PermGroup) ``` diff --git a/src/GAP/wrappers.jl b/src/GAP/wrappers.jl index 3591823434c2..f4e3093013f2 100644 --- a/src/GAP/wrappers.jl +++ b/src/GAP/wrappers.jl @@ -78,6 +78,8 @@ GAP.@wrap DescriptionOfRootOfUnity(x::Any)::GapObj GAP.@wrap DeterminantOfCharacter(x::GapObj)::GapObj GAP.@wrap Dimension(x::GapObj)::Int GAP.@wrap DimensionOfHighestWeightModule(x::GapObj, y::GapObj)::GapInt +GAP.@wrap DoubleCoset(x::GapObj, y::GapObj, z::GapObj)::GapObj +GAP.@wrap DoubleCosetRepsAndSizes(x::GapObj, y::GapObj, z::GapObj)::GapObj GAP.@wrap DominantCharacter(x::GapObj, y::GapObj)::GapObj GAP.@wrap E(x::Any)::GapInt GAP.@wrap EigenvaluesChar(x::GapObj, y::GAP.Obj)::GapObj @@ -138,9 +140,10 @@ GAP.@wrap Indicator(x::GapObj, y::GapObj, z::GapInt)::GapObj GAP.@wrap InducedClassFunction(x::GapObj, y::GapObj)::GapObj GAP.@wrap InducedClassFunctionsByFusionMap(x::GapObj, y::GapObj, z::GapObj, t::GapObj)::GapObj GAP.@wrap InducedCyclic(x::GapObj)::GapObj -GAP.@wrap InitFusion(x::GapObj, y::GapObj)::GapObj GAP.@wrap InducedCyclic(x::GapObj, y::GapObj)::GapObj +GAP.@wrap InitFusion(x::GapObj, y::GapObj)::GapObj GAP.@wrap INT_FFE_DEFAULT(x::Any)::GapInt +GAP.@wrap Intersection(x::GapObj)::GapObj GAP.@wrap IntFFE(x::Any)::GapInt GAP.@wrap Inverse(x::GapObj)::GapObj GAP.@wrap Irr(x::GapObj)::GapObj @@ -312,6 +315,7 @@ GAP.@wrap PrimePGroup(x::GapObj)::GapInt GAP.@wrap PrimitiveElement(x::GapObj)::GapObj GAP.@wrap Projection(x::GapObj)::GapObj GAP.@wrap Projection(x::GapObj, i::Int)::GapObj +GAP.@wrap Random(x::GapObj, y::GapObj)::GAP.Obj GAP.@wrap Range(x::GapObj)::GapObj GAP.@wrap RecognizeGroup(x::GapObj)::GapObj GAP.@wrap ReduceCoeffs(x::GapObj, y::GapObj) @@ -320,6 +324,8 @@ GAP.@wrap Representative(x::GapObj)::GAP.Obj GAP.@wrap RepresentativeTom(x::GapObj, y::Int)::GapObj GAP.@wrap RepresentativeAction(x::GapObj, y::GapObj, z::GapObj)::GapObj GAP.@wrap RestrictedMapping(x::GapObj, y::GapObj)::GapObj +GAP.@wrap RightCoset(x::GapObj, y::GapObj)::GapObj +GAP.@wrap RightTransversal(x::GapObj, y::GapObj)::GapObj GAP.@wrap RootSystem(x::GapObj)::GapObj GAP.@wrap SLPforElement(x::GapObj, y::GapObj)::GAP.Obj GAP.@wrap ScalarProduct(x::GapObj, y::GapObj, z::GapObj)::GAP.Obj diff --git a/src/Groups/cosets.jl b/src/Groups/cosets.jl index 048b5418071d..d7942e023641 100644 --- a/src/Groups/cosets.jl +++ b/src/Groups/cosets.jl @@ -24,7 +24,7 @@ function _group_coset(G::GAPGroup, H::GAPGroup, repr::GAPGroupElem, side::Symbol end function ==(x::GroupCoset, y::GroupCoset) - return x.X == y.X && x.side == y.side + return GapObj(x) == GapObj(y) && x.side == y.side end function Base.show(io::IO, ::MIME"text/plain", x::GroupCoset) @@ -32,7 +32,7 @@ function Base.show(io::IO, ::MIME"text/plain", x::GroupCoset) io = pretty(io) println(io, "$side coset of ", Lowercase(), x.H) print(io, Indent()) - println(io, "with representative ", x.repr) + println(io, "with representative ", representative(x)) print(io, "in ", Lowercase(), x.G) print(io, Dedent()) end @@ -44,7 +44,7 @@ function Base.show(io::IO, x::GroupCoset) else print(io, "$side coset of ") io = pretty(io) - print(terse(io), Lowercase(), x.H, " with representative ", x.repr) + print(terse(io), Lowercase(), x.H, " with representative ", representative(x)) end end @@ -73,8 +73,8 @@ Right coset of Sym(3) ``` """ function right_coset(H::GAPGroup, g::GAPGroupElem) - @req GAPWrap.IsSubset(parent(g).X, H.X) "H is not a subgroup of parent(g)" - return _group_coset(parent(g), H, g, :right, GAP.Globals.RightCoset(H.X,g.X)) + @req GAPWrap.IsSubset(GapObj(parent(g)), GapObj(H)) "H is not a subgroup of parent(g)" + return _group_coset(parent(g), H, g, :right, GAPWrap.RightCoset(GapObj(H), GapObj(g))) end """ @@ -101,8 +101,8 @@ Left coset of Sym(3) ``` """ function left_coset(H::GAPGroup, g::GAPGroupElem) - @req GAPWrap.IsSubset(parent(g).X, H.X) "H is not a subgroup of parent(g)" - return _group_coset(parent(g), H, g, :left, GAP.Globals.RightCoset(GAP.Globals.ConjugateSubgroup(H.X,GAP.Globals.Inverse(g.X)),g.X)) + @req GAPWrap.IsSubset(GapObj(parent(g)), GapObj(H)) "H is not a subgroup of parent(g)" + return _group_coset(parent(g), H, g, :left, GAPWrap.RightCoset(GAPWrap.ConjugateSubgroup(GapObj(H), GAPWrap.Inverse(GapObj(g))), GapObj(g))) end @@ -126,24 +126,24 @@ Base.:*(g::GAPGroupElem, H::GAPGroup) = left_coset(H,g) function Base.:*(c::GroupCoset, y::GAPGroupElem) @assert y in c.G "element not in the group" if c.side == :right - return right_coset(c.H, c.repr*y) + return right_coset(c.H, representative(c)*y) else - return left_coset(c.H^y, c.repr*y) + return left_coset(c.H^y, representative(c)*y) end end function Base.:*(y::GAPGroupElem, c::GroupCoset) @assert y in c.G "element not in the group" if c.side == :left - return left_coset(c.H, y*c.repr) + return left_coset(c.H, y*representative(c)) else - return right_coset(c.H^(y^-1), y*c.repr) + return right_coset(c.H^(y^-1), y*representative(c)) end end function Base.:*(c::GroupCoset, d::GroupCoset) @req (c.side == :right && d.side == :left) "Wrong input" - return double_coset(c.H, c.repr*d.repr, d.H) + return double_coset(c.H, representative(c)*representative(d), d.H) end """ @@ -238,7 +238,7 @@ julia> is_bicoset(fH) true ``` """ -is_bicoset(C::GroupCoset) = GAPWrap.IsBiCoset(C.X) +is_bicoset(C::GroupCoset) = GAPWrap.IsBiCoset(GapObj(C)) """ right_cosets(G::GAPGroup, H::GAPGroup; check::Bool=true) @@ -317,6 +317,8 @@ struct SubgroupTransversal{T<: GAPGroup, S<: GAPGroup, E<: GAPGroupElem} <: Abst X::GapObj # underlying *right* transversal in GAP end +GAP.julia_to_gap(T::SubgroupTransversal) = T.X + function Base.show(io::IO, ::MIME"text/plain", x::SubgroupTransversal) side = x.side === :left ? "Left" : "Right" println(io, "$side transversal of length $(length(x)) of") @@ -343,7 +345,7 @@ Base.hash(x::SubgroupTransversal, h::UInt) = h # FIXME Base.length(T::SubgroupTransversal) = index(Int, T.G, T.H) function Base.getindex(T::SubgroupTransversal, i::Int) - res = group_element(T.G, T.X[i]) + res = group_element(T.G, GapObj(T)[i]) if T.side === :left res = inv(res) end @@ -397,7 +399,7 @@ function right_transversal(G::T1, H::T2; check::Bool=true) where T1 <: GAPGroup _check_compatible(G, H) end return SubgroupTransversal{T1, T2, eltype(T1)}(G, H, :right, - GAP.Globals.RightTransversal(G.X, H.X)) + GAPWrap.RightTransversal(GapObj(G), GapObj(H))) end """ @@ -437,11 +439,11 @@ function left_transversal(G::T1, H::T2; check::Bool=true) where T1 <: GAPGroup w _check_compatible(G, H) end return SubgroupTransversal{T1, T2, eltype(T1)}(G, H, :left, - GAP.Globals.RightTransversal(G.X, H.X)) + GAPWrap.RightTransversal(GapObj(G), GapObj(H))) end Base.IteratorSize(::Type{<:GroupCoset}) = Base.SizeUnknown() -Base.iterate(G::GroupCoset) = iterate(G, GAPWrap.Iterator(G.X)) +Base.iterate(G::GroupCoset) = iterate(G, GAPWrap.Iterator(GapObj(G))) function Base.iterate(G::GroupCoset, state) GAPWrap.IsDoneIterator(state) && return nothing @@ -463,16 +465,28 @@ struct GroupDoubleCoset{T <: GAPGroup, S <: GAPGroupElem} H::GAPGroup K::GAPGroup repr::S - X::GapObj + X::Ref{GapObj} + size::Ref{ZZRingElem} + + function GroupDoubleCoset(G::T, H::GAPGroup, K::GAPGroup, representative::S) where {T<: GAPGroup, S<:GAPGroupElem} + return new{T, S}(G, H, K, representative, Ref{GapObj}(), Ref{ZZRingElem}()) + end end -GAP.julia_to_gap(obj::GroupDoubleCoset) = obj.X +function GAP.julia_to_gap(C::GroupDoubleCoset) + if !isassigned(C.X) + C.X[] = GAPWrap.DoubleCoset(GapObj(C.H), GapObj(representative(C)), GapObj(C.K)) + end + return C.X[] +end Base.hash(x::GroupDoubleCoset, h::UInt) = h # FIXME Base.eltype(::Type{GroupDoubleCoset{T,S}}) where {T,S} = S function ==(x::GroupDoubleCoset, y::GroupDoubleCoset) - return x.X == y.X + # Avoid creating a GAP object if the result is obviously `false`. + isassigned(x.size) && isassigned(y.size) && order(x) != order(y) && return false + return GapObj(x) == GapObj(y) end function Base.show(io::IO, ::MIME"text/plain", x::GroupDoubleCoset) @@ -480,7 +494,7 @@ function Base.show(io::IO, ::MIME"text/plain", x::GroupDoubleCoset) println(io, "Double coset of ", Lowercase(), x.H) print(io, Indent()) println(io, "and ", Lowercase(), x.K) - println(io, "with representative ", x.repr) + println(io, "with representative ", representative(x)) print(io, "in ", Lowercase(), x.G) print(io, Dedent()) end @@ -492,7 +506,7 @@ function Base.show(io::IO, x::GroupDoubleCoset) print(io, "Double coset of ") io = pretty(io) print(terse(io), Lowercase(), x.H, - " and ", Lowercase(), x.K, " with representative ", x.repr) + " and ", Lowercase(), x.K, " with representative ", representative(x)) end end @@ -526,9 +540,9 @@ Double coset of Sym(3) """ function double_coset(G::GAPGroup, g::GAPGroupElem, H::GAPGroup) #T what if g is in some subgroup of a group of which G, H are also a subgroup? - @req GAPWrap.IsSubset(parent(g).X,G.X) "G is not a subgroup of parent(g)" - @req GAPWrap.IsSubset(parent(g).X,H.X) "H is not a subgroup of parent(g)" - return GroupDoubleCoset(parent(g),G,H,g,GAP.Globals.DoubleCoset(G.X,g.X,H.X)) + @req GAPWrap.IsSubset(GapObj(parent(g)), GapObj(G)) "G is not a subgroup of parent(g)" + @req GAPWrap.IsSubset(GapObj(parent(g)), GapObj(H)) "H is not a subgroup of parent(g)" + return GroupDoubleCoset(parent(g), G, H, g) end Base.:*(H::GAPGroup, g::GAPGroupElem, K::GAPGroup) = double_coset(H,g,K) @@ -558,31 +572,42 @@ julia> double_cosets(G,H,K) ``` """ function double_cosets(G::T, H::GAPGroup, K::GAPGroup; check::Bool=true) where T <: GAPGroup - if !check - dcs = GAP.Globals.DoubleCosetsNC(G.X,H.X,K.X) - else + if check @assert is_subset(H, G) "H is not a subgroup of G" @assert is_subset(K, G) "K is not a subgroup of G" - dcs = GAP.Globals.DoubleCosets(G.X,H.X,K.X) end - res = Vector{GroupDoubleCoset{T,elem_type(T)}}(undef, length(dcs)) - for i = 1:length(res) - dc = dcs[i] - g = group_element(G, GAPWrap.Representative(dc)) - res[i] = GroupDoubleCoset(G,H,K,g,dc) + dcs = GAPWrap.DoubleCosetRepsAndSizes(GapObj(G), GapObj(H), GapObj(K)) + res = Vector{GroupDoubleCoset{T, elem_type(T)}}(undef, length(dcs)) + for i in 1:length(res) + g = group_element(G, dcs[i][1]) + C = GroupDoubleCoset(G, H, K, g) + n = dcs[i][2] + C.size[] = ZZRingElem(n) + res[i] = C end - return res - #return [GroupDoubleCoset(G,H,K,group_element(G.X,GAPWrap.Representative(dc)),dc) for dc in dcs] + return res end - """ - order(C::Union{GroupCoset,GroupDoubleCoset}) + order(::Type{T} = ZZRingElem, C::Union{GroupCoset,GroupDoubleCoset}) -Return the cardinality of the (double) coset `C`. +Return the cardinality of the (double) coset `C`, +as an instance of the type `T`. """ -order(C::Union{GroupCoset,GroupDoubleCoset}) = GAPWrap.Size(C.X) -Base.length(C::Union{GroupCoset,GroupDoubleCoset}) = GAPWrap.Size(C.X) +order(C::Union{GroupCoset,GroupDoubleCoset}) = order(ZZRingElem, C) + +function order(::Type{T}, C::GroupCoset) where T <: IntegerUnion + return T(GAPWrap.Size(GapObj(C))) +end + +function order(::Type{T}, C::GroupDoubleCoset) where T <: IntegerUnion + if !isassigned(C.size) + C.size[] = ZZRingElem(GAPWrap.Size(GapObj(C))) + end + return T(C.size[]) +end + +Base.length(C::Union{GroupCoset,GroupDoubleCoset}) = order(C) """ rand(rng::Random.AbstractRNG = Random.GLOBAL_RNG, C::Union{GroupCoset,GroupDoubleCoset}) @@ -593,7 +618,7 @@ using the random number generator `rng`. Base.rand(C::Union{GroupCoset,GroupDoubleCoset}) = Base.rand(Random.GLOBAL_RNG, C) function Base.rand(rng::Random.AbstractRNG, C::Union{GroupCoset,GroupDoubleCoset}) - s = GAP.Globals.Random(GAP.wrap_rng(rng), C.X) + s = GAPWrap.Random(GAP.wrap_rng(rng), GapObj(C)) return group_element(C.G, s) end @@ -620,7 +645,7 @@ right_acting_group(C::GroupDoubleCoset) = C.K Base.IteratorSize(::Type{<:GroupDoubleCoset}) = Base.SizeUnknown() -Base.iterate(G::GroupDoubleCoset) = iterate(G, GAPWrap.Iterator(G.X)) +Base.iterate(G::GroupDoubleCoset) = iterate(G, GAPWrap.Iterator(GapObj(G))) function Base.iterate(G::GroupDoubleCoset, state) GAPWrap.IsDoneIterator(state) && return nothing @@ -634,18 +659,19 @@ end Return a vector containing all elements belonging to all groups and cosets in `V`. """ -function intersect(V::AbstractVector{Union{<: GAPGroup, GroupCoset, GroupDoubleCoset}}) +function Base.intersect(V::AbstractVector{Union{<: GAPGroup, GroupCoset, GroupDoubleCoset}}) if V[1] isa GAPGroup G = V[1] else G = V[1].G end - l = GAP.Obj([v.X for v in V]) - ints = GAP.Globals.Intersection(l) - L = Vector{typeof(G)}(undef, length(ints)) + l = GAP.Obj(V; recursive = true) + ints = GAPWrap.Intersection(l) + L = Vector{eltype(G)}(undef, length(ints)) for i in 1:length(ints) L[i] = group_element(G,ints[i]) end return L end +#TODO: Can this method get called at all? diff --git a/src/Groups/perm.jl b/src/Groups/perm.jl index d98261c898b7..02ee42069df5 100644 --- a/src/Groups/perm.jl +++ b/src/Groups/perm.jl @@ -149,6 +149,28 @@ function perm(L::AbstractVector{<:IntegerUnion}) return PermGroupElem(symmetric_group(length(L)), GAPWrap.PermList(GapObj(L;recursive=true))) end +""" + smaller_degree_permutation_representation(G::PermGroup) -> PermGroup, map + +Return an isomorphic permutation group of smaller or equal degree +and the isomorphism from `G` to that group. + +# Examples +```jldoctest +julia> g = symmetric_group(4); + +julia> s, _ = sylow_subgroup(g, 3); + +julia> rho = smaller_degree_permutation_representation(s) +(Permutation group of degree 3 and order 3, Hom: s -> permutation group) +``` +""" +function smaller_degree_permutation_representation(G::PermGroup) + mp = GAP.Globals.SmallerDegreePermutationRepresentation(GapObj(G)) + img = PermGroup(GAP.Globals.Image(mp)) + return img, GAPGroupHomomorphism(G, img, mp) +end + @doc raw""" perm(G::PermGroup, L::AbstractVector{<:IntegerUnion}) diff --git a/src/exports.jl b/src/exports.jl index 47cc86cafc25..a5d4979ffc07 100644 --- a/src/exports.jl +++ b/src/exports.jl @@ -1395,6 +1395,7 @@ export slpoly_ring export small_generating_set, has_small_generating_set, set_small_generating_set export small_group export small_group_identification, has_small_group_identification +export smaller_degree_permutation_representation export snub_cube export snub_dodecahedron export socle, has_socle, set_socle diff --git a/test/Groups/Permutations.jl b/test/Groups/Permutations.jl index daf1fc1efdaa..d1d0b961a0fb 100644 --- a/test/Groups/Permutations.jl +++ b/test/Groups/Permutations.jl @@ -190,3 +190,10 @@ end @test cc == [[1, 2, 3], [4, 5], [6, 7], [8, 9, 10], [11, 12, 13, 14, 15]] end end + +@testset "smaller_degree_permuation_group" begin + c = cperm(1:3,4:6,7:9) + G,_ = sub(symmetric_group(9), [c]) + H,iso = smaller_degree_permutation_representation(G) + @test degree(H) true) == "G-set" + @test AbstractAlgebra.PrettyPrinting.repr_terse(Omega) == "G-set" @test isa(Omega, GSet) @test length(Omega) == 6 @test length(orbits(Omega)) == 1 @@ -321,7 +321,7 @@ end G = symmetric_group(5) H = sylow_subgroup(G, 2)[1] Omega = right_cosets(G, H) - @test repr(Omega, context = :supercompact => true) == "Right cosets of groups" + @test AbstractAlgebra.PrettyPrinting.repr_terse(Omega) == "Right cosets of groups" @test isa(Omega, GSet) @test acting_group(Omega) == G @test length(Omega) == index(G, H) @@ -382,7 +382,7 @@ end G = symmetric_group(5) H = sylow_subgroup(G, 2)[1] Omega = left_cosets(G, H) - @test repr(Omega, context = :supercompact => true) == "Left cosets of groups" + @test AbstractAlgebra.PrettyPrinting.repr_terse(Omega) == "Left cosets of groups" @test isa(Omega, GSet) @test acting_group(Omega) == G @test length(Omega) == index(G, H) diff --git a/test/Groups/subgroups_and_cosets.jl b/test/Groups/subgroups_and_cosets.jl index 35831c72e215..abf2fcea09b9 100644 --- a/test/Groups/subgroups_and_cosets.jl +++ b/test/Groups/subgroups_and_cosets.jl @@ -157,11 +157,13 @@ end C = right_coset(H, G[1]) @test is_right(C) @test order(C) == length(collect(C)) - @test repr(C, context = :supercompact => true) == "Right coset of a group" + @test order(C) isa ZZRingElem + @test order(Int, C) isa Int + @test AbstractAlgebra.PrettyPrinting.repr_terse(C) == "Right coset of a group" T = right_transversal(G, H) @test length(T) == index(G, H) - @test repr(T, context = :supercompact => true) == "Right transversal of groups" + @test AbstractAlgebra.PrettyPrinting.repr_terse(T) == "Right transversal of groups" @test_throws ArgumentError right_transversal(H, G) @test_throws ArgumentError left_transversal(H, G) @@ -217,7 +219,7 @@ end H = sub(G, [cperm(G,[1,2,3]), cperm(G,[2,3,4])])[1] L = right_cosets(G,H) - @test repr(L, context = :supercompact => true) == "Right cosets of groups" + @test AbstractAlgebra.PrettyPrinting.repr_terse(L) == "Right cosets of groups" @test_throws ArgumentError right_cosets(H, G) T = right_transversal(G,H) @test length(L)==10 @@ -269,6 +271,24 @@ end @test intersect(lc,H)==[] end +@testset "Double cosets" begin + G = symmetric_group(5) + x = G(cperm([1, 2, 3])) + y = G(cperm([1, 4, 5])) + z = G(cperm([1, 2], [3, 4])) + H = sub(G, [y])[1] + K = sub(G, [z])[1] + + dc = double_coset(H, x, K) + @test !isassigned(dc.X) + @test !isassigned(dc.size) + GapObj(dc) + @test isassigned(dc.X) + order(dc) + @test isassigned(dc.size) + @test GapObj([dc]; recursive = true) isa GapObj +end + @testset "Predicates for groups" begin @test !is_simple(alternating_group(4)) @test is_simple(alternating_group(5))