analogsignal.py 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. '''
  2. This module implements :class:`AnalogSignal`, an array of analog signals.
  3. :class:`AnalogSignal` inherits from :class:`basesignal.BaseSignal` which
  4. derives from :class:`BaseNeo`, and from :class:`quantites.Quantity`which
  5. in turn 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. import logging
  16. try:
  17. import scipy.signal
  18. except ImportError as err:
  19. HAVE_SCIPY = False
  20. else:
  21. HAVE_SCIPY = True
  22. import numpy as np
  23. import quantities as pq
  24. from neo.core.baseneo import BaseNeo, MergeError, merge_annotations, intersect_annotations
  25. from neo.core.dataobject import DataObject
  26. from copy import copy, deepcopy
  27. from neo.core.basesignal import BaseSignal
  28. logger = logging.getLogger("Neo")
  29. def _get_sampling_rate(sampling_rate, sampling_period):
  30. '''
  31. Gets the sampling_rate from either the sampling_period or the
  32. sampling_rate, or makes sure they match if both are specified
  33. '''
  34. if sampling_period is None:
  35. if sampling_rate is None:
  36. raise ValueError("You must provide either the sampling rate or " + "sampling period")
  37. elif sampling_rate is None:
  38. sampling_rate = 1.0 / sampling_period
  39. elif sampling_period != 1.0 / sampling_rate:
  40. raise ValueError('The sampling_rate has to be 1/sampling_period')
  41. if not hasattr(sampling_rate, 'units'):
  42. raise TypeError("Sampling rate/sampling period must have units")
  43. return sampling_rate
  44. def _new_AnalogSignalArray(cls, signal, units=None, dtype=None, copy=True, t_start=0 * pq.s,
  45. sampling_rate=None, sampling_period=None, name=None, file_origin=None,
  46. description=None, array_annotations=None, annotations=None,
  47. channel_index=None, segment=None):
  48. '''
  49. A function to map AnalogSignal.__new__ to function that
  50. does not do the unit checking. This is needed for pickle to work.
  51. '''
  52. obj = cls(signal=signal, units=units, dtype=dtype, copy=copy,
  53. t_start=t_start, sampling_rate=sampling_rate,
  54. sampling_period=sampling_period, name=name,
  55. file_origin=file_origin, description=description,
  56. array_annotations=array_annotations, **annotations)
  57. obj.channel_index = channel_index
  58. obj.segment = segment
  59. return obj
  60. class AnalogSignal(BaseSignal):
  61. '''
  62. Array of one or more continuous analog signals.
  63. A representation of several continuous, analog signals that
  64. have the same duration, sampling rate and start time.
  65. Basically, it is a 2D array: dim 0 is time, dim 1 is
  66. channel index
  67. Inherits from :class:`quantities.Quantity`, which in turn inherits from
  68. :class:`numpy.ndarray`.
  69. *Usage*::
  70. >>> from neo.core import AnalogSignal
  71. >>> import quantities as pq
  72. >>>
  73. >>> sigarr = AnalogSignal([[1, 2, 3], [4, 5, 6]], units='V',
  74. ... sampling_rate=1*pq.Hz)
  75. >>>
  76. >>> sigarr
  77. <AnalogSignal(array([[1, 2, 3],
  78. [4, 5, 6]]) * mV, [0.0 s, 2.0 s], sampling rate: 1.0 Hz)>
  79. >>> sigarr[:,1]
  80. <AnalogSignal(array([2, 5]) * V, [0.0 s, 2.0 s],
  81. sampling rate: 1.0 Hz)>
  82. >>> sigarr[1, 1]
  83. array(5) * V
  84. *Required attributes/properties*:
  85. :signal: (quantity array 2D, numpy array 2D, or list (data, channel))
  86. The data itself.
  87. :units: (quantity units) Required if the signal is a list or NumPy
  88. array, not if it is a :class:`Quantity`
  89. :t_start: (quantity scalar) Time when signal begins
  90. :sampling_rate: *or* **sampling_period** (quantity scalar) Number of
  91. samples per unit time or
  92. interval between two samples.
  93. If both are specified, they are
  94. checked for consistency.
  95. *Recommended attributes/properties*:
  96. :name: (str) A label for the dataset.
  97. :description: (str) Text description.
  98. :file_origin: (str) Filesystem path or URL of the original data file.
  99. *Optional attributes/properties*:
  100. :dtype: (numpy dtype or str) Override the dtype of the signal array.
  101. :copy: (bool) True by default.
  102. :array_annotations: (dict) Dict mapping strings to numpy arrays containing annotations \
  103. for all data points
  104. Note: Any other additional arguments are assumed to be user-specific
  105. metadata and stored in :attr:`annotations`.
  106. *Properties available on this object*:
  107. :sampling_rate: (quantity scalar) Number of samples per unit time.
  108. (1/:attr:`sampling_period`)
  109. :sampling_period: (quantity scalar) Interval between two samples.
  110. (1/:attr:`quantity scalar`)
  111. :duration: (Quantity) Signal duration, read-only.
  112. (size * :attr:`sampling_period`)
  113. :t_stop: (quantity scalar) Time when signal ends, read-only.
  114. (:attr:`t_start` + :attr:`duration`)
  115. :times: (quantity 1D) The time points of each sample of the signal,
  116. read-only.
  117. (:attr:`t_start` + arange(:attr:`shape`[0])/:attr:`sampling_rate`)
  118. :channel_index:
  119. (deprecated) access to the channel_index attribute of the principal ChannelIndex
  120. associated with this signal.
  121. *Slicing*:
  122. :class:`AnalogSignal` objects can be sliced. When taking a single
  123. column (dimension 0, e.g. [0, :]) or a single element,
  124. a :class:`~quantities.Quantity` is returned.
  125. Otherwise an :class:`AnalogSignal` (actually a view) is
  126. returned, with the same metadata, except that :attr:`t_start`
  127. is changed if the start index along dimension 1 is greater than 1.
  128. Note that slicing an :class:`AnalogSignal` may give a different
  129. result to slicing the underlying NumPy array since signals
  130. are always two-dimensional.
  131. *Operations available on this object*:
  132. == != + * /
  133. '''
  134. _single_parent_objects = ('Segment', 'ChannelIndex')
  135. _single_parent_attrs = ('segment', 'channel_index')
  136. _quantity_attr = 'signal'
  137. _necessary_attrs = (('signal', pq.Quantity, 2),
  138. ('sampling_rate', pq.Quantity, 0),
  139. ('t_start', pq.Quantity, 0))
  140. _recommended_attrs = BaseNeo._recommended_attrs
  141. def __new__(cls, signal, units=None, dtype=None, copy=True, t_start=0 * pq.s,
  142. sampling_rate=None, sampling_period=None, name=None, file_origin=None,
  143. description=None, array_annotations=None, **annotations):
  144. '''
  145. Constructs new :class:`AnalogSignal` from data.
  146. This is called whenever a new class:`AnalogSignal` is created from
  147. the constructor, but not when slicing.
  148. __array_finalize__ is called on the new object.
  149. '''
  150. signal = cls._rescale(signal, units=units)
  151. obj = pq.Quantity(signal, units=units, dtype=dtype, copy=copy).view(cls)
  152. if obj.ndim == 1:
  153. obj.shape = (-1, 1)
  154. if t_start is None:
  155. raise ValueError('t_start cannot be None')
  156. obj._t_start = t_start
  157. obj._sampling_rate = _get_sampling_rate(sampling_rate, sampling_period)
  158. obj.segment = None
  159. obj.channel_index = None
  160. return obj
  161. def __init__(self, signal, units=None, dtype=None, copy=True, t_start=0 * pq.s,
  162. sampling_rate=None, sampling_period=None, name=None, file_origin=None,
  163. description=None, array_annotations=None, **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. DataObject.__init__(self, name=name, file_origin=file_origin, description=description,
  174. array_annotations=array_annotations, **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__, np.array(self), self.units, self.dtype,
  181. True, self.t_start, self.sampling_rate,
  182. self.sampling_period, self.name, self.file_origin,
  183. self.description, self.array_annotations,
  184. self.annotations, self.channel_index, self.segment)
  185. def _array_finalize_spec(self, obj):
  186. '''
  187. Set default values for attributes specific to :class:`AnalogSignal`.
  188. Common attributes are defined in
  189. :meth:`__array_finalize__` in :class:`basesignal.BaseSignal`),
  190. which is called every time a new signal is created
  191. and calls this method.
  192. '''
  193. self._t_start = getattr(obj, '_t_start', 0 * pq.s)
  194. self._sampling_rate = getattr(obj, '_sampling_rate', None)
  195. return obj
  196. def __repr__(self):
  197. '''
  198. Returns a string representing the :class:`AnalogSignal`.
  199. '''
  200. return ('<%s(%s, [%s, %s], sampling rate: %s)>' % (self.__class__.__name__,
  201. super().__repr__(),
  202. self.t_start, self.t_stop,
  203. self.sampling_rate))
  204. def get_channel_index(self):
  205. """
  206. """
  207. if self.channel_index:
  208. return self.channel_index.index
  209. else:
  210. return None
  211. def __getitem__(self, i):
  212. '''
  213. Get the item or slice :attr:`i`.
  214. '''
  215. if isinstance(i, (int, np.integer)): # a single point in time across all channels
  216. obj = super().__getitem__(i)
  217. obj = pq.Quantity(obj.magnitude, units=obj.units)
  218. elif isinstance(i, tuple):
  219. obj = super().__getitem__(i)
  220. j, k = i
  221. if isinstance(j, (int, np.integer)): # extract a quantity array
  222. obj = pq.Quantity(obj.magnitude, units=obj.units)
  223. else:
  224. if isinstance(j, slice):
  225. if j.start:
  226. obj.t_start = (self.t_start + j.start * self.sampling_period)
  227. if j.step:
  228. obj.sampling_period *= j.step
  229. elif isinstance(j, np.ndarray):
  230. raise NotImplementedError(
  231. "Arrays not yet supported") # in the general case, would need to return
  232. # IrregularlySampledSignal(Array)
  233. else:
  234. raise TypeError("%s not supported" % type(j))
  235. if isinstance(k, (int, np.integer)):
  236. obj = obj.reshape(-1, 1)
  237. if self.channel_index:
  238. obj.channel_index = self.channel_index.__getitem__(k)
  239. obj.array_annotate(**deepcopy(self.array_annotations_at_index(k)))
  240. elif isinstance(i, slice):
  241. obj = super().__getitem__(i)
  242. if i.start:
  243. obj.t_start = self.t_start + i.start * self.sampling_period
  244. obj.array_annotations = deepcopy(self.array_annotations)
  245. elif isinstance(i, np.ndarray):
  246. # Indexing of an AnalogSignal is only consistent if the resulting number of
  247. # samples is the same for each trace. The time axis for these samples is not
  248. # guaranteed to be continuous, so returning a Quantity instead of an AnalogSignal here.
  249. new_time_dims = np.sum(i, axis=0)
  250. if len(new_time_dims) and all(new_time_dims == new_time_dims[0]):
  251. obj = np.asarray(self).T.__getitem__(i.T)
  252. obj = obj.T.reshape(self.shape[1], -1).T
  253. obj = pq.Quantity(obj, units=self.units)
  254. else:
  255. raise IndexError("indexing of an AnalogSignals needs to keep the same number of "
  256. "sample for each trace contained")
  257. else:
  258. raise IndexError("index should be an integer, tuple, slice or boolean numpy array")
  259. return obj
  260. def __setitem__(self, i, value):
  261. """
  262. Set an item or slice defined by :attr:`i` to `value`.
  263. """
  264. # because AnalogSignals are always at least two-dimensional,
  265. # we need to handle the case where `i` is an integer
  266. if isinstance(i, int):
  267. i = slice(i, i + 1)
  268. elif isinstance(i, tuple):
  269. j, k = i
  270. if isinstance(k, int):
  271. i = (j, slice(k, k + 1))
  272. return super().__setitem__(i, value)
  273. # sampling_rate attribute is handled as a property so type checking can
  274. # be done
  275. @property
  276. def sampling_rate(self):
  277. '''
  278. Number of samples per unit time.
  279. (1/:attr:`sampling_period`)
  280. '''
  281. return self._sampling_rate
  282. @sampling_rate.setter
  283. def sampling_rate(self, rate):
  284. '''
  285. Setter for :attr:`sampling_rate`
  286. '''
  287. if rate is None:
  288. raise ValueError('sampling_rate cannot be None')
  289. elif not hasattr(rate, 'units'):
  290. raise ValueError('sampling_rate must have units')
  291. self._sampling_rate = rate
  292. # sampling_period attribute is handled as a property on underlying rate
  293. @property
  294. def sampling_period(self):
  295. '''
  296. Interval between two samples.
  297. (1/:attr:`sampling_rate`)
  298. '''
  299. return 1. / self.sampling_rate
  300. @sampling_period.setter
  301. def sampling_period(self, period):
  302. '''
  303. Setter for :attr:`sampling_period`
  304. '''
  305. if period is None:
  306. raise ValueError('sampling_period cannot be None')
  307. elif not hasattr(period, 'units'):
  308. raise ValueError('sampling_period must have units')
  309. self.sampling_rate = 1. / period
  310. # t_start attribute is handled as a property so type checking can be done
  311. @property
  312. def t_start(self):
  313. '''
  314. Time when signal begins.
  315. '''
  316. return self._t_start
  317. @t_start.setter
  318. def t_start(self, start):
  319. '''
  320. Setter for :attr:`t_start`
  321. '''
  322. if start is None:
  323. raise ValueError('t_start cannot be None')
  324. self._t_start = start
  325. @property
  326. def duration(self):
  327. '''
  328. Signal duration
  329. (:attr:`size` * :attr:`sampling_period`)
  330. '''
  331. return self.shape[0] / self.sampling_rate
  332. @property
  333. def t_stop(self):
  334. '''
  335. Time when signal ends.
  336. (:attr:`t_start` + :attr:`duration`)
  337. '''
  338. return self.t_start + self.duration
  339. @property
  340. def times(self):
  341. '''
  342. The time points of each sample of the signal
  343. (:attr:`t_start` + arange(:attr:`shape`)/:attr:`sampling_rate`)
  344. '''
  345. return self.t_start + np.arange(self.shape[0]) / self.sampling_rate
  346. def __eq__(self, other):
  347. '''
  348. Equality test (==)
  349. '''
  350. if (isinstance(other, AnalogSignal) and (
  351. self.t_start != other.t_start or self.sampling_rate != other.sampling_rate)):
  352. return False
  353. return super().__eq__(other)
  354. def _check_consistency(self, other):
  355. '''
  356. Check if the attributes of another :class:`AnalogSignal`
  357. are compatible with this one.
  358. '''
  359. if isinstance(other, AnalogSignal):
  360. for attr in "t_start", "sampling_rate":
  361. if getattr(self, attr) != getattr(other, attr):
  362. raise ValueError(
  363. "Inconsistent values of %s" % attr) # how to handle name and annotations?
  364. def _repr_pretty_(self, pp, cycle):
  365. '''
  366. Handle pretty-printing the :class:`AnalogSignal`.
  367. '''
  368. pp.text("{cls} with {channels} channels of length {length}; "
  369. "units {units}; datatype {dtype} ".format(cls=self.__class__.__name__,
  370. channels=self.shape[1],
  371. length=self.shape[0],
  372. units=self.units.dimensionality.string,
  373. dtype=self.dtype))
  374. if self._has_repr_pretty_attrs_():
  375. pp.breakable()
  376. self._repr_pretty_attrs_(pp, cycle)
  377. def _pp(line):
  378. pp.breakable()
  379. with pp.group(indent=1):
  380. pp.text(line)
  381. _pp("sampling rate: {}".format(self.sampling_rate))
  382. _pp("time: {} to {}".format(self.t_start, self.t_stop))
  383. def time_index(self, t):
  384. """Return the array index (or indices) corresponding to the time (or times) `t`"""
  385. i = (t - self.t_start) * self.sampling_rate
  386. i = np.rint(i.simplified.magnitude).astype(np.int)
  387. return i
  388. def time_slice(self, t_start, t_stop):
  389. '''
  390. Creates a new AnalogSignal corresponding to the time slice of the
  391. original AnalogSignal between times t_start, t_stop. Note, that for
  392. numerical stability reasons if t_start does not fall exactly on
  393. the time bins defined by the sampling_period it will be rounded to
  394. the nearest sampling bin. The time bin for t_stop will be chosen to
  395. make the duration of the resultant signal as close as possible to
  396. t_stop - t_start. This means that for a given duration, the size
  397. of the slice will always be the same.
  398. '''
  399. # checking start time and transforming to start index
  400. if t_start is None:
  401. i = 0
  402. t_start = 0 * pq.s
  403. else:
  404. i = self.time_index(t_start)
  405. # checking stop time and transforming to stop index
  406. if t_stop is None:
  407. j = len(self)
  408. else:
  409. delta = (t_stop - t_start) * self.sampling_rate
  410. j = i + int(np.rint(delta.simplified.magnitude))
  411. if (i < 0) or (j > len(self)):
  412. raise ValueError('t_start, t_stop have to be within the analog \
  413. signal duration')
  414. # Time slicing should create a deep copy of the object
  415. obj = deepcopy(self[i:j])
  416. obj.t_start = self.t_start + i * self.sampling_period
  417. return obj
  418. def time_shift(self, t_shift):
  419. """
  420. Shifts a :class:`AnalogSignal` to start at a new time.
  421. Parameters:
  422. -----------
  423. t_shift: Quantity (time)
  424. Amount of time by which to shift the :class:`AnalogSignal`.
  425. Returns:
  426. --------
  427. new_sig: :class:`AnalogSignal`
  428. New instance of a :class:`AnalogSignal` object starting at t_shift later than the
  429. original :class:`AnalogSignal` (the original :class:`AnalogSignal` is not modified).
  430. """
  431. new_sig = deepcopy(self)
  432. new_sig.t_start = new_sig.t_start + t_shift
  433. return new_sig
  434. def splice(self, signal, copy=False):
  435. """
  436. Replace part of the current signal by a new piece of signal.
  437. The new piece of signal will overwrite part of the current signal
  438. starting at the time given by the new piece's `t_start` attribute.
  439. The signal to be spliced in must have the same physical dimensions,
  440. sampling rate, and number of channels as the current signal and
  441. fit within it.
  442. If `copy` is False (the default), modify the current signal in place.
  443. If `copy` is True, return a new signal and leave the current one untouched.
  444. In this case, the new signal will not be linked to any parent objects.
  445. """
  446. if signal.t_start < self.t_start:
  447. raise ValueError("Cannot splice earlier than the start of the signal")
  448. if signal.t_stop > self.t_stop:
  449. raise ValueError("Splice extends beyond signal")
  450. if signal.sampling_rate != self.sampling_rate:
  451. raise ValueError("Sampling rates do not match")
  452. i = self.time_index(signal.t_start)
  453. j = i + signal.shape[0]
  454. if copy:
  455. new_signal = deepcopy(self)
  456. new_signal.segment = None
  457. new_signal.channel_index = None
  458. new_signal[i:j, :] = signal
  459. return new_signal
  460. else:
  461. self[i:j, :] = signal
  462. return self
  463. def downsample(self, downsampling_factor, **kwargs):
  464. """
  465. Downsample the data of a signal.
  466. This method reduces the number of samples of the AnalogSignal to a fraction of the
  467. original number of samples, defined by `downsampling_factor`.
  468. This method is a wrapper of scipy.signal.decimate and accepts the same set of keyword
  469. arguments, except for specifying the axis of resampling, which is fixed to the first axis
  470. here.
  471. Parameters:
  472. -----------
  473. downsampling_factor: integer
  474. Factor used for decimation of samples. Scipy recommends to call decimate multiple times
  475. for downsampling factors higher than 13 when using IIR downsampling (default).
  476. Returns:
  477. --------
  478. downsampled_signal: :class:`AnalogSignal`
  479. New instance of a :class:`AnalogSignal` object containing the resampled data points.
  480. The original :class:`AnalogSignal` is not modified.
  481. Note:
  482. -----
  483. For resampling the signal with a fixed number of samples, see `resample` method.
  484. """
  485. if not HAVE_SCIPY:
  486. raise ImportError('Decimating requires availability of scipy.signal')
  487. # Resampling is only permitted along the time axis (axis=0)
  488. if 'axis' in kwargs:
  489. kwargs.pop('axis')
  490. downsampled_data = scipy.signal.decimate(self.magnitude, downsampling_factor, axis=0,
  491. **kwargs)
  492. downsampled_signal = self.duplicate_with_new_data(downsampled_data)
  493. # since the number of channels stays the same, we can also copy array annotations here
  494. downsampled_signal.array_annotations = self.array_annotations.copy()
  495. downsampled_signal.sampling_rate = self.sampling_rate / downsampling_factor
  496. return downsampled_signal
  497. def resample(self, sample_count, **kwargs):
  498. """
  499. Resample the data points of the signal.
  500. This method interpolates the signal and returns a new signal with a fixed number of
  501. samples defined by `sample_count`.
  502. This method is a wrapper of scipy.signal.resample and accepts the same set of keyword
  503. arguments, except for specifying the axis of resampling which is fixed to the first axis
  504. here, and the sample positions. .
  505. Parameters:
  506. -----------
  507. sample_count: integer
  508. Number of desired samples. The resulting signal starts at the same sample as the
  509. original and is sampled regularly.
  510. Returns:
  511. --------
  512. resampled_signal: :class:`AnalogSignal`
  513. New instance of a :class:`AnalogSignal` object containing the resampled data points.
  514. The original :class:`AnalogSignal` is not modified.
  515. Note:
  516. -----
  517. For reducing the number of samples to a fraction of the original, see `downsample` method
  518. """
  519. if not HAVE_SCIPY:
  520. raise ImportError('Resampling requires availability of scipy.signal')
  521. # Resampling is only permitted along the time axis (axis=0)
  522. if 'axis' in kwargs:
  523. kwargs.pop('axis')
  524. if 't' in kwargs:
  525. kwargs.pop('t')
  526. resampled_data, resampled_times = scipy.signal.resample(self.magnitude, sample_count,
  527. t=self.times, axis=0, **kwargs)
  528. resampled_signal = self.duplicate_with_new_data(resampled_data)
  529. resampled_signal.sampling_rate = (sample_count / self.shape[0]) * self.sampling_rate
  530. # since the number of channels stays the same, we can also copy array annotations here
  531. resampled_signal.array_annotations = self.array_annotations.copy()
  532. return resampled_signal
  533. def rectify(self, **kwargs):
  534. """
  535. Rectify the signal.
  536. This method rectifies the signal by taking the absolute value.
  537. This method is a wrapper of numpy.absolute() and accepts the same set of keyword
  538. arguments.
  539. Returns:
  540. --------
  541. resampled_signal: :class:`AnalogSignal`
  542. New instance of a :class:`AnalogSignal` object containing the rectified data points.
  543. The original :class:`AnalogSignal` is not modified.
  544. """
  545. # Use numpy to get the absolute value of the signal
  546. rectified_data = np.absolute(self.magnitude, **kwargs)
  547. rectified_signal = self.duplicate_with_new_data(rectified_data)
  548. # the sampling rate stays constant
  549. rectified_signal.sampling_rate = self.sampling_rate
  550. # since the number of channels stays the same, we can also copy array annotations here
  551. rectified_signal.array_annotations = self.array_annotations.copy()
  552. return rectified_signal
  553. def concatenate(self, *signals, overwrite=False, padding=False):
  554. """
  555. Concatenate multiple neo.AnalogSignal objects across time.
  556. Units, sampling_rate and number of signal traces must be the same
  557. for all signals. Otherwise a ValueError is raised.
  558. Note that timestamps of concatenated signals might shift in oder to
  559. align the sampling times of all signals.
  560. Parameters
  561. ----------
  562. signals: neo.AnalogSignal objects
  563. AnalogSignals that will be concatenated
  564. overwrite : bool
  565. If True, samples of the earlier (lower index in `signals`)
  566. signals are overwritten by that of later (higher index in `signals`)
  567. signals.
  568. If False, samples of the later are overwritten by earlier signal.
  569. Default: False
  570. padding : bool, scalar quantity
  571. Sampling values to use as padding in case signals do not overlap.
  572. If False, do not apply padding. Signals have to align or
  573. overlap. If True, signals will be padded using
  574. np.NaN as pad values. If a scalar quantity is provided, this
  575. will be used for padding. The other signal is moved
  576. forward in time by maximum one sampling period to
  577. align the sampling times of both signals.
  578. Default: False
  579. Returns
  580. -------
  581. signal: neo.AnalogSignal
  582. concatenated output signal
  583. """
  584. # Sanity of inputs
  585. if not hasattr(signals, '__iter__'):
  586. raise TypeError('signals must be iterable')
  587. if not all([isinstance(a, AnalogSignal) for a in signals]):
  588. raise TypeError('Entries of anasiglist have to be of type neo.AnalogSignal')
  589. if len(signals) == 0:
  590. return self
  591. signals = [self] + list(signals)
  592. # Check required common attributes: units, sampling_rate and shape[-1]
  593. shared_attributes = ['units', 'sampling_rate']
  594. attribute_values = [tuple((getattr(anasig, attr) for attr in shared_attributes))
  595. for anasig in signals]
  596. # add shape dimensions that do not relate to time
  597. attribute_values = [(attribute_values[i] + (signals[i].shape[1:],))
  598. for i in range(len(signals))]
  599. if not all([attrs == attribute_values[0] for attrs in attribute_values]):
  600. raise MergeError(
  601. f'AnalogSignals have to share {shared_attributes} attributes to be concatenated.')
  602. units, sr, shape = attribute_values[0]
  603. # find gaps between Analogsignals
  604. combined_time_ranges = self._concatenate_time_ranges(
  605. [(s.t_start, s.t_stop) for s in signals])
  606. missing_time_ranges = self._invert_time_ranges(combined_time_ranges)
  607. if len(missing_time_ranges):
  608. diffs = np.diff(np.asarray(missing_time_ranges), axis=1)
  609. else:
  610. diffs = []
  611. if padding is False and any(diffs > signals[0].sampling_period):
  612. raise MergeError(f'Signals are not continuous. Can not concatenate signals with gaps. '
  613. f'Please provide a padding value.')
  614. if padding is not False:
  615. logger.warning('Signals will be padded using {}.'.format(padding))
  616. if padding is True:
  617. padding = np.NaN * units
  618. if isinstance(padding, pq.Quantity):
  619. padding = padding.rescale(units).magnitude
  620. else:
  621. raise MergeError('Invalid type of padding value. Please provide a bool value '
  622. 'or a quantities object.')
  623. t_start = min([a.t_start for a in signals])
  624. t_stop = max([a.t_stop for a in signals])
  625. n_samples = int(np.rint(((t_stop - t_start) * sr).rescale('dimensionless').magnitude))
  626. shape = (n_samples,) + shape
  627. # Collect attributes and annotations across all concatenated signals
  628. kwargs = {}
  629. common_annotations = signals[0].annotations
  630. common_array_annotations = signals[0].array_annotations
  631. for anasig in signals[1:]:
  632. common_annotations = intersect_annotations(common_annotations, anasig.annotations)
  633. common_array_annotations = intersect_annotations(common_array_annotations,
  634. anasig.array_annotations)
  635. kwargs['annotations'] = common_annotations
  636. kwargs['array_annotations'] = common_array_annotations
  637. for name in ("name", "description", "file_origin"):
  638. attr = [getattr(s, name) for s in signals]
  639. if all([a == attr[0] for a in attr]):
  640. kwargs[name] = attr[0]
  641. else:
  642. kwargs[name] = f'concatenation ({attr})'
  643. conc_signal = AnalogSignal(np.full(shape=shape, fill_value=padding, dtype=signals[0].dtype),
  644. sampling_rate=sr, t_start=t_start, units=units, **kwargs)
  645. if not overwrite:
  646. signals = signals[::-1]
  647. while len(signals) > 0:
  648. conc_signal.splice(signals.pop(0), copy=False)
  649. return conc_signal
  650. def _concatenate_time_ranges(self, time_ranges):
  651. time_ranges = sorted(time_ranges)
  652. new_ranges = time_ranges[:1]
  653. for t_start, t_stop in time_ranges[1:]:
  654. # time range are non continuous -> define new range
  655. if t_start > new_ranges[-1][1]:
  656. new_ranges.append((t_start, t_stop))
  657. # time range is continuous -> extend time range
  658. elif t_stop > new_ranges[-1][1]:
  659. new_ranges[-1] = (new_ranges[-1][0], t_stop)
  660. return new_ranges
  661. def _invert_time_ranges(self, time_ranges):
  662. i = 0
  663. new_ranges = []
  664. while i < len(time_ranges) - 1:
  665. new_ranges.append((time_ranges[i][1], time_ranges[i + 1][0]))
  666. i += 1
  667. return new_ranges