diff --git a/.github/workflows/runtests.yml b/.github/workflows/runtests.yml index e4c950e..6ccb8a7 100644 --- a/.github/workflows/runtests.yml +++ b/.github/workflows/runtests.yml @@ -14,7 +14,7 @@ jobs: matrix: version: - '1.3' - - '1.5' + - '1' - 'nightly' os: - ubuntu-latest diff --git a/Project.toml b/Project.toml index bd6bd42..01f4198 100644 --- a/Project.toml +++ b/Project.toml @@ -1,20 +1,28 @@ name = "Groups" uuid = "5d8bd718-bd84-11e8-3b40-ad14f4a32557" authors = ["Marek Kaluba "] -version = "0.5.2" +version = "0.6.0" [deps] AbstractAlgebra = "c3fe647b-3220-5bb0-a1ea-a7954cac585d" -LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" +GroupsCore = "d5909c97-4eac-4ecc-a3dc-fdd0858a4120" +KnuthBendix = "c2604015-7b3d-4a30-8a26-9074551ec60a" +OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" ThreadsX = "ac1d9e8a-700a-412c-b207-f0111f4b6c0d" [compat] -AbstractAlgebra = "^0.10.0" +AbstractAlgebra = "0.15, 0.16" +GroupsCore = "^0.3" +KnuthBendix = "^0.2.1" +OrderedCollections = "1" ThreadsX = "^0.1.0" -julia = "1.3, 1.4, 1.5" +julia = "1.3, 1.4, 1.5, 1.6" [extras] +AbstractAlgebra = "c3fe647b-3220-5bb0-a1ea-a7954cac585d" +BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Test"] +test = ["Test", "BenchmarkTools", "AbstractAlgebra"] diff --git a/README.md b/README.md index fe6af31..7a64708 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,152 @@ [![Build Status](https://travis-ci.org/kalmarek/Groups.jl.svg?branch=master)](https://travis-ci.org/kalmarek/Groups.jl) [![codecov](https://codecov.io/gh/kalmarek/Groups.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/kalmarek/Groups.jl) -A very rudimentary implementation of finitely-presented groups (syllable representation). Relatively complete are only [automorphism groups of free groups](https://github.com/kalmarek/Groups.jl/blob/master/src/AutGroup.jl) and [wreath products](https://github.com/kalmarek/Groups.jl/blob/master/src/WreathProducts.jl) (which are not finitely-presented, but based on the standard normal form). +An implementation of finitely-presented groups together with normalization (using Knuth-Bendix procedure). -Have a look into `test` directory for eample use. +The package implements `AbstractFPGroup` with three concrete types: `FreeGroup`, `FPGroup` and `AutomorphismGroup`. Here's an example usage: + +```julia +julia> using Groups, GroupsCore + +julia> A = Alphabet([:a, :A, :b, :B, :c, :C], [2, 1, 4, 3, 6, 5]) +Alphabet of Symbol: + 1. :a = (:A)⁻¹ + 2. :A = (:a)⁻¹ + 3. :b = (:B)⁻¹ + 4. :B = (:b)⁻¹ + 5. :c = (:C)⁻¹ + 6. :C = (:c)⁻¹ + +julia> F = FreeGroup(A) +free group on 3 generators + +julia> a,b,c = gens(F) +3-element Vector{FPGroupElement{FreeGroup{Symbol}, KnuthBendix.Word{UInt8}}}: + a + b + c + +julia> a*inv(a) +(empty word) + +julia> (a*b)^2 +a*b*a*b + +julia> commutator(a, b) +A*B*a*b + +julia> x = a*b; y = inv(b)*a; + +julia> x*y +a^2 + +``` +Let's create a quotient of the free group above: +```julia +julia> ε = one(F); + +julia> G = FPGroup(F, [a^2 => ε, b^3=> ε, (a*b)^7=>ε, (a*b*a*inv(b))^6 => ε, commutator(a, c) => ε, commutator(b, c) => ε ]) +┌ Warning: Maximum number of rules (100) reached. The rewriting system may not be confluent. +│ You may retry `knuthbendix` with a larger `maxrules` kwarg. +└ @ KnuthBendix ~/.julia/packages/KnuthBendix/i93Np/src/kbs.jl:6 +⟨a, b, c | a^2 => (empty word), b^3 => (empty word), a*b*a*b*a*b*a*b*a*b*a*b*a*b => (empty word), a*b*a*B*a*b*a*B*a*b*a*B*a*b*a*B*a*b*a*B*a*b*a*B => (empty word), A*C*a*c => (empty word), B*C*b*c => (empty word)⟩ + +``` +As you can see from the warning, the Knuth-Bendix procedure has not completed successfully. This means that we only are able to approximate the word problem in `G`, i.e. if the equality (`==`) of two group elements may return `false` even if group elements are equal. Let us try with a larger maximal number of rules in the underlying rewriting system. + +```julia +julia> G = FPGroup(F, [a^2 => ε, b^3=> ε, (a*b)^7=>ε, (a*b*a*inv(b))^6 => ε, commutator(a, c) => ε, commutator(b, c) => ε ], maxrules=500) +⟨a, b, c | a^2 => (empty word), b^3 => (empty word), a*b*a*b*a*b*a*b*a*b*a*b*a*b => (empty word), a*b*a*B*a*b*a*B*a*b*a*B*a*b*a*B*a*b*a*B*a*b*a*B => (empty word), A*C*a*c => (empty word), B*C*b*c => (empty word)⟩ + +``` +This time there was no warning, i.e. Knuth-Bendix completion was successful and we may treat the equality (`==`) as true mathematical equality. Note that `G` is the direct product of `ℤ = ⟨ c ⟩` and a quotient of van Dyck `(2,3,7)`-group. Let's create a random word and reduce it as an element of `G`. +```julia +julia> using Random; Random.seed!(1); w = Groups.Word(rand(1:length(A), 16)) +KnuthBendix.Word{UInt16}: 4·6·1·1·1·6·5·1·5·2·3·6·2·4·2·6 + +julia> F(w) # freely reduced w +B*C*a^4*c*A*b*C*A*B*A*C + +julia> G(w) # w as an element of G +B*a*b*a*B*a*C^2 + +julia> F(w) # freely reduced w +B*C*a^4*c*A*b*C*A*B*A*C + +julia> word(ans) # the underlying word in A +KnuthBendix.Word{UInt8}: 4·6·1·1·1·1·5·2·3·6·2·4·2·6 + +julia> G(w) # w as an element of G +B*a*b*a*B*a*C^2 + +julia> word(ans) # the underlying word in A +KnuthBendix.Word{UInt8}: 4·1·3·1·4·1·6·6 + +``` +As we can see the underlying words change according to where they are reduced. +Note that a word `w` (of type `Word <: AbstractWord`) is just a sequence of numbers -- pointers to letters of an `Alphabet`. Without the alphabet `w` has no meaning. + +### Automorphism Groups + +Relatively complete is the support for the automorphisms of free groups, as given by Gersten presentation: +```julia +julia> saut = SpecialAutomorphismGroup(F, maxrules=100) +┌ Warning: Maximum number of rules (100) reached. The rewriting system may not be confluent. +│ You may retry `knuthbendix` with a larger `maxrules` kwarg. +└ @ KnuthBendix ~/.julia/packages/KnuthBendix/i93Np/src/kbs.jl:6 +automorphism group of free group on 3 generators + +julia> S = gens(saut) +12-element Vector{Automorphism{FreeGroup{Symbol},…}}: + ϱ₁.₂ + ϱ₁.₃ + ϱ₂.₁ + ϱ₂.₃ + ϱ₃.₁ + ϱ₃.₂ + λ₁.₂ + λ₁.₃ + λ₂.₁ + λ₂.₃ + λ₃.₁ + λ₃.₂ + +julia> x, y, z = S[1], S[12], S[6]; + +julia> f = x*y*inv(z) +ϱ₁.₂*λ₃.₂*ϱ₃.₂^-1 + +julia> g = inv(z)*y*x +ϱ₃.₂^-1*ϱ₁.₂*λ₃.₂ + +julia> word(f), word(g) +(KnuthBendix.Word{UInt8}: 1·12·18, KnuthBendix.Word{UInt8}: 18·1·12) + +``` +Even though Knuth-Bendix did not finish successfully in automorphism groups we have another ace in our sleeve to solve the word problem: evaluation. +Lets have a look at the images of generators under those automorphisms: +```julia +julia> evaluate(f) # or to be more verbose... +(a*b, b, b*c*B) + +julia> Groups.domain(g) +(a, b, c) + +julia> Groups.evaluate!(Groups.domain(g), g) +(a*b, b, b*c*B) + +``` +Since these automorphism map the standard generating set to the same new generating set, they should be considered as equal! And indeed they are: +```julia +julia> f == g +true +``` +This is what is happening behind the scenes: + 1. words are reduced using a rewriting system + 2. if resulting words are equal `true` is returned + 3. if they are not equal `Groups.equality_data` is computed for each argument (here: the images of generators) and the result of comparison is returned. + +Moreover we try to amortize the cost of computing those images. That is a hash of `equality_daata` is lazily stored in each group element and used as needed. Essentially only if `true` is returned, but comparison of words returns `false` recomputation of images is needed (to guard against hash collisions). + +---- +This package was developed for computations in [1712.07167](https://arxiv.org/abs/1712.07167) and in [1812.03456](https://arxiv.org/abs/1812.03456). If you happen to use this package please cite either of them. diff --git a/src/AutGroup.jl b/src/AutGroup.jl deleted file mode 100644 index 1ce4b00..0000000 --- a/src/AutGroup.jl +++ /dev/null @@ -1,346 +0,0 @@ -export Automorphism, AutGroup, Aut, SAut - -############################################################################### -# -# AutSymbol/ AutGroup / Automorphism -# - -struct RTransvect - i::Int8 - j::Int8 -end - -struct LTransvect - i::Int8 - j::Int8 -end - -struct FlipAut - i::Int8 -end - -struct PermAut - perm::Generic.Perm{Int8} -end - -struct Identity end - -struct AutSymbol <: GSymbol - id::Symbol - pow::Int8 - fn::Union{LTransvect, RTransvect, PermAut, FlipAut, Identity} -end - -# taken from ValidatedNumerics, under under the MIT "Expat" License: -# https://github.com/JuliaIntervals/ValidatedNumerics.jl/blob/master/LICENSE.md -function subscriptify(n::Integer) - subscript_0 = Int(0x2080) # Char(0x2080) -> subscript 0 - return join([Char(subscript_0 + i) for i in reverse(digits(n))], "") -end - -function id_autsymbol() - return AutSymbol(Symbol("(id)"), 0, Identity()) -end - -function transvection_R(i::Integer, j::Integer, pow::Integer=1) - if 0 < i < 10 && 0 < j < 10 - id = Symbol(:ϱ, subscriptify(i), subscriptify(j)) - else - id = Symbol(:ϱ, subscriptify(i), "." ,subscriptify(j)) - end - return AutSymbol(id, pow, RTransvect(i, j)) -end - -function transvection_L(i::Integer, j::Integer, pow::Integer=1) - if 0 < i < 10 && 0 < j < 10 - id = Symbol(:λ, subscriptify(i), subscriptify(j)) - else - id = Symbol(:λ, subscriptify(i), "." ,subscriptify(j)) - end - return AutSymbol(id, pow, LTransvect(i, j)) -end - -function flip(i::Integer, pow::Integer=1) - iseven(pow) && return id_autsymbol() - id = Symbol(:ɛ, subscriptify(i)) - return AutSymbol(id, 1, FlipAut(i)) -end - -function AutSymbol(p::Generic.Perm, pow::Integer=1) - if pow != 1 - p = p^pow - end - - if any(p.d[i] != i for i in eachindex(p.d)) - id = Symbol(:σ, "₍", join([subscriptify(i) for i in p.d],""), "₎") - return AutSymbol(id, 1, PermAut(p)) - end - return id_autsymbol() -end - -ϱ(i::Integer, j::Integer, pow::Integer=1) = transvection_R(i, j, pow) -λ(i::Integer, j::Integer, pow::Integer=1) = transvection_L(i, j, pow) -ε(i::Integer, pow::Integer=1) = flip(i, pow) -σ(v::Generic.Perm, pow::Integer=1) = AutSymbol(v, pow) - -function change_pow(s::AutSymbol, n::Integer) - iszero(n) && id_autsymbol() - - symbol = s.fn - if symbol isa FlipAut - return flip(symbol.i, n) - elseif symbol isa PermAut - return AutSymbol(symbol.perm, n) - elseif symbol isa RTransvect - return transvection_R(symbol.i, symbol.j, n) - elseif symbol isa LTransvect - return transvection_L(symbol.i, symbol.j, n) - elseif symbol isa Identity - return id_autsymbol() - else - throw(DomainError("Unknown type of AutSymbol: $s")) - end -end - -############################################################################### -# -# AutGroup / Automorphism -# - -mutable struct AutGroup{N} <: AbstractFPGroup - objectGroup::FreeGroup - gens::Vector{AutSymbol} -end - -mutable struct Automorphism{N} <: GWord{AutSymbol} - symbols::Vector{AutSymbol} - modified::Bool - savedhash::UInt - parent::AutGroup{N} - - function Automorphism{N}(f::Vector{AutSymbol}) where {N} - return new{N}(f, true, zero(UInt)) - end -end - -elem_type(::Type{AutGroup{N}}) where N = Automorphism{N} -parent_type(::Type{Automorphism{N}}) where N = AutGroup{N} - -function AutGroup(G::FreeGroup; special=false) - S = AutSymbol[] - n = length(gens(G)) - n == 0 && return AutGroup{n}(G, S) - - indexing = [[i,j] for i in 1:n for j in 1:n if i≠j] - - rmuls = [ϱ(i,j) for (i,j) in indexing] - lmuls = [λ(i,j) for (i,j) in indexing] - - append!(S, [rmuls; lmuls]) - - if !special - flips = [ε(i) for i in 1:n] - syms = [σ(p) for p in SymmetricGroup(Int8(n))][2:end] - - append!(S, [flips; syms]) - end - return AutGroup{n}(G, S) -end - -Aut(G::Group) = AutGroup(G) -SAut(G::Group) = AutGroup(G, special=true) - -Automorphism{N}(s::AutSymbol) where N = Automorphism{N}(AutSymbol[s]) - -function (G::AutGroup{N})(f::AutSymbol) where N - g = Automorphism{N}([f]) - setparent!(g, G) - return g -end - -(G::AutGroup{N})(g::Automorphism{N}) where N = (setparent!(g, G); g) - -############################################################################### -# -# AutSymbol defining functions && evaluation -# NOTE: all automorphisms operate on a tuple of FreeWords INPLACE! -# - -function (ϱ::RTransvect)(v, pow::Integer=1) - rmul!(v[ϱ.i], v[ϱ.j]^pow) - return v -end - -function (λ::LTransvect)(v, pow::Integer=1) - lmul!(v[λ.i], v[λ.j]^pow) - return v -end - -function (σ::PermAut)(v, pow::Integer=1) - w = deepcopy(v) - s = (σ.perm^pow).d - @inbounds for k in eachindex(v) - v[k].symbols = w[s[k]].symbols - end - return v -end - -function (ɛ::FlipAut)(v, pow::Integer=1) - @inbounds if isodd(pow) - v[ɛ.i].symbols = inv(v[ɛ.i]).symbols - end - return v -end - -(::Identity)(v, pow::Integer=1) = v - -############################################################################### -# -# Functional call overloads for evaluation of AutSymbol and Automorphism -# - -(s::AutSymbol)(v::NTuple{N, T}) where {N, T} = s.fn(v, s.pow)::NTuple{N, T} - -function (f::Automorphism{N})(v::NTuple{N, T}) where {N, T} - for s in syllables(f) - v = s(v)::NTuple{N, T} - end - return v -end - -function domain(G::AutGroup{N}) where N - F = G.objectGroup - return ntuple(i->F(F.gens[i]), N) -end - -evaluate(f::Automorphism) = f(domain(parent(f))) - -############################################################################### -# -# hashing && equality -# - -function hash_internal( - g::Automorphism, - h::UInt = 0x7d28276b01874b19; # hash(Automorphism) - # alternatively: 0xcbf29ce484222325 from FNV-1a algorithm - images = compute_images(g), - prime = 0x00000100000001b3, # prime from FNV-1a algorithm -) - return foldl((h,x) -> hash(x, h)*prime, images, init = hash(parent(g), h)) -end - -function compute_images(g::Automorphism) - images = evaluate(g) - for im in images - reduce!(im) - end - return images -end - -function (==)(g::Automorphism{N}, h::Automorphism{N}) where N - syllables(g) == syllables(h) && return true - img_computed, imh_computed = false, false - - if ismodified(g) - img = compute_images(g) # sets modified bit - hash(g, images=img) - img_computed = true - end - - if ismodified(h) - imh = compute_images(h) # sets modified bit - hash(h, images=imh) - imh_computed = true - end - - @assert !ismodified(g) && !ismodified(h) - # cheap - # if hashes differ, images must have differed as well - hash(g) != hash(h) && return false - - # hashes equal, hence either equal elements, or a hash conflict - begin - if !img_computed - img_task = Threads.@spawn img = compute_images(g) - # img = compute_images(g) - end - if !imh_computed - imh_task = Threads.@spawn imh = compute_images(h) - # imh = compute_images(h) - end - !img_computed && fetch(img_task) - !imh_computed && fetch(imh_task) - end - - img != imh && @warn "hash collision in == :" g h - return img == imh -end - -############################################################################### -# -# String I/O -# - -function show(io::IO, G::AutGroup) - print(io, "Automorphism Group of $(G.objectGroup)\n") - print(io, "Generated by $(gens(G))") -end - -############################################################################### -# -# Reduction -# - -getperm(s::AutSymbol) = s.fn.perm^s.pow - -function simplifyperms!(::Type{Bool}, w::Automorphism{N}) where N - reduced = true - for i in 1:syllablelength(w)-1 - s, ns = syllables(w)[i], syllables(w)[i+1] - if isone(s) - continue - elseif s.fn isa PermAut && ns.fn isa PermAut - reduced = false - setmodified!(w) - syllables(w)[i+1] = AutSymbol(getperm(s)*getperm(ns)) - syllables(w)[i] = change_pow(s, 0) - end - end - filter!(!isone, syllables(w)) - return reduced -end - -function reduce!(w::Automorphism) - reduced = false - while !reduced - reduced = simplifyperms!(Bool, w) && freereduce!(Bool, w) - end - return w -end - -############################################################################### -# -# Abelianization (natural Representation to GL(N,Z)) -# - -abelianize(A::Automorphism{N}) where N = image(A, abelianize; n=N) - -# homomorphism definition -abelianize(; n::Integer=1) = Matrix{Int}(I, n, n) -abelianize(a::AutSymbol; n::Int=1) = abelianize(a.fn, n, a.pow) - -function abelianize(a::Union{RTransvect, LTransvect}, n::Int, pow) - x = Matrix{Int}(I, n, n) - x[a.i,a.j] = pow - return x -end - -function abelianize(a::FlipAut, n::Int, pow) - x = Matrix{Int}(I, n, n) - x[a.i,a.i] = -1 - return x -end - -abelianize(a::PermAut, n::Integer, pow) = Matrix{Int}(I, n, n)[(a.perm^pow).d, :] -abelianize(a::Identity, n::Integer, pow) = abelianize(;n=n) diff --git a/src/DirectPower.jl b/src/DirectPower.jl deleted file mode 100644 index 8274465..0000000 --- a/src/DirectPower.jl +++ /dev/null @@ -1,192 +0,0 @@ -export DirectPowerGroup, DirectPowerGroupElem - -############################################################################### -# -# DirectPowerGroup / DirectPowerGroupElem Constructors -# -############################################################################### - -""" - DirectPowerGroup(G::Group, n::Int) <: Group -Return `n`-fold direct product of `G`. The group operation is -`*` distributed component-wise, with component-wise identity as neutral element. -""" -struct DirectPowerGroup{N, T<:Group} <: Group - group::T -end - -DirectPowerGroup(G::Gr, N::Int) where Gr<:Group = DirectPowerGroup{N,Gr}(G) - -function DirectPower(G::Group, H::Group) - G == H || throw(DomainError( - "Direct Powers are defined only for the same groups")) - return DirectPowerGroup(G,2) -end - -DirectPower(H::Group, G::DirectPowerGroup) = DirectPower(G,H) - -function DirectPower(G::DirectPowerGroup{N}, H::Group) where N - G.group == H || throw(DomainError( - "Direct Powers are defined only for the same groups")) - return DirectPowerGroup(G.group, N+1) -end - -struct DirectPowerGroupElem{N, T<:GroupElem} <: GroupElem - elts::NTuple{N,T} -end - -function DirectPowerGroupElem(v::Vector{GrEl}) where GrEl<:GroupElem - return DirectPowerGroupElem(tuple(v...)) -end - -############################################################################### -# -# Type and parent object methods -# -############################################################################### - -elem_type(::Type{DirectPowerGroup{N,T}}) where {N,T} = - DirectPowerGroupElem{N, elem_type(T)} - -parent_type(::Type{DirectPowerGroupElem{N,T}}) where {N,T} = - DirectPowerGroup{N, parent_type(T)} - -parent(g::DirectPowerGroupElem{N, T}) where {N,T} = - DirectPowerGroup(parent(first(g.elts)), N) - -############################################################################### -# -# AbstractVector interface -# -############################################################################### - -Base.size(g::DirectPowerGroupElem{N}) where N = (N,) -Base.IndexStyle(::Type{DirectPowerGroupElem}) = Base.LinearFast() -Base.getindex(g::DirectPowerGroupElem, i::Int) = g.elts[i] - -############################################################################### -# -# Parent object call overloads -# -############################################################################### - -""" - (G::DirectPowerGroup)(a::Vector, check::Bool=true) -Constructs element of the `n`-fold direct product group `G` by coercing each -element of vector `a` to `G.group`. If `check` flag is set to `false` neither -check on the correctness nor coercion is performed. -""" -function (G::DirectPowerGroup{N})(a::Vector, check::Bool=true) where N - if check - N == length(a) || throw(DomainError( - "Can not coerce to DirectPowerGroup: lengths differ")) - a = (G.group).(a) - end - return DirectPowerGroupElem(a) -end - -function (G::DirectPowerGroup{N})(a::NTuple{N, GrEl}) where {N, GrEl} - return DirectPowerGroupElem(G.group.(a)) -end - -(G::DirectPowerGroup{N})(a::Vararg{GrEl, N}) where {N, GrEl} = DirectPowerGroupElem(G.group.(a)) - -function Base.one(G::DirectPowerGroup{N}) where N - return DirectPowerGroupElem(ntuple(i->one(G.group),N)) -end - -(G::DirectPowerGroup)(g::DirectPowerGroupElem) = G(g.elts) - -############################################################################### -# -# Basic manipulation -# -############################################################################### - -function hash(G::DirectPowerGroup{N}, h::UInt) where N - return hash(G.group, hash(N, hash(DirectPowerGroup,h))) -end - -function hash(g::DirectPowerGroupElem, h::UInt) - return hash(g.elts, hash(DirectPowerGroupElem, h)) -end - -############################################################################### -# -# String I/O -# -############################################################################### - -function show(io::IO, G::DirectPowerGroup{N}) where N - print(io, "$(N)-fold direct product of $(G.group)") -end - -function show(io::IO, g::DirectPowerGroupElem) - print(io, "[$(join(g.elts,","))]") -end - -############################################################################### -# -# Comparison -# -############################################################################### - -function (==)(G::DirectPowerGroup{N}, H::DirectPowerGroup{M}) where {N,M} - N == M || return false - G.group == H.group || return false - return true -end - -(==)(g::DirectPowerGroupElem, h::DirectPowerGroupElem) = g.elts == h.elts - -############################################################################### -# -# Group operations -# -############################################################################### - -function *(g::DirectPowerGroupElem{N}, h::DirectPowerGroupElem{N}, check::Bool=true) where N - if check - parent(g) == parent(h) || throw(DomainError( - "Can not multiply elements of different groups!")) - end - return DirectPowerGroupElem(ntuple(i-> g.elts[i]*h.elts[i], N)) -end - -^(g::DirectPowerGroupElem, n::Integer) = Base.power_by_squaring(g, n) - -function inv(g::DirectPowerGroupElem{N}) where {N} - return DirectPowerGroupElem(ntuple(i-> inv(g.elts[i]), N)) -end - -############################################################################### -# -# Misc -# -############################################################################### - -order(G::DirectPowerGroup{N}) where N = order(G.group)^N - -function iterate(G::DirectPowerGroup{N}) where N - elts = collect(G.group) - - indices = CartesianIndices(ntuple(i -> order(G.group), N)) - idx, s = iterate(indices) - g = DirectPowerGroupElem(ntuple(i -> elts[idx[i]], N)) - return g, (elts, indices, s) -end - -function iterate(G::DirectPowerGroup{N}, state) where N - elts, indices, s = state - res = iterate(indices, s) - if res == nothing - return nothing - else - idx, s = res - end - g = DirectPowerGroupElem(ntuple(i -> elts[idx[i]], N)) - return g, (elts, indices, s) -end - -eltype(::Type{DirectPowerGroup{N, G}}) where {N, G} = DirectPowerGroupElem{N, elem_type(G)} -Base.length(G::DirectPowerGroup) = order(G) diff --git a/src/FPGroups.jl b/src/FPGroups.jl deleted file mode 100644 index ff25389..0000000 --- a/src/FPGroups.jl +++ /dev/null @@ -1,135 +0,0 @@ -############################################################################### -# -# FPSymbol/FPGroupElem/FPGroup definition -# -############################################################################### - -struct FPSymbol <: GSymbol - id::Symbol - pow::Int -end - -FPGroupElem = GroupWord{FPSymbol} - -mutable struct FPGroup <: AbstractFPGroup - gens::Vector{FPSymbol} - rels::Dict{FreeGroupElem, FreeGroupElem} - - function FPGroup(gens::Vector{T}, rels::Dict{FreeGroupElem, FreeGroupElem}) where {T<:GSymbol} - G = new(gens) - G.rels = Dict(G(k) => G(v) for (k,v) in rels) - return G - end -end - -export FPGroupElem, FPGroup - -############################################################################### -# -# Type and parent object methods -# - -AbstractAlgebra.elem_type(::Type{FPGroup}) = FPGroupElem -AbstractAlgebra.parent_type(::Type{FPGroupElem}) = FPGroup - -############################################################################### -# -# FPSymbol constructors -# - -FPSymbol(s::Symbol) = FPSymbol(s, 1) -FPSymbol(s::String) = FPSymbol(Symbol(s)) -FPSymbol(s::GSymbol) = FPSymbol(s.id, s.pow) - -FPGroup(n::Int, symbol::String="f") = FPGroup([Symbol(symbol,i) for i in 1:n]) -FPGroup(a::AbstractVector) = FPGroup([FPSymbol(i) for i in a]) -FPGroup(gens::Vector{FPSymbol}) = FPGroup(gens, Dict{FreeGroupElem, FreeGroupElem}()) - -FPGroup(H::FreeGroup) = FPGroup([FPSymbol(s) for s in H.gens]) - -############################################################################### -# -# Parent object call overloads -# - -function (G::FPGroup)(w::GWord) - if isempty(w) - return one(G) - end - - @boundscheck for s in syllables(w) - i = findfirst(g -> g.id == s.id, G.gens) - i == 0 && throw(DomainError("Symbol $s does not belong to $G.")) - s.pow % G.gens[i].pow != 0 && throw( - DomainError("Symbol $s doesn't belong to $G.")) - end - - w = FPGroupElem(FPSymbol.(syllables(w))) - setparent!(w, G) - return reduce!(w) -end - -(G::FPGroup)(s::GSymbol) = G(FPGroupElem(s)) - -############################################################################### -# -# String I/O -# - -function show(io::IO, G::FPGroup) - print(io, "FPgroup on $(length(G.gens)) generators ") - strrels = join(G.rels, ", ") - if length(strrels) > 200 - print(io, "⟨ ", join(G.gens, ", "), " | $(length(G.rels)) relation(s) ⟩.") - else - print(io, "⟨ ", join(G.gens, ", "), " | ", join(G.rels, ", "), " ⟩.") - end -end - -function reduce!(W::FPGroupElem) - reduced = false - while !reduced - W = replace(W, parent(W).rels) - reduced = freereduce!(Bool, W) - end - return W -end - -############################################################################### -# -# Misc -# -############################################################################### - -freepreimage(G::FPGroup) = parent(first(keys(G.rels))) -freepreimage(g::FPGroupElem) = freepreimage(parent(g))(syllables(g)) - -function add_rels!(G::FPGroup, newrels::Dict{FreeGroupElem,FreeGroupElem}) - for w in keys(newrels) - haskey(G.rels, w) && continue - G.rels[w] = newrels[w] - end - return G -end - -function Base.:/(G::FPGroup, newrels::Vector{FPGroupElem}) - for r in newrels - parent(r) == G || throw(DomainError( - "Can not form quotient group: $r is not an element of $G")) - end - H = deepcopy(G) - F = freepreimage(H) - newrels = Dict(freepreimage(r) => one(F) for r in newrels) - add_rels!(H, newrels) - return H -end - -function Base.:/(F::FreeGroup, rels::Vector{FreeGroupElem}) - for r in rels - parent(r) == F || throw(DomainError( - "Can not form quotient group: $r is not an element of $F")) - end - G = FPGroup(FPSymbol.(F.gens)) - G.rels = Dict(rel => one(F) for rel in unique(rels)) - return G -end diff --git a/src/FreeGroup.jl b/src/FreeGroup.jl deleted file mode 100644 index 9800d62..0000000 --- a/src/FreeGroup.jl +++ /dev/null @@ -1,73 +0,0 @@ -############################################################################### -# -# FreeSymbol/FreeGroupElem/FreeGroup definition -# - -struct FreeSymbol <: GSymbol - id::Symbol - pow::Int -end - -FreeGroupElem = GroupWord{FreeSymbol} - -mutable struct FreeGroup <: AbstractFPGroup - gens::Vector{FreeSymbol} - - function FreeGroup(gens::AbstractVector{T}) where {T<:GSymbol} - G = new(gens) - G.gens = gens - return G - end -end - -export FreeGroupElem, FreeGroup - -############################################################################### -# -# Type and parent object methods -# - -AbstractAlgebra.elem_type(::Type{FreeGroup}) = FreeGroupElem -AbstractAlgebra.parent_type(::Type{FreeGroupElem}) = FreeGroup - -############################################################################### -# -# FreeSymbol constructors -# - -FreeSymbol(s::Symbol) = FreeSymbol(s,1) -FreeSymbol(s::AbstractString) = FreeSymbol(Symbol(s)) -FreeSymbol(s::GSymbol) = FreeSymbol(s.id, s.pow) - -FreeGroup(n::Int, symbol::String="f") = FreeGroup([Symbol(symbol,i) for i in 1:n]) -FreeGroup(a::AbstractVector) = FreeGroup(FreeSymbol.(a)) - -############################################################################### -# -# Parent object call overloads -# - -function (G::FreeGroup)(w::GroupWord{FreeSymbol}) - for s in syllables(w) - i = findfirst(g -> g.id == s.id, G.gens) - isnothing(i) && throw(DomainError( - "Symbol $s does not belong to $G.")) - s.pow % G.gens[i].pow == 0 || throw(DomainError( - "Symbol $s doesn't belong to $G.")) - end - setparent!(w, G) - return reduce!(w) -end - -(G::FreeGroup)(s::GSymbol) = G(FreeGroupElem(s)) -(G::FreeGroup)(v::AbstractVector{<:GSymbol}) = G(FreeGroupElem(FreeSymbol.(v))) - -############################################################################### -# -# String I/O -# - -function show(io::IO, G::FreeGroup) - print(io, "Free group on $(length(G.gens)) generators: ") - join(io, G.gens, ", ") -end diff --git a/src/Groups.jl b/src/Groups.jl index 7d2d607..93924cf 100644 --- a/src/Groups.jl +++ b/src/Groups.jl @@ -1,162 +1,23 @@ module Groups -using AbstractAlgebra -import AbstractAlgebra: Group, GroupElem, Ring -import AbstractAlgebra: parent, parent_type, elem_type -import AbstractAlgebra: order, gens, matrix_repr - -import Base: length, ==, hash, show, convert, eltype, iterate -import Base: inv, reduce, *, ^, power_by_squaring -import Base: findfirst, findnext, findlast, findprev, replace -import Base: deepcopy_internal - -using LinearAlgebra +using GroupsCore using ThreadsX +import KnuthBendix +import KnuthBendix: AbstractWord, Alphabet, Word +import KnuthBendix: alphabet +import Random -export gens, FreeGroup, Aut, SAut +import OrderedCollections: OrderedSet + +export Alphabet, AutomorphismGroup, FreeGroup, FreeGroup, FPGroup, FPGroupElement, SpecialAutomorphismGroup +export alphabet, evaluate, word include("types.jl") - -include("FreeGroup.jl") -include("FPGroups.jl") -include("AutGroup.jl") - -include("symbols.jl") -include("fallbacks.jl") -include("words.jl") include("hashing.jl") -include("freereduce.jl") -include("arithmetic.jl") -include("findreplace.jl") +include("normalform.jl") +include("autgroups.jl") -include("DirectPower.jl") -include("WreathProducts.jl") - -############################################################################### -# -# String I/O -# - -function Base.show(io::IO, W::GWord) - if length(W) == 0 - print(io, "(id)") - else - join(io, (string(s) for s in syllables(W)), "*") - end -end - -function Base.show(io::IO, s::T) where {T<:GSymbol} - if s.pow == 1 - print(io, string(s.id)) - else - print(io, "$(s.id)^$(s.pow)") - end -end - -############################################################################### -# -# Misc -# - -""" - gens(G::AbstractFPGroups) -Return vector of generators of `G`, as its elements. -""" -AbstractAlgebra.gens(G::AbstractFPGroup) = G.(G.gens) - -""" - wlmetric_ball(S::AbstractVector{<:GroupElem} - [, center=one(first(S)); radius=2, op=*]) -Compute metric ball as a list of elements of non-decreasing length, given the -word-length metric on the group generated by `S`. The ball is centered at `center` -(by default: the identity element). `radius` and `op` keywords specify the -radius and multiplication operation to be used. -""" -function wlmetric_ball_serial( - S::AbstractVector{T}; - radius = 2, - op = *, -) where {T<:Union{GroupElem,NCRingElem}} - old = unique!([one(first(S)), S...]) - sizes = [1, length(old)] - for i = 2:radius - new = collect(op(o, s) for o in @view(old[sizes[end-1]:end]) for s in S) - append!(old, new) - resize!(new, 0) - old = unique!(old) - push!(sizes, length(old)) - end - return old, sizes[2:end] -end - -function wlmetric_ball_thr( - S::AbstractVector{T}; - radius = 2, - op = *, -) where {T<:Union{GroupElem,NCRingElem}} - old = unique!([one(first(S)), S...]) - sizes = [1, length(old)] - for r = 2:radius - begin - new = ThreadsX.collect( - op(o, s) for o in @view(old[sizes[end-1]:end]) for s in S - ) - ThreadsX.foreach(hash, new) - end - append!(old, new) - resize!(new, 0) - old = ThreadsX.unique(old) - push!(sizes, length(old)) - end - return old, sizes[2:end] -end - -function wlmetric_ball_serial( - S::AbstractVector{T}, - center::T; - radius = 2, - op = *, -) where {T<:Union{GroupElem,NCRingElem}} - E, sizes = wlmetric_ball_serial(S, radius = radius, op = op) - isone(center) && return E, sizes - return c .* E, sizes -end - -function wlmetric_ball_thr( - S::AbstractVector{T}, - center::T; - radius = 2, - op = *, -) where {T<:Union{GroupElem,NCRingElem}} - E, sizes = wlmetric_ball_thr(S, radius = radius, op = op) - isone(center) && return E, sizes - return c .* E, sizes -end - -function wlmetric_ball( - S::AbstractVector{T}, - center::T = one(first(S)); - radius = 2, - op = *, - threading = true, -) where {T<:Union{GroupElem,NCRingElem}} - threading && return wlmetric_ball_thr(S, center, radius = radius, op = op) - return return wlmetric_ball_serial(S, center, radius = radius, op = op) -end - -""" - image(w::GWord, homomorphism; kwargs...) -Evaluate homomorphism `homomorphism` on a group word (element) `w`. -`homomorphism` needs to implement -> `hom(w; kwargs...)`, -where `hom(;kwargs...)` returns the value at the identity element. -""" -function image(w::GWord, hom; kwargs...) - return reduce( - *, - (hom(s; kwargs...) for s in syllables(w)), - init = hom(; kwargs...), - ) -end +include("groups/sautFn.jl") +include("wl_ball.jl") end # of module Groups diff --git a/src/WreathProducts.jl b/src/WreathProducts.jl deleted file mode 100644 index a6d4ab2..0000000 --- a/src/WreathProducts.jl +++ /dev/null @@ -1,210 +0,0 @@ -export WreathProduct, WreathProductElem - -import AbstractAlgebra: AbstractPermutationGroup, AbstractPerm - -############################################################################### -# -# WreathProduct / WreathProductElem -# -############################################################################### - -""" - WreathProduct(N, P) <: Group -Return the wreath product of a group `N` by permutation group `P`, usually -written as `N ≀ P`. The multiplication inside wreath product is defined as -> `(n, σ) * (m, τ) = (n*σ(m), στ)` -where `σ(m)` denotes the action (from the right) of the permutation group on -`n-tuples` of elements from `N` - -# Arguments: -* `N::Group` : the single factor of the `DirectPower` group `N` -* `P::AbstractPermutationGroup` acting on `DirectPower` of `N` -""" -struct WreathProduct{N, T<:Group, PG<:AbstractPermutationGroup} <: Group - N::DirectPowerGroup{N, T} - P::PG - - function WreathProduct(G::Gr, P::PG) where - {Gr <: Group, PG <: AbstractPermutationGroup} - N = DirectPowerGroup(G, Int(P.n)) - return new{Int(P.n), Gr, PG}(N, P) - end -end - -struct WreathProductElem{N, T<:GroupElem, P<:AbstractPerm} <: GroupElem - n::DirectPowerGroupElem{N, T} - p::P - - function WreathProductElem(n::DirectPowerGroupElem{N,T}, p::P, - check::Bool=true) where {N, T, P<:AbstractPerm} - if check - N == length(p.d) || throw(DomainError( - "Can't form WreathProductElem: lengths differ")) - end - return new{N, T, P}(n, p) - end -end - -############################################################################### -# -# Type and parent object methods -# -############################################################################### - -elem_type(::Type{WreathProduct{N, T, PG}}) where {N, T, PG} = WreathProductElem{N, elem_type(T), elem_type(PG)} - -parent_type(::Type{WreathProductElem{N, T, P}}) where {N, T, P} = - WreathProduct{N, parent_type(T), parent_type(P)} - -parent(g::WreathProductElem) = WreathProduct(parent(g.n[1]), parent(g.p)) - -############################################################################### -# -# Parent object call overloads -# -############################################################################### - -function (G::WreathProduct{N})(g::WreathProductElem{N}) where {N} - n = G.N(g.n) - p = G.P(g.p) - return WreathProductElem(n, p) -end - -""" - (G::WreathProduct)(n::DirectPowerGroupElem, p::Generic.Perm) -Create an element of wreath product `G` by coercing `n` and `p` to `G.N` and -`G.P`, respectively. -""" -(G::WreathProduct)(n::DirectPowerGroupElem, p::Generic.Perm) = WreathProductElem(n,p) - -Base.one(G::WreathProduct) = WreathProductElem(one(G.N), one(G.P), false) - -""" - (G::WreathProduct)(p::Generic.Perm) -Return the image of permutation `p` in `G` via embedding `p → (id,p)`. -""" -(G::WreathProduct)(p::Generic.Perm) = G(one(G.N), p) - -""" - (G::WreathProduct)(n::DirectPowerGroupElem) -Return the image of `n` in `G` via embedding `n → (n, ())`. This is the -embedding that makes the sequence `1 → N → G → P → 1` exact. -""" -(G::WreathProduct)(n::DirectPowerGroupElem) = G(n, one(G.P)) - -(G::WreathProduct)(n,p) = G(G.N(n), G.P(p)) - -############################################################################### -# -# Basic manipulation -# -############################################################################### - -function hash(G::WreathProduct, h::UInt) - return hash(G.N, hash(G.P, hash(WreathProduct, h))) -end - -function hash(g::WreathProductElem, h::UInt) - return hash(g.n, hash(g.p, hash(WreathProductElem, h))) -end - -############################################################################### -# -# String I/O -# -############################################################################### - -function show(io::IO, G::WreathProduct) - print(io, "Wreath Product of $(G.N.group) by $(G.P)") -end - -function show(io::IO, g::WreathProductElem) - print(io, "($(g.n)≀$(g.p))") -end - -############################################################################### -# -# Comparison -# -############################################################################### - -function (==)(G::WreathProduct, H::WreathProduct) - G.N == H.N || return false - G.P == H.P || return false - return true -end - -function (==)(g::WreathProductElem, h::WreathProductElem) - g.n == h.n || return false - g.p == h.p || return false - return true -end - -############################################################################### -# -# Group operations -# -############################################################################### - -(p::Generic.Perm)(n::DirectPowerGroupElem) = DirectPowerGroupElem(n.elts[p.d]) - -""" - *(g::WreathProductElem, h::WreathProductElem) -Return the group operation of wreath product elements, i.e. -> `g*h = (g.n*g.p(h.n), g.p*h.p)`, -where `g.p(h.n)` denotes the action of `g.p::Generic.Perm` on -`h.n::DirectPowerGroupElem` via standard permutation of coordinates. -""" -function *(g::WreathProductElem, h::WreathProductElem) - return WreathProductElem(g.n*g.p(h.n), g.p*h.p, false) -end - -^(g::WreathProductElem, n::Integer) = Base.power_by_squaring(g, n) - -""" - inv(g::WreathProductElem) -Return the inverse of element of a wreath product, according to the formula -> `g^-1 = (g.n, g.p)^-1 = (g.p^-1(g.n^-1), g.p^-1)`. -""" -function inv(g::WreathProductElem) - pinv = inv(g.p) - return WreathProductElem(pinv(inv(g.n)), pinv, false) -end - -############################################################################### -# -# Misc -# -############################################################################### - -matrix_repr(g::WreathProductElem) = Any[matrix_repr(g.p) g.n] - -function iterate(G::WreathProduct) - n, state_N = iterate(G.N) - p, state_P = iterate(G.P) - return G(n,p), (state_N, p, state_P) -end - -function iterate(G::WreathProduct, state) - state_N, p, state_P = state - res = iterate(G.N, state_N) - - if res == nothing - resP = iterate(G.P, state_P) - if resP == nothing - return nothing - else - n, state_N = iterate(G.N) - p, state_P = resP - end - else - n, state_N = res - end - - return G(n,p), (state_N, p, state_P) -end - -eltype(::Type{WreathProduct{N,G,PG}}) where {N,G,PG} = WreathProductElem{N, elem_type(G), elem_type(PG)} - -order(G::WreathProduct) = order(G.P)*order(G.N) -length(G::WreathProduct) = order(G) diff --git a/src/arithmetic.jl b/src/arithmetic.jl deleted file mode 100644 index f84527d..0000000 --- a/src/arithmetic.jl +++ /dev/null @@ -1,93 +0,0 @@ -function Base.inv(W::T) where T<:GWord - length(W) == 0 && return one(W) - G = parent(W) - w = T([inv(s) for s in Iterators.reverse(syllables(W))]) - return setparent!(w, G) -end - -############################################################################### -# -# Binary operators -# - -function Base.push!(w::GWord{T}, s::T) where T <: GSymbol - push!(syllables(w), s) - return w -end - -function Base.pushfirst!(w::GWord{T}, s::T) where T <: GSymbol - pushfirst!(syllables(w), s) - return w -end - -function Base.append!(w::T, v::T) where T <: GWord - append!(syllables(w), syllables(v)) - return w -end - -function Base.prepend!(w::T, v::T) where T <: GWord - prepend!(syllables(w), syllables(v)) - return w -end - -Base.append!(w::T, v::T, others::Vararg{T,N}) where {N,T <: GWord} = - append!(append!(w, v), others...) - -function rmul!(out::T, x::T, y::T) where T<: GWord - if out === x - out = deepcopy(out) - return freereduce!(append!(out, y)) - elseif out === y - out = deepcopy(out) - return freereduce!(prepend!(out, x)) - else - slenx = syllablelength(x) - sleny = syllablelength(y) - resize!(syllables(out), slenx+sleny) - syllables(out)[1:slenx] .= syllables(x) - syllables(out)[slenx+1:slenx+sleny] .= syllables(y) - return freereduce!(out) - end -end - -rmul!(out::T, v::T) where T<:GWord = freereduce!(append!(out, v)) -lmul!(out::T, v::T) where T<:GWord = freereduce!(prepend!(out, v)) - -lmul!(out::T, x::T, y::T) where T <: GWord = rmul!(out, y, x) - -AbstractAlgebra.mul!(out::T, x::T, y::T) where T <: GWord = rmul!(out, x, y) - -(*)(W::GW, Z::GW) where GW <: GWord = rmul!(deepcopy(W), W, Z) -(*)(W::GWord, s::GSymbol) = freereduce!(push!(deepcopy(W), s)) -(*)(s::GSymbol, W::GWord) = freereduce!(pushfirst!(deepcopy(W), s)) - -function power_by_squaring(W::GWord, p::Integer) - if p < 0 - return power_by_squaring(inv(W), -p) - elseif p == 0 - return one(W) - elseif p == 1 - return W - elseif p == 2 - return W*W - end - W = deepcopy(W) - t = trailing_zeros(p) + 1 - p >>= t - while (t -= 1) > 0 - append!(W, W) - end - Z = deepcopy(W) - while p > 0 - t = trailing_zeros(p) + 1 - p >>= t - while (t -= 1) >= 0 - append!(W, W) - end - append!(Z, W) - end - - return freereduce!(Z) -end - -(^)(x::GWord, n::Integer) = power_by_squaring(x,n) diff --git a/src/autgroups.jl b/src/autgroups.jl new file mode 100644 index 0000000..d214005 --- /dev/null +++ b/src/autgroups.jl @@ -0,0 +1,107 @@ +function KnuthBendix.Alphabet(S::AbstractVector{<:GSymbol}) + S = unique!([S; inv.(S)]) + inversions = [findfirst(==(inv(s)), S) for s in S] + return Alphabet(S, inversions) +end + +struct AutomorphismGroup{G<:Group,T,R,S} <: AbstractFPGroup + group::G + gens::Vector{T} + rws::R + domain::S +end + +object(G::AutomorphismGroup) = G.group +rewriting(G::AutomorphismGroup) = G.rws + +function equality_data(f::FPGroupElement{<:AutomorphismGroup}) + imf = evaluate(f) + # return normalform!.(imf) + + tmp = one(first(imf)) + for g in imf + normalform!(tmp, g) + copyto!(g, tmp) + end + return imf +end + +function Base.:(==)(g::A, h::A) where {A<:FPGroupElement{<:AutomorphismGroup}} + @assert parent(g) === parent(h) + + if _isvalidhash(g) && _isvalidhash(h) + hash(g) != hash(h) && return false + end + + length(word(g)) > 8 && normalform!(g) + length(word(h)) > 8 && normalform!(h) + + word(g) == word(h) && return true + + img_computed, imh_computed = false, false + + if !_isvalidhash(g) + img = equality_data(g) + _update_savedhash!(g, img) + img_computed = true + end + if !_isvalidhash(h) + imh = equality_data(h) + _update_savedhash!(h, imh) + imh_computed = true + end + + @assert _isvalidhash(g) + @assert _isvalidhash(h) + + hash(g) != hash(h) && return false + + # words are different, but hashes agree + if !img_computed + img = equality_data(g) + end + if !imh_computed + imh = equality_data(h) + end + + equal = img == imh + equal || @warn "hash collision in == :" g h + + return equal +end + +function Base.isone(g::FPGroupElement{<:AutomorphismGroup}) + if length(word(g)) > 8 + normalform!(g) + end + return evaluate(g) == parent(g).domain +end + +# eye-candy + +Base.show(io::IO, ::Type{<:FPGroupElement{<:AutomorphismGroup{T}}}) where {T} = + print(io, "Automorphism{$T,…}") + +Base.show(io::IO, A::AutomorphismGroup) = print(io, "automorphism group of ", object(A)) + +## Automorphism Evaluation + +domain(f::FPGroupElement{<:AutomorphismGroup}) = deepcopy(parent(f).domain) +# tuple(gens(object(parent(f)))...) + +evaluate(f::FPGroupElement{<:AutomorphismGroup}) = evaluate!(domain(f), f) + +function evaluate!( + t::NTuple{N,T}, + f::FPGroupElement{<:AutomorphismGroup{<:Group}}, + tmp = one(first(t)), +) where {N, T} + A = alphabet(f) + AF = alphabet(object(parent(f))) + for idx in word(f) + t = @inbounds evaluate!(t, A[idx], AF, tmp)::NTuple{N,T} + end + return t +end + +evaluate!(t::NTuple{N, T}, s::GSymbol, A, tmp=one(first(t))) where {N, T} = throw("you need to implement `evaluate!(::$(typeof(t)), ::$(typeof(s)), ::Alphabet, tmp=one(first(t)))`") diff --git a/src/fallbacks.jl b/src/fallbacks.jl deleted file mode 100644 index 8fccc98..0000000 --- a/src/fallbacks.jl +++ /dev/null @@ -1,16 +0,0 @@ -# workarounds -Base.one(G::Generic.SymmetricGroup) = Generic.Perm(G.n) - -# fallback definitions -# note: the user should implement those on type, when possible -Base.eltype(w::GW) where GW<:GWord = eltype(GW) -AbstractAlgebra.elem_type(G::Gr) where Gr <:AbstractFPGroup = elem_type(Gr) - -AbstractAlgebra.parent_type(g::Gw) where Gw <:GWord = parent_type(parent(Gr)) - -function Base.one(G::Gr) where Gr <: AbstractFPGroup - El = elem_type(G) - id = El(eltype(El)[]) - id.parent = G - return id -end diff --git a/src/findreplace.jl b/src/findreplace.jl deleted file mode 100644 index 50fc358..0000000 --- a/src/findreplace.jl +++ /dev/null @@ -1,182 +0,0 @@ -############################################################################### -# -# Replacement of symbols / sub-words -# - -issubsymbol(s::GSymbol, t::GSymbol) = - s.id == t.id && (0 ≤ s.pow ≤ t.pow || 0 ≥ s.pow ≥ t.pow) - -function issubsymbol(s::FreeSymbol, w::GWord, sindex::Integer) - @boundscheck 1 ≤ sindex ≤ syllablelength(w) || throw(BoundsError(w, sindex)) - return issubsymbol(s, syllables(w)[sindex]) -end - -function issubword(z::GWord, w::GWord, sindex::Integer) - isempty(z) && return true - @boundscheck 1 ≤ sindex ≤ syllablelength(w) || throw(BoundsError(w, sindex)) - n = syllablelength(z) - n == 1 && return issubsymbol(first(syllables(z)), syllables(w)[sindex]) - - lastindex = sindex + n - 1 - lastindex > syllablelength(w) && return false - - issubsymbol(first(z), syllables(w)[sindex]) || return false - issubsymbol(syllables(z)[end], syllables(w)[lastindex]) || return false - for (zidx, widx) in zip(2:n-1, sindex+1:lastindex-1) - syllables(z)[zidx] == syllables(w)[widx] || return false - end - return true -end - -"""doc -Find the first syllable index k>=i such that Z < syllables(W)[k:k+syllablelength(Z)-1] -""" -function findnext(subword::GWord, word::GWord, start::Integer) - @boundscheck 1 ≤ start ≤ syllablelength(word) || throw(BoundsError(word, start)) - isempty(subword) && return start - stop = syllablelength(word) - syllablelength(subword) +1 - - for idx in start:1:stop - issubword(subword, word, idx) && return idx - end - return nothing -end - -function findnext(s::FreeSymbol, word::GWord, start::Integer) - @boundscheck 1 ≤ start ≤ syllablelength(word) || throw(BoundsError(word, start)) - isone(s) && return start - stop = syllablelength(word) - - for idx in start:1:stop - issubsymbol(s, word, idx) && return idx - end - return nothing -end - -function findprev(subword::GWord, word::GWord, start::Integer) - @boundscheck 1 ≤ start ≤ syllablelength(word) || throw(BoundsError(word, start)) - isempty(subword) && return start - stop = 1 - - for idx in start:-1:1 - issubword(subword, word, idx) && return idx - end - return nothing -end - -function findprev(s::FreeSymbol, word::GWord, start::Integer) - @boundscheck 1 ≤ start ≤ syllablelength(word) || throw(BoundsError(word, start)) - isone(s) && return start - stop = 1 - - for idx in start:-1:stop - issubsymbol(s, word, idx) && return idx - end - return nothing -end - -findfirst(subword::GWord, word::GWord) = findnext(subword, word, 1) -findlast(subword::GWord, word::GWord) = - findprev(subword, word, syllablelength(word)-syllablelength(subword)+1) - -function replace!(out::GW, W::GW, lhs_rhs::Pair{GS, T}; count::Integer=typemax(Int)) where - {GS<:GSymbol, T<:GWord, GW<:GWord} - (count == 0 || isempty(W)) && return W - count < 0 && throw(DomainError(count, "`count` must be non-negative.")) - - lhs, rhs = lhs_rhs - - sW = syllables(W) - sW_idx = 1 - r = something(findnext(lhs, W, sW_idx), 0) - - sout = syllables(out) - resize!(sout, 0) - sizehint!(sout, syllablelength(W)) - - c = 0 - - while !iszero(r) - append!(sout, view(sW, sW_idx:r-1)) - a, b = divrem(sW[r].pow, lhs.pow) - - if b != 0 - push!(sout, change_pow(sW[r], b)) - end - - append!(sout, repeat(syllables(rhs), a)) - - sW_idx = r+1 - sW_idx > syllablelength(W) && break - - r = something(findnext(lhs, W, sW_idx), 0) - c += 1 - c == count && break - end - append!(sout, sW[sW_idx:end]) - return freereduce!(out) -end - -function replace!(out::GW, W::GW, lhs_rhs::Pair{T, T}; count::Integer=typemax(Int)) where - {GW<:GWord, T <: GWord} - (count == 0 || isempty(W)) && return W - count < 0 && throw(DomainError(count, "`count` must be non-negative.")) - - lhs, rhs = lhs_rhs - lhs_slen = syllablelength(lhs) - lhs_slen == 1 && return replace!(out, W, first(syllables(lhs))=>rhs; count=count) - - sW = syllables(W) - sW_idx = 1 - r = something(findnext(lhs, W, sW_idx), 0) - - sout = syllables(out) - resize!(sout, 0) - sizehint!(sout, syllablelength(W)) - - c = 0 - - while !iszero(r) - append!(sout, view(sW, sW_idx:r-1)) - - exp = sW[r].pow - first(syllables(lhs)).pow - if exp != 0 - push!(sout, change_pow(sW[r], exp)) - end - - append!(sout, syllables(rhs)) - - exp = sW[r+lhs_slen-1].pow - last(syllables(lhs)).pow - if exp != 0 - push!(sout, change_pow(sW[r+lhs_slen-1], exp)) - end - - sW_idx = r+lhs_slen - sW_idx > syllablelength(W) && break - - r = something(findnext(lhs, W, sW_idx), 0) - c += 1 - c == count && break - end - - # copy the rest - append!(sout, sW[sW_idx:end]) - return freereduce!(out) -end - -function replace(W::GW, lhs_rhs::Pair{T, T}; count::Integer=typemax(Int)) where - {GW<:GWord, T <: GWord} - return replace!(one(W), W, lhs_rhs; count=count) -end - -function replace(W::GW, subst_dict::Dict{T,T}) where {GW<:GWord, T<:GWord} - out = W - for toreplace in reverse!(sort!(collect(keys(subst_dict)), by=length)) - replacement = subst_dict[toreplace] - if length(toreplace) > length(out) - continue - end - out = replace(out, toreplace=>replacement) - end - return out -end diff --git a/src/freereduce.jl b/src/freereduce.jl deleted file mode 100644 index 715bc30..0000000 --- a/src/freereduce.jl +++ /dev/null @@ -1,49 +0,0 @@ -############################################################################### -# -# Naive reduction -# - -function freereduce!(::Type{Bool}, w::GWord) - if syllablelength(w) == 1 - filter!(!isone, syllables(w)) - return syllablelength(w) == 1 - end - - reduced = true - @inbounds for i in 1:syllablelength(w)-1 - s, ns = syllables(w)[i], syllables(w)[i+1] - if isone(s) - continue - elseif s.id === ns.id - reduced = false - p1 = s.pow - p2 = ns.pow - - syllables(w)[i+1] = change_pow(s, p1 + p2) - syllables(w)[i] = change_pow(s, 0) - end - end - if !reduced - filter!(!isone, syllables(w)) - setmodified!(w) - end - return reduced -end - -function freereduce!(w::GWord) - reduced = false - while !reduced - reduced = freereduce!(Bool, w) - end - return w -end - -reduce!(w::GWord) = freereduce!(w) - -""" - reduce(w::GWord) -performs reduction/simplification of a group element (word in generators). -The default reduction is the reduction in the free group reduction. -More specific procedures should be dispatched on `GWord`s type parameter. -""" -reduce(w::GWord) = reduce!(deepcopy(w)) diff --git a/src/groups/gersten_relations.jl b/src/groups/gersten_relations.jl new file mode 100644 index 0000000..79014b8 --- /dev/null +++ b/src/groups/gersten_relations.jl @@ -0,0 +1,105 @@ +function gersten_alphabet(n::Integer; commutative::Bool = true) + indexing = [(i, j) for i in 1:n for j in 1:n if i ≠ j] + S = [ϱ(i, j) for (i, j) in indexing] + + if !commutative + append!(S, [λ(i, j) for (i, j) in indexing]) + end + + return Alphabet(S) +end + +function _commutation_rule( + ::Type{W}, + A::Alphabet, + x::S, + y::S, +) where {S,T,W<:AbstractWord{T}} + return W(T[A[x], A[y]]) => W(T[A[y], A[x]]) +end + +function _pentagonal_rule( + ::Type{W}, + A::Alphabet, + x::S, + y::S, + z::S, +) where {S,T,W<:AbstractWord{T}} + # x·y·x⁻¹·y⁻¹ => z, i.e. z·y·x => x·y + return W(T[A[z], A[y], A[x]]) => W(T[A[x], A[y]]) +end +function _hexagonal_rule( + ::Type{W}, + A::Alphabet, + x::S, + y::S, + z::S, + w::S, +) where {S,T,W<:AbstractWord{T}} + # x·y⁻¹·z => z·w⁻¹·x + return W(T[A[x], A[inv(y)], A[z]]) => W(T[A[z], A[w^-1], A[x]]) +end + +gersten_relations(n::Integer; commutative) = + gersten_relations(Word{UInt8}, n, commutative = commutative) + +function gersten_relations(::Type{W}, n::Integer; commutative) where {W<:AbstractWord} + @assert n > 1 "Gersten relations are defined only for n>1, got n=$n" + A = gersten_alphabet(n, commutative = commutative) + @assert length(A) <= KnuthBendix._max_alphabet_length(W) "Type $W can not represent words over alphabet with $(length(A)) letters." + + rels = Pair{W,W}[] + + for (i, j, k, l) in Iterators.product(1:n, 1:n, 1:n, 1:n) + if i ≠ j && k ≠ l && k ≠ i && k ≠ j && l ≠ i + push!(rels, _commutation_rule(W, A, ϱ(i, j), ϱ(k, l))) + + commutative && continue + + push!(rels, _commutation_rule(W, A, λ(i, j), λ(k, l))) + end + end + + if !commutative + for (i, j, k, l) in Iterators.product(1:n, 1:n, 1:n, 1:n) + if (i ≠ j && k ≠ l && k ≠ j && l ≠ i) + push!(rels, _commutation_rule(W, A, ϱ(i, j), λ(k, l))) + push!(rels, _commutation_rule(W, A, λ(i, j), ϱ(k, l))) + end + end + end + + # pentagonal rule: + # x*y*inv(x)*inv(y)=>z + + for (i, j, k) in Iterators.product(1:n, 1:n, 1:n) + if (i ≠ j && k ≠ i && k ≠ j) + push!(rels, _pentagonal_rule(W, A, ϱ(i, j)^-1, ϱ(j, k)^-1, ϱ(i, k)^-1)) + push!(rels, _pentagonal_rule(W, A, ϱ(i, j)^-1, ϱ(j, k), ϱ(i, k))) + + commutative && continue + + push!(rels, _pentagonal_rule(W, A, ϱ(i, j), λ(j, k), ϱ(i, k)^-1)) + push!(rels, _pentagonal_rule(W, A, ϱ(i, j), λ(j, k)^-1, ϱ(i, k))) + + # the same as above, but with ϱ ↔ λ: + push!(rels, _pentagonal_rule(W, A, λ(i, j)^-1, λ(j, k)^-1, λ(i, k)^-1)) + push!(rels, _pentagonal_rule(W, A, λ(i, j)^-1, λ(j, k), λ(i, k))) + + push!(rels, _pentagonal_rule(W, A, λ(i, j), ϱ(j, k), λ(i, k)^-1)) + push!(rels, _pentagonal_rule(W, A, λ(i, j), ϱ(j, k)^-1, λ(i, k))) + end + end + + if !commutative + for (i, j) in Iterators.product(1:n, 1:n) + if i ≠ j + push!(rels, _hexagonal_rule(W, A, ϱ(i, j), ϱ(j, i), λ(i, j), λ(j, i))) + w = W([A[ϱ(i, j)], A[ϱ(j, i)^-1], A[λ(i, j)]]) + push!(rels, w^2 => inv(A, w)^2) + end + end + end + + return A, rels +end diff --git a/src/groups/sautFn.jl b/src/groups/sautFn.jl new file mode 100644 index 0000000..c47612f --- /dev/null +++ b/src/groups/sautFn.jl @@ -0,0 +1,20 @@ +include("transvections.jl") +include("gersten_relations.jl") + +function SpecialAutomorphismGroup(F::FreeGroup; ordering = KnuthBendix.LenLex, kwargs...) + + n = length(alphabet(F)) ÷ 2 + A, rels = gersten_relations(n, commutative = false) + S = KnuthBendix.letters(A)[1:2(n^2-n)] + + rws = KnuthBendix.RewritingSystem(rels, ordering(A)) + KnuthBendix.knuthbendix!(rws; kwargs...) + return AutomorphismGroup(F, S, rws, ntuple(i -> gens(F, i), n)) +end + +KnuthBendix.alphabet(G::AutomorphismGroup{<:FreeGroup}) = alphabet(rewriting(G)) + +function relations(G::AutomorphismGroup{<:FreeGroup}) + n = length(alphabet(object(G))) ÷ 2 + return last(gersten_relations(n, commutative = false)) +end diff --git a/src/groups/transvections.jl b/src/groups/transvections.jl new file mode 100644 index 0000000..e405359 --- /dev/null +++ b/src/groups/transvections.jl @@ -0,0 +1,83 @@ +struct Transvection <: GSymbol + id::Symbol + ij::UInt8 + inv::Bool + + function Transvection(id::Symbol, i::Integer, j::Integer, inv = false) + @assert id in (:ϱ, :λ) + return new(id, _indices(UInt8(i), UInt8(j)), inv) + end +end + +ϱ(i, j) = Transvection(:ϱ, i, j) +λ(i, j) = Transvection(:λ, i, j) + +_indices(ij::UInt8) = (ij & 0xf0) >> 4, (ij & 0x0f) + +function _indices(i::UInt8, j::UInt8) + @boundscheck @assert i < typemax(i) ÷ 2 + @boundscheck @assert j < typemax(j) ÷ 2 + sizeof + return (i << 4) + j +end + +indices(t::Transvection) = Int.(_indices(t.ij)) + +function Base.getproperty(t::Transvection, s::Symbol) + s === :i && return first(indices(t)) + s === :j && return last(indices(t)) + return Core.getfield(t, s) +end + +function Base.show(io::IO, t::Transvection) + id = if t.id === :ϱ + 'ϱ' + else # if t.id === :λ + 'λ' + end + print(io, id, subscriptify(t.i), '.', subscriptify(t.j)) + t.inv && print(io, "^-1") +end + +Base.inv(t::Transvection) = Transvection(t.id, _indices(t.ij)..., !t.inv) + +Base.:(==)(t::Transvection, s::Transvection) = + t.id === s.id && t.ij == s.ij && t.inv == s.inv +Base.hash(t::Transvection, h::UInt) = hash(t.id, hash(t.ij, hash(t.inv, h))) + +Base.@propagate_inbounds function evaluate!(v::NTuple{T, N}, t::Transvection, A::Alphabet, tmp=one(first(v))) where {T, N} + i, j = indices(t) + @assert i ≤ length(v) && j ≤ length(v) + + @inbounds begin + if t.id === :ϱ + if !t.inv + append!(word(v[i]), word(v[j])) + else + # append!(word(v[i]), inv(A, word(v[j]))) + for l in Iterators.reverse(word(v[j])) + push!(word(v[i]), inv(A, l)) + end + end + else # if t.id === :λ + if !t.inv + # prepend!(word(v[i]), word(v[j])) + for l in Iterators.reverse(word(v[j])) + pushfirst!(word(v[i]), l) + end + else + # prepend!(word(v[i]), inv(A, word(v[j]))) + for l in word(v[j]) + pushfirst!(word(v[i]), inv(A, l)) + end + end + end + + _setnormalform!(v[i], false) + _setvalidhash!(v[i], false) + end + normalform!(tmp, v[i]) + copyto!(v[i], tmp) + + return v +end diff --git a/src/hashing.jl b/src/hashing.jl index 316bd06..b8d7e4b 100644 --- a/src/hashing.jl +++ b/src/hashing.jl @@ -1,34 +1,46 @@ -############################################################################### -# -# hashing, deepcopy and == -# +## Hashing -function hash_internal(W::GWord) - reduce!(W) - h = hasparent(W) ? hash(parent(W)) : zero(UInt) - return hash(syllables(W), hash(typeof(W), h)) -end +equality_data(g::FPGroupElement) = (normalform!(g); word(g)) -function hash(W::GWord, h::UInt=UInt(0); kwargs...) - if ismodified(W) - savehash!(W, hash_internal(W; kwargs...)) - unsetmodified!(W) - end - return xor(savedhash(W), h) -end +bitget(h::UInt, n::Int) = Bool((h & (1 << n)) >> n) +bitclear(h::UInt, n::Int) = h & ~(1 << n) +bitset(h::UInt, n::Int) = h | (1 << n) +bitset(h::UInt, v::Bool, n::Int) = v ? bitset(h, n) : bitclear(h, n) -# WARNING: Due to specialised (constant) hash function of GWords this one is actually necessary! -function Base.deepcopy_internal(W::T, dict::IdDict) where T<:GWord - G = parent(W) - g = T(deepcopy(syllables(W))) - setparent!(g, G) +# We store hash of a word in field `savedhash` to use it as cheap proxy to +# determine non-equal words. Additionally bits of `savehash` store boolean +# information as follows +# * `savedhash & 1` (the first bit): is word in normal form? +# * `savedhash & 2` (the second bit): is the hash valid? +const __BITFLAGS_MASK = ~(~(UInt(0)) << 2) + +isnormalform(g::FPGroupElement) = bitget(g.savedhash, 0) +_isvalidhash(g::FPGroupElement) = bitget(g.savedhash, 1) + +_setnormalform(h::UInt, v::Bool) = bitset(h, v, 0) +_setvalidhash(h::UInt, v::Bool) = bitset(h, v, 1) + +_setnormalform!(g::FPGroupElement, v::Bool) = g.savedhash = _setnormalform(g.savedhash, v) +_setvalidhash!(g::FPGroupElement, v::Bool) = g.savedhash = _setvalidhash(g.savedhash, v) + +# To update hash use this internal method, possibly only after computing the +# normal form of `g`: +function _update_savedhash!(g::FPGroupElement, data) + h = hash(data, hash(parent(g))) + h = (h << count_ones(__BITFLAGS_MASK)) | (__BITFLAGS_MASK & g.savedhash) + g.savedhash = _setvalidhash(h, true) return g end -function (==)(W::T, Z::T) where T <: GWord - hash(W) != hash(Z) && return false # distinguishes parent and parentless words - if hasparent(W) && hasparent(Z) - parent(W) != parent(Z) && return false - end - return syllables(W) == syllables(Z) +function Base.hash(g::FPGroupElement, h::UInt) + _isvalidhash(g) || _update_savedhash!(g, equality_data(g)) + return hash(g.savedhash >> count_ones(__BITFLAGS_MASK), h) +end + +function Base.copyto!(res::FPGroupElement, g::FPGroupElement) + @assert parent(res) === parent(g) + resize!(word(res), length(word(g))) + copyto!(word(res), word(g)) + res.savedhash = g.savedhash + return res end diff --git a/src/iteration.jl b/src/iteration.jl new file mode 100644 index 0000000..04ce088 --- /dev/null +++ b/src/iteration.jl @@ -0,0 +1,57 @@ +mutable struct FPGroupIter{S, T, GEl} + seen::S + seen_iter_state::T + current::GEl + gen_idx::Int + u_tmp::GEl + v_tmp::GEl +end + +function Base.iterate(G::AbstractFPGroup) + seen = OrderedSet([one(G)]) + current, seen_state = iterate(seen) + gr_iter = FPGroupIter(seen, seen_state, current, 1, one(G), one(G)) + return one(G), gr_iter +end + +function _next_elt!(itr::FPGroupIter) + res = iterate(itr.seen, itr.seen_iter_state) + res === nothing && return true + + itr.current = first(res) + itr.seen_iter_state = last(res) + itr.gen_idx = 1 + + return false +end + +function Base.iterate(G::AbstractFPGroup, state) + if state.gen_idx > length(alphabet(G)) + finished = _next_elt!(state) + finished && return nothing + end + + elt = let u = state.u_tmp, v = state.v_tmp, current = state.current + copyto!(v, current) + push!(word(v), state.gen_idx) + _setnormalform!(v, false) + _setvalidhash!(v, false) + @assert !isnormalform(v) + + resize!(word(u), 0) + normalform!(u, v) + end + + state.gen_idx += 1 + + if elt in state.seen + return iterate(G, state) + else + @assert isnormalform(elt) + push!(state.seen, deepcopy(elt)) + return elt, state + end +end + +# Groups.Core default: +# Base.IteratorSize(::Type{<:AbstractFPGroup}) = Base.SizeUnknown() diff --git a/src/normalform.jl b/src/normalform.jl new file mode 100644 index 0000000..f1d67ff --- /dev/null +++ b/src/normalform.jl @@ -0,0 +1,46 @@ +""" + 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 + + _setnormalform!(g, true) + _setvalidhash!(g, false) + @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) + else + resize!(word(res), 0) + normalform!(word(res), g) + _setnormalform!(res, true) + _setvalidhash!(res, false) + 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)) + return KnuthBendix.rewrite_from_left!(res, word(g), rewriting(parent(g))) +end diff --git a/src/symbols.jl b/src/symbols.jl deleted file mode 100644 index a6258bc..0000000 --- a/src/symbols.jl +++ /dev/null @@ -1,22 +0,0 @@ -change_pow(s::S, n::Integer) where S<:GSymbol = S(s.id, n) - -function Base.iterate(s::GS, i=1) where GS<:GSymbol - return i <= abs(s.pow) ? (change_pow(s, sign(s.pow)), i+1) : nothing -end -Base.size(s::GSymbol) = (abs(s.pow), ) -Base.length(s::GSymbol) = first(size(s)) - -Base.eltype(s::GS) where GS<:GSymbol = GS - -Base.isone(s::GSymbol) = iszero(s.pow) -Base.inv(s::GSymbol) = change_pow(s, -s.pow) -Base.hash(s::S, h::UInt) where S<:GSymbol = hash(s.id, hash(s.pow, hash(S, h))) - -function (==)(s::GSymbol, t::GSymbol) - isone(s) && isone(t) && return true - s.pow == t.pow && s.id == t.id && return true - return false -end - -Base.convert(::Type{GS}, s::GSymbol) where GS<:GSymbol = GS(s.id, s.pow) -Base.convert(::Type{GS}, s::GS) where GS<:GSymbol = s diff --git a/src/types.jl b/src/types.jl index e3a93bf..bed9221 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,43 +1,210 @@ -abstract type AbstractFPGroup <: Group end +## "Abstract" definitions """ - ::GSymbol -Represents a syllable. Abstract type which all group symbols of -`AbstractFPGroups` should subtype. Each concrete subtype should implement fields: - * `id` which is the `Symbol` representation/identification of a symbol - * `pow` which is the (multiplicative) exponent of a symbol. + AbstractFPGroup + +An Abstract type representing finitely presented groups. Every instance `` must implement + * `KnuthBendix.alphabet(G::MyFPGroup)` + * `rewriting(G::MyFPGroup)` : return the rewriting object which must implement + > `KnuthBendix.rewrite_from_left!(u, v, rewriting(G))`. +By default `alphabet(G)` is returned, which amounts to free rewriting in `G`. + * `relations(G::MyFPGroup)` : return a set of defining relations. + +AbstractFPGroup may also override `word_type(::Type{MyFPGroup}) = Word{UInt16}`, +which controls the word type used for group elements. If a group has more than `255` generators you need to define e.g. +> `word_type(::Type{MyFPGroup}) = Word{UInt16}` """ -abstract type GSymbol end +abstract type AbstractFPGroup <: GroupsCore.Group end -abstract type GWord{T<:GSymbol} <: GroupElem end +word_type(G::AbstractFPGroup) = word_type(typeof(G)) +# the default: +word_type(::Type{<:AbstractFPGroup}) = Word{UInt8} -""" - W::GroupWord{T} <: GWord{T<:GSymbol} <:GroupElem -Basic representation of element of a finitely presented group. -* `syllables(W)` return particular group syllables which multiplied constitute `W` -group as a word in generators. -* `parent(W)` return the parent group. +# the default (results in free rewriting) +rewriting(G::AbstractFPGroup) = alphabet(G) -As the reduction (inside the parent group) of word to normal form may be time -consuming, we provide a shortcut that is useful in practice: -`savehash!(W, h)` and `ismodified(W)` functions. -When computing `hash(W)`, a reduction to normal form is performed and a -persistent hash is stored inside `W`, setting `ismodified(W)` flag to `false`. -This hash can be accessed by `savedhash(W)`. -Future comparisons of `W` try not to perform reduction and use the stored hash as shortcut. Only when hashes collide reduction is performed. Whenever word `W` is -changed, `ismodified(W)` returns `false` and stored hash is invalidated. -""" - -mutable struct GroupWord{T} <: GWord{T} - symbols::Vector{T} - modified::Bool - savedhash::UInt - parent::Group - - function GroupWord{T}(symbols::AbstractVector{<:GSymbol}) where T - return new{T}(symbols, true, zero(UInt)) - end - GroupWord(v::AbstractVector{T}) where T<:GSymbol = GroupWord{T}(v) - GroupWord{T}(s::GSymbol) where T<:GSymbol = GroupWord{T}(T[s]) - GroupWord(s::T) where T<:GSymbol = GroupWord{T}(s) +Base.@propagate_inbounds 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)} + +include("iteration.jl") + +GroupsCore.ngens(G::AbstractFPGroup) = length(G.gens) + +function GroupsCore.gens(G::AbstractFPGroup, i::Integer) + @boundscheck 1 <= i <= GroupsCore.ngens(G) + l = alphabet(G)[G.gens[i]] + return FPGroupElement(word_type(G)([l]), 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[] + nletters = length(alphabet(G)) + return FPGroupElement(word_type(G)(rand(1:nletters, l)), G) +end + +Base.isfinite(::AbstractFPGroup) = (@warn "using generic isfinite(::AbstractFPGroup): the returned `false` might be wrong"; false) + +## 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) + KnuthBendix.print_repr(io, word(f), alphabet(f)) +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) + normalform!(g) + normalform!(h) + hash(g) != hash(h) && return false + return word(g) == word(h) +end + +function Base.deepcopy_internal(g::FPGroupElement, stackdict::IdDict) + return FPGroupElement(copy(word(g)), g.savedhash, 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 : (@warn "using generic isfiniteorder(::FPGroupElement): the returned `false` might be wrong"; false) + +# 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)) + ltrs = KnuthBendix.letters(A) + gens = Vector{eltype(ltrs)}() + invs = Vector{eltype(ltrs)}() + for l in ltrs + l ∈ invs && continue + push!(gens, l) + push!(invs, inv(A, l)) + end + + return FreeGroup(gens, A) +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)) +end + +Base.show(io::IO, F::FreeGroup) = print(io, "free group on $(ngens(F)) generators") + +# mandatory methods: +KnuthBendix.alphabet(F::FreeGroup) = F.alphabet +relations(F::FreeGroup) = Pair{eltype(F)}[] + +# GroupsCore interface: +# these are mathematically correct +Base.isfinite(::FreeGroup) = false + +GroupsCore.isfiniteorder(g::FPGroupElement{<:FreeGroup}) = isone(g) ? true : false + +## FP Groups + +struct FPGroup{T,R,S} <: AbstractFPGroup + gens::Vector{T} + relations::Vector{Pair{S,S}} + rws::R +end + +KnuthBendix.alphabet(G::FPGroup) = alphabet(rewriting(G)) +rewriting(G::FPGroup) = G.rws + +relations(G::FPGroup) = G.relations + +function FPGroup( + G::AbstractFPGroup, + rels::AbstractVector{<:Pair{GEl,GEl}}; + ordering = KnuthBendix.LenLex, + kwargs..., +) where {GEl<:FPGroupElement} + + O = ordering(alphabet(G)) + for (lhs, rhs) in rels + @assert parent(lhs) === parent(rhs) === G + end + word_rels = [word(lhs) => word(rhs) for (lhs, rhs) in [relations(G); rels]] + rws = KnuthBendix.RewritingSystem(word_rels, O) + + KnuthBendix.knuthbendix!(rws; kwargs...) + + return FPGroup(G.gens, rels, rws) +end + +function Base.show(io::IO, G::FPGroup) + print(io, "⟨") + join(io, gens(G), ", ") + print(io, " | ") + join(io, relations(G), ", ") + print(io, "⟩") +end + +## GSymbol aka letter of alphabet + +abstract type GSymbol end +Base.literal_pow(::typeof(^), t::GSymbol, ::Val{-1}) = inv(t) + +function subscriptify(n::Integer) + subscript_0 = Int(0x2080) # Char(0x2080) -> subscript 0 + return join([Char(subscript_0 + i) for i in reverse(digits(n))], "") end diff --git a/src/wl_ball.jl b/src/wl_ball.jl new file mode 100644 index 0000000..f7f4e12 --- /dev/null +++ b/src/wl_ball.jl @@ -0,0 +1,62 @@ +""" + wlmetric_ball(S::AbstractVector{<:GroupElem} + [, center=one(first(S)); radius=2, op=*]) +Compute metric ball as a list of elements of non-decreasing length, given the +word-length metric on the group generated by `S`. The ball is centered at `center` +(by default: the identity element). `radius` and `op` keywords specify the +radius and multiplication operation to be used. +""" +function wlmetric_ball_serial(S::AbstractVector{T}; radius = 2, op = *) where {T} + @assert radius > 0 + old = unique!([one(first(S)), S...]) + sizes = [1, length(old)] + for i in 2:radius + new = collect(op(o, s) for o in @view(old[sizes[end-1]:end]) for s in S) + append!(old, new) + resize!(new, 0) + old = unique!(old) + push!(sizes, length(old)) + end + return old, sizes[2:end] +end + +function wlmetric_ball_thr(S::AbstractVector{T}; radius = 2, op = *) where {T} + @assert radius > 0 + old = unique!([one(first(S)), S...]) + sizes = [1, length(old)] + for r in 2:radius + begin + new = + ThreadsX.collect(op(o, s) for o in @view(old[sizes[end-1]:end]) for s in S) + ThreadsX.foreach(hash, new) + end + append!(old, new) + resize!(new, 0) + old = ThreadsX.unique(old) + push!(sizes, length(old)) + end + return old, sizes[2:end] +end + +function wlmetric_ball_serial(S::AbstractVector{T}, center::T; radius = 2, op = *) where {T} + E, sizes = wlmetric_ball_serial(S, radius = radius, op = op) + isone(center) && return E, sizes + return c .* E, sizes +end + +function wlmetric_ball_thr(S::AbstractVector{T}, center::T; radius = 2, op = *) where {T} + E, sizes = wlmetric_ball_thr(S, radius = radius, op = op) + isone(center) && return E, sizes + return c .* E, sizes +end + +function wlmetric_ball( + S::AbstractVector{T}, + center::T = one(first(S)); + radius = 2, + op = *, + threading = true, +) where {T} + threading && return wlmetric_ball_thr(S, center, radius = radius, op = op) + return wlmetric_ball_serial(S, center, radius = radius, op = op) +end diff --git a/src/words.jl b/src/words.jl deleted file mode 100644 index 1a1c76e..0000000 --- a/src/words.jl +++ /dev/null @@ -1,43 +0,0 @@ -syllablelength(w::GWord) = length(w.symbols) -syllables(w::GWord) = w.symbols -ismodified(w::GWord) = w.modified -setmodified!(w::GWord) = (w.modified = true; w) -unsetmodified!(w::GWord) = (w.modified = false; w) -savehash!(w::GWord, h::UInt) = (w.savedhash = h; w) -savedhash(w::GWord) = w.savedhash -parent(w::GWord) = w.parent -hasparent(w::GWord) = isdefined(w, :parent) -setparent!(w::GWord, G::AbstractFPGroup) = (w.parent = G; w) - -Base.isempty(w::GWord) = isempty(syllables(w)) -Base.isone(w::GWord) = (freereduce!(w); isempty(w)) -Base.one(w::GWord) = one(parent(w)) - -function Base.iterate(w::GWord, state=(syllable=1, pow=1)) - state.syllable > syllablelength(w) && return nothing - next = iterate(syllables(w)[state.syllable], state.pow) - next === nothing && return iterate(w, (syllable=state.syllable+1, pow=1)) - return first(next), (syllable=state.syllable, pow=last(next)) -end - -Base.eltype(::Type{<:GWord{T}}) where T = T -Base.length(w::GWord) = isempty(w) ? 0 : sum(length, syllables(w)) -Base.size(w::GWord) = (length(w),) -Base.lastindex(w::GWord) = length(w) - -Base.@propagate_inbounds function Base.getindex(w::GWord, i::Integer) - csum = 0 - idx = 0 - @boundscheck 0 < i <= length(w) || throw(BoundsError(w, i)) - while csum < i - idx += 1 - csum += length(syllables(w)[idx]) - end - return first(syllables(w)[idx]) -end - -Base.@propagate_inbounds Base.getindex(w::GWord, itr) = [w[i] for i in itr] - -# no setindex! for syllable based words - -Base.convert(::Type{GW}, s::GSymbol) where GW <: GWord = GW(s) diff --git a/test/AutFn.jl b/test/AutFn.jl new file mode 100644 index 0000000..a33dc65 --- /dev/null +++ b/test/AutFn.jl @@ -0,0 +1,189 @@ +@testset "Automorphisms" begin + + @testset "Transvections" begin + + @test Groups.Transvection(:ϱ, 1, 2) isa Groups.GSymbol + @test Groups.Transvection(:ϱ, 1, 2) isa Groups.Transvection + @test Groups.Transvection(:λ, 1, 2) isa Groups.GSymbol + @test Groups.Transvection(:λ, 1, 2) isa Groups.Transvection + t = Groups.Transvection(:ϱ, 1, 2) + @test inv(t) isa Groups.GSymbol + @test inv(t) isa Groups.Transvection + + @test t != inv(t) + + s = Groups.Transvection(:ϱ, 1, 2) + @test t == s + @test hash(t) == hash(s) + + s_ = Groups.Transvection(:ϱ, 1, 3) + @test s_ != s + @test hash(s_) != hash(s) + + @test Groups.gersten_alphabet(3) isa Alphabet + A = Groups.gersten_alphabet(3) + @test length(A) == 12 + + @test sprint(show, Groups.ϱ(1, 2)) == "ϱ₁.₂" + @test sprint(show, Groups.λ(3, 2)) == "λ₃.₂" + end + + A4 = Alphabet( + [:a,:A,:b,:B,:c,:C,:d,:D], + [ 2, 1, 4, 3, 6, 5, 8, 7] + ) + + A5 = Alphabet( + [:a,:A,:b,:B,:c,:C,:d,:D,:e,:E], + [ 2, 1, 4, 3, 6, 5, 8, 7,10, 9] + ) + + F4 = FreeGroup([:a, :b, :c, :d], A4) + a,b,c,d = gens(F4) + D = ntuple(i->gens(F4, i), 4) + + @testset "Transvection action correctness" begin + i,j = 1,2 + r = Groups.Transvection(:ϱ,i,j) + l = Groups.Transvection(:λ,i,j) + + (t::Groups.Transvection)(v::Tuple) = Groups.evaluate!(v, t, A4) + + @test r(deepcopy(D)) == (a*b, b, c, d) + @test inv(r)(deepcopy(D)) == (a*b^-1,b, c, d) + @test l(deepcopy(D)) == (b*a, b, c, d) + @test inv(l)(deepcopy(D)) == (b^-1*a,b, c, d) + + i,j = 3,1 + r = Groups.Transvection(:ϱ,i,j) + l = Groups.Transvection(:λ,i,j) + @test r(deepcopy(D)) == (a, b, c*a, d) + @test inv(r)(deepcopy(D)) == (a, b, c*a^-1,d) + @test l(deepcopy(D)) == (a, b, a*c, d) + @test inv(l)(deepcopy(D)) == (a, b, a^-1*c,d) + + i,j = 4,3 + r = Groups.Transvection(:ϱ,i,j) + l = Groups.Transvection(:λ,i,j) + @test r(deepcopy(D)) == (a, b, c, d*c) + @test inv(r)(deepcopy(D)) == (a, b, c, d*c^-1) + @test l(deepcopy(D)) == (a, b, c, c*d) + @test inv(l)(deepcopy(D)) == (a, b, c, c^-1*d) + + i,j = 2,4 + r = Groups.Transvection(:ϱ,i,j) + l = Groups.Transvection(:λ,i,j) + @test r(deepcopy(D)) == (a, b*d, c, d) + @test inv(r)(deepcopy(D)) == (a, b*d^-1,c, d) + @test l(deepcopy(D)) == (a, d*b, c, d) + @test inv(l)(deepcopy(D)) == (a, d^-1*b,c, d) + end + + A = SpecialAutomorphismGroup(F4, maxrules=1000) + + @testset "AutomorphismGroup constructors" begin + @test A isa Groups.AbstractFPGroup + @test A isa AutomorphismGroup + @test alphabet(A) isa Alphabet + @test Groups.relations(A) isa Vector{<:Pair} + @test sprint(show, A) == "automorphism group of free group on 4 generators" + end + + @testset "Automorphisms: hash and evaluate" begin + @test Groups.domain(gens(A, 1)) == D + g, h = gens(A, 1), gens(A, 8) + + @test evaluate(g*h) == evaluate(h*g) + @test (g*h).savedhash == zero(UInt) + + @test sprint(show, typeof(g)) == "Automorphism{FreeGroup{Symbol},…}" + + a = g*h + b = h*g + @test hash(a) != zero(UInt) + @test hash(a) == hash(b) + @test a.savedhash == b.savedhash + + @test length(unique([a,b])) == 1 + @test length(unique([g*h, h*g])) == 1 + + # Not so simple arithmetic: applying starting on the left: + # ϱ₁₂*ϱ₂₁⁻¹*λ₁₂*ε₂ == σ₂₁₃₄ + + g = gens(A, 1) + x1, x2, x3, x4 = Groups.domain(g) + @test evaluate(g) == (x1*x2, x2, x3, x4) + + g = g*inv(gens(A, 4)) # ϱ₂₁ + @test evaluate(g) == (x1*x2, x1^-1, x3, x4) + + g = g*gens(A, 13) + @test evaluate(g) == (x2, x1^-1, x3, x4) + end + + @testset "Automorphisms: SAut(F₄)" begin + N = 4 + G = SpecialAutomorphismGroup(FreeGroup(N)) + + S = gens(G) + @test S isa Vector{<:FPGroupElement{<:AutomorphismGroup{<:FreeGroup}}} + + @test length(S) == 2*N*(N-1) + @test length(unique(S)) == length(S) + + S_sym = [S; inv.(S)] + @test length(S_sym) == length(unique(S_sym)) + + pushfirst!(S_sym, one(G)) + + B_2 = [i*j for (i,j) in Base.product(S_sym, S_sym)] + @test length(B_2) == 2401 + @test length(unique(B_2)) == 1777 + + @test all(g->isone(inv(g)*g), B_2) + @test all(g->isone(g*inv(g)), B_2) + end + + @testset "GroupsCore conformance" begin + test_Group_interface(A) + g = A(rand(1:length(alphabet(A)), 10)) + h = A(rand(1:length(alphabet(A)), 10)) + + test_GroupElement_interface(g, h) + end + +end + +# using Random +# using GroupsCore +# +# A = New.SpecialAutomorphismGroup(FreeGroup(4), maxrules=2000, ordering=KnuthBendix.RecursivePathOrder) +# +# # for seed in 1:1000 +# let seed = 68 +# N = 14 +# Random.seed!(seed) +# g = A(rand(1:length(KnuthBendix.alphabet(A)), N)) +# h = A(rand(1:length(KnuthBendix.alphabet(A)), N)) +# @info "seed=$seed" g h +# @time isone(g*inv(g)) +# @time isone(inv(g)*g) +# @info "" length(word(New.normalform!(g*inv(g)))) length(word(New.normalform!(inv(g)*g))) +# a = commutator(g, h, g) +# b = conj(inv(g), h) * conj(conj(g, h), g) +# +# @info length(word(a)) +# @info length(word(b)) +# +# w = a*inv(b) +# @info length(word(w)) +# New.normalform!(w) +# @info length(word(w)) +# +# +# # +# # @time ima = evaluate(a) +# # @time imb = evaluate(b) +# # @info "" a b ima imb +# # @time a == b +# end diff --git a/test/AutGroup-tests.jl b/test/AutGroup-tests.jl deleted file mode 100644 index 234ac60..0000000 --- a/test/AutGroup-tests.jl +++ /dev/null @@ -1,286 +0,0 @@ -@testset "Automorphisms" begin - - G = SymmetricGroup(Int8(4)) - - @testset "AutSymbol" begin - @test_throws MethodError Groups.AutSymbol(:a) - @test_throws MethodError Groups.AutSymbol(:a, 1) - f = Groups.AutSymbol(:a, 1, Groups.FlipAut(2)) - @test f isa Groups.GSymbol - @test f isa Groups.AutSymbol - @test Groups.AutSymbol(perm"(4)") isa Groups.AutSymbol - @test Groups.AutSymbol(perm"(1,2,3,4)") isa Groups.AutSymbol - @test Groups.transvection_R(1,2) isa Groups.AutSymbol - @test Groups.transvection_R(3,4) isa Groups.AutSymbol - @test Groups.flip(3) isa Groups.AutSymbol - - @test Groups.id_autsymbol() isa Groups.AutSymbol - @test inv(Groups.id_autsymbol()) isa Groups.AutSymbol - x = Groups.id_autsymbol() - @test inv(x) == Groups.id_autsymbol() - end - - a,b,c,d = gens(FreeGroup(4)) - D = NTuple{4,FreeGroupElem}([a,b,c,d]) - - @testset "flip correctness" begin - @test Groups.flip(1)(deepcopy(D)) == (a^-1, b,c,d) - @test Groups.flip(2)(deepcopy(D)) == (a, b^-1,c,d) - @test Groups.flip(3)(deepcopy(D)) == (a, b,c^-1,d) - @test Groups.flip(4)(deepcopy(D)) == (a, b,c,d^-1) - @test inv(Groups.flip(1))(deepcopy(D)) == (a^-1, b,c,d) - @test inv(Groups.flip(2))(deepcopy(D)) == (a, b^-1,c,d) - @test inv(Groups.flip(3))(deepcopy(D)) == (a, b,c^-1,d) - @test inv(Groups.flip(4))(deepcopy(D)) == (a, b,c,d^-1) - end - - @testset "perm correctness" begin - σ = Groups.AutSymbol(perm"(4)") - @test σ(deepcopy(D)) == deepcopy(D) - @test inv(σ)(deepcopy(D)) == deepcopy(D) - - σ = Groups.AutSymbol(perm"(1,2,3,4)") - @test σ(deepcopy(D)) == (b, c, d, a) - @test inv(σ)(deepcopy(D)) == (d, a, b, c) - - σ = Groups.AutSymbol(perm"(1,2)(4,3)") - @test σ(deepcopy(D)) == (b, a, d, c) - @test inv(σ)(deepcopy(D)) == (b, a, d, c) - - σ = Groups.AutSymbol(perm"(1,2,3)(4)") - @test σ(deepcopy(D)) == (b, c, a, d) - @test inv(σ)(deepcopy(D)) == (c, a, b, d) - end - - @testset "rmul/transvection_R correctness" begin - i,j = 1,2 - r = Groups.transvection_R(i,j) - l = Groups.transvection_L(i,j) - @test r(deepcopy(D)) == (a*b, b, c, d) - @test inv(r)(deepcopy(D)) == (a*b^-1,b, c, d) - @test l(deepcopy(D)) == (b*a, b, c, d) - @test inv(l)(deepcopy(D)) == (b^-1*a,b, c, d) - - i,j = 3,1 - r = Groups.transvection_R(i,j) - l = Groups.transvection_L(i,j) - @test r(deepcopy(D)) == (a, b, c*a, d) - @test inv(r)(deepcopy(D)) == (a, b, c*a^-1,d) - @test l(deepcopy(D)) == (a, b, a*c, d) - @test inv(l)(deepcopy(D)) == (a, b, a^-1*c,d) - - i,j = 4,3 - r = Groups.transvection_R(i,j) - l = Groups.transvection_L(i,j) - @test r(deepcopy(D)) == (a, b, c, d*c) - @test inv(r)(deepcopy(D)) == (a, b, c, d*c^-1) - @test l(deepcopy(D)) == (a, b, c, c*d) - @test inv(l)(deepcopy(D)) == (a, b, c, c^-1*d) - - i,j = 2,4 - r = Groups.transvection_R(i,j) - l = Groups.transvection_L(i,j) - @test r(deepcopy(D)) == (a, b*d, c, d) - @test inv(r)(deepcopy(D)) == (a, b*d^-1,c, d) - @test l(deepcopy(D)) == (a, d*b, c, d) - @test inv(l)(deepcopy(D)) == (a, d^-1*b,c, d) - end - - @testset "AutGroup/Automorphism constructors" begin - - f = Groups.AutSymbol(:a, 1, Groups.FlipAut(1)) - @test isa(Automorphism{3}(f), Groups.GWord) - @test isa(Automorphism{3}(f), Automorphism) - @test isa(AutGroup(FreeGroup(3)), AbstractAlgebra.Group) - @test isa(AutGroup(FreeGroup(1)), Groups.AbstractFPGroup) - - A = AutGroup(FreeGroup(1)) - @test Groups.gens(A) isa Vector{Automorphism{1}} - @test length(Groups.gens(A)) == 1 - @test length(Groups.gens(Aut(FreeGroup(1)))) == 1 - @test Groups.gens(A) == [A(Groups.flip(1))] - - A = AutGroup(FreeGroup(1), special=true) - @test isempty(Groups.gens(A)) - @test Groups.gens(SAut(FreeGroup(1))) == Automorphism{1}[] - - A = AutGroup(FreeGroup(2)) - @test length(Groups.gens(A)) == 7 - Agens = Groups.gens(A) - @test A(first(Agens)) isa Automorphism - - @test A(Groups.transvection_R(1,2)) isa Automorphism - @test A(Groups.transvection_R(1,2)) in Agens - - @test A(Groups.transvection_R(2,1)) isa Automorphism - @test A(Groups.transvection_R(2,1)) in Agens - - @test A(Groups.transvection_R(1,2)) isa Automorphism - @test A(Groups.transvection_R(1,2)) in Agens - - @test A(Groups.transvection_R(2,1)) isa Automorphism - @test A(Groups.transvection_R(2,1)) in Agens - - @test A(Groups.flip(1)) isa Automorphism - @test A(Groups.flip(1)) in Agens - - @test A(Groups.flip(2)) isa Automorphism - @test A(Groups.flip(2)) in Agens - - @test A(Groups.AutSymbol(perm"(1,2)")) isa Automorphism - @test A(Groups.AutSymbol(perm"(1,2)")) in Agens - - @test A(Groups.id_autsymbol()) isa Automorphism - end - - A = AutGroup(FreeGroup(4)) - - @testset "eltary functions" begin - - f = Groups.AutSymbol(perm"(1,2,3,4)") - @test (Groups.change_pow(f, 2)).pow == 1 - @test (Groups.change_pow(f, -2)).pow == 1 - @test (inv(f)).pow == 1 - - g = Groups.AutSymbol(perm"(1,2)(3,4)") - @test isa(inv(g), Groups.AutSymbol) - - @test_throws MethodError g*f - - @test A(g)^-1 == A(inv(g)) - - h = Groups.transvection_R(1,2) - - @test collect(A(g)*A(h)) == [g, h] - @test collect(A(h)^2) == [h, h] - end - - @testset "reductions/arithmetic" begin - f = Groups.AutSymbol(perm"(1,2,3,4)") - - f² = push!(A(f), f) - @test Groups.simplifyperms!(Bool, f²) == false - @test f²^2 == one(A) - @test !isone(f²) - - a = A(Groups.λ(1,2))*Groups.ε(2) - b = Groups.ε(2)*A(inv(Groups.λ(1,2))) - @test a*b == b*a - @test a^3 * b^3 == one(A) - g,h = Groups.gens(A)[[1,8]] # (g, h) = (ϱ₁₂, ϱ₃₂) - - @test Groups.domain(A) == NTuple{4, FreeGroupElem}(gens(A.objectGroup)) - - @test (g*h)(Groups.domain(A)) == (h*g)(Groups.domain(A)) - @test (g*h).savedhash == zero(UInt) - @test (h*g).savedhash == zero(UInt) - a = g*h - b = h*g - @test hash(a) != zero(UInt) - @test hash(b) == hash(a) - @test a.savedhash == b.savedhash - @test length(unique([a,b])) == 1 - @test length(unique([g*h, h*g])) == 1 - - # Not so simple arithmetic: applying starting on the left: - # ϱ₁₂*ϱ₂₁⁻¹*λ₁₂*ε₂ == σ₂₁₃₄ - - g = A(Groups.transvection_R(1,2)) - x1, x2, x3, x4 = Groups.domain(A) - @test g(Groups.domain(A)) == (x1*x2, x2, x3, x4) - g = g*inv(A(Groups.transvection_R(2,1))) - @test g(Groups.domain(A)) == (x1*x2, x1^-1, x3, x4) - g = g*A(Groups.transvection_L(1,2)) - @test g(Groups.domain(A)) == (x2, x1^-1, x3, x4) - g = g*A(Groups.flip(2)) - @test g(Groups.domain(A)) == (x2, x1, x3, x4) - - @test g(Groups.domain(A)) == A(Groups.AutSymbol(perm"(1,2)(4)"))(Groups.domain(A)) - - @test g == A(Groups.AutSymbol(perm"(1,2)(4)")) - - g_im = g(Groups.domain(A)) - @test length.(g_im) == (1,1,1,1) - - g = A(Groups.σ(perm"(1,2)(4)")) - h = A(Groups.σ(perm"(2,3,4)")) - @test g*h isa Groups.Automorphism{4} - f = g*h - Groups - @test Groups.syllablelength(f) == 2 - @test Groups.reduce!(f) isa Groups.Automorphism{4} - @test Groups.syllablelength(f) == 1 - end - - @testset "specific Aut(F4) tests" begin - N = 4 - G = AutGroup(FreeGroup(N)) - S = G.gens - @test isa(S, Vector{Groups.AutSymbol}) - S = [G(s) for s in unique(S)] - @test isa(S, Vector{Automorphism{N}}) - @test S == gens(G) - @test length(S) == 51 - S_inv = [S..., [inv(s) for s in S]...] - @test length(unique(S_inv)) == 75 - - G = AutGroup(FreeGroup(N), special=true) - S = gens(G) - S_inv = [one(G), S..., [inv(s) for s in S]...] - S_inv = unique(S_inv) - B_2 = [i*j for (i,j) in Base.product(S_inv, S_inv)] - @test length(B_2) == 2401 - @test length(unique(B_2)) == 1777 - end - - @testset "abelianization homomorphism" begin - N = 4 - G = AutGroup(FreeGroup(N)) - S = unique([gens(G); inv.(gens(G))]) - R = 3 - - @test Groups.abelianize(one(G)) isa Matrix{Int} - @test Groups.abelianize(one(G)) == Matrix{Int}(I, N, N) - - M = Matrix{Int}(I, N, N) - M[1,2] = 1 - ϱ₁₂ = G(Groups.ϱ(1,2)) - λ₁₂ = G(Groups.λ(1,2)) - - @test Groups.abelianize(ϱ₁₂) == M - @test Groups.abelianize(λ₁₂) == M - - M[1,2] = -1 - - @test Groups.abelianize(ϱ₁₂^-1) == M - @test Groups.abelianize(λ₁₂^-1) == M - - @test Groups.abelianize(ϱ₁₂*λ₁₂^-1) == Matrix{Int}(I, N, N) - @test Groups.abelianize(λ₁₂^-1*ϱ₁₂) == Matrix{Int}(I, N, N) - - M = Matrix{Int}(I, N, N) - M[2,2] = -1 - ε₂ = G(Groups.flip(2)) - - @test Groups.abelianize(ε₂) == M - @test Groups.abelianize(ε₂^2) == Matrix{Int}(I, N, N) - - M = [0 1 0 0; 0 0 0 1; 0 0 1 0; 1 0 0 0] - - σ = G(Groups.AutSymbol(perm"(1,2,4)")) - @test Groups.abelianize(σ) == M - @test Groups.abelianize(σ^3) == Matrix{Int}(I, N, N) - @test Groups.abelianize(σ)^3 == Matrix{Int}(I, N, N) - - @test Groups.abelianize(G(Groups.id_autsymbol())) == Matrix{Int}(I, N, N) - - function test_homomorphism(S, r) - for elts in Iterators.product([[g for g in S] for _ in 1:r]...) - prod(Groups.abelianize.(elts)) == Groups.abelianize(prod(elts)) || error("linear representaton test failed at $elts") - end - return 0 - end - - @test test_homomorphism(S, R) == 0 - end -end diff --git a/test/DirectPower-tests.jl b/test/DirectPower-tests.jl deleted file mode 100644 index 65722f5..0000000 --- a/test/DirectPower-tests.jl +++ /dev/null @@ -1,89 +0,0 @@ -@testset "DirectPowers" begin - - ×(a,b) = Groups.DirectPower(a,b) - - @testset "Constructors" begin - G = SymmetricGroup(3) - - @test Groups.DirectPowerGroup(G,2) isa AbstractAlgebra.Group - @test G×G isa AbstractAlgebra.Group - @test Groups.DirectPowerGroup(G,2) isa Groups.DirectPowerGroup{2, Generic.SymmetricGroup{Int64}} - - @test (G×G)×G == DirectPowerGroup(G, 3) - @test (G×G)×G == (G×G)×G - - GG = DirectPowerGroup(G,2) - @test one(G×G) isa GroupElem - @test (G×G)((one(G), one(G))) isa GroupElem - @test (G×G)([one(G), one(G)]) isa GroupElem - - @test Groups.DirectPowerGroupElem((one(G), one(G))) == one(G×G) - @test GG(one(G), one(G)) == one(G×G) - - g = perm"(1,2,3)" - - @test GG(g, g^2) isa GroupElem - @test GG(g, g^2) isa Groups.DirectPowerGroupElem{2, Generic.Perm{Int64}} - - h = GG(g,g^2) - - @test h == GG(h) - - @test GG(g, g^2) isa GroupElem - @test GG(g, g^2) isa Groups.DirectPowerGroupElem - - @test_throws MethodError GG(g,g,g) - @test GG(g,g^2) == h - - @test h[1] == g - @test h[2] == g^2 - h = GG(g, one(G)) - @test h == GG(g, one(G)) - end - - @testset "Basic arithmetic" begin - G = SymmetricGroup(3) - GG = G×G - i = perm"(1,3)" - g = perm"(1,2,3)" - - h = GG(g,g^2) - k = GG(g^3, g^2) - - @test h^2 == GG(g^2,g) - @test h^6 == one(GG) - - @test h*h == h^2 - @test h*k == GG(g,g) - - @test h*inv(h) == one(G×G) - - w = GG(g,i)*GG(i,g) - @test w == GG(perm"(1,2)(3)", perm"(2,3)") - @test w == inv(w) - @test w^2 == w*w == one(GG) - end - - @testset "elem/parent_types" begin - G = SymmetricGroup(3) - g = perm"(1,2,3)" - - @test elem_type(G×G) == DirectPowerGroupElem{2, elem_type(G)} - @test elem_type(G×G×G) == DirectPowerGroupElem{3, elem_type(G)} - @test parent_type(typeof((G×G)(g,g^2))) == Groups.DirectPowerGroup{2, typeof(G)} - @test parent(DirectPowerGroupElem((g,g^2,g^3))) == DirectPowerGroup(G,3) - end - - @testset "Misc" begin - G = SymmetricGroup(3) - GG = Groups.DirectPowerGroup(G,3) - @test order(GG) == 216 - - @test isa(collect(GG), Vector{Groups.DirectPowerGroupElem{3, elem_type(G)}}) - elts = vec(collect(GG)) - - @test length(elts) == 216 - @test all([g*inv(g) == one(GG) for g in elts]) - @test all(inv(g*h) == inv(h)*inv(g) for g in elts for h in elts) - end -end diff --git a/test/FPGroup-tests.jl b/test/FPGroup-tests.jl deleted file mode 100644 index ddf29e9..0000000 --- a/test/FPGroup-tests.jl +++ /dev/null @@ -1,18 +0,0 @@ -@testset "FPGroups definitions" begin - F = FreeGroup(["a", "b", "c"]) - a,b,c = Groups.gens(F) - R = [a^2, a*b*a, c*b*a] - @test F/R isa FPGroup - @test F isa FreeGroup - G = F/R - A,B,C = Groups.gens(G) - - @test Groups.reduce!(A^2) == one(G) - @test Groups.reduce!(A*B*A*A) == A - @test Groups.reduce!(A*A*B*A) == A - - @test Groups.freepreimage(G) == F - @test Groups.freepreimage(B^2) == b^2 - - @test G/[B^2, C*B*C] isa FPGroup -end diff --git a/test/FreeGroup-tests.jl b/test/FreeGroup-tests.jl deleted file mode 100644 index 49f4b82..0000000 --- a/test/FreeGroup-tests.jl +++ /dev/null @@ -1,188 +0,0 @@ -@testset "Groups.FreeSymbols" begin - s = Groups.FreeSymbol(:s) - t = Groups.FreeSymbol(:t) - - @testset "constructors" begin - @test isa(Groups.FreeSymbol(:aaaaaaaaaaaaaaaa), Groups.GSymbol) - @test Groups.FreeSymbol(:abc).pow == 1 - @test isa(s, Groups.FreeSymbol) - @test isa(t, Groups.FreeSymbol) - end - @testset "eltary functions" begin - @test length(s) == 1 - @test Groups.change_pow(s, 0) == Groups.change_pow(t, 0) - @test length(Groups.change_pow(s, 0)) == 0 - @test inv(s).pow == -1 - @test Groups.FreeSymbol(:s, 3) == Groups.change_pow(s, 3) - @test Groups.FreeSymbol(:s, 3) != Groups.FreeSymbol(:t, 3) - @test Groups.change_pow(inv(s), -3) == inv(Groups.change_pow(s, 3)) - end - @testset "powers" begin - s⁴ = Groups.change_pow(s,4) - @test s⁴.pow == 4 - @test Groups.change_pow(s, 4) == Groups.FreeSymbol(:s, 4) - end -end - -@testset "FreeGroupSymbols manipulation" begin - s = Groups.FreeSymbol("s") - t = Groups.FreeSymbol(:t, -2) - - @test isa(Groups.GroupWord(s), Groups.GWord{Groups.FreeSymbol}) - @test isa(Groups.GroupWord(s), FreeGroupElem) - @test isa(FreeGroupElem(s), Groups.GWord) - @test isa(convert(FreeGroupElem, s), Groups.GWord) - @test isa(convert(FreeGroupElem, s), FreeGroupElem) - @test isa(Vector{FreeGroupElem}([s,t]), Vector{FreeGroupElem}) - @test length(FreeGroupElem(s)) == 1 - @test length(FreeGroupElem(t)) == 2 - @test Groups.FreeSymbol(:s, 1) != Groups.FreeSymbol(:s, 2) - @test Groups.FreeSymbol(:s, 1) != Groups.FreeSymbol(:t, 1) - @test collect(Groups.FreeSymbol(:s, 2)) == [i for i in Groups.FreeSymbol(:s, 2)] == [s, s] -end - -@testset "FreeGroup" begin - @test isa(FreeGroup(["s", "t"]), AbstractAlgebra.Group) - G = FreeGroup(["s", "t"]) - s, t = gens(G) - - @testset "elements constructors" begin - @test isa(one(G), FreeGroupElem) - @test eltype(G.gens) == Groups.FreeSymbol - @test length(G.gens) == 2 - @test eltype(gens(G)) == FreeGroupElem - @test length(gens(G)) == 2 - - tt, ss = Groups.FreeSymbol(:t), Groups.FreeSymbol(:s) - @test Groups.GroupWord([tt, inv(tt)]) isa FreeGroupElem - - @test collect(s*t) == Groups.syllables(s*t) - @test collect(t^2) == [tt, tt] - ttinv = Groups.FreeSymbol(:t, -1) - w = t^-2*s^3*t^2 - @test collect(w) == [inv(tt), inv(tt), ss, ss, ss, tt, tt] - @test w[1] == inv(tt) - @test w[2] == inv(tt) - @test w[3] == ss - @test w[3:5] == [ss, ss, ss] - @test w[end] == tt - - @test collect(ttinv) == [ttinv] - - @test isone(t^0) - @test !isone(t^1) - end - - @testset "internal arithmetic" begin - - @test (s*s).symbols == (s^2).symbols - @test hash([t^1,s^1]) == hash([t^2*inv(t),s*inv(s)*s]) - - t_symb = Groups.FreeSymbol(:t) - tt = deepcopy(t) - @test string(Groups.rmul!(tt, tt, inv(tt))) == "(id)" - tt = deepcopy(t) - @test string(Groups.lmul!(tt, tt, inv(tt))) == "(id)" - - w = deepcopy(t) - @test length(Groups.rmul!(w, t)) == 2 - @test length(Groups.lmul!(w, inv(t))) == 1 - w = AbstractAlgebra.mul!(w, w, s) - @test length(w) == 2 - @test length(Groups.lmul!(w, inv(s))) == 3 - - tt = deepcopy(t) - push!(tt, inv(t_symb)) - @test string(tt) == "t*t^-1" - tt = deepcopy(t) - pushfirst!(tt, inv(t_symb)) - @test string(tt) == "t^-1*t" - - tt = deepcopy(t) - append!(tt, inv(t)) - @test string(tt) == "t*t^-1" - - tt = deepcopy(t) - prepend!(tt, inv(t)) - @test string(tt) == "t^-1*t" - - tt = deepcopy(t) - append!(tt, s, inv(t)) - @test string(tt) == "t*s*t^-1" - - o = one(t) - o_inv = inv(o) - @test o == o_inv - @test o !== o_inv - Groups.rmul!(o, t) - @test o != o_inv - end - - @testset "reductions" begin - @test length(one(G).symbols) == 0 - @test length((one(G)*one(G)).symbols) == 0 - @test one(G) == one(G)*one(G) - w = deepcopy(s) - push!(Groups.syllables(w), (s^-1).symbols[1]) - @test Groups.reduce!(w) == one(parent(w)) - o = (t*s)^3 - @test o == t*s*t*s*t*s - p = (t*s)^-3 - @test p == s^-1*t^-1*s^-1*t^-1*s^-1*t^-1 - @test o*p == one(parent(o*p)) - w = FreeGroupElem([o.symbols..., p.symbols...]) - w.parent = G - @test Groups.syllables(Groups.reduce(w)) == Vector{Groups.FreeSymbol}([]) - end - - @testset "Group operations" begin - @test parent(s) == G - @test parent(s) === parent(deepcopy(s)) - @test isa(s*t, FreeGroupElem) - @test parent(s*t) == parent(s^2) - @test s*s == s^2 - @test inv(s*s) == inv(s^2) - @test inv(s)^2 == inv(s^2) - @test inv(s)*inv(s) == inv(s^2) - @test inv(s*t) == inv(t)*inv(s) - w = s*t*s^-1 - @test inv(w) == s*t^-1*s^-1 - @test (t*s*t^-1)^10 == t*s^10*t^-1 - @test (t*s*t^-1)^-10 == t*s^-10*t^-1 - end - - @testset "replacements" begin - a = Groups.FreeSymbol(:a) - b = Groups.FreeSymbol(:b) - @test Groups.issubsymbol(a, Groups.change_pow(a,2)) == true - @test Groups.issubsymbol(a, Groups.change_pow(a,-2)) == false - @test Groups.issubsymbol(b, Groups.change_pow(a,-2)) == false - @test Groups.issubsymbol(inv(b), Groups.change_pow(b,-2)) == true - - c = s*t*s^-1*t^-1 - @test findfirst(s^-1*t^-1, c) == 3 - @test findnext(s^-1*t^-1, c*s^-1,3) == 3 - @test findnext(s^-1*t^-1, c*s^-1*t^-1, 4) == 5 - @test findfirst(c, c*t) === nothing - - @test findlast(s^-1*t^-1, c) == 3 - @test findprev(s, s*t*s*t, 4) == 3 - @test findprev(s*t, s*t*s*t, 2) == 1 - @test findprev(Groups.FreeSymbol(:t, 2), c, 4) === nothing - - w = s*t*s^-1 - subst = Dict{FreeGroupElem, FreeGroupElem}(w => s^1, s*t^-1 => t^4) - @test Groups.replace(c, s*t=>one(G)) == s^-1*t^-1 - @test Groups.replace(c, w=>subst[w]) == s*t^-1 - @test Groups.replace(s*c*t^-1, w=>subst[w]) == s^2*t^-2 - @test Groups.replace(t*c*t, w=>subst[w]) == t*s - @test Groups.replace(s*c*s*c*s, subst) == s*t^4*s*t^4*s - - G = FreeGroup(["x", "y"]) - x,y = gens(G) - - @test Groups.replace(x*y^9, y^2=>y) == x*y^5 - @test Groups.replace(x^3, x^2=>y) == x*y - @test Groups.replace(y*x^3*y, x^2=>y) == y*x*y^2 - end -end diff --git a/test/WreathProd-tests.jl b/test/WreathProd-tests.jl deleted file mode 100644 index 158e056..0000000 --- a/test/WreathProd-tests.jl +++ /dev/null @@ -1,98 +0,0 @@ -@testset "WreathProducts" begin - S_3 = SymmetricGroup(3) - S_2 = SymmetricGroup(2) - b = perm"(1,2,3)" - a = perm"(1,2)" - - @testset "Constructors" begin - @test Groups.WreathProduct(S_2, S_3) isa AbstractAlgebra.Group - B3 = Groups.WreathProduct(S_2, S_3) - @test B3 isa Groups.WreathProduct - @test B3 isa WreathProduct{3, Generic.SymmetricGroup{Int}, Generic.SymmetricGroup{Int}} - - aa = Groups.DirectPowerGroupElem((a^0 ,a, a^2)) - - @test Groups.WreathProductElem(aa, b) isa AbstractAlgebra.GroupElem - x = Groups.WreathProductElem(aa, b) - @test x isa Groups.WreathProductElem - @test x isa - Groups.WreathProductElem{3, Generic.Perm{Int}, Generic.Perm{Int}} - - @test B3.N == Groups.DirectPowerGroup(S_2, 3) - @test B3.P == S_3 - - @test B3(aa, b) == Groups.WreathProductElem(aa, b) - w = B3(aa, b) - @test B3(w) == w - @test B3(b) == Groups.WreathProductElem(one(B3.N), b) - @test B3(aa) == Groups.WreathProductElem(aa, one(S_3)) - - @test B3((a^0 ,a, a^2), b) isa WreathProductElem - - @test B3((a^0 ,a, a^2), b) == B3(aa, b) - end - - @testset "Types" begin - B3 = Groups.WreathProduct(S_2, S_3) - - @test elem_type(B3) == Groups.WreathProductElem{3, Generic.Perm{Int}, Generic.Perm{Int}} - - @test parent_type(typeof(one(B3))) == Groups.WreathProduct{3, parent_type(typeof(one(B3.N.group))), Generic.SymmetricGroup{Int}} - - @test parent(one(B3)) == Groups.WreathProduct(S_2,S_3) - @test parent(one(B3)) == B3 - end - - @testset "Basic operations on WreathProductElem" begin - aa = Groups.DirectPowerGroupElem((a^0 ,a, a^2)) - B3 = Groups.WreathProduct(S_2, S_3) - g = B3(aa, b) - - @test g.p == b - @test g.n == DirectPowerGroupElem(aa.elts) - - h = deepcopy(g) - @test h == g - @test !(g === h) - - g = B3(Groups.DirectPowerGroupElem((a ,a, a^2)), g.p) - - @test g.n[1] == parent(g.n[1])(a) - @test g != h - - @test hash(g) != hash(h) - end - - @testset "Group arithmetic" begin - B4 = Groups.WreathProduct(SymmetricGroup(3), SymmetricGroup(4)) - - id, a, b = perm"(3)", perm"(1,2)(3)", perm"(1,2,3)" - - x = B4((id,a,b,id), perm"(1,2,3)(4)") - @test inv(x) == B4((inv(b),id, a,id), perm"(1,3,2)(4)") - - y = B4((a,id,a,b), perm"(1,4)(2,3)") - @test inv(y) == B4((inv(b), a,id, a), perm"(1,4)(2,3)") - - @test x*y == B4((id,id,b*a,b), perm"(1,3,4)(2)") - @test y*x == B4(( a, b, id,b), perm"(1,4,2)(3)") - - @test inv(x)*y == B4((inv(b)*a,a,a,b), perm"(1,2,4)(3)") - @test y*inv(x) == B4((a,a,a,id), perm"(1,4,3)(2)") - - @test (x*y)^6 == ((x*y)^2)^3 - end - - @testset "Iteration" begin - Wr = WreathProduct(SymmetricGroup(2),SymmetricGroup(4)) - - elts = collect(Wr) - @test elts isa Vector{Groups.WreathProductElem{4, Generic.Perm{Int}, Generic.Perm{Int}}} - @test order(Wr) == 2^4*factorial(4) - - @test length(elts) == order(Wr) - @test all((g*inv(g) == one(Wr) for g in elts)) - @test all(inv(g*h) == inv(h)*inv(g) for g in elts for h in elts) - end - -end diff --git a/test/benchmarks.jl b/test/benchmarks.jl new file mode 100644 index 0000000..4a6db1f --- /dev/null +++ b/test/benchmarks.jl @@ -0,0 +1,72 @@ +using BenchmarkTools +using Test + +using Groups +using Groups.New + +function wl_ball(F; radius::Integer) + g, state = iterate(F) + while length(word(g)) <= radius + res = iterate(F, state) + isnothing(res) && break + g, state = res + end + elts = collect(state.seen) + elts = resize!(elts, length(elts)-1) + return elts +end + +@testset "Benchmarks" begin + N = 4 + + @testset "iteration: FreeGroup" begin + FN = FreeGroup(N) + R = 8 + + let G = FN + S = unique([gens(G); inv.(gens(G))]) + + sizes1 = last(Groups.wlmetric_ball(S, radius=R, threading=false)) + sizes2 = last(Groups.wlmetric_ball(S, radius=R, threading=true)) + + l = length(wl_ball(G, radius=R)) + + @test sizes1 == sizes2 + @test last(sizes1) == l + + @info "Ball of radius $R in $(parent(first(S)))" sizes=sizes1 + @info "serial" + @time Groups.wlmetric_ball(S, radius=R, threading=false) + @info "threaded" + @time Groups.wlmetric_ball(S, radius=R, threading=true) + @info "iteration" + @time wl_ball(G, radius=R) + end + end + + @testset "iteration: SAut(F_n)" begin + R = 4 + FN = FreeGroup(N) + SAutFN = New.SpecialAutomorphismGroup(FN) + + let G = SAutFN + S = unique([gens(G); inv.(gens(G))]) + + sizes1 = last(Groups.wlmetric_ball(S, radius=R, threading=false)) + sizes2 = last(Groups.wlmetric_ball(S, radius=R, threading=true)) + + l = length(wl_ball(G, radius=R)) + + @test sizes1 == sizes2 + @test last(sizes1) == l + + @info "Ball of radius $R in $(parent(first(S)))" sizes=sizes1 + @info "serial" + @time Groups.wlmetric_ball(S, radius=R, threading=false) + @info "threaded" + @time Groups.wlmetric_ball(S, radius=R, threading=true) + @info "iteration" + @time wl_ball(G, radius=R) + end + end +end diff --git a/test/fp_groups.jl b/test/fp_groups.jl new file mode 100644 index 0000000..89d83b5 --- /dev/null +++ b/test/fp_groups.jl @@ -0,0 +1,59 @@ +@testset "FPGroups" begin + A = Alphabet([:a, :A, :b, :B, :c, :C], [2,1,4,3,6,5]) + + @test FreeGroup(A) isa FreeGroup + @test sprint(show, FreeGroup(A)) == "free group on 3 generators" + + F = FreeGroup([:a, :b, :c], A) + @test sprint(show, F) == "free group on 3 generators" + + a,b,c = gens(F) + @test c*b*a isa FPGroupElement + + # quotient of F: + G = FPGroup(F, [a*b=>b*a, a*c=>c*a, b*c=>c*b]) + + @test G isa FPGroup + @test sprint(show, G) == "⟨a, b, c | a*b => b*a, a*c => c*a, b*c => c*b⟩" + @test rand(G) isa FPGroupElement + + f = a*c*b + @test word(f) isa Word{UInt8} + + aG,bG,cG = gens(G) + + @test aG isa FPGroupElement + @test_throws AssertionError aG == a + @test word(aG) == word(a) + + g = aG*cG*bG + + @test_throws AssertionError f == g + @test word(f) == word(g) + @test word(g) == [1, 5, 3] + Groups.normalform!(g) + @test word(g) == [1, 3, 5] + + let g = aG*cG*bG + # test that we normalize g before printing + @test sprint(show, g) == "a*b*c" + end + + # quotient of G + H = FPGroup(G, [aG^2=>cG, bG*cG=>aG], maxrules=200) + + h = H(word(g)) + + @test h isa FPGroupElement + @test_throws AssertionError h == g + @test_throws AssertionError h*g + + Groups.normalform!(h) + @test h == H([5]) + + @testset "GroupsCore conformance: H" begin + test_Group_interface(H) + test_GroupElement_interface(rand(H, 2)...) + end + +end diff --git a/test/free_groups.jl b/test/free_groups.jl new file mode 100644 index 0000000..ced86a8 --- /dev/null +++ b/test/free_groups.jl @@ -0,0 +1,71 @@ +@testset "FreeGroup" begin + + A3 = Alphabet([:a, :b, :c, :A, :B, :C], [4,5,6,1,2,3]) + F3 = FreeGroup([:a, :b, :c], A3) + @test F3 isa FreeGroup + + @test gens(F3) isa Vector + + @test eltype(F3) <: FPGroupElement{<:FreeGroup} + + w = F3([1,2,3,4]) + W = inv(w) + @test deepcopy(w) !== w + @test deepcopy(w).word !== w.word + + @test isone(w*W) + + @test alphabet(w) == A3 + + @testset "generic iteration" begin + w, s = iterate(F3) + @test isone(w) + w, s = iterate(F3, s) + @test w == gens(F3, 1) + + a,b,c = gens(F3) + + function test_iteration(F, n=1000) + w, s = iterate(F) + for i in 1:n + w, s = iterate(F, s) + end + return w + end + + k = test_iteration(F3, 10) + @test k == a*b^-1 + + @time k = test_iteration(F3, 1000) + @test k == (a^2)*c^2*a^-1 + end + + @testset "wl_ball" begin + function wl_ball(F; radius::Integer) + g, state = iterate(F) + while length(word(g)) <= radius + res = iterate(F, state) + isnothing(res) && break + g, state = res + end + elts = collect(state.seen) + elts = resize!(elts, length(elts)-1) + return elts + end + + E4 = wl_ball(F3, radius=4) + @test length(E4) == 937 + @test word(last(E4)) == Word([6])^4 + + E8, t, _ = @timed wl_ball(F3, radius=8) + @test length(E8) == 585937 + @test word(last(E8)) == Word([6])^8 + @test t/10^9 < 1 + end + + @testset "GroupsCore conformance" begin + test_Group_interface(F3) + test_GroupElement_interface(rand(F3, 2)...) + end + +end diff --git a/test/runtests.jl b/test/runtests.jl index 077c3b3..e69bce8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,27 +1,33 @@ using Test -using AbstractAlgebra +import AbstractAlgebra using Groups -using LinearAlgebra +import KnuthBendix: Word + +using GroupsCore +include(joinpath(pathof(GroupsCore), "..", "..", "test", "conformance_test.jl")) @testset "Groups" begin - @testset "wlmetric_ball" begin - M = MatrixAlgebra(zz, 3) - w = one(M); w[1,2] = 1; - r = one(M); r[2,3] = -3; - s = one(M); s[1,3] = 2; s[3,2] = -1; + @testset "wlmetric_ball" begin + M = AbstractAlgebra.MatrixAlgebra(AbstractAlgebra.zz, 3) + w = one(M); w[1,2] = 1; + r = one(M); r[2,3] = -3; + s = one(M); s[1,3] = 2; s[3,2] = -1; - S = [w,r,s]; S = unique([S; inv.(S)]); - _, sizes = Groups.wlmetric_ball(S, radius=4); - @test sizes == [7, 33, 141, 561] - _, sizes = Groups.wlmetric_ball_serial(S, radius=4); - @test sizes == [7, 33, 141, 561] - end + S = [w,r,s]; S = unique([S; inv.(S)]); + _, sizes = Groups.wlmetric_ball(S, radius=4); + @test sizes == [7, 33, 141, 561] + _, sizes = Groups.wlmetric_ball_serial(S, radius=4); + @test sizes == [7, 33, 141, 561] + end - include("FreeGroup-tests.jl") - include("AutGroup-tests.jl") - include("DirectPower-tests.jl") - include("WreathProd-tests.jl") - include("FPGroup-tests.jl") + include("free_groups.jl") + include("fp_groups.jl") + + include("AutFn.jl") + + # if !haskey(ENV, "CI") + # include("benchmarks.jl") + # end end