Skip to content

Fix IrregularlySampledSignal.time_slice returning the whole signal for an empty window - #1899

Open
Yi-111-a wants to merge 1 commit into
NeuralEnsemble:masterfrom
Yi-111-a:irs-time-slice-empty-window
Open

Fix IrregularlySampledSignal.time_slice returning the whole signal for an empty window#1899
Yi-111-a wants to merge 1 commit into
NeuralEnsemble:masterfrom
Yi-111-a:irs-time-slice-empty-window

Conversation

@Yi-111-a

Copy link
Copy Markdown

Fixes #1888.

The bug

IrregularlySampledSignal.time_slice computes a correct boolean mask, but then
recovers the integer slice bounds by walking it:

id_start = None
id_stop = None
for i in indices:
    if id_start is None:
        if i:
            id_start = count
    else:
        if not i:
            id_stop = count
            break
    count += 1

new_st = deepcopy(self[id_start:id_stop])

None carries two meanings here: "no match found yet" inside the loop, and
"unbounded" in the slice afterwards. When no sample falls inside the window both
stay None, self[None:None] evaluates to self[:], and the entire signal is
returned.

Segment.time_slice appends the result unconditionally, so a segment sliced to a
window in which one irregularly sampled channel has no samples comes back holding
that channel's whole recording — timestamped outside the window that was asked
for — while its events and epochs for the same window are correctly dropped.

Reproducer against current master

import numpy as np
import quantities as pq
from neo.core import IrregularlySampledSignal

times = np.array([1.0, 2.0, 30.0, 40.0]) * pq.s
sig = IrregularlySampledSignal(times, np.arange(4.0).reshape(-1, 1) * pq.mV)
print(sig.time_slice(5 * pq.s, 20 * pq.s).times)   # [ 1.  2. 30. 40.] s

The fix

Return an empty signal when nothing matched:

if id_start is None:
    return deepcopy(self[0:0])

I chose "empty" over raising ValueError deliberately — as the issue says, the
sibling classes split on this and it is a real design question. I went with empty
because:

  • Event.time_slice and Epoch.time_slice return empty for the same situation;
  • an empty IrregularlySampledSignal is already a state this class supports. The
    constructor accepts one, and test_time_slice_empty already asserts that
    slicing one returns an empty one. The current code satisfies that test only by
    accident, because its input is empty to begin with;
  • AnalogSignal.time_slice raises, and Segment.time_slice propagates that, so
    raising here would widen what can fail inside a segment slice. Returning empty
    changes behaviour only in the case that is currently wrong.

If you would rather raise here, I am happy to rework it — the mask is already
correct, so it is a one-line change either way.

I left Segment.time_slice alone. It still appends the now-empty signal
unconditionally; whether a channel with no samples should be dropped like an empty
Event seems worth deciding separately, and it is not needed to fix the reported
behaviour.

Verification

  • pytest neo/test/coretest/test_irregularysampledsignal.py — 70 passed, 1 skipped
  • pytest neo/test/coretest/ — 621 passed, 11 skipped
  • The new test_time_slice_window_without_samples fails on master with
    AssertionError: [ 1. 2. 30. 40.] s != [] s and passes with the fix.
  • Windows that do contain samples are unaffected: time_slice(None, None),
    time_slice(3, None), time_slice(None, 3), and the existing
    test_time_slice_out_of_boundries all still pass.

`IrregularlySampledSignal.time_slice` builds a correct boolean mask but
recovers the integer slice bounds by walking it with `id_start`/`id_stop`
initialised to `None`. Those `None`s serve two purposes at once: a "no
match found yet" flag inside the loop, and an unbounded slice bound
afterwards. When no sample falls inside the window both stay `None`, so
`self[None:None]` evaluates to `self[:]` and the entire signal is
returned.

Return an empty signal in that case. That matches `Event.time_slice` and
`Epoch.time_slice`, and it is already a state this class supports: the
constructor accepts an empty signal, and `test_time_slice_empty` asserts
that slicing one returns an empty one. Windows that do contain samples
are unaffected.

Add a regression test covering a window inside a gap between samples, a
window entirely after the last sample, and a window entirely before the
first one.

Fixes NeuralEnsemble#1888
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

IrregularlySampledSignal.time_slice returns the whole signal when the window contains no samples

1 participant