example_blackrock.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. # -*- coding: utf-8 -*-
  2. """
  3. Example code for loading and processing of a recording of the reach-
  4. to-grasp experiments conducted at the Institute de Neurosciences de la Timone
  5. by Thomas Brochier and Alexa Riehle from the original Blaclrock files and
  6. odML files, using the custom loading routine `ReachGraspIO`.
  7. Authors: Julia Sprenger, Lyuba Zehl, Michael Denker
  8. Copyright (c) 2017, Institute of Neuroscience and Medicine (INM-6),
  9. Forschungszentrum Juelich, Germany
  10. All rights reserved.
  11. Redistribution and use in source and binary forms, with or without
  12. modification, are permitted provided that the following conditions are met:
  13. * Redistributions of source code must retain the above copyright notice, this
  14. list of conditions and the following disclaimer.
  15. * Redistributions in binary form must reproduce the above copyright notice,
  16. this list of conditions and the following disclaimer in the documentation
  17. and/or other materials provided with the distribution.
  18. * Neither the names of the copyright holders nor the names of the contributors
  19. may be used to endorse or promote products derived from this software without
  20. specific prior written permission.
  21. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
  22. ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  23. WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  24. DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
  25. FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  26. DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
  27. SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
  28. CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
  29. OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30. OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31. """
  32. import os
  33. import numpy as np
  34. import matplotlib.pyplot as plt
  35. import quantities as pq
  36. from neo import Block, Segment
  37. from elephant.signal_processing import butter
  38. from reachgraspio import reachgraspio
  39. from neo.utils import cut_segment_by_epoch, add_epoch, get_events
  40. from neo_utils import load_segment
  41. # =============================================================================
  42. # Load data
  43. #
  44. # As a first step, we partially load the data file into memory as a Neo object.
  45. # =============================================================================
  46. # Specify the path to the recording session to load, eg,
  47. # '/home/user/l101210-001'
  48. session_name = os.path.join('..', 'datasets_blackrock', 'i140703-001')
  49. # session_name = os.path.join('..', 'datasets_blackrock', 'l101210-001')
  50. odml_dir = os.path.join('..', 'datasets_blackrock')
  51. # Open the session for reading
  52. session = reachgraspio.ReachGraspIO(session_name, odml_directory=odml_dir)
  53. # Read a the complete dataset in lazy mode generating all neo objects,
  54. # but not loading data into memory. The lazy neo structure will contain objects
  55. # to capture all recorded data types (time series at 1000Hz (ns2) and 30kHz (ns6)
  56. # scaled to units of voltage, sorted spike trains, spike waveforms and events)
  57. # of the recording session and return it as a Neo Block. The
  58. # time shift of the ns2 signal (LFP) induced by the online filter is
  59. # automatically corrected for by a heuristic factor stored in the metadata
  60. # (correct_filter_shifts=True).
  61. block = session.read_block(lazy=True, correct_filter_shifts=True)
  62. # Validate there is only a single Segment present in the block
  63. assert len(block.segments) == 1
  64. # loading data content of all data objects during the first 300 seconds
  65. data_segment = load_segment(block.segments[0], time_range=(None, 300*pq.s))
  66. # =============================================================================
  67. # Create offline filtered LFP
  68. #
  69. # Here, we construct one offline filtered LFP from each ns5 (monkey L) or ns6
  70. # (monkey N) raw recording trace. For monkey N, this filtered LFP can be
  71. # compared to the LFPs in the ns2 file (note that monkey L contains only
  72. # behavioral signals in the ns2 file). Also, we assign telling names to each
  73. # Neo AnalogSignal, which is used for plotting later on in this script.
  74. # =============================================================================
  75. target_channel_id = 62
  76. nsx_to_anasig_name = {2: 'LFP signal (online filtered)',
  77. 5: 'raw signal',
  78. 6: 'raw signal'}
  79. filtered_anasig = None
  80. raw_anasig = None
  81. # identify neuronal signals and provide labels for plotting
  82. for anasig in data_segment.analogsignals:
  83. # skip non-neuronal signals
  84. if not anasig.annotations['neural_signal']:
  85. continue
  86. # identify nsx source of signals in this AnalogSignal object
  87. if 'nsx' in anasig.annotations:
  88. nsx = anasig.annotations['nsx']
  89. else:
  90. nsx = np.unique(anasig.array_annotations['nsx'])
  91. assert len(nsx) == 1, 'Different nsx sources in AnalogSignal'
  92. nsx = nsx[0]
  93. if nsx == 2:
  94. # AnalogSignal is LFP from ns2
  95. anasig.name = f'LFP (online filter, ns2)'
  96. filtered_anasig = anasig
  97. elif nsx in [5, 6]:
  98. # AnalogSignal is raw signal from ns5 or ns6
  99. anasig.name = f'raw (ns{nsx})'
  100. raw_anasig = anasig
  101. # Create LFP signal by filtering raw signal if not present already
  102. if filtered_anasig is None:
  103. # Use the Elephant library to filter the signal, filter only target channel
  104. channel_ids = np.asarray(raw_anasig.array_annotations['channel_ids'], dtype=int)
  105. target_channel_index = np.where(target_channel_id == channel_ids)[0]
  106. raw_signal = raw_anasig[:, target_channel_index]
  107. f_anasig = butter(
  108. raw_signal,
  109. highpass_freq=None,
  110. lowpass_freq=250 * pq.Hz,
  111. order=4)
  112. if 'nsx' in anasig.annotations:
  113. nsx = anasig.annotations['nsx']
  114. else:
  115. nsx = anasig.array_annotations["nsx"][0]
  116. f_anasig.name = f'LFP (offline filtered ns{nsx})'
  117. # Attach all offline filtered LFPs to the segment of data
  118. data_segment.analogsignals.append(f_anasig)
  119. # =============================================================================
  120. # Construct analysis epochs
  121. #
  122. # In this step we extract and cut the data into time segments (termed analysis
  123. # epochs) that we wish to analyze. We contrast these analysis epochs to the
  124. # behavioral trials that are defined by the experiment as occurrence of a Trial
  125. # Start (TS-ON) event in the experiment. Concretely, here our analysis epochs
  126. # are constructed as a cutout of 25ms of data around the TS-ON event of all
  127. # successful behavioral trials.
  128. # =============================================================================
  129. # Get Trial Start (TS-ON) events of all successful behavioral trials
  130. # (corresponds to performance code 255, which is accessed for convenience and
  131. # better legibility in the dictionary attribute performance_codes of the
  132. # ReachGraspIO class).
  133. #
  134. # To this end, we filter all event objects of the loaded data to match the name
  135. # "TrialEvents", which is the Event object containing all Events available (see
  136. # documentation of ReachGraspIO). From this Event object we extract only events
  137. # matching "TS-ON" and the desired trial performance code (which are
  138. # annotations of the Event object).
  139. start_events = get_events(
  140. data_segment,
  141. name='TrialEvents',
  142. trial_event_labels='TS-ON',
  143. performance_in_trial=session.performance_codes['correct_trial'])
  144. print('got start events.')
  145. # Extract single Neo Event object containing all TS-ON triggers
  146. assert len(start_events) == 1
  147. start_event = start_events[0]
  148. # Construct analysis epochs from 10ms before the TS-ON of a successful
  149. # behavioral trial to 15ms after TS-ON. The name "analysis_epochs" is given to
  150. # the resulting Neo Epoch object. The object is not attached to the Neo
  151. # Segment. The parameter event2 of add_epoch() is left empty, since we are
  152. # cutting around a single event, as opposed to cutting between two events.
  153. pre = -10 * pq.ms
  154. post = 15 * pq.ms
  155. epoch = add_epoch(
  156. data_segment,
  157. event1=start_event, event2=None,
  158. pre=pre, post=post,
  159. attach_result=False,
  160. name='analysis_epochs',
  161. array_annotations=start_event.array_annotations)
  162. print('added epoch.')
  163. # Create new segments of data cut according to the analysis epochs of the
  164. # 'analysis_epochs' Neo Epoch object. The time axes of all segments are aligned
  165. # such that each segment starts at time 0 (parameter reset_times); annotations
  166. # describing the analysis epoch are carried over to the segments. A new Neo
  167. # Block named "data_cut_to_analysis_epochs" is created to capture all cut
  168. # analysis epochs. For execution time reason, we are only considering the
  169. # first 10 epochs here.
  170. cut_trial_block = Block(name="data_cut_to_analysis_epochs")
  171. cut_trial_block.segments = cut_segment_by_epoch(
  172. data_segment, epoch[:10], reset_time=True)
  173. # =============================================================================
  174. # Plot data
  175. # =============================================================================
  176. # Determine the first existing trial ID i from the Event object containing all
  177. # start events. Then, by calling the filter() function of the Neo Block
  178. # "data_cut_to_analysis_epochs" containing the data cut into the analysis
  179. # epochs, we ask to return all Segments annotated by the behavioral trial ID i.
  180. # In this case this call should return one matching analysis epoch around TS-ON
  181. # belonging to behavioral trial ID i. For monkey N, this is trial ID 1, for
  182. # monkey L this is trial ID 2 since trial ID 1 is not a correct trial.
  183. trial_id = int(np.min(start_event.array_annotations['trial_id']))
  184. trial_segments = cut_trial_block.filter(
  185. targdict={"trial_id": trial_id}, objects=Segment)
  186. assert len(trial_segments) == 1
  187. trial_segment = trial_segments[0]
  188. # Create figure
  189. fig = plt.figure(facecolor='w')
  190. time_unit = pq.CompoundUnit('1./30000*s')
  191. amplitude_unit = pq.microvolt
  192. nsx_colors = {2: 'k', 5: 'r', 6: 'b'}
  193. # Loop through all AnalogSignal objects and plot the signal of the target channel
  194. # in a color corresponding to its sampling frequency (i.e., originating from the ns2/ns5 or ns2/ns6).
  195. for i, anasig in enumerate(trial_segment.analogsignals):
  196. # only visualize neural data
  197. if anasig.annotations['neural_signal']:
  198. if 'nsx' in anasig.annotations:
  199. nsx = anasig.annotations['nsx']
  200. else:
  201. nsx = anasig.array_annotations['nsx'][0]
  202. channel_ids = np.asarray(anasig.array_annotations['channel_ids'], dtype=int)
  203. target_channel_index = np.where(channel_ids == target_channel_id)[0]
  204. target_signal = anasig[:, target_channel_index]
  205. plt.plot(
  206. target_signal.times.rescale(time_unit),
  207. target_signal.squeeze().rescale(amplitude_unit),
  208. label=target_signal.name,
  209. color=nsx_colors[nsx])
  210. # Loop through all spike trains and plot the spike time, and overlapping the
  211. # wave form of the spike used for spike sorting stored separately in the nev
  212. # file.
  213. for st in trial_segment.spiketrains:
  214. color = np.random.rand(3,)
  215. if st.annotations['channel_id'] == target_channel_id:
  216. for spike_id, spike in enumerate(st):
  217. # Plot spike times
  218. plt.axvline(
  219. spike.rescale(time_unit).magnitude,
  220. color=color,
  221. label='Unit ID %i' % st.annotations['unit_id'])
  222. # Plot waveforms
  223. waveform = st.waveforms[spike_id, 0, :]
  224. waveform_times = np.arange(len(waveform))*time_unit + spike
  225. plt.plot(
  226. waveform_times.rescale(time_unit).magnitude,
  227. waveform.rescale(amplitude_unit),
  228. '--',
  229. linewidth=2,
  230. color=color,
  231. zorder=0)
  232. # Loop through all events
  233. for event in trial_segment.events:
  234. if event.name == 'TrialEvents':
  235. for ev_id, ev in enumerate(event):
  236. plt.axvline(
  237. ev.rescale(time_unit),
  238. alpha=0.2,
  239. linewidth=3,
  240. linestyle='dashed',
  241. label=f'event {event.array_annotations["trial_event_labels"][ev_id]}')
  242. # Finishing touches on the plot
  243. plt.autoscale(enable=True, axis='x', tight=True)
  244. plt.xlabel(time_unit.name)
  245. plt.ylabel(amplitude_unit.name)
  246. plt.legend(loc=4, fontsize=10)
  247. # Save plot
  248. file_name = 'example_plot_from_blackrock_%s' % session_name
  249. for file_format in ['eps', 'png', 'pdf']:
  250. fig.savefig(file_name + '.%s' % file_format, dpi=400, format=file_format)