event.py 14 KB

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