convert2nwb.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. '''
  2. Convert to NWB
  3. Run this script to convert derived data generated by the Brain-wide
  4. infra-slow dynamics study at UoL to the Neurodata Without Borders (NWB)
  5. file format.
  6. The script works by loading general, animal, and recording session
  7. metadata from nwbParams, nwbAnimalParams, nwbSessionParams, respectively.
  8. It then locates the derived data MAT files for each animal and converts
  9. them into derived data NWB files dividing the data per recording session.
  10. The derived data include spiking, waveform, pupil area size fluctuation,
  11. and total facial movement data.
  12. The conversion pipeline depends on the specific structure of derived data
  13. MAT files used in this study. The way the pipeline is organised is
  14. dictated by the fact that the conversion procedure was adopted late in
  15. the study. Ideally NWB file format should be adopted early in the study.
  16. You can use this pipeline to get an idea of how to convert your own
  17. ecephys data to the NWB file format to store spiking and behavioural
  18. data combined with some metadata.
  19. '''
  20. from datetime import datetime
  21. from dateutil.tz import tzlocal
  22. import numpy as np
  23. from pynwb import NWBFile, NWBHDF5IO, TimeSeries
  24. from pynwb.behavior import PupilTracking, BehavioralTimeSeries
  25. from pynwb.core import DynamicTable
  26. from pynwb.ecephys import ElectricalSeries, LFP
  27. from pynwb.file import Subject
  28. from localFunctions import createElectrodeTable, array2list, getSpikes, reshapeWaveforms, concatenateMat, parsePeriod, markQualitySamples
  29. # Load general NWB parameters
  30. exec(open("nwbParams.py").read())
  31. # Load animal-specific parameters
  32. exec(open("nwbAnimalParams.py").read())
  33. # Load session-specific parameters
  34. exec(open("nwbSessionParams.py").read())
  35. # Generate NWB files for every recording session
  36. if not os.path.isdir(animalDerivedDataFolderNWB):
  37. os.makedirs(animalDerivedDataFolderNWB, exist_ok=True) # Folder to store animal's converted NWB data
  38. derivedData = animalDerivedDataFile
  39. for iSess in range(len(sessionID)):
  40. # Assign NWB file fields
  41. nwb = NWBFile(
  42. session_description = sessionDescription[iSess],
  43. identifier = animalID + '_' + sessionID[iSess],
  44. session_start_time = sessionStartTime[iSess],
  45. experimenter = experimenter, # optional
  46. session_id = sessionID[iSess], # optional
  47. institution = institution, # optional
  48. related_publications = publications, # optional
  49. notes = sessionNotes[iSess], # optional
  50. lab = lab) # optional
  51. # Create subject object
  52. nwb.subject = Subject(
  53. subject_id = animalID,
  54. age = age,
  55. description = description,
  56. species = species,
  57. sex = sex)
  58. # Create electrode tables: Info about each recording channel
  59. input = {
  60. "iElectrode": 0,
  61. "electrodeDescription": electrodeDescription[iSess],
  62. "electrodeManufacturer": electrodeManufacturer[iSess],
  63. "nShanks": nShanks[iSess],
  64. "nChannelsPerShank": nChannelsPerShank[iSess],
  65. "electrodeLocation": electrodeLocation[iSess],
  66. "electrodeCoordinates": electrodeCoordinates[iSess],
  67. "sessionID": sessionID[iSess],
  68. "electrodeLabel": electrodeLabel[iSess]}
  69. if probeInserted[iSess][input["iElectrode"]] and endCh[iSess][input["iElectrode"]].any():
  70. (tbl1, columnLabels, columnDescription) = createElectrodeTable(nwb, input)
  71. else:
  72. tbl1 = []; columnLabels = []; columnDescription = []
  73. input["iElectrode"] = 1
  74. if probeInserted[iSess][input["iElectrode"]] and endCh[iSess][input["iElectrode"]].any():
  75. (tbl2, columnLabels, columnDescription) = createElectrodeTable(nwb, input)
  76. else:
  77. tbl2 = []
  78. if not len(columnLabels):
  79. columnLabels = []; columnDescription = []
  80. if len(tbl1) and len(tbl2):
  81. tbl = array2list(np.append(tbl1, tbl2, axis=0), columnLabels, columnDescription)
  82. elif len(tbl1) and not len(tbl2):
  83. tbl = array2list(tbl1, columnLabels, columnDescription)
  84. elif not len(tbl1) and len(tbl2):
  85. tbl = array2list(tbl2, columnLabels, columnDescription)
  86. else:
  87. tbl = []
  88. if len(tbl):
  89. for iColumn in range(len(columnLabels)):
  90. if columnLabels[iColumn] is not 'location' and columnLabels[iColumn] is not 'group':
  91. nwb.add_electrode_column(name=columnLabels[iColumn], description=columnDescription[iColumn])
  92. for iElec in range(len(tbl[0].data)):
  93. nwb.add_electrode(
  94. channel_id=tbl[0].data[iElec],
  95. channel_local_index=tbl[1].data[iElec],
  96. x=tbl[2].data[iElec],
  97. y=tbl[3].data[iElec],
  98. z=float(tbl[4].data[iElec]),
  99. imp=tbl[5].data[iElec],
  100. location=tbl[6].data[iElec],
  101. filtering=tbl[7].data[iElec],
  102. group=tbl[8].data[iElec],
  103. channel_label=tbl[9].data[iElec],
  104. probe_label=tbl[10].data[iElec]
  105. )
  106. nwb.electrodes.to_dataframe()
  107. # Load spike times from the MAT file
  108. (spikes, metadata, derivedData, columnLabels, columnDescription) = getSpikes(derivedData, animalID, sessionID[iSess], tbl)
  109. # Load and reshape unit waveforms
  110. if probeInserted[iSess][0] and len(spikes) and endCh[iSess][0].any():
  111. waveformsFile1 = os.path.join(electrodeFolder[iSess][0], 'waveforms.mat')
  112. if os.path.isfile(waveformsFile1):
  113. waveformsProbe1 = h5py.File(waveformsFile1,'r')
  114. else:
  115. waveformsProbe1 = []
  116. waveformMeans1 = reshapeWaveforms(waveformsProbe1, 1, metadata)
  117. else:
  118. waveformMeans1 = []
  119. if probeInserted[iSess][1] and len(spikes) and endCh[iSess][1].any():
  120. waveformsFile2 = os.path.join(electrodeFolder[iSess][1], 'waveforms.mat')
  121. if os.path.isfile(waveformsFile2):
  122. waveformsProbe2 = h5py.File(waveformsFile2,'r')
  123. else:
  124. waveformsProbe2 = []
  125. waveformMeans2 = reshapeWaveforms(waveformsProbe2, 2, metadata)
  126. else:
  127. waveformMeans2 = []
  128. waveformMeans = np.squeeze(np.array(waveformMeans1 + waveformMeans2))
  129. # Store spiking and waveform data inside the nwb object
  130. if len(spikes):
  131. for iColumn in range(len(columnLabels)-1):
  132. nwb.add_unit_column(name=columnLabels[iColumn], description=columnDescription[iColumn])
  133. for iUnit in range(len(spikes)):
  134. nwb.add_unit(
  135. cluster_id=metadata[iUnit,0],
  136. local_cluster_id=metadata[iUnit,1],
  137. type=metadata[iUnit,2],
  138. peak_channel_index=metadata[iUnit,3],
  139. peak_channel_id=metadata[iUnit,4],
  140. local_peak_channel_id=metadata[iUnit,5],
  141. rel_horz_pos=metadata[iUnit,6],
  142. rel_vert_pos=metadata[iUnit,7],
  143. isi_violations=metadata[iUnit,8],
  144. isolation_distance=metadata[iUnit,9],
  145. area=metadata[iUnit,10],
  146. probe_id=metadata[iUnit,11],
  147. electrode_group=metadata[iUnit,12],
  148. spike_times=spikes[iUnit],
  149. waveform_mean=waveformMeans[iUnit]
  150. )
  151. # Add behavioural data: Pupil area size
  152. acceptablePeriod = np.array(derivedData.get('dataStruct/eyeData/' + animalID + '_s' + sessionID[iSess] + '/period')).transpose() # Acceptable quality range in seconds
  153. acceptablePeriod = parsePeriod(acceptablePeriod, derivedData)
  154. videoFrameTimes = np.array(derivedData.get('dataStruct/eyeData/' + animalID + '_s' + sessionID[iSess] + '/frameTimes')).transpose() # seconds
  155. pupilAreaSize = np.array(derivedData.get('dataStruct/eyeData/' + animalID + '_s' + sessionID[iSess] + '/pupilArea')).transpose() # pixels^2
  156. if len(pupilAreaSize.shape) and not (len(pupilAreaSize) == 2 and not pupilAreaSize[0] and not pupilAreaSize[1]):
  157. acceptableSamples = markQualitySamples(acceptablePeriod, videoFrameTimes)
  158. pupilAreaSize = TimeSeries(
  159. name='TimeSeries',
  160. data=pupilAreaSize.squeeze(),
  161. timestamps=videoFrameTimes.squeeze(),
  162. unit='pixels^2',
  163. control=acceptableSamples.squeeze(),
  164. control_description='low quality samples that should be excluded from analyses; acceptable quality samples',
  165. description='Pupil area size over the recording session measured in pixels^2. ' +
  166. 'Acceptable quality period starting and ending times are given by data_continuity parameter. ' +
  167. 'The full data range can be divided into multiple acceptable periods.'
  168. )
  169. behaviour_module = nwb.create_processing_module(name="behavior", description="contains behavioral data")
  170. pupilTracking = PupilTracking(pupilAreaSize)
  171. behaviour_module.add(pupilTracking)
  172. # Add behavioural data: Total movement of the facial area
  173. acceptablePeriod = np.array(derivedData.get('dataStruct/motionData/' + animalID + '_s' + sessionID[iSess] + '/period')).transpose() # Acceptable quality range in seconds
  174. acceptablePeriod = parsePeriod(acceptablePeriod, derivedData)
  175. videoFrameTimes = np.array(derivedData.get('dataStruct/motionData/' + animalID + '_s' + sessionID[iSess] + '/frameTimes')).transpose() # seconds
  176. totalFaceMovement = np.array(derivedData.get('dataStruct/motionData/' + animalID + '_s' + sessionID[iSess] + '/sa')).transpose() # z-scored change in the frame pixels' content with respect to the previous frame
  177. if len(totalFaceMovement.shape) and not (len(totalFaceMovement) == 2 and not totalFaceMovement[0] and not totalFaceMovement[1]):
  178. acceptableSamples = markQualitySamples(acceptablePeriod, videoFrameTimes)
  179. totalFaceMovement = TimeSeries(
  180. name='TimeSeries',
  181. data=totalFaceMovement.squeeze(),
  182. timestamps=videoFrameTimes.squeeze(),
  183. unit='a.u.',
  184. control=acceptableSamples.squeeze(),
  185. control_description='low quality samples that should be excluded from analyses; acceptable quality samples',
  186. description='Z-scored change in the frame pixels'' content with respect to the previous frame. ' +
  187. 'It measures the total movement of objects inside the video.'
  188. )
  189. if not ('behaviour_module' in locals() or 'behaviour_module' in globals()):
  190. behaviour_module = nwb.create_processing_module(name="behavior", description="contains behavioral data")
  191. behavioralTimeSeries = BehavioralTimeSeries(totalFaceMovement)
  192. behaviour_module.add(behavioralTimeSeries)
  193. else:
  194. acceptablePeriod = []; videoFrameTimes = []; pupilAreaSize = []; totalFaceMovement = [];
  195. # Save the NWB file for the session
  196. if iSess < 9:
  197. nwbFilename = os.path.join(animalDerivedDataFolderNWB, 'ecephys_session_0' + str(iSess+1) + '.nwb')
  198. else:
  199. nwbFilename = os.path.join(animalDerivedDataFolderNWB, 'ecephys_session_' + str(iSess+1) + '.nwb')
  200. with NWBHDF5IO(nwbFilename, "w") as io:
  201. io.write(nwb)
  202. # Delete variables as a precaution
  203. try:
  204. del behaviour_module, acceptableSamples
  205. except:
  206. print('behaviour_module does not exist')
  207. del nwb, input, tbl, spikes, metadata, columnLabels, columnDescription, waveformMeans
  208. del acceptablePeriod, videoFrameTimes, pupilAreaSize, totalFaceMovement