Skip to content

Commit d5cdc14

Browse files
ajkeller34timholy
authored andcommitted
Implement indexing by value with atvalue (#52)
1 parent c5ea220 commit d5cdc14

File tree

7 files changed

+120
-24
lines changed

7 files changed

+120
-24
lines changed

README.md

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,13 @@ Collaboration is welcome! This is still a work-in-progress. See [the roadmap](ht
1313
## Example of currently-implemented behavior:
1414

1515
```julia
16-
julia> Pkg.clone("https://github.com/JuliaArrays/AxisArrays.jl")
16+
julia> Pkg.add("AxisArrays")
1717
using AxisArrays, Unitful
1818
import Unitful: s, ms, µs
1919

2020
julia> fs = 40000 # Generate a 40kHz noisy signal, with spike-like stuff added for testing
2121
y = randn(60*fs+1)*3
22-
for spk = (sin.(0.8:0.2:8.6) .* [0:0.01:.1; .15:.1:.95; 1:-.05:.05] .* 50,
22+
for spk = (sin.(0.8:0.2:8.6) .* [0:0.01:.1; .15:.1:.95; 1:-.05:.05] .* 50,
2323
sin.(0.8:0.4:8.6) .* [0:0.02:.1; .15:.1:1; 1:-.2:.1] .* 50)
2424
i = rand(round(Int,.001fs):1fs)
2525
while i+length(spk)-1 < length(y)
@@ -62,7 +62,7 @@ And data, a 2-element Array{Float64,1}:
6262
julia> A[Axis{:chan}(:c2), Axis{:time}(1:5)]
6363
1-dimensional AxisArray{Float64,1,...} with axes:
6464
:time, 0.0 s:2.5e-5 s:0.0001 s
65-
A[Axis{:chan}(:c2), Axis{:time}(1:5)]:
65+
And data, a 5-element Array{Float64,1}:
6666
-6.12181
6767
0.304668
6868
15.7366
@@ -92,8 +92,23 @@ julia> axes(ans, 1)
9292
AxisArrays.Axis{:time,StepRangeLen{Quantity{Float64, Dimensions:{𝐓}, Units:{s}},Base.TwicePrecision{Quantity{Float64, Dimensions:{𝐓}, Units:{s}}},Base.TwicePrecision{Quantity{Float64, Dimensions:{𝐓}, Units:{s}}}}}(5.0e-5 s:2.5e-5 s:0.0002 s)
9393
```
9494

95+
You can also index by a single value on an axis using `atvalue`. This will drop
96+
a dimension. Indexing with an `Interval` type retains dimensions, even
97+
when the ends of the interval are equal:
98+
99+
```jl
100+
julia> A[atvalue(2.5e-5s), :c1]
101+
0.152334
102+
103+
julia> A[2.5e-5s..2.5e-5s, :c1]
104+
1-dimensional AxisArray{Float64,1,...} with axes:
105+
:time, 2.5e-5 s:2.5e-5 s:2.5e-5 s
106+
And data, a 1-element Array{Float64,1}:
107+
0.152334
108+
```
109+
95110
Sometimes, though, what we're really interested in is a window of time about a
96-
specific index. The operation above (looking for values in the window from 40µs
111+
specific index. One of the operations above (looking for values in the window from 40µs
97112
to 220µs) might be more clearly expressed as a symmetrical window about a
98113
specific index where we know something interesting happened. To represent this,
99114
we use the `atindex` function:

docs/src/index.md

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# AxisArrays
22

3-
[![Build Status](https://travis-ci.org/JuliaArrays/AxisArrays.jl.svg?branch=master)](https://travis-ci.org/JuliaArrays/AxisArrays.jl) [![Coverage Status](https://coveralls.io/repos/JuliaArrays/AxisArrays.jl/badge.svg?branch=master)](https://coveralls.io/r/JuliaArrays/AxisArrays.jl?branch=master)
3+
[![Build Status](https://travis-ci.org/JuliaArrays/AxisArrays.jl.svg?branch=master)](https://travis-ci.org/JuliaArrays/AxisArrays.jl) [![Coverage Status](https://coveralls.io/repos/github/JuliaArrays/AxisArrays.jl/badge.svg?branch=master)](https://coveralls.io/github/JuliaArrays/AxisArrays.jl?branch=master)
44

55
This package for the Julia language provides an array type (the `AxisArray`) that knows about its dimension names and axis values.
66
This allows for indexing with the axis name without incurring any runtime overhead.
@@ -13,14 +13,14 @@ Collaboration is welcome! This is still a work-in-progress. See [the roadmap](ht
1313
## Example of currently-implemented behavior:
1414

1515
```julia
16-
julia> Pkg.clone("https://github.com/JuliaArrays/AxisArrays.jl")
17-
using AxisArrays, SIUnits
18-
import SIUnits.ShortUnits: s, ms, µs
16+
julia> Pkg.add("AxisArrays")
17+
using AxisArrays, Unitful
18+
import Unitful: s, ms, µs
1919

2020
julia> fs = 40000 # Generate a 40kHz noisy signal, with spike-like stuff added for testing
2121
y = randn(60*fs+1)*3
22-
for spk = (sin(0.8:0.2:8.6) .* [0:0.01:.1; .15:.1:.95; 1:-.05:.05] .* 50,
23-
sin(0.8:0.4:8.6) .* [0:0.02:.1; .15:.1:1; 1:-.2:.1] .* 50)
22+
for spk = (sin.(0.8:0.2:8.6) .* [0:0.01:.1; .15:.1:.95; 1:-.05:.05] .* 50,
23+
sin.(0.8:0.4:8.6) .* [0:0.02:.1; .15:.1:1; 1:-.2:.1] .* 50)
2424
i = rand(round(Int,.001fs):1fs)
2525
while i+length(spk)-1 < length(y)
2626
y[i:i+length(spk)-1] += spk
@@ -54,16 +54,15 @@ indices in *any* order, just so long as we annotate them with the axis name:
5454

5555
```jl
5656
julia> A[Axis{:time}(4)]
57-
2-dimensional AxisArray{Float64,2,...} with axes:
58-
:time, 7.5e-5 s:2.5e-5 s:7.5e-5 s
59-
:chan, [:c1,:c2]
60-
And data, a 1x2 SubArray{Float64,2,Array{Float64,2},Tuple{UnitRange{Int64},Colon},2}:
57+
2-dimensional AxisArray{Float64,1,...} with axes:
58+
:chan, Symbol[:c1,:c2]
59+
And data, a 2-element Array{Float64,1}:
6160
-1.4144 -2.82879
6261

6362
julia> A[Axis{:chan}(:c2), Axis{:time}(1:5)]
6463
1-dimensional AxisArray{Float64,1,...} with axes:
6564
:time, 0.0 s:2.5e-5 s:0.0001 s
66-
And data, a 5-element SubArray{Float64,1,Array{Float64,2},Tuple{UnitRange{Int64},Int64},2}:
65+
And data, a 5-element Array{Float64,1}:
6766
-6.12181
6867
0.304668
6968
15.7366
@@ -80,7 +79,7 @@ still has the correct time information for those datapoints!
8079
julia> A[40µs .. 220µs, :c1]
8180
1-dimensional AxisArray{Float64,1,...} with axes:
8281
:time, 5.0e-5 s:2.5e-5 s:0.0002 s
83-
And data, a 7-element SubArray{Float64,1,Array{Float64,2},Tuple{UnitRange{Int64},Int64},2}:
82+
And data, a 7-element Array{Float64,1}:
8483
7.86831
8584
-1.4144
8685
-2.02881
@@ -90,11 +89,26 @@ And data, a 7-element SubArray{Float64,1,Array{Float64,2},Tuple{UnitRange{Int64}
9089
-1.97716
9190

9291
julia> axes(ans, 1)
93-
AxisArrays.Axis{:time,SIUnits.SIRange{FloatRange{Float64},Float64,0,0,1,0,0,0,0,0,0}}(5.0e-5 s:2.5e-5 s:0.0002 s)
92+
AxisArrays.Axis{:time,StepRangeLen{Quantity{Float64, Dimensions:{𝐓}, Units:{s}},Base.TwicePrecision{Quantity{Float64, Dimensions:{𝐓}, Units:{s}}},Base.TwicePrecision{Quantity{Float64, Dimensions:{𝐓}, Units:{s}}}}}(5.0e-5 s:2.5e-5 s:0.0002 s)
93+
```
94+
95+
You can also index by a single value on an axis using `atvalue`. This will drop
96+
a dimension. Indexing with an `Interval` type retains dimensions, even
97+
when the ends of the interval are equal:
98+
99+
```jl
100+
julia> A[atvalue(2.5e-5s), :c1]
101+
0.152334
102+
103+
julia> A[2.5e-5s..2.5e-5s, :c1]
104+
1-dimensional AxisArray{Float64,1,...} with axes:
105+
:time, 2.5e-5 s:2.5e-5 s:2.5e-5 s
106+
And data, a 1-element Array{Float64,1}:
107+
0.152334
94108
```
95109

96110
Sometimes, though, what we're really interested in is a window of time about a
97-
specific index. The operation above (looking for values in the window from 40µs
111+
specific index. One of the operations above (looking for values in the window from 40µs
98112
to 220µs) might be more clearly expressed as a symmetrical window about a
99113
specific index where we know something interesting happened. To represent this,
100114
we use the `atindex` function:
@@ -125,7 +139,7 @@ julia> idxs = find(diff(A[:,:c1] .< -15) .> 0)
125139
julia> spks = A[atindex(-200µs .. 800µs, idxs), :c1]
126140
2-dimensional AxisArray{Float64,2,...} with axes:
127141
:time_sub, -0.000175 s:2.5e-5 s:0.000775 s
128-
:time_rep, SIUnits.SIQuantity{Float64,0,0,1,0,0,0,0,0,0}[0.178725 s,0.806825 s,0.88305 s,1.47485 s,1.50465 s,1.53805 s,1.541025 s,2.16365 s,2.368425 s,2.739 s 57.797925 s,57.924075 s,58.06075 s,58.215125 s,58.6403 s,58.96215 s,58.990225 s,59.001325 s,59.48395 s,59.611525 s]
142+
:time_rep, Quantity{Float64, Dimensions:{𝐓}, Units:{s}}[0.178725 s,0.806825 s,0.88305 s,1.47485 s,1.50465 s,1.53805 s,1.541025 s,2.16365 s,2.368425 s,2.739 s 57.797925 s,57.924075 s,58.06075 s,58.215125 s,58.6403 s,58.96215 s,58.990225 s,59.001325 s,59.48395 s,59.611525 s]
129143
And data, a 39x242 Array{Float64,2}:
130144
-1.53038 4.72882 5.8706 -0.231564 0.624714 3.44076
131145
-2.24961 2.12414 5.69936 7.00179 2.30993 5.20432

src/AxisArrays.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ using Base: tail
66
using RangeArrays, IntervalSets
77
using Compat
88

9-
export AxisArray, Axis, axisnames, axisvalues, axisdim, axes, atindex
9+
export AxisArray, Axis, axisnames, axisvalues, axisdim, axes, atindex, atvalue
1010

1111
# From IntervalSets:
1212
export ClosedInterval, ..

src/indexing.jl

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@ const Idx = Union{Real,Colon,AbstractArray{Int}}
22

33
using Base: ViewIndex, @propagate_inbounds, tail
44

5+
immutable Value{T}
6+
val::T
7+
tol::T
8+
end
9+
Value(x, tol=Base.rtoldefault(typeof(x))*abs(x)) = Value(promote(x,tol)...)
10+
atvalue(x; rtol=Base.rtoldefault(typeof(x)), atol=zero(x)) = Value(x, atol+rtol*abs(x))
11+
12+
# For throwing a BoundsError with a Value index, we need to define the following
13+
# (note that we could inherit them for free, were Value <: Number)
14+
Base.start(::Value) = false
15+
Base.next(x::Value, state) = (x, true)
16+
Base.done(x::Value, state) = state
17+
18+
# How to show Value objects (e.g. in a BoundsError)
19+
Base.show(io::IO, v::Value) =
20+
print(io, string("Value(", v.val, ", tol=", v.tol, ")"))
21+
522
# Defer IndexStyle to the wrapped array
623
@compat Base.IndexStyle{T,N,D,Ax}(::Type{AxisArray{T,N,D,Ax}}) = IndexStyle(D)
724

@@ -170,14 +187,27 @@ axisindexes(ax, idx) = axisindexes(axistrait(ax.val), ax.val, idx)
170187
axisindexes(::Type{Unsupported}, ax, idx) = error("elementwise indexing is not supported for axes of type $(typeof(ax))")
171188
axisindexes(t, ax, idx) = error("cannot index $(typeof(ax)) with $(typeof(idx)); expected $(eltype(ax)) axis value or Integer index")
172189

173-
# Dimensional axes may be indexed directy by their elements if Non-Real and unique
190+
# Dimensional axes may be indexed directly by their elements if Non-Real and unique
174191
# Maybe extend error message to all <: Numbers if Base allows it?
175-
axisindexes{T<:Real}(::Type{Dimensional}, ax::AbstractVector{T}, idx::T) = error("indexing by axis value is not supported for axes with $(eltype(ax)) elements; use an ClosedInterval instead")
192+
axisindexes(::Type{Dimensional}, ax::AbstractVector, idx::Real) =
193+
throw(ArgumentError("invalid index: $idx. Use `atvalue` when indexing by value."))
176194
function axisindexes(::Type{Dimensional}, ax::AbstractVector, idx)
177195
idxs = searchsorted(ax, ClosedInterval(idx,idx))
178196
length(idxs) > 1 && error("more than one datapoint lies on axis value $idx; use an interval to return all values")
179197
idxs[1]
180198
end
199+
# Dimensional axes may always be indexed by value if in a Value type wrapper.
200+
function axisindexes(::Type{Dimensional}, ax::AbstractVector, idx::Value)
201+
idxs = searchsorted(ax, ClosedInterval(idx.val,idx.val))
202+
length(idxs) > 1 && error("more than one datapoint lies on axis value $idx; use an interval to return all values")
203+
if length(idxs) == 1
204+
idxs[1]
205+
else # it's zero
206+
last(idxs) > 0 && abs(ax[last(idxs)] - idx.val) < idx.tol && return last(idxs)
207+
first(idxs) <= length(ax) && abs(ax[first(idxs)] - idx.val) < idx.tol && return first(idxs)
208+
throw(BoundsError(ax, idx))
209+
end
210+
end
181211

182212
# Dimensional axes may be indexed by intervals to select a range
183213
axisindexes{T}(::Type{Dimensional}, ax::AbstractVector{T}, idx::ClosedInterval) = searchsorted(ax, idx)

test/REQUIRE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
OffsetArrays
2-
Unitful 0.1.0
2+
Unitful 0.2.6

test/indexing.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,3 +165,38 @@ A = AxisArray(rand(2,2), :x, :y)
165165
acc = zeros(Int, 4, 1, 2)
166166
Base.mapreducedim!(x->x>5, +, acc, A3)
167167
@test acc == reshape([1 3; 2 3; 2 3; 2 3], 4, 1, 2)
168+
169+
# Indexing by value using `atvalue`
170+
A = AxisArray([1 2; 3 4], Axis{:x}([1.0,4.0]), Axis{:y}([2.0,6.1]))
171+
@test @inferred(A[atvalue(1.0)]) == @inferred(A[atvalue(1.0), :]) == [1,2]
172+
# `atvalue` doesn't require same type:
173+
@test @inferred(A[atvalue(1)]) == @inferred(A[atvalue(1), :]) ==[1,2]
174+
@test A[atvalue(4.0)] == A[atvalue(4.0),:] == [3,4]
175+
@test A[atvalue(4)] == A[atvalue(4),:] == [3,4]
176+
@test_throws BoundsError A[atvalue(5.0)]
177+
@test @inferred(A[atvalue(1.0), atvalue(2.0)]) == 1
178+
@test @inferred(A[:, atvalue(2.0)]) == [1,3]
179+
@test @inferred(A[Axis{:x}(atvalue(4.0))]) == [3,4]
180+
@test @inferred(A[Axis{:y}(atvalue(6.1))]) == [2,4]
181+
@test @inferred(A[Axis{:x}(atvalue(4.00000001))]) == [3,4]
182+
@test @inferred(A[Axis{:x}(atvalue(2.0, atol=5))]) == [1,2]
183+
@test_throws BoundsError A[Axis{:x}(atvalue(4.00000001, rtol=0))]
184+
185+
# Indexing by value into an OffsetArray
186+
A = AxisArray(OffsetArrays.OffsetArray([1 2; 3 4], 0:1, 1:2),
187+
Axis{:x}([1.0,4.0]), Axis{:y}([2.0,6.1]))
188+
@test_broken @inferred(A[atvalue(4.0)]) == [3,4]
189+
@test @inferred(A[:, atvalue(2.0)]) == OffsetArrays.OffsetArray([1,3], 0:1)
190+
@test_throws BoundsError A[atvalue(5.0)]
191+
192+
# Indexing by value directly is forbidden for indexes that are Real
193+
@test_throws ArgumentError A[4.0]
194+
@test_throws ArgumentError A[BigFloat(1.0)]
195+
@test_throws ArgumentError A[1.0f0]
196+
if VERSION == v"0.5.2"
197+
# Cannot compose @test_broken with @test_throws (Julia #21098)
198+
# A[:,6.1] incorrectly throws a BoundsError instead of an ArgumentError on Julia 0.5.2
199+
@test_broken A[:,6.1]
200+
else
201+
@test_throws ArgumentError A[:,6.1]
202+
end

test/readme.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ axes(ax, 1)
2222
A[atindex(-90µs .. 90µs, 5), :c2]
2323
idxs = find(diff(A[:,:c1] .< -15) .> 0)
2424
spks = A[atindex(-200µs .. 800µs, idxs), :c1]
25-
25+
A[atvalue(2.5e-5s), :c1]
26+
A[2.5e-5s..2.5e-5s, :c1]
27+
A[atvalue(25.0µs)]
2628

2729
# # A possible "dynamic verification" strategy
2830
# const readmefile = joinpath(dirname(dirname(@__FILE__)), "README.md")

0 commit comments

Comments
 (0)