1
1

utils.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627
  1. '''
  2. This module defines multiple utility functions for filtering, creation, slicing,
  3. etc. of neo.core objects.
  4. '''
  5. import neo
  6. import copy
  7. import warnings
  8. import numpy as np
  9. import quantities as pq
  10. def get_events(container, **properties):
  11. """
  12. This function returns a list of Event objects, corresponding to given
  13. key-value pairs in the attributes or annotations of the Event.
  14. Parameter:
  15. ---------
  16. container: Block or Segment
  17. The Block or Segment object to extract data from.
  18. Keyword Arguments:
  19. ------------------
  20. The Event properties to filter for.
  21. Each property name is matched to an attribute or an
  22. (array-)annotation of the Event. The value of property corresponds
  23. to a valid entry or a list of valid entries of the attribute or
  24. (array-)annotation.
  25. If the value is a list of entries of the same
  26. length as the number of events in the Event object, the list entries
  27. are matched to the events in the Event object. The resulting Event
  28. object contains only those events where the values match up.
  29. Otherwise, the value is compared to the attribute or (array-)annotation
  30. of the Event object as such, and depending on the comparison, either the
  31. complete Event object is returned or not.
  32. If no keyword arguments is passed, all Event Objects will
  33. be returned in a list.
  34. Returns:
  35. --------
  36. events: list
  37. A list of Event objects matching the given criteria.
  38. Example:
  39. --------
  40. >>> import neo
  41. >>> from neo.utils import get_events
  42. >>> import quantities as pq
  43. >>> event = neo.Event(times=[0.5, 10.0, 25.2] * pq.s)
  44. >>> event.annotate(event_type='trial start')
  45. >>> event.array_annotate(trial_id=[1, 2, 3])
  46. >>> seg = neo.Segment()
  47. >>> seg.events = [event]
  48. # Will return a list with the complete event object
  49. >>> get_events(seg, event_type='trial start')
  50. # Will return an empty list
  51. >>> get_events(seg, event_type='trial stop')
  52. # Will return a list with an Event object, but only with trial 2
  53. >>> get_events(seg, trial_id=2)
  54. # Will return a list with an Event object, but only with trials 1 and 2
  55. >>> get_events(seg, trial_id=[1, 2])
  56. """
  57. if isinstance(container, neo.Segment):
  58. return _get_from_list(container.events, prop=properties)
  59. elif isinstance(container, neo.Block):
  60. event_lst = []
  61. for seg in container.segments:
  62. event_lst += _get_from_list(seg.events, prop=properties)
  63. return event_lst
  64. else:
  65. raise TypeError(
  66. 'Container needs to be of type Block or Segment, not %s '
  67. 'in order to extract Events.' % (type(container)))
  68. def get_epochs(container, **properties):
  69. """
  70. This function returns a list of Epoch objects, corresponding to given
  71. key-value pairs in the attributes or annotations of the Epoch.
  72. Parameters:
  73. -----------
  74. container: Block or Segment
  75. The Block or Segment object to extract data from.
  76. Keyword Arguments:
  77. ------------------
  78. The Epoch properties to filter for.
  79. Each property name is matched to an attribute or an
  80. (array-)annotation of the Epoch. The value of property corresponds
  81. to a valid entry or a list of valid entries of the attribute or
  82. (array-)annotation.
  83. If the value is a list of entries of the same
  84. length as the number of epochs in the Epoch object, the list entries
  85. are matched to the epochs in the Epoch object. The resulting Epoch
  86. object contains only those epochs where the values match up.
  87. Otherwise, the value is compared to the attribute or (array-)annotation
  88. of the Epoch object as such, and depending on the comparison, either the
  89. complete Epoch object is returned or not.
  90. If no keyword arguments is passed, all Epoch Objects will
  91. be returned in a list.
  92. Returns:
  93. --------
  94. epochs: list
  95. A list of Epoch objects matching the given criteria.
  96. Example:
  97. --------
  98. >>> import neo
  99. >>> from neo.utils import get_epochs
  100. >>> import quantities as pq
  101. >>> epoch = neo.Epoch(times=[0.5, 10.0, 25.2] * pq.s,
  102. ... durations=[100, 100, 100] * pq.ms,
  103. ... epoch_type='complete trial')
  104. >>> epoch.array_annotate(trial_id=[1, 2, 3])
  105. >>> seg = neo.Segment()
  106. >>> seg.epochs = [epoch]
  107. # Will return a list with the complete event object
  108. >>> get_epochs(seg, epoch_type='complete trial')
  109. # Will return an empty list
  110. >>> get_epochs(seg, epoch_type='error trial')
  111. # Will return a list with an Event object, but only with trial 2
  112. >>> get_epochs(seg, trial_id=2)
  113. # Will return a list with an Event object, but only with trials 1 and 2
  114. >>> get_epochs(seg, trial_id=[1, 2])
  115. """
  116. if isinstance(container, neo.Segment):
  117. return _get_from_list(container.epochs, prop=properties)
  118. elif isinstance(container, neo.Block):
  119. epoch_list = []
  120. for seg in container.segments:
  121. epoch_list += _get_from_list(seg.epochs, prop=properties)
  122. return epoch_list
  123. else:
  124. raise TypeError(
  125. 'Container needs to be of type Block or Segment, not %s '
  126. 'in order to extract Epochs.' % (type(container)))
  127. def _get_from_list(input_list, prop=None):
  128. """
  129. Internal function
  130. """
  131. output_list = []
  132. # empty or no dictionary
  133. if not prop or bool([b for b in prop.values() if b == []]):
  134. output_list += [e for e in input_list]
  135. # dictionary is given
  136. else:
  137. for ep in input_list:
  138. if isinstance(ep, neo.Epoch) or isinstance(ep, neo.Event):
  139. sparse_ep = ep.copy()
  140. elif isinstance(ep, neo.io.proxyobjects.EpochProxy) \
  141. or isinstance(ep, neo.io.proxyobjects.EventProxy):
  142. # need to load the Event/Epoch in order to be able to filter by array annotations
  143. sparse_ep = ep.load()
  144. for k in prop.keys():
  145. sparse_ep = _filter_event_epoch(sparse_ep, k, prop[k])
  146. # if there is nothing left, it cannot filtered
  147. if sparse_ep is None:
  148. break
  149. if sparse_ep is not None:
  150. output_list.append(sparse_ep)
  151. return output_list
  152. def _filter_event_epoch(obj, annotation_key, annotation_value):
  153. """
  154. Internal function.
  155. This function returns a copy of a Event or Epoch object, which only
  156. contains attributes or annotations corresponding to requested key-value
  157. pairs.
  158. Parameters:
  159. -----------
  160. obj : Event
  161. The Event or Epoch object to modify.
  162. annotation_key : string, int or float
  163. The name of the annotation used to filter.
  164. annotation_value : string, int, float, list or np.ndarray
  165. The accepted value or list of accepted values of the attributes or
  166. annotations specified by annotation_key. For each entry in obj the
  167. respective annotation defined by annotation_key is compared to the
  168. annotation value. The entry of obj is kept if the attribute or
  169. annotation is equal or contained in annotation_value.
  170. Returns:
  171. --------
  172. obj : Event or Epoch
  173. The Event or Epoch object with every event or epoch removed that does
  174. not match the filter criteria (i.e., where none of the entries in
  175. annotation_value match the attribute or annotation annotation_key.
  176. """
  177. valid_ids = _get_valid_ids(obj, annotation_key, annotation_value)
  178. if len(valid_ids) == 0:
  179. return None
  180. return _event_epoch_slice_by_valid_ids(obj, valid_ids)
  181. def _event_epoch_slice_by_valid_ids(obj, valid_ids):
  182. """
  183. Internal function
  184. """
  185. if type(obj) is neo.Event or type(obj) is neo.Epoch:
  186. sparse_obj = copy.deepcopy(obj[valid_ids])
  187. else:
  188. raise TypeError('Can only slice Event and Epoch objects by valid IDs.')
  189. return sparse_obj
  190. def _get_valid_ids(obj, annotation_key, annotation_value):
  191. """
  192. Internal function
  193. """
  194. valid_mask = np.zeros(obj.shape)
  195. if annotation_key in obj.annotations and obj.annotations[annotation_key] == annotation_value:
  196. valid_mask = np.ones(obj.shape)
  197. elif annotation_key == 'labels':
  198. # wrap annotation value to be list
  199. if not type(annotation_value) in [list, np.ndarray]:
  200. annotation_value = [annotation_value]
  201. valid_mask = np.in1d(obj.labels, annotation_value)
  202. elif annotation_key in obj.array_annotations:
  203. # wrap annotation value to be list
  204. if not type(annotation_value) in [list, np.ndarray]:
  205. annotation_value = [annotation_value]
  206. valid_mask = np.in1d(obj.array_annotations[annotation_key], annotation_value)
  207. elif hasattr(obj, annotation_key) and getattr(obj, annotation_key) == annotation_value:
  208. valid_mask = np.ones(obj.shape)
  209. valid_ids = np.where(valid_mask)[0]
  210. return valid_ids
  211. def add_epoch(
  212. segment, event1, event2=None, pre=0 * pq.s, post=0 * pq.s,
  213. attach_result=True, **kwargs):
  214. """
  215. Create Epochs around a single Event, or between pairs of events. Starting
  216. and end time of the Epoch can be modified using pre and post as offsets
  217. before the and after the event(s). Additional keywords will be directly
  218. forwarded to the Epoch intialization.
  219. Parameters:
  220. -----------
  221. segment : Segment
  222. The segment in which the final Epoch object is added.
  223. event1 : Event
  224. The Event objects containing the start events of the epochs. If no
  225. event2 is specified, these event1 also specifies the stop events, i.e.,
  226. the Epoch is cut around event1 times.
  227. event2: Event
  228. The Event objects containing the stop events of the epochs. If no
  229. event2 is specified, event1 specifies the stop events, i.e., the Epoch
  230. is cut around event1 times. The number of events in event2 must match
  231. that of event1.
  232. pre, post: Quantity (time)
  233. Time offsets to modify the start (pre) and end (post) of the resulting
  234. Epoch. Example: pre=-10*ms and post=+25*ms will cut from 10 ms before
  235. event1 times to 25 ms after event2 times
  236. attach_result: bool
  237. If True, the resulting Epoch object is added to segment.
  238. Keyword Arguments:
  239. ------------------
  240. Passed to the Epoch object.
  241. Returns:
  242. --------
  243. epoch: Epoch
  244. An Epoch object with the calculated epochs (one per entry in event1).
  245. See also:
  246. ---------
  247. Event.to_epoch()
  248. """
  249. if event2 is None:
  250. event2 = event1
  251. if not isinstance(segment, neo.Segment):
  252. raise TypeError(
  253. 'Segment has to be of type Segment, not %s' % type(segment))
  254. # load the full event if a proxy object has been given as an argument
  255. if isinstance(event1, neo.io.proxyobjects.EventProxy):
  256. event1 = event1.load()
  257. if isinstance(event2, neo.io.proxyobjects.EventProxy):
  258. event2 = event2.load()
  259. for event in [event1, event2]:
  260. if not isinstance(event, neo.Event):
  261. raise TypeError(
  262. 'Events have to be of type Event, not %s' % type(event))
  263. if len(event1) != len(event2):
  264. raise ValueError(
  265. 'event1 and event2 have to have the same number of entries in '
  266. 'order to create epochs between pairs of entries. Match your '
  267. 'events before generating epochs. Current event lengths '
  268. 'are %i and %i' % (len(event1), len(event2)))
  269. times = event1.times + pre
  270. durations = event2.times + post - times
  271. if any(durations < 0):
  272. raise ValueError(
  273. 'Can not create epoch with negative duration. '
  274. 'Requested durations %s.' % durations)
  275. elif any(durations == 0):
  276. raise ValueError('Can not create epoch with zero duration.')
  277. if 'name' not in kwargs:
  278. kwargs['name'] = 'epoch'
  279. if 'labels' not in kwargs:
  280. kwargs['labels'] = [u'{}_{}'.format(kwargs['name'], i)
  281. for i in range(len(times))]
  282. ep = neo.Epoch(times=times, durations=durations, **kwargs)
  283. ep.annotate(**event1.annotations)
  284. ep.array_annotate(**event1.array_annotations)
  285. if attach_result:
  286. segment.epochs.append(ep)
  287. segment.create_relationship()
  288. return ep
  289. def match_events(event1, event2):
  290. """
  291. Finds pairs of Event entries in event1 and event2 with the minimum delay,
  292. such that the entry of event1 directly precedes the entry of event2.
  293. Returns filtered two events of identical length, which contain matched
  294. entries.
  295. Parameters:
  296. -----------
  297. event1, event2: Event
  298. The two Event objects to match up.
  299. Returns:
  300. --------
  301. event1, event2: Event
  302. Event objects with identical number of events, containing only those
  303. events that could be matched against each other. A warning is issued if
  304. not all events in event1 or event2 could be matched.
  305. """
  306. # load the full event if a proxy object has been given as an argument
  307. if isinstance(event1, neo.io.proxyobjects.EventProxy):
  308. event1 = event1.load()
  309. if isinstance(event2, neo.io.proxyobjects.EventProxy):
  310. event2 = event2.load()
  311. id1, id2 = 0, 0
  312. match_ev1, match_ev2 = [], []
  313. while id1 < len(event1) and id2 < len(event2):
  314. time1 = event1.times[id1]
  315. time2 = event2.times[id2]
  316. # wrong order of events
  317. if time1 >= time2:
  318. id2 += 1
  319. # shorter epoch possible by later event1 entry
  320. elif id1 + 1 < len(event1) and event1.times[id1 + 1] < time2:
  321. # there is no event in 2 until the next event in 1
  322. id1 += 1
  323. # found a match
  324. else:
  325. match_ev1.append(id1)
  326. match_ev2.append(id2)
  327. id1 += 1
  328. id2 += 1
  329. if id1 < len(event1):
  330. warnings.warn(
  331. 'Could not match all events to generate epochs. Missed '
  332. '%s event entries in event1 list' % (len(event1) - id1))
  333. if id2 < len(event2):
  334. warnings.warn(
  335. 'Could not match all events to generate epochs. Missed '
  336. '%s event entries in event2 list' % (len(event2) - id2))
  337. event1_matched = _event_epoch_slice_by_valid_ids(
  338. obj=event1, valid_ids=match_ev1)
  339. event2_matched = _event_epoch_slice_by_valid_ids(
  340. obj=event2, valid_ids=match_ev2)
  341. return event1_matched, event2_matched
  342. def cut_block_by_epochs(block, properties=None, reset_time=False):
  343. """
  344. This function cuts Segments in a Block according to multiple Neo
  345. Epoch objects.
  346. The function alters the Block by adding one Segment per Epoch entry
  347. fulfilling a set of conditions on the Epoch attributes and annotations. The
  348. original segments are removed from the block.
  349. A dictionary contains restrictions on which Epochs are considered for
  350. the cutting procedure. To this end, it is possible to
  351. specify accepted (valid) values of specific annotations on the source
  352. Epochs.
  353. The resulting cut segments may either retain their original time stamps, or
  354. be shifted to a common starting time.
  355. Parameters
  356. ----------
  357. block: Block
  358. Contains the Segments to cut according to the Epoch criteria provided
  359. properties: dictionary
  360. A dictionary that contains the Epoch keys and values to filter for.
  361. Each key of the dictionary is matched to an attribute or an
  362. annotation or an array_annotation of the Event.
  363. The value of each dictionary entry corresponds to a valid entry or a
  364. list of valid entries of the attribute or (array) annotation.
  365. If the value belonging to the key is a list of entries of the same
  366. length as the number of epochs in the Epoch object, the list entries
  367. are matched to the epochs in the Epoch object. The resulting Epoch
  368. object contains only those epochs where the values match up.
  369. Otherwise, the value is compared to the attributes or annotation of the
  370. Epoch object as such, and depending on the comparison, either the
  371. complete Epoch object is returned or not.
  372. If None or an empty dictionary is passed, all Epoch Objects will
  373. be considered
  374. reset_time: bool
  375. If True the times stamps of all sliced objects are set to fall
  376. in the range from 0 to the duration of the epoch duration.
  377. If False, original time stamps are retained.
  378. Default is False.
  379. Returns:
  380. --------
  381. None
  382. """
  383. if not isinstance(block, neo.Block):
  384. raise TypeError(
  385. 'block needs to be a Block, not %s' % type(block))
  386. new_block = neo.Block()
  387. for seg in block.segments:
  388. epochs = _get_from_list(seg.epochs, prop=properties)
  389. if len(epochs) > 1:
  390. warnings.warn(
  391. 'Segment %s contains multiple epochs with '
  392. 'requested properties (%s). Sub-segments can '
  393. 'have overlapping times' % (seg.name, properties))
  394. elif len(epochs) == 0:
  395. warnings.warn(
  396. 'No epoch is matching the requested epoch properties %s. '
  397. 'No cutting of segment %s performed.' % (properties, seg.name))
  398. for epoch in epochs:
  399. new_segments = cut_segment_by_epoch(
  400. seg, epoch=epoch, reset_time=reset_time)
  401. new_block.segments.extend(new_segments)
  402. new_block.create_many_to_one_relationship(force=True)
  403. return new_block
  404. def cut_segment_by_epoch(seg, epoch, reset_time=False):
  405. """
  406. Cuts a Segment according to an Epoch object
  407. The function returns a list of Segments, where each segment corresponds
  408. to an epoch in the Epoch object and contains the data of the original
  409. Segment cut to that particular Epoch.
  410. The resulting segments may either retain their original time stamps,
  411. or can be shifted to a common time axis.
  412. Parameters
  413. ----------
  414. seg: Segment
  415. The Segment containing the original uncut data.
  416. epoch: Epoch
  417. For each epoch in this input, one segment is generated according to
  418. the epoch time and duration.
  419. reset_time: bool
  420. If True the times stamps of all sliced objects are set to fall
  421. in the range from 0 to the duration of the epoch duration.
  422. If False, original time stamps are retained.
  423. Default is False.
  424. Returns:
  425. --------
  426. segments: list of Segments
  427. Per epoch in the input, a Segment with AnalogSignal and/or
  428. SpikeTrain Objects will be generated and returned. Each Segment will
  429. receive the annotations of the corresponding epoch in the input.
  430. """
  431. if not isinstance(seg, neo.Segment):
  432. raise TypeError(
  433. 'Seg needs to be of type Segment, not %s' % type(seg))
  434. if not isinstance(epoch, neo.Epoch):
  435. raise TypeError(
  436. 'Epoch needs to be of type Epoch, not %s' % type(epoch))
  437. segments = []
  438. for ep_id in range(len(epoch)):
  439. subseg = seg.time_slice(epoch.times[ep_id],
  440. epoch.times[ep_id] + epoch.durations[ep_id],
  441. reset_time=reset_time)
  442. subseg.annotate(**copy.copy(epoch.annotations))
  443. # Add array-annotations of Epoch
  444. for key, val in epoch.array_annotations.items():
  445. if len(val):
  446. subseg.annotations[key] = copy.copy(val[ep_id])
  447. segments.append(subseg)
  448. return segments
  449. def is_block_rawio_compatible(block, return_problems=False):
  450. """
  451. The neo.rawio layer have some restriction compared to neo.io layer:
  452. * consistent channels across segments
  453. * no IrregularlySampledSignal
  454. * consistent sampling rate across segments
  455. This function tests if a neo.Block that could be written in a nix file could be read
  456. back with the NIXRawIO.
  457. Parameters
  458. ----------
  459. block: Block
  460. A block
  461. return_problems: bool (False by default)
  462. Controls whether a list of str that describe problems is also provided as return value
  463. Returns:
  464. --------
  465. is_rawio_compatible: bool
  466. Compatible or not.
  467. problems: list of str
  468. Optional, depending on value of `return_problems`.
  469. A list that describe problems for rawio compatibility.
  470. """
  471. assert len(block.segments) > 0, "This block doesn't have segments"
  472. problems = []
  473. # check that all Segments have the same number of object.
  474. n_sig = len(block.segments[0].analogsignals)
  475. n_st = len(block.segments[0].spiketrains)
  476. n_ev = len(block.segments[0].events)
  477. n_ep = len(block.segments[0].epochs)
  478. sig_count_consistent = True
  479. for seg in block.segments:
  480. if len(seg.analogsignals) != n_sig:
  481. problems.append('Number of AnalogSignals is not consistent across segments')
  482. sig_count_consistent = False
  483. if len(seg.spiketrains) != n_st:
  484. problems.append('Number of SpikeTrains is not consistent across segments')
  485. if len(seg.events) != n_ev:
  486. problems.append('Number of Events is not consistent across segments')
  487. if len(seg.epochs) != n_ep:
  488. problems.append('Number of Epochs is not consistent across segments')
  489. # check for AnalogSigal that sampling_rate/units/number of channel
  490. # is consistent across segments.
  491. if sig_count_consistent:
  492. seg0 = block.segments[0]
  493. for i in range(n_sig):
  494. for seg in block.segments:
  495. if seg.analogsignals[i].sampling_rate != seg0.analogsignals[i].sampling_rate:
  496. problems.append('AnalogSignals have inconsistent sampling rate across segments')
  497. if seg.analogsignals[i].shape[1] != seg0.analogsignals[i].shape[1]:
  498. problems.append('AnalogSignals have inconsistent channel count across segments')
  499. if seg.analogsignals[i].units != seg0.analogsignals[i].units:
  500. problems.append('AnalogSignals have inconsistent units across segments')
  501. # check no IrregularlySampledSignal
  502. for seg in block.segments:
  503. if len(seg.irregularlysampledsignals) > 0:
  504. problems.append('IrregularlySampledSignals are not raw compatible')
  505. # returns
  506. is_rawio_compatible = (len(problems) == 0)
  507. if return_problems:
  508. return is_rawio_compatible, problems
  509. else:
  510. return is_rawio_compatible