Scheduled service maintenance on November 22


On Friday, November 22, 2024, between 06:00 CET and 18:00 CET, GIN services will undergo planned maintenance. Extended service interruptions should be expected. We will try to keep downtimes to a minimum, but recommend that users avoid critical tasks, large data uploads, or DOI requests during this time.

We apologize for any inconvenience.

analogsignal.py 24 KB

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