diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..134d3f5f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 99 + labels: + - "dependencies" + - "github-actions" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e53e38fa..2c8f15e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,49 +1,47 @@ name: CI on: - - pull_request - - push + push: + branches: + - main + tags: ['*'] + pull_request: + workflow_dispatch: +concurrency: + # Skip intermediate builds: always. + # Cancel intermediate builds: only if it is a pull request build. + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ startsWith(github.ref, 'refs/pull/') }} jobs: test: name: Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} runs-on: ${{ matrix.os }} + timeout-minutes: 60 + permissions: # needed to allow julia-actions/cache to proactively delete old caches that it has created + actions: write + contents: read strategy: + fail-fast: false matrix: version: - - '1.6' + - 'lts' - '1' - - 'nightly' + - 'pre' os: - ubuntu-latest - - macOS-latest - - windows-latest arch: - x64 - allow_failures: - - julia: nightly - fail-fast: false steps: - - uses: actions/checkout@v3 - - uses: julia-actions/setup-julia@v1 + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@v2 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v3 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- - - uses: julia-actions/julia-buildpkg@latest - - uses: julia-actions/julia-runtest@latest + - uses: julia-actions/cache@v2 + - uses: julia-actions/julia-buildpkg@v1 + - uses: julia-actions/julia-runtest@v1 - uses: julia-actions/julia-processcoverage@v1 - uses: codecov/codecov-action@v4 with: - file: ./lcov.info - flags: unittests - name: codecov-umbrella - fail_ci_if_error: false + files: lcov.info token: ${{ secrets.CODECOV_TOKEN }} + fail_ci_if_error: false diff --git a/Project.toml b/Project.toml index 96cc70e3..5b173e8f 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["Marek Kaluba ", "Mikołaj Pabiszczak right-hand side). -To access the iterator (FIXME: ???) over all rules stored in a rewriting system `KnuthBendix.rules` function should be used. - -Each subtype of this abstract type should implement the following interface -(see docstring for that abstract type for more information): -pushing, popping, appending, inserting, deleting rules, emptying the whole structure and obtaining the length of it -(i.e. number of rules stored in the structure - both active and inactive). - -TODO: add more precise list and info about methods with `AbstractRewritingSystem` which we use in rewriting. - -Specific places where certain interfaces arw used: - -* `push!(rws)` - used in `forceconfluece!` and `deriverule!` to add rules to stack or rewriting system; -* `pop!(rws)` - during `deriverule!`, to get the rules from a stack; -* `empty!(rws)` - used in the beginning of some of `knuthbendix` procedure implementations; -* `isactive(rws, i::Integer)` - at various stages of `knuthbendix` procedure to check if the rule is active; -* `setinactive!(rws, i::Integer)` - during `deriverule!`, to mark rules as inactive. - -### `KnuthBendix.RewritingSystem` - -We implemented a simple `RewritingSystem{W<:AbstractWord, O<:WordOrdering} <:AbstractRewritingSystem{W}` -which stores the defining ordering in one of its fields. -Each rule (in the current implementation) consist of an ordered pair of words `(lhs, rhs)::Tuple{W,W}` -(i.e. `Base.lt(ordering(rws), lhs, rhs)` holds), which represents rewriting rule -> `lhs` → `rhs`. - -**IDEA:** create a distinct type/class for a single rewriting rule and store the rules as a list of this objects. -Such rule could store also its status (active or not), etc. See #24. - -Inside `RewritingSystem{W}` we also store `rules::Vector{W}` and (paired with it) `act::BitArray`, -which stores `1` when the corresponding rule in `rules` is active and `0` otherwise. -Note that iterating over `KnuthBendix.rules(rws)` returns all rules, regardless of their active status. - -**TODO:** return an specialized iterator over rules of `rws` which iterates only over active rules. -This would surely make code simpler/easier to read and more generic. - -### Simplifying rules - -Method `simplifyrule!(lhs::AbstractWord, rhs::AbstractWord, A::Alphabet)` was suggested in the book by Sims. -It takes a rule (typically prodcued during Knuth-Bendix procedure) and (as of now) -cancels out maximal invertible prefix and suffix of both sides of the rule. - -## States and Automata - -### States - -Before describing automata itself we should describe a single state in it. -`KnuthBendix.State{N, W}<:KnuthBendix.AbstractState` struct is indexed by two parameters: -* `N` the length of the alphabet we use (a letter and its inverse are considered as two distinct elements of the alphabet), and -* `W` particular type of `AbstractWord` that we use. - -#### Details of implementation - -A state has a `name`: a word corresponding to the shortest path from initial state to the state in mention -(that way it is easy to define a length function for a state - as mentioned in Sims - this is just a length of the name). - -A field `terminal` is a boolean which indicates whether the state represents a word, -which is a left-hand side of some rewriting rule in `RewritingSystem`. -In case this is the case (field `terminal` is `true`) field `rrule` should be equal to the `Word` representing -the right-hand side of that rewriting rule. - -Field `ined` (incoming edges) is a vector of instances of `State` struct - those instances of `State` -from which there is an edge leading to a state in mention -(so for a `State` with name "abc" there must be - in particular - a `State` with name "ab" in the `ined` list). -What is more: always on the first place on that list should be a state representing (having field `name`) -the `Word` of the form `name[1:end-1]` (i.e. the name which is sort of "the closest predecessor" of that state/word) -- compare the algorithm for `makeindexautomaton`. -[BTW: in the case of index automata all the `inedges` should be labelled by the same letter.] - -Field `outed` (outcoming edges) - is a `NTuple` (i.e. tuple of the size equal to the size of alphabet) -of states to which there is an edge from the state in mention. -Since index automata are deterministic there is only one edge for every letter in the alphabet coming out of our state. -Thus we use N-tuples - indexes of the tuple correspond to indexes of the letters in the `Alphabet` struct. -I.e. the idea is as follows: if we want to travel from a state `s` (say with name "ab") -through the edge labelled by letter "c" (say it is stored in the `Alphabet` at the index 3) -we just take call `s.outed[3]` and we would land in the state named "abc" -(provided of course, that all the states are defined within automaton). - -Field `isfailstate` (boolean) is a field that is used to represent kind of an empty state / failure state -which is created when we create a new automaton -(this state is necessary to represent edges which are not yet constructed in the tuple `outed`). -Since `ined` and `outed` introduce circular connections between states, -there is a need to have a unique "noedge / failstate" state in the whole automaton to which we can point -(in order to prevent additional allocations and in order to have the N-Tuple representing outedges of parametrized by `{N, State{N, W}`). - -**IMPORTANT:** states should not be created on its own. -One should always create an `Automaton` first and the add states inside it via `push!` and `addedge!` interface. - -**IDEA:** When working with groups we could try to utilise the fact that a letter and its inverse are closely related -and that each letter has an inverse (so that maybe we could skip some fields). - -### Automata - -`Automaton{N, W} <: AbstractAutomaton{N, W}` is the basic structure that contatins states. -Recall that an instance of `State` should not "live" outside of automaton. -Automata are paramaterized the same way the `State`s are -(i.e. by `N` - size of the `Alphabet` and `W` - the particular type of `AbstractWord` used). - -Field `states` is a list of states in the automaton. -During the declaration of automaton the unique initial state corresponding to empty word is created and appended to that list. - -Field `abt` contains alphabet used. -Field `failstate` is a field that contains a unique state representing lack of edge / failure state - -it is created during the declaration of the automaton. -Field `stateslengths` is a list of lengths of states (indexes corresponding to the indexes in `states`). - -The following constitutes a interface to `Automaton{N, W}` and `State{N,W}`: -* `push!(at::Automaton, name::W)`: pushes a new state with the `name` to the automaton; -* `addedge!(at::Automaton, label::Integer, src::Integer, dst::Integer)`: -adds the edge with a given `label` (index of the letter in thegiven alphabet) directed -from `src` state (index of the source state in the list of states stored in the automaton) -to `dst` state (index of the source state in the list of states stored in the automaton); -* `removeedge!(a::Automaton, label::Integer, from::Integer, to::Integer)` as above, but removes edge; -* `walk(a::AbstractAutomaton, signature::AbstractWord[, state=initialstate(a)])`: walks or traces the automaton -according to the path given by the `signature`, starting from `state`. -Returns a tuple `(idx, state)` where `idx` is the length of prefix of signature which was successfully traced and -`state` is the final state. -Note that if `idx ≠ length(signature)` there is no path in the automaton corresponding to the full signature. -* `makeindexautomaton!(a::Automaton, rws::RewritingSystem)` builds an index automaton corrresponding to -the given rewriting system on the automaton `a`; -* `updateautomaton!(a::Automaton, rws::RewritingSystem)`: this is an interface designed for future - -to be used to update existing index automaton instead of rebuilding it from scratch. -At the moment it performs rebuilding automaton from scratch. - -**IDEA:** Implement automata as matrices (consult Sims' book: -chapter about automata and chapter about implementation considerations at the end of the book). - -## Helper structures - -### BufferPair - -`BufferPair{T} <: AbstractBufferPair{T}` is a pair of `BufferWord{T}` (stored in fields `_vWord` and `_wWord`), -that are used for rewriting. -This structure is used to limit the number of allocations while performing Knuth-Bendix procedure. -Two `BufferPair`s are needed for an efficient rewriting. One is used in rewriting the left-hand side of the rule -and the other in rewriting of the right-hand side (which is performed in `deriverule!`). -`BufferPair`s are stored in `kbWork{T}` helper structure - see below. - -### kbWork - -`kbWork{T}` is a helper structure used to iterate over rewriting system in Knuth-Bendix procedure. -It has the following fields: -* `i` field is the iterator over the outer loop and -* `j` is the iterator over the inner loop -* `lhsPair` and `rhsPair` are inner `BufferPair`s used for rewriting. -* `_inactiverules` is just a list of inactive rules in the `RewritingSystem` subjected to Knuth-Bendix procedure. - -The aim of this structure is to: -* enable deletion of inactive rules (which requires updating working indexes `i` and `j` of Knuth-Bendix procedure). -This deletion is performed by the `removeinactive!(rws::RewritingSystem, work::kbWork)` function which is called -periodically during certain implementations of Knuth-Bendix procedure. -* reduce the number of allocations caused by rules rewriting (by maintaining the needed temporary words in `BufferPair`s). - -This structure is created inside `knuthbendix` and is passed to `forceconfluence!` and `deriverule!`. - -## Knuth-Bendix procedure - -As of now there 4 implementations: -* `crude`, which is basically 1-1 implementation of `KBS1` from the Sims' book. -* `naive`, which is an implementation of `KBS2` from the Sims' book with simplification of rules incorporated. -* `deletion`, which is based on `KBS2`, uses simplification of rules and periodically removes inactive rules. -* `automata`, which is uses automata for rewriting. Simplification of the rules is also incorporated. - -Versions `deletion` and `automata` can be considered the best (fastest) ones at the moment. -The problem with `automata` is that the index automaton needs to be rebuilt quite often -(due to the changes in `RewritingSystem`) - this can hinder the efficiency gain obtained by faster rewriting. diff --git a/src/Automata/backtrack.jl b/src/Automata/backtrack.jl index bbf1f720..9d5835ab 100644 --- a/src/Automata/backtrack.jl +++ b/src/Automata/backtrack.jl @@ -401,6 +401,5 @@ function irreducible_words( max_length::Integer = typemax(UInt), ) oracle = IrreducibleWordsOracle(min_lenght, max_length) - bs = BacktrackSearch(ia, oracle) return BacktrackSearch(ia, oracle) end diff --git a/src/FPMonoids.jl b/src/FPMonoids.jl new file mode 100644 index 00000000..bcde9da5 --- /dev/null +++ b/src/FPMonoids.jl @@ -0,0 +1,245 @@ +module FPMonoids +import GroupsCore as GC +import KnuthBendix as KB +import KnuthBendix: alphabet, Word + +export FreeMonoid, FPMonoid, FPMonoidElement + +# abstract methods + +abstract type AbstractFPMonoid{I} <: GC.Monoid end + +Base.one(M::AbstractFPMonoid{I}) where {I} = M(one(Word{I}), true) +GC.gens(M::AbstractFPMonoid{I}, i::Integer) where {I} = M(Word{I}(I[i])) +GC.gens(M::AbstractFPMonoid) = [GC.gens(M, i) for i in 1:length(alphabet(M))] +GC.ngens(M::AbstractFPMonoid) = length(alphabet(M)) + +# coercing to monoid +function (M::AbstractFPMonoid{I})( + w::AbstractVector{<:Integer}, + reduced = false, +) where {I} + res = FPMonoidElement(w, M, reduced) + if length(w) > __wl_limit(M) + normalform!(res) + end + return res +end + +function Base.eltype(::Type{M}) where {M<:AbstractFPMonoid{I}} where {I} + return FPMonoidElement{I,M} +end + +__wl_limit(::AbstractFPMonoid) = 256 + +# types and constructors + +const Relation{I} = NTuple{2,Word{I}} + +struct FreeMonoid{I,A} <: AbstractFPMonoid{I} + alphabet::A + + function FreeMonoid{I}(a::KB.Alphabet) where {I<:Integer} + length(a) < typemax(I) || + throw("Too many letters in alphabet. Try with $(widen(I))") + return new{I,typeof(a)}(a) + end +end + +FreeMonoid(a::KB.Alphabet) = FreeMonoid{UInt16}(a) +FreeMonoid(n::Integer) = FreeMonoid(KB.Alphabet([Symbol('a', i) for i in 1:n])) + +struct FPMonoid{I,A,IA<:KB.IndexAutomaton} <: AbstractFPMonoid{I} + alphabet::A + relations::Vector{Relation{I}} # in case you need them later + idx_automaton::IA + confluent::Bool +end + +# Accessors and basic manipulation +KB.alphabet(M::Union{<:FPMonoid,FreeMonoid}) = M.alphabet +rewriting(M::FreeMonoid) = alphabet(M) +rewriting(M::FPMonoid) = M.idx_automaton + +Base.isfinite(M::FreeMonoid) = isempty(alphabet(M)) +Base.isfinite(m::FPMonoid) = isfinite(m.idx_automaton) # finiteness of the language + +mutable struct FPMonoidElement{I,M<:AbstractFPMonoid{I}} <: GC.MonoidElement + word::Word{I} + parent::M + normalform::Bool + + function FPMonoidElement( + w::AbstractVector, + M::AbstractFPMonoid{I}, + reduced = false, + ) where {I} + return new{I,typeof(M)}(w, M, reduced) + end +end + +Base.parent(m::FPMonoidElement) = m.parent +word(m::FPMonoidElement) = m.word +isnormal(m::FPMonoidElement) = m.normalform + +Base.one(m::FPMonoidElement) = one(parent(m)) +Base.isone(m::FPMonoidElement) = (normalform!(m); isone(word(m))) +# this is technically a lie: +GC.isfiniteorder(x::FPMonoidElement) = isone(x) + +# actual user-constructors for Monoid: + +function FPMonoid(rws::KB.RewritingSystem) + a = KB.alphabet(rws) + if !KB.isreduced(rws) + rws = KB.reduce!(rws) + end + rels = [Tuple(r) for r in rws.rules_orig] + return FPMonoid(a, rels, KB.IndexAutomaton(rws), KB.isconfluent(rws)) +end + +function Base.:(/)( + M::FreeMonoid{I}, + rels::AbstractArray{<:FPMonoidElement}, +) where {I} + return M / [(r, one(M)) for r in rels] +end + +function Base.:(/)( + M::FreeMonoid{I}, + rels::AbstractArray{<:Tuple{<:FPMonoidElement,<:FPMonoidElement}}, + ordering = KB.LenLex(alphabet(M)); + settings = KB.Settings(), +) where {I} + A = M.alphabet + new_rels = Relation{I}[word.(r) for r in rels] + + rws = KB.RewritingSystem(new_rels, ordering) + R = KB.knuthbendix!(settings, rws) + + return FPMonoid(A, new_rels, KB.IndexAutomaton(R), KB.isconfluent(R)) +end + +function Base.show(io::IO, ::MIME"text/plain", M::FreeMonoid) + return print(io, "free monoid over $(alphabet(M))") +end +function Base.show(io::IO, ::MIME"text/plain", M::FPMonoid) + return print( + io, + "monoid defined by $(length(M.relations)) relations over $(alphabet(M))", + ) +end + +function Base.:(*)(m::FPMonoidElement, ms::FPMonoidElement...) + all(==(parent(m)), parent.(ms)) || throw( + DomainError( + (parent(m), parent.(ms)...), + "cannot multiply elements from different monoids", + ), + ) + return parent(m)(*(word(m), word.(ms)...)) +end + +Base.:(^)(m::FPMonoidElement, n::Integer) = (parent(m))(word(m)^n) + +""" + normalform!(m::FPMonoidElement[, tmp::AbstractWord]) +Reduce `m` to its normalform, as defined by the rewriting of `parent(m)`. +""" +function normalform!(m::FPMonoidElement) + isnormal(m) && return m + I = eltype(word(m)) + return normalform!(m, KB.Words.BufferWord{I}(I[])) +end +function normalform!(m::FPMonoidElement, buffer::KB.Words.BufferWord) + w = word(m) + KB.Words.store!(buffer, w) + KB.rewrite!(w, buffer, rewriting(parent(m))) + empty!(buffer) + m.normalform = true + return m +end + +function Base.:(==)(m1::FPMonoidElement, m2::FPMonoidElement) + parent(m1) === parent(m2) || return false + normalform!(m1) + normalform!(m2) + return word(m1) == word(m2) +end + +function Base.hash(m::FPMonoidElement, h::UInt) + normalform!(m) + return hash(word(m), hash(parent(m), h)) +end + +function Base.deepcopy_internal(m::FPMonoidElement, stackdict::IdDict) + M = parent(m) + return M(Base.deepcopy_internal(word(m), stackdict), isnormal(m)) +end + +function Base.show(io::IO, m::FPMonoidElement) + m = normalform!(m) + return KB.print_repr(io, word(m), alphabet(parent(m))) +end + +Base.IteratorSize(::Type{<:FreeMonoid}) = Base.IsInfinite() + +# eltype and IteratorSie implemented for AbstractMonoid +function Base.iterate(M::FPMonoid) + idxA = rewriting(M) + itr = KB.Automata.irreducible_words(idxA) + w, st = iterate(itr) + return M(w, true), (itr, st) +end + +function Base.iterate(M::FPMonoid, state) + itr, st = state + k = iterate(itr, st) + isnothing(k) && return nothing + return M(first(k), true), (itr, last(k)) +end + +function GC.order(::Type{I}, M::FreeMonoid) where {I} + isfinite(M) && return one(I) + throw(GC.InfiniteOrder(M)) +end + +function GC.order(::Type{I}, M::FPMonoid) where {I} + if isfinite(M) + return convert(I, KB.Automata.num_irreducible_words(rewriting(M))) + end + + verb = M.confluent ? "is" : "appears to be" + msg = "monoid $verb infinite" + if !M.confluent + msg *= " (but the underlying rewriting system is not confluent)" + end + @error(msg) + + throw(GC.InfiniteOrder(M)) +end + +Base.length(M::AbstractFPMonoid) = GC.order(Int, M) + +elements(M::FPMonoid, max_word_length) = elements(M, 0, max_word_length) + +function elements(M::FPMonoid, min_word_lenght, max_word_length) + words_itr = KB.Automata.irreducible_words( + rewriting(M), + min_word_lenght, + max_word_length, + ) + elts = [M(w, true) for w in words_itr] + counts = zeros(Int, max_word_length + 1) + for m in elts + counts[length(word(m))+1] += 1 + end + sizes = cumsum(counts) + elts = sort!(elts, by = word, order = KB.LenLex(alphabet(M))) + return elts, Dict(m => sizes[m+1] for m in min_word_lenght:max_word_length) +end + +Base.adjoint(m::FPMonoidElement) = parent(m)(reverse(m.word)) + +end # of module Monoids + diff --git a/src/KnuthBendix.jl b/src/KnuthBendix.jl index d5864cd4..29927c50 100644 --- a/src/KnuthBendix.jl +++ b/src/KnuthBendix.jl @@ -5,6 +5,7 @@ using ProgressMeter export Alphabet, Word, RewritingSystem export LenLex, WreathOrder, Recursive, WeightedLex export alphabet, isconfluent, ordering, knuthbendix +export FPMonoids include("utils/packed_vector.jl") # include("utils/subset_vector.jl") @@ -37,6 +38,7 @@ include("confluence_check.jl") include("parsing.jl") include("examples.jl") +include("FPMonoids.jl") include("precompile_tools.jl") end diff --git a/src/Words/Words.jl b/src/Words/Words.jl index 6d7fea1d..e9416ec0 100644 --- a/src/Words/Words.jl +++ b/src/Words/Words.jl @@ -8,4 +8,18 @@ include("words_impl.jl") include("bufferwords.jl") include("searchindex.jl") +# make copyto! allocation free on julia-1.6 +Base.copyto!(w::Word, v::Word) = (Base.copyto!(w.ptrs, v.ptrs); w) +function Base.copyto!(w::Word, v::BufferWord) + Base.copyto!(w.ptrs, @view v.storage[v.lidx:v.ridx]) + return w +end +function Base.copyto!(w::BufferWord, v::Word) + @assert internal_length(w) ≥ length(v) + Base.copyto!(w.storage, v.ptrs) + w.lidx = 1 + w.ridx = length(v) + return w +end + end diff --git a/src/Words/bufferwords.jl b/src/Words/bufferwords.jl index 9ded96fc..66f36a3f 100644 --- a/src/Words/bufferwords.jl +++ b/src/Words/bufferwords.jl @@ -46,7 +46,7 @@ mutable struct BufferWord{T} <: AbstractWord{T} end end -function BufferWord(v::Union{<:Vector{<:Integer},<:AbstractVector{<:Integer}}) +function BufferWord(v::AbstractVector{<:Integer}) return BufferWord{UInt16}(v) end diff --git a/src/Words/interface.jl b/src/Words/interface.jl index 468a137a..19979968 100644 --- a/src/Words/interface.jl +++ b/src/Words/interface.jl @@ -53,6 +53,12 @@ end Base.convert(::Type{W}, w::AbstractWord) where {W<:AbstractWord} = W(w) Base.convert(::Type{W}, w::W) where {W<:AbstractWord} = w +function Base.convert( + ::Type{W}, + v::AbstractVector{<:Integer}, +) where {W<:AbstractWord} + return W(v) +end Base.empty!(w::AbstractWord) = resize!(w, 0) @@ -70,13 +76,10 @@ function store!(w::AbstractWord, vs::AbstractWord...) return w end -function Base.:*(w::AbstractWord, v::AbstractWord) +function Base.:*(w::AbstractWord, vs::AbstractWord...) out = similar(w) copyto!(out, w) - isone(v) && return out - resize!(out, length(w) + length(v)) - copyto!(out, length(w) + 1, v, 1) - # append!(out, v) + append!(out, vs...) return out end diff --git a/src/precompile_tools.jl b/src/precompile_tools.jl index f3c007fc..db795576 100644 --- a/src/precompile_tools.jl +++ b/src/precompile_tools.jl @@ -2,22 +2,9 @@ using PrecompileTools @setup_workload begin @compile_workload begin n = 6 - Al = Alphabet(['a', 'b', 'B']) - setinverse!(Al, 'b', 'B') - - a, b, B = Word.([i] for i in 1:3) - ε = one(a) - - eqns = [ - (b * B, ε), - (B * b, ε), - (a^2, ε), - (b^3, ε), - ((a * b)^7, ε), - ((a * b * a * B)^n, ε), - ] - - R = RewritingSystem(eqns, LenLex(Al)) - knuthbendix(R) + R = ExampleRWS.triangle_237_quotient(n) + RC = knuthbendix(R) + M = FPMonoids.FPMonoid(RC) + collect(M) end end diff --git a/test/fpmonoids.jl b/test/fpmonoids.jl new file mode 100644 index 00000000..49901ba5 --- /dev/null +++ b/test/fpmonoids.jl @@ -0,0 +1,111 @@ +using GroupsCore +import KnuthBendix.FPMonoids as Monoids + +include(joinpath(pathof(GroupsCore), "..", "..", "test", "conformance_test.jl")) + +@testset "FPMonoids" begin + M = Monoids.FreeMonoid(4) + str = sprint(show, MIME"text/plain"(), M) + @test occursin("free monoid", str) + test_GroupsCore_interface(M) + + M² = M / [a^2 for a in gens(M)] + test_GroupsCore_interface(M²) + @testset "Monoid of commuting projections (quantum stuff)" begin + function example_monoid(n::Integer) + A = KB.Alphabet( + append!( + [Symbol('a', i) for i in 1:n], + [Symbol('b', i) for i in 1:n], + ), + ) + F = Monoids.FreeMonoid(A) + rels = let S = gens(F) + squares = [(g^2, one(g)) for g in S] + @views a, b = S[1:n], S[n+1:end] + aibj = + [(a[i] * b[j], b[j] * a[i]) for i in 1:n for j in 1:n] + [squares; aibj] + end + + return F / rels + end + + N = 5 + M = example_monoid(N) + test_GroupsCore_interface(M) + + a = gens(M)[1:N] + b = gens(M)[N+1:2N] + + w = *(a...) + i = 1 + @test b[i] * w == w * b[i] + + biw = b[i] * w + @test Monoids.word(biw) == [N + i; 1:N] + @test !Monoids.isnormal(biw) + biw_dc = deepcopy(biw) + + @test parent(biw_dc) == parent(biw) + @test Monoids.word(biw_dc) == Monoids.word(biw) + @test Monoids.word(biw_dc) !== Monoids.word(biw) + + @test Monoids.normalform!(biw_dc) isa Monoids.FPMonoidElement + @test Monoids.word(biw_dc) != Monoids.word(biw) + @test Monoids.word(biw_dc) == [1:N; N + i] + + @test sprint(show, MIME"text/plain"(), biw) == "a1*a2*a3*a4*a5*b1" + @test sprint(show, MIME"text/plain"(), M) == + "monoid defined by 35 relations over Alphabet{Symbol}: [:a1, :a2, :a3, :a4, :a5, :b1, :b2, :b3, :b4, :b5]" + + len = 10 + wa, wb = M(rand(1:N, len)), M(rand(N+1:2N, len)) + @test wa * wb == wb * wa + + @test !isfinite(M) + + elts = collect(Iterators.take(M, 10)) + @test first(elts) == one(M) + a1 = gens(M, 1) + a2 = gens(M, 2) + @test last(elts) == a1 * a2 * a1 * a2 * a1 * a2 * a1 * a2 * a1 + @test_throws GroupsCore.InfiniteOrder length(M) + + @test (wa * wb)' == wb' * wa' + w, v = rand(M, 2) + @test (w * v)' == v' * w' + + elts, sizes = Monoids.elements(M, 4) + @test isone(elts[sizes[0]]) + @test all(e -> length(Monoids.word(e)) == 1, elts[sizes[0]+1:sizes[1]]) + @test all(e -> length(Monoids.word(e)) == 2, elts[sizes[1]+1:sizes[2]]) + @test all(e -> length(Monoids.word(e)) == 3, elts[sizes[2]+1:sizes[3]]) + @test all(e -> length(Monoids.word(e)) == 4, elts[sizes[3]+1:sizes[4]]) + + eltsᵀ = elts' + @test isone(eltsᵀ[sizes[0]]) + B2 = elts[sizes[0]+1:sizes[2]] + @test all(≤(sizes[2]), [findfirst(==(e'), elts) for e in B2]) + + end + + @testset "237-triangle monoid" begin + R237 = KB.ExampleRWS.triangle_237_quotient(6) + M237 = Monoids.FPMonoid(R237) + + R237_c = KB.knuthbendix(R237) + M237_c = Monoids.FPMonoid(R237_c) + + w = [3, 3, 3] + @test !isone(M237(w)) # B·B·B + @test isone(M237_c(w)) # B·B·B + + @test_throws GroupsCore.InfiniteOrder GroupsCore.order(Int, M237) + @test GroupsCore.order(Int, M237_c) == 1092 + @test collect(M237_c) isa Vector{<:MonoidElement} + @test collect(M237_c) isa Vector{<:Monoids.FPMonoidElement} + + test_GroupsCore_interface(M237_c) + end +end diff --git a/test/runtests.jl b/test/runtests.jl index b71549c6..ec41a128 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,6 +21,8 @@ include("abstract_words.jl") include("test_examples.jl") include("gapdoc_examples.jl") + include("fpmonoids.jl") + include("kbmag_parsing.jl") if !haskey(ENV, "CI") || v"1.6" ≤ VERSION < v"1.7"