diff --git a/README.md b/README.md index 1328f9e..707b335 100644 --- a/README.md +++ b/README.md @@ -5,25 +5,84 @@ [![Code Style: Blue](https://img.shields.io/badge/code%20style-blue-4495d1.svg)](https://github.com/invenia/BlueStyle) [![PkgEval](https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/C/CommonRLSpaces.svg)](https://JuliaCI.github.io/NanosoldierReports/pkgeval_badges/report.html) +## Introduction + +A space is simply a set of objects. In a reinforcement learning context, spaces define the sets of possible states, actions, and observations. + +In Julia, spaces can be represented by a variety of objects. For instance, a small discrete action set might be represented with `["up", "left", "down", "right"]`, or an interval of real numbers might be represented with an object from the [`IntervalSets`](https://github.com/JuliaMath/IntervalSets.jl) package. In general, the space defined by any Julia object is the set of objects `x` for which `x in space` returns `true`. + +In addition to establishing the definition above, this package provides three useful tools: + +1. Traits to communicate about the properties of spaces, e.g. whether they are continuous or discrete, how many subspaces they have, and how to interact with them. +2. Functions such as `product` for constructing more complex spaces +3. Constructors to for spaces whose elements are arrays, such as `ArraySpace` and `Box`. + +## Concepts and Interface + +### Interface for all spaces + +Since a space is simply a set of objects, a wide variety of common Julia types including `Vector`, `Set`, `Tuple`, and `Dict`1can represent a space. +Because of this inclusive definition, there is a very minimal interface that all spaces are expected to implement. Specifically, it consists of +- `in(x, space)`, which tests whether `x` is a member of the set `space` (this can also be called with the `x in space` syntax). +- `rand(space)`, which returns a valid member of the set2. +- `eltype(space)`, which returns the type of the elements in the space. + +In addition, the `SpaceStyle` trait is always defined. Calling `SpaceStyle(space)` will return either a `FiniteSpaceStyle`, `ContinuousSpaceStyle`, `HybridSpaceStyle`, or an `UnknownSpaceStyle` object. + +### Finite discrete spaces + +Spaces with a finite number of elements have `FiniteSpaceStyle`. These spaces are guaranteed to be iterable, implementing Julia's [iteration interface](https://docs.julialang.org/en/v1/manual/interfaces/). In particular `collect(space)` will return all elements in an array. + +### Continuous spaces + +Continuous spaces represent sets that have an uncountable number of elements they have a `SpaceStyle` of type `ContinuousSpaceStyle`. CommonRLSpaces does not adopt a rigorous mathematical definition of a continuous set, but, roughly, elements in the interior of a continuous space have other elements very close to them. + +Continuous spaces have some additional interface functions: + +- `bounds(space)` returns upper and lower bounds in a tuple. For example, if `space` is a unit circle, `bounds(space)` will return `([-1.0, -1.0], [1.0, 1.0])`. This allows agents to choose policies that appropriately cover the space e.g. a normal distribution with a mean of `mean(bounds(space))` and a standard deviation of half the distance between the bounds. +- `clamp(x, space)` returns an element of `space` that is near `x`. i.e. if `space` is a unit circle, `clamp([2.0, 0.0], space)` might return `[1.0, 0.0]`. This allows for a convenient way for an agent to find a valid action if they sample actions from a distribution that doesn't match the space exactly (e.g. a normal distribution). +- `clamp!(x, space)`, similar to `clamp`, but clamps `x` in place. + +### Hybrid spaces + +The interface for hybrid continuous-discrete spaces is currently planned, but not yet defined. If the space style is not `FiniteSpaceStyle` or `ContinuousSpaceStyle`, it is `UnknownSpaceStyle`. + +### Spaces of arrays + +[need to figure this out, but I think `elsize(space)` should return the size of the arrays in the space] + +### Cartesian products of spaces + +The Cartesian product of two spaces `a` and `b` can be constructed with `c = product(a, b)`. + +The exact form of the resulting space is unspecified and should be considered an implementation detail. The only guarantees are (1) that there will be one unique element of `c` for every combination of one object from `a` and one object from `b` and (2) that the resulting space conforms to the interface above. + +The `TupleSpaceProduct` constructor provides a specialized Cartesian product where each element is a tuple, i.e. `TupleSpaceProduct(a, b)` has elements of type `Tuple{eltype(a), eltype(b)}`. + +--- + +1Note: the elements of a space represented by a `Dict` are key-value `Pair`s. +2[TODO: should we make any guarantees about whether `rand(space)` is drawn from a uniform distribution?] + ## Usage ### Construction |Category|Style|Example| |:---|:----|:-----| -|Enumerable discrete space| `DiscreteSpaceStyle{()}()` | `Space((:cat, :dog))`, `Space(0:1)`, `Space(1:2)`, `Space(Bool)`| -|Multi-dimensional discrete space| `DiscreteSpaceStyle{(3,4)}()` | `Space((:cat, :dog), 3, 4)`, `Space(0:1, 3, 4)`, `Space(1:2, 3, 4)`, `Space(Bool, 3, 4)`| -|Multi-dimensional variable discrete space| `DiscreteSpaceStyle{(2,)}()` | `Space(SVector((:cat, :dog), (:litchi, :longan, :mango))`, `Space([-1:1, (false, true)])`| -|Continuous space| `ContinuousSpaceStyle{()}()` | `Space(-1.2..3.3)`, `Space(Float32)`| -|Multi-dimensional continuous space| `ContinuousSpaceStyle{(3,4)}()` | `Space(-1.2..3.3, 3, 4)`, `Space(Float32, 3, 4)`| +|Enumerable discrete space| `FiniteSpaceStyle{()}()` | `(:cat, :dog)`, `0:1`, `["a","b","c"]` | +|One dimensional continuous space| `ContinuousSpaceStyle{()}()` | `-1.2..3.3`, `Interval(1.0, 2.0)` | +|Multi-dimensional discrete space| `FiniteSpaceStyle{(3,4)}()` | `ArraySpace((:cat, :dog), 3, 4)`, `ArraySpace(0:1, 3, 4)`, `ArraySpace(1:2, 3, 4)`, `ArraySpace(Bool, 3, 4)`| +|Multi-dimensional variable discrete space| `FiniteSpaceStyle{(2,)}()` | `product((:cat, :dog), (:litchi, :longan, :mango))`, `product(-1:1, (false, true))`| +|Multi-dimensional continuous space| `ContinuousSpaceStyle{(2,)}()` or `ContinuousSpaceStyle{(3,4)}()` | `Box([-1.0, -2.0], [2.0, 4.0])`, `product(-1.2..3.3, -4.6..5.0)`, `ArraySpace(-1.2..3.3, 3, 4)`, `ArraySpace(Float32, 3, 4)` | +|Multi-dimensional hybrid space| `HybridSpaceStyle{(2,),()}()` | `product(-1.2..3.3, -4.6..5.0, [:cat, :dog])`, `product(Box([-1.0, -2.0], [2.0, 4.0]), [1,2,3])`| ### API ```julia julia> using CommonRLSpaces -julia> s = Space((:litchi, :longan, :mango)) -Space{Tuple{Symbol, Symbol, Symbol}}((:litchi, :longan, :mango)) +julia> s = (:litchi, :longan, :mango) julia> rand(s) :litchi @@ -31,44 +90,52 @@ julia> rand(s) julia> rand(s) in s true -julia> size(s) -() +julia> length(s) +3 ``` ```julia -julia> s = Space(UInt8, 2,3) -Space{Matrix{UnitRange{UInt8}}}(UnitRange{UInt8}[0x00:0xff 0x00:0xff 0x00:0xff; 0x00:0xff 0x00:0xff 0x00:0xff]) +julia> s = ArraySpace(1:5, 2,3) +CommonRLSpaces.RepeatedSpace{UnitRange{Int64}, Tuple{Int64, Int64}}(1:5, (2, 3)) julia> rand(s) -2×3 Matrix{UInt8}: - 0x7b 0x38 0xf3 - 0x6a 0xe1 0x28 +2×3 Matrix{Int64}: + 4 1 1 + 3 2 2 julia> rand(s) in s true julia> SpaceStyle(s) -DiscreteSpaceStyle{(2, 3)}() +FiniteSpaceStyle() -julia> size(s) +julia> elsize(s) (2, 3) ``` ```julia -julia> s = Space(SVector(-1..1, 0..1)) -Space{SVector{2, ClosedInterval{Int64}}}(ClosedInterval{Int64}[-1..1, 0..1]) +julia> s = product(-1..1, 0..1) +Box{StaticArraysCore.SVector{2, Float64}}([-1.0, 0.0], [1.0, 1.0]) julia> rand(s) -2-element SVector{2, Float64} with indices SOneTo(2): - 0.5563101538643473 - 0.9227368869418011 +2-element StaticArraysCore.SVector{2, Float64} with indices SOneTo(2): + 0.03049072910834738 + 0.6295234114874269 julia> rand(s) in s true julia> SpaceStyle(s) -ContinuousSpaceStyle{(2,)}() +ContinuousSpaceStyle() -julia> size(s) +julia> elsize(s) (2,) -``` \ No newline at end of file + +julia> bounds(s) +([-1.0, 0.0], [1.0, 1.0]) + +julia> clamp([5, 5], s) +2-element StaticArraysCore.SizedVector{2, Float64, Vector{Float64}} with indices SOneTo(2): + 1.0 + 1.0 +``` diff --git a/src/CommonRLSpaces.jl b/src/CommonRLSpaces.jl index e48970a..7b68225 100644 --- a/src/CommonRLSpaces.jl +++ b/src/CommonRLSpaces.jl @@ -2,11 +2,34 @@ module CommonRLSpaces using Reexport -@reexport using FillArrays @reexport using IntervalSets -@reexport using StaticArrays -@reexport import Base: OneTo + +using StaticArrays +using FillArrays +using Random +import Base: clamp + +export + SpaceStyle, + AbstractSpaceStyle, + FiniteSpaceStyle, + ContinuousSpaceStyle, + UnknownSpaceStyle, + bounds, + elsize include("basic.jl") +export + Box, + ArraySpace + +include("array.jl") + +export + product, + TupleProduct + +include("product.jl") + end diff --git a/src/array.jl b/src/array.jl new file mode 100644 index 0000000..7f764e5 --- /dev/null +++ b/src/array.jl @@ -0,0 +1,81 @@ +abstract type AbstractArraySpace end +# Maybe AbstractArraySpace should have an eltype parameter so that you could call convert(AbstractArraySpace{Float32}, space) + +""" + Box(lower, upper) + +A Box represents a space of real-valued arrays bounded element-wise above by `upper` and below by `lower`, e.g. `Box([-1, -2], [3, 4]` represents the two-dimensional vector space that is the Cartesian product of the two closed sets: ``[-1, 3] \\times [-2, 4]``. + +The elements of a Box are always `AbstractArray`s with `AbstractFloat` elements. `Box`es always have `ContinuousSpaceStyle`, and products of `Box`es with `Box`es or `ClosedInterval`s are `Box`es when the dimensions are compatible. +""" +struct Box{A<:AbstractArray{<:AbstractFloat}} <: AbstractArraySpace + lower::A + upper::A + + Box{A}(lower, upper) where {A<:AbstractArray} = new(lower, upper) +end + +function Box(lower, upper; convert_to_static::Bool=false) + @assert size(lower) == size(upper) + sz = size(lower) + continuous_lower = convert(AbstractArray{float(eltype(lower))}, lower) + continuous_upper = convert(AbstractArray{float(eltype(upper))}, upper) + if convert_to_static + final_lower = SArray{Tuple{sz...}}(continuous_lower) + final_upper = SArray{Tuple{sz...}}(continuous_upper) + else + final_lower, final_upper = promote(continuous_lower, continuous_upper) + end + return Box{typeof(final_lower)}(final_lower, final_upper) +end + +# By default, convert builtin arrays to static +Box(lower::Array, upper::Array) = Box(lower, upper; convert_to_static=true) + +SpaceStyle(::Box) = ContinuousSpaceStyle() + +function Base.rand(rng::AbstractRNG, sp::Random.SamplerTrivial{<:Box}) + box = sp[] + return box.lower + rand_similar(rng, box.lower) .* (box.upper-box.lower) +end + +rand_similar(rng::AbstractRNG, a::StaticArray) = rand(rng, typeof(a)) +rand_similar(rng::AbstractRNG, a::AbstractArray) = rand(rng, eltype(a), size(a)...) + +Base.in(x::AbstractArray, b::Box) = all(b.lower .<= x .<= b.upper) + +Base.eltype(::Box{A}) where A = A +elsize(b::Box) = size(b.lower) + +bounds(b::Box) = (b.lower, b.upper) +Base.clamp(x::AbstractArray, b::Box) = clamp.(x, b.lower, b.upper) + +Base.convert(t::Type{<:Box}, i::ClosedInterval) = t(SA[minimum(i)], SA[maximum(i)]) + +struct RepeatedSpace{B, S<:Tuple} <: AbstractArraySpace + base_space::B + elsize::S +end + +""" + ArraySpace(base_space, size...) + +Create a space of Arrays with shape `size`, where each element of the array is drawn from `base_space`. +""" +ArraySpace(base_space, size...) = RepeatedSpace(base_space, size) + +SpaceStyle(s::RepeatedSpace) = SpaceStyle(s.base_space) + +Base.rand(rng::AbstractRNG, sp::Random.SamplerTrivial{<:RepeatedSpace}) = rand(rng, sp[].base_space, sp[].elsize...) + +Base.in(x::AbstractArray, s::RepeatedSpace) = all(entry in s.base_space for entry in x) +Base.eltype(s::RepeatedSpace) = AbstractArray{eltype(s.base_space), length(s.elsize)} +Base.eltype(s::RepeatedSpace{<:AbstractInterval}) = AbstractArray{Random.gentype(s.base_space), length(s.elsize)} +elsize(s::RepeatedSpace) = s.elsize + +function bounds(s::RepeatedSpace) + bs = bounds(s.base_space) + return (Fill(first(bs), s.elsize...), Fill(last(bs), s.elsize...)) +end + +Base.clamp(x::AbstractArray, s::RepeatedSpace) = map(entry -> clamp(entry, s.base_space), x) diff --git a/src/basic.jl b/src/basic.jl index af6d747..3bf5314 100644 --- a/src/basic.jl +++ b/src/basic.jl @@ -1,97 +1,64 @@ -export Space, SpaceStyle, DiscreteSpaceStyle, ContinuousSpaceStyle, TupleSpace, NamedSpace, DictSpace +abstract type AbstractSpaceStyle end -using Random +struct FiniteSpaceStyle <: AbstractSpaceStyle end +struct ContinuousSpaceStyle <: AbstractSpaceStyle end +struct UnknownSpaceStyle <: AbstractSpaceStyle end -struct Space{T} - s::T -end - -Space(s::Type{T}) where {T<:Integer} = Space(typemin(T):typemax(T)) -Space(s::Type{T}) where {T<:AbstractFloat} = Space(typemin(T) .. typemax(T)) - -Space(x, dims::Int...) = Space(Fill(x, dims)) -Space(x::Type{T}, dim::Int, extra_dims::Int...) where {T<:Integer} = Space(Fill(typemin(x):typemax(T), dim, extra_dims...)) -Space(x::Type{T}, dim::Int, extra_dims::Int...) where {T<:AbstractFloat} = Space(Fill(typemin(x) .. typemax(T), dim, extra_dims...)) -Space(x::Type{T}, dim::Int, extra_dims::Int...) where {T} = Space(Fill(T, dim, extra_dims...)) - -Base.size(s::Space) = size(SpaceStyle(s)) -Base.length(s::Space) = length(SpaceStyle(s), s) -Base.getindex(s::Space, i...) = getindex(SpaceStyle(s), s, i...) -Base.:(==)(s1::Space, s2::Space) = s1.s == s2.s - -##### - -abstract type AbstractSpaceStyle{S} end - -struct DiscreteSpaceStyle{S} <: AbstractSpaceStyle{S} end -struct ContinuousSpaceStyle{S} <: AbstractSpaceStyle{S} end +""" + SpaceStyle(space) -SpaceStyle(::Space{<:Tuple}) = DiscreteSpaceStyle{()}() -SpaceStyle(::Space{<:AbstractVector{<:Number}}) = DiscreteSpaceStyle{()}() -SpaceStyle(::Space{<:AbstractInterval}) = ContinuousSpaceStyle{()}() +Holy-style trait that describes whether the space is continuous, finite discrete, or an unknown type. See CommonRLInterface for a more detailed description of the styles. +""" +SpaceStyle(space::Any) = UnknownSpaceStyle() -SpaceStyle(s::Space{<:AbstractArray{<:Tuple}}) = DiscreteSpaceStyle{size(s.s)}() -SpaceStyle(s::Space{<:AbstractArray{<:AbstractRange}}) = DiscreteSpaceStyle{size(s.s)}() -SpaceStyle(s::Space{<:AbstractArray{<:AbstractInterval}}) = ContinuousSpaceStyle{size(s.s)}() +SpaceStyle(::Tuple) = FiniteSpaceStyle() +SpaceStyle(::NamedTuple) = FiniteSpaceStyle() -Base.size(::AbstractSpaceStyle{S}) where {S} = S -Base.length(::DiscreteSpaceStyle{()}, s) = length(s.s) -Base.getindex(::DiscreteSpaceStyle{()}, s, i...) = getindex(s.s, i...) -Base.length(::DiscreteSpaceStyle, s) = mapreduce(length, *, s.s) +function SpaceStyle(x::Union{AbstractArray,AbstractDict,AbstractSet,AbstractRange}) + if Base.IteratorSize(x) isa Union{Base.HasLength, Base.HasShape} && length(x) < Inf + return FiniteSpaceStyle() + else + return UnknownSpaceStyle() + end +end -##### +SpaceStyle(::AbstractInterval) = ContinuousSpaceStyle() -Random.rand(rng::Random.AbstractRNG, s::Space) = rand(rng, s.s) +promote_spacestyle(::FiniteSpaceStyle, ::FiniteSpaceStyle) = FiniteSpaceStyle() +promote_spacestyle(::ContinuousSpaceStyle, ::ContinuousSpaceStyle) = ContinuousSpaceStyle() +promote_spacestyle(_, _) = UnknownSpaceStyle() -Random.rand( - rng::Random.AbstractRNG, - s::Union{ - <:Space{<:AbstractArray{<:Tuple}}, - <:Space{<:AbstractArray{<:AbstractRange}}, - <:Space{<:AbstractArray{<:AbstractInterval}} - } -) = map(x -> rand(rng, x), s.s) +# handle case of 3 or more +promote_spacestyle(s1, s2, s3, others...) = foldl(promote_spacestyle, (s1, s2, s3, args...)) -Base.in(x, s::Space) = x in s.s -Base.in(x, s::Space{<:Type}) = x isa s.s +"Return the size of the objects in a space. This is guaranteed to be defined if the objects in the space are arrays, but otherwise it may not be defined." +function elsize end # note: different than Base.elsize -Base.in( - x, - s::Union{ - <:Space{<:AbstractArray{<:Tuple}}, - <:Space{<:AbstractArray{<:AbstractRange}}, - <:Space{<:AbstractArray{<:AbstractInterval}} - } -) = size(x) == size(s) && all(x -> x[1] in x[2], zip(x, s.s)) +""" + bounds(space) -function Random.rand(rng::AbstractRNG, s::Interval{:closed,:closed,T}) where {T} - if s == typemin(T) .. typemax(T) - rand(T) - else - r = rand(rng) +Return a `Tuple` containing lower and upper bounds for the elements in a space. - if r == 0.0 - r = rand(Bool) - end +For example, if `space` is a unit circle, `bounds(space)` will return `([-1.0, -1.0], [1.0, 1.0])`. This allows agents to choose policies that appropriately cover the space e.g. a normal distribution with a mean of `mean(bounds(space))` and a standard deviation of half the distance between the bounds. - r * (s.right - s.left) + s.left - end -end +`bounds` should be defined for ContinuousSpaceStyle spaces. -Base.iterate(s::Space, args...) = iterate(SpaceStyle(s), s, args...) -Base.iterate(::DiscreteSpaceStyle{()}, s::Space, args...) = iterate(s.s, args...) +# Example +```juliadoctest +julia> bounds(1..2) +(1, 2) +``` +""" +function bounds end -##### +""" + clamp(x, space) -const TupleSpace = Tuple{Vararg{Space}} -const NamedSpace = NamedTuple{<:Any,<:TupleSpace} -const VectorSpace = Vector{<:Space} -const DictSpace = Dict{<:Any,<:Space} +Return an element of `space` that is near `x`. -Random.rand(rng::AbstractRNG, s::Union{TupleSpace,NamedSpace,VectorSpace}) = map(x -> rand(rng, x), s) -Random.rand(rng::AbstractRNG, s::DictSpace) = Dict(k => rand(rng, s[k]) for k in keys(s)) +For example, if `space` is a unit circle, `clamp([2.0, 0.0], space)` might return `[1.0, 0.0]`. This allows for a convenient way for an agent to find a valid action if they sample actions from a distribution that doesn't match the space exactly (e.g. a normal distribution). +""" +function clamp end -Base.in(xs::Tuple, ts::TupleSpace) = length(xs) == length(ts) && all(((x, s),) -> x in s, zip(xs, ts)) -Base.in(xs::AbstractVector, ts::VectorSpace) = length(xs) == length(ts) && all(((x, s),) -> x in s, zip(xs, ts)) -Base.in(xs::NamedTuple{names}, ns::NamedTuple{names,<:TupleSpace}) where {names} = all(((x, s),) -> x in s, zip(xs, ns)) -Base.in(xs::Dict, ds::DictSpace) = length(xs) == length(ds) && all(k -> haskey(ds, k) && xs[k] in ds[k], keys(xs)) \ No newline at end of file +bounds(i::AbstractInterval) = (infimum(i), supremum(i)) +Base.clamp(x, i::AbstractInterval) = IntevalSets.clamp(x, i) diff --git a/src/product.jl b/src/product.jl new file mode 100644 index 0000000..f8e5506 --- /dev/null +++ b/src/product.jl @@ -0,0 +1,64 @@ +product(i1::ClosedInterval, i2::ClosedInterval) = Box(SA[minimum(i1), minimum(i2)], SA[maximum(i1), maximum(i2)]) + +product(b::Box, i::ClosedInterval) = product(b, convert(Box, i)) +product(i::ClosedInterval, b::Box) = product(convert(Box, i), b) +product(b1::Box{<:AbstractVector}, b2::Box{<:AbstractVector}) = Box(vcat(b1.lower, b2.lower), vcat(b1.upper, b2.upper)) +function product(b1::Box, b2::Box) + if size(b1.lower, 2) == size(b2.lower, 2) # same number of columns + return Box(vcat(b1.lower, b2.lower), vcat(b1.upper, b2.upper)) + else + return TupleSpaceProduct((b1, b2)) + end +end + +# handle case of 3 or more +product(s1, s2, s3, args...) = foldl(product, (s1, s2, s3, args...)) # not totally sure if this should be foldl or foldr + +struct TupleProduct{T<:Tuple} + ss::T +end + +""" + TupleProduct(space1, space2, ...) + +Create a space representing the Cartesian product of the argument. Each element is a `Tuple` containing one element from each of the constituent spaces. + +Use `subspaces` to access a `Tuple` containing the constituent spaces. +""" +TupleProduct(s1, s2, others...) = TupleProduct((s1, s2, others...)) + +"Return a `Tuple` containing the spaces used to create a `TupleProduct`" +subspaces(s::TupleProduct) = s.ss + +product(s1::TupleProduct, s2::TupleProduct) = TupleProduct(subspaces(s1)..., subspaces(s2)...) + +# handle any case not covered elsewhere by making a TupleProduct +# if one of the members is already a TupleProduct, we add put them together in a new "flat" TupleProduct +# note: if we had defined product(s1::TupleProduct, s2) it might be annoying because product(s1, s2::AnotherProduct) would be ambiguous with it +function product(s1, s2) + if s1 isa TupleProduct + return TupleProduct(subspaces(s1)..., s2) + elseif s2 isa TupleProduct + return TupleProduct(s1, subspaces(s2)...) + else + return TupleProduct(s1, s2) + end +end + +SpaceStyle(s::TupleProduct) = promote_spacestyle(map(SpaceStyle, subspaces(s))...) + +Base.rand(rng::AbstractRNG, sp::Random.SamplerTrivial{<:TupleProduct}) = map(s->rand(rng, s), subspaces(sp[])) +function Base.in(element, space::TupleProduct) + @assert length(element) == length(subspaces(space)) + return all(element[i] in s for (i, s) in enumerate(subspaces(space))) +end +Base.eltype(space::TupleProduct) = Tuple{map(eltype, subspaces(space))...} + +Base.length(space::TupleProduct) = mapreduce(length, *, subspaces(space)) +Base.iterate(space, args...) = iterate(Iterators.product(subspaces(space)...), args...) + +function bounds(s::TupleProduct) + bds = map(bounds, subspaces(s)) + return (first.(bds), last.(bds)) +end +Base.clamp(x, s::TupleProduct) = map(clamp, x, subspaces(s)) diff --git a/test/array.jl b/test/array.jl new file mode 100644 index 0000000..30e3fec --- /dev/null +++ b/test/array.jl @@ -0,0 +1,88 @@ +@testset "Box" begin + @testset "Box with StaticVector" begin + lower = SA[-1.0, -2.0] + upper = SA[1.0, 2.0] + b = Box(lower, upper) + @test @inferred SpaceStyle(b) == ContinuousSpaceStyle() + @test eltype(b) <: AbstractArray{Float64} + @test eltype(b) <: StaticArray + @test @inferred typeof(rand(b)) == eltype(b) + @test @inferred [0.0, 0.0] in b + @test @inferred rand(b) in b + @test @inferred bounds(b) == (lower, upper) + @test @inferred clamp(SA[3.0, 4.0], b) in b + @test @inferred elsize(b) == (2,) + end + + @testset "Box with Vector{Float64}" begin + lower = [-1.0, -2.0] + upper = [1.0, 2.0] + b = Box(lower, upper; convert_to_static=false) + @test @inferred SpaceStyle(b) == ContinuousSpaceStyle() + @test @inferred eltype(b) == Vector{Float64} + @test @inferred [0.0, 0.0] in b + @test @inferred rand(b) in b + @test @inferred bounds(b) == (lower, upper) + @test @inferred clamp([3.0, 4.0], b) in b + @test @inferred elsize(b) == (2,) + end + + @testset "Box with Vector{Int}" begin + lower = [-1, -2] + upper = [1, 2] + b = Box(lower, upper) + @test @inferred SpaceStyle(b) == ContinuousSpaceStyle() + @test eltype(b) <: AbstractVector{Float64} + @test @inferred [0.0, 0.0] in b + @test @inferred rand(b) in b + @test @inferred bounds(b) == (lower, upper) + @test @inferred clamp([3.0, 4.0], b) in b + @test @inferred elsize(b) == (2,) + end + + @testset "Box with Matrix" begin + lower = -[1 2; 3 4] + upper = [1 2; 3 4] + b = Box(lower, upper) + @test @inferred SpaceStyle(b) == ContinuousSpaceStyle() + @test eltype(b) <: AbstractMatrix{Float64} + @test @inferred zeros(2,2) in b + @test @inferred rand(b) in b + @test @inferred bounds(b) == (lower, upper) + @test @inferred clamp([3 -4; 5 -6], b) in b + @test @inferred elsize(b) == (2,2) + end + + @testset "Box comparison" begin + @test Box([1,2], [3,4]) == Box([1,2], [3,4]) + @test Box([1,2], [3,4]) != Box([1,3], [3,4]) + end + + @testset "Interval to box conversion" begin + @test convert(Box, 1..2) == Box([1], [2]) + end + + @testset "ArraySpace with Range" begin + s = ArraySpace(1:5, 3, 4) + @test @inferred SpaceStyle(s) == FiniteSpaceStyle() + @test eltype(s) <: AbstractMatrix{eltype(1:5)} + @test @inferred ones(Int, 3, 4) in s + @test @inferred rand(s) in s + @test rand(s) isa Matrix{Int} + # @test @inferred bounds(s) == (ones(Int, 3, 4), 5*ones(Int, 3, 4)) # note: not actually required by interface since this is FiniteSpaceStyle + @test_broken collect(s) isa Vector{Matrix{Int}} + @test @inferred elsize(s) == (3,4) + end + + @testset "ArraySpace with IntervalSet" begin + s = ArraySpace(1..5, 3, 4) + @test @inferred SpaceStyle(s) == ContinuousSpaceStyle() + @test eltype(s) <: AbstractMatrix{Float64} + @test @inferred ones(Float64, 3, 4) in s + @test @inferred rand(s) in s + @test rand(s) isa Matrix{Float64} + @test @inferred bounds(s) == (ones(3, 4), 5*ones(3, 4)) + @test @inferred clamp(zeros(3,4), s) == ones(3,4) + @test @inferred elsize(s) == (3,4) + end +end diff --git a/test/basic.jl b/test/basic.jl new file mode 100644 index 0000000..5a47696 --- /dev/null +++ b/test/basic.jl @@ -0,0 +1,15 @@ +@testset "Styles of types outside of this package" begin + struct TestSpace1 end + @test SpaceStyle(TestSpace1()) == UnknownSpaceStyle() + @test SpaceStyle((1,2)) == FiniteSpaceStyle() + @test SpaceStyle((a=1, b=2)) == FiniteSpaceStyle() + @test SpaceStyle([1,2]) == FiniteSpaceStyle() + @test SpaceStyle(Dict(:a=>1)) == FiniteSpaceStyle() + @test SpaceStyle(Set([1,2])) == FiniteSpaceStyle() + @test SpaceStyle(1..2) == ContinuousSpaceStyle() +end + +@testset "Bounds and clamp for IntervalSets" begin + @test bounds(1..2) == (1,2) + @test clamp(0.1, 1..2) == 1 +end diff --git a/test/product.jl b/test/product.jl new file mode 100644 index 0000000..0c89c5f --- /dev/null +++ b/test/product.jl @@ -0,0 +1,41 @@ +@testset "Product of boxes" begin + @test product(Box([1], [2]), Box([3], [4])) == Box([1,3], [2,4]) + @test product(Box(ones(2,1), 2*ones(2,1)), Box([3], [4])) == Box(@SMatrix([1;1;3]), @SMatrix([2;2;4])) +end + +@testset "Product of intervals" begin + @test @inferred product(1..2, 3..4) == Box([1,3], [2,4]) + @test @inferred product(1..2, 3..4, 5..6) == Box([1,3,5], [2,4,6]) + @test @inferred product(1..2, 3..4, 5..6, 7..8) == Box([1,3,5,7], [2,4,6,8]) +end + +@testset "Product of Box and interval" begin + @test @inferred product(Box([1,3], [2,4]), 5..6) == Box([1,3,5], [2,4,6]) + @test @inferred product(5..6, Box([1,3], [2,4])) == Box([5,1,3], [6,2,4]) +end + +@testset "TupleProduct discrete" begin + tp = TupleProduct([1,2], [3,4]) + @test @inferred rand(tp) in tp + @test (1,3) in tp + @test !((1,2) in tp) + @test @inferred eltype(tp) == Tuple{Int, Int} + @test @inferred SpaceStyle(tp) == FiniteSpaceStyle() + elems = @inferred collect(tp) + @test @inferred all(e in tp for e in elems) + @test @inferred all(e in elems for e in tp) +end + +@testset "TupleProduct continuous" begin + tp = TupleProduct(1..2, 3..4) + @test @inferred rand(tp) in tp + @test (1,3) in tp + @test !((1,2) in tp) + @test_broken eltype(tp) == Tuple{Float64, Float64} + @test @inferred SpaceStyle(tp) == ContinuousSpaceStyle() + @test @inferred bounds(tp) == ((1,3), (2,4)) + @test @inferred bounds(TupleProduct(1..2, 3..4, 5..6)) == ((1,3,5), (2,4,6)) + @test @inferred clamp((0,0), tp) == (1, 3) +end + + diff --git a/test/runtests.jl b/test/runtests.jl index bc921e5..7cc5f6d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,55 +1,10 @@ using CommonRLSpaces using Test -@testset "CommonRLSpaces.jl" begin - @testset "Spaces" begin - s1 = Space((:cat, :dog)) - @test :cat in s1 - @test !(nothing in s1) - - s2 = Space(0:1) - @test 0 in s2 - @test !(0.5 in s2) - - s3 = Space(Bool) - @test false in s3 - @test true in s3 - - s4 = Space(Float64) - @test rand() in s4 - @test 0 in s4 - - s5 = Space(Float64, 3, 4) - @test rand(3, 4) in s5 - - s6 = Space(SVector((:cat, :dog), (:litchi, :longan, :mango))) - @test SVector(:dog, :litchi) in s6 +using StaticArrays - s7 = Space([-1 .. 1, 2 .. 3]) - @test [0, 2] in s7 - @test !([-5, 5] in s7) - - for _ in 1:100 - for s in [s1, s2, s3, s4, s5, s6, s7] - @test rand(s) in s - end - end - end - - @testset "Meta Spaces" begin - s1 = (Space(1:2), Space(Float64, 2, 3)) - @test (1, rand(2, 3)) in s1 - - s2 = (a=Space(1:2), b=Space(Float64, 2, 3)) - @test (a=1, b=rand(2, 3)) in s2 - - s3 = Dict(:a => Space(1:2), :b => Space(Float64, 2, 3)) - @test Dict(:a => 1, :b => rand(2, 3)) in s3 - - for _ in 1:100 - for s in [s1, s2, s3] - rand(s) in s - end - end - end +@testset "CommonRLSpaces.jl" begin + include("basic.jl") + include("array.jl") + include("product.jl") end