Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add consecutive_overlapping_subspans #19

Merged
merged 10 commits into from
Sep 20, 2023
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "AlignedSpans"
uuid = "72438786-fd5d-49ef-8843-650acbdfe662"
authors = ["Beacon Biosignals, Inc."]
version = "0.2.5"
version = "0.2.6"

[deps]
ArrowTypes = "31f734f8-188a-4ce0-8406-c8a06bd891cd"
Expand Down
1 change: 1 addition & 0 deletions docs/src/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ AlignedSpan(sample_rate, span, mode::SpanRoundingMode)
AlignedSpans.ConstantSamplesRoundingMode
AlignedSpan(sample_rate, span, mode::ConstantSamplesRoundingMode)
consecutive_subspans
consecutive_overlapping_subspans
n_samples
AlignedSpans.indices
```
Expand Down
2 changes: 1 addition & 1 deletion src/AlignedSpans.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ using TimeSpans: TimeSpans, start, stop, format_duration
using StructTypes, ArrowTypes

export SpanRoundingMode, RoundInward, RoundSpanDown, ConstantSamplesRoundingMode
export AlignedSpan, consecutive_subspans, n_samples
export AlignedSpan, consecutive_subspans, n_samples, consecutive_overlapping_subspans

# Make our own method so we can add methods for Intervals without piracy
duration(span) = TimeSpans.duration(span)
Expand Down
75 changes: 64 additions & 11 deletions src/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,76 @@ Returns the number of samples present in the span `aligned`.
n_samples(aligned::AlignedSpan) = length(indices(aligned))

"""
consecutive_subspans(span::AlignedSpan, duration::Period)
consecutive_subspans(span::AlignedSpan, duration::Period; keep_last=true)
consecutive_subspans(span::AlignedSpan, n_window_samples::Int; keep_last=true)

Creates an iterator of `AlignedSpan` such that each `AlignedSpan` has consecutive indices
which cover all of the original `span`'s indices. In particular,
which cover the original `span`'s indices (when `keep_last=true`).
ericphanson marked this conversation as resolved.
Show resolved Hide resolved

* Each span has `n = n_samples(span.sample_rate, duration)` samples, except possibly
If `keep_last=true` (the default behavior), then the last span may have fewer samples than the others, and

* Each span has `n_window_samples` samples (which is calculated as `n_samples(span.sample_rate, duration)` if `duration::Period` is supplied), except possibly
the last one, which may have fewer.
* The number of subspans is given by `cld(n_samples(span), n)`
* The number of samples in the last subspan is `r = rem(n_samples(span), n)` unless `r=0`, in which
case the the last subspan has the same number of samples as the rest, namely `n`.
* The number of subspans is given by `cld(n_samples(span), n_window_samples)`,
* The number of samples in the last subspan is `r = rem(n_samples(span), n_window_samples)` unless `r=0`, in which
case the the last subspan has the same number of samples as the rest, namely `n_window_samples`.
* All of the indices of `span` are guaranteed to be covered by exactly one subspan

If `keep_last=false`, then all spans will have the same number of samples:
* Each span has `n_window_samples` samples (which is calculated as `n_samples(span.sample_rate, duration)` if `duration::Period` is supplied)
* The number of subspans is given by `fld(n_samples(span), n_window_samples)`
* The last part of the `span`'s indices may not be covered (when we can't fit in another subspan of length `n_window_samples`)
"""
function consecutive_subspans(span::AlignedSpan, duration::Period)
n = n_samples(span.sample_rate, duration)
return consecutive_subspans(span::AlignedSpan, n)
function consecutive_subspans(span::AlignedSpan, duration::Period; keep_last=true)
n_window_samples = n_samples(span.sample_rate, duration)
return consecutive_subspans(span::AlignedSpan, n_window_samples; keep_last)
end

function consecutive_subspans(span::AlignedSpan, n::Int)
index_groups = Iterators.partition((span.first_index):(span.last_index), n)
function consecutive_subspans(span::AlignedSpan, n_window_samples::Int; keep_last=true)
index_groups = Iterators.partition((span.first_index):(span.last_index),
n_window_samples)
Comment on lines +42 to +43
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can use the existing utility function here for clarity/brevity:

Suggested change
index_groups = Iterators.partition((span.first_index):(span.last_index),
n_window_samples)
index_groups = Iterators.partition(indices(span), n_window_samples)

if !keep_last
r = rem(n_samples(span), n_window_samples)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
r = rem(n_samples(span), n_window_samples)
n, r = fldmod(n_samples(span), n_window_samples)

Note that currently you're using rem with fld, which have different rounding semantics. You probably want them to match, so to get fld behavior you can use fldmod or equivalently divrem with RoundDown.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this matter if all quantities are >0?

if r != 0
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know the tests test this branch, bc originally I accidentally had r==0 and they failed!

# Drop the last element
grps = Iterators.take(index_groups, fld(n_samples(span), n_window_samples))
return (AlignedSpan(span.sample_rate, first(I), last(I)) for I in grps)
Comment on lines +48 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
grps = Iterators.take(index_groups, fld(n_samples(span), n_window_samples))
return (AlignedSpan(span.sample_rate, first(I), last(I)) for I in grps)
index_groups = Iterators.take(index_groups, n)

(Uses the definition of n in the previous suggestion.) If you redefine the binding then you don't have to rewrite the comprehension which is otherwise the same as the one outside of this block.

end
end
return (AlignedSpan(span.sample_rate, first(I), last(I)) for I in index_groups)
end

"""
consecutive_overlapping_subspans(span::AlignedSpan, duration::Period,
hop_duration::Period)
consecutive_overlapping_subspans(span::AlignedSpan, n_window_samples::Int, n_hop_samples::Int)

Create an iterator of `AlignedSpan` such that each `AlignedSpan` has
`n_window_samples` (calculated as `n_samples(span.sample_rate, duration)` if `duration::Period` is supplied) samples, shifted by
`n_hop_samples` (calculated as `n_samples(span.sample_rate, hop_duration)` if `hop_duration::Period` is supplied) samples between
consecutive spans.

!!! warning
When `n_samples(span)` is not an integer multiple of `n_window_samples`, only AlignedSpans with `n_window_samples`
samples will be returned. This is analgous to `consecutive_subspans` with `keep_last=false`, which is not the default behavior for `consecutive_subspans`.

Note: If `hop_duration` cannot be represented as an integer number of samples,
rounding will occur to ensure that all output AlignedSpans will have the
same number of samples. When rounding occurs, the output hop_duration will be:
ericphanson marked this conversation as resolved.
Show resolved Hide resolved
`Nanosecond(n_samples(samp_rate, hop_duration) / samp_rate * 1e9)`
"""
function consecutive_overlapping_subspans(span::AlignedSpan, duration::Period,
hop_duration::Period)
n_window_samples = n_samples(span.sample_rate, duration)
n_hop_samples = n_samples(span.sample_rate, hop_duration)
return consecutive_overlapping_subspans(span::AlignedSpan, n_window_samples,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return consecutive_overlapping_subspans(span::AlignedSpan, n_window_samples,
return consecutive_overlapping_subspans(span, n_window_samples,

Was the type assertion there doing something?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copy-paste error

n_hop_samples)
end

function consecutive_overlapping_subspans(span::AlignedSpan, n_window_samples::Int,
n_hop_samples::Int)
index_groups = Iterators.partition((span.first_index):(span.last_index - n_window_samples + 1),
n_hop_samples)
return (AlignedSpan(span.sample_rate, first(I), first(I) + n_window_samples - 1)
for I in index_groups)
end
86 changes: 86 additions & 0 deletions test/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ function test_subspans(aligned, sample_rate, dur)
else
@test n_samples(subspans[end]) == n_samples(sample_rate, dur)
end

# Ends at the end
@test subspans[end].last_index == aligned.last_index

# all test w/ `keep_last=false`
test_subspans_skip_last(aligned, sample_rate, dur)
return nothing
end

function test_subspans_skip_last(aligned, sample_rate, dur)
@show aligned, sample_rate, dur
subspans = collect(consecutive_subspans(aligned, dur; keep_last=false))
@test length(subspans) == fld(n_samples(aligned), n_samples(sample_rate, dur))
for i in 1:(length(subspans) - 1)
@test subspans[i + 1].first_index == subspans[i].last_index + 1 # consecutive indices
@test n_samples(subspans[i]) == n_samples(sample_rate, dur) # each has as many samples as prescribed by the duration
end

# Does not necessarily end all the way at the end, but gets within `n`
@test aligned.last_index - n_samples(sample_rate, dur) <= subspans[end].last_index <=
aligned.last_index
ericphanson marked this conversation as resolved.
Show resolved Hide resolved
return nothing
end

@testset "consecutive_subspans" begin
Expand All @@ -37,3 +59,67 @@ end

@test_throws ArgumentError consecutive_subspans(aligned, Millisecond(1))
end

@testset "consecutive_overlapping_subspans" begin
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this whole testset is untouched from the private package in which it was copied from

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(new stuff added in 8eabc1e)

# when window_duration == hop duration and window_duration fits into
# samples_span exactly n times, the output of consecutive_overlapping_subspans
# should equal that of consecutive_subspans
samples_span = AlignedSpan(128, 1, 128 * 120)
window_samples = 10 * 128
subspans = consecutive_overlapping_subspans(samples_span, window_samples,
window_samples)
og_subspans = consecutive_subspans(samples_span, window_samples)
@test all(collect(subspans) .== collect(og_subspans))

# Check w/ Period's
subspans2 = consecutive_overlapping_subspans(samples_span, Second(10),
Second(10))
@test all(collect(subspans) .== collect(subspans2))

# when window_duration == hop duration but window_duration does not
# fit evenly into samples_span, consecutive_subspans will return a
# last AlignedSpan with n_samples < n_samples(window_duration), whereas
# consecutive_overlapping_subspans will omit the last window and only
# return AlignedSpans with n_samples = n_samples(window_duration)
window_samples = 11 * 128
subspans = consecutive_overlapping_subspans(samples_span, window_samples,
window_samples)
og_subspans = consecutive_subspans(samples_span, window_samples)
c_subspans = collect(subspans)
@test length(collect(og_subspans)) - 1 == length(c_subspans)
@test all(n_samples.(c_subspans) .== window_samples)

# Check w/ Period's
subspans2 = consecutive_overlapping_subspans(samples_span, Second(11),
Second(11))
@test all(collect(subspans) .== collect(subspans2))

# when hop_samples < window_samples
window_samples = 10 * 128
hop_samples = 5 * 128
n_complete_windows = fld((n_samples(samples_span) - window_samples), hop_samples) + 1
subspans = consecutive_overlapping_subspans(samples_span, window_samples, hop_samples)
c_subspans = collect(subspans)
@test length(c_subspans) == n_complete_windows
@test all(n_samples.(c_subspans) .== window_samples)

# Check w/ Period's
subspans2 = consecutive_overlapping_subspans(samples_span, Second(10),
Second(5))
@test all(collect(subspans) .== collect(subspans2))

# hop_samples < windows_samples and window_samples does not fit exactly into
# samples_span
window_samples = 11 * 128
hop_samples = 5 * 128
n_complete_windows = fld((n_samples(samples_span) - window_samples), hop_samples) + 1
subspans = consecutive_overlapping_subspans(samples_span, window_samples, hop_samples)
c_subspans = collect(subspans)
@test length(c_subspans) == n_complete_windows
@test all(n_samples.(c_subspans) .== window_samples)

# Check w/ Period's
subspans2 = consecutive_overlapping_subspans(samples_span, Second(11),
Second(5))
@test all(collect(subspans) .== collect(subspans2))
end