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