convert2nwb.m 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. % Convert to NWB
  2. %
  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. %
  7. % The script works by loading general, animal, and recording session
  8. % metadata from nwbParams, nwbAnimalParams, nwbSessionParams, respectively.
  9. % It then locates the derived data MAT files for each animal and converts
  10. % them into derived data NWB files dividing the data per recording session.
  11. % The derived data include spiking, waveform, pupil area size fluctuation,
  12. % and total facial movement data.
  13. %
  14. % The conversion pipeline depends on the specific structure of derived data
  15. % MAT files used in this study. The way the pipeline is organised is
  16. % dictated by the fact that the conversion procedure was adopted late in
  17. % the study. Ideally NWB file format should be adopted early in the study.
  18. %
  19. % You can use this pipeline to get an idea of how to convert your own
  20. % ecephys data to the NWB file format to store spiking and behavioural
  21. % data combined with some metadata.
  22. cleanUp
  23. % Load general NWB parameters
  24. nwbParams
  25. % Load animal-specific parameters
  26. nwbAnimalParams
  27. % Load session-specific parameters
  28. nwbSessionParams
  29. % Generate Matlab classes from NWB core schema files
  30. generateCore;
  31. % Generate NWB files for every recording session
  32. if ~exist(animalDerivedDataFolderNWB, 'file')
  33. mkdir(animalDerivedDataFolderNWB) % Folder to store animal's converted NWB data
  34. end
  35. derivedData = animalDerivedDataFile;
  36. for iSess = 1:numel(sessionID)
  37. % Assign NWB file fields
  38. nwb = NwbFile( ...
  39. 'session_description', sessionDescription{iSess},...
  40. 'identifier', [animalID '_' sessionID{iSess}], ...
  41. 'session_start_time', sessionStartTime{iSess}, ...
  42. 'general_experimenter', experimenter, ... % optional
  43. 'general_session_id', sessionID{iSess}, ... % optional
  44. 'general_institution', institution, ... % optional
  45. 'general_related_publications', publications, ... % optional
  46. 'general_notes', sessionNotes{iSess}, ... % optional
  47. 'general_lab', lab); % optional
  48. % Create subject object
  49. subject = types.core.Subject( ...
  50. 'subject_id', animalID, ...
  51. 'age', age, ...
  52. 'description', description, ...
  53. 'species', species, ...
  54. 'sex', sex);
  55. nwb.general_subject = subject;
  56. % Create electrode tables: Info about each recording channel
  57. input.iElectrode = 1;
  58. input.electrodeDescription = electrodeDescription{iSess};
  59. input.electrodeManufacturer = electrodeManufacturer{iSess};
  60. input.nShanks = nShanks{iSess};
  61. input.nChannelsPerShank = nChannelsPerShank{iSess};
  62. input.electrodeLocation = electrodeLocation{iSess};
  63. input.electrodeCoordinates = electrodeCoordinates{iSess};
  64. input.sessionID = sessionID{iSess};
  65. input.electrodeLabel = electrodeLabel{iSess};
  66. if probeInserted{iSess}{input.iElectrode} && ~isempty(endCh{iSess}{1})
  67. tbl1 = createElectrodeTable(nwb, input);
  68. else
  69. tbl1 = [];
  70. end
  71. input.iElectrode = 2;
  72. if probeInserted{iSess}{input.iElectrode} && ~isempty(endCh{iSess}{2})
  73. tbl2 = createElectrodeTable(nwb, input);
  74. else
  75. tbl2 = [];
  76. end
  77. tbl = [tbl1; tbl2];
  78. electrode_table = util.table2nwb(tbl, 'all electrodes');
  79. nwb.general_extracellular_ephys_electrodes = electrode_table;
  80. % Load spike times from the MAT file
  81. [spikes, metadata, derivedData] = getSpikes(derivedData, animalID, sessionID{iSess}, tbl);
  82. [spike_times_vector, spike_times_index] = util.create_indexed_column(spikes);
  83. spike_times_vector.description = 'Session spike times';
  84. spike_times_index.description = 'Indices dividing spike times into units';
  85. % Load and reshape unit waveforms
  86. waveformsFile1 = [electrodeFolder{iSess}{1} filesep 'waveforms.mat'];
  87. if exist(waveformsFile1, 'file')
  88. waveformsProbe1 = load(waveformsFile1);
  89. else
  90. waveformsProbe1 = [];
  91. waveformsProbe1.maxWaveforms = [];
  92. end
  93. if probeInserted{iSess}{1} && ~isempty(spikes)
  94. [waveformMat1, waveformVecGrp1, waveformVec1, waveformMeans1, nWaveformSamples1] = reshapeWaveforms(waveformsProbe1, 1, metadata, nCh{iSess}{1});
  95. else
  96. waveformMat1 = []; waveformVecGrp1 = {}; waveformVec1 = {}; waveformMeans1 = {}; nWaveformSamples1 = 0;
  97. end
  98. waveformsFile2 = [electrodeFolder{iSess}{2} filesep 'waveforms.mat'];
  99. if exist(waveformsFile2, 'file')
  100. waveformsProbe2 = load(waveformsFile2);
  101. else
  102. waveformsProbe2 = [];
  103. waveformsProbe2.maxWaveforms = [];
  104. end
  105. if probeInserted{iSess}{2} && ~isempty(spikes)
  106. [waveformMat2, waveformVecGrp2, waveformVec2, waveformMeans2, nWaveformSamples2] = reshapeWaveforms(waveformsProbe2, 2, metadata, nCh{iSess}{2});
  107. else
  108. waveformMat2 = []; waveformVecGrp2 = {}; waveformVec2 = {}; waveformMeans2 = {}; nWaveformSamples2 = 0;
  109. end
  110. maxWaveforms = [waveformsProbe1.maxWaveforms; waveformsProbe2.maxWaveforms];
  111. waveformMat = [waveformMat1; waveformMat2];
  112. waveformVec = [waveformVec1; waveformVec2];
  113. waveformMeans = [waveformMeans1; waveformMeans2];
  114. nWaveformSamples = max([nWaveformSamples1 nWaveformSamples2]);
  115. for iWave = 1:numel(waveformMeans)
  116. if isempty(waveformMeans{iWave})
  117. waveformMeans{iWave} = nan(1,nWaveformSamples);
  118. end
  119. end
  120. % Create waveform indices
  121. % These indices are not used as our waveform array has a different form and meaning than the one used in the NWB file.
  122. % We only store mean waveforms on maximum amplitude channels.
  123. % More on ragged array indexing used in NWB files see https://nwb-schema.readthedocs.io/en/latest/format_description.html
  124. [waveforms, waveformIndex] = util.create_indexed_column(waveformVec');
  125. waveformIndexIndex = {};
  126. if ~isempty(waveformVec1)
  127. waveformIndexIndex1 = reshape(waveformIndex.data(1:size(waveformVec1,1)),nCh{iSess}{1},size(waveformVec1,1)/nCh{iSess}{1})';
  128. for iUnit = 1:size(waveformVecGrp1,1)
  129. waveformIndexIndex = [waveformIndexIndex; waveformIndexIndex1(iUnit,:)];
  130. end
  131. end
  132. if ~isempty(waveformVec2)
  133. waveformIndexIndex2 = reshape(waveformIndex.data(size(waveformVec1,1)+1:end),nCh{iSess}{2},size(waveformVec2,1)/nCh{iSess}{2})';
  134. for iUnit = 1:size(waveformVecGrp2,1)
  135. waveformIndexIndex = [waveformIndexIndex; waveformIndexIndex2(iUnit,:)];
  136. end
  137. end
  138. [inds, waveformIndexIndex] = util.create_indexed_column(waveformIndexIndex');
  139. % Store spiking and waveform data inside the nwb object
  140. % see https://neurodatawithoutborders.github.io/matnwb/doc/+types/+core/Units.html
  141. if ~isempty(spikes)
  142. nwb.units = types.core.Units( ...
  143. 'colnames', {'cluster_id','local_cluster_id','type',...
  144. 'peak_channel_index','peak_channel_id',... % Provide the column order. All column names have to be defined below
  145. 'local_peak_channel_id','rel_horz_pos','rel_vert_pos',...
  146. 'isi_violations','isolation_distance','area','probe_id',...
  147. 'electrode_group','spike_times','spike_times_index'}, ...
  148. 'description', 'Units table', ...
  149. 'id', types.hdmf_common.ElementIdentifiers( ...
  150. 'data', int64(0:length(spikes) - 1)), ...
  151. 'cluster_id', types.hdmf_common.VectorData( ...
  152. 'data', cell2mat(metadata{:,1}), ...
  153. 'description', 'Unique cluster id'), ...
  154. 'local_cluster_id', types.hdmf_common.VectorData( ...
  155. 'data', cell2mat(metadata{:,2}), ...
  156. 'description', 'Local cluster id on the probe'), ...
  157. 'type', types.hdmf_common.VectorData( ...
  158. 'data', metadata{:,3}, ...
  159. 'description', 'Cluster type: unit vs mua'), ...
  160. 'peak_channel_index', types.hdmf_common.VectorData( ...
  161. 'data', cell2mat(metadata{:,4}), ...
  162. 'description', 'Peak channel row index in the electrode table'), ...
  163. 'peak_channel_id', types.hdmf_common.VectorData( ...
  164. 'data', cell2mat(metadata{:,5}), ...
  165. 'description', 'Unique ID of the channel with the largest cluster waveform amplitude'), ...
  166. 'local_peak_channel_id', types.hdmf_common.VectorData( ...
  167. 'data', cell2mat(metadata{:,6}), ...
  168. 'description', 'Local probe channel with the largest cluster waveform amplitude'), ...
  169. 'rel_horz_pos', types.hdmf_common.VectorData( ...
  170. 'data', cell2mat(metadata{:,7})./1000, ...
  171. 'description', 'Probe-relative horizontal position in mm'), ...
  172. 'rel_vert_pos', types.hdmf_common.VectorData( ...
  173. 'data', cell2mat(metadata{:,8})./1000, ...
  174. 'description', 'Probe tip-relative vertical position in mm'), ...
  175. 'isi_violations', types.hdmf_common.VectorData( ...
  176. 'data', cell2mat(metadata{:,9}), ...
  177. 'description', 'Interstimulus interval violations (unit quality measure)'), ...
  178. 'isolation_distance', types.hdmf_common.VectorData( ...
  179. 'data', cell2mat(metadata{:,10}), ...
  180. 'description', 'Cluster isolation distance (unit quality measure)'), ...
  181. 'area', types.hdmf_common.VectorData( ...
  182. 'data', metadata{:,11}, ...
  183. 'description', ['Brain area where the unit is located. Internal thalamic ' ...
  184. 'nuclei divisions are not precise, because they are derived from unit locations on the probe.']), ...
  185. 'probe_id', types.hdmf_common.VectorData( ...
  186. 'data', metadata{:,12}, ...
  187. 'description', 'Probe id where the unit is located'), ...
  188. 'spike_times', spike_times_vector, ...
  189. 'spike_times_index', spike_times_index, ...
  190. 'electrode_group', types.hdmf_common.VectorData( ...
  191. 'data', metadata{:,13}, ...
  192. 'description', 'Recording channel groups'), ...
  193. 'waveform_mean', types.hdmf_common.VectorData( ...
  194. 'data', cell2mat(waveformMeans), ...
  195. 'description', ['Mean waveforms on the probe channel with the largest waveform amplitude. ' ...
  196. 'MUA waveforms are excluded. The order that waveforms are stored match the order of units in the unit table.']) ...
  197. );
  198. end
  199. % Add behavioural data: Pupil area size
  200. % see https://neurodatawithoutborders.github.io/matnwb/doc/+types/+core/TimeSeries.html
  201. % and https://neurodatawithoutborders.github.io/matnwb/doc/+types/+core/PupilTracking.html
  202. if isfield(derivedData.dataStruct, 'eyeData') && isfield(derivedData.dataStruct.eyeData, [animalID '_s' sessionID{iSess}])
  203. acceptablePeriod = derivedData.dataStruct.eyeData.([animalID '_s' sessionID{iSess}]).period; % Acceptable quality range in seconds
  204. videoFrameTimes = derivedData.dataStruct.eyeData.([animalID '_s' sessionID{iSess}]).frameTimes; % seconds
  205. acceptableSamples = markQualitySamples(acceptablePeriod, videoFrameTimes);
  206. pupilAreaSize = derivedData.dataStruct.eyeData.([animalID '_s' sessionID{iSess}]).pupilArea; % pixels^2
  207. pupilAreaSize = types.core.TimeSeries( ...
  208. 'data', pupilAreaSize, ...
  209. 'timestamps', videoFrameTimes, ...
  210. 'data_unit', 'pixels^2', ...
  211. 'starting_time_rate', videoFrameRate,...
  212. 'control', uint8(acceptableSamples),...
  213. 'control_description', {'low quality samples that should be excluded from analyses';...
  214. 'acceptable quality samples'},...
  215. 'description', ['Pupil area size over the recording session measured in pixels^2. ' ...
  216. 'Acceptable quality period starting and ending times are given by data_continuity parameter. ' ...
  217. 'The full data range can be divided into multiple acceptable periods.'] ...
  218. );
  219. pupilTracking = types.core.PupilTracking('TimeSeries', pupilAreaSize);
  220. behaviorModule = types.core.ProcessingModule('description', 'contains behavioral data');
  221. behaviorModule.nwbdatainterface.set('PupilTracking', pupilTracking);
  222. else
  223. behaviorModule = [];
  224. end
  225. % Add behavioural data: Total movement of the facial area
  226. % see https://neurodatawithoutborders.github.io/matnwb/doc/+types/+core/TimeSeries.html
  227. % and https://neurodatawithoutborders.github.io/matnwb/doc/+types/+core/BehavioralTimeSeries.html
  228. if isfield(derivedData.dataStruct, 'motionData') && isfield(derivedData.dataStruct.motionData, [animalID '_s' sessionID{iSess}])
  229. acceptablePeriod = derivedData.dataStruct.motionData.([animalID '_s' sessionID{iSess}]).period; % Acceptable quality range in seconds
  230. videoFrameTimes = derivedData.dataStruct.motionData.([animalID '_s' sessionID{iSess}]).frameTimes; % seconds
  231. acceptableSamples = markQualitySamples(acceptablePeriod, videoFrameTimes);
  232. totalFaceMovement = derivedData.dataStruct.motionData.([animalID '_s' sessionID{iSess}]).sa; % z-scored change in the frame pixels' content with respect to the previous frame
  233. totalFaceMovement = types.core.TimeSeries( ...
  234. 'data', totalFaceMovement, ...
  235. 'timestamps', videoFrameTimes, ...
  236. 'data_unit', 'a.u.', ...
  237. 'control', uint8(acceptableSamples),...
  238. 'control_description', {'low quality samples that should be excluded from analyses';...
  239. 'acceptable quality samples'},...
  240. 'description', ['Z-scored change in the frame pixels'' content with respect to the previous frame. ' ...
  241. 'It measures the total movement of objects inside the video.'] ...
  242. );
  243. behavioralTimeSeries = types.core.BehavioralTimeSeries('TimeSeries', totalFaceMovement);
  244. if ~exist('behaviorModule', 'var') || isempty(behaviorModule)
  245. behaviorModule = types.core.ProcessingModule('description', 'contains behavioral data');
  246. end
  247. behaviorModule.nwbdatainterface.set('BehavioralTimeSeries', behavioralTimeSeries);
  248. end
  249. if exist('behaviorModule', 'var') && ~isempty(behaviorModule)
  250. nwb.processing.set('behavior', behaviorModule);
  251. end
  252. % Save the NWB file for the session
  253. % If you encounter an hdf5lib2 error, you may need make sure that you are
  254. % not storing cell arrays of numbers in VectorData and that you delete
  255. % old NWB files rather than try overwriting them. For more details see
  256. % https://github.com/NeurodataWithoutBorders/matnwb/issues/462
  257. if iSess < 10
  258. nwbExport(nwb, [animalDerivedDataFolderNWB filesep 'ecephys_session_0' num2str(iSess) '.nwb']);
  259. else
  260. nwbExport(nwb, [animalDerivedDataFolderNWB filesep 'ecephys_session_' num2str(iSess) '.nwb']);
  261. end
  262. end
  263. % Read the NWB file
  264. % nwb2 = nwbRead('ecephys_session_01.nwb');
  265. % a = nwb2.units.getRow(1);
  266. %% Local functions
  267. function tbl = createElectrodeTable(nwb, input)
  268. % tbl = createElectrodeTable(nwb, input)
  269. %
  270. % Function creates an electrode table with the following columns:
  271. % channel_id: a unnique probe channel ID formed by combining session ID,
  272. % probe reference number, and channel number relative to the
  273. % tip of the probe.
  274. % channel_local_index: channel index relative to the tip of the probe.
  275. % Channel indices are only unique within a probe.
  276. % x: channel AP brain surface coordinate (probe inisertion location; mm).
  277. % y: channel ML brain surface coordinate (probe inisertion location; mm).
  278. % z: channel location relative to the tip of the probe in mm.
  279. % imp: channel impedance.
  280. % location: channel brain area location.
  281. % filtering: type of LFP filtering applied.
  282. % group: channel electrode group (e.g., shank 1). NWB documentation on
  283. % ElectrodeGroup datatype is provided in the following links:
  284. % https://nwb-schema.readthedocs.io/en/latest/format.html#electrodegroup
  285. % https://nwb-schema.readthedocs.io/en/latest/format.html#sec-electrodegroup-src
  286. % channel_label
  287. % probe_label.
  288. % The rows of the table correspond to individual recording channels.
  289. %
  290. % Input: nwb - nwb object.
  291. % input - structure with the following fields:
  292. % iElectrode: electrode reference number.
  293. % electrodeDescription: a cell array (n probes) with probe
  294. % desciptions.
  295. % electrodeManufacturer: a cell array of electrode manufacturers.
  296. % nShanks: a cell array of number of shanks.
  297. % nChannelsPerShank: a cell array of electrode number of
  298. % recording channels per shank.
  299. % electrodeLocation: a cell array (n channels) of channel brain
  300. % area locations.
  301. % electrodeCoordinates: a cell array (n probes) with recording
  302. % channel coordinate arrays (n channels by
  303. % 3).
  304. % sessionID: a string with the session ID.
  305. % electrodeLabel: a cell array (n probes) with probe labels.
  306. %
  307. % Output: tbl - a Matlab table object with rows and columns as described
  308. % above.
  309. % Parse input
  310. iEl = input.iElectrode;
  311. nSh = input.nShanks;
  312. nCh = input.nChannelsPerShank;
  313. % Create a table with given column labels
  314. variables = {'channel_id', 'channel_local_index', 'x', 'y', 'z', 'imp', 'location', 'filtering', 'group', 'channel_label', 'probe_label'};
  315. tbl = cell2table(cell(0, length(variables)), 'VariableNames', variables);
  316. % Register the probe device
  317. device = types.core.Device(...
  318. 'description', input.electrodeDescription{iEl}, ...
  319. 'manufacturer', input.electrodeManufacturer{iEl} ...
  320. );
  321. nwb.general_devices.set(['probe' num2str(iEl)], device);
  322. for iShank = 1:nSh{iEl}
  323. % Register a shank electrode group (only one because this is a single shank probe)
  324. electrode_group = types.core.ElectrodeGroup( ...
  325. 'description', ['electrode group for probe' num2str(iEl)], ...
  326. 'location', input.electrodeLocation{iEl}{end}, ...
  327. 'device', types.untyped.SoftLink(device), ...
  328. 'position', table(input.electrodeCoordinates{iEl}(1,1), ...
  329. input.electrodeCoordinates{iEl}(1,2), ...
  330. input.electrodeCoordinates{iEl}(1,3), ...
  331. 'VariableNames',{'x','y','z'}) ...
  332. );
  333. nwb.general_extracellular_ephys.set(['probe' num2str(iEl)], electrode_group);
  334. group_object_view = types.untyped.ObjectView(electrode_group);
  335. % Populate the electrode table
  336. for iCh = 1:nCh{iEl}
  337. if iCh < 10
  338. channelID = str2double([input.sessionID num2str(iEl) '00' num2str(iCh)]);
  339. elseif iCh < 99
  340. channelID = str2double([input.sessionID num2str(iEl) '0' num2str(iCh)]);
  341. else
  342. channelID = str2double([input.sessionID num2str(iEl) num2str(iCh)]);
  343. end
  344. channel_label = ['probe' num2str(iEl) 'shank' num2str(iShank) 'elec' num2str(iCh)];
  345. tbl = [tbl; ...
  346. {channelID, iCh, input.electrodeCoordinates{iEl}(iCh, 1), input.electrodeCoordinates{iEl}(iCh, 2), input.electrodeCoordinates{iEl}(iCh, 3),...
  347. NaN, input.electrodeLocation{iEl}{iCh}, 'unknown', group_object_view, channel_label, input.electrodeLabel{iEl}}]; %#ok<*AGROW>
  348. end
  349. end
  350. end
  351. function [spikes, metadataTbl, derivedData] = getSpikes(animalDerivedDataFile, animalID, sessionID, electrodeTbl)
  352. % [spikes, metadataTbl, derivedData] = getSpikes(animalDerivedDataFile, animalID, sessionID, electrodeTbl)
  353. %
  354. % Function loads Neuronexus spiking data from a MAT file with a custom data
  355. % structure. Input:
  356. % animalDerivedDataFile - a string with derived data file name or already
  357. % loaded data.
  358. % animalID - an animal ID string.
  359. % sessionID - a session of interest ID string.
  360. % electrodeTbl - a Matlab table with electrode information generated by
  361. % the function createElectrodeTable.
  362. % Output: spikes - a 1-by-n cell array (n units) with unit spike times in
  363. % seconds.
  364. % metadataTbl - a Matlab table with rows corresponding to
  365. % individual clusters (units) and columns to:
  366. % cluster_id: a unique cluster ID formed by combining session
  367. % ID, probe reference number, and unit cluster ID.
  368. % local_cluster_id: a unit cluster ID. This is only unique
  369. % within the probe.
  370. % type - activity type: single unit (unit) or multi-unit (mua).
  371. % channel_index: recording channel with the highest unit peak
  372. % index relative to the tip of the probe.
  373. % channel_id: a corresponding unnique probe channel ID formed by
  374. % combining session ID, probe reference number, and
  375. % channel number relative to the tip of the probe.
  376. % local_channel_id: a corresponding channel index relative to the
  377. % tip of the probe. Channel indices are only
  378. % unique within a probe.
  379. % rel_horz_position: relative horizontal position in um.
  380. % rel_vert_position: probe tip-relative vertical position in um.
  381. % isi_violations: interspike interval violations, a cluster
  382. % quality measure.
  383. % isolation_distance: cluster isolation distance, a cluster
  384. % quality measure.
  385. % area: unit brain area location.
  386. % probe_id: probe label.
  387. % electrode_group: channel electrode group (e.g., shank 1). NWB
  388. % documentation on ElectrodeGroup datatype is
  389. % provided in the following links:
  390. % https://nwb-schema.readthedocs.io/en/latest/format.html#electrodegroup
  391. % https://nwb-schema.readthedocs.io/en/latest/format.html#sec-electrodegroup-src
  392. % derivedData - animal data loaded from the MAT derived data file.
  393. % Data series names with different brain areas
  394. if ~isstruct(animalDerivedDataFile)
  395. derivedData = load(animalDerivedDataFile);
  396. else
  397. derivedData = animalDerivedDataFile;
  398. end
  399. dataSeriesNames = {};
  400. for iSeries = 1:11
  401. dataSeriesNames{iSeries} = [animalID '_s' sessionID num2str(iSeries)];
  402. end
  403. dataSeriesNames{iSeries+1} = [animalID '_s' sessionID];
  404. % Series data
  405. seriesDerivedData = {};
  406. for iSeries = 1:numel(dataSeriesNames)
  407. if isfield(derivedData.dataStruct.seriesData, dataSeriesNames{iSeries})
  408. seriesDerivedData{iSeries} = derivedData.dataStruct.seriesData.(dataSeriesNames{iSeries});
  409. srData = seriesDerivedData{iSeries}.conf.samplingParams.srData;
  410. else
  411. seriesDerivedData{iSeries} = [];
  412. end
  413. end
  414. % Series unit data
  415. seriesDerivedUnitData = {};
  416. for iSeries = 1:numel(dataSeriesNames)
  417. if ~isempty(seriesDerivedData{iSeries})
  418. seriesDerivedUnitData{iSeries} = seriesDerivedData{iSeries}.shankData.shank1;
  419. else
  420. seriesDerivedUnitData{iSeries} = [];
  421. end
  422. end
  423. % Series population data
  424. seriesDerivedPopulationData = {};
  425. for iSeries = 1:numel(dataSeriesNames)
  426. if ~isempty(seriesDerivedData{iSeries})
  427. seriesDerivedPopulationData{iSeries} = seriesDerivedData{iSeries}.popData;
  428. else
  429. seriesDerivedPopulationData{iSeries} = [];
  430. end
  431. end
  432. % Spike array
  433. sparseSpikes = [];
  434. for iSeries = 1:numel(dataSeriesNames)
  435. if ~isempty(seriesDerivedPopulationData{iSeries})
  436. sparseSpikes = concatenateMat(sparseSpikes, seriesDerivedPopulationData{iSeries}.spkDB);
  437. end
  438. end
  439. % Spike times
  440. nRows = size(sparseSpikes,1);
  441. if nRows
  442. timeVector = (1:size(sparseSpikes,2))./srData;
  443. for iUnit = 1:nRows
  444. spikes{iUnit} = timeVector(logical(full(sparseSpikes(iUnit,:)))); %#ok<*SAGROW>
  445. end
  446. else
  447. spikes = [];
  448. end
  449. % Unit metadata: [local_unit_id type local_probe_channel horizontal_position vertical_position ...
  450. % isi_violations isolation_distance anterior_posterior_ccf_coordinate ...
  451. % dorsal_ventral_ccf_coordinate left_right_ccf_coordinate]
  452. metadata = [];
  453. if nRows
  454. for iSeries = 1:numel(dataSeriesNames)
  455. if ~isempty(seriesDerivedPopulationData{iSeries})
  456. metadata = concatenateMat(metadata, seriesDerivedPopulationData{iSeries}.muaMetadata);
  457. end
  458. end
  459. end
  460. % Unit metadata: [metadata area]
  461. if nRows
  462. areas = {};
  463. for iSeries = 1:numel(dataSeriesNames)
  464. if ~isempty(seriesDerivedPopulationData{iSeries})
  465. if iSeries == 1
  466. areas = [areas; repmat({'S1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  467. elseif iSeries == 2
  468. areas = [areas; repmat({'VB'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  469. elseif iSeries == 3
  470. areas = [areas; repmat({'Po'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  471. elseif iSeries == 4
  472. areas = [areas; repmat({'LP'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  473. elseif iSeries == 5
  474. areas = [areas; repmat({'DG'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  475. elseif iSeries == 6
  476. areas = [areas; repmat({'CA1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  477. elseif iSeries == 7
  478. areas = [areas; repmat({'RSC'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  479. elseif iSeries == 8
  480. areas = [areas; repmat({'VB'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  481. elseif iSeries == 9
  482. areas = [areas; repmat({'LP'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  483. elseif iSeries == 10
  484. areas = [areas; repmat({'LGN'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  485. elseif iSeries == 11
  486. areas = [areas; repmat({'CA3'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  487. elseif iSeries == 12
  488. areas = [areas; repmat({'VB'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  489. end
  490. end
  491. end
  492. metadata = [num2cell(metadata) areas];
  493. end
  494. % Unit metadata: correct unit type
  495. if nRows
  496. type = {};
  497. for iSeries = 1:numel(dataSeriesNames)
  498. if ~isempty(seriesDerivedPopulationData{iSeries})
  499. units = ismember(seriesDerivedPopulationData{iSeries}.spkDB_units, seriesDerivedUnitData{iSeries}.units);
  500. typeArea = repmat({'mua'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1);
  501. typeArea(units) = {'unit'};
  502. type = [type; typeArea];
  503. end
  504. end
  505. metadata(:,2) = type;
  506. end
  507. % Unit metadata: [metadata probe_id]
  508. if nRows
  509. probeLabel = {};
  510. for iSeries = 1:numel(dataSeriesNames)
  511. if ~isempty(seriesDerivedPopulationData{iSeries})
  512. if iSeries == 1
  513. probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  514. elseif iSeries == 2
  515. probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  516. elseif iSeries == 3
  517. probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  518. elseif iSeries == 4
  519. probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  520. elseif iSeries == 5
  521. probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  522. elseif iSeries == 6
  523. probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  524. elseif iSeries == 7
  525. probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  526. elseif iSeries == 8
  527. probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  528. elseif iSeries == 9
  529. probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  530. elseif iSeries == 10
  531. probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  532. elseif iSeries == 11
  533. probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  534. elseif iSeries == 12
  535. probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
  536. end
  537. end
  538. end
  539. metadata = [metadata probeLabel];
  540. end
  541. % Unit metadata: [unit_id metadata]
  542. if nRows
  543. unitIDs = zeros(nRows,1);
  544. for iUnit = 1:nRows
  545. if strcmpi(metadata{iUnit, end}, 'probe1')
  546. unitID = [num2str(sessionID) '1'];
  547. else
  548. unitID = [num2str(sessionID) '2'];
  549. end
  550. if metadata{iUnit, 1} < 9
  551. unitID = [unitID '000' num2str(metadata{iUnit, 1})];
  552. elseif metadata{iUnit, 1} < 99
  553. unitID = [unitID '00' num2str(metadata{iUnit, 1})];
  554. elseif metadata{iUnit, 1} < 999
  555. unitID = [unitID '0' num2str(metadata{iUnit, 1})];
  556. else
  557. unitID = [unitID num2str(metadata{iUnit, 1})];
  558. end
  559. unitIDs(iUnit) = str2double(unitID);
  560. end
  561. metadata = [num2cell(unitIDs) metadata];
  562. end
  563. % Unit metadata: [metadata(:,1:3) probe_channel_index probe_channel_id metadata(:,4:end)]
  564. if nRows
  565. channelIndices = zeros(nRows,1);
  566. channelIDs = zeros(nRows,1);
  567. electrodeGroups = {};
  568. for iUnit = 1:nRows
  569. ind = table2array(electrodeTbl(:,2)) == cell2mat(metadata(iUnit,4)) &...
  570. contains(table2array(electrodeTbl(:,end)), metadata(iUnit,end));
  571. channelIndices(iUnit) = find(ind);
  572. channelIDs(iUnit) = table2array(electrodeTbl(ind,1));
  573. electrodeGroups = [electrodeGroups; table2array(electrodeTbl(ind,9))];
  574. end
  575. metadata = [metadata(:,1:3) num2cell(channelIndices) num2cell(channelIDs) metadata(:,4:end)];
  576. end
  577. % Unit metadata: [metadata electrode_group]
  578. if nRows
  579. metadataTbl = table(metadata(:,1), metadata(:,2), metadata(:,3), metadata(:,4), ...
  580. metadata(:,5), metadata(:,6), metadata(:,7), metadata(:,8), ...
  581. metadata(:,9), metadata(:,10), metadata(:,11), metadata(:,12), electrodeGroups, ...
  582. 'VariableNames', {'cluster_id', 'local_cluster_id', 'type',...
  583. 'channel_index', 'channel_id', 'local_channel_id',...
  584. 'rel_horz_pos', 'rel_vert_pos', 'isi_violations',...
  585. 'isolation_distance', 'area', 'probe_id', 'electrode_group'});
  586. else
  587. metadataTbl = [];
  588. end
  589. end
  590. function [reshapedWaveformsMat, reshapedWaveformsVecGrp, reshapedWaveformsVec, waveformsMean, nWaveformSamples] = reshapeWaveforms(waveforms, iEl, metadata, nCh)
  591. % [reshapedWaveformsMat, reshapedWaveformsVecGrp, reshapedWaveformsVec, waveformsMean, nWaveformSamples] = reshapeWaveforms(waveforms, iEl, metadata, nCh)
  592. %
  593. % Function extracts relevant waveform information and reshapes the waveform
  594. % array which is 3-dimensional with the first dimension being the unit, the
  595. % second being the sample point, and the third one being the recording
  596. % channel.
  597. % Input: waveforms - a strucuture loaded from the waveforms MAT file.
  598. % Relevant fields are waveforms (described above),
  599. % maxWaveforms (same as waveforms but excluding all
  600. % channels except for the maximum amplitude one), and
  601. % cluIDs (unit cluster IDs corresponding to the
  602. % dimension one in waveforms and maxWaveforms).
  603. % iEl - probe reference number.
  604. % metadata - a Matlab unit table produced by the function
  605. % getSpikes.
  606. % nCh - number of recording channels with waveforms for the same
  607. % unit.
  608. % Output: reshapedWaveformsMat - a 2D array reshaping waveforms.waveforms
  609. % array by collapsing the third dimension
  610. % and stacking all waveforms vertically one
  611. % unit after another.
  612. % reshapedWaveformsVecGrp - a column cell array reshaping
  613. % waveforms.waveforms array and grouping
  614. % all waveforms from the same unit
  615. % together. Cell array entries correspond
  616. % to individual units. The missing MUAs
  617. % correspond to empty cell arrays.
  618. % reshapedWaveformsVec - a cell array with rows from
  619. % reshapedWaveformsMat. Missing MUAs are
  620. % also included as empty cells times the
  621. % number of recording channels.
  622. % waveformsMean - waveforms.waveforms converted into a cell column
  623. % array. MUAs are NaNs.
  624. % nWaveformSamples - a total number of data sample points forming a
  625. % single waveform.
  626. nWaveformSamples = size(waveforms.maxWaveforms,2);
  627. if isfield(waveforms, 'waveforms')
  628. reshapedWaveformsMat = zeros(size(waveforms.waveforms,1)*size(waveforms.waveforms,3),size(waveforms.waveforms,2));
  629. else
  630. reshapedWaveformsMat = [];
  631. end
  632. reshapedWaveformsVecGrp = {};
  633. reshapedWaveformsVec = {};
  634. waveformsMean = {};
  635. metadataInds = ismember(table2cell(metadata(:,12)), ['probe' num2str(iEl)]);
  636. metadata = metadata(metadataInds,:);
  637. for iUnit = 1:size(metadata,1)
  638. if isfield(waveforms, 'cluIDs')
  639. row = find(ismember(waveforms.cluIDs, cell2mat(table2cell(metadata(iUnit,2)))));
  640. else
  641. row = [];
  642. end
  643. if isfield(waveforms, 'cluIDs') && sum(ismember(waveforms.cluIDs, cell2mat(table2cell(metadata(iUnit,2)))))
  644. unitWaveformMat = squeeze(waveforms.waveforms(row,:,:))';
  645. reshapedWaveformsMat((row-1)*size(waveforms.waveforms,3)+1:row*size(waveforms.waveforms,3),:) = unitWaveformMat;
  646. reshapedWaveformsVecGrp = [reshapedWaveformsVecGrp; {unitWaveformMat}];
  647. for iWave = 1:size(unitWaveformMat,1)
  648. reshapedWaveformsVec = [reshapedWaveformsVec; {unitWaveformMat(iWave,:)}];
  649. end
  650. waveformsMean = [waveformsMean; {waveforms.maxWaveforms(row,:)}];
  651. else
  652. reshapedWaveformsVecGrp = [reshapedWaveformsVecGrp; {[]}];
  653. if isfield(waveforms, 'waveforms')
  654. for iWave = 1:size(waveforms.waveforms,3)
  655. reshapedWaveformsVec = [reshapedWaveformsVec; {[]}];
  656. end
  657. else
  658. for iWave = 1:nCh
  659. reshapedWaveformsVec = [reshapedWaveformsVec; {[]}];
  660. end
  661. end
  662. waveformsMean = [waveformsMean; {nan(1,size(waveforms.maxWaveforms,2))}];
  663. end
  664. end
  665. end
  666. function acceptableSamples = markQualitySamples(acceptablePeriod, videoFrameTimes)
  667. % acceptableSamples = markQualitySamples(acceptablePeriod, videoFrameTimes)
  668. %
  669. % Function marks acceptable behavioural samples given the sample times and
  670. % the range of acceptable time periods.
  671. % Input: acceptablePeriod - a vector or a cell array of vectors marking the
  672. % beginning and end of acceptable time periods.
  673. % videoFrameTimes - a vector with sample times.
  674. % Ouptut: acceptableSamples - a logical vector marking acceptable samples
  675. % by ones.
  676. if isempty(acceptablePeriod) || isempty(videoFrameTimes)
  677. acceptableSamples = [];
  678. else
  679. acceptableSamples = false(size(videoFrameTimes));
  680. if iscell(acceptablePeriod)
  681. for iPeriod = 1:numel(acceptablePeriod)
  682. acceptableSamples(videoFrameTimes >= acceptablePeriod{iPeriod}(1) & videoFrameTimes <= acceptablePeriod{iPeriod}(2)) = true;
  683. end
  684. else
  685. acceptableSamples(videoFrameTimes >= acceptablePeriod(1) & videoFrameTimes <= acceptablePeriod(2)) = true;
  686. end
  687. end
  688. end