event.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. # -*- coding: utf-8 -*-
  2. '''
  3. This module defines :class:`Event`, an array of events.
  4. :class:`Event` derives from :class:`BaseNeo`, from
  5. :module:`neo.core.baseneo`.
  6. '''
  7. # needed for python 3 compatibility
  8. from __future__ import absolute_import, division, print_function
  9. import sys
  10. from copy import deepcopy
  11. import numpy as np
  12. import quantities as pq
  13. from neo.core.baseneo import merge_annotations
  14. from neo.core.dataobject import DataObject, ArrayDict
  15. from neo.core.epoch import Epoch
  16. PY_VER = sys.version_info[0]
  17. def _new_event(cls, times=None, labels=None, units=None, name=None, file_origin=None,
  18. description=None, array_annotations=None, annotations=None, segment=None):
  19. '''
  20. A function to map Event.__new__ to function that
  21. does not do the unit checking. This is needed for pickle to work.
  22. '''
  23. e = Event(times=times, labels=labels, units=units, name=name, file_origin=file_origin,
  24. description=description, array_annotations=array_annotations, **annotations)
  25. e.segment = segment
  26. return e
  27. class Event(DataObject):
  28. '''
  29. Array of events.
  30. *Usage*::
  31. >>> from neo.core import Event
  32. >>> from quantities import s
  33. >>> import numpy as np
  34. >>>
  35. >>> evt = Event(np.arange(0, 30, 10)*s,
  36. ... labels=np.array(['trig0', 'trig1', 'trig2'],
  37. ... dtype='S'))
  38. >>>
  39. >>> evt.times
  40. array([ 0., 10., 20.]) * s
  41. >>> evt.labels
  42. array(['trig0', 'trig1', 'trig2'],
  43. dtype='|S5')
  44. *Required attributes/properties*:
  45. :times: (quantity array 1D) The time of the events.
  46. :labels: (numpy.array 1D dtype='S') Names or labels for the events.
  47. *Recommended attributes/properties*:
  48. :name: (str) A label for the dataset.
  49. :description: (str) Text description.
  50. :file_origin: (str) Filesystem path or URL of the original data file.
  51. *Optional attributes/properties*:
  52. :array_annotations: (dict) Dict mapping strings to numpy arrays containing annotations \
  53. for all data points
  54. Note: Any other additional arguments are assumed to be user-specific
  55. metadata and stored in :attr:`annotations`.
  56. '''
  57. _single_parent_objects = ('Segment',)
  58. _quantity_attr = 'times'
  59. _necessary_attrs = (('times', pq.Quantity, 1), ('labels', np.ndarray, 1, np.dtype('S')))
  60. def __new__(cls, times=None, labels=None, units=None, name=None, description=None,
  61. file_origin=None, array_annotations=None, **annotations):
  62. if times is None:
  63. times = np.array([]) * pq.s
  64. if labels is None:
  65. labels = np.array([], dtype='S')
  66. if units is None:
  67. # No keyword units, so get from `times`
  68. try:
  69. units = times.units
  70. dim = units.dimensionality
  71. except AttributeError:
  72. raise ValueError('you must specify units')
  73. else:
  74. if hasattr(units, 'dimensionality'):
  75. dim = units.dimensionality
  76. else:
  77. dim = pq.quantity.validate_dimensionality(units)
  78. # check to make sure the units are time
  79. # this approach is much faster than comparing the
  80. # reference dimensionality
  81. if (len(dim) != 1 or list(dim.values())[0] != 1 or not isinstance(list(dim.keys())[0],
  82. pq.UnitTime)):
  83. ValueError("Unit %s has dimensions %s, not [time]" % (units, dim.simplified))
  84. obj = pq.Quantity(times, units=dim).view(cls)
  85. obj.labels = labels
  86. obj.segment = None
  87. return obj
  88. def __init__(self, times=None, labels=None, units=None, name=None, description=None,
  89. file_origin=None, array_annotations=None, **annotations):
  90. '''
  91. Initialize a new :class:`Event` instance.
  92. '''
  93. DataObject.__init__(self, name=name, file_origin=file_origin, description=description,
  94. array_annotations=array_annotations, **annotations)
  95. def __reduce__(self):
  96. '''
  97. Map the __new__ function onto _new_event, so that pickle
  98. works
  99. '''
  100. return _new_event, (self.__class__, np.array(self), self.labels, self.units, self.name,
  101. self.file_origin, self.description, self.array_annotations,
  102. self.annotations, self.segment)
  103. def __array_finalize__(self, obj):
  104. super(Event, self).__array_finalize__(obj)
  105. self.annotations = getattr(obj, 'annotations', None)
  106. self.name = getattr(obj, 'name', None)
  107. self.file_origin = getattr(obj, 'file_origin', None)
  108. self.description = getattr(obj, 'description', None)
  109. self.segment = getattr(obj, 'segment', None)
  110. # Add empty array annotations, because they cannot always be copied,
  111. # but do not overwrite existing ones from slicing etc.
  112. # This ensures the attribute exists
  113. if not hasattr(self, 'array_annotations'):
  114. self.array_annotations = ArrayDict(self._get_arr_ann_length())
  115. def __repr__(self):
  116. '''
  117. Returns a string representing the :class:`Event`.
  118. '''
  119. # need to convert labels to unicode for python 3 or repr is messed up
  120. if PY_VER == 3:
  121. labels = self.labels.astype('U')
  122. else:
  123. labels = self.labels
  124. objs = ['%s@%s' % (label, time) for label, time in zip(labels, self.times)]
  125. return '<Event: %s>' % ', '.join(objs)
  126. def _repr_pretty_(self, pp, cycle):
  127. super(Event, self)._repr_pretty_(pp, cycle)
  128. def rescale(self, units):
  129. '''
  130. Return a copy of the :class:`Event` converted to the specified
  131. units
  132. '''
  133. obj = super(Event, self).rescale(units)
  134. obj.segment = self.segment
  135. return obj
  136. @property
  137. def times(self):
  138. return pq.Quantity(self)
  139. def merge(self, other):
  140. '''
  141. Merge the another :class:`Event` into this one.
  142. The :class:`Event` objects are concatenated horizontally
  143. (column-wise), :func:`np.hstack`).
  144. If the attributes of the two :class:`Event` are not
  145. compatible, and Exception is raised.
  146. '''
  147. othertimes = other.times.rescale(self.times.units)
  148. times = np.hstack([self.times, othertimes]) * self.times.units
  149. kwargs = {}
  150. for name in ("name", "description", "file_origin"):
  151. attr_self = getattr(self, name)
  152. attr_other = getattr(other, name)
  153. if attr_self == attr_other:
  154. kwargs[name] = attr_self
  155. else:
  156. kwargs[name] = "merge(%s, %s)" % (attr_self, attr_other)
  157. print('Event: merge annotations')
  158. merged_annotations = merge_annotations(self.annotations, other.annotations)
  159. kwargs.update(merged_annotations)
  160. kwargs['array_annotations'] = self._merge_array_annotations(other)
  161. evt = Event(times=times, labels=kwargs['array_annotations']['labels'], **kwargs)
  162. return evt
  163. def _copy_data_complement(self, other):
  164. '''
  165. Copy the metadata from another :class:`Event`.
  166. Note: Array annotations can not be copied here because length of data can change
  167. '''
  168. # Note: Array annotations cannot be copied
  169. # because they are linked to their respective timestamps
  170. for attr in ("name", "file_origin", "description", "annotations"):
  171. setattr(self, attr, getattr(other, attr,
  172. None)) # Note: Array annotations cannot be copied
  173. # because length of data can be changed # here which would cause inconsistencies #
  174. # This includes labels and durations!!!
  175. def __deepcopy__(self, memo):
  176. cls = self.__class__
  177. new_ev = cls(times=self.times, labels=self.labels, units=self.units, name=self.name,
  178. description=self.description, file_origin=self.file_origin)
  179. new_ev.__dict__.update(self.__dict__)
  180. memo[id(self)] = new_ev
  181. for k, v in self.__dict__.items():
  182. try:
  183. setattr(new_ev, k, deepcopy(v, memo))
  184. except TypeError:
  185. setattr(new_ev, k, v)
  186. return new_ev
  187. def __getitem__(self, i):
  188. obj = super(Event, self).__getitem__(i)
  189. try:
  190. obj.array_annotate(**deepcopy(self.array_annotations_at_index(i)))
  191. except AttributeError: # If Quantity was returned, not Event
  192. pass
  193. return obj
  194. def duplicate_with_new_data(self, signal, units=None):
  195. '''
  196. Create a new :class:`Event` with the same metadata
  197. but different data
  198. Note: Array annotations can not be copied here because length of data can change
  199. '''
  200. if units is None:
  201. units = self.units
  202. else:
  203. units = pq.quantity.validate_dimensionality(units)
  204. new = self.__class__(times=signal, units=units)
  205. new._copy_data_complement(self)
  206. # Note: Array annotations cannot be copied here, because length of data can be changed
  207. return new
  208. def time_slice(self, t_start, t_stop):
  209. '''
  210. Creates a new :class:`Event` corresponding to the time slice of
  211. the original :class:`Event` between (and including) times
  212. :attr:`t_start` and :attr:`t_stop`. Either parameter can also be None
  213. to use infinite endpoints for the time interval.
  214. '''
  215. _t_start = t_start
  216. _t_stop = t_stop
  217. if t_start is None:
  218. _t_start = -np.inf
  219. if t_stop is None:
  220. _t_stop = np.inf
  221. indices = (self >= _t_start) & (self <= _t_stop)
  222. new_evt = self[indices]
  223. return new_evt
  224. def set_labels(self, labels):
  225. self.array_annotate(labels=labels)
  226. def get_labels(self):
  227. return self.array_annotations['labels']
  228. labels = property(get_labels, set_labels)
  229. def to_epoch(self, pairwise=False, durations=None):
  230. """
  231. Returns a new Epoch object based on the times and labels in the Event object.
  232. This method has three modes of action.
  233. 1. By default, an array of `n` event times will be transformed into
  234. `n-1` epochs, where the end of one epoch is the beginning of the next.
  235. This assumes that the events are ordered in time; it is the
  236. responsibility of the caller to check this is the case.
  237. 2. If `pairwise` is True, then the event times will be taken as pairs
  238. representing the start and end time of an epoch. The number of
  239. events must be even, otherwise a ValueError is raised.
  240. 3. If `durations` is given, it should be a scalar Quantity or a
  241. Quantity array of the same size as the Event.
  242. Each event time is then taken as the start of an epoch of duration
  243. given by `durations`.
  244. `pairwise=True` and `durations` are mutually exclusive. A ValueError
  245. will be raised if both are given.
  246. If `durations` is given, epoch labels are set to the corresponding
  247. labels of the events that indicate the epoch start
  248. If `durations` is not given, then the event labels A and B bounding
  249. the epoch are used to set the labels of the epochs in the form 'A-B'.
  250. """
  251. if pairwise:
  252. # Mode 2
  253. if durations is not None:
  254. raise ValueError("Inconsistent arguments. "
  255. "Cannot give both `pairwise` and `durations`")
  256. if self.size % 2 != 0:
  257. raise ValueError("Pairwise conversion of events to epochs"
  258. " requires an even number of events")
  259. times = self.times[::2]
  260. durations = self.times[1::2] - times
  261. labels = np.array(
  262. ["{}-{}".format(a, b) for a, b in zip(self.labels[::2], self.labels[1::2])])
  263. elif durations is None:
  264. # Mode 1
  265. times = self.times[:-1]
  266. durations = np.diff(self.times)
  267. labels = np.array(
  268. ["{}-{}".format(a, b) for a, b in zip(self.labels[:-1], self.labels[1:])])
  269. else:
  270. # Mode 3
  271. times = self.times
  272. labels = self.labels
  273. return Epoch(times=times, durations=durations, labels=labels)