Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions doc/documentation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ There are also **examples**, which contain a short use-case to highlight MNE-fun
auto_tutorials/plot_object_evoked.rst
auto_tutorials/plot_object_source_estimate.rst
auto_tutorials/plot_info.rst
auto_tutorials/plot_object_annotations.rst


.. raw:: html
Expand Down
5 changes: 3 additions & 2 deletions doc/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,14 @@ MNE-Python core terminology and general concepts


annotations
One annotation is defined by an onset, a duration and a string
An annotation is defined by an onset, a duration, and a string
description. It can contain information about the experiments, but
also details on signals marked by a human: bad data segments,
sleep scores, sleep events (spindles, K-complex) etc.
An :class:`Annotations` object is a container of multiple annotations.
See :class:`Annotations` page for the API of the corresponding
object class.
object class and :ref:`sphx_glr_auto_tutorials_plot_object_annotations.py`
for a tutorial on how to manipulate such objects.

channels
Channels refer to MEG sensors, EEG electrodes or any extra electrode
Expand Down
2 changes: 2 additions & 0 deletions doc/whats_new.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Current
Changelog
~~~~~~~~~

- New tutorial in the documentation regarding :class:`mne.Annotations` by `Joan Massich`_ and `Alex Gramfort`_

- Add :meth:`mne.Epochs.shift_time` that shifts the time axis of :class:`mne.Epochs` by `Thomas Hartmann`_

- Add :func:`mne.viz.plot_arrowmap` computes arrowmaps using Hosaka-Cohen transformation from magnetometer or gradiometer data, these arrows represents an estimation of the current flow underneath the MEG sensors by `Sheraz Khan`_
Expand Down
45 changes: 33 additions & 12 deletions mne/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,16 @@ class Annotations(object):
Array of strings containing description for each annotation. If a
string, all the annotations are given the same description. To reject
epochs, use description starting with keyword 'bad'. See example above.
orig_time : float | int | instance of datetime | array of int | None
orig_time : float | int | instance of datetime | array of int | None | str
A POSIX Timestamp, datetime or an array containing the timestamp as the
first element and microseconds as the second element. Determines the
starting time of annotation acquisition. If None (default),
starting time is determined from beginning of raw data acquisition.
In general, ``raw.info['meas_date']`` (or None) can be used for syncing
the annotations with raw data if their acquisiton is started at the
same time.
same time. If it is a string, it should conform to the ISO8601 format.
More precisely to this '%Y-%m-%d %H:%M:%S.%f' particular case of the
ISO8601 format where the delimiter between date and time is ' '.

Notes
-----
Expand Down Expand Up @@ -143,12 +145,7 @@ class Annotations(object):
def __init__(self, onset, duration, description,
orig_time=None): # noqa: D102
if orig_time is not None:
if isinstance(orig_time, datetime):
orig_time = float(time.mktime(orig_time.timetuple()))
elif not np.isscalar(orig_time):
orig_time = orig_time[0] + orig_time[1] / 1000000.
else: # isscalar
orig_time = float(orig_time) # np.int not serializable
orig_time = _handle_meas_date(orig_time)
self.orig_time = orig_time

onset = np.array(onset, dtype=float)
Expand Down Expand Up @@ -180,8 +177,12 @@ def __repr__(self):
for kind in kinds]
kinds = ', '.join(kinds[:3]) + ('' if len(kinds) <= 3 else '...')
kinds = (': ' if len(kinds) > 0 else '') + kinds
return ('<Annotations | %s segment%s %s >'
% (len(self.onset), _pl(len(self.onset)), kinds))
if self.orig_time is None:
orig = 'orig_time : None'
else:
orig = 'orig_time : %s' % datetime.utcfromtimestamp(self.orig_time)
return ('<Annotations | %s segment%s %s, %s>'
% (len(self.onset), _pl(len(self.onset)), kinds, orig))

def __len__(self):
"""Return the number of annotations."""
Expand Down Expand Up @@ -358,15 +359,35 @@ def _combine_annotations(one, two, one_n_samples, one_first_samp,


def _handle_meas_date(meas_date):
"""Convert meas_date to seconds."""
"""Convert meas_date to seconds.

If `meas_date` is a string, it should conform to the ISO8601 format.
More precisely to this '%Y-%m-%d %H:%M:%S.%f' particular case of the
ISO8601 format where the delimiter between date and time is ' '.

Otherwise, this function returns 0. Note that ISO8601 allows for ' ' or 'T'
as delimiters between date and time.
"""
if meas_date is None:
meas_date = 0
elif isinstance(meas_date, string_types):
ACCEPTED_ISO8601 = '%Y-%m-%d %H:%M:%S.%f'
try:
meas_date = datetime.strptime(meas_date, ACCEPTED_ISO8601)
except ValueError:
meas_date = 0
else:
unix_ref_time = datetime.utcfromtimestamp(0)
meas_date = (meas_date - unix_ref_time).total_seconds()
meas_date = round(meas_date, 6) # round that 6th decimal
elif isinstance(meas_date, datetime):
meas_date = float(time.mktime(meas_date.timetuple()))
elif not np.isscalar(meas_date):
if len(meas_date) > 1:
meas_date = meas_date[0] + meas_date[1] / 1000000.
else:
meas_date = meas_date[0]
return meas_date
return float(meas_date)


def _sync_onset(raw, onset, inverse=False):
Expand Down
15 changes: 15 additions & 0 deletions mne/tests/test_annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -635,4 +635,19 @@ def _constant_id(*args, **kwargs):
assert event_id == expected_event_id


@pytest.mark.parametrize('meas_date, out', [
pytest.param('toto', 0, id='invalid string'),
pytest.param(None, 0, id='None'),
pytest.param(42, 42.0, id='Scalar'),
pytest.param(3.14, 3.14, id='Float'),
pytest.param((3, 140000), 3.14, id='Scalar touple'),
pytest.param('2002-12-03 19:01:11.720100', 1038942071.7201,
id='valid iso8601 string'),
pytest.param('2002-12-03T19:01:11.720100', 0,
id='invalid iso8601 string')])
def test_handle_meas_date(meas_date, out):
"""Test meas date formats."""
assert _handle_meas_date(meas_date) == out


run_tests_if_main()
170 changes: 170 additions & 0 deletions tutorials/plot_object_annotations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
"""
The **events** and :class:`Annotations <mne.Annotations>` data structures
=========================================================================

Events and :class:`Annotations <mne.Annotations>` are quite similar.
This tutorial highlights their differences and similarities, and tries to shed
some light on which one is preferred to use in different situations when using
MNE.

Here are the definitions from the :ref:`glossary`.

events
Events correspond to specific time points in raw data; e.g., triggers,
experimental condition events, etc. MNE represents events with integers
that are stored in numpy arrays of shape (n_events, 3). Such arrays are
classically obtained from a trigger channel, also referred to as stim
channel.

annotations
An annotation is defined by an onset, a duration, and a string
description. It can contain information about the experiments, but
also details on signals marked by a human: bad data segments,
sleep scores, sleep events (spindles, K-complex) etc.

Both events and :class:`Annotations <mne.Annotations>` can be seen as triplets
where the first element answers to **when** something happens and the last
element refers to **what** it is.
The main difference is that events represent the onset in samples taking into
account the first sample value
(:attr:`raw.first_samp <mne.io.Raw.first_samp>`), and the description is
an integer value.
In contrast, :class:`Annotations <mne.Annotations>` represents the
``onset`` in seconds (relative to the reference ``orig_time``),
and the ``description`` is an arbitrary string.
There is no correspondence between the second element of events and
:class:`Annotations <mne.Annotations>`.
For events, the second element corresponds to the previous value on the
stimulus channel from which events are extracted. In practice, the second
element is therefore in most cases zero.
The second element of :class:`Annotations <mne.Annotations>` is a float
indicating its duration in seconds.

See :ref:`sphx_glr_auto_examples_io_plot_read_events.py`
for a complete example of how to read, select, and visualize **events**;
and :ref:`sphx_glr_auto_tutorials_plot_artifacts_correction_rejection.py` to
learn how :class:`Annotations <mne.Annotations>` are used to mark bad segments
of data.

An example of events and annotations
------------------------------------

The following example shows the recorded events in `sample_audvis_raw.fif` and
marks bad segments due to eye blinks.
"""

import os.path as op
import numpy as np

import mne

# Load the data
data_path = mne.datasets.sample.data_path()
fname = op.join(data_path, 'MEG', 'sample', 'sample_audvis_raw.fif')
raw = mne.io.read_raw_fif(fname)

# Plot the events
events = mne.find_events(raw)

# Specify event_id dictionary based on the experiment
event_id = {'Auditory/Left': 1, 'Auditory/Right': 2,
'Visual/Left': 3, 'Visual/Right': 4,
'smiley': 5, 'button': 32}
color = {1: 'green', 2: 'yellow', 3: 'red', 4: 'c', 5: 'black', 32: 'blue'}

mne.viz.plot_events(events, raw.info['sfreq'], raw.first_samp, color=color,
event_id=event_id)

# Create some annotations specifying onset, duration and description
annotated_blink_raw = raw.copy()
eog_events = mne.preprocessing.find_eog_events(raw)
n_blinks = len(eog_events)
# Center to cover the whole blink with full duration of 0.5s:
Copy link
Member

Choose a reason for hiding this comment

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

this may be a bit misleading - the events are centered and we are actually de-cenetering them by pushing them back in time to mark onset, not event center.

onset = eog_events[:, 0] / raw.info['sfreq'] - 0.25
duration = np.repeat(0.5, n_blinks)
description = ['bad blink'] * n_blinks
annot = mne.Annotations(onset, duration, description,
orig_time=raw.info['meas_date'])
annotated_blink_raw.set_annotations(annot)

annotated_blink_raw.plot() # plot the annotated raw


###############################################################################
# Working with Annotations
# ------------------------
#
# An important element of :class:`Annotations <mne.Annotations>` is
# ``orig_time`` which is the time reference for the ``onset``.
# It is key to understand that when calling
# :func:`raw.set_annotations <mne.io.Raw.set_annotations>`, the given
# annotations are copied and transformed so that
# :class:`raw.annotations.orig_time <mne.Annotations>`
# matches the recording time of the raw object.
# Refer to the documentation of :class:`Annotations <mne.Annotations>` to see
# the expected behavior depending on ``meas_date`` and ``orig_time``.
# Where ``meas_date`` is the recording time stored in
# :class:`Info <mne.Info>`.
# You can find more information about :class:`Info <mne.Info>` in
# :ref:`sphx_glr_auto_tutorials_plot_info.py`.
#
# We'll now manipulate some simulated annotations.
# The first annotations has ``orig_time`` set to ``None`` while the
# second is set to a chosen POSIX timestamp for illustration purposes.

###############################################################################

# Create an annotation object without orig_time
annot_none = mne.Annotations(onset=[0, 2, 9], duration=[0.5, 4, 0],
description=['foo', 'bar', 'foo'],
orig_time=None)
print(annot_none)

# Create an annotation object with orig_time
orig_time = '2002-12-03 19:01:31.676071'
annot_orig = mne.Annotations(onset=[22, 24, 31], duration=[0.5, 4, 0],
description=['foo', 'bar', 'foo'],
orig_time=orig_time)
print(annot_orig)

###############################################################################
# Now we create two raw objects, set the annotations and plot them to compare
# them.

# Create two cropped copies of raw with the two previous annotations
raw_a = raw.copy().crop(tmax=12).set_annotations(annot_none)
raw_b = raw.copy().crop(tmax=12).set_annotations(annot_orig)

# Plot the raw objects
raw_a.plot()
raw_b.plot()

# Show the annotations in the raw objects
print(raw_a.annotations)
print(raw_b.annotations)

# Show that the onsets are the same
np.set_printoptions(precision=6)
print(raw_a.annotations.onset)
print(raw_b.annotations.onset)

###############################################################################
#
# Notice that for the case where ``orig_time`` is ``None``,
# one assumes that the orig_time is the time of the first sample of data.

raw_delta = (1 / raw.info['sfreq'])
print('raw.first_sample is {}'.format(raw.first_samp * raw_delta))
print('annot_none.onset[0] is {}'.format(annot_none.onset[0]))
print('raw_a.annotations.onset[0] is {}'.format(raw_a.annotations.onset[0]))

###############################################################################
#
# It is possible to concatenate two annotations with the + operator like for
# lists if both share the same ``orig_time``

annot = mne.Annotations(onset=[10], duration=[0.5],
description=['foobar'],
orig_time=orig_time)
annot = annot_orig + annot # concatenation
print(annot)