diff --git a/Project.toml b/Project.toml index c89da1c..7de2f34 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "Groups" uuid = "5d8bd718-bd84-11e8-3b40-ad14f4a32557" authors = ["Marek Kaluba "] -version = "0.7.8" +version = "0.8" [deps] GroupsCore = "d5909c97-4eac-4ecc-a3dc-fdd0858a4120" @@ -14,10 +14,10 @@ Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" [compat] -GroupsCore = "0.4" +GroupsCore = "0.5" KnuthBendix = "0.4" OrderedCollections = "1" -PermutationGroups = "0.4" +PermutationGroups = "0.6" StaticArrays = "1" julia = "1.6" diff --git a/src/Groups.jl b/src/Groups.jl index 9237429..a364c6f 100644 --- a/src/Groups.jl +++ b/src/Groups.jl @@ -27,6 +27,7 @@ include(joinpath("constructions", "constructions.jl")) import .Constructions include("types.jl") +include("rand.jl") include("hashing.jl") include("normalform.jl") include("autgroups.jl") diff --git a/src/autgroups.jl b/src/autgroups.jl index 75b8eba..f361b37 100644 --- a/src/autgroups.jl +++ b/src/autgroups.jl @@ -36,8 +36,8 @@ function Base.:(==)( hash(g) != hash(h) && return false end - length(word(g)) > 8 && normalform!(g) - length(word(h)) > 8 && normalform!(h) + normalform!(g) + normalform!(h) word(g) == word(h) && return true @@ -138,43 +138,47 @@ end # forward evaluate by substitution -struct LettersMap{T,A} - indices_map::Dict{Int,T} +struct LettersMap{W<:AbstractWord,A} + indices_map::Dict{Int,W} A::A end function LettersMap(a::FPGroupElement{<:AutomorphismGroup}) dom = domain(a) - @assert all(isone ∘ length ∘ word, dom) - A = alphabet(first(dom)) - first_letters = first.(word.(dom)) - img = evaluate!(dom, a) + if all(isone ∘ length ∘ word, dom) + A = alphabet(first(dom)) + first_letters = first.(word.(dom)) + img = evaluate!(dom, a) - # (dom[i] → img[i] is a map from domain to images) - # we need a map from alphabet indices → (gens, gens⁻¹) → images - # here we do it for elements of the domain - # (trusting it's a set of generators that define a) - @assert length(dom) == length(img) + # (dom[i] → img[i] is a map from domain to images) + # we need a map from alphabet indices → (gens, gens⁻¹) → images + # here we do it for elements of the domain + # (trusting it's a set of generators that define a) + @assert length(dom) == length(img) - indices_map = - Dict(A[A[fl]] => word(im) for (fl, im) in zip(first_letters, img)) - # inverses of generators are dealt lazily in getindex + indices_map = + Dict(Int(fl) => word(im) for (fl, im) in zip(first_letters, img)) + # inverses of generators are dealt lazily in getindex + else + throw("LettersMap is not implemented for non-generators in domain") + end return LettersMap(indices_map, A) end -function Base.getindex(lm::LettersMap, i::Integer) +function Base.getindex(lm::LettersMap{W}, i::Integer) where {W} # here i is an index of an alphabet @boundscheck 1 ≤ i ≤ length(lm.A) if !haskey(lm.indices_map, i) - img = if haskey(lm.indices_map, inv(i, lm.A)) - inv(lm.indices_map[inv(i, lm.A)], lm.A) + I = inv(i, lm.A) + if haskey(lm.indices_map, I) + img = inv(lm.indices_map[I], lm.A) + lm.indices_map[i] = img else - @warn "LetterMap: neither $i nor its inverse has assigned value" - one(valtype(lm.indices_map)) + lm.indices_map[i] = W([i]) + lm.indices_map[I] = W([I]) end - lm.indices_map[i] = img end return lm.indices_map[i] end @@ -185,9 +189,10 @@ function (a::FPGroupElement{<:AutomorphismGroup})(g::FPGroupElement) return parent(g)(img_w) end -evaluate(w::AbstractWord, lm::LettersMap) = evaluate!(one(w), w, lm) +evaluate(w::AbstractWord, lm::LettersMap) = evaluate!(similar(w), w, lm) function evaluate!(res::AbstractWord, w::AbstractWord, lm::LettersMap) + resize!(res, 0) for i in w append!(res, lm[i]) end diff --git a/src/constructions/direct_power.jl b/src/constructions/direct_power.jl index 7e69b68..3bb3114 100644 --- a/src/constructions/direct_power.jl +++ b/src/constructions/direct_power.jl @@ -59,15 +59,6 @@ end GroupsCore.ngens(G::DirectPower) = _nfold(G) * ngens(G.group) -function GroupsCore.gens(G::DirectPower, i::Integer) - k = ngens(G.group) - ci = CartesianIndices((k, _nfold(G))) - @boundscheck checkbounds(ci, i) - r, c = Tuple(ci[i]) - tup = ntuple(j -> j == c ? gens(G.group, r) : one(G.group), _nfold(G)) - return DirectPowerElement(tup, G) -end - function GroupsCore.gens(G::DirectPower) N = _nfold(G) S = gens(G.group) @@ -78,14 +69,6 @@ end Base.isfinite(G::DirectPower) = isfinite(G.group) -function Base.rand( - rng::Random.AbstractRNG, - rs::Random.SamplerTrivial{<:DirectPower}, -) - G = rs[] - return DirectPowerElement(rand(rng, G.group, _nfold(G)), G) -end - GroupsCore.parent(g::DirectPowerElement) = g.parent function Base.:(==)(g::DirectPowerElement, h::DirectPowerElement) @@ -94,13 +77,6 @@ end Base.hash(g::DirectPowerElement, h::UInt) = hash(g.elts, hash(parent(g), h)) -function Base.deepcopy_internal(g::DirectPowerElement, stackdict::IdDict) - return DirectPowerElement( - Base.deepcopy_internal(g.elts, stackdict), - parent(g), - ) -end - Base.inv(g::DirectPowerElement) = DirectPowerElement(inv.(g.elts), parent(g)) function Base.:(*)(g::DirectPowerElement, h::DirectPowerElement) @@ -108,6 +84,39 @@ function Base.:(*)(g::DirectPowerElement, h::DirectPowerElement) return DirectPowerElement(g.elts .* h.elts, parent(g)) end +# to make sure that parents are never copied i.e. +# g and deepcopy(g) share their parent +Base.deepcopy_internal(G::DirectPower, ::IdDict) = G + +################## Implementing Group Interface Done! + +function GroupsCore.gens(G::DirectPower, i::Integer) + k = ngens(G.group) + ci = CartesianIndices((k, _nfold(G))) + @boundscheck checkbounds(ci, i) + r, c = Tuple(ci[i]) + tup = ntuple(j -> j == c ? gens(G.group, r) : one(G.group), _nfold(G)) + return DirectPowerElement(tup, G) +end + +# Overloading rand: the PRA of GroupsCore is known for not performing +# well on direct sums +function Random.Sampler( + RNG::Type{<:Random.AbstractRNG}, + G::DirectPower, + repetition::Random.Repetition = Val(Inf), +) + return Random.SamplerTrivial(G) +end + +function Base.rand( + rng::Random.AbstractRNG, + rs::Random.SamplerTrivial{<:DirectPower}, +) + G = rs[] + return DirectPowerElement(rand(rng, G.group, _nfold(G)), G) +end + function GroupsCore.order(::Type{I}, g::DirectPowerElement) where {I<:Integer} return convert(I, reduce(lcm, (order(I, h) for h in g.elts); init = one(I))) end @@ -117,10 +126,13 @@ Base.isone(g::DirectPowerElement) = all(isone, g.elts) function Base.show(io::IO, G::DirectPower) n = _nfold(G) nn = n == 1 ? "1-st" : n == 2 ? "2-nd" : n == 3 ? "3-rd" : "$n-th" - return print(io, "Direct $(nn) power of $(G.group)") + return print(io, "Direct $(nn) power of ", G.group) end + function Base.show(io::IO, g::DirectPowerElement) - return print(io, "( ", join(g.elts, ", "), " )") + print(io, "( ") + join(io, g.elts, ", ") + return print(" )") end # convienience: diff --git a/src/constructions/direct_product.jl b/src/constructions/direct_product.jl index a522f6e..adfe0b7 100644 --- a/src/constructions/direct_product.jl +++ b/src/constructions/direct_product.jl @@ -72,14 +72,6 @@ end Base.isfinite(G::DirectProduct) = isfinite(G.first) && isfinite(G.last) -function Base.rand( - rng::Random.AbstractRNG, - rs::Random.SamplerTrivial{<:DirectProduct}, -) - G = rs[] - return DirectProductElement((rand(rng, G.first), rand(rng, G.last)), G) -end - GroupsCore.parent(g::DirectProductElement) = g.parent function Base.:(==)(g::DirectProductElement, h::DirectProductElement) @@ -88,13 +80,6 @@ end Base.hash(g::DirectProductElement, h::UInt) = hash(g.elts, hash(parent(g), h)) -function Base.deepcopy_internal(g::DirectProductElement, stackdict::IdDict) - return DirectProductElement( - Base.deepcopy_internal(g.elts, stackdict), - parent(g), - ) -end - function Base.inv(g::DirectProductElement) return DirectProductElement(inv.(g.elts), parent(g)) end @@ -104,6 +89,30 @@ function Base.:(*)(g::DirectProductElement, h::DirectProductElement) return DirectProductElement(g.elts .* h.elts, parent(g)) end +# to make sure that parents are never copied i.e. +# g and deepcopy(g) share their parent +Base.deepcopy_internal(G::DirectProduct, ::IdDict) = G + +################## Implementing Group Interface Done! + +# Overloading rand: the PRA of GroupsCore is known for not performing +# well on direct sums +function Random.Sampler( + RNG::Type{<:Random.AbstractRNG}, + G::DirectProduct, + repetition::Random.Repetition = Val(Inf), +) + return Random.SamplerTrivial(G) +end + +function Base.rand( + rng::Random.AbstractRNG, + rs::Random.SamplerTrivial{<:DirectProduct}, +) + G = rs[] + return DirectProductElement((rand(rng, G.first), rand(rng, G.last)), G) +end + function GroupsCore.order(::Type{I}, g::DirectProductElement) where {I<:Integer} return convert(I, lcm(order(I, first(g.elts)), order(I, last(g.elts)))) end @@ -111,10 +120,13 @@ end Base.isone(g::DirectProductElement) = all(isone, g.elts) function Base.show(io::IO, G::DirectProduct) - return print(io, "Direct product of $(G.first) and $(G.last)") + return print(io, "Direct product of ", G.first, " and ", G.last) end + function Base.show(io::IO, g::DirectProductElement) - return print(io, "( $(join(g.elts, ",")) )") + print(io, "( ") + join(io, g.elts, ", ") + return print(io, " )") end # convienience: diff --git a/src/constructions/wreath_product.jl b/src/constructions/wreath_product.jl index cf0608a..2f99208 100644 --- a/src/constructions/wreath_product.jl +++ b/src/constructions/wreath_product.jl @@ -1,7 +1,4 @@ -import PermutationGroups: - AbstractPermutationGroup, - AbstractPermutation, - degree +import PermutationGroups as PG """ WreathProduct(G::Group, P::AbstractPermutationGroup) <: Group @@ -16,20 +13,20 @@ product is defined as where `m^σ` denotes the action (from the right) of the permutation `σ` on `d`-tuples of elements from `G`. """ -struct WreathProduct{DP<:DirectPower,PGr<:AbstractPermutationGroup} <: +struct WreathProduct{DP<:DirectPower,PGr<:PG.AbstractPermutationGroup} <: GroupsCore.Group N::DP P::PGr - function WreathProduct(G::Group, P::AbstractPermutationGroup) - N = DirectPower{degree(P)}(G) + function WreathProduct(G::Group, P::PG.AbstractPermutationGroup) + N = DirectPower{PG.AP.degree(P)}(G) return new{typeof(N),typeof(P)}(N, P) end end struct WreathProductElement{ DPEl<:DirectPowerElement, - PEl<:AbstractPermutation, + PEl<:PG.AP.AbstractPermutation, Wr<:WreathProduct, } <: GroupsCore.GroupElement n::DPEl @@ -38,7 +35,7 @@ struct WreathProductElement{ function WreathProductElement( n::DirectPowerElement, - p::AbstractPermutation, + p::PG.AP.AbstractPermutation, W::WreathProduct, ) return new{typeof(n),typeof(p),typeof(W)}(n, p, W) @@ -97,14 +94,6 @@ end Base.isfinite(G::WreathProduct) = isfinite(G.N) && isfinite(G.P) -function Base.rand( - rng::Random.AbstractRNG, - rs::Random.SamplerTrivial{<:WreathProduct}, -) - G = rs[] - return WreathProductElement(rand(rng, G.N), rand(rng, G.P), G) -end - GroupsCore.parent(g::WreathProductElement) = g.parent function Base.:(==)(g::WreathProductElement, h::WreathProductElement) @@ -115,15 +104,7 @@ function Base.hash(g::WreathProductElement, h::UInt) return hash(g.n, hash(g.p, hash(g.parent, h))) end -function Base.deepcopy_internal(g::WreathProductElement, stackdict::IdDict) - return WreathProductElement( - Base.deepcopy_internal(g.n, stackdict), - Base.deepcopy_internal(g.p, stackdict), - parent(g), - ) -end - -function _act(p::AbstractPermutation, n::DirectPowerElement) +function _act(p::PG.AP.AbstractPermutation, n::DirectPowerElement) return DirectPowerElement( ntuple(i -> n.elts[i^p], length(n.elts)), parent(n), @@ -140,11 +121,36 @@ function Base.:(*)(g::WreathProductElement, h::WreathProductElement) return WreathProductElement(g.n * _act(g.p, h.n), g.p * h.p, parent(g)) end +# to make sure that parents are never copied i.e. +# g and deepcopy(g) share their parent +Base.deepcopy_internal(G::WreathProduct, ::IdDict) = G + +################## Implementing Group Interface Done! + +# Overloading rand: the PRA of GroupsCore is known for not performing +# well on direct sums +function Random.Sampler( + RNG::Type{<:Random.AbstractRNG}, + G::WreathProduct, + repetition::Random.Repetition = Val(Inf), +) + return Random.SamplerTrivial(G) +end + +function Base.rand( + rng::Random.AbstractRNG, + rs::Random.SamplerTrivial{<:WreathProduct}, +) + G = rs[] + return WreathProductElement(rand(rng, G.N), rand(rng, G.P), G) +end + Base.isone(g::WreathProductElement) = isone(g.n) && isone(g.p) function Base.show(io::IO, G::WreathProduct) - return print(io, "Wreath product of $(G.N.group) by $(G.P)") + return print(io, "Wreath product of ", G.N.group, " by ", G.P) end -Base.show(io::IO, g::WreathProductElement) = print(io, "( $(g.n)≀$(g.p) )") -Base.copy(g::WreathProductElement) = WreathProductElement(g.n, g.p, parent(g)) +function Base.show(io::IO, g::WreathProductElement) + return print(io, "( ", g.n, "≀", g.p, " )") +end diff --git a/src/normalform.jl b/src/normalform.jl index 7b7742c..613e51a 100644 --- a/src/normalform.jl +++ b/src/normalform.jl @@ -2,8 +2,10 @@ normalform!(g::FPGroupElement) Compute the normal form of `g`, possibly modifying `g` in-place. """ -@inline function normalform!(g::AbstractFPGroupElement) - isnormalform(g) && return g +@inline function normalform!(g::AbstractFPGroupElement; force = false) + if !force + isnormalform(g) && return g + end let w = one(word(g)) w = normalform!(w, g) @@ -36,9 +38,9 @@ end """ normalform!(res::AbstractWord, g::FPGroupElement) -Append the normal form of `g` to word `res`, modifying `res` in place. +Write the normal form of `g` to word `res`, modifying `res` in place. -Defaults to the rewriting in the free group. +The particular implementation of the normal form depends on `parent(g)`. """ @inline function normalform!(res::AbstractWord, g::AbstractFPGroupElement) isone(res) && isnormalform(g) && return append!(res, word(g)) diff --git a/src/rand.jl b/src/rand.jl new file mode 100644 index 0000000..1ead576 --- /dev/null +++ b/src/rand.jl @@ -0,0 +1,62 @@ +""" + PoissonSampler +For a finitely presented group PoissonSampler returns group elements represented +by words of length at most `R ~ Poisson(λ)` chosen uniformly at random. + +For finitely presented groups the Product Replacement Algorithm +(see `PRASampler` from `GroupsCore.jl`) doesn't make much sense due to +overly long words it produces. We therefore resort to a pseudo-random method, +where a word `w` of length `R` is chosen uniformly at random among all +words of length `R` where `R` follows the Poisson distribution. + +!!! note + Due to the choice of the parameters (`λ=8`) and the floating point + arithmetic the sampler will always return group elements represented by + words of length at most `42`. +""" +struct PoissonSampler{G,T} <: Random.Sampler{T} + group::G + λ::Int +end + +function PoissonSampler(G::AbstractFPGroup; λ) + return PoissonSampler{typeof(G),eltype(G)}(G, λ) +end + +function __poisson_invcdf(val; λ) + # __poisson_pdf(k, λ) = λ^k * ℯ^-λ / factorial(k) + # pdf = ntuple(k -> __poisson_pdf(k - 1, λ), 21) + # cdf = accumulate(+, pdf) + # radius = something(findfirst(>(val), cdf) - 1, 0) + # this is the iterative version: + pdf = ℯ^-λ + cdf = pdf + k = 0 + while cdf < val + k += 1 + pdf = pdf * λ / k + cdf += pdf + end + return k +end + +function Random.rand(rng::Random.AbstractRNG, sampler::PoissonSampler) + R = __poisson_invcdf(rand(rng); λ = sampler.λ) + + G = sampler.group + n = length(alphabet(G)) + W = word_type(G) + T = eltype(W) + + letters = rand(rng, T(1):T(n), R) + word = W(letters, false) + return G(word) +end + +function Random.Sampler( + RNG::Type{<:Random.AbstractRNG}, + G::AbstractFPGroup, + repetition::Random.Repetition = Val(Inf), +) + return PoissonSampler(G; λ = 8) +end diff --git a/src/types.jl b/src/types.jl index d6a567e..3b7c150 100644 --- a/src/types.jl +++ b/src/types.jl @@ -67,17 +67,6 @@ function GroupsCore.gens(G::AbstractFPGroup) return [gens(G, i) for i in 1:GroupsCore.ngens(G)] end -# TODO: ProductReplacementAlgorithm -function Base.rand( - rng::Random.AbstractRNG, - rs::Random.SamplerTrivial{<:AbstractFPGroup}, -) - l = rand(10:100) - G = rs[] - nletters = length(alphabet(G)) - return FPGroupElement(word_type(G)(rand(1:nletters, l)), G) -end - function Base.isfinite(::AbstractFPGroup) return ( @warn "using generic isfinite(::AbstractFPGroup): the returned `false` might be wrong"; false @@ -110,10 +99,6 @@ mutable struct FPGroupElement{Gr<:AbstractFPGroup,W<:AbstractWord} <: end end -function Base.show(io::IO, ::Type{<:FPGroupElement{Gr}}) where {Gr} - return print(io, FPGroupElement, "{$Gr, …}") -end - function Base.copy(f::FPGroupElement) return FPGroupElement(copy(word(f)), parent(f), f.savedhash) end @@ -134,18 +119,33 @@ function Base.:(==)(g::AbstractFPGroupElement, h::AbstractFPGroupElement) @boundscheck @assert parent(g) === parent(h) normalform!(g) normalform!(h) + # I. compare hashes of the normalform + # II. compare some data associated to FPGroupElement, + # e.g. word, image of the domain etc. hash(g) != hash(h) && return false - return equality_data(g) == equality_data(h) + equality_data(g) == equality_data(h) && return true # compares + + # if this failed it is still possible that the words together can be + # rewritten even further, so we + # 1. rewrite word(g⁻¹·h) w.r.t. rewriting(parent(g)) + # 2. check if the result is empty + G = parent(g) + + g⁻¹h = append!(inv(word(g), alphabet(G)), word(h)) + # similar + empty preserve the storage size + # saves some re-allocations if res does not represent id + res = similar(word(g)) + resize!(res, 0) + res = KnuthBendix.rewrite!(res, g⁻¹h, rewriting(G)) + return isone(res) end function Base.deepcopy_internal(g::FPGroupElement, stackdict::IdDict) - haskey(stackdict, objectid(g)) && return stackdict[objectid(g)] - cw = if haskey(stackdict, objectid(word(g))) - stackdict[objectid(word(g))] - else - copy(word(g)) - end - return FPGroupElement(cw, parent(g), g.savedhash) + haskey(stackdict, g) && return stackdict[g] + cw = Base.deepcopy_internal(word(g), stackdict) + h = FPGroupElement(cw, parent(g), g.savedhash) + stackdict[g] = h + return h end function Base.inv(g::GEl) where {GEl<:AbstractFPGroupElement} @@ -192,9 +192,16 @@ struct FreeGroup{T,O} <: AbstractFPGroup end end -FreeGroup(gens, A::Alphabet) = FreeGroup(gens, KnuthBendix.LenLex(A)) +function FreeGroup(n::Integer) + symbols = + collect(Iterators.flatten((Symbol(:f, i), Symbol(:F, i)) for i in 1:n)) + inverses = collect(Iterators.flatten((2i, 2i - 1) for i in 1:n)) + return FreeGroup(Alphabet(symbols, inverses)) +end -function FreeGroup(A::Alphabet) +FreeGroup(A::Alphabet) = FreeGroup(KnuthBendix.LenLex(A)) + +function __group_gens(A::Alphabet) @boundscheck @assert all(KnuthBendix.hasinverse(l, A) for l in A) gens = Vector{eltype(A)}() invs = Vector{eltype(A)}() @@ -203,20 +210,12 @@ function FreeGroup(A::Alphabet) push!(gens, l) push!(invs, inv(l, A)) end - - return FreeGroup(gens, A) + return gens end -function FreeGroup(n::Integer) - symbols = Symbol[] - inverses = Int[] - sizehint!(symbols, 2n) - sizehint!(inverses, 2n) - for i in 1:n - push!(symbols, Symbol(:f, i), Symbol(:F, i)) - push!(inverses, 2i, 2i - 1) - end - return FreeGroup(symbols[1:2:2n], Alphabet(symbols, inverses)) +function FreeGroup(O::KnuthBendix.WordOrdering) + grp_gens = __group_gens(alphabet(O)) + return FreeGroup(grp_gens, O) end function Base.show(io::IO, F::FreeGroup) @@ -265,10 +264,18 @@ function FPGroup( end function Base.show(io::IO, ::MIME"text/plain", G::FPGroup) - print(io, "Finitely presented group generated by:\n\t{") - Base.print_array(io, permutedims(gens(G))) - println(io, " },") - println(io, "subject to relations:") + println( + io, + "Finitely presented group generated by $(ngens(G)) element", + ngens(G) > 1 ? 's' : "", + ": ", + ) + join(io, gens(G), ", ", ", and ") + println( + io, + "\n subject to relation", + length(relations(G)) > 1 ? 's' : "", + ) return Base.print_array(io, relations(G)) end diff --git a/test/AutFn.jl b/test/AutFn.jl index c3f035b..85b1837 100644 --- a/test/AutFn.jl +++ b/test/AutFn.jl @@ -38,7 +38,7 @@ [2, 1, 4, 3, 6, 5, 8, 7, 10, 9] ) - F4 = FreeGroup([:a, :b, :c, :d], A4) + F4 = FreeGroup(A4) a, b, c, d = gens(F4) D = ntuple(i -> gens(F4, i), 4) diff --git a/test/fp_groups.jl b/test/fp_groups.jl index 87e6ef7..47511a6 100644 --- a/test/fp_groups.jl +++ b/test/fp_groups.jl @@ -4,7 +4,7 @@ @test FreeGroup(A) isa FreeGroup @test sprint(show, FreeGroup(A)) == "free group on 3 generators" - F = FreeGroup([:a, :b, :c], A) + F = FreeGroup([:a, :b, :c], Groups.KnuthBendix.LenLex(A)) @test sprint(show, F) == "free group on 3 generators" a, b, c = gens(F) diff --git a/test/free_groups.jl b/test/free_groups.jl index f35d85e..9604a1d 100644 --- a/test/free_groups.jl +++ b/test/free_groups.jl @@ -1,7 +1,7 @@ @testset "FreeGroup" begin A3 = Alphabet([:a, :b, :c, :A, :B, :C], [4,5,6,1,2,3]) - F3 = FreeGroup([:a, :b, :c], A3) + F3 = FreeGroup(A3) @test F3 isa FreeGroup @test gens(F3) isa Vector