analogsignal.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. # -*- coding: utf-8 -*-
  2. '''
  3. This module implements :class:`AnalogSignal`, an array of analog signals.
  4. :class:`AnalogSignal` inherits from :class:`basesignal.BaseSignal` and
  5. :class:`quantities.Quantity`, which inherits from :class:`numpy.array`.
  6. Inheritance from :class:`numpy.array` is explained here:
  7. http://docs.scipy.org/doc/numpy/user/basics.subclassing.html
  8. In brief:
  9. * Initialization of a new object from constructor happens in :meth:`__new__`.
  10. This is where user-specified attributes are set.
  11. * :meth:`__array_finalize__` is called for all new objects, including those
  12. created by slicing. This is where attributes are copied over from
  13. the old object.
  14. '''
  15. # needed for python 3 compatibility
  16. from __future__ import absolute_import, division, print_function
  17. import logging
  18. import numpy as np
  19. import quantities as pq
  20. from neo.core.baseneo import BaseNeo, MergeError, merge_annotations
  21. from neo.core.channelindex import ChannelIndex
  22. from copy import copy, deepcopy
  23. logger = logging.getLogger("Neo")
  24. from neo.core import basesignal
  25. def _get_sampling_rate(sampling_rate, sampling_period):
  26. '''
  27. Gets the sampling_rate from either the sampling_period or the
  28. sampling_rate, or makes sure they match if both are specified
  29. '''
  30. if sampling_period is None:
  31. if sampling_rate is None:
  32. raise ValueError("You must provide either the sampling rate or " +
  33. "sampling period")
  34. elif sampling_rate is None:
  35. sampling_rate = 1.0 / sampling_period
  36. elif sampling_period != 1.0 / sampling_rate:
  37. raise ValueError('The sampling_rate has to be 1/sampling_period')
  38. if not hasattr(sampling_rate, 'units'):
  39. raise TypeError("Sampling rate/sampling period must have units")
  40. return sampling_rate
  41. def _new_AnalogSignalArray(cls, signal, units=None, dtype=None, copy=True,
  42. t_start=0*pq.s, sampling_rate=None,
  43. sampling_period=None, name=None, file_origin=None,
  44. description=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. **annotations)
  55. obj.channel_index = channel_index
  56. obj.segment = segment
  57. return obj
  58. class AnalogSignal(basesignal.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. Note: Any other additional arguments are assumed to be user-specific
  101. metadata and stored in :attr:`annotations`.
  102. *Properties available on this object*:
  103. :sampling_rate: (quantity scalar) Number of samples per unit time.
  104. (1/:attr:`sampling_period`)
  105. :sampling_period: (quantity scalar) Interval between two samples.
  106. (1/:attr:`quantity scalar`)
  107. :duration: (Quantity) Signal duration, read-only.
  108. (size * :attr:`sampling_period`)
  109. :t_stop: (quantity scalar) Time when signal ends, read-only.
  110. (:attr:`t_start` + :attr:`duration`)
  111. :times: (quantity 1D) The time points of each sample of the signal,
  112. read-only.
  113. (:attr:`t_start` + arange(:attr:`shape`[0])/:attr:`sampling_rate`)
  114. :channel_index:
  115. access to the channel_index attribute of the principal ChannelIndex
  116. associated with this signal.
  117. *Slicing*:
  118. :class:`AnalogSignal` objects can be sliced. When taking a single
  119. column (dimension 0, e.g. [0, :]) or a single element,
  120. a :class:`~quantities.Quantity` is returned.
  121. Otherwise an :class:`AnalogSignal` (actually a view) is
  122. returned, with the same metadata, except that :attr:`t_start`
  123. is changed if the start index along dimension 1 is greater than 1.
  124. *Operations available on this object*:
  125. == != + * /
  126. '''
  127. _single_parent_objects = ('Segment', 'ChannelIndex')
  128. _quantity_attr = 'signal'
  129. _necessary_attrs = (('signal', pq.Quantity, 2),
  130. ('sampling_rate', pq.Quantity, 0),
  131. ('t_start', pq.Quantity, 0))
  132. _recommended_attrs = BaseNeo._recommended_attrs
  133. def __new__(cls, signal, units=None, dtype=None, copy=True,
  134. t_start=0 * pq.s, sampling_rate=None, sampling_period=None,
  135. name=None, file_origin=None, description=None,
  136. **annotations):
  137. '''
  138. Constructs new :class:`AnalogSignal` from data.
  139. This is called whenever a new class:`AnalogSignal` is created from
  140. the constructor, but not when slicing.
  141. __array_finalize__ is called on the new object.
  142. '''
  143. if units is None:
  144. if not hasattr(signal, "units"):
  145. raise ValueError("Units must be specified")
  146. elif isinstance(signal, pq.Quantity):
  147. # could improve this test, what if units is a string?
  148. if units != signal.units:
  149. signal = signal.rescale(units)
  150. obj = pq.Quantity(signal, units=units, dtype=dtype, copy=copy).view(cls)
  151. if obj.ndim == 1:
  152. obj.shape = (-1, 1)
  153. if t_start is None:
  154. raise ValueError('t_start cannot be None')
  155. obj._t_start = t_start
  156. obj._sampling_rate = _get_sampling_rate(sampling_rate, sampling_period)
  157. obj.segment = None
  158. obj.channel_index = None
  159. return obj
  160. def __init__(self, signal, units=None, dtype=None, copy=True,
  161. t_start=0 * pq.s, sampling_rate=None, sampling_period=None,
  162. name=None, file_origin=None, description=None,
  163. **annotations):
  164. '''
  165. Initializes a newly constructed :class:`AnalogSignal` instance.
  166. '''
  167. # This method is only called when constructing a new AnalogSignal,
  168. # not when slicing or viewing. We use the same call signature
  169. # as __new__ for documentation purposes. Anything not in the call
  170. # signature is stored in annotations.
  171. # Calls parent __init__, which grabs universally recommended
  172. # attributes and sets up self.annotations
  173. BaseNeo.__init__(self, name=name, file_origin=file_origin,
  174. description=description, **annotations)
  175. def __reduce__(self):
  176. '''
  177. Map the __new__ function onto _new_AnalogSignalArray, so that pickle
  178. works
  179. '''
  180. return _new_AnalogSignalArray, (self.__class__,
  181. np.array(self),
  182. self.units,
  183. self.dtype,
  184. True,
  185. self.t_start,
  186. self.sampling_rate,
  187. self.sampling_period,
  188. self.name,
  189. self.file_origin,
  190. self.description,
  191. self.annotations,
  192. self.channel_index,
  193. self.segment)
  194. def __deepcopy__(self, memo):
  195. cls = self.__class__
  196. new_AS = cls(np.array(self), units=self.units, dtype=self.dtype,
  197. t_start=self.t_start, sampling_rate=self.sampling_rate,
  198. sampling_period=self.sampling_period, name=self.name,
  199. file_origin=self.file_origin, description=self.description)
  200. new_AS.__dict__.update(self.__dict__)
  201. memo[id(self)] = new_AS
  202. for k, v in self.__dict__.items():
  203. try:
  204. setattr(new_AS, k, deepcopy(v, memo))
  205. except:
  206. setattr(new_AS, k, v)
  207. return new_AS
  208. def __array_finalize__(self, obj):
  209. '''
  210. This is called every time a new :class:`AnalogSignal` is created.
  211. It is the appropriate place to set default values for attributes
  212. for :class:`AnalogSignal` constructed by slicing or viewing.
  213. User-specified values are only relevant for construction from
  214. constructor, and these are set in __new__. Then they are just
  215. copied over here.
  216. '''
  217. super(AnalogSignal, self).__array_finalize__(obj)
  218. self._t_start = getattr(obj, '_t_start', 0 * pq.s)
  219. self._sampling_rate = getattr(obj, '_sampling_rate', None)
  220. # The additional arguments
  221. self.annotations = getattr(obj, 'annotations', {})
  222. # Globally recommended attributes
  223. self.name = getattr(obj, 'name', None)
  224. self.file_origin = getattr(obj, 'file_origin', None)
  225. self.description = getattr(obj, 'description', None)
  226. # Parent objects
  227. self.segment = getattr(obj, 'segment', None)
  228. self.channel_index = getattr(obj, 'channel_index', None)
  229. def __repr__(self):
  230. '''
  231. Returns a string representing the :class:`AnalogSignal`.
  232. '''
  233. return ('<%s(%s, [%s, %s], sampling rate: %s)>' %
  234. (self.__class__.__name__,
  235. super(AnalogSignal, self).__repr__(), self.t_start,
  236. self.t_stop, self.sampling_rate))
  237. def get_channel_index(self):
  238. """
  239. """
  240. if self.channel_index:
  241. return self.channel_index.index
  242. else:
  243. return None
  244. def __getslice__(self, i, j):
  245. '''
  246. Get a slice from :attr:`i` to :attr:`j`.
  247. Doesn't get called in Python 3, :meth:`__getitem__` is called instead
  248. '''
  249. return self.__getitem__(slice(i, j))
  250. def __getitem__(self, i):
  251. '''
  252. Get the item or slice :attr:`i`.
  253. '''
  254. obj = super(AnalogSignal, self).__getitem__(i)
  255. if isinstance(i, (int, np.integer)): # a single point in time across all channels
  256. obj = pq.Quantity(obj.magnitude, units=obj.units)
  257. elif isinstance(i, tuple):
  258. j, k = i
  259. if isinstance(j, (int, np.integer)): # extract a quantity array
  260. obj = pq.Quantity(obj.magnitude, units=obj.units)
  261. else:
  262. if isinstance(j, slice):
  263. if j.start:
  264. obj.t_start = (self.t_start +
  265. j.start * self.sampling_period)
  266. if j.step:
  267. obj.sampling_period *= j.step
  268. elif isinstance(j, np.ndarray):
  269. raise NotImplementedError("Arrays not yet supported")
  270. # in the general case, would need to return IrregularlySampledSignal(Array)
  271. else:
  272. raise TypeError("%s not supported" % type(j))
  273. if isinstance(k, (int, np.integer)):
  274. obj = obj.reshape(-1, 1)
  275. if self.channel_index:
  276. obj.channel_index = self.channel_index.__getitem__(k)
  277. elif isinstance(i, slice):
  278. if i.start:
  279. obj.t_start = self.t_start + i.start * self.sampling_period
  280. else:
  281. raise IndexError("index should be an integer, tuple or slice")
  282. return obj
  283. def __setitem__(self, i, value):
  284. """
  285. Set an item or slice defined by :attr:`i` to `value`.
  286. """
  287. # because AnalogSignals are always at least two-dimensional,
  288. # we need to handle the case where `i` is an integer
  289. if isinstance(i, int):
  290. i = slice(i, i + 1)
  291. elif isinstance(i, tuple):
  292. j, k = i
  293. if isinstance(k, int):
  294. i = (j, slice(k, k + 1))
  295. return super(AnalogSignal, self).__setitem__(i, value)
  296. # sampling_rate attribute is handled as a property so type checking can
  297. # be done
  298. @property
  299. def sampling_rate(self):
  300. '''
  301. Number of samples per unit time.
  302. (1/:attr:`sampling_period`)
  303. '''
  304. return self._sampling_rate
  305. @sampling_rate.setter
  306. def sampling_rate(self, rate):
  307. '''
  308. Setter for :attr:`sampling_rate`
  309. '''
  310. if rate is None:
  311. raise ValueError('sampling_rate cannot be None')
  312. elif not hasattr(rate, 'units'):
  313. raise ValueError('sampling_rate must have units')
  314. self._sampling_rate = rate
  315. # sampling_period attribute is handled as a property on underlying rate
  316. @property
  317. def sampling_period(self):
  318. '''
  319. Interval between two samples.
  320. (1/:attr:`sampling_rate`)
  321. '''
  322. return 1. / self.sampling_rate
  323. @sampling_period.setter
  324. def sampling_period(self, period):
  325. '''
  326. Setter for :attr:`sampling_period`
  327. '''
  328. if period is None:
  329. raise ValueError('sampling_period cannot be None')
  330. elif not hasattr(period, 'units'):
  331. raise ValueError('sampling_period must have units')
  332. self.sampling_rate = 1. / period
  333. # t_start attribute is handled as a property so type checking can be done
  334. @property
  335. def t_start(self):
  336. '''
  337. Time when signal begins.
  338. '''
  339. return self._t_start
  340. @t_start.setter
  341. def t_start(self, start):
  342. '''
  343. Setter for :attr:`t_start`
  344. '''
  345. if start is None:
  346. raise ValueError('t_start cannot be None')
  347. self._t_start = start
  348. @property
  349. def duration(self):
  350. '''
  351. Signal duration
  352. (:attr:`size` * :attr:`sampling_period`)
  353. '''
  354. return self.shape[0] / self.sampling_rate
  355. @property
  356. def t_stop(self):
  357. '''
  358. Time when signal ends.
  359. (:attr:`t_start` + :attr:`duration`)
  360. '''
  361. return self.t_start + self.duration
  362. @property
  363. def times(self):
  364. '''
  365. The time points of each sample of the signal
  366. (:attr:`t_start` + arange(:attr:`shape`)/:attr:`sampling_rate`)
  367. '''
  368. return self.t_start + np.arange(self.shape[0]) / self.sampling_rate
  369. def rescale(self, units):
  370. '''
  371. Return a copy of the AnalogSignal converted to the specified
  372. units
  373. '''
  374. to_dims = pq.quantity.validate_dimensionality(units)
  375. if self.dimensionality == to_dims:
  376. to_u = self.units
  377. signal = np.array(self)
  378. else:
  379. to_u = pq.Quantity(1.0, to_dims)
  380. from_u = pq.Quantity(1.0, self.dimensionality)
  381. try:
  382. cf = pq.quantity.get_conversion_factor(from_u, to_u)
  383. except AssertionError:
  384. raise ValueError('Unable to convert between units of "%s" \
  385. and "%s"' % (from_u._dimensionality,
  386. to_u._dimensionality))
  387. signal = cf * self.magnitude
  388. new = self.__class__(signal=signal, units=to_u,
  389. sampling_rate=self.sampling_rate)
  390. new._copy_data_complement(self)
  391. new.channel_index = self.channel_index
  392. new.segment = self.segment
  393. new.annotations.update(self.annotations)
  394. return new
  395. def duplicate_with_new_array(self, signal):
  396. '''
  397. Create a new :class:`AnalogSignal` with the same metadata
  398. but different data
  399. '''
  400. #signal is the new signal
  401. new = self.__class__(signal=signal, units=self.units,
  402. sampling_rate=self.sampling_rate)
  403. new._copy_data_complement(self)
  404. new.annotations.update(self.annotations)
  405. return new
  406. def __eq__(self, other):
  407. '''
  408. Equality test (==)
  409. '''
  410. if (self.t_start != other.t_start or
  411. self.sampling_rate != other.sampling_rate):
  412. return False
  413. return super(AnalogSignal, self).__eq__(other)
  414. def _check_consistency(self, other):
  415. '''
  416. Check if the attributes of another :class:`AnalogSignal`
  417. are compatible with this one.
  418. '''
  419. if isinstance(other, AnalogSignal):
  420. for attr in "t_start", "sampling_rate":
  421. if getattr(self, attr) != getattr(other, attr):
  422. raise ValueError("Inconsistent values of %s" % attr)
  423. # how to handle name and annotations?
  424. def _copy_data_complement(self, other):
  425. '''
  426. Copy the metadata from another :class:`AnalogSignal`.
  427. '''
  428. for attr in ("t_start", "sampling_rate", "name", "file_origin",
  429. "description", "annotations"):
  430. setattr(self, attr, getattr(other, attr, None))
  431. def __rsub__(self, other, *args):
  432. '''
  433. Backwards subtraction (other-self)
  434. '''
  435. return self.__mul__(-1, *args) + other
  436. def _repr_pretty_(self, pp, cycle):
  437. '''
  438. Handle pretty-printing the :class:`AnalogSignal`.
  439. '''
  440. pp.text("{cls} with {channels} channels of length {length}; "
  441. "units {units}; datatype {dtype} ".format(
  442. cls=self.__class__.__name__,
  443. channels=self.shape[1],
  444. length=self.shape[0],
  445. units=self.units.dimensionality.string,
  446. dtype=self.dtype))
  447. if self._has_repr_pretty_attrs_():
  448. pp.breakable()
  449. self._repr_pretty_attrs_(pp, cycle)
  450. def _pp(line):
  451. pp.breakable()
  452. with pp.group(indent=1):
  453. pp.text(line)
  454. for line in ["sampling rate: {0}".format(self.sampling_rate),
  455. "time: {0} to {1}".format(self.t_start, self.t_stop)
  456. ]:
  457. _pp(line)
  458. def time_slice(self, t_start, t_stop):
  459. '''
  460. Creates a new AnalogSignal corresponding to the time slice of the
  461. original AnalogSignal between times t_start, t_stop. Note, that for
  462. numerical stability reasons if t_start, t_stop do not fall exactly on
  463. the time bins defined by the sampling_period they will be rounded to
  464. the nearest sampling bins.
  465. '''
  466. # checking start time and transforming to start index
  467. if t_start is None:
  468. i = 0
  469. else:
  470. t_start = t_start.rescale(self.sampling_period.units)
  471. i = (t_start - self.t_start) / self.sampling_period
  472. i = int(np.rint(i.magnitude))
  473. # checking stop time and transforming to stop index
  474. if t_stop is None:
  475. j = len(self)
  476. else:
  477. t_stop = t_stop.rescale(self.sampling_period.units)
  478. j = (t_stop - self.t_start) / self.sampling_period
  479. j = int(np.rint(j.magnitude))
  480. if (i < 0) or (j > len(self)):
  481. raise ValueError('t_start, t_stop have to be withing the analog \
  482. signal duration')
  483. # we're going to send the list of indicies so that we get *copy* of the
  484. # sliced data
  485. obj = super(AnalogSignal, self).__getitem__(np.arange(i, j, 1))
  486. obj.t_start = self.t_start + i * self.sampling_period
  487. return obj
  488. def merge(self, other):
  489. '''
  490. Merge another :class:`AnalogSignal` into this one.
  491. The :class:`AnalogSignal` objects are concatenated horizontally
  492. (column-wise, :func:`np.hstack`).
  493. If the attributes of the two :class:`AnalogSignal` are not
  494. compatible, an Exception is raised.
  495. '''
  496. if self.sampling_rate != other.sampling_rate:
  497. raise MergeError("Cannot merge, different sampling rates")
  498. if self.t_start != other.t_start:
  499. raise MergeError("Cannot merge, different t_start")
  500. if self.segment != other.segment:
  501. raise MergeError("Cannot merge these two signals as they belong to different segments.")
  502. if hasattr(self, "lazy_shape"):
  503. if hasattr(other, "lazy_shape"):
  504. if self.lazy_shape[0] != other.lazy_shape[0]:
  505. raise MergeError("Cannot merge signals of different length.")
  506. merged_lazy_shape = (self.lazy_shape[0], self.lazy_shape[1] + other.lazy_shape[1])
  507. else:
  508. raise MergeError("Cannot merge a lazy object with a real object.")
  509. if other.units != self.units:
  510. other = other.rescale(self.units)
  511. stack = np.hstack(map(np.array, (self, other)))
  512. kwargs = {}
  513. for name in ("name", "description", "file_origin"):
  514. attr_self = getattr(self, name)
  515. attr_other = getattr(other, name)
  516. if attr_self == attr_other:
  517. kwargs[name] = attr_self
  518. else:
  519. kwargs[name] = "merge(%s, %s)" % (attr_self, attr_other)
  520. merged_annotations = merge_annotations(self.annotations,
  521. other.annotations)
  522. kwargs.update(merged_annotations)
  523. signal = AnalogSignal(stack, units=self.units, dtype=self.dtype,
  524. copy=False, t_start=self.t_start,
  525. sampling_rate=self.sampling_rate,
  526. **kwargs)
  527. signal.segment = self.segment
  528. # merge channel_index (move to ChannelIndex.merge()?)
  529. if self.channel_index and other.channel_index:
  530. signal.channel_index = ChannelIndex(
  531. index=np.arange(signal.shape[1]),
  532. channel_ids=np.hstack([self.channel_index.channel_ids,
  533. other.channel_index.channel_ids]),
  534. channel_names=np.hstack([self.channel_index.channel_names,
  535. other.channel_index.channel_names]))
  536. else:
  537. signal.channel_index = ChannelIndex(index=np.arange(signal.shape[1]))
  538. if hasattr(self, "lazy_shape"):
  539. signal.lazy_shape = merged_lazy_shape
  540. return signal