analogsignal.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538
  1. # -*- coding: utf-8 -*-
  2. '''
  3. This module implements :class:`AnalogSignal`, an array of analog signals.
  4. :class:`AnalogSignal` inherits from :class:`basesignal.BaseSignal` which
  5. derives from :class:`BaseNeo`, and from :class:`quantites.Quantity`which
  6. in turn inherits from :class:`numpy.array`.
  7. Inheritance from :class:`numpy.array` is explained here:
  8. http://docs.scipy.org/doc/numpy/user/basics.subclassing.html
  9. In brief:
  10. * Initialization of a new object from constructor happens in :meth:`__new__`.
  11. This is where user-specified attributes are set.
  12. * :meth:`__array_finalize__` is called for all new objects, including those
  13. created by slicing. This is where attributes are copied over from
  14. the old object.
  15. '''
  16. # needed for python 3 compatibility
  17. from __future__ import absolute_import, division, print_function
  18. import logging
  19. import numpy as np
  20. import quantities as pq
  21. from neo.core.baseneo import BaseNeo, MergeError, merge_annotations
  22. from neo.core.dataobject import DataObject
  23. from neo.core.channelindex import ChannelIndex
  24. from copy import copy, deepcopy
  25. from neo.core.basesignal import BaseSignal
  26. logger = logging.getLogger("Neo")
  27. def _get_sampling_rate(sampling_rate, sampling_period):
  28. '''
  29. Gets the sampling_rate from either the sampling_period or the
  30. sampling_rate, or makes sure they match if both are specified
  31. '''
  32. if sampling_period is None:
  33. if sampling_rate is None:
  34. raise ValueError("You must provide either the sampling rate or " + "sampling period")
  35. elif sampling_rate is None:
  36. sampling_rate = 1.0 / sampling_period
  37. elif sampling_period != 1.0 / sampling_rate:
  38. raise ValueError('The sampling_rate has to be 1/sampling_period')
  39. if not hasattr(sampling_rate, 'units'):
  40. raise TypeError("Sampling rate/sampling period must have units")
  41. return sampling_rate
  42. def _new_AnalogSignalArray(cls, signal, units=None, dtype=None, copy=True, t_start=0 * pq.s,
  43. sampling_rate=None, sampling_period=None, name=None, file_origin=None,
  44. description=None, array_annotations=None, annotations=None,
  45. channel_index=None, segment=None):
  46. '''
  47. A function to map AnalogSignal.__new__ to function that
  48. does not do the unit checking. This is needed for pickle to work.
  49. '''
  50. obj = cls(signal=signal, units=units, dtype=dtype, copy=copy,
  51. t_start=t_start, sampling_rate=sampling_rate,
  52. sampling_period=sampling_period, name=name,
  53. file_origin=file_origin, description=description,
  54. array_annotations=array_annotations, **annotations)
  55. obj.channel_index = channel_index
  56. obj.segment = segment
  57. return obj
  58. class AnalogSignal(BaseSignal):
  59. '''
  60. Array of one or more continuous analog signals.
  61. A representation of several continuous, analog signals that
  62. have the same duration, sampling rate and start time.
  63. Basically, it is a 2D array: dim 0 is time, dim 1 is
  64. channel index
  65. Inherits from :class:`quantities.Quantity`, which in turn inherits from
  66. :class:`numpy.ndarray`.
  67. *Usage*::
  68. >>> from neo.core import AnalogSignal
  69. >>> import quantities as pq
  70. >>>
  71. >>> sigarr = AnalogSignal([[1, 2, 3], [4, 5, 6]], units='V',
  72. ... sampling_rate=1*pq.Hz)
  73. >>>
  74. >>> sigarr
  75. <AnalogSignal(array([[1, 2, 3],
  76. [4, 5, 6]]) * mV, [0.0 s, 2.0 s], sampling rate: 1.0 Hz)>
  77. >>> sigarr[:,1]
  78. <AnalogSignal(array([2, 5]) * V, [0.0 s, 2.0 s],
  79. sampling rate: 1.0 Hz)>
  80. >>> sigarr[1, 1]
  81. array(5) * V
  82. *Required attributes/properties*:
  83. :signal: (quantity array 2D, numpy array 2D, or list (data, channel))
  84. The data itself.
  85. :units: (quantity units) Required if the signal is a list or NumPy
  86. array, not if it is a :class:`Quantity`
  87. :t_start: (quantity scalar) Time when signal begins
  88. :sampling_rate: *or* **sampling_period** (quantity scalar) Number of
  89. samples per unit time or
  90. interval between two samples.
  91. If both are specified, they are
  92. checked for consistency.
  93. *Recommended attributes/properties*:
  94. :name: (str) A label for the dataset.
  95. :description: (str) Text description.
  96. :file_origin: (str) Filesystem path or URL of the original data file.
  97. *Optional attributes/properties*:
  98. :dtype: (numpy dtype or str) Override the dtype of the signal array.
  99. :copy: (bool) True by default.
  100. :array_annotations: (dict) Dict mapping strings to numpy arrays containing annotations \
  101. for all data points
  102. Note: Any other additional arguments are assumed to be user-specific
  103. metadata and stored in :attr:`annotations`.
  104. *Properties available on this object*:
  105. :sampling_rate: (quantity scalar) Number of samples per unit time.
  106. (1/:attr:`sampling_period`)
  107. :sampling_period: (quantity scalar) Interval between two samples.
  108. (1/:attr:`quantity scalar`)
  109. :duration: (Quantity) Signal duration, read-only.
  110. (size * :attr:`sampling_period`)
  111. :t_stop: (quantity scalar) Time when signal ends, read-only.
  112. (:attr:`t_start` + :attr:`duration`)
  113. :times: (quantity 1D) The time points of each sample of the signal,
  114. read-only.
  115. (:attr:`t_start` + arange(:attr:`shape`[0])/:attr:`sampling_rate`)
  116. :channel_index:
  117. access to the channel_index attribute of the principal ChannelIndex
  118. associated with this signal.
  119. *Slicing*:
  120. :class:`AnalogSignal` objects can be sliced. When taking a single
  121. column (dimension 0, e.g. [0, :]) or a single element,
  122. a :class:`~quantities.Quantity` is returned.
  123. Otherwise an :class:`AnalogSignal` (actually a view) is
  124. returned, with the same metadata, except that :attr:`t_start`
  125. is changed if the start index along dimension 1 is greater than 1.
  126. Note that slicing an :class:`AnalogSignal` may give a different
  127. result to slicing the underlying NumPy array since signals
  128. are always two-dimensional.
  129. *Operations available on this object*:
  130. == != + * /
  131. '''
  132. _single_parent_objects = ('Segment', 'ChannelIndex')
  133. _quantity_attr = 'signal'
  134. _necessary_attrs = (('signal', pq.Quantity, 2),
  135. ('sampling_rate', pq.Quantity, 0),
  136. ('t_start', pq.Quantity, 0))
  137. _recommended_attrs = BaseNeo._recommended_attrs
  138. def __new__(cls, signal, units=None, dtype=None, copy=True, t_start=0 * pq.s,
  139. sampling_rate=None, sampling_period=None, name=None, file_origin=None,
  140. description=None, array_annotations=None, **annotations):
  141. '''
  142. Constructs new :class:`AnalogSignal` from data.
  143. This is called whenever a new class:`AnalogSignal` is created from
  144. the constructor, but not when slicing.
  145. __array_finalize__ is called on the new object.
  146. '''
  147. signal = cls._rescale(signal, units=units)
  148. obj = pq.Quantity(signal, units=units, dtype=dtype, copy=copy).view(cls)
  149. if obj.ndim == 1:
  150. obj.shape = (-1, 1)
  151. if t_start is None:
  152. raise ValueError('t_start cannot be None')
  153. obj._t_start = t_start
  154. obj._sampling_rate = _get_sampling_rate(sampling_rate, sampling_period)
  155. obj.segment = None
  156. obj.channel_index = None
  157. return obj
  158. def __init__(self, signal, units=None, dtype=None, copy=True, t_start=0 * pq.s,
  159. sampling_rate=None, sampling_period=None, name=None, file_origin=None,
  160. description=None, array_annotations=None, **annotations):
  161. '''
  162. Initializes a newly constructed :class:`AnalogSignal` instance.
  163. '''
  164. # This method is only called when constructing a new AnalogSignal,
  165. # not when slicing or viewing. We use the same call signature
  166. # as __new__ for documentation purposes. Anything not in the call
  167. # signature is stored in annotations.
  168. # Calls parent __init__, which grabs universally recommended
  169. # attributes and sets up self.annotations
  170. DataObject.__init__(self, name=name, file_origin=file_origin, description=description,
  171. array_annotations=array_annotations, **annotations)
  172. def __reduce__(self):
  173. '''
  174. Map the __new__ function onto _new_AnalogSignalArray, so that pickle
  175. works
  176. '''
  177. return _new_AnalogSignalArray, (self.__class__, np.array(self), self.units, self.dtype,
  178. True, self.t_start, self.sampling_rate,
  179. self.sampling_period, self.name, self.file_origin,
  180. self.description, self.array_annotations,
  181. self.annotations, self.channel_index, self.segment)
  182. def _array_finalize_spec(self, obj):
  183. '''
  184. Set default values for attributes specific to :class:`AnalogSignal`.
  185. Common attributes are defined in
  186. :meth:`__array_finalize__` in :class:`basesignal.BaseSignal`),
  187. which is called every time a new signal is created
  188. and calls this method.
  189. '''
  190. self._t_start = getattr(obj, '_t_start', 0 * pq.s)
  191. self._sampling_rate = getattr(obj, '_sampling_rate', None)
  192. return obj
  193. def __deepcopy__(self, memo):
  194. cls = self.__class__
  195. new_signal = cls(np.array(self), units=self.units, dtype=self.dtype, t_start=self.t_start,
  196. sampling_rate=self.sampling_rate, sampling_period=self.sampling_period,
  197. name=self.name, file_origin=self.file_origin,
  198. description=self.description)
  199. new_signal.__dict__.update(self.__dict__)
  200. memo[id(self)] = new_signal
  201. for k, v in self.__dict__.items():
  202. try:
  203. setattr(new_signal, k, deepcopy(v, memo))
  204. except TypeError:
  205. setattr(new_signal, k, v)
  206. return new_signal
  207. def __repr__(self):
  208. '''
  209. Returns a string representing the :class:`AnalogSignal`.
  210. '''
  211. return ('<%s(%s, [%s, %s], sampling rate: %s)>' % (self.__class__.__name__,
  212. super(AnalogSignal, self).__repr__(),
  213. self.t_start, self.t_stop,
  214. self.sampling_rate))
  215. def get_channel_index(self):
  216. """
  217. """
  218. if self.channel_index:
  219. return self.channel_index.index
  220. else:
  221. return None
  222. def __getitem__(self, i):
  223. '''
  224. Get the item or slice :attr:`i`.
  225. '''
  226. if isinstance(i, (int, np.integer)): # a single point in time across all channels
  227. obj = super(AnalogSignal, self).__getitem__(i)
  228. obj = pq.Quantity(obj.magnitude, units=obj.units)
  229. elif isinstance(i, tuple):
  230. obj = super(AnalogSignal, self).__getitem__(i)
  231. j, k = i
  232. if isinstance(j, (int, np.integer)): # extract a quantity array
  233. obj = pq.Quantity(obj.magnitude, units=obj.units)
  234. else:
  235. if isinstance(j, slice):
  236. if j.start:
  237. obj.t_start = (self.t_start + j.start * self.sampling_period)
  238. if j.step:
  239. obj.sampling_period *= j.step
  240. elif isinstance(j, np.ndarray):
  241. raise NotImplementedError(
  242. "Arrays not yet supported") # in the general case, would need to return
  243. # IrregularlySampledSignal(Array)
  244. else:
  245. raise TypeError("%s not supported" % type(j))
  246. if isinstance(k, (int, np.integer)):
  247. obj = obj.reshape(-1, 1)
  248. if self.channel_index:
  249. obj.channel_index = self.channel_index.__getitem__(k)
  250. obj.array_annotate(**deepcopy(self.array_annotations_at_index(k)))
  251. elif isinstance(i, slice):
  252. obj = super(AnalogSignal, self).__getitem__(i)
  253. if i.start:
  254. obj.t_start = self.t_start + i.start * self.sampling_period
  255. obj.array_annotations = deepcopy(self.array_annotations)
  256. elif isinstance(i, np.ndarray):
  257. # Indexing of an AnalogSignal is only consistent if the resulting number of
  258. # samples is the same for each trace. The time axis for these samples is not
  259. # guaranteed to be continuous, so returning a Quantity instead of an AnalogSignal here.
  260. new_time_dims = np.sum(i, axis=0)
  261. if len(new_time_dims) and all(new_time_dims == new_time_dims[0]):
  262. obj = np.asarray(self).T.__getitem__(i.T)
  263. obj = obj.T.reshape(self.shape[1], -1).T
  264. obj = pq.Quantity(obj, units=self.units)
  265. else:
  266. raise IndexError("indexing of an AnalogSignals needs to keep the same number of "
  267. "sample for each trace contained")
  268. else:
  269. raise IndexError("index should be an integer, tuple, slice or boolean numpy array")
  270. return obj
  271. def __setitem__(self, i, value):
  272. """
  273. Set an item or slice defined by :attr:`i` to `value`.
  274. """
  275. # because AnalogSignals are always at least two-dimensional,
  276. # we need to handle the case where `i` is an integer
  277. if isinstance(i, int):
  278. i = slice(i, i + 1)
  279. elif isinstance(i, tuple):
  280. j, k = i
  281. if isinstance(k, int):
  282. i = (j, slice(k, k + 1))
  283. return super(AnalogSignal, self).__setitem__(i, value)
  284. # sampling_rate attribute is handled as a property so type checking can
  285. # be done
  286. @property
  287. def sampling_rate(self):
  288. '''
  289. Number of samples per unit time.
  290. (1/:attr:`sampling_period`)
  291. '''
  292. return self._sampling_rate
  293. @sampling_rate.setter
  294. def sampling_rate(self, rate):
  295. '''
  296. Setter for :attr:`sampling_rate`
  297. '''
  298. if rate is None:
  299. raise ValueError('sampling_rate cannot be None')
  300. elif not hasattr(rate, 'units'):
  301. raise ValueError('sampling_rate must have units')
  302. self._sampling_rate = rate
  303. # sampling_period attribute is handled as a property on underlying rate
  304. @property
  305. def sampling_period(self):
  306. '''
  307. Interval between two samples.
  308. (1/:attr:`sampling_rate`)
  309. '''
  310. return 1. / self.sampling_rate
  311. @sampling_period.setter
  312. def sampling_period(self, period):
  313. '''
  314. Setter for :attr:`sampling_period`
  315. '''
  316. if period is None:
  317. raise ValueError('sampling_period cannot be None')
  318. elif not hasattr(period, 'units'):
  319. raise ValueError('sampling_period must have units')
  320. self.sampling_rate = 1. / period
  321. # t_start attribute is handled as a property so type checking can be done
  322. @property
  323. def t_start(self):
  324. '''
  325. Time when signal begins.
  326. '''
  327. return self._t_start
  328. @t_start.setter
  329. def t_start(self, start):
  330. '''
  331. Setter for :attr:`t_start`
  332. '''
  333. if start is None:
  334. raise ValueError('t_start cannot be None')
  335. self._t_start = start
  336. @property
  337. def duration(self):
  338. '''
  339. Signal duration
  340. (:attr:`size` * :attr:`sampling_period`)
  341. '''
  342. return self.shape[0] / self.sampling_rate
  343. @property
  344. def t_stop(self):
  345. '''
  346. Time when signal ends.
  347. (:attr:`t_start` + :attr:`duration`)
  348. '''
  349. return self.t_start + self.duration
  350. @property
  351. def times(self):
  352. '''
  353. The time points of each sample of the signal
  354. (:attr:`t_start` + arange(:attr:`shape`)/:attr:`sampling_rate`)
  355. '''
  356. return self.t_start + np.arange(self.shape[0]) / self.sampling_rate
  357. def __eq__(self, other):
  358. '''
  359. Equality test (==)
  360. '''
  361. if (isinstance(other, AnalogSignal) and (
  362. self.t_start != other.t_start or self.sampling_rate != other.sampling_rate)):
  363. return False
  364. return super(AnalogSignal, self).__eq__(other)
  365. def _check_consistency(self, other):
  366. '''
  367. Check if the attributes of another :class:`AnalogSignal`
  368. are compatible with this one.
  369. '''
  370. if isinstance(other, AnalogSignal):
  371. for attr in "t_start", "sampling_rate":
  372. if getattr(self, attr) != getattr(other, attr):
  373. raise ValueError(
  374. "Inconsistent values of %s" % attr) # how to handle name and annotations?
  375. def _repr_pretty_(self, pp, cycle):
  376. '''
  377. Handle pretty-printing the :class:`AnalogSignal`.
  378. '''
  379. pp.text("{cls} with {channels} channels of length {length}; "
  380. "units {units}; datatype {dtype} ".format(cls=self.__class__.__name__,
  381. channels=self.shape[1],
  382. length=self.shape[0],
  383. units=self.units.dimensionality.string,
  384. dtype=self.dtype))
  385. if self._has_repr_pretty_attrs_():
  386. pp.breakable()
  387. self._repr_pretty_attrs_(pp, cycle)
  388. def _pp(line):
  389. pp.breakable()
  390. with pp.group(indent=1):
  391. pp.text(line)
  392. for line in ["sampling rate: {0}".format(self.sampling_rate),
  393. "time: {0} to {1}".format(self.t_start, self.t_stop)]:
  394. _pp(line)
  395. def time_index(self, t):
  396. """Return the array index corresponding to the time `t`"""
  397. t = t.rescale(self.sampling_period.units)
  398. i = (t - self.t_start) / self.sampling_period
  399. i = int(np.rint(i.magnitude))
  400. return i
  401. def time_slice(self, t_start, t_stop):
  402. '''
  403. Creates a new AnalogSignal corresponding to the time slice of the
  404. original AnalogSignal between times t_start, t_stop. Note, that for
  405. numerical stability reasons if t_start, t_stop do not fall exactly on
  406. the time bins defined by the sampling_period they will be rounded to
  407. the nearest sampling bins.
  408. '''
  409. # checking start time and transforming to start index
  410. if t_start is None:
  411. i = 0
  412. else:
  413. i = self.time_index(t_start)
  414. # checking stop time and transforming to stop index
  415. if t_stop is None:
  416. j = len(self)
  417. else:
  418. j = self.time_index(t_stop)
  419. if (i < 0) or (j > len(self)):
  420. raise ValueError('t_start, t_stop have to be withing the analog \
  421. signal duration')
  422. # we're going to send the list of indicies so that we get *copy* of the
  423. # sliced data
  424. obj = super(AnalogSignal, self).__getitem__(np.arange(i, j, 1))
  425. # If there is any data remaining, there will be data for every channel
  426. # In this case, array_annotations need to stay available
  427. # super.__getitem__ cannot do this, so it needs to be done here
  428. if len(obj) > 0:
  429. obj.array_annotations = self.array_annotations
  430. obj.t_start = self.t_start + i * self.sampling_period
  431. return obj
  432. def splice(self, signal, copy=False):
  433. """
  434. Replace part of the current signal by a new piece of signal.
  435. The new piece of signal will overwrite part of the current signal
  436. starting at the time given by the new piece's `t_start` attribute.
  437. The signal to be spliced in must have the same physical dimensions,
  438. sampling rate, and number of channels as the current signal and
  439. fit within it.
  440. If `copy` is False (the default), modify the current signal in place.
  441. If `copy` is True, return a new signal and leave the current one untouched.
  442. In this case, the new signal will not be linked to any parent objects.
  443. """
  444. if signal.t_start < self.t_start:
  445. raise ValueError("Cannot splice earlier than the start of the signal")
  446. if signal.t_stop > self.t_stop:
  447. raise ValueError("Splice extends beyond signal")
  448. if signal.sampling_rate != self.sampling_rate:
  449. raise ValueError("Sampling rates do not match")
  450. i = self.time_index(signal.t_start)
  451. j = i + signal.shape[0]
  452. if copy:
  453. new_signal = deepcopy(self)
  454. new_signal.segment = None
  455. new_signal.channel_index = None
  456. new_signal[i:j, :] = signal
  457. return new_signal
  458. else:
  459. self[i:j, :] = signal
  460. return self