diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9592764 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Problem/bug report +about: Create a report for incorrect/unexpected/erroring behavior +title: '' +labels: '' +assignees: '' + +--- + +**Describe the problem** + +A clear and concise description of what the problem is, and what you were expecting to happen instead. + +**Minimal Working Example** + +Please provide a piece of code that leads to the bug you encounter. + +This code should be **runnable**, **self-contained**, and minimal. Remove all irrelevant dependencies +and actively try to reduce the code to its smallest possible version. +This will help us identify and fix the problem faster. + +**Package versions** + +Please provide the versions you use. To do this, run the code: +```julia +import Pkg +Pkg.status(["Package1", "Package2"]; # etc. + mode = PKGMODE_MANIFEST +) +``` diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..c39ff6d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest a new feature for this project +title: '' +labels: '' +assignees: '' + +--- + +**Describe the feature you'd like to have** + +**Cite scientific papers related to the feature/algorithm** + +**If possible, sketch out an implementation strategy** diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml new file mode 100644 index 0000000..7a9c79f --- /dev/null +++ b/.github/workflows/CompatHelper.yml @@ -0,0 +1,44 @@ +name: CompatHelper +on: + schedule: + - cron: 0 0 * * * + workflow_dispatch: +permissions: + contents: write + pull-requests: write +jobs: + CompatHelper: + runs-on: ubuntu-latest + steps: + - name: Check if Julia is already available in the PATH + id: julia_in_path + run: which julia + continue-on-error: true + - name: Install Julia, but only if it is not already available in the PATH + uses: julia-actions/setup-julia@v1 + with: + version: '1' + arch: ${{ runner.arch }} + if: steps.julia_in_path.outcome != 'success' + - name: "Add the General registry via Git" + run: | + import Pkg + ENV["JULIA_PKG_SERVER"] = "" + Pkg.Registry.add("General") + shell: julia --color=yes {0} + - name: "Install CompatHelper" + run: | + import Pkg + name = "CompatHelper" + uuid = "aa819f21-2bde-4658-8897-bab36330d9b7" + version = "3" + Pkg.add(; name, uuid, version) + shell: julia --color=yes {0} + - name: "Run CompatHelper" + run: | + import CompatHelper + CompatHelper.main() + shell: julia --color=yes {0} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/TagBot.yml b/.github/workflows/TagBot.yml new file mode 100644 index 0000000..f49313b --- /dev/null +++ b/.github/workflows/TagBot.yml @@ -0,0 +1,15 @@ +name: TagBot +on: + issue_comment: + types: + - created + workflow_dispatch: +jobs: + TagBot: + if: github.event_name == 'workflow_dispatch' || github.actor == 'JuliaTagBot' + runs-on: ubuntu-latest + steps: + - uses: JuliaRegistries/TagBot@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + ssh: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/doccleanup.yml b/.github/workflows/doccleanup.yml new file mode 100644 index 0000000..bc29462 --- /dev/null +++ b/.github/workflows/doccleanup.yml @@ -0,0 +1,26 @@ +name: Doc Preview Cleanup + +on: + pull_request: + types: [closed] + +jobs: + doc-preview-cleanup: + runs-on: ubuntu-latest + steps: + - name: Checkout gh-pages branch + uses: actions/checkout@v2 + with: + ref: gh-pages + - name: Delete preview and history + push changes + run: | + if [ -d "previews/PR$PRNUM" ]; then + git config user.name "Documenter.jl" + git config user.email "documenter@juliadocs.github.io" + git rm -rf "previews/PR$PRNUM" + git commit -m "delete preview" + git branch gh-pages-new $(echo "delete history" | git commit-tree HEAD^{tree}) + git push --force origin gh-pages-new:gh-pages + fi + env: + PRNUM: ${{ github.event.number }} diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml new file mode 100644 index 0000000..724cecd --- /dev/null +++ b/.github/workflows/documentation.yml @@ -0,0 +1,26 @@ +name: Documentation + +on: + push: + branches: + - main + tags: '*' + pull_request: + +jobs: + build: + permissions: + contents: write + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: '1' + - name: Install dependencies + run: julia --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - name: Build and deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # If authenticating with GitHub Actions token + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # If authenticating with SSH deploy key for using TagBot + run: julia --project=docs/ docs/make.jl diff --git a/.github/workflows/test_coverage.yml b/.github/workflows/test_coverage.yml new file mode 100644 index 0000000..5e9a78b --- /dev/null +++ b/.github/workflows/test_coverage.yml @@ -0,0 +1,53 @@ +name: CI - test & coverage +on: + pull_request: + branches: + - main + - '**' # matches every branch + push: + branches: + - main + tags: '*' +jobs: + test: + name: Tests, Julia ${{ matrix.version }} - ${{ matrix.os }} - ${{ matrix.arch }} - ${{ github.event_name }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + version: + - '1' + os: [ubuntu-latest] # adjust according to need, e.g. os: [ubuntu-latest] if testing only on linux + arch: + - x64 + steps: + # Cancel ongoing CI test runs if pushing to branch again before the previous tests + # have finished + - name: Cancel ongoing test runs for previous commits + uses: styfle/cancel-workflow-action@0.6.0 + with: + access_token: ${{ github.token }} + + # Do tests + - uses: actions/checkout@v2 + - uses: julia-actions/setup-julia@v1 + with: + version: ${{ matrix.version }} + arch: ${{ matrix.arch }} + - uses: actions/cache@v1 + env: + cache-name: cache-artifacts + with: + path: ~/.julia/artifacts + key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} + restore-keys: | + ${{ runner.os }}-test-${{ env.cache-name }}- + ${{ runner.os }}-test- + ${{ runner.os }}- + - uses: julia-actions/julia-buildpkg@v1 + + - uses: julia-actions/julia-runtest@v1 + - uses: julia-actions/julia-processcoverage@v1 + - uses: codecov/codecov-action@v1 + with: + file: lcov.info diff --git a/CHANGELOG.md b/CHANGELOG.md index d2e8431..57757ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,12 @@ Changelog for StateSpaceSets.jl is kept w.r.t. version 1.3 # 2.0 -- `StateSpaceSet` now subtypes `AbstractVector`, in particular `StateSpaceSet{V<:AbstractVector} <: AbstractVector{X}`. This lead to the breaking change that `size(ssset) = (length(ssset), )` while before `size` was `length(ssset), dimension(ssset)`. Now you have to use `dimension(ssset)` exclusively to get the "number of columns" in the state space set. -- The keyword `container` can be given to all functions that make state space sets +- `StateSpaceSet` now subtypes `AbstractVector`, in particular `StateSpaceSet{V<:AbstractVector} <: AbstractVector{V}`. This leads to the breaking change that `size(ssset) = (length(ssset), )` while before `size` was `length(ssset), dimension(ssset)`. Now you have to use `dimension(ssset)` exclusively to get the "number of columns" in the state space set. +- `StateSpaceSet` now supports arbitrary inner vectors as state space points. + The keyword `container` can be given to all functions that make state space sets and sets the type of the container of the inner vectors. This is the abstract type and is typically `SVector` or `Vector`. -- All deprecations of v1 have been removed. Primarily this includes `Dataset` and an old version of `statespace_sampler`. +- All deprecations of v1 have been removed. Primarily this includes the `Dataset` name and an old version of `statespace_sampler`. # 1.5 diff --git a/Project.toml b/Project.toml index d3b8b5a..80e9e8f 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ name = "StateSpaceSets" uuid = "40b095a5-5852-4c12-98c7-d43bf788e795" authors = ["George Datseris "] repo = "https://github.com/JuliaDynamics/StateSpaceSets.jl.git" -version = "1.5.0" +version = "2.0.0" [deps] Distances = "b4f34e82-e78d-54a5-968a-f98e89d6e8f7" diff --git a/src/neighborhoods.jl b/src/neighborhoods.jl index 31e047f..b80385f 100644 --- a/src/neighborhoods.jl +++ b/src/neighborhoods.jl @@ -6,13 +6,10 @@ using Neighborhood, Distances export WithinRange, NeighborNumber export Euclidean, Chebyshev, Cityblock -Neighborhood.KDTree(D::AbstractStateSpaceSet, metric::Metric = Euclidean(); kwargs...) = -KDTree(vec(D), metric; kwargs...) - # Convenience extensions for ::StateSpaceSet in bulksearches for f ∈ (:bulkisearch, :bulksearch) for nt ∈ (:NeighborNumber, :WithinRange) @eval Neighborhood.$(f)(ss::KDTree, D::AbstractStateSpaceSet, st::$nt, args...; kwargs...) = - $(f)(ss, D.data, st, args...; kwargs...) + $(f)(ss, vec(D), st, args...; kwargs...) end end diff --git a/src/set_distance.jl b/src/set_distance.jl index c4fc913..641828e 100644 --- a/src/set_distance.jl +++ b/src/set_distance.jl @@ -49,6 +49,8 @@ The distance is calculated with the metric given to `Hausdorff` which defaults t `Hausdorff` is 2x slower than [`StrictlyMinimumDistance`](@ref), however it is a proper metric in the space of sets of state space sets. + +This metric only works for `StateSpaceSet`s whose elements are `SVector`s. """ struct Hausdorff{M<:Metric} metric::M @@ -79,6 +81,8 @@ Brute force performs better for datasets that are either large dimensional or have a small amount of points. Deciding a cutting point is not trivial, and is recommended to simply benchmark the [`set_distance`](@ref) function to make a decision. + +If `brute = false` this metric only works for `StateSpaceSet`s whose elements are `SVector`s. """ struct StrictlyMinimumDistance{M<:Metric} brute::Bool diff --git a/src/statespaceset.jl b/src/statespaceset.jl index d0ae5a1..72ff08f 100644 --- a/src/statespaceset.jl +++ b/src/statespaceset.jl @@ -3,14 +3,14 @@ using Base.Iterators: flatten using Statistics export AbstractStateSpaceSet, minima, maxima -export SVector, SMatrix +export SVector, SMatrix, MVector export minmaxima, columns, standardize, dimension export cov, cor, mean_and_cov # D = dimension, T = element type, V = container type # note that the container type is given as keyword `container` to # all functions that somehow end up making a state space set. -abstract type AbstractStateSpaceSet{D, T, V} <: AbstractVector{V} end +abstract type AbstractStateSpaceSet{D, T<:Real, V} <: AbstractVector{V} end # Core extensions and functions: """ @@ -27,7 +27,7 @@ containertype(::AbstractStateSpaceSet{D,T,V}) where {D,T,V} = V # Base extensions ########################################################################################### for f in ( - :length, :sort!, :iterate, :eachindex, :eachrow, :firstindex, :lastindex, :size, + :length, :sort!, :iterate, :firstindex, :lastindex, :size, ) @eval Base.$(f)(d::AbstractStateSpaceSet, args...; kwargs...) = $(f)(vec(d), args...; kwargs...) end @@ -85,98 +85,43 @@ function Base.dotview(d::AbstractStateSpaceSet, ::Colon, ::Int) end ########################################################################### -# Appending +# Appending/concatenating ########################################################################### Base.append!(d1::AbstractStateSpaceSet, d2::AbstractStateSpaceSet) = (append!(vec(d1), vec(d2)); d1) Base.push!(d::AbstractStateSpaceSet, new_item) = (push!(vec(d), new_item); d) +Base.vcat(d1::AbstractStateSpaceSet, d2::AbstractStateSpaceSet) = append!(copy(d1), d2) -function Base.hcat(d::AbstractStateSpaceSet{D, T, V}, x::AbstractVector{<:Real}) where {D, T, V} - L = length(d) - L == length(x) || error("statespaceset and vector must be of same length") - if V == SVector{D, T} - V2 = SVector{D+1, T} - else - V2 = V # it is `Vector{T}` instead - end - data = Vector{V2}(undef, L) - @inbounds for i in 1:L - if V == SVector{D, T} - e = V2(d[i]..., x[i]) - else - e = vcat(d[i], x[i]) - end - data[i] = e - end - return StateSpaceSet(data) -end - -function Base.hcat(x::AbstractVector{<:Real}, d::AbstractStateSpaceSet{D, T, V}) where {D, T, V} - L = length(d) - L == length(x) || error("statespaceset and vector must be of same length") - if V == SVector{D, T} - V2 = SVector{D+1, T} - else - V2 = V # it is `Vector{T}` instead - end - data = Vector{V2}(undef, L) - @inbounds for i in 1:L - if V <: SVector - e = V2(x[i], d[i]...) - else - e = vcat(x[i], d[i]) - end - data[i] = e - end - return StateSpaceSet(data) -end - -function Base.hcat(ds::AbstractStateSpaceSet{<: Any, T}...) where {T} - Ls = length.(ds) - maxlen = maximum(Ls) - all(Ls .== maxlen) || error("StateSpaceSets must be of same length") - V = containertype(first(ds)) - if V <: SVector - newdim = sum(dimension.(ds)) - V2 = SVector{newdim, T} - else - V2 = V # it is `Vector` - end - v = Vector{V}(undef, maxlen) - for i = 1:maxlen - v[i] = V(collect(Iterators.flatten(d[i] for d in ds))) - end - return StateSpaceSet(v) -end - -# TODO: This can probably be done more efficiently by pre-computing the size of the -# `SVector`s, and doing something similar to the explicit two-argument versions above. -# However, it's not immediately clear how to do this efficiently. This implementation -# converts every input to a dataset first and promotes everything to a common type. -# It's not optimal, because it allocates unnecessarily, but it works. -# If this method is made more efficient, the method above can be dropped. function Base.hcat(xs::Union{AbstractVector{<:Real}, AbstractStateSpaceSet}...) ds = StateSpaceSet.(xs) Ls = length.(ds) maxlen = maximum(Ls) all(Ls .== maxlen) || error("StateSpaceSets must be of same length") - V = typeof(xs[1][1]) + V = findcontainertype(xs...) newdim = sum(dimension.(ds)) T = promote_type(eltype.(ds)...) if V <: SVector V2 = SVector{newdim, T} else - V2 = V # it is `Vector` + V2 = Vector{T} end - v = Vector{v}(undef, maxlen) + v = Vector{V2}(undef, maxlen) for i = 1:maxlen if V <: SVector - e = V(Iterators.flatten(d[i] for d in xs)...,) + e = V2(Iterators.flatten(d[i] for d in xs)...,) else - e = V(collect(Iterators.flatten(d[i] for d in xs))) + e = V2(collect(Iterators.flatten(d[i] for d in xs))) end v[i] = e end - return StateSpaceSet(v) + return StateSpaceSet(v; container = V2) +end + +function findcontainertype(xs::Union{AbstractVector{<:Real}, AbstractStateSpaceSet}...) + for x in xs + if x isa AbstractStateSpaceSet + return containertype(x) + end + end end ##################################################################################### diff --git a/src/statespaceset_concrete.jl b/src/statespaceset_concrete.jl index 7619c89..409b911 100644 --- a/src/statespaceset_concrete.jl +++ b/src/statespaceset_concrete.jl @@ -1,7 +1,7 @@ export StateSpaceSet """ - StateSpaceSet{D, T, V} <: AbstractVector{V} + StateSpaceSet{D, T<:Real, V} <: AbstractVector{V} A dedicated interface for sets in a state space. It is an **ordered container of equally-sized points** of length `D`, @@ -20,13 +20,13 @@ When iterated over, it iterates over its contained points. Constructing a `StateSpaceSet` is done in three ways: -1. By giving in each individual **columns** of the state space set as `Vector{<:Real}`, - as in `StateSpaceSet(x, y, z, ...)`. -2. By giving in a matrix whose rows are the state space points `StateSpaceSet(m)`. -3. By giving in directly a vector of vectors `StateSpaceSet(v_of_v)`. +1. By giving in each individual **columns** of the state space set as `Vector{<:Real}`: + `StateSpaceSet(x, y, z, ...)`. +2. By giving in a matrix whose rows are the state space points: `StateSpaceSet(m)`. +3. By giving in directly a vector of vectors (state space points): `StateSpaceSet(v_of_v)`. -The first two constructors allow for the keyword `container` which sets the type of `V`. -At the moment options are only `SVector` or `Vector`, and by default `SVector` is used. +All constructors allow for the keyword `container` which sets the type of `V` (the type of inner vectors). +At the moment options are only `SVector`, `MVector`, or `Vector`, and by default `SVector` is used. ## Description of indexing @@ -61,12 +61,32 @@ StateSpaceSet{D, T}() where {D,T} = StateSpaceSet(SVector{D,T}[]) StateSpaceSet{D, T}(s::StateSpaceSet{D, T}) where {D,T} = s StateSpaceSet(s::StateSpaceSet) = s StateSpaceSet{D,T}(v::Vector{V}) where {D,T,V<:AbstractVector} = StateSpaceSet{D,T,V}(v) +function StateSpaceSet(v::Vector{V}; container = SVector) where {V<:AbstractVector} + n = length(v[1]) + t = eltype(v[1]) + for p in v + length(p) != n && error("Inner vectors must all have same length") + end + # TODO: There must be a way to generalize this to any container! + # we can use `Base.typename(typeof(v[1])).wrapper` but this is so internal... :( + if container <: SVector + U = SVector{n, t} + elseif container <: MVector + U = MVector{n, t} + else + U = Vector{t} + end + if U != V + u = U.(v) + else + u = v + end + return StateSpaceSet{n,t,U}(u) +end ########################################################################### # StateSpaceSet(Vectors of stuff) ########################################################################### -StateSpaceSet(s::AbstractVector{T}) where {T<:Real} = StateSpaceSet(SVector.(s)) - function StateSpaceSet(vecs::AbstractVector{T}...; container = SVector) where {T<:Real} data = _ssset(vecs...) if container != SVector @@ -113,7 +133,13 @@ function StateSpaceSet(mat::AbstractMatrix{T}; warn = true, container = SVector) N, D = size(mat) warn && D > 100 && @warn "You are attempting to make a StateSpaceSet of dimensions > 100" warn && D > N && @warn "You are attempting to make a StateSpaceSet of a matrix with more columns than rows." - V = container == SVector ? SVector{D,T} : Vector{T} + if container <: SVector + V = SVector{D,T} + elseif container <: MVector + V = MVector{D,T} + else + V = Vector{T} + end data = [V(row) for row in eachrow(mat)] StateSpaceSet{D,T}(data) end diff --git a/src/statespaceset_stats.jl b/src/statespaceset_stats.jl index 04e0ac5..ff4712e 100644 --- a/src/statespaceset_stats.jl +++ b/src/statespaceset_stats.jl @@ -53,7 +53,7 @@ function minmaxima(data::AbstractStateSpaceSet{D, T, V}) where {D, T<:Real, V} end end end - return v(mi), V(ma) + return V(mi), V(ma) end ##################################################################################### diff --git a/test/dataset_tests.jl b/test/dataset_tests.jl deleted file mode 100644 index a7e98b5..0000000 --- a/test/dataset_tests.jl +++ /dev/null @@ -1,167 +0,0 @@ -using Test, StateSpaceSets -using Statistics - -println("\nTesting StateSpaceSet...") - -@testset "StateSpaceSet" begin - original = rand(1001,3) - data = StateSpaceSet(original) - xs = columns(data) - - @testset "Basics" begin - x, y, z = StateSpaceSet(rand(10, 2)), StateSpaceSet(rand(10, 2)), rand(10) - @test StateSpaceSet(x) == x # identity - @test StateSpaceSet(x, y,) isa StateSpaceSet - @test StateSpaceSet(x, y, y) isa StateSpaceSet - @test size(StateSpaceSet(x, y)) == (10, 4) - end - - @testset "iteration" begin - for (i, point) in enumerate(data) - @test point == original[i, :] - end - q = map(x -> x[1], data) - @test length(q) == length(data) - @test q isa Vector{Float64} - end - - @testset "Concatenation/Append" begin - - @testset "append" begin - D1, D2 = StateSpaceSet([1:10 2:11]), StateSpaceSet([3:12 4:13]) - append!(D1, D2) - @test length(D1) == 20 - d1 = [1:10 |> collect; 3:12 |> collect] - d2 = [2:11 |> collect; 4:13 |> collect] - @test D1 == StateSpaceSet([d1 d2]) - end - - types = [Int, Float64] - @testset "hcat with identical element type ($(T))" for T in types - x1, x2, x3 = T.([1:5 2:6]), T.([3:7 4:8]), T.(5:9) - D1, D2, D3 = StateSpaceSet(x1), StateSpaceSet(x2), StateSpaceSet(x3) - y = T.(1:5) |> collect - @test hcat(D1, y) == StateSpaceSet([1:5 2:6 1:5]) - @test hcat(D1, D2) == StateSpaceSet([1:5 2:6 3:7 4:8]) - @test hcat(D1, D2, D3) == StateSpaceSet([1:5 2:6 3:7 4:8 5:9]) - @test hcat(D1, y) |> size == (5, 3) - @test hcat(y, D1) |> size == (5, 3) - @test hcat(D1, y) == StateSpaceSet(([1:5 2:6 y])) - @test hcat(y, D1) == StateSpaceSet(([y 1:5 2:6])) - - x, y, z, w = rand(100), rand(100), StateSpaceSet(rand(100, 2)), StateSpaceSet(rand(Int, 100, 3)) - @test StateSpaceSet(x, y, z, w) == StateSpaceSet(StateSpaceSet(x), StateSpaceSet(y), z, w) - end - - # TODO: By construction, these errors will occur, because the type constraints are - # not imposed on the vector inputs, only the dataset input. In contrast, for - # hcat on datasets only, the we force all datasets to have the same element type. - # - # Should we force the element types of the dataset and vector to be identical and - # throw an informative error message if they are not? - @testset "hcat with nonidentical element types" begin - D = StateSpaceSet([1:5 2:6]) # StateSpaceSet{2, Int} - x = rand(length(D)) # Vector{Float64} - @test_throws InexactError hcat(D, x) - @test_throws InexactError hcat(x, D) - end - end - - @testset "Methods & Indexing" begin - a = data[:, 1] - b = data[:, 2] - c = data[:, 3] - - @test data[1,1] isa Float64 - @test a isa Vector{Float64} - @test StateSpaceSet(a, b, c) == data - @test size(StateSpaceSet(a, b)) == (1001, 2) - - @test data[5] isa SVector{3, Float64} - @test data[11:20] isa StateSpaceSet - @test data[:, 2:3][:, 1] == data[:, 2] - - @test size(data[1:10,1:2]) == (10,2) - @test data[1:10,1:2] == StateSpaceSet(a[1:10], b[1:10]) - @test data[1:10, SVector(1, 2)] == data[1:10, 1:2] - e = data[5, SVector(1,2)] - @test e isa SVector{2, Float64} - - sub = view(data, 11:20) - @test sub isa StateSpaceSets.SubStateSpaceSet - @test sub[2] == data[12] - @test dimension(sub) == dimension(data) - d = sub[:, 1] - @test d isa Vector{Float64} - e = sub[5, 1:2] - @test e isa Vector{Float64} - @test length(e) == 2 - e = sub[5, SVector(1,2)] - @test e isa SVector{2, Float64} - f = sub[5:8, 1:2] - @test f isa StateSpaceSet - - # setindex - data[1] = SVector(0.1,0.1,0.1) - @test data[1] == SVector(0.1,0.1,0.1) - @test_throws ErrorException (data[:,1] .= 0) - end - - @testset "copy" begin - d = StateSpaceSet(rand(10, 2)) - v = vec(d) - d2 = copy(d) - d2[1] == d[1] - d2[1] = SVector(5.0, 5.0) - @test d2[1] != d[1] - end - - @testset "minmax" begin - mi = minima(data) - ma = maxima(data) - mimi, mama = minmaxima(data) - @test mimi == mi - @test mama == ma - for i in 1:3 - @test mi[i] < ma[i] - a,b = extrema(xs[i]) - @test a == mi[i] - @test b == ma[i] - end - end - - @testset "Conversions" begin - m = Matrix(data) - @test StateSpaceSet(m) == data - - m = rand(1000, 4) - @test Matrix(StateSpaceSet(m)) == m - end - - @testset "standardize" begin - r = standardize(data) - rs = columns(r) - for x in rs - m, s = mean(x), std(x) - @test abs(m) < 1e-8 - @test abs(s - 1) < 1e-8 - end - end -end - -@testset "timeseries" begin - x = rand(1000) - @test dimension(x) == 1 - mi, ma = minmaxima(x) - @test mi isa SVector - @test 0 ≤ mi[1] ≤ 1 - @test 0 ≤ ma[1] ≤ 1 - @test minima(x) == mi - @test maxima(x) == ma -end - -@testset "cov/cor" begin - x = StateSpaceSet(rand(1000, 2)) - @test Statistics.cov(Matrix(x)) ≈ StateSpaceSets.cov(x) - @test Statistics.cor(Matrix(x)) ≈ StateSpaceSets.cor(x) -end diff --git a/test/runtests.jl b/test/runtests.jl index f65cde5..c999d9a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -4,8 +4,8 @@ defaultname(file) = uppercasefirst(replace(splitext(basename(file))[1], '_' => ' testfile(file, testname=defaultname(file)) = @testset "$testname" begin; include(file); end @testset "StateSpaceSets.jl" begin - testfile("dataset_tests.jl") - testfile("dataset_distance_tests.jl") + testfile("ssset_tests.jl") + testfile("ssset_distance_tests.jl") testfile("utils_tests.jl") testfile("sampler_tests.jl") end diff --git a/test/dataset_distance_tests.jl b/test/ssset_distance_tests.jl similarity index 100% rename from test/dataset_distance_tests.jl rename to test/ssset_distance_tests.jl diff --git a/test/ssset_tests.jl b/test/ssset_tests.jl new file mode 100644 index 0000000..f634e79 --- /dev/null +++ b/test/ssset_tests.jl @@ -0,0 +1,155 @@ +using Test, StateSpaceSets +using Statistics + +println("\nTesting StateSpaceSet...") + +o = [rand(SVector{3, Float64}) for _ in 1:10] +s = StateSpaceSet(o) + +@testset "construction" begin + x = rand(10) + y = rand(10) + z = rand(10, 4) + + s1 = StateSpaceSet(x) + s2 = StateSpaceSet(x, y) + s3 = StateSpaceSet(z) + + for (i, q) in enumerate((s1, s2, s, s3)) + @test dimension(q) == i + @test size(q) == (10,) + @test length(q) == 10 + end + + @testset "MVector" begin + m1 = StateSpaceSet(x, y; container = MVector) + @test m1[1] isa MVector + m2 = StateSpaceSet(rand(10,10); container = MVector) + @test m2[1] isa MVector + m3 = StateSpaceSet([rand(3) for _ in 1:10]; container = MVector) + @test m3[1] isa MVector + end + + @testset "not reals" begin + q = fill("a", 10) + @test_throws MethodError StateSpaceSet(q) + end + +end + + +@testset "iteration" begin + for (i, point) in enumerate(s) + @test point == o[i] + end + q = map(x -> x[1], s) + @test length(q) == length(o) + @test q isa Vector{Float64} + + f(x) = x[1] - x[2] + r = f.(s) + @test r isa Vector{Float64} + @test all(x -> -2 < x < 2, r) +end + + +@testset "append" begin + s1 = deepcopy(s) + append!(s1, s) + @test length(s1) == 20 + push!(s1, s[end]) + @test length(s1) == 21 +end + +@testset "hcat" begin + x1 = 1:5; x2 = 2:6; x3 = 3:7; x4 = 4:8 + @testset "T=$(T)" for T in (SVector, Vector) + D1, D2 = StateSpaceSet(x1, x2; container = T), StateSpaceSet(x3, x4; container = T) + @test hcat(D1, x1) == StateSpaceSet(x1, x2, x1) + @test hcat(D1, D2) == StateSpaceSet(x1, x2, x3, x4) + @test hcat(x1, D1) == StateSpaceSet(x1, x1, x2) + @test hcat(x1, D1, x2) == StateSpaceSet(x1, x1, x2, x2) + @test StateSpaceSets.containertype(hcat(D1, x1)) <: T + end +end + + +@testset "indexing" begin + s = [rand(SVector{3, Float64}) for _ in 1:100] + s = StateSpaceSet(s) + x = s[:, 1] + @test x isa Vector{Float64} + @test s[5] isa SVector{3, Float64} + @test s[11:20] isa StateSpaceSet + @test s[:, 2:3][:, 1] == s[:, 2] + + sub = view(s, 11:20) + @test sub isa StateSpaceSets.SubStateSpaceSet + @test sub[2] == s[12] + @test dimension(sub) == dimension(s) + d = sub[:, 1] + @test d isa Vector{Float64} + + # setindex + s[1] = SVector(0.1,0.1,0.1) + @test s[1] == SVector(0.1,0.1,0.1) + @test_throws ErrorException (s[:,1] .= 0) +end + +@testset "copy" begin + d = StateSpaceSet(rand(10, 2)) + v = vec(d) + d2 = copy(d) + d2[1] == d[1] + d2[1] = SVector(5.0, 5.0) + @test d2[1] != d[1] +end + +@testset "minmax" begin + mi = minima(s) + ma = maxima(s) + mimi, mama = minmaxima(s) + @test mimi == mi + @test mama == ma + xs = columns(s) + for i in 1:3 + @test mi[i] < ma[i] + a,b = extrema(xs[i]) + @test a == mi[i] + @test b == ma[i] + end +end + +@testset "Matrix" begin + m = Matrix(s) + @test StateSpaceSet(m) == s + m = rand(1000, 4) + @test Matrix(StateSpaceSet(m)) == m +end + +@testset "standardize" begin + r = standardize(s) + rs = columns(r) + for x in rs + m, s = mean(x), std(x) + @test abs(m) < 1e-8 + @test abs(s - 1) < 1e-8 + end +end + +@testset "timeseries" begin + x = rand(1000) + @test dimension(x) == 1 + mi, ma = minmaxima(x) + @test mi isa SVector + @test 0 ≤ mi[1] ≤ 1 + @test 0 ≤ ma[1] ≤ 1 + @test minima(x) == mi + @test maxima(x) == ma +end + +@testset "cov/cor" begin + x = StateSpaceSet(rand(1000, 2)) + @test Statistics.cov(Matrix(x)) ≈ StateSpaceSets.cov(x) + @test Statistics.cor(Matrix(x)) ≈ StateSpaceSets.cor(x) +end