AlignedSpans

See API documentation for how to construct AlignedSpans, along with some utilities, or below for some examples and motivation.

Continuous -> Discrete

Continuous timespans can be rounded (or "aligned") to the individual sample values by using the constructor AlignedSpan, which takes a sample_rate, a span, and a description of how to round time endpoints to indices. This constructs an AlignedSpan which supports Onda indexing. Internally, an AlignedSpan stores indices, not times, and any rounding happens when it is created instead of when indexing into samples.

Rounding options:

  • SpanRoundingMode: consists of a RoundingMode for the start and stop of the span.
    • The alias RoundInward = SpanRoundingMode(RoundUp, RoundDown), for example, constructs the largest span such that all samples are entirely contained within span.
    • The alias RoundSpanDown = SpanRoundingMode(RoundDown, RoundDown) matches the rounding semantics of TimeSpans.index_from_time(sample_rate, span).
  • ConstantSamplesRoundingMode consists of a RoundingMode for the start alone. The stop is determined from the start plus a number of samples which is a function only of the sampling rate and the duration of the span.

Also provides a helper consecutive_subspans to partition an AlignedSpan into smaller consecutive AlignedSpans of equal size (except possibly the last one).

Discrete -> Continuous

AlignedSpan's support TimeSpans.start and TimeSpans.stop, so they can be used a continuous-time spans. The semantics of this are:

For any index included in an AlignedSpan, the time at which the corresponding sample occurred (inclusive) to the time at which the next sample occurred (exclusive) is associated to the continuous-time representation of the span.

As an example, if the sample rate is 1, and indices 2:3 are associated to a span, then the associated TimeSpan is TimeSpan(Second(1), Second(3)). That's because sample 2 occur at time Second(1), and is considered to "last" until sample 3, which occurs at Second(2). Next, sample 3 occurs at time Second(2) and is considered to "last" until sample 4, which occurs at Second(3). Therefore, the total span associated to 2:3 is Second(1) to Second(3).

In diagram form, the inclusive interval of indices [2, 3] is associated inclusive-exclusive interval of seconds, [1, 3):

Index       1   [2    3]    4     5
Time (s)    0   [1    2     3)    4

This choice of conversion matches the inclusive-inclusive indexing of Julia integer indices to the inclusive-exclusive semantics of TimeSpans.jl, and allows for roundtripping and sensible durations:

julia> using AlignedSpans, TimeSpans, Dates

julia> aligned = AlignedSpan(1, 2, 3)
AlignedSpan(1.0, 2, 3)

julia> ts = TimeSpan(aligned)
TimeSpan(00:00:01.000000000, 00:00:03.000000000)

julia> aligned == AlignedSpan(1, ts, RoundInward)
true

julia> aligned == AlignedSpan(1, ts, RoundSpanDown)
true

julia> duration(aligned) == duration(ts) == Second(2)
true
Warning

For non-integer sample rates, roundtripping perfectly is not always possible.

Note on roundtripping

Quick example

Let's consider the following TimeSpan

julia> using TimeSpans, AlignedSpans, Dates
julia> span = TimeSpan(Millisecond(1500), Millisecond(3500))TimeSpan(00:00:01.500000000, 00:00:03.500000000)

If we have a 1 Hz signal, there are various ways we can index into it using this TimeSpan. One option is to round the endpoints down:

julia> down_span = AlignedSpan(1, span, RoundSpanDown)AlignedSpan(1.0, 2, 4)
julia> n_samples(down_span)3

The second sample of our signal occurs at time 1s (since we have a 1Hz signal that starts at 0s). When we round the starting endpoint down from 1.5s to the nearest sample, we find that sample. This can be seen as "the last sample that occurred before time 1.5s".

Perhaps instead we would like to round the endpoints "inward" to only consider samples occurring with the time span:

julia> in_span = AlignedSpan(1, span, RoundInward)AlignedSpan(1.0, 3, 4)
julia> n_samples(in_span)2

Motivation

Let's say I want to plot some samples over time, and I have a nice function plot(::TimeSpan, ::Samples) to use.

julia> using TimeSpans, Onda, Dates
julia> sample_rate = 1 # 1 Hz -> slow to exaggerate the effect1
julia> samples = Samples(permutedims(0:10), SamplesInfo("feature", ["a"], "microvolt", 0.5, 0.0, UInt16, sample_rate), false)ERROR: UndefVarError: `SamplesInfo` not defined
julia> span = TimeSpan(Millisecond(1500), Millisecond(4000))TimeSpan(00:00:01.500000000, 00:00:04.000000000)

Now I want to execute some call

plot(span, samples[:, span])

for some plot that understands TimeSpans – doesn't matter what function, exactly.

What is wrong with this?

Let's take a look at the samples we are plotting:

julia> samples[:, span] # TimeSpans v0.2; v0.3 will have one more sampleERROR: UndefVarError: `samples` not defined

These are three samples that correspond to times 1s, 2s, and 3s. However, what we gave to the x-axis of our plotting function is TimeSpan(Millisecond(1500), Millisecond(3500)), which starts at 1.5s and goes to 3.5s. In other words, our plot will have an incorrect 0.5s offset!

Note that plot is just an example; any function where one is separately passing both a "timespan of interest" and "feature values from that timespan" will have similar issues if one isn't careful about what exactly samples[:, span] is doing.

The fix

Let's take the same setup, with our

julia> spanTimeSpan(00:00:01.500000000, 00:00:04.000000000)

This time, we do

julia> using AlignedSpans
julia> aligned_span = AlignedSpan(samples.info.sample_rate, span, RoundSpanDown)ERROR: UndefVarError: `samples` not defined
julia> samples[:, aligned_span]ERROR: UndefVarError: `samples` not defined

Here, I get the same samples. However, now I have the actual span corresponding to those samples, namely aligned_span. So if I call my

plot(aligned_span, samples[:, aligned_span])

I'll have the correct alignment between the points on the x-axis and y-axis.