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

[WIP [ENH] metadata attribute for Annotations #12213

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
38 changes: 36 additions & 2 deletions mne/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import os.path as op
import re
import uuid
import warnings
from collections import Counter, OrderedDict
from collections.abc import Iterable
Expand Down Expand Up @@ -282,8 +283,35 @@ def __init__(
self.onset, self.duration, self.description, self.ch_names = _check_o_d_s_c(
onset, duration, description, ch_names
)
self._id = np.array(
[str(uuid.uuid4()) for _ in range(len(self.onset))], dtype=str
)
self._sort() # ensure we're sorted

# We need this, bc if we create Annotations from events which are not sorted,
# and we then directly query for the according IDs, we cannot do this simply by
# index as the Annotations will already on creation be sorted.
def _get_id(self, onset, duration, description):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm afraid that this is far from optimal and might be slow once there are many annotations.

onset = np.asarray(onset, dtype=float)
duration = np.asarray(duration, dtype=float)
description = np.asarray(description, dtype=str)

ids = [None] * len(onset)

for i, ev in enumerate(zip(onset, duration, description)):
# find all matching annotations by onset:
idx = np.where(
(self.onset == ev[0])
& (self.duration == ev[1])
& (self.description == ev[2])
)[0]
if len(idx) > 1:
raise ValueError("Found multiple matching annotations")
elif len(idx) == 1:
ids[i] = self._id[idx[0]]

return ids

@property
def orig_time(self):
"""The time base of the Annotations."""
Expand Down Expand Up @@ -359,26 +387,29 @@ def __iter__(self):
def __getitem__(self, key, *, with_ch_names=None):
"""Propagate indexing and slicing to the underlying numpy structure."""
if isinstance(key, int_like):
out_keys = ("onset", "duration", "description", "orig_time")
out_keys = ("onset", "duration", "description", "orig_time", "_id")
out_vals = (
self.onset[key],
self.duration[key],
self.description[key],
self.orig_time,
self._id[key],
)
if with_ch_names or (with_ch_names is None and self._any_ch_names()):
out_keys += ("ch_names",)
out_vals += (self.ch_names[key],)
return OrderedDict(zip(out_keys, out_vals))
else:
key = list(key) if isinstance(key, tuple) else key
return Annotations(
annots = Annotations(
onset=self.onset[key],
duration=self.duration[key],
description=self.description[key],
orig_time=self.orig_time,
ch_names=self.ch_names[key],
)
annots._id = self._id[np.sort(key)] # Annotations are always sorted
return annots

@fill_doc
def append(self, onset, duration, description, ch_names=None):
Expand Down Expand Up @@ -412,10 +443,12 @@ def append(self, onset, duration, description, ch_names=None):
onset, duration, description, ch_names = _check_o_d_s_c(
onset, duration, description, ch_names
)
_id = np.array([str(uuid.uuid4()) for _ in range(len(onset))], dtype=str)
self.onset = np.append(self.onset, onset)
self.duration = np.append(self.duration, duration)
self.description = np.append(self.description, description)
self.ch_names = np.append(self.ch_names, ch_names)
self._id = np.append(self._id, _id)
self._sort()
return self

Expand Down Expand Up @@ -564,6 +597,7 @@ def _sort(self):
self.duration = self.duration[order]
self.description = self.description[order]
self.ch_names = self.ch_names[order]
self._id = self._id[order]

@verbose
def crop(
Expand Down
7 changes: 5 additions & 2 deletions mne/utils/docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,11 @@ def _reflow_param_docstring(docstring, has_first_line=True, width=75):
] = """
adjacency : scipy.sparse.spmatrix | None | False
Defines adjacency between locations in the data, where "locations" can be
spatial vertices, frequency bins, time points, etc. For spatial vertices,
see: :func:`mne.channels.find_ch_adjacency`. If ``False``, assumes
spatial vertices, frequency bins, time points, etc. For spatial vertices
(i.e. sensor space data), see :func:`mne.channels.find_ch_adjacency` or
:func:`mne.spatial_inter_hemi_adjacency`. For source space data, see
:func:`mne.spatial_src_adjacency` or
:func:`mne.spatio_temporal_src_adjacency`. If ``False``, assumes
no adjacency (each location is treated as independent and unconnected).
If ``None``, a regular lattice adjacency is assumed, connecting
each {sp} location to its neighbor(s) along the last dimension
Expand Down
1 change: 1 addition & 0 deletions tools/circleci_dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

python -m pip install --upgrade "pip!=20.3.0" build
python -m pip install --upgrade --progress-bar off --only-binary "numpy,scipy,matplotlib,pandas,statsmodels" git+https://github.com/sphinx-gallery/sphinx-gallery.git -ve .[full,test,doc]
python -m pip install git+https://github.com/mne-tools/mne-bids.git