1
0
mirror of https://github.com/kalmarek/Groups.jl.git synced 2024-11-30 17:20:27 +01:00

Compare commits

..

12 Commits

Author SHA1 Message Date
Marek Kaluba
1fbd7b875b
Merge pull request #31 from kalmarek/mk/update_to_PG_0.6
Update to PermutationGroups-0.6
2024-02-13 12:08:48 +01:00
c13226a625
bump to v0.8 2024-02-13 11:16:04 +01:00
Marek Kaluba
7776ac4c6e
Merge branch 'master' into mk/update_to_PG_0.6 2024-02-12 14:04:07 +01:00
6ca9497dab
tweak the printing of FPGroup 2024-02-12 12:38:28 +01:00
f4d018f087
update constructions to PG-0.6 2024-02-12 12:37:33 +01:00
add9a6f287
when everything fails in == try rewriting inv(g)*h 2024-02-12 12:36:04 +01:00
ba6d58ec77
rewrite/fix deepcopy_internal 2024-02-12 12:33:57 +01:00
a33b871754
simplify the constructors of the FreeGroup 2024-02-12 12:17:48 +01:00
efbb4eada8
remove the show overload for Type{<:FPGroupElement} 2024-02-12 12:16:32 +01:00
148a472dd2
add PoissonSampler for FPGroupElements
Words of length following the Poisson(λ=8) distribution are
chosen uniformly at random.
2024-02-12 12:16:03 +01:00
5993cb328f
normalform! should be a no-op for normalized words 2024-02-12 11:36:17 +01:00
8137e40998
make LettersMap a bit safer to use 2024-02-12 11:34:51 +01:00
12 changed files with 253 additions and 146 deletions

View File

@ -1,7 +1,7 @@
name = "Groups"
uuid = "5d8bd718-bd84-11e8-3b40-ad14f4a32557"
authors = ["Marek Kaluba <kalmar@amu.edu.pl>"]
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"

View File

@ -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")

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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

View File

@ -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))

62
src/rand.jl Normal file
View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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