Skip to content

Commit

Permalink
serialization of more group attributes
Browse files Browse the repository at this point in the history
The idea is to implement serializations directly for the underlying GAP objects,
instead of implementing them for the Oscar objects.

For boolean and numeric attributes, the two alternatives do not really differ,
but for attributes whose values are themselves groups, for example `center(g)`,
we can simply serialize `GAP.Globals.Center(GapObj(g))`, together
with its attributes, instead of first creating `center(g)` anew
and then serializing its first component.

Another advantage is that this way, it will be easier to deal with
"Oscar attributes" of groups that do not rely on GAP's attribute mechanism.

(The serialization of permutation groups gets changed with this approach;
if we proceed like this then a fallback must be provided, in order to deal
with the deserialization of files containing permutation groups that were
serialized with the old method.)

Currently there is a problem with cyclic references, and the handling of
attribute values that are GAP lists is not yet supported.

Here is an example for the cyclic reference problem:
```
julia> g = small_group(4, 1);

julia> path = mktempdir();  filenamev = joinpath(path, "v");

julia> save(filenamev, g)

julia> Oscar.reset_global_serializer_state();

julia> loadedv = load(filenamev);

julia> has_center(g)
false

julia> center(g);

julia> save(filenamev, g)

julia> Oscar.reset_global_serializer_state()
Dict{Base.UUID, Any}()

julia> loadedv = load(filenamev)
Internal error: encountered unexpected error during compilation of showerror:
StackOverflowError()
```
  • Loading branch information
ThomasBreuer committed Oct 11, 2024
1 parent fd5741d commit 357fc3e
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 42 deletions.
145 changes: 143 additions & 2 deletions src/Serialization/GAP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,62 @@ function install_GAP_deserialization(filtsymbol::Symbol, meth::Function)
return
end

#############################################################################
#
# attributes handling
#
const GapObj_attributes = [
# boolean
:IsAbelian, :IsAlmostSimpleGroup, :IsCyclic, :IsElementaryAbelian,
:IsFinite, :IsNilpotentGroup, :IsPerfectGroup, :IsQuasisimpleGroup,
:IsSimpleGroup, :IsSolvableGroup, :IsSporadicSimpleGroup,
:IsSupersolvableGroup,
# numeric Int
:DerivedLength, :NilpotencyClassOfGroup, :NrConjugacyClasses, :NrMovedPoints,
# numeric ZZRingElem
:Exponent,
# numeric ZZRingElem or infinity
:Size,
# array of numeric
# :AbelianInvariants, :IdGroup, :TransitiveIdentification,
#TODO: GAP lists of integers
# more involved GapObj
:Center,
# :DerivedSeriesOfGroup,
#TODO: GAP lists of GAP objects
]

function save_attrs(s::SerializerState, G::T) where T <: GapObj
save_data_dict(s, :attrs) do
for attr in attrs_list(s, T)
testerfunc = getproperty(GAP.Globals, Symbol(string("Has", attr)))
if testerfunc(G)
getterfunc = getproperty(GAP.Globals, attr)
attr_value = getterfunc(G)
save_typed_object(s, attr_value, attr)
end
end
end
end

function load_attrs(s::DeserializerState, G::T) where T <: GapObj
!with_attrs(s) && return
haskey(s, :attrs) && load_node(s, :attrs) do d
for attr in attrs_list(s, T)
if haskey(d, attr)
func = getproperty(GAP.Globals, Symbol(string("Set", attr)))
attr_value = load_typed_object(s, attr)
func(G, attr_value)
end
end
end
end

#############################################################################
#
# the Oscar (de)serialization methods that delegate to GAP's method selection
#
@register_serialization_type GapObj uses_id
@register_serialization_type GapObj uses_id [GapObj_attributes;]

function save_object(s::SerializerState, X::GapObj)
GAP.Globals.SerializeInOscar(X, s)
Expand All @@ -43,7 +94,9 @@ function load_object(s::DeserializerState, T::Type{GapObj})
GAP_T = load_node(s, :GapType) do gap_type_data
return GapObj(gap_type_data)
end
return GAP.Globals.DeserializeInOscar(GAPWrap.ValueGlobal(GAP_T), s, T)
X = GAP.Globals.DeserializeInOscar(GAPWrap.ValueGlobal(GAP_T), s, T)
load_attrs(s, X)
return X
end
end

Expand Down Expand Up @@ -73,6 +126,88 @@ install_GAP_serialization(:IsFamily,
error("serialization of GAP family object $X is deliberately not supported")
end)

# - `IsInt`, `IsInfinity`, `IsNegInfinity`:
# used for large group orders (that are `GapObj`s)
install_GAP_serialization(:IsInt,
function(X::GapObj, s::SerializerState)
save_data_dict(s) do
save_object(s, "IsInt", :GapType)
save_object(s, ZZRingElem(X), :int)
end
end)

install_GAP_deserialization(
:IsInt,
function(filt::GapObj, s::DeserializerState, T)
val = load_object(s, ZZRingElem, :int)
return GapObj(val)
end)

install_GAP_serialization(:IsInfinity,
function(X::GapObj, s::SerializerState)
save_data_dict(s) do
save_object(s, "IsInfinity", :GapType)
end
end)

install_GAP_deserialization(
:IsInfinity,
function(filt::GapObj, s::DeserializerState, T)
return GAP.Globals.infinity
end)

install_GAP_serialization(:IsNegInfinity,
function(X::GapObj, s::SerializerState)
save_data_dict(s) do
save_object(s, "IsNegInfinity", :GapType)
end
end)

install_GAP_deserialization(
:IsNegInfinity,
function(filt::GapObj, s::DeserializerState, T)
return GAP.Globals.Ninfinity
end)

# # - `IsList`:
# # used for lists of `GapObj`s
# install_GAP_serialization(:IsList,
# function(X::GapObj, s::SerializerState)
# save_data_dict(s) do
# save_object(s, "IsList", :GapType)
# save_object(s, Vector{GAP.Obj}(X), :list)
# end
# end)
#
# install_GAP_deserialization(
# :IsList,
# function(filt::GapObj, s::DeserializerState, T)
# val = load_object(s, Vector{GAP.Obj}, :list)
# return GapObj(val)
# end)

# - `IsPermGroup`:
# Just store generators as lists of images.
install_GAP_serialization(:IsPermGroup,
function(X::GapObj, s::SerializerState)
save_data_dict(s) do
save_object(s, "IsPermGroup", :GapType)
# generators
save_object(s, [Vector{Int}(GAP.Globals.ListPerm(x))
for x in GAP.Globals.GeneratorsOfGroup(X)], :gens)
save_attrs(s, X)
end
end)

install_GAP_deserialization(
:IsPermGroup,
function(filt::GapObj, s::DeserializerState, T)
vgens = load_object(s, Vector, Vector{Int}, :gens)
length(vgens) == 0 && return GAP.Globals.TrivialGroup(GAP.Globals.IsPermGroup)
ggens = [GAP.Globals.PermList(GapObj(x)) for x in vgens]
return GAP.Globals.Group(GapObj(ggens))
end)

# - `IsFreeGroup`:
# full free group or subgroup of it,
# distinguished by presence of `:freeGroup` and `:gens` in case of a subgroup
Expand Down Expand Up @@ -108,6 +243,7 @@ install_GAP_serialization(:IsFreeGroup,
names = Vector{String}(Xnames)
end
save_object(s, names, :names)
save_attrs(s, X)
end
else
# subgroup of a full free group: save the full group and generators
Expand All @@ -117,6 +253,7 @@ install_GAP_serialization(:IsFreeGroup,
save_typed_object(s, F, :freeGroup)
# store generators
save_object(s, [Vector{Int}(GAPWrap.ExtRepOfObj(x)) for x in GAPWrap.GeneratorsOfGroup(X)], :gens)
save_attrs(s, X)
end
end
end)
Expand Down Expand Up @@ -179,6 +316,7 @@ install_GAP_serialization(:IsSubgroupFpGroup,
# relators
relators = GAP.getbangproperty(elfam, :relators)::GapObj
save_object(s, [Vector{Int}(GAPWrap.ExtRepOfObj(x)) for x in relators], :relators)
save_attrs(s, X)
end
else
# subgroup of a full f.p. group: save the full group and generators
Expand All @@ -188,6 +326,7 @@ install_GAP_serialization(:IsSubgroupFpGroup,
save_typed_object(s, F, :wholeGroup)
# store generators
save_object(s, [Vector{Int}(GAPWrap.ExtRepOfObj(x)) for x in GAPWrap.GeneratorsOfGroup(X)], :gens)
save_attrs(s, X)
end
end
end)
Expand Down Expand Up @@ -275,6 +414,7 @@ install_GAP_serialization(:IsPcGroup,
end
end
save_object(s, rels, :comm_rels)
save_attrs(s, X)
end
else
# save full group and generators
Expand All @@ -284,6 +424,7 @@ install_GAP_serialization(:IsPcGroup,
save_typed_object(s, G, :fullGroup)
save_object(s, [Vector{Int}(GAP.Globals.ExponentsOfPcElement(fullpcgs, x)::GapObj)
for x in GAP.Globals.InducedPcgsWrtHomePcgs(X)::GapObj], :gens)
save_attrs(s, X)
end
end
end)
Expand Down
41 changes: 3 additions & 38 deletions src/Serialization/Groups.jl
Original file line number Diff line number Diff line change
Expand Up @@ -110,57 +110,22 @@ function load_type_params(s::DeserializerState, ::Type{<:GrpElemUnionType})
end


#############################################################################
# attributes handling
const GAPGroup_attributes = [
:order, :is_abelian, :is_nilpotent, :is_perfect, :is_simple, :is_solvable
]

function save_attrs(s::SerializerState, G::T) where T <: GAPGroup
save_data_dict(s, :attrs) do
for attr in attrs_list(s, T)
func = Symbol(string("has_", attr))
if @eval $func($G)
attr_value = @eval $attr($G)
save_typed_object(s, attr_value, attr)
end
end
end
end

function load_attrs(s::DeserializerState, G::T) where T <: GAPGroup
!with_attrs(s) && return
haskey(s, :attrs) && load_node(s, :attrs) do d
for attr in attrs_list(s, T)
if haskey(d, attr)
func = Symbol(string("set_", attr))
attr_value = load_typed_object(s, attr)
@eval $func($G, $attr_value)
end
end
end
end

##############################################################################
# PermGroup

@register_serialization_type PermGroup uses_id [GAPGroup_attributes;]
@register_serialization_type PermGroup uses_id

function save_object(s::SerializerState, G::PermGroup)
n = degree(G)
save_data_dict(s) do
save_object(s, n, :degree)
save_object(s, [Vector{Int}(GAPWrap.ListPerm(GapObj(x))) for x in gens(G)], :gens)
save_attrs(s, G)
save_typed_object(s, GapObj(G), :X)
end
end

function load_object(s::DeserializerState, ::Type{PermGroup})
n = load_object(s, Int, :degree)
generators = load_object(s, Vector, (Vector{Int}, Int), :gens)
G = permutation_group(n, [perm(x) for x in generators])
load_attrs(s, G)
return G
return PermGroup(load_typed_object(s, :X), n)
end


Expand Down
42 changes: 42 additions & 0 deletions test/Serialization/GAP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,38 @@
@test_throws ErrorException Oscar.save(filenamex, x)
end

@testset "IsPermGroup" begin
G = GAP.Globals.SymmetricGroup(6)
test_save_load_roundtrip(path, G) do loaded
@test G == loaded
end

# load into a new Julia session
filenameG = joinpath(path, "G")
Oscar.save(filenameG, G)
Oscar.reset_global_serializer_state()
loaded = Oscar.load(filenameG)
@test GAP.Globals.GeneratorsOfGroup(loaded) == GAP.Globals.GeneratorsOfGroup(G)
@test GAP.Globals.HasSize(G) # attribute value

G = GAP.Globals.SymmetricGroup(20)
Oscar.save(filenameG, G)
Oscar.reset_global_serializer_state()
loaded = Oscar.load(filenameG)
@test GAP.Globals.GeneratorsOfGroup(loaded) == GAP.Globals.GeneratorsOfGroup(G)
@test GAP.Globals.HasSize(G)
@test !GAP.Globals.IsSmallIntRep(GAP.Globals.Size(G))

G = GAP.Globals.DihedralGroup(GAP.Globals.IsPermGroup, 8)
Z = GAP.Globals.Center(G)
@test GAP.Globals.HasCenter(G)
Oscar.save(filenameG, G)
Oscar.reset_global_serializer_state()
loaded = Oscar.load(filenameG)
@test GAP.Globals.HasCenter(loaded)
@test GAP.Globals.Center(loaded) == Z
end

@testset "IsFreeGroup" begin
wfilts = [:IsSyllableWordsFamily, :IsLetterWordsFamily]
for wfilt in [getproperty(GAP.Globals, x) for x in wfilts]
Expand Down Expand Up @@ -67,6 +99,16 @@
loaded = Oscar.load(filenamev)
@test GAP.Globals.GeneratorsOfGroup(loaded[2])[1] in loaded[1]
@test GAP.Globals.GeneratorsOfGroup(loaded[3])[1] in loaded[1]

# attributes
@test !GAP.Globals.HasSize(F)
GAP.Globals.Size(F)
@test GAP.Globals.HasSize(F)
Oscar.save(filenameF, F)
Oscar.reset_global_serializer_state()
loadedF = Oscar.load(filenameF)
@test GAP.Globals.HasSize(loadedF)
@test GAP.Globals.Size(loadedF) == GAP.Globals.Size(F)
end
end

Expand Down
24 changes: 22 additions & 2 deletions test/Serialization/Groups.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
mktempdir() do path
@testset "Permutation groups" begin
G = symmetric_group(5)
is_finite(G)
is_abelian(G)
is_nilpotent(G)
is_perfect(G)
is_simple(G)
is_solvable(G)

# single element
x = gen(G, 1)
Expand Down Expand Up @@ -43,6 +49,14 @@
Oscar.reset_global_serializer_state()
loadedv = load(filenamev)
@test parent(loadedv[1]) === loadedv[3]
loadedG = loadedv[3]
@test has_order(loadedG) && order(loadedG) == order(G)
@test has_is_finite(G) && is_finite(loadedG) == is_finite(G)
@test has_is_abelian(G) && is_abelian(loadedG) == is_abelian(G)
@test has_is_nilpotent(G) && is_nilpotent(loadedG) == is_nilpotent(G)
@test has_is_perfect(G) && is_perfect(loadedG) == is_perfect(G)
@test has_is_simple(G) && is_simple(loadedG) == is_simple(G)
@test has_is_solvable(G) && is_solvable(loadedG) == is_solvable(G)

loadedw = load(filenamew)
@test parent(loadedw[1]) === parent(loadedw[2])
Expand Down Expand Up @@ -172,6 +186,9 @@
paras = [(1, 1), (5, 1), (24, 12)]
for (n, i) in paras
G = small_group(n, i)
order(G)
# center(G)
#TODO: problem with cyclic references

# single element
x = rand(G)
Expand Down Expand Up @@ -212,9 +229,12 @@
# simulate loading into a fresh Julia session
Oscar.reset_global_serializer_state()
loadedv = load(filenamev)
@test parent(loadedv[1]) === loadedv[3]
loadedG = loadedv[3]
@test parent(loadedv[1]) === loadedG
@test parent(loadedv[2]) === loadedv[4]
@test is_subgroup(loadedv[4], loadedv[3])[1]
@test is_subgroup(loadedv[4], loadedG)[1]
@test has_order(loadedG)
# @test has_center(loadedG) && order(center(loadedG)[1]) == order(center(G)[1])

loadedw = load(filenamew)
@test parent(loadedv[1]) === parent(loadedw[2])
Expand Down

0 comments on commit 357fc3e

Please sign in to comment.