event.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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. import numpy as np
  11. import quantities as pq
  12. from neo.core.baseneo import BaseNeo, merge_annotations
  13. PY_VER = sys.version_info[0]
  14. def _new_event(cls, signal, times = None, labels=None, units=None, name=None,
  15. file_origin=None, description=None,
  16. annotations=None, segment=None):
  17. '''
  18. A function to map Event.__new__ to function that
  19. does not do the unit checking. This is needed for pickle to work.
  20. '''
  21. e = Event(signal=signal, times=times, labels=labels, units=units, name=name, file_origin=file_origin,
  22. description=description, **annotations)
  23. e.segment = segment
  24. return e
  25. class Event(BaseNeo, pq.Quantity):
  26. '''
  27. Array of events.
  28. *Usage*::
  29. >>> from neo.core import Event
  30. >>> from quantities import s
  31. >>> import numpy as np
  32. >>>
  33. >>> evt = Event(np.arange(0, 30, 10)*s,
  34. ... labels=np.array(['trig0', 'trig1', 'trig2'],
  35. ... dtype='S'))
  36. >>>
  37. >>> evt.times
  38. array([ 0., 10., 20.]) * s
  39. >>> evt.labels
  40. array(['trig0', 'trig1', 'trig2'],
  41. dtype='|S5')
  42. *Required attributes/properties*:
  43. :times: (quantity array 1D) The time of the events.
  44. :labels: (numpy.array 1D dtype='S') Names or labels for the events.
  45. *Recommended attributes/properties*:
  46. :name: (str) A label for the dataset.
  47. :description: (str) Text description.
  48. :file_origin: (str) Filesystem path or URL of the original data file.
  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. _quantity_attr = 'times'
  54. _necessary_attrs = (('times', pq.Quantity, 1),
  55. ('labels', np.ndarray, 1, np.dtype('S')))
  56. def __new__(cls, times=None, labels=None, units=None, name=None, description=None,
  57. file_origin=None, **annotations):
  58. if times is None:
  59. times = np.array([]) * pq.s
  60. if labels is None:
  61. labels = np.array([], dtype='S')
  62. if units is None:
  63. # No keyword units, so get from `times`
  64. try:
  65. units = times.units
  66. dim = units.dimensionality
  67. except AttributeError:
  68. raise ValueError('you must specify units')
  69. else:
  70. if hasattr(units, 'dimensionality'):
  71. dim = units.dimensionality
  72. else:
  73. dim = pq.quantity.validate_dimensionality(units)
  74. # check to make sure the units are time
  75. # this approach is much faster than comparing the
  76. # reference dimensionality
  77. if (len(dim) != 1 or list(dim.values())[0] != 1 or
  78. not isinstance(list(dim.keys())[0], pq.UnitTime)):
  79. ValueError("Unit %s has dimensions %s, not [time]" %
  80. (units, dim.simplified))
  81. obj = pq.Quantity(times, units=dim).view(cls)
  82. obj.labels = labels
  83. obj.segment = None
  84. return obj
  85. def __init__(self, times=None, labels=None, units=None, name=None, description=None,
  86. file_origin=None, **annotations):
  87. '''
  88. Initialize a new :class:`Event` instance.
  89. '''
  90. BaseNeo.__init__(self, name=name, file_origin=file_origin,
  91. description=description, **annotations)
  92. def __reduce__(self):
  93. '''
  94. Map the __new__ function onto _new_BaseAnalogSignal, so that pickle
  95. works
  96. '''
  97. return _new_event, (self.__class__, self.times, np.array(self), self.labels, self.units,
  98. self.name, self.file_origin, self.description,
  99. self.annotations, self.segment)
  100. def __array_finalize__(self, obj):
  101. super(Event, self).__array_finalize__(obj)
  102. self.labels = getattr(obj, 'labels', None)
  103. self.annotations = getattr(obj, 'annotations', None)
  104. self.name = getattr(obj, 'name', None)
  105. self.file_origin = getattr(obj, 'file_origin', None)
  106. self.description = getattr(obj, 'description', None)
  107. self.segment = getattr(obj, 'segment', None)
  108. def __repr__(self):
  109. '''
  110. Returns a string representing the :class:`Event`.
  111. '''
  112. # need to convert labels to unicode for python 3 or repr is messed up
  113. if PY_VER == 3:
  114. labels = self.labels.astype('U')
  115. else:
  116. labels = self.labels
  117. objs = ['%s@%s' % (label, time) for label, time in zip(labels,
  118. self.times)]
  119. return '<Event: %s>' % ', '.join(objs)
  120. @property
  121. def times(self):
  122. return pq.Quantity(self)
  123. def merge(self, other):
  124. '''
  125. Merge the another :class:`Event` into this one.
  126. The :class:`Event` objects are concatenated horizontally
  127. (column-wise), :func:`np.hstack`).
  128. If the attributes of the two :class:`Event` are not
  129. compatible, and Exception is raised.
  130. '''
  131. othertimes = other.times.rescale(self.times.units)
  132. times = np.hstack([self.times, othertimes]) * self.times.units
  133. labels = np.hstack([self.labels, other.labels])
  134. kwargs = {}
  135. for name in ("name", "description", "file_origin"):
  136. attr_self = getattr(self, name)
  137. attr_other = getattr(other, name)
  138. if attr_self == attr_other:
  139. kwargs[name] = attr_self
  140. else:
  141. kwargs[name] = "merge(%s, %s)" % (attr_self, attr_other)
  142. merged_annotations = merge_annotations(self.annotations,
  143. other.annotations)
  144. kwargs.update(merged_annotations)
  145. return Event(times=times, labels=labels, **kwargs)
  146. def _copy_data_complement(self, other):
  147. '''
  148. Copy the metadata from another :class:`Event`.
  149. '''
  150. for attr in ("labels", "name", "file_origin", "description",
  151. "annotations"):
  152. setattr(self, attr, getattr(other, attr, None))
  153. def duplicate_with_new_data(self, signal):
  154. '''
  155. Create a new :class:`Event` with the same metadata
  156. but different data
  157. '''
  158. new = self.__class__(times=signal)
  159. new._copy_data_complement(self)
  160. return new
  161. def time_slice(self, t_start, t_stop):
  162. '''
  163. Creates a new :class:`Event` corresponding to the time slice of
  164. the original :class:`Event` between (and including) times
  165. :attr:`t_start` and :attr:`t_stop`. Either parameter can also be None
  166. to use infinite endpoints for the time interval.
  167. '''
  168. _t_start = t_start
  169. _t_stop = t_stop
  170. if t_start is None:
  171. _t_start = -np.inf
  172. if t_stop is None:
  173. _t_stop = np.inf
  174. indices = (self >= _t_start) & (self <= _t_stop)
  175. new_evt = self[indices]
  176. return new_evt
  177. def as_array(self, units=None):
  178. """
  179. Return the event times as a plain NumPy array.
  180. If `units` is specified, first rescale to those units.
  181. """
  182. if units:
  183. return self.rescale(units).magnitude
  184. else:
  185. return self.magnitude
  186. def as_quantity(self):
  187. """
  188. Return the event times as a quantities array.
  189. """
  190. return self.view(pq.Quantity)