diff --git a/Project.toml b/Project.toml index 52d1c05b1..2ef635d4d 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "DimensionalData" uuid = "0703355e-b756-11e9-17c0-8b28908087d0" authors = ["Rafael Schouten "] -version = "0.11.2" +version = "0.12.0" [deps] ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" diff --git a/README.md b/README.md index 3972232d7..754e54822 100644 --- a/README.md +++ b/README.md @@ -14,12 +14,6 @@ syntax, and additional functionality found in NamedDimensions.jl. It has similar goals to pythons [xarray](http://xarray.pydata.org/en/stable/), and is primarily written for use with spatial data in [GeoData.jl](https://github.com/rafaqz/GeoData.jl). -:exclamation: | Status -:-----------: | :------- - - This is a work in progress under active development, it may be a while before - the interface stabilises and things are fully documented. - ## Dimensions @@ -35,7 +29,31 @@ We can use dim wrappers for indexing, so that the dimension order in the underly does not need to be known: ```julia -a[X(1:10), Y(1:4)] +julia> using DimensionalData + +julia> A = DimArray(rand(40, 50), (X, Y)); + +julia> A[Y(1), X(1:10)] +DimArray with dimensions: + X: 1:10 (NoIndex) +and referenced dimensions: + Y: 1 (NoIndex) +and data: 10-element Array{Float64,1} +[0.515774, 0.575247, 0.429075, 0.234041, 0.4484, 0.302562, 0.911098, 0.541537, 0.267234, 0.370663] +``` + +And this has no runtime cost: + +```julia +julia> using BenchmarkTools + +julia> @btime A[X(1), Y(2)] + 25.068 ns (1 allocation: 16 bytes) +0.7302366320496405 + +julia> @btime parent(A)[1, 2] + 34.061 ns (1 allocation: 16 bytes) +0.7302366320496405 ``` The core component is the `AbstractDimension`, and types that inherit from it, @@ -45,16 +63,68 @@ define manually using the `@dim` macro. Dims can be used for indexing and views without knowing dimension order: ```julia -a[X(20)] -view(a, X(1:20), Y(30:40)) +A[X(10)] +view(A, Y(30:40), X(1:20)) ``` And for indicating dimensions to reduce or permute in julia `Base` and `Statistics` functions that have dims arguments: ```julia -`mean(a, dims=Time)` -`permutedims(a, [X, Y, Z, Time])` +using Statistics + +A = DimArray(rand(10, 10, 100), (X, Y, Ti)); +mean(A, dims=Ti) +permutedims(A, [Ti, Y, X]) +``` + +You can also use arbitrary symbol to create `Dim{X}` dimensions: + + +```julia +julia> A = DimArray(rand(10, 20, 30), (:a, :b, :c)); + +julia> A[a=2:5, c=9] + +DimArray with dimensions: + Dim{:a}: 2:5 (NoIndex) + Dim{:b}: Base.OneTo(20) (NoIndex) +and referenced dimensions: + Dim{:c}: 9 (NoIndex) +and data: 4×20 Array{Float64,2} + 0.868237 0.528297 0.32389 … 0.89322 0.6776 0.604891 + 0.635544 0.0526766 0.965727 0.50829 0.661853 0.410173 + 0.732377 0.990363 0.728461 0.610426 0.283663 0.00224321 + 0.0849853 0.554705 0.594263 0.217618 0.198165 0.661853 +``` + +Other methods also work: + +```julia +julia> bounds(A, (:b, :c)) + +((1, 20), (1, 30)) + +julia> mean(A, dim=Dim{:b}) + +julia> mean(A, dims=Dim{:b}) +DimArray with dimensions: + Dim{:a}: Base.OneTo(10) (NoIndex) + Dim{:b}: 1 (NoIndex) + Dim{:c}: Base.OneTo(30) (NoIndex) +and data: 10×1×30 Array{Float64,3} +[:, :, 1] + 0.543099 + 0.542407 + 0.540647 + 0.513554 + 0.601689 + 0.601558 + 0.46997 + 0.524254 + 0.601844 + 0.520966 +[and 29 more slices...] ``` @@ -75,18 +145,27 @@ Selectors find indices in the dimension based on values `At`, `Near`, or We can use selectors with dim wrappers: ```julia -a[X(Between(1, 10)), Y(At(25.7))] +A[X(Between(1, 10)), Y(At(25.7))] ``` Without dim wrappers selectors must be in the right order: ```julia using Unitful -a[Near(23u"s"), Between(10.5u"m", 50.5u"m")] + +julia> A = DimArray(rand(10, 20), (X((1:10:100)u"m"), Ti((1:5:100)u"s"))) + +julia> A[Between(10.5u"m", 50.5u"m"), Near(23u"s")] +DimArray with dimensions: + X: (11:10:41) m (Sampled: Ordered Regular Points) +and referenced dimensions: + Time (type Ti): 21 s (Sampled: Ordered Regular Points) +and data: 4-element Array{Float64,1} +[0.819172, 0.418113, 0.461722, 0.379877] ``` -For values other than `Int`/`AbstractArray`/`Colon` (which are set aside for regular indexing) the `At` -selector is assumed, and can be dropped completely: +For values other than `Int`/`AbstractArray`/`Colon` (which are set aside for +regular indexing) the `At` selector is assumed, and can be dropped completely: ```julia julia> A = DimArray(rand(3, 3), (X(Val((:a, :b, :c))), Y([25.6, 25.7, 25.8]))) @@ -102,6 +181,33 @@ julia> A[:b, 25.8] 0.61839141062599 ``` +Using all `Val` indexes (only recommended for small arrays) +you can index with named dimensions `At` arbitrary values with no +runtime cost: + + +```julia +using BenchmarkTools + +julia> A = DimArray(rand(3, 3), (cat=Val((:a, :b, :c)), + val=Val((5.0, 6.0, 7.0)))) +DimArray with dimensions: + Dim{:cat}: Val{(:a, :b, :c)}() (Categorical: Unordered) + Dim{:val}: Val{(5.0, 6.0, 7.0)}() (Categorical: Unordered) +and data: 3×3 Array{Float64,2} + 0.993357 0.765515 0.914423 + 0.405196 0.98223 0.330779 + 0.365312 0.388873 0.88732 + +julia> @btime A[:a, 7.0] + 26.333 ns (1 allocation: 16 bytes) +0.32927504968939925 + +julia> @btime A[cat=:a, val=7.0] + 31.920 ns (2 allocations: 48 bytes) +0.7476441117572306 +```` + It's also easy to write your own custom `Selector` if your need a different behaviour. _Example usage:_ @@ -141,6 +247,7 @@ Base and Statistics methods: - `mean`, `median`, `extrema`, `std`, `var`, `cor`, `cov` - `permutedims`, `adjoint`, `transpose`, `Transpose` - `mapslices`, `eachslice` +- `fill` _Example usage:_ diff --git a/docs/src/course.md b/docs/src/course.md index 921ceb1e8..e21b9161e 100644 --- a/docs/src/course.md +++ b/docs/src/course.md @@ -20,29 +20,42 @@ A `DimArray` with labelled dimensions is constructed by: ```@example main using DimensionalData A = DimArray(rand(5, 5), (X, Y)) +A[Y(1), X(2)] ``` -But often we want to provide values for the dimension. +Or we can use the `Dim{X}` dim by using `Symbol`s: + +```@example main +A = DimArray(rand(5, 5), (:a, :b)) +A[a=3, b=5] +``` + +But often, we want to provide a lookup index for the dimension: ```@example main using Dates -t = Ti(DateTime(2001):Month(1):DateTime(2001,12)) -x = X(10:10:100) +t = DateTime(2001):Month(1):DateTime(2001,12) +x = 10:10:100 +A = DimArray(rand(12, 10), (X(x), Ti(t))) ``` Here both `X` and `Ti` are dimensions from `DimensionalData`. The currently -exported dimensions are `X, Y, Z, Ti`. `Ti` is shortening of `Time` - -to avoid the conflict with `Dates.Time`. +exported dimensions are `X, Y, Z, Ti`. `Ti` is shortening of `Time` - to avoid +the conflict with `Dates.Time`. -We pass a `Tuple` of the dimensions to the constructor of `DimArray`, -after the array: +The length of each dimension index has to match the size of the corresponding +array axis. + +This can also be done with `Symbol`, using `Dim{X}`: ```@example main -A = DimArray(rand(12, 10), (t, x)) +A = DimArray(rand(12, 10), (:time=t, :distance=x)) ``` -The length of each dimension index has to match the size of the corresponding -array axis. +Symbols can be more convenient than defining dims with `@dim`, but have some +downsides. They don't inherit from a specific `Dimension` type, so plots will +not know what axis to put them on. If you need to specify the dimension `mode` +or `metadata` manually, the `Dim{X}` syntax becomes less beneficial. ## Indexing the array by name and index @@ -115,17 +128,17 @@ standard `Array`: A[1:5] ``` -selects the first 5 entries of the underlying array. In the case that `A` has -only one dimension, it will be retained. Multidimensional -`AbstracDimArray` indexed this way will return a regular array. +This selects the first 5 entries of the underlying array. In the case that `A` +has only one dimension, it will be retained. Multidimensional `AbstracDimArray` +indexed this way will return a regular array. ## Specifying `dims` keyword arguments with `Dimension` -In many Julia functions like `size, sum`, you can specify the dimension along -which to perform the operation, as an `Int`. It is also possible to do this -using `Dimension` types with `AbstractDimArray`: +In many Julia functions like `size` or `sum`, you can specify the dimension +along which to perform the operation as an `Int`. It is also possible to do this +using [`Dimension`](@ref) types with `AbstractDimArray`: ```@example main sum(A; dims=X) @@ -158,17 +171,22 @@ changes. DimensionalData provides types for specifying details about the dimension index. This enables optimisations with `Selector`s, and modified behaviours such as selection of intervals or points, which will give slightly different results for -selectors like [`Between`](@ref). - -The major categories are [`Categorical`](@ref), [`Sampled`](@ref) and -[`NoIndex`](@ref), which are all types of [`Aligned`](@ref). -[`Unaligned`](@ref) also exists to handle dimensions with an index that is -rotated or otherwise transformed in relation to the underlying array, such as -[`Transformed`](@ref). These are a work in progress. - -[`Aligned`] types will be detected automatically if not specified. A -Dimension containing and index of `String`, `Char` or `Symbol` will be labelled -with [`Categorical`](@ref). A range will be [`Sampled`](@ref), -defaulting to [`Points`](@ref) and [`Regular`](@ref). +selectors like [`Between`](@ref) for [`Points`](@ref) and [`Intervals`](@ref). + +It also allows plots to always be the right way up when either the index or the +array is backwards - reverseing the data lazily when required for plotting if +reqiured, not when loaded. + +The major categories of [`IndexMode`](@ref) are [`Categorical`](@ref), +[`Sampled`](@ref) and [`NoIndex`](@ref), which are all subtypes of +[`Aligned`](@ref). [`Unaligned`](@ref) also exists to handle dimensions with an +index that is rotated or otherwise transformed in relation to the underlying +array, such as [`Transformed`](@ref). These are a work in progress. + +[`Aligned`](@ref) types will be detected automatically if not specified. A +Dimension containing and index of `String`, `Char` or `Symbol` will be given the +[`Categorical`](@ref) mode. A range will be [`Sampled`](@ref), defaulting to +[`Points`](@ref) and [`Regular`](@ref), with the [`Order`](@ref) detected +automatically. See the api docs for specifics about these [`IndexMode`](@ref)s. diff --git a/docs/src/developer.md b/docs/src/developer.md index a602fd200..c2f4a662a 100644 --- a/docs/src/developer.md +++ b/docs/src/developer.md @@ -11,6 +11,8 @@ - Minimal interface: implementing a dimension-aware type should be easy. - Functional style: structs are always rebuilt, and other than the array data, fields are not mutated in place. +- Laziness. Label data correctly, and manipulate them when needed - + instead of standardising eagerly. - Least surprise: everything works the same as in Base, but with named dims. If a method accepts numeric indices or `dims=X` in base, you should be able to use DimensionalData.jl dims. @@ -45,22 +47,27 @@ packages and scripts. ### Syntax AxisArrays.jl is verbose by default: `a[Axis{:y}(1)]` vs `a[Y(1)]` used here. -NamedDims.jl has concise syntax, but the dimensions are no longer types. +NamedDims.jl has concise syntax, but the dimensions are no longer types, +NamedDims.jl syntax can now be replicated using `Dim{:X}`: +```julia +A = Dimarray(rand(4, 5), (:a, :b) +A[:b=5, :a=3] = 25.0 +``` ## Data types and the interface -DimensionalData.jl provides the concrete `DimenstionalArray` type. But it's +DimensionalData.jl provides the concrete `DimArray` type. But it's core purpose is to be easily used with other array types. Some of the functionality in DimensionalData.jl will work without inheriting from `AbstractDimArray`. The main requirement define a `dims` method -that returns a `Tuple` of `AbstractDimension` that matches the dimension order +that returns a `Tuple` of `Dimension` that matches the dimension order and axis values of your data. Define `rebuild`, and base methods for `similar` and `parent` if you want the metadata to persist through transformations (see the `DimArray` and `AbstractDimArray` types). A `refdims` method returns the lost dimensions of a previous transformation, passed in to the `rebuild` method. `refdims` can be discarded, the main loss being plot labels. -Inheriting from `AbstractDimArray` will give all the functionality +Inheriting from `AbstractDimArray` will give nearly all the functionality of using `DimArray`. diff --git a/src/DimensionalData.jl b/src/DimensionalData.jl index 6483e0c39..c4734b46f 100644 --- a/src/DimensionalData.jl +++ b/src/DimensionalData.jl @@ -42,11 +42,11 @@ export Unaligned, Transformed export AbstractDimArray, DimArray, AbstractDimensionalArray, DimensionalArray -export data, dims, refdims, mode, metadata, name, shortname, - val, label, units, order, bounds, locus, mode, <| +export data, dims, refdims, mode, metadata, name, shortname, label, units, + val, index, order, sampling, span, bounds, locus, relation, <| -export dimnum, hasdim, otherdims, commondims, setdims, swapdims, rebuild, - modify, dimwise, dimwise! +export dimnum, hasdim, otherdims, commondims, setdims, swapdims, sortdims, + rebuild, modify, dimwise, dimwise! export order, indexorder, arrayorder, reverseindex, reversearray, reorderindex, @@ -54,7 +54,6 @@ export order, indexorder, arrayorder, export @dim - include("interface.jl") include("mode.jl") include("dimension.jl") @@ -72,4 +71,6 @@ include("prettyprint.jl") const AbstractDimensionalArray = AbstractDimArray const DimensionalArray = DimArray +const DD = DimensionalData + end diff --git a/src/array.jl b/src/array.jl index d4cd0ffff..eae702e1e 100644 --- a/src/array.jl +++ b/src/array.jl @@ -1,6 +1,17 @@ """ -Parent type for all dimensional arrays. +Supertype for all "dim" arrays. + +These arrays return a [`Tuple`](@ref) of [`Dimension`](@ref) +from a [`dims`](@ref) method, and can be rebuilt using [`rebuild`](@ref). + +`parent` must return the source array. + +They should have [`metadata`](@ref), [`name`](@ref) and [`refdims`](@ref) +methods, although these are optional. + +A [`rebuild`](@ref) method for `AbstractDimArray` must accept +`data`, `dims`, `refdims`, `name`, `metadata` arguments. """ abstract type AbstractDimArray{T,N,D<:Tuple,A} <: AbstractArray{T,N} end @@ -9,32 +20,9 @@ const AbstractDimMatrix = AbstractDimArray{T,2} where T const StandardIndices = Union{AbstractArray,Colon,Integer} -# Interface methods ############################################################ -""" - bounds(A::AbstractArray) -Returns a tuple of bounds for each array axis. -""" -bounds(A::AbstractArray) = bounds(dims(A)) -""" - bounds(A::AbstractArray, dims) - -Returns the bounds for the specified dimension(s). -`dims` can be a `Dimension`, a dimension type, or a tuple of either. -""" -bounds(A::AbstractArray, dims::DimOrDimType) = - bounds(DimensionalData.dims(A), dims) - -""" - metadata(A::AbstractArray, dims) - -Returns the bounds for the specified dimension(s). -`dims` can be a `Dimension`, a dimension type, or a tuple of either. -""" -metadata(A::AbstractDimArray, dim) = metadata(dims(A, dim)) -metadata(A::AbstractDimArray, dims::Tuple) = - map(metadata, DimensionalData.dims(A, dims)) +# DimensionalData.jl interface methods ############################################################ # Standard fields @@ -45,21 +33,35 @@ name(A::AbstractDimArray) = A.name metadata(A::AbstractDimArray) = A.metadata label(A::AbstractDimArray) = name(A) -@inline rebuild(A::AbstractArray, data, dims::Tuple=dims(A), refdims=refdims(A), +""" + rebuild(A::AbstractDimArray, data, dims=dims(A), refdims=refdims(A), + name=name(A), metadata=metadata(A)) => AbstractDimArray + +Rebuild and `AbstractDimArray` with some field changes. All types +that inherit from `AbstractDimArray` must define this method if they +have any additional fields or alternate field order. + +They can discard arguments like `refdims`, `name` and `metadata`. + +This method can also be used with keyword arguments in place of regular arguments. +""" +@inline rebuild(A::AbstractDimArray, data, dims::Tuple=dims(A), refdims=refdims(A), name=name(A), metadata=metadata(A)) = rebuild(A, data, dims, refdims, name, metadata) -order(A::AbstractDimArray, args...) = order(dims(A, args...)) -arrayorder(A::AbstractDimArray, args...) = arrayorder(dims(A, args...)) -indexorder(A::AbstractDimArray, args...) = indexorder(dims(A, args...)) - +# Dipatch on Tuple of Dimension, and map +for func in (:index, :mode, :metadata, :sampling, :span, :bounds, + :locus, :order, :arrayorder, :indexorder, :relation) + @eval ($func)(A::AbstractDimArray, args...) = ($func)(dims(A, args...)) +end + # Array interface methods ###################################################### -Base.size(A::AbstractDimArray) = size(data(A)) -Base.axes(A::AbstractDimArray) = axes(data(A)) -Base.iterate(A::AbstractDimArray, args...) = iterate(data(A), args...) -Base.IndexStyle(A::AbstractDimArray) = Base.IndexStyle(data(A)) +Base.size(A::AbstractDimArray) = size(parent(A)) +Base.axes(A::AbstractDimArray) = axes(parent(A)) +Base.iterate(A::AbstractDimArray, args...) = iterate(parent(A), args...) +Base.IndexStyle(A::AbstractDimArray) = Base.IndexStyle(parent(A)) Base.parent(A::AbstractDimArray) = data(A) @inline Base.axes(A::AbstractDimArray, dims::DimOrDimType) = axes(A, dimnum(A, dims)) @@ -68,28 +70,31 @@ Base.parent(A::AbstractDimArray) = data(A) @inline rebuildsliced(A, data, I, name::String=name(A)) = rebuild(A, data, slicedims(A, I)..., name) + +# getindex/view/setindex! ====================================================== + # Standard indices Base.@propagate_inbounds Base.getindex(A::AbstractDimArray, i::Integer, I::Integer...) = - getindex(data(A), i, I...) + getindex(parent(A), i, I...) Base.@propagate_inbounds Base.getindex(A::AbstractDimArray, i::StandardIndices, I::StandardIndices...) = - rebuildsliced(A, getindex(data(A), i, I...), (i, I...)) + rebuildsliced(A, getindex(parent(A), i, I...), (i, I...)) Base.@propagate_inbounds Base.view(A::AbstractDimArray, i::StandardIndices, I::StandardIndices...) = - rebuildsliced(A, view(data(A), i, I...), (i, I...)) + rebuildsliced(A, view(parent(A), i, I...), (i, I...)) Base.@propagate_inbounds Base.setindex!(A::AbstractDimArray, x, i::StandardIndices, I::StandardIndices...) = - setindex!(data(A), x, i, I...) + setindex!(parent(A), x, i, I...) # No indices -Base.@propagate_inbounds Base.getindex(A::AbstractDimArray) = getindex(data(A)) -Base.@propagate_inbounds Base.view(A::AbstractDimArray) = view(data(A)) -Base.@propagate_inbounds Base.setindex!(A::AbstractDimArray, x) = setindex!(data(A), x) +Base.@propagate_inbounds Base.getindex(A::AbstractDimArray) = getindex(parent(A)) +Base.@propagate_inbounds Base.view(A::AbstractDimArray) = view(parent(A)) +Base.@propagate_inbounds Base.setindex!(A::AbstractDimArray, x) = setindex!(parent(A), x) # Cartesian indices Base.@propagate_inbounds Base.getindex(A::AbstractDimArray, I::CartesianIndex) = - getindex(data(A), I) + getindex(parent(A), I) Base.@propagate_inbounds Base.view(A::AbstractDimArray, I::CartesianIndex) = - view(data(A), I) + view(parent(A), I) Base.@propagate_inbounds Base.setindex!(A::AbstractDimArray, x, I::CartesianIndex) = - setindex!(data(A), x, I) + setindex!(parent(A), x, I) # Dimension indexing. Additional methods for dispatch ambiguity Base.@propagate_inbounds Base.getindex(A::AbstractDimArray, dim::Dimension, dims::Dimension...) = @@ -105,6 +110,25 @@ Base.@propagate_inbounds Base.setindex!(A::AbstractDimArray, x, dim::Dimension, Base.@propagate_inbounds Base.setindex!(A::AbstractArray, x, dim::Dimension, dims::Dimension...) = setindex!(A, x, dims2indices(A, (dim, dims...))...) +# Symbol keyword-argument indexing. This allows indexing with A[somedim=25.0] for Dim{:somedim} +Base.@propagate_inbounds Base.getindex(A::AbstractDimArray, args::Dimension...; kwargs...) = + getindex(A, args..., _kwargdims(kwargs.data)...) +Base.@propagate_inbounds Base.view(A::AbstractDimArray, args::Dimension...; kwargs...) = + view(A, args..., _kwargdims(kwargs.data)...) +Base.@propagate_inbounds Base.setindex!(A::AbstractDimArray, x, args::Dimension...; kwargs...) = + setindex!(A, x, args..., _kwargdims(kwargs)...) + + +_kwargdims(kwargs::Base.Iterators.Pairs) = _kwargdims(kwargs.data) +_kwargdims(kwargsdata::NamedTuple) = + _kwargdims(_kwargdimtypes(kwargsdata), values(kwargsdata)) +_kwargdims(dims::Tuple, vals::Tuple) = + (rebuild(dims[1], vals[1]), _kwargdims(tail(dims), tail(vals))...) +_kwargdims(dims::Tuple{}, vals::Tuple{}) = () + +_kwargdimtypes(::NamedTuple{Keys}) where Keys = map(k -> Dim{k}(), Keys) + + # Selector indexing without dim wrappers. Must be in the right order! Base.@propagate_inbounds Base.getindex(A::AbstractDimArray, i, I...) = getindex(A, sel2indices(A, maybeselector(i, I...))...) @@ -115,44 +139,53 @@ Base.@propagate_inbounds Base.setindex!(A::AbstractDimArray, x, i, I...) = # Linear indexing returns Array Base.@propagate_inbounds Base.getindex(A::AbstractDimArray{<:Any, N} where N, i::Union{Colon,AbstractArray}) = - getindex(data(A), i) + getindex(parent(A), i) # Exempt 1D DimArrays Base.@propagate_inbounds Base.getindex(A::AbstractDimArray{<:Any, 1}, i::Union{Colon,AbstractArray}) = - rebuildsliced(A, getindex(data(A), i), (i,)) + rebuildsliced(A, getindex(parent(A), i), (i,)) # Linear indexing returns unwrapped SubArray Base.@propagate_inbounds Base.view(A::AbstractDimArray{<:Any, N} where N, i::StandardIndices) = - view(data(A), i) + view(parent(A), i) # Exempt 1D DimArrays Base.@propagate_inbounds Base.view(A::AbstractDimArray{<:Any, 1}, i::StandardIndices) = - rebuildsliced(A, view(data(A), i), (i,)) + rebuildsliced(A, view(parent(A), i), (i,)) -Base.copy(A::AbstractDimArray) = rebuild(A, copy(data(A))) +# Methods that create copies of an AbstractDimArray ####################################### -Base.copy!(dst::AbstractDimArray{T,N}, src::AbstractDimArray{T,N}) where {T,N} = copy!(parent(dst), parent(src)) -Base.copy!(dst::AbstractDimArray{T,N}, src::AbstractArray{T,N}) where {T,N} = copy!(parent(dst), src) -Base.copy!(dst::AbstractArray{T,N}, src::AbstractDimArray{T,N}) where {T,N} = copy!(dst, parent(src)) -# Most of these methods are for resolving ambiguity errors -Base.copy!(dst::SparseArrays.SparseVector, src::AbstractDimArray{T,1}) where T = copy!(dst, parent(src)) -Base.copy!(dst::AbstractDimArray{T,1}, src::AbstractArray{T,1}) where T = copy!(parent(dst), src) -Base.copy!(dst::AbstractArray{T,1}, src::AbstractDimArray{T,1}) where T = copy!(dst, parent(src)) -Base.copy!(dst::AbstractDimArray{T,1}, src::AbstractDimArray{T,1}) where T = copy!(parent(dst), parent(src)) - -Base.Array(A::AbstractDimArray) = data(A) +# Similar # Need to cover a few type signatures to avoid ambiguity with base -# Don't remove these even though they look redundant Base.similar(A::AbstractDimArray) = - rebuild(A, similar(data(A)), dims(A), refdims(A), "") + rebuild(A, similar(parent(A)), dims(A), refdims(A), "") Base.similar(A::AbstractDimArray, ::Type{T}) where T = - rebuild(A, similar(data(A), T), dims(A), refdims(A), "") + rebuild(A, similar(parent(A), T), dims(A), refdims(A), "") # If the shape changes, use the wrapped array: Base.similar(A::AbstractDimArray, ::Type{T}, I::Tuple{Int,Vararg{Int}}) where T = - similar(data(A), T, I) + similar(parent(A), T, I) Base.similar(A::AbstractDimArray, ::Type{T}, i::Integer, I::Vararg{<:Integer}) where T = - similar(data(A), T, i, I...) + similar(parent(A), T, i, I...) + + +# Copy +for func in (:copy, :one, :oneunit, :zero) + @eval begin + (Base.$func)(A::AbstractDimArray) = rebuild(A, ($func)(parent(A))) + end +end + +Base.Array(A::AbstractDimArray) = Array(parent(A)) + +Base.copy!(dst::AbstractDimArray{T,N}, src::AbstractDimArray{T,N}) where {T,N} = copy!(parent(dst), parent(src)) +Base.copy!(dst::AbstractDimArray{T,N}, src::AbstractArray{T,N}) where {T,N} = copy!(parent(dst), src) +Base.copy!(dst::AbstractArray{T,N}, src::AbstractDimArray{T,N}) where {T,N} = copy!(dst, parent(src)) +# Most of these methods are for resolving ambiguity errors +Base.copy!(dst::SparseArrays.SparseVector, src::AbstractDimArray{T,1}) where T = copy!(dst, parent(src)) +Base.copy!(dst::AbstractDimArray{T,1}, src::AbstractArray{T,1}) where T = copy!(parent(dst), src) +Base.copy!(dst::AbstractArray{T,1}, src::AbstractDimArray{T,1}) where T = copy!(dst, parent(src)) +Base.copy!(dst::AbstractDimArray{T,1}, src::AbstractDimArray{T,1}) where T = copy!(parent(dst), parent(src)) # Concrete implementation ###################################################### @@ -194,23 +227,26 @@ julia> A[Near(DateTime(2001, 5, 4)), Between(20, 50)]; ``` """ DimArray(A::AbstractArray, dims, name::String=""; refdims=(), metadata=nothing) = - DimArray(A, formatdims(A, _to_tuple(dims)), refdims, name, metadata) + DimArray(A, formatdims(A, dims), refdims, name, metadata) DimArray(A::AbstractDimArray; dims=dims(A), refdims=refdims(A), name=name(A), metadata=metadata(A)) = - DimArray(A, formatdims(data(A), _to_tuple(dims)), refdims, name, metadata) + DimArray(A, formatdims(parent(A), dims), refdims, name, metadata) DimArray(; data, dims, refdims=(), name="", metadata=nothing) = - DimArray(A, formatdims(A, _to_tuple(dims)), refdims, name, metadata) + DimArray(A, formatdims(A, dims), refdims, name, metadata) -_to_tuple(t::T where T <: Tuple) = t -_to_tuple(t) = tuple(t) +""" + rebuild(A::DimArray, data::AbstractArray, dims::Tuple, + refdims::Tuple, name::AbstractString, metadata) => DimArray -# AbstractDimArray interface +Rebuild a `DimArray` with new fields. Handling partial field +update is dealth with in `rebuild` for `AbstractDimArray`. +""" @inline rebuild(A::DimArray, data::AbstractArray, dims::Tuple, refdims::Tuple, name::AbstractString, metadata) = DimArray(data, dims, refdims, name, metadata) # Array interface (AbstractDimArray takes care of everything else) Base.@propagate_inbounds Base.setindex!(A::DimArray, x, I::Vararg{StandardIndices}) = - setindex!(data(A), x, I...) + setindex!(parent(A), x, I...) """ DimArray(f::Function, dim::Dimension [, name]) @@ -222,3 +258,29 @@ the given dimension. Optionally provide a name for the result. DimArray(f::Function, dim::Dimension, name=string(nameof(f), "(", name(dim), ")")) = DimArray(f.(val(dim)), (dim,), name) + +""" + Base.fill(x::T, dims::Vararg{Dimension,N}}) => DimArray{T,N} + Base.fill(x::T, dims::Tuple{Vararg{Dimension,N}}) => DimArray{T,N} + +Create a [`DimArray`](@ref) from a fill value `x` and `Dimension`s. + +A `Dimension` with an `AbstractVector` value will set the array axis +to the vector length, while the vector values become the dimension index. + +A `Dimension` holding an `Integer` will set the length +of the Array axis, and set the dimension mode to [`NoIndex`](@ref). +""" +Base.fill(x, dim1::Dimension, dims::Dimension...) = fill(x, (dim1, dims...)) +Base.fill(x, dims::Tuple{<:Dimension,Vararg{<:Dimension}}) = begin + dims = map(_intdim2rangedim, dims) + DimArray(fill(x, map(length, dims)), dims) +end + +_intdim2rangedim(dim::Dimension{<:Integer}) = begin + mode_ = mode(dim) isa AutoMode ? NoIndex() : mode(dim) + basetypeof(dim)(Base.OneTo(val(dim)), mode_, metadata(dim)) +end +_intdim2rangedim(dim::Dimension{<:AbstractArray}) = dim +_intdim2rangedim(dim::Dimension) = + error("dim $(basetypeof(dim)) must hold an Integer or an AbstractArray. Has $(val(dim))") diff --git a/src/broadcast.jl b/src/broadcast.jl index de3f8b675..28c08e360 100644 --- a/src/broadcast.jl +++ b/src/broadcast.jl @@ -56,7 +56,7 @@ function Base.copyto!(dest::AbstractArray, bc::Broadcasted{DimensionalStyle{S}}) return if A isa Nothing || _dims isa Nothing dest else - rebuild(A, data(dest), _dims, refdims(A), "") + rebuild(A, parent(dest), _dims, refdims(A), "") end end @@ -67,7 +67,7 @@ function Base.copyto!(dest::AbstractDimArray, bc::Broadcasted{DimensionalStyle{S return if A isa Nothing || _dims isa Nothing dest else - rebuild(A, data(dest), _dims, refdims(A), "") + rebuild(A, parent(dest), _dims, refdims(A), "") end end @@ -84,7 +84,7 @@ _unwrap_broadcasted(bc::Broadcasted{DimensionalStyle{S}}) where S = begin return Broadcasted{S}(bc.f, innerargs) end _unwrap_broadcasted(x) = x -_unwrap_broadcasted(nda::AbstractDimArray) = data(nda) +_unwrap_broadcasted(nda::AbstractDimArray) = parent(nda) # Get the first dimensional array inthe broadcast _firstdimarray(x::Broadcasted) = _firstdimarray(x.args) diff --git a/src/dimension.jl b/src/dimension.jl index 677bf8d94..f85100bac 100644 --- a/src/dimension.jl +++ b/src/dimension.jl @@ -1,14 +1,14 @@ """ -Dimension is the abstract supertype of all dimension types. +Supertype of all dimension types. -Example concrete implementations are `X`, `Y`, `Z`, -`Ti` (Time), and the custom `Dim{:custom}` dimension. +Example concrete implementations are [`X`](@ref), [`Y`](@ref), [`Z`](@ref), +[`Ti`](@ref) (Time), and the custom [`Dim`]@ref) dimension. -`Dimension`s label the axes of an `AbstractDimesnionalArray`, -or other dimensional data. +`Dimension`s label the axes of an [`AbstractDimesnionalArray`](@ref), +or other dimensional objects, and are used to index into the array. They may also provide an alternate index to lookup for each array axis. -This may be any `AbstractArray` matching the array axis length, or a `Val` +This may be any `AbstractVector` matching the array axis length, or a `Val` holding a tuple for compile-time index lookups. `Dimension`s also have `mode` and `metadata` fields. @@ -25,24 +25,25 @@ contexts, like geospatial data in GeoData.jl. By default it is `nothing`. Example: ```jldoctest Dimension -using Dates +using DimensionalData, Dates + x = X(2:2:10) y = Y(['a', 'b', 'c']) ti = Ti(DateTime(2021, 1):Month(1):DateTime(2021, 12)) -A = DimArray(rand(3, 5, 12), (y, x, ti)) +A = DimArray(zeros(3, 5, 12), (y, x, ti)) # output DimArray with dimensions: - Y: Char[a, b, c] - X: 2:2:10 - Time (type Ti): DateTime("2021-01-01T00:00:00"):Month(1):DateTime("2021-12-01T00:00:00") + Y: Char[a, b, c] (Categorical: Unordered) + X: 2:2:10 (Sampled: Ordered Regular Points) + Time (type Ti): DateTime("2021-01-01T00:00:00"):Month(1):DateTime("2021-12-01T00:00:00") (Sampled: Ordered Regular Points) and data: 3×5×12 Array{Float64,3} [:, :, 1] - 0.590845 0.460085 0.200586 0.579672 0.066423 - 0.766797 0.794026 0.298614 0.648882 0.956753 - 0.566237 0.854147 0.246837 0.0109059 0.646691 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 [and 11 more slices...] ``` @@ -55,12 +56,12 @@ x = A[X(2), Y(3)] # output DimArray with dimensions: - Time (type Ti): DateTime("2021-01-01T00:00:00"):Month(1):DateTime("2021-12-01T00:00:00") + Time (type Ti): DateTime("2021-01-01T00:00:00"):Month(1):DateTime("2021-12-01T00:00:00") (Sampled: Ordered Regular Points) and referenced dimensions: - Y: c - X: 4 + Y: c (Categorical: Unordered) + X: 4 (Sampled: Ordered Regular Points) and data: 12-element Array{Float64,1} -[0.854147, 0.950498, 0.496169, 0.658815, 0.082207, 0.431188, 0.0878598, 0.468079, 0.0677996, 0.836482, 0.0813266, 0.661835] +[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0] ``` A `Dimension` can also wrap [`Selector`](@ref). @@ -71,12 +72,12 @@ x = A[X(Between(3, 4)), Y(At('b'))] # output DimArray with dimensions: - X: 4:2:4 - Time (type Ti): DateTime("2021-01-01T00:00:00"):Month(1):DateTime("2021-12-01T00:00:00") + X: 4:2:4 (Sampled: Ordered Regular Points) + Time (type Ti): DateTime("2021-01-01T00:00:00"):Month(1):DateTime("2021-12-01T00:00:00") (Sampled: Ordered Regular Points) and referenced dimensions: - Y: b + Y: b (Categorical: Unordered) and data: 1×12 Array{Float64,2} - 0.794026 0.842714 0.0460428 0.499531 … 0.182757 0.140473 0.52376 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ``` `Dimension` objects may have [`mode`](@ref) and [`metadata`](@ref) fields @@ -85,36 +86,36 @@ to track additional information about the data and the index, and their relation abstract type Dimension{T,IM,M} end """ -Abstract supertype for independent dimensions. Thise will plot on the X axis. +Supertype for independent dimensions. Thise will plot on the X axis. """ abstract type IndependentDim{T,IM,M} <: Dimension{T,IM,M} end """ -Abstract supertype for Dependent dimensions. These will plot on the Y axis. +Supertype for Dependent dimensions. These will plot on the Y axis. """ abstract type DependentDim{T,IM,M} <: Dimension{T,IM,M} end """ -Abstract parent type for all X dimensions. +Supertype for all X dimensions. """ abstract type XDim{T,IM,M} <: IndependentDim{T,IM,M} end """ -Abstract parent type for all Y dimensions. +Supertype for all Y dimensions. """ abstract type YDim{T,IM,M} <: DependentDim{T,IM,M} end """ -Abstract parent type for all Z dimensions. +Supertype for all Z dimensions. """ abstract type ZDim{T,IM,M} <: Dimension{T,IM,M} end """ -Abstract parent type for all time dimensions. +Supertype for all time dimensions. -An index in a `TimeDime` with `Interval` sampling the locus will automatically be -set to `Start()`, as a date/time index generally defines the start of a -month, second etc, not the central point as is more common with spatial data. +In a `TimeDime` with `Interval` sampling the locus will automatically +be set to `Start()`. Dates and times generally refer to the start of a +month, hour, second etc., not the central point as is more common with spatial data. `""" abstract type TimeDim{T,IM,M} <: IndependentDim{T,IM,M} end @@ -128,43 +129,64 @@ const DimOrDimType = Union{Dimension,DimType} const AllDims = Union{Dimension,DimTuple,DimType,DimTypeTuple,VectorOfDim} -# Getters -val(dim::Dimension) = dim.val -mode(dim::Dimension) = dim.mode -mode(dim::Type{<:Dimension}) = NoIndex() -metadata(dim::Dimension) = dim.metadata - -order(dim::Dimension) = order(mode(dim)) -indexorder(dim::Dimension) = indexorder(order(dim)) -arrayorder(dim::Dimension) = arrayorder(order(dim)) -relationorder(dim::Dimension) = relationorder(order(dim)) - -locus(dim::Dimension) = locus(mode(dim)) -sampling(dim::Dimension) = sampling(mode(dim)) +# DimensionalData interface methods -index(dim::Dimension) = unwrap(val(dim)) +""" + rebuild(dim::Dimension, val, mode=mode(dim), metadata=metadata(dim)) => Dimension + rebuild(dim::Dimension, val=val(dim), mode=mode(dim), metadata=metadata(dim)) => Dimension -# DimensionalData interface methods +Rebuild dim with fields from `dim`, and new fields passed in. +""" rebuild(dim::D, val, mode::IndexMode=mode(dim), metadata=metadata(dim)) where D <: Dimension = constructorof(D)(val, mode, metadata) -dims(x::Dimension) = x -dims(x::DimTuple) = x +dims(dim::Union{Dimension,DimType}) = dim +dims(dims::DimTuple) = dims + +val(dim::Dimension) = dim.val +mode(dim::Dimension) = dim.mode +mode(dim::DimType) = NoIndex() +metadata(dim::Dimension) = dim.metadata + +index(dim::Dimension{<:AbstractArray}) = val(dim) +index(dim::Dimension{<:Val}) = unwrap(val(dim)) + name(dim::Dimension) = name(typeof(dim)) shortname(d::Dimension) = shortname(typeof(d)) -shortname(d::Type{<:Dimension}) = name(d) # Use `name` as fallback +shortname(d::DimType) = name(d) # Use `name` as fallback units(dim::Dimension) = metadata(dim) == nothing ? nothing : get(metadata(dim), :units, nothing) +order(dim::Dimension) = order(mode(dim)) +span(dim::Dimension) = span(mode(dim)) +sampling(dim::Dimension) = sampling(mode(dim)) +locus(dim::Dimension) = locus(mode(dim)) bounds(dim::Dimension) = bounds(mode(dim), dim) -bounds(dims::DimTuple) = map(bounds, dims) -bounds(dims::Tuple{}) = () -bounds(dims::DimTuple, lookupdims::Tuple) = map(l -> bounds(dims, l), lookupdims) -bounds(dims::DimTuple, lookupdim::DimOrDimType) = bounds(dims[dimnum(dims, lookupdim)]) + +indexorder(dim::Dimension) = indexorder(order(dim)) +arrayorder(dim::Dimension) = arrayorder(order(dim)) +relation(dim::Dimension) = relation(order(dim)) + +modetype(dim::Dimension) = typeof(mode(dim)) +modetype(::Type{<:Dimension{<:Any,Mo}}) where Mo = Mo +modetype(::UnionAll) = NoIndex +modetype(::Type{UnionAll}) = NoIndex + + +# Dipatch on Tuple{<:Dimension}, and map to single dim methods +for func in (:val, :index, :mode, :metadata, :order, :sampling, :span, :bounds, :locus, + :name, :shortname, :label, :units, :arrayorder, :indexorder, :relation) + @eval begin + ($func)(dims_::DimTuple) = map($func, dims_) + ($func)(dims_::Tuple{}) = () + ($func)(dims_::DimTuple, lookup) = ($func)(dims(dims_, symbol2dim(lookup))) + end +end # Base methods + Base.eltype(dim::Type{<:Dimension{T}}) where T = T Base.eltype(dim::Type{<:Dimension{A}}) where A<:AbstractArray{T} where T = T Base.size(dim::Dimension) = size(val(dim)) @@ -204,14 +226,14 @@ Base.:(==)(dim1::Dimension, dim2::Dimension) = """ -Dimensions with user-set type paremeters +Supertype for Dimensions with user-set type paremeters """ abstract type ParametricDimension{X,T,IM,M} <: Dimension{T,IM,M} end """ Dim{:X}() - Dim{:X}(val, mode, metadata) - Dim{:X}(val=:; [mode=AutoMode()], [metadata=nothing]) + Dim{:X}(val=:; mode=AutoMode(), metadata=nothing) + Dim{:X}(val, mode, metadata=nothing) A generic dimension. For use when custom dims are required when loading data from a file. The sintax is ugly and verbose to use for indexing, @@ -219,13 +241,15 @@ ie `Dim{:lat}(1:9)` rather than `Lat(1:9)`. This is the main reason they are not the only type of dimension availabile. ```jldoctest +using DimensionalData + dim = Dim{:custom}(['a', 'b', 'c']) # output -dimension Dim custom (type Dim): +dimension Dim{:custom} (type Dim): val: Char[a, b, c] -mode: AutoMode{AutoOrder}(AutoOrder()) +mode: AutoMode metadata: nothing type: Dim{:custom,Array{Char,1},AutoMode{AutoOrder},Nothing} ``` @@ -234,13 +258,13 @@ struct Dim{X,T,IM<:IndexMode,M} <: ParametricDimension{X,T,IM,M} val::T mode::IM metadata::M - Dim{X}(val, mode, metadata) where X = - new{X,typeof(val),typeof(mode),typeof(metadata)}(val, mode, metadata) + Dim{X}(val::T, mode::IM, metadata::M=nothing) where {X,T,IM,M} = + new{X,T,IM,M}(val, mode, metadata) end - Dim{X}(val=:; mode=AutoMode(), metadata=nothing) where X = Dim{X}(val, mode, metadata) -name(::Type{<:Dim{X}}) where X = "Dim $X" + +name(::Type{<:Dim{X}}) where X = "Dim{:$X}" shortname(::Type{<:Dim{X}}) where X = "$X" basetypeof(::Type{<:Dim{X}}) where {X} = Dim{X} @@ -295,6 +319,8 @@ dimmacro(typ, supertype, name=string(typ), shortname=string(typ)) = end $typ(val=:; mode=AutoMode(), metadata=nothing) = $typ(val, mode, metadata) + $typ(val, mode, metadata=nothing) = + $typ(val, mode, metadata) DimensionalData.name(::Type{<:$typ}) = $name DimensionalData.shortname(::Type{<:$typ}) = $shortname end) @@ -302,7 +328,7 @@ dimmacro(typ, supertype, name=string(typ), shortname=string(typ)) = # Define some common dimensions. """ - X(val=:) + X(val=:; mode=AutoMode(), metadata=nothing) X [`Dimension`](@ref). `X <: XDim <: IndependentDim` @@ -318,7 +344,7 @@ mean(A; dims=X) @dim X XDim """ - Y(val=:) + Y(val=:; mode=AutoMode(), metadata=nothing) Y [`Dimension`](@ref). `Y <: YDim <: DependentDim` @@ -334,7 +360,7 @@ mean(A; dims=Y) @dim Y YDim """ - Z(val=:) + Z(val=:; mode=AutoMode(), metadata=nothing) Z [`Dimension`](@ref). `Z <: ZDim <: Dimension` @@ -350,7 +376,7 @@ mean(A; dims=Z) @dim Z ZDim """ - Ti(val=:) + Ti(val=:; mode=AutoMode(), metadata=nothing) Time [`Dimension`](@ref). `Ti <: TimeDim <: IndependentDim` @@ -377,44 +403,51 @@ const Time = Ti # For some backwards compat """ - formatdims(A, dims) + formatdims(A, dims) => Tuple{Vararg{<:Dimension,N}} Format the passed-in dimension(s) `dims` to match the array `A`. This means converting indexes of `Tuple` to `LinRange`, and running -`identify` on . -Errors are also thrown if -dims don't match the array dims or size. +`identify`. Errors are also thrown if dims don't match the array dims or size. If a [`IndexMode`](@ref) hasn't been specified, an mode is chosen based on the type and element type of the index: """ -formatdims(A::AbstractArray{T,N} where T, dims::NTuple{N,Any}) where N = - formatdims(axes(A), dims) -formatdims(axes::Tuple{Vararg{<:AbstractRange}}, - dims::Tuple{Vararg{<:Union{<:Dimension,<:UnionAll}}}) = - map(formatdims, axes, dims) - -formatdims(axis::AbstractRange, dimtype::Type{<:Dimension}) = +formatdims(A::AbstractArray, dims) = formatdims(A, (dims,)) +formatdims(A::AbstractArray, dims::NamedTuple) = begin + dims = map((k, v) -> Dim{k}(v), keys(dims), values(dims)) + _formatdims(axes(A), dims) +end +formatdims(A::AbstractArray{<:Any,N}, dims::Tuple{Vararg{<:Any,N}}) where N = + _formatdims(axes(A), dims) +formatdims(A::AbstractArray{<:Any,N}, dims::Tuple{Vararg{<:Any,M}}) where {N,M} = + throw(DimensionMismatch("Array A has $N axes, while the number of dims is $M")) + + +_formatdims(axes::Tuple{Vararg{<:AbstractRange}}, dims::Tuple) = + map(_formatdims, axes, dims) +_formatdims(axis::AbstractRange, dimname::Symbol) = + Dim{dimname}(axis, NoIndex(), nothing) +_formatdims(axis::AbstractRange, dimtype::Type{<:Dimension}) = dimtype(axis, NoIndex(), nothing) -formatdims(axis::AbstractRange, dim::Dimension) = begin +_formatdims(axis::AbstractRange, dim::Dimension) = begin checkaxis(dim, axis) rebuild(dim, val(dim), identify(mode(dim), basetypeof(dim), val(dim))) end -formatdims(axis::AbstractRange, dim::Dimension{<:NTuple{2}}) = begin +_formatdims(axis::AbstractRange, dim::Dimension{<:NTuple{2}}) = begin start, stop = val(dim) range = LinRange(start, stop, length(axis)) - formatdims(axis, rebuild(dim, range)) + _formatdims(axis, rebuild(dim, range)) end # Dimensions holding colon dispatch on mode -formatdims(axis::AbstractRange, dim::Dimension{Colon}) = - formatdims(mode(dim), axis, dim) +_formatdims(axis::AbstractRange, dim::Dimension{Colon}) = + _formatdims(mode(dim), axis, dim) # Dimensions holding colon has the array axis inserted as the index -formatdims(mode::AutoMode, axis::AbstractRange, dim::Dimension{Colon}) = +_formatdims(mode::AutoMode, axis::AbstractRange, dim::Dimension{Colon}) = rebuild(dim, axis, NoIndex()) # Dimensions holding colon has the array axis inserted as the index -formatdims(mode::IndexMode, axis::AbstractRange, dim::Dimension{Colon}) = +_formatdims(mode::IndexMode, axis::AbstractRange, dim::Dimension{Colon}) = rebuild(dim, axis, mode) checkaxis(dim, axis) = diff --git a/src/interface.jl b/src/interface.jl index 649c566b6..9244d52a2 100644 --- a/src/interface.jl +++ b/src/interface.jl @@ -1,29 +1,38 @@ # Key methods to add for a new dimensional data type """ - data(x) + rebuild(x, args...) + rebuild(x; kwargs...) + +Rebuild an object struct with updated field values. -Return the data wrapped by the dimentional array. This may not be -the same as `Base.parent`, as it should never include data outside the -bounds of the dimensions. +This is an abstraction that alows inbuilt and custom types to be rebuilt +functionally to update them, as most objects in DimensionalData are immutable. -In a disk based [`AbstractDimArray`](@ref), `data` may need to -load data from disk. +`x` can be a `AbstractDimArray`, a `Dimension`, `IndexMode` or other custom types. + +The arguments reuired are defined for the abstract type that has a `rebuild` method. """ -function data end -data(x) = x +function rebuild end +rebuild(x; kwargs...) = ConstructionBase.setproperties(x, (;kwargs...)) """ - dims(x) + dims(x) => Tuple{Vararg{<:Dimension}} + dims(x, dims::Tuple) => Tuple{Vararg{<:Dimension}} + dims(x, dim) => Dimension Return a tuple of `Dimension`s for an object, in the order that matches the axes or columns etc. of the underlying data. + +`dims` can be `Dimension`, `Dimension` types, or `Symbols` for `Dim{Symbol}`. + +The default is to return `nothing`. """ function dims end dims(x) = nothing """ - refdims(x) + refdims(x) => Tuple{Vararg{<:Dimension}} Reference dimensions for an array that is a slice or view of another array with more dimensions. @@ -32,91 +41,215 @@ array with more dimensions. and the new reference dimensions. Refdims can be stored in a field or disgarded, as it is mostly to give context to plots. Ignoring refdims will simply leave some captions empty. + +The default is to return an empty `Tuple` `()`. """ function refdims end refdims(x) = () -""" - rebuild(x, args...) - rebuild(x; kwargs...) - -Rebuild an object struct with updated values. -""" -function rebuild end -rebuild(x; kwargs...) = ConstructionBase.setproperties(x, (;kwargs...)) """ val(x) + val(dims::Tuple) => Tuple + val(A::AbstractDimArray, dims::Tuple) => Tuple + +Return the contained value of a wrapper object. -Return the contained value of a wrapper object, otherwise just returns the object. +`dims` can be `Dimension`, `Dimension` types, or `Symbols` for `Dim{Symbol}`. + +Objects that don't define a `val` method are returned unaltered. """ function val end """ - metadata(x) + index(dim::Dimension{<:Val}) => Tuple + index(dim::Dimension{<:AbstractArray}) => AbstractArray + index(dims::NTuple{N}) => Tuple{Vararg{Union{AbstractArray,Tuple},N}} + +Return the contained index of a `Dimension`. + +Only valid when a `Dimension` contains an `AbstractArray` +or a Val tuple like `Val{(:a, :b)}()`. The `Val` is unwrapped +to return just the `Tuple` -Return the metadata of a dimension or data object. +`dims` can be a `Dimension`, or a tuple of `Dimension`. """ -function metadata end +function index end """ - mode(x) + mode(dim:Dimension) => IndexMode + mode(dims::Tuple) => Tuple{Vararg{<:IndexMode,N}} + mode(A::AbstractDimArray, [dims::Tuple]) => Tuple + +Returns the [`IndexMode`](@ref) of a dimension. This dictates +properties of the dimension such as array axis and index order, +and sampling properties. -Return the `IndexMode` of a dimension. +`dims` can be a `Dimension`, a dimension type, or a tuple of either. """ function mode end """ - bounds(x, [dims]) + metadata(dim::Dimension) + metadata(dims::Tuple{<:Dimension,Vararg}) + metadata(A::AbstractDimArray, dims::Tuple) => (Dim metadata) + metadata(A::AbstractDimArray) => (Array metadata) + +Returns the metadata for an array or the specified dimension(s). +`dims` can be a `Symbol` (with `Dim{X}`, a `Dimension`, a `Dimension` type, +or a mixed tuple. + +`dims` can be a `Dimension`, a dimension type, or a tuple of either. +""" +function metadata end + +""" + bounds(dim::Dimension) => Tuple{T,T}} + bounds(dims::Tuple{<:Dimension,Vararg}) => Tuple{Vararg{<:Tuple{T,T}}} + bounds(A::AbstractArray, [dims]) => Tuple{Vararg{Tuple{T,T},N}} + bounds(A::AbstractArray, dim) => Tuple{T,T} Return the bounds of all dimensions of an object, of a specific dimension, or of a tuple of dimensions. -Returns a length 2 `Tuple` in ascending order. +`dims` can be a `Dimension`, a dimension type, or a tuple of either. """ function bounds end """ - name(x) + name(x) => String + name(xs::NTuple{N,<:Dimension}) => NTuple{N,String} + name(A::AbstractDimArray, dims::NTuple{N}) => NTuple{N,String} -Get the name of data or a dimension. +Get the name of an array or Dimension, or a tuple of of either. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. """ function name end name(x) = name(typeof(x)) name(x::Type) = "" -name(xs::Tuple) = map(name, xs) """ - shortname(x) + shortname(x) => String + shortname(xs::NTuple{N}) => NTuple{N,String} + shortname(A::AbstractDimArray, dims::NTuple{N}) => NTuple{N,String} -Get the short name of array data or a dimension. +Get the shortname of an array or Dimension, or a tuple of of either. This may be a shorter version more suitable for small labels than `name`, but it may also be identical to `name`. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. """ function shortname end shortname(x) = shortname(typeof(x)) -shortname(xs::Tuple) = map(shortname, xs) shortname(x::Type) = "" """ - units(x) + units(x) => Union{Nothing,Any} + units(::NTuple{N}) => NTuple{N} + unit(A::AbstractDimArray, dims::NTuple{N}) => NTuple{N,String} -Return the units of a dimension. This could be a string, a unitful unit, or nothing. +Get the units of an array or `Dimension`, or a tuple of of either. -Units do not have a field, and may or may not be included in `metadata`. +Units do not have a set field, and may or may not be included in `metadata`. This method is to facilitate use in labels and plots when units are available, -not a guarantee that they will be. +not a guarantee that they will be. If not available, `nothing` is returned. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. """ function units end units(x) = nothing -units(xs::Tuple) = map(units, xs) """ - label(x) + label(x) => String + label(dims::NTuple{N,<:Dimension}) => NTuple{N,String} + label(A::AbstractDimArray, dims::NTuple{N,<:Dimension}) => NTuple{N,String} Get a plot label for data or a dimension. This will include the name and units if they exist, and anything else that should be shown on a plot. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. """ function label end label(x) = string(name(x), (units(x) === nothing ? "" : string(" ", units(x)))) -label(xs::Tuple) = join(map(label, xs), ", ") + + +""" + order(dim:Dimension) => Order + order(dims::Tuple) => Tuple{Vararg{<:Order,N}} + order(A::AbstractDimArray, [dims::Tuple]) => Tuple + +Return the [`Order`](@ref) for each dimension. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. +""" +function order end + +""" + sampling(dim:Dimension) => Sampling + sampling(dims::Tuple) => Tuple{Vararg{<:Sampling,N}} + sampling(A::AbstractDimArray, [dims::Tuple]) => Tuple + +Return the [`Sampling`](@ref) for each dimension. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. +""" +function sampling end + +""" + span(dim:Dimension) => Span + span(dims::Tuple) => Tuple{Vararg{<:Span,N}} + span(A::AbstractDimArray, [dims::Tuple]) => Tuple + +Return the [`Span`](@ref) for each dimension. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. +""" +function span end + +""" + locus(dim:Dimension) => Locus + locus(dims::Tuple) => Tuple{Vararg{<:Locus,N}} + locus(A::AbstractDimArray, [dims::Tuple]) => Tuple + +Return the [`Locus`](@ref) for each dimension. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. +""" +function locus end + +""" + arrayorder(dim:Dimension) => Union{Forward,Reverse} + arrayorder(dims::Tuple) => Tuple{Vararg{<:Union{Forward,Reverse},N}} + arrayorder(A::AbstractDimArray, [dims::Tuple]) => Tuple + +Return the [`Order`](@ref) (`Forward` or `Reverse`) of the array, +for each dimension. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. +""" +function arrayorder end + +""" + indexorder(dim:Dimension) => Union{Forward,Reverse} + indexorder(dims::Tuple) => Tuple{Vararg{<:Union{Forward,Reverse},N}} + indexorder(A::AbstractDimArray, [dims::Tuple]) => Tuple + +Return the [`Order`](@ref) (`Forward` or `Reverse`) of the dimension index, +for each dimension. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. +""" +function indexorder end + +""" + relation(dim:Dimension) => Union{Forward,Reverse} + relation(dims::Tuple) => Tuple{Vararg{<:Union{Forward,Reverse},N}} + relation(A::AbstractDimArray, [dims::Tuple]) => Tuple + +Return the relation (`Forward` or `Reverse`) between the dimension index +and the array axis, for each dimension. + +`dims` can be `Dimension`s, `Dimension` types, or `Symbols` for `Dim{Symbol}`. +""" +function relation end diff --git a/src/matmul.jl b/src/matmul.jl index daa8808f6..82f5ed839 100644 --- a/src/matmul.jl +++ b/src/matmul.jl @@ -55,15 +55,15 @@ Base.:*(A::Adjoint{<:Any,<:RealHermSymComplexHerm}, B::AbstractDimVector) = rebu rebuildmul(A::AbstractDimVector, B::AbstractDimMatrix) = begin # Vector has no dim 2 to compare - rebuild(A, data(A) * data(B), (first(dims(A)), last(dims(B)),)) + rebuild(A, parent(A) * parent(B), (first(dims(A)), last(dims(B)),)) end rebuildmul(A::AbstractDimMatrix, B::AbstractDimVector) = begin comparedims(last(dims(A)), first(dims(B))) - rebuild(A, data(A) * data(B), (first(dims(A)),)) + rebuild(A, parent(A) * parent(B), (first(dims(A)),)) end rebuildmul(A::AbstractDimMatrix, B::AbstractDimMatrix) = begin comparedims(last(dims(A)), first(dims(B))) - rebuild(A, data(A) * data(B), (first(dims(A)), last(dims(B)))) + rebuild(A, parent(A) * parent(B), (first(dims(A)), last(dims(B)))) end rebuildmul(A::AbstractDimVector, B::AbstractMatrix) = rebuild(A, parent(A) * B, (first(dims(A)), AnonDim(Base.OneTo(size(B, 2))))) diff --git a/src/methods.jl b/src/methods.jl index 15527df03..93808b0a9 100644 --- a/src/methods.jl +++ b/src/methods.jl @@ -12,16 +12,16 @@ for (mod, fname) in ((:Base, :sum), (:Base, :prod), (:Base, :maximum), (:Base, : _fname = Symbol('_', fname) @eval begin # Returns a scalar - @inline ($mod.$fname)(A::AbstractDimArray) = ($mod.$fname)(data(A)) + @inline ($mod.$fname)(A::AbstractDimArray) = ($mod.$fname)(parent(A)) # Returns a reduced array @inline ($mod.$_fname)(A::AbstractDimArray, dims::Union{Int,Base.Dims,AllDims}) = - rebuild(A, ($mod.$_fname)(data(A), dimnum(A, dims)), reducedims(A, dims)) + rebuild(A, ($mod.$_fname)(parent(A), dimnum(A, dims)), reducedims(A, dims)) @inline ($mod.$_fname)(A::AbstractDimArray, dims::Colon) = - rebuild(A, ($mod.$_fname)(data(A), dimnum(A, dims(A))), reducedims(A, dims(A))) + rebuild(A, ($mod.$_fname)(parent(A), dimnum(A, dims(A))), reducedims(A, dims(A))) @inline ($mod.$_fname)(f, A::AbstractDimArray, dims::Union{Int,Base.Dims,AllDims}) = - rebuild(A, ($mod.$_fname)(f, data(A), dimnum(A, dims)), reducedims(A, dims)) + rebuild(A, ($mod.$_fname)(f, parent(A), dimnum(A, dims)), reducedims(A, dims)) @inline ($mod.$_fname)(f, A::AbstractDimArray, dims::Colon) = - rebuild(A, ($mod.$_fname)(f, data(A), dimnum(A, dims(A))), reducedims(A, dims(A))) + rebuild(A, ($mod.$_fname)(f, parent(A), dimnum(A, dims(A))), reducedims(A, dims(A))) end end @@ -29,26 +29,26 @@ for (mod, fname) in ((:Statistics, :std), (:Statistics, :var)) _fname = Symbol('_', fname) @eval begin # Returns a scalar - @inline ($mod.$fname)(A::AbstractDimArray) = ($mod.$fname)(data(A)) + @inline ($mod.$fname)(A::AbstractDimArray) = ($mod.$fname)(parent(A)) # Returns a reduced array - @inline ($mod.$_fname)(A::AbstractArray, corrected::Bool, mean, dims::AllDims) = + @inline ($mod.$_fname)(A::AbstractDimArray, corrected::Bool, mean, dims::AllDims) = rebuild(A, ($mod.$_fname)(A, corrected, mean, dimnum(A, dims)), reducedims(A, dims)) @inline ($mod.$_fname)(A::AbstractDimArray, corrected::Bool, mean, dims::Union{Int,Base.Dims}) = - rebuild(A, ($mod.$_fname)(data(A), corrected, mean, dims), reducedims(A, dims)) + rebuild(A, ($mod.$_fname)(parent(A), corrected, mean, dims), reducedims(A, dims)) end end -Statistics.median(A::AbstractDimArray) = Statistics.median(data(A)) +Statistics.median(A::AbstractDimArray) = Statistics.median(parent(A)) Statistics._median(A::AbstractDimArray, dims::AllDims) = - rebuild(A, Statistics._median(data(A), dimnum(A, dims)), reducedims(A, dims)) + rebuild(A, Statistics._median(parent(A), dimnum(A, dims)), reducedims(A, dims)) Statistics._median(A::AbstractDimArray, dims::Union{Int,Base.Dims}) = - rebuild(A, Statistics._median(data(A), dims), reducedims(A, dims)) + rebuild(A, Statistics._median(parent(A), dims), reducedims(A, dims)) Base._mapreduce_dim(f, op, nt::NamedTuple{(),<:Tuple}, A::AbstractDimArray, dims::Union{Int,Base.Dims,AllDims}) = - rebuild(A, Base._mapreduce_dim(f, op, nt, data(A), dimnum(A, dims)), reducedims(A, dims)) + rebuild(A, Base._mapreduce_dim(f, op, nt, parent(A), dimnum(A, dims)), reducedims(A, dims)) Base._mapreduce_dim(f, op, nt::NamedTuple{(),<:Tuple}, A::AbstractDimArray, dims::Colon) = - Base._mapreduce_dim(f, op, nt, data(A), dims) + Base._mapreduce_dim(f, op, nt, parent(A), dims) # TODO: Unfortunately Base/accumulate.jl kwargs methods all force dims to be Integer. # accumulate wont work unless that is relaxed, or we copy half of the file here. @@ -57,26 +57,27 @@ Base._mapreduce_dim(f, op, nt::NamedTuple{(),<:Tuple}, A::AbstractDimArray, dims Base._extrema_dims(f, A::AbstractArray, dims::AllDims) = begin dnums = dimnum(A, dims) - rebuild(A, Base._extrema_dims(f, data(A), dnums), reducedims(A, dnums)) + rebuild(A, Base._extrema_dims(f, parent(A), dnums), reducedims(A, dnums)) end # Dimension dropping + Base._dropdims(A::AbstractDimArray, dim::DimOrDimType) = - rebuildsliced(A, Base._dropdims(data(A), dimnum(A, dim)), + rebuildsliced(A, Base._dropdims(parent(A), dimnum(A, dim)), dims2indices(A, basetypeof(dim)(1))) Base._dropdims(A::AbstractDimArray, dims::DimTuple) = - rebuildsliced(A, Base._dropdims(data(A), dimnum(A, dims)), + rebuildsliced(A, Base._dropdims(parent(A), dimnum(A, dims)), dims2indices(A, Tuple((basetypeof(d)(1) for d in dims)))) # Function application -@inline Base.map(f, A::AbstractDimArray) = rebuild(A, map(f, data(A))) +@inline Base.map(f, A::AbstractDimArray) = rebuild(A, map(f, parent(A))) Base.mapslices(f, A::AbstractDimArray; dims=1, kwargs...) = begin dimnums = dimnum(A, dims) - _data = mapslices(f, data(A); dims=dimnums, kwargs...) + _data = mapslices(f, parent(A); dims=dimnums, kwargs...) rebuild(A, _data, reducedims(A, DimensionalData.dims(A, dimnums))) end @@ -99,7 +100,7 @@ end for fname in (:cor, :cov) @eval Statistics.$fname(A::AbstractDimArray{T,2}; dims=1, kwargs...) where T = begin - newdata = Statistics.$fname(data(A); dims=dimnum(A, dims), kwargs...) + newdata = Statistics.$fname(parent(A); dims=dimnum(A, dims), kwargs...) removed_idx = dimnum(A, dims) newrefdims = $dims(A)[removed_idx] newdims = $dims(A)[3 - removed_idx] @@ -108,13 +109,15 @@ for fname in (:cor, :cov) end -# Reverse. +# Reverse + Base.reverse(A::AbstractDimArray{T,N}; dims=1) where {T,N} = reversearray(A; dims=dims) Base.reverse(dim::Dimension) = reverseindex(dim) # Rotations + struct Rot90 end struct Rot180 end struct Rot270 end @@ -158,16 +161,16 @@ for (pkg, fname) in [(:Base, :permutedims), (:Base, :adjoint), (:Base, :transpose), (:LinearAlgebra, :Transpose)] @eval begin @inline $pkg.$fname(A::AbstractDimArray{T,2}) where T = - rebuild(A, $pkg.$fname(data(A)), reverse(dims(A))) + rebuild(A, $pkg.$fname(parent(A)), reverse(dims(A))) @inline $pkg.$fname(A::AbstractDimArray{T,1}) where T = - rebuild(A, $pkg.$fname(data(A)), (AnonDim(Base.OneTo(1)), dims(A)...)) + rebuild(A, $pkg.$fname(parent(A)), (AnonDim(Base.OneTo(1)), dims(A)...)) end end for fname in [:permutedims, :PermutedDimsArray] @eval begin @inline Base.$fname(A::AbstractDimArray{T,N}, perm) where {T,N} = - rebuild(A, $fname(data(A), dimnum(A, perm)), permutedims(dims(A), perm)) + rebuild(A, $fname(parent(A), dimnum(A, perm)), sortdims(dims(A), perm)) end end @@ -203,7 +206,7 @@ _catifcatdim(catdims::Tuple, ds) = _catifcatdim(catdim, ds) = basetypeof(catdim) <: basetypeof(ds[1]) ? vcat(ds...) : ds[1] Base.vcat(dims::Dimension...) = - rebuild(dims[1], vcat(map(val, dims)...), vcat(map(mode, dims)...)) + rebuild(dims[1], vcat(val(dims)...), vcat(mode(dims)...)) Base.vcat(modes::IndexMode...) = first(modes) Base.vcat(modes::AbstractSampled...) = @@ -222,18 +225,15 @@ _vcat_modes(::Intervals, ::Irregular, modes...) = begin end _vcat_modes(::Points, ::Irregular, modes...) = first(modes) + Base.inv(A::AbstractDimArray{T, 2}) where T = rebuild(A, inv(parent(A)), reverse(map(flipindex, dims(A)))) + # Index breaking # TODO: change the index and traits of the reduced dimension # and return a DimArray. -Base.unique(A::AbstractDimArray{<:Any,1}) = unique(data(A)) +Base.unique(A::AbstractDimArray{<:Any,1}) = unique(parent(A)) Base.unique(A::AbstractDimArray; dims::DimOrDimType) = - unique(data(A); dims=dimnum(A, dims)) - - -# TODO cov, cor mapslices, eachslice, reverse, sort and sort! need _methods without kwargs in base so -# we can dispatch on dims. Instead we dispatch on array type for now, which means -# these aren't usefull unless you inherit from AbstractDimArray. + unique(parent(A); dims=dimnum(A, dims)) diff --git a/src/mode.jl b/src/mode.jl index 5cd655038..ba83cccca 100644 --- a/src/mode.jl +++ b/src/mode.jl @@ -4,20 +4,28 @@ Traits for the order of the array, index and the relation between them. abstract type Order end """ + Ordered(index, array, relation) + Ordered(; index=Forward(), array=Forward(), relation=Forward()) + Container object for dimension and array ordering. -The default is `Ordered(Forward()`, `Forward(), Forward())` +## Fields + +Each can have a value of `Forward` or `Reverse`. -All combinations of forward and reverse order for data and indices seem to occurr +- `index`: The order of the dimension index +- `array`: The order of array axis, in terms of how you would want to plot it +- `relation`: The relation between the index and the array. + +All combinations of forward and reverse order for data and index seem to occurr in real datasets, as strange as that seems. We cover these possibilities by specifying -the order of both explicitly. +the order of both explicitly, and the direction of the relationship between them. Knowing the order of indices is important for using methods like `searchsortedfirst()` to find indices in sorted lists of values. Knowing the order of the data is then -required to map to the actual indices. It's also used to plot the data later - which -always happens in smallest to largest order. +required to map to the actual indices. It's also used to plot the data later. -Base also defines Forward and Reverse, but they seem overly complicated for our purposes. +The default is `Ordered(Forward()`, `Forward(), Forward())` """ struct Ordered{D,A,R} <: Order index::D @@ -29,10 +37,16 @@ Ordered(; index=Forward(), array=Forward(), relation=Forward()) = indexorder(order::Ordered) = order.index arrayorder(order::Ordered) = order.array -relationorder(order::Ordered) = order.relation +relation(order::Ordered) = order.relation """ + Unordered(relation=Forward()) + Trait indicating that the array or dimension has no order. +This means the index cannot be searched with `searchsortedfirst`, +or similar methods, and that plotting order does not matter. + +It still has a relation between the array axis and the dimension index. """ struct Unordered{R} <: Order relation::R @@ -41,14 +55,14 @@ Unordered() = Unordered(Forward()) indexorder(order::Unordered) = Unordered() arrayorder(order::Unordered) = Unordered() -relationorder(order::Unordered) = order.relation +relation(order::Unordered) = order.relation """ AutoOrder() Order will be found automatically where possible. -This will fail for all types without `isless` methods. +This will fail for all dim eltypes without `isless` methods. """ struct AutoOrder <: Order end @@ -62,15 +76,16 @@ struct UnknownOrder <: Order end """ Forward() -Indicates that the array or dimension is in the normal forward order. +Indicates that the array axis, dimension index or the relation +between them is in the normal forward order. """ struct Forward <: Order end """ Reverse() -Indicates that the array or dimension is in the reverse order. -Selector lookup or plotting will be reversed. +Indicates that the array axis, dimension index or the relation +between them is in reverse order. """ struct Reverse <: Order end @@ -78,26 +93,26 @@ Base.reverse(::Reverse) = Forward() Base.reverse(::Forward) = Reverse() reverseindex(o::Unordered) = - Unordered(reverse(relationorder(o))) + Unordered(reverse(relation(o))) reverseindex(o::Ordered) = - Ordered(reverse(indexorder(o)), arrayorder(o), reverse(relationorder(o))) + Ordered(reverse(indexorder(o)), arrayorder(o), reverse(relation(o))) reversearray(o::Unordered) = - Unordered(reverse(relationorder(o))) + Unordered(reverse(relation(o))) reversearray(o::Ordered) = - Ordered(indexorder(o), reverse(arrayorder(o)), reverse(relationorder(o))) + Ordered(indexorder(o), reverse(arrayorder(o)), reverse(relation(o))) flipindex(o::Unordered) = o flipindex(o::Ordered) = - Ordered(reverse(indexorder(o)), arrayorder(o), relationorder(o)) + Ordered(reverse(indexorder(o)), arrayorder(o), relation(o)) fliparray(o::Unordered) = o fliparray(o::Ordered) = - Ordered(indexorder(o), reverse(arrayorder(o)), relationorder(o)) + Ordered(indexorder(o), reverse(arrayorder(o)), relation(o)) -fliprelation(o::Unordered) = Unordered(reverse(relationorder(o))) +fliprelation(o::Unordered) = Unordered(reverse(relation(o))) fliprelation(o::Ordered) = - Ordered(indexorder(o), arrayorder(o), reverse(relationorder(o))) + Ordered(indexorder(o), arrayorder(o), reverse(relation(o))) isrev(::Forward) = false isrev(::Reverse) = true @@ -105,42 +120,55 @@ isrev(::Reverse) = true """ Locii indicate the position of index values in cells. -Locii are often `Start` for time series, but often `Center` -for spatial data. +These allow for values array cells to align with the [`Start`](@ref), +[`Center`](@ref), or [`End`](@ref) of values in the dimension index. + +This means they can be plotted with correct axis markers, and allows automatic +converrsions to between formats with different standards (such as NetCDF and GeoTiff). + +Locii are often `Start` for time series, but often `Center` for spatial data. + +These are reflected in the default values: `Ti` dimensions with `Sampled` index mode +will default to `Start` Locii. All others default to `Center`. """ abstract type Locus end """ Center() -Indicates dimension index that matches the center coordinates/time/position. +Indicates a dimension value is for the center of its corresponding array cell, +in the direction of the dimension index order. """ struct Center <: Locus end """ Start() -Indicates dimension index that matches the start coordinates/time/position. +Indicates a dimension value is for the start of its corresponding array cell, +in the direction of the dimension index order. """ struct Start <: Locus end """ End() -Indicates dimension index that matches the end coordinates/time/position. +Indicates a dimension value is for the end of its corresponding array cell, +in the direction of the dimension index order. """ struct End <: Locus end """ AutoLocus() -Indicates dimension where the index position is not known. +Indicates a dimension where the index position is not yet known. +This will be filled with a default on object construction. """ struct AutoLocus <: Locus end """ -Indicates the sampling method used by the index. +Indicates the sampling method used by the index: [`Points`](@ref) +or [`Intervals`](@ref). """ abstract type Sampling end @@ -148,6 +176,8 @@ abstract type Sampling end Points() [`Sampling`](@ref) mode where single samples at exact points. + +These are always plotted at the center of array cells. """ struct Points <: Sampling end @@ -156,19 +186,30 @@ locus(sampling::Points) = Center() """ Intervals(locus::Locus) -[`Sampling`](@ref) mode where samples are the mean (or similar) value over an interval. +[`Sampling`](@ref) mode where samples are the mean (or similar) +value over an interval. + +Intervals require a [`Locus`](@ref) of `Start`, `Center` or `End` +to define where in the interval the index values refer to. """ struct Intervals{L} <: Sampling locus::L end Intervals() = Intervals(AutoLocus()) + +""" + rebuild(::Intervals, locus::Locus) => Intervals + +Rebuild `Intervals` with a new Locus. +""" rebuild(::Intervals, locus) = Intervals(locus) locus(sampling::Intervals) = sampling.locus """ -Mode defining the type of interval used in a InervalSampling index. +Defines the type of span used in a [`Sampling`](@ref) index. +These are [`Regular`](@ref) or [`Irregular`](@ref). """ abstract type Span end @@ -206,6 +247,8 @@ Irregular(lowerbound, upperbound) = Irregular((lowerbound, upperbound)) bounds(span::Irregular) = span.bounds """ + AutoSpan() + Span will be guessed and replaced by a constructor. """ struct AutoSpan <: Span end @@ -214,10 +257,10 @@ struct AutoSpan <: Span end """ -IndexModes are types defining the behaviour of a dimension, how they are plotted and -how [`Selector`](@ref)s like [`Between`](@ref) work. +Types defining the behaviour of a dimension, how they are plotted and +how [`Selector`](@ref)s like [`Between`](@ref) work on them. -An IndexMode may be a simple type like [`NoIndex`](@ref) indicating that the index is +An `IndexMode` may be a simple type like [`NoIndex`](@ref) indicating that the index is just the underlying array axis. It could also be a [`Categorical`](@ref) index indicating the index is ordered or unordered categories, or a [`Sampled`](@ref) index indicating sampling along some transect. @@ -227,13 +270,14 @@ abstract type IndexMode end bounds(mode::IndexMode, dim) = bounds(indexorder(mode), mode, dim) bounds(::Forward, ::IndexMode, dim) = first(dim), last(dim) bounds(::Reverse, ::IndexMode, dim) = last(dim), first(dim) -bounds(::Unordered, ::IndexMode, dim) = error("Cannot call `bounds` on an unordered mode") +bounds(::Unordered, ::IndexMode, dim) = (nothing, nothing) dims(::IndexMode) = nothing +dims(::Type{<:IndexMode}) = nothing order(::IndexMode) = Unordered() arrayorder(mode::IndexMode) = arrayorder(order(mode)) indexorder(mode::IndexMode) = indexorder(order(mode)) -relationorder(mode::IndexMode) = relationorder(order(mode)) +relation(mode::IndexMode) = relation(order(mode)) locus(mode::IndexMode) = Center() Base.step(mode::T) where T <: IndexMode = @@ -244,8 +288,8 @@ slicemode(mode::IndexMode, index, I) = mode """ AutoMode() -Automatic [`IndexMode`](@ref), the default mode. It will be converted automatically to -another [`IndexMode`](@ref) when it is possible to detect it from the index. +Automatic [`IndexMode`](@ref), the default mode. It will be converted automatically +to another [`IndexMode`](@ref) when it is possible to detect it from the index. """ struct AutoMode{O<:Order} <: IndexMode order::O @@ -259,7 +303,7 @@ const Auto = AutoMode """ -Supertype for [`IndexMode`](@ref) where the index is aligned with the array axes. +Supertype for [`IndexMode`](@ref)s where the index is aligned with the array axes. This is by far the most common case. """ abstract type Aligned{O} <: IndexMode end @@ -276,24 +320,26 @@ An [`IndexMode`](@ref) that is identical to the array axis. Defining a [`DimArray`](@ref) without passing an index to the dimension, the IndexMode will be `NoIndex`: -```jldoctest +```jldoctest NoIndex +using DimensionalData + A = DimArray(rand(3, 3), (X, Y)) map(mode, dims(A)) # output -(NoIndex(), NoIndex()) +(NoIndex, NoIndex) ``` Is identical to: -```jldoctest +```jldoctest NoIndex A = DimArray(rand(3, 3), (X(; mode=NoIndex()), Y(; mode=NoIndex()))) map(mode, dims(A)) # output -(NoIndex(), NoIndex()) +(NoIndex, NoIndex) ``` """ struct NoIndex <: Aligned{Ordered{Forward,Forward,Forward}} end @@ -304,6 +350,9 @@ order(mode::NoIndex) = Ordered(Forward(), Forward(), Forward()) Abstract supertype for [`IndexMode`](@ref)s where the index is aligned with the array, and is independent of other dimensions. [`Sampled`](@ref) is provided by this package, `Projected` in GeoData.jl also extends [`AbstractSampled`](@ref), adding crs projections. + +A `rebuild` method for `AbstractSampled` must accept `order`, `span` +and `sampling`, arguments. """ abstract type AbstractSampled{O<:Order,Sp<:Span,Sa<:Sampling} <: Aligned{O} end @@ -373,7 +422,8 @@ It is capable of representing gridded data from a wide range of sources, allowin correct `bounds` and [`Selector`](@ref)s for points or intervals of regular, irregular, forward and reverse indexes. -The `Sampled` mode is assigned for all indexes of `AbstractRange` not assigned to [`Categorical`](@ref). +The `Sampled` mode is assigned for all indexes of `AbstractRange` +not assigned to [`Categorical`](@ref). ## Fields @@ -389,7 +439,9 @@ The `Sampled` mode is assigned for all indexes of `AbstractRange` not assigned t Create an array with [`Interval`] sampling. -```jldoctest +```jldoctest Sampled +using DimensionalData + dims_ = (X(100:-10:10; mode=Sampled(sampling=Intervals())), Y([1, 4, 7, 10]; mode=Sampled(span=Regular(2), sampling=Intervals()))) A = DimArray(rand(10, 4), dims_) @@ -397,7 +449,7 @@ map(mode, dims(A)) # output -(Sampled{Ordered{DimensionalData.Reverse,DimensionalData.Forward,DimensionalData.Forward},Regular{Int64},Intervals{Center}}(Ordered{DimensionalData.Reverse,DimensionalData.Forward,DimensionalData.Forward}(DimensionalData.Reverse(), DimensionalData.Forward(), DimensionalData.Forward()), Regular{Int64}(-10), Intervals{Center}(Center())), Sampled{Ordered{DimensionalData.Forward,DimensionalData.Forward,DimensionalData.Forward},Regular{Int64},Intervals{Center}}(Ordered{DimensionalData.Forward,DimensionalData.Forward,DimensionalData.Forward}(DimensionalData.Forward(), DimensionalData.Forward(), DimensionalData.Forward()), Regular{Int64}(2), Intervals{Center}(Center()))) +(Sampled: Ordered Regular Intervals, Sampled: Ordered Regular Intervals) ``` """ struct Sampled{O,Sp,Sa} <: AbstractSampled{O,Sp,Sa} @@ -408,6 +460,12 @@ end Sampled(; order=AutoOrder(), span=AutoSpan(), sampling=Points()) = Sampled(order, span, sampling) +""" + rebuild(m::Sampled, order, span, sampling) => Sampled + rebuild(m::Sampled, order=order(m), span=span(m), sampling=sampling(m)) => Sampled + +Rebuild `Sampled` `IndexMode` with new field values +""" rebuild(m::Sampled, order=order(m), span=span(m), sampling=sampling(m)) = Sampled(order, span, sampling) @@ -416,6 +474,8 @@ rebuild(m::Sampled, order=order(m), span=span(m), sampling=sampling(m)) = [`IndexMode`](@ref)s for dimensions where the values are categories. [`Categorical`](@ref) is the provided concrete implementation. + +A `rebuild` method for `AbstractCategorical` must accept the `order` argumen. """ abstract type AbstractCategorical{O} <: Aligned{O} end @@ -440,14 +500,16 @@ it instead defaults to [`Unordered`]. Create an array with [`Interval`] sampling. -```jldoctest +```jldoctest Categorical +using DimensionalData + dims_ = X(["one", "two", "thee"]), Y([:a, :b, :c, :d]) A = DimArray(rand(3, 4), dims_) map(mode, dims(A)) # output -(Categorical{Unordered{DimensionalData.Forward}}(Unordered{DimensionalData.Forward}(DimensionalData.Forward())), Categorical{Unordered{DimensionalData.Forward}}(Unordered{DimensionalData.Forward}(DimensionalData.Forward()))) +(Categorical: Unordered, Categorical: Unordered) ``` """ struct Categorical{O<:Order} <: AbstractCategorical{O} @@ -455,8 +517,14 @@ struct Categorical{O<:Order} <: AbstractCategorical{O} end Categorical(; order=Unordered()) = Categorical(order) -rebuild(mode::Categorical, order) = Categorical(order) +""" + rebuild(mode::Categorical, order::Order) + rebuild(mode::Categorical; order=order(mode)) + +Rebuild `Categorical` `IndexMode` with new order. +""" +rebuild(mode::Categorical, order) = Categorical(order) @@ -487,7 +555,7 @@ from CoordinateTransformations.jl may be useful. ## Example ```jldoctest -using CoordinateTransformations +using DimensionalData, CoordinateTransformations m = LinearMap([0.5 0.0; 0.0 0.5]) A = [1 2 3 4 @@ -508,11 +576,19 @@ struct Transformed{F,D} <: Unaligned f::F dim::D end +Transformed(f, D::UnionAll) = Transformed(f, D()) -transform(mode::Transformed) = mode.f +transformfunc(mode::Transformed) = mode.f dims(mode::Transformed) = mode.dim +dims(::Type{<:Transformed{<:Any,D}}) where D = D + +""" + rebuild(mode::Transformed, f, dim) + rebuild(mode::Transformed, f=transformfunct(mode), dim=dims(mode)) -rebuild(mode::Transformed, f=transform(mode), dim=dims(mode)) = +Rebuild the `Transformed` `IndexMode`. +""" +rebuild(mode::Transformed, f=transformfunct(mode), dim=dims(mode)) = Transformed(f, dim) # TODO bounds @@ -541,7 +617,7 @@ identify(mode::Auto, dimtype::Type, index::Val) = order(mode) isa AutoOrder ? Categorical(Unordered()) : Categorical(order(mode)) # Sampled -identify(mode::AbstractSampled, dimtype::Type, index::AbstractArray) = begin +identify(mode::AbstractSampled, dimtype::Type, index) = begin mode = rebuild(mode, identify(order(mode), dimtype, index), identify(span(mode), dimtype, index), @@ -552,13 +628,17 @@ end # Order identify(order::Order, dimtype::Type, index) = order identify(order::AutoOrder, dimtype::Type, index) = _orderof(index) -identify(order::AutoOrder, dimtype::Type, index::AbstractUnitRange) = Ordered() -_orderof(index::AbstractRange) = - Ordered(index=_indexorder(index)) -_orderof(index::AbstractArray) = begin +_orderof(index::AbstractUnitRange) = Ordered() +_orderof(index::AbstractRange) = Ordered(index=_indexorder(index)) +_orderof(index::Val) = _detectorder(unwrap(index)) +_orderof(index::AbstractArray) = _detectorder(index) + +function _detectorder(index) + # This is awful. But we don't know if we can + # call `issorted` on the contents of `index`. local sorted - local indord + local indord try indord = _indexorder(index) sorted = issorted(index; rev=isrev(indord)) @@ -567,24 +647,24 @@ _orderof(index::AbstractArray) = begin end sorted ? Ordered(index=indord) : Unordered() end -≈ -_indexorder(index::AbstractArray) = +_indexorder(index) = first(index) <= last(index) ? Forward() : Reverse() # Span -identify(span::AutoSpan, dimtype::Type, index::AbstractArray) = +identify(span::AutoSpan, dimtype::Type, index::Union{AbstractArray,Val}) = Irregular() identify(span::AutoSpan, dimtype::Type, index::AbstractRange) = Regular(step(index)) -identify(span::Regular{AutoStep}, dimtype::Type, index::AbstractArray) = +identify(span::Regular{AutoStep}, dimtype::Type, index::Union{AbstractArray,Val}) = throw(ArgumentError("`Regular` must specify `step` size with an index other than `AbstractRange`")) -identify(span::Regular, dimtype::Type, index::AbstractArray) = +identify(span::Regular, dimtype::Type, index::Union{AbstractArray,Val}) = span identify(span::Regular{AutoStep}, dimtype::Type, index::AbstractRange) = Regular(step(index)) identify(span::Regular, dimtype::Type, index::AbstractRange) = begin - step(span) isa Number && !(step(span) ≈ step(index)) && throw(ArgumentError("mode step $(step(span)) does not match index step $(step(index))")) + step(span) isa Number && !(step(span) ≈ step(index)) && + throw(ArgumentError("mode step $(step(span)) does not match index step $(step(index))")) span end identify(span::Irregular{Nothing}, dimtype, index) = diff --git a/src/plotrecipes.jl b/src/plotrecipes.jl index 9e04c93ba..bc6950ed1 100644 --- a/src/plotrecipes.jl +++ b/src/plotrecipes.jl @@ -24,17 +24,17 @@ end HistogramLike(), Afwd elseif sertype in [:hline] :yguide --> label(Afwd) - data(Afwd) + parent(Afwd) elseif sertype in [:vline, :andrews] :xguide --> label(Afwd) - data(Afwd) + parent(Afwd) elseif sertype in [:violin, :dotplot, :boxplot] ViolinLike(), Afwd elseif sertype in [:plot, :histogram2d, :none, :line, :path, :steppre, :steppost, :sticks, :scatter, :hexbin, :barbins, :scatterbins, :stepbins, :bins2d, :bar] SeriesLike(), Afwd else - data(Afwd) + parent(Afwd) end end @@ -42,7 +42,7 @@ end dim = dims(A, 1) :xguide --> label(dim) :yguide --> label(A) - unwrap(index(dim)), parent(A) + index(dim), parent(A) end @recipe function f(::SeriesLike, A::AbstractArray{T,2}) where T A = maybe_permute(A, (IndependentDim, DependentDim)) @@ -50,14 +50,14 @@ end :xguide --> label(ind) :yguide --> label(A) :legendtitle --> label(dep) - :label --> permutedims(index(dep)) - index(ind), data(A) + :label --> permutedims(val(dep)) + index(ind), parent(A) end @recipe function f(::HistogramLike, A::AbstractArray{T,1}) where T dim = dims(A, 1) :xguide --> label(A) - index(dim), data(A) + index(dim), parent(A) end @recipe function f(::HistogramLike, A::AbstractArray{T,2}) where T A = maybe_permute(A, (IndependentDim, DependentDim)) @@ -65,13 +65,13 @@ end :xguide --> label(A) :legendtitle --> label(dep) :label --> permutedims(index(dep)) - index(ind), data(A) + index(ind), parent(A) end @recipe function f(::ViolinLike, A::AbstractArray{T,1}) where T dim = dims(A, 1) :yguide --> label(A) - data(A) + parent(A) end @recipe function f(::ViolinLike, A::AbstractArray{T,2}) where T A = maybe_permute(A, (IndependentDim, DependentDim)) @@ -80,14 +80,14 @@ end :yguide --> label(A) :legendtitle --> label(dep) :label --> permutedims(index(dep)) - data(A) + parent(A) end @recipe function f(::HeatMapLike, A::AbstractArray{T,1}) where T dim = dims(A, 1) :xguide --> label(dim) :yguide --> label(A) - index(dim), data(A) + index(dim), parent(A) end @recipe function f(::HeatMapLike, A::AbstractArray{T,2}) where T @@ -97,11 +97,11 @@ end :yguide --> label(y) :zguide --> label(A) :colorbar_title --> label(A) - reverse(map(index, dims(A)))..., data(A) + reverse(map(index, dims(A)))..., parent(A) end @recipe function f(::ImageLike, A::AbstractArray{T,2}) where T - data(A) + parent(A) end maybe_permute(A, dims) = all(hasdim(A, dims)) ? permutedims(A, dims) : A @@ -110,14 +110,15 @@ forwardorder(A::AbstractArray) = reorderindex(A, Forward()) |> a -> reorderrelation(a, Forward()) refdims_title(A::AbstractArray) = join(map(refdims_title, refdims(A)), ", ") -refdims_title(dim::Dimension) = string(name(dim), ": ", refdims_title(mode(dim), dim)) -refdims_title(mode::AbstractSampled, dim::Dimension) = begin - start, stop = map(string, bounds(dim)) +refdims_title(refdim::Dimension) = + string(name(refdim), ": ", refdims_title(mode(refdim), refdim)) +refdims_title(mode::AbstractSampled, refdim::Dimension) = begin + start, stop = map(string, bounds(refdim)) if start == stop start else "$start to $stop" end end -refdims_title(mode::IndexMode, dim::Dimension) = string(index(dim)) +refdims_title(mode::IndexMode, refdim::Dimension) = string(val(refdim)) diff --git a/src/prettyprint.jl b/src/prettyprint.jl index e4957c366..407c2c4b7 100644 --- a/src/prettyprint.jl +++ b/src/prettyprint.jl @@ -20,9 +20,9 @@ Base.show(io::IO, A::AbstractDimArray) = begin end print(io, "and") printstyled(io, " data: "; color=:green) - dataA = data(A) + dataA = parent(A) print(io, summary(dataA), "\n") - custom_show(io, data(A)) + custom_show(io, parent(A)) end # Short printing for DimArray Base.show(io::IO, ::MIME"text/plain", A::AbstractDimArray) = show(io, A) @@ -54,9 +54,17 @@ Base.show(io::IO, dim::Dimension) = begin printstyled(io, nameof(typeof(dim)); color=:red) print(io, ")") end - print(io, ": ") + printdimproperties(io, dim) +end +Base.show(io::IO, dim::Dim) = begin + printstyled(io, name(dim); color=:red) + printdimproperties(io, dim) +end +printdimproperties(io, dim) = begin + print(io, ": ") _printdimval(io, val(dim)) + print(io, " (", mode(dim), ")") end _printdimval(io, A::AbstractArray) = printlimited(io, A) @@ -73,6 +81,22 @@ function printlimited(io, v::AbstractVector) print(io, s*svals*"]") end +Base.show(io::IO, mode::IndexMode) = _printmode(io, mode) +Base.show(io::IO, mode::AbstractSampled) = begin + _printmode(io, mode) + _printorder(io, mode) + print(io, " ", nameof(typeof(span(mode)))) + print(io, " ", nameof(typeof(sampling(mode)))) +end +Base.show(io::IO, mode::AbstractCategorical) = begin + _printmode(io, mode) + _printorder(io, mode) +end + +_printmode(io, mode) = printstyled(io, nameof(typeof(mode)); color=:green) + +_printorder(io, mode) = print(io, ": ", nameof(typeof(order(mode)))) + # Thanks to Michael Abbott for the following function custom_show(io::IO, A::AbstractArray{T,0}) where T = Base.show(IOContext(io, :compact => true, :limit => true), A) diff --git a/src/primitives.jl b/src/primitives.jl index 903ddbf23..d8fab1986 100644 --- a/src/primitives.jl +++ b/src/primitives.jl @@ -3,19 +3,46 @@ const UnionAllTupleOrVector = Union{Vector{UnionAll},Tuple{UnionAll,Vararg}} -@inline Base.permutedims(tosort::DimTuple, perm::Union{Vector{<:Integer},Tuple{<:Integer,Vararg}}) = - map(p -> tosort[p], Tuple(perm)) -@inline Base.permutedims(tosort::DimTuple, order::UnionAllTupleOrVector) = - _sortdims(tosort, Tuple(map(d -> basetypeof(d), order))) -@inline Base.permutedims(tosort::UnionAllTupleOrVector, order::DimTuple) = - _sortdims(Tuple(map(d -> basetypeof(d), tosort)), order) -@inline Base.permutedims(tosort::DimTuple, order::VectorOfDim) = - _sortdims(tosort, Tuple(order)) -@inline Base.permutedims(tosort::VectorOfDim, order::DimTuple) = - _sortdims(Tuple(tosort), order) -@inline Base.permutedims(tosort::DimTuple, order::DimTuple) = - _sortdims(tosort, order) +""" + sortdims(tosort, order) => Tuple + +Sort dimensions `tosort` by `order`. Dimensions +in `order` but missing from `tosort` are replaced with `nothing`. + +`tosort` and `order` can be `Tuple`s or `Vector`s or Dimension +or dimension type. Abstract supertypes like [`TimeDim`](@ref) +can be used in `order`. +""" +@inline sortdims(tosort, order::Union{Vector{<:Integer},Tuple{<:Integer,Vararg}}) = + map(p -> tosort[p], Tuple(order)) +@inline sortdims(tosort, order) = + _sortdims(_maybeconstruct(Tuple(tosort)), Tuple(order)) + +@generated _sortdims(tosort::Tuple{Vararg{<:Dimension}}, + order::Tuple{Vararg{<:Dimension}}) = begin + indexexps = [] + ts = (tosort.parameters...,) + allreadyfound = Int[] + for (i, od) in enumerate(order.parameters) + # Make sure we don't find the same dim twice + found = 0 + while true + found = findnext(sd -> dimsmatch(sd, od), ts, found + 1) + if found == nothing + push!(indexexps, :(nothing)) + break + elseif !(found in allreadyfound) + push!(indexexps, :(tosort[$found])) + push!(allreadyfound, found) + break + end + end + end + Expr(:tuple, indexexps...) +end +# Fallback for Unionall reuired for plotting by abstract +# Dimension type @inline _sortdims(tosort::Tuple, order::Tuple) = _sortdims(tosort, order, ()) @inline _sortdims(tosort::Tuple, order::Tuple, rejected) = # Match dims to the order, and also check if the mode has a @@ -32,22 +59,35 @@ const UnionAllTupleOrVector = Union{Vector{UnionAll},Tuple{UnionAll,Vararg}} @inline _sortdims(tosort::Tuple, order::Tuple{}, rejected) = () @inline _sortdims(tosort::Tuple{}, order::Tuple{}, rejected) = () +@inline _maybeconstruct(dims::Array) = _maybeconstruct((dims...,)) +@inline _maybeconstruct(dims::Tuple) = + (_maybeconstruct(dims[1]), _maybeconstruct(tail(dims))...) +@inline _maybeconstruct(::Tuple{}) = () +@inline _maybeconstruct(dim::Dimension) = dim +@inline _maybeconstruct(dimtype::UnionAll) = + isabstracttype(dimtype) ? dimtype : dimtype() +@inline _maybeconstruct(dimtype::DimType) = + isabstracttype(dimtype) ? dimtype : dimtype() + """ - commondims(x, lookup) + commondims(x, lookup) => Tuple{Vararg{<:Dimension}} + +This is basically `dims(x, lookup)` where the order of the original is kept, +unlike [`dims`](@ref) where the lookup tuple determines the order -This is basically `dims` where the order of the original is kept, -unlike `dims` where the lookup tuple determines the order +Also unlike `dims`,`commondims` always returns a `Tuple`, no matter the input. """ -commondims(A::AbstractArray, B::AbstractArray) = commondims(dims(A), dims(B)) -commondims(A::AbstractArray, lookup) = commondims(dims(A), lookup) -commondims(dims::Tuple, lookup) = commondims(dims, (lookup,)) -commondims(dims::Tuple, lookup::Tuple) = +@inline commondims(A::AbstractArray, B::AbstractArray) = commondims(dims(A), dims(B)) +@inline commondims(A::AbstractArray, lookup) = commondims(dims(A), lookup) +@inline commondims(dims::Tuple, lookup) = commondims(dims, (lookup,)) +@inline commondims(dims::Tuple, lookup::Tuple) = _commondims(symbol2dim(dims), symbol2dim(lookup)) +@inline _commondims(dims::Tuple, lookup::Tuple) = if hasdim(lookup, dims[1]) (dims[1], commondims(tail(dims), lookup)...) else commondims(tail(dims), lookup) end -commondims(dims::Tuple{}, lookup::Tuple) = () +@inline _commondims(dims::Tuple{}, lookup::Tuple) = () """ @@ -56,103 +96,93 @@ commondims(dims::Tuple{}, lookup::Tuple) = () Compare 2 dimensions are of the same base type, or are at least rotations/transformations of the same type. """ -@inline dimsmatch(dims::Tuple, lookups::Tuple) = - all(map(dimsmatch, dims, lookups)) -@inline dimsmatch(dim::DimOrDimType, match::DimOrDimType) = - basetypeof(dim) <: basetypeof(match) || basetypeof(dim) <: basetypeof(dims(mode(match))) +@inline dimsmatch(dims::Tuple, lookups::Tuple) = all(map(dimsmatch, dims, lookups)) +@inline dimsmatch(dim::Dimension, match::Dimension) = dimsmatch(typeof(dim), typeof(match)) +@inline dimsmatch(dim::Type, match::Dimension) = dimsmatch(dim, typeof(match)) +@inline dimsmatch(dim::Dimension, match::Type) = dimsmatch(typeof(dim), match) @inline dimsmatch(dim::DimOrDimType, match::Nothing) = false @inline dimsmatch(dim::Nothing, match::DimOrDimType) = false @inline dimsmatch(dim::Nothing, match::Nothing) = false +@inline dimsmatch(dim::Type, match::Type) = + basetypeof(dim) <: basetypeof(match) || + basetypeof(dim) <: basetypeof(dims(modetype(match))) || + basetypeof(dims(modetype(dim))) <: basetypeof(match) """ - dims2indices(dim::Dimension, lookup, [emptyval=Colon()]) + dims2indices(dim::Dimension, lookup, [emptyval=Colon()]) => NTuple{Union{Colon,AbstractArray,Int}} Convert a `Dimension` or `Selector` lookup to indices, ranges or Colon. """ @inline dims2indices(dim::Dimension, lookup, emptyval=Colon()) = - _dims2indices(mode(dim), dim, lookup, emptyval) + _dims2indices(dim, lookup, emptyval) @inline dims2indices(dim::Dimension, lookup::StandardIndices, emptyval=Colon()) = lookup +@inline dims2indices(A, lookup, emptyval=Colon()) = + dims2indices(dims(A), lookup, emptyval) -""" - dims2indices(A, lookup, [emptyval=Colon()]) - -Convert `Dimension` or `Selector` to regular indices for any object with a `dims` method, -usually an array. -""" -@inline dims2indices(A, lookup, emptyval=Colon()) = begin - dims_ = dims(A) - dims_ isa Nothing && throw(ArgumentError("Object does not define a `dims` method")) - dims2indices(dims_, lookup, emptyval) -end -""" -dims2indices(dims, lookup, [emptyval=Colon()]) +@noinline dimerror() = throw(ArgumentError("Object does not define a `dims` method")) -Convert `Dimension` or `Selector` to regular indices for `dims` - a `Tuple` of `Dimension`. -`lookup` can be a `Tuple` or a single object. -""" @inline dims2indices(dims::DimTuple, lookup, emptyval=Colon()) = dims2indices(dims, (lookup,), emptyval) # Standard array indices are simply returned -@inline dims2indices(dims::DimTuple, lookup::Tuple{Vararg{StandardIndices}}, +@inline dims2indices(dims::DimTuple, lookup::Tuple{Vararg{<:StandardIndices}}, emptyval=Colon()) = lookup # Otherwise attempt to convert dims to indices @inline dims2indices(dims::DimTuple, lookup::Tuple, emptyval=Colon()) = - _dims2indices(map(mode, dims), dims, permutedims(lookup, dims), emptyval) -# Recursively apply dims2indices over tuples of dims and lookups -@inline _dims2indices(modes::Tuple{<:Aligned,Vararg}, dims::Tuple, lookup::Tuple, emptyval) = - (_dims2indices(modes[1], dims[1], lookup[1], emptyval), - _dims2indices(tail(modes), tail(dims), tail(lookup), emptyval)...) + _dims2indices(map(mode, dims), dims, sortdims(lookup, dims), emptyval) + +# Handle tuples with @generated @inline _dims2indices(modes::Tuple{}, dims::Tuple{}, lookup::Tuple{}, emptyval) = () +@generated _dims2indices(modes::Tuple, dims::Tuple, lookup::Tuple, emptyval) = + _dims2indices_inner(modes, dims, lookup, emptyval) + +_dims2indices_inner(modes::Type, dims::Type, lookup::Type, emptyval) = begin + unalligned = Expr(:tuple) + ualookups = Expr(:tuple) + alligned = Expr(:tuple) + dimmerge = Expr(:tuple) + a_count = ua_count = 0 + for (i, mp) in enumerate(modes.parameters) + if mp <: Unaligned + ua_count += 1 + push!(unalligned.args, :(dims[$i])) + push!(ualookups.args, :(lookup[$i])) + push!(dimmerge.args, :(uadims[$ua_count])) + else + a_count += 1 + push!(alligned.args, :(_dims2indices(dims[$i], lookup[$i], emptyval))) + # Update the merged tuple + push!(dimmerge.args, :(adims[$a_count])) + end + end + + if length(unalligned.args) > 1 + # Output the dimmerge, that will combine uadims and adims in the right order + quote + adims = $alligned + # Unaligned dims have to be run together as a set + uadims = unalligned2indices($unalligned, $ualookups) + $dimmerge + end + else + alligned + end +end # Single dim methods # A Dimension type always means Colon(), as if it was constructed with the default value. -@inline _dims2indices(mode, dim::Dimension, lookup::Type{<:Dimension}, emptyval) = Colon() +@inline _dims2indices(dim::Dimension, lookup::Type{<:Dimension}, emptyval) = Colon() # Nothing means nothing was passed for this dimension, return the emptyval -@inline _dims2indices(mode, dim::Dimension, lookup::Nothing, emptyval) = emptyval +@inline _dims2indices(dim::Dimension, lookup::Nothing, emptyval) = emptyval # Simply unwrap dimensions -@inline _dims2indices(mode, dim::Dimension, lookup::Dimension, emptyval) = val(lookup) +@inline _dims2indices(dim::Dimension, lookup::Dimension, emptyval) = val(lookup) # Pass `Selector`s to sel2indices -@inline _dims2indices(mode, dim::Dimension, lookup::Dimension{<:Selector}, emptyval) = - sel2indices(val(lookup), mode, dim) - - -# Deal with unaligned mode that need multiple dimensions indexed together -@inline _dims2indices(modes::Tuple{<:Unaligned,Vararg}, dims::Tuple, lookup::Tuple, emptyval) = begin - # Split dims and lookups into aligned and unaligned - (unaligneddims, unalignedlookup), (aligneddims, alignedlookup) = splitmodes(modes, dims, lookup) - # Convert aligned and unaligned separately. This is recursive, so there may have been - # other `Aligned` dims previously. There is at maximum one block of `Unaligned` dims in - # any set, so we don't have to worry about finding more at the end. - (unaligned2indices(map(mode, unaligneddims), unaligneddims, unalignedlookup, emptyval)..., - _dims2indices(map(mode, aligneddims), aligneddims, alignedlookup, emptyval)...) -end - -# For `Unaligned` mode, `Selector`s select on mode dimensions -@inline unaligned2indices(modes::Tuple, dims::Tuple, - lookup::Tuple{Dimension{<:Selector},Vararg}, emptyval) = - sel2indices(map(val, lookup), modes, dims) -# For non-selector dims, use regular dimension indexing -@inline unaligned2indices(modes::Tuple, dims::Tuple, lookup::Tuple, emptyval) = - (_dims2indices(modes[1], dims[1], lookup[1], emptyval), - _dims2indices(tail(modes), tail(dims), tail(lookup), emptyval)...) - -# Split out dims with Aligned and Unaligned modes -@inline splitmodes(modes::Tuple{Unaligned,Vararg}, dims, lookup) = begin - (unaligneddims, unalignedlookup), aligned = splitmodes(tail(modes), tail(dims), tail(lookup)) - unaligned = (dims[1], unaligneddims...), (lookup[1], unalignedlookup...) - unaligned, aligned -end -@inline splitmodes(modes::Tuple{IndexMode,Vararg}, dims, lookup) = begin - unaligned, (aligneddims, alignedlookup) = splitmodes(tail(modes), tail(dims), tail(lookup)) - aligned = (dims[1], aligneddims...), (lookup[1], alignedlookup...) - unaligned, aligned -end -@inline splitmodes(modes::Tuple{}, dims, lookup) = ((), ()), ((), ()) +@inline _dims2indices(dim::Dimension, lookup::Dimension{<:Selector}, emptyval) = + sel2indices(dim, val(lookup)) """ - slicedims(A, I) + slicedims(x, I) => Tuple{Tuple,Tuple} Slice the dimensions to match the axis values of the new array @@ -162,6 +192,11 @@ the new struct but are useful to give context to plots. Called at the array level the returned tuple will also include the previous reference dims attached to the array. + +# Arguments + +- `x`: An `AbstractDimArray`, `Tuple` of `Dimension`, or `Dimension` +- `I`: A tuple of `Integer`, `Colon` or `AbstractArray` """ function slicedims(A, I) end @@ -180,24 +215,24 @@ end @inline slicedims(dims::Tuple{}, I::Tuple{}) = (), () @inline slicedims(d::Dimension, i::Colon) = (d,), () -# TODO why is `relate` used here? we care about the index order not the relation order -@inline slicedims(d::Dimension, i::Number) = +@inline slicedims(d::Dimension, i::Integer) = (), (rebuild(d, d[relate(d, i)], slicemode(mode(d), val(d), i)),) # TODO deal with unordered arrays trashing the index order @inline slicedims(d::Dimension{<:Union{AbstractArray,Val}}, i::AbstractArray) = (rebuild(d, d[relate(d, i)], slicemode(mode(d), val(d), i)),), () @inline slicedims(d::Dimension{<:Colon}, i::Colon) = (d,), () @inline slicedims(d::Dimension{<:Colon}, i::AbstractArray) = (d,), () -@inline slicedims(d::Dimension{<:Colon}, i::Number) = (), (d,) +@inline slicedims(d::Dimension{<:Colon}, i::Integer) = (), (d,) -@inline relate(d::Dimension, i) = maybeflip(relationorder(d), d, i) +@inline relate(d::Dimension, i) = maybeflip(relation(d), d, i) @inline maybeflip(::Forward, d, i) = i @inline maybeflip(::Reverse, d, i::Integer) = lastindex(d) - i + 1 @inline maybeflip(::Reverse, d, i::AbstractArray) = reverse(lastindex(d) .- i .+ 1) """ - dimnum(x, lookup) + dimnum(x, lookup::Tuple) => NTuple{Int} + dimnum(x, lookup) => Int Get the number(s) of `Dimension`(s) as ordered in the dimensions of an object. @@ -209,18 +244,18 @@ The return type will be a Tuple of `Int` or a single `Int`, depending on wether `lookup` is a `Tuple` or single `Dimension`. ## Example + ```jldoctest -julia> A = DimArray(ones(10, 10, 10), (X, Y, Z)); +julia> using DimensionalData +julia> A = DimArray(ones(10, 10, 10), (X, Y, Z)); -julia> dimnum(A, Z) -3 ``` """ @inline dimnum(A, lookup) = dimnum(dims(A), lookup) @inline dimnum(d::Tuple, lookup) = dimnum(d, (lookup,))[1] @inline dimnum(d::Tuple, lookup::AbstractArray) = dimnum(d, (lookup...,)) -@inline dimnum(d::Tuple, lookup::Tuple) = _dimnum(d, lookup, (), 1) +@inline dimnum(d::Tuple, lookup::Tuple) = _dimnum(d, symbol2dim(lookup), (), 1) # Match dim and lookup, also check if the mode has a transformed dimension that matches @inline _dimnum(d::Tuple, lookup::Tuple, rejected, n) = @@ -235,13 +270,14 @@ julia> dimnum(A, Z) @inline _dimnum(dims::Tuple{}, lookup::Tuple{Number,Vararg}, rejected, n) = lookup # Throw an error if the lookup is not found @inline _dimnum(dims::Tuple{}, lookup::Tuple, rejected, n) = - throw(ArgumentError("No $(basetypeof(lookup[1])) in dims")) + throw(ArgumentError("No $(name(lookup[1])) in dims")) # Return an empty tuple when we run out of lookups @inline _dimnum(dims::Tuple, lookup::Tuple{}, rejected, n) = () @inline _dimnum(dims::Tuple{}, lookup::Tuple{}, rejected, n) = () """ - hasdim(x, lookup) + hasdim(x, lookup::Tuple) => NTUple{Bool} + hasdim(x, lookup) => Bool ## Arguments - `x`: any object with a `dims` method, a `Tuple` of `Dimension` or a single `Dimension`. @@ -251,17 +287,15 @@ Check if an object or tuple contains an `Dimension`, or a tuple of dimensions. ## Example ```jldoctest -julia> A = DimArray(ones(10, 10, 10), (X, Y, Z)); +julia> using DimensionalData -julia> hasdim(A, X) -true +julia> A = DimArray(ones(10, 10, 10), (X, Y, Z)); -julia> hasdim(A, Ti) -false ``` """ @inline hasdim(A::AbstractArray, lookup) = hasdim(dims(A), lookup) @inline hasdim(d::Tuple, lookup::Tuple) = map(l -> hasdim(d, l), lookup) +@inline hasdim(d::Tuple, lookup::Symbol) = hasdim(d, symbol2dim(lookup)) @inline hasdim(d::Tuple, lookup::DimOrDimType) = if dimsmatch(d[1], lookup) true @@ -281,19 +315,16 @@ A tuple holding the unmatched dimensions is always returned. ## Example ```jldoctest -julia> A = DimArray(ones(10, 10, 10), (X, Y, Z)); +julia> using DimensionalData -julia> otherdims(A, X) -(Y: Base.OneTo(10), Z: Base.OneTo(10)) +julia> A = DimArray(ones(10, 10, 10), (X, Y, Z)); -julia> otherdims(A, Ti) -(X: Base.OneTo(10), Y: Base.OneTo(10), Z: Base.OneTo(10)) ``` """ @inline otherdims(A::AbstractArray, lookup) = otherdims(dims(A), lookup) @inline otherdims(dims::Tuple, lookup::DimOrDimType) = otherdims(dims, (lookup,)) @inline otherdims(dims::Tuple, lookup::Tuple) = - _otherdims(dims, _sortdims(lookup, dims)) + _otherdims(dims, _sortdims(symbol2dim(lookup), symbol2dim(dims))) #= Work with a sorted lookup where the missing dims are `nothing`. Then we can compare with `dimsmatch`, and splat away the matches. =# @@ -318,6 +349,8 @@ and returns a new object or tuple with the dimension updated. # Example ```jldoctest +using DimensionalData + A = DimArray(ones(10, 10), (X, Y(10:10:100))) B = setdims(A, Y('a':'j')) val(dims(B, Y)) @@ -327,7 +360,8 @@ val(dims(B, Y)) 'a':1:'j' ``` """ -@inline setdims(A, newdims::Union{Dimension,DimTuple}) = rebuild(A, data(A), setdims(dims(A), newdims)) +@inline setdims(A, newdims::Union{Dimension,DimTuple}) = + rebuild(A, parent(A), setdims(dims(A), newdims)) @inline setdims(dims::DimTuple, newdims::DimTuple) = map(nd -> setdims(dims, nd), newdims) # TODO handle the multiples of the same dim. @inline setdims(dims::DimTuple, newdim::Dimension) = map(d -> setdims(d, newdim), dims) @@ -335,7 +369,8 @@ val(dims(B, Y)) basetypeof(dim) <: basetypeof(newdim) ? newdim : dim """ - swapdims(x, newdims) + swapdims(x::T, newdims) => T + swapdims(dims::Tuple, newdims) => Tuple{Dimension} Swap dimensions for the passed in dimensions, in the order passed. @@ -346,23 +381,19 @@ objectes replace the original dimension. `nothing` leaves the original dimension as-is. ## Arguments -- `x`: any object with a `dims` method, a `Tuple` of `Dimension` or a single `Dimension`. -- `newdim`: Tuple or single `Dimension` or dimension `Type`. +- `x`: any object with a `dims` method or a `Tuple` of `Dimension`. +- `newdim`: Tuple of `Dimension` or dimension `Type`. # Example ```jldoctest -julia> A = DimArray(ones(10, 10, 10), (X, Y, Z)); - - -julia> B = swapdims(A, (Z, Dim{:custom}, Ti)); +julia> using DimensionalData +julia> A = DimArray(ones(10, 10, 10), (X, Y, Z)); -julia> dimnum(B, Ti) -3 ``` """ @inline swapdims(A::AbstractArray, newdims::Tuple) = - rebuild(A, data(A), formatdims(A, swapdims(dims(A), newdims))) + rebuild(A, parent(A), formatdims(A, swapdims(dims(A), newdims))) @inline swapdims(dims::DimTuple, newdims::Tuple) = map((d, nd) -> _swapdims(d, nd), dims, newdims) @@ -385,10 +416,10 @@ cell step, sampling type and order. @inline reducedims(A, dimstoreduce) = reducedims(A, (dimstoreduce,)) @inline reducedims(A, dimstoreduce::Tuple) = reducedims(dims(A), dimstoreduce) @inline reducedims(dims::DimTuple, dimstoreduce::Tuple) = - map(reducedims, dims, permutedims(dimstoreduce, dims)) + map(reducedims, dims, sortdims(dimstoreduce, dims)) # Map numbers to corresponding dims. Not always type-stable @inline reducedims(dims::DimTuple, dimstoreduce::Tuple{Vararg{Int}}) = - map(reducedims, dims, permutedims(map(i -> dims[i], dimstoreduce), dims)) + map(reducedims, dims, sortdims(map(i -> dims[i], dimstoreduce), dims)) # Reduce matching dims but ignore nothing vals - they are the dims not being reduced @inline reducedims(dim::Dimension, ::Nothing) = dim @@ -456,20 +487,16 @@ any combination of either. ## Example ```jldoctest -julia> A = DimArray(ones(10, 10, 10), (X, Y, Z)); +julia> using DimensionalData +julia> A = DimArray(ones(10, 10, 10), (X, Y, Z)); -julia> dims(A, Z) -dimension Z: -val: Base.OneTo(10) -mode: NoIndex() -metadata: nothing -type: Z{Base.OneTo{Int64},NoIndex,Nothing} ``` """ @inline dims(A::AbstractArray, lookup) = dims(dims(A), lookup) @inline dims(d::DimTuple, lookup) = dims(d, (lookup,))[1] -@inline dims(d::DimTuple, lookup::Tuple) = _dims(d, lookup, (), d) +@inline dims(d::DimTuple, lookup::Tuple) = + _dims(d, symbol2dim(lookup), (), d) @inline _dims(d, lookup::Tuple, rejected, remaining) = if dimsmatch(remaining[1], lookup[1]) @@ -485,7 +512,7 @@ type: Z{Base.OneTo{Int64},NoIndex,Nothing} @inline _dims(d, lookup::Tuple{Number,Vararg}, rejected, remaining::Tuple{}) = () # Throw an error if the lookup is not found @inline _dims(d, lookup::Tuple, rejected, remaining::Tuple{}) = - throw(ArgumentError("No $(basetypeof(lookup[1])) in dims")) + throw(ArgumentError("No $(name(lookup[1])) in dims")) # Return an empty tuple when we run out of lookups @inline _dims(d, lookup::Tuple{}, rejected, remaining::Tuple) = () @inline _dims(d, lookup::Tuple{}, rejected, remaining::Tuple{}) = () @@ -525,7 +552,13 @@ function comparedims end @inline comparedims(a::AnonDim, b::Dimension) = b @inline comparedims(a::Dimension, b::Dimension) = begin basetypeof(a) == basetypeof(b) || - throw(DimensionMismatch("$(basetypeof(a)) and $(basetypeof(b)) dims on the same axis")) + throw(DimensionMismatch("$(name(a)) and $(name(b)) dims on the same axis")) # TODO compare the mode, and maybe the index. return a end + +@inline symbol2dim(s::Symbol) = Dim{s}() +@inline symbol2dim(dim::Dimension) = dim +@inline symbol2dim(dimtype::Type{<:Dimension}) = dimtype +@inline symbol2dim(dims::Tuple) = map(symbol2dim, dims) +@inline symbol2dim(dim) = dim diff --git a/src/selector.jl b/src/selector.jl index a95c3aeb4..c52e8262f 100644 --- a/src/selector.jl +++ b/src/selector.jl @@ -2,10 +2,19 @@ Selectors are wrappers that indicate that passed values are not the array indices, but values to be selected from the dimension index, such as `DateTime` objects for a `Ti` dimension. + +Selectors provided in DimensionalData are: + +- [`At`](@ref) +- [`Between`](@ref) +- [`Near`](@ref) +- [`Where`](@ref) +- [`Contains`](@ref) + """ abstract type Selector{T} end -const SelectorOrStandard = Union{Selector, StandardIndices} +const SelectorOrStandard = Union{Selector,StandardIndices} val(sel::Selector) = sel.val rebuild(sel::Selector, val) = basetypeof(sel)(val) @@ -36,6 +45,8 @@ and wont be used. ## Example ```jldoctest +using DimensionalData + A = DimArray([1 2 3; 4 5 6], (X(10:10:20), Y(5:7))) A[X(At(20)), Y(At(6))] @@ -58,17 +69,20 @@ rtol(sel::At) = sel.rtol """ Near(x) -Selector that selects the nearest index. -With [`Points`](@ref) this is simply the index nearest to the -contained value, however with [`Intervals`](@ref) it is the interval -_center_ nearest to the contained value. This will be offset from the -index value for [`Start`](@ref) and [`End`](@ref) loci. +Selector that selects the nearest index to `x`. + +With [`Points`](@ref) this is simply the index values nearest to the `x`, +however with [`Intervals`](@ref) it is the interval _center_ nearest to `x`. +This will be offset from the index value for [`Start`](@ref) and +[`End`](@ref) loci. ## Example ```jldoctest +using DimensionalData + A = DimArray([1 2 3; 4 5 6], (X(10:10:20), Y(5:7))) -A[X(Near(23)), Y(Near(5.1))] +A[X(Near(23)), Y(Near(5.1))] # output @@ -90,6 +104,8 @@ Can only be used for [`Intervals`](@ref) or [`Categorical`](@ref). ## Example ```jldoctest +using DimensionalData + dims_ = X(10:10:20; mode=Sampled(sampling=Intervals())), Y(5:7; mode=Sampled(sampling=Intervals())) A = DimArray([1 2 3; 4 5 6], dims_) @@ -107,24 +123,38 @@ end """ Between(a, b) -Selector that retreive all indices located between 2 values. +Selector that retreive all indices located between 2 values, +evaluated with `>=` for the lower value, and `<` for the upper value. +This means the same value will not be counted twice in 2 `Between` +selections. For [`Intervals`](@ref) the whole interval must be lie between the values. For [`Points`](@ref) the points must fall between -the 2 values. These different sampling traits will often give different -results with the same index and values - this is the intended behaviour. +the values. Different [`Sampling`](@ref) types may give different +results with the same input - this is the intended behaviour. + +`Between` for [`Irregular`](@ref) intervals is a little complicated. The +interval is the distance between a value and the next (for [`Start`](ref) locus) +or previous (for [`End`](@ref) locus) value. + +For [`Center`](@ref), we take the mid point between two index values +as the start and end of each interval. This may or may not make sense for +the values in your indes, so use `Between` with `Irregular` `Intervals(Center())` +with caution. ## Example ```jldoctest +using DimensionalData + A = DimArray([1 2 3; 4 5 6], (X(10:10:20), Y(5:7))) -A[X(Between(15, 25)), Y(Between(4, 6.5))] +A[X(Between(15, 25)), Y(Between(4, 6.5))] # output DimArray with dimensions: - X: 20:10:20 - Y: 5:6 + X: 20:10:20 (Sampled: Ordered Regular Points) + Y: 5:6 (Sampled: Ordered Regular Points) and data: 1×2 Array{Int64,2} 4 5 ``` @@ -144,14 +174,16 @@ a single value from the index and returns a `Bool`. ## Example ```jldoctest +using DimensionalData + A = DimArray([1 2 3; 4 5 6], (X(10:10:20), Y(19:21))) A[X(Where(x -> x > 15)), Y(Where(x -> x in (19, 21)))] # output DimArray with dimensions: - X: Int64[20] - Y: Int64[19, 21] + X: Int64[20] (Sampled: Ordered Regular Points) + Y: Int64[19, 21] (Sampled: Ordered Regular Points) and data: 1×2 Array{Int64,2} 4 6 ``` @@ -162,41 +194,50 @@ end val(sel::Where) = sel.f -# Get the dims in the same order as the mode -# This would be called after RegularIndex and/or Categorical -# dimensions are removed -@inline _dims2indices(mode::Transformed, dims::Tuple, lookups::Tuple, emptyval) = - sel2indices(mode, dims, map(val, permutedims(dimz, dims(mode)))) +# sel2indices ========================================================================== + +# Converts Selectors to regular indices +# @inline sel2indices(A::AbstractArray, lookup) = sel2indices(dims(A), lookup) @inline sel2indices(dims::Tuple, lookup) = sel2indices(dims, (lookup,)) @inline sel2indices(dims::Tuple, lookup::Tuple) = - map((d, l) -> sel2indices(l, mode(d), d), dims, lookup) + map((d, l) -> sel2indices(d, l), dims, lookup) + # First filter based on rough selector properties ----------------- -# Mode is passed in from dims2indices # Standard indices are just returned. -@inline sel2indices(sel::StandardIndices, ::IndexMode, ::Dimension) = sel +@inline sel2indices(::Dimension, sel::StandardIndices) = sel # Vectors are mapped -@inline sel2indices(sel::Selector{<:AbstractVector}, mode::IndexMode, dim::Dimension) = - [sel2indices(mode, dim, rebuild(sel, v)) for v in val(sel)] -@inline sel2indices(sel::Selector, mode::IndexMode, dim::Dimension) = - sel2indices(mode, dim, sel) +@inline sel2indices(dim::Dimension, sel::Selector{<:AbstractVector}) = + [sel2indices(mode(dim), dim, rebuild(sel, v)) for v in val(sel)] +@inline sel2indices(dim::Dimension, sel::Selector) = + sel2indices(mode(dim), dim, sel) + + +# Where selector ============================== +# Yes this is everything. +# Where doesn't need mode specialisation +@inline sel2indices(dim::Dimension, sel::Where) = + [i for (i, v) in enumerate(index(dim)) if sel.f(v)] + -@inline sel2indices(sel::Where, mode::IndexMode, dim::Dimension) = - [i for (i, v) in enumerate(val(dim)) if sel.f(v)] # Then dispatch based on IndexMode ----------------- -# NoIndex +# Selectors can have varied behaviours depending on +# the index mode. + +# NoIndex ----------------------------- # This just converts the selector to standard indices. Implemented just # so the Selectors actually work, not because what they do is useful or interesting. -@inline sel2indices(mode::NoIndex, dim::Dimension, sel::Union{At,Near,Contains}) = val(sel) +@inline sel2indices(mode::NoIndex, dim::Dimension, sel::Union{At,Near,Contains}) = + val(sel) @inline sel2indices(mode::NoIndex, dim::Dimension, sel::Union{Between}) = val(sel)[1]:val(sel)[2] -# Categorical +# Categorical IndexMode ------------------------- @inline sel2indices(mode::Categorical, dim::Dimension, sel::Selector) = if sel isa Union{Contains,Near} sel2indices(Points(), mode, dim, At(val(sel))) @@ -204,17 +245,18 @@ val(sel::Where) = sel.f sel2indices(Points(), mode, dim, sel) end -# Sampled + +# Sampled IndexMode ----------------------------- @inline sel2indices(mode::AbstractSampled, dim::Dimension, sel::Selector) = sel2indices(sampling(mode), mode, dim, sel) # For Sampled filter based on sampling type and selector ----------------- -# At selector +# At selector ------------------------- @inline sel2indices(sampling::Sampling, mode::IndexMode, dim::Dimension, sel::At) = at(sampling, mode, dim, sel) -# Near selector +# Near selector ----------------------- @inline sel2indices(sampling::Sampling, mode::IndexMode, dim::Dimension, sel::Near) = begin if span(mode) isa Irregular && locus(mode) isa Union{Start,End} error("Near is not implemented for Irregular with Start or End loci. Use Contains") @@ -222,274 +264,240 @@ val(sel::Where) = sel.f near(sampling, mode, dim, sel) end -# Contains selector +# Contains selector ------------------- @inline sel2indices(sampling::Points, mode::T, dim::Dimension, sel::Contains) where T = throw(ArgumentError("`Contains` has no meaning with `Points`. Use `Near`")) @inline sel2indices(sampling::Intervals, mode::IndexMode, dim::Dimension, sel::Contains) = contains(sampling, mode, dim, sel) -# Between selector +# Between selector -------------------- @inline sel2indices(sampling::Sampling, mode::IndexMode, dim::Dimension, sel::Between{<:Tuple}) = between(sampling, mode, dim, sel) -# Transformed IndexMode + +# Unaligned IndexMode ------------------------------------------ + +# unalligned2indices is callled directly from dims2indices # We use the transformation from the first Transformed dim. # In practice the others could be empty. -@inline sel2indices(sel::Tuple{Vararg{<:Selector}}, modes::Tuple{Vararg{<:Transformed}}, - dims::DimTuple) = - map(_to_int, sel, transform(modes[1])([map(val, sel)...])) +@inline unalligned2indices(dims::DimTuple, sel::Tuple) = sel +@inline unalligned2indices(dims::DimTuple, sel::Tuple{<:Dimension,Vararg{<:Dimension}}) = + unalligned2indices(dims, map(val, sel)) +@inline unalligned2indices(dims::DimTuple, sel::Tuple{<:Selector,Vararg{<:Selector}}) = begin + coords = [map(val, sel)...] + transformed = transformfunc(mode(dims[1]))(coords) + map(_to_int, sel, transformed) +end _to_int(::At, x) = convert(Int, x) _to_int(::Near, x) = round(Int, x) + # Selector methods +# at ============================================================================= + @inline at(dim::Dimension, sel::At) = at(sampling(mode(dim)), mode(dim), dim, sel) @inline at(::Sampling, mode::IndexMode, dim::Dimension, sel::At) = relate(dim, at(dim, val(sel), atol(sel), rtol(sel))) @inline at(dim::Dimension{<:Val{Index}}, selval, atol::Nothing, rtol::Nothing) where Index = begin - i = findfirst(x -> x == selval, Index) + i = findfirst(x -> x == unwrap(selval), Index) i == nothing && selvalnotfound(dim, selval) return i end -@inline at(dim::Dimension{<:Val{Index}}, selval::Val, atol::Nothing, rtol::Nothing) where Index = begin - i = findfirst(x -> x == unwrap(selval), Index) +@inline at(dim::Dimension{<:Val{Index}}, selval::Val{X}, atol::Nothing, rtol::Nothing) where {Index,X} = begin + i = findfirst(x -> x == X, Index) i == nothing && selvalnotfound(dim, selval) return i end @inline at(dim::Dimension, selval, atol::Nothing, rtol::Nothing) = begin - i = findfirst(x -> x == selval, val(dim)) + i = findfirst(x -> x == selval, index(dim)) i == nothing && selvalnotfound(dim, selval) return i end -# @inline at(dim::Dimension, selval, atol, rtol) = begin -# # This is not particularly efficient. It should be separated -# # out for unordered dims and otherwise treated as an ordered list. -# i = findfirst(x -> isapprox(x, selval; atol=atol, rtol=rtol), val(dim)) -# i == nothing && selvalnotfound(dim, selval) -# return i -# end - -@noinline selvalnotfound(dim, selval) = + +@noinline selvalnotfound(dim, selval) = throw(ArgumentError("$selval not found in $dim")) -near(dim::Dimension, sel::Near) = - near(sampling(mode(dim)), mode(dim), dim, sel) + +# near =========================================================================== + +# Finds the nearest point in the index, adjusting for locus if necessary. +# In Intevals we are finding the nearest point to the center of the interval. + +near(dim::Dimension, sel::Near) = near(sampling(mode(dim)), mode(dim), dim, sel) near(::Sampling, mode::IndexMode, dim::Dimension, sel::Near) = begin order = indexorder(dim) order isa Unordered && throw(ArgumentError("`Near` has no meaning in an `Unordered` index")) - relate(dim, near(locus(mode), order, dim, sel)) -end -# Start is just offset Center -near(::Start, order::Order, dim::Dimension, sel::Near) = - near(Center(), order, dim, Near(val(sel) - abs(step(dim)) / 2)) -near(::Center, order::Forward, dim::Dimension, sel) = begin - selval = val(sel) - i = _inbounds(_searchfirst(order, dim, selval), dim) - if i <= firstindex(dim) - firstindex(dim) - else - abs(dim[i] - selval) <= abs(dim[i - 1] - selval) ? i : i - 1 - end -end -near(::Center, order::Reverse, dim::Dimension, sel::Near) = begin - selval = val(sel) - i = _inbounds(_searchlast(order, dim, selval), dim) - if i >= lastindex(dim) - lastindex(dim) - else - abs(dim[i] - selval) <= abs(dim[i + 1] - selval) ? i : i + 1 - end -end -# End is offset and backwards. -near(::End, order::Forward, dim::Dimension, sel::Near) = begin - selval = val(sel) + step(dim) / 2 - i = _inbounds(_searchfirst(order, dim, selval), dim) - if i <= firstindex(dim) - firstindex(dim) - else - abs(dim[i] - selval) < abs(dim[i - 1] - selval) ? i : i - 1 - end -end -near(::End, order::Reverse, dim::Dimension, sel::Near) = begin - selval = val(sel) - step(dim) / 2 - i = _inbounds(_searchlast(order, dim, selval), dim) - if i >= lastindex(dim) - lastindex(dim) + locus = DD.locus(dim) + + v = _locus_adjust(locus, val(sel), dim) + i = _inbounds(_searchorder(order)(order, dim, v), dim) + i = if (order isa Forward ? (<=) : (>=))(i, _dimlower(order, dim)) + _dimlower(order, dim) else - abs(dim[i] - selval) < abs(dim[i + 1] - selval) ? i : i + 1 + previ = _prevind(order, i) + vl, vi = map(abs, (dim[previ] - v, dim[i] - v)) + # We have to use the right >/>= for Start/End locus + _lt(locus)(vl, vi) ? previ : i end + relate(dim, i) end +_locus_adjust(locus::Start, v, dim) = v - abs(step(dim)) / 2 +_locus_adjust(locus::Center, v, dim) = v +_locus_adjust(locus::End, v, dim) = v + abs(step(dim)) / 2 + + +# contains ================================================================================ + +# Finds which interval contains a point contains(dim::Dimension, sel::Contains) = contains(sampling(mode(dim)), mode(dim), dim, sel) +# Points -------------------------------------- contains(::Points, ::IndexMode, dim::Dimension, sel::Contains) = throw(ArgumentError("Points IndexMode cannot use 'Contains', use 'Near' instead.")) -contains(sampling::Sampling, mode::IndexMode, dim::Dimension, sel::Contains) = - relate(dim, contains(indexorder(mode), span(mode), locus(mode), sampling, dim, sel)) -contains(order::Forward, span::Regular, ::Start, ::Intervals, dim::Dimension, sel::Contains) = begin - v = val(sel) - s = val(span) - (v < first(dim) || v >= last(dim) + s) && throw(BoundsError()) - i = _searchlast(order, dim, v) - if !(val(dim) isa AbstractRange) # Check the value is in this cell - (dim[i] + abs(s) > v) || error("No span for $v") +# Intervals ----------------------------------- +contains(sampling::Intervals, mode::IndexMode, dim::Dimension, sel::Contains) = + relate(dim, contains(span(mode), sampling, indexorder(mode), locus(mode), dim, sel)) + +# Regular Intervals --------------------------- +contains(span::Regular, ::Intervals, order, locus, dim::Dimension, sel::Contains) = begin + v = val(sel); s = abs(val(span)) + _locus_checkbounds(locus, bounds(dim), v) + i = _whichsearch(locus, order)(order, dim, maybeaddhalf(locus, s, v)) + # Check the value is in this cell - it might not be for Val or Vector. + if !(val(dim) isa AbstractRange) + _lt(locus)(v, dim[i] + s) || error("No interval contains $(v)") end i end -contains(order::Reverse, span::Regular, ::Start, ::Intervals, dim::Dimension, sel::Contains) = begin - v = val(sel) - (v < last(dim) || v >= first(dim) - val(span)) && throw(BoundsError()) - i = _searchfirst(order, dim, v) - if !(val(dim) isa AbstractRange) # Check the value is in this cell - (dim[i] + abs(val(span)) > v) || error("No span for $v") - end - i + +# Irregular Intervals ------------------------- +contains(span::Irregular, ::Intervals, order::Order, locus::Locus, dim::Dimension, sel::Contains) = begin + _locus_checkbounds(locus, bounds(span), val(sel)) + _whichsearch(locus, order)(order, dim, val(sel)) end -contains(order::Forward, span::Regular, ::End, ::Intervals, dim::Dimension, sel::Contains) = begin +contains(span::Irregular, ::Intervals, order::Order, locus::Center, dim::Dimension, sel::Contains) = begin v = val(sel) - (v <= first(dim) - val(span) || v > last(dim)) && throw(BoundsError()) + _locus_checkbounds(locus, bounds(span), v) i = _searchfirst(order, dim, v) - if !(val(dim) isa AbstractRange) # Check the value is in this cell - (dim[i] - abs(val(span)) <= v) || error("No span for $v") - end - i -end -contains(order::Reverse, span::Regular, ::End, ::Intervals, dim::Dimension, sel::Contains) = begin - v = val(sel) - (v <= last(dim) + val(span) || v > first(dim)) && throw(BoundsError()) - i = _searchlast(order, dim, v) - if !(val(dim) isa AbstractRange) # Check the value is in this cell - (dim[i] - abs(val(span)) <= v) || error("No span for $v") - end - i -end -contains(order::Forward, span::Regular, ::Center, ::Intervals, dim::Dimension, sel::Contains) = begin - half = abs(val(span) / 2) - v = val(sel) - (v < first(dim) - half || v >= last(dim) + half) && throw(BoundsError()) - i = _searchlast(order, dim, v + half) - if !(val(dim) isa AbstractRange) # Check the value is in this cell - (dim[i] <= v - abs(half)) || (dim[i] > v + abs(half)) && error("No span for $v") - end - i -end -contains(order::Reverse, span::Regular, ::Center, ::Intervals, dim::Dimension, sel::Contains) = begin - half = abs(val(span) / 2) - v = val(sel) - (v < last(dim) - half || v >= first(dim) + half) && throw(BoundsError()) - i = _searchfirst(order, dim, v + half) - if !(val(dim) isa AbstractRange) # Check the value is in this cell - (dim[i] <= v - abs(half)) || (dim[i] > v + abs(half)) && error("No span for $v") - end - i -end -contains(order::Order, ::Irregular, ::Start, ::Intervals, dim::Dimension, sel::Contains) = begin - i = _searchlast(order, dim, val(sel)) - checkbounds(val(dim), i) - i -end -contains(order::Order, ::Irregular, ::End, ::Intervals, dim::Dimension, sel::Contains) = begin - i = _searchfirst(order, dim, val(sel)) - checkbounds(val(dim), i) - i -end -contains(order::Reverse, ::Irregular, ::Center, ::Intervals, dim::Dimension, sel::Contains) = begin - i = _searchlast(order, dim, val(sel)) - checkbounds(val(dim), i) - if i == firstindex(dim) - firstindex(dim) - else - (dim[i] + dim[i - 1]) / 2 <= val(sel) ? i - 1 : i - end -end -contains(order::Forward, ::Irregular, ::Center, ::Intervals, dim::Dimension, sel::Contains) = begin - i = _searchlast(order, dim, val(sel)) - checkbounds(val(dim), i) - if i == lastindex(dim) - lastindex(dim) - else - (dim[i] + dim[i + 1]) / 2 <= val(sel) ? i + 1 : i - end -end + i <= firstindex(dim) && return firstindex(dim) + i > lastindex(dim) && return lastindex(dim) + + interval = abs(dim[i] - dim[i - 1]) + distance = abs(dim[i] - v) + _order_lt(order)(interval / 2, distance) ? i - 1 : i +end + +_whichsearch(::Locus, ::Forward) = _searchlast +_whichsearch(::Locus, ::Reverse) = _searchfirst +_whichsearch(::End, ::Forward) = _searchfirst +_whichsearch(::End, ::Reverse) = _searchlast + +maybeaddhalf(::Locus, s, v) = v +maybeaddhalf(::Center, s, v) = v + s / 2 + +_order_lt(::Forward) = (<) +_order_lt(::Reverse) = (<=) + + +# between ================================================================================ + +# Finds all values between two points, adjusted for locus where necessary between(dim::Dimension, sel::Between) = between(sampling(mode(dim)), mode(dim), dim, sel) between(sampling::Sampling, mode::IndexMode, dim::Dimension, sel::Between) = begin order = indexorder(dim) - order isa Unordered && throw(ArgumentError("Cannot use `Between` on an unordered mode")) - between(order, sampling, mode, dim, sel) -end - -between(order::Forward, ::Points, ::IndexMode, dim::Dimension, sel::Between) = begin - low, high = _sorttuple(sel) - a = _inbounds(_searchfirst(order, dim, low), dim) - b = _inbounds(_searchlast(order, dim, high), dim) + order isa Unordered && throw(ArgumentError("Cannot use `Between` with an Unordered IndexMode")) + a, b = between(sampling, order, mode, dim, sel) relate(dim, a:b) end -between(order::Reverse, ::Points, ::IndexMode, dim::Dimension, sel::Between) = begin - low, high = _sorttuple(sel) - a = _inbounds(_searchlast(order, dim, high), dim) - b = _inbounds(_searchfirst(order, dim, low), dim) - relate(dim, a:b) -end -between(order::Order, s::Intervals, mode::IndexMode, dim::Dimension, sel::Between) = - between(span(mode), order, mode, dim, sel) -between(span::Regular, order::Forward, mode::IndexMode, dim::Dimension, sel::Between) = begin - low, high = _sorttuple(sel) .+ _locus_adjustment(mode, span) - a = _inbounds(_searchfirst(order, dim, low), dim) - b = _inbounds(_searchlast(order, dim, high), dim) - relate(dim, a:b) + +# Points ------------------------------------ +between(sampling::Points, o::Order, ::IndexMode, dim::Dimension, sel::Between) = begin + b1, b2 = _maybeflip(o, _sorttuple(sel)) + s1, s2 = _maybeflip(o, (_searchfirst, _searchlast)) + _inbounds((s1(o, dim, b1), s2(o, dim, b2)), dim) end -between(span::Regular, order::Reverse, mode::IndexMode, dim::Dimension, sel::Between) = begin - low, high = _sorttuple(sel) .+ _locus_adjustment(mode, span) - a = _inbounds(_searchfirst(order, dim, high), dim) - b = _inbounds(_searchlast(order, dim, low), dim) - relate(dim, a:b) + +# Intervals ------------------------- +between(sampling::Intervals, o::Order, mode::IndexMode, dim::Dimension, sel::Between) = + between(span(mode), sampling, o, mode, dim, sel) + +# Regular Intervals ------------------------- +between(span::Regular, ::Intervals, o::Order, mode::IndexMode, dim::Dimension, sel::Between) = begin + b1, b2 = _maybeflip(o, _sorttuple(sel) .+ _locus_adjust(mode)) + _inbounds((_searchfirst(o, dim, b1), _searchlast(o, dim, b2)), dim) end -between(span::Irregular, order::Forward, ::IndexMode, dim::Dimension, sel::Between) = begin - low, high = _sorttuple(sel) - a = _inbounds(_searchfirst(order, dim, low), dim) - b = _inbounds(_searchlast(order, dim, high), dim) - relate(dim, a:b) + +_locus_adjust(mode) = _locus_adjust(locus(mode), abs(step(span(mode)))) +_locus_adjust(locus::Start, step) = zero(step), -step +_locus_adjust(locus::Center, step) = step/2, -step/2 +_locus_adjust(locus::End, step) = step, zero(step) + + +# Irregular Intervals ----------------------- + +struct Upper end +struct Lower end + +between(span::Irregular, ::Intervals, o::Order, mode::IndexMode, d::Dimension, sel::Between) = begin + l, h = _sorttuple(sel) + bl, bh = bounds(span) + a = l <= bl ? _dimlower(o, d) : between(Lower(), locus(mode), o, d, l) + b = h >= bh ? _dimupper(o, d) : between(Upper(), locus(mode), o, d, h) + _maybeflip(o, (a, b)) end -between(span::Irregular, order::Reverse, ::IndexMode, dim::Dimension, sel::Between) = begin - low, high = _sorttuple(sel) - a = _inbounds(_searchlast(order, dim, high), dim) - b = _inbounds(_searchfirst(order, dim, low), dim) - relate(dim, a:b) + +between(x, locus::Union{Start,End}, o::Order, d::Dimension, v) = + _search(x, o, d, v) - ordscalar(o) * (locscalar(locus) + endshift(x)) +between(x, locus::Center, o::Order, d::Dimension, v) = begin + r = ordscalar(o); sh = endshift(x) + i = _search(x, o, d, v) + interval = abs(d[i] - d[i-r]) + distance = abs(d[i] - v) + # Use the right >/>= to match interval bounds + _lt(x)(distance, (interval / 2)) ? i - sh * r : i - (1 + sh) * r end -_searchlast(::Forward, dim::Dimension, v) = searchsortedlast(val(dim), v) -_searchlast(::Reverse, dim::Dimension, v) = searchsortedlast(val(dim), v; rev=true) -_searchlast(::Forward, dim::Dimension{<:Val{Index}}, v) where Index = - searchsortedlast(Index, v) -_searchlast(::Reverse, dim::Dimension{<:Val{Index}}, v) where Index = - searchsortedlast(Index, v; rev=true) +locscalar(::Start) = 1 +locscalar(::End) = 0 +endshift(::Lower) = -1 +endshift(::Upper) = 1 +ordscalar(::Forward) = 1 +ordscalar(::Reverse) = -1 + +_search(x, order, dim, v) = + _inbounds(_searchorder(order)(order, dim, v; lt=_lt(x)), dim) + +_lt(::Lower) = (<) +_lt(::Upper) = (<=) + -@inline _searchfirst(::Forward, dim::Dimension, v) = searchsortedfirst(val(dim), v) -@inline _searchfirst(::Reverse, dim::Dimension, v) = searchsortedfirst(val(dim), v; rev=true) -@inline _searchfirst(::Forward, dim::Dimension{<:Val{Index}}, v) where Index = - searchsortedfirst(Index, v) -@inline _searchfirst(::Reverse, dim::Dimension{<:Val{Index}}, v) where Index = - searchsortedfirst(Index, v; rev=true) +# Shared utils ============================================================================ -_locus_adjustment(mode::AbstractSampled, span::Regular) = - _locus_adjustment(locus(mode), abs(step(span))) -_locus_adjustment(locus::Start, step) = zero(step), -step -_locus_adjustment(locus::Center, step) = step * 0.5, step * -0.5 -_locus_adjustment(locus::End, step) = step, zero(step) +_searchlast(o::Order, dim::Dimension, v; kwargs...) = + searchsortedlast(val(dim), v; rev=isrev(o), kwargs...) +_searchlast(o::Order, dim::Dimension{<:Val{Index}}, v; kwargs...) where Index = + searchsortedlast(Index, v; rev=isrev(o), kwargs...) +_searchfirst(o::Order, dim::Dimension, v; kwargs...) = + searchsortedfirst(val(dim), v; rev=isrev(o), kwargs...) +_searchfirst(o::Order, dim::Dimension{<:Val{Index}}, v; kwargs...) where Index = + searchsortedfirst(Index, v; rev=isrev(o), kwargs...) # Return an inbounds index +_inbounds(is::Tuple, dim::Dimension) = map(i -> _inbounds(i, dim), is) _inbounds(i::Int, dim::Dimension) = if i > lastindex(dim) lastindex(dim) @@ -501,3 +509,27 @@ _inbounds(i::Int, dim::Dimension) = _sorttuple(sel::Between) = _sorttuple(val(sel)) _sorttuple((a, b)) = a < b ? (a, b) : (b, a) + +_maybeflip(o::Forward, (a, b)) = (a, b) +_maybeflip(o::Reverse, (a, b)) = (b, a) + +_lt(::Locus) = (<) +_lt(::End) = (<=) +_gt(::Locus) = (>=) +_gt(::End) = (>) + +_locus_ineq(locus) = _lt(locus), _gt(locus) + +_locus_checkbounds(loc, (l, h), v) = + (_lt(loc)(v, l) || _gt(loc)(v, h)) && throw(BoundsError()) + +_prevind(::Forward, i) = i - 1 +_prevind(::Reverse, i) = i + 1 + +_dimlower(o::Forward, d) = firstindex(d) +_dimlower(o::Reverse, d) = lastindex(d) +_dimupper(o::Forward, d) = lastindex(d) +_dimupper(o::Reverse, d) = firstindex(d) + +_searchorder(::Forward) = _searchfirst +_searchorder(::Reverse) = _searchlast diff --git a/src/utils.jl b/src/utils.jl index 78f739b3d..b23ea3010 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,16 +1,57 @@ +""" + reversearray(A; dims) => AbstractDimArray + reversearray(dim::Dimension) => Dimension + +Reverse the array order, and update the dim to match. +""" +function reversearray end + +""" + reverseindex(A; dims) => AbstractDimArray + reverseindex(dim::Dimension) => Dimension + +Reverse the dimension index. +""" +function reverseindex end + +""" + fliparray(A; dims) => AbstractDimArray + fliparray(dim::Dimension) => Dimension + +`Flip` the array order without changing any data. +""" +function fliparray end + +""" + flipindex(A; dims) => AbstractDimArray + flipindex(dim::Dimension) => Dimension + +`Flip` the index order without changing any data. +""" +function flipindex end + +""" + fliprelation(A; dims) => AbstractDimArray + fliprelation(dim::Dimension) => Dimension + +`Flip` the relation between the dimension order and the array axis, +without actually changing any data. +""" +function fliprelation end for func in (:reversearray, :reverseindex, :fliparray, :flipindex, :fliprelation) if func != :reversearray @eval begin - ($func)(A::AbstractDimArray{T,N}; dims=1) where {T,N} = begin + ($func)(A::AbstractDimArray{T,N}; dims) where {T,N} = begin dnum = dimnum(A, dims) # Reverse the dimension. TODO: make this type stable newdims = $func(DimensionalData.dims(A), dnum) - rebuild(A, data(A), newdims) + rebuild(A, parent(A), newdims) end end end @eval begin + # TODO rewrite this it's awful and not type-stable @inline ($func)(dimstorev::Tuple, dnum) = begin dim = if length(dimstorev) == dnum ($func)(dimstorev[end]) @@ -29,7 +70,7 @@ reversearray(A::AbstractDimArray{T,N}; dims=1) where {T,N} = begin # Reverse the dimension. TODO: make this type stable newdims = reversearray(DimensionalData.dims(A), dnum) # Reverse the data - newdata = reverse(data(A); dims=dnum) + newdata = reverse(parent(A); dims=dnum) rebuild(A, newdata, newdims) end @inline reversearray(dim::Dimension) = @@ -53,8 +94,8 @@ end """ reorderindex(A, order::Union{Order,Dimension{<:Order},Tuple}) -Reorder index to `order`, or reorder index for the the given -dimension(s) to the `Order` they wrap. +Reorder every dims index to `order`, or reorder index for +the the given dimension(s) to the `Order` they wrap. `order` can be an [`Order`](@ref), a single [`Dimension`](@ref) or a `Tuple` of `Dimension`. @@ -64,8 +105,8 @@ function reorderindex end """ reorderarray(A, order::Union{Order,Dimension{<:Order},Tuple}) -Reorder array to `order`, or reorder array for the the given -dimension(s) to the `Order` they wrap. +Reorder the array to `order` for every axis, or reorder array +for the the given dimension(s) to the `Order` they wrap. `order` can be an [`Order`](@ref), a single [`Dimension`](@ref) or a `Tuple` of `Dimension`. @@ -75,8 +116,10 @@ function reorderarray end """ reorderrelation(A, order::Union{Order,Dimension{<:Order},Tuple}) -Reorder relation to `order`, or reorder relation for the the given -dimension(s) to the `Order` they wrap. +Reorder relation to `order` for every dimension, or reorder relation +for the the given dimension(s) to the `Order` they wrap. + +This will reverse the array, not the dimension index. `order` can be an [`Order`](@ref), a single [`Dimension`](@ref) or a `Tuple` of `Dimension`. @@ -85,13 +128,14 @@ function reorderrelation end for target in (:index, :array, :relation) - local order = Symbol(target, :order) reorder = Symbol(:reorder, target) - reverse = if target == :relation + if target == :relation # Revsersing the relation reverses the array, not the index - :reversearray + reverse = :reversearray + ord = relation else - Symbol(:reverse, target) + reverse = Symbol(:reverse, target) + ord = Symbol(target, :order) end @eval begin @@ -111,7 +155,7 @@ for target in (:index, :array, :relation) A end ($reorder)(A::AbstractDimArray, dim::DimOrDimType, order::Order) = - if order == ($order)(dims(A, dim)) + if order == ($ord)(dims(A, dim)) A else ($reverse)(A; dims=dim) @@ -122,28 +166,27 @@ end """ - modify(f, A::AbstractDimArray) + modify(f, A::AbstractDimArray) => AbstractDimArray -Modify the parent data, rebuilding the `AbstractDimArray` wrapper. -`f` must return a `AbstractArray` of the same size as the original. +Modify the parent data, rebuilding the `AbstractDimArray` wrapper without +change. `f` must return a `AbstractArray` of the same size as the original. """ modify(f, A::AbstractDimArray) = begin - newdata = f(data(A)) + newdata = f(parent(A)) size(newdata) == size(A) || error("$f returns an array with a different size") rebuild(A, newdata) end """ - dimwise!(f, A::AbstractDimArray, B::AbstractDimArray) + dimwise(f, A::AbstractDimArray{T,N}, B::AbstractDimArray{T2,M) => AbstractDimArray{T3,N} -Dimension-wise application of function `f`. +Dimension-wise application of function `f` to `A` and `B`. ## Arguments -`a`: `AbstractDimArray` to broacast from, along dimensions not in `b`. --`b`: `AbstractDimArray` to broadcast from all diensions. - Dimensions must be a subset of a. +-`b`: `AbstractDimArray` to broadcast from all diensions. Dimensions must be a subset of a. This is like broadcasting over every slice of `A` if it is sliced by the dimensions of `B`, and storing the value in `dest`. @@ -152,7 +195,7 @@ dimwise(f, A::AbstractDimArray, B::AbstractDimArray) = dimwise!(f, similar(A, promote_type(eltype(A), eltype(B))), A, B) """ - dimwise!(f, dest::AbstractDimArray, A::AbstractDimArray, B::AbstractDimArray) + dimwise!(f, dest::AbstractDimArray{T1,N}, A::AbstractDimArray{T2,N}, B::AbstractDimArray) => dest Dimension-wise application of function `f`. @@ -160,8 +203,7 @@ Dimension-wise application of function `f`. -`dest`: `AbstractDimArray` to update -`a`: `AbstractDimArray` to broacast from, along dimensions not in `b`. --`b`: `AbstractDimArray` to broadcast from all diensions. - Dimensions must be a subset of a. +-`b`: `AbstractDimArray` to broadcast from all diensions. Dimensions must be a subset of a. This is like broadcasting over every slice of `A` if it is sliced by the dimensions of `B`, and storing the value in `dest`. @@ -195,14 +237,14 @@ dimwise_generators(dims::Tuple) = begin end """ - basetypeof(x) + basetypeof(x) => Type -Get the base type of an object - the minimum required to +Get the "base" type of an object - the minimum required to define the object without it's fields. By default this is the full `UnionAll` for the type. But custom `basetypeof` methods can be defined for types with free type parameters. -In DimensionalData this is primariliy used for comparing dimensions, +In DimensionalData this is primariliy used for comparing `Dimension`s, where `Dim{:x}` is different from `Dim{:y}`. """ basetypeof(x) = basetypeof(typeof(x)) diff --git a/test/array.jl b/test/array.jl index 3cd153d03..482845064 100644 --- a/test/array.jl +++ b/test/array.jl @@ -38,7 +38,7 @@ end a = da[X(1), Y(1:2)] @test a == [1, 2] @test typeof(a) <: DimArray{Int,1} - @test typeof(data(a)) <: Array{Int,1} + @test typeof(parent(a)) <: Array{Int,1} @test dims(a) == (Y(LinRange(-38.0, -36.0, 2), Sampled(Ordered(), Regular(2.0), Points()), Dict(:meta => "Y")),) @test refdims(a) == @@ -51,7 +51,7 @@ end a = da[X(:), Y(:)] @test a == [1 2; 3 4] @test typeof(a) <: DimArray{Int,2} - @test typeof(data(a)) <: Array{Int,2} + @test typeof(parent(a)) <: Array{Int,2} @test typeof(dims(a)) <: Tuple{<:X,<:Y} @test dims(a) == (X(LinRange(143.0, 145.0, 2), Sampled(Ordered(), Regular(2.0), Points()), Dict(:meta => "X")), @@ -67,7 +67,7 @@ end v = @inferred view(da, Y(1), X(1)) @test v[] == 1 @test typeof(v) <: DimArray{Int,0} - @test typeof(data(v)) <:SubArray{Int,0} + @test typeof(parent(v)) <:SubArray{Int,0} @test typeof(dims(v)) == Tuple{} @test dims(v) == () @test refdims(v) == @@ -80,7 +80,7 @@ end v = @inferred view(da, Y(1), X(1:2)) @test v == [1, 3] @test typeof(v) <: DimArray{Int,1} - @test typeof(data(v)) <: SubArray{Int,1} + @test typeof(parent(v)) <: SubArray{Int,1} @test typeof(dims(v)) <: Tuple{<:X} @test dims(v) == (X(LinRange(143.0, 145.0, 2), @@ -94,7 +94,7 @@ end v = @inferred view(da, Y(1:2), X(1:1)) @test v == [1 2] @test typeof(v) <: DimArray{Int,2} - @test typeof(data(v)) <: SubArray{Int,2} + @test typeof(parent(v)) <: SubArray{Int,2} @test typeof(dims(v)) <: Tuple{<:X,<:Y} @test dims(v) == (X(LinRange(143.0, 143.0, 1), @@ -105,7 +105,7 @@ end v = @inferred view(da, Y(Base.OneTo(2)), X(1)) @test v == [1, 2] - @test typeof(data(v)) <: SubArray{Int,1} + @test typeof(parent(v)) <: SubArray{Int,1} @test typeof(dims(v)) <: Tuple{<:Y} @test dims(v) == (Y(LinRange(-38.0, -36.0, 2), @@ -115,6 +115,9 @@ end @test bounds(v) == ((-38.0, -36.0),) end +@testset "" begin +end + a2 = [1 2 3 4 3 4 5 6 4 5 6 7] @@ -134,8 +137,37 @@ da2 = DimArray(a2, dimz2, "test2"; refdims=refdimz) @testset "arbitrary dimension names also work for indexing" begin @test da2[Dim{:row}(2)] == [3, 4, 5, 6] @test da2[Dim{:column}(4)] == [4, 6, 7] + @test da2[column=4] == [4, 6, 7] @test da2[Dim{:column}(1), Dim{:row}(3)] == 4 - @inferred getindex(da2, Dim{:column}(1), Dim{:row}(3)) + @test da2[column=1, Dim{:row}(3)] == 4 + @test da2[Dim{:column}(1), row=3] == 4 + @test da2[column=1, row=3] == 4 + @test view(da2, column=1, row=3) == fill(4) + @test view(da2, column=1, Dim{:row}(1)) == fill(1) + da2_set = deepcopy(da2) + da2_set[Dim{:column}(1), Dim{:row}(1)] = 99 + @test da2_set[1, 1] == 99 + da2_set[column=2, row=2] = 88 + @test da2_set[2, 2] == 88 + da2_set[Dim{:row}(3), column=3] = 77 + @test da2_set[3, 3] == 77 + + # We can also construct without using `Dim{X}` + @test dims(DimArray(a2, (:a, :b))) == dims(DimArray(a2, (Dim{:a}, Dim{:b}))) + + # Inrerence + @inferred getindex(da2, column=1, row=3) + @inferred view(da2, column=1, row=3) + @inferred setindex!(da2_set, 77, Dim{:row}(1), column=2) + # With a large type + + if VERSION >= v"1.5" + da4 = DimArray(zeros(1, 2, 3, 4, 5, 6, 7, 8), (:a, :b, :c, :d, :d, :f, :g, :h)) + @inferred getindex(da2, a=1, b=2, c=3, d=4, e=5) + # Type inference breaks with 6 arguments. + # @inferred getindex(da2, a=1, b=2, c=3, d=4, e=5, f=6) + # @code_warntype getindex(da2, a=1, b=2, c=3, d=4, e=5, f=6) + end end @testset "size and axes" begin @@ -146,6 +178,35 @@ end @inferred axes(da2, Dim{:column}) end +@testset "copy and friends" begin + rebuild(da2, copy(parent(da2))) + + dac = copy(da2) + @test dac == da2 + @test dims(dac) == dims(da2) + @test refdims(dac) == refdims(da2) == (Ti(1:1),) + @test name(dac) == name(da2) == "test2" + @test metadata(dac) == metadata(da2) + dadc = deepcopy(da2) + @test dadc == da2 + @test dims(dadc) == dims(da2) + @test refdims(dadc) == refdims(da2) == (Ti(1:1),) + @test name(dadc) == name(da2) == "test2" + @test metadata(dadc) == metadata(da2) + + o = one(da) + @test o == [1 0; 0 1] + @test dims(o) == dims(da) + + ou = oneunit(da) + @test ou == [1 0; 0 1] + @test dims(ou) == dims(da) + + z = zero(da) + @test z == [0 0; 0 0] + @test dims(z) == dims(da) +end + @testset "OffsetArray" begin oa = OffsetArray(a2, -1:1, 5:8) @testset "Regular dimensions don't work: axes must match" begin @@ -179,9 +240,14 @@ end @test dims(da_float) == dims(da2) @test refdims(da_float) == refdims(da2) + # Changing the axis size removes dims. da_size_float = similar(da2, Float64, (10, 10)) @test eltype(da_size_float) == Float64 @test size(da_size_float) == (10, 10) + @test typeof(da_size_float) <: Array{Float64,2} + da_size_float_splat = similar(da2, Float64, 10, 10) + @test size(da_size_float_splat) == (10, 10) + @test typeof(da_size_float_splat) <: Array{Float64,2} sda = DimArray(sprand(Float64, 10, 10, .5), (X, Y)) sparse_size_int = similar(sda, Int64, (5, 5)) @@ -214,10 +280,10 @@ end @testset "eachindex" begin # Should have linear index da = DimArray(ones(5, 2, 4), (Y(10:2:18), Ti(10:11), X(1:4))) - @test eachindex(da) == eachindex(data(da)) + @test eachindex(da) == eachindex(parent(da)) # Should have cartesian index sda = DimArray(sprand(10, 10, .1), (Y(1:10), X(1:10))) - @test eachindex(sda) == eachindex(data(sda)) + @test eachindex(sda) == eachindex(parent(sda)) end @testset "convert" begin @@ -226,23 +292,6 @@ end @test ac == a2 end -@testset "copy" begin - rebuild(da2, copy(data(da2))) - - dac = copy(da2) - @test dac == da2 - @test dims(dac) == dims(da2) - @test refdims(dac) == refdims(da2) == (Ti(1:1),) - @test name(dac) == name(da2) == "test2" - @test metadata(dac) == metadata(da2) - dadc = deepcopy(da2) - @test dadc == da2 - @test dims(dadc) == dims(da2) - @test refdims(dadc) == refdims(da2) == (Ti(1:1),) - @test name(dadc) == name(da2) == "test2" - @test metadata(dadc) == metadata(da2) -end - if VERSION > v"1.1-" dimz = (X(LinRange(143.0, 145.0, 3); mode=Sampled(order=Ordered()), metadata=Dict(:meta => "X")), Y(LinRange(-38.0, -36.0, 4); mode=Sampled(order=Ordered()), metadata=Dict(:meta => "Y"))) @@ -258,12 +307,33 @@ if VERSION > v"1.1-" @test db == da2 copy!(dc, a2) @test db == a2 + + @testset "vector copy! (ambiguity fix)" begin + v = zeros(3) + dv = DimArray(zeros(3), X) + copy!(v, DimArray([1.0, 2.0, 3.0], X)) + @test v == [1.0, 2.0, 3.0] + copy!(dv, DimArray([9.9, 9.9, 9.9], X)) + @test dv == [9.9, 9.9, 9.9] + copy!(dv, [5.0, 5.0, 5.0]) + @test dv == [5.0, 5.0, 5.0] + end + end end @testset "constructor" begin da = DimArray(rand(5, 4), (X, Y)) @test_throws DimensionMismatch DimArray(1:5, X(1:6)) - @test_throws MethodError DimArray(1:5, (X(1:5), Y(1:2))) + @test_throws DimensionMismatch DimArray(1:5, (X(1:5), Y(1:2))) end + +@testset "fill constructor" begin + da = fill(5.0, (X(4), Y(40.0:10.0:80.0))) + @test parent(da) == fill(5.0, (4, 5)) + @test dims(da) == + (X(Base.OneTo(4), NoIndex(), nothing), + Y(40.0:10.0:80.0, Sampled(Ordered(), Regular(10.0), Points()), nothing)) + @test_throws ErrorException fill(5.0, (X(:e), Y(8))) +end diff --git a/test/broadcast.jl b/test/broadcast.jl index e68c3f8e7..b12ca69c0 100644 --- a/test/broadcast.jl +++ b/test/broadcast.jl @@ -14,7 +14,7 @@ using DimensionalData, Test end @testset "in place" begin - @test data(da .= 1 .* da .+ 7) == 8 * ones(3) + @test parent(da .= 1 .* da .+ 7) == 8 * ones(3) @test dims(da .= 1 .* da .+ 7) == dims(da) end @@ -61,8 +61,8 @@ using DimensionalData, Test a = DimArray(reshape(1:12, (4, 3)), (X, Y)) b = DimArray(1:3, Y) @test_throws DimensionMismatch a .* b - @test_throws DimensionMismatch data(a) .* data(b) - @test data(a) .* data(b)' == data(a .* b') + @test_throws DimensionMismatch parent(a) .* parent(b) + @test parent(a) .* parent(b)' == parent(a .* b') @test dims(a .* b') == dims(a) end diff --git a/test/dimension.jl b/test/dimension.jl index 12eac1829..18b7a5e56 100644 --- a/test/dimension.jl +++ b/test/dimension.jl @@ -1,9 +1,12 @@ using DimensionalData, Test, Unitful -using DimensionalData: Forward, slicedims, basetypeof +using DimensionalData: Forward, slicedims, basetypeof, formatdims @dim TestDim "Test dimension" @testset "dims creation macro" begin + @test TestDim(1:10, Sampled()) == TestDim(1:10, Sampled(), nothing) + @test TestDim(1:10; mode=Categorical()) == TestDim(1:10, Categorical(), nothing) + @test name(TestDim) == "Test dimension" @test label(TestDim) == "Test dimension" @test shortname(TestDim) == "TestDim" @@ -33,45 +36,97 @@ using DimensionalData: Forward, slicedims, basetypeof @test iterate(TestDim(10:20)) == iterate(10:20) end -# Basic dim and array initialisation -a = ones(5, 4) -# Must construct with a tuple for dims/refdims +@testset "formatdims" begin + A = [1 2 3; 4 5 6] + @test formatdims(A, (X, Y)) == (X(Base.OneTo(2), NoIndex(), nothing), + Y(Base.OneTo(3), NoIndex(), nothing)) + @test formatdims(zeros(3), Ti) == (Ti(Base.OneTo(3), NoIndex(), nothing),) + @test formatdims(A, (:a, :b)) == (Dim{:a}(Base.OneTo(2), NoIndex(), nothing), + Dim{:b}(Base.OneTo(3), NoIndex(), nothing)) + @test formatdims(51:100, :c) == (Dim{:c}(Base.OneTo(50), NoIndex(), nothing),) + @test formatdims(A, (a=[:A, :B], b=(10.0:10.0:30.0))) == + (Dim{:a}([:A, :B], Categorical(Unordered()), nothing), + Dim{:b}(10.0:10:30.0, Sampled(Ordered(), Regular(10.0), Points()), nothing)) + @test formatdims(A, (X([:A, :B]; metadata=5), + Y(10.0:10.0:30.0, Categorical(Ordered()), Dict("metadata"=>1)))) == + (X([:A, :B], Categorical(Unordered()), 5), + Y(10.0:10:30.0, Categorical(Ordered()), Dict("metadata"=>1))) + @test formatdims(zeros(3, 4), + (Dim{:row}(Val((:A, :B, :C))), + Dim{:column}(Val((-20, -10, 0, 10)), Sampled(), nothing))) == + (Dim{:row}(Val((:A, :B, :C)), Categorical(), nothing), + Dim{:column}(Val((-20, -10, 0, 10)), Sampled(Ordered(),Irregular(),Points()), nothing)) +end -@test_throws MethodError DimArray(a, X((140, 148))) -@test_throws MethodError DimArray(a, (X((140, 148)), Y((2, 11))), Z(1)) -da = DimArray(a, (X((140, 148)), Y((2, 11)))) +@testset "Basic dim and array initialisation" begin + a = ones(5, 4) -dimz = dims(da) -@test d = typeof(slicedims(dimz, (2:4, 3))) == - typeof(((X(LinRange(142,146,3); mode=Sampled(order=Ordered(), span=Regular(2.0))),), - (Y(8.0, mode=Sampled(order=Ordered(), span=Regular(3.0))),))) -@test name(dimz) == ("X", "Y") -@test shortname(dimz) == ("X", "Y") -@test units(dimz) == (nothing, nothing) -@test label(dimz) == ("X, Y") + @test_throws DimensionMismatch DimArray(a, X) + @test_throws DimensionMismatch DimArray(a, (X, Y, Z)) + + da = DimArray(a, (X(LinRange(140, 148, 5)), Y(LinRange(2, 11, 4)))) + dimz = dims(da) + + @test val(dimz) == index(dimz) == (LinRange(140, 148, 5), LinRange(2, 11, 4)) + @test name(dimz) == ("X", "Y") + @test shortname(dimz) == ("X", "Y") + @test units(dimz) == (nothing, nothing) + @test label(dimz) == ("X", "Y") + @test sampling(dimz) == (Points(), Points()) + @test span(dimz) == (Regular(2.0), Regular(3.0)) + @test locus(dimz) == (Center(), Center()) + @test order(dimz) == (Ordered(), Ordered()) + @test arrayorder(dimz) == (Forward(), Forward()) + @test indexorder(dimz) == (Forward(), Forward()) + @test relation(dimz) == (Forward(), Forward()) + @test bounds(dimz) == ((140, 148), (2, 11)) + + @test slicedims(dimz, (2:4, 3)) == + ((X(LinRange(142,146,3); mode=Sampled(order=Ordered(), span=Regular(2.0))),), + (Y(8.0, mode=Sampled(order=Ordered(), span=Regular(3.0))),)) +end a = [1 2 3 4 2 3 4 5 3 4 5 6] da = DimArray(a, (X((143, 145)), Y((-38, -35)))) -dimz = dims(da) +dimz = dims(da) @testset "dims" begin @test dims(dimz) === dimz @test dims(dimz, X) === dimz[1] @test dims(dimz, Y) === dimz[2] @test_throws ArgumentError dims(dimz, Ti) - @test typeof(dims(da)) == + @test typeof(dims(da)) == Tuple{X{LinRange{Float64},Sampled{Ordered{Forward,Forward,Forward},Regular{Float64},Points},Nothing}, Y{LinRange{Float64},Sampled{Ordered{Forward,Forward,Forward},Regular{Float64},Points},Nothing}} end -@testset "arbitrary dim names" begin - dimz = (Dim{:row}((10, 30)), Dim{:column}((-20, 10))) - @test name(dimz) == ("Dim row", "Dim column") +@testset "arbitrary dim names and Val index" begin + dimz = formatdims(zeros(3, 4), + (Dim{:row}(Val((:A, :B, :C))), + Dim{:column}(Val((-20, -10, 0, 10)), Sampled(Ordered(),Regular(10),Points()), nothing)) + ) + @test name(dimz) == ("Dim{:row}", "Dim{:column}") @test shortname(dimz) == ("row", "column") - @test label(dimz) == ("Dim row, Dim column") + @test label(dimz) == ("Dim{:row}", "Dim{:column}") @test basetypeof(dimz[1]) == Dim{:row} + @test length.(dimz) == (3, 4) + @test firstindex(dimz[1]) == 1 + @test lastindex(dimz[1]) == 3 + @test ndims(dimz[1]) == 1 + @test Array(dimz[1]) == [:A, :B, :C] + + @testset "specify dim with Symbol" begin + @test_throws ArgumentError arrayorder(dimz, :x) + # TODO Does this make sense? + @test arrayorder(dimz, :row) == Unordered() + @test arrayorder(dimz, :column) == Forward() + @test bounds(dimz, :row) == (nothing, nothing) + @test bounds(dimz, :column) == (-20, 10) + @test index(dimz, :row) == (:A, :B, :C) + @test index(dimz, :column) == (-20, -10, 0, 10) + end end @testset "repeating dims of the same type is allowed" begin @@ -80,7 +135,7 @@ end @test dims(dimz, (X, X)) === dimz @test dimnum(dimz, (X, X)) === (1, 2) @test hasdim(dimz, (X, X)) === (true, true) - @test permutedims(dimz, (X, X)) === dimz + @test sortdims(dimz, (X, X)) === dimz end @testset "applying function on a dimension" begin diff --git a/test/methods.jl b/test/methods.jl index 8b7cab244..f78fd006a 100644 --- a/test/methods.jl +++ b/test/methods.jl @@ -8,7 +8,7 @@ using DimensionalData: Forward, Reverse, Rot90, Rot180, Rot270, Rot360, rotdims, a = [1 2; 3 4] dimz = (X((143, 145)), Y((-38, -36))) da = DimArray(a, dimz) - @test map(x -> 2x, da) == [2 4; 6 8] + @test @inferred map(x -> 2x, da) == [2 4; 6 8] @test map(x -> 2x, da) isa DimArray{Int64,2} end @@ -16,55 +16,66 @@ end a = [1 2; 3 4] dimz = X((143, 145); mode=Sampled()), Y((-38, -36); mode=Sampled()) da = DimArray(a, dimz) - @test sum(da; dims=X()) == sum(a; dims=1) - @test sum(da; dims=Y()) == sum(a; dims=2) + + @test @inferred sum(da; dims=X()) == sum(a; dims=1) + @test @inferred sum(da; dims=Y()) == sum(a; dims=2) @test dims(sum(da; dims=Y())) == (X(LinRange(143.0, 145.0, 2), Sampled(Ordered(), Regular(2.0), Points()), nothing), Y([-37.0], Sampled(Ordered(), Regular(4.0), Points()), nothing)) - @test prod(da; dims=X) == [3 8] - @test prod(da; dims=2) == [2 12]' + + @test @inferred prod(da; dims=X) == [3 8] + @test @inferred prod(da; dims=2) == [2 12]' resultdimz = (X([144.0], Sampled(Ordered(), Regular(4.0), Points()), nothing), Y(LinRange(-38.0, -36.0, 2), Sampled(Ordered(), Regular(2.0), Points()), nothing)) @test typeof(dims(prod(da; dims=X()))) == typeof(resultdimz) - @test bounds(dims(prod(da; dims=X()))) == bounds(resultdimz) - @test maximum(x -> 2x, da; dims=X) == [6 8] - @test maximum(x -> 2x, da; dims=2) == [4 8]' - @test maximum(da; dims=X) == [3 4] - @test maximum(da; dims=2) == [2 4]' - @test minimum(da; dims=1) == [1 2] - @test minimum(da; dims=Y()) == [1 3]' + @test @inferred bounds(dims(prod(da; dims=X()))) == bounds(resultdimz) + + @test @inferred maximum(x -> 2x, da; dims=X) == [6 8] + @test @inferred maximum(x -> 2x, da; dims=2) == [4 8]' + @test @inferred maximum(da; dims=X) == [3 4] + @test @inferred maximum(da; dims=2) == [2 4]' + + @test @inferred minimum(da; dims=1) == [1 2] + @test @inferred minimum(da; dims=Y()) == [1 3]' @test dims(minimum(da; dims=X())) == (X([144.0], Sampled(Ordered(), Regular(4.0), Points()), nothing), Y(LinRange(-38.0, -36.0, 2), Sampled(Ordered(), Regular(2.0), Points()), nothing)) + @test mean(da; dims=1) == [2.0 3.0] @test mean(da; dims=Y()) == [1.5 3.5]' @test dims(mean(da; dims=Y())) == (X(LinRange(143.0, 145.0, 2), Sampled(Ordered(), Regular(2.0), Points()), nothing), Y([-37.0], Sampled(Ordered(), Regular(4.0), Points()), nothing)) + @test mapreduce(x -> x > 3, +, da; dims=X) == [0 1] @test mapreduce(x -> x > 3, +, da; dims=2) == [0 1]' @test dims(mapreduce(x-> x > 3, +, da; dims=Y())) == (X(LinRange(143.0, 145.0, 2), Sampled(Ordered(), Regular(2.0), Points()), nothing), Y([-37.0], Sampled(Ordered(), Regular(4.0), Points()), nothing)) + @test reduce(+, da) == reduce(+, a) @test reduce(+, da; dims=X) == [4 6] @test reduce(+, da; dims=Y()) == [3 7]' @test dims(reduce(+, da; dims=Y())) == (X(LinRange(143.0, 145.0, 2), Sampled(Ordered(), Regular(2.0), Points()), nothing), Y([-37.0], Sampled(Ordered(), Regular(4.0), Points()), nothing)) + @test std(da) === std(a) @test std(da; dims=1) == [1.4142135623730951 1.4142135623730951] @test std(da; dims=Y()) == [0.7071067811865476 0.7071067811865476]' + @test var(da; dims=1) == [2.0 2.0] @test var(da; dims=Y()) == [0.5 0.5]' + @test dims(var(da; dims=Y())) == + (X(LinRange(143.0, 145.0, 2), Sampled(Ordered(), Regular(2.0), Points()), nothing), + Y([-37.0], Sampled(Ordered(), Regular(4.0), Points()), nothing)) + if VERSION > v"1.1-" @test extrema(da; dims=Y) == permutedims([(1, 2) (3, 4)]) @test extrema(da; dims=X) == [(1, 3) (2, 4)] end - @test dims(var(da; dims=Y())) == - (X(LinRange(143.0, 145.0, 2), Sampled(Ordered(), Regular(2.0), Points()), nothing), - Y([-37.0], Sampled(Ordered(), Regular(4.0), Points()), nothing)) + a = [1 2 3; 4 5 6] da = DimArray(a, dimz) @test median(da) == 3.5 @@ -88,20 +99,20 @@ end end if VERSION > v"1.1-" - @testset "iteration methods" begin + @testset "eachslice" begin a = [1 2 3 4 3 4 5 6 5 6 7 8] - # eachslice da = DimArray(a, (Y((10, 30)), Ti(1:4))) @test [mean(s) for s in eachslice(da; dims=Ti)] == [3.0, 4.0, 5.0, 6.0] @test [mean(s) for s in eachslice(da; dims=2)] == [3.0, 4.0, 5.0, 6.0] + slices = [s .* 2 for s in eachslice(da; dims=Y)] - @test map(sin, da) == map(sin, a) @test slices[1] == [2, 4, 6, 8] @test slices[2] == [6, 8, 10, 12] @test slices[3] == [10, 12, 14, 16] dims(slices[1]) == (Ti(1.0:1.0:4.0),) + slices = [s .* 2 for s in eachslice(da; dims=Ti)] @test slices[1] == [2, 6, 10] dims(slices[1]) == (Y(10.0:10.0:30.0),) @@ -113,7 +124,7 @@ end da = DimArray(zeros(5, 4), (Y((10, 20); mode=Sampled()), X(1:4; mode=Sampled()))) tda = transpose(da) - @test tda == transpose(data(da)) + @test tda == transpose(parent(da)) resultdims = (X(1:4; mode=Sampled(Ordered(), Regular(1), Points())), Y(LinRange(10.0, 20.0, 5); mode=Sampled(Ordered(), Regular(2.5), Points()))) @test typeof(dims(tda)) == typeof(resultdims) @@ -121,20 +132,20 @@ end @test size(tda) == (4, 5) tda = Transpose(da) - @test tda == Transpose(data(da)) + @test tda == Transpose(parent(da)) @test dims(tda) == (X(1:4; mode=Sampled(Ordered(), Regular(1), Points())), Y(LinRange(10.0, 20.0, 5); mode=Sampled(Ordered(), Regular(2.5), Points()))) @test size(tda) == (4, 5) @test typeof(tda) <: DimArray ada = adjoint(da) - @test ada == adjoint(data(da)) + @test ada == adjoint(parent(da)) @test dims(ada) == (X(1:4; mode=Sampled(Ordered(), Regular(1), Points())), Y(LinRange(10.0, 20.0, 5); mode=Sampled(Ordered(), Regular(2.5), Points()))) @test size(ada) == (4, 5) dsp = permutedims(da) - @test permutedims(data(da)) == data(dsp) + @test permutedims(parent(da)) == parent(dsp) @test dims(dsp) == reverse(dims(da)) end @@ -144,16 +155,16 @@ end Ti(10:11; mode=Sampled()), X(1:4; mode=Sampled()))) dsp = permutedims(da, [3, 1, 2]) - @test permutedims(da, [X, Y, Ti]) == permutedims(da, (X, Y, Ti)) @test permutedims(da, [X(), Y(), Ti()]) == permutedims(da, (X(), Y(), Ti())) dsp = permutedims(da, (X(), Y(), Ti())) - @test dsp == permutedims(data(da), (3, 1, 2)) + @test dsp == permutedims(parent(da), (3, 1, 2)) @test dims(dsp) == (X(1:4; mode=Sampled(Ordered(), Regular(1), Points())), Y(LinRange(10.0, 20.0, 5); mode=Sampled(Ordered(), Regular(2.5), Points())), Ti(10:11; mode=Sampled(Ordered(), Regular(1), Points()))) + dsp = PermutedDimsArray(da, (3, 1, 2)) - @test dsp == PermutedDimsArray(data(da), (3, 1, 2)) + @test dsp == PermutedDimsArray(parent(da), (3, 1, 2)) @test typeof(dsp) <: DimArray end @@ -235,7 +246,7 @@ end Ti(1:4; mode=Sampled(Ordered(), Regular(1), Intervals(Start()))))) @test refdims(ms) == () ms = mapslices(sum, da; dims=Ti) - @test data(ms) == [10 18 26]' + @test parent(ms) == [10 18 26]' end @testset "array info" begin @@ -255,6 +266,7 @@ end da = DimArray(a, (X(1:2), Y(1:3))) b = [7 8 9; 10 11 12] db = DimArray(b, (X(3:4), Y(1:3))) + @test cat(da, db; dims=X()) == [1 2 3; 4 5 6; 7 8 9; 10 11 12] testdims = (X([1, 2, 3, 4]; mode=Sampled(Ordered(), Regular(1), Points())), Y(1:3; mode=Sampled(Ordered(), Regular(1), Points()))) @@ -277,9 +289,3 @@ end @test unique(da; dims=X()) == [1 1 6] @test unique(da; dims=Y) == [1 6; 1 6] end - -# These need fixes in base. kwargs are ::Integer so we can't add methods -# or dispatch on AbstractDimension in underscore _methods -# accumulate -# cumsum -# cumprod diff --git a/test/mode.jl b/test/mode.jl index 137d0c9b7..2692dac10 100644 --- a/test/mode.jl +++ b/test/mode.jl @@ -1,7 +1,7 @@ using DimensionalData, Test, Unitful using DimensionalData: Forward, Reverse, reversearray, reverseindex, slicebounds, slicemode, identify, - indexorder, arrayorder, relationorder + indexorder, arrayorder, relation @testset "identify IndexMode" begin @testset "identify Categorical from Auto" begin @@ -34,6 +34,7 @@ using DimensionalData: Forward, Reverse, @test identify(Auto(), X, 10:-2:1) == Sampled(Ordered(Reverse(), Forward(), Forward()), Regular(-2), Points()) end + end @testset "identify Sampled" begin @@ -68,10 +69,10 @@ end @testset "order" begin @test indexorder(Ordered()) == Forward() @test arrayorder(Ordered()) == Forward() - @test relationorder(Ordered()) == Forward() + @test relation(Ordered()) == Forward() @test indexorder(Unordered()) == Unordered() @test arrayorder(Unordered()) == Unordered() - @test relationorder(Unordered()) == Forward() + @test relation(Unordered()) == Forward() end @testset "reverse" begin @@ -205,7 +206,7 @@ end last(dim), first(dim) @test bounds(dim) == (10, 15) dim = X(index; mode=Sampled(order=Unordered(), sampling=Points())) - @test_throws ErrorException bounds(dim) + @test bounds(dim) == (nothing, nothing) end @testset "Categorical" begin @@ -215,7 +216,7 @@ end dim = X(index; mode=Categorical(; order=Ordered(;index=Reverse()))) @test bounds(dim) == (:d, :a) dim = X(index; mode=Categorical(; order=Unordered())) - @test_throws ErrorException bounds(dim) + @test bounds(dim) == (nothing, nothing) end end diff --git a/test/plotrecipes.jl b/test/plotrecipes.jl index 944ecf7f4..a2f818b03 100644 --- a/test/plotrecipes.jl +++ b/test/plotrecipes.jl @@ -44,7 +44,7 @@ histogram(da2) stephist(da2) barhist(da2) scatterhist(da2) -histogram2d(data(da2)) +histogram2d(parent(da2)) histogram2d(da2) hline(da2) vline(da2) @@ -60,25 +60,27 @@ boxplot(da2) violin(da2) ea_histogram(da2) +nothing + # Not sure how recipes work for this # andrewsplot(da2) # TODO handle everything -# These don't seem to work for plot(data(da2)) +# These don't seem to work for plot(parent(da2)) # path3d(da2) -# hexbin(data(da1)) +# hexbin(parent(da1)) # plot(da2; seriestype=:histogram3d) # Crashes GR -# groupedbar(data(da2)) +# groupedbar(parent(da2)) # surface(da2) # plot(da2; seriestype=:bins2d) # plot(da2; seriestype=:volume) # plot(da2; seriestype=:stepbins) -# plot(data(da2); seriestype=:barbins) -# plot(data(da2); seriestype=:contour3d) +# plot(parent(da2); seriestype=:barbins) +# plot(parent(da2); seriestype=:contour3d) # pie(da2) # # Crashes GR for some reason diff --git a/test/primitives.jl b/test/primitives.jl index 742627f0e..3ca1f6934 100644 --- a/test/primitives.jl +++ b/test/primitives.jl @@ -1,21 +1,28 @@ using DimensionalData, Test -using DimensionalData: val, basetypeof, slicedims, dims2indices, formatdims, mode, +using DimensionalData: val, basetypeof, slicedims, dims2indices, mode, @dim, reducedims, XDim, YDim, ZDim, Forward, commondims -dimz = (X(), Y()) - -@testset "permutedims" begin - @test permutedims((Y(1:2), X(1)), dimz) == (X(1), Y(1:2)) - @test permutedims((X(1),), dimz) == (X(1), nothing) - @test permutedims((Y(), X()), dimz) == (X(:), Y(:)) - @test permutedims([Y(), X()], dimz) == (X(:), Y(:)) - @test permutedims((Y, X), dimz) == (X, Y) - @test permutedims([Y, X], dimz) == (X, Y) - @test permutedims(dimz, (Y(), X())) == (Y(:), X(:)) - @test permutedims(dimz, [Y(), X()]) == (Y(:), X(:)) - @test permutedims(dimz, (Y, X) ) == (Y(:), X(:)) - @test permutedims(dimz, [Y, X] ) == (Y(:), X(:)) + +@testset "sortdims" begin + dimz = (X(), Y(), Z(), Ti()) + @test @inferred sortdims((Y(1:2), X(1)), dimz) == (X(1), Y(1:2), nothing, nothing) + @test @inferred sortdims((Ti(1),), dimz) == (nothing, nothing, nothing, Ti(1)) + @test @inferred sortdims((Y, X, Z, Ti), dimz) == (X(), Y(), Z(), Ti()) + @test @inferred sortdims((Y(), X(), Z(), Ti()), dimz) == (X(), Y(), Z(), Ti()) + @test @inferred sortdims([Y(), X(), Z(), Ti()], dimz) == (X(), Y(), Z(), Ti()) + @test @inferred sortdims((Z(), Y(), X()), dimz) == (X(), Y(), Z(), nothing) + @test @inferred sortdims(dimz, (Y(), Z())) == (Y(), Z()) + @test @inferred sortdims(dimz, [Ti(), X(), Z()]) == (Ti(), X(), Z()) + @test @inferred sortdims(dimz, (Y, Ti) ) == (Y(), Ti()) + @test @inferred sortdims(dimz, [Ti, Z, X, Y] ) == (Ti(), Z(), X(), Y()) + # Transformed + @test @inferred sortdims((Y(1:2; mode=Transformed(identity, Z())), X(1)), (X(), Z())) == + (X(1), Y(1:2; mode=Transformed(identity, Z()))) + # Abstract + @test @inferred sortdims((Z(), Y(), X()), (XDim, TimeDim)) == (X(), nothing) + # Repeating + @test @inferred sortdims((Z(:a), Y(), Z(:b)), (YDim, Z(), ZDim)) == (Y(), Z(:a), Z(:b)) end a = [1 2 3; 4 5 6] @@ -58,10 +65,9 @@ end @testset "dims2indices" begin emptyval = Colon() - @test DimensionalData._dims2indices(mode(dimz[1]), dimz[1], Y, Nothing) == Colon() + @test DimensionalData._dims2indices(dimz[1], Y, Nothing) == Colon() @test dims2indices(dimz, (Y(),), emptyval) == (Colon(), Colon()) @test dims2indices(dimz, (Y(1),), emptyval) == (Colon(), 1) - # Time is just ignored if it's not in dims. Should this be an error? @test dims2indices(dimz, (Ti(4), X(2))) == (2, Colon()) @test dims2indices(dimz, (Y(2), X(3:7)), emptyval) == (3:7, 2) @test dims2indices(dimz, (X(2), Y([1, 3, 4])), emptyval) == (2, [1, 3, 4]) @@ -89,6 +95,11 @@ end @test dimnum(da, Y()) == 2 @test dimnum(da, (Y, X())) == (2, 1) @test_throws ArgumentError dimnum(da, Ti) == (2, 1) + + @testset "with Dim{X} and symbols" begin + @test dimnum((Dim{:a}(), Dim{:b}()), :a) == 1 + @test dimnum((Dim{:a}(), Y(), Dim{:b}()), (:b, :a, Y())) == (3, 1, 2) + end end @testset "reducedims" begin @@ -136,13 +147,27 @@ end @test_throws ArgumentError dims(da, Ti) x = dims(da, X) @test dims(x) == x + + @testset "with Dim{X} and symbols" begin + A = DimArray(zeros(4, 5), (:one, :two)) + @test dims(A) == + (Dim{:one}(Base.OneTo(4), NoIndex(), nothing), + Dim{:two}(Base.OneTo(5), NoIndex(), nothing)) + @test dims(A, :two) == Dim{:two}(Base.OneTo(5), NoIndex(), nothing) + end end @testset "commondims" begin - commondims(da, X) == (dims(da, X),) + @test commondims(da, X) == (dims(da, X),) # Dims are always in the base order - commondims(da, (X, Y)) == dims(da, (X, Y)) - commondims(da, (Y, X)) == dims(da, (X, Y)) + @test commondims(da, (X, Y)) == dims(da, (X, Y)) + @test commondims(da, (Y, X)) == dims(da, (X, Y)) + @test basetypeof(commondims(da, DimArray(zeros(5), Y))[1]) <: Y + + @testset "with Dim{X} and symbols" begin + @test commondims((Dim{:a}(), Dim{:b}()), (:a, :c)) == (Dim{:a}(),) + @test commondims((Dim{:a}(), Y(), Dim{:b}()), (Y, :b, :z)) == (Y(), Dim{:b}()) + end end @testset "hasdim" begin @@ -151,13 +176,20 @@ end @test hasdim(dims(da), Y) == true @test hasdim(dims(da), (X, Y)) == (true, true) @test hasdim(dims(da), (X, Ti)) == (true, false) - # Abstract - @test hasdim(dims(da), (XDim, YDim)) == (true, true) - # TODO : should this actually be (true, false) ? - # Do we remove the second one for hasdim as well? - @test hasdim(dims(da), (XDim, XDim)) == (true, true) - @test hasdim(dims(da), (ZDim, YDim)) == (false, true) - @test hasdim(dims(da), (ZDim, ZDim)) == (false, false) + + @testset "hasdim for Abstract types" begin + @test hasdim(dims(da), (XDim, YDim)) == (true, true) + # TODO : should this actually be (true, false) ? + # Do we remove the second one for hasdim as well? + @test hasdim(dims(da), (XDim, XDim)) == (true, true) + @test hasdim(dims(da), (ZDim, YDim)) == (false, true) + @test hasdim(dims(da), (ZDim, ZDim)) == (false, false) + end + + @testset "with Dim{X} and symbols" begin + @test hasdim((Dim{:a}(), Dim{:b}()), (:a, :c)) == (true, false) + @test hasdim((Dim{:a}(), Dim{:b}()), (:b, :a, :c)) == (true, true, false) + end end @testset "otherdims" begin @@ -167,6 +199,11 @@ end @test otherdims(A, Z) == dims(A, (X, Y)) @test otherdims(A, (X, Z)) == dims(A, (Y,)) @test otherdims(A, Ti) == dims(A, (X, Y, Z)) + + @testset "with Dim{X} and symbols" begin + @test otherdims((Z(), Dim{:a}(), Y(), Dim{:b}()), (:b, :a, Y)) == (Z(),) + @test otherdims((Dim{:a}(), Dim{:b}(), Ti()), (:a, :c)) == (Dim{:b}(), Ti()) + end end @testset "setdims" begin diff --git a/test/selector.jl b/test/selector.jl index f711a54bd..51ffebf31 100644 --- a/test/selector.jl +++ b/test/selector.jl @@ -1,6 +1,6 @@ using DimensionalData, Test, Unitful, Combinatorics using DimensionalData: Forward, Reverse, Ordered, - arrayorder, indexorder, relationorder, between, at, near, contains + arrayorder, indexorder, relation, between, at, near, contains a = [1 2 3 4 5 6 7 8 @@ -13,209 +13,213 @@ A = DimArray([1 2 3; 4 5 6], dims_) @testset "selector primitives" begin - @testset "Regular Intervals IndexMode with range" begin + @testset "Regular Intervals with range" begin # Order: index, array, relation (array order is irrelevent here, it's just for plotting) - startfwdfwd = Ti(5.0:30.0; mode=Sampled(Ordered(Forward(),Forward(),Forward()), Regular(1), Intervals(Start()))) - startfwdrev = Ti(5.0:30.0; mode=Sampled(Ordered(Forward(),Forward(),Reverse()), Regular(1), Intervals(Start()))) - startrevfwd = Ti(30.0:-1.0:5.0; mode=Sampled(Ordered(Reverse(),Forward(),Forward()), Regular(-1), Intervals(Start()))) - startrevrev = Ti(30.0:-1.0:5.0; mode=Sampled(Ordered(Reverse(),Forward(),Reverse()), Regular(-1), Intervals(Start()))) + # Varnames: locusindexorderrelation + + startfwdfwd = Ti(11.0:30.0; mode=Sampled(Ordered(Forward(),Forward(),Forward()), Regular(1), Intervals(Start()))) + startfwdrev = Ti(11.0:30.0; mode=Sampled(Ordered(Forward(),Forward(),Reverse()), Regular(1), Intervals(Start()))) + startrevfwd = Ti(30.0:-1.0:11.0; mode=Sampled(Ordered(Reverse(),Forward(),Forward()), Regular(-1), Intervals(Start()))) + startrevrev = Ti(30.0:-1.0:11.0; mode=Sampled(Ordered(Reverse(),Forward(),Reverse()), Regular(-1), Intervals(Start()))) @testset "Any at" begin - @test at(startfwdfwd, At(30)) == 26 + @test at(startfwdfwd, At(30)) == 20 @test at(startrevfwd, At(30)) == 1 @test at(startfwdrev, At(30)) == 1 - @test at(startrevrev, At(30)) == 26 + @test at(startrevrev, At(30)) == 20 end @testset "Start between" begin - @test between(startfwdfwd, Between(9.9, 15.1)) === 6:10 - @test between(startfwdrev, Between(9.9, 15.1)) === 17:1:21 - @test between(startrevfwd, Between(9.9, 15.1)) === 17:21 - @test between(startrevrev, Between(9.9, 15.1)) === 6:1:10 - @test between(startfwdfwd, Between(10, 15)) === 6:10 - @test between(startfwdrev, Between(10, 15)) === 17:1:21 - @test between(startrevfwd, Between(10, 15)) === 17:21 - @test between(startrevrev, Between(10, 15)) === 6:1:10 + @test between(startfwdfwd, Between(11, 14)) === 1:3 + @test between(startfwdrev, Between(11, 14)) === 18:1:20 + @test between(startrevfwd, Between(11, 14)) === 18:20 + @test between(startrevrev, Between(11, 14)) === 1:1:3 + @test between(startfwdfwd, Between(11.1, 13.9)) === 2:2 + @test between(startfwdrev, Between(11.1, 13.9)) === 19:1:19 + @test between(startrevfwd, Between(11.1, 13.9)) === 19:19 + @test between(startrevrev, Between(11.1, 13.9)) === 2:1:2 # Input order doesn't matter - @test between(startfwdfwd, Between(15, 10)) === 6:10 + @test between(startfwdfwd, Between(14, 11)) === 1:3 end @testset "Start contains" begin - @test_throws BoundsError contains(startfwdfwd, Contains(4.9)) + @test_throws BoundsError contains(startfwdfwd, Contains(10.9)) @test_throws BoundsError contains(startfwdfwd, Contains(31)) - @test_throws BoundsError contains(startrevfwd, Contains(4.9)) + @test_throws BoundsError contains(startrevfwd, Contains(10.9)) @test_throws BoundsError contains(startrevfwd, Contains(31)) - @test contains(startfwdfwd, Contains(5)) == 1 - @test contains(startfwdfwd, Contains(5.9)) == 1 - @test contains(startfwdfwd, Contains(6.0)) == 2 - @test contains(startfwdfwd, Contains(30.0)) == 26 - @test contains(startfwdfwd, Contains(29.9)) == 25 - @test contains(startrevfwd, Contains(5.9)) == 26 - @test contains(startrevfwd, Contains(6.0)) == 25 + @test contains(startfwdfwd, Contains(11)) == 1 + @test contains(startfwdfwd, Contains(11.9)) == 1 + @test contains(startfwdfwd, Contains(12.0)) == 2 + @test contains(startfwdfwd, Contains(30.0)) == 20 + @test contains(startfwdfwd, Contains(29.9)) == 19 + @test contains(startrevfwd, Contains(11.9)) == 20 + @test contains(startrevfwd, Contains(12.0)) == 19 @test contains(startrevfwd, Contains(30.9)) == 1 @test contains(startrevfwd, Contains(30.0)) == 1 @test contains(startrevfwd, Contains(29.0)) == 2 - @test contains(startfwdrev, Contains(5.9)) == 26 - @test contains(startfwdrev, Contains(6.0)) == 25 + @test contains(startfwdrev, Contains(11.9)) == 20 + @test contains(startfwdrev, Contains(12.0)) == 19 @test contains(startfwdrev, Contains(30.0)) == 1 @test contains(startfwdrev, Contains(29.9)) == 2 - @test contains(startrevrev, Contains(5.9)) == 1 - @test contains(startrevrev, Contains(6.0)) == 2 - @test contains(startrevrev, Contains(29.9)) == 25 - @test contains(startrevrev, Contains(30.0)) == 26 + @test contains(startrevrev, Contains(11.9)) == 1 + @test contains(startrevrev, Contains(12.0)) == 2 + @test contains(startrevrev, Contains(29.9)) == 19 + @test contains(startrevrev, Contains(30.0)) == 20 end @testset "Start near" begin @test bounds(startfwdfwd) == bounds(startfwdrev) == bounds(startrevrev) == bounds(startrevfwd) - @test near(startfwdfwd, Near(50)) == 26 - @test near(startfwdfwd, Near(0)) == 1 - @test near(startfwdfwd, Near(5.9)) == 1 - @test near(startfwdfwd, Near(6.0)) == 2 - @test near(startfwdfwd, Near(30.0)) == 26 - @test near(startfwdfwd, Near(29.9)) == 25 - @test near(startfwdrev, Near(5.9)) == 26 - @test near(startfwdrev, Near(6.0)) == 25 + @test near(startfwdfwd, Near(-100)) == 1 + @test near(startfwdfwd, Near(11.9)) == 1 + @test near(startfwdfwd, Near(12.0)) == 2 + @test near(startfwdfwd, Near(30.0)) == 20 + @test near(startfwdfwd, Near(29.9)) == 19 + @test near(startfwdrev, Near(11.9)) == 20 + @test near(startfwdrev, Near(12.0)) == 19 @test near(startfwdrev, Near(29.9)) == 2 @test near(startfwdrev, Near(30.0)) == 1 - @test near(startrevfwd, Near(5.9)) == 26 - @test near(startrevfwd, Near(6.0)) == 25 + @test near(startrevfwd, Near(11.9)) == 20 + @test near(startrevfwd, Near(12.0)) == 19 @test near(startrevfwd, Near(29.0)) == 2 @test near(startrevfwd, Near(30.0)) == 1 - @test near(startrevrev, Near(5.9)) == 1 - @test near(startrevrev, Near(6.0)) == 2 - @test near(startrevrev, Near(29.9)) == 25 - @test near(startrevrev, Near(30.0)) == 26 + @test near(startrevrev, Near(11.9)) == 1 + @test near(startrevrev, Near(12.0)) == 2 + @test near(startrevrev, Near(29.9)) == 19 + @test near(startrevrev, Near(30.0)) == 20 + @test near(startfwdfwd, Near(100)) == 20 end - centerfwdfwd = Ti((5.0:30.0); mode=Sampled(Ordered(Forward(),Forward(),Forward()), Regular(1), Intervals(Center()))) - centerfwdrev = Ti((5.0:30.0); mode=Sampled(Ordered(Forward(),Forward(),Reverse()), Regular(1), Intervals(Center()))) - centerrevfwd = Ti((30.0:-1.0:5.0); mode=Sampled(Ordered(Reverse(),Forward(),Forward()), Regular(-1), Intervals(Center()))) - centerrevrev = Ti((30.0:-1.0:5.0); mode=Sampled(Ordered(Reverse(),Forward(),Reverse()), Regular(-1), Intervals(Center()))) + centerfwdfwd = Ti((11.0:30.0); mode=Sampled(Ordered(Forward(),Forward(),Forward()), Regular(1), Intervals(Center()))) + centerfwdrev = Ti((11.0:30.0); mode=Sampled(Ordered(Forward(),Forward(),Reverse()), Regular(1), Intervals(Center()))) + centerrevfwd = Ti((30.0:-1.0:11.0); mode=Sampled(Ordered(Reverse(),Forward(),Forward()), Regular(-1), Intervals(Center()))) + centerrevrev = Ti((30.0:-1.0:11.0); mode=Sampled(Ordered(Reverse(),Forward(),Reverse()), Regular(-1), Intervals(Center()))) @testset "Center between" begin - @test between(centerfwdfwd, Between(9.9, 15.1)) === 7:10 - @test between(centerfwdrev, Between(9.9, 15.1)) === 17:1:20 - @test between(centerrevfwd, Between(9.9, 15.1)) === 17:20 - @test between(centerrevrev, Between(9.9, 15.1)) === 7:1:10 - @test between(centerfwdfwd, Between(10, 15)) === 7:10 - @test between(centerfwdrev, Between(10, 15)) === 17:1:20 - @test between(centerrevfwd, Between(10, 15)) === 17:20 - @test between(centerrevrev, Between(10, 15)) === 7:1:10 + @test between(centerfwdfwd, Between(10.5, 14.6)) === 1:4 + @test between(centerfwdrev, Between(10.5, 14.6)) === 17:1:20 + @test between(centerrevfwd, Between(10.5, 14.6)) === 17:20 + @test between(centerrevrev, Between(10.5, 14.6)) === 1:1:4 + @test between(centerfwdfwd, Between(10.6, 14.4)) === 2:3 + @test between(centerfwdrev, Between(10.6, 14.4)) === 18:1:19 + @test between(centerrevfwd, Between(10.6, 14.4)) === 18:19 + @test between(centerrevrev, Between(10.6, 14.4)) === 2:1:3 # Input order doesn't matter - @test between(centerfwdfwd, Between(15, 10)) === 7:10 + @test between(centerfwdfwd, Between(15, 10)) === 1:4 end @testset "Center contains" begin - @test_throws BoundsError contains(centerfwdfwd, Contains(4.4)) + @test_throws BoundsError contains(centerfwdfwd, Contains(10.4)) @test_throws BoundsError contains(centerfwdfwd, Contains(30.5)) - @test_throws BoundsError contains(centerrevfwd, Contains(4.4)) + @test_throws BoundsError contains(centerrevfwd, Contains(10.4)) @test_throws BoundsError contains(centerrevfwd, Contains(30.5)) - @test contains(centerfwdfwd, Contains(4.5)) == 1 - @test contains(centerfwdfwd, Contains(30.4)) == 26 - @test contains(centerfwdfwd, Contains(29.5)) == 26 - @test contains(centerfwdfwd, Contains(29.4)) == 25 - @test contains(centerrevfwd, Contains(4.5)) == 26 + @test contains(centerfwdfwd, Contains(10.5)) == 1 + @test contains(centerfwdfwd, Contains(30.4)) == 20 + @test contains(centerfwdfwd, Contains(29.5)) == 20 + @test contains(centerfwdfwd, Contains(29.4)) == 19 + @test contains(centerrevfwd, Contains(10.5)) == 20 @test contains(centerrevfwd, Contains(30.4)) == 1 @test contains(centerrevfwd, Contains(29.5)) == 1 @test contains(centerrevfwd, Contains(29.4)) == 2 @test contains(centerfwdrev, Contains(29.5)) == 1 @test contains(centerfwdrev, Contains(29.4)) == 2 - @test contains(centerrevrev, Contains(29.5)) == 26 - @test contains(centerrevrev, Contains(29.4)) == 25 + @test contains(centerrevrev, Contains(29.5)) == 20 + @test contains(centerrevrev, Contains(29.4)) == 19 end @testset "Center near" begin - @test near(centerfwdfwd, Near(4.4)) == 1 - @test near(centerfwdfwd, Near(30.5)) == 26 - @test near(centerrevfwd, Near(4.4)) == 26 + @test near(centerfwdfwd, Near(10.4)) == 1 + @test near(centerfwdfwd, Near(30.5)) == 20 + @test near(centerrevfwd, Near(10.4)) == 20 @test near(centerrevfwd, Near(30.5)) == 1 - @test near(centerfwdfwd, Near(4.5)) == 1 - @test near(centerfwdfwd, Near(30.4)) == 26 - @test near(centerfwdfwd, Near(29.5)) == 26 - @test near(centerfwdfwd, Near(29.4)) == 25 - @test near(centerrevfwd, Near(4.5)) == 26 + @test near(centerfwdfwd, Near(10.5)) == 1 + @test near(centerfwdfwd, Near(30.4)) == 20 + @test near(centerfwdfwd, Near(29.5)) == 20 + @test near(centerfwdfwd, Near(29.4)) == 19 + @test near(centerrevfwd, Near(10.5)) == 20 @test near(centerrevfwd, Near(30.4)) == 1 @test near(centerrevfwd, Near(29.5)) == 1 @test near(centerrevfwd, Near(29.4)) == 2 @test near(centerfwdrev, Near(29.5)) == 1 @test near(centerfwdrev, Near(29.4)) == 2 - @test near(centerrevrev, Near(29.5)) == 26 - @test near(centerrevrev, Near(29.4)) == 25 + @test near(centerrevrev, Near(29.5)) == 20 + @test near(centerrevrev, Near(29.4)) == 19 end - endfwdfwd = Ti((5.0:30.0); mode=Sampled(Ordered(Forward(),Forward(),Forward()), Regular(1), Intervals(End()))) - endfwdrev = Ti((5.0:30.0); mode=Sampled(Ordered(Forward(),Forward(),Reverse()), Regular(1), Intervals(End()))) - endrevfwd = Ti((30.0:-1.0:5.0); mode=Sampled(Ordered(Reverse(),Forward(),Forward()), Regular(-1), Intervals(End()))) - endrevrev = Ti((30.0:-1.0:5.0); mode=Sampled(Ordered(Reverse(),Forward(),Reverse()), Regular(-1), Intervals(End()))) + endfwdfwd = Ti((11.0:30.0); mode=Sampled(Ordered(Forward(),Forward(),Forward()), Regular(1), Intervals(End()))) + endfwdrev = Ti((11.0:30.0); mode=Sampled(Ordered(Forward(),Forward(),Reverse()), Regular(1), Intervals(End()))) + endrevfwd = Ti((30.0:-1.0:11.0); mode=Sampled(Ordered(Reverse(),Forward(),Forward()), Regular(-1), Intervals(End()))) + endrevrev = Ti((30.0:-1.0:11.0); mode=Sampled(Ordered(Reverse(),Forward(),Reverse()), Regular(-1), Intervals(End()))) @testset "End between" begin - @test between(endfwdfwd, Between(9.9, 15.1)) === 7:11 - @test between(endfwdrev, Between(9.9, 15.1)) === 16:1:20 - @test between(endrevfwd, Between(9.9, 15.1)) === 16:20 - @test between(endrevrev, Between(9.9, 15.1)) === 7:1:11 - @test between(endfwdfwd, Between(10, 15)) === 7:11 + @test between(endfwdfwd, Between(10.1, 14.9)) === 2:4 + @test between(endfwdrev, Between(10.1, 14.9)) === 17:1:19 + @test between(endrevfwd, Between(10.1, 14.9)) === 17:19 + @test between(endrevrev, Between(10.1, 14.9)) === 2:1:4 + @test between(endfwdfwd, Between(10, 15)) === 1:5 @test between(endfwdrev, Between(10, 15)) === 16:1:20 @test between(endrevfwd, Between(10, 15)) === 16:20 - @test between(endrevrev, Between(10, 15)) === 7:1:11 + @test between(endrevrev, Between(10, 15)) === 1:1:5 # Input order doesn't matter - @test between(endfwdfwd, Between(15, 10)) === 7:11 + @test between(endfwdfwd, Between(15, 10)) === 1:5 end @testset "End contains" begin - @test_throws BoundsError contains(endfwdfwd, Contains(4)) + @test_throws BoundsError contains(endfwdfwd, Contains(10)) @test_throws BoundsError contains(endfwdfwd, Contains(30.1)) - @test_throws BoundsError contains(endrevfwd, Contains(4)) + @test_throws BoundsError contains(endrevfwd, Contains(10)) @test_throws BoundsError contains(endrevfwd, Contains(30.1)) - @test contains(endfwdfwd, Contains(4.1)) == 1 - @test contains(endfwdfwd, Contains(5.0)) == 1 - @test contains(endfwdfwd, Contains(5.1)) == 2 - @test contains(endfwdfwd, Contains(29.0)) == 25 - @test contains(endfwdfwd, Contains(29.1)) == 26 - @test contains(endfwdfwd, Contains(30.0)) == 26 - @test contains(endrevfwd, Contains(4.1)) == 26 - @test contains(endrevfwd, Contains(5.0)) == 26 - @test contains(endrevfwd, Contains(5.1)) == 25 + @test contains(endfwdfwd, Contains(10.1)) == 1 + @test contains(endfwdfwd, Contains(11.0)) == 1 + @test contains(endfwdfwd, Contains(11.1)) == 2 + @test contains(endfwdfwd, Contains(29.0)) == 19 + @test contains(endfwdfwd, Contains(29.1)) == 20 + @test contains(endfwdfwd, Contains(30.0)) == 20 + @test contains(endrevfwd, Contains(10.1)) == 20 + @test contains(endrevfwd, Contains(11.0)) == 20 + @test contains(endrevfwd, Contains(11.1)) == 19 @test contains(endrevfwd, Contains(29.0)) == 2 @test contains(endrevfwd, Contains(29.1)) == 1 @test contains(endrevfwd, Contains(30.0)) == 1 - @test contains(endrevrev, Contains(5.0)) == 1 - @test contains(endrevrev, Contains(5.1)) == 2 - @test contains(endrevrev, Contains(29.0)) == 25 - @test contains(endrevrev, Contains(29.1)) == 26 - @test contains(endrevfwd, Contains(5.0)) == 26 - @test contains(endrevfwd, Contains(5.1)) == 25 + @test contains(endrevrev, Contains(11.0)) == 1 + @test contains(endrevrev, Contains(11.1)) == 2 + @test contains(endrevrev, Contains(29.0)) == 19 + @test contains(endrevrev, Contains(29.1)) == 20 + @test contains(endrevfwd, Contains(11.0)) == 20 + @test contains(endrevfwd, Contains(11.1)) == 19 @test contains(endrevfwd, Contains(29.0)) == 2 @test contains(endrevfwd, Contains(29.1)) == 1 end @testset "End near" begin - @test near(endfwdfwd, Near(4)) == 1 - @test near(endfwdfwd, Near(5.0)) == 1 - @test near(endfwdfwd, Near(5.1)) == 2 - @test near(endfwdfwd, Near(29.0)) == 25 - @test near(endfwdfwd, Near(29.1)) == 26 - @test near(endfwdfwd, Near(30.0)) == 26 - @test near(endfwdfwd, Near(31.1)) == 26 - @test near(endrevfwd, Near(4)) == 26 - @test near(endrevfwd, Near(5.0)) == 26 - @test near(endrevfwd, Near(5.1)) == 25 + @test near(endfwdfwd, Near(10)) == 1 + @test near(endfwdfwd, Near(11.0)) == 1 + @test near(endfwdfwd, Near(11.1)) == 2 + @test near(endfwdfwd, Near(29.0)) == 19 + @test near(endfwdfwd, Near(29.1)) == 20 + @test near(endfwdfwd, Near(30.0)) == 20 + @test near(endfwdfwd, Near(31.1)) == 20 + @test near(endrevfwd, Near(10)) == 20 + @test near(endrevfwd, Near(11.0)) == 20 + @test near(endrevfwd, Near(11.1)) == 19 @test near(endrevfwd, Near(29.0)) == 2 @test near(endrevfwd, Near(29.1)) == 1 @test near(endrevfwd, Near(30.0)) == 1 @test near(endrevfwd, Near(31.1)) == 1 - @test near(endrevrev, Near(5.0)) == 1 - @test near(endrevrev, Near(5.1)) == 2 - @test near(endrevrev, Near(29.0)) == 25 - @test near(endrevrev, Near(29.1)) == 26 - @test near(endrevfwd, Near(5.0)) == 26 - @test near(endrevfwd, Near(5.1)) == 25 + @test near(endrevrev, Near(11.0)) == 1 + @test near(endrevrev, Near(11.1)) == 2 + @test near(endrevrev, Near(29.0)) == 19 + @test near(endrevrev, Near(29.1)) == 20 + @test near(endrevfwd, Near(11.0)) == 20 + @test near(endrevfwd, Near(11.1)) == 19 @test near(endrevfwd, Near(29.0)) == 2 @test near(endrevfwd, Near(29.1)) == 1 + @test near(endfwdfwd, Near(-100)) == 1 + @test near(endfwdfwd, Near(100)) == 20 end end - @testset "RegulaSpan Intervals mode with array" begin - # Order: index, array, relation (array order is irrelevent here, it's just for plotting) + + @testset "Regular Intervals with array" begin startfwd = Ti([1, 3, 4, 5]; mode=Sampled(Ordered(index=Forward()), Regular(1), Intervals(Start()))) startrev = Ti([5, 4, 3, 1]; mode=Sampled(Ordered(index=Reverse()), Regular(-1), Intervals(Start()))) @test_throws BoundsError contains(startfwd, Contains(0.9)) @@ -226,7 +230,6 @@ A = DimArray([1 2 3; 4 5 6], dims_) @test contains(startfwd, Contains(3)) == 2 @test contains(startfwd, Contains(5.9)) == 4 @test_throws BoundsError contains(startfwd, Contains(6)) - @test_throws BoundsError contains(startrev, Contains(0.9)) @test contains(startrev, Contains(1.0)) == 4 @test contains(startrev, Contains(1.9)) == 4 @@ -235,9 +238,182 @@ A = DimArray([1 2 3; 4 5 6], dims_) @test contains(startrev, Contains(3)) == 3 @test contains(startrev, Contains(5.9)) == 1 @test_throws BoundsError contains(startrev, Contains(6)) - end + @testset "Irregular Intervals with array" begin + # Order: index, array, relation (array order is irrelevent here, it's just for plotting) + # Varnames: locusindexorderrelation + args = Irregular(1.0, 121.0), Intervals(Start()) + startfwdfwd = Ti((1:10).^2; mode=Sampled(Ordered(Forward(),Forward(),Forward()), args...)) + startfwdrev = Ti((1:10).^2; mode=Sampled(Ordered(Forward(),Forward(),Reverse()), args...)) + startrevfwd = Ti((10:-1:1).^2; mode=Sampled(Ordered(Reverse(),Forward(),Forward()), args...)) + startrevrev = Ti((10:-1:1).^2; mode=Sampled(Ordered(Reverse(),Forward(),Reverse()), args...)) + + @testset "Any at" begin + @test at(startfwdfwd, At(25)) == 5 + @test at(startfwdrev, At(25)) == 6 + @test at(startrevfwd, At(25)) == 6 + @test at(startrevrev, At(25)) == 5 + @test at(startfwdfwd, At(100)) == 10 + @test at(startfwdrev, At(100)) == 1 + @test at(startrevfwd, At(100)) == 1 + @test at(startrevrev, At(100)) == 10 + end + + @testset "Start between" begin + @test between(startfwdfwd, Between(9, 36)) === 3:5 + @test between(startfwdrev, Between(9, 36)) === 6:1:8 + @test between(startrevfwd, Between(9, 36)) === 6:8 + @test between(startrevrev, Between(9, 36)) === 3:1:5 + @test between(startfwdfwd, Between(9.1, 35.0)) === 4:4 + @test between(startfwdrev, Between(9.1, 35.9)) === 7:1:7 + @test between(startrevfwd, Between(9.1, 35.9)) === 7:7 + @test between(startrevrev, Between(9.1, 35.9)) === 4:1:4 + # Input order doesn't matter + @test between(startfwdfwd, Between(36, 9)) === 3:5 + # Handle searchorted overflow + @test between(startfwdfwd, Between(-100, 9)) === 1:2 + @test between(startfwdfwd, Between(80, 150)) === 9:10 + @test between(startfwdrev, Between(-100, 9)) === 9:1:10 + @test between(startfwdrev, Between(80, 150)) === 1:1:2 + @test between(startrevfwd, Between(-100, 9)) === 9:10 + @test between(startrevfwd, Between(80, 150)) === 1:2 + @test between(startrevrev, Between(-100, 9)) === 1:1:2 + @test between(startrevrev, Between(80, 150)) === 9:1:10 + end + + @testset "Start contains" begin + @test_throws BoundsError contains(startfwdfwd, Contains(0.9)) + @test_throws BoundsError contains(startfwdfwd, Contains(121.1)) + @test_throws BoundsError contains(startfwdrev, Contains(0.9)) + @test_throws BoundsError contains(startfwdrev, Contains(121.1)) + @test contains(startfwdfwd, Contains(1)) == 1 + @test contains(startfwdfwd, Contains(3.9)) == 1 + @test contains(startfwdfwd, Contains(4.0)) == 2 + @test contains(startfwdfwd, Contains(100.0)) == 10 + @test contains(startfwdfwd, Contains(99.9)) == 9 + @test contains(startfwdrev, Contains(3.9)) == 10 + @test contains(startfwdrev, Contains(4.0)) == 9 + @test contains(startfwdrev, Contains(100.0)) == 1 + @test contains(startfwdrev, Contains(99.9)) == 2 + @test_throws BoundsError contains(startrevrev, Contains(0.9)) + @test_throws BoundsError contains(startrevrev, Contains(121.1)) + @test_throws BoundsError contains(startrevfwd, Contains(0.9)) + @test_throws BoundsError contains(startrevfwd, Contains(121.1)) + @test contains(startrevfwd, Contains(3.9)) == 10 + @test contains(startrevfwd, Contains(4.0)) == 9 + @test contains(startrevfwd, Contains(120.9)) == 1 + @test contains(startrevfwd, Contains(100.0)) == 1 + @test contains(startrevfwd, Contains(99.0)) == 2 + @test contains(startrevrev, Contains(3.9)) == 1 + @test contains(startrevrev, Contains(4.0)) == 2 + @test contains(startrevrev, Contains(100.0)) == 10 + @test contains(startrevrev, Contains(99.9)) == 9 + end + + args = Irregular(0.5, 111.5), Intervals(Center()) + centerfwdfwd =Ti((1.0:10.0).^2; mode=Sampled(Ordered(Forward(),Forward(),Forward()), args...)) + centerfwdrev =Ti((1.0:10.0).^2; mode=Sampled(Ordered(Forward(),Forward(),Reverse()), args...)) + centerrevfwd =Ti((10.0:-1.0:1.0).^2; mode=Sampled(Ordered(Reverse(),Forward(),Forward()), args...)) + centerrevrev =Ti((10.0:-1.0:1.0).^2; mode=Sampled(Ordered(Reverse(),Forward(),Reverse()), args...)) + + @testset "Center between" begin + @test between(centerfwdfwd, Between(6.5, 30.5)) === 3:5 + @test between(centerfwdfwd, Between(6.6, 30.4)) === 4:4 + @test between(centerfwdrev, Between(6.5, 30.5)) === 6:1:8 + @test between(centerfwdrev, Between(6.6, 30.4)) === 7:1:7 + @test between(centerrevfwd, Between(6.5, 30.5)) === 6:8 + @test between(centerrevfwd, Between(6.6, 30.4)) === 7:7 + @test between(centerrevrev, Between(6.5, 30.5)) === 3:1:5 + @test between(centerrevrev, Between(6.6, 30.4)) === 4:1:4 + # Input order doesn't matter + @test between(centerfwdfwd, Between(30.5, 6.5)) === 3:5 + end + + @testset "Center contains" begin + @test_throws BoundsError contains(centerfwdfwd, Contains(0.4)) + @test_throws BoundsError contains(centerfwdfwd, Contains(111.5)) + @test contains(centerfwdfwd, Contains(0.5)) == 1 + @test contains(centerfwdfwd, Contains(111.4)) == 10 + @test contains(centerfwdfwd, Contains(90.5)) == 10 + @test contains(centerfwdfwd, Contains(90.4)) == 9 + @test contains(centerfwdrev, Contains(90.6)) == 1 + @test contains(centerfwdrev, Contains(90.5)) == 1 + @test contains(centerfwdrev, Contains(90.4)) == 2 + @test contains(centerfwdrev, Contains(72.5)) == 2 + @test contains(centerfwdrev, Contains(72.4)) == 3 + @test_throws BoundsError contains(centerrevfwd, Contains(0.4)) + @test_throws BoundsError contains(centerrevfwd, Contains(111.5)) + @test contains(centerrevfwd, Contains(72.5)) == 2 + @test contains(centerrevfwd, Contains(72.4)) == 3 + @test contains(centerrevfwd, Contains(0.5)) == 10 + @test contains(centerrevfwd, Contains(111.4)) == 1 + @test contains(centerrevfwd, Contains(90.5)) == 1 + @test contains(centerrevfwd, Contains(90.4)) == 2 + + @test contains(centerrevrev, Contains(90.5)) == 10 + @test contains(centerrevrev, Contains(90.4)) == 9 + end + + # @testset "Center near" begi + + args = Irregular(0.0, 100.0), Intervals(End()) + endfwdfwd = Ti((1.0:10.0).^2; mode=Sampled(Ordered(Forward(),Forward(),Forward()), args...)) + endfwdrev = Ti((1.0:10.0).^2; mode=Sampled(Ordered(Forward(),Forward(),Reverse()), args...)) + endrevfwd = Ti((10.0:-1.0:1.0).^2; mode=Sampled(Ordered(Reverse(),Forward(),Forward()), args...)) + endrevrev = Ti((10.0:-1.0:1.0).^2; mode=Sampled(Ordered(Reverse(),Forward(),Reverse()), args...)) + + @testset "End between" begin + @test between(endfwdfwd, Between(4, 25)) === 3:5 + @test between(endfwdrev, Between(4, 25)) === 6:1:8 + @test between(endrevfwd, Between(4, 25)) === 6:8 + @test between(endrevrev, Between(4, 25)) === 3:1:5 + @test between(endfwdfwd, Between(4.1, 24.9)) === 4:4 + @test between(endfwdrev, Between(4.1, 24.9)) === 7:1:7 + @test between(endrevfwd, Between(4.1, 24.9)) === 7:7 + @test between(endrevrev, Between(4.1, 24.9)) === 4:1:4 + # Input order doesn't matter + @test between(endfwdfwd, Between(25, 4)) === 3:5 + # Handle searchorted overflow + @test between(endfwdfwd, Between(-100, 4)) === 1:2 + @test between(endfwdfwd, Between(-100, 4)) === 1:2 + @test between(endfwdfwd, Between(64, 150)) === 9:10 + @test between(endfwdrev, Between(-100, 4)) === 9:1:10 + @test between(endfwdrev, Between(64, 150)) === 1:1:2 + @test between(endrevfwd, Between(-100, 4)) === 9:10 + @test between(endrevfwd, Between(64, 150)) === 1:2 + @test between(endrevrev, Between(-100, 4)) === 1:1:2 + @test between(endrevrev, Between(64, 150)) === 9:1:10 + end + + @testset "End contains" begin + @test_throws BoundsError contains(endfwdfwd, Contains(-0.1)) + @test_throws BoundsError contains(endfwdfwd, Contains(100.1)) + @test_throws BoundsError contains(endrevfwd, Contains(-0.1)) + @test_throws BoundsError contains(endrevfwd, Contains(100.1)) + @test contains(endfwdfwd, Contains(0.1)) == 1 + @test contains(endfwdfwd, Contains(1.0)) == 1 + @test contains(endfwdfwd, Contains(1.1)) == 2 + @test contains(endfwdfwd, Contains(81.0)) == 9 + @test contains(endfwdfwd, Contains(81.1)) == 10 + @test contains(endfwdfwd, Contains(100.0)) == 10 + @test contains(endrevfwd, Contains(0.1)) == 10 + @test contains(endrevfwd, Contains(1.0)) == 10 + @test contains(endrevfwd, Contains(1.1)) == 9 + @test contains(endrevfwd, Contains(81.0)) == 2 + @test contains(endrevfwd, Contains(81.1)) == 1 + @test contains(endrevfwd, Contains(100.0)) == 1 + @test contains(endrevrev, Contains(1.0)) == 1 + @test contains(endrevrev, Contains(1.1)) == 2 + @test contains(endrevrev, Contains(81.0)) == 9 + @test contains(endrevrev, Contains(81.1)) == 10 + @test contains(endrevfwd, Contains(1.0)) == 10 + @test contains(endrevfwd, Contains(1.1)) == 9 + @test contains(endrevfwd, Contains(81.0)) == 2 + @test contains(endrevfwd, Contains(81.1)) == 1 + end + + end @testset "Points mode" begin @@ -275,6 +451,10 @@ A = DimArray([1 2 3; 4 5 6], dims_) @test near(revrev, Near(30.1)) == 26 end + @testset "near" begin + @test_throws ArgumentError contains(fwdfwd, Contains(50)) + end + end end @@ -285,31 +465,31 @@ end Ti((1:4)u"s"; mode=Sampled()))) @test At(10.0) == At(10.0, nothing, nothing) - @test At(10.0; atol=0.0, rtol=Base.rtoldefault(Float64)) == + @test At(10.0; atol=0.0, rtol=Base.rtoldefault(Float64)) == At(10.0, 0.0, Base.rtoldefault(Float64)) Near([10, 20]) @test Between(10, 20) == Between((10, 20)) @testset "selectors with dim wrappers" begin - @test da[Y(At([10, 30])), Ti(At([1u"s", 4u"s"]))] == [1 4; 9 12] + @test @inferred da[Y(At([10, 30])), Ti(At([1u"s", 4u"s"]))] == [1 4; 9 12] @test_throws ArgumentError da[Y(At([9, 30])), Ti(At([1u"s", 4u"s"]))] - @test view(da, Y(At(20)), Ti(At((3:4)u"s"))) == [7, 8] - @test view(da, Y(Near(17)), Ti(Near([1.5u"s", 3.1u"s"]))) == [6, 7] - @test view(da, Y(Between(9, 21)), Ti(At((3:4)u"s"))) == [3 4; 7 8] + @test @inferred view(da, Y(At(20)), Ti(At((3:4)u"s"))) == [7, 8] + @test @inferred view(da, Y(Near(17)), Ti(Near([1.5u"s", 3.1u"s"]))) == [6, 7] + @test @inferred view(da, Y(Between(9, 21)), Ti(At((3:4)u"s"))) == [3 4; 7 8] end @testset "selectors without dim wrappers" begin - @test da[At(20:10:30), At(1u"s")] == [5, 9] - @test view(da, Between(9, 31), Near((3:4)u"s")) == [3 4; 7 8; 11 12] - @test view(da, Near(22), At([3.0u"s", 4.0u"s"])) == [7, 8] - @test view(da, At(20), At((2:3)u"s")) == [6, 7] - @test view(da, Near(13), Near([1.3u"s", 3.3u"s"])) == [1, 3] + @test @inferred da[At(20:10:30), At(1u"s")] == [5, 9] + @test @inferred view(da, Between(9, 31), Near((3:4)u"s")) == [3 4; 7 8; 11 12] + @test @inferred view(da, Near(22), At([3.0u"s", 4.0u"s"])) == [7, 8] + @test @inferred view(da, At(20), At((2:3)u"s")) == [6, 7] + @test @inferred view(da, Near(13), Near([1.3u"s", 3.3u"s"])) == [1, 3] # Near works with a tuple input - @test view(da, Near([13]), Near([1.3u"s", 3.3u"s"])) == [1 3] - @test view(da, Between(11, 20), At((2:3)u"s")) == [6 7] + @test @inferred view(da, Near([13]), Near([1.3u"s", 3.3u"s"])) == [1 3] + @test @inferred view(da, Between(11, 20), At((2:3)u"s")) == [6 7] # Between also accepts a tuple input - @test view(da, Between((11, 20)), Between((2u"s", 3u"s"))) == [6 7] + @test @inferred view(da, Between((11, 20)), Between((2u"s", 3u"s"))) == [6 7] end @testset "mixed selectors and standard" begin @@ -354,10 +534,10 @@ end ] for idx in indices from2d = da[idx] - @test from2d == data(da)[idx] + @test from2d == parent(da)[idx] @test !(from2d isa AbstractDimArray) - from1d = da[Y <| At(10)][idx] - @test from1d == data(da)[1, :][idx] + from1d = da[Y(At(10))][idx] + @test from1d == parent(da)[1, :][idx] @test from1d isa AbstractDimArray end end @@ -371,10 +551,10 @@ end ] for idx in indices from2d = view(da, idx) - @test from2d == view(data(da), idx) + @test from2d == view(parent(da), idx) @test from2d isa SubArray from1d = view(da[Y(At(10))], idx) - @test from1d == view(data(da)[1, :], idx) + @test from1d == view(parent(da)[1, :], idx) @test from1d isa AbstractDimArray end end @@ -389,13 +569,13 @@ end for idx in indices # 2D case da2d = copy(da) - a2d = copy(data(da2d)) + a2d = copy(parent(da2d)) replacement = zero(a2d[idx]) @test setindex!(da2d, replacement, idx) == setindex!(a2d, replacement, idx) @test da2d == a2d # 1D array - da1d = da[Y <| At(10)] - a1d = copy(data(da1d)) + da1d = da[Y(At(10))] + a1d = copy(parent(da1d)) @test setindex!(da1d, replacement, idx) == setindex!(a1d, replacement, idx) @test da1d == a1d end @@ -418,7 +598,7 @@ end Ti((1:1:4)u"s"; mode=Sampled(order=Ordered(Forward(), Forward(), Reverse()))))) @test indexorder(dims(da_ffr, Ti)) == Forward() @test arrayorder(dims(da_ffr, Ti)) == Forward() - @test relationorder(dims(da_ffr, Ti)) == Reverse() + @test relation(dims(da_ffr, Ti)) == Reverse() @test da_ffr[Y<|At(20), Ti<|At((3.0:4.0)u"s")] == [6, 5] @test da_ffr[Y<|At([20, 30]), Ti<|At((3.0:4.0)u"s")] == [6 5; 2 1] @test da_ffr[Y<|Near(22), Ti<|Near([3.3u"s", 4.3u"s"])] == [6, 5] @@ -465,7 +645,7 @@ end dc = DimArray(c, (Y((10, 30)), Ti((1:4)u"s"))) dc[Near(11), At(3u"s")] = 100 @test c[1, 3] == 100 - dc[Ti<|Near(2.2u"s"), Y<|Between(10, 30)] = [200, 201, 202] + @inferred setindex!(dc, [200, 201, 202], Ti<|Near(2.2u"s"), Y<|Between(10, 30)) @test c[1:3, 2] == [200, 201, 202] end @@ -476,23 +656,23 @@ end Ti((1:4)u"s"; mode=Sampled(sampling=Intervals())))) @testset "selectors with dim wrappers" begin - @test da[Y(At([10, 30])), Ti(At([1u"s", 4u"s"]))] == [1 4; 9 12] + @test @inferred da[Y(At([10, 30])), Ti(At([1u"s", 4u"s"]))] == [1 4; 9 12] @test_throws ArgumentError da[Y(At([9, 30])), Ti(At([1u"s", 4u"s"]))] - @test view(da, Y(At(20)), Ti(At((3:4)u"s"))) == [7, 8] - @test view(da, Y(Contains(17)), Ti(Contains([1.9u"s", 3.1u"s"]))) == [5, 7] - @test view(da, Y(Between(4, 26)), Ti(At((3:4)u"s"))) == [3 4; 7 8] + @test @inferred view(da, Y(At(20)), Ti(At((3:4)u"s"))) == [7, 8] + @test @inferred view(da, Y(Contains(17)), Ti(Contains([1.9u"s", 3.1u"s"]))) == [5, 7] + @test @inferred view(da, Y(Between(4, 26)), Ti(At((3:4)u"s"))) == [3 4; 7 8] end @testset "selectors without dim wrappers" begin - @test da[At(20:10:30), At(1u"s")] == [5, 9] - @test view(da, Between(4, 36), Near((3:4)u"s")) == [3 4; 7 8; 11 12] - @test view(da, Near(22), At([3.0u"s", 4.0u"s"])) == [7, 8] - @test view(da, At(20), At((2:3)u"s")) == [6, 7] - @test view(da, Near(13), Near([1.3u"s", 3.3u"s"])) == [1, 3] - @test view(da, Near([13]), Near([1.3u"s", 3.3u"s"])) == [1 3] - @test view(da, Between(11, 26), At((2:3)u"s")) == [6 7] + @test @inferred da[At(20:10:30), At(1u"s")] == [5, 9] + @test @inferred view(da, Between(4, 36), Near((3:4)u"s")) == [3 4; 7 8; 11 12] + @test @inferred view(da, Near(22), At([3.0u"s", 4.0u"s"])) == [7, 8] + @test @inferred view(da, At(20), At((2:3)u"s")) == [6, 7] + @test @inferred view(da, Near(13), Near([1.3u"s", 3.3u"s"])) == [1, 3] + @test @inferred view(da, Near([13]), Near([1.3u"s", 3.3u"s"])) == [1 3] + @test @inferred view(da, Between(11, 26), At((2:3)u"s")) == [6 7] # Between also accepts a tuple input - @test view(da, Between((11, 26)), Between((2u"s", 4u"s"))) == [6 7] + @test @inferred view(da, Between((11, 26)), Between((2u"s", 4u"s"))) == [6 7] end end @@ -500,9 +680,9 @@ end @testset "Selectors on NoIndex" begin dimz = Ti(), Y() da = DimArray(a, dimz) - @test da[Ti(At([1, 2])), Y(Contains(2))] == [2, 6] - @test da[Near(2), Between(2, 4)] == [6, 7, 8] - @test da[Contains([1, 3]), Near([2, 3, 4])] == [2 3 4; 10 11 12] + @test @inferred da[Ti(At([1, 2])), Y(Contains(2))] == [2, 6] + @test @inferred da[Near(2), Between(2, 4)] == [6, 7, 8] + @test @inferred da[Contains([1, 3]), Near([2, 3, 4])] == [2 3 4; 10 11 12] end @testset "Selectors on Categorical" begin @@ -513,12 +693,12 @@ end dimz = Ti([:one, :two, :three]; mode=Categorical(Ordered())), Y([:a, :b, :c, :d]; mode=Categorical(Ordered())) da = DimArray(a, dimz) - @test da[Ti(At([:one, :two])), Y(Contains(:b))] == [2, 6] - @test da[At(:two), Between(:b, :d)] == [6, 7, 8] - @test da[:two, :b] == 6 + @test @inferred da[Ti(At([:one, :two])), Y(Contains(:b))] == [2, 6] + @test @inferred da[At(:two), Between(:b, :d)] == [6, 7, 8] + @test @inferred da[:two, :b] == 6 # Near and contains are just At - @test da[Contains([:one, :three]), Near([:b, :c, :d])] == [2 3 4; 10 11 12] - + @test @inferred da[Contains([:one, :three]), Near([:b, :c, :d])] == [2 3 4; 10 11 12] + dimz = Ti([:one, :two, :three]; mode=Categorical(Unordered())), Y([:a, :b, :c, :d]; mode=Categorical(Unordered())) da = DimArray(a, dimz) @@ -531,14 +711,14 @@ end valdimz = Ti(Val((2.4, 2.5, 2.6)); mode=Categorical(Ordered())), Y(Val((:a, :b, :c, :d)); mode=Categorical(Ordered())) da = DimArray(a, valdimz) - @test da[Val(2.5), Val(:c)] == 7 - @test da[2.4, :a] == 1 + @test @inferred da[Val(2.5), Val(:c)] == 7 + @test @inferred da[2.4, :a] == 1 end @testset "Where " begin dimz = Ti((1:1:3)u"s"), Y(10:10:40) da = DimArray(a, dimz) - @test da[Y(Where(x -> x >= 30)), Ti(Where(x -> x in([1u"s", 3u"s"])))] == [3 4; 11 12] + @test @inferred da[Y(Where(x -> x >= 30)), Ti(Where(x -> x in([1u"s", 3u"s"])))] == [3 4; 11 12] end @testset "TranformedIndex" begin @@ -551,25 +731,26 @@ end Z() @testset "permutedims works on mode dimensions" begin - @test permutedims((Y(), Z(), X()), dimz) == (X(), Y(), Z()) + DimensionalData.modetype(typeof(dimz[1])) + @test @inferred sortdims((Y(), Z(), X()), dimz) == (X(), Y(), Z()) end da = DimArray(reshape(a, 3, 4, 1), dimz) @testset "Indexing with array dims indexes the array as usual" begin - @test da[Dim{:trans1}(3), Dim{:trans2}(1), Z(1)] == 9 + @test @inferred da[Dim{:trans1}(3), Dim{:trans2}(1), Z(1)] == 9 # Using selectors works the same as indexing with mode # dims - it applies the transform function. # It's not clear this should be allowed or makes sense, # but it works anyway because the permutation is correct either way. - @test da[Dim{:trans1}(At(6)), Dim{:trans2}(At(2)), Z(1)] == 9 + @test @inferred da[Dim{:trans1}(At(6)), Dim{:trans2}(At(2)), Z(1)] == 9 end @testset "Indexing with mode dims uses the transformation" begin - @test da[X(Near(6.1)), Y(Near(8.5)), Z(1)] == 12 - @test da[X(At(4.0)), Y(At(2.0)), Z(1)] == 5 + @test @inferred da[X(Near(6.1)), Y(Near(8.5)), Z(1)] == 12 + @test @inferred da[X(At(4.0)), Y(At(2.0)), Z(1)] == 5 @test_throws InexactError da[X(At(6.1)), Y(At(8)), Z(1)] # Indexing directly with mode dims also just works, but maybe shouldn't? - @test da[X(2), Y(2), Z(1)] == 6 + @test @inferred da[X(2), Y(2), Z(1)] == 6 end end