Skip to content

Commit d275efa

Browse files
Fix for annotation with no duration (#399)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent 77501ad commit d275efa

File tree

6 files changed

+66
-3
lines changed

6 files changed

+66
-3
lines changed

doc/development/changes/authors.inc

+1
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@
44
.. _Kyuhwa Lee: https://github.com/dbdq
55
.. _Mathieu Scheltienne: https://mathieu.scheltienne.net
66
.. _Thomas Binns: https://github.com/tsbinns
7+
.. _Tom Ma: https://github.com/myd7349
78
.. _Toni M. Brotons: https://github.com/toni-neurosc

doc/development/changes/latest.rst

+2
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ Version 1.9
2020
===========
2121

2222
- Fix sphinx formatting in tutorials (:pr:`388` and :pr:`389` by `Thomas Binns`_)
23+
- Fix source links in the documentation (:pr:`398` by `Tom Ma`_)
24+
- Fix stream of annotations by a :class:`~mne_lsl.player.PlayerLSL` when the annotation duration is 0 (:pr:`399` by `Mathieu Scheltienne`_)

src/mne_lsl/player/player_lsl.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class PlayerLSL(BasePlayer):
6161
the chunk are pushed on the annotation :class:`~mne_lsl.lsl.StreamOutlet`. The
6262
:class:`~mne.Annotations` are pushed with a timestamp corrected for the annotation
6363
onset in regards to the chunk beginning. However, :class:`~mne.Annotations` push is
64-
*not* delayed until the the annotation timestamp or until the end of the chunk.
64+
*not* delayed until the annotation timestamp or until the end of the chunk.
6565
Thus, an :class:`~mne.Annotations` can arrived at the client
6666
:class:`~mne_lsl.lsl.StreamInlet` "ahead" of time, i.e. earlier than the current
6767
time (as returned by the function :func:`~mne_lsl.lsl.local_clock`). Thus, it is
@@ -90,6 +90,12 @@ class PlayerLSL(BasePlayer):
9090
streamed on a channel correspond to the duration of the :class:`~mne.Annotations`.
9191
Thus, a sample on this :class:`~mne_lsl.lsl.StreamOutlet` is a one-hot encoded
9292
vector of the :class:`~mne.Annotations` description/duration.
93+
94+
.. note::
95+
96+
If the duration of an annotatation is ``0``, then the one-hot encoded vector
97+
becomes a null vector. In this special case, the value ``-1`` is encoded and
98+
denotes an annotation with a duration of ``0``.
9399
"""
94100

95101
def __init__(
@@ -345,7 +351,9 @@ def _stream_annotations(
345351
]
346352
)
347353
data = np.zeros((timestamps.size, len(self._annotations_names)))
348-
data[np.arange(timestamps.size), idx_] = self.annotations.duration[idx]
354+
durations = self.annotations.duration[idx]
355+
durations[durations == 0] = -1
356+
data[np.arange(timestamps.size), idx_] = durations
349357
# push as a chunk all annotations in the [start:stop] range
350358
with catch_warnings():
351359
filterwarnings(

src/mne_lsl/player/player_lsl.pyi

+7-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ class PlayerLSL(BasePlayer):
6060
the chunk are pushed on the annotation :class:`~mne_lsl.lsl.StreamOutlet`. The
6161
:class:`~mne.Annotations` are pushed with a timestamp corrected for the annotation
6262
onset in regards to the chunk beginning. However, :class:`~mne.Annotations` push is
63-
*not* delayed until the the annotation timestamp or until the end of the chunk.
63+
*not* delayed until the annotation timestamp or until the end of the chunk.
6464
Thus, an :class:`~mne.Annotations` can arrived at the client
6565
:class:`~mne_lsl.lsl.StreamInlet` "ahead" of time, i.e. earlier than the current
6666
time (as returned by the function :func:`~mne_lsl.lsl.local_clock`). Thus, it is
@@ -89,6 +89,12 @@ class PlayerLSL(BasePlayer):
8989
streamed on a channel correspond to the duration of the :class:`~mne.Annotations`.
9090
Thus, a sample on this :class:`~mne_lsl.lsl.StreamOutlet` is a one-hot encoded
9191
vector of the :class:`~mne.Annotations` description/duration.
92+
93+
.. note::
94+
95+
If the duration of an annotatation is ``0``, then the one-hot encoded vector
96+
becomes a null vector. In this special case, the value ``-1`` is encoded and
97+
denotes an annotation with a duration of ``0``.
9298
"""
9399

94100
_name: Incomplete

tests/player/test_player_lsl.py

+41
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,47 @@ def test_player_annotations(raw_annotations, close_io, chunk_size, request):
459459
player.stop()
460460

461461

462+
@pytest.mark.slow
463+
def test_player_annotations_no_duration(raw_annotations, close_io, chunk_size, request):
464+
"""Test player with annotations."""
465+
name = f"P_{request.node.name}"
466+
source_id = uuid.uuid4().hex
467+
annotations = sorted(set(raw_annotations.annotations.description))
468+
raw_annotations.annotations.duration.fill(0) # overwrite durations
469+
player = Player(
470+
raw_annotations, chunk_size=chunk_size, name=name, source_id=source_id
471+
)
472+
assert f"Player: {name}" in repr(player)
473+
assert player.name == name
474+
assert player.source_id == source_id
475+
assert player.fname == Path(raw_annotations.filenames[0])
476+
streams = resolve_streams(timeout=0.1)
477+
assert (name, source_id) not in [
478+
(stream.name, stream.source_id) for stream in streams
479+
]
480+
player.start()
481+
streams = resolve_streams(timeout=2)
482+
assert (name, source_id) in [(stream.name, stream.source_id) for stream in streams]
483+
assert (f"{name}-annotations", source_id) in [
484+
(stream.name, stream.source_id) for stream in streams
485+
]
486+
# compare with a Stream object for simplicity
487+
stream = Stream(bufsize=40, stype="annotations", source_id=source_id)
488+
stream.connect(processing_flags=["clocksync"])
489+
assert stream.info["ch_names"] == annotations
490+
time.sleep(3) # acquire some annotations
491+
for picks in ("bad_test", "test2", "test3"):
492+
data, ts = stream.get_data(picks=picks)
493+
data = data.squeeze()
494+
assert ts.size == data.size
495+
idx = np.where(data != 0.0)[0]
496+
assert_allclose(data[idx], [-1] * idx.size)
497+
# clean-up
498+
stream.disconnect()
499+
close_io()
500+
player.stop()
501+
502+
462503
@pytest.fixture
463504
def raw_annotations_1000_samples() -> BaseRaw:
464505
"""Return a 1000 sample raw object with annotations."""

tutorials/20_player_annotations.py

+5
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222
channels, each corresponding to one of the 3 descriptions. When an annotation is
2323
streamed, it's duration is encoded as the value on its channel while the other channels
2424
remain to zero.
25+
26+
.. note::
27+
28+
Annotation with a duration equal to zero are special cased and yield an encoded
29+
value of ``-1``.
2530
"""
2631

2732
# sphinx_gallery_thumbnail_number = 2

0 commit comments

Comments
 (0)