diff --git a/src/Groups.jl b/src/Groups.jl index 0789323..9d60ad1 100644 --- a/src/Groups.jl +++ b/src/Groups.jl @@ -21,6 +21,12 @@ include("freereduce.jl") include("arithmetic.jl") include("findreplace.jl") +module New +include("new_types.jl") +include("normalform.jl") + +end # module New + ############################################################################### # # String I/O diff --git a/src/new_types.jl b/src/new_types.jl new file mode 100644 index 0000000..ff49ee5 --- /dev/null +++ b/src/new_types.jl @@ -0,0 +1,211 @@ +using GroupsCore +# using Groups +# import Groups.AbstractFPGroup +import KnuthBendix +import KnuthBendix: AbstractWord, Alphabet, Word, RewritingSystem +import KnuthBendix: alphabet +using Random + +## "Abstract" definitions + +""" + AbstractFPGroup + +An Abstract type representing finitely presented groups. Every instance `` must implement + * `KnuthBendix.alphabet(G::MyFPGroup)` + +By default `word_type(::Type{MyFPGroup}) = Word{UInt16}` returns the default +word type used for group elements. You may wish to override this if e.g. to +> `word_type(::Type{MyFPGroup}) = Word{UInt8}` +if your group has less than `255` generators. +""" +abstract type AbstractFPGroup <: GroupsCore.Group end + +word_type(G::AbstractFPGroup) = word_type(typeof(G)) +# the default: +word_type(::Type{<:AbstractFPGroup}) = Word{UInt16} + +function (G::AbstractFPGroup)(word::AbstractVector{<:Integer}) + @boundscheck @assert all(l -> 1<= l <=length(KnuthBendix.alphabet(G)), word) + return FPGroupElement(word_type(G)(word), G) +end + +## Group Interface + +Base.one(G::AbstractFPGroup) = FPGroupElement(one(word_type(G)), G) + +Base.eltype(::Type{FPG}) where {FPG<:AbstractFPGroup} = + FPGroupElement{FPG, word_type(FPG)} + +struct FPGroupIter{GEl} + elts::Vector{GEl} + seen::Set{GEl} + u::GEl + v::GEl +end + +FPGroupIter(G::AbstractFPGroup) = + FPGroupIter([one(G)], Set([one(G)]), one(G), one(G)) + +Base.iterate(G::AbstractFPGroup) = one(G), (FPGroupIter(G), 1, 1) +@inline function Base.iterate(G::AbstractFPGroup, state) + iter, elt_idx, gen_idx = state + + if gen_idx > length(alphabet(G)) + elt_idx == length(iter.elts) && return nothing + gen_idx = 1 + elt_idx += 1 + end + + + res = let (u, v) = (iter.u, iter.v), elt = iter.elts[elt_idx] + copyto!(v, elt) # this invalidates normalform of v + @assert !isnormalform(v) + push!(word(v), gen_idx) + resize!(word(u), 0) + + normalform!(u, v) + end + + if res in iter.seen + return iterate(G, (iter, elt_idx, gen_idx+1)) + else + w = deepcopy(res) + @assert isnormalform(w) + push!(iter.elts, w) + push!(iter.seen, w) + state = (iter, elt_idx, gen_idx+1) + return w, state + end +end + +# the default: +# Base.IteratorSize(::Type{<:AbstractFPGroup}) = Base.SizeUnknown() + +GroupsCore.ngens(G::AbstractFPGroup) = length(G.gens) + +function GroupsCore.gens(G::AbstractFPGroup, i::Integer) + @boundscheck 1<=i<=GroupsCore.ngens(G) + return FPGroupElement(word_type(G)([i]), G) +end +GroupsCore.gens(G::AbstractFPGroup) = [gens(G, i) for i in 1:GroupsCore.ngens(G)] + +# TODO: ProductReplacementAlgorithm +function Base.rand( + rng::Random.AbstractRNG, + rs::Random.SamplerTrivial{<:AbstractFPGroup}, + ) + l = rand(10:100) + G = rs[] + symmgens_len = length(alphabet(F)) + return FPGroupElement(word_type(G)(rand(1:symmgens_len, l)), G) +end + +## FPGroupElement + +mutable struct FPGroupElement{G<:AbstractFPGroup, W<:AbstractWord} <: GroupElement + word::W + savedhash::UInt + parent::G + + FPGroupElement(word::W, G::AbstractFPGroup) where W<:AbstractWord = + new{typeof(G), W}(word, UInt(0), G) + + FPGroupElement(word::W, hash::UInt, G::AbstractFPGroup) where W<:AbstractWord = + new{typeof(G), W}(word, hash, G) +end + +word(f::FPGroupElement) = f.word + +#convenience +KnuthBendix.alphabet(g::FPGroupElement) = alphabet(parent(g)) + +function Base.show(io::IO, f::FPGroupElement) + f = normalform!(f) + print(io, KnuthBendix.string_repr(word(f), alphabet(f))) +end + +## Hashing + +# We store hash of a word in field `savedhash` to use it as cheap replacement +# for equality checking. In the last bit of `savehash` we store the information +# whether the word is already reduced (in normal form) or not. Hence +isnormalform(g::FPGroupElement) = Bool(g.savedhash & 1) + +# To update hash use this internal method, possibly only after computing the +# normal form of `g`: +_update_savedhash!(g::FPGroupElement, h = hash(word(g))) = + (g.savedhash = h | 1; return g) + +# If `g` has been mutated (e.g. `word(g)` has been modified, `_invalidate_hash` +# must be called. +_invalidate_hash!(g::FPGroupElement) = g.savedhash = g.savedhash & 0 + +# Accessor for the saved hash +@inline _savedhash(f::FPGroupElement) = f.savedhash + +function Base.hash(g::FPGroupElement, h::UInt) + normalform!(g) + # compute the best approximation of the normal form + return hash(parent(g), _savedhash(g) ⊻ h) +end + +function Base.copyto!(res::FPGroupElement, g::FPGroupElement) + resize!(word(res), length(word(g))) + copyto!(word(res), word(g)) + _invalidate_hash!(res) + return res +end + +## GroupElement Interface for FPGroupElement + +Base.parent(f::FPGroupElement) = f.parent +GroupsCore.parent_type(::Type{<:FPGroupElement{G}}) where G = G + +function Base.:(==)(g::FPGroupElement, h::FPGroupElement) + @boundscheck @assert parent(g) === parent(h) + hash(g) != hash(h) && return false + # hash reduces to normal form behind the scenes + return word(g) == word(h) +end + +function Base.deepcopy_internal(g::FPGroupElement, stackdict::IdDict) + return FPGroupElement(copy(word(g)), _savedhash(g), parent(g)) +end + +Base.inv(g::FPGroupElement) = + (G = parent(g); FPGroupElement(inv(alphabet(G), word(g)), G)) + +function Base.:(*)(g::FPGroupElement, h::FPGroupElement) + @boundscheck @assert parent(g) === parent(h) + return FPGroupElement(word(g)*word(h), parent(g)) +end + +GroupsCore.isfiniteorder(g::FPGroupElement) = isone(g) ? true : throw("Not Implemented") + +# additional methods: +Base.isone(g::FPGroupElement) = (normalform!(g); isempty(word(g))) + +## Free Groups + +struct FreeGroup{T} <: AbstractFPGroup + gens::Vector{T} + alphabet::KnuthBendix.Alphabet{T} + + function FreeGroup(gens, A::KnuthBendix.Alphabet) where W + @assert length(gens) == length(unique(gens)) + @assert all(l->l in KnuthBendix.letters(A), gens) + return new{eltype(gens)}(gens, A) + end +end + +function FreeGroup(a::Alphabet) + @boundscheck @assert all(KnuthBendix.hasinverse(l, A) + for l in KnuthBendix.letters(a)) + return FreeGroup(KnuthBendix.letters(a), a) +end + +Base.show(io::IO, F::FreeGroup) = print(io, "free group on $(ngens(F)) generators") + +# mandatory methods: +KnuthBendix.alphabet(F::FreeGroup) = F.alphabet diff --git a/src/normalform.jl b/src/normalform.jl new file mode 100644 index 0000000..945cfc2 --- /dev/null +++ b/src/normalform.jl @@ -0,0 +1,76 @@ +""" + normalform!(g::FPGroupElement) +Compute the normal form of `g`, possibly modifying `g` in-place. +""" +@inline function normalform!(g::FPGroupElement) + isnormalform(g) && return g + + let w = one(word(g)) + w = normalform!(w, g) + resize!(word(g), length(w)) + copyto!(word(g), w) + end + + g = _update_savedhash!(g) + @assert isnormalform(g) + return g +end + +""" + normalform!(res::GEl, g::GEl) where GEl<:FPGroupElement +Compute the normal fom of `g`, storing it in `res`. +""" +function normalform!(res::GEl, g::GEl) where GEl<:FPGroupElement + @boundscheck @assert parent(res) === parent(g) + if isnormalform(g) + copyto!(res, g) + _update_savedhash!(res, g.savedhash) + else + resize!(word(res), 0) + normalform!(word(res), g) + res = _update_savedhash!(res) + end + return res +end + +""" + normalform!(res::AbstractWord, g::FPGroupElement) +Append the normal form of `g` to word `res`, modifying `res` in place. + +Defaults to the rewriting in the free group. +""" +@inline function normalform!(res::AbstractWord, g::FPGroupElement) + isone(res) && isnormalform(g) && return append!(res, word(g)) + if isnormalform(g) && inv(alphabet(g), last(out)) != first(word(g)) + return append!(res, word(g)) + end + return free_rewrite!(res, word(g), alphabet(g)) +end + +""" + free_rewrite!(v::AbstractWord, w::AbstractWord, A::Alphabet) +Append `w` to `v` applying free reductions as defined by the inverses of `A`. +""" +free_rewrite!(v::AbstractWord, w::AbstractWord, A::Alphabet) = + KnuthBendix.rewrite_from_left!(v, w, A) + +function KnuthBendix.rewrite_from_left!( + v::AbstractWord, + w::AbstractWord, + A::Alphabet + ) + while !isone(w) + if isone(v) + push!(v, popfirst!(w)) + else + # the first check is for monoids only + if KnuthBendix.hasinverse(last(v), A) && inv(A, last(v)) == first(w) + pop!(v) + popfirst!(w) + else + push!(v, popfirst!(w)) + end + end + end + return v +end