diff --git a/toolbox/core/bst_get.m b/toolbox/core/bst_get.m index 61c56e04d2..841f9b43f2 100644 --- a/toolbox/core/bst_get.m +++ b/toolbox/core/bst_get.m @@ -222,7 +222,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Francois Tadel, 2008-2021 +% Authors: Francois Tadel, 2008-2022 % Martin Cousineau, 2017 %% ==== PARSE INPUTS ==== @@ -2029,7 +2029,7 @@ if (nargout >= 5) && ~isempty(sStudy) sItem = sStudy.NoiseCov(iItem); end - case 'data' + case {'data', 'spike'} [sStudy, iStudy, iItem] = bst_get('DataFile', FileName, iStudies); if (nargout >= 5) && ~isempty(sStudy) sItem = sStudy.Data(iItem); @@ -3693,14 +3693,14 @@ {'.xyz'}, 'EEG: EEGLAB (*.xyz)', 'EEGLAB-XYZ'; ... {'.sfp'}, 'EEG: EGI (*.sfp)', 'EGI'; ... {'.txt'}, 'EEG: ASCII: XYZ (*.txt)', 'ASCII_XYZ-EEG'; ... - {'.txt'}, 'EEG: ASCII: XYZ_MNI (*.*)', 'ASCII_XYZ_MNI-EEG'; ... - {'.txt'}, 'EEG: ASCII: XYZ_World (*.*)', 'ASCII_XYZ_WORLD-EEG'; ... + {'.txt'}, 'EEG: ASCII: XYZ_MNI (*.txt)', 'ASCII_XYZ_MNI-EEG'; ... + {'.txt'}, 'EEG: ASCII: XYZ_World (*.txt)', 'ASCII_XYZ_WORLD-EEG'; ... {'.txt'}, 'EEG: ASCII: Name,XYZ (*.txt)', 'ASCII_NXYZ-EEG'; ... - {'.txt'}, 'EEG: ASCII: Name,XYZ_MNI (*.*)', 'ASCII_NXYZ_MNI-EEG'; ... - {'.txt'}, 'EEG: ASCII: Name,XYZ_World (*.*)', 'ASCII_NXYZ_WORLD-EEG'; ... + {'.txt'}, 'EEG: ASCII: Name,XYZ_MNI (*.txt)', 'ASCII_NXYZ_MNI-EEG'; ... + {'.txt'}, 'EEG: ASCII: Name,XYZ_World (*.txt)', 'ASCII_NXYZ_WORLD-EEG'; ... {'.txt'}, 'EEG: ASCII: XYZ,Name (*.txt)', 'ASCII_XYZN-EEG'; ... - {'.txt'}, 'EEG: ASCII: XYZ_MNI,Name (*.*)', 'ASCII_XYZN_MNI-EEG'; ... - {'.txt'}, 'EEG: ASCII: XYZ_World,Name (*.*)', 'ASCII_XYZN_WORLD-EEG'; ... + {'.txt'}, 'EEG: ASCII: XYZ_MNI,Name (*.txt)', 'ASCII_XYZN_MNI-EEG'; ... + {'.txt'}, 'EEG: ASCII: XYZ_World,Name (*.txt)', 'ASCII_XYZN_WORLD-EEG'; ... {'.txt'}, 'NIRS: Brainsight (*.txt)', 'BRAINSIGHT-TXT'; ... }; case 'labelin' diff --git a/toolbox/core/bst_memory.m b/toolbox/core/bst_memory.m index fc9e3167d4..d2401582a3 100644 --- a/toolbox/core/bst_memory.m +++ b/toolbox/core/bst_memory.m @@ -3227,7 +3227,7 @@ function CheckFrequencies() delete(hFigHist); end % Close spike sorting figure - process_spikesorting_supervised('CloseFigure'); + panel_spikes('CloseFigure'); % Restore default window manager if ~ismember(bst_get('Layout', 'WindowManager'), {'TileWindows', 'WeightWindows', 'FullArea', 'FullScreen', 'None'}) bst_set('Layout', 'WindowManager', 'TileWindows'); diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index a696687e5e..c52dbc61e9 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -395,6 +395,75 @@ PlugDesc(end).LoadFolders = {'toolbox'}; PlugDesc(end).DeleteFiles = {'ExampleDespiking.m', 'appendixpaper.pdf', 'downsample2x.m', 'examplelfpdespiking.mat', 'sta.m', ... 'toolbox/delineSignal.m', 'toolbox/despikeLFPbyChunks.asv', 'toolbox/despikeLFPbyChunks.m'}; + + % === ELECTROPHYSIOLOGY: Kilosort === + PlugDesc(end+1) = GetStruct('kilosort'); + PlugDesc(end).Version = 'github-master'; + PlugDesc(end).Category = 'e-phys'; + PlugDesc(end).URLzip = 'https://github.com/cortex-lab/KiloSort/archive/refs/heads/master.zip'; + PlugDesc(end).URLinfo = 'https://papers.nips.cc/paper/2016/hash/1145a30ff80745b56fb0cecf65305017-Abstract.html'; + PlugDesc(end).TestFile = 'fitTemplates.m'; + PlugDesc(end).ReadmeFile = 'readme.md'; + PlugDesc(end).CompiledStatus = 0; + PlugDesc(end).LoadFolders = {'*'}; + PlugDesc(end).RequiredPlugs = {'kilosort-wrapper'; 'phy'; 'npy-matlab'}; + PlugDesc(end).InstalledFcn = 'process_spikesorting_kilosort(''copyKilosortConfig'', bst_fullfile(bst_get(''UserPluginsDir''), ''kilosort'', ''Kilosort-master'', ''configFiles'', ''StandardConfig_MOVEME.m''), bst_fullfile(bst_get(''UserPluginsDir''), ''kilosort'', ''Kilosort-master'', ''KilosortStandardConfig.m''));'; + + + % === ELECTROPHYSIOLOGY: Kilosort Wrapper === + PlugDesc(end+1) = GetStruct('kilosort-wrapper'); + PlugDesc(end).Version = 'github-master'; + PlugDesc(end).Category = 'e-phys'; + PlugDesc(end).URLzip = 'https://github.com/brendonw1/KilosortWrapper/archive/refs/heads/master.zip'; + PlugDesc(end).URLinfo = 'https://zenodo.org/record/3604165'; + PlugDesc(end).TestFile = 'Kilosort2Neurosuite.m'; + PlugDesc(end).ReadmeFile = 'README.md'; + PlugDesc(end).CompiledStatus = 0; + + % === ELECTROPHYSIOLOGY: phy === + PlugDesc(end+1) = GetStruct('phy'); + PlugDesc(end).Version = 'github-master'; + PlugDesc(end).Category = 'e-phys'; + PlugDesc(end).URLzip = 'https://github.com/cortex-lab/phy/archive/refs/heads/master.zip'; + PlugDesc(end).URLinfo = 'https://phy.readthedocs.io/en/latest/'; + PlugDesc(end).TestFile = 'feature_view_custom_grid.py'; + PlugDesc(end).LoadFolders = {'*'}; + PlugDesc(end).ReadmeFile = 'README.md'; + PlugDesc(end).CompiledStatus = 0; + PlugDesc(end).RequiredPlugs = {'npy-matlab'}; + + % === ELECTROPHYSIOLOGY: npy-matlab === + PlugDesc(end+1) = GetStruct('npy-matlab'); + PlugDesc(end).Version = 'github-master'; + PlugDesc(end).Category = 'e-phys'; + PlugDesc(end).URLzip = 'https://github.com/kwikteam/npy-matlab/archive/refs/heads/master.zip'; + PlugDesc(end).URLinfo = 'https://github.com/kwikteam/npy-matlab'; + PlugDesc(end).TestFile = 'constructNPYheader.m'; + PlugDesc(end).LoadFolders = {'*'}; + PlugDesc(end).ReadmeFile = 'README.md'; + PlugDesc(end).CompiledStatus = 0; + + % === ELECTROPHYSIOLOGY: ultramegasort2000 === + PlugDesc(end+1) = GetStruct('ultramegasort2000'); + PlugDesc(end).Version = 'github-master'; + PlugDesc(end).Category = 'e-phys'; + PlugDesc(end).URLzip = 'https://github.com/danamics/UMS2K/archive/refs/heads/master.zip'; + PlugDesc(end).URLinfo = 'https://github.com/danamics/UMS2K/blob/master/UltraMegaSort2000%20Manual.pdf'; + PlugDesc(end).TestFile = 'UltraMegaSort2000 Manual.pdf'; + PlugDesc(end).LoadFolders = {'*'}; + PlugDesc(end).ReadmeFile = 'README.md'; + PlugDesc(end).CompiledStatus = 0; + + % === ELECTROPHYSIOLOGY: waveclus === + PlugDesc(end+1) = GetStruct('waveclus'); + PlugDesc(end).Version = 'github-master'; + PlugDesc(end).Category = 'e-phys'; + PlugDesc(end).URLzip = 'https://github.com/csn-le/wave_clus/archive/refs/heads/master.zip'; + PlugDesc(end).URLinfo = 'https://journals.physiology.org/doi/full/10.1152/jn.00339.2018'; + PlugDesc(end).TestFile = 'wave_clus.m'; + PlugDesc(end).LoadFolders = {'*'}; + PlugDesc(end).ReadmeFile = 'README.md'; + PlugDesc(end).CompiledStatus = 0; % === NIRSTORM === PlugDesc(end+1) = GetStruct('nirstorm'); diff --git a/toolbox/db/db_add.m b/toolbox/db/db_add.m index 7b8834748a..2751860c6a 100644 --- a/toolbox/db/db_add.m +++ b/toolbox/db/db_add.m @@ -23,7 +23,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Francois Tadel, 2011-2019 +% Authors: Francois Tadel, 2011-2022 %% ===== GET INPUT FILE ===== if (nargin < 3) || isempty(isReload) @@ -91,10 +91,15 @@ end % Surfaces subtypes if ismember(fileType, {'fibers', 'fem'}) - fileSubType = fileType; fileType = 'tess'; + fileSubType = [fileType, '_']; end isAnatomy = ismember(fileType, {'subjectimage', 'tess'}); + % Spikes: file tag is data + if strcmp(fileType, 'spike') + fileType = 'data'; + fileSubType = '0ephys_'; + end % Create a new output filename c = clock; strTime = sprintf('%02.0f%02.0f%02.0f_%02.0f%02.0f', c(1)-2000, c(2:5)); diff --git a/toolbox/db/db_add_data.m b/toolbox/db/db_add_data.m index 829f6d1a14..3f58ccf18d 100644 --- a/toolbox/db/db_add_data.m +++ b/toolbox/db/db_add_data.m @@ -89,7 +89,7 @@ deletedFile{end+1} = sStudy.Timefreq(iItem).FileName; end sStudy.Timefreq(iItem) = sNew; - case 'data' + case {'data', 'spike'} % Create new descriptor sNew = db_template('Data'); sNew.FileName = FileName; diff --git a/toolbox/db/private/db_parse_study.m b/toolbox/db/private/db_parse_study.m index 0e45c7e439..d230826610 100644 --- a/toolbox/db/private/db_parse_study.m +++ b/toolbox/db/private/db_parse_study.m @@ -46,7 +46,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Francois Tadel, 2008-2012 +% Authors: Francois Tadel, 2008-2022 %% ===== PARSE INPUTS ===== if ~file_exist(bst_fullfile(studiesDir, studySubDir)) @@ -207,7 +207,7 @@ sStudy(1).NoiseCov(2) = noisecovInfo; end - case 'data' + case {'data', 'spike'} % % Get data filename without the trial block % [groupName, iTrial] = str_remove_trial(filenameRelative); % dataInfo = []; diff --git a/toolbox/gui/figure_timefreq.m b/toolbox/gui/figure_timefreq.m index 8df23c6971..03364841b2 100644 --- a/toolbox/gui/figure_timefreq.m +++ b/toolbox/gui/figure_timefreq.m @@ -1098,8 +1098,10 @@ function UpdateFigurePlot(hFig, isForced) PlotTimefreqSurfHigh(hAxes, Time, Freqs, TF, TFmask); elseif TfInfo.DisplayAsDots PlotTimefreqAsDots(hAxes, Time, TF); - elseif TfInfo.DisplayAsPhase - PlotTimefreqAsPhase(hAxes, Time, Freqs, TF); + elseif TfInfo.DisplayAsPhase + phase_info = in_bst_timefreq(TfInfo.FileName, 0, 'neurons', 'RowNames'); + iNeuron = find(ismember(phase_info.RowNames, RowNames)); + PlotTimefreqAsPhase(hAxes, Time, Freqs, TF, phase_info.neurons.phase, iNeuron); else PlotTimefreqSurf(hAxes, Time, FullTimeVector, Freqs, TF, TFmask); end @@ -1241,7 +1243,7 @@ function UpdateFigurePlot(hFig, isForced) %% ===== PLOT TIME-FREQ AS PHASE ===== -function hSurf = PlotTimefreqAsPhase(hAxes, Time, Freqs, TF) +function hSurf = PlotTimefreqAsPhase(hAxes, Time, Freqs, TF, phase_info, iNeuron) % Delete previous objects surfTag = 'TimefreqSurf'; hOld = findobj(hAxes, '-depth', 1, 'tag', surfTag); @@ -1262,7 +1264,7 @@ function UpdateFigurePlot(hFig, isForced) hSurf = circ_plot(single_neuron_and_channel_phase,'hist',[], nBins,true,true,'linewidth',2,'color','r','Parent', hAxes); set(hSurf, 'Tag', surfTag); - title(hAxes, {['Rayleigh test p=' num2str(pval_rayleigh)], ['Omnibus test p=' num2str(pval_omnibus)], ['Preferred phase: ' num2str(mean_value_degrees) '^o']}) + title(hAxes, {['Total neurons: ' num2str(phase_info.total_spikes(iNeuron))],['Rayleigh test p=' num2str(pval_rayleigh)], ['Preferred phase: ' num2str(mean_value_degrees) '^o']}) end diff --git a/toolbox/gui/panel_spikes.m b/toolbox/gui/panel_spikes.m index 45eea49c06..8f56be5009 100644 --- a/toolbox/gui/panel_spikes.m +++ b/toolbox/gui/panel_spikes.m @@ -23,6 +23,7 @@ % % Authors: Martin Cousineau, 2018 + eval(macro_method); end @@ -37,7 +38,6 @@ jPanelNew = gui_component('Panel'); jPanelTop = gui_component('Panel'); jPanelNew.add(jPanelTop, BorderLayout.NORTH); - TB_DIM = java_scaled('dimension', 25, 25); % ===== FREQUENCY FILTERING ===== jPanelSpikes = gui_component('Panel'); @@ -81,21 +81,21 @@ function ElectrodesListValueChanged_Callback(varargin) end % Select electrode GlobalData.SpikeSorting.Selected = jItem.getUserData(); - process_spikesorting_supervised('LoadElectrode'); + LoadElectrode(); end function ButtonSaveAndNextElectrode(varargin) global GlobalData; % Save current electrode bst_progress('start', 'Spike Sorting', 'Saving electrode...'); - process_spikesorting_supervised('SaveElectrode'); + SaveElectrode(); % Load next electrode bst_progress('text', 'Loading next electrode...'); - nextElectrode = process_spikesorting_supervised('GetNextElectrode'); + nextElectrode = GetNextElectrode(); if GlobalData.SpikeSorting.Selected ~= nextElectrode GlobalData.SpikeSorting.Selected = nextElectrode; - process_spikesorting_supervised('LoadElectrode'); + LoadElectrode(); end UpdatePanel(); @@ -107,7 +107,7 @@ function ButtonSaveAndNextElectrode(varargin) function UpdatePanel() global GlobalData; ctrl = bst_get('PanelControls', 'Spikes'); - if process_spikesorting_supervised('FigureIsOpen', 1) + if FigureIsOpen(1) gui_enable(ctrl.jPanel, 1); UpdateElectrodesList(); if strcmpi(GlobalData.SpikeSorting.Data.Device, 'kilosort') @@ -120,6 +120,8 @@ function UpdatePanel() end end + +%% ===== UPDATE ELECTRODE LIST ===== function UpdateElectrodesList(varargin) global GlobalData; import org.brainstorm.list.*; @@ -167,11 +169,14 @@ function UpdateElectrodesList(varargin) java_setcb(jModel, 'ContentsChangedCallback', bakCallback); end + %% ===== FOCUS CHANGED ====== function FocusChangedCallback(isFocused) %#ok UpdatePanel(); end + +%% ===== GET SPIKE NAME ===== function spikeName = GetSpikeName(i) global GlobalData; if strcmpi(GlobalData.SpikeSorting.Data.Device, 'kilosort') @@ -184,3 +189,542 @@ function FocusChangedCallback(isFocused) %#ok spikeName = [spikeName ' *']; end end + + +%% ================================================================================================= +% ===== EXTERNAL CALLBACKS ======================================================================== +% ================================================================================================= + +%% ===== OPEN SPIKE FILE ===== +function OpenSpikeFile(SpikeFile) + global GlobalData; + + % Load input file + DataMat = in_bst_data(SpikeFile); + + % Make sure spikes exist and were generated by WaveClus + if ~isfield(DataMat, 'Spikes') || ~isstruct(DataMat.Spikes) ... + || ~isfield(DataMat, 'Parent') ... + || exist(DataMat.Parent, 'dir') ~= 7 ... + || isempty(dir(DataMat.Parent)) + bst_error('No spikes found. Make sure to run the unsupervised Spike Sorter first.', 'Load spike file', 0); + return; + end + + switch lower(DataMat.Device) + case 'waveclus' + % Load plugin + [isInstalled, errMsg] = bst_plugin('Install', 'waveclus'); + if ~isInstalled + error(errMsg); + end + case 'ultramegasort2000' + % Load plugin + [isInstalled, errMsg] = bst_plugin('Install', 'ultramegasort2000'); + if ~isInstalled + error(errMsg); + end + case 'kilosort' + KlustersExecutable = bst_get('KlustersExecutable'); + if isempty(KlustersExecutable) || exist(KlustersExecutable, 'file') ~= 2 + % Try a common places for Klusters to be installed + commonPaths = {'C:\Program Files (x86)\Klusters\bin\klusters.exe', ... + 'C:\Program Files\Klusters\bin\klusters.exe'}; + foundPath = 0; + for iPath = 1:length(commonPaths) + if exist(commonPaths{iPath}, 'file') == 2 + KlustersExecutable = commonPaths{iPath}; + bst_set('KlustersExecutable', KlustersExecutable); + foundPath = 1; + break; + end + end + % If we cannot find it, prompt user + if ~foundPath + [res, isCancel] = java_dialog('question', ... + ['

We cannot find an installation of Klusters on your computer.
', ... + 'Would you like to download it or look for the Klusters executable yourself?'], ... + 'Klusters executable', [], {'Download', 'Pick executable', 'Cancel'}, 'Cancel'); + if isCancel || isempty(res) || strcmpi(res, 'Cancel') + return; + end + if strcmpi(res, 'Download') + % Display web page + klusters_url = 'http://neurosuite.sourceforge.net/'; + status = web(klusters_url, '-browser'); + if (status ~= 0) + web(klusters_url); + end + return; + end + % For Windows, look for EXE files. + if ~isempty(strfind(bst_get('OsType'), 'win')) + filters = {'*.exe', 'Klusters executable (*.exe)'}; + else + filters = {'*', 'Klusters executable'}; + end + KlustersExecutable = java_getfile('open', 'Klusters executable', [], 'single', 'files', filters, {}); + if isempty(KlustersExecutable) || exist(KlustersExecutable, 'file') ~= 2 + return; + end + end + bst_set('KlustersExecutable', KlustersExecutable); + end + otherwise + bst_error('The chosen spike sorter is currently unsupported by Brainstorm.'); + end + + CloseFigure(); + + GlobalData.SpikeSorting = struct(); + GlobalData.SpikeSorting.Data = DataMat; + GlobalData.SpikeSorting.Selected = 0; + GlobalData.SpikeSorting.Fig = -1; + + gui_brainstorm('ShowToolTab', 'Spikes'); + OpenFigure(); + panel_spikes('UpdatePanel'); +end + + +%% ===== OPEN FIGURE ===== +function OpenFigure() + global GlobalData; + + bst_progress('start', 'Spike Sorting', 'Loading spikes...'); + CloseFigure(); + + GlobalData.SpikeSorting.Selected = GetNextElectrode(); + + electrodeFile = bst_fullfile(... + GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Path, ... + GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).File); + + switch lower(GlobalData.SpikeSorting.Data.Device) + case 'waveclus' + GlobalData.SpikeSorting.Fig = wave_clus(electrodeFile); + + % Some Wave Clus visual hacks + load_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'load_data_button'); + if ishandle(load_button) + load_button.Visible = 'off'; + end + save_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'save_clusters_button'); + if ishandle(save_button) + save_button.Visible = 'off'; + end + + case 'ultramegasort2000' + DataMat = load(electrodeFile, 'spikes'); + GlobalData.SpikeSorting.Fig = figure('Units', 'Normalized', 'Position', ... + DataMat.spikes.params.display.default_figure_size); + % Just open figure, rest of the code in LoadElectrode() + + case 'kilosort' + % Do nothing. + + otherwise + bst_error('This spike sorting structure is currently unsupported by Brainstorm.'); + end + + panel_spikes('UpdatePanel'); + LoadElectrode(); + + % Close Spike panel when you close the figure + function my_closereq(src, callbackdata) + delete(src); + panel_spikes('UpdatePanel'); + end + if FigureIsOpen() + GlobalData.SpikeSorting.Fig.CloseRequestFcn = @my_closereq; + end + + bst_progress('stop'); +end + + +%% ===== IS FIGURE OPEN ===== +function isOpen = FigureIsOpen(varargin) + global GlobalData; + + if nargin < 1 + lenient = 0; + else + lenient = varargin{1}; + end + + isOpen = isfield(GlobalData, 'SpikeSorting') ... + && (isfield(GlobalData.SpikeSorting, 'Fig') ... + && ishandle(GlobalData.SpikeSorting.Fig)) ... + || (lenient && strcmpi(GlobalData.SpikeSorting.Data.Device, 'kilosort')); + % For KiloSort, we're not sure it is open since it's outside matlab... +end + + +%% ===== CLOSE FIGURE ===== +function CloseFigure() + global GlobalData; + if ~FigureIsOpen() + return; + end + close(GlobalData.SpikeSorting.Fig); + panel_spikes('UpdatePanel'); +end + + +%% ===== LOAD ELECTRODE ===== +function LoadElectrode() + global GlobalData; + if ~FigureIsOpen(1) + return; + end + + electrodeFile = bst_fullfile(... + GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Path, ... + GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).File); + + switch lower(GlobalData.SpikeSorting.Data.Device) + case 'waveclus' + wave_clus('load_data_button_Callback', GlobalData.SpikeSorting.Fig, ... + electrodeFile, guidata(GlobalData.SpikeSorting.Fig)); + + name_text = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'file_name'); + if ishandle(name_text) + name_text.String = panel_spikes('GetSpikeName', GlobalData.SpikeSorting.Selected); + end + + case 'ultramegasort2000' + % Reload figure altogether, same behavior as builtin load... + DataMat = load(electrodeFile, 'spikes'); + clf(GlobalData.SpikeSorting.Fig, 'reset'); + splitmerge_tool(DataMat.spikes, 'all', GlobalData.SpikeSorting.Fig); + + % Some UMS2k visual hacks + save_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'saveButton'); + if ishandle(save_button) + save_button.Visible = 'off'; + end + save_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'saveFileButton'); + if ishandle(save_button) + save_button.Visible = 'off'; + end + load_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'loadFileButton'); + if ishandle(load_button) + load_button.Visible = 'off'; + end + + case 'kilosort' + KlustersExecutable = bst_get('KlustersExecutable'); + status = system(['"' KlustersExecutable, '" "', electrodeFile, '" &']); + if status ~= 0 + bst_error('An error has occurred, could not start Klusters.'); + end + + otherwise + bst_error('This spike sorting structure is currently unsupported by Brainstorm.'); + end +end + + +%% ===== SAVE ELECTRODE ===== +function SaveElectrode() + global GlobalData; + + if ~FigureIsOpen(1) + return; + end + + electrodeFile = bst_fullfile(... + GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Path, ... + GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).File); + + % Save through Spike Sorting software + switch lower(GlobalData.SpikeSorting.Data.Device) + case 'waveclus' + % WaveClus takes a screenshot of the figure when saving, which + % is pretty slow. If we change the figure tag it skips this. + save_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'save_clusters_button'); + fig_tag = GlobalData.SpikeSorting.Fig.Tag; + GlobalData.SpikeSorting.Fig.Tag = 'wave_clus_tmp'; + wave_clus('save_clusters_button_Callback', save_button, ... + [], guidata(GlobalData.SpikeSorting.Fig), 0); + GlobalData.SpikeSorting.Fig.Tag = fig_tag; + + case 'ultramegasort2000' + figdata = get(GlobalData.SpikeSorting.Fig, 'UserData'); + spikes = figdata.spikes; + save(electrodeFile, 'spikes'); + OutMat = struct(); + OutMat.pathname = GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Path; + OutMat.filename = GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).File; + set(figdata.sfb, 'UserData', OutMat); + + case 'kilosort' + % Do nothing. + + otherwise + bst_error('This spike sorting structure is currently unsupported by Brainstorm.'); + end + + % Save updated brainstorm file + GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Mod = 1; + bst_save(GlobalData.SpikeSorting.Data.Name, GlobalData.SpikeSorting.Data, 'v6'); + + % Add event to linked raw file + CreateSpikeEvents(GlobalData.SpikeSorting.Data.RawFile, ... + GlobalData.SpikeSorting.Data.Device, ... + electrodeFile, ... + GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Name, ... + 1); +end + + +%% ===== NEXT ELECTRODE ===== +function nextElectrode = GetNextElectrode() + global GlobalData; + if ~isfield(GlobalData, 'SpikeSorting') ... + || ~isfield(GlobalData.SpikeSorting, 'Selected') ... + || isempty(GlobalData.SpikeSorting.Selected) + GlobalData.SpikeSorting.Selected = 0; + end + + numSpikes = length(GlobalData.SpikeSorting.Data.Spikes); + nextElectrode = []; + + if GlobalData.SpikeSorting.Selected < numSpikes + nextElectrode = GlobalData.SpikeSorting.Selected + 1; + while nextElectrode <= numSpikes && ... + isempty(GlobalData.SpikeSorting.Data.Spikes(nextElectrode).File) + nextElectrode = nextElectrode + 1; + end + end + if isempty(nextElectrode) || nextElectrode > numSpikes || isempty(GlobalData.SpikeSorting.Data.Spikes(nextElectrode).File) + nextElectrode = GlobalData.SpikeSorting.Selected; + end +end + + +%% ===== CREATE SPIKE EVENTS ===== +function newEvents = CreateSpikeEvents(rawFile, deviceType, electrodeFile, electrodeName, import, eventNamePrefix) + global GlobalData; + if nargin < 6 + eventNamePrefix = ''; + elseif ~isempty(eventNamePrefix) + eventNamePrefix = [eventNamePrefix ' ']; + end + newEvents = struct(); + DataMat = in_bst_data(rawFile); + eventName = [eventNamePrefix GetSpikesEventPrefix() ' ' electrodeName]; + gotEvents = 0; + + % Load spike data and convert to Brainstorm event format + switch lower(deviceType) + case 'waveclus' + if exist(electrodeFile, 'file') == 2 + ElecData = load(electrodeFile, 'cluster_class'); + neurons = unique(ElecData.cluster_class(ElecData.cluster_class(:,1) > 0,1)); + numNeurons = length(neurons); + tmpEvents = struct(); + if numNeurons == 1 + tmpEvents(1).epochs = ones(1, sum(ElecData.cluster_class(:,1) ~= 0)); + tmpEvents(1).times = ElecData.cluster_class(ElecData.cluster_class(:,1) ~= 0, 2)' ./ 1000 + DataMat.F.prop.times(1); + else + for iNeuron = 1:numNeurons + tmpEvents(iNeuron).epochs = ones(1, length(ElecData.cluster_class(ElecData.cluster_class(:,1) == iNeuron, 1))); + tmpEvents(iNeuron).times = ElecData.cluster_class(ElecData.cluster_class(:,1) == iNeuron, 2)' ./ 1000 + DataMat.F.prop.times(1); + end + end + else + numNeurons = 0; + end + + case 'ultramegasort2000' + ElecData = load(electrodeFile, 'spikes'); + ElecData.spikes.spiketimes = double(ElecData.spikes.spiketimes); + numNeurons = size(ElecData.spikes.labels,1); + tmpEvents = struct(); + if numNeurons == 1 + tmpEvents(1).epochs = ones(1,length(ElecData.spikes.assigns)); + tmpEvents(1).times = ElecData.spikes.spiketimes + DataMat.F.prop.times(1); + elseif numNeurons > 1 + for iNeuron = 1:numNeurons + tmpEvents(iNeuron).epochs = ones(1,length(ElecData.spikes.assigns(ElecData.spikes.assigns == ElecData.spikes.labels(iNeuron,1)))); + tmpEvents(iNeuron).times = ElecData.spikes.spiketimes(ElecData.spikes.assigns == ElecData.spikes.labels(iNeuron,1)) + DataMat.F.prop.times(1); + end + end + + case 'kilosort' + [newEvents, Channels_new_montages] = process_spikesorting_kilosort('LoadKlustersEvents', ... + GlobalData.SpikeSorting.Data, GlobalData.SpikeSorting.Selected); + gotEvents = 1; + + % In the case of Kilosort, the entire Shank is considered the + % 'electrode'. Therefore there are multiple events assigned + % simultaneously on every manual inspection. + channelsInMontage = Channels_new_montages(ismember({Channels_new_montages.Group}, electrodeName)); + + eventName = cell(length(channelsInMontage), 1); + for iChannel = 1:length(channelsInMontage) + eventName{iChannel} = ['Spikes Channel ' channelsInMontage(iChannel).Name]; + end + + otherwise + bst_error('This spike sorting structure is currently unsupported by Brainstorm.'); + end + + if ~gotEvents + if numNeurons == 1 + newEvents(1).label = eventName; + newEvents(1).color = [rand(1,1), rand(1,1), rand(1,1)]; + newEvents(1).epochs = tmpEvents(1).epochs; + newEvents(1).times = tmpEvents(1).times; + newEvents(1).reactTimes = []; + newEvents(1).select = 1; + newEvents(1).notes = cell(1, size(newEvents(1).times, 2)); + newEvents(1).channels = repmat({{electrodeName}}, 1, size(newEvents(1).times, 2)); + + + elseif numNeurons > 1 + for iNeuron = 1:numNeurons + newEvents(iNeuron).label = [eventName ' |' num2str(iNeuron) '|']; + newEvents(iNeuron).color = [rand(1,1), rand(1,1), rand(1,1)]; + newEvents(iNeuron).epochs = tmpEvents(iNeuron).epochs; + newEvents(iNeuron).times = tmpEvents(iNeuron).times; + newEvents(iNeuron).reactTimes = []; + newEvents(iNeuron).select = 1; + newEvents(iNeuron).notes = cell(1, size(newEvents(iNeuron).times, 2)); + newEvents(iNeuron).channels = repmat({{electrodeName}}, 1, size(newEvents(iNeuron).times, 2)); + + end + else + % This electrode just picked up noise, no event to add. + newEvents(1).label = eventName; + newEvents(1).color = [rand(1,1), rand(1,1), rand(1,1)]; + newEvents(1).epochs = []; + newEvents(1).times = []; + newEvents(1).reactTimes = []; + newEvents(1).select = 1; + newEvents(1).channels = cell(1, size(newEvents(1).times, 2)); + newEvents(1).notes = cell(1, size(newEvents(1).times, 2)); + end + end + + if import + ProtocolInfo = bst_get('ProtocolInfo'); + % Add event to linked raw file + numEvents = length(DataMat.F.events); + % Delete existing event(s) + if numEvents > 0 + if ~iscell(eventName) % Waveclus / UltraMegaSort2000 + iDelEvents = cellfun(@(x) ~isempty(x), strfind({DataMat.F.events.label}, strtrim(eventName))); + else % Kilosort - Delete all spiking events that are derived from any channels from the shank that is being currently manually spike-sorted + iDelEvents = false(length(eventName), length({DataMat.F.events.label})); + for iEventName = 1:length(eventName) + iDelEvents(iEventName,:) = cellfun(@(x) ~isempty(x), strfind({DataMat.F.events.label}, strtrim(eventName{iEventName}))); + end + end + iDelEvents = any(iDelEvents,1); + + DataMat.F.events = DataMat.F.events(~iDelEvents); + numEvents = length(DataMat.F.events); + end + % Add as new event(s); + if ~isempty(fieldnames(newEvents)) + for iEvent = 1:length(newEvents) + DataMat.F.events(numEvents + iEvent) = newEvents(iEvent); + end + end + bst_save(bst_fullfile(ProtocolInfo.STUDIES, rawFile), DataMat, 'v6'); + end +end + + +%% ===== GET SPIKES EVENT PREFIX ===== +function prefix = GetSpikesEventPrefix(varargin) + if length(varargin) < 1 + prefix = 'Spikes Channel'; + else + prefix = {'Spikes Channel', 'Spikes Noise'}; + end +end + + +%% ===== IS SPIKE EVENT ===== +function isSpikeEvent = IsSpikeEvent(eventLabel) + prefixes = GetSpikesEventPrefix('all'); + + isSpikeEvent = false(length(prefixes), 1); + for iPrefix = 1:length(prefixes) + isSpikeEvent(iPrefix) = strncmp(eventLabel, prefixes{iPrefix}, length(prefixes{iPrefix})); + end + isSpikeEvent = any(isSpikeEvent,1); +end + + +%% ===== GET NEURON FROM EVENT ===== +function neuron = GetNeuronOfSpikeEvent(eventLabel) + markers = strfind(eventLabel, '|'); + if length(markers) > 1 + neuron = str2num(eventLabel(markers(end-1)+1:markers(end)-1)); + else + neuron = []; + end +end + + +%% ===== GET CHANNEL FROM EVENT ===== +function channel = GetChannelOfSpikeEvent(eventLabel) + if ~IsSpikeEvent(eventLabel) + channel = []; + return; + end + + eventLabel = strtrim(eventLabel); + prefix = GetSpikesEventPrefix(); + neuron = GetNeuronOfSpikeEvent(eventLabel); + bounds = [length(prefix) + 2, 0]; % 'Spikes Channel ' + + if ~isempty(neuron) + bounds(2) = length(num2str(neuron)) + 3; % ' |31|' + end + + try + channel = eventLabel(bounds(1):end-bounds(2)); + catch + channel = []; + end +end + + +%% ===== IS FIRST NEURON ===== +function isFirst = IsFirstNeuron(eventLabel, onlyIsFirst) + % onlyIsFirst = We assume a channel with a single neuron counts as a first neuron. + if nargin < 2 + onlyIsFirst = 1; + end + neuron = GetNeuronOfSpikeEvent(eventLabel); + isFirst = neuron == 1; + if onlyIsFirst && isempty(neuron) + isFirst = 1; + end +end + + +%% ===== DELETE SPIKE EVENTS ====== +function DeleteSpikeEvents(rawFile) + ProtocolInfo = bst_get('ProtocolInfo'); + DataMat = in_bst_data(rawFile); + events = DataMat.F.events; + iKeepEvents = []; + + for iEvent = 1:length(events) + if ~IsSpikeEvent(events(iEvent).label) + iKeepEvents(end+1) = iEvent; + end + end + + DataMat.F.events = DataMat.F.events(iKeepEvents); + bst_save(bst_fullfile(ProtocolInfo.STUDIES, rawFile), DataMat, 'v6'); +end + + diff --git a/toolbox/gui/panel_spikesorting_options.m b/toolbox/gui/panel_spikesorting_options.m index b788845ce3..b1a9c5041e 100644 --- a/toolbox/gui/panel_spikesorting_options.m +++ b/toolbox/gui/panel_spikesorting_options.m @@ -23,6 +23,7 @@ % =============================================================================@ % % Authors: Martin Cousineau, 2018 +% Francois Tadel, 2022 eval(macro_method); end @@ -40,34 +41,36 @@ panelName = []; return; end - % Check chosen spike sorter spikeSorter = sProcess.options.spikesorter.Value; - % Create main main panel - jPanelNew = gui_river(); + % Create main panel + jPanelNew = gui_component('panel'); + % Create edit box + jTextOptions = java_create('javax.swing.JTextArea', 'Ljava.lang.String;', 'Loading...'); + jTextOptions.setBackground(Color(1,1,1)); + jTextOptions.setMargin(java_create('java.awt.Insets', 'IIII', 10,25,10,25)); + jTextOptions.setFont(bst_get('Font', 12, 'Courier New')); + jPanelNew.add(JScrollPane(jTextOptions)); + % Validation button + gui_component('button', jPanelNew, BorderLayout.SOUTH, 'Save options', [], [], @ButtonOk_Callback); - % ===== FREQUENCY PANEL ===== - jTextOptions = gui_component('textarea', jPanelNew, 'br hfill', 'test'); - - % ===== VALIDATION BUTTON ===== - gui_component('Button', jPanelNew, 'br right', 'OK', [], [], @ButtonOk_Callback); + % Set maximum panel size + maxSize = java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment.getMaximumWindowBounds(); + jPanelNew.setPreferredSize(java.awt.Dimension(700, maxSize.getHeight() - 200)); - % ===== PANEL CREATION ===== - % Put everything in a big scroll panel - jPanelScroll = javax.swing.JScrollPane(jPanelNew); - %jPanelScroll.add(jPanelNew); - %jPanelScroll.setPreferredSize(jPanelNew.getPreferredSize()); % Return a mutex to wait for panel close bst_mutex('create', panelName); % Controls list - ctrl = struct('jTextOptions', jTextOptions, ... - 'spikeSorter', spikeSorter); + ctrl = struct('jTextOptions', jTextOptions, ... + 'spikeSorter', spikeSorter); % Create the BstPanel object that is returned by the function - bstPanelNew = BstPanel(panelName, jPanelScroll, ctrl); + bstPanelNew = BstPanel(panelName, jPanelNew, ctrl); + % Load options file into the panel UpdatePanel(); + %% ================================================================================= % === INTERNAL CALLBACKS ========================================================== % ================================================================================= @@ -104,17 +107,19 @@ function ButtonOk_Callback(varargin) end end + %% ===== UPDATE PANEL ===== function UpdatePanel(varargin) + % Get option file [optionFile, skipLines] = GetSpikeSorterOptionFile(spikeSorter); - + % Options file is not found if exist(optionFile, 'file') ~= 2 java_dialog('error', 'Could not find spike-sorter''s parameters file.'); bstPanelNew = []; bst_mutex('release', panelName); return; end - + % Read options file fid = fopen(optionFile,'rt'); idx = 1; optionText = {}; @@ -128,53 +133,48 @@ function UpdatePanel(varargin) end end fclose(fid); + % Set as panel contents jTextOptions.setText(char(join(optionText, newline))); end end + +%% ===== GET OPTIONS FILE ===== function [optionFile, skipLines, skipValidate] = GetSpikeSorterOptionFile(spikeSorter) skipLines = 0; skipValidate = 0; - + optionFile = []; + + % Get plugin + PlugDesc = bst_plugin('GetInstalled', spikeSorter); + % Install plugin if not available + if isempty(PlugDesc) + [isInstalled, errMsg, PlugDesc] = bst_plugin('Install', spikeSorter, 1); + if ~isInstalled + error(errMsg); + end + end + + % Get default options file for each spike sorting application switch lower(spikeSorter) case 'waveclus' - optionFile = bst_fullfile(bst_get('BrainstormUserDir'), 'waveclus', 'set_parameters.m'); + optionFile = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, 'set_parameters.m'); skipLines = 2; - case 'ultramegasort2000' - optionFile = bst_fullfile(bst_get('BrainstormUserDir'), 'UltraMegaSort2000', 'ss_default_params.m'); + optionFile = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, 'ss_default_params.m'); skipLines = 2; skipValidate = 1; - case 'kilosort' - optionFile = bst_fullfile(bst_get('BrainstormUserDir'), 'kilosort', 'KilosortStandardConfig.m'); - - otherwise - error('The chosen spike sorter is currently unsupported by Brainstorm.'); + optionFile = bst_fullfile(PlugDesc.Path, PlugDesc.SubFolder, 'KilosortStandardConfig.m'); end - - % Check whether the file exists, i.e. the spike sorter is installed - if exist(optionFile, 'file') ~= 2 - if java_dialog('confirm', ... - ['The ' spikeSorter ' spike-sorter is not installed on your computer.' 10 10 ... - 'Download and install the latest version?'], spikeSorter) - switch lower(spikeSorter) - case 'waveclus' - process_spikesorting_waveclus('downloadAndInstallWaveClus'); - - case 'ultramegasort2000' - process_spikesorting_ultramegasort2000('downloadAndInstallUltraMegaSort2000'); - - case 'kilosort' - process_spikesorting_kilosort('downloadAndInstallKiloSort'); - - otherwise - error('The chosen spike sorter is currently unsupported by Brainstorm.'); - end - end + % Handling errors: File not found + if ~file_exist(optionFile) + error(['File not found: ' optionFile]); end end + +%% ===== VALIDATE OPTIONS FILE ===== function passed = ValidateOptions(textOptions) try eval(textOptions); diff --git a/toolbox/io/file_fullpath.m b/toolbox/io/file_fullpath.m index b492b74a5e..5a7f6151f7 100644 --- a/toolbox/io/file_fullpath.m +++ b/toolbox/io/file_fullpath.m @@ -22,7 +22,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Francois Tadel, 2011-2012 +% Authors: Francois Tadel, 2011-2022 % Empty input if isempty(FileName) @@ -49,7 +49,7 @@ tmpFile = bst_fullfile(ProtocolInfo.SUBJECTS, FileName); end isAnatomy = 1; - case {'brainstormstudy', 'study', 'studysubject', 'condition', 'rawcondition', 'channel', 'headmodel', 'data', 'rawdata', 'results', 'kernel', 'pdata', 'presults', 'noisecov', 'ndatacov', 'dipoles', 'timefreq', 'spectrum', 'ptimefreq', 'pspectrum', 'matrix', 'pmatrix', 'proj', 'image', 'video', 'videolink', 'spikes'} + case {'brainstormstudy', 'study', 'studysubject', 'condition', 'rawcondition', 'channel', 'headmodel', 'data', 'rawdata', 'results', 'kernel', 'pdata', 'presults', 'noisecov', 'ndatacov', 'dipoles', 'timefreq', 'spectrum', 'ptimefreq', 'pspectrum', 'matrix', 'pmatrix', 'proj', 'image', 'video', 'videolink', 'spike'} if ~file_exist(FileName) tmpFile = bst_fullfile(ProtocolInfo.STUDIES, FileName); end diff --git a/toolbox/io/file_gettype.m b/toolbox/io/file_gettype.m index 547f76dc02..173cbf5ecc 100644 --- a/toolbox/io/file_gettype.m +++ b/toolbox/io/file_gettype.m @@ -28,7 +28,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Francois Tadel, 2008-2019 +% Authors: Francois Tadel, 2008-2022 %% ===== INPUT: FILE ===== @@ -58,7 +58,9 @@ % If it is a Matlab .mat file : look for valid tags in fileName if (length(fileExt) >= 4) && (isequal(fileExt(1:4), '.mat')) - if ~isempty(strfind(fileName, '_data')) + if ~isempty(strfind(fileName, '_data_0ephys')) + fileType = 'spike'; + elseif ~isempty(strfind(fileName, '_data')) fileType = 'data'; elseif ~isempty(strfind(fileName, '_results')) fileType = 'results'; @@ -175,9 +177,7 @@ elseif isfield(sMat, 'VideoStart') fileType = 'videolink'; elseif isfield(sMat, 'Spikes') - fileType = 'spikes'; - elseif isfield(sMat, 'Points') - fileType = 'fibers'; + fileType = 'spike'; else fileType = 'unknown'; end diff --git a/toolbox/io/file_short.m b/toolbox/io/file_short.m index a2eacf7878..d55be348a4 100644 --- a/toolbox/io/file_short.m +++ b/toolbox/io/file_short.m @@ -22,7 +22,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Francois Tadel, 2012 +% Authors: Francois Tadel, 2012-2022 % Empty input if isempty(FileName) @@ -49,7 +49,7 @@ case {'brainstormsubject', 'subject', 'subjectimage', 'anatomy', 'scalp', 'outerskull', 'innerskull', 'cortex', 'fibers', 'fem', 'other', 'tess'} FileName = file_win2unix(strrep(FileName, ProtocolInfo.SUBJECTS, '')); isAnatomy = 1; - case {'brainstormstudy', 'study', 'studysubject', 'condition', 'rawcondition', 'channel', 'headmodel', 'data', 'rawdata', 'results', 'kernel', 'pdata', 'presults', 'noisecov', 'ndatacov', 'dipoles', 'timefreq', 'spectrum', 'ptimefreq', 'pspectrum', 'matrix', 'pmatrix', 'proj', 'image', 'video', 'videolink'} + case {'brainstormstudy', 'study', 'studysubject', 'condition', 'rawcondition', 'channel', 'headmodel', 'data', 'rawdata', 'results', 'kernel', 'pdata', 'presults', 'noisecov', 'ndatacov', 'dipoles', 'timefreq', 'spectrum', 'ptimefreq', 'pspectrum', 'matrix', 'pmatrix', 'proj', 'image', 'video', 'videolink', 'spike'} FileName = file_win2unix(strrep(FileName, ProtocolInfo.STUDIES, '')); case 'link' % Keep it the way it is diff --git a/toolbox/io/import_channel.m b/toolbox/io/import_channel.m index 79b168a36a..7a308204f8 100644 --- a/toolbox/io/import_channel.m +++ b/toolbox/io/import_channel.m @@ -231,9 +231,6 @@ ChannelMat = AllChannelMats{1}; end - - - case {'INTRANAT', 'INTRANAT_MNI'} switch (fExt) case 'pts' @@ -303,17 +300,17 @@ FileUnits = 'mm'; case {'ASCII_XYZ', 'ASCII_XYZ_MNI', 'ASCII_XYZ_WORLD'} % (*.*) - ChannelMat = in_channel_ascii(ChannelFile, {'X','Y','Z'}, 0, .01); + ChannelMat = in_channel_ascii(ChannelFile, {'X','Y','Z'}, 0, .001); ChannelMat.Comment = 'Channels'; - FileUnits = 'cm'; + FileUnits = 'mm'; case {'ASCII_NXYZ', 'ASCII_NXYZ_MNI', 'ASCII_NXYZ_WORLD'} % (*.*) - ChannelMat = in_channel_ascii(ChannelFile, {'Name','X','Y','Z'}, 0, .01); + ChannelMat = in_channel_ascii(ChannelFile, {'Name','X','Y','Z'}, 0, .001); ChannelMat.Comment = 'Channels'; - FileUnits = 'cm'; + FileUnits = 'mm'; case {'ASCII_XYZN', 'ASCII_XYZN_MNI', 'ASCII_XYZN_WORLD'} % (*.*) - ChannelMat = in_channel_ascii(ChannelFile, {'X','Y','Z','Name'}, 0, .01); + ChannelMat = in_channel_ascii(ChannelFile, {'X','Y','Z','Name'}, 0, .001); ChannelMat.Comment = 'Channels'; - FileUnits = 'cm'; + FileUnits = 'mm'; case 'ASCII_NXY' % (*.*) ChannelMat = in_channel_ascii(ChannelFile, {'Name','X','Y'}, 0, .000875); ChannelMat.Comment = 'Channels'; diff --git a/toolbox/io/in_data_plx.m b/toolbox/io/in_data_plx.m new file mode 100644 index 0000000000..11d21e07e9 --- /dev/null +++ b/toolbox/io/in_data_plx.m @@ -0,0 +1,71 @@ +function [DataMat, ChannelMat] = in_data_plx( DataFile ) +% IN_DATA_PLX: Read a PLX file. +% +% INPUT: +% - DataFile : Full path to a recordings file (called 'data' files in Brainstorm) +% OUTPUT: +% - DataMat : Brainstorm standard recordings ('data') structure + +% @============================================================================= +% This function is part of the Brainstorm software: +% https://neuroimage.usc.edu/brainstorm +% +% Copyright (c) University of Southern California & McGill University +% This software is distributed under the terms of the GNU General Public License +% as published by the Free Software Foundation. Further details on the GPLv3 +% license can be found at http://www.gnu.org/copyleft/gpl.html. +% +% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE +% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY +% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY +% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. +% +% For more information type "brainstorm license" at command prompt. +% =============================================================================@ +% +% Authors: Konstantinos Nasiotis 2022 + + +%% ===== GET OPTIONS ===== +% Get file info +[sFile, ChannelMat] = in_fopen_plexon(DataFile); % This call gets all of the file info (channels, events etc.) + +%% Now Read entire recording and assign to DataMat +% Initialize returned structures +DataMat = db_template('DataMat'); +DataMat.Comment = 'EEG/PLX'; +DataMat.DataType = 'recordings'; +DataMat.Device = 'Plexon'; + +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('start', 'Plexon importer', 'Reading entire recording'); +end +% Read entire file +DataMat.F = in_fread_plexon(sFile, [], [], []); + +% Add the events +DataMat.Events = sFile.events; + +% Build time vector +DataMat.Time = sFile.prop.times(1):1/sFile.prop.sfreq:sFile.prop.times(2); + +% ChannelFlag +DataMat.ChannelFlag = ones(size(DataMat.F,1), 1); + +% Replace NaN with 0 +DataMat.F(isnan(DataMat.F)) = 0; + +% Save number of trials averaged +DataMat.nAvg = 1; + +% Build comment +[fPath, fBase, fExt] = bst_fileparts(DataFile); +DataMat.Comment = fBase; + +isProgress = bst_progress('isVisible'); +if isProgress + bst_progress('stop'); +end +end diff --git a/toolbox/io/in_events_nwb.m b/toolbox/io/in_events_nwb.m index f51e595c23..5d573a374a 100644 --- a/toolbox/io/in_events_nwb.m +++ b/toolbox/io/in_events_nwb.m @@ -122,7 +122,7 @@ events_spikes(iNeuron).times = times; events_spikes(iNeuron).reactTimes = []; events_spikes(iNeuron).select = 1; - events_spikes(iNeuron).channels = cell(1, size(events_spikes(iNeuron).times, 2)); + events_spikes(iNeuron).channels = repmat({{ChannelMat.Channel(theChannel).Name}}, 1, size(events_spikes(iNeuron).times, 2)); events_spikes(iNeuron).notes = cell(1, size(events_spikes(iNeuron).times, 2)); % Check on which epoch each event belongs to diff --git a/toolbox/io/in_fopen.m b/toolbox/io/in_fopen.m index efa9f908e2..2d650f15db 100644 --- a/toolbox/io/in_fopen.m +++ b/toolbox/io/in_fopen.m @@ -158,7 +158,13 @@ [sFile, ChannelMat] = in_fopen_intan(DataFile); end case 'EEG-PLEXON' - [sFile, ChannelMat] = in_fopen_plexon(DataFile); + [fPath, fBase, fExt] = bst_fileparts(DataFile); + switch lower(fExt) + case '.plx' % New Plexon reader, with conversion to .bst format for faster access + [DataMat, ChannelMat] = in_data_plx(DataFile); + case '.pl2' % Old Plexon reader + [sFile, ChannelMat] = in_fopen_plexon(DataFile); + end case 'EEG-TDT' [sFile, ChannelMat] = in_fopen_tdt(DataFile); case {'NWB', 'NWB-CONTINUOUS'} diff --git a/toolbox/io/in_fopen_blackrock.m b/toolbox/io/in_fopen_blackrock.m index 8e2495a701..46d40a8729 100644 --- a/toolbox/io/in_fopen_blackrock.m +++ b/toolbox/io/in_fopen_blackrock.m @@ -129,7 +129,7 @@ % Time factor tFactorNev = double(hdr.SamplingFreq) / double(nev.MetaTags.TimeRes); % Get spike event BST prefix - spikeEventPrefix = process_spikesorting_supervised('GetSpikesEventPrefix'); + spikeEventPrefix = panel_spikes('GetSpikesEventPrefix'); % Use spikes if ~isempty(nev.Data.Spikes.TimeStamp) diff --git a/toolbox/io/in_fopen_plexon.m b/toolbox/io/in_fopen_plexon.m index 484bea743a..52f94dd2a9 100644 --- a/toolbox/io/in_fopen_plexon.m +++ b/toolbox/io/in_fopen_plexon.m @@ -70,6 +70,7 @@ channelsWithTimeseriesNames = all_Channel_names(channels_with_timetraces); %% ===== CREATE CHANNEL FILE ===== +bst_progress('text', 'Plexon: Creating channel file'); all_signalTypesWithoutNumbers = regexprep(all_Channel_names,'[\d"]','')'; signalTypesWithoutNumbers = regexprep(channelsWithTimeseriesNames,'[\d"]','')'; @@ -143,7 +144,7 @@ sFile.comment = Comment; sFile.prop.nAvg = 1; sFile.prop.sfreq = Fs; -sFile.prop.times = [ts, (fn - 1)/Fs + ts]; +sFile.prop.times = [ts, (fn - 1)/Fs+ts]; % No info on bad channels sFile.channelflag = ones(hdr.ChannelCount, 1); @@ -168,7 +169,8 @@ % Store in Brainstorm event structure for iEvt = 1:length(iPresentEvents) - + bst_progress('text', sprintf('Plexon: Acquisition events [%d/%d]', iEvt, length(iPresentEvents))); + % Get event times [n, ts, sv] = plx_event_ts(DataFile, evchans(iPresentEvents(iEvt))); times = ts; % In seconds @@ -188,44 +190,50 @@ %% Read the Spikes events -if sum(spikes_tscounts(1,:))>0 && ~strcmp(selectedSignalType, 'AI') % If spikes exist and not analog input selected +if sum(spikes_tscounts(2,:))>0 && ~strcmp(selectedSignalType, 'AI') % If spikes exist and not analog input selected + iEvt = 0; + nUnique_events = sum(sum(spikes_tscounts(2:end,2:end)>0)); % First row of spike_tscounts is unsorted spikes. First column of spikes_tscounts is ignored - unique_events = sum(sum(spikes_tscounts(:,2:end)>0)); % First row of spikes_tscounts is ignored - % Initialize list of events - events = repmat(db_template('event'), 1, unique_events); + events = repmat(db_template('event'), 1, nUnique_events); iEnteredEvent = 1; - spike_event_prefix = process_spikesorting_supervised('GetSpikesEventPrefix'); + spike_event_prefix = panel_spikes('GetSpikesEventPrefix'); for iChannel = 1:size(spikes_tscounts,2)-1 - nNeurons = sum(spikes_tscounts(:,iChannel+1)>0); % spikes_tscounts: rows = different units on the same channel, columns = channels - - for iNeuron = 1:length(nNeurons) - - if length(nNeurons)>1 - event_label_postfix = [' |' num2str(iNeuron) '|']; - else - event_label_postfix = ''; + nNeurons = sum(spikes_tscounts(2:end,iChannel+1)>0); % spikes_tscounts: rows = different units on the same channel, columns = channels + + for iNeuron = 1:nNeurons + if spikes_tscounts(iNeuron+1, iChannel+1)>0 + iEvt = iEvt + 1; + bst_progress('text', sprintf('Plexon: Spiking events [%d/%d]', iEvt, nUnique_events)); + + if nNeurons>1 + event_label_postfix = [' |' num2str(iNeuron) '|']; + else + event_label_postfix = ''; + end + + [n, spikeTimes] = plx_ts(DataFile, iChannel, iNeuron); + + % Fill the event fields + events(iEnteredEvent).label = [spike_event_prefix ' ' all_Channel_names{iChannels_selected(iChannel)} event_label_postfix]; + events(iEnteredEvent).color = rand(1,3); + events(iEnteredEvent).times = spikeTimes'; + events(iEnteredEvent).epochs = ones(1, size(events(iEnteredEvent).times, 2)); + events(iEnteredEvent).reactTimes = []; + events(iEnteredEvent).select = 1; + events(iEnteredEvent).notes = cell(1, size(events(iEnteredEvent).times, 2)); + events(iEnteredEvent).channels = repmat({{all_Channel_names{iChannels_selected(iChannel)}}}, 1, size(events(iEnteredEvent).times, 2)); + + iEnteredEvent = iEnteredEvent + 1; end - - [n, spikeTimes] = plx_ts(DataFile, iChannel, iNeuron-1); - - % Fill the event fields - events(iEnteredEvent).label = [spike_event_prefix ' ' all_Channel_names{iChannels_selected(iChannel)}]; - events(iEnteredEvent).color = rand(1,3); - events(iEnteredEvent).times = spikeTimes'; - events(iEnteredEvent).epochs = ones(1, size(events(iEnteredEvent).times, 2)); - events(iEnteredEvent).reactTimes = []; - events(iEnteredEvent).select = 1; - events(iEnteredEvent).channels = cell(1, size(events(iEnteredEvent).times, 2)); - events(iEnteredEvent).notes = cell(1, size(events(iEnteredEvent).times, 2)); - iEnteredEvent = iEnteredEvent + 1; end end % Import this list sFile = import_events(sFile, [], events); end -end \ No newline at end of file + +end diff --git a/toolbox/io/in_fopen_tdt.m b/toolbox/io/in_fopen_tdt.m index 8b3756a4ab..c2ace439ee 100644 --- a/toolbox/io/in_fopen_tdt.m +++ b/toolbox/io/in_fopen_tdt.m @@ -42,18 +42,12 @@ hdr.BaseFolder = DataFolder; - - - %% ===== FILE COMMENT ===== % Comment: BaseFolder Comment = DataFolder; - %% ===== READ DATA HEADERS ===== - -bst_progress('start', 'TDT', 'Reading headers...'); - +bst_progress('text', 'TDT: Reading headers...'); % Load one second segment to see what type of signals exist in this dataset % Use as general sampling rate the rate of the HIGHEST sampled signal @@ -79,7 +73,6 @@ LFP_label_exists = 0; - ii = 1; for iStream = 1:length(all_streams) stream_info(iStream).label = all_streams{iStream}; @@ -99,22 +92,25 @@ general_sampling_rate = data_new.streams.(all_streams{iStream}).fs; LFP_label_exists = 1; end + popup_labels{iStream} = [all_streams{iStream} ' - ' num2str(stream_info(iStream).fs) ' Hz']; end if ~LFP_label_exists [indx,tf] = listdlg('PromptString',{'Select the label of the electrophysiological signal that is present in this dataset',... 'If more streams are present, they will be resampled to match the Fs of this stream.',''},... - 'SelectionMode','single','ListString',all_streams); + 'SelectionMode','single','ListString', popup_labels); data_new = TDTbin2mat(DataFolder, 'STORE', all_streams{indx},'T1', 0, 'T2', 1); % 1 second segment general_sampling_rate = data_new.streams.(all_streams{indx}).fs; LFP_label_exists = 1; + if isempty(data_new.streams) + error('The selected stream is empty'); + end if isempty(indx) - bst_error('No stream was selected') - stop + error('No stream was selected'); end end @@ -171,7 +167,7 @@ %% Check for acquisition events -bst_progress('start', 'TDT', 'Collecting acquisition events...'); +bst_progress('text', 'TDT: Collecting acquisition events...'); disp('Getting Acquisition System events') NO_data = TDTbin2mat(DataFolder, 'TYPE', 2); % Just load epocs / events @@ -206,7 +202,12 @@ iindex = iindex+1; - events(iindex).label = [NO_data.epocs.(all_event_Labels{iEvent}).name num2str(conditions_in_event(iCondition))]; + + if strcmp(all_event_Labels{iEvent}, 'Note') + events(iindex).label = NO_data.epocs.Note.notes{iCondition}; + else + events(iindex).label = [NO_data.epocs.(all_event_Labels{iEvent}).name num2str(conditions_in_event(iCondition))]; + end events(iindex).color = rand(1,3); events(iindex).epochs = ones(1,length(selected_Events_for_condition)) ; events(iindex).times = NO_data.epocs.(all_event_Labels{iEvent}).onset(selected_Events_for_condition)'; @@ -222,18 +223,10 @@ %% Check for spike events - check_for_spikes = 1; - - - - - - if check_for_spikes - bst_progress('start', 'TDT', 'Collecting spiking events...'); - disp('Getting spiking events') + bst_progress('text', 'TDT: Collecting spiking events...'); NO_data = TDTbin2mat(DataFolder, 'TYPE', 3); % Just load spikes are_there_spikes = ~isempty(NO_data.snips); else @@ -241,8 +234,6 @@ end - - %%%%%%%% disp('***************************************************') disp('CHECK THE SPIKES. THEY ARE ONLY ASSIGNED ON RIG TWO') @@ -250,8 +241,6 @@ %%%%%%%% - - if are_there_spikes if ~exist ('events','var') @@ -302,7 +291,7 @@ events(last_event_index).times = NO_data.snips.(all_spike_event_Labels{iSpikeDetectedField}).ts(SpikesOfThatNeuronOnChannel_Indices)'; events(last_event_index).reactTimes = []; events(last_event_index).select = 1; - events(last_event_index).channels = cell(1, size(events(last_event_index).times, 2)); + events(last_event_index).channels = repmat({{ChannelMat.Channel(channels_are_EEG_on_selected_RIG(iChannel)).Name}}, 1, size(events(last_event_index).times, 2)); events(last_event_index).notes = cell(1, size(events(last_event_index).times, 2)); end end @@ -312,6 +301,5 @@ % Import this list sFile = import_events(sFile, [], events); -end - +end diff --git a/toolbox/io/in_fread_plexon.m b/toolbox/io/in_fread_plexon.m index 65a4018c17..176ec8c2ec 100644 --- a/toolbox/io/in_fread_plexon.m +++ b/toolbox/io/in_fread_plexon.m @@ -1,10 +1,7 @@ function F = in_fread_plexon(sFile, SamplesBounds, iChannels, precision) % IN_FREAD_PLEXON Read a block of recordings from a Plexon file % -% USAGE: F = in_fread_intan(sFile, SamplesBounds=[], iChannels=[]) - -% % This function is using the importer developed by Benjamin Kraus (2013) -% https://www.mathworks.com/matlabcentral/fileexchange/42160-readplxfilec +% USAGE: F = in_fread_plexon(sFile, SamplesBounds=[], iChannels=[]) % @============================================================================= % This function is part of the Brainstorm software: @@ -43,7 +40,11 @@ iChannels = 1:sFile.header.ChannelCount; end if (nargin < 2) || isempty(SamplesBounds) - SamplesBounds = round((sFile.prop.times - sFile.header.FirstTimeStamp) .* sFile.prop.sfreq); + % Read entire recording + SamplesBounds = round((sFile.prop.times - sFile.header.FirstTimeStamp) .* sFile.prop.sfreq) + 1; +else + % Readjust the samples call based on the starting time value + SamplesBounds = SamplesBounds - round(sFile.header.FirstTimeStamp* sFile.prop.sfreq) + 1; end @@ -55,11 +56,9 @@ % Initialize Brainstorm output F = zeros(nChannels, nSamples, precision); -for iChannel = 1:nChannels - [adfreq, n, data] = plx_ad_span_v(sFile.filename, iSelectedChannels(iChannel)-1, SamplesBounds(1), SamplesBounds(2)); - F(iChannel,:) = data; -end +for iChannel = 1:nChannels + % plx_ad_span_v returns values in mV + [adfreq, n, data] = plx_ad_span_v(sFile.filename, iSelectedChannels(iChannel)-1, SamplesBounds(1), SamplesBounds(2)); + F(iChannel,:) = data./1000; % Convert to V end - - - +end \ No newline at end of file diff --git a/toolbox/io/in_fread_tdt.m b/toolbox/io/in_fread_tdt.m index 51ce277397..bcfdd5a1b2 100644 --- a/toolbox/io/in_fread_tdt.m +++ b/toolbox/io/in_fread_tdt.m @@ -89,7 +89,14 @@ for iStream = 1:length(streams_to_load) data = TDTbin2mat(sFile.filename, 'TYPE', 4, 'STORE', streams_to_load{iStream}, 'T1', SamplesBounds(1)/Fs, 'T2', SamplesBounds(2)/Fs); - data = data.streams.(streams_to_load{iStream}); + + % TDTbin2mat return values only when the stream is enabled within the + % requested time period. If nothing is returned, fill it with zeros. + if ~isempty(data.streams) + data = data.streams.(streams_to_load{iStream}); + else + data.data = zeros(length(selected_channels_from_stream{iStream}),nSamples); + end % DO THE EXTRAPOLATION HERE FOR THE LOW SAMPLED SIGNALS (EYE TRACES ETC.) @@ -117,7 +124,6 @@ % DROP SAMPLES HERE FOR THE HIGH SAMPLED SIGNALS (LED, EMG ETC.) elseif stream_info(iStream).fs > Fs - high_sampled_signal = double(data.data(selected_channels_from_stream{iStream},:)); % Make sure the signal has all the samples we expect @@ -127,7 +133,6 @@ high_sampled_signal2(:,1:min(nGottenSamples,nExpectedSamples)) = high_sampled_signal; high_sampled_signal = high_sampled_signal2; - % high_sampled_signal = double(data.data(selected_channels_from_stream{iStream},:)); nSamplesToDrop = size(high_sampled_signal,2) - nSamples; diff --git a/toolbox/io/in_spikesorting_convertforkilosort.m b/toolbox/io/in_spikesorting_convertforkilosort.m deleted file mode 100644 index 220f3f3479..0000000000 --- a/toolbox/io/in_spikesorting_convertforkilosort.m +++ /dev/null @@ -1,98 +0,0 @@ -function converted_raw_File = in_spikesorting_convertforkilosort( varargin ) -% IN_SPIKESORTING_RAWELECTRODES: Loads and creates if needed separate raw -% electrode files for spike sorting purposes. -% -% USAGE: OutputFiles = in_spikesorting_convertforkilosort(sInputs, ram) - -% @============================================================================= -% This function is part of the Brainstorm software: -% https://neuroimage.usc.edu/brainstorm -% -% Copyright (c) University of Southern California & McGill University -% This software is distributed under the terms of the GNU General Public License -% as published by the Free Software Foundation. Further details on the GPLv3 -% license can be found at http://www.gnu.org/copyleft/gpl.html. -% -% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE -% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY -% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF -% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY -% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. -% -% For more information type "brainstorm license" at command prompt. -% =============================================================================@ -% -% Authors: Konstantinos Nasiotis, 2018-2019; Martin Cousineau, 2018 - -sInput = varargin{1}; -if nargin < 2 || isempty(varargin{2}) - ram = 1e9; % 1 GB -else - ram = varargin{2}; -end - -protocol = bst_get('ProtocolInfo'); -parentPath = bst_fullfile(bst_get('BrainstormTmpDir'), ... - 'Unsupervised_Spike_Sorting', ... - protocol.Comment, ... - sInput.FileName); - -% Make sure the temporary directory exist, otherwise create it -if ~exist(parentPath, 'dir') - mkdir(parentPath); -end - -DataMat = in_bst_data(sInput.FileName, 'F'); -ChannelMat = in_bst_channel(sInput.ChannelFile); -sFile = DataMat.F; -fileSamples = round(sFile.prop.times .* sFile.prop.sfreq); - -% Separate the file to max length based on RAM -numChannels = length(ChannelMat.Channel); -max_samples = ram / 8 / numChannels; - -converted_raw_File = bst_fullfile(parentPath, ['raw_data_no_header_' sInput.Condition(5:end) '.dat']); - -bst_progress('start', 'Spike-sorting', 'Converting to KiloSort Input...', 0, ceil(fileSamples(2)/max_samples)); - -if exist(converted_raw_File, 'file') == 2 - disp('File already converted') - return -end - -ImportOptions = db_template('ImportOptions'); - -%% Check if a projector has been computed and ask if the selected components -% should be removed -if ~isempty(ChannelMat.Projector) - isOk = java_dialog('confirm', ... - ['(ICA/PCA) Artifact components have been computed for removal.' 10 10 ... - 'Remove the selected components?'], 'Artifact Removal'); - if isOk - ImportOptions.UseCtfComp = 0; - ImportOptions.UseSsp = 1; - end -end - -%% Convert the acquisition system file to an int16 without a header. -fid = fopen(converted_raw_File, 'a'); - -isegment = 1; -nsegment_max = 0; - -while (nsegment_max < fileSamples(2)) - nsegment_min = (isegment-1) * max_samples; - nsegment_max = isegment * max_samples - 1; - if (nsegment_max > fileSamples(2)) - nsegment_max = fileSamples(2); - end - - F = in_fread(sFile, ChannelMat, [], [nsegment_min,nsegment_max], [], ImportOptions); - - F = F*10^6 ; % This assumes that F signals are in V. I convert it to uV there are big numbers and int16 precision doesn't zero it out. - fwrite(fid, F,'int16'); - - isegment = isegment + 1; -end -fclose(fid); - diff --git a/toolbox/io/in_spikesorting_rawelectrodes.m b/toolbox/io/in_spikesorting_rawelectrodes.m deleted file mode 100644 index d3df475553..0000000000 --- a/toolbox/io/in_spikesorting_rawelectrodes.m +++ /dev/null @@ -1,168 +0,0 @@ -function sFiles = in_spikesorting_rawelectrodes( varargin ) -% IN_SPIKESORTING_RAWELECTRODES: Loads and creates if needed separate raw -% electrode files for spike sorting purposes. -% -% USAGE: OutputFiles = in_spikesorting_rawelectrodes(sInput, ram, parallel) - -% @============================================================================= -% This function is part of the Brainstorm software: -% https://neuroimage.usc.edu/brainstorm -% -% Copyright (c) University of Southern California & McGill University -% This software is distributed under the terms of the GNU General Public License -% as published by the Free Software Foundation. Further details on the GPLv3 -% license can be found at http://www.gnu.org/copyleft/gpl.html. -% -% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE -% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY -% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF -% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY -% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. -% -% For more information type "brainstorm license" at command prompt. -% =============================================================================@ -% -% Authors: Konstantinos Nasiotis, 2018; Martin Cousineau, 2018 - -sInput = varargin{1}; -if nargin < 2 || isempty(varargin{2}) - ram = 1e9; % 1 GB -else - ram = varargin{2}; -end -if nargin < 3 || isempty(varargin{3}) - parallel = 0; -else - parallel = varargin{3}; -end - -protocol = bst_get('ProtocolInfo'); -parentPath = bst_fullfile(bst_get('BrainstormTmpDir'), ... - 'Unsupervised_Spike_Sorting', ... - protocol.Comment, ... - sInput.FileName); - -% Make sure the temporary directory exist, otherwise create it -if ~exist(parentPath, 'dir') - mkdir(parentPath); -end - - -% Check whether the electrode files already exist -ChannelMat = in_bst_channel(sInput.ChannelFile); -numChannels = length(ChannelMat.Channel); - -% New channelNames - Without any special characters. -cleanNames = str_remove_spec_chars({ChannelMat.Channel.Name}); - -missingFile = 0; -sFiles = {}; -for iChannel = 1:numChannels - chanFile = bst_fullfile(parentPath, ['raw_elec_' cleanNames{iChannel} '.mat']); - if ~exist(chanFile, 'file') - missingFile = 1; - else - sFiles{end+1} = chanFile; - end -end -if ~missingFile - return; -else - % Clear any remaining intermediate file - for iFile = 1:length(sFiles) - delete(sFiles{iFile}); - end -end - -% Otherwise, generate all of them again. -DataMat = in_bst_data(sInput.FileName, 'F'); -sFile = DataMat.F; -sr = sFile.prop.sfreq; -samples = [0,0]; -max_samples = ram / 8 / numChannels; -total_samples = round((sFile.prop.times(2) - sFile.prop.times(1)) .* sFile.prop.sfreq); % (Blackrock/Ripple complained). Removed +1 -num_segments = ceil(total_samples / max_samples); -num_samples_per_segment = ceil(total_samples / num_segments); -bst_progress('start', 'Spike-sorting', 'Demultiplexing raw file...', 0, (parallel == 0) * num_segments * numChannels); - - -sFiles = {}; -for iChannel = 1:numChannels - sFiles{end + 1} = bst_fullfile(parentPath, ['raw_elec_' cleanNames{iChannel}]); -end - -% Special case for supported acquisition systems: Save temporary files -% using single precision instead of double to save disk space -ImportOptions = db_template('ImportOptions'); -if ismember(sFile.format, {'EEG-AXION', 'EEG-BLACKROCK', 'EEG-INTAN', 'EEG-PLEXON'}) - precision = 'single'; -else - precision = 'double'; -end -ImportOptions.Precision = precision; - -% Check if a projector has been computed and ask if the selected components -% should be removed -if ~isempty(ChannelMat.Projector) - isOk = java_dialog('confirm', ... - ['(ICA/PCA) Artifact components have been computed for removal.' 10 10 ... - 'Remove the selected components?'], 'Artifact Removal'); - if isOk - ImportOptions.UseSsp = 1; - end -end - -% Read data in segments -for iSegment = 1:num_segments - samples(1) = (iSegment - 1) * num_samples_per_segment; - if iSegment < num_segments - samples(2) = iSegment * num_samples_per_segment - 1; - else - samples(2) = total_samples; - end - - F = in_fread(sFile, ChannelMat, [], samples, [], ImportOptions); - - % Append segment to individual channel file - if parallel - parfor iChannel = 1:numChannels - electrode_data = F(iChannel,:); - fid = fopen([sFiles{iChannel} '.bin'], 'a'); - fwrite(fid, electrode_data, precision); - fclose(fid); - end - else - for iChannel = 1:numChannels - electrode_data = F(iChannel,:); - fid = fopen([sFiles{iChannel} '.bin'], 'a'); - fwrite(fid, electrode_data, precision); - fclose(fid); - bst_progress('inc', 1); - end - end -end - -% Convert channel files to Matlab -bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...', 0, (parallel == 0) * numChannels); -if parallel - parfor iChannel = 1:numChannels - convert2mat(sFiles{iChannel}, sr, precision); - end -else - for iChannel = 1:numChannels - convert2mat(sFiles{iChannel}, sr, precision); - bst_progress('inc', 1); - end -end - -sFiles = cellfun(@(x) [x '.mat'], sFiles, 'UniformOutput', 0); - -end - -function convert2mat(chanFile, sr, precision) - fid = fopen([chanFile '.bin'], 'rb'); - data = fread(fid, precision); - fclose(fid); - save([chanFile '.mat'], 'data', 'sr'); - file_delete([chanFile '.bin'], 1 ,3); -end diff --git a/toolbox/io/out_demultiplex.m b/toolbox/io/out_demultiplex.m new file mode 100644 index 0000000000..c66f311301 --- /dev/null +++ b/toolbox/io/out_demultiplex.m @@ -0,0 +1,138 @@ +function outFiles = out_demultiplex(DataFile, ChannelFile, OutputDir, UseSsp, ram, parallel) +% OUT_DEMULTIPLEX: Load a raw data file and creates separate electrode files. + +% @============================================================================= +% This function is part of the Brainstorm software: +% https://neuroimage.usc.edu/brainstorm +% +% Copyright (c) University of Southern California & McGill University +% This software is distributed under the terms of the GNU General Public License +% as published by the Free Software Foundation. Further details on the GPLv3 +% license can be found at http://www.gnu.org/copyleft/gpl.html. +% +% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE +% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY +% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY +% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. +% +% For more information type "brainstorm license" at command prompt. +% =============================================================================@ +% +% Authors: Konstantinos Nasiotis, 2018-2022 +% Martin Cousineau, 2018 +% Francois Tadel, 2022 + +% If the output directory doesn't exist: create it +if ~exist(OutputDir, 'dir') + mkdir(OutputDir); +end + +% Load channel file +ChannelMat = in_bst_channel(ChannelFile); +numChannels = length(ChannelMat.Channel); +% Channel names: Remove any special characters +cleanNames = str_remove_spec_chars({ChannelMat.Channel.Name}); + +% Assemble output filenames +outFiles = cellfun(@(c)bst_fullfile(OutputDir, ['raw_elec_', c]), cleanNames, 'UniformOutput', 0); +% If all files already exist: nothing else to do in this function +isFileOk = cellfun(@(c)exist([c, '.mat'], 'file'), outFiles); +if all(isFileOk) + % Add the .mat extension to the file names + disp(['BST> Channels already demultiplexed in: ' OutputDir]); + outFiles = cellfun(@(x) [x '.mat'], outFiles, 'UniformOutput', 0); + return; +% If some files already exist: delete all intermediate existing file, before generating them again +elseif any(isFileOk) + delete(outFiles{isFileOk}); +end + +% Load input data file +DataMat = in_bst_data(DataFile, 'F'); +sFile = DataMat.F; +sr = sFile.prop.sfreq; + +% Apply SSP/ICA when reading from data files +ImportOptions = db_template('ImportOptions'); +ImportOptions.UseCtfComp = 0; +ImportOptions.UseSsp = UseSsp; +% Special case for supported acquisition systems: Save temporary files +% using single precision instead of double to save disk space +if ismember(sFile.format, {'EEG-AXION', 'EEG-BLACKROCK', 'EEG-INTAN', 'EEG-PLEXON'}) + precision = 'single'; + nBytes = 4; +else + precision = 'double'; + nBytes = 8; +end +ImportOptions.Precision = precision; + +% Separate the file to max length based on RAM +max_samples = ram / nBytes / numChannels; +total_samples = round((sFile.prop.times(2) - sFile.prop.times(1)) .* sFile.prop.sfreq); % (Blackrock/Ripple complained). Removed +1 +num_segments = ceil(total_samples / max_samples); +num_samples_per_segment = ceil(total_samples / num_segments); + +% Loop on segments +for iSegment = 1:num_segments + sampleBounds(1) = (iSegment - 1) * num_samples_per_segment + round(sFile.prop.times(1)* sFile.prop.sfreq); + if iSegment < num_segments + sampleBounds(2) = sampleBounds(1) + num_samples_per_segment - 1; + else + sampleBounds(2) = total_samples + round(sFile.prop.times(1)* sFile.prop.sfreq); + end + % Read recordings + F = in_fread(sFile, ChannelMat, [], sampleBounds, [], ImportOptions); + % Append segment to individual channel file + if parallel + bst_progress('start', 'Spike-sorting', 'Demultiplexing raw file...'); + parfor iChannel = 1:numChannels + electrode_data = F(iChannel,:); + fid = fopen([outFiles{iChannel} '.bin'], 'a'); + fwrite(fid, electrode_data, precision); + fclose(fid); + end + else + bst_progress('start', 'Spike-sorting', 'Demultiplexing raw file...', 0, num_segments * numChannels); + for iChannel = 1:numChannels + electrode_data = F(iChannel,:); + fid = fopen([outFiles{iChannel} '.bin'], 'a'); + fwrite(fid, electrode_data, precision); + fclose(fid); + bst_progress('inc', 1); + end + end +end + +% Convert binary files per channel to Matlab files +if parallel + bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...'); + parfor iChannel = 1:numChannels + convert2mat(outFiles{iChannel}, sr, precision); + end +else + bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...', 0, numChannels); + for iChannel = 1:numChannels + convert2mat(outFiles{iChannel}, sr, precision); + bst_progress('inc', 1); + end +end + +% Add the .mat extension to the file names +outFiles = cellfun(@(x) [x '.mat'], outFiles, 'UniformOutput', 0); + +end + + +%% ===== CONVERT BIN TO MAT ===== +function convert2mat(chanFile, sr, precision) + % Read .bin file + fid = fopen([chanFile '.bin'], 'rb'); + data = fread(fid, precision); + fclose(fid); + % Save .mat file + save([chanFile '.mat'], 'data', 'sr'); + % Delete .bin file + delete([chanFile '.bin']); +end diff --git a/toolbox/process/bst_process.m b/toolbox/process/bst_process.m index a008ca2896..5eedefefc9 100644 --- a/toolbox/process/bst_process.m +++ b/toolbox/process/bst_process.m @@ -99,7 +99,11 @@ if isParallel try if (bst_get('MatlabVersion') >= 802) - hPool = parpool; + hPool = gcp('nocreate'); + if isempty(hPool) + bst_progress('start', 'Process', 'Starting parallel pool...'); + hPool = parpool; + end else matlabpool open; end @@ -1523,7 +1527,7 @@ end % Look for items in database switch (FileType) - case 'data' + case {'data', 'spike'} [tmp, iDb, iList] = intersect({sStudy.Data.FileName}, GroupFileNames); sItems = sStudy.Data(iDb); if ~isempty(sItems) && strcmpi(sItems(1).DataType, 'raw') diff --git a/toolbox/process/functions/process_convert_raw_to_lfp.m b/toolbox/process/functions/process_convert_raw_to_lfp.m index 3bab666637..5644cb8d43 100644 --- a/toolbox/process/functions/process_convert_raw_to_lfp.m +++ b/toolbox/process/functions/process_convert_raw_to_lfp.m @@ -23,19 +23,19 @@ % =============================================================================@ % % Authors: Konstantinos Nasiotis 2018 -% Francois Tadel, 2021 +% Francois Tadel, 2022 eval(macro_method); end %% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description the process sProcess.Comment = 'Convert Raw to LFP'; - sProcess.Category = 'custom'; + sProcess.Category = 'File'; sProcess.SubGroup = 'Electrophysiology'; - sProcess.Index = 1203; + sProcess.Index = 1205; sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/RawToLFP'; % Definition of the input accepted by this process sProcess.InputTypes = {'raw'}; @@ -45,256 +45,216 @@ sProcess.isSeparator = 1; sProcess.processDim = 1; % Process channel by channel - sProcess.options.despikeLFP.Comment = 'Despike LFP Highly Recommended if analysis uses SFC or STA'; - sProcess.options.despikeLFP.Type = 'checkbox'; - sProcess.options.despikeLFP.Value = 1; - - sProcess.options.filterbounds.Comment = 'LFP filtering limits'; - sProcess.options.filterbounds.Type = 'range'; - sProcess.options.filterbounds.Value = {[0.5, 150],'Hz',1}; - - % Definition of the options - % === Freq list - sProcess.options.freqlist.Comment = 'Notch filter Frequencies (Hz):'; + % === Demultiplexing + sProcess.options.demultlabel.Comment = 'Demultiplexing options:'; + sProcess.options.demultlabel.Type = 'label'; + % RAM limitation + sProcess.options.binsize.Comment ='Maximum RAM to use: '; + sProcess.options.binsize.Type = 'value'; + sProcess.options.binsize.Value = {2, 'GB', 1}; % This is used in case the electrodes are not separated yet (no spike sorting done), ot the temp folder was emptied + % Use SSP/ICA + sProcess.options.usessp.Comment = 'Apply the existing SSP/ICA projectors'; + sProcess.options.usessp.Type = 'checkbox'; + sProcess.options.usessp.Value = 1; + % === LFP options + sProcess.options.lfplabel.Comment = '
LFP computation options:'; + sProcess.options.lfplabel.Type = 'label'; + % Downsample + sProcess.options.LFP_fs.Comment = 'Sampling rate of the LFP signals: '; + sProcess.options.LFP_fs.Type = 'value'; + sProcess.options.LFP_fs.Value = {1000, 'Hz', 0}; + % Notch filter + sProcess.options.freqlist.Comment = 'Notch filter (Hz): '; sProcess.options.freqlist.Type = 'value'; sProcess.options.freqlist.Value = {[], 'list', 2}; - sProcess.options.freqlistHelp.Comment = 'Frequencies for notch filter (leave empty for no selection)'; - sProcess.options.freqlistHelp.Type = 'label'; - - sProcess.options.paral.Comment = 'Parallel processing'; - sProcess.options.paral.Type = 'checkbox'; - sProcess.options.paral.Value = 1; - - sProcess.options.binsizeHelp.Comment = 'The memory value below will be used in case the channels were not separated'; - sProcess.options.binsizeHelp.Type = 'label'; - - sProcess.options.binsize.Comment = 'Memory to use for demultiplexing'; - sProcess.options.binsize.Type = 'value'; - sProcess.options.binsize.Value = {1, 'GB', 1}; % This is used in case the electrodes are not separated yet (no spike sorting done), ot the temp folder was emptied - -end + % Band-pass filter + sProcess.options.filterbounds.Comment = 'Band-pass filter: '; + sProcess.options.filterbounds.Type = 'range'; + sProcess.options.filterbounds.Value = {[0.5, 150], 'Hz', 1}; + % Despike + sProcess.options.despikeLFP.Comment = 'Despike LFP  (Highly Recommended if analysis uses SFC or STA)'; + sProcess.options.despikeLFP.Type = 'checkbox'; + sProcess.options.despikeLFP.Value = 1; + % Parallel processing + sProcess.options.parallel.Comment = 'Parallel processing'; + sProcess.options.parallel.Type = 'checkbox'; + sProcess.options.parallel.Value = 1; +end %% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok +function Comment = FormatComment(sProcess) Comment = sProcess.Comment; end %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs, method) %#ok +function OutputFiles = Run(sProcess, sInput) OutputFiles = {}; - for iInput = 1:length(sInputs) - sInput = sInputs(iInput); - %% Parameters - filterBounds = sProcess.options.filterbounds.Value{1}; % Filtering bounds for the LFP - notchFilterFreqs = sProcess.options.freqlist.Value{1}; % Notch Filter frequencies for the LFP - % Output frequency - NewFreq = 1000; - - - % Get method name - if (nargin < 3) - method = []; - end - - %% Check for dependencies - % If DespikeLFP is selected: Install DeriveLFP plugin - if sProcess.options.despikeLFP.Value - [isInstalled, errMsg] = bst_plugin('Install', 'derivelfp'); - if ~isInstalled - bst_report('Error', sProcess, [], errMsg); - return; - end - end - - % Check for Signal Processing toolbox - if ~bst_get('UseSigProcToolbox') - bst_report('Warning', sProcess, [], [... - 'The Signal Processing Toolbox is not available. Using the EEGLAB method instead (results may be much less accurate).' 10 ... - 'This method is based on a FFT-based low-pass filter, followed by a spline interpolation.' 10 ... - 'Make sure you remove the DC offset before resampling; EEGLAB function does not work well when the signals are not centered.']); + % ===== OPTIONS ===== + isDespike = sProcess.options.despikeLFP.Value; + BandPass = sProcess.options.filterbounds.Value{1}; % Filtering bounds for the LFP + NotchFreqs = sProcess.options.freqlist.Value{1}; % Notch Filter frequencies for the LFP + LFP_fs = sProcess.options.LFP_fs.Value{1}; % Output frequency + BinSize = sProcess.options.binsize.Value{1}; + isParallel = sProcess.options.parallel.Value; + UseSsp = sProcess.options.usessp.Value; + % Get protocol info + ProtocolInfo = bst_get('ProtocolInfo'); + BrainstormTmpDir = bst_get('BrainstormTmpDir'); + + % ===== DEPENDENCIES ===== + % Not available in the compiled version + if bst_iscompiled() + error('This function is not available in the compiled version of Brainstorm.'); + end + % Despike requirements + if isDespike + % Check for the Optimization toolbox + if exist('fminunc', 'file') ~= 2 + bst_report('Error', sProcess, sInput, 'This process requires the Optimization Toolbox.'); + return; end - - % Prepare parallel pool, if requested - if sProcess.options.paral.Value - try - poolobj = gcp('nocreate'); - if isempty(poolobj) - parpool; - end - catch - sProcess.options.paral.Value = 0; - poolobj = []; - end - else - poolobj = []; + % Install plugin + [isInstalled, errMsg] = bst_plugin('Install', 'derivelfp'); + if ~isInstalled + bst_report('Error', sProcess, [], errMsg); + return; end + end - %% Check if the files are separated per channel. If not do it now. - % These files will be converted to LFP right after - sFiles_temp_mat = in_spikesorting_rawelectrodes(sInput, sProcess.options.binsize.Value{1}(1) * 1e9, sProcess.options.paral.Value); - % Load full input file - sMat = in_bst(sInput.FileName, [], 0); - Fs = 1 / diff(sMat.Time(1:2)); % This is the original sampling rate - % Inialize LFP matrix - LFP = zeros(length(sFiles_temp_mat), length(downsample(sMat.Time,round(Fs/NewFreq)))); % This shouldn't create a memory problem - - %% Initialize - % Prepare output file - ProtocolInfo = bst_get('ProtocolInfo'); - newCondition = [sInput.Condition, '_LFP']; - - if mod(Fs,NewFreq) ~= 0 - % This should never be an issue. Never heard of an acquisition - % system that doesn't record in multiples of 1kHz. - warning(['The downsampling might not be accurate. This process downsamples from ' num2str(Fs) ' to ' num2str(NewFreq) ' Hz']) + % ===== LOAD INPUTS ===== + % Load input file + DataMat = in_bst(sInput.FileName, [], 0); + sFileIn = DataMat.F; + Fs = sFileIn.prop.sfreq; % Original sampling rate + % Check sampling rate + if (Fs < LFP_fs) + bst_report('Error', sProcess, sInput, 'The requested LFP sampling rate is higher than the RAW signal sampling rate. No need to use this function'); + return; + elseif mod(round(Fs),round(LFP_fs)) ~= 0 + % This should never be an issue. Never heard of an acquisition system that doesn't record in multiples of 1kHz. + bst_report('Warning', sProcess, sInput, ['The downsampling might not be accurate. This process downsamples from ' num2str(Fs) ' to ' num2str(LFP_fs) ' Hz']); + end + % Demultiplex channels + demultiplexDir = bst_fullfile(BrainstormTmpDir, 'Unsupervised_Spike_Sorting', ProtocolInfo.Comment, sInput.FileName); + ElecFiles = out_demultiplex(sInput.FileName, sInput.ChannelFile, demultiplexDir, UseSsp, BinSize * 1e9, isParallel); + % Load channel file + ChannelMat = in_bst_channel(sInput.ChannelFile); + nChannels = length(ChannelMat.Channel); + + % ===== OUTPUT FILE ===== + % Get input study + sStudyInput = bst_get('AnyFile', sInput.FileName); + % New study path + newStudyPath = file_unique(bst_fullfile(bst_fileparts(bst_fileparts(file_fullpath(sStudyInput.FileName))), [sInput.Condition, '_LFP'])); + % New folder name + [tmp, newCondition] = bst_fileparts(newStudyPath); + % Create output folder + iOutputStudy = db_add_condition(sInput.SubjectName, newCondition, [], sStudyInput.DateOfStudy); + if isempty(iOutputStudy) + bst_report('Error', sProcess, sInput, ['Output folder could not be created:' 10 newPath]); + return; + end + sOutputStudy = bst_get('Study', iOutputStudy); + + % Get new condition name + newStudyPath = bst_fileparts(file_fullpath(sOutputStudy.FileName)); + % Full output filename + RawFileOut = bst_fullfile(newStudyPath, [strrep(newCondition, '@raw', '') '.bst']); % *** + RawFileFormat = 'BST-BIN'; + + % Number of time points in output file + newTimeVector = downsample(DataMat.Time, round(Fs/LFP_fs)); + nTimeOut = length(newTimeVector); + % Template structure for the creation of the output raw file + sFileTemplate = sFileIn; + sFileTemplate.prop.sfreq = LFP_fs; + sFileTemplate.prop.times = [newTimeVector(1), newTimeVector(end)]; + sFileTemplate.header.sfreq = LFP_fs; + sFileTemplate.header.nsamples = nTimeOut; + % Convert events to new sampling rate + sFileTemplate.events = panel_record('ChangeTimeVector', sFileTemplate.events, Fs, newTimeVector); + % Update file comment + sFileTemplate.CommentTag = sprintf('resample(%dHz)', round(LFP_fs)); + % History + sFileTemplate = bst_history('add', sFileTemplate, 'raw2lfp', sprintf('Filter [%0.1f-%0.1f]Hz - Resample from %0.2f Hz to %0.2f Hz', BandPass(1), BandPass(2), Fs, LFP_fs)); + + % ===== DERIVE LFP ===== + % Get channel names with no special characters + cleanChannelNames = str_remove_spec_chars({ChannelMat.Channel.Name}); + % Inialize LFP matrix + LFP = zeros(length(ElecFiles), nTimeOut); % This shouldn't create a memory problem + % Process with or without the Parallel toolbox + if isParallel + bst_progress('start', 'Raw2LFP', 'Converting raw signals to LFP...'); + parfor iChannel = 1:nChannels + LFP(iChannel,:) = ProcessChannel(ElecFiles{iChannel}, isDespike, NotchFreqs, BandPass, sFileIn, ChannelMat, cleanChannelNames, LFP_fs); end - - % Get new condition name - newStudyPath = file_unique(bst_fullfile(ProtocolInfo.STUDIES, sInput.SubjectName, newCondition)); - % Output file name derives from the condition name - [tmp, rawBaseOut, rawBaseExt] = bst_fileparts(newStudyPath); - rawBaseOut = strrep([rawBaseOut rawBaseExt], '@raw', ''); - % Full output filename - RawFileOut = bst_fullfile(newStudyPath, [rawBaseOut '.bst']); % *** - RawFileFormat = 'BST-BIN'; % *** - ChannelMat = in_bst_channel(sInput.ChannelFile); % *** - nChannels = length(ChannelMat.Channel); - % Get input study (to copy the creation date) - sInputStudy = bst_get('AnyFile', sInput.FileName); - - sStudy = bst_get('ChannelFile', sInput.ChannelFile); - [tmp, iSubject] = bst_get('Subject', sStudy.BrainStormSubject, 1); - - % Get new condition name - [tmp, ConditionName] = bst_fileparts(newStudyPath, 1); - % Create output condition - iOutputStudy = db_add_condition(sInput.SubjectName, ConditionName, [], sInputStudy.DateOfStudy); - - ChannelMatOut = ChannelMat; - sFileTemplate = sMat.F; - - %% Get the transformed channelnames that were used on the signal data naming. This is used in the derive lfp function in order to find the spike events label - % New channelNames - Without any special characters. - cleanChannelNames = str_remove_spec_chars({ChannelMat.Channel.Name}); - - %% Update fields before initializing the header on the binary file - sFileTemplate.prop.sfreq = NewFreq; - sFileTemplate.prop.times = round(sFileTemplate.prop.times(1) * NewFreq) / NewFreq + [0, size(LFP,2)-1] ./ NewFreq; - sFileTemplate.header.sfreq = NewFreq; - sFileTemplate.header.nsamples = size(LFP,2); - - % Update file - sFileTemplate.CommentTag = sprintf('resample(%dHz)', round(NewFreq)); - sFileTemplate.HistoryComment = sprintf('Filter [%0.1f-%0.1f]Hz - Resample from %0.2f Hz to %0.2f Hz (%s)', filterBounds(1), filterBounds(2), Fs, NewFreq, method); - sFileTemplate.despikeLFP = sProcess.options.despikeLFP.Value; - - % Convert events to new sampling rate - newTimeVector = panel_time('GetRawTimeVector', sFileTemplate); - sFileTemplate.events = panel_record('ChangeTimeVector', sFileTemplate.events, Fs, newTimeVector); - - %% Create an empty Brainstorm-binary file and assign the correct samples-times - % The sFileOut is what will be the final - [sFileOut, errMsg] = out_fopen(RawFileOut, RawFileFormat, sFileTemplate, ChannelMat); - - - %% Filter and derive LFP - bst_progress('start', 'Spike-sorting', 'Converting RAW signals to LFP...', 0, (sProcess.options.paral.Value == 0) * nChannels); - if sProcess.options.despikeLFP.Value - if sProcess.options.paral.Value - parfor iChannel = 1:nChannels - LFP(iChannel,:) = BayesianSpikeRemoval(sFiles_temp_mat{iChannel}, filterBounds, sMat.F, ChannelMat, cleanChannelNames, notchFilterFreqs); - end - else - for iChannel = 1:nChannels - LFP(iChannel,:) = BayesianSpikeRemoval(sFiles_temp_mat{iChannel}, filterBounds, sMat.F, ChannelMat, cleanChannelNames, notchFilterFreqs); - bst_progress('inc', 1); - end - end - else - if sProcess.options.paral.Value - parfor iChannel = 1:nChannels - LFP(iChannel,:) = filter_and_downsample(sFiles_temp_mat{iChannel}, Fs, filterBounds, notchFilterFreqs); - end - else - for iChannel = 1:nChannels - LFP(iChannel,:) = filter_and_downsample(sFiles_temp_mat{iChannel}, Fs, filterBounds, notchFilterFreqs); - bst_progress('inc', 1); - end - end + else + bst_progress('start', 'Raw2LFP', 'Converting raw signals to LFP...', 0, nChannels); + for iChannel = 1:nChannels + LFP(iChannel,:) = ProcessChannel(ElecFiles{iChannel}, isDespike, NotchFreqs, BandPass, sFileIn, ChannelMat, cleanChannelNames, LFP_fs); + bst_progress('inc', 1); end - - % WRITE OUT - sFileOut = out_fwrite(sFileOut, ChannelMatOut, [], [], [], LFP); + end - % Import the RAW file in the database viewer and open it immediately - RawFile = import_raw({sFileOut.filename}, 'BST-BIN', iSubject); - RawFile = RawFile{1}; - - % Modify it slightly since this is an LFP raw file - [sStudy, iStudy] = bst_get('DataFile', RawFile); - RawMat = load(RawFile); - RawMat.Comment = 'Link to LFP file'; - RawNewFile = strrep(RawFile, 'data_0raw', 'data_0lfp'); - bst_save(RawNewFile, RawMat, 'v6'); - OutputFiles{end + 1} = RawNewFile; - delete(RawFile); - db_reload_studies(iStudy); + % ===== SAVE OUTPUT FILE ===== + % Open RAW file for writing + [sFileOut, errMsg] = out_fopen(RawFileOut, RawFileFormat, sFileTemplate, ChannelMat); + if ~isempty(errMsg) + bst_report('Error', sProcess, sInput, ['Output file could not be created:' 10 errMsg]); + return; end + % Save data to output raw file + sFileOut = out_fwrite(sFileOut, ChannelMat, [], [], [], LFP); + % Get subject index + [tmp, iSubject] = bst_get('Subject', sStudyInput.BrainStormSubject, 1); + % Import the output RAW file in the database + OutputFiles = import_raw({sFileOut.filename}, 'BST-BIN', iSubject); end -function data = filter_and_downsample(inputFilename, Fs, filterBounds, notchFilterFreqs) - sMat = load(inputFilename); % Make sure that a variable named data is loaded here. This file is saved as an output from the separator - - if ~isempty(notchFilterFreqs) - % Apply notch filter - data = process_notch('Compute', sMat.data, sMat.sr, notchFilterFreqs)'; - else - data = sMat.data'; +%% ===== PROCESS CHANNEL ===== +function data = ProcessChannel(ElecFile, isDespike, NotchFreqs, BandPass, sFileIn, ChannelMat, cleanChannelNames, LFP_fs) + % Load electrode file + load(ElecFile, 'data', 'sr'); + data = data'; + % Apply notch filter + if ~isempty(NotchFreqs) + data = process_notch('Compute', data, sr, NotchFreqs); end - - % Apply final filter - data = bst_bandpass_hfilter(data, Fs, filterBounds(1), filterBounds(2), 0, 0); - data = downsample(data, round(Fs/1000)); % The file now has a different sampling rate (fs/30) = 1000Hz. + % Spike removal + if isDespike + % Get channel name from electrode file name + [tmp, ChannelName] = fileparts(ElecFile); + ChannelName = strrep(ChannelName, 'raw_elec_', ''); + data = BayesianSpikeRemoval(ChannelName, data, sr, sFileIn, ChannelMat, cleanChannelNames, BandPass); + data = data'; + end + % Band-pass filter + data = bst_bandpass_hfilter(data, sr, BandPass(1), BandPass(2), 0, 0); + % Resample + data = process_resample('Compute', data, linspace(0, size(data,2)/sr, size(data,2)), LFP_fs); end -%% BAYESIAN DESPIKING -function data_derived = BayesianSpikeRemoval(inputFilename, filterBounds, sFile, ChannelMat, cleanChannelNames, notchFilterFreqs) - - sMat = load(inputFilename); % Make sure that a variable named data is loaded here. This file is saved as an output from the separator - - %% Instead of just filtering and then downsampling, DeriveLFP is used, as in: - % https://www.ncbi.nlm.nih.gov/pubmed/21068271 - - if ~isempty(notchFilterFreqs) - % Apply notch filter - data_deligned = process_notch('Compute', sMat.data, sMat.sr, notchFilterFreqs); - else - data_deligned = sMat.data; - end - - Fs = sMat.sr; +%% ===== BAYESIAN SPIKE REMOVAL ===== +% Reference: https://www.ncbi.nlm.nih.gov/pubmed/21068271 +function data_derived = BayesianSpikeRemoval(ChannelName, data, Fs, sFile, ChannelMat, cleanChannelNames, BandPass) % Assume that a spike lasts 3ms - nSegment = sMat.sr * 0.003; + nSegment = round(Fs * 0.003); Bs = eye(nSegment); % 60x60 opts.displaylevel = 0; % 0 gets rid of all the outputs % 2 shows the optimization steps - %% Get the channel Index of the file that is imported - [tmp, ChannelName] = fileparts(inputFilename); - ChannelName = strrep(ChannelName, 'raw_elec_', ''); - % I need to find the transformed channelname index that is used at the filename. + % Find the transformed channelname index that is used at the filename. iChannel = find(ismember(cleanChannelNames, ChannelName)); - - % Get the index of the event that show this electrode's spikes allEventLabels = {sFile.events.label}; - spike_event_prefix = process_spikesorting_supervised('GetSpikesEventPrefix'); + spike_event_prefix = panel_spikes('GetSpikesEventPrefix'); % First check if there is only one neuron on the channel iEventforElectrode = find(ismember(allEventLabels, [spike_event_prefix ' ' ChannelMat.Channel(iChannel).Name])); % Find the index of the spike-events that correspond to that electrode (Exact string match) %Then check if there are multiple @@ -304,8 +264,7 @@ iEventforElectrode = find(not(cellfun('isempty', strfind(allEventLabels, [spike_event_prefix ' ' ChannelMat.Channel(iChannel).Name ' |'])))); end end - - + % If there are no neurons picked up from that electrode, continue % Apply despiking around the spiking times if ~isempty(iEventforElectrode) % If there are spikes on that electrode @@ -316,39 +275,26 @@ % Since spktimes are the time of the peak of each spike, we subtract 15 % from spktimes to obtain the start times of the spikes - if mod(length(data_deligned),2)~=0 - - data_deligned_temp = [data_deligned;0]; - g = fitLFPpowerSpectrum(data_deligned_temp,filterBounds(1),filterBounds(2),sFile.prop.sfreq); - S = zeros(length(data_deligned_temp),1); + if mod(length(data),2)~=0 + data_temp = [data 0]'; + g = fitLFPpowerSpectrum(data_temp,BandPass(1),BandPass(2),sFile.prop.sfreq); + S = zeros(length(data_temp),1); iSpk = round(spkSamples - nSegment/2); iSpk = iSpk(iSpk > 0); % Only keep positive indices S(iSpk) = 1; % This assumes the spike starts at 1/2 before the trough of the spike - data_derived = despikeLFP(data_deligned_temp,S,Bs,g,opts); - data_derived = data_derived.z'; - data_derived = bst_bandpass_hfilter(data_derived, Fs, filterBounds(1), filterBounds(2), 0, 0); - - + data_derived = despikeLFP(data_temp,S,Bs,g,opts); + data_derived = data_derived.z; else - g = fitLFPpowerSpectrum(data_deligned,filterBounds(1),filterBounds(2),sFile.prop.sfreq); - S = zeros(length(data_deligned),1); + g = fitLFPpowerSpectrum(data,BandPass(1),BandPass(2),sFile.prop.sfreq); + S = zeros(length(data),1); iSpk = round(spkSamples - nSegment/2); iSpk = iSpk(iSpk > 0); % Only keep positive indices S(iSpk) = 1; % This assumes the spike starts at 1/2 before the trough of the spike - data_derived = despikeLFP(data_deligned,S,Bs,g,opts); - data_derived = data_derived.z'; - data_derived = bst_bandpass_hfilter(data_derived, Fs, filterBounds(1), filterBounds(2), 0, 0); - - + data_derived = despikeLFP(data,S,Bs,g,opts); + data_derived = data_derived.z; end else - data_derived = data_deligned'; - data_derived = bst_bandpass_hfilter(data_derived, Fs, filterBounds(1), filterBounds(2), 0, 0); + data_derived = data; end - - data_derived = downsample(data_derived, round(sMat.sr/1000)); % The file now has a different sampling rate (fs/30) = 1000Hz - end - - diff --git a/toolbox/process/functions/process_noise_correlation.m b/toolbox/process/functions/process_noise_correlation.m index f04c1c6842..33f3c723a4 100644 --- a/toolbox/process/functions/process_noise_correlation.m +++ b/toolbox/process/functions/process_noise_correlation.m @@ -23,154 +23,98 @@ % =============================================================================@ % % Authors: Konstantinos Nasiotis, 2018 +% Francois Tadel, 2022 eval(macro_method); end %% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description the process - sProcess.Comment = 'Noise Correlation'; + sProcess.Comment = 'Noise correlation'; sProcess.FileTag = 'NoiseCorrelation'; - sProcess.Category = 'custom'; + sProcess.Category = 'Custom'; sProcess.SubGroup = 'Electrophysiology'; - sProcess.Index = 1508; - sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Noise_Correlation'; + sProcess.Index = 1215; + sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Noise_correlation'; % Definition of the input accepted by this process sProcess.InputTypes = {'data'}; sProcess.OutputTypes = {'timefreq'}; sProcess.nInputs = 1; - sProcess.nMinFiles = 1; - % === Sensor types - sProcess.options.sensortypes.Comment = 'Sensor types or names (empty=all): '; - sProcess.options.sensortypes.Type = 'text'; - sProcess.options.sensortypes.Value = 'EEG'; + sProcess.nMinFiles = 2; % Time window sProcess.options.timewindow.Comment = 'Time window:'; sProcess.options.timewindow.Type = 'range'; - sProcess.options.timewindow.Value = {[0, 0.200],'ms',[]}; + sProcess.options.timewindow.Value = {[0, 0.200],'ms',[]}; end %% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok +function Comment = FormatComment(sProcess) Comment = sProcess.Comment; end %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInputs) % Initialize returned values OutputFiles = {}; - % Extract method name from the process name - strProcess = strrep(strrep(func2str(sProcess.Function), 'process_', ''), 'data', ''); + % Get options + TimeWindow = sProcess.options.timewindow.Value{1}; - tfOPTIONS.Method = strProcess; - % Add other options - - if isfield(sProcess.options, 'sensortypes') - tfOPTIONS.SensorTypes = sProcess.options.sensortypes.Value; - else - tfOPTIONS.SensorTypes = []; - end - - % Time window - if isfield(sProcess.options, 'timewindow') && ~isempty(sProcess.options.timewindow) && ~isempty(sProcess.options.timewindow.Value)... - && iscell(sProcess.options.timewindow.Value) && (sProcess.options.timewindow.Value{1}(1) < sProcess.options.timewindow.Value{1}(2)) - time_window = sProcess.options.timewindow.Value{1}; - else - bst_report('Error', sProcess, sInputs, 'Check window inputs'); - return; - end - - tfOPTIONS.TimeVector = in_bst(sInputs(1).FileName, 'Time'); - - - % === OUTPUT STUDY === - % Get output study - [tmp, iStudy] = bst_process('GetOutputStudy', sProcess, sInputs); - tfOPTIONS.iTargetStudy = iStudy; - - - % Get channel file - sChannel = bst_get('ChannelForStudy', iStudy); - % Load channel file - ChannelMat = in_bst_channel(sChannel.FileName); - - - %% Get only the unique neurons along all of the trials - nTrials = length(sInputs); - - if nTrials == 1 - bst_report('Error', sProcess, sInputs, 'More trials are needed for Noise Correlation computation.'); - return; - end - - % This loads the information from ALL TRIALS on ALL_TRIALS_files - % (Shouldn't create a memory problem). - ALL_TRIALS_files = struct(); - for iFile = 1:nTrials - DataMat = in_bst(sInputs(iFile).FileName); - ALL_TRIALS_files(iFile).Events = DataMat.Events; - end - - %% Create a cell that holds all of the labels and one for the unique labels - % This will be used to take the averages using the appropriate indices - uniqueNeurons = {}; % Unique neuron labels (each trial might have different number of neurons). We need everything that appears. - for iFile = 1:nTrials - for iEvent = 1:length(ALL_TRIALS_files(iFile).Events) - if process_spikesorting_supervised('IsSpikeEvent', ALL_TRIALS_files(iFile).Events(iEvent).label) && any(ALL_TRIALS_files(iFile).Events(iEvent).times > time_window(1) & ALL_TRIALS_files(iFile).Events(iEvent).times < time_window(2)) - uniqueNeurons{end+1} = ALL_TRIALS_files(iFile).Events(iEvent).label; + % ===== LOAD INPUTS ===== + % Loads the events information, and get the list of unique neurons + DataMats = cell(1, length(sInputs)); + uniqueNeurons = {}; % Unique neuron labels (each trial might have different number of neurons). We need everything that appears. + for iFile = 1:length(sInputs) + % Load file + DataMats{iFile} = in_bst_data(sInputs(iFile).FileName, 'Events'); + % Find unique neurons + for iEvent = 1:length(DataMats{iFile}.Events) + if panel_spikes('IsSpikeEvent', DataMats{iFile}.Events(iEvent).label) && any(DataMats{iFile}.Events(iEvent).times > TimeWindow(1) & DataMats{iFile}.Events(iEvent).times < TimeWindow(2)) + uniqueNeurons{end+1} = DataMats{iFile}.Events(iEvent).label; end end end - uniqueNeurons = unique(uniqueNeurons,'stable'); - - - %% Sort the neurons based on the array they belong to. + % If no neuron was found + if isempty(uniqueNeurons) + bst_report('Error', sProcess, sCurrentInputs(1), 'No neurons/spiking events detected.'); + return; + end + % Sort the neurons based on the array they belong to. % The visualization is greatly affected by the order of the neurons. + uniqueNeurons = unique(uniqueNeurons, 'stable'); uniqueNeurons = sort_nat(uniqueNeurons); + - - %% === START COMPUTATION === - protocol = bst_get('ProtocolInfo'); - - - %% Gather the spikes + % ===== CORRELATION COMPUTATION ===== + % Gather the spikes all_binned = zeros(length(sInputs), length(uniqueNeurons)); for iFile = 1:length(sInputs) - - trial = load(fullfile(protocol.STUDIES, sInputs(iFile).FileName), 'Events'); - for iNeuron = 1:length(uniqueNeurons) - for iEvent = 1:length(trial.Events) - - if strcmp(trial.Events(iEvent).label, uniqueNeurons{iNeuron}) - all_binned(iFile, iNeuron) = length(trial.Events(iEvent).times(trial.Events(iEvent).times > time_window(1) & trial.Events(iEvent).times < time_window(2))); + for iEvent = 1:length(DataMats{iFile}.Events) + evt = DataMats{iFile}.Events(iEvent); + if strcmp(evt.label, uniqueNeurons{iNeuron}) + all_binned(iFile, iNeuron) = length(evt.times( (evt.times > TimeWindow(1)) & (evt.times < TimeWindow(2)) )); break end - end end - end - - %% Subtract mean from each neuron (this is needed for noise correlation) + % Subtract mean from each neuron (this is needed for noise correlation) all_binned = all_binned - repmat(mean(all_binned), size(all_binned,1),1); - - - %% Compute the Correlation for nxn Neurons + + % Compute the Correlation for nxn Neurons noise_correlation = zeros(1,size(all_binned, 2), size(all_binned, 2)); - opts.normalize = true; opts.nTrials = 1; opts.flagStatistics = 0; connectivity = bst_correlation(all_binned', all_binned', opts); noise_correlation(1,:,:) = connectivity; - %% Get list of unique conditions for output label + % Get list of unique conditions for output label conditions = unique({sInputs.Condition}); condition = []; for iCond = 1:length(conditions) @@ -180,43 +124,36 @@ condition = [condition conditions{iCond}]; end - %% Build the output file - - tfOPTIONS.ParentFiles = {sInputs.FileName}; + % ===== SAVE FILE ===== % Prepare output file structure - FileMat = db_template('timefreqmat'); - FileMat.TF = noise_correlation; - FileMat.Time = 1:length(uniqueNeurons); - FileMat.TFmask = true(size(noise_correlation, 2), size(noise_correlation, 3)); - FileMat.Freqs = 1:size(FileMat.TF, 3); - FileMat.Comment = ['Noise Correlation: ' condition]; - FileMat.DataType = 'data'; - FileMat.RowNames = {'nxn Noise Correlation'}; - FileMat.NeuronNames = uniqueNeurons; - FileMat.Measure = 'power'; - FileMat.Method = 'morlet'; - FileMat.DataFile = []; % Leave blank because multiple parents - FileMat.Options = tfOPTIONS; - + TfMat = db_template('timefreqmat'); + TfMat.TF = noise_correlation; + TfMat.Time = 1:length(uniqueNeurons); + TfMat.TFmask = true(size(noise_correlation, 2), size(noise_correlation, 3)); + TfMat.Freqs = 1:size(TfMat.TF, 3); + TfMat.Comment = ['Noise Correlation: ' condition]; + TfMat.DataType = 'data'; + TfMat.RowNames = {'NxN Noise Correlation'}; + TfMat.NeuronNames = uniqueNeurons; + TfMat.Measure = 'power'; + TfMat.Method = 'morlet'; + TfMat.DataFile = []; % Leave blank because multiple parents + TfMat.Options = []; % Add history field - FileMat = bst_history('add', FileMat, 'compute', ... - ['Noise correlation: [' num2str(time_window(1)) ', ' num2str(time_window(2)) '] ms']); + TfMat = bst_history('add', TfMat, 'compute', ['Noise correlation: [' num2str(TimeWindow(1)) ', ' num2str(TimeWindow(2)) '] ms']); + % History: List files + TfMat = bst_history('add', TfMat, 'noise_correlation', 'List of input files:'); + for iFile = 1:length(sInputs) + TfMat = bst_history('add', TfMat, 'average', [' - ' sInputs(iFile).FileName]); + end % Get output study - sTargetStudy = bst_get('Study', iStudy); + [tmp, iTargetStudy] = bst_process('GetOutputStudy', sProcess, sInputs); + sTargetStudy = bst_get('Study', iTargetStudy); % Output filename - FileName = bst_process('GetNewFilename', bst_fileparts(sTargetStudy.FileName), 'timefreq_noise_correlation'); - OutputFiles = {FileName}; + OutputFiles{1} = bst_process('GetNewFilename', bst_fileparts(sTargetStudy.FileName), 'timefreq_noise_correlation'); % Save output file and add to database - bst_save(FileName, FileMat, 'v6'); - db_add_data(tfOPTIONS.iTargetStudy, FileName, FileMat); - - % Display report to user - bst_report('Info', sProcess, sInputs, 'Success'); - disp('BST> process_noise_Correlation: Success'); + bst_save(OutputFiles{1}, TfMat, 'v6'); + db_add_data(iTargetStudy, OutputFiles{1}, TfMat); end - - - - diff --git a/toolbox/process/functions/process_psth_per_channel.m b/toolbox/process/functions/process_psth_per_channel.m new file mode 100644 index 0000000000..5d692fc0e1 --- /dev/null +++ b/toolbox/process/functions/process_psth_per_channel.m @@ -0,0 +1,145 @@ +function varargout = process_psth_per_channel( varargin ) +% PROCESS_PSTH_PER_CHANNEL: Computes the PSTH per channel. + +% It displays the binned firing rate on each channel (of only the first +% neuron on each channel if multiple have been detected). This can be nicely +% visualized on the cortical surface if the positions of the electrodes +% have been set, and show real time firing rate. + +% @============================================================================= +% This function is part of the Brainstorm software: +% https://neuroimage.usc.edu/brainstorm +% +% Copyright (c) University of Southern California & McGill University +% This software is distributed under the terms of the GNU General Public License +% as published by the Free Software Foundation. Further details on the GPLv3 +% license can be found at http://www.gnu.org/copyleft/gpl.html. +% +% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE +% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY +% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY +% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. +% +% For more information type "brainstorm license" at command prompt. +% =============================================================================@ +% +% Authors: Konstantinos Nasiotis, 2018-2019 +% Francois Tadel, 2022 + +eval(macro_method); +end + + +%% ===== GET DESCRIPTION ===== +function sProcess = GetDescription() + % Description the process + sProcess.Comment = 'PSTH per channel'; + sProcess.FileTag = 'raster'; + sProcess.Category = 'File'; + sProcess.SubGroup = 'Electrophysiology'; + sProcess.Index = 1229; + sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions'; + % Definition of the input accepted by this process + sProcess.InputTypes = {'data'}; + sProcess.OutputTypes = {'data'}; + sProcess.nInputs = 1; + sProcess.nMinFiles = 1; + % Options: Bin size + sProcess.options.binsize.Comment = 'Bin size: '; + sProcess.options.binsize.Type = 'value'; + sProcess.options.binsize.Value = {0.05, 'ms', 1}; +end + + +%% ===== FORMAT COMMENT ===== +function Comment = FormatComment(sProcess) + Comment = sProcess.Comment; +end + + +%% ===== RUN ===== +function OutputFiles = Run(sProcess, sInput) + % Initialize returned values + OutputFiles = {}; + + % ==== OPTIONS ===== + % Bin size + if isfield(sProcess.options, 'binsize') && ~isempty(sProcess.options.binsize) && ~isempty(sProcess.options.binsize.Value) && iscell(sProcess.options.binsize.Value) && sProcess.options.binsize.Value{1} > 0 + bin_size = sProcess.options.binsize.Value{1}; + else + bst_report('Error', sProcess, sInput, 'Positive bin size required.'); + return; + end + + % ===== LOAD INPUT FILES ===== + % Load data file + DataMat = in_bst_data(sInput.FileName, 'Time', 'Events', 'Comment', 'Device', 'ChannelFlag', 'History'); + sampling_rate = round(abs(1. / (DataMat.Time(2) - DataMat.Time(1)))); + % Load channel file + ChannelMat = in_bst_channel(sInput.ChannelFile); + + % ===== COMPUTE BINNING ===== + % Define bins + nBins = floor(length(DataMat.Time) / (bin_size * sampling_rate)); + bins = linspace(DataMat.Time(1), DataMat.Time(end), nBins+1); + single_file_binning = zeros(length(ChannelMat.Channel), nBins); + % Process channel by channel + for iChan = 1:length(ChannelMat.Channel) + for iEvent = 1:size(DataMat.Events,2) + + % Bin ONLY THE FIRST NEURON'S SPIKES if there are multiple neurons! + if panel_spikes('IsSpikeEvent', DataMat.Events(iEvent).label) ... + && panel_spikes('IsFirstNeuron', DataMat.Events(iEvent).label) ... + && strcmp(ChannelMat.Channel(iChan).Name, panel_spikes('GetChannelOfSpikeEvent', DataMat.Events(iEvent).label)) + + outside_up = DataMat.Events(iEvent).times >= bins(end); % This snippet takes care of some spikes that occur outside of the window of Time due to precision incompatibility. + DataMat.Events(iEvent).times(outside_up) = bins(end) - 0.001; % Make sure it is inside the bin. Add 1ms offset + outside_down = DataMat.Events(iEvent).times <= bins(1); + DataMat.Events(iEvent).times(outside_down) = bins(1) + 0.001; % Make sure it is inside the bin. Add 1ms offset + + [tmp, bin_it_belongs_to] = histc(DataMat.Events(iEvent).times, bins); + + unique_bin = unique(bin_it_belongs_to); + occurences = [unique_bin; histc(bin_it_belongs_to, unique_bin)]; + + single_file_binning(iChan,occurences(1,:)) = occurences(2,:)/bin_size; % The division by the bin_size gives the Firing Rate + break + end + end + + end + + % Events have to be converted to the sampling rate of the binning + convertedEvents = DataMat.Events; + for iEvent = 1:length(DataMat.Events) + [tmp, bin_it_belongs_to] = histc(DataMat.Events(iEvent).times, bins); + bin_it_belongs_to(bin_it_belongs_to==0) = 1; + convertedEvents(iEvent).times = bins(bin_it_belongs_to); + end + Events = convertedEvents; + + + % ===== SAVE RESULTS ===== + % Prepare output file structure + FileMat = db_template('datamat'); + FileMat.F = single_file_binning; + FileMat.Time = diff(bins(1:2))/2+bins(1:end-1); + FileMat.Comment = ['PSTH: ' DataMat.Comment]; + FileMat.DataType = 'recordings'; + FileMat.ChannelFlag = DataMat.ChannelFlag; + FileMat.Device = DataMat.Device; + FileMat.Events = Events; + FileMat.nAvg = 1; + FileMat.History = DataMat.History; + + % Add history field + FileMat = bst_history('add', FileMat, 'ptsh', ['PSTH per electrode: ' num2str(bin_size) ' ms']); + FileMat = bst_history('add', FileMat, 'ptsh', ['Input file: ' sInput.FileName]); + % Output filename + FileName = bst_process('GetNewFilename', bst_fileparts(sInput.FileName), 'data_psth'); + OutputFiles = {FileName}; + % Save output file and add to database + bst_save(FileName, FileMat, 'v6'); + db_add_data(sInput.iStudy, FileName, FileMat); +end diff --git a/toolbox/process/functions/process_psth_per_electrode.m b/toolbox/process/functions/process_psth_per_electrode.m deleted file mode 100644 index 729df74b1a..0000000000 --- a/toolbox/process/functions/process_psth_per_electrode.m +++ /dev/null @@ -1,211 +0,0 @@ -function varargout = process_psth_per_electrode( varargin ) -% PROCESS_PSTH_PER_ELECTRODE: Computes the PSTH per electrode. - -% It displays the binned firing rate on each electrode (of only the first -% neuron on each electrode if multiple have been detected). This can be nicely -% visualized on the cortical surface if the positions of the electrodes -% have been set, and show real time firing rate. -% -% USAGE: sProcess = process_PSTH_per_electrode('GetDescription') -% OutputFiles = process_PSTH_per_electrode('Run', sProcess, sInput) - -% @============================================================================= -% This function is part of the Brainstorm software: -% https://neuroimage.usc.edu/brainstorm -% -% Copyright (c) University of Southern California & McGill University -% This software is distributed under the terms of the GNU General Public License -% as published by the Free Software Foundation. Further details on the GPLv3 -% license can be found at http://www.gnu.org/copyleft/gpl.html. -% -% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE -% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY -% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF -% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY -% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. -% -% For more information type "brainstorm license" at command prompt. -% =============================================================================@ -% -% Author: Konstantinos Nasiotis, 2018-2019; - -eval(macro_method); -end - - -%% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok - % Description the process - sProcess.Comment = 'PSTH Per Electrode'; - sProcess.FileTag = 'raster'; - sProcess.Category = 'custom'; - sProcess.SubGroup = 'Electrophysiology'; - sProcess.Index = 1505; - sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Raster_Plots'; - % Definition of the input accepted by this process - sProcess.InputTypes = {'data'}; - sProcess.OutputTypes = {'data'}; - sProcess.nInputs = 1; - sProcess.nMinFiles = 1; - % Options: Sensor types - sProcess.options.sensortypes.Comment = 'Sensor types or names (empty=all): '; - sProcess.options.sensortypes.Type = 'text'; - sProcess.options.sensortypes.Value = 'EEG'; - % Options: Bin size - sProcess.options.binsize.Comment = 'Bin size: '; - sProcess.options.binsize.Type = 'value'; - sProcess.options.binsize.Value = {0.05, 'ms', 1}; -end - - -%% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok - Comment = sProcess.Comment; -end - - -%% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok - % Initialize returned values - OutputFiles = {}; - % Extract method name from the process name - strProcess = strrep(strrep(func2str(sProcess.Function), 'process_', ''), 'data', ''); - - tfOPTIONS.Method = strProcess; - % Add other options - - if isfield(sProcess.options, 'sensortypes') - tfOPTIONS.SensorTypes = sProcess.options.sensortypes.Value; - else - tfOPTIONS.SensorTypes = []; - end - - % Bin size - if isfield(sProcess.options, 'binsize') && ~isempty(sProcess.options.binsize) && ~isempty(sProcess.options.binsize.Value) && iscell(sProcess.options.binsize.Value) && sProcess.options.binsize.Value{1} > 0 - bin_size = sProcess.options.binsize.Value{1}; - else - bst_report('Error', sProcess, sInputs, 'Positive bin size required.'); - return; - end - - % If a time window was specified - if isfield(sProcess.options, 'timewindow') && ~isempty(sProcess.options.timewindow) && ~isempty(sProcess.options.timewindow.Value) && iscell(sProcess.options.timewindow.Value) - tfOPTIONS.TimeWindow = sProcess.options.timewindow.Value{1}; - elseif ~isfield(tfOPTIONS, 'TimeWindow') - tfOPTIONS.TimeWindow = []; - end - - tfOPTIONS.TimeVector = in_bst(sInputs(1).FileName, 'Time'); - - - % === OUTPUT STUDY === - % Get output study - [tmp, iStudy] = bst_process('GetOutputStudy', sProcess, sInputs); - tfOPTIONS.iTargetStudy = iStudy; - - - % Get channel file - sChannel = bst_get('ChannelForStudy', iStudy); - % Load channel file - ChannelMat = in_bst_channel(sChannel.FileName); - - % === START COMPUTATION === - sampling_rate = round(abs(1. / (tfOPTIONS.TimeVector(2) - tfOPTIONS.TimeVector(1)))); - - temp = in_bst(sInputs(1).FileName); - nElectrodes = size(temp.ChannelFlag,1); - nTrials = length(sInputs); - nBins = floor(length(tfOPTIONS.TimeVector) / (bin_size * sampling_rate)); - bins = linspace(temp.Time(1), temp.Time(end), nBins+1); - - for ifile = 1:length(sInputs) - trial = in_bst(sInputs(ifile).FileName); - single_file_binning = zeros(nElectrodes, nBins); - - for ielectrode = 1:size(trial.F,1) - for ievent = 1:size(trial.Events,2) - - % Bin ONLY THE FIRST NEURON'S SPIKES if there are multiple neurons! - if process_spikesorting_supervised('IsSpikeEvent', trial.Events(ievent).label) ... - && process_spikesorting_supervised('IsFirstNeuron', trial.Events(ievent).label) ... - && strcmp(ChannelMat.Channel(ielectrode).Name, process_spikesorting_supervised('GetChannelOfSpikeEvent', trial.Events(ievent).label)) - - outside_up = trial.Events(ievent).times >= bins(end); % This snippet takes care of some spikes that occur outside of the window of Time due to precision incompatibility. - trial.Events(ievent).times(outside_up) = bins(end) - 0.001; % Make sure it is inside the bin. Add 1ms offset - outside_down = trial.Events(ievent).times <= bins(1); - trial.Events(ievent).times(outside_down) = bins(1) + 0.001; % Make sure it is inside the bin. Add 1ms offset - - [tmp, bin_it_belongs_to] = histc(trial.Events(ievent).times, bins); - - unique_bin = unique(bin_it_belongs_to); - occurences = [unique_bin; histc(bin_it_belongs_to, unique_bin)]; - - single_file_binning(ielectrode,occurences(1,:)) = occurences(2,:)/bin_size; % The division by the bin_size gives the Firing Rate - break - end - end - - end - - - % Events have to be converted to the sampling rate of the binning - convertedEvents = trial.Events; - - for iEvent = 1:length(trial.Events) - [tmp, bin_it_belongs_to] = histc(trial.Events(iEvent).times, bins); - - bin_it_belongs_to(bin_it_belongs_to==0) = 1; - convertedEvents(iEvent).times = bins(bin_it_belongs_to); - - end - Events = convertedEvents; - - - - - %% - tfOPTIONS.ParentFiles = {sInputs.FileName}; - - % Prepare output file structure - FileMat.F = single_file_binning; - FileMat.Time = diff(bins(1:2))/2+bins(1:end-1); - - FileMat.Std = []; - FileMat.Comment = ['PSTH: ' trial.Comment]; - FileMat.DataType = 'recordings'; - - FileMat.ChannelFlag = temp.ChannelFlag; - FileMat.Device = trial.Device; - FileMat.Events = Events; - - FileMat.nAvg = 1; - FileMat.ColormapType = []; - FileMat.DisplayUnits = []; - FileMat.History = trial.History; - - % Add history field - FileMat = bst_history('add', FileMat, 'compute', ... - ['PSTH per electrode: ' num2str(bin_size) ' ms']); - - % Get output study - sTargetStudy = bst_get('Study', iStudy); - % Output filename - FileName = bst_process('GetNewFilename', bst_fileparts(sTargetStudy.FileName), 'data_psth'); - OutputFiles = {FileName}; - % Save output file and add to database - bst_save(FileName, FileMat, 'v6'); - db_add_data(tfOPTIONS.iTargetStudy, FileName, FileMat); - - end - - - - - % Display report to user - bst_report('Info', sProcess, sInputs, 'Success'); - disp('BST> process_timefreq: Success'); -end - - - - diff --git a/toolbox/process/functions/process_psth_per_neuron.m b/toolbox/process/functions/process_psth_per_neuron.m index 0157fc7d15..a54623cc7d 100644 --- a/toolbox/process/functions/process_psth_per_neuron.m +++ b/toolbox/process/functions/process_psth_per_neuron.m @@ -1,8 +1,5 @@ function varargout = process_psth_per_neuron( varargin ) -% PROCESS_RASTERPLOT_PER_NEURON: Computes a rasterplot per electrode. -% -% USAGE: sProcess = process_rasterplot_per_neuron('GetDescription') -% OutputFiles = process_rasterplot_per_neuron('Run', sProcess, sInput) +% PROCESS_PSTH_PER_NEURON: Computes the PSTH (peristimulus time histogram) per neuron. % @============================================================================= % This function is part of the Brainstorm software: @@ -22,30 +19,27 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Author: Konstantinos Nasiotis, 2019; +% Authors: Konstantinos Nasiotis, 2019 +% Francois Tadel, 2022 eval(macro_method); end %% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description the process - sProcess.Comment = 'PSTH Per Neuron'; + sProcess.Comment = 'PSTH per neuron'; sProcess.FileTag = 'psth'; - sProcess.Category = 'custom'; + sProcess.Category = 'Custom'; sProcess.SubGroup = 'Electrophysiology'; - sProcess.Index = 1507; - sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/'; + sProcess.Index = 1227; + sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions'; % Definition of the input accepted by this process sProcess.InputTypes = {'data'}; sProcess.OutputTypes = {'data'}; sProcess.nInputs = 1; sProcess.nMinFiles = 1; - % Options: Sensor types - sProcess.options.sensortypes.Comment = 'Sensor types or names (empty=all): '; - sProcess.options.sensortypes.Type = 'text'; - sProcess.options.sensortypes.Value = 'EEG'; % Options: Bin size sProcess.options.binsize.Comment = 'Bin size: '; sProcess.options.binsize.Type = 'value'; @@ -54,116 +48,82 @@ %% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok +function Comment = FormatComment(sProcess) Comment = sProcess.Comment; end %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInputs) % Initialize returned values OutputFiles = {}; - % Extract method name from the process name - strProcess = strrep(strrep(func2str(sProcess.Function), 'process_', ''), 'data', ''); - - % Add other options - tfOPTIONS.Method = strProcess; - if isfield(sProcess.options, 'sensortypes') - tfOPTIONS.SensorTypes = sProcess.options.sensortypes.Value; - else - tfOPTIONS.SensorTypes = []; - end - - % Bin size + % Get options if isfield(sProcess.options, 'binsize') && ~isempty(sProcess.options.binsize) && ~isempty(sProcess.options.binsize.Value) && iscell(sProcess.options.binsize.Value) && sProcess.options.binsize.Value{1} > 0 bin_size = sProcess.options.binsize.Value{1}; else bst_report('Error', sProcess, sInputs, 'Positive bin size required.'); return; end - - % If a time window was specified - if isfield(sProcess.options, 'timewindow') && ~isempty(sProcess.options.timewindow) && ~isempty(sProcess.options.timewindow.Value) && iscell(sProcess.options.timewindow.Value) - tfOPTIONS.TimeWindow = sProcess.options.timewindow.Value{1}; - elseif ~isfield(tfOPTIONS, 'TimeWindow') - tfOPTIONS.TimeWindow = []; - end - - tfOPTIONS.TimeVector = in_bst(sInputs(1).FileName, 'Time'); + % ===== PROCESS FILES ===== % Check how many event groups we're processing listComments = cellfun(@str_remove_parenth, {sInputs.Comment}, 'UniformOutput', 0); [uniqueComments,tmp,iData2List] = unique(listComments); nLists = length(uniqueComments); - % Process each even group seperately for iList = 1:nLists - sCurrentInputs = sInputs(iData2List == iList); - - % === OUTPUT STUDY === - % Get output study - [tmp, iStudy] = bst_process('GetOutputStudy', sProcess, sCurrentInputs); - tfOPTIONS.iTargetStudy = iStudy; - - - % Get channel file - sChannel = bst_get('ChannelForStudy', iStudy); - % Load channel file - ChannelMat = in_bst_channel(sChannel.FileName); - - %% Get only the unique neurons along all of the trials + % === LOAD INPUTS === + % Get trials in this group + sCurrentInputs = sInputs(iData2List == iList); nTrials = length(sCurrentInputs); - % I get the files outside of the parfor so it won't fail. - % This loads the information from ALL TRIALS on ALL_TRIALS_files - % (Shouldn't create a memory problem). - ALL_TRIALS_files = struct(); + % Get all the neuron labels (each trial might have different number of neurons) + DataMats = cell(1, nTrials); + labelsNeurons = {}; for iFile = 1:nTrials - DataMat = in_bst(sCurrentInputs(iFile).FileName); - ALL_TRIALS_files(iFile).Events = DataMat.Events; - end - - % Create a cell that holds all of the labels and one for the unique labels - % This will be used to take the averages using the appropriate indices - labelsForDropDownMenu = {}; % Unique neuron labels (each trial might have different number of neurons). We need everything that appears. - for iFile = 1:nTrials - for iEvent = 1:length(ALL_TRIALS_files(iFile).Events) - if process_spikesorting_supervised('IsSpikeEvent', ALL_TRIALS_files(iFile).Events(iEvent).label) - labelsForDropDownMenu{end+1} = ALL_TRIALS_files(iFile).Events(iEvent).label; + % Load file + DataMats{iFile} = in_bst_data(sCurrentInputs(iFile).FileName, 'Events'); + % Save spike events + for iEvent = 1:length(DataMats{iFile}.Events) + if panel_spikes('IsSpikeEvent', DataMats{iFile}.Events(iEvent).label) + labelsNeurons{end+1} = DataMats{iFile}.Events(iEvent).label; end end end - labelsForDropDownMenu = unique(labelsForDropDownMenu,'stable'); - labelsForDropDownMenu = sort_nat(labelsForDropDownMenu); + % If no neuron was found + if isempty(labelsNeurons) + bst_report('Error', sProcess, sCurrentInputs(1), 'No neurons/spiking events detected.'); + return; + end + % Sort neurons alphabetically + labelsNeurons = unique(labelsNeurons, 'stable'); + labelsNeurons = sort_nat(labelsNeurons); - %% === START COMPUTATION === - sampling_rate = round(abs(1. / (tfOPTIONS.TimeVector(2) - tfOPTIONS.TimeVector(1)))); - - temp = in_bst(sCurrentInputs(1).FileName); - nElectrodes = size(temp.ChannelFlag,1); - nBins = floor(length(tfOPTIONS.TimeVector) / (bin_size * sampling_rate)); - raster = zeros(length(labelsForDropDownMenu), nBins, nTrials); - bins = linspace(temp.Time(1), temp.Time(end), nBins+1); + % ===== COMPUTE BINNING ===== + % Get file time + DataMat = in_bst_data(sCurrentInputs(1).FileName, 'Time'); + sampling_rate = round(abs(1. / (DataMat.Time(2) - DataMat.Time(1)))); + % Define bins + nBins = floor(length(DataMat.Time) / (bin_size * sampling_rate)); + raster = zeros(length(labelsNeurons), nBins, nTrials); + bins = linspace(DataMat.Time(1), DataMat.Time(end), nBins+1); bst_progress('start', 'PSTH per Neuron', 'Binning Spikes...', 0, length(sCurrentInputs)); + for iFile = 1:length(sCurrentInputs) + single_file_binning = zeros(length(labelsNeurons), nBins); + for iNeuron = 1:length(labelsNeurons) + for ievent = 1:size(DataMats{iFile}.Events,2) + evt = DataMats{iFile}.Events(ievent); + if strcmp(evt.label, labelsNeurons{iNeuron}) + outside_up = evt.times >= bins(end); % This snippet takes care of some spikes that occur outside of the window of Time due to precision incompatibility. + evt.times(outside_up) = bins(end) - 0.001; % I assign those spikes just 1ms inside the bin + outside_down = evt.times <= bins(1); + evt.times(outside_down) = bins(1) + 0.001; % I assign those spikes just 1ms inside the bin - for ifile = 1:length(sCurrentInputs) - trial = in_bst(sCurrentInputs(ifile).FileName); - single_file_binning = zeros(length(labelsForDropDownMenu), nBins); - - for iNeuron = 1:length(labelsForDropDownMenu) - for ievent = 1:size(trial.Events,2) - if strcmp(trial.Events(ievent).label, labelsForDropDownMenu{iNeuron}) - - outside_up = trial.Events(ievent).times >= bins(end); % This snippet takes care of some spikes that occur outside of the window of Time due to precision incompatibility. - trial.Events(ievent).times(outside_up) = bins(end) - 0.001; % I assign those spikes just 1ms inside the bin - outside_down = trial.Events(ievent).times <= bins(1); - trial.Events(ievent).times(outside_down) = bins(1) + 0.001; % I assign those spikes just 1ms inside the bin - - [tmp, bin_it_belongs_to] = histc(trial.Events(ievent).times, bins); + [tmp, bin_it_belongs_to] = histc(evt.times, bins); unique_bin = unique(bin_it_belongs_to); occurences = [unique_bin; histc(bin_it_belongs_to, unique_bin)]; @@ -173,25 +133,19 @@ end end end - - raster(:, :, ifile) = single_file_binning; + raster(:, :, iFile) = single_file_binning; bst_progress('inc', 1); end - - %% Compute the 95% confidence intervals + % ===== COMPUTE 95% CONFIDENCE INTERVALS ===== % Initialize the 3 vectors that will be plotted (mean, and 95% confidence intervals) - meanData = zeros(length(labelsForDropDownMenu), nBins); % nNeurons x nBins - CI = zeros(length(labelsForDropDownMenu), nBins, 1, 2); % nNeurons x nBins x 1 (unused STD dimension) x 2 (upper-lower bound) + meanData = zeros(length(labelsNeurons), nBins); % nNeurons x nBins + CI = zeros(length(labelsNeurons), nBins, 1, 2); % nNeurons x nBins x 1 (unused STD dimension) x 2 (upper-lower bound) - - bst_progress('start', 'PSTH per Neuron', 'Performing permutation test for 95% confidence intervals...', 0, length(labelsForDropDownMenu)); + bst_progress('start', 'PSTH per Neuron', 'Performing permutation test for 95% confidence intervals...', 0, length(labelsNeurons)); % Assign the confidence intervals values - for iNeuron = 1:length(labelsForDropDownMenu) - - disp([num2str(iNeuron) '/' num2str(length(labelsForDropDownMenu)) ' done']) - + for iNeuron = 1:length(labelsNeurons) for iBin = 1:nBins meanData(iNeuron, iBin) = mean(raster(iNeuron,iBin,:)); @@ -203,42 +157,32 @@ end - - %% Build the output file - tfOPTIONS.ParentFiles = {sCurrentInputs.FileName}; - + % ===== SAVE RESULTS ===== % Prepare output file structure - FileMat.Value = meanData; - FileMat.Std = CI; - FileMat.Description = labelsForDropDownMenu'; - FileMat.Time = diff(bins(1:2))/2+bins(1:end-1); - FileMat.ChannelFlag = ones(length(labelsForDropDownMenu),1); - FileMat.nAvg = 1; - FileMat.Events = []; - FileMat.SurfaceFile = []; - FileMat.Atlas = []; - FileMat.DisplayUnits = 'Spikes/sec'; + TfMat = db_template('timefreqmat'); + TfMat.Value = meanData; + TfMat.Std = CI; + TfMat.Comment = ['PSTH: ' uniqueComments{iList}]; + TfMat.Description = labelsNeurons'; + TfMat.Time = diff(bins(1:2))/2+bins(1:end-1); + TfMat.ChannelFlag = ones(length(labelsNeurons),1); + TfMat.nAvg = 1; + TfMat.DisplayUnits = 'Spikes/sec'; % Add history field - FileMat = bst_history('add', FileMat, 'compute', 'PSTH per neuron'); - FileMat.Comment = ['PSTH: ' uniqueComments{iList}]; + TfMat = bst_history('add', TfMat, 'compute', 'PSTH per neuron'); + for iFile = 1:length(sInputs) + TfMat = bst_history('add', TfMat, 'average', [' - ' sInputs(iFile).FileName]); + end % Get output study - sTargetStudy = bst_get('Study', iStudy); + [tmp, iTargetStudy] = bst_process('GetOutputStudy', sProcess, sCurrentInputs); + sTargetStudy = bst_get('Study', iTargetStudy); % Output filename FileName = bst_process('GetNewFilename', bst_fileparts(sTargetStudy.FileName), 'matrix'); OutputFiles = {FileName}; % Save output file and add to database - bst_save(FileName, FileMat, 'v6'); - db_add_data(tfOPTIONS.iTargetStudy, FileName, FileMat); + bst_save(FileName, TfMat, 'v6'); + db_add_data(iTargetStudy, FileName, TfMat); end - - - % Display report to user - bst_report('Info', sProcess, sInputs, 'Success'); - disp('BST> process_timefreq: Success'); end - - - - diff --git a/toolbox/process/functions/process_rasterplot_per_neuron.m b/toolbox/process/functions/process_rasterplot_per_neuron.m index 697ecfffbe..ee6ec97187 100644 --- a/toolbox/process/functions/process_rasterplot_per_neuron.m +++ b/toolbox/process/functions/process_rasterplot_per_neuron.m @@ -1,8 +1,5 @@ function varargout = process_rasterplot_per_neuron( varargin ) -% PROCESS_RASTERPLOT_PER_NEURON: Computes a rasterplot per neuron. -% -% USAGE: sProcess = process_rasterplot_per_neuron('GetDescription') -% OutputFiles = process_rasterplot_per_neuron('Run', sProcess, sInput) +% PROCESS_RASTERPLOT_PER_NEURON: Computes a raster plot per neuron % @============================================================================= % This function is part of the Brainstorm software: @@ -22,133 +19,101 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018-2019; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018 +% Martin Cousineau, 2018 +% Francois Tadel, 2022 eval(macro_method); end %% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description the process - sProcess.Comment = 'Raster Plot Per Neuron'; + sProcess.Comment = 'Raster plot per neuron'; sProcess.FileTag = 'raster'; sProcess.Category = 'custom'; sProcess.SubGroup = 'Electrophysiology'; - sProcess.Index = 1506; - sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Raster_Plots'; + sProcess.Index = 1225; + sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Raster_plots'; % Definition of the input accepted by this process sProcess.InputTypes = {'data'}; sProcess.OutputTypes = {'timefreq'}; sProcess.nInputs = 1; sProcess.nMinFiles = 1; - % Options: Sensor types - sProcess.options.sensortypes.Comment = 'Sensor types or names (empty=all): '; - sProcess.options.sensortypes.Type = 'text'; - sProcess.options.sensortypes.Value = 'EEG'; end %% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok +function Comment = FormatComment(sProcess) Comment = sProcess.Comment; end %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInputs) % Initialize returned values OutputFiles = {}; - % Extract method name from the process name - strProcess = strrep(strrep(func2str(sProcess.Function), 'process_', ''), 'data', ''); - - % Add other options - tfOPTIONS.Method = strProcess; - if isfield(sProcess.options, 'sensortypes') - tfOPTIONS.SensorTypes = sProcess.options.sensortypes.Value; - else - tfOPTIONS.SensorTypes = []; - end - - % If a time window was specified - if isfield(sProcess.options, 'timewindow') && ~isempty(sProcess.options.timewindow) && ~isempty(sProcess.options.timewindow.Value) && iscell(sProcess.options.timewindow.Value) - tfOPTIONS.TimeWindow = sProcess.options.timewindow.Value{1}; - elseif ~isfield(tfOPTIONS, 'TimeWindow') - tfOPTIONS.TimeWindow = []; - end - - tfOPTIONS.TimeVector = in_bst(sInputs(1).FileName, 'Time'); + % ===== PROCESS FILES ===== % Check how many event groups we're processing listComments = cellfun(@str_remove_parenth, {sInputs.Comment}, 'UniformOutput', 0); [uniqueComments,tmp,iData2List] = unique(listComments); nLists = length(uniqueComments); - % Process each even group seperately for iList = 1:nLists - sCurrentInputs = sInputs(iData2List == iList); - - % === OUTPUT STUDY === - % Get output study - [tmp, iStudy] = bst_process('GetOutputStudy', sProcess, sCurrentInputs); - tfOPTIONS.iTargetStudy = iStudy; - - % Get channel file - sChannel = bst_get('ChannelForStudy', iStudy); - % Load channel file - ChannelMat = in_bst_channel(sChannel.FileName); - %% Get only the unique neurons along all of the trials + % === LOAD INPUTS === + % Get trials in this group + sCurrentInputs = sInputs(iData2List == iList); nTrials = length(sCurrentInputs); - - % I get the files outside of the parfor so it won't fail. - % This loads the information from ALL TRIALS on ALL_TRIALS_files - % (Shouldn't create a memory problem). - ALL_TRIALS_files = struct(); - for iFile = 1:nTrials - DataMat = in_bst(sCurrentInputs(iFile).FileName); - ALL_TRIALS_files(iFile).Events = DataMat.Events; - end - - % Create a cell that holds all of the labels and one for the unique labels - % This will be used to take the averages using the appropriate indices - labelsForDropDownMenu = {}; % Unique neuron labels (each trial might have different number of neurons). We need everything that appears. + + % Get all the neuron labels (each trial might have different number of neurons) + DataMats = cell(1, nTrials); + labelsNeurons = {}; for iFile = 1:nTrials - for iEvent = 1:length(ALL_TRIALS_files(iFile).Events) - if process_spikesorting_supervised('IsSpikeEvent', ALL_TRIALS_files(iFile).Events(iEvent).label) - labelsForDropDownMenu{end+1} = ALL_TRIALS_files(iFile).Events(iEvent).label; + % Load file + DataMats{iFile} = in_bst_data(sCurrentInputs(iFile).FileName, 'Events'); + % Save spike events + for iEvent = 1:length(DataMats{iFile}.Events) + if panel_spikes('IsSpikeEvent', DataMats{iFile}.Events(iEvent).label) + labelsNeurons{end+1} = DataMats{iFile}.Events(iEvent).label; end end end - labelsForDropDownMenu = unique(labelsForDropDownMenu,'stable'); - labelsForDropDownMenu = sort_nat(labelsForDropDownMenu); + % If no neuron was found + if isempty(labelsNeurons) + bst_report('Error', sProcess, sCurrentInputs(1), 'No neurons/spiking events detected.'); + return; + end + % Sort neurons alphabetically + labelsNeurons = unique(labelsNeurons,'stable'); + labelsNeurons = sort_nat(labelsNeurons); - %% === START COMPUTATION === - sampling_rate = round(abs(1. / (tfOPTIONS.TimeVector(2) - tfOPTIONS.TimeVector(1)))); + % ===== COMPUTE BINNING ===== + % Get file time + DataMat = in_bst_data(sCurrentInputs(1).FileName, 'Time'); + TimeVector = DataMat.Time; + % Define bins + nBins = length(TimeVector); + raster = zeros(length(labelsNeurons), nBins, nTrials); + bins = linspace(TimeVector(1), TimeVector(end), nBins); - temp = in_bst(sCurrentInputs(1).FileName); - nElectrodes = size(temp.ChannelFlag,1); - nBins = length(tfOPTIONS.TimeVector); - raster = zeros(length(labelsForDropDownMenu), nBins, nTrials); - bins = linspace(temp.Time(1), temp.Time(end), nBins); + bst_progress('start', 'Raster plot per neuron', 'Binning spikes...', 0, length(sCurrentInputs)); - bst_progress('start', 'Raster Plot per Neuron', 'Binning Spikes...', 0, length(sCurrentInputs)); - - for ifile = 1:length(sCurrentInputs) - trial = in_bst(sCurrentInputs(ifile).FileName); - single_file_binning = zeros(length(labelsForDropDownMenu), nBins); - - for iNeuron = 1:length(labelsForDropDownMenu) - for ievent = 1:size(trial.Events,2) - if strcmp(trial.Events(ievent).label, labelsForDropDownMenu{iNeuron}) - - outside_up = trial.Events(ievent).times >= bins(end); % This snippet takes care of some spikes that occur outside of the window of Time due to precision incompatibility. - trial.Events(ievent).times(outside_up) = bins(end) - 0.001; % I assign those spikes just 1ms inside the bin - outside_down = trial.Events(ievent).times <= bins(1); - trial.Events(ievent).times(outside_down) = bins(1) + 0.001; % I assign those spikes just 1ms inside the bin - - [tmp, bin_it_belongs_to] = histc(trial.Events(ievent).times, bins); + for iFile = 1:nTrials + single_file_binning = zeros(length(labelsNeurons), nBins); + for iNeuron = 1:length(labelsNeurons) + for ievent = 1:size(DataMats{iFile}.Events,2) + evt = DataMats{iFile}.Events(ievent); + if strcmp(evt.label, labelsNeurons{iNeuron}) + outside_up = evt.times >= bins(end); % This snippet takes care of some spikes that occur outside of the window of Time due to precision incompatibility. + evt.times(outside_up) = bins(end) - 0.001; % I assign those spikes just 1ms inside the bin + outside_down = evt.times <= bins(1); + evt.times(outside_down) = bins(1) + 0.001; % I assign those spikes just 1ms inside the bin + + [tmp, bin_it_belongs_to] = histc(evt.times, bins); unique_bin = unique(bin_it_belongs_to); occurences = [unique_bin; histc(bin_it_belongs_to, unique_bin)]; @@ -159,59 +124,39 @@ end end - raster(:, :, ifile) = single_file_binning; + raster(:, :, iFile) = single_file_binning; bst_progress('inc', 1); end - %% Build the output file - tfOPTIONS.ParentFiles = {sCurrentInputs.FileName}; + % ===== SAVE RESULTS ===== % Prepare output file structure - FileMat.TF = raster; - FileMat.Time = tfOPTIONS.TimeVector; - FileMat.TFmask = true(size(raster, 2), size(raster, 3)); - FileMat.Freqs = 1:size(FileMat.TF, 3); - FileMat.Std = []; - FileMat.Comment = ['Raster Plot: ' uniqueComments{iList}]; - FileMat.DataType = 'data'; - FileMat.TimeBands = []; - FileMat.RefRowNames = []; - FileMat.RowNames = labelsForDropDownMenu; - FileMat.Measure = 'power'; - FileMat.Method = 'morlet'; - FileMat.DataFile = []; % Leave blank because multiple parents - FileMat.SurfaceFile = []; - FileMat.GridLoc = []; - FileMat.GridAtlas = []; - FileMat.Atlas = []; - FileMat.HeadModelFile = []; - FileMat.HeadModelType = []; - FileMat.nAvg = []; - FileMat.ColormapType = []; - FileMat.DisplayUnits = 'Spikes'; - FileMat.Options = tfOPTIONS; - FileMat.History = []; + TfMat = db_template('timefreqmat'); + TfMat.TF = raster; + TfMat.Time = TimeVector; + TfMat.Freqs = 1:size(TfMat.TF, 3); + TfMat.Comment = ['Raster Plot: ' uniqueComments{iList}]; + TfMat.DataType = 'data'; + TfMat.RowNames = labelsNeurons; + TfMat.Measure = 'power'; + TfMat.Method = 'morlet'; + TfMat.DataFile = []; % Leave blank because multiple parents + TfMat.DisplayUnits = 'Spikes'; + TfMat.Options = []; % Add history field - FileMat = bst_history('add', FileMat, 'compute', ... - ['Raster Plot per neuron']); - + TfMat = bst_history('add', TfMat, 'compute', 'Raster plot per neuron'); + for iFile = 1:length(sInputs) + TfMat = bst_history('add', TfMat, 'average', [' - ' sInputs(iFile).FileName]); + end % Get output study - sTargetStudy = bst_get('Study', iStudy); + [tmp, iTargetStudy] = bst_process('GetOutputStudy', sProcess, sCurrentInputs); + sTargetStudy = bst_get('Study', iTargetStudy); % Output filename - FileName = bst_process('GetNewFilename', bst_fileparts(sTargetStudy.FileName), 'timefreq_rasterplot'); - OutputFiles = {FileName}; + OutputFiles{1} = bst_process('GetNewFilename', bst_fileparts(sTargetStudy.FileName), 'timefreq_rasterplot'); % Save output file and add to database - bst_save(FileName, FileMat, 'v6'); - db_add_data(tfOPTIONS.iTargetStudy, FileName, FileMat); + bst_save(OutputFiles{1}, TfMat, 'v6'); + db_add_data(iTargetStudy, OutputFiles{1}, TfMat); end - - % Display report to user - bst_report('Info', sProcess, sInputs, 'Success'); - disp('BST> process_timefreq: Success'); end - - - - diff --git a/toolbox/process/functions/process_spike_field_coherence.m b/toolbox/process/functions/process_spike_field_coherence.m index 8e6b7f57b4..e6a45d6ae0 100644 --- a/toolbox/process/functions/process_spike_field_coherence.m +++ b/toolbox/process/functions/process_spike_field_coherence.m @@ -1,23 +1,10 @@ function varargout = process_spike_field_coherence( varargin ) % PROCESS_SPIKE_FIELD_COHERENCE: Computes the spike field coherence. -% - -% There are two different TimeWindow Notations here: -% 1. Timewindow around the spike (This is the one that is asked as input when the function is called). -% 2. Timewindow of the trials imported to the function. - -% The function selects a TimeWindow around the Spike. -% Then applies an FFT to each Spike TimeWindow. -% Then nomralizes by the FFT of the spike triggered average on the averages of -% the SpikeWindow FFTs. - -% If this Spike TimeWindow is outside the TimeWindow of the Trial, the -% spike is ignored for computation. - - -% USAGE: sProcess = process_spike_field_coherence('GetDescription') -% OutputFiles = process_spike_field_coherence('Run', sProcess, sInput) +% DESCRIPTION: Algorithm +% - Selects a time window around the spike +% - Applies a FFT to each spike +% - Normalizes by the FFT of the spike triggered average on the averages of the spikes FFTs. % @============================================================================= % This function is part of the Brainstorm software: @@ -37,161 +24,115 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018 +% Martin Cousineau, 2018 +% Francois Tadel, 2022 eval(macro_method); end %% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description the process - sProcess.Comment = 'Spike Field Coherence'; + sProcess.Comment = 'Spike field coherence'; sProcess.FileTag = 'SFC'; - sProcess.Category = 'custom'; + sProcess.Category = 'Custom'; sProcess.SubGroup = 'Electrophysiology'; - sProcess.Index = 1607; - sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Spike_Field_Coherence'; + sProcess.Index = 1220; + sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Spike_field_coherence'; % Definition of the input accepted by this process sProcess.InputTypes = {'data'}; sProcess.OutputTypes = {'timefreq'}; sProcess.nInputs = 1; sProcess.nMinFiles = 1; + % Options: Segment around spike + sProcess.options.timewindow.Comment = 'Spike time window: '; + sProcess.options.timewindow.Type = 'range'; + sProcess.options.timewindow.Value = {[-0.150, 0.150],'ms',[]}; % Options: Sensor types sProcess.options.sensortypes.Comment = 'Sensor types or names (empty=all): '; sProcess.options.sensortypes.Type = 'text'; - sProcess.options.sensortypes.Value = 'EEG'; - % Options: Parallel Processing - sProcess.options.paral.Comment = 'Parallel processing'; - sProcess.options.paral.Type = 'checkbox'; - sProcess.options.paral.Value = 1; - % Options: Segment around spike - sProcess.options.timewindow.Comment = 'Spike Time window: '; - sProcess.options.timewindow.Type = 'range'; - sProcess.options.timewindow.Value = {[-0.150, 0.150],'ms',[]}; - + sProcess.options.sensortypes.Value = 'EEG, SEEG'; + % Options: Parallel + sProcess.options.parallel.Comment = 'Parallel processing'; + sProcess.options.parallel.Type = 'checkbox'; + sProcess.options.parallel.Value = 0; end %% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok +function Comment = FormatComment(sProcess) Comment = sProcess.Comment; end %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInputs) % Initialize returned values OutputFiles = {}; - % Extract method name from the process name - strProcess = strrep(strrep(func2str(sProcess.Function), 'process_', ''), 'timefreq', 'morlet'); - - % Add other options - tfOPTIONS.Method = strProcess; - if isfield(sProcess.options, 'sensortypes') - tfOPTIONS.SensorTypes = sProcess.options.sensortypes.Value; - else - tfOPTIONS.SensorTypes = []; - end - - % If a time window was specified - if isfield(sProcess.options, 'timewindow') && ~isempty(sProcess.options.timewindow) && ~isempty(sProcess.options.timewindow.Value) && iscell(sProcess.options.timewindow.Value) - tfOPTIONS.TimeWindow = sProcess.options.timewindow.Value{1}; - elseif ~isfield(tfOPTIONS, 'TimeWindow') - tfOPTIONS.TimeWindow = []; - end - - tfOPTIONS.TimeVector = in_bst(sInputs(1).FileName, 'Time'); - - if sProcess.options.timewindow.Value{1}(1)>=0 || sProcess.options.timewindow.Value{1}(2)<=0 - bst_report('Error', sProcess, sInputs, 'The time-selection must be around the spikes.'); - return; - elseif sProcess.options.timewindow.Value{1}(1)==tfOPTIONS.TimeVector(1) && sProcess.options.timewindow.Value{1}(2)==tfOPTIONS.TimeVector(end) - bst_report('Error', sProcess, sInputs, 'The spike window has to be smaller than the trial window'); - return; - end + % Get options + isParallel = sProcess.options.parallel.Value; + TimeWindow = sProcess.options.timewindow.Value{1}; + SensorTypes = sProcess.options.sensortypes.Value; + % ===== PROCESS FILES ===== % Check how many event groups we're processing listComments = cellfun(@str_remove_parenth, {sInputs.Comment}, 'UniformOutput', 0); [uniqueComments,tmp,iData2List] = unique(listComments); nLists = length(uniqueComments); - % Process each even group seperately for iList = 1:nLists - sCurrentInputs = sInputs(iData2List == iList); - - % === OUTPUT STUDY === - % Get output study - [tmp, iStudy] = bst_process('GetOutputStudy', sProcess, sCurrentInputs); - tfOPTIONS.iTargetStudy = iStudy; - - % Get channel file - sChannel = bst_get('ChannelForStudy', iStudy); - % Load channel file - ChannelMat = in_bst_channel(sChannel.FileName); - - % === START COMPUTATION === - sampling_rate = round(abs(1. / (tfOPTIONS.TimeVector(2) - tfOPTIONS.TimeVector(1)))); + % === LOAD INPUTS === + % Get trials in this group + sCurrentInputs = sInputs(iData2List == iList); + nTrials = length(sCurrentInputs); - % Select the appropriate sensors - nElectrodes = 0; - selectedChannels = []; - for iChannel = 1:length(ChannelMat.Channel) - if strcmp(ChannelMat.Channel(iChannel).Type, 'EEG') || strcmp(ChannelMat.Channel(iChannel).Type, 'SEEG') - nElectrodes = nElectrodes + 1; - selectedChannels(end + 1) = iChannel; - end + % Load all the trials outside of the parfor so it won't fail + DataMats = cell(1, nTrials); + for iFile = 1:nTrials + DataMats{iFile} = in_bst_data(sCurrentInputs(iFile).FileName); end - nTrials = length(sCurrentInputs); - time_segmentAroundSpikes = linspace(sProcess.options.timewindow.Value{1}(1), sProcess.options.timewindow.Value{1}(2), abs(sProcess.options.timewindow.Value{1}(2))* sampling_rate + abs(sProcess.options.timewindow.Value{1}(1))* sampling_rate + 1); - - % Prepare parallel pool, if requested - if sProcess.options.paral.Value - try - poolobj = gcp('nocreate'); - if isempty(poolobj) - parpool; - end - catch - sProcess.options.paral.Value = 0; - end - else - poolobj = []; + % Check time window + if TimeWindow(1)>=0 || TimeWindow(2)<=0 + bst_report('Error', sProcess, sInputs, 'The time-selection must be around the spikes.'); + return; + elseif (TimeWindow(1) <= DataMats{1}.Time(1)) && (TimeWindow(2) >= DataMats{1}.Time(end)) + bst_report('Error', sProcess, sInputs, 'The spike window has to be smaller than the trial window.'); + return; end + % Sampling frequency + sampling_rate = round(abs(1. / (DataMats{1}.Time(2) - DataMats{1}.Time(1)))); - - %% Compute the FFTs and collect all the average FFTs and LFPs for each trial. - everything = struct(); % This is a struct 1xnTrials - - % I get the files outside of the parfor so it won't fail. - % This loads the information from ALL TRIALS on ALL_TRIALS_files - % (Shouldn't create a memory problem). - ALL_TRIALS_files = struct(); - for iFile = 1:nTrials - ALL_TRIALS_files(iFile).trial = in_bst(sCurrentInputs(iFile).FileName); + % Load channel file + ChannelMat = in_bst_channel(sCurrentInputs(1).ChannelFile); + % Find channel indices + iChannels = channel_find(ChannelMat.Channel, SensorTypes); + if isempty(iChannels) + bst_report('Error', sProcess, sInputs, ['Channels not found: "' SensorTypes '".']); + return; end - % Start Parallel Pool if requested - if ~isempty(poolobj) + % === COMPUTE FFT === + % Time around spike + time_segmentAroundSpikes = linspace(TimeWindow(1), TimeWindow(2), abs(TimeWindow(2))* sampling_rate + abs(TimeWindow(1))* sampling_rate + 1); + % Compute FFTs (in parallel if possible) + FFT_trials = cell(1, nTrials); + Freqs = cell(1, nTrials); + if isParallel parfor iFile = 1:nTrials - [FFTs_single_trial, Freqs] = get_FFTs(ALL_TRIALS_files(iFile).trial, selectedChannels, sProcess, time_segmentAroundSpikes, sampling_rate, ChannelMat); - everything(iFile).FFTs_single_trial = FFTs_single_trial; - everything(iFile).Freqs = Freqs; + [FFT_trials{iFile}, Freqs{iFile}] = get_FFTs(DataMats{iFile}, iChannels, TimeWindow, time_segmentAroundSpikes, sampling_rate, ChannelMat); end else for iFile = 1:nTrials - [FFTs_single_trial, Freqs] = get_FFTs(ALL_TRIALS_files(iFile).trial, selectedChannels, sProcess, time_segmentAroundSpikes, sampling_rate, ChannelMat); - everything(iFile).FFTs_single_trial = FFTs_single_trial; - everything(iFile).Freqs = Freqs; + [FFT_trials{iFile}, Freqs{iFile}] = get_FFTs(DataMats{iFile}, iChannels, TimeWindow, time_segmentAroundSpikes, sampling_rate, ChannelMat); end end - - - %% Calculate the SFC + % ===== COMPUTE SFC ===== % The Spike Field Coherence should be a 3d matrix % Number of neurons x Frequencies x Electrodes % Ultimately the user will select the NEURON that wants to be displayed, @@ -199,46 +140,41 @@ % coherence of the spikes of that neuron with the LFPs on every % electrode on all frequencies. - % Create a cell that holds all of the labels and one for the unique labels % This will be used to take the averages using the appropriate indices all_labels = struct; - labelsForDropDownMenu = {}; % Unique neuron labels (each trial might have different number of neurons). We need everything that appears. + labelsNeurons = {}; % Unique neuron labels (each trial might have different number of neurons). We need everything that appears. for iFile = 1:nTrials - for iNeuron = 1:length(everything(iFile).FFTs_single_trial) - if ~isempty(everything(iFile).FFTs_single_trial(iNeuron)) % An empty struct here would be caused by no selection of spikes. This would be caused by the combination of large windows around the spiking events, and small trial window - all_labels.labels{iNeuron,iFile} = everything(iFile).FFTs_single_trial(iNeuron).label; - if process_spikesorting_supervised('IsSpikeEvent', everything(iFile).FFTs_single_trial(iNeuron).label) - labelsForDropDownMenu{end+1} = everything(iFile).FFTs_single_trial(iNeuron).label; + for iNeuron = 1:length(FFT_trials{iFile}) + if ~isempty(FFT_trials{iFile}(iNeuron)) % An empty struct here would be caused by no selection of spikes. This would be caused by the combination of large windows around the spiking events, and small trial window + all_labels.labels{iNeuron,iFile} = FFT_trials{iFile}(iNeuron).label; + if panel_spikes('IsSpikeEvent', FFT_trials{iFile}(iNeuron).label) + labelsNeurons{end+1} = FFT_trials{iFile}(iNeuron).label; end end end end - % Give an error if there were no spikes on any of the selected trials - if isempty(labelsForDropDownMenu) + if isempty(labelsNeurons) bst_report('Error', sProcess, sInputs, ['No spikes selected for ' uniqueComments{iList} '.' ... 'Select a smaller time-window around the spikes, or make sure there are spikes on these trials.']); return; end - - all_labels = all_labels.labels; - labelsForDropDownMenu = unique(labelsForDropDownMenu,'stable'); - + all_labels = all_labels.labels; + labelsNeurons = unique(labelsNeurons,'stable'); - SFC = zeros(length(labelsForDropDownMenu), length(everything(1).Freqs), nElectrodes); % Number of neurons x Frequencies x Electrodes + SFC = zeros(length(labelsNeurons), length(Freqs{iFile}), length(iChannels)); % Number of neurons x Frequencies x Electrodes - for iNeuron = 1:length(labelsForDropDownMenu) - - temp_All_trials_sum_LFP = zeros(1,nElectrodes, length(time_segmentAroundSpikes)); - temp_All_trials_sum_FFT = zeros(length(everything(1).Freqs), nElectrodes); + for iNeuron = 1:length(labelsNeurons) - - %% For each TRIAL, get the index of the label that corresponds to the appropriate neuron. + temp_All_trials_sum_LFP = zeros(1, length(iChannels), length(time_segmentAroundSpikes)); + temp_All_trials_sum_FFT = zeros(length(Freqs{iFile}), length(iChannels)); + + % For each TRIAL, get the index of the label that corresponds to the appropriate neuron. for ii = 1:size(all_labels,1) for jj = 1:size(all_labels,2) - logicalEvents(ii,jj) = strcmp(all_labels{ii,jj}, labelsForDropDownMenu{iNeuron}); + logicalEvents(ii,jj) = strcmp(all_labels{ii,jj}, labelsNeurons{iNeuron}); end end @@ -253,13 +189,13 @@ end - %% Take the Averages of the appropriate indices + % Take the Averages of the appropriate indices divideBy = 0; for iTrial = 1:size(all_labels,2) if iEvents(iTrial)~=0 - temp_All_trials_sum_LFP = temp_All_trials_sum_LFP + everything(iTrial).FFTs_single_trial(iEvents(iTrial)).sumLFP; - temp_All_trials_sum_FFT = temp_All_trials_sum_FFT + everything(iTrial).FFTs_single_trial(iEvents(iTrial)).sumFFT; - divideBy = divideBy + everything(iTrial).FFTs_single_trial(iEvents(iTrial)).nSpikes; + temp_All_trials_sum_LFP = temp_All_trials_sum_LFP + FFT_trials{iTrial}(iEvents(iTrial)).sumLFP; + temp_All_trials_sum_FFT = temp_All_trials_sum_FFT + FFT_trials{iTrial}(iEvents(iTrial)).sumFFT; + divideBy = divideBy + FFT_trials{iTrial}(iEvents(iTrial)).nSpikes; end end @@ -267,7 +203,6 @@ average_FFT = temp_All_trials_sum_FFT./divideBy; % Get The FFT of the AverageLFP - FFTofAverageLFP = compute_FFT(average_LFP, time_segmentAroundSpikes); SFC_singleNeuron = squeeze(FFTofAverageLFP)./average_FFT; % Normalize by the FFT of the average LFP @@ -275,77 +210,56 @@ % and the division by 0 would give NaN as an output. This line takes care of that. SFC(iNeuron,:,:) = SFC_singleNeuron; - - end - %% Wrap everything into the output file - - tfOPTIONS.ParentFiles = {sCurrentInputs.FileName}; + % ===== SAVE FILE ===== % Prepare output file structure - FileMat = db_template('timefreqmat'); - FileMat.TF = SFC; - FileMat.Time = everything(1).Freqs; % These values are in order to trick Brainstorm with the correct values (This needs to be improved. Talk to Martin) - FileMat.TFmask = []; - FileMat.Freqs = 1:nElectrodes; % These values are in order to trick Brainstorm with the correct values (This needs to be improved. Talk to Martin) - FileMat.Comment = ['Spike Field Coherence: ' uniqueComments{iList}]; - FileMat.DataType = 'data'; - FileMat.RowNames = labelsForDropDownMenu; - FileMat.Measure = 'power'; - FileMat.Method = 'morlet'; - FileMat.DataFile = []; % Leave blank because multiple parents - FileMat.Options = tfOPTIONS; + TfMat = db_template('timefreqmat'); + TfMat.TF = SFC; + TfMat.Time = Freqs{iFile}; % These values are in order to trick Brainstorm with the correct values (This needs to be improved. Talk to Martin) + TfMat.Freqs = 1:length(iChannels); % These values are in order to trick Brainstorm with the correct values (This needs to be improved. Talk to Martin) + TfMat.Comment = ['Spike Field Coherence: ' uniqueComments{iList}]; + TfMat.DataType = 'data'; + TfMat.RowNames = labelsNeurons; + TfMat.Measure = 'power'; + TfMat.Method = 'morlet'; + TfMat.DataFile = []; % Leave blank because multiple parents + TfMat.Options = []; % Add history field - FileMat = bst_history('add', FileMat, 'compute', ... - ['Spike Field Coherence: [' num2str(tfOPTIONS.TimeWindow(1)) ', ' num2str(tfOPTIONS.TimeWindow(2)) '] ms']); + TfMat = bst_history('add', TfMat, 'compute', ['Spike Field Coherence: [' num2str(TimeWindow(1)) ', ' num2str(TimeWindow(2)) '] ms']); + for iFile = 1:length(sInputs) + TfMat = bst_history('add', TfMat, 'average', [' - ' sInputs(iFile).FileName]); + end % Get output study - sTargetStudy = bst_get('Study', iStudy); + [tmp, iTargetStudy] = bst_process('GetOutputStudy', sProcess, sCurrentInputs); + sTargetStudy = bst_get('Study', iTargetStudy); % Output filename FileName = bst_process('GetNewFilename', bst_fileparts(sTargetStudy.FileName), 'timefreq_spike_field_coherence'); OutputFiles{end + 1} = FileName; % Save output file and add to database - bst_save(FileName, FileMat, 'v6'); - db_add_data(tfOPTIONS.iTargetStudy, FileName, FileMat); - end - - % Display report to user - bst_report('Info', sProcess, sInputs, 'Success'); - disp('BST> process_spike_field_coherence: Success'); - - - % Close parallel pool - if sProcess.options.paral.Value - if ~isempty(poolobj) - delete(poolobj); - end + bst_save(FileName, TfMat, 'v6'); + db_add_data(iTargetStudy, FileName, TfMat); end end - - - - -function [all, Freqs] = get_FFTs(trial, selectedChannels, sProcess, time_segmentAroundSpikes, sampling_rate, ChannelMat) - %% Get the events that show the NEURONS' activity - - % Important Variable here! +%% ===== GET FFT ===== +function [all, Freqs] = get_FFTs(trial, iChannels, TimeWindow, time_segmentAroundSpikes, sampling_rate, ChannelMat) + %% Get the events that show the NEURONS' activity ===== spikeEvents = []; % The spikeEvents variable holds the indices of the events that correspond to spikes. - allChannelEvents = cellfun(@(x) process_spikesorting_supervised('GetChannelOfSpikeEvent', x), ... + allChannelEvents = cellfun(@(x) panel_spikes('GetChannelOfSpikeEvent', x), ... {trial.Events.label}, 'UniformOutput', 0); allChannelEvents = allChannelEvents(~cellfun('isempty', allChannelEvents)); - if isempty(allChannelEvents) - bst_report('Error', sProcess, sInputs, 'No spike event found in this file.'); - return; + error('No spike event found in this file.'); end - for iElec = 1:length(selectedChannels) - ielectrode = selectedChannels(iElec); + for iElec = 1:length(iChannels) + ielectrode = iChannels(iElec); iEvents = find(strcmp(allChannelEvents, ChannelMat.Channel(ielectrode).Name)); % Find the index of the spike-events that correspond to that electrode (Exact string match) if ~isempty(iEvents) spikeEvents(end+1:end+length(iEvents)) = iEvents; @@ -358,18 +272,18 @@ % Check that the entire segment around the spikes i.e. :[-150,150]ms % is inside the trial segment and keep only those events - iSel = trial.Events(spikeEvents(iNeuron)).times > trial.Time(1) + abs(sProcess.options.timewindow.Value{1}(1)) & ... - trial.Events(spikeEvents(iNeuron)).times < trial.Time(end) - abs(sProcess.options.timewindow.Value{1}(2)); + iSel = trial.Events(spikeEvents(iNeuron)).times > trial.Time(1) + abs(TimeWindow(1)) & ... + trial.Events(spikeEvents(iNeuron)).times < trial.Time(end) - abs(TimeWindow(2)); events_within_segment = round(trial.Events(spikeEvents(iNeuron)).times(iSel) .* sampling_rate); %% Create a matrix that holds all the segments around the spike % of that neuron, for all electrodes. - allSpikeSegments_singleNeuron_singleTrial = zeros(length(events_within_segment),size(trial.F(selectedChannels,:),1),abs(sProcess.options.timewindow.Value{1}(2))* sampling_rate + abs(sProcess.options.timewindow.Value{1}(1))* sampling_rate + 1); + allSpikeSegments_singleNeuron_singleTrial = zeros(length(events_within_segment),size(trial.F(iChannels,:),1),abs(TimeWindow(2))* sampling_rate + abs(TimeWindow(1))* sampling_rate + 1); for ispike = 1:length(events_within_segment) - allSpikeSegments_singleNeuron_singleTrial(ispike,:,:) = trial.F(selectedChannels, ... - events_within_segment(ispike) - abs(sProcess.options.timewindow.Value{1}(1)) * sampling_rate + round(abs(trial.Time(1)) * sampling_rate) + 1: ... - events_within_segment(ispike) + abs(sProcess.options.timewindow.Value{1}(2)) * sampling_rate + round(abs(trial.Time(1)) * sampling_rate) + 1 ... + allSpikeSegments_singleNeuron_singleTrial(ispike,:,:) = trial.F(iChannels, ... + events_within_segment(ispike) - abs(TimeWindow(1)) * sampling_rate + round(abs(trial.Time(1)) * sampling_rate) + 1: ... + events_within_segment(ispike) + abs(TimeWindow(2)) * sampling_rate + round(abs(trial.Time(1)) * sampling_rate) + 1 ... ); end @@ -390,15 +304,13 @@ iEventsToRemove = find([all.nSpikes]==0); all = all(~ismember(1:length(all),iEventsToRemove)); - end - - +%% ===== COMPUTE FFT ===== function [TF, Freqs] = compute_FFT(F, time) - %% This function if made for 3-dimensional F + % This function if made for 3-dimensional F dim = 3; % Next power of 2 from length of signal @@ -408,8 +320,6 @@ sfreq = 1 / (time(2) - time(1)); % Positive frequency bins spanned by FFT Freqs = sfreq / 2 * linspace(0, 1, NFFT / 2 + 1); - % Keep only first and last time instants - time = time([1, end]); % Remove mean of the signal F = bst_bsxfun(@minus, F, mean(F,dim)); @@ -426,14 +336,12 @@ % (x2 to recover full power from negative frequencies) TF = 2 * Ffft(:, :, 1:floor(NFFT / 2) + 1) ./ nTime; % I added floor - %%%%%%%%%%%% This is added. SFC doesn't need the complex values %%%%%%% TF = abs(TF) .^ 2; %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % Permute dimensions: time and frequency TF = permute(TF, [1 3 2]); - end diff --git a/toolbox/process/functions/process_spike_triggered_average.m b/toolbox/process/functions/process_spike_triggered_average.m index a8d8501021..28d187d4ac 100644 --- a/toolbox/process/functions/process_spike_triggered_average.m +++ b/toolbox/process/functions/process_spike_triggered_average.m @@ -1,20 +1,6 @@ function varargout = process_spike_triggered_average( varargin ) % PROCESS_SPIKE_TRIGGERED_AVERAGE: Computes the spike triggered average. -% - -% There are two different TimeWindow Notations here: -% 1. Timewindow around the spike (This is the one that is asked as input when the function is called). -% 2. Timewindow of the trials imported to the function. - -% The function selects a TimeWindow around the Spike of a specific neuron. -% Then averages the LFPs oe each electrode. -% If this Spike TimeWindow is outside the TimeWindow of the Trial, the -% spike is ignored for computation. - - - -% USAGE: sProcess = process_spike_triggered_average('GetDescription') -% OutputFiles = process_spike_triggered_average('Run', sProcess, sInput) +% Select a time window around the spikes of a specific neuron and average the LFPs of each electrode % @============================================================================= % This function is part of the Brainstorm software: @@ -35,151 +21,96 @@ % =============================================================================@ % % Authors: Konstantinos Nasiotis, 2018-2019 +% Francois Tadel, 2022 eval(macro_method); end %% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description the process - sProcess.Comment = 'Spike Triggered Average'; + sProcess.Comment = 'Spike triggered average'; sProcess.FileTag = 'STA'; - sProcess.Category = 'custom'; + sProcess.Category = 'Custom'; sProcess.SubGroup = 'Electrophysiology'; - sProcess.Index = 1506; - sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Spike_triggered_Average'; + sProcess.Index = 1230; + sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Spike_triggered_average'; % Definition of the input accepted by this process sProcess.InputTypes = {'data'}; sProcess.OutputTypes = {'data'}; sProcess.nInputs = 1; - sProcess.nMinFiles = 1; - % Options: Sensor types - sProcess.options.sensortypes.Comment = 'Sensor types or names (empty=all): '; - sProcess.options.sensortypes.Type = 'text'; - sProcess.options.sensortypes.Value = 'EEG'; - % Options: Parallel Processing - sProcess.options.paral.Comment = 'Parallel processing'; - sProcess.options.paral.Type = 'checkbox'; - sProcess.options.paral.Value = 1; + sProcess.nMinFiles = 2; % Options: Segment around spike - sProcess.options.timewindow.Comment = 'Spike Time window: '; + sProcess.options.timewindow.Comment = 'Spike time window: '; sProcess.options.timewindow.Type = 'range'; sProcess.options.timewindow.Value = {[-0.150, 0.150],'ms',[]}; - + % Options: Parallel Processing + sProcess.options.parallel.Comment = 'Parallel processing'; + sProcess.options.parallel.Type = 'checkbox'; + sProcess.options.parallel.Value = 0; end %% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok +function Comment = FormatComment(sProcess) Comment = sProcess.Comment; end %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInputs) % Initialize returned values OutputFiles = {}; - % Extract method name from the process name - strProcess = strrep(strrep(func2str(sProcess.Function), 'process_', ''), 'data', ''); - - % Add other options - tfOPTIONS.Method = strProcess; - if isfield(sProcess.options, 'sensortypes') - tfOPTIONS.SensorTypes = sProcess.options.sensortypes.Value; - else - tfOPTIONS.SensorTypes = []; - end - - % If a time window was specified - if isfield(sProcess.options, 'timewindow') && ~isempty(sProcess.options.timewindow) && ~isempty(sProcess.options.timewindow.Value) && iscell(sProcess.options.timewindow.Value) - tfOPTIONS.TimeWindow = sProcess.options.timewindow.Value{1}; - elseif ~isfield(tfOPTIONS, 'TimeWindow') - tfOPTIONS.TimeWindow = []; - end + % Get options + isParallel = sProcess.options.parallel.Value; + TimeWindow = sProcess.options.timewindow.Value{1}; - tfOPTIONS.TimeVector = in_bst(sInputs(1).FileName, 'Time'); - if sProcess.options.timewindow.Value{1}(1)>=0 || sProcess.options.timewindow.Value{1}(2)<=0 - bst_report('Error', sProcess, sInputs, 'The time-selection must be around the spikes.'); - elseif sProcess.options.timewindow.Value{1}(1)==tfOPTIONS.TimeVector(1) && sProcess.options.timewindow.Value{1}(2)==tfOPTIONS.TimeVector(end) - bst_report('Error', sProcess, sInputs, 'The spike window has to be smaller than the trial window'); - end - - - % === OUTPUT STUDY === - % Get output study - [tmp, iStudy] = bst_process('GetOutputStudy', sProcess, sInputs); - tfOPTIONS.iTargetStudy = iStudy; - - % Get channel file - sChannel = bst_get('ChannelForStudy', iStudy); - % Load channel file - ChannelMat = in_bst_channel(sChannel.FileName); - - - % === START COMPUTATION === - sampling_rate = round(abs(1. / (tfOPTIONS.TimeVector(2) - tfOPTIONS.TimeVector(1)))); - - selectedChannels = []; - nChannels = 0; - for iChannel = 1:length(ChannelMat.Channel) - if strcmp(ChannelMat.Channel(iChannel).Type, 'EEG') || strcmp(ChannelMat.Channel(iChannel).Type, 'SEEG') - nChannels = nChannels + 1; - selectedChannels(end + 1) = iChannel; - end - end - - + % ===== LOAD INPUTS ===== + % Loads all the data outside of the parfor, so it doesn't fail nTrials = length(sInputs); - time_segmentAroundSpikes = linspace(sProcess.options.timewindow.Value{1}(1), sProcess.options.timewindow.Value{1}(2), abs(sProcess.options.timewindow.Value{1}(2))* sampling_rate + abs(sProcess.options.timewindow.Value{1}(1))* sampling_rate + 1); - - - % Prepare parallel pool, if requested - if sProcess.options.paral.Value - try - poolobj = gcp('nocreate'); - if isempty(poolobj) - parpool; - end - catch - sProcess.options.paral.Value = 0; - poolobj = []; + DataMats = cell(1, nTrials); + ChannelFlag = []; + for iFile = 1:length(sInputs) + DataMats{iFile} = in_bst_data(sInputs(iFile).FileName); + if isempty(ChannelFlag) + ChannelFlag = DataMats{iFile}.ChannelFlag; + else + ChannelFlag(DataMats{iFile}.ChannelFlag == -1) = -1; end - else - poolobj = []; end - - - - %% Collect all the average LFPs for each trial for all Neurons. - everything = struct(); % This is a struct 1xnTrials - - % I get the files outside of the parfor so it won't fail. - % This loads the information from ALL TRIALS on ALL_TRIALS_files - % (Shouldn't create a memory problem). - ALL_TRIALS_files = struct(); - for iFile = 1:nTrials - ALL_TRIALS_files(iFile).trial = in_bst(sInputs(iFile).FileName); + % Check time window + if TimeWindow(1)>=0 || TimeWindow(2)<=0 + bst_report('Error', sProcess, sInputs, 'The time-selection must be around the spikes.'); + return; + elseif (TimeWindow(1) <= DataMats{1}.Time(1)) && (TimeWindow(2) >= DataMats{1}.Time(end)) + bst_report('Error', sProcess, sInputs, 'The spike window has to be smaller than the trial window.'); + return; end + % Sampling frequency + sampling_rate = round(abs(1. / (DataMats{1}.Time(2) - DataMats{1}.Time(1)))); + % Load channel file + ChannelMat = in_bst_channel(sInputs(1).ChannelFile); + - - % Optimize this - if ~isempty(poolobj) + % === START COMPUTATION === + % Input time window + time_segmentAroundSpikes = linspace(TimeWindow(1), TimeWindow(2), abs(TimeWindow(2))* sampling_rate + abs(TimeWindow(1))* sampling_rate + 1); + % Get LPFs + LFP_trials = cell(1, nTrials); + if isParallel parfor iFile = 1:nTrials - [LFPs_single_trial] = get_LFPs(ALL_TRIALS_files(iFile).trial, nChannels, sProcess, time_segmentAroundSpikes, sampling_rate, ChannelMat); - everything(iFile).LFPs_single_trial = LFPs_single_trial; + LFP_trials{iFile} = get_LFPs(DataMats{iFile}, ChannelMat, TimeWindow, time_segmentAroundSpikes, sampling_rate); end else for iFile = 1:nTrials - [LFPs_single_trial] = get_LFPs(ALL_TRIALS_files(iFile).trial, nChannels, sProcess, time_segmentAroundSpikes, sampling_rate, ChannelMat); - everything(iFile).LFPs_single_trial = LFPs_single_trial; + LFP_trials{iFile} = get_LFPs(DataMats{iFile}, ChannelMat, TimeWindow, time_segmentAroundSpikes, sampling_rate); end end - - - - %% Calculate the STA + + + % ===== COMPUTE SPIKE TRIGGERED AVERAGE ===== % The Spike Triggered Average should be a 3d matrix % Number of neurons x Frequencies x Electrodes % Ultimately the user will select the NEURON that wants to be displayed, @@ -187,169 +118,125 @@ % coherence of the spikes of that neuron with the LFPs on every % electrode on all frequencies. - % Create a cell that holds all of the labels and one for the unique labels % This will be used to take the averages using the appropriate indices - all_labels = struct; - labelsForDropDownMenu = {}; % Unique neuron labels (each trial might have different number of neurons). We need everything that appears. + all_labels = {}; + labelsNeurons = {}; % Unique neuron labels (each trial might have different number of neurons). We need everything that appears. for iFile = 1:nTrials - for iNeuron = 1:length(everything(iFile).LFPs_single_trial) - all_labels.labels{iNeuron,iFile} = everything(iFile).LFPs_single_trial(iNeuron).label; - labelsForDropDownMenu{end+1} = everything(iFile).LFPs_single_trial(iNeuron).label; + for iNeuron = 1:length(LFP_trials{iFile}) + all_labels{iNeuron,iFile} = LFP_trials{iFile}(iNeuron).label; + labelsNeurons{end+1} = LFP_trials{iFile}(iNeuron).label; end end - all_labels = all_labels.labels; - labelsForDropDownMenu = unique(labelsForDropDownMenu,'stable'); - - + labelsNeurons = unique(labelsNeurons,'stable'); - - - - %% Compute STA per individual Neuron - - for iNeuron = 1:length(labelsForDropDownMenu) - %% For each TRIAL, get the index of the label that corresponds to the appropriate neuron. - + % Compute STA per individual neuron + for iNeuron = 1:length(labelsNeurons) + % For each TRIAL, get the index of the label that corresponds to the appropriate neuron. for ii = 1:size(all_labels,1) for jj = 1:size(all_labels,2) - logicalEvents(ii,jj) = strcmp(all_labels{ii,jj}, labelsForDropDownMenu{iNeuron}); + logicalEvents(ii,jj) = strcmp(all_labels{ii,jj}, labelsNeurons{iNeuron}); end end - iEvents = zeros(size(all_labels,2),1); - for iTrial = 1:size(all_labels,2) - temp = find(logicalEvents(:,iTrial)); + for iFile = 1:size(all_labels,2) + temp = find(logicalEvents(:,iFile)); if ~isempty(temp) - iEvents(iTrial) = temp; + iEvents(iFile) = temp; else - iEvents(iTrial) = 0; % This shows that that neuron didn't fire any spikes on that trial + iEvents(iFile) = 0; % This shows that that neuron didn't fire any spikes on that trial end end + % Compute the averages of the appropriate indices STA_single_neuron = zeros(length(ChannelMat.Channel), length(time_segmentAroundSpikes)); std_single_neuron = zeros(length(ChannelMat.Channel), length(time_segmentAroundSpikes)); - - %% Take the Averages of the appropriate indices divideBy = 0; - for iTrial = 1:size(all_labels,2) - if iEvents(iTrial)~=0 - STA_single_neuron = STA_single_neuron + everything(iTrial).LFPs_single_trial(iEvents(iTrial)).nSpikes * everything(iTrial).LFPs_single_trial(iEvents(iTrial)).avgLFP; % The avgLFP are sum actually. - divideBy = divideBy + everything(iTrial).LFPs_single_trial(iEvents(iTrial)).nSpikes; + for iFile = 1:size(all_labels,2) + if iEvents(iFile)~=0 + STA_single_neuron = STA_single_neuron + LFP_trials{iFile}(iEvents(iFile)).nSpikes * LFP_trials{iFile}(iEvents(iFile)).avgLFP; % The avgLFP are sum actually. + divideBy = divideBy + LFP_trials{iFile}(iEvents(iFile)).nSpikes; % Here I have the assumption that the LFPs on all trials % have homogeneity in their variance (Cohen, 1988, p.67): % http://www.utstat.toronto.edu/~brunner/oldclass/378f16/readings/CohenPower.pdf % https://www.statisticshowto.datasciencecentral.com/pooled-standard-deviation/ - std_single_neuron = std_single_neuron + (everything(iTrial).LFPs_single_trial(iEvents(iTrial)).nSpikes-1) * everything(iTrial).LFPs_single_trial(iEvents(iTrial)).stdLFP.^2; + std_single_neuron = std_single_neuron + (LFP_trials{iFile}(iEvents(iFile)).nSpikes-1) * LFP_trials{iFile}(iEvents(iFile)).stdLFP.^2; end end - + % Divide by total number of averages STA_single_neuron = (STA_single_neuron./divideBy)'; std_single_neuron = sqrt(std_single_neuron./(divideBy - size(all_labels,2))); - %% Get meaningful label from neuron name - better_label = process_spikesorting_supervised('GetChannelOfSpikeEvent', labelsForDropDownMenu{iNeuron}); - neuron = process_spikesorting_supervised('GetNeuronOfSpikeEvent', labelsForDropDownMenu{iNeuron}); + % Get meaningful label from neuron name + better_label = panel_spikes('GetChannelOfSpikeEvent', labelsNeurons{iNeuron}); + neuron = panel_spikes('GetNeuronOfSpikeEvent', labelsNeurons{iNeuron}); if ~isempty(neuron) better_label = [better_label ' #' num2str(neuron)]; end - - %% Fill the fields of the output files - tfOPTIONS.ParentFiles = {sInputs.FileName}; + + % ===== SAVE FILE ===== % Prepare output file structure - FileMat.F = STA_single_neuron'; - FileMat.Time = time_segmentAroundSpikes; + FileMat = db_template('datamat'); + FileMat.F = STA_single_neuron'; + FileMat.Time = time_segmentAroundSpikes; + FileMat.Std = 2 .* std_single_neuron; % MULTIPLY BY 2 TO GET 95% CONFIDENCE (ASSUMING NORMAL DISTRIBUTION) + FileMat.Comment = ['Spike Triggered Average: ' str_remove_parenth(DataMats{1}.Comment) ' (' better_label ')']; + FileMat.DataType = 'recordings'; + FileMat.ChannelFlag = ChannelFlag; + FileMat.Device = DataMats{1}.Device; + FileMat.nAvg = 1; + FileMat.History = DataMats{1}.History; - FileMat.Std = 2 .* std_single_neuron; % MULTIPLY BY 2 TO GET 95% CONFIDENCE (ASSUMING NORMAL DISTRIBUTION) - - FileMat.Comment = ['Spike Triggered Average: ' ... - str_remove_parenth(ALL_TRIALS_files(1).trial.Comment) ... - ' (' better_label ')']; - FileMat.DataType = 'recordings'; - - temp = in_bst(sInputs(1).FileName, 'ChannelFlag'); - FileMat.ChannelFlag = temp.ChannelFlag; - FileMat.Device = ALL_TRIALS_files(1).trial.Device; - FileMat.Events = []; - - FileMat.nAvg = 1; - FileMat.ColormapType = []; - FileMat.DisplayUnits = []; - FileMat.History = ALL_TRIALS_files(1).trial.History; - % Add history field - FileMat = bst_history('add', FileMat, 'compute', ... - ['Spike Triggered Average: [' num2str(tfOPTIONS.TimeWindow(1)) ', ' num2str(tfOPTIONS.TimeWindow(2)) '] ms']); - + FileMat = bst_history('add', FileMat, 'compute', ['Spike Triggered Average: [' num2str(TimeWindow(1)) ', ' num2str(TimeWindow(2)) '] ms']); + for iFile = 1:length(sInputs) + FileMat = bst_history('add', FileMat, 'average', [' - ' sInputs(iFile).FileName]); + end % Get output study - sTargetStudy = bst_get('Study', iStudy); + [tmp, iTargetStudy] = bst_process('GetOutputStudy', sProcess, sInputs); + sTargetStudy = bst_get('Study', iTargetStudy); % Output filename FileName = bst_process('GetNewFilename', bst_fileparts(sTargetStudy.FileName), 'data_STA'); OutputFiles = {FileName}; % Save output file and add to database bst_save(FileName, FileMat, 'v6'); - db_add_data(tfOPTIONS.iTargetStudy, FileName, FileMat); - - end - - - % Display report to user - bst_report('Info', sProcess, sInputs, 'Success'); - disp('BST> process_spike_triggered_average: Success'); - - - % Close parallel pool - if sProcess.options.paral.Value - if ~isempty(poolobj) - delete(poolobj); - end + db_add_data(iTargetStudy, FileName, FileMat); end end - - - - -function all = get_LFPs(trial, nChannels, sProcess, time_segmentAroundSpikes, sampling_rate, ChannelMat) - %% Get the events that show NEURONS' activity - - % Important Variable here! +%% ===== GET LFP ===== +% Get the events that show neurons activity +function all = get_LFPs(trial, ChannelMat, TimeWindow, time_segmentAroundSpikes, sampling_rate) spikeEvents = []; % The spikeEvents variable holds the indices of the events that correspond to spikes. - allChannelEvents = cellfun(@(x) process_spikesorting_supervised('GetChannelOfSpikeEvent', x), ... - {trial.Events.label}, 'UniformOutput', 0); - - for ielectrode = 1: nChannels %selectedChannels + allChannelEvents = cellfun(@(x) panel_spikes('GetChannelOfSpikeEvent', x), {trial.Events.label}, 'UniformOutput', 0); + for ielectrode = 1:length(ChannelMat.Channel) iEvents = find(strcmp(allChannelEvents, ChannelMat.Channel(ielectrode).Name)); % Find the index of the spike-events that correspond to that electrode (Exact string match) if ~isempty(iEvents) spikeEvents(end+1:end+length(iEvents)) = iEvents; end end + % Get segments around each spike, FOR EACH NEURON all = struct(); - %% Get segments around each spike, FOR EACH NEURON for iNeuron = 1:length(spikeEvents) % iNeuron is the iEvent - - % Check that the entire segment around the spikes [-150,150]ms - % is inside the trial segment and keep only those events - iSel = trial.Events(spikeEvents(iNeuron)).times > trial.Time(1) + abs(sProcess.options.timewindow.Value{1}(1)) & ... - trial.Events(spikeEvents(iNeuron)).times < trial.Time(end) - abs(sProcess.options.timewindow.Value{1}(2)); + % Check that the entire segment around the spikes [-150,150]ms is inside the trial segment and keep only those events + iSel = trial.Events(spikeEvents(iNeuron)).times > trial.Time(1) + abs(TimeWindow(1)) & ... + trial.Events(spikeEvents(iNeuron)).times < trial.Time(end) - abs(TimeWindow(2)); events_within_segment = round(trial.Events(spikeEvents(iNeuron)).times(iSel) .* sampling_rate); - %% Create a matrix that holds all the segments around the spike - % of that neuron, for all electrodes. + % Create a matrix that holds all the segments around the spike of that neuron, for all electrodes. allSpikeSegments_singleNeuron_singleTrial = zeros(length(events_within_segment),length(ChannelMat.Channel),length(time_segmentAroundSpikes)); - for ispike = 1:length(events_within_segment) - allSpikeSegments_singleNeuron_singleTrial(ispike,:,:) = trial.F(:, round(abs(trial.Time(1))*sampling_rate) + events_within_segment(ispike) - round(abs(sProcess.options.timewindow.Value{1}(1)) * sampling_rate) + 1: ... - round(abs(trial.Time(1))*sampling_rate) + events_within_segment(ispike) + round(abs(sProcess.options.timewindow.Value{1}(2)) * sampling_rate) + 1 ... - ); + allSpikeSegments_singleNeuron_singleTrial(ispike,:,:) = trial.F(:, ... + round(abs(trial.Time(1))*sampling_rate) + events_within_segment(ispike) - round(abs(TimeWindow(1)) * sampling_rate) + 1 : ... + round(abs(trial.Time(1))*sampling_rate) + events_within_segment(ispike) + round(abs(TimeWindow(2)) * sampling_rate) + 1); end all(iNeuron).label = trial.Events(spikeEvents(iNeuron)).label; @@ -357,16 +244,12 @@ all(iNeuron).avgLFP = squeeze(sum(allSpikeSegments_singleNeuron_singleTrial,1)); all(iNeuron).stdLFP = squeeze(std(allSpikeSegments_singleNeuron_singleTrial,[],1)); all(iNeuron).Used = 0; % This indicates if this entry has already been used for computing the SFC (some spikes might not appear on every trial imported, so a new Neuron should be identified on a later trial). - - end - %% Check if any events had no spikes in the time-region of interest and remove them! + % Check if any events had no spikes in the time-region of interest and remove them! % Some spikes might be on the edges of the trial. Ultimately, the % Spikes Channel i events would be considered in the STA (I mean the event group, not the events themselves), % but there would be a zeroed avgLFP included. Get rid of those events iEventsToRemove = find([all.nSpikes]==0); - all = all(~ismember(1:length(all),iEventsToRemove)); - -end \ No newline at end of file +end diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index 94221638bc..18c051db76 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -7,8 +7,6 @@ % for each electrode that can be used later for supervised spike-sorting. % When all spikes on all electrodes have been clustered, all the spikes for % each neuron is assigned to an events file in brainstorm format. -% -% USAGE: OutputFiles = process_spikesorting_kilosort('Run', sProcess, sInputs) % @============================================================================= % This function is part of the Brainstorm software: @@ -28,17 +26,19 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018-2022 +% Martin Cousineau, 2018 +% Francois Tadel, 2022 eval(macro_method); end %% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description the process sProcess.Comment = 'KiloSort'; - sProcess.Category = 'Custom'; + sProcess.Category = 'File'; sProcess.SubGroup = {'Electrophysiology','Unsupervised Spike Sorting'}; sProcess.Index = 1203; sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/SpikeSorting'; @@ -48,431 +48,394 @@ sProcess.nInputs = 1; sProcess.nMinFiles = 1; sProcess.isSeparator = 0; + % Spike sorter name sProcess.options.spikesorter.Type = 'text'; sProcess.options.spikesorter.Value = 'kilosort'; sProcess.options.spikesorter.Hidden = 1; - sProcess.options.GPU.Comment = 'GPU processing'; - sProcess.options.GPU.Type = 'checkbox'; - sProcess.options.GPU.Value = 0; + % RAM limitation sProcess.options.binsize.Comment = 'Maximum RAM to use: '; sProcess.options.binsize.Type = 'value'; sProcess.options.binsize.Value = {2, 'GB', 1}; - + % GPU + sProcess.options.GPU.Comment = 'GPU processing'; + sProcess.options.GPU.Type = 'checkbox'; + sProcess.options.GPU.Value = 0; + % Use SSP/ICA + sProcess.options.usessp.Comment = 'Apply the existing SSP/ICA projectors'; + sProcess.options.usessp.Type = 'checkbox'; + sProcess.options.usessp.Value = 1; + % Separator + sProcess.options.sep1.Type = 'label'; + sProcess.options.sep1.Comment = '
'; % Options: Edit parameters - sProcess.options.edit.Comment = {'panel_spikesorting_options', 'Parameters: '}; + sProcess.options.edit.Comment = {'panel_spikesorting_options', 'KiloSort parameters: '}; sProcess.options.edit.Type = 'editpref'; sProcess.options.edit.Value = []; - % Show warning that pre-spikesorted events will be overwritten - sProcess.options.warning.Comment = 'Spike Events created from the acquisition system will be overwritten'; + % Label: Reset options + sProcess.options.edit_help.Comment = 'To restore default options: re-install the kilosort plugin.'; + sProcess.options.edit_help.Type = 'label'; + % Label: Warning that pre-spikesorted events will be overwritten + sProcess.options.warning.Comment = '
Warning: Existing spike events will be overwritten'; sProcess.options.warning.Type = 'label'; end %% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok +function Comment = FormatComment(sProcess) Comment = sProcess.Comment; end %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInput) OutputFiles = {}; - ProtocolInfo = bst_get('ProtocolInfo'); + % ===== DEPENDENCIES ===== % Not available in the compiled version if bst_iscompiled() error('This function is not available in the compiled version of Brainstorm.'); end - % Check for Excel writer toolbox - TestExcel = 'excelWriterTest.xlsx'; - try - xlswrite(TestExcel, 1); - delete(TestExcel); - catch - bst_report('Error', sProcess, sInputs, 'This process requires Excel installed. Apologies to Linux users.'); - return; - end % Check for the Signal Processing toolbox if ~bst_get('UseSigProcToolbox') - bst_report('Error', sProcess, sInputs, 'This process requires the Signal Processing Toolbox.'); + bst_report('Error', sProcess, sInput, 'This process requires the Signal Processing Toolbox.'); return; end % Check for the Statistics toolbox if exist('cvpartition', 'file') ~= 2 - bst_report('Error', sProcess, sInputs, 'This process requires the Statistics and Machine Learning Toolbox.'); + bst_report('Error', sProcess, sInput, 'This process requires the Statistics and Machine Learning Toolbox.'); return; end - % Check for the Parallel Computing toolbox (external dependencies) + % Check for the Parallel Computing toolbox (external dependencies - Kilosort2NeuroSuite in kilosort-wrapper) if (exist('matlabpool', 'file') ~= 2) && (exist('parpool', 'file') ~= 2) - bst_report('Error', sProcess, sInputs, 'This process requires the Parallel Computing Toolbox.'); + bst_report('Error', sProcess, sInput, 'This process requires the Parallel Computing Toolbox.'); return; end + % Load plugin + [isInstalled, errMsg] = bst_plugin('Install', 'kilosort'); + if ~isInstalled + error(errMsg); + end + + % ===== OPTIONS ===== + % Get options + BinSize = sProcess.options.binsize.Value{1}; + UseSsp = sProcess.options.usessp.Value; + % Initialize KiloSort Parameters (This initially is a copy of StandardConfig_MOVEME) + KilosortStandardConfig(); + ops.GPU = sProcess.options.GPU.Value; - % Ensure we are including the KiloSort folder in the Matlab path - KiloSortDir = bst_fullfile(bst_get('BrainstormUserDir'), 'kilosort'); - if exist(KiloSortDir, 'file') - addpath(genpath(KiloSortDir)); + % File path + bst_progress('text', 'Kilosort: Reading input files...'); + [fPath, fBase] = bst_fileparts(file_fullpath(sInput.FileName)); + % Remove "data_0raw" or "data_" tag + if (length(fBase) > 10 && strcmp(fBase(1:10), 'data_0raw_')) + fBase = fBase(11:end); + elseif (length(fBase) > 5) && strcmp(fBase(1:5), 'data_') + fBase = fBase(6:end); end - - % Install KiloSort if missing - if ~exist('make_eMouseData.m', 'file') - rmpath(genpath(KiloSortDir)); - isOk = java_dialog('confirm', ... - ['The KiloSort spike-sorter is not installed on your computer.' 10 10 ... - 'Download and install the latest version?'], 'KiloSort'); - if ~isOk - bst_report('Error', sProcess, sInputs, 'This process requires the KiloSort spike-sorter.'); - return; - end - downloadAndInstallKiloSort(); + + % ===== LOAD INPUTS ===== + % Load input files + DataMat = in_bst_data(sInput.FileName, 'F'); + sFile = DataMat.F; + ChannelMat = in_bst_channel(sInput.ChannelFile); + + % Make sure we perform the spike sorting on the channels that have spikes. IS THIS REALLY NECESSARY? it would just take longer + numChannels = 0; + for iChannel = 1:length(ChannelMat.Channel) + if strcmp(ChannelMat.Channel(iChannel).Type,'EEG') || strcmp(ChannelMat.Channel(iChannel).Type,'SEEG') + numChannels = numChannels + 1; + end end - %% Prepare parallel pool - try - poolobj = gcp('nocreate'); - if isempty(poolobj) - parpool; + % ===== OUTPUT FOLDER ===== + outputPath = bst_fullfile(fPath, [fBase '_kilosort_spikes']); + previous_directory = pwd; + % If output folder already exists: delete it + if exist(outputPath, 'dir') == 7 + % Move Matlab out of the folder to be deleted + if ~isempty(strfind(previous_directory, outputPath)) + cd(bst_fileparts(outputPath)); + end + % Delete existing output folder + try + rmdir(outputPath, 's'); + catch + error(['Could not remove spikes folder: ' 10 outputPath 10 ' Make sure this folder is not open in another program (e.g. Klusters).']) end - catch - poolobj = []; end + % Create output folder + mkdir(outputPath); - %% Initialize KiloSort Parameters (This is a copy of StandardConfig_MOVEME) - KilosortStandardConfig(); - ops.GPU = sProcess.options.GPU.Value; + + % ===== DATA CONVERSION ===== + % Prepare the ChannelMat File + % This is a file that just contains information for the location of the electrodes. + Nchannels = numChannels; + connected = true(Nchannels, 1); + chanMap = 1:Nchannels; + chanMap0ind = chanMap - 1; + % Get the channels in the montage + % First check if any montages have been assigned + [Channels, Montages, channelsMontage,montageOccurences] = ParseMontage(ChannelMat); - %% Compute on each raw input independently - for i = 1:length(sInputs) - [fPath, fBase] = bst_fileparts(sInputs(i).FileName); - % Remove "data_0raw" or "data_" tag - if (length(fBase) > 10 && strcmp(fBase(1:10), 'data_0raw_')) - fBase = fBase(11:end); - elseif (length(fBase) > 5) && strcmp(fBase(1:5), 'data_') - fBase = fBase(6:end); - end - - DataMat = in_bst_data(sInputs(i).FileName, 'F'); - ChannelMat = in_bst_channel(sInputs(i).ChannelFile); - - - %% Make sure we perform the spike sorting on the channels that have spikes. IS THIS REALLY NECESSARY? it would just take longer - - numChannels = 0; - for iChannel = 1:length(ChannelMat.Channel) - if strcmp(ChannelMat.Channel(iChannel).Type,'EEG') || strcmp(ChannelMat.Channel(iChannel).Type,'SEEG') - numChannels = numChannels + 1; - end - end - - sFile = DataMat.F; - - - - %% %%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% - outputPath = bst_fullfile(ProtocolInfo.STUDIES, fPath, [fBase '_kilosort_spikes']); - - % Clear if directory already exists - if exist(outputPath, 'dir') == 7 - try - rmdir(outputPath, 's'); - catch - error('Couldnt remove spikes folder. Make sure the current directory is not that folder.') - end - end - - mkdir(outputPath); - - %% Prepare the ChannelMat File - % This is a file that just contains information for the location of - % the electrodes. - - Nchannels = numChannels; - connected = true(Nchannels, 1); - chanMap = 1:Nchannels; - chanMap0ind = chanMap - 1; - - - %% Use the same algorithm that I use for the 2d channel display for converting 3d to 2d - - Channels = ChannelMat.Channel; - - try - Montages = unique({Channels.Group}); - channelsMontage = zeros(1,length(Channels)); - montageOccurences = zeros(1,length(Montages)); - for iChannel = 1:length(Channels) - for iMontage = 1:length(Montages) - if strcmp(Channels(iChannel).Group, Montages{iMontage}) - channelsMontage(iChannel) = iMontage; - montageOccurences(iMontage) = montageOccurences(iMontage)+1; - end + % Adjust the possible clusters based on the number of channels + doubleChannels = 2*max(montageOccurences); % Each Montage will be treated as its own entity. + ops.Nfilt = ceil(doubleChannels/32)*32; % number of clusters to use (2-4 times more than Nchan, should be a multiple of 32) + + + % If the coordinates are assigned, convert 3d to 2d + if sum(sum([ChannelMat.Channel.Loc]))~=0 % If values are already assigned + alreadyAssignedLocations = 1; + else + alreadyAssignedLocations = 0; + end + + channelsCoords = zeros(length(Channels),3); % THE 3D COORDINATES + if alreadyAssignedLocations + for iChannel = 1:length(Channels) + for iMontage = 1:length(Montages) + if strcmp(Channels(iChannel).Group, Montages{iMontage}) + channelsCoords(iChannel,1:3) = Channels(iChannel).Loc; end end - - catch - Montages = 'All'; - - for iChannel = 1:length(Channels) - Channels(iChannel).Group = 'All'; - end - - montageOccurences = length(Channels); - channelsMontage = ones(1,length(Channels)); % This holds the code of the montage each channel holds end - - %% Adjust the possible clusters based on the number of channels - - doubleChannels = 2*max(montageOccurences); % Each Montage will be treated as its own entity. - ops.Nfilt = ceil(doubleChannels/32)*32; % number of clusters to use (2-4 times more than Nchan, should be a multiple of 32) - - - %% If the coordinates are assigned, convert 3d to 2d - - if sum(sum([ChannelMat.Channel.Loc]))~=0 % If values are already assigned - alreadyAssignedLocations = 1; - else - alreadyAssignedLocations = 0; + % APPLY TRANSORMATION TO A FLAT SURFACE (X-Y COORDINATES: IGNORE Z) + converted_coordinates = zeros(length(Channels),3); + for iMontage = 1:length(Montages) + single_array_coords = channelsCoords(channelsMontage==iMontage,:); + % SVD approach + [U, S, V] = svd(single_array_coords-mean(single_array_coords)); + lower_rank = 2;% Get only the first two components + converted_coordinates(channelsMontage==iMontage,:)=U(:,1:lower_rank)*S(1:lower_rank,1:lower_rank)*V(:,1:lower_rank)'+mean(single_array_coords); end - - - channelsCoords = zeros(length(Channels),3); % THE 3D COORDINATES - - if alreadyAssignedLocations - for iChannel = 1:length(Channels) - for iMontage = 1:length(Montages) - if strcmp(Channels(iChannel).Group, Montages{iMontage}) - channelsCoords(iChannel,1:3) = Channels(iChannel).Loc; - end - end - end - % APPLY TRANSORMATION TO A FLAT SURFACE (X-Y COORDINATES: IGNORE Z) - converted_coordinates = zeros(length(Channels),3); - for iMontage = 1:length(Montages) - single_array_coords = channelsCoords(channelsMontage==iMontage,:); - % SVD approach - [U, S, V] = svd(single_array_coords-mean(single_array_coords)); - lower_rank = 2;% Get only the first two components - converted_coordinates(channelsMontage==iMontage,:)=U(:,1:lower_rank)*S(1:lower_rank,1:lower_rank)*V(:,1:lower_rank)'+mean(single_array_coords); - end + xcoords = converted_coordinates(:,1); + ycoords = converted_coordinates(:,2); + else + xcoords = (1:length(Channels))'; + ycoords = ones(length(Channels),1); + end + + kcoords = channelsMontage'; % grouping of channels (i.e. tetrode groups) + fs = sFile.prop.sfreq; % sampling frequency - xcoords = converted_coordinates(:,1); - ycoords = converted_coordinates(:,2); - else - xcoords = [1:length(Channels)]'; - ycoords = ones(length(Channels),1); - end - - kcoords = channelsMontage'; % grouping of channels (i.e. tetrode groups) - fs = sFile.prop.sfreq; % sampling frequency + save(bst_fullfile(outputPath, 'chanMap.mat'), ... + 'chanMap','connected', 'xcoords', 'ycoords', 'kcoords', 'chanMap0ind', 'fs') + + + % Width of the spike-waveforms - NEEDS TO BE EVEN + ops.nt0 = 0.0017*fs; % Width of the spike Waveforms. (1.7ms) THIS NEEDS TO BE EVEN. AN ODD VALUE DOESN'T GIVE ANY WAVEFORMS (The Kilosort2Neurosuite Function doesn't accommodate odd numbers) + if mod(ops.nt0,2) + ops.nt0 =ops.nt0+1; + end + ops.nt0 = round(ops.nt0); % Rounding error if not force integer here + + % Case of less neighbors (default config file value) than actual channels + % For enabling PHY, make sure the value is less than the maximum + % number of channels (maybe equal is also OK, probably not) and not empty. +% ops.nNeighPC = []; % visualization only (Phy): number of channnels to mask the PCs, leave empty to skip (12) +% ops.nNeigh = []; + if ops.nNeighPC > numChannels + ops.nNeighPC = numChannels - 1; + ops.nNeigh = numChannels - 1; + end + + % Kilosort outputs a rez.mat file. The supervised part (Klusters) gets as input the rez file, and a .xml file (with parameters). + % Create .xml + xmlFile = bst_fullfile(outputPath, [fBase '.xml']); + CreateXML(ChannelMat, fs, xmlFile, ops); + + cd(outputPath); + + % Convert to the right input for KiloSort + converted_raw_File = ConvertForKilosort(sInput, BinSize * 1e9, UseSsp); % This converts into int16. + - save(bst_fullfile(outputPath, 'chanMap.mat'), ... - 'chanMap','connected', 'xcoords', 'ycoords', 'kcoords', 'chanMap0ind', 'fs') - - - %% Width of the spike-waveforms - NEEDS TO BE EVEN - ops.nt0 = 0.0017*fs; % Width of the spike Waveforms. (1.7ms) THIS NEEDS TO BE EVEN. AN ODD VALUE DOESN'T GIVE ANY WAVEFORMS (The Kilosort2Neurosuite Function doesn't accommodate odd numbers) - if mod(ops.nt0,2) - ops.nt0 =ops.nt0+1; + % ===== SPIKE SORTING ===== + bst_progress('text', 'Kilosort: Spike-sorting'); + % Some residual parameters that need the outputPath and the converted Raw signal + ops.fbinary = converted_raw_File; % will be created for 'openEphys' + ops.fproc = bst_fullfile(outputPath, 'temp_wh.bin'); % residual from RAM of preprocessed data % It was .dat, I changed it to .bin - Make sure this is correct + ops.chanMap = bst_fullfile(outputPath, 'chanMap.mat'); % make this file using createChannelMapFile.m + ops.root = outputPath; % 'openEphys' only: where raw files are + ops.basename = fBase; + ops.fs = fs; % sampling rate + ops.NchanTOT = numChannels; % total number of channels + ops.Nchan = numChannels; % number of active channels + + % Initialize GPU (will erase any existing GPU arrays) + if ops.GPU + gpuDevice(1); + end + + [rez, DATA, uproj] = preprocessData(ops); % preprocess data and extract spikes for initialization + try + rez = fitTemplates(rez, DATA, uproj); % fit templates iteratively + catch + if ops.GPU + % ~\.brainstorm\plugins\kilosort\KiloSort-master\CUD?\mexGPUall.m + % needs to be called and compile the .cu files. + % Suggested environment: Matlab 2018a, CUDA 9.0, VS 13. + bst_report('Error', sProcess, sInput, 'Error trying to spike-sort on the GPU. Have you set up CUDA correctly? Check https://github.com/cortex-lab/KiloSort for installation instructions'); + return; + else + bst_report('Error', sProcess, sInput, 'Error with Kilosort while training on the CPU'); + return; end + end - - - - %% Kilosort outputs a rez.mat file. The supervised part (Klusters) gets as input the rez file, and a .xml file (with parameters). - % I can create this .xml file from an excel file according to what - % the Buzsaki lab uses. - % The buzsaki lab has a converter for "intan" files. Using this: - - % Create .xml file (Compatible with Buzsaki lab inputs) - xml_filename = bst_fullfile(outputPath, [fBase '.xlsx']); + rez = fullMPMU(rez, DATA);% extract final spike times (overlapping extraction) + + % Save matlab results file + save(fullfile(ops.root, 'rez.mat'), 'rez', '-v7.3'); + % remove temporary file + delete(ops.fproc); - A1 = {'SEE DERIVATION BELOW','','','','','X','Y','','BY VERTICAL POSITION/SHANK (IE FOR DISPLAY)','','','Neuroscope Channel'; - 'Neronexus/ Omnetics site','Intan pin','Intan Channel','','','X Coordinates','Y Coordinates','','','Neuronexus/ Omnetics Site','Intan Pin','Intan Channel'}; - - uniqueKCoords = unique(kcoords)'; - nChannelsInMontage = cell(length(uniqueKCoords),1); - for iType = uniqueKCoords - nChannelsInMontage{iType} = find(kcoords==iType); - end + % Now convert the rez.mat and the .xml to Neuroscope format so it can be read from Klusters + % Downloaded from: https://github.com/brendonw1/KilosortWrapper + % This creates 4 types of files x Number of montages (Groups of electrodes) + % .clu: holds the cluster each spike belongs to + % .fet: holds the feature values of each spike + % .res: holds the spiketimes + % .spk: holds the spike waveforms + Kilosort2Neurosuite(rez) - ii = 0; - for iType = uniqueKCoords - for iChannel = nChannelsInMontage{iType}' % 1x96 - ii = ii+1; - A3{ii,1} = iChannel; - A3{ii,2} = iChannel-1; % Acquisition system codename - INTAN STARTS CHANNEL NUMBERING FROM 0. These .xlsx are made for INTAN I assume - A3{ii,3} = iChannel-1; - A3{ii,4} = ['SHANK ' num2str(iType)]; - A3{ii,5} = ''; - A3{ii,6} = xcoords(iChannel); % x coord - THIS PROBABLY SHOULD BE RELATIVE TO EACH ARRAY - NOT GLOBAL COORDINATES - A3{ii,7} = ycoords(iChannel); - A3{ii,8} = ''; - A3{ii,9} = ['SHANK ' num2str(iType)]; - A3{ii,10} = iChannel; % This is for the display - Neuronexus/Omnetics Site - A3{ii,11} = iChannel-1; % This is for the display - Intan Pin - A3{ii,12} = iChannel-1; % This is for the display - Intan Channel - end - end - - sheet = 1; - xlswrite(xml_filename,A1,sheet,'A1') - xlswrite(xml_filename,A3,sheet,'A3') - - - previous_directory = pwd; - cd(outputPath); - - - % Some defaults values I found in bz.MakeXMLFromProbeMaps - defaults.NumberOfChannels = length(kcoords); - defaults.SampleRate = fs; - defaults.BitsPerSample = 16; - defaults.VoltageRange = 20; - defaults.Amplification = 1000; - defaults.LfpSampleRate = 1250; - defaults.PointsPerWaveform = ops.nt0; - defaults.PeakPointInWaveform = 16; - defaults.FeaturesPerWave = 3; - - [tmp, xmlFileBase] = bst_fileparts(xml_filename); - bz_MakeXMLFromProbeMaps({xmlFileBase}, '','',1,defaults) % This creates a Barcode_f096_kilosort_spikes.xml - weird_xml_filename = dir('*.xml'); - [tmp, weird_xml_fileBase] = bst_fileparts(weird_xml_filename.name); - file_move([weird_xml_fileBase '.xml'],[xmlFileBase '.xml']); % Barcode_f096.xml - - - %% Convert to the right input for KiloSort - - bst_progress('start', 'KiloSort spike-sorting', 'Converting to KiloSort Input...'); - - converted_raw_File = in_spikesorting_convertforkilosort(sInputs(i), sProcess.options.binsize.Value{1} * 1e9); % This converts into int16. - - %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% - bst_progress('text', 'Spike-sorting...'); - - - - %% Some residual parameters that need the outputPath and the converted Raw signal - ops.fbinary = converted_raw_File; % will be created for 'openEphys' - ops.fproc = bst_fullfile(outputPath, 'temp_wh.bin'); % residual from RAM of preprocessed data % It was .dat, I changed it to .bin - Make sure this is correct - ops.chanMap = bst_fullfile(outputPath, 'chanMap.mat'); % make this file using createChannelMapFile.m - ops.root = outputPath; % 'openEphys' only: where raw files are - ops.basename = xmlFileBase; - ops.fs = fs; % sampling rate - ops.NchanTOT = numChannels; % total number of channels - ops.Nchan = numChannels; % number of active channels - - - %% KiloSort - if ops.GPU - gpuDevice(1); % initialize GPU (will erase any existing GPU arrays) - end - - - [rez, DATA, uproj] = preprocessData(ops); % preprocess data and extract spikes for initialization - rez = fitTemplates(rez, DATA, uproj); % fit templates iteratively - rez = fullMPMU(rez, DATA);% extract final spike times (overlapping extraction) - - %% save matlab results file - save(fullfile(ops.root, 'rez.mat'), 'rez', '-v7.3'); - % remove temporary file - delete(ops.fproc); + % Restore current folder + cd(previous_directory); + - - - %% Now convert the rez.mat and the .xml to Neuroscope format so it can be read from Klusters - % Downloaded from: https://github.com/brendonw1/KilosortWrapper - % This creates 4 types of files x Number of montages (Groups of electrodes) - % .clu: holds the cluster each spike belongs to - % .fet: holds the feature values of each spike - % .res: holds the spiketimes - % .spk: holds the spike waveforms - - - Kilosort2Neurosuite(rez) - - - - %% %%%%%%%%%%%%%%%%%%% Create Brainstorm Events %%%%%%%%%%%%%%%%%%% - - bst_progress('text', 'Saving events file...'); - - % Delete existing spike events - process_spikesorting_supervised('DeleteSpikeEvents', sInputs(i).FileName); - - sFile.RawFile = sInputs(i).FileName; - convertKilosort2BrainstormEvents(sFile, ChannelMat, bst_fullfile(ProtocolInfo.STUDIES, fPath), rez); - - cd(previous_directory); - - % Fetch FET files - spikes = []; - if ~iscell(Montages) - Montages = {Montages}; + % ===== IMPORT EVENTS ===== + bst_progress('text', 'Saving events file...'); + + % Delete existing spike events + panel_spikes('DeleteSpikeEvents', sInput.FileName); + % Add events to file + sFile.RawFile = sInput.FileName; + ImportKilosortEvents(sFile, ChannelMat, fPath, rez); + + % Fetch FET files + spikes = []; + if ~iscell(Montages) + Montages = {Montages}; + end + for iMontage = 1:length(Montages) + fetFile = dir(bst_fullfile(outputPath, ['*.fet.' num2str(iMontage)])); + if isempty(fetFile) + continue; end - for iMontage = 1:length(Montages) - fetFile = dir(bst_fullfile(outputPath, ['*.fet.' num2str(iMontage)])); - if isempty(fetFile) - continue; - end - curStruct = struct(); - curStruct.Path = outputPath; - curStruct.File = fetFile.name; - curStruct.Name = Montages{iMontage}; - curStruct.Mod = 0; - if isempty(spikes) - spikes = curStruct; - else - spikes(end+1) = curStruct; - end + curStruct = struct(); + curStruct.Path = outputPath; + curStruct.File = fetFile.name; + curStruct.Name = Montages{iMontage}; + curStruct.Mod = 0; + if isempty(spikes) + spikes = curStruct; + else + spikes(end+1) = curStruct; end - - % ===== SAVE LINK FILE ===== - % Build output filename - NewBstFile = bst_fullfile(ProtocolInfo.STUDIES, fPath, ['data_0ephys_' fBase '.mat']); - % Build output structure - DataMat = struct(); - %DataMat.F = sFile; - DataMat.Comment = 'KiloSort Spike Sorting'; - DataMat.DataType = 'raw';%'ephys'; - DataMat.Device = 'KiloSort'; - DataMat.Parent = outputPath; - DataMat.Spikes = spikes; - DataMat.RawFile = sInputs(i).FileName; - DataMat.Name = NewBstFile; - % Add history field - DataMat = bst_history('add', DataMat, 'import', ['Link to unsupervised electrophysiology files: ' outputPath]); - % Save file on hard drive - bst_save(NewBstFile, DataMat, 'v6'); - % Add file to database - sOutputStudy = db_add_data(sInputs(i).iStudy, NewBstFile, DataMat); - % Return new file - OutputFiles{end+1} = NewBstFile; - - % ===== UPDATE DATABASE ===== - % Update links - db_links('Study', sInputs(i).iStudy); - panel_protocols('UpdateNode', 'Study', sInputs(i).iStudy); - end - - %%%%%%%%%%%%%%%%%%%%%% Prepare to exit %%%%%%%%%%%%%%%%%%%%%%% - % Turn off parallel processing and return to the initial directory - if ~isempty(poolobj) - delete(poolobj); end + + % ===== SAVE SPIKE FILE ===== + % Build output filename + NewBstFilePrefix = bst_fullfile(fPath, ['data_0ephys_kilo_' fBase]); + NewBstFile = [NewBstFilePrefix '.mat']; + iFile = 1; + commentSuffix = ''; + while exist(NewBstFile, 'file') == 2 + iFile = iFile + 1; + NewBstFile = [NewBstFilePrefix '_' num2str(iFile) '.mat']; + commentSuffix = [' (' num2str(iFile) ')']; + end + % Build output structure + DataMat_spikesorter = struct(); + DataMat_spikesorter.Comment = ['KiloSort Spike Sorting' commentSuffix]; + DataMat_spikesorter.DataType = 'raw';%'ephys'; + DataMat_spikesorter.Device = 'KiloSort'; + DataMat_spikesorter.Parent = outputPath; + DataMat_spikesorter.Spikes = spikes; + DataMat_spikesorter.RawFile = sInput.FileName; + DataMat_spikesorter.Name = NewBstFile; + % Add history field + DataMat_spikesorter = bst_history('add', DataMat_spikesorter, 'import', ['Link to unsupervised electrophysiology files: ' outputPath]); + % Save file on hard drive + bst_save(NewBstFile, DataMat_spikesorter, 'v6'); + % Add file to database + db_add_data(sInput.iStudy, file_short(NewBstFile), DataMat_spikesorter); + % Return new file + OutputFiles{end+1} = NewBstFile; + + % ===== UPDATE DATABASE ===== + % Update links + db_links('Study', sInput.iStudy); + panel_protocols('UpdateNode', 'Study', sInput.iStudy); end +%% ===== CONVERT FOR KILOSORT ===== +% Loads and creates separate raw electrode files for KiloSort (int16 file with no header) +function converted_raw_File = ConvertForKilosort(sInput, ram, UseSsp) + % Output folder + ProtocolInfo = bst_get('ProtocolInfo'); + parentPath = bst_fullfile(bst_get('BrainstormTmpDir'), 'Unsupervised_Spike_Sorting', ProtocolInfo.Comment, sInput.FileName); + % Check if file already converted + converted_raw_File = bst_fullfile(parentPath, ['raw_data_no_header_' sInput.Condition(5:end) '.dat']); + if exist(converted_raw_File, 'file') == 2 + disp('BST> File already converted to KiloSort input.') + return + end + % Make sure the temporary directory exists, otherwise create it + if ~exist(parentPath, 'dir') + mkdir(parentPath); + end + % Apply SSP/ICA when reading from data files + ImportOptions = db_template('ImportOptions'); + ImportOptions.UseCtfComp = 0; + ImportOptions.UseSsp = UseSsp; + % Load input files + DataMat = in_bst_data(sInput.FileName, 'F'); + sFile = DataMat.F; + ChannelMat = in_bst_channel(sInput.ChannelFile); + + % Separate the file to max length based on RAM + numChannels = length(ChannelMat.Channel); + max_samples = ram / 8 / numChannels; % Double precision + total_samples = round((sFile.prop.times(2) - sFile.prop.times(1)) .* sFile.prop.sfreq); + num_segments = ceil(total_samples / max_samples); + num_samples_per_segment = ceil(total_samples / num_segments); + + % Progress bar + bst_progress('start', 'KiloSort spike sorting', 'Converting to int16 .dat file', 0, num_segments); + % Open file + fid = fopen(converted_raw_File, 'a'); + % Loop on segments + for iSegment = 1:num_segments + sampleBounds(1) = (iSegment - 1) * num_samples_per_segment + round(sFile.prop.times(1)* sFile.prop.sfreq); + if iSegment < num_segments + sampleBounds(2) = sampleBounds(1) + num_samples_per_segment - 1; + else + sampleBounds(2) = total_samples + round(sFile.prop.times(1)* sFile.prop.sfreq); + end + % Read recordings + F = in_fread(sFile, ChannelMat, [], sampleBounds, [], ImportOptions); + % Adaptive conversion to int16 to avoid saturation + max_abs_value = max([abs(max(max(F))) abs(min(min(F)))]); + F = int16(F ./ max_abs_value * 15000); % The choice of 15000 for maximum is in part abstract - for 32567 the clusters look weird + % Write to .dat file + fwrite(fid, F, 'int16'); + % Increment progress bar + bst_progress('inc', 1); + end + % Close file + fclose(fid); +end -function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) - events = struct(); - index = 0; - +%% ===== IMPORT KILOSORT EVENTS ===== +function ImportKilosortEvents(sFile, ChannelMat, parentPath, rez) % st: first column is the spike time in samples, % second column is the spike template, % third column is the extracted amplitude, @@ -490,39 +453,41 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) [tmp, amplitude_max_channel(i)] = max(range(templates(:,:,i)')); %CHANNEL WHERE EACH TEMPLATE HAS THE BIGGEST AMPLITUDE end - % I assign each spike on the channel that it has the highest amplitude for the template it was matched with amplitude_max_channel = amplitude_max_channel'; - spike2ChannelAssignment = amplitude_max_channel(spikeTemplates); - spikeEventPrefix = process_spikesorting_supervised('GetSpikesEventPrefix'); + spikeEventPrefix = panel_spikes('GetSpikesEventPrefix'); + index = 0; + events_spikes = struct(); % Fill the events fields for iCluster = 1:length(unique(spikeTemplates)) selectedSpikes = find(spikeTemplates==uniqueClusters(iCluster)); - index = index+1; % Write the packet to events + events_spikes(index).color = rand(1,3); + events_spikes(index).epochs = ones(1,length(spikeTimes(selectedSpikes))); + events_spikes(index).times = spikeTimes(selectedSpikes)'./sFile.prop.sfreq + sFile.prop.times(1); + events_spikes(index).reactTimes = []; + events_spikes(index).select = 1; + events_spikes(index).notes = cell(1, size(events_spikes(index).times, 2)); + if uniqueClusters(iCluster)==1 || uniqueClusters(iCluster)==0 - events(index).label = ['Spikes Noise |' num2str(uniqueClusters(iCluster)) '|']; + events_spikes(index).label = ['Spikes Noise |' num2str(uniqueClusters(iCluster)) '|']; + events_spikes(index).channels = cell(1, size(events_spikes(index).times, 2)); else - events(index).label = [spikeEventPrefix ' ' ChannelMat.Channel(amplitude_max_channel(uniqueClusters(iCluster))).Name ' |' num2str(uniqueClusters(iCluster)) '|']; + events_spikes(index).label = [spikeEventPrefix ' ' ChannelMat.Channel(amplitude_max_channel(uniqueClusters(iCluster))).Name ' |' num2str(uniqueClusters(iCluster)) '|']; + events_spikes(index).channels = repmat({{ChannelMat.Channel(amplitude_max_channel(uniqueClusters(iCluster))).Name}}, 1, size(events_spikes(index).times, 2)); end - events(index).color = rand(1,3); - events(index).epochs = ones(1,length(spikeTimes(selectedSpikes))); - events(index).times = spikeTimes(selectedSpikes)' ./ sFile.prop.sfreq; % The timestamps are in SAMPLES - events(index).reactTimes = []; - events(index).select = 1; - events(index).channels = cell(1, size(events(index).times, 2)); - events(index).notes = cell(1, size(events(index).times, 2)); end + index = 0; % Add existing non-spike events for backup DataMat = in_bst_data(sFile.RawFile); existingEvents = DataMat.F.events; for iEvent = 1:length(existingEvents) - if ~process_spikesorting_supervised('IsSpikeEvent', existingEvents(iEvent).label) + if ~panel_spikes('IsSpikeEvent', existingEvents(iEvent).label) if index == 0 events = existingEvents(iEvent); else @@ -531,173 +496,28 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) index = index + 1; end end - - save(fullfile(parentPath,'events_UNSUPERVISED.mat'),'events') -end - - -%% ===== DOWNLOAD AND INSTALL KiloSort ===== -function downloadAndInstallKiloSort() - - % Kilosort just does unsupervised clustering. In order to visualize the - % clusters and perform supervised clustering, you need to download a - % python software called Phy. So 3 things are needed: - % 1. KiloSort - % 2. Phy - % 3. npy-matlab that enables input-output from Matlab to Python - - KiloSortDir = bst_fullfile(bst_get('BrainstormUserDir'), 'kilosort'); - KiloSortTmpDir = bst_fullfile(bst_get('BrainstormUserDir'), 'kilosort_tmp'); - - % If folders exists: delete - if isdir(KiloSortDir) - file_delete(KiloSortDir, 1, 3); - end - - % Create folders - mkdir(KiloSortDir); - if ~isdir(KiloSortTmpDir) - mkdir(KiloSortTmpDir); - end - - - % Download KiloSort - url_KiloSort = 'https://github.com/cortex-lab/KiloSort/archive/master.zip'; - KiloSortZipFile = bst_fullfile(KiloSortTmpDir, 'kilosort.zip'); - if exist(KiloSortZipFile, 'file') ~= 2 - errMsg = gui_brainstorm('DownloadFile', url_KiloSort, KiloSortZipFile, 'KiloSort download'); - - % Check if the download was succesful and try again if it wasn't - time_before_entering = clock; - updated_time = clock; - time_out = 60;% timeout within 60 seconds of trying to download the file - - % Keep trying to download until a timeout is reached - while etime(updated_time, time_before_entering) time_out && ~isempty(errMsg) - error(['Impossible to download KiloSort.' 10 errMsg]); - end - end - - - % Download KiloSortWrapper (For conversion to Neurosuite - Klusters) - url_KiloSort_wrapper = 'https://github.com/brendonw1/KilosortWrapper/archive/master.zip'; - KiloSortWrapperZipFile = bst_fullfile(KiloSortTmpDir, 'kilosort_wrapper.zip'); - if exist(KiloSortWrapperZipFile, 'file') ~= 2 - errMsg = gui_brainstorm('DownloadFile', url_KiloSort_wrapper, KiloSortWrapperZipFile, 'KiloSortWrapper download'); - - % Check if the download was succesful and try again if it wasn't - time_before_entering = clock; - updated_time = clock; - time_out = 60;% timeout within 60 seconds of trying to download the file - - % Keep trying to download until a timeout is reached - while etime(updated_time, time_before_entering) time_out && ~isempty(errMsg) - error(['Impossible to download KiloSortWrapper.' 10 errMsg]); - end - end - % Download Phy - url_Phy = 'https://github.com/kwikteam/phy/archive/master.zip'; - PhyZipFile = bst_fullfile(KiloSortTmpDir, 'phy.zip'); - if exist(PhyZipFile, 'file') ~= 2 - errMsg = gui_brainstorm('DownloadFile', url_Phy, PhyZipFile, 'Phy download'); - - % Check if the download was succesful and try again if it wasn't - time_before_entering = clock; - updated_time = clock; - time_out = 60;% timeout within 60 seconds of trying to download the file - - % Keep trying to download until a timeout is reached - while etime(updated_time, time_before_entering) time_out && ~isempty(errMsg) - error(['Impossible to download Phy.' 10 errMsg]); - end + if ~isempty(existingEvents) + events = [events events_spikes]; + else + events = events_spikes; end - % Download npy-matlab - url_npy = 'https://github.com/kwikteam/npy-matlab/archive/master.zip'; - npyZipFile = bst_fullfile(KiloSortTmpDir, 'npy.zip'); - if exist(npyZipFile, 'file') ~= 2 - errMsg = gui_brainstorm('DownloadFile', url_npy, npyZipFile, 'npy-matlab download'); - - % Check if the download was succesful and try again if it wasn't - time_before_entering = clock; - updated_time = clock; - time_out = 60;% timeout within 60 seconds of trying to download the file - - % Keep trying to download until a timeout is reached - while etime(updated_time, time_before_entering) time_out && ~isempty(errMsg) - error(['Impossible to download npy-matlab.' 10 errMsg]); - end - end + save(fullfile(parentPath,'events_UNSUPERVISED.mat'),'events') - % Unzip KiloSort zip-file - bst_progress('start', 'KiloSort', 'Installing KiloSort...'); - unzip(KiloSortZipFile, KiloSortTmpDir); - % Move KiloSort directory to proper location - file_move(bst_fullfile(KiloSortTmpDir, 'KiloSort-master'), ... - bst_fullfile(KiloSortDir, 'kilosort')); - % Copy config file - copyKilosortConfig(bst_fullfile(KiloSortDir, 'kilosort', 'configFiles', 'StandardConfig_MOVEME.m'), ... - bst_fullfile(KiloSortDir, 'KilosortStandardConfig.m')); - - % Unzip KiloSort Wrapper zip-file - unzip(KiloSortWrapperZipFile, KiloSortTmpDir); - % Move KiloSort Wrapper directory to proper location - file_move(bst_fullfile(KiloSortTmpDir, 'KilosortWrapper-master'), ... - bst_fullfile(KiloSortDir, 'wrapper')); - - % Unzip Phy zip-file - unzip(PhyZipFile, KiloSortTmpDir); - % Move Phy directory to proper location - file_move(bst_fullfile(KiloSortTmpDir, 'phy-master'), ... - bst_fullfile(KiloSortDir, 'phy')); - - - % Unzip npy-matlab zip-file - unzip(npyZipFile, KiloSortTmpDir); - % Move npy directory to proper location - file_move(bst_fullfile(KiloSortTmpDir, 'npy-matlab-master'), ... - bst_fullfile(KiloSortDir, 'npy')); - - % Delete unnecessary files - file_delete(KiloSortTmpDir, 1, 3); - % Add KiloSort to Matlab path - addpath(genpath(KiloSortDir)); + % Assign the unsupervised spike sorted events to the link to raw file + DataMat.F.events = events; + [folder, filename_link2Raw, extension] = bst_fileparts(sFile.RawFile); + bst_save(bst_fullfile(parentPath, [filename_link2Raw extension]), DataMat, 'v6'); end -function events = LoadKlustersEvents(SpikeSortedMat, iMontage) + +%% ===== LOAD KLUSTERS EVENTS ===== +function [events, Channels] = LoadKlustersEvents(SpikeSortedMat, iMontage) % Information about the Neuroscope file can be found here: % http://neurosuite.sourceforge.net/formats.html - %% Load necessary files + % Load necessary files ChannelMat = in_bst_channel(bst_get('ChannelFileForStudy', SpikeSortedMat.RawFile)); DataMat = in_bst_data(SpikeSortedMat.RawFile, 'F'); sFile = DataMat.F; @@ -706,14 +526,14 @@ function downloadAndInstallKiloSort() [tmp, study] = fileparts(study); sMontage = num2str(iMontage); clu = load(bst_fullfile(SpikeSortedMat.Parent, [study '.clu.' sMontage])); - res = load(bst_fullfile(SpikeSortedMat.Parent, [study '.res.' sMontage])); fet = dlmread(bst_fullfile(SpikeSortedMat.Parent, [study '.fet.' sMontage])); - - ChannelsInMontage = ChannelMat.Channel(strcmp({ChannelMat.Channel.Group},SpikeSortedMat.Spikes(iMontage).Name)); % Only the channels from the Montage should be loaded here to be used in the spike-events - + % Get the channels that belong in the selected montage + [Channels, Montages, channelsMontage,montageOccurences] = ParseMontage(ChannelMat); + ChannelsInMontage = ChannelMat.Channel(channelsMontage == iMontage); % Only the channels from the Montage should be loaded here to be used in the spike-events - %% The combination of the .clu files and the .fet file is enough to use on the converter. + + % The combination of the .clu files and the .fet file is enough to use on the converter. % Brainstorm assign each spike to a SINGLE NEURON on each electrode. This % converter picks up the electrode that showed the strongest (absolute) @@ -736,7 +556,7 @@ function downloadAndInstallKiloSort() events = struct(); index = 0; - spikesPrefix = process_spikesorting_supervised('GetSpikesEventPrefix'); + spikesPrefix = panel_spikes('GetSpikesEventPrefix'); uniqueClusters = unique(clu(2:end))'; % The first entry is just the number of clusters @@ -748,21 +568,26 @@ function downloadAndInstallKiloSort() index = index+1; % Write the packet to events + events(index).color = rand(1,3); + events(index).times = fet(selectedSpikes,end)' ./ sFile.prop.sfreq + sFile.prop.times(1); + events(index).epochs = ones(1,length(events(index).times)); + events(index).reactTimes = []; + events(index).select = 1; + events(index).notes = cell(1, size(events(index).times, 2)); + if uniqueClusters(iCluster)==1 || uniqueClusters(iCluster)==0 - events(index).label = ['Spikes Noise |' num2str(uniqueClusters(iCluster)) '|']; + events(index).label = ['Spikes Noise |' num2str(uniqueClusters(iCluster)) '|']; + events(index).channels = cell(1, size(events(index).times, 2)); else - events(index).label = [spikesPrefix ' ' ChannelsInMontage(iElectrode).Name ' |' num2str(uniqueClusters(iCluster)) '|']; + events(index).label = [spikesPrefix ' ' ChannelsInMontage(iElectrode).Name ' |' num2str(uniqueClusters(iCluster)) '|']; + events(index).channels = repmat({{ChannelsInMontage(iElectrode).Name}}, 1, size(events(index).times, 2)); end - events(index).color = rand(1,3); - events(index).times = fet(selectedSpikes,end)' ./ sFile.prop.sfreq; % The timestamps are in SAMPLES - events(index).epochs = ones(1,length(events(index).times)); - events(index).reactTimes = []; - events(index).select = 1; - events(index).channels = cell(1, size(events(index).times, 2)); - events(index).notes = cell(1, size(events(index).times, 2)); end end + +%% ===== COPY KILOSORT CONFIG ===== +% Called by bst_plugin after installing the kilosort plugin function copyKilosortConfig(defaultFile, outputFile) if exist(outputFile, 'file') == 2 delete(outputFile); @@ -783,3 +608,257 @@ function copyKilosortConfig(defaultFile, outputFile) fclose(inFid); fclose(outFid); end + + +%% ===== CREATE XML ===== +function CreateXML(ChannelMat, Fs, xmlFile, ops) + % Kilosort is designed to be used on shanks - this is like a probe + % The users need to assign specific channels to specific shanks. + % The following code takes into account several cases that can be + % encountered: e.g. all channels already assigned to groups, none, or + % partially + % Sequentially, an .xml file with metadata is populated to be used in Klusters + + %% First check if any montages have been assigned + allMontages = {ChannelMat.Channel.Group}; + nEmptyMontage = length(find(cellfun(@isempty,allMontages))); + + if nEmptyMontage == length(ChannelMat.Channel) + keepChannels = find(ismember({ChannelMat.Channel.Type}, 'EEG') | ismember({ChannelMat.Channel.Type}, 'SEEG')); + + % No montages have been assigned. Assign all EEG/SEEG channels to a + % single montage + for iChannel = 1:length(ChannelMat.Channel) + if strcmp(ChannelMat.Channel(iChannel).Type, 'EEG') || strcmp(ChannelMat.Channel(iChannel).Type, 'SEEG') + ChannelMat.Channel(iChannel).Group = 'GROUP1'; % Just adding an entry here + end + end + temp_ChannelsMat = ChannelMat.Channel(keepChannels); + + elseif nEmptyMontage == 0 + keepChannels = 1:length(ChannelMat.Channel); + temp_ChannelsMat = ChannelMat.Channel(keepChannels); + else + % ADD AN EXTRA MONTAGE FOR CHANNELS THAT HAVENT BEEN ASSIGNED TO A MONTAGE + for iChannel = 1:length(ChannelMat.Channel) + if isempty(ChannelMat.Channel(iChannel).Group) + ChannelMat.Channel(iChannel).Group = 'EMPTYGROUP'; % Just adding an entry here + end + temp_ChannelsMat = ChannelMat.Channel; + end + end + + montages = unique({temp_ChannelsMat.Group},'stable'); + montages = montages(find(~cellfun(@isempty, montages))); + + NumChansPerProbe = []; + ChannelsInMontage = cell(length(montages),2); + for iMontage = 1:length(montages) + ChannelsInMontage{iMontage,1} = ChannelMat.Channel(strcmp({ChannelMat.Channel.Group}, montages{iMontage})); % Only the channels from the Montage should be loaded here to be used in the spike-events + + for iChannel = 1:length(ChannelsInMontage{iMontage}) + ChannelsInMontage{iMontage,2} = [ChannelsInMontage{iMontage,2} find(strcmp({ChannelMat.Channel.Name}, ChannelsInMontage{iMontage}(iChannel).Name))]; + end + NumChansPerProbe = [NumChansPerProbe length(ChannelsInMontage{iMontage,2})]; + end + nMontages = length(montages); + + %% Define text components to assemble later + + chunk1 = {'';... + '';... + ' ';... + ' 16 '}; + + channelcountlinestart = ' '; + channelcountlineend = ''; + + chunk2 = {[' ' num2str(Fs) ''];... + ' 20';... + ' 1000';... + ' 0';... + ' ';... + ' ';... + % % % % % [' ' num2str(defaults.LfpSampleRate) ''];... + ' 1250';... + ' ';... + ' ';... + ' ';... + ' lfp';... + % % % % % [' ' num2str(defaults.LfpSampleRate) ''];... + ' 1250';... + ' ';... + % ' ';... + % ' whl';... + % ' 39.0625';... + % ' ';... + ' ';... + ' ';... + ' '}; + + anatomygroupstart = ' ';%repeats w every new anatomical group + anatomychannelnumberline_start = ' ';%for each channel in an anatomical group - first part of entry + anatomychannelnumberline_end = '';%for each channel in an anatomical group - last part of entry + anatomygroupend = ' ';%comes at end of each anatomical group + + chunk3 = {' ';... + '';... + '';... + ' '};%comes after anatomical groups and before spike groups + + spikegroupstart = {' ';... + ' '};%repeats w every new spike group + spikechannelnumberline_start = ' ';%for each channel in a spike group - first part of entry + spikechannelnumberline_end = '';%for each channel in a spike group - last part of entry + spikegroupend = {' ';... + % [' ' num2str(defaults.PointsPerWaveform) ''];... + % [' ' num2str(defaults.PeakPointInWaveform) ''];... + % [' ' num2str(defaults.FeaturesPerWave) ''];... + [' ' num2str(ops.nt0) ''];... + ' 16';... + ' 3';... + ' '};%comes at end of each spike group + + chunk4 = {' ';... + '';... + '';... + '';... + '0.2';... + '';... + '';... + '';... + '';... + '';... + ''}; + + channelcolorstart = ' ';... + channelcolorlinestart = ' '; + channelcolorlineend = ''; + channelcolorend = {' #0080ff';... + ' #0080ff';... + ' #0080ff';... + ' '}; + + channeloffsetstart = ' '; + channeloffsetlinestart = ' '; + channeloffsetlineend = ''; + channeloffsetend = {' 0';... + ' '}; + + chunk5 = { '';... + '';... + ''}; + + + %% Make basic text + s = chunk1; + s = cat(1,s,[channelcountlinestart, num2str(length(ChannelMat.Channel)) channelcountlineend]); + s = cat(1,s,chunk2); + + for iMontage = 1:nMontages %for each probe + s = cat(1,s,anatomygroupstart); + for iChannelWithinMontage = 1:NumChansPerProbe(iMontage)%for each spike group + thischan = ChannelsInMontage{iMontage,2}(iChannelWithinMontage) - 1; + s = cat(1,s,[anatomychannelnumberline_start, num2str(thischan) anatomychannelnumberline_end]); + end + s = cat(1,s,anatomygroupend); + end + + s = cat(1,s,chunk3); + + for iMontage = 1:nMontages + s = cat(1,s,spikegroupstart); + for iChannelWithinMontage = 1:NumChansPerProbe(iMontage) + thischan = ChannelsInMontage{iMontage,2}(iChannelWithinMontage) - 1; + s = cat(1,s,[spikechannelnumberline_start, num2str(thischan) spikechannelnumberline_end]); + end + s = cat(1,s,spikegroupend); + end + + s = cat(1,s, chunk4); + + for iMontage = 1:nMontages + for iChannelWithinMontage = 1:NumChansPerProbe(iMontage) + s = cat(1,s,channelcolorstart); + thischan = ChannelsInMontage{iMontage,2}(iChannelWithinMontage) - 1; + s = cat(1,s,[channelcolorlinestart, num2str(thischan) channelcolorlineend]); + s = cat(1,s,channelcolorend); + s = cat(1,s,channeloffsetstart); + s = cat(1,s,[channeloffsetlinestart, num2str(thischan) channeloffsetlineend]); + s = cat(1,s,channeloffsetend); + end + end + + s = cat(1,s, chunk5); + + % Output + charcelltotext(s, xmlFile); +end + + +%% ===== CHAR CELL TO TEXT ==== +function charcelltotext(charcell,filename) + %based on matlab help. Writes each row of the character cell (charcell) to a line of + %text in the filename specified by "filename". Char should be a cell array + %with format of a 1 column with many rows, each row with a single string of text. + fid = fopen(filename, 'w'); + for row = 1:size(charcell, 1) + fprintf(fid, '%s \n', charcell{row,:}); + end + fclose(fid); +end + + +%% ===== GET MONTAGE ===== +% Get the channels in the montage +function [Channels, Montages, channelsMontage, montageOccurences] = ParseMontage(ChannelMat) + % First check if any montages have been assigned + Channels = ChannelMat.Channel; + allMontages = {Channels.Group}; + nEmptyMontage = length(find(cellfun(@isempty,allMontages))); + + if nEmptyMontage == length(Channels) + keepChannels = find(ismember({Channels.Type}, 'EEG') | ismember({Channels.Type}, 'SEEG')); + + % No montages have been assigned. Assign all EEG/SEEG channels to a single montage + for iChannel = 1:length(Channels) + if strcmp(Channels(iChannel).Type, 'EEG') || strcmp(ChannelMat.Channel(iChannel).Type, 'SEEG') + Channels(iChannel).Group = 'All'; % Just adding an entry here + end + end + temp_ChannelsMat = Channels(keepChannels); + + elseif nEmptyMontage == 0 + keepChannels = 1:length(Channels); + temp_ChannelsMat = Channels(keepChannels); + else + % ADD AN EXTRA MONTAGE FOR CHANNELS THAT HAVENT BEEN ASSIGNED TO A MONTAGE + for iChannel = 1:length(Channels) + if isempty(Channels(iChannel).Group) + Channels(iChannel).Group = 'EMPTYGROUP'; % Just adding an entry here + end + temp_ChannelsMat = Channels; + end + end + + Montages = unique({temp_ChannelsMat.Group},'stable'); + Montages = Montages(find(~cellfun(@isempty, Montages))); + + channelsMontage = zeros(1,length(Channels)); + montageOccurences = zeros(1,length(Montages)); + for iChannel = 1:length(Channels) + for iMontage = 1:length(Montages) + if strcmp(Channels(iChannel).Group, Montages{iMontage}) + channelsMontage(iChannel) = iMontage; + montageOccurences(iMontage) = montageOccurences(iMontage)+1; + end + end + end +end + + diff --git a/toolbox/process/functions/process_spikesorting_supervised.m b/toolbox/process/functions/process_spikesorting_supervised.m deleted file mode 100644 index fc9ba8e9e7..0000000000 --- a/toolbox/process/functions/process_spikesorting_supervised.m +++ /dev/null @@ -1,563 +0,0 @@ -function varargout = process_spikesorting_supervised( varargin ) -% PROCESS_SPIKESORTING_SUPERVISED: -% This process opens up a supervised Spike Sorting program allowing for -% manual correction of unsupervised spike sorted events. -% -% USAGE: OutputFiles = process_spikesorting_supervised('Run', sProcess, sInputs) - -% @============================================================================= -% This function is part of the Brainstorm software: -% https://neuroimage.usc.edu/brainstorm -% -% Copyright (c) University of Southern California & McGill University -% This software is distributed under the terms of the GNU General Public License -% as published by the Free Software Foundation. Further details on the GPLv3 -% license can be found at http://www.gnu.org/copyleft/gpl.html. -% -% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE -% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY -% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF -% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY -% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. -% -% For more information type "brainstorm license" at command prompt. -% =============================================================================@ -% -% Authors: Martin Cousineau, 2018 - -eval(macro_method); -end - - -%% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok - % Description the process - sProcess.Comment = 'Supervised spike sorting'; - sProcess.Category = 'Custom'; - sProcess.SubGroup = 'Electrophysiology'; - sProcess.Index = 1202; - sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/SpikeSorting#Supervised_Spike_Sorting'; - % Definition of the input accepted by this process - sProcess.InputTypes = {'raw'}; - sProcess.OutputTypes = {'raw'}; - sProcess.nInputs = 1; - sProcess.nMinFiles = 1; - sProcess.isSeparator = 1; -end - - -%% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok - Comment = sProcess.Comment; -end - - -%% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok - global GlobalData; - OutputFiles = {}; - ProtocolInfo = bst_get('ProtocolInfo'); - - % Compute on each raw input independently - for i = 1:length(sInputs) - sInput = sInputs(i); - DataMat = in_bst_data(sInput.FileName); - - % Make sure spikes exist and were generated by WaveClus - if ~isfield(DataMat, 'Spikes') || ~isstruct(DataMat.Spikes) ... - || ~isfield(DataMat, 'Parent') ... - || exist(DataMat.Parent, 'dir') ~= 7 ... - || isempty(dir(DataMat.Parent)) - bst_report('Error', sProcess, sInput, ... - 'No spikes found. Make sure to run the unsupervised Spike Sorter first.'); - return; - end - - switch lower(DataMat.Device) - case 'waveclus' - % Ensure we are including the WaveClus folder in the Matlab path - waveclusDir = bst_fullfile(bst_get('BrainstormUserDir'), 'waveclus'); - if exist(waveclusDir, 'file') - addpath(genpath(waveclusDir)); - end - - % Install WaveClus if missing - if ~exist('wave_clus_font', 'file') - rmpath(genpath(waveclusDir)); - isOk = java_dialog('confirm', ... - ['The WaveClus spike-sorter is not installed on your computer.' 10 10 ... - 'Download and install the latest version?'], 'WaveClus'); - if ~isOk - bst_report('Error', sProcess, sInputs, 'This process requires the WaveClus spike-sorter.'); - return; - end - process_spikesorting_waveclus('downloadAndInstallWaveClus'); - end - - case 'ultramegasort2000' - % Ensure we are including the UltraMegaSort2000 folder in the Matlab path - UltraMegaSort2000Dir = bst_fullfile(bst_get('BrainstormUserDir'), 'UltraMegaSort2000'); - if exist(UltraMegaSort2000Dir, 'file') - addpath(genpath(UltraMegaSort2000Dir)); - end - - if ~exist('ss_default_params', 'file') - rmpath(genpath(UltraMegaSort2000Dir)); - isOk = java_dialog('confirm', ... - ['The UltraMegaSort2000 spike-sorter is not installed on your computer.' 10 10 ... - 'Download and install the latest version?'], 'UltraMegaSort2000'); - if ~isOk - bst_report('Error', sProcess, sInputs, 'This process requires the UltraMegaSort2000 spike-sorter.'); - return; - end - process_spikesorting_ultramegasort2000('downloadAndInstallUltraMegaSort2000'); - end - - case 'kilosort' - KlustersExecutable = bst_get('KlustersExecutable'); - if isempty(KlustersExecutable) || exist(KlustersExecutable, 'file') ~= 2 - % Try a common places for Klusters to be installed - commonPaths = {'C:\Program Files (x86)\Klusters\bin\klusters.exe', ... - 'C:\Program Files\Klusters\bin\klusters.exe'}; - foundPath = 0; - for iPath = 1:length(commonPaths) - if exist(commonPaths{iPath}, 'file') == 2 - KlustersExecutable = commonPaths{iPath}; - bst_set('KlustersExecutable', KlustersExecutable); - foundPath = 1; - break; - end - end - % If we cannot find it, prompt user - if ~foundPath - [res, isCancel] = java_dialog('question', ... - ['

We cannot find an installation of Klusters on your computer.
', ... - 'Would you like to download it or look for the Klusters executable yourself?'], ... - 'Klusters executable', [], {'Download', 'Pick executable', 'Cancel'}, 'Cancel'); - if isCancel || isempty(res) || strcmpi(res, 'Cancel') - return; - end - if strcmpi(res, 'Download') - % Display web page - klusters_url = 'http://neurosuite.sourceforge.net/'; - status = web(klusters_url, '-browser'); - if (status ~= 0) - web(klusters_url); - end - return; - end - % For Windows, look for EXE files. - if ~isempty(strfind(bst_get('OsType'), 'win')) - filters = {'*.exe', 'Klusters executable (*.exe)'}; - else - filters = {'*', 'Klusters executable'}; - end - KlustersExecutable = java_getfile('open', 'Klusters executable', [], 'single', 'files', filters, {}); - if isempty(KlustersExecutable) || exist(KlustersExecutable, 'file') ~= 2 - return; - end - end - bst_set('KlustersExecutable', KlustersExecutable); - end - - otherwise - bst_error('The chosen spike sorter is currently unsupported by Brainstorm.'); - end - - CloseFigure(); - - GlobalData.SpikeSorting = struct(); - GlobalData.SpikeSorting.Data = DataMat; - GlobalData.SpikeSorting.Selected = 0; - GlobalData.SpikeSorting.Fig = -1; - - gui_brainstorm('ShowToolTab', 'Spikes'); - OpenFigure(); - panel_spikes('UpdatePanel'); - end - -end - -function OpenFigure() - global GlobalData; - - bst_progress('start', 'Spike Sorting', 'Loading spikes...'); - CloseFigure(); - - GlobalData.SpikeSorting.Selected = GetNextElectrode(); - - electrodeFile = bst_fullfile(... - GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Path, ... - GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).File); - - switch lower(GlobalData.SpikeSorting.Data.Device) - case 'waveclus' - GlobalData.SpikeSorting.Fig = wave_clus(electrodeFile); - - % Some Wave Clus visual hacks - load_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'load_data_button'); - if ishandle(load_button) - load_button.Visible = 'off'; - end - save_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'save_clusters_button'); - if ishandle(save_button) - save_button.Visible = 'off'; - end - - case 'ultramegasort2000' - DataMat = load(electrodeFile, 'spikes'); - GlobalData.SpikeSorting.Fig = figure('Units', 'Normalized', 'Position', ... - DataMat.spikes.params.display.default_figure_size); - % Just open figure, rest of the code in LoadElectrode() - - case 'kilosort' - % Do nothing. - - otherwise - bst_error('This spike sorting structure is currently unsupported by Brainstorm.'); - end - - panel_spikes('UpdatePanel'); - LoadElectrode(); - - % Close Spike panel when you close the figure - function my_closereq(src, callbackdata) - delete(src); - panel_spikes('UpdatePanel'); - end - if FigureIsOpen() - GlobalData.SpikeSorting.Fig.CloseRequestFcn = @my_closereq; - end - - bst_progress('stop'); -end - -function isOpen = FigureIsOpen(varargin) - global GlobalData; - - if nargin < 1 - lenient = 0; - else - lenient = varargin{1}; - end - - isOpen = isfield(GlobalData, 'SpikeSorting') ... - && (isfield(GlobalData.SpikeSorting, 'Fig') ... - && ishandle(GlobalData.SpikeSorting.Fig)) ... - || (lenient && strcmpi(GlobalData.SpikeSorting.Data.Device, 'kilosort')); - % For KiloSort, we're not sure it is open since it's outside matlab... -end - -function CloseFigure() - global GlobalData; - if ~FigureIsOpen() - return; - end - - close(GlobalData.SpikeSorting.Fig); - panel_spikes('UpdatePanel'); -end - -function LoadElectrode() - global GlobalData; - if ~FigureIsOpen(1) - return; - end - - electrodeFile = bst_fullfile(... - GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Path, ... - GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).File); - - switch lower(GlobalData.SpikeSorting.Data.Device) - case 'waveclus' - wave_clus('load_data_button_Callback', GlobalData.SpikeSorting.Fig, ... - electrodeFile, guidata(GlobalData.SpikeSorting.Fig)); - - name_text = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'file_name'); - if ishandle(name_text) - name_text.String = panel_spikes('GetSpikeName', GlobalData.SpikeSorting.Selected); - end - - case 'ultramegasort2000' - % Reload figure altogether, same behavior as builtin load... - DataMat = load(electrodeFile, 'spikes'); - clf(GlobalData.SpikeSorting.Fig, 'reset'); - splitmerge_tool(DataMat.spikes, 'all', GlobalData.SpikeSorting.Fig); - - % Some UMS2k visual hacks - save_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'saveButton'); - if ishandle(save_button) - save_button.Visible = 'off'; - end - save_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'saveFileButton'); - if ishandle(save_button) - save_button.Visible = 'off'; - end - load_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'loadFileButton'); - if ishandle(load_button) - load_button.Visible = 'off'; - end - - case 'kilosort' - KlustersExecutable = bst_get('KlustersExecutable'); - status = system(['"' KlustersExecutable, '" "', electrodeFile, '" &']); - if status ~= 0 - bst_error('An error has occurred, could not start Klusters.'); - end - - otherwise - bst_error('This spike sorting structure is currently unsupported by Brainstorm.'); - end -end - -function SaveElectrode() - global GlobalData; - - if ~FigureIsOpen(1) - return; - end - - electrodeFile = bst_fullfile(... - GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Path, ... - GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).File); - - % Save through Spike Sorting software - switch lower(GlobalData.SpikeSorting.Data.Device) - case 'waveclus' - % WaveClus takes a screenshot of the figure when saving, which - % is pretty slow. If we change the figure tag it skips this. - save_button = findall(GlobalData.SpikeSorting.Fig, 'Tag', 'save_clusters_button'); - fig_tag = GlobalData.SpikeSorting.Fig.Tag; - GlobalData.SpikeSorting.Fig.Tag = 'wave_clus_tmp'; - wave_clus('save_clusters_button_Callback', save_button, ... - [], guidata(GlobalData.SpikeSorting.Fig), 0); - GlobalData.SpikeSorting.Fig.Tag = fig_tag; - - case 'ultramegasort2000' - figdata = get(GlobalData.SpikeSorting.Fig, 'UserData'); - spikes = figdata.spikes; - save(electrodeFile, 'spikes'); - OutMat = struct(); - OutMat.pathname = GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Path; - OutMat.filename = GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).File; - set(figdata.sfb, 'UserData', OutMat); - - case 'kilosort' - % Do nothing. - - otherwise - bst_error('This spike sorting structure is currently unsupported by Brainstorm.'); - end - - % Save updated brainstorm file - GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Mod = 1; - bst_save(GlobalData.SpikeSorting.Data.Name, GlobalData.SpikeSorting.Data, 'v6'); - - % Add event to linked raw file - CreateSpikeEvents(GlobalData.SpikeSorting.Data.RawFile, ... - GlobalData.SpikeSorting.Data.Device, ... - electrodeFile, ... - GlobalData.SpikeSorting.Data.Spikes(GlobalData.SpikeSorting.Selected).Name, ... - 1); -end - -function nextElectrode = GetNextElectrode() - global GlobalData; - if ~isfield(GlobalData, 'SpikeSorting') ... - || ~isfield(GlobalData.SpikeSorting, 'Selected') ... - || isempty(GlobalData.SpikeSorting.Selected) - GlobalData.SpikeSorting.Selected = 0; - end - - numSpikes = length(GlobalData.SpikeSorting.Data.Spikes); - nextElectrode = []; - - if GlobalData.SpikeSorting.Selected < numSpikes - nextElectrode = GlobalData.SpikeSorting.Selected + 1; - while nextElectrode <= numSpikes && ... - isempty(GlobalData.SpikeSorting.Data.Spikes(nextElectrode).File) - nextElectrode = nextElectrode + 1; - end - end - if isempty(nextElectrode) || nextElectrode > numSpikes || isempty(GlobalData.SpikeSorting.Data.Spikes(nextElectrode).File) - nextElectrode = GlobalData.SpikeSorting.Selected; - end -end - -function newEvents = CreateSpikeEvents(rawFile, deviceType, electrodeFile, electrodeName, import, eventNamePrefix) - global GlobalData; - if nargin < 6 - eventNamePrefix = ''; - elseif ~isempty(eventNamePrefix) - eventNamePrefix = [eventNamePrefix ' ']; - end - newEvents = struct(); - DataMat = in_bst_data(rawFile); - eventName = [eventNamePrefix GetSpikesEventPrefix() ' ' electrodeName]; - gotEvents = 0; - - % Load spike data and convert to Brainstorm event format - switch lower(deviceType) - case 'waveclus' - if exist(electrodeFile, 'file') == 2 - ElecData = load(electrodeFile, 'cluster_class'); - neurons = unique(ElecData.cluster_class(ElecData.cluster_class(:,1) > 0,1)); - numNeurons = length(neurons); - tmpEvents = struct(); - if numNeurons == 1 - tmpEvents(1).epochs = ones(1, sum(ElecData.cluster_class(:,1) ~= 0)); - tmpEvents(1).times = ElecData.cluster_class(ElecData.cluster_class(:,1) ~= 0, 2)' ./ 1000; - else - for iNeuron = 1:numNeurons - tmpEvents(iNeuron).epochs = ones(1, length(ElecData.cluster_class(ElecData.cluster_class(:,1) == iNeuron, 1))); - tmpEvents(iNeuron).times = ElecData.cluster_class(ElecData.cluster_class(:,1) == iNeuron, 2)' ./ 1000; - end - end - else - numNeurons = 0; - end - - case 'ultramegasort2000' - ElecData = load(electrodeFile, 'spikes'); - ElecData.spikes.spiketimes = double(ElecData.spikes.spiketimes); - numNeurons = size(ElecData.spikes.labels,1); - tmpEvents = struct(); - if numNeurons == 1 - tmpEvents(1).epochs = ones(1,length(ElecData.spikes.assigns)); - tmpEvents(1).times = ElecData.spikes.spiketimes; - elseif numNeurons > 1 - for iNeuron = 1:numNeurons - tmpEvents(iNeuron).epochs = ones(1,length(ElecData.spikes.assigns(ElecData.spikes.assigns == ElecData.spikes.labels(iNeuron,1)))); - tmpEvents(iNeuron).times = ElecData.spikes.spiketimes(ElecData.spikes.assigns == ElecData.spikes.labels(iNeuron,1)); - end - end - - case 'kilosort' - newEvents = process_spikesorting_kilosort('LoadKlustersEvents', ... - GlobalData.SpikeSorting.Data, GlobalData.SpikeSorting.Selected); - gotEvents = 1; - - otherwise - bst_error('This spike sorting structure is currently unsupported by Brainstorm.'); - end - - if ~gotEvents - if numNeurons == 1 - newEvents(1).label = eventName; - newEvents(1).color = [rand(1,1), rand(1,1), rand(1,1)]; - newEvents(1).epochs = tmpEvents(1).epochs; - newEvents(1).times = tmpEvents(1).times; - newEvents(1).reactTimes = []; - newEvents(1).select = 1; - newEvents(1).channels = cell(1, size(newEvents(1).times, 2)); - newEvents(1).notes = cell(1, size(newEvents(1).times, 2)); - elseif numNeurons > 1 - for iNeuron = 1:numNeurons - newEvents(iNeuron).label = [eventName ' |' num2str(iNeuron) '|']; - newEvents(iNeuron).color = [rand(1,1), rand(1,1), rand(1,1)]; - newEvents(iNeuron).epochs = tmpEvents(iNeuron).epochs; - newEvents(iNeuron).times = tmpEvents(iNeuron).times; - newEvents(iNeuron).reactTimes = []; - newEvents(iNeuron).select = 1; - newEvents(iNeuron).channels = cell(1, size(newEvents(iNeuron).times, 2)); - newEvents(iNeuron).notes = cell(1, size(newEvents(iNeuron).times, 2)); - end - else - % This electrode just picked up noise, no event to add. - newEvents(1).label = eventName; - newEvents(1).color = [rand(1,1), rand(1,1), rand(1,1)]; - newEvents(1).epochs = []; - newEvents(1).times = []; - newEvents(1).reactTimes = []; - newEvents(1).select = 1; - newEvents(1).channels = cell(1, size(newEvents(1).times, 2)); - newEvents(1).notes = cell(1, size(newEvents(1).times, 2)); - end - end - - if import - ProtocolInfo = bst_get('ProtocolInfo'); - % Add event to linked raw file - numEvents = length(DataMat.F.events); - % Delete existing event(s) - if numEvents > 0 - iDelEvents = cellfun(@(x) ~isempty(x), strfind({DataMat.F.events.label}, strtrim(eventName))); - DataMat.F.events = DataMat.F.events(~iDelEvents); - numEvents = length(DataMat.F.events); - end - % Add as new event(s); - for iEvent = 1:length(newEvents) - DataMat.F.events(numEvents + iEvent) = newEvents(iEvent); - end - bst_save(bst_fullfile(ProtocolInfo.STUDIES, rawFile), DataMat, 'v6'); - end -end - -function prefix = GetSpikesEventPrefix() - prefix = 'Spikes Channel'; -end - -function isSpikeEvent = IsSpikeEvent(eventLabel) - prefix = GetSpikesEventPrefix(); - isSpikeEvent = strncmp(eventLabel, prefix, length(prefix)); -end - -function neuron = GetNeuronOfSpikeEvent(eventLabel) - markers = strfind(eventLabel, '|'); - if length(markers) > 1 - neuron = str2num(eventLabel(markers(end-1)+1:markers(end)-1)); - else - neuron = []; - end -end - -function channel = GetChannelOfSpikeEvent(eventLabel) - if ~IsSpikeEvent(eventLabel) - channel = []; - return; - end - - eventLabel = strtrim(eventLabel); - prefix = GetSpikesEventPrefix(); - neuron = GetNeuronOfSpikeEvent(eventLabel); - bounds = [length(prefix) + 2, 0]; % 'Spikes Channel ' - - if ~isempty(neuron) - bounds(2) = length(num2str(neuron)) + 3; % ' |31|' - end - - try - channel = eventLabel(bounds(1):end-bounds(2)); - catch - channel = []; - end -end - -function isFirst = IsFirstNeuron(eventLabel, onlyIsFirst) - % onlyIsFirst = We assume a channel with a single neuron counts as a first neuron. - if nargin < 2 - onlyIsFirst = 1; - end - - neuron = GetNeuronOfSpikeEvent(eventLabel); - isFirst = neuron == 1; - if onlyIsFirst && isempty(neuron) - isFirst = 1; - end -end - -function DeleteSpikeEvents(rawFile) - ProtocolInfo = bst_get('ProtocolInfo'); - DataMat = in_bst_data(rawFile); - events = DataMat.F.events; - iKeepEvents = []; - - for iEvent = 1:length(events) - if ~IsSpikeEvent(events(iEvent).label) - iKeepEvents(end+1) = iEvent; - end - end - - DataMat.F.events = DataMat.F.events(iKeepEvents); - bst_save(bst_fullfile(ProtocolInfo.STUDIES, rawFile), DataMat, 'v6'); -end diff --git a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m index 4b204e1df5..eb9c17f9af 100644 --- a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m +++ b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m @@ -7,8 +7,6 @@ % for each electrode that can be used later for supervised spike-sorting. % When all spikes on all electrodes have been clustered, all the spikes for % each neuron is assigned to an events file in brainstorm format. -% -% USAGE: OutputFiles = process_spikesorting_ultramegasort2000('Run', sProcess, sInputs) % @============================================================================= % This function is part of the Brainstorm software: @@ -28,7 +26,9 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018-2019; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018-2022 +% Martin Cousineau, 2018 +% Francois Tadel, 2022 eval(macro_method); end @@ -38,7 +38,7 @@ function sProcess = GetDescription() %#ok % Description the process sProcess.Comment = 'UltraMegaSort2000'; - sProcess.Category = 'Custom'; + sProcess.Category = 'File'; sProcess.SubGroup = {'Electrophysiology','Unsupervised Spike Sorting'}; sProcess.Index = 1202; sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/SpikeSorting'; @@ -48,18 +48,25 @@ sProcess.nInputs = 1; sProcess.nMinFiles = 1; sProcess.isSeparator = 0; + % Spike sorter name sProcess.options.spikesorter.Type = 'text'; sProcess.options.spikesorter.Value = 'ultramegasort2000'; sProcess.options.spikesorter.Hidden = 1; + % RAM limitation sProcess.options.binsize.Comment = 'Maximum RAM to use: '; sProcess.options.binsize.Type = 'value'; sProcess.options.binsize.Value = {2, 'GB', 1}; - sProcess.options.paral.Comment = 'Parallel processing'; - sProcess.options.paral.Type = 'checkbox'; - sProcess.options.paral.Value = 0; - % ==== Parameters - sProcess.options.label1.Comment = '
Filtering parameters:'; - sProcess.options.label1.Type = 'label'; + % Parallel processing + sProcess.options.parallel.Comment = 'Parallel processing'; + sProcess.options.parallel.Type = 'checkbox'; + sProcess.options.parallel.Value = 0; + % Use SSP/ICA + sProcess.options.usessp.Comment = 'Apply the existing SSP/ICA projectors'; + sProcess.options.usessp.Type = 'checkbox'; + sProcess.options.usessp.Value = 1; + % Separator + sProcess.options.sep1.Type = 'label'; + sProcess.options.sep1.Comment = '
'; % === Low bound sProcess.options.highpass.Comment = 'Lower cutoff frequency:'; sProcess.options.highpass.Type = 'value'; @@ -68,15 +75,19 @@ sProcess.options.lowpass.Comment = 'Upper cutoff frequency:'; sProcess.options.lowpass.Type = 'value'; sProcess.options.lowpass.Value = {5000,'Hz ',0}; + % Separator + sProcess.options.sep2.Type = 'label'; + sProcess.options.sep2.Comment = '
'; % Options: Options - sProcess.options.edit.Comment = {'panel_spikesorting_options', 'Parameters: '}; + sProcess.options.edit.Comment = {'panel_spikesorting_options', 'UltraMegaSort2000 parameters: '}; sProcess.options.edit.Type = 'editpref'; sProcess.options.edit.Value = []; - - % Show warning that pre-spikesorted events will be overwritten - sProcess.options.warning.Comment = 'Spike Events created from the acquisition system will be overwritten'; + % Label: Reset options + sProcess.options.edit_help.Comment = 'To restore default options: re-install the ultramegasort plugin.'; + sProcess.options.edit_help.Type = 'label'; + % Label: Warning that pre-spikesorted events will be overwritten + sProcess.options.warning.Comment = '
Warning: Existing spike events will be overwritten'; sProcess.options.warning.Type = 'label'; - end @@ -87,239 +98,175 @@ %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInput) %#ok OutputFiles = {}; - ProtocolInfo = bst_get('ProtocolInfo'); - + + % ===== DEPENDENCIES ===== % Not available in the compiled version if bst_iscompiled() - bst_report('Error', sProcess, sInputs, 'This function is not available in the compiled version of Brainstorm.'); + bst_report('Error', sProcess, sInput, 'This function is not available in the compiled version of Brainstorm.'); return end - if sProcess.options.binsize.Value{1} <= 0 - bst_report('Error', sProcess, sInputs, 'Invalid maximum amount of RAM specified.'); - return + % Load plugin + [isInstalled, errMsg] = bst_plugin('Install', 'ultramegasort2000'); + if ~isInstalled + error(errMsg); end - - % Ensure we are including the UltraMegaSort2000 folder in the Matlab path - UltraMegaSort2000Dir = bst_fullfile(bst_get('BrainstormUserDir'), 'UltraMegaSort2000'); - if exist(UltraMegaSort2000Dir, 'file') - addpath(genpath(UltraMegaSort2000Dir)); + + % ===== OPTIONS ===== + % Get option: bin size + BinSize = sProcess.options.binsize.Value{1}; + if (BinSize <= 0) + bst_report('Error', sProcess, sInput, 'Invalid maximum amount of RAM specified.'); + return end + % Get other options + isParallel = sProcess.options.parallel.Value; + UseSsp = sProcess.options.usessp.Value; + LowPass = sProcess.options.lowpass.Value{1}(1); + HighPass = sProcess.options.highpass.Value{1}(1); - % Install UltraMegaSort2000 if missing - if ~exist('UltraMegaSort2000 Manual.pdf', 'file') - rmpath(genpath(UltraMegaSort2000Dir)); - isOk = java_dialog('confirm', ... - ['The UltraMegaSort2000 spike-sorter is not installed on your computer.' 10 10 ... - 'Download and install the latest version?'], 'UltraMegaSort2000'); - if ~isOk - bst_report('Error', sProcess, sInputs, 'This process requires the UltraMegaSort2000 spike-sorter.'); - return; - end - downloadAndInstallUltraMegaSort2000(); + % ===== LOAD INPUTS ===== + % Get protocol info + ProtocolInfo = bst_get('ProtocolInfo'); + BrainstormTmpDir = bst_get('BrainstormTmpDir'); + % File path + [fPath, fBase] = bst_fileparts(file_fullpath(sInput.FileName)); + % Remove "data_0raw" or "data_" tag + if (length(fBase) > 10 && strcmp(fBase(1:10), 'data_0raw_')) + fBase = fBase(11:end); + elseif (length(fBase) > 5) && strcmp(fBase(1:5), 'data_') + fBase = fBase(6:end); end - % Compute on each raw input independently - for i = 1:length(sInputs) - [fPath, fBase] = bst_fileparts(sInputs(i).FileName); - % Remove "data_0raw" or "data_" tag - if (length(fBase) > 10 && strcmp(fBase(1:10), 'data_0raw_')) - fBase = fBase(11:end); - elseif (length(fBase) > 5) && strcmp(fBase(1:5), 'data_') - fBase = fBase(6:end); - end - - DataMat = in_bst_data(sInputs(i).FileName, 'F'); - sFile = DataMat.F; - ChannelMat = in_bst_channel(sInputs(i).ChannelFile); - numChannels = length(ChannelMat.Channel); - sFiles = in_spikesorting_rawelectrodes(sInputs(i), ... - sProcess.options.binsize.Value{1} * 1e9, ... - sProcess.options.paral.Value); - - % Prepare parallel pool, if requested - if sProcess.options.paral.Value - try - poolobj = gcp('nocreate'); - if isempty(poolobj) - parpool; - end - catch - sProcess.options.paral.Value = 0; - poolobj = []; - end - else - poolobj = []; + % Load input files + DataMat = in_bst_data(sInput.FileName, 'F'); + sFile = DataMat.F; + % Check filtering frequencies + nyq = floor(sFile.prop.sfreq/2); + if (LowPass >= nyq) + bst_report('Error', sProcess, sInput, ['Higher cutoff frequency must be lower than Nyquist frequency (' num2str(nyq) ' Hz).']); + return; + elseif (HighPass >= LowPass) + bst_report('Error', sProcess, sInput, 'Higher cutoff frequency must be lower lower cutoff frequency.'); + return; + end + % Load channel file + ChannelMat = in_bst_channel(sInput.ChannelFile); + numChannels = length(ChannelMat.Channel); + % Demultiplex channels + demultiplexDir = bst_fullfile(BrainstormTmpDir, 'Unsupervised_Spike_Sorting', ProtocolInfo.Comment, sInput.FileName); + elecFiles = out_demultiplex(sInput.FileName, sInput.ChannelFile, demultiplexDir, UseSsp, BinSize * 1e9, isParallel); + + % ===== OUTPUT FOLDER ===== + outputPath = bst_fullfile(fPath, [fBase '_ums2k_spikes']); + previous_directory = pwd; + % If output folder already exists: delete it + if exist(outputPath, 'dir') == 7 + % Move Matlab out of the folder to be deleted + if ~isempty(strfind(previous_directory, outputPath)) + cd(bst_fileparts(outputPath)); end - - %%%%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% - outputPath = bst_fullfile(ProtocolInfo.STUDIES, fPath, [fBase '_ums2k_spikes']); - - % Clear if directory already exists - if exist(outputPath, 'dir') == 7 + % Delete existing output folder + try rmdir(outputPath, 's'); + catch + error(['Could not remove spikes folder: ' 10 outputPath 10 ' Make sure this folder is not open in another program.']) end - mkdir(outputPath); - - %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% - if sProcess.options.paral.Value - bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); - else - bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); - end - - %% UltraMegaSort2000 needs manual filtering of the raw files - - Fs = sFile.prop.sfreq; - - % The Get_spikes saves the _spikes files at the current directory. - previous_directory = pwd; - cd(outputPath); + end + % Create output folder + mkdir(outputPath); + - if sProcess.options.paral.Value - parfor ielectrode = 1:numChannels - do_UltraMegaSorting(sFiles{ielectrode}, sFile, sProcess.options.lowpass, sProcess.options.highpass, Fs); - end - else - for ielectrode = 1:numChannels - do_UltraMegaSorting(sFiles{ielectrode}, sFile, sProcess.options.lowpass, sProcess.options.highpass, Fs); - bst_progress('inc', 1); - end - end - - %%%%%%%%%%%%%%%%%%%%% Create Brainstorm Events %%%%%%%%%%%%%%%%%%% - bst_progress('text', 'Saving events file...'); - cd(previous_directory); - - % Delete existing spike events - process_spikesorting_supervised('DeleteSpikeEvents', sInputs(i).FileName); - - % ===== SAVE LINK FILE ===== - % Build output filename - NewBstFilePrefix = bst_fullfile(ProtocolInfo.STUDIES, fPath, ['data_0ephys_ums2k_' fBase]); - NewBstFile = [NewBstFilePrefix '.mat']; - iFile = 1; - commentSuffix = ''; - while exist(NewBstFile, 'file') == 2 - iFile = iFile + 1; - NewBstFile = [NewBstFilePrefix '_' num2str(iFile) '.mat']; - commentSuffix = [' (' num2str(iFile) ')']; + % ===== SPIKE SORTING ===== + cd(outputPath); + if isParallel + bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); + parfor ielectrode = 1:numChannels + do_UltraMegaSorting(elecFiles{ielectrode}, sFile, LowPass, HighPass, sFile.prop.sfreq); end - % Build output structure - DataMat = struct(); - DataMat.Comment = ['UltraMegaSort2000 Spike Sorting' commentSuffix]; - DataMat.DataType = 'raw';%'ephys'; - DataMat.Device = 'ultramegasort2000'; - DataMat.Name = NewBstFile; - DataMat.Parent = outputPath; - DataMat.RawFile = sInputs(i).FileName; - DataMat.Spikes = struct(); - % Build spikes structure - spikes = dir(bst_fullfile(outputPath, 'times_raw_elec*.mat')); - spikes = sort_nat({spikes.name}); - for iSpike = 1:length(spikes) - DataMat.Spikes(iSpike).Path = outputPath; - DataMat.Spikes(iSpike).File = spikes{iSpike}; - if exist(bst_fullfile(outputPath, DataMat.Spikes(iSpike).File), 'file') ~= 2 - DataMat.Spikes(iSpike).File = ''; - end - DataMat.Spikes(iSpike).Name = ChannelMat.Channel(iSpike).Name; - DataMat.Spikes(iSpike).Mod = 0; + else + bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); + for ielectrode = 1:numChannels + do_UltraMegaSorting(elecFiles{ielectrode}, sFile, LowPass, HighPass, sFile.prop.sfreq); + bst_progress('inc', 1); end - % Save events file for backup - SaveBrainstormEvents(DataMat, 'events_UNSUPERVISED.mat'); - % Add history field - DataMat = bst_history('add', DataMat, 'import', ['Link to unsupervised electrophysiology files: ' outputPath]); - % Save file on hard drive - bst_save(NewBstFile, DataMat, 'v6'); - % Add file to database - sOutputStudy = db_add_data(sInputs(i).iStudy, NewBstFile, DataMat); - % Return new file - OutputFiles{end+1} = NewBstFile; - - % ===== UPDATE DATABASE ===== - % Update links - db_links('Study', sInputs(i).iStudy); - panel_protocols('UpdateNode', 'Study', sInputs(i).iStudy); end + % Restore current folder + cd(previous_directory); + - %%%%%%%%%%%%%%%%%%%%%% Prepare to exit %%%%%%%%%%%%%%%%%%%%%%% - % Turn off parallel processing and return to the initial directory + % ===== IMPORT EVENTS ===== + bst_progress('start', 'UltraMegaSort2000', 'Gathering spiking events...'); + % Delete existing spike events + panel_spikes('DeleteSpikeEvents', sInput.FileName); - if sProcess.options.paral.Value - if ~isempty(poolobj) - delete(poolobj); + % Build output filename + NewBstFilePrefix = bst_fullfile(fPath, ['data_0ephys_ums2k_' fBase]); + NewBstFile = [NewBstFilePrefix '.mat']; + iFile = 1; + commentSuffix = ''; + while exist(NewBstFile, 'file') == 2 + iFile = iFile + 1; + NewBstFile = [NewBstFilePrefix '_' num2str(iFile) '.mat']; + commentSuffix = [' (' num2str(iFile) ')']; + end + % Build output structure + DataMat = struct(); + DataMat.Comment = ['UltraMegaSort2000 Spike Sorting' commentSuffix]; + DataMat.DataType = 'raw'; + DataMat.Device = 'ultramegasort2000'; + DataMat.Name = NewBstFile; + DataMat.Parent = outputPath; + DataMat.RawFile = sInput.FileName; + DataMat.Spikes = struct(); + % Build spikes structure + spikes = dir(bst_fullfile(outputPath, 'times_raw_elec*.mat')); + spikes = sort_nat({spikes.name}); + for iSpike = 1:length(spikes) + DataMat.Spikes(iSpike).Path = outputPath; + DataMat.Spikes(iSpike).File = spikes{iSpike}; + if exist(bst_fullfile(outputPath, DataMat.Spikes(iSpike).File), 'file') ~= 2 + DataMat.Spikes(iSpike).File = ''; end + DataMat.Spikes(iSpike).Name = ChannelMat.Channel(iSpike).Name; + DataMat.Spikes(iSpike).Mod = 0; end -end + % Save events file for backup + SaveBrainstormEvents(DataMat, 'events_UNSUPERVISED.mat'); + % Add history field + DataMat = bst_history('add', DataMat, 'import', ['Link to unsupervised electrophysiology files: ' outputPath]); + % Save file on hard drive + bst_save(NewBstFile, DataMat, 'v6'); + % Add file to database + db_add_data(sInput.iStudy, file_short(NewBstFile), DataMat); + % Return new file + OutputFiles{end+1} = NewBstFile; -%% ===== DOWNLOAD AND INSTALL UltraMegaSort2000 ===== -function downloadAndInstallUltraMegaSort2000() - UltraMegaSort2000Dir = bst_fullfile(bst_get('BrainstormUserDir'), 'UltraMegaSort2000'); - UltraMegaSort2000TmpDir = bst_fullfile(bst_get('BrainstormUserDir'), 'UltraMegaSort2000_tmp'); - url = 'https://github.com/danamics/UMS2K/archive/master.zip'; - % If folders exists: delete - if isdir(UltraMegaSort2000Dir) - file_delete(UltraMegaSort2000Dir, 1, 3); - end - if isdir(UltraMegaSort2000TmpDir) - file_delete(UltraMegaSort2000TmpDir, 1, 3); - end - % Create folder - mkdir(UltraMegaSort2000TmpDir); - % Download file - zipFile = bst_fullfile(UltraMegaSort2000TmpDir, 'master.zip'); - errMsg = gui_brainstorm('DownloadFile', url, zipFile, 'UltraMegaSort2000 download'); - - % Check if the download was succesful and try again if it wasn't - time_before_entering = clock; - updated_time = clock; - time_out = 60;% timeout within 60 seconds of trying to download the file - - % Keep trying to download until a timeout is reached - while etime(updated_time, time_before_entering) time_out && ~isempty(errMsg) - error(['Impossible to download UltraMegaSort2000.' 10 errMsg]); - end - % Unzip file - bst_progress('start', 'UltraMegaSort2000', 'Installing UltraMegaSort2000...'); - unzip(zipFile, UltraMegaSort2000TmpDir); - % Get parent folder of the unzipped file - diropen = dir(fullfile(UltraMegaSort2000TmpDir, 'MATLAB*')); - idir = find([diropen.isdir] & ~cellfun(@(c)isequal(c(1),'.'), {diropen.name}), 1); - newUltraMegaSort2000Dir = bst_fullfile(UltraMegaSort2000TmpDir, diropen(idir).name, 'UMS2K-master'); - % Move UltraMegaSort2000 directory to proper location - file_move(newUltraMegaSort2000Dir, UltraMegaSort2000Dir); - % Delete unnecessary files - file_delete(UltraMegaSort2000TmpDir, 1, 3); - % Add UltraMegaSort2000 to Matlab path - addpath(genpath(UltraMegaSort2000Dir)); + % ===== UPDATE DATABASE ===== + % Update links + db_links('Study', sInput.iStudy); + panel_protocols('UpdateNode', 'Study', sInput.iStudy); end -function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) +%% ===== SAVE BRAINSTORM EVENTS ===== +function SaveBrainstormEvents(SpikeMat, outputFile, eventNamePrefix) if nargin < 3 eventNamePrefix = ''; end - numElectrodes = length(sFile.Spikes); + numElectrodes = length(SpikeMat.Spikes); iNewEvent = 0; events = struct(); % Add existing non-spike events for backup - DataMat = in_bst_data(sFile.RawFile); + DataMat = in_bst_data(SpikeMat.RawFile); existingEvents = DataMat.F.events; for iEvent = 1:length(existingEvents) - if ~process_spikesorting_supervised('IsSpikeEvent', existingEvents(iEvent).label) + if ~panel_spikes('IsSpikeEvent', existingEvents(iEvent).label) if iNewEvent == 0 events = existingEvents(iEvent); else @@ -330,13 +277,13 @@ function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) end for iElectrode = 1:numElectrodes - newEvents = process_spikesorting_supervised(... + newEvents = panel_spikes(... 'CreateSpikeEvents', ... - sFile.RawFile, ... - sFile.Device, ... - bst_fullfile(sFile.Parent, sFile.Spikes(iElectrode).File), ... - sFile.Spikes(iElectrode).Name, ... - 0, eventNamePrefix); + SpikeMat.RawFile, ... + SpikeMat.Device, ... + bst_fullfile(SpikeMat.Parent, SpikeMat.Spikes(iElectrode).File), ... + SpikeMat.Spikes(iElectrode).Name, ... + 1, eventNamePrefix); if iNewEvent == 0 events = newEvents; @@ -348,33 +295,35 @@ function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) end end - save(bst_fullfile(sFile.Parent, outputFile),'events'); + save(bst_fullfile(SpikeMat.Parent, outputFile),'events'); end -function do_UltraMegaSorting(electrodeFile, sFile, lowPass, highPass, Fs) + +%% ===== ULTRAMEGA SORTING ===== +function do_UltraMegaSorting(electrodeFile, sFile, LowPass, HighPass, Fs) + [path, filename] = fileparts(electrodeFile); + % Apply BST bandpass filter try - % Apply BST bandpass filter DataMat = load(electrodeFile, 'data'); - filtered_data_temp = bst_bandpass_hfilter(DataMat.data', Fs, highPass.Value{1}(1), lowPass.Value{1}(1), 0, 0); - - filtered_data = cell(1,1); - filtered_data{1} = filtered_data_temp'; %should be a column vector clear filter - + filtered_data = bst_bandpass_hfilter(DataMat.data', Fs, HighPass, LowPass, 0, 0); + catch e + error(['Frequency filtering failed, try with a different frequency band.' 10 'Error: ' e.message]); + end + % Convert to a column vector in a cell array + filtered_data = {filtered_data'}; + % Run spike sorting + try spikes = ss_default_params(sFile.prop.sfreq); spikes = ss_detect(filtered_data,spikes); spikes = ss_align(spikes); spikes = ss_kmeans(spikes); spikes = ss_energy(spikes); spikes = ss_aggregate(spikes); - - [path, filename] = fileparts(electrodeFile); save(['times_' filename '.mat'], 'spikes') catch e % If an error occurs, just don't create the spike file. - [path, filename] = fileparts(electrodeFile); clean_label = strrep(filename,'raw_elec_',''); disp(e); disp(['Warning: Spiking failed on electrode ' clean_label '. Skipping this electrode.']); end - end diff --git a/toolbox/process/functions/process_spikesorting_waveclus.m b/toolbox/process/functions/process_spikesorting_waveclus.m index 336491a74b..698a9d8dca 100644 --- a/toolbox/process/functions/process_spikesorting_waveclus.m +++ b/toolbox/process/functions/process_spikesorting_waveclus.m @@ -7,8 +7,6 @@ % for each electrode that can be used later for supervised spike-sorting. % When all spikes on all electrodes have been clustered, all the spikes for % each neuron is assigned to an events file in brainstorm format. -% -% USAGE: OutputFiles = process_spikesorting_waveclus('Run', sProcess, sInputs) % @============================================================================= % This function is part of the Brainstorm software: @@ -28,7 +26,9 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018-2019; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018-2022 +% Martin Cousineau, 2018 +% Francois Tadel, 2022 eval(macro_method); end @@ -38,7 +38,7 @@ function sProcess = GetDescription() %#ok % Description the process sProcess.Comment = 'WaveClus'; - sProcess.Category = 'Custom'; + sProcess.Category = 'File'; sProcess.SubGroup = {'Electrophysiology','Unsupervised Spike Sorting'}; sProcess.Index = 1201; sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/SpikeSorting'; @@ -48,27 +48,38 @@ sProcess.nInputs = 1; sProcess.nMinFiles = 1; sProcess.isSeparator = 0; + % Spike sorter name sProcess.options.spikesorter.Type = 'text'; sProcess.options.spikesorter.Value = 'waveclus'; sProcess.options.spikesorter.Hidden = 1; + % RAM limitation sProcess.options.binsize.Comment = 'Maximum RAM to use: '; sProcess.options.binsize.Type = 'value'; sProcess.options.binsize.Value = {2, 'GB', 1}; - sProcess.options.paral.Comment = 'Parallel processing'; - sProcess.options.paral.Type = 'checkbox'; - sProcess.options.paral.Value = 0; - sProcess.options.make_plots.Comment = 'Create images'; + % Parallel processing + sProcess.options.parallel.Comment = 'Parallel processing'; + sProcess.options.parallel.Type = 'checkbox'; + sProcess.options.parallel.Value = 0; + % Use SSP/ICA + sProcess.options.usessp.Comment = 'Apply the existing SSP/ICA projectors'; + sProcess.options.usessp.Type = 'checkbox'; + sProcess.options.usessp.Value = 1; + % Save images + sProcess.options.make_plots.Comment = 'Save images of the clustered spikes'; sProcess.options.make_plots.Type = 'checkbox'; sProcess.options.make_plots.Value = 0; - % Channel name comment - sProcess.options.make_plotshelp.Comment = 'This saves images of the clustered spikes'; - sProcess.options.make_plotshelp.Type = 'label'; + % Separator + sProcess.options.sep1.Type = 'label'; + sProcess.options.sep1.Comment = '
'; % Options: Options - sProcess.options.edit.Comment = {'panel_spikesorting_options', 'Parameters: '}; + sProcess.options.edit.Comment = {'panel_spikesorting_options', 'Waveclus parameters: '}; sProcess.options.edit.Type = 'editpref'; sProcess.options.edit.Value = []; - % Show warning that pre-spikesorted events will be overwritten - sProcess.options.warning.Comment = 'Spike Events created from the acquisition system will be overwritten'; + % Label: Reset options + sProcess.options.edit_help.Comment = 'To restore default options: re-install the waveclus plugin.'; + sProcess.options.edit_help.Type = 'label'; + % Label: Warning that pre-spikesorted events will be overwritten + sProcess.options.warning.Comment = '
Warning: Existing spike events will be overwritten'; sProcess.options.warning.Type = 'label'; end @@ -80,245 +91,165 @@ %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInput) %#ok OutputFiles = {}; - ProtocolInfo = bst_get('ProtocolInfo'); + % ===== DEPENDENCIES ===== % Not available in the compiled version if bst_iscompiled() - bst_report('Error', sProcess, sInputs, 'This function is not available in the compiled version of Brainstorm.'); + bst_report('Error', sProcess, sInput, 'This function is not available in the compiled version of Brainstorm.'); return end - - if sProcess.options.binsize.Value{1} <= 0 - bst_report('Error', sProcess, sInputs, 'Invalid maximum amount of RAM specified.'); + % Load plugin + [isInstalled, errMsg] = bst_plugin('Install', 'waveclus'); + if ~isInstalled + error(errMsg); + end + + % ===== OPTIONS ===== + % Get option: bin size + BinSize = sProcess.options.binsize.Value{1}; + if (BinSize <= 0) + bst_report('Error', sProcess, sInput, 'Invalid maximum amount of RAM specified.'); return end + % Get other options + isParallel = sProcess.options.parallel.Value; + UseSsp = sProcess.options.usessp.Value; - % Ensure we are including the WaveClus folder in the Matlab path - waveclusDir = bst_fullfile(bst_get('BrainstormUserDir'), 'waveclus'); - if exist(waveclusDir, 'file') - addpath(genpath(waveclusDir)); - end - - % Install WaveClus if missing - if ~exist('wave_clus_font', 'file') - rmpath(genpath(waveclusDir)); - isOk = java_dialog('confirm', ... - ['The WaveClus spike-sorter is not installed on your computer.' 10 10 ... - 'Download and install the latest version?'], 'WaveClus'); - if ~isOk - bst_report('Error', sProcess, sInputs, 'This process requires the WaveClus spike-sorter.'); - return; - end - downloadAndInstallWaveClus(); + % ===== LOAD INPUTS ===== + % Get protocol info + ProtocolInfo = bst_get('ProtocolInfo'); + BrainstormTmpDir = bst_get('BrainstormTmpDir'); + % File path + [fPath, fBase] = bst_fileparts(file_fullpath(sInput.FileName)); + % Remove "data_0raw" or "data_" tag + if (length(fBase) > 10 && strcmp(fBase(1:10), 'data_0raw_')) + fBase = fBase(11:end); + elseif (length(fBase) > 5) && strcmp(fBase(1:5), 'data_') + fBase = fBase(6:end); end + % Load input files + ChannelMat = in_bst_channel(sInput.ChannelFile); + numChannels = length(ChannelMat.Channel); + % Demultiplex channels + demultiplexDir = bst_fullfile(BrainstormTmpDir, 'Unsupervised_Spike_Sorting', ProtocolInfo.Comment, sInput.FileName); + elecFiles = out_demultiplex(sInput.FileName, sInput.ChannelFile, demultiplexDir, UseSsp, BinSize * 1e9, isParallel); - % Prepare parallel pool, if requested - if sProcess.options.paral.Value + % ===== OUTPUT FOLDER ===== + outputPath = bst_fullfile(fPath, [fBase '_waveclus_spikes']); + previous_directory = pwd; + % If output folder already exists: delete it + if exist(outputPath, 'dir') == 7 + % Move Matlab out of the folder to be deleted + if ~isempty(strfind(previous_directory, outputPath)) + cd(bst_fileparts(outputPath)); + end + % Delete existing output folder try - poolobj = gcp('nocreate'); - if isempty(poolobj) - parpool; - end + rmdir(outputPath, 's'); catch - sProcess.options.paral.Value = 0; - poolobj = []; + error(['Could not remove spikes folder: ' 10 outputPath 10 ' Make sure this folder is not open in another program.']) end - else - poolobj = []; end + % Create output folder + mkdir(outputPath); - % Compute on each raw input independently - for i = 1:length(sInputs) - [fPath, fBase] = bst_fileparts(sInputs(i).FileName); - % Remove "data_0raw" or "data_" tag - if (length(fBase) > 10 && strcmp(fBase(1:10), 'data_0raw_')) - fBase = fBase(11:end); - elseif (length(fBase) > 5) && strcmp(fBase(1:5), 'data_') - fBase = fBase(6:end); - end - - ChannelMat = in_bst_channel(sInputs(i).ChannelFile); - numChannels = length(ChannelMat.Channel); - sFiles = in_spikesorting_rawelectrodes(sInputs(i), ... - sProcess.options.binsize.Value{1} * 1e9, ... - sProcess.options.paral.Value); - - %%%%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% - outputPath = bst_fullfile(ProtocolInfo.STUDIES, fPath, [fBase '_waveclus_spikes']); - - % Clear if directory already exists - if exist(outputPath, 'dir') == 7 - rmdir(outputPath, 's'); - end - mkdir(outputPath); - - %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% - if sProcess.options.paral.Value - bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); - else - bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); - end - - % The Get_spikes saves the _spikes files at the current directory. - previous_directory = pwd; - cd(outputPath); - - if sProcess.options.paral.Value - parfor ielectrode = 1:numChannels - if ismember(upper(ChannelMat.Channel(ielectrode).Type), {'EEG', 'SEEG'}) % Perform spike sorting only on the channels that are (S)EEG - Get_spikes(sFiles{ielectrode}); - end + % ===== SPIKE SORTING ===== + % The function Get_spikes saves the _spikes files at the current directory + cd(outputPath); + if isParallel + bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); + parfor ielectrode = 1:numChannels + if ismember(upper(ChannelMat.Channel(ielectrode).Type), {'EEG', 'SEEG'}) % Perform spike sorting only on the channels that are (S)EEG + Get_spikes(elecFiles{ielectrode}); end - else - for ielectrode = 1:numChannels - if ismember(upper(ChannelMat.Channel(ielectrode).Type), {'EEG', 'SEEG'}) - Get_spikes(sFiles{ielectrode}); - end - bst_progress('inc', 1); - end - end - - %%%%%%%%%%%%%%%%%%%%%% Do the clustering %%%%%%%%%%%%%%%%%%%%%%%%%% - bst_progress('start', 'Spike-sorting', 'Clustering detected spikes...'); - - % The optional inputs in Do_clustering have to be true or false, not 1 or 0 - if sProcess.options.paral.Value - parallel = true; - else - parallel = false; - end - if sProcess.options.make_plots.Value - make_plots = true; - else - make_plots = false; - end - - % Do the clustering - Do_clustering(1:numChannels, 'parallel', parallel, 'make_plots', make_plots); - - %%%%%%%%%%%%%%%%%%%%% Create Brainstorm Events %%%%%%%%%%%%%%%%%%% - bst_progress('text', 'Saving events file...'); - cd(previous_directory); - - % Delete existing spike events - process_spikesorting_supervised('DeleteSpikeEvents', sInputs(i).FileName); - - % ===== SAVE LINK FILE ===== - % Build output filename - NewBstFilePrefix = bst_fullfile(ProtocolInfo.STUDIES, fPath, ['data_0ephys_wclus_' fBase]); - NewBstFile = [NewBstFilePrefix '.mat']; - iFile = 1; - commentSuffix = ''; - while exist(NewBstFile, 'file') == 2 - iFile = iFile + 1; - NewBstFile = [NewBstFilePrefix '_' num2str(iFile) '.mat']; - commentSuffix = [' (' num2str(iFile) ')']; end - % Build output structure - DataMat = struct(); - DataMat.Comment = ['WaveClus Spike Sorting' commentSuffix]; - DataMat.DataType = 'raw';%'ephys'; - DataMat.Device = 'waveclus'; - DataMat.Name = NewBstFile; - DataMat.Parent = outputPath; - DataMat.RawFile = sInputs(i).FileName; - DataMat.Spikes = struct(); - % Build spikes structure - spikes = dir(bst_fullfile(outputPath, 'raw_elec*_spikes.mat')); - spikes = sort_nat({spikes.name}); - % New channelNames - Without any special characters. - cleanChannelNames = str_remove_spec_chars({ChannelMat.Channel.Name}); - for iChannel = 1:length(cleanChannelNames) - DataMat.Spikes(iChannel).Path = outputPath; - DataMat.Spikes(iChannel).File = ['times_raw_elec_' cleanChannelNames{iChannel} '.mat']; - if exist(bst_fullfile(outputPath, DataMat.Spikes(iChannel).File), 'file') ~= 2 - DataMat.Spikes(iChannel).File = ''; - disp(['The threshold was not crossed for Channel: ' ChannelMat.Channel(iChannel).Name]); + else + bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); + for ielectrode = 1:numChannels + if ismember(upper(ChannelMat.Channel(ielectrode).Type), {'EEG', 'SEEG'}) + Get_spikes(elecFiles{ielectrode}); end - DataMat.Spikes(iChannel).Name = ChannelMat.Channel(iChannel).Name; - DataMat.Spikes(iChannel).Mod = 0; - end - % Save events file for backup - SaveBrainstormEvents(DataMat, 'events_UNSUPERVISED.mat'); - % Add history field - DataMat = bst_history('add', DataMat, 'import', ['Link to unsupervised electrophysiology files: ' outputPath]); - % Save file on hard drive - bst_save(NewBstFile, DataMat, 'v6'); - % Add file to database - sOutputStudy = db_add_data(sInputs(i).iStudy, NewBstFile, DataMat); - % Return new file - OutputFiles{end+1} = NewBstFile; - - % ===== UPDATE DATABASE ===== - % Update links - db_links('Study', sInputs(i).iStudy); - panel_protocols('UpdateNode', 'Study', sInputs(i).iStudy); - end - - %%%%%%%%%%%%%%%%%%%%%% Prepare to exit %%%%%%%%%%%%%%%%%%%%%%% - % Turn off parallel processing and return to the initial directory - - if sProcess.options.paral.Value - if ~isempty(poolobj) - delete(poolobj); + bst_progress('inc', 1); end end - -end -%% ===== DOWNLOAD AND INSTALL WAVECLUS ===== -function downloadAndInstallWaveClus() - waveclusDir = bst_fullfile(bst_get('BrainstormUserDir'), 'waveclus'); - waveclusTmpDir = bst_fullfile(bst_get('BrainstormUserDir'), 'waveclus_tmp'); - url = 'https://github.com/csn-le/wave_clus/archive/master.zip'; - - % If folders exists: delete - if isdir(waveclusDir) - file_delete(waveclusDir, 1, 3); + % ===== SPIKE SORTING ===== + bst_progress('start', 'Spike-sorting', 'Clustering detected spikes...'); + % The optional inputs in Do_clustering have to be true or false, not 1 or 0 + if isParallel + parallel = true; + else + parallel = false; end - if isdir(waveclusTmpDir) - file_delete(waveclusTmpDir, 1, 3); + if sProcess.options.make_plots.Value + make_plots = true; + else + make_plots = false; end - % Create folder - mkdir(waveclusTmpDir); - % Download file - zipFile = bst_fullfile(waveclusTmpDir, 'waveclus.zip'); - errMsg = gui_brainstorm('DownloadFile', url, zipFile, 'WaveClus download'); - - % Check if the download was succesful and try again if it wasn't - time_before_entering = clock; - updated_time = clock; - time_out = 60;% timeout within 60 seconds of trying to download the file + % Do the clustering + Do_clustering(1:numChannels, 'parallel', parallel, 'make_plots', make_plots); + % Restore current folder + cd(previous_directory); - % Keep trying to download until a timeout is reached - while etime(updated_time, time_before_entering) time_out && ~isempty(errMsg) - error(['Impossible to download WaveClus.' 10 errMsg]); + % Build output structure + DataMat = struct(); + DataMat.Comment = ['WaveClus Spike Sorting' commentSuffix]; + DataMat.DataType = 'raw'; + DataMat.Device = 'waveclus'; + DataMat.Name = NewBstFile; + DataMat.Parent = outputPath; + DataMat.RawFile = sInput.FileName; + DataMat.Spikes = struct(); + % New channelNames - Without any special characters. + cleanChannelNames = str_remove_spec_chars({ChannelMat.Channel.Name}); + for iChannel = 1:length(cleanChannelNames) + DataMat.Spikes(iChannel).Path = outputPath; + DataMat.Spikes(iChannel).File = ['times_raw_elec_' cleanChannelNames{iChannel} '.mat']; + if exist(bst_fullfile(outputPath, DataMat.Spikes(iChannel).File), 'file') ~= 2 + DataMat.Spikes(iChannel).File = ''; + disp(['The threshold was not crossed for Channel: ' ChannelMat.Channel(iChannel).Name]); + end + DataMat.Spikes(iChannel).Name = ChannelMat.Channel(iChannel).Name; + DataMat.Spikes(iChannel).Mod = 0; end - % Unzip file - bst_progress('start', 'WaveClus', 'Installing WaveClus...'); - unzip(zipFile, waveclusTmpDir); - % Get parent folder of the unzipped file - diropen = dir(fullfile(waveclusTmpDir, 'MATLAB*')); - idir = find([diropen.isdir] & ~cellfun(@(c)isequal(c(1),'.'), {diropen.name}), 1); - newWaveclusDir = bst_fullfile(waveclusTmpDir, diropen(idir).name, 'wave_clus-master'); - % Move WaveClus directory to proper location - file_move(newWaveclusDir, waveclusDir); - % Delete unnecessary files - file_delete(waveclusTmpDir, 1, 3); - % Add WaveClus to Matlab path - addpath(genpath(waveclusDir)); + % Save events file for backup + SaveBrainstormEvents(DataMat, 'events_UNSUPERVISED.mat'); + % Add history field + DataMat = bst_history('add', DataMat, 'import', ['Link to unsupervised electrophysiology files: ' outputPath]); + % Save file on hard drive + bst_save(NewBstFile, DataMat, 'v6'); + % Add file to database + db_add_data(sInput.iStudy, file_short(NewBstFile), DataMat); + % Return new file + OutputFiles{end+1} = NewBstFile; + + % ===== UPDATE DATABASE ===== + % Update links + db_links('Study', sInput.iStudy); + panel_protocols('UpdateNode', 'Study', sInput.iStudy); end + +%% ===== SAVE BRAINSTORM EVENTS ===== function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) if nargin < 3 eventNamePrefix = ''; @@ -332,7 +263,7 @@ function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) DataMat = in_bst_data(sFile.RawFile); existingEvents = DataMat.F.events; for iEvent = 1:length(existingEvents) - if ~process_spikesorting_supervised('IsSpikeEvent', existingEvents(iEvent).label) + if ~panel_spikes('IsSpikeEvent', existingEvents(iEvent).label) if iNewEvent == 0 events = existingEvents(iEvent); else @@ -343,14 +274,14 @@ function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) end for iElectrode = 1:numElectrodes - newEvents = process_spikesorting_supervised(... + newEvents = panel_spikes(... 'CreateSpikeEvents', ... sFile.RawFile, ... sFile.Device, ... bst_fullfile(sFile.Parent, sFile.Spikes(iElectrode).File), ... sFile.Spikes(iElectrode).Name, ... - 0, eventNamePrefix); - + 1, eventNamePrefix); % Design choice: 0 means the unsupervised spiking events will not be automatically loaded to the link to raw file. They will start appearing only after the users manually spike-sort + % 1 would link them automatically. The problem with that, is that if the users don't finish manual spike-sorting, there is a mix of both. if iNewEvent == 0 events = newEvents; iNewEvent = length(newEvents); diff --git a/toolbox/process/functions/process_spiking_phase_locking.m b/toolbox/process/functions/process_spiking_phase_locking.m index cb4492eb7e..17d6d27825 100644 --- a/toolbox/process/functions/process_spiking_phase_locking.m +++ b/toolbox/process/functions/process_spiking_phase_locking.m @@ -1,6 +1,5 @@ function varargout = process_spiking_phase_locking( varargin ) -% PROCESS_SPIKING_PHASE_LOCKING: Computes the phase locking of spikes on -% the timeseries. +% PROCESS_SPIKING_PHASE_LOCKING: Computes the phase locking of spikes on the timeseries. % @============================================================================= % This function is part of the Brainstorm software: @@ -21,27 +20,28 @@ % =============================================================================@ % % Authors: Konstantinos Nasiotis, 2020 +% Francois Tadel, 2022 eval(macro_method); end %% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description the process - sProcess.Comment = 'Spiking Phase Locking'; + sProcess.Comment = 'Spiking phase locking'; sProcess.FileTag = 'phaseLocking'; - sProcess.Category = 'custom'; - sProcess.SubGroup = {'Electrophysiology', 'Phase'}; - sProcess.Index = 2223; - sProcess.Description = 'https://www.jstatsoft.org/article/view/v031i10'; + sProcess.Category = 'Custom'; + sProcess.SubGroup = 'Electrophysiology'; + sProcess.Index = 1235; + sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Spiking_phase_locking_values'; % Definition of the input accepted by this process sProcess.InputTypes = {'data'}; sProcess.OutputTypes = {'timefreq'}; sProcess.nInputs = 1; sProcess.nMinFiles = 1; % Options: Sensor types - sProcess.options.sensortypes.Comment = 'Sensor types, indices, names or Groups (empty=all): '; + sProcess.options.sensortypes.Comment = 'Sensor types, indices, names or groups (empty=all): '; sProcess.options.sensortypes.Type = 'text'; sProcess.options.sensortypes.Value = 'EEG'; % Band-pass filter @@ -49,198 +49,162 @@ sProcess.options.bandpass.Type = 'range'; sProcess.options.bandpass.Value = {[600, 800], 'Hz', 1}; % Phase Binning - sProcess.options.phaseBin.Comment = 'Phase Binning: '; + sProcess.options.phaseBin.Comment = 'Phase binning: '; sProcess.options.phaseBin.Type = 'value'; sProcess.options.phaseBin.Value = {30, 'degrees', 0}; end %% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok +function Comment = FormatComment(sProcess) Comment = sProcess.Comment; end %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInputs) % Initialize returned values OutputFiles = {}; - % Extract method name from the process name - strProcess = strrep(strrep(func2str(sProcess.Function), 'process_', ''), 'data', ''); - - % Add other options - tfOPTIONS.Method = strProcess; - if isfield(sProcess.options, 'sensortypes') - tfOPTIONS.SensorTypes = sProcess.options.sensortypes.Value; - else - tfOPTIONS.SensorTypes = []; - end - - % Bin size - if isfield(sProcess.options, 'phaseBin') && ~isempty(sProcess.options.phaseBin) && ~isempty(sProcess.options.phaseBin.Value) && iscell(sProcess.options.phaseBin.Value) && sProcess.options.phaseBin.Value{1} > 0 - bin_size = sProcess.options.phaseBin.Value{1}; - else - bst_report('Error', sProcess, sInputs, 'Positive phase bin size required.'); - return; - end - - % === OUTPUT STUDY === - % Get output study - [tmp, iStudy] = bst_process('GetOutputStudy', sProcess, sInputs); - tfOPTIONS.iTargetStudy = iStudy; + % Get options + SensorTypes = sProcess.options.sensortypes.Value; + BandPass = sProcess.options.bandpass.Value{1}; + PhaseBin = sProcess.options.phaseBin.Value{1}; + % ===== PROCESS FILES ===== % Check how many event groups we're processing listComments = cellfun(@str_remove_parenth, {sInputs.Comment}, 'UniformOutput', 0); [uniqueComments,tmp,iData2List] = unique(listComments); nLists = length(uniqueComments); - % Process each event group seperately for iList = 1:nLists - sCurrentInputs = sInputs(iData2List == iList); - - %% Get channel file - sChannel = bst_get('ChannelForStudy', iStudy); - ChannelMat = in_bst_channel(sChannel.FileName); - dataMat_channelFlag = in_bst_data(sCurrentInputs(1).FileName, 'ChannelFlag'); - - iSelectedChannels = select_channels(ChannelMat, dataMat_channelFlag.ChannelFlag, sProcess.options.sensortypes.Value); - nChannels = length(iSelectedChannels); - - if isempty(iSelectedChannels) - bst_report('Error', sProcess, sCurrentInputs(1), 'No channels to process. Make sure that the Names/Groups assigned are correct'); - return; - end - - %% Get only the unique neurons along all of the trials - progressPos = bst_progress('get'); bst_progress('text', ['Detecting unique neurons on all "' uniqueComments{iList} '" trials...']); + % === LOAD INPUTS === + % Get trials in this group + sCurrentInputs = sInputs(iData2List == iList); nTrials = length(sCurrentInputs); - % I get the files outside of the parfor so it won't fail. - % This loads the information from ALL TRIALS on ALL_TRIALS_files - % (Shouldn't create a memory problem). - ALL_TRIALS_files = struct(); - for iFile = 1:nTrials - DataMat = in_bst(sCurrentInputs(iFile).FileName); - ALL_TRIALS_files(iFile).Events = DataMat.Events; - progressPos = bst_progress('set', iFile/nTrials*100); - end - - % ADD AN IF STATEMENT HERE TO GENERALIZE ON ALL EVENTS, NOT JUST SPIKES - % THE FUNCTION SHOULD BE MODIFIED TO ENABLE INPUT OF THE EVENTS FROM - % THE USER - - % Create a cell that holds all of the labels and one for the unique labels - % This will be used to take the averages using the appropriate indices - neuronLabels = {}; % Unique neuron labels (each trial might have different number of neurons). We need everything that appears. - for iFile = 1:nTrials - for iEvent = 1:length(ALL_TRIALS_files(iFile).Events) - if process_spikesorting_supervised('IsSpikeEvent', ALL_TRIALS_files(iFile).Events(iEvent).label) - neuronLabels{end+1} = ALL_TRIALS_files(iFile).Events(iEvent).label; + % Loads all the data + ChannelFlag = []; + neuronLabels = {}; + for iFile = 1:length(sInputs) + % Load file + DataEvt = in_bst_data(sInputs(iFile).FileName, 'Events', 'ChannelFlag'); + % Accumulate bad channels: good channels must be good for all the input files + if isempty(ChannelFlag) + ChannelFlag = DataEvt.ChannelFlag; + else + ChannelFlag(DataEvt.ChannelFlag == -1) = -1; + end + % Find neuron events + for iEvent = 1:length(DataEvt.Events) + if panel_spikes('IsSpikeEvent', DataEvt.Events(iEvent).label) + neuronLabels{end+1} = DataEvt.Events(iEvent).label; end end end - + % If no neuron was found if isempty(neuronLabels) bst_report('Error', sProcess, sCurrentInputs(1), 'No neurons/spiking events detected.'); return; - end - - neuronLabels = unique(neuronLabels,'stable'); + end + % Sort neurons alphabetically + neuronLabels = unique(neuronLabels, 'stable'); neuronLabels = sort_nat(neuronLabels); + + % Load channel file (from the first file in the list) + ChannelMat = in_bst_channel(sCurrentInputs(1).ChannelFile); + % Select good channels + iSelectedChannels = select_channels(ChannelMat, ChannelFlag, SensorTypes); + nChannels = length(iSelectedChannels); + if isempty(iSelectedChannels) + bst_report('Error', sProcess, sCurrentInputs(1), 'No channels to process. Make sure that the Names/Groups assigned are correct'); + return; + end % Now get the labels for the Dropdown - this is to show the spiking % firing rate from each neuron based on the oscillations on each % selected electrode. - labelsForDropDownMenu = cell(length(neuronLabels)*nChannels,1); for iNeuron = 1:length(neuronLabels) for iChannel = 1:nChannels labelsForDropDownMenu{(iNeuron-1)*nChannels + iChannel} = ['Neuron ' erase(neuronLabels{iNeuron},'Spikes Channel ') ' - Ch ' ChannelMat.Channel(iSelectedChannels(iChannel)).Name]; end end - - %% Accumulate the phases that each neuron fired upon - nBins = round(360/sProcess.options.phaseBin.Value{1}) + 1; - all_phases = zeros(length(labelsForDropDownMenu), nBins-1); - - EDGES = linspace(-pi,pi,nBins); - centerOfBins = EDGES(1:end-1) + (pi/180*sProcess.options.phaseBin.Value{1})/2; - - progressPos = bst_progress('set',0); + %% ===== COMPUTE PHASES ===== bst_progress('text', 'Accumulating spiking phases for each neuron...'); + % Accumulate the phases that each neuron fired upon + nBins = round(360/PhaseBin) + 1; + all_phases = zeros(length(labelsForDropDownMenu), nBins-1); + total_spikes = zeros(length(labelsForDropDownMenu), 1); + EDGES = linspace(-pi,pi,nBins); + centerOfBins = EDGES(1:end-1) + (pi/180*PhaseBin)/2; + for iFile = 1:nTrials - - % Collect required fields - DataMat = in_bst(sCurrentInputs(iFile).FileName); + % Load data file + DataMat = in_bst_data(sCurrentInputs(iFile).FileName); events = DataMat.Events; + if isempty(events) + continue; + end - if ~isempty(events) - %% Filter the data based on the user input - sFreq = round(1/diff(DataMat.Time(1:2))); - [filtered_F, FiltSpec, Messages] = process_bandpass('Compute', DataMat.F(iSelectedChannels,:), sFreq, sProcess.options.bandpass.Value{1}(1), sProcess.options.bandpass.Value{1}(2)); + % Filter the data based on the user input + sFreq = round(1/diff(DataMat.Time(1:2))); + filtered_F = process_bandpass('Compute', DataMat.F(iSelectedChannels,:), sFreq, BandPass(1), BandPass(2)); + % Compute the phase + angle_filtered_F = zeros(size(filtered_F)); + for iChannel = 1:size(filtered_F,1) + angle_filtered_F(iChannel,:) = angle(hilbert(filtered_F(iChannel,:))); + end - angle_filtered_F = zeros(size(filtered_F)); - for iChannel = 1:size(filtered_F,1) - angle_filtered_F(iChannel,:) = angle(hilbert(filtered_F(iChannel,:))); - end + for iNeuron = 1:length(neuronLabels) + iEvent_Neuron = find(ismember({events.label},neuronLabels{iNeuron})); + if ~isempty(iEvent_Neuron) + + % Make sure the spike is not at the edge of the + % time window of the trial (This causes problems during the binning) + events(iEvent_Neuron).times = events(iEvent_Neuron).times(events(iEvent_Neuron).times>DataMat.Time(1) & ... + events(iEvent_Neuron).times-pi/6; + % figure(1); + % plot(DataMat.Time, angle_filtered_F(1,:)) + % hold on + % plot(DataMat.Time(iClosest), angle_filtered_F(1,iClosest),'*') + % =============================================================== - if ~isempty(iEvent_Neuron) - - % Make sure the spike is not at the edge of the - % time window of the trial (This causes problems during the binning) - events(iEvent_Neuron).times = events(iEvent_Neuron).times(events(iEvent_Neuron).times>DataMat.Time(1) & ... - events(iEvent_Neuron).times-pi/6; -% % iClosest = 1:length(DataMat.Time); -% -% figure(1); -% plot(DataMat.Time, angle_filtered_F(1,:)) -% hold on -% plot(DataMat.Time(iClosest), angle_filtered_F(1,iClosest),'*') - %% - - % Function hist fails to give correct output when a single - % spike occurs. Taking care of it here - if length(iClosest) == 1 - single_spike_entry = zeros(nChannels, nBins-1); - for iChannel = 1:nChannels - [temp,edges] = histcounts(angle_filtered_F(iChannel,iClosest),EDGES); - iBin = find(temp); - single_spike_entry(iChannel, iBin) = 1; - end - all_phases((iNeuron-1)*nChannels+1:iNeuron*nChannels,:) = all_phases((iNeuron-1)*nChannels+1:iNeuron*nChannels,:) + single_spike_entry; - else -% [all_phases_single_neuron, bins] = hist(angle_filtered_F(:,iClosest)', EDGES_extended); - [all_phases_single_neuron,edges] = histcounts(angle_filtered_F(:,iClosest)',EDGES); - - if size(all_phases_single_neuron, 1) ~= 1 % If a vector then transpose to - all_phases_single_neuron = all_phases_single_neuron'; - end - all_phases((iNeuron-1)*nChannels+1:iNeuron*nChannels,:) = all_phases((iNeuron-1)*nChannels+1:iNeuron*nChannels,:) + all_phases_single_neuron; + if size(all_phases_single_neuron, 1) ~= 1 % If a vector then transpose to + all_phases_single_neuron = all_phases_single_neuron'; end - - end + all_phases((iNeuron-1)*nChannels+1:iNeuron*nChannels,:) = all_phases((iNeuron-1)*nChannels+1:iNeuron*nChannels,:) + all_phases_single_neuron; + end end end - bst_progress('set', round(iFile / nTrials * 100)); end - %% Compute the p-values for both Rayleigh and Omnibus tests + % Compute the p-values for both Rayleigh and Omnibus tests pValues = struct; preferredPhase = zeros(size(all_phases,1),1); - for iNeuron = 1:size(all_phases,1) bins_with_values = all_phases(iNeuron,:)~=0; [pValues(iNeuron).Rayleigh, z] = circ_rtest(EDGES(bins_with_values), all_phases(iNeuron,bins_with_values)); @@ -255,69 +219,49 @@ mean_value = circ_mean(single_neuron_phase); preferredPhase(iNeuron) = mean_value * (180/pi); end - - - %% Change the dimensions to make it compatible with Brainstorm TF + + % Change the dimensions to make it compatible with Brainstorm TF all_phases = permute(all_phases, [1,3,2]); - %% Build the output file - tfOPTIONS.ParentFiles = {sCurrentInputs.FileName}; + % ===== SAVE FILE ===== % Prepare output file structure - FileMat.TF = all_phases; - FileMat.TFmask = true(size(all_phases, 2), size(all_phases, 3)); - FileMat.Std = []; - FileMat.Comment = ['Phase Locking: ' uniqueComments{iList} ' | band (' num2str(sProcess.options.bandpass.Value{1}(1)) ',' num2str(sProcess.options.bandpass.Value{1}(2)) ')Hz']; - FileMat.DataType = 'data'; - FileMat.Time = 1; - FileMat.TimeBands = []; - FileMat.Freqs = centerOfBins; - FileMat.RefRowNames = []; - FileMat.RowNames = labelsForDropDownMenu; - FileMat.Measure = 'power'; - FileMat.Method = 'morlet'; - FileMat.DataFile = []; % Leave blank because multiple parents - FileMat.SurfaceFile = []; - FileMat.GridLoc = []; - FileMat.GridAtlas = []; - FileMat.Atlas = []; - FileMat.HeadModelFile = []; - FileMat.HeadModelType = []; - FileMat.nAvg = []; - FileMat.ColormapType = []; - FileMat.DisplayUnits = []; - FileMat.Options = tfOPTIONS; - FileMat.History = []; - - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % This is added here for future statistics - Let's hear it from Francois - FileMat.neurons.phase.pValues = pValues; - FileMat.neurons.phase.preferredPhase = preferredPhase; - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + TfMat = db_template('timefreqmat'); + TfMat.TF = all_phases; + TfMat.Comment = ['Phase Locking: ' uniqueComments{iList} ' | band (' num2str(BandPass(1)) ',' num2str(BandPass(2)) ')Hz']; + TfMat.DataType = 'data'; + TfMat.Time = 1; + TfMat.Freqs = centerOfBins; + TfMat.RowNames = labelsForDropDownMenu; + TfMat.Measure = 'power'; + TfMat.Method = 'morlet'; + TfMat.DataFile = []; % Leave blank because multiple parents + TfMat.Options = []; + % Save phases + TfMat.neurons.phase.pValues = pValues; + TfMat.neurons.phase.preferredPhase = preferredPhase; + TfMat.neurons.phase.total_spikes = total_spikes; % Add history field - FileMat = bst_history('add', FileMat, 'compute', ... - ['Spiking phase locking per neuron']); + TfMat = bst_history('add', TfMat, 'compute', 'Spiking phase locking per neuron'); % Get output study - sTargetStudy = bst_get('Study', iStudy); + [tmp, iTargetStudy] = bst_process('GetOutputStudy', sProcess, sInputs); + sTargetStudy = bst_get('Study', iTargetStudy); % Output filename FileName = bst_process('GetNewFilename', bst_fileparts(sTargetStudy.FileName), 'timefreq_spiking_phase_locking'); OutputFiles = {FileName}; % Save output file and add to database - bst_save(FileName, FileMat, 'v6'); - db_add_data(tfOPTIONS.iTargetStudy, FileName, FileMat); - + bst_save(FileName, TfMat, 'v6'); + db_add_data(iTargetStudy, FileName, TfMat); end - % Display report to user - bst_report('Info', sProcess, sInputs, 'Success'); - disp('BST> process_spiking_phase_locking: Success'); end -function iChannels = select_channels(ChannelMat, ChannelFlag, target) - % Get channels to process +%% ===== SELECT CHANNELS ===== +function iChannels = select_channels(ChannelMat, ChannelFlag, target) + % Get channels to process iChannels = channel_find(ChannelMat.Channel, target); % Check for Group selection if ~iscell(target) @@ -329,7 +273,7 @@ end end - %% Select which channels to compute the spiking phase on + % Select which channels to compute the spiking phase on if isempty(iChannels) if ~all(cellfun(@isempty,{ChannelMat.Channel.Group})) % In case not all groups are empty allGroups = upper(unique({ChannelMat.Channel.Group})); diff --git a/toolbox/process/functions/process_tuning_curves.m b/toolbox/process/functions/process_tuning_curves.m index 6a9e604198..340a6c2728 100644 --- a/toolbox/process/functions/process_tuning_curves.m +++ b/toolbox/process/functions/process_tuning_curves.m @@ -1,7 +1,4 @@ function varargout = process_tuning_curves( varargin ) -% PROCESS_TUNING_CURVES -% -% USAGE: OutputFiles = process_tuning_curves('Run', sProcess, sInputs) % @============================================================================= % This function is part of the Brainstorm software: @@ -21,19 +18,21 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Martin Cousineau, 2018; Konstantinos Nasiotis, 2018 +% Authors: Martin Cousineau, 2018 +% Konstantinos Nasiotis, 2018 +% Francois Tadel, 2022 eval(macro_method); end %% ===== GET DESCRIPTION ===== -function sProcess = GetDescription() %#ok +function sProcess = GetDescription() % Description the process - sProcess.Comment = 'Tuning Curves'; + sProcess.Comment = 'Tuning curves'; sProcess.Category = 'Custom'; sProcess.SubGroup = 'Electrophysiology'; - sProcess.Index = 1203; + sProcess.Index = 1210; sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions'; % Definition of the input accepted by this process sProcess.InputTypes = {'raw'}; @@ -61,119 +60,116 @@ %% ===== FORMAT COMMENT ===== -function Comment = FormatComment(sProcess) %#ok +function Comment = FormatComment(sProcess) Comment = sProcess.Comment; end %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInput) OutputFiles = {}; % Check if the user actually selected Neurons and Conditions if ~isfield(sProcess.options, 'spikesel') || isempty(sProcess.options.spikesel.Value) - bst_report('Error', sProcess, sInputs, 'You have to select the Neurons that will be displayed'); + bst_report('Error', sProcess, sInput, 'You have to select the Neurons that will be displayed'); return; end if ~isfield(sProcess.options, 'eventsel') || isempty(sProcess.options.eventsel.Value) - bst_report('Error', sProcess, sInputs, 'You have to select the Conditions that will be displayed'); + bst_report('Error', sProcess, sInput, 'You have to select the Conditions that will be displayed'); return; elseif length(sProcess.options.eventsel.Value) < 2 - bst_report('Error', sProcess, sInputs, 'You should select at least two Conditions to be displayed'); + bst_report('Error', sProcess, sInput, 'You should select at least two Conditions to be displayed'); return; end - OutputFiles = {}; - ProtocolInfo = bst_get('ProtocolInfo'); - - % Compute on each raw input independently - for iFile = 1:length(sInputs) - % Read the link to raw file and the Events - raw_link = load(fullfile(ProtocolInfo.STUDIES,sInputs(iFile).FileName)); - events = raw_link.F.events; - allEventLabels = {events.label}'; + % Read the link to raw file and the Events + raw_link = in_bst_data(sInput.FileName); + events = raw_link.F.events; + allEventLabels = {events.label}'; - % Initialize the output file. Its size will be nNeurons x nEvents selected - final_matrix = cell(length(sProcess.options.spikesel.Value),length(sProcess.options.eventsel.Value)); - - % Compute the spikes in the bin around the Events selected - for iNeuron = 1:length(sProcess.options.spikesel.Value) - index_NeuronEvents = find(ismember(allEventLabels, sProcess.options.spikesel.Value{iNeuron})); % Find the index of the spike-events that correspond to that electrode (Exact string match) - times_NeuronEvents = events(index_NeuronEvents).times; + % Initialize the output file. Its size will be nNeurons x nEvents selected + final_matrix = cell(length(sProcess.options.spikesel.Value),length(sProcess.options.eventsel.Value)); - for iEvent = 1:length(sProcess.options.eventsel.Value) - index_StimulusEvents = find(ismember(allEventLabels, sProcess.options.eventsel.Value{iEvent})); % Find the index of the spike-events that correspond to that electrode (Exact string match) - times_StimulusEvents = events(index_StimulusEvents).times; - - for iSampleEvent = 1:length(times_StimulusEvents) - condition_success = sum((times_NeuronEvents>times_StimulusEvents(iSampleEvent) - sProcess.options.timewindow.Value{1}(1)) & (times_NeuronEvents < times_StimulusEvents(iSampleEvent) + sProcess.options.timewindow.Value{1}(2))); - if condition_success - final_matrix{iNeuron, iEvent} = [final_matrix{iNeuron, iEvent} condition_success]; - end + % Compute the spikes in the bin around the Events selected + for iNeuron = 1:length(sProcess.options.spikesel.Value) + index_NeuronEvents = find(ismember(allEventLabels, sProcess.options.spikesel.Value{iNeuron})); % Find the index of the spike-events that correspond to that electrode (Exact string match) + times_NeuronEvents = events(index_NeuronEvents).times; + + for iEvent = 1:length(sProcess.options.eventsel.Value) + index_StimulusEvents = find(ismember(allEventLabels, sProcess.options.eventsel.Value{iEvent})); % Find the index of the spike-events that correspond to that electrode (Exact string match) + times_StimulusEvents = events(index_StimulusEvents).times; + + for iSampleEvent = 1:length(times_StimulusEvents) + condition_success = sum((times_NeuronEvents>times_StimulusEvents(iSampleEvent) - sProcess.options.timewindow.Value{1}(1)) & (times_NeuronEvents < times_StimulusEvents(iSampleEvent) + sProcess.options.timewindow.Value{1}(2))); + if condition_success + final_matrix{iNeuron, iEvent} = [final_matrix{iNeuron, iEvent} condition_success]; end end + end + + + % Initialize the 3 vectors that will be plotted (mean, and 95% confidence intervals) + meanData = zeros(1,length(sProcess.options.eventsel.Value)); + CI = zeros(2,length(sProcess.options.eventsel.Value)); + + + % Assign the confidence intervals values + for iCondition = 1:length(sProcess.options.eventsel.Value) + y = final_matrix{iNeuron,iCondition}./(sProcess.options.timewindow.Value{1}(2) - sProcess.options.timewindow.Value{1}(1)); % Convert to firing rate + meanData(iCondition) = mean(y); - - % Initialize the 3 vectors that will be plotted (mean, and 95% confidence intervals) - meanData = zeros(1,length(sProcess.options.eventsel.Value)); - CI = zeros(2,length(sProcess.options.eventsel.Value)); - + % Compute the 95% confidence intervals + RESAMPLING = 1000; % Number of permutations - % Assign the confidence intervals values - for iCondition = 1:length(sProcess.options.eventsel.Value) - y = final_matrix{iNeuron,iCondition}./(sProcess.options.timewindow.Value{1}(2) - sProcess.options.timewindow.Value{1}(1)); % Convert to firing rate - meanData(iCondition) = mean(y); - - % Compute the 95% confidence intervals - RESAMPLING = 1000; % Number of permutations + if length(y)>1 CI(:,iCondition) = bootci(RESAMPLING, {@mean, y}, 'type','cper','alpha',0.05); + else + CI(:,iCondition) = [0;0]; end + end - - - %% Create the plot - %TODO: brainstorm figure? - figure((iFile-1)*100 + iNeuron); % Each Link to Raw file figure set will be separated by an index of 100. - set(gcf, 'Name', sProcess.options.spikesel.Value{iNeuron}); - - x = 1:length(sProcess.options.eventsel.Value); - - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - %%%%%%%%%%%%%%%%%% MARTIN - THIS NEEDS TO BE SUBSTITUTED WITH - %%%%%%%%%%%%%%%%%% THE BRAINSTORM FUNCTIONS THAT DISPLAY THE - %%%%%%%%%%%%%%%%%% HALO. - %%%%%%%%%%%%%%%%%% Vectors to be plotted: - %%%%%%%%%%%%%%%%%% 1. meanData (that will be the center vector) - %%%%%%%%%%%%%%%%%% 2. CI(1,:) (Bottom confidence interval) - %%%%%%%%%%%%%%%%%% 3. CI(2,:) (Top confidence interval) - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - % Create the plot with the confidence intervals - plot(x, CI(1,:), 'k--', 'LineWidth', 1.5); - hold on; - plot(x, CI(2,:), 'k--', 'LineWidth', 1.5); - - x2 = [x, fliplr(x)]; - inBetween = [CI(1,:), fliplr(CI(2,:))]; - fill(x2, inBetween, 'g','FaceAlpha',.4); - plot(x, meanData,'LineWidth', 2) + %% Create the plot + %TODO: brainstorm figure? + figure(iNeuron); % Each Link to Raw file figure set will be separated by an index of 100. + set(gcf, 'Name', sProcess.options.spikesel.Value{iNeuron}); + + x = 1:length(sProcess.options.eventsel.Value); + + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + %%%%%%%%%%%%%%%%%% MARTIN - THIS NEEDS TO BE SUBSTITUTED WITH + %%%%%%%%%%%%%%%%%% THE BRAINSTORM FUNCTIONS THAT DISPLAY THE + %%%%%%%%%%%%%%%%%% HALO. + %%%%%%%%%%%%%%%%%% Vectors to be plotted: + %%%%%%%%%%%%%%%%%% 1. meanData (that will be the center vector) + %%%%%%%%%%%%%%%%%% 2. CI(1,:) (Bottom confidence interval) + %%%%%%%%%%%%%%%%%% 3. CI(2,:) (Top confidence interval) + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + + % Create the plot with the confidence intervals + plot(x, CI(1,:), 'k--', 'LineWidth', 1.5); + hold on; + plot(x, CI(2,:), 'k--', 'LineWidth', 1.5); + + x2 = [x, fliplr(x)]; + inBetween = [CI(1,:), fliplr(CI(2,:))]; + fill(x2, inBetween, 'g','FaceAlpha',.4); + plot(x, meanData,'LineWidth', 2) - %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - - set(gca, 'Xtick', 1:length(sProcess.options.eventsel.Value), 'Xticklabel', sProcess.options.eventsel.Value); - xlabel('Condition'); - ylabel('Firing Rate (Spikes/second)'); - grid on - - if max(CI(2,:)) == 0 - axis([0 length(sProcess.options.eventsel.Value)+1 0 Inf]); - else - axis([0 length(sProcess.options.eventsel.Value)+1 0 max(CI(2,:)) + std(CI(2,:))]); - end + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + + set(gca, 'Xtick', 1:length(sProcess.options.eventsel.Value), 'Xticklabel', sProcess.options.eventsel.Value); + xlabel('Condition'); + ylabel('Firing Rate (Spikes/second)'); + grid on + + if max(CI(2,:)) == 0 + axis([0 length(sProcess.options.eventsel.Value)+1 0 Inf]); + else + axis([0 length(sProcess.options.eventsel.Value)+1 0 max(CI(2,:)) + std(CI(2,:))]); end - end - end diff --git a/toolbox/process/panel_process_select.m b/toolbox/process/panel_process_select.m index b4375a5c9e..d50b0604ca 100644 --- a/toolbox/process/panel_process_select.m +++ b/toolbox/process/panel_process_select.m @@ -2090,7 +2090,7 @@ function ScoutSelection_Callback(iProcess, optName, AtlasList, jCombo, jList, jC for iEvent = 1:length(DataEvents) label = DataEvents(iEvent).label; - isSpikeEvent = process_spikesorting_supervised('IsSpikeEvent', label); + isSpikeEvent = panel_spikes('IsSpikeEvent', label); if (excludeSpikes && ~isSpikeEvent) || (onlySpikes && isSpikeEvent) EventList{end + 1} = label; diff --git a/toolbox/script/tutorial_ephys.m b/toolbox/script/tutorial_ephys.m new file mode 100644 index 0000000000..612ebb02b7 --- /dev/null +++ b/toolbox/script/tutorial_ephys.m @@ -0,0 +1,251 @@ +function tutorial_ephys(tutorial_dir) +% TUTORIAL_EPHYS: Script that reproduces the online tutorials "Brainstorm's Suite for Multi-unit Electrophysiology" +% +% REFERENCE: +% - https://neuroimage.usc.edu/brainstorm/e-phys/Introduction +% - https://neuroimage.usc.edu/brainstorm/e-phys/SpikeSorting +% - https://neuroimage.usc.edu/brainstorm/e-phys/RawToLFP +% - https://neuroimage.usc.edu/brainstorm/e-phys/functions +% +% INPUTS: +% tutorial_dir: Directory where the sample_ephys.zip file has been unzipped + +% @============================================================================= +% This function is part of the Brainstorm software: +% https://neuroimage.usc.edu/brainstorm +% +% Copyright (c) University of Southern California & McGill University +% This software is distributed under the terms of the GNU General Public License +% as published by the Free Software Foundation. Further details on the GPLv3 +% license can be found at http://www.gnu.org/copyleft/gpl.html. +% +% FOR RESEARCH PURPOSES ONLY. THE SOFTWARE IS PROVIDED "AS IS," AND THE +% UNIVERSITY OF SOUTHERN CALIFORNIA AND ITS COLLABORATORS DO NOT MAKE ANY +% WARRANTY, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF +% MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, NOR DO THEY ASSUME ANY +% LIABILITY OR RESPONSIBILITY FOR THE USE OF THIS SOFTWARE. +% +% For more information type "brainstorm license" at command prompt. +% =============================================================================@ +% +% Author: Francois Tadel, 2022 + + +%% ===== FILES TO IMPORT ===== +% You have to specify the folder in which the tutorial dataset is unzipped +if (nargin == 0) || isempty(tutorial_dir) || ~file_exist(tutorial_dir) + error('The first argument must be the full path to the tutorial dataset folder.'); +end +% Build the path of the files to import +T1Nii = fullfile(tutorial_dir, 'sample_ephys', 'floyd_t1.nii'); +CortexMesh = fullfile(tutorial_dir, 'sample_ephys', 'floyd_cortex.mesh'); +PlxFile = fullfile(tutorial_dir, 'sample_ephys', 'ytu288c-01.plx'); +PosFile = fullfile(tutorial_dir, 'sample_ephys', 'ytu288c-01_electrodes.txt'); +EvtFile = fullfile(tutorial_dir, 'sample_ephys', 'ytu288c-01_events.csv'); +% Check if the folder contains the required files +if ~file_exist(T1Nii) || ~file_exist(PlxFile) + error(['The folder ' tutorial_dir ' does not contain the folder from the file sample_ephys.zip.']); +end +% Subject name +SubjectName = 'Floyd'; + + +%% ===== CREATE PROTOCOL ===== +% Start brainstorm without the GUI +if ~brainstorm('status') + brainstorm nogui +end +% Protocol name +ProtocolName = 'Tutorial_e-Phys'; +% Delete existing protocol +gui_brainstorm('DeleteProtocol', ProtocolName); +% Create new protocol +gui_brainstorm('CreateProtocol', ProtocolName, 0, 0); +% Start a new report +bst_report('Start'); + + +%% ===== IMPORT ANATOMY ===== +% Process: Import MRI +bst_process('CallProcess', 'process_import_mri', [], [], ... + 'subjectname', SubjectName, ... + 'mrifile', {T1Nii, 'ALL'}); +% Process: MNI normalization +bst_process('CallProcess', 'process_mni_normalize', [], [], ... + 'subjectname', SubjectName, ... + 'method', 'maff8'); +% Process: Generate head surface +bst_process('CallProcess', 'process_generate_head', [], [], ... + 'subjectname', SubjectName, ... + 'nvertices', 10000, ... + 'erodefactor', 0, ... + 'fillfactor', 2); +% Import cortex surface +iSubject = 1; +[iSurf, CortexFile] = import_surfaces(iSubject, CortexMesh, 'MESH', 0); + + +%% ===== ACCESS THE RECORDINGS ===== +% Process: Create link to raw file +sFilesRaw = bst_process('CallProcess', 'process_import_data_raw', [], [], ... + 'subjectname', SubjectName, ... + 'datafile', {PlxFile, 'EEG-PLEXON'}); +% Process: Import from file +bst_process('CallProcess', 'process_evt_import', sFilesRaw, [], ... + 'evtfile', {EvtFile, 'CSV-TIME'}, ... + 'delete', 0); +% Process: Add EEG positions +bst_process('CallProcess', 'process_channel_addloc', sFilesRaw, [], ... + 'channelfile', {PosFile, 'ASCII_NXYZ_WORLD'}, ... + 'fixunits', 0, ... + 'vox2ras', 1); +% Process: Set channels type +bst_process('CallProcess', 'process_channel_settype', sFilesRaw, [], ... + 'sensortypes', '', ... + 'newtype', 'SEEG'); + +% Display anatomy and sensors +hFig = view_channels_3d(sFilesRaw.ChannelFile, 'SEEG', 'scalp', 1); +hFig = view_surface(CortexFile{1}, 0.8, [1 0 0], hFig); +figure_3d('SetStandardView', hFig, 'right'); +bst_report('Snapshot', hFig, [], 'Anatomy'); +close(hFig); + + +%% ===== SPIKE SORTING ===== +% Not executed here, in order to keep the original spiking events from the PLX file + +% % Process: WaveClus +% sFilesWavclus = bst_process('CallProcess', 'process_spikesorting_waveclus', sFilesRaw, [], ... +% 'spikesorter', 'waveclus', ... +% 'binsize', 8, ... +% 'parallel', 0, ... +% 'usessp', 1, ... +% 'make_plots', 0, ... +% 'edit', 0); + +% % Process: UltraMegaSort2000 +% sFilesUMS = bst_process('CallProcess', 'process_spikesorting_ultramegasort2000', sFilesRaw, [], ... +% 'spikesorter', 'ultramegasort2000', ... +% 'binsize', 40, ... +% 'parallel', 0, ... +% 'usessp', 1, ... +% 'highpass', 700, ... +% 'lowpass', 4800, ... +% 'edit', 4800); + +% % Process: KiloSort +% sFilesKilo = bst_process('CallProcess', 'process_spikesorting_kilosort', sFilesRaw, [], ... +% 'spikesorter', 'kilosort', ... +% 'binsize', 40, ... +% 'GPU', 0, ... +% 'usessp', 1, ... +% 'edit', 1); + + +%% ===== CONVERT TO LFP ===== +% Process: Convert Raw to LFP +sFilesLfp = bst_process('CallProcess', 'process_convert_raw_to_lfp', sFilesRaw, [], ... + 'binsize', 40, ... + 'usessp', 1, ... + 'LFP_fs', 1000, ... + 'freqlist', [], ... + 'filterbounds', [0.5, 150], ... + 'despikeLFP', 0, ... + 'parallel', 0); +% Process: Import MEG/EEG: Events +sFilesLfpEpochs = bst_process('CallProcess', 'process_import_data_event', sFilesLfp, [], ... + 'subjectname', SubjectName, ... + 'condition', '', ... + 'eventname', 'Stim On 1, Stim On 2, Stim On 3, Stim On 4, Stim On 5, Stim On 6, Stim On 7, Stim On 8, Stim On 9', ... + 'timewindow', [], ... + 'epochtime', [-0.5, 1], ... + 'split', 0, ... + 'createcond', 0, ... + 'ignoreshort', 1, ... + 'usectfcomp', 1, ... + 'usessp', 1, ... + 'freq', 500, ... + 'baseline', 'all', ... + 'blsensortypes', 'SEEG'); + + +%% ===== TUNING CURVES ===== +% Process: Tuning curves +bst_process('CallProcess', 'process_tuning_curves', sFilesLfp, [], ... + 'eventsel', {'Stim On 1', 'Stim On 2', 'Stim On 3', 'Stim On 4', 'Stim On 5', 'Stim On 6', 'Stim On 7', 'Stim On 8', 'Stim On 9'}, ... + 'spikesel', {'Spikes Channel AD06', 'Spikes Channel AD08 |1|'}, ... + 'timewindow', [0.05, 0.12]); + + +%% ===== NOISE CORRELATION ===== +% Process: Noise correlation +sFilesNoiseCorr = bst_process('CallProcess', 'process_noise_correlation', sFilesLfpEpochs, [], ... + 'timewindow', [0, 0.3]); +% Process: Snapshot: Time-frequency maps +bst_process('CallProcess', 'process_snapshot', sFilesNoiseCorr, [], ... + 'type', 'timefreq', ... % Time-frequency maps + 'modality', 6, ... % SEEG + 'Comment', 'Noise correlation'); + + +%% ===== SPIKE FIELD COHERENCE ===== +% Process: Select data files in: Floyd/*/Stim On 1 +sFilesStim1 = bst_process('CallProcess', 'process_select_files_data', [], [], ... + 'subjectname', SubjectName, ... + 'condition', '', ... + 'tag', 'Stim On 1', ... + 'includebad', 0, ... + 'includeintra', 0, ... + 'includecommon', 0); +% Process: Spike field coherence +sFilesSFC = bst_process('CallProcess', 'process_spike_field_coherence', sFilesStim1, [], ... + 'timewindow', [-0.15, 0.15], ... + 'sensortypes', 'EEG, SEEG', ... + 'parallel', 0); +% Process: Snapshot: Time-frequency maps +hFig = view_timefreq(sFilesSFC.FileName, 'SingleSensor', 'Spikes Channel AD03'); +bst_report('Snapshot', hFig, [], 'Spike field coherence'); +close(hFig); + + +%% ===== RASTER PLOT ===== +% Process: Raster plot per neuron +sFilesRaster = bst_process('CallProcess', 'process_rasterplot_per_neuron', sFilesStim1, []); +% Process: Snapshot: Time-frequency maps +hFig = view_timefreq(sFilesRaster.FileName, 'SingleSensor', 'Spikes Channel AD01 |1|'); +panel_time('SetCurrentTime', 0.158); +bst_report('Snapshot', hFig, [], 'Raster plot per neuron'); +close(hFig); + + +%% ===== SPIKE TRIGGERED AVERAGE ===== +% Process: Spike triggered average +sFilesAvg = bst_process('CallProcess', 'process_spike_triggered_average', sFilesStim1, [], ... + 'timewindow', [-0.15, 0.15], ... + 'parallel', 0); + +% Process: Select data files in: Floyd/*/Stim On 1 (AD01 #1) +sFilesAvgAD01 = bst_process('CallProcess', 'process_select_files_data', [], [], ... + 'subjectname', SubjectName, ... + 'condition', '', ... + 'tag', 'Stim On 1 (AD01 #1)', ... + 'includebad', 0, ... + 'includeintra', 0, ... + 'includecommon', 0); +% Process: Snapshot: Recordings time series +bst_process('CallProcess', 'process_snapshot', sFilesAvgAD01, [], ... + 'type', 'data', ... % Recordings time series + 'modality', 6, ... % SEEG + 'Comment', 'Spike triggered average: AD01 #1'); +% View 2DLayout +hFig = view_topography(sFilesAvgAD01.FileName, 'SEEG', '2DLayout'); +bst_report('Snapshot', hFig, [], 'Spike triggered average: AD01 #1'); +close(hFig); + +% Save and display report +ReportFile = bst_report('Save', []); +bst_report('Open', ReportFile); + + + diff --git a/toolbox/tree/node_create_study.m b/toolbox/tree/node_create_study.m index 09aae41adc..1ed277272b 100644 --- a/toolbox/tree/node_create_study.m +++ b/toolbox/tree/node_create_study.m @@ -33,7 +33,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Francois Tadel, 2008-2015 +% Authors: Francois Tadel, 2008-2022 % Martin Cousineau, 2020 @@ -198,7 +198,9 @@ % Node modifier (0=none, 1=bad) Modifier = sStudy.Data(iData).BadTrial; % Create node - if strcmpi(sStudy.Data(iData).DataType, 'raw') + if ~isempty(strfind(sStudy.Data(iData).FileName, 'data_0ephys')) + nodeType = 'spike'; + elseif strcmpi(sStudy.Data(iData).DataType, 'raw') nodeType = 'rawdata'; else nodeType = 'data'; diff --git a/toolbox/tree/node_delete.m b/toolbox/tree/node_delete.m index af67de8cd1..1b0e15769e 100644 --- a/toolbox/tree/node_delete.m +++ b/toolbox/tree/node_delete.m @@ -531,7 +531,26 @@ function node_delete(bstNodes, isUserConfirm) end iModifiedStudies = unique(iItem); end - + +%% ===== SPIKE FILE ===== + case 'spike' + bst_progress('start', 'Delete nodes', 'Deleting files...'); + % Delete file + FullFilesList = cellfun(@(f)bst_fullfile(ProtocolInfo.STUDIES,f), FileName', 'UniformOutput',0); + if (file_delete(FullFilesList, ~isUserConfirm) == 1) + iUniqueStudy = unique(iItem); + for i=1:length(iUniqueStudy) + iStudy = iUniqueStudy(i); + iData = iSubItem(iItem == iStudy); + sStudy = bst_get('Study', iStudy); + % Remove file description from database + sStudy.Data(iData) = []; + % Study was modified + bst_set('Study', iStudy, sStudy); + end + iModifiedStudies = unique(iItem); + end + %% ===== TIMEFREQ FILE ===== case {'timefreq', 'spectrum'} bst_progress('start', 'Delete nodes', 'Deleting files...'); diff --git a/toolbox/tree/node_rename.m b/toolbox/tree/node_rename.m index b543a3bfb5..7a33317c4d 100644 --- a/toolbox/tree/node_rename.m +++ b/toolbox/tree/node_rename.m @@ -22,7 +22,7 @@ function node_rename(bstNode, newComment) % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Francois Tadel, 2008-2015 +% Authors: Francois Tadel, 2008-2022 %% ===== INITIALIZATION ===== @@ -185,7 +185,7 @@ function node_rename(bstNode, newComment) end %% ===== DATA (Comment) ===== - case {'data', 'rawdata'} + case {'data', 'rawdata', 'spike'} iStudy = iItem; iData = iSubItem; sStudy = bst_get('Study', iStudy); diff --git a/toolbox/tree/tree_callbacks.m b/toolbox/tree/tree_callbacks.m index 5330cafe2a..e5592b178e 100644 --- a/toolbox/tree/tree_callbacks.m +++ b/toolbox/tree/tree_callbacks.m @@ -30,7 +30,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Francois Tadel, 2008-2021 +% Authors: Francois Tadel, 2008-2022 import org.brainstorm.icon.*; import java.awt.event.KeyEvent; @@ -69,7 +69,7 @@ switch lower(nodeType) case {'surface', 'scalp', 'cortex', 'outerskull', 'innerskull', 'fibers', 'fem', 'other', 'subject', 'studysubject', 'anatomy', 'volatlas'} filenameFull = bst_fullfile(ProtocolInfo.SUBJECTS, filenameRelative); - case {'study', 'condition', 'rawcondition', 'channel', 'headmodel', 'data','rawdata', 'datalist', 'results', 'kernel', 'pdata', 'presults', 'ptimefreq', 'pspectrum', 'image', 'video', 'videolink', 'noisecov', 'ndatacov', 'dipoles','timefreq', 'spectrum', 'matrix', 'matrixlist', 'pmatrix'} + case {'study', 'condition', 'rawcondition', 'channel', 'headmodel', 'data','rawdata', 'datalist', 'results', 'kernel', 'pdata', 'presults', 'ptimefreq', 'pspectrum', 'image', 'video', 'videolink', 'noisecov', 'ndatacov', 'dipoles','timefreq', 'spectrum', 'matrix', 'matrixlist', 'pmatrix', 'spike'} filenameFull = bst_fullfile(ProtocolInfo.STUDIES, filenameRelative); case 'link' filenameFull = filenameRelative; @@ -257,11 +257,7 @@ % ===== DATA ===== % View data file (MEG and EEG) case {'data', 'pdata', 'rawdata'} - if ~isempty(strfind(filenameRelative, '_0ephys_')) - bst_process('CallProcess', 'process_spikesorting_supervised', filenameRelative, []); - else - view_timeseries(filenameRelative); - end + view_timeseries(filenameRelative); % ===== DATA/MATRIX LIST ===== % Expand node @@ -307,6 +303,10 @@ % Display on existing figures view_dipoles(filenameFull); + % ===== SPIKES ===== + case 'spike' + panel_spikes('OpenSpikeFile', filenameRelative); + % ===== TIME-FREQUENCY ===== case {'timefreq', 'ptimefreq'} % Get study @@ -1829,7 +1829,11 @@ else gui_component('MenuItem', jPopup, [], 'Merge dipoles', IconLoader.ICON_DIPOLES, [], @(h,ev)dipoles_merge(GetAllFilenames(bstNodes))); end - + +%% ===== POPUP: SPIKE ===== + case 'spike' + gui_component('MenuItem', jPopup, [], 'Supervised spike sorting', IconLoader.ICON_SPIKE_SORTING, [], @(h,ev)panel_spikes('OpenSpikeFile', filenameRelative)); + %% ===== POPUP: TIME-FREQ ===== case {'timefreq', 'ptimefreq'} % Get study description @@ -2334,7 +2338,7 @@ gui_component('MenuItem', jMenuFile, [], 'View file history', IconLoader.ICON_MATLAB, [], @(h,ev)bst_history('view', filenameFull)); end % ===== VIEW HISTOGRAM ===== - if isfile && ~ismember(nodeType, {'subject', 'study', 'studysubject', 'condition', 'rawcondition', 'datalist', 'matrixlist', 'image', 'channel', 'rawdata', 'dipoles', 'mri', 'surface', 'cortex', 'anatomy', 'head', 'innerskull', 'outerskull', 'other'}) + if isfile && ~ismember(nodeType, {'subject', 'study', 'studysubject', 'condition', 'rawcondition', 'datalist', 'matrixlist', 'image', 'channel', 'rawdata', 'dipoles', 'mri', 'surface', 'cortex', 'anatomy', 'head', 'innerskull', 'outerskull', 'spike', 'other'}) gui_component('MenuItem', jMenuFile, [], 'View histogram', IconLoader.ICON_HISTOGRAM, [], @(h,ev)view_histogram(GetAllFilenames(bstNodes))); end if (jMenuFile.getMenuComponentCount() > 0) @@ -2802,8 +2806,6 @@ function fcnPopupImportChannel(bstNodes, jMenu, isAddLoc) jMenuType = jMenuBs; elseif ~isempty(strfind(fList(iFile).name, 'BrainProducts')) jMenuType = jMenuBp; - elseif ~isempty(strfind(fList(iFile).name, 'BioSemi')) - jMenuType = jMenuBs; elseif ~isempty(strfind(fList(iFile).name, 'GSN')) || ~isempty(strfind(fList(iFile).name, 'U562')) jMenuType = jMenuEgi; elseif ~isempty(strfind(fList(iFile).name, 'Neuroscan'))