123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741 |
- % Convert to NWB
- %
- % Run this script to convert derived data generated by the Brain-wide
- % infra-slow dynamics study at UoL to the Neurodata Without Borders (NWB)
- % file format.
- %
- % The script works by loading general, animal, and recording session
- % metadata from nwbParams, nwbAnimalParams, nwbSessionParams, respectively.
- % It then locates the derived data MAT files for each animal and converts
- % them into derived data NWB files dividing the data per recording session.
- % The derived data include spiking, waveform, pupil area size fluctuation,
- % and total facial movement data.
- %
- % The conversion pipeline depends on the specific structure of derived data
- % MAT files used in this study. The way the pipeline is organised is
- % dictated by the fact that the conversion procedure was adopted late in
- % the study. Ideally NWB file format should be adopted early in the study.
- %
- % You can use this pipeline to get an idea of how to convert your own
- % ecephys data to the NWB file format to store spiking and behavioural
- % data combined with some metadata.
- cleanUp
- % Load general NWB parameters
- nwbParams
- % Load animal-specific parameters
- nwbAnimalParams
- % Load session-specific parameters
- nwbSessionParams
- % Generate Matlab classes from NWB core schema files
- generateCore;
- % Generate NWB files for every recording session
- if ~exist(animalDerivedDataFolderNWB, 'file')
- mkdir(animalDerivedDataFolderNWB) % Folder to store animal's converted NWB data
- end
- derivedData = animalDerivedDataFile;
- for iSess = 1:numel(sessionID)
- % Assign NWB file fields
- nwb = NwbFile( ...
- 'session_description', sessionDescription{iSess},...
- 'identifier', [animalID '_' sessionID{iSess}], ...
- 'session_start_time', sessionStartTime{iSess}, ...
- 'general_experimenter', experimenter, ... % optional
- 'general_session_id', sessionID{iSess}, ... % optional
- 'general_institution', institution, ... % optional
- 'general_related_publications', publications, ... % optional
- 'general_notes', sessionNotes{iSess}, ... % optional
- 'general_lab', lab); % optional
-
- % Create subject object
- subject = types.core.Subject( ...
- 'subject_id', animalID, ...
- 'age', age, ...
- 'description', description, ...
- 'species', species, ...
- 'sex', sex);
- nwb.general_subject = subject;
-
- % Create electrode tables: Info about each recording channel
- input.iElectrode = 1;
- input.electrodeDescription = electrodeDescription{iSess};
- input.electrodeManufacturer = electrodeManufacturer{iSess};
- input.nShanks = nShanks{iSess};
- input.nChannelsPerShank = nChannelsPerShank{iSess};
- input.electrodeLocation = electrodeLocation{iSess};
- input.electrodeCoordinates = electrodeCoordinates{iSess};
- input.sessionID = sessionID{iSess};
- input.electrodeLabel = electrodeLabel{iSess};
- if probeInserted{iSess}{input.iElectrode} && ~isempty(endCh{iSess}{1})
- tbl1 = createElectrodeTable(nwb, input);
- else
- tbl1 = [];
- end
-
- input.iElectrode = 2;
- if probeInserted{iSess}{input.iElectrode} && ~isempty(endCh{iSess}{2})
- tbl2 = createElectrodeTable(nwb, input);
- else
- tbl2 = [];
- end
-
- tbl = [tbl1; tbl2];
- electrode_table = util.table2nwb(tbl, 'all electrodes');
- nwb.general_extracellular_ephys_electrodes = electrode_table;
-
- % Load spike times from the MAT file
- [spikes, metadata, derivedData] = getSpikes(derivedData, animalID, sessionID{iSess}, tbl);
- [spike_times_vector, spike_times_index] = util.create_indexed_column(spikes);
- spike_times_vector.description = 'Session spike times';
- spike_times_index.description = 'Indices dividing spike times into units';
-
- % Load and reshape unit waveforms
- waveformsFile1 = [electrodeFolder{iSess}{1} filesep 'waveforms.mat'];
- if exist(waveformsFile1, 'file')
- waveformsProbe1 = load(waveformsFile1);
- else
- waveformsProbe1 = [];
- waveformsProbe1.maxWaveforms = [];
- end
- if probeInserted{iSess}{1} && ~isempty(spikes)
- [waveformMat1, waveformVecGrp1, waveformVec1, waveformMeans1, nWaveformSamples1] = reshapeWaveforms(waveformsProbe1, 1, metadata, nCh{iSess}{1});
- else
- waveformMat1 = []; waveformVecGrp1 = {}; waveformVec1 = {}; waveformMeans1 = {}; nWaveformSamples1 = 0;
- end
- waveformsFile2 = [electrodeFolder{iSess}{2} filesep 'waveforms.mat'];
- if exist(waveformsFile2, 'file')
- waveformsProbe2 = load(waveformsFile2);
- else
- waveformsProbe2 = [];
- waveformsProbe2.maxWaveforms = [];
- end
- if probeInserted{iSess}{2} && ~isempty(spikes)
- [waveformMat2, waveformVecGrp2, waveformVec2, waveformMeans2, nWaveformSamples2] = reshapeWaveforms(waveformsProbe2, 2, metadata, nCh{iSess}{2});
- else
- waveformMat2 = []; waveformVecGrp2 = {}; waveformVec2 = {}; waveformMeans2 = {}; nWaveformSamples2 = 0;
- end
- maxWaveforms = [waveformsProbe1.maxWaveforms; waveformsProbe2.maxWaveforms];
- waveformMat = [waveformMat1; waveformMat2];
- waveformVec = [waveformVec1; waveformVec2];
- waveformMeans = [waveformMeans1; waveformMeans2];
- nWaveformSamples = max([nWaveformSamples1 nWaveformSamples2]);
- for iWave = 1:numel(waveformMeans)
- if isempty(waveformMeans{iWave})
- waveformMeans{iWave} = nan(1,nWaveformSamples);
- end
- end
-
- % Create waveform indices
- % These indices are not used as our waveform array has a different form and meaning than the one used in the NWB file.
- % We only store mean waveforms on maximum amplitude channels.
- % More on ragged array indexing used in NWB files see https://nwb-schema.readthedocs.io/en/latest/format_description.html
- [waveforms, waveformIndex] = util.create_indexed_column(waveformVec');
- waveformIndexIndex = {};
- if ~isempty(waveformVec1)
- waveformIndexIndex1 = reshape(waveformIndex.data(1:size(waveformVec1,1)),nCh{iSess}{1},size(waveformVec1,1)/nCh{iSess}{1})';
- for iUnit = 1:size(waveformVecGrp1,1)
- waveformIndexIndex = [waveformIndexIndex; waveformIndexIndex1(iUnit,:)];
- end
- end
- if ~isempty(waveformVec2)
- waveformIndexIndex2 = reshape(waveformIndex.data(size(waveformVec1,1)+1:end),nCh{iSess}{2},size(waveformVec2,1)/nCh{iSess}{2})';
- for iUnit = 1:size(waveformVecGrp2,1)
- waveformIndexIndex = [waveformIndexIndex; waveformIndexIndex2(iUnit,:)];
- end
- end
- [inds, waveformIndexIndex] = util.create_indexed_column(waveformIndexIndex');
-
- % Store spiking and waveform data inside the nwb object
- % see https://neurodatawithoutborders.github.io/matnwb/doc/+types/+core/Units.html
- if ~isempty(spikes)
- nwb.units = types.core.Units( ...
- 'colnames', {'cluster_id','local_cluster_id','type',...
- 'peak_channel_index','peak_channel_id',... % Provide the column order. All column names have to be defined below
- 'local_peak_channel_id','rel_horz_pos','rel_vert_pos',...
- 'isi_violations','isolation_distance','area','probe_id',...
- 'spike_times','spike_times_index'}, ...
- 'description', 'Units table', ...
- 'id', types.hdmf_common.ElementIdentifiers( ...
- 'data', int64(0:length(spikes) - 1)), ...
- 'cluster_id', types.hdmf_common.VectorData( ...
- 'data', cell2mat(metadata{:,1}), ...
- 'description', 'Unique cluster id'), ...
- 'local_cluster_id', types.hdmf_common.VectorData( ...
- 'data', cell2mat(metadata{:,2}), ...
- 'description', 'Local cluster id on the probe'), ...
- 'type', types.hdmf_common.VectorData( ...
- 'data', metadata{:,3}, ...
- 'description', 'Cluster type: unit vs mua'), ...
- 'peak_channel_index', types.hdmf_common.VectorData( ...
- 'data', cell2mat(metadata{:,4}), ...
- 'description', 'Peak channel row index in the electrode table'), ...
- 'peak_channel_id', types.hdmf_common.VectorData( ...
- 'data', cell2mat(metadata{:,5}), ...
- 'description', 'Unique ID of the channel with the largest cluster waveform amplitude'), ...
- 'local_peak_channel_id', types.hdmf_common.VectorData( ...
- 'data', cell2mat(metadata{:,6}), ...
- 'description', 'Local probe channel with the largest cluster waveform amplitude'), ...
- 'rel_horz_pos', types.hdmf_common.VectorData( ...
- 'data', cell2mat(metadata{:,7})./1000, ...
- 'description', 'Probe-relative horizontal position in mm'), ...
- 'rel_vert_pos', types.hdmf_common.VectorData( ...
- 'data', cell2mat(metadata{:,8})./1000, ...
- 'description', 'Probe tip-relative vertical position in mm'), ...
- 'isi_violations', types.hdmf_common.VectorData( ...
- 'data', cell2mat(metadata{:,9}), ...
- 'description', 'Interstimulus interval violations (unit quality measure)'), ...
- 'isolation_distance', types.hdmf_common.VectorData( ...
- 'data', cell2mat(metadata{:,10}), ...
- 'description', 'Cluster isolation distance (unit quality measure)'), ...
- 'area', types.hdmf_common.VectorData( ...
- 'data', metadata{:,11}, ...
- 'description', ['Brain area where the unit is located. Internal thalamic' ...
- 'nuclei divisions are not precise, because they are derived from unit locations on the probe.']), ...
- 'probe_id', types.hdmf_common.VectorData( ...
- 'data', metadata{:,12}, ...
- 'description', 'Probe id where the unit is located'), ...
- 'spike_times', spike_times_vector, ...
- 'spike_times_index', spike_times_index, ...
- 'electrode_group', types.hdmf_common.VectorData( ...
- 'data', metadata{:,13}, ...
- 'description', 'Recording channel groups'), ...
- 'electrodes', types.hdmf_common.DynamicTableRegion('table', ...
- types.untyped.ObjectView('/general/extracellular_ephys/electrodes'), ...
- 'description', 'Probe recording channels', ...
- 'data', cell2mat(table2array(metadata(:,1)))), ...
- 'waveform_mean', types.hdmf_common.VectorData( ...
- 'data', cell2mat(waveformMeans), ...
- 'description', ['Mean waveforms on the probe channel with the largest waveform amplitude.' ...
- 'MUA waveforms are excluded. The order that waveforms are stored match the order of units in the unit table.']) ...
- );
- end
-
- % Add behavioural data: Pupil area size
- % see https://neurodatawithoutborders.github.io/matnwb/doc/+types/+core/TimeSeries.html
- % and https://neurodatawithoutborders.github.io/matnwb/doc/+types/+core/PupilTracking.html
- if isfield(derivedData.dataStruct, 'eyeData') && isfield(derivedData.dataStruct.eyeData, [animalID '_s' sessionID{iSess}])
- acceptablePeriod = derivedData.dataStruct.eyeData.([animalID '_s' sessionID{iSess}]).period; % Acceptable quality range in seconds
- videoFrameTimes = derivedData.dataStruct.eyeData.([animalID '_s' sessionID{iSess}]).frameTimes; % seconds
- acceptableSamples = markQualitySamples(acceptablePeriod, videoFrameTimes);
- pupilAreaSize = derivedData.dataStruct.eyeData.([animalID '_s' sessionID{iSess}]).pupilArea; % pixels^2
- pupilAreaSize = types.core.TimeSeries( ...
- 'data', pupilAreaSize, ...
- 'timestamps', videoFrameTimes, ...
- 'data_unit', 'pixels^2', ...
- 'starting_time_rate', videoFrameRate,...
- 'control', uint8(acceptableSamples),...
- 'control_description', {'low quality samples that should be excluded from analyses';...
- 'acceptable quality samples'},...
- 'description', ['Pupil area size over the recording session measured in pixels^2' ...
- 'Acceptable quality period starting and ending times are given by data_continuity parameter.' ...
- 'The full data range can be divided into multiple acceptable periods'] ...
- );
-
- pupilTracking = types.core.PupilTracking('TimeSeries', pupilAreaSize);
- behaviorModule = types.core.ProcessingModule('description', 'contains behavioral data');
- behaviorModule.nwbdatainterface.set('PupilTracking', pupilTracking);
- else
- behaviorModule = [];
- end
-
- % Add behavioural data: Total movement of the facial area
- % see https://neurodatawithoutborders.github.io/matnwb/doc/+types/+core/TimeSeries.html
- % and https://neurodatawithoutborders.github.io/matnwb/doc/+types/+core/BehavioralTimeSeries.html
- if isfield(derivedData.dataStruct, 'motionData') && isfield(derivedData.dataStruct.motionData, [animalID '_s' sessionID{iSess}])
- acceptablePeriod = derivedData.dataStruct.motionData.([animalID '_s' sessionID{iSess}]).period; % Acceptable quality range in seconds
- videoFrameTimes = derivedData.dataStruct.motionData.([animalID '_s' sessionID{iSess}]).frameTimes; % seconds
- acceptableSamples = markQualitySamples(acceptablePeriod, videoFrameTimes);
- totalFaceMovement = derivedData.dataStruct.motionData.([animalID '_s' sessionID{iSess}]).sa; % z-scored change in the frame pixels' content with respect to the previous frame
- totalFaceMovement = types.core.TimeSeries( ...
- 'data', totalFaceMovement, ...
- 'timestamps', videoFrameTimes, ...
- 'data_unit', 'a.u.', ...
- 'control', uint8(acceptableSamples),...
- 'control_description', {'low quality samples that should be excluded from analyses';...
- 'acceptable quality samples'},...
- 'description', ['Z-scored change in the frame pixels'' content with respect to the previous frame.' ...
- 'It measures the total movement of objects inside the video.'] ...
- );
-
- behavioralTimeSeries = types.core.BehavioralTimeSeries('TimeSeries', totalFaceMovement);
- if ~exist('behaviorModule', 'var') || isempty(behaviorModule)
- behaviorModule = types.core.ProcessingModule('description', 'contains behavioral data');
- end
- behaviorModule.nwbdatainterface.set('BehavioralTimeSeries', behavioralTimeSeries);
- end
-
- if exist('behaviorModule', 'var') && ~isempty(behaviorModule)
- nwb.processing.set('behavior', behaviorModule);
- end
-
- % Save the NWB file for the session
- % If you encounter an hdf5lib2 error, you may need make sure that you are
- % not storing cell arrays of numbers in VectorData and that you delete
- % old NWB files rather than try overwriting them. For more details see
- % https://github.com/NeurodataWithoutBorders/matnwb/issues/462
- if iSess < 10
- nwbExport(nwb, [animalDerivedDataFolderNWB filesep 'ecephys_session_0' num2str(iSess) '.nwb']);
- else
- nwbExport(nwb, [animalDerivedDataFolderNWB filesep 'ecephys_session_' num2str(iSess) '.nwb']);
- end
- end
- % Read the NWB file
- % nwb2 = nwbRead('ecephys_session_01.nwb');
- % a = nwb2.units.getRow(1);
- %% Local functions
- function tbl = createElectrodeTable(nwb, input)
- % tbl = createElectrodeTable(nwb, input)
- %
- % Function creates an electrode table with the following columns:
- % channel_id: a unnique probe channel ID formed by combining session ID,
- % probe reference number, and channel number relative to the
- % tip of the probe.
- % channel_local_index: channel index relative to the tip of the probe.
- % Channel indices are only unique within a probe.
- % x: channel AP brain surface coordinate (probe inisertion location; mm).
- % y: channel ML brain surface coordinate (probe inisertion location; mm).
- % z: channel location relative to the tip of the probe in mm.
- % imp: channel impedance.
- % location: channel brain area location.
- % filtering: type of LFP filtering applied.
- % group: channel electrode group (e.g., shank 1). NWB documentation on
- % ElectrodeGroup datatype is provided in the following links:
- % https://nwb-schema.readthedocs.io/en/latest/format.html#electrodegroup
- % https://nwb-schema.readthedocs.io/en/latest/format.html#sec-electrodegroup-src
- % channel_label
- % probe_label.
- % The rows of the table correspond to individual recording channels.
- %
- % Input: nwb - nwb object.
- % input - structure with the following fields:
- % iElectrode: electrode reference number.
- % electrodeDescription: a cell array (n probes) with probe
- % desciptions.
- % electrodeManufacturer: a cell array of electrode manufacturers.
- % nShanks: a cell array of number of shanks.
- % nChannelsPerShank: a cell array of electrode number of
- % recording channels per shank.
- % electrodeLocation: a cell array (n channels) of channel brain
- % area locations.
- % electrodeCoordinates: a cell array (n probes) with recording
- % channel coordinate arrays (n channels by
- % 3).
- % sessionID: a string with the session ID.
- % electrodeLabel: a cell array (n probes) with probe labels.
- %
- % Output: tbl - a Matlab table object with rows and columns as described
- % above.
- % Parse input
- iEl = input.iElectrode;
- nSh = input.nShanks;
- nCh = input.nChannelsPerShank;
- % Create an table with given column labels
- variables = {'channel_id', 'channel_local_index', 'x', 'y', 'z', 'imp', 'location', 'filtering', 'group', 'channel_label', 'probe_label'};
- tbl = cell2table(cell(0, length(variables)), 'VariableNames', variables);
- % Register the probe device
- device = types.core.Device(...
- 'description', input.electrodeDescription{iEl}, ...
- 'manufacturer', input.electrodeManufacturer{iEl} ...
- );
- nwb.general_devices.set(['probe' num2str(iEl)], device);
- for iShank = 1:nSh{iEl}
-
- % Register a shank electrode group (only one because this is a single shank probe)
- electrode_group = types.core.ElectrodeGroup( ...
- 'description', ['electrode group for probe' num2str(iEl)], ...
- 'location', input.electrodeLocation{iEl}{end}, ...
- 'device', types.untyped.SoftLink(device), ...
- 'position', table(input.electrodeCoordinates{iEl}(1,1), ...
- input.electrodeCoordinates{iEl}(1,2), ...
- input.electrodeCoordinates{iEl}(1,3), ...
- 'VariableNames',{'x','y','z'}) ...
- );
- nwb.general_extracellular_ephys.set(['probe' num2str(iEl)], electrode_group);
- group_object_view = types.untyped.ObjectView(electrode_group);
-
- % Populate the electrode table
- for iCh = 1:nCh{iEl}
- if iCh < 10
- channelID = str2double([input.sessionID num2str(iEl) '00' num2str(iCh)]);
- elseif iCh < 99
- channelID = str2double([input.sessionID num2str(iEl) '0' num2str(iCh)]);
- else
- channelID = str2double([input.sessionID num2str(iEl) num2str(iCh)]);
- end
- channel_label = ['probe' num2str(iEl) 'shank' num2str(iShank) 'elec' num2str(iCh)];
- tbl = [tbl; ...
- {channelID, iCh, input.electrodeCoordinates{iEl}(iCh, 1), input.electrodeCoordinates{iEl}(iCh, 2), input.electrodeCoordinates{iEl}(iCh, 3),...
- NaN, input.electrodeLocation{iEl}{iCh}, 'unknown', group_object_view, channel_label, input.electrodeLabel{iEl}}]; %#ok<*AGROW>
- end
- end
- end
- function [spikes, metadataTbl, derivedData] = getSpikes(animalDerivedDataFile, animalID, sessionID, electrodeTbl)
- % getSpikes(animalDerivedDataFile, animalID, sessionID, electrodeTbl)
- %
- % Function loads Neuronexus spiking data from a MAT file with a custom data
- % structure. Input:
- % animalDerivedDataFile - a string with derived data file name or already
- % loaded data.
- % animalID - an animal ID string.
- % sessionID - a session of interest ID string.
- % electrodeTbl - a Matlab table with electrode information generated by
- % the function createElectrodeTable.
- % Output: spikes - a 1-by-n cell array (n units) with unit spike times in
- % seconds.
- % metadataTbl - a Matlab table with rows corresponding to
- % individual clusters (units) and columns to:
- % cluster_id: a unique cluster ID formed by combining session
- % ID, probe reference number, and unit cluster ID.
- % local_cluster_id: a unit cluster ID. This is only unique
- % within the probe.
- % type - activity type: single unit (unit) or multi-unit (mua).
- % channel_index: recording channel with the highest unit peak
- % index relative to the tip of the probe.
- % channel_id: a corresponding unnique probe channel ID formed by
- % combining session ID, probe reference number, and
- % channel number relative to the tip of the probe.
- % local_channel_id: a corresponding channel index relative to the
- % tip of the probe. Channel indices are only
- % unique within a probe.
- % rel_horz_position: relative horizontal position in um.
- % rel_vert_position: probe tip-relative vertical position in um.
- % isi_violations: interspike interval violations, a cluster
- % quality measure.
- % isolation_distance: cluster isolation distance, a cluster
- % quality measure.
- % area: unit brain area location.
- % probe_id: probe label.
- % electrode_group: channel electrode group (e.g., shank 1). NWB
- % documentation on ElectrodeGroup datatype is
- % provided in the following links:
- % https://nwb-schema.readthedocs.io/en/latest/format.html#electrodegroup
- % https://nwb-schema.readthedocs.io/en/latest/format.html#sec-electrodegroup-src
- % derivedData - animal data loaded from the MAT derived data file.
- % Data series names with different brain areas
- if ~isstruct(animalDerivedDataFile)
- derivedData = load(animalDerivedDataFile);
- else
- derivedData = animalDerivedDataFile;
- end
- dataSeriesNames = {};
- for iSeries = 1:11
- dataSeriesNames{iSeries} = [animalID '_s' sessionID num2str(iSeries)];
- end
- dataSeriesNames{iSeries+1} = [animalID '_s' sessionID];
- % Series data
- seriesDerivedData = {};
- for iSeries = 1:numel(dataSeriesNames)
- if isfield(derivedData.dataStruct.seriesData, dataSeriesNames{iSeries})
- seriesDerivedData{iSeries} = derivedData.dataStruct.seriesData.(dataSeriesNames{iSeries});
- srData = seriesDerivedData{iSeries}.conf.samplingParams.srData;
- else
- seriesDerivedData{iSeries} = [];
- end
- end
- % Series unit data
- seriesDerivedUnitData = {};
- for iSeries = 1:numel(dataSeriesNames)
- if ~isempty(seriesDerivedData{iSeries})
- seriesDerivedUnitData{iSeries} = seriesDerivedData{iSeries}.shankData.shank1;
- else
- seriesDerivedUnitData{iSeries} = [];
- end
- end
- % Series population data
- seriesDerivedPopulationData = {};
- for iSeries = 1:numel(dataSeriesNames)
- if ~isempty(seriesDerivedData{iSeries})
- seriesDerivedPopulationData{iSeries} = seriesDerivedData{iSeries}.popData;
- else
- seriesDerivedPopulationData{iSeries} = [];
- end
- end
- % Spike array
- sparseSpikes = [];
- for iSeries = 1:numel(dataSeriesNames)
- if ~isempty(seriesDerivedPopulationData{iSeries})
- sparseSpikes = concatenateMat(sparseSpikes, seriesDerivedPopulationData{iSeries}.spkDB);
- end
- end
- % Spike times
- nRows = size(sparseSpikes,1);
- if nRows
- timeVector = (1:size(sparseSpikes,2))./srData;
- for iUnit = 1:nRows
- spikes{iUnit} = timeVector(logical(full(sparseSpikes(iUnit,:)))); %#ok<*SAGROW>
- end
- else
- spikes = [];
- end
- % Unit metadata: [local_unit_id type local_probe_channel horizontal_position vertical_position ...
- % isi_violations isolation_distance anterior_posterior_ccf_coordinate ...
- % dorsal_ventral_ccf_coordinate left_right_ccf_coordinate]
- metadata = [];
- if nRows
- for iSeries = 1:numel(dataSeriesNames)
- if ~isempty(seriesDerivedPopulationData{iSeries})
- metadata = concatenateMat(metadata, seriesDerivedPopulationData{iSeries}.muaMetadata);
- end
- end
- end
- % Unit metadata: [metadata area]
- if nRows
- areas = {};
- for iSeries = 1:numel(dataSeriesNames)
- if ~isempty(seriesDerivedPopulationData{iSeries})
- if iSeries == 1
- areas = [areas; repmat({'S1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 2
- areas = [areas; repmat({'VB'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 3
- areas = [areas; repmat({'Po'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 4
- areas = [areas; repmat({'LP'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 5
- areas = [areas; repmat({'DG'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 6
- areas = [areas; repmat({'CA1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 7
- areas = [areas; repmat({'RSC'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 8
- areas = [areas; repmat({'VB'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 9
- areas = [areas; repmat({'LP'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 10
- areas = [areas; repmat({'LGN'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 11
- areas = [areas; repmat({'CA3'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 12
- areas = [areas; repmat({'VB'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- end
- end
- end
- metadata = [num2cell(metadata) areas];
- end
- % Unit metadata: correct unit type
- if nRows
- type = {};
- for iSeries = 1:numel(dataSeriesNames)
- if ~isempty(seriesDerivedPopulationData{iSeries})
- units = ismember(seriesDerivedPopulationData{iSeries}.spkDB_units, seriesDerivedUnitData{iSeries}.units);
- typeArea = repmat({'mua'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1);
- typeArea(units) = {'unit'};
- type = [type; typeArea];
- end
- end
- metadata(:,2) = type;
- end
- % Unit metadata: [metadata probe_id]
- if nRows
- probeLabel = {};
- for iSeries = 1:numel(dataSeriesNames)
- if ~isempty(seriesDerivedPopulationData{iSeries})
- if iSeries == 1
- probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 2
- probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 3
- probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 4
- probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 5
- probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 6
- probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 7
- probeLabel = [probeLabel; repmat({'probe2'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 8
- probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 9
- probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 10
- probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 11
- probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- elseif iSeries == 12
- probeLabel = [probeLabel; repmat({'probe1'}, size(seriesDerivedPopulationData{iSeries}.muaMetadata,1), 1)];
- end
- end
- end
- metadata = [metadata probeLabel];
- end
- % Unit metadata: [unit_id metadata]
- if nRows
- unitIDs = zeros(nRows,1);
- for iUnit = 1:nRows
- if strcmpi(metadata{iUnit, end}, 'probe1')
- unitID = [num2str(sessionID) '1'];
- else
- unitID = [num2str(sessionID) '2'];
- end
- if metadata{iUnit, 1} < 9
- unitID = [unitID '000' num2str(metadata{iUnit, 1})];
- elseif metadata{iUnit, 1} < 99
- unitID = [unitID '00' num2str(metadata{iUnit, 1})];
- elseif metadata{iUnit, 1} < 999
- unitID = [unitID '0' num2str(metadata{iUnit, 1})];
- else
- unitID = [unitID num2str(metadata{iUnit, 1})];
- end
- unitIDs(iUnit) = str2double(unitID);
- end
- metadata = [num2cell(unitIDs) metadata];
- end
- % Unit metadata: [metadata(:,1:3) probe_channel_index probe_channel_id metadata(:,4:end)]
- if nRows
- channelIndices = zeros(nRows,1);
- channelIDs = zeros(nRows,1);
- electrodeGroups = {};
- for iUnit = 1:nRows
- ind = table2array(electrodeTbl(:,2)) == cell2mat(metadata(iUnit,4)) &...
- contains(table2array(electrodeTbl(:,end)), metadata(iUnit,end));
- channelIndices(iUnit) = find(ind);
- channelIDs(iUnit) = table2array(electrodeTbl(ind,1));
- electrodeGroups = [electrodeGroups; table2array(electrodeTbl(ind,9))];
- end
- metadata = [metadata(:,1:3) num2cell(channelIndices) num2cell(channelIDs) metadata(:,4:end)];
- end
- % Unit metadata: [metadata electrode_group]
- if nRows
- metadataTbl = table(metadata(:,1), metadata(:,2), metadata(:,3), metadata(:,4), ...
- metadata(:,5), metadata(:,6), metadata(:,7), metadata(:,8), ...
- metadata(:,9), metadata(:,10), metadata(:,11), metadata(:,12), electrodeGroups, ...
- 'VariableNames', {'cluster_id', 'local_cluster_id', 'type',...
- 'channel_index', 'channel_id', 'local_channel_id',...
- 'rel_horz_pos', 'rel_vert_pos', 'isi_violations',...
- 'isolation_distance', 'area', 'probe_id', 'electrode_group'});
- else
- metadataTbl = [];
- end
- end
- function [reshapedWaveformsMat, reshapedWaveformsVecGrp, reshapedWaveformsVec, waveformsMean, nWaveformSamples] = reshapeWaveforms(waveforms, iEl, metadata, nCh)
- % [reshapedWaveformsMat, reshapedWaveformsVecGrp, reshapedWaveformsVec, waveformsMean] = reshapeWaveforms(waveforms, iEl, metadata)
- %
- % Function extracts relevant waveform information and reshapes the waveform
- % array which is 3-dimensional with the first dimension being the unit, the
- % second being the sample point, and the third one being the recording
- % channel.
- % Input: waveforms - a strucuture loaded from the waveforms MAT file.
- % Relevant fields are waveforms (described above),
- % maxWaveforms (same as waveforms but excluding all
- % channels except for the maximum amplitude one), and
- % cluIDs (unit cluster IDs corresponding to the
- % dimension one in waveforms and maxWaveforms).
- % iEl - probe reference number.
- % metadata - a Matlab unit table produced by the function
- % getSpikes.
- % nCh - number of recording channels with waveforms for the same
- % unit.
- % Output: reshapedWaveformsMat - a 2D array reshaping waveforms.waveforms
- % array by collapsing the third dimension
- % and stacking all waveforms vertically one
- % unit after another.
- % reshapedWaveformsVecGrp - a column cell array reshaping
- % waveforms.waveforms array and grouping
- % all waveforms from the same unit
- % together. Cell array entries correspond
- % to individual units. The missing MUAs
- % correspond to empty cell arrays.
- % reshapedWaveformsVec - a cell array with rows from
- % reshapedWaveformsMat. Missing MUAs are
- % also included as empty cells times the
- % number of recording channels.
- % waveformsMean - waveforms.waveforms converted into a cell column
- % array. MUAs are NaNs.
- % nWaveformSamples - a total number of data sample points forming a
- % single waveform.
- nWaveformSamples = size(waveforms.maxWaveforms,2);
- if isfield(waveforms, 'waveforms')
- reshapedWaveformsMat = zeros(size(waveforms.waveforms,1)*size(waveforms.waveforms,3),size(waveforms.waveforms,2));
- else
- reshapedWaveformsMat = [];
- end
- reshapedWaveformsVecGrp = {};
- reshapedWaveformsVec = {};
- waveformsMean = {};
- metadataInds = ismember(table2cell(metadata(:,12)), ['probe' num2str(iEl)]);
- metadata = metadata(metadataInds,:);
- for iUnit = 1:size(metadata,1)
- if isfield(waveforms, 'cluIDs')
- row = find(ismember(waveforms.cluIDs, cell2mat(table2cell(metadata(iUnit,2)))));
- else
- row = [];
- end
- if isfield(waveforms, 'cluIDs') && sum(ismember(waveforms.cluIDs, cell2mat(table2cell(metadata(iUnit,2)))))
- unitWaveformMat = squeeze(waveforms.waveforms(row,:,:))';
- reshapedWaveformsMat((row-1)*size(waveforms.waveforms,3)+1:row*size(waveforms.waveforms,3),:) = unitWaveformMat;
- reshapedWaveformsVecGrp = [reshapedWaveformsVecGrp; {unitWaveformMat}];
- for iWave = 1:size(unitWaveformMat,1)
- reshapedWaveformsVec = [reshapedWaveformsVec; {unitWaveformMat(iWave,:)}];
- end
- waveformsMean = [waveformsMean; {waveforms.maxWaveforms(row,:)}];
- else
- reshapedWaveformsVecGrp = [reshapedWaveformsVecGrp; {[]}];
- if isfield(waveforms, 'waveforms')
- for iWave = 1:size(waveforms.waveforms,3)
- reshapedWaveformsVec = [reshapedWaveformsVec; {[]}];
- end
- else
- for iWave = 1:nCh
- reshapedWaveformsVec = [reshapedWaveformsVec; {[]}];
- end
- end
- waveformsMean = [waveformsMean; {nan(1,size(waveforms.maxWaveforms,2))}];
- end
- end
- end
- function acceptableSamples = markQualitySamples(acceptablePeriod, videoFrameTimes)
- % acceptableSamples = markQualitySamples(acceptablePeriod, videoFrameTimes)
- %
- % Function marks acceptable behavioural samples given the sample times and
- % the range of acceptable time periods.
- % Input: acceptablePeriod - a vector or a cell array of vectors marking the
- % beginning and end of acceptable time periods.
- % videoFrameTimes - a vector with sample times.
- % Ouptut: acceptableSamples - a logical vector marking acceptable samples
- % by ones.
- if isempty(acceptablePeriod) || isempty(videoFrameTimes)
- acceptableSamples = [];
- else
- acceptableSamples = false(size(videoFrameTimes));
- if iscell(acceptablePeriod)
- for iPeriod = 1:numel(acceptablePeriod)
- acceptableSamples(videoFrameTimes >= acceptablePeriod{iPeriod}(1) & videoFrameTimes <= acceptablePeriod{iPeriod}(2)) = true;
- end
- else
- acceptableSamples(videoFrameTimes >= acceptablePeriod(1) & videoFrameTimes <= acceptablePeriod(2)) = true;
- end
- end
- end
|