Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mk/renaming #22

Merged
merged 9 commits into from
Apr 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "GroupsCore"
uuid = "d5909c97-4eac-4ecc-a3dc-fdd0858a4120"
authors = ["Marek Kaluba <kalmar@amu.edu.pl> and contributors"]
version = "0.1.1"
version = "0.2.0"

[deps]
AbstractAlgebra = "c3fe647b-3220-5bb0-a1ea-a7954cac585d"
Expand Down
70 changes: 47 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,19 @@ i.e. parent objects behave locally as singletons.

## `Group` methods

#### Iteration
* `Base.eltype(::Type{G}) where G<:Group`: return the type of elements
* `Base.iterate(G::Group[, state])`: iteration functionality
* `Base.IteratorSize(::Type{MyGroup}) [= Base.SizeUnknown()]` should be
modified to return the following only if if **all instances of `MyGroup`**
- are finite: `Base.HasLength()` / `Base.HasShape{N}()`,
- are infinite: `Base.IsInfinite()`.

In the first case one should also define `Base.length(G::MyGroup)::Int` to be
a "best effort", cheap computation of length of the group iterator. For
practical reasons the largest group you could iterate over in your lifetime
is of order that fits into an Int (`factorial(20)` nanoseconds comes to ~77
years).


Additionally the following assumptions are placed on the iteration:
* `first(G)` must be the identity
* iteration over a finitely generated group should exhaust every fixed radius ball (in
word-length metric) around the identity in finite time.
### Assumptions

`GroupsCore` implement the following methods with default values, wich may not
be generally true for all groups.
The intent of those functions is to limit the extent of the required interface.
**Special care is needed** when implementing groups to override those which may
be incorrect.
* `GroupsCore.hasgens(::Group) = true` (this is based on the assumption that
reasonably generic functions manipulating groups can be implemented only with
access to a generating set)
* `Base.length(G) = order(Int, G)` (for finite groups only). If this value is
incorrect, one needs to redefine it e.g. setting
`Base.length(G) = convert(Int, order(G))`. See notes on `length` below.

#### Obligatory methods
* `Base.one(G::Group)`: return the identity of the group
Expand All @@ -46,12 +40,42 @@ return mathematically correct answer. An infinite group must throw
`GroupsCore.InfiniteOrder` exception.
* `GroupsCore.gens(G::Group)`: return a random-accessed collection of
generators of `G`; if a group does not come with a generating set (or it may be
prohibitively expensive to compute, or if the group is not finitely generated ), one needs to alter
`GroupsCore.hasgens(::Group) = false`.
prohibitively expensive to compute, or if the group is not finitely generated,
or... when it doesn't make sense to ask for generators), one needs to redefine
`GroupsCore.hasgens(::Group)`.
* `Base.rand(rng::Random.AbstractRNG, rs::Random.Sampler{GT}) where GT<:Group`:
to enable asking for random group elements treating group as a collection, i.e.
calling `rand(G, 2, 2)`.

#### Iteration
* `Base.eltype(G::Group)`: return the type of elements of `G`

If `GroupsCore.hasgens(::Gr) where Gr<:Group` returns true (the default), one
needs to implement the iterator interface:

* `Base.iterate(G::Group[, state])`: iteration functionality
* `Base.IteratorSize(::Type{Gr}) where {Gr<:Group} [= Base.SizeUnknown()]`
* should be modified to return the following only if **all instances of `Gr`**
- are finite: `Base.HasLength()` / `Base.HasShape{N}()`,
- are infinite: `Base.IsInfinite()`.
* Note: if iterator size is `HasShape{N}()` one needs to implement `size(G::Group)` as well. For `HasLength()` we provide the default `length(G::Group) = order(Int, G).`
!!! warning
`Base.length(G::Group)::Int` should be used only for iteration purposes.
The intention is to provide a "best effort", cheap computation of length of the
group iterator. This might or might not be the correct length (as computed with
multiprecision integers).
To obtain the correct answer `GroupsCore.order(::Group)` should be used.

For practical reasons the largest group you could iterate over in your lifetime
is of order that fits into an Int (`factorial(20)` nanoseconds comes to ~77
years), therefore `typemax(Int)` is a reasonable value, even for infinite groups.


Additionally the following assumptions are placed on the iteration:
* `first(G::Group)` must be the identity
* iteration over a finitely generated group should exhaust every fixed radius
ball (in word-length metric) around the identity in finite time.

## `GroupElement` methods
#### Obligatory methods
* `Base.parent(g::GroupElement)`: return the parent object of a given group
Expand Down Expand Up @@ -81,7 +105,7 @@ implemented:
* `Base.literal_pow(::typeof(^), g, Val{-1})` → `inv(g)`
* `Base.:(/)(g, h)` → `g*h^-1`
* `Base.conj(g, h)`, `Base.:(^)(g, h)` → `h^-1*g*h`
* `Base.comm(g, h)` → `g^-1*h^-1*g*h` and its `Vararg` (`foldl`) version.
* `Base.commutator(g, h)` → `g^-1*h^-1*g*h` and its `Vararg` (`foldl`) version.
* `Base.isequal(g,h)` → `g == h` (a weaker/cheaper equality)
* `Base.:(^)(g, n::Integer)` → powering by squaring.

Expand Down Expand Up @@ -127,7 +151,7 @@ allowed.
* `GroupsCore.conj!(out::GEl, g::GEl, h::GEl) where GEl<:GroupElement`: return
`h^-1*g*h, `possibly modifying `out`. Aliasing of `g` or `h` with `out` is
allowed.
* `GroupsCore.comm!(out::GEl, g::GEl, h::GEl) where GEl<:GroupElement`: return
* `GroupsCore.commutator!(out::GEl, g::GEl, h::GEl) where GEl<:GroupElement`: return
`g^-1*h^-1*g*h`, possibly modifying `out`. Aliasing of `g` or `h` with `out` is
allowed.

Expand Down
4 changes: 2 additions & 2 deletions src/GroupsCore.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ const GroupElement = AbstractAlgebra.GroupElem
# abstract type GroupElement end

export Group, GroupElement
export comm, gens, hasgens, isfiniteorder, ngens, order
# export one!, inv!, mul!, conj!, comm!, div_left!, div_right!
export commutator, gens, hasgens, isfiniteorder, ngens, order
# export one!, inv!, mul!, conj!, commutator!, div_left!, div_right!

struct InterfaceNotImplemented <: Exception
family::Symbol
Expand Down
14 changes: 7 additions & 7 deletions src/group_elements.jl
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,15 @@ Alias for [`conj`](@ref GroupsCore.conj).
Base.:(^)(g::GEl, h::GEl) where {GEl <: GroupElement} = conj(g, h)

@doc Markdown.doc"""
comm(g::GEl, h::GEl, k::GEl...) where {GEl <: GroupElement}
commutator(g::GEl, h::GEl, k::GEl...) where {GEl <: GroupElement}

Return the left associative iterated commutator $[[g, h], ...]$, where
$[g, h] = g^{-1} h^{-1} g h$.
"""
function comm(g::GEl, h::GEl, k::GEl...) where {GEl <: GroupElement}
res = comm!(similar(g), g, h)
function commutator(g::GEl, h::GEl, k::GEl...) where {GEl <: GroupElement}
res = commutator!(similar(g), g, h)
for l in k
res = comm!(res, res, l)
res = commutator!(res, res, l)
end
return res
end
Expand Down Expand Up @@ -255,13 +255,13 @@ function conj!(out::GEl, g::GEl, h::GEl) where {GEl <: GroupElement}
end

@doc Markdown.doc"""
comm!(out::GEl, g::GEl, h::GEl) where {GEl <: GroupElement}
commutator!(out::GEl, g::GEl, h::GEl) where {GEl <: GroupElement}

Return $g^{-1} h^{-1} g h$, possibly modifying `out`. Aliasing of `g` or `h`
with `out` is allowed.
"""
function comm!(out::GEl, g::GEl, h::GEl) where {GEl <: GroupElement}
# TODO: can we make comm! with 3 arguments without allocation??
function commutator!(out::GEl, g::GEl, h::GEl) where {GEl <: GroupElement}
# TODO: can we make commutator! with 3 arguments without allocation??
out = conj!(out, g, h)
return div_left!(out, out, g)
end
37 changes: 28 additions & 9 deletions src/groups.jl
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,25 @@ end
################################################################################

Base.eltype(::Type{Gr}) where {Gr <: Group} =
throw(InterfaceNotImplemented(:Iteration, "Base.eltype(::$(typeof(Gr)))"))
throw(InterfaceNotImplemented(:Iteration, "Base.eltype(::Type{$Gr})"))

Base.iterate(G::Group) =
throw(InterfaceNotImplemented(:Iteration, "Base.iterate(::$(typeof(G)))"))
Base.iterate(G::Group, state) = throw(
InterfaceNotImplemented(:Iteration, "Base.iterate(::$(typeof(G)), state)"),
)
function Base.iterate(G::Group)
hasgens(G) && throw(
InterfaceNotImplemented(:Iteration, "Base.iterate(::$(typeof(G)))")
)
throw(ArgumentError(
"Group does not seem to have generators. Did you alter `hasgens(::$(typeof(G)))`?",
))
end

function Base.iterate(G::Group, state)
hasgens(G) && throw(
InterfaceNotImplemented(:Iteration, "Base.iterate(::$(typeof(G)), state)"),
)
throw(ArgumentError(
"Group does not seem to have generators. Did you alter `hasgens(::$(typeof(G)))`?",
))
end

@doc Markdown.doc"""
IteratorSize(::Type{Gr}) where {Gr <: Group}
Expand All @@ -84,7 +96,14 @@ Given the type of a group, return one of the following values:
* `Base.SizeUnknown()` otherwise (the default).
"""
Base.IteratorSize(::Type{Gr}) where {Gr <: Group} = Base.SizeUnknown()
Base.length(G::Group) = order(Int, G)
# cheating here, not great, but nobody should use this function except iteration.
Base.length(G::Group) =
isfinite(G) ? order(Int, G) : throw(
"""You're trying to iterate over an infinite group.
If you know what you're doing choose an appropriate integer and redefine
`Base.length(::$(typeof(G)))::Int`.
"""
)

################################################################################
# Default implementations
Expand Down Expand Up @@ -115,9 +134,9 @@ hasgens(G::Group) = true
function gens(G::Group, i::Integer)
hasgens(G) && return gens(G)[i]
# TODO: throw something more specific
throw(
throw(ArgumentError(
"Group does not seem to have generators. Did you alter `hasgens(::$(typeof(G)))`?",
)
))
end

function ngens(G::Group)
Expand Down
28 changes: 15 additions & 13 deletions test/conformance_test.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ function test_Group_interface(G::Group)
@test GroupsCore.elem_type(typeof(G)) == eltype(G)
@test one(G) isa eltype(G)

@test first(iterate(G)) isa eltype(G)
_, s = iterate(G)
@test first(iterate(G, s)) isa eltype(G)
@test isone(first(G))
if GroupsCore.hasgens(G)
@test first(iterate(G)) isa eltype(G)
_, s = iterate(G)
@test first(iterate(G, s)) isa eltype(G)
@test isone(first(G))
end
else
@test isfinite(G) == false
end
Expand Down Expand Up @@ -139,10 +141,10 @@ function test_GroupElement_interface(g::GEl, h::GEl) where {GEl<:GroupElement}
@test ^(g, h) == inv(h) * g * h
@test (g, h) == (old_g, old_h)

@test comm(g, h) == g^-1 * h^-1 * g * h
@test commutator(g, h) == g^-1 * h^-1 * g * h
@test (g, h) == (old_g, old_h)

@test comm(g, h, g) == conj(inv(g), h) * conj(conj(g, h), g)
@test commutator(g, h, g) == conj(inv(g), h) * conj(conj(g, h), g)
@test (g, h) == (old_g, old_h)

@test isone(g * inv(g)) && isone(inv(g) * g)
Expand Down Expand Up @@ -177,12 +179,12 @@ function test_GroupElement_interface(g::GEl, h::GEl) where {GEl<:GroupElement}
@test similar(g) isa typeof(g)
end

one!, inv!, mul!, conj!, comm!, div_left!, div_right! = (
one!, inv!, mul!, conj!, commutator!, div_left!, div_right! = (
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So... to color the bike shed... it is inv, mul, div... why not expand those? Because Julia defines them, I guess... but then comm fits better into that sequence. OTOH if we only expand things that we add over Base, shouldn't we so it for all and also expand conj?

Don't get me wrong, I am not opposed, but I am confused: what's the rule here?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tbh. I have no idea, but this change was requested by @thofma in #20 ...

GroupsCore.one!,
GroupsCore.inv!,
GroupsCore.mul!,
GroupsCore.conj!,
GroupsCore.comm!,
GroupsCore.commutator!,
GroupsCore.div_left!,
GroupsCore.div_right!,
)
Expand Down Expand Up @@ -235,20 +237,20 @@ function test_GroupElement_interface(g::GEl, h::GEl) where {GEl<:GroupElement}
g = deepcopy(old_g)
end

@testset "comm!" begin
@testset "commutator!" begin
res = old_g^-1 * old_h^-1 * old_g * old_h

@test comm!(out, g, h) == res
@test commutator!(out, g, h) == res
@test (g, h) == (old_g, old_h)

@test comm!(out, g, h) == res
@test commutator!(out, g, h) == res
@test (g, h) == (old_g, old_h)

@test comm!(g, g, h) == res
@test commutator!(g, g, h) == res
@test h == old_h
g = deepcopy(old_g)

@test comm!(h, g, h) == res
@test commutator!(h, g, h) == res
@test g == old_g
h = deepcopy(old_h)
end
Expand Down
16 changes: 16 additions & 0 deletions test/test_notsatisfied.jl
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
module TestNotImplemented

using GroupsCore
using Test

struct SomeGroup <: Group end

struct SomeGroupElement <: GroupElement
Expand Down Expand Up @@ -30,6 +35,15 @@ end
@test_throws INI iterate(G)
@test_throws INI iterate(G, 1)

GroupsCore.hasgens(::SomeGroup) = false

@test_throws ArgumentError iterate(G)
@test_throws ArgumentError iterate(G, 1)
@test_throws ArgumentError gens(G, 1)

# revert to the default
GroupsCore.hasgens(::SomeGroup) = true

# Assumption 1: Groups are of unknown size
@test Base.IteratorSize(G) == Base.SizeUnknown()
@test_throws ArgumentError Base.isfinite(G)
Expand Down Expand Up @@ -92,3 +106,5 @@ end
end

end

end # of module TestNotImplemented