diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 0000000..d77d3a0 --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,11 @@ +name: TagBot +on: + schedule: + - cron: 0 * * * * +jobs: + TagBot: + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/Project.toml b/Project.toml index f35039c..f35fdfb 100644 --- a/Project.toml +++ b/Project.toml @@ -1,15 +1,15 @@ name = "Groups" uuid = "5d8bd718-bd84-11e8-3b40-ad14f4a32557" authors = ["Marek Kaluba "] -version = "0.4.2" +version = "0.5.0" [deps] AbstractAlgebra = "c3fe647b-3220-5bb0-a1ea-a7954cac585d" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -Markdown = "d6f4376e-aef5-505a-96c1-9c027394607a" +ThreadsX = "ac1d9e8a-700a-412c-b207-f0111f4b6c0d" [compat] -AbstractAlgebra = "^0.9.0" +AbstractAlgebra = "^0.10.0" [extras] Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" diff --git a/src/AutGroup.jl b/src/AutGroup.jl index fa05877..1ce4b00 100644 --- a/src/AutGroup.jl +++ b/src/AutGroup.jl @@ -35,9 +35,7 @@ end # https://github.com/JuliaIntervals/ValidatedNumerics.jl/blob/master/LICENSE.md function subscriptify(n::Integer) subscript_0 = Int(0x2080) # Char(0x2080) -> subscript 0 - @assert 0 <= n <= 9 - return Char(subscript_0 + n) - # return [Char(subscript_0 + i) for i in reverse(digits(n))]) + return join([Char(subscript_0 + i) for i in reverse(digits(n))], "") end function id_autsymbol() @@ -45,18 +43,26 @@ function id_autsymbol() end function transvection_R(i::Integer, j::Integer, pow::Integer=1) - id = Symbol("ϱ", subscriptify(i), subscriptify(j)) + 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) - id = Symbol("λ", subscriptify(i), subscriptify(j)) + 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)) + id = Symbol(:ɛ, subscriptify(i)) return AutSymbol(id, 1, FlipAut(i)) end @@ -66,7 +72,7 @@ function AutSymbol(p::Generic.Perm, pow::Integer=1) end if any(p.d[i] != i for i in eachindex(p.d)) - id = Symbol("σ", "₍", [subscriptify(i) for i in p.d]..., "₎") + id = Symbol(:σ, "₍", join([subscriptify(i) for i in p.d],""), "₎") return AutSymbol(id, 1, PermAut(p)) end return id_autsymbol() @@ -78,9 +84,8 @@ end σ(v::Generic.Perm, pow::Integer=1) = AutSymbol(v, pow) function change_pow(s::AutSymbol, n::Integer) - if n == zero(n) - return id_autsymbol() - end + iszero(n) && id_autsymbol() + symbol = s.fn if symbol isa FlipAut return flip(symbol.i, n) @@ -162,14 +167,12 @@ end # function (ϱ::RTransvect)(v, pow::Integer=1) - append!(v[ϱ.i], v[ϱ.j]^pow) - freereduce!(v[ϱ.i]) + rmul!(v[ϱ.i], v[ϱ.j]^pow) return v end function (λ::LTransvect)(v, pow::Integer=1) - prepend!(v[λ.i], v[λ.j]^pow) - freereduce!(v[λ.i]) + lmul!(v[λ.i], v[λ.j]^pow) return v end @@ -217,41 +220,60 @@ evaluate(f::Automorphism) = f(domain(parent(f))) # hashing && equality # -function hash_internal(g::Automorphism, images = freereduce!.(evaluate(g)), - h::UInt = 0x7d28276b01874b19) # hash(Automorphism) - return hash(images, hash(parent(g), h)) +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 = reduce!.(evaluate(g)) - savehash!(g, hash_internal(g, images)) - unsetmodified!(g) + images = evaluate(g) + for im in images + reduce!(im) + end return images end function (==)(g::Automorphism{N}, h::Automorphism{N}) where N - img_c, imh_c = false, false + syllables(g) == syllables(h) && return true + img_computed, imh_computed = false, false if ismodified(g) - img = compute_images(g) - img_c = true + img = compute_images(g) # sets modified bit + hash(g, images=img) + img_computed = true end if ismodified(h) - imh = compute_images(h) - imh_c = true + imh = compute_images(h) # sets modified bit + hash(h, images=imh) + imh_computed = true end @assert !ismodified(g) && !ismodified(h) # cheap - hash(g) != hash(h) && return false # hashes differ, so images must have differed as well - # equal elements, or possibly hash conflict - if !img_c - img = compute_images(g) - end - if !imh_c - imh = compute_images(h) + # 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 diff --git a/src/DirectPower.jl b/src/DirectPower.jl index 567444e..8274465 100644 --- a/src/DirectPower.jl +++ b/src/DirectPower.jl @@ -6,9 +6,9 @@ export DirectPowerGroup, DirectPowerGroupElem # ############################################################################### -@doc doc""" +""" DirectPowerGroup(G::Group, n::Int) <: Group -Implements `n`-fold direct product of `G`. The group operation is +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 @@ -70,11 +70,11 @@ Base.getindex(g::DirectPowerGroupElem, i::Int) = g.elts[i] # ############################################################################### -@doc doc""" +""" (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. +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 @@ -131,20 +131,12 @@ end # ############################################################################### -@doc doc""" - ==(g::DirectPowerGroup, h::DirectPowerGroup) -> Checks if two direct product groups are the same. -""" function (==)(G::DirectPowerGroup{N}, H::DirectPowerGroup{M}) where {N,M} N == M || return false G.group == H.group || return false return true end -@doc doc""" - ==(g::DirectPowerGroupElem, h::DirectPowerGroupElem) -> Checks if two direct product group elements are the same. -""" (==)(g::DirectPowerGroupElem, h::DirectPowerGroupElem) = g.elts == h.elts ############################################################################### @@ -153,11 +145,6 @@ end # ############################################################################### -@doc doc""" - *(g::DirectPowerGroupElem, h::DirectPowerGroupElem) -> Return the direct-product group operation of elements, i.e. component-wise -> operation as defined by `operations` field of the parent object. -""" function *(g::DirectPowerGroupElem{N}, h::DirectPowerGroupElem{N}, check::Bool=true) where N if check parent(g) == parent(h) || throw(DomainError( @@ -168,10 +155,6 @@ end ^(g::DirectPowerGroupElem, n::Integer) = Base.power_by_squaring(g, n) -@doc doc""" - inv(g::DirectPowerGroupElem) -> Return the inverse of the given element in the direct product group. -""" function inv(g::DirectPowerGroupElem{N}) where {N} return DirectPowerGroupElem(ntuple(i-> inv(g.elts[i]), N)) end diff --git a/src/Groups.jl b/src/Groups.jl index 4312ac2..1c2cb9c 100644 --- a/src/Groups.jl +++ b/src/Groups.jl @@ -11,7 +11,7 @@ import Base: findfirst, findnext, findlast, findprev, replace import Base: deepcopy_internal using LinearAlgebra -using Markdown +using ThreadsX export gens, FreeGroup, Aut, SAut @@ -32,17 +32,11 @@ include("findreplace.jl") include("DirectPower.jl") include("WreathProducts.jl") - ############################################################################### # # String I/O # -@doc doc""" - show(io::IO, W::GWord) -> The actual string produced by show depends on the eltype of `W.symbols`. - -""" function Base.show(io::IO, W::GWord) if length(W) == 0 print(io, "(id)") @@ -53,9 +47,9 @@ end function Base.show(io::IO, s::T) where {T<:GSymbol} if s.pow == 1 - print(io, string(s.id)) + print(io, string(s.id)) else - print(io, "$(s.id)^$(s.pow)") + print(io, "$(s.id)^$(s.pow)") end end @@ -64,43 +58,109 @@ end # Misc # -@doc doc""" +""" gens(G::AbstractFPGroups) -> returns vector of generators of `G`, as its elements. +Return vector of generators of `G`, as its elements. """ AbstractAlgebra.gens(G::AbstractFPGroup) = G.(G.gens) -@doc doc""" - metric_ball(S::Vector{GroupElem}, center=Id; radius=2, op=*) +""" + 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 group generated by `S`. The ball is centered at `center` +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 generate_balls(S::AbstractVector{T}, center::T=one(first(S)); - radius=2, op=*) where T<:Union{GroupElem, NCRingElem} - sizes = Int[] - B = [one(first(S))] - for i in 1:radius - BB = [op(i,j) for (i,j) in Base.product(B,S)] - B = unique([B; vec(BB)]) - push!(sizes, length(B)) + +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(i, j) + for (i, j) in Base.product(@view(old[sizes[end-1]:end]), S) + ) + append!(old, new) + resize!(new, 0) + old = unique!(old) + push!(sizes, length(old)) end - isone(center) && return B, sizes - return c.*B, sizes + return old, sizes[2:end] end -@doc doc""" - image(A::GWord, homomorphism; kwargs...) -Evaluate homomorphism `homomorphism` on a GWord `A`. -`homomorphism` needs implement - > `hom(s; kwargs...)`, -where `hom(;kwargs...)` evaluates the value at the identity element. +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(*, + return reduce( + *, (hom(s; kwargs...) for s in syllables(w)), - init = hom(;kwargs...)) + init = hom(; kwargs...), + ) end end # of module Groups diff --git a/src/WreathProducts.jl b/src/WreathProducts.jl index 3754892..a6d4ab2 100644 --- a/src/WreathProducts.jl +++ b/src/WreathProducts.jl @@ -8,18 +8,17 @@ import AbstractAlgebra: AbstractPermutationGroup, AbstractPerm # ############################################################################### -@doc doc""" +""" WreathProduct(N, P) <: Group -> Implements Wreath product of a group `N` by permutation group $P = S_n$, -> usually written as $N \wr 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` +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 group $N$ -* `P::Generic.PermGroup` : full `PermutationGroup` +* `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} @@ -71,25 +70,25 @@ function (G::WreathProduct{N})(g::WreathProductElem{N}) where {N} return WreathProductElem(n, p) end -@doc doc""" +""" (G::WreathProduct)(n::DirectPowerGroupElem, p::Generic.Perm) -> Creates an element of wreath product `G` by coercing `n` and `p` to `G.N` and -> `G.P`, respectively. +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) -@doc doc""" +""" (G::WreathProduct)(p::Generic.Perm) -> Returns the image of permutation `p` in `G` via embedding `p -> (id,p)`. +Return the image of permutation `p` in `G` via embedding `p → (id,p)`. """ (G::WreathProduct)(p::Generic.Perm) = G(one(G.N), p) -@doc doc""" +""" (G::WreathProduct)(n::DirectPowerGroupElem) -> Returns the image of `n` in `G` via embedding `n -> (n,())`. This is the -> embedding that makes the sequence `1 -> N -> G -> P -> 1` exact. +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)) @@ -149,14 +148,12 @@ end (p::Generic.Perm)(n::DirectPowerGroupElem) = DirectPowerGroupElem(n.elts[p.d]) -@doc doc""" +""" *(g::WreathProductElem, h::WreathProductElem) -> Return the wreath product group operation of elements, i.e. -> +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. +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) @@ -164,9 +161,9 @@ end ^(g::WreathProductElem, n::Integer) = Base.power_by_squaring(g, n) -@doc doc""" +""" inv(g::WreathProductElem) -> Returns the inverse of element of a wreath product, according to the formula +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) diff --git a/src/freereduce.jl b/src/freereduce.jl index ef56c2c..715bc30 100644 --- a/src/freereduce.jl +++ b/src/freereduce.jl @@ -4,14 +4,18 @@ # function freereduce!(::Type{Bool}, w::GWord) + if syllablelength(w) == 1 + filter!(!isone, syllables(w)) + return syllablelength(w) == 1 + end + reduced = true - for i in 1:syllablelength(w)-1 + @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 + elseif s.id === ns.id reduced = false - setmodified!(w) p1 = s.pow p2 = ns.pow @@ -19,7 +23,10 @@ function freereduce!(::Type{Bool}, w::GWord) syllables(w)[i] = change_pow(s, 0) end end - filter!(!isone, syllables(w)) + if !reduced + filter!(!isone, syllables(w)) + setmodified!(w) + end return reduced end @@ -33,11 +40,10 @@ end reduce!(w::GWord) = freereduce!(w) -@doc doc""" +""" reduce(w::GWord) -> performs reduction/simplification of a group element (word in generators). -> The default reduction is the free group reduction -> More specific procedures should be dispatched on `GWord`s type parameter. - +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/hashing.jl b/src/hashing.jl index ff799eb..316bd06 100644 --- a/src/hashing.jl +++ b/src/hashing.jl @@ -9,9 +9,9 @@ function hash_internal(W::GWord) return hash(syllables(W), hash(typeof(W), h)) end -function hash(W::GWord, h::UInt) +function hash(W::GWord, h::UInt=UInt(0); kwargs...) if ismodified(W) - savehash!(W, hash_internal(W)) + savehash!(W, hash_internal(W; kwargs...)) unsetmodified!(W) end return xor(savedhash(W), h) diff --git a/src/types.jl b/src/types.jl index 1de0a34..e3a93bf 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,31 +1,31 @@ abstract type AbstractFPGroup <: Group end -@doc doc""" +""" ::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. +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. """ abstract type GSymbol end abstract type GWord{T<:GSymbol} <: GroupElem end -@doc doc""" +""" W::GroupWord{T} <: GWord{T<:GSymbol} <:GroupElem -> Basic representation of element of a finitely presented group. `W.symbols` -> fieldname contains particular group symbols which multiplied constitute a -> group element, i.e. a word in generators. -> As reduction (inside group) of such word may be time consuming we provide -> `savedhash` and `modified` fields as well: -> hash (used e.g. in the `unique` function) is calculated by reducing the word, -> setting `modified` flag to `false` and computing the hash which is stored in -> `savedhash` field. -> whenever word `W` is changed `W.modified` is set to `false`; -> Future comparisons don't perform reduction (and use `savedhash`) as long as -> `modified` flag remains `false`. +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. +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} diff --git a/test/runtests.jl b/test/runtests.jl index 25241ff..7e29894 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -13,7 +13,7 @@ using LinearAlgebra s = one(M); s[1,3] = 2; s[3,2] = -1; S = [w,r,s]; S = unique([S; inv.(S)]); - _, sizes = Groups.generate_balls(S, radius=4); + _, sizes = Groups.wlmetric_ball(S, radius=4); @test sizes == [7, 33, 141, 561] end