Skip to content
Closed
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/changes/dev/13892.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix :meth:`mne.Annotations.__getitem__` (i.e., ``raw.annotations[i]["onset"]``) to return onset times in ``raw.times`` reference (0-indexed from ``first_samp``) instead of ``meas_date``-relative time, making the value directly usable with :meth:`mne.io.BaseRaw.get_data` and :meth:`mne.io.BaseRaw.plot`, by `Sanskaar Undale`_.
1 change: 1 addition & 0 deletions doc/changes/names.inc
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@
.. _Samuel Deslauriers-Gauthier: https://github.com/sdeslauriers
.. _Samuel Louviot: https://github.com/Sam54000
.. _Samuel Powell: https://github.com/samuelpowell
.. _Sanskaar Undale: https://github.com/SanskaarUndale21
.. _Santeri Ruuskanen: https://github.com/ruuskas
.. _Santi Martínez: https://github.com/szz-dvl
.. _Sara Sommariva: https://github.com/sarasommariva
Expand Down
23 changes: 19 additions & 4 deletions mne/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,8 +512,12 @@ def __getitem__(self, key, *, with_ch_names=None, with_extras=True):
"""Propagate indexing and slicing to the underlying numpy structure."""
if isinstance(key, int_like):
out_keys = ("onset", "duration", "description", "orig_time")
# When attached to a Raw object, subtract _raw_first_time so that
# ann["onset"] is in raw.times reference (usable directly with
# raw.get_data(tmin=...) and raw.plot(start=...)).
_raw_ft = getattr(self, "_raw_first_time", 0.0)
out_vals = (
self.onset[key],
self.onset[key] - _raw_ft,
self.duration[key],
self.description[key],
self.orig_time,
Expand All @@ -527,14 +531,19 @@ def __getitem__(self, key, *, with_ch_names=None, with_extras=True):
return OrderedDict(zip(out_keys, out_vals))
else:
key = list(key) if isinstance(key, tuple) else key
return Annotations(
result = Annotations(
onset=self.onset[key],
duration=self.duration[key],
description=self.description[key],
orig_time=self.orig_time,
ch_names=self.ch_names[key],
extras=[self.extras[i] for i in np.arange(len(self.extras))[key]],
)
# Propagate the raw-times reference tag so that sliced annotations
# still return onsets in raw.times reference.
if hasattr(self, "_raw_first_time"):
result._raw_first_time = self._raw_first_time
return result

@fill_doc
def append(self, onset, duration, description, ch_names=None, *, extras=None):
Expand Down Expand Up @@ -1497,11 +1506,15 @@ def get_annotations_per_epoch(self, *, with_extras=False):

# for each Epoch-Annotation overlap occurrence:
for annot_ix, epo_ix in zip(*np.nonzero(all_cases)):
# Use the raw onset array (internal reference) so the relative
# onset calculation stays consistent with epoch_tzeros, which is
# also in the internal (meas_date-relative) reference frame.
this_onset = self._annotations.onset[annot_ix]
this_annot = self._annotations[annot_ix]
this_tzero = epoch_tzeros[epo_ix]
# adjust annotation onset to be relative to epoch tzero...
annot = (
this_annot["onset"] - this_tzero,
this_onset - this_tzero,
this_annot["duration"],
this_annot["description"],
)
Expand Down Expand Up @@ -2370,8 +2383,10 @@ def events_from_annotations(
good_events = annot_offset - _onsets >= chunk_duration - tol
if good_events.any():
_onsets = _onsets[good_events]
# annot["onset"] is in raw.times reference (origin=None),
# not meas_date reference, so use origin=None here.
_inds = raw.time_as_index(
_onsets, use_rounding=use_rounding, origin=annotations.orig_time
_onsets, use_rounding=use_rounding, origin=None
)
_inds += raw.first_samp
inds = np.append(inds, _inds)
Expand Down
2 changes: 1 addition & 1 deletion mne/export/_brainvision.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def _mne_annots2pybv_events(raw):
for annot in raw.annotations:
# handle onset and duration: seconds to sample, relative to
# raw.first_samp / raw.first_time
onset = annot["onset"] - raw.first_time
onset = annot["onset"]
onset = raw.time_as_index(onset).astype(int)[0]
duration = int(annot["duration"] * raw.info["sfreq"])

Expand Down
8 changes: 7 additions & 1 deletion mne/io/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,10 @@ def set_annotations(
meas_date - new_annotations.orig_time
).total_seconds()
new_annotations._orig_time = meas_date
# Tag with _raw_first_time so the public onset accessor can return
# values in raw.times reference (0-indexed from first_samp) instead
# of the internal meas_date-relative reference.
new_annotations._raw_first_time = self._first_time

self._annotations = new_annotations

Expand Down Expand Up @@ -1730,7 +1734,9 @@ def crop_by_annotations(self, annotations=None, *, verbose=None):

raws = []
for annot in annotations:
onset = annot["onset"] - self.first_time
# annot["onset"] is already in raw.times reference (0-indexed from
# first_samp) when the annotation comes from raw.annotations.
onset = annot["onset"]
# be careful about near-zero errors (crop is very picky about this,
Comment thread
SanskaarUndale21 marked this conversation as resolved.
# e.g., -1e-8 is an error)
if -self.info["sfreq"] / 2 < onset < 0:
Expand Down
Loading