From 6c5cdb5780f145c4992ee76412b4df0f9bc37e63 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Mon, 21 Feb 2022 11:42:32 -0500 Subject: [PATCH 01/43] Update bst_plugin.m to accommodate Invasive Neurophysiology toolboxes --- toolbox/core/bst_plugin.m | 46 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index a5aec59219..64313441eb 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -395,6 +395,51 @@ 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'}; + + % === 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; % === NIRSTORM === PlugDesc(end+1) = GetStruct('nirstorm'); @@ -2215,6 +2260,7 @@ function MenuUpdate(jPlugs) % Is installed? PlugRef = GetSupported(PlugName); Plug = GetInstalled(PlugName); + if ~isempty(Plug) isInstalled = 1; elseif ~isempty(PlugRef) From 697327d478dcba1dd74dc833d7460386034ed8c1 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Tue, 22 Feb 2022 11:26:22 -0500 Subject: [PATCH 02/43] Updated plexon importers --- toolbox/io/in_fopen_plexon.m | 78 ++++++++++++++++++++++++------------ toolbox/io/in_fread_plexon.m | 16 ++++---- 2 files changed, 60 insertions(+), 34 deletions(-) diff --git a/toolbox/io/in_fopen_plexon.m b/toolbox/io/in_fopen_plexon.m index 484bea743a..0b9432f053 100644 --- a/toolbox/io/in_fopen_plexon.m +++ b/toolbox/io/in_fopen_plexon.m @@ -54,6 +54,11 @@ %% ===== READ DATA HEADERS ===== +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('start', 'Plexon importer', 'Reading header'); +end + hdr.chan_headers = {}; hdr.chan_files = {}; hdr.extension = plexonFormat; @@ -70,6 +75,7 @@ channelsWithTimeseriesNames = all_Channel_names(channels_with_timetraces); %% ===== CREATE CHANNEL FILE ===== +bst_progress('text', 'Creating channel file'); all_signalTypesWithoutNumbers = regexprep(all_Channel_names,'[\d"]','')'; signalTypesWithoutNumbers = regexprep(channelsWithTimeseriesNames,'[\d"]','')'; @@ -143,7 +149,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); @@ -155,6 +161,11 @@ names = cellstr(names); % Convert to cell so it can be used in regexprep iPresentEvents = find(logical(evcounts)); +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('start', 'Plexon importer', 'Gathering acquisition events', 0, length(iPresentEvents)); +end + % Read the events if ~isempty(iPresentEvents) @@ -181,6 +192,8 @@ events(iEvt).select = 1; events(iEvt).channels = cell(1, size(events(iEvt).times, 2)); events(iEvt).notes = cell(1, size(events(iEvt).times, 2)); + + bst_progress('inc', 1); end % Import this list sFile = import_events(sFile, [], events); @@ -188,44 +201,59 @@ %% 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 - unique_events = sum(sum(spikes_tscounts(:,2:end)>0)); % First row of spikes_tscounts is ignored - + 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 + + isProgress = bst_progress('isVisible'); + if ~isProgress + bst_progress('start', 'Plexon importer', 'Gathering spiking events', 0, nUnique_events); + end + % 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'); 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 + 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 + 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).channels = cell(1, size(events(iEnteredEvent).times, 2)); + events(iEnteredEvent).notes = cell(1, size(events(iEnteredEvent).times, 2)); + iEnteredEvent = iEnteredEvent + 1; + + bst_progress('inc', 1); - for iNeuron = 1:length(nNeurons) - - if length(nNeurons)>1 - event_label_postfix = [' |' num2str(iNeuron) '|']; - else - event_label_postfix = ''; 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 + +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('stop'); +end + end \ No newline at end of file diff --git a/toolbox/io/in_fread_plexon.m b/toolbox/io/in_fread_plexon.m index 65a4018c17..c0c1cbc922 100644 --- a/toolbox/io/in_fread_plexon.m +++ b/toolbox/io/in_fread_plexon.m @@ -3,9 +3,6 @@ % % 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 - % @============================================================================= % This function is part of the Brainstorm software: % https://neuroimage.usc.edu/brainstorm @@ -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,8 @@ % Initialize Brainstorm output F = zeros(nChannels, nSamples, precision); -for iChannel = 1:nChannels +for iChannel = 1:nChannels [adfreq, n, data] = plx_ad_span_v(sFile.filename, iSelectedChannels(iChannel)-1, SamplesBounds(1), SamplesBounds(2)); F(iChannel,:) = data; end -end - - - +end \ No newline at end of file From 70b1aae6e8d7763686d957832589f2dea467cc28 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Thu, 24 Feb 2022 22:59:26 -0500 Subject: [PATCH 03/43] Updated kilosort-plugin installation Automated copying of default config file during installation --- toolbox/core/bst_plugin.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index 64313441eb..a790baac73 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -407,6 +407,8 @@ 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'); From 1bfa762bbbbbd10b5b98e8c3d3da63e8a01eebff Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Thu, 24 Feb 2022 22:59:55 -0500 Subject: [PATCH 04/43] Update in_fread_plexon.m Values will be in Volts instead of mV --- toolbox/io/in_fread_plexon.m | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/toolbox/io/in_fread_plexon.m b/toolbox/io/in_fread_plexon.m index c0c1cbc922..e8b879cdb5 100644 --- a/toolbox/io/in_fread_plexon.m +++ b/toolbox/io/in_fread_plexon.m @@ -56,8 +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; +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 From 71abe434025074960d977da5bdf3080d7590ef5b Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Thu, 24 Feb 2022 23:10:19 -0500 Subject: [PATCH 05/43] Updated spike-sorter input converters Specifically for the Kilosort converter: the conversion from double to int16 has been achieved by F/max(max(abs(F))) * 15000. 15000 was selected as the middle of the maximum value that an int16 value can be. Tried 32678 but Kilosort was giving weird outputs. So far all the datasets I spike-sorted show reasonable results. --- .../io/in_spikesorting_convertforkilosort.m | 62 ++++++++++++++----- toolbox/io/in_spikesorting_rawelectrodes.m | 58 +++++++++++------ 2 files changed, 84 insertions(+), 36 deletions(-) diff --git a/toolbox/io/in_spikesorting_convertforkilosort.m b/toolbox/io/in_spikesorting_convertforkilosort.m index 220f3f3479..1efd8f9f10 100644 --- a/toolbox/io/in_spikesorting_convertforkilosort.m +++ b/toolbox/io/in_spikesorting_convertforkilosort.m @@ -22,7 +22,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018-2019; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018-2019, 2022; Martin Cousineau, 2018 sInput = varargin{1}; if nargin < 2 || isempty(varargin{2}) @@ -49,14 +49,21 @@ % Separate the file to max length based on RAM numChannels = length(ChannelMat.Channel); -max_samples = ram / 8 / numChannels; +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); 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)); +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('start', 'Spike-sorting', 'Converting to KiloSort Input...', 0, ceil((fileSamples(2)-fileSamples(1))/max_samples)); +end if exist(converted_raw_File, 'file') == 2 - disp('File already converted') + disp('File already converted to kilosort input') return end @@ -77,22 +84,45 @@ %% Convert the acquisition system file to an int16 without a header. fid = fopen(converted_raw_File, 'a'); -isegment = 1; -nsegment_max = 0; +num_segments = ceil(total_samples / max_samples); +num_samples_per_segment = ceil(total_samples / num_segments); + +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('show'); +end +bst_progress('start', 'Kilosort spike sorting', 'Converting to int16 .dat file', 0, num_segments); + -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); +sampleBounds_all = cell(num_segments,1); +sampleBounds = [0,0]; +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; 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'); + F = in_fread(sFile, ChannelMat, [], sampleBounds, [], ImportOptions); - isegment = isegment + 1; + % 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 + + fwrite(fid, F, 'int16'); + + bst_progress('inc', 1); + sampleBounds_all{iSegment} = sampleBounds; % This is here for an easy check that there is no overlap between segments + + clear F end fclose(fid); +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('stop'); +end + + diff --git a/toolbox/io/in_spikesorting_rawelectrodes.m b/toolbox/io/in_spikesorting_rawelectrodes.m index d3df475553..a94e2722d8 100644 --- a/toolbox/io/in_spikesorting_rawelectrodes.m +++ b/toolbox/io/in_spikesorting_rawelectrodes.m @@ -22,7 +22,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018, 2022; Martin Cousineau, 2018 sInput = varargin{1}; if nargin < 2 || isempty(varargin{2}) @@ -74,34 +74,42 @@ 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'; + nBytes = 4; else precision = 'double'; + nBytes = 8; end ImportOptions.Precision = precision; -% Check if a projector has been computed and ask if the selected components +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); + +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('start', 'Spike-sorting', 'Demultiplexing raw file...', 0, (parallel == 0) * num_segments * numChannels); +end + +sFiles = {}; +for iChannel = 1:numChannels + sFiles{end + 1} = bst_fullfile(parentPath, ['raw_elec_' cleanNames{iChannel}]); +end + + +%% Check if a projector has been computed and ask if the selected components % should be removed if ~isempty(ChannelMat.Projector) isOk = java_dialog('confirm', ... @@ -112,16 +120,19 @@ end end -% Read data in segments + +%% Read data in segments +sampleBounds_all = cell(num_segments,1); +sampleBounds = [0,0]; for iSegment = 1:num_segments - samples(1) = (iSegment - 1) * num_samples_per_segment; + sampleBounds(1) = (iSegment - 1) * num_samples_per_segment + round(sFile.prop.times(1)* sFile.prop.sfreq); if iSegment < num_segments - samples(2) = iSegment * num_samples_per_segment - 1; + sampleBounds(2) = sampleBounds(1) + num_samples_per_segment - 1; else - samples(2) = total_samples; + sampleBounds(2) = total_samples; end - F = in_fread(sFile, ChannelMat, [], samples, [], ImportOptions); + F = in_fread(sFile, ChannelMat, [], sampleBounds, [], ImportOptions); % Append segment to individual channel file if parallel @@ -140,10 +151,16 @@ bst_progress('inc', 1); end end + clear F + sampleBounds_all{iSegment} = sampleBounds; % This is here for an easy check that there is no overlap between segments end -% Convert channel files to Matlab -bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...', 0, (parallel == 0) * numChannels); + +%% Convert binary files per channel to Matlab files +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...', 0, (parallel == 0) * numChannels); +end if parallel parfor iChannel = 1:numChannels convert2mat(sFiles{iChannel}, sr, precision); @@ -159,6 +176,7 @@ end + function convert2mat(chanFile, sr, precision) fid = fopen([chanFile '.bin'], 'rb'); data = fread(fid, precision); From b756d378218160325c4dd5087f7f7e22587e16e3 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Thu, 24 Feb 2022 23:14:05 -0500 Subject: [PATCH 06/43] Update process_spikesorting_kilosort.m Added .xml converter straight from Brainstorm environment. Removed .xslx dependency - Kilosort can be used in non-Windows systems now. Enabled event-dot-visualization on spiking events Moved Kilosort installation to the plugin manager --- .../functions/process_spikesorting_kilosort.m | 655 +++++++++--------- 1 file changed, 341 insertions(+), 314 deletions(-) diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index 94221638bc..fb1e1b79c7 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -28,7 +28,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018, 2022; Martin Cousineau, 2018 eval(macro_method); end @@ -83,15 +83,6 @@ 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.'); @@ -108,25 +99,14 @@ return; end - % 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)); - 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 plugin + [isInstalled, errMsg] = bst_plugin('Install', 'kilosort'); + if ~isInstalled + error(errMsg); end + %% Prepare parallel pool try poolobj = gcp('nocreate'); @@ -137,6 +117,7 @@ poolobj = []; end + %% Initialize KiloSort Parameters (This is a copy of StandardConfig_MOVEME) KilosortStandardConfig(); ops.GPU = sProcess.options.GPU.Value; @@ -167,8 +148,6 @@ sFile = DataMat.F; - - %% %%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% outputPath = bst_fullfile(ProtocolInfo.STUDIES, fPath, [fBase '_kilosort_spikes']); @@ -177,7 +156,7 @@ try rmdir(outputPath, 's'); catch - error('Couldnt remove spikes folder. Make sure the current directory is not that folder.') + error('Couldnt remove spikes folder. Make sure the current directory is not that folder or that Klusters is not open.') end end @@ -192,35 +171,9 @@ 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 - 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 - + %% Get the channels in the montage + % First check if any montages have been assigned + [Channels, Montages, channelsMontage,montageOccurences] = deal_with_channels_and_groups(ChannelMat); %% Adjust the possible clusters based on the number of channels @@ -278,111 +231,55 @@ ops.nt0 =ops.nt0+1; 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']); - - 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); + %% 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 - - 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') + %% 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_bst(ChannelMat, fs, xmlFile, ops) 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...'); + %% %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% + bst_progress('text', 'Kilosort spike sorting'); - - - %% Some residual parameters that need the outputPath and the converted Raw signal + % 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.basename = fBase; ops.fs = fs; % sampling rate ops.NchanTOT = numChannels; % total number of channels ops.Nchan = numChannels; % number of active channels - - %% KiloSort + % 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); - - %% 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) @@ -391,11 +288,9 @@ % .res: holds the spiketimes % .spk: holds the spike waveforms - Kilosort2Neurosuite(rez) - %% %%%%%%%%%%%%%%%%%%% Create Brainstorm Events %%%%%%%%%%%%%%%%%%% bst_progress('text', 'Saving events file...'); @@ -463,11 +358,18 @@ if ~isempty(poolobj) delete(poolobj); end + + isProgress = bst_progress('isVisible'); + if ~isProgress + bst_progress('stop'); + end + + cd(previous_directory); + end - function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) events = struct(); @@ -490,7 +392,6 @@ 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); @@ -504,18 +405,20 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) index = index+1; % Write the packet to events + events(index).color = rand(1,3); + events(index).epochs = ones(1,length(spikeTimes(selectedSpikes))); + events(index).times = spikeTimes(selectedSpikes)'./sFile.prop.sfreq + sFile.prop.times(1); + 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 = [spikeEventPrefix ' ' ChannelMat.Channel(amplitude_max_channel(uniqueClusters(iCluster))).Name ' |' num2str(uniqueClusters(iCluster)) '|']; + events(index).label = [spikeEventPrefix ' ' ChannelMat.Channel(amplitude_max_channel(uniqueClusters(iCluster))).Name ' |' num2str(uniqueClusters(iCluster)) '|']; + events(index).channels = repmat({{ChannelMat.Channel(amplitude_max_channel(uniqueClusters(iCluster))).Name}}, 1, size(events(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 % Add existing non-spike events for backup @@ -531,169 +434,20 @@ 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 - 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 - - % 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')); + save(fullfile(parentPath,'events_UNSUPERVISED.mat'),'events') + %% Assign the unsupervised spike sorted events to the link to raw file + % Design choice: DO NOT ADD IT. + % Maybe revisit. A way for this to be added, is if the spiking events + % get deleted if the users finally decide to do manual spike-sorting. - % 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')); +% sFile = import_events(sFile, [], events); - % Delete unnecessary files - file_delete(KiloSortTmpDir, 1, 3); - % Add KiloSort to Matlab path - addpath(genpath(KiloSortDir)); end -function events = LoadKlustersEvents(SpikeSortedMat, iMontage) + +function [events, Channels] = LoadKlustersEvents(SpikeSortedMat, iMontage) % Information about the Neuroscope file can be found here: % http://neurosuite.sourceforge.net/formats.html @@ -709,9 +463,11 @@ function downloadAndInstallKiloSort() 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] = deal_with_channels_and_groups(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. @@ -748,18 +504,20 @@ 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 @@ -783,3 +541,272 @@ function copyKilosortConfig(defaultFile, outputFile) fclose(inFid); fclose(outFid); end + + +function createXML_bst(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); + +%add channel count here + +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 + + +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. + +[nrows,ncols]= size(charcell); + +fid = fopen(filename, 'w'); + +for row=1:nrows + fprintf(fid, '%s \n', charcell{row,:}); +end + +fclose(fid); +end + + +function [Channels, Montages, channelsMontage,montageOccurences] = deal_with_channels_and_groups(ChannelMat) + + %% Get the channels in the montage + % 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 + + From b2c5288c62fce15c9f261bc5f287c2a7af64f6df Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Thu, 24 Feb 2022 23:18:36 -0500 Subject: [PATCH 07/43] Update process_spikesorting_supervised.m Redesigned behavior when manual spike-sorting is performed in Klusters. All the spiking events that belong to electrodes that belong to the shank/Montage will be removed, and then the new manual spiking selections will be added to the link-to-raw file --- .../process_spikesorting_supervised.m | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/toolbox/process/functions/process_spikesorting_supervised.m b/toolbox/process/functions/process_spikesorting_supervised.m index fc9ba8e9e7..9295326ddf 100644 --- a/toolbox/process/functions/process_spikesorting_supervised.m +++ b/toolbox/process/functions/process_spikesorting_supervised.m @@ -433,9 +433,19 @@ function SaveElectrode() end case 'kilosort' - newEvents = process_spikesorting_kilosort('LoadKlustersEvents', ... + [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.'); @@ -481,25 +491,45 @@ function SaveElectrode() numEvents = length(DataMat.F.events); % Delete existing event(s) if numEvents > 0 - iDelEvents = cellfun(@(x) ~isempty(x), strfind({DataMat.F.events.label}, strtrim(eventName))); + 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); - for iEvent = 1:length(newEvents) - DataMat.F.events(numEvents + iEvent) = newEvents(iEvent); + 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 -function prefix = GetSpikesEventPrefix() - prefix = 'Spikes Channel'; +function prefix = GetSpikesEventPrefix(varargin) + if length(varargin) < 1 + prefix = 'Spikes Channel'; + else + prefix = {'Spikes Channel', 'Spikes Noise'}; + end end function isSpikeEvent = IsSpikeEvent(eventLabel) - prefix = GetSpikesEventPrefix(); - isSpikeEvent = strncmp(eventLabel, prefix, length(prefix)); + 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 function neuron = GetNeuronOfSpikeEvent(eventLabel) From 4ff8c40856e0cd6ca1e0d0e713b955715ca95402 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Fri, 25 Feb 2022 00:08:14 -0500 Subject: [PATCH 08/43] Update bst_plugin.m Added ultramegasort2000 and waveclus spikesorters --- toolbox/core/bst_plugin.m | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index a790baac73..807ebb90d0 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -442,6 +442,28 @@ 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'); From 9b18f7f85bb5947d4573e6ea8dd197619285297f Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Fri, 25 Feb 2022 00:08:38 -0500 Subject: [PATCH 09/43] Update process_spikesorting_supervised.m Accommodate different starting timepoint than 0 --- .../functions/process_spikesorting_supervised.m | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/toolbox/process/functions/process_spikesorting_supervised.m b/toolbox/process/functions/process_spikesorting_supervised.m index 9295326ddf..7f2e42d68e 100644 --- a/toolbox/process/functions/process_spikesorting_supervised.m +++ b/toolbox/process/functions/process_spikesorting_supervised.m @@ -406,11 +406,11 @@ function SaveElectrode() 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; + 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; + tmpEvents(iNeuron).times = ElecData.cluster_class(ElecData.cluster_class(:,1) == iNeuron, 2)' ./ 1000 + DataMat.F.prop.times(1); end end else @@ -424,11 +424,11 @@ function SaveElectrode() tmpEvents = struct(); if numNeurons == 1 tmpEvents(1).epochs = ones(1,length(ElecData.spikes.assigns)); - tmpEvents(1).times = ElecData.spikes.spiketimes; + 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)); + tmpEvents(iNeuron).times = ElecData.spikes.spiketimes(ElecData.spikes.assigns == ElecData.spikes.labels(iNeuron,1)) + DataMat.F.prop.times(1); end end @@ -459,8 +459,10 @@ function SaveElectrode() 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)); + 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) '|']; @@ -469,8 +471,9 @@ function SaveElectrode() 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)); + newEvents(iNeuron).channels = repmat({{electrodeName}}, 1, size(newEvents(iNeuron).times, 2)); + end else % This electrode just picked up noise, no event to add. From b986abfcaf97323bf1c79912aa6b8c5b2b009bc8 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Fri, 25 Feb 2022 01:10:35 -0500 Subject: [PATCH 10/43] Updated spikesorters Moved installation to the plugin manager --- .../functions/process_spikesorting_kilosort.m | 3 +- .../process_spikesorting_ultramegasort2000.m | 85 ++++-------------- .../functions/process_spikesorting_waveclus.m | 89 ++++--------------- 3 files changed, 34 insertions(+), 143 deletions(-) diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index fb1e1b79c7..9844d5fb17 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -111,6 +111,7 @@ try poolobj = gcp('nocreate'); if isempty(poolobj) + bst_progress('text', 'Startin parallel pool'); parpool; end catch @@ -118,7 +119,7 @@ end - %% Initialize KiloSort Parameters (This is a copy of StandardConfig_MOVEME) + %% Initialize KiloSort Parameters (This initially is a copy of StandardConfig_MOVEME) KilosortStandardConfig(); ops.GPU = sProcess.options.GPU.Value; diff --git a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m index 4b204e1df5..c5eda441e5 100644 --- a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m +++ b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m @@ -28,7 +28,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018-2019; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018-2019, 2022; Martin Cousineau, 2018 eval(macro_method); end @@ -101,23 +101,11 @@ return 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)); - end - - % 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 plugin + [isInstalled, errMsg] = bst_plugin('Install', 'ultramegasort2000'); + if ~isInstalled + error(errMsg); end % Compute on each raw input independently @@ -162,11 +150,16 @@ end mkdir(outputPath); - %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% + %% %%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% + isProgress = bst_progress('isVisible'); if sProcess.options.paral.Value - bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); + if isProgress + bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); + end else - bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); + if isProgress + bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); + end end %% UltraMegaSort2000 needs manual filtering of the raw files @@ -253,56 +246,10 @@ end end -end - - -%% ===== 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); + if isProgress + bst_progress('stop'); 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)); end diff --git a/toolbox/process/functions/process_spikesorting_waveclus.m b/toolbox/process/functions/process_spikesorting_waveclus.m index 336491a74b..3bf5ff7289 100644 --- a/toolbox/process/functions/process_spikesorting_waveclus.m +++ b/toolbox/process/functions/process_spikesorting_waveclus.m @@ -28,7 +28,7 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018-2019; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018-2019, 2022; Martin Cousineau, 2018 eval(macro_method); end @@ -95,23 +95,11 @@ return end - % 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 plugin + [isInstalled, errMsg] = bst_plugin('Install', 'waveclus'); + if ~isInstalled + error(errMsg); end % Prepare parallel pool, if requested @@ -119,6 +107,7 @@ try poolobj = gcp('nocreate'); if isempty(poolobj) + bst_progress('text', 'Starting parallel pool'); parpool; end catch @@ -155,9 +144,10 @@ mkdir(outputPath); %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% - if sProcess.options.paral.Value + isProgress = bst_progress('isVisible'); + if sProcess.options.paral.Value && isProgress bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); - else + elseif isProgress bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); end @@ -181,7 +171,7 @@ end %%%%%%%%%%%%%%%%%%%%%% Do the clustering %%%%%%%%%%%%%%%%%%%%%%%%%% - bst_progress('start', 'Spike-sorting', 'Clustering detected spikes...'); + bst_progress('text', 'Clustering detected spikes...'); % The optional inputs in Do_clustering have to be true or false, not 1 or 0 if sProcess.options.paral.Value @@ -266,57 +256,10 @@ 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); - end - if isdir(waveclusTmpDir) - file_delete(waveclusTmpDir, 1, 3); - 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 - - % 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]); - 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)); end function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) @@ -349,8 +292,8 @@ function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) sFile.Device, ... bst_fullfile(sFile.Parent, sFile.Spikes(iElectrode).File), ... sFile.Spikes(iElectrode).Name, ... - 0, eventNamePrefix); - + 0, 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); From 21552d1f1fa3a8ea35c9349c1974ce079105c354 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Fri, 25 Feb 2022 01:11:58 -0500 Subject: [PATCH 11/43] Assigned channels on each spiking event This allows dots-visualizations along the raw file exploration --- toolbox/io/in_events_nwb.m | 2 +- toolbox/io/in_fopen_plexon.m | 11 ++++++----- toolbox/io/in_fopen_tdt.m | 36 ++++++++++++------------------------ 3 files changed, 19 insertions(+), 30 deletions(-) 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_plexon.m b/toolbox/io/in_fopen_plexon.m index 0b9432f053..b1a27122d7 100644 --- a/toolbox/io/in_fopen_plexon.m +++ b/toolbox/io/in_fopen_plexon.m @@ -55,7 +55,7 @@ %% ===== READ DATA HEADERS ===== isProgress = bst_progress('isVisible'); -if ~isProgress +if isProgress bst_progress('start', 'Plexon importer', 'Reading header'); end @@ -162,7 +162,7 @@ iPresentEvents = find(logical(evcounts)); isProgress = bst_progress('isVisible'); -if ~isProgress +if isProgress bst_progress('start', 'Plexon importer', 'Gathering acquisition events', 0, length(iPresentEvents)); end @@ -206,7 +206,7 @@ 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 isProgress = bst_progress('isVisible'); - if ~isProgress + if isProgress bst_progress('start', 'Plexon importer', 'Gathering spiking events', 0, nUnique_events); end @@ -237,8 +237,9 @@ 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)); + events(iEnteredEvent).channels = repmat({{all_Channel_names{iChannels_selected(iChannel)}}}, 1, size(events(iEnteredEvent).times, 2)); + iEnteredEvent = iEnteredEvent + 1; bst_progress('inc', 1); @@ -252,7 +253,7 @@ end isProgress = bst_progress('isVisible'); -if ~isProgress +if isProgress bst_progress('stop'); end diff --git a/toolbox/io/in_fopen_tdt.m b/toolbox/io/in_fopen_tdt.m index 8b3756a4ab..100b5e85e6 100644 --- a/toolbox/io/in_fopen_tdt.m +++ b/toolbox/io/in_fopen_tdt.m @@ -42,18 +42,15 @@ hdr.BaseFolder = DataFolder; - - - %% ===== FILE COMMENT ===== % Comment: BaseFolder Comment = DataFolder; - %% ===== READ DATA HEADERS ===== - -bst_progress('start', 'TDT', 'Reading headers...'); - +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('start', 'TDT', 'Reading headers...'); +end % 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 +76,6 @@ LFP_label_exists = 0; - ii = 1; for iStream = 1:length(all_streams) stream_info(iStream).label = all_streams{iStream}; @@ -171,7 +167,7 @@ %% Check for acquisition events -bst_progress('start', 'TDT', 'Collecting acquisition events...'); +bst_progress('text', 'Collecting acquisition events...'); disp('Getting Acquisition System events') NO_data = TDTbin2mat(DataFolder, 'TYPE', 2); % Just load epocs / events @@ -222,18 +218,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', 'Collecting spiking events...'); NO_data = TDTbin2mat(DataFolder, 'TYPE', 3); % Just load spikes are_there_spikes = ~isempty(NO_data.snips); else @@ -241,8 +229,6 @@ end - - %%%%%%%% disp('***************************************************') disp('CHECK THE SPIKES. THEY ARE ONLY ASSIGNED ON RIG TWO') @@ -250,8 +236,6 @@ %%%%%%%% - - if are_there_spikes if ~exist ('events','var') @@ -302,7 +286,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 +296,10 @@ % Import this list sFile = import_events(sFile, [], events); -end +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('stop'); +end +end \ No newline at end of file From c1b04008979de5635f9528f212645d99d955d7fd Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Fri, 25 Feb 2022 12:24:39 -0500 Subject: [PATCH 12/43] Unsupervised spikes are automatically loaded Design change - the spikesorters when they finish their unsupervised part automatically attach the events to the link to raw file. The reason for this design change is that the previous commit allows deletion of the relevant events that are manually spike sorted --- .../functions/process_spikesorting_kilosort.m | 60 +++++++++---------- .../process_spikesorting_ultramegasort2000.m | 2 +- .../functions/process_spikesorting_waveclus.m | 2 +- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index 9844d5fb17..13c3dfc276 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -111,7 +111,7 @@ try poolobj = gcp('nocreate'); if isempty(poolobj) - bst_progress('text', 'Startin parallel pool'); + bst_progress('text', 'Starting parallel pool'); parpool; end catch @@ -254,6 +254,7 @@ 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', 'Kilosort spike sorting'); % Some residual parameters that need the outputPath and the converted Raw signal @@ -330,21 +331,20 @@ % 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; + DataMat_spikesorter = struct(); + DataMat_spikesorter.Comment = 'KiloSort Spike Sorting'; + DataMat_spikesorter.DataType = 'raw';%'ephys'; + DataMat_spikesorter.Device = 'KiloSort'; + DataMat_spikesorter.Parent = outputPath; + DataMat_spikesorter.Spikes = spikes; + DataMat_spikesorter.RawFile = sInputs(i).FileName; + DataMat_spikesorter.Name = NewBstFile; % Add history field - DataMat = bst_history('add', DataMat, 'import', ['Link to unsupervised electrophysiology files: ' outputPath]); + DataMat_spikesorter = bst_history('add', DataMat_spikesorter, 'import', ['Link to unsupervised electrophysiology files: ' outputPath]); % Save file on hard drive - bst_save(NewBstFile, DataMat, 'v6'); + bst_save(NewBstFile, DataMat_spikesorter, 'v6'); % Add file to database - sOutputStudy = db_add_data(sInputs(i).iStudy, NewBstFile, DataMat); + sOutputStudy = db_add_data(sInputs(i).iStudy, NewBstFile, DataMat_spikesorter); % Return new file OutputFiles{end+1} = NewBstFile; @@ -373,8 +373,7 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) - events = struct(); - index = 0; + events_spikes = struct(); % st: first column is the spike time in samples, % second column is the spike template, @@ -399,6 +398,7 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) spikeEventPrefix = process_spikesorting_supervised('GetSpikesEventPrefix'); + index = 0; % Fill the events fields for iCluster = 1:length(unique(spikeTemplates)) selectedSpikes = find(spikeTemplates==uniqueClusters(iCluster)); @@ -406,22 +406,23 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) index = index+1; % Write the packet to events - events(index).color = rand(1,3); - events(index).epochs = ones(1,length(spikeTimes(selectedSpikes))); - events(index).times = spikeTimes(selectedSpikes)'./sFile.prop.sfreq + sFile.prop.times(1); - events(index).reactTimes = []; - events(index).select = 1; - events(index).notes = cell(1, size(events(index).times, 2)); + 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(index).channels = cell(1, size(events(index).times, 2)); + 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(index).channels = repmat({{ChannelMat.Channel(amplitude_max_channel(uniqueClusters(iCluster))).Name}}, 1, size(events(index).times, 2)); + 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 end + index = 0; % Add existing non-spike events for backup DataMat = in_bst_data(sFile.RawFile); existingEvents = DataMat.F.events; @@ -435,15 +436,14 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) index = index + 1; end end + events = [events events_spikes]; save(fullfile(parentPath,'events_UNSUPERVISED.mat'),'events') %% Assign the unsupervised spike sorted events to the link to raw file - % Design choice: DO NOT ADD IT. - % Maybe revisit. A way for this to be added, is if the spiking events - % get deleted if the users finally decide to do manual spike-sorting. - -% sFile = import_events(sFile, [], events); + DataMat.F.events = events; + [folder, filename_link2Raw, extension] = bst_fileparts(sFile.RawFile); + bst_save(bst_fullfile(parentPath, [filename_link2Raw extension]), DataMat, 'v6'); end diff --git a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m index c5eda441e5..77c7d64830 100644 --- a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m +++ b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m @@ -283,7 +283,7 @@ function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) sFile.Device, ... bst_fullfile(sFile.Parent, sFile.Spikes(iElectrode).File), ... sFile.Spikes(iElectrode).Name, ... - 0, eventNamePrefix); + 1, eventNamePrefix); if iNewEvent == 0 events = newEvents; diff --git a/toolbox/process/functions/process_spikesorting_waveclus.m b/toolbox/process/functions/process_spikesorting_waveclus.m index 3bf5ff7289..f652525ff8 100644 --- a/toolbox/process/functions/process_spikesorting_waveclus.m +++ b/toolbox/process/functions/process_spikesorting_waveclus.m @@ -292,7 +292,7 @@ function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) sFile.Device, ... bst_fullfile(sFile.Parent, sFile.Spikes(iElectrode).File), ... sFile.Spikes(iElectrode).Name, ... - 0, 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, 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; From 3cc50524e5507451cb834f6510650fcada874cda Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Sat, 12 Mar 2022 15:03:30 -0500 Subject: [PATCH 13/43] Separated .plx and .pl2 importers In case of .plx, the entire recording is converted to a .bst to avoid the inefficient loading that .plx files have --- toolbox/io/in_data_plx.m | 70 ++++++++++++++++++++++++++++++++++++ toolbox/io/in_fopen.m | 10 +++++- toolbox/io/in_fread_plexon.m | 2 +- 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 toolbox/io/in_data_plx.m diff --git a/toolbox/io/in_data_plx.m b/toolbox/io/in_data_plx.m new file mode 100644 index 0000000000..c0c84349a5 --- /dev/null +++ b/toolbox/io/in_data_plx.m @@ -0,0 +1,70 @@ +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 = linspace(sFile.prop.times(1),sFile.prop.times(2), size(DataMat.F,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_fopen.m b/toolbox/io/in_fopen.m index 7a25008753..4f09511373 100644 --- a/toolbox/io/in_fopen.m +++ b/toolbox/io/in_fopen.m @@ -152,7 +152,15 @@ case 'EEG-INTAN' [sFile, ChannelMat] = in_fopen_intan(DataFile); case 'EEG-PLEXON' - [sFile, ChannelMat] = in_fopen_plexon(DataFile); +% [sFile, ChannelMat] = in_fopen_plexon(DataFile); + [fPath, fBase, fExt] = bst_fileparts(DataFile); + switch lower(fExt) + case '.plx' + [DataMat, ChannelMat] = in_data_plx(DataFile); + case '.pl2' + [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_fread_plexon.m b/toolbox/io/in_fread_plexon.m index e8b879cdb5..176ec8c2ec 100644 --- a/toolbox/io/in_fread_plexon.m +++ b/toolbox/io/in_fread_plexon.m @@ -1,7 +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=[]) +% USAGE: F = in_fread_plexon(sFile, SamplesBounds=[], iChannels=[]) % @============================================================================= % This function is part of the Brainstorm software: From eef0c0fb781b0727c936cba64e8a2e55ed37696e Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Sat, 12 Mar 2022 16:00:25 -0500 Subject: [PATCH 14/43] Corrected precision error in timevector in_data_plx linspace created precision error on the computation of Fs. Fixed bug --- toolbox/io/in_data_plx.m | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/toolbox/io/in_data_plx.m b/toolbox/io/in_data_plx.m index c0c84349a5..11d21e07e9 100644 --- a/toolbox/io/in_data_plx.m +++ b/toolbox/io/in_data_plx.m @@ -49,7 +49,8 @@ DataMat.Events = sFile.events; % Build time vector -DataMat.Time = linspace(sFile.prop.times(1),sFile.prop.times(2), size(DataMat.F,2)); +DataMat.Time = sFile.prop.times(1):1/sFile.prop.sfreq:sFile.prop.times(2); + % ChannelFlag DataMat.ChannelFlag = ones(size(DataMat.F,1), 1); From 7f6f5cfbc2452811e721268556ffe35f4839f8e1 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Sun, 13 Mar 2022 03:57:06 -0400 Subject: [PATCH 15/43] Updated convert to LFP CONVERT TO LFP: Added option to select the sampling rate of the LFPs. Substituted the resampling function from Matlab's 'downsample', to BST 'resample' Bug Fix on demultiplexers - In_spikesorting_rawelectrodes, in_spikesorting_convertforkilosort --- .../io/in_spikesorting_convertforkilosort.m | 4 +- toolbox/io/in_spikesorting_rawelectrodes.m | 2 +- .../functions/process_convert_raw_to_lfp.m | 59 +++++++++++-------- 3 files changed, 38 insertions(+), 27 deletions(-) diff --git a/toolbox/io/in_spikesorting_convertforkilosort.m b/toolbox/io/in_spikesorting_convertforkilosort.m index 1efd8f9f10..b6f79a9190 100644 --- a/toolbox/io/in_spikesorting_convertforkilosort.m +++ b/toolbox/io/in_spikesorting_convertforkilosort.m @@ -101,9 +101,9 @@ if iSegment < num_segments sampleBounds(2) = sampleBounds(1) + num_samples_per_segment - 1; else - sampleBounds(2) = total_samples; + sampleBounds(2) = total_samples + round(sFile.prop.times(1)* sFile.prop.sfreq); end - + F = in_fread(sFile, ChannelMat, [], sampleBounds, [], ImportOptions); % Adaptive conversion to int16 to avoid saturation diff --git a/toolbox/io/in_spikesorting_rawelectrodes.m b/toolbox/io/in_spikesorting_rawelectrodes.m index a94e2722d8..5805179de4 100644 --- a/toolbox/io/in_spikesorting_rawelectrodes.m +++ b/toolbox/io/in_spikesorting_rawelectrodes.m @@ -129,7 +129,7 @@ if iSegment < num_segments sampleBounds(2) = sampleBounds(1) + num_samples_per_segment - 1; else - sampleBounds(2) = total_samples; + sampleBounds(2) = total_samples + round(sFile.prop.times(1)* sFile.prop.sfreq); end F = in_fread(sFile, ChannelMat, [], sampleBounds, [], ImportOptions); diff --git a/toolbox/process/functions/process_convert_raw_to_lfp.m b/toolbox/process/functions/process_convert_raw_to_lfp.m index 3bab666637..74da300882 100644 --- a/toolbox/process/functions/process_convert_raw_to_lfp.m +++ b/toolbox/process/functions/process_convert_raw_to_lfp.m @@ -49,6 +49,10 @@ sProcess.options.despikeLFP.Type = 'checkbox'; sProcess.options.despikeLFP.Value = 1; + 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}; + sProcess.options.filterbounds.Comment = 'LFP filtering limits'; sProcess.options.filterbounds.Type = 'range'; sProcess.options.filterbounds.Value = {[0.5, 150],'Hz',1}; @@ -92,8 +96,7 @@ 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; - + LFP_fs = sProcess.options.LFP_fs.Value{1}; % Get method name if (nargin < 3) @@ -138,19 +141,25 @@ 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 + Fs = sMat.F.prop.sfreq; % 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 + LFP = zeros(length(sFiles_temp_mat), length(downsample(sMat.Time, round(Fs/LFP_fs)))); % This shouldn't create a memory problem + %% Make a check if the requested sampling rate is higher than the original sampling rate + + if Fs < LFP_fs + bst_report('Error', sProcess, sInputs(iInput), 'The requested LFP sampling rate is higher than the RAW signal sampling rate. No need to use this function'); + end + %% Initialize % Prepare output file ProtocolInfo = bst_get('ProtocolInfo'); newCondition = [sInput.Condition, '_LFP']; - if mod(Fs,NewFreq) ~= 0 + if mod(Fs,LFP_fs) ~= 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']) + warning(['The downsampling might not be accurate. This process downsamples from ' num2str(Fs) ' to ' num2str(LFP_fs) ' Hz']) end % Get new condition name @@ -182,46 +191,47 @@ 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.prop.sfreq = LFP_fs; +% sFileTemplate.prop.times = round(sFileTemplate.prop.times(1) * NewFreq) / NewFreq + [0, size(LFP,2)-1] ./ NewFreq; + sFileTemplate.prop.times = [sFileTemplate.prop.times(1) (size(LFP,2)-1) ./ LFP_fs]; + sFileTemplate.header.sfreq = LFP_fs; 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.CommentTag = sprintf('resample(%dHz)', round(LFP_fs)); + sFileTemplate.HistoryComment = sprintf('Filter [%0.1f-%0.1f]Hz - Resample from %0.2f Hz to %0.2f Hz (%s)', filterBounds(1), filterBounds(2), Fs, LFP_fs, method); sFileTemplate.despikeLFP = sProcess.options.despikeLFP.Value; % Convert events to new sampling rate - newTimeVector = panel_time('GetRawTimeVector', sFileTemplate); +% newTimeVector = panel_time('GetRawTimeVector', sFileTemplate); + newTimeVector = downsample(sMat.Time, round(Fs/1000)); 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); + LFP(iChannel,:) = BayesianSpikeRemoval(sFiles_temp_mat{iChannel}, filterBounds, sMat.F, ChannelMat, cleanChannelNames, notchFilterFreqs, LFP_fs); end else for iChannel = 1:nChannels - LFP(iChannel,:) = BayesianSpikeRemoval(sFiles_temp_mat{iChannel}, filterBounds, sMat.F, ChannelMat, cleanChannelNames, notchFilterFreqs); + LFP(iChannel,:) = BayesianSpikeRemoval(sFiles_temp_mat{iChannel}, filterBounds, sMat.F, ChannelMat, cleanChannelNames, notchFilterFreqs, LFP_fs); 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); + LFP(iChannel,:) = filter_and_downsample(sFiles_temp_mat{iChannel}, Fs, filterBounds, notchFilterFreqs, LFP_fs); end else for iChannel = 1:nChannels - LFP(iChannel,:) = filter_and_downsample(sFiles_temp_mat{iChannel}, Fs, filterBounds, notchFilterFreqs); + LFP(iChannel,:) = filter_and_downsample(sFiles_temp_mat{iChannel}, Fs, filterBounds, notchFilterFreqs, LFP_fs); bst_progress('inc', 1); end end @@ -247,7 +257,7 @@ end -function data = filter_and_downsample(inputFilename, Fs, filterBounds, notchFilterFreqs) +function data = filter_and_downsample(inputFilename, Fs, filterBounds, notchFilterFreqs, LFP_fs) 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) @@ -259,12 +269,12 @@ % 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. + [data, time_out] = process_resample('Compute', data, linspace(0, length(data)/sMat.sr, length(data)), LFP_fs); end %% BAYESIAN DESPIKING -function data_derived = BayesianSpikeRemoval(inputFilename, filterBounds, sFile, ChannelMat, cleanChannelNames, notchFilterFreqs) +function data_derived = BayesianSpikeRemoval(inputFilename, filterBounds, sFile, ChannelMat, cleanChannelNames, notchFilterFreqs, LFP_fs) sMat = load(inputFilename); % Make sure that a variable named data is loaded here. This file is saved as an output from the separator @@ -280,7 +290,7 @@ Fs = sMat.sr; % Assume that a spike lasts 3ms - nSegment = sMat.sr * 0.003; + nSegment = round(sMat.sr * 0.003); Bs = eye(nSegment); % 60x60 opts.displaylevel = 0; % 0 gets rid of all the outputs % 2 shows the optimization steps @@ -347,8 +357,9 @@ data_derived = bst_bandpass_hfilter(data_derived, Fs, filterBounds(1), filterBounds(2), 0, 0); end - data_derived = downsample(data_derived, round(sMat.sr/1000)); % The file now has a different sampling rate (fs/30) = 1000Hz - +% data_derived = downsample(data_derived, round(sMat.sr/LFP_fs)); % The file now has a different sampling rate (fs/30) = 1000Hz + [data_derived, time_out] = process_resample('Compute', data_derived, linspace(0, length(data_derived)/Fs, length(data_derived)), LFP_fs); + end From a99d65b1fe3f543ac9120c566f595a0afa36255f Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Thu, 24 Mar 2022 18:21:19 -0400 Subject: [PATCH 16/43] fixed rounding error in Kilosort --- toolbox/process/functions/process_spikesorting_kilosort.m | 1 + 1 file changed, 1 insertion(+) diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index 13c3dfc276..2dc865577b 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -231,6 +231,7 @@ 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 From 857636221b5ae5ad43cdcb13c07b6578f054ff7d Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Fri, 25 Mar 2022 22:07:02 -0400 Subject: [PATCH 17/43] function changes Spiking phase locking: display of total number of neurons, don't display omnibus test Tuning curves: took into account case where a single spike occured. --- toolbox/gui/figure_timefreq.m | 12 +++++++----- .../functions/process_spiking_phase_locking.m | 13 ++++++------- toolbox/process/functions/process_tuning_curves.m | 7 ++++++- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/toolbox/gui/figure_timefreq.m b/toolbox/gui/figure_timefreq.m index 52eb4c2ece..66fd13441d 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 @@ -1382,7 +1384,7 @@ function ConfigureAxes(hAxes, Time, FullTimeVector, Freqs, TfInfo, MinMaxVal, Lo elseif ~isempty(strfind(lower(TfInfo.FileName), 'rasterplot')) xlabel(hAxes, 'Time (s)'); ylabel(hAxes, 'Trials'); - elseif ~isempty(strfind(lower(TfInfo.FileName), 'spiking_phase_locking')) + elseif ~isempty(strfind(lower(TfInfo.FileName), 'spiking_phase_locking`')) xlabel(hAxes, ' '); ylabel(hAxes, ' '); else diff --git a/toolbox/process/functions/process_spiking_phase_locking.m b/toolbox/process/functions/process_spiking_phase_locking.m index cb4492eb7e..519d3d628d 100644 --- a/toolbox/process/functions/process_spiking_phase_locking.m +++ b/toolbox/process/functions/process_spiking_phase_locking.m @@ -162,7 +162,8 @@ %% Accumulate the phases that each neuron fired upon nBins = round(360/sProcess.options.phaseBin.Value{1}) + 1; - all_phases = zeros(length(labelsForDropDownMenu), nBins-1); + all_phases = zeros(length(labelsForDropDownMenu), nBins-1); + total_spikes = zeros(length(labelsForDropDownMenu), 1); EDGES = linspace(-pi,pi,nBins); @@ -197,7 +198,8 @@ events(iEvent_Neuron).times = events(iEvent_Neuron).times(events(iEvent_Neuron).times>DataMat.Time(1) & ... events(iEvent_Neuron).times1 + CI(:,iCondition) = bootci(RESAMPLING, {@mean, y}, 'type','cper','alpha',0.05); + else + CI(:,iCondition) = [0;0]; + end end From 42ac8e3196457868205fed9c82437c2b22b9eba9 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Sat, 26 Mar 2022 00:24:07 -0400 Subject: [PATCH 18/43] TDT loader fix Fixed display when one of the streams is disabled during acquisition --- toolbox/io/in_fopen_tdt.m | 14 +++++++++++--- toolbox/io/in_fread_tdt.m | 11 ++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/toolbox/io/in_fopen_tdt.m b/toolbox/io/in_fopen_tdt.m index 100b5e85e6..8299a3a53c 100644 --- a/toolbox/io/in_fopen_tdt.m +++ b/toolbox/io/in_fopen_tdt.m @@ -95,22 +95,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) + bst_error('The selected stream is empty') + end if isempty(indx) bst_error('No stream was selected') - stop end end @@ -202,7 +205,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)'; 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; From ef2df6ab7c6b21ca0e87626673693dc1337dbf34 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Sat, 26 Mar 2022 03:20:00 -0400 Subject: [PATCH 19/43] Intan importer update Intan has inefficient loader (read_Intan_RHD2000). The entire recording is loaded every time by default. The previous importer (2018) modified Intan's importer to provide efficient importing. The new importer uses Intan's 2022 importer, but converts the recording to a .bst for efficient loading --- ...read_Intan_RHD2000_file_Brainstorm_2022.m} | 321 ++++++++++-------- toolbox/io/in_data_rhd.m | 71 ++++ toolbox/io/in_fopen.m | 9 +- toolbox/io/in_fopen_intan.m | 2 +- toolbox/io/in_fread_intan.m | 2 +- 5 files changed, 253 insertions(+), 152 deletions(-) rename external/intan/{read_Intan_RHD2000_file.m => read_Intan_RHD2000_file_Brainstorm_2022.m} (72%) create mode 100644 toolbox/io/in_data_rhd.m diff --git a/external/intan/read_Intan_RHD2000_file.m b/external/intan/read_Intan_RHD2000_file_Brainstorm_2022.m similarity index 72% rename from external/intan/read_Intan_RHD2000_file.m rename to external/intan/read_Intan_RHD2000_file_Brainstorm_2022.m index 4e3c695607..9b97045378 100644 --- a/external/intan/read_Intan_RHD2000_file.m +++ b/external/intan/read_Intan_RHD2000_file_Brainstorm_2022.m @@ -1,7 +1,7 @@ function newHeader = read_Intan_RHD2000_file(filename,loadData,loadEvents,iSamplesStart,nSamplesToLoad,precision) % Modified for Brainstorm -% Author: Konstantinos Nasiotis 2018 +% Author: Konstantinos Nasiotis 2018, 2022 % This file was modified to offer on the fly small segment data selection @@ -23,10 +23,10 @@ % read_Intan_RHD2000_file % -% Version 2.0, 20 October 2016 +% Version 3.0, 8 February 2021 % -% Reads Intan Technologies RHD2000 data file generated by evaluation board -% GUI or Intan Recording Controller. Data are parsed and placed into +% Reads Intan Technologies RHD data file generated by Intan USB interface +% board or Intan Recording Controller. Data are parsed and placed into % variables that appear in the base MATLAB workspace. Therefore, it is % recommended to execute a 'clear' command before running this program to % clear all other variables from the base workspace. @@ -40,15 +40,11 @@ % [file, path, filterindex] = ... % uigetfile('*.rhd', 'Select an RHD2000 Data File', 'MultiSelect', 'off'); - +% % if (file == 0) % return; % end -% -% Read most recent file automatically. -% path = 'C:\Users\Reid\Documents\RHD2132\testing\'; -% d = dir([path '*.rhd']); -% file = d(end).name; + if nargin < 2 || isempty(loadData) loadData = 1; @@ -66,6 +62,7 @@ precision = 'double'; end +% tic; % filename = [path,file]; fid = fopen(filename, 'r'); @@ -134,11 +131,11 @@ num_temp_sensor_channels = fread(fid, 1, 'int16'); end -% If data file is from GUI v1.3 or later, load eval board mode. -eval_board_mode = 0; +% If data file is from GUI v1.3 or later, load board mode. +board_mode = 0; if ((data_file_main_version_number == 1 && data_file_secondary_version_number >= 3) ... || (data_file_main_version_number > 1)) - eval_board_mode = fread(fid, 1, 'int16'); + board_mode = fread(fid, 1, 'int16'); end % If data file is from v2.0 or later (Intan Recording Controller), @@ -287,7 +284,7 @@ num_board_dig_in_channels, plural(num_board_dig_in_channels)); fprintf(1, 'Found %d board digital output channel%s.\n', ... num_board_dig_out_channels, plural(num_board_dig_out_channels)); -fprintf(1, 'Found %d temperature sensors channel%s.\n', ... +fprintf(1, 'Found %d temperature sensor channel%s.\n', ... num_temp_sensor_channels, plural(num_temp_sensor_channels)); fprintf(1, '\n'); @@ -322,54 +319,48 @@ data_present = 1; end -num_data_blocks_ALL = bytes_remaining / bytes_per_block; +num_data_blocks = bytes_remaining / bytes_per_block; +num_amplifier_samples = num_samples_per_data_block * num_data_blocks; +num_aux_input_samples = (num_samples_per_data_block / 4) * num_data_blocks; +num_supply_voltage_samples = 1 * num_data_blocks; +num_board_adc_samples = num_samples_per_data_block * num_data_blocks; +num_board_dig_in_samples = num_samples_per_data_block * num_data_blocks; +num_board_dig_out_samples = num_samples_per_data_block * num_data_blocks; -%% Check how many data blocks of size num_samples_per_data_block will be loaded -if ~isempty(nSamplesToLoad) - num_data_blocks = ceil(nSamplesToLoad/num_samples_per_data_block); +record_time = num_amplifier_samples / sample_rate; + +if (data_present) + if ~loadData + fprintf(1, 'File contains %0.3f seconds of data. Amplifiers were sampled at %0.2f kS/s.\n', ... + record_time, sample_rate / 1000); + fprintf(1, '\n'); + end else - num_data_blocks = num_data_blocks_ALL; + fprintf(1, 'Header file contains no data. Amplifiers were sampled at %0.2f kS/s.\n', ... + sample_rate / 1000); + fprintf(1, '\n'); end -%% - - - -num_amplifier_samples = num_samples_per_data_block * (num_data_blocks + 1); -num_aux_input_samples = (num_samples_per_data_block / 4) * (num_data_blocks + 1); -num_supply_voltage_samples = 1 * (num_data_blocks + 1); -num_board_adc_samples = num_samples_per_data_block * (num_data_blocks + 1); - - -record_time = num_amplifier_samples / sample_rate; - -if data_present && loadData +if (data_present) && loadData % Pre-allocate memory for data. - fprintf(1, 'Allocating memory for data...\n'); +% fprintf(1, 'Allocating memory for data...\n'); t_amplifier = zeros(1, num_amplifier_samples); - amplifier_data = zeros(num_amplifier_channels, num_amplifier_samples, precision); + amplifier_data = zeros(num_amplifier_channels, num_amplifier_samples); aux_input_data = zeros(num_aux_input_channels, num_aux_input_samples); supply_voltage_data = zeros(num_supply_voltage_channels, num_supply_voltage_samples); temp_sensor_data = zeros(num_temp_sensor_channels, num_supply_voltage_samples); board_adc_data = zeros(num_board_adc_channels, num_board_adc_samples); - - - if loadEvents - num_board_dig_in_samples = num_samples_per_data_block * (num_data_blocks_ALL + 1); - num_board_dig_out_samples = num_samples_per_data_block * (num_data_blocks_ALL + 1); - - board_dig_in_data = zeros(num_board_dig_in_channels, num_board_dig_in_samples); - board_dig_in_raw = zeros(1, num_board_dig_in_samples); - board_dig_out_data = zeros(num_board_dig_out_channels, num_board_dig_out_samples); - board_dig_out_raw = zeros(1, num_board_dig_out_samples); - end + board_dig_in_data = zeros(num_board_dig_in_channels, num_board_dig_in_samples); + board_dig_in_raw = zeros(1, num_board_dig_in_samples); + board_dig_out_data = zeros(num_board_dig_out_channels, num_board_dig_out_samples); + board_dig_out_raw = zeros(1, num_board_dig_out_samples); % Read sampled data from file. - fprintf(1, 'Reading data from file...\n'); +% fprintf(1, 'Reading data from file...\n'); amplifier_index = 1; aux_input_index = 1; @@ -380,99 +371,50 @@ print_increment = 10; percent_done = print_increment; - - - %% Select specific blocks, based on the time selection - - % The starting block is based on the iSamplesStart selection - - iStartingBlock = floor(iSamplesStart/num_samples_per_data_block)+1; - - selectedBlocks = iStartingBlock + (0:num_data_blocks); - - - - for iBlock=1:num_data_blocks_ALL + for i=1:num_data_blocks % In version 1.2, we moved from saving timestamps as unsigned % integeters to signed integers to accomidate negative (adjusted) % timestamps for pretrigger data. - if ((data_file_main_version_number == 1 && data_file_secondary_version_number >= 2) ... || (data_file_main_version_number > 1)) - if ismember(iBlock, selectedBlocks) - t_amplifier(amplifier_index:(amplifier_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'int32'); - else - fseek(fid, num_samples_per_data_block*4,'cof'); - end + t_amplifier(amplifier_index:(amplifier_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'int32'); else - if ismember(iBlock, selectedBlocks) - t_amplifier(amplifier_index:(amplifier_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'uint32'); - else - fseek(fid, num_samples_per_data_block*4,'cof'); - end + t_amplifier(amplifier_index:(amplifier_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'uint32'); end if (num_amplifier_channels > 0) - if ismember(iBlock, selectedBlocks) - amplifier_data(:, amplifier_index:(amplifier_index + num_samples_per_data_block - 1)) = fread(fid, [num_samples_per_data_block, num_amplifier_channels], 'uint16')'; - else - fseek(fid, num_samples_per_data_block*num_amplifier_channels*2,'cof'); - end + amplifier_data(:, amplifier_index:(amplifier_index + num_samples_per_data_block - 1)) = fread(fid, [num_samples_per_data_block, num_amplifier_channels], 'uint16')'; end if (num_aux_input_channels > 0) - if ismember(iBlock, selectedBlocks) - aux_input_data(:, aux_input_index:(aux_input_index + (num_samples_per_data_block / 4) - 1)) = fread(fid, [(num_samples_per_data_block / 4), num_aux_input_channels], 'uint16')'; - else - fseek(fid, (num_samples_per_data_block / 4)*num_aux_input_channels*2,'cof'); - end + aux_input_data(:, aux_input_index:(aux_input_index + (num_samples_per_data_block / 4) - 1)) = fread(fid, [(num_samples_per_data_block / 4), num_aux_input_channels], 'uint16')'; end if (num_supply_voltage_channels > 0) - if ismember(iBlock, selectedBlocks) - supply_voltage_data(:, supply_voltage_index) = fread(fid, [1, num_supply_voltage_channels], 'uint16')'; - else - fseek(fid, num_supply_voltage_channels*2,'cof'); - end + supply_voltage_data(:, supply_voltage_index) = fread(fid, [1, num_supply_voltage_channels], 'uint16')'; end if (num_temp_sensor_channels > 0) - if ismember(iBlock, selectedBlocks) - temp_sensor_data(:, supply_voltage_index) = fread(fid, [1, num_temp_sensor_channels], 'int16')'; - else - fseek(fid, num_temp_sensor_channels*2,'cof'); - end + temp_sensor_data(:, supply_voltage_index) = fread(fid, [1, num_temp_sensor_channels], 'int16')'; end if (num_board_adc_channels > 0) - if ismember(iBlock, selectedBlocks) - board_adc_data(:, board_adc_index:(board_adc_index + num_samples_per_data_block - 1)) = fread(fid, [num_samples_per_data_block, num_board_adc_channels], 'uint16')'; - else - fseek(fid, num_samples_per_data_block*num_board_adc_channels*2,'cof'); - end + board_adc_data(:, board_adc_index:(board_adc_index + num_samples_per_data_block - 1)) = fread(fid, [num_samples_per_data_block, num_board_adc_channels], 'uint16')'; end if (num_board_dig_in_channels > 0) - if ismember(iBlock, selectedBlocks) && loadEvents - board_dig_in_raw(board_dig_in_index:(board_dig_in_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'uint16'); - else - fseek(fid, num_samples_per_data_block*2,'cof'); - end + board_dig_in_raw(board_dig_in_index:(board_dig_in_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'uint16'); end if (num_board_dig_out_channels > 0) - if ismember(iBlock, selectedBlocks) && loadEvents - board_dig_out_raw(board_dig_out_index:(board_dig_out_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'uint16'); - else - fseek(fid, num_samples_per_data_block*2,'cof'); - end + board_dig_out_raw(board_dig_out_index:(board_dig_out_index + num_samples_per_data_block - 1)) = fread(fid, num_samples_per_data_block, 'uint16'); end - if ismember(iBlock, selectedBlocks) - amplifier_index = amplifier_index + num_samples_per_data_block; - aux_input_index = aux_input_index + (num_samples_per_data_block / 4); - supply_voltage_index = supply_voltage_index + 1; - board_adc_index = board_adc_index + num_samples_per_data_block; - end - - % The board dig in and out are for the events. I need them to be - % updated constantly, not only when I'm in a selectedBlock. - board_dig_in_index = board_dig_in_index + num_samples_per_data_block; + amplifier_index = amplifier_index + num_samples_per_data_block; + aux_input_index = aux_input_index + (num_samples_per_data_block / 4); + supply_voltage_index = supply_voltage_index + 1; + board_adc_index = board_adc_index + num_samples_per_data_block; + board_dig_in_index = board_dig_in_index + num_samples_per_data_block; board_dig_out_index = board_dig_out_index + num_samples_per_data_block; - + +% fraction_done = 100 * (i / num_data_blocks); +% if (fraction_done >= percent_done) +% fprintf(1, '%d%% done...\n', percent_done); +% percent_done = percent_done + print_increment; +% end end % Make sure we have read exactly the right amount of data. @@ -481,7 +423,6 @@ %error('Error: End of file not reached.'); end - %% The code above imported the blocks that contain the required samples. % Now they need to be chopped since the blocks are not necessarily the % size requested from the iSamplesStart,nSamplesToLoad inputs. @@ -499,35 +440,33 @@ aux_input_data = aux_input_data(:,ceil(start/4):round(stop/4)); % 9x45 -> 9x25 end end + end - % Close data file. fclose(fid); -if data_present && loadData +if (data_present) && loadData - fprintf(1, 'Parsing data...\n'); +% fprintf(1, 'Parsing data...\n'); - if loadEvents - % Extract digital input channels to separate variables. - for iBlock=1:num_board_dig_in_channels - mask = 2^(board_dig_in_channels(iBlock).native_order) * ones(size(board_dig_in_raw)); - board_dig_in_data(iBlock, :) = (bitand(board_dig_in_raw, mask) > 0); - end - for iBlock=1:num_board_dig_out_channels - mask = 2^(board_dig_out_channels(iBlock).native_order) * ones(size(board_dig_out_raw)); - board_dig_out_data(iBlock, :) = (bitand(board_dig_out_raw, mask) > 0); - end + % Extract digital input channels to separate variables. + for i=1:num_board_dig_in_channels + mask = 2^(board_dig_in_channels(i).native_order) * ones(size(board_dig_in_raw)); + board_dig_in_data(i, :) = (bitand(board_dig_in_raw, mask) > 0); + end + for i=1:num_board_dig_out_channels + mask = 2^(board_dig_out_channels(i).native_order) * ones(size(board_dig_out_raw)); + board_dig_out_data(i, :) = (bitand(board_dig_out_raw, mask) > 0); end % Scale voltage levels appropriately. amplifier_data = 0.195 * (amplifier_data - 32768); % units = microvolts aux_input_data = 37.4e-6 * aux_input_data; % units = volts supply_voltage_data = 74.8e-6 * supply_voltage_data; % units = volts - if (eval_board_mode == 1) + if (board_mode == 1) board_adc_data = 152.59e-6 * (board_adc_data - 32768); % units = volts - elseif (eval_board_mode == 13) % Intan Recording Controller + elseif (board_mode == 13) % Intan Recording Controller board_adc_data = 312.5e-6 * (board_adc_data - 32768); % units = volts else board_adc_data = 50.354e-6 * board_adc_data; % units = volts @@ -552,28 +491,31 @@ t_temp_sensor = t_supply_voltage; % If the software notch filter was selected during the recording, apply the - % same notch filter to amplifier data here. - if (notch_filter_frequency > 0) + % same notch filter to amplifier data here. But don't do this for v3.0+ + % files (from Intan RHX software) because RHX saves notch-filtered data. + if (notch_filter_frequency > 0 && data_file_main_version_number < 3) fprintf(1, 'Applying notch filter...\n'); print_increment = 10; percent_done = print_increment; - for iBlock=1:num_amplifier_channels - amplifier_data(iBlock,:) = ... - notch_filter(amplifier_data(iBlock,:), sample_rate, notch_filter_frequency, 10, precision); - - fraction_done = 100 * (iBlock / num_amplifier_channels); - if (fraction_done >= percent_done) - fprintf(1, '%d%% done...\n', percent_done); - percent_done = percent_done + print_increment; - end + for i=1:num_amplifier_channels + amplifier_data(i,:) = ... + notch_filter(amplifier_data(i,:), sample_rate, notch_filter_frequency, 10); + +% fraction_done = 100 * (i / num_amplifier_channels); +% if (fraction_done >= percent_done) +% fprintf(1, '%d%% done...\n', percent_done); +% percent_done = percent_done + print_increment; +% end end end + end + %%%%%%%%%%%%%%%%%%%%%% NAS - ADDITION FOR BRAINSTORM %%%%%%%%%%%%%%%%%%%%%% newHeader = struct; newHeader.frequency_parameters = frequency_parameters; @@ -597,9 +539,7 @@ newHeader.spike_triggers = spike_triggers; newHeader.supply_voltage_channels = supply_voltage_channels; newHeader.magic_number = magic_number; -newHeader.nSamples = num_data_blocks_ALL*num_samples_per_data_block; - - +newHeader.nSamples = num_data_blocks*num_samples_per_data_block; if data_present && loadData @@ -618,7 +558,79 @@ end end %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% - +% +% +% % Move variables to base workspace. +% +% % new for version 2.01: move filename info to base workspace +% filename = file; +% move_to_base_workspace(filename); +% move_to_base_workspace(path); +% +% move_to_base_workspace(notes); +% move_to_base_workspace(frequency_parameters); +% if (data_file_main_version_number > 1) +% move_to_base_workspace(reference_channel); +% end +% +% if (num_amplifier_channels > 0) +% move_to_base_workspace(amplifier_channels); +% if (data_present) +% move_to_base_workspace(amplifier_data); +% move_to_base_workspace(t_amplifier); +% end +% move_to_base_workspace(spike_triggers); +% end +% if (num_aux_input_channels > 0) +% move_to_base_workspace(aux_input_channels); +% if (data_present) +% move_to_base_workspace(aux_input_data); +% move_to_base_workspace(t_aux_input); +% end +% end +% if (num_supply_voltage_channels > 0) +% move_to_base_workspace(supply_voltage_channels); +% if (data_present) +% move_to_base_workspace(supply_voltage_data); +% move_to_base_workspace(t_supply_voltage); +% end +% end +% if (num_board_adc_channels > 0) +% move_to_base_workspace(board_adc_channels); +% if (data_present) +% move_to_base_workspace(board_adc_data); +% move_to_base_workspace(t_board_adc); +% end +% end +% if (num_board_dig_in_channels > 0) +% move_to_base_workspace(board_dig_in_channels); +% if (data_present) +% move_to_base_workspace(board_dig_in_data); +% move_to_base_workspace(t_dig); +% end +% end +% if (num_board_dig_out_channels > 0) +% move_to_base_workspace(board_dig_out_channels); +% if (data_present) +% move_to_base_workspace(board_dig_out_data); +% move_to_base_workspace(t_dig); +% end +% end +% if (num_temp_sensor_channels > 0) +% if (data_present) +% move_to_base_workspace(temp_sensor_data); +% move_to_base_workspace(t_temp_sensor); +% end +% end +% +% fprintf(1, 'Done! Elapsed time: %0.1f seconds\n', toc); +% if (data_present) +% fprintf(1, 'Extracted data are now available in the MATLAB workspace.\n'); +% else +% fprintf(1, 'Extracted waveform information is now available in the MATLAB workspace.\n'); +% end +% fprintf(1, 'Type ''whos'' to see variables.\n'); +% fprintf(1, '\n'); return @@ -662,7 +674,7 @@ return -function out = notch_filter(in, fSample, fNotch, Bandwidth, precision) +function out = notch_filter(in, fSample, fNotch, Bandwidth) % out = notch_filter(in, fSample, fNotch, Bandwidth) % @@ -695,7 +707,7 @@ b1 = -2*cos(2*pi*Fc); b2 = 1; -out = zeros(size(in), precision); +out = zeros(size(in)); out(1) = in(1); out(2) = in(2); % (If filtering a continuous data stream, change out(1) and out(2) to the @@ -709,4 +721,15 @@ return +function move_to_base_workspace(variable) + +% move_to_base_workspace(variable) +% +% Move variable from function workspace to base MATLAB workspace so +% user will have access to it after the program ends. + +variable_name = inputname(1); +assignin('base', variable_name, variable); + +return; diff --git a/toolbox/io/in_data_rhd.m b/toolbox/io/in_data_rhd.m new file mode 100644 index 0000000000..abe2b1af05 --- /dev/null +++ b/toolbox/io/in_data_rhd.m @@ -0,0 +1,71 @@ +function [DataMat, ChannelMat] = in_data_rhd( DataFile ) +% IN_DATA_RHD: Read a RHD 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_intan(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/Intan'; +DataMat.DataType = 'recordings'; +DataMat.Device = 'Intan'; + +isProgress = bst_progress('isVisible'); +if ~isProgress + bst_progress('start', 'Intan importer', 'Reading entire recording'); +end +% Read entire file +DataMat.F = in_fread_intan(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 \ No newline at end of file diff --git a/toolbox/io/in_fopen.m b/toolbox/io/in_fopen.m index 7a25008753..26d6b18c94 100644 --- a/toolbox/io/in_fopen.m +++ b/toolbox/io/in_fopen.m @@ -150,7 +150,14 @@ case 'SPM-DAT' [sFile, ChannelMat] = in_fopen_spm(DataFile); case 'EEG-INTAN' - [sFile, ChannelMat] = in_fopen_intan(DataFile); +% [sFile, ChannelMat] = in_fopen_intan(DataFile); + [fPath, fBase, fExt] = bst_fileparts(DataFile); + switch lower(fExt) + case '.rhd' + [DataMat, ChannelMat] = in_data_rhd(DataFile); + case '.pl2' + [sFile, ChannelMat] = in_fopen_intan(DataFile); + end case 'EEG-PLEXON' [sFile, ChannelMat] = in_fopen_plexon(DataFile); case 'EEG-TDT' diff --git a/toolbox/io/in_fopen_intan.m b/toolbox/io/in_fopen_intan.m index f5d67a6f4f..35912306eb 100644 --- a/toolbox/io/in_fopen_intan.m +++ b/toolbox/io/in_fopen_intan.m @@ -70,7 +70,7 @@ % Read the header switch (hdr.FileExt) case '.rhd' - newHeader = read_Intan_RHD2000_file(DataFile,1,1,1,100); + newHeader = read_Intan_RHD2000_file_Brainstorm_2022(DataFile,1,1,1,100); case '.rhs' newHeader = read_Intan_RHS2000_file(DataFile,1,1,1,100); end diff --git a/toolbox/io/in_fread_intan.m b/toolbox/io/in_fread_intan.m index a07e6dfe57..2754493f75 100644 --- a/toolbox/io/in_fread_intan.m +++ b/toolbox/io/in_fread_intan.m @@ -45,7 +45,7 @@ % Read the corresponding recordings switch (sFile.header.FileExt) case '.rhd' - data_and_headers = read_Intan_RHD2000_file(sFile.header.DataFile, 1, 0, SamplesBounds(1) + 1, nSamples, precision); + data_and_headers = read_Intan_RHD2000_file_Brainstorm_2022(sFile.header.DataFile, 1, 0, SamplesBounds(1) + 1, nSamples, precision); case '.rhs' data_and_headers = read_Intan_RHS2000_file(sFile.header.DataFile, 1, 0, SamplesBounds(1) + 1, nSamples); end From c6af024df6615f114ec184476d2614e051f11595 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Tue, 29 Mar 2022 17:55:25 -0400 Subject: [PATCH 20/43] renamed Intan importer files to match master --- ...0_file_Brainstorm_2022.m => read_Intan_RHD2000_bst_2022.m.m} | 2 +- ...ead_Intan_RHS2000_file.m => read_Intan_RHS2000_bst_2018.m.m} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename external/intan/{read_Intan_RHD2000_file_Brainstorm_2022.m => read_Intan_RHD2000_bst_2022.m.m} (99%) rename external/intan/{read_Intan_RHS2000_file.m => read_Intan_RHS2000_bst_2018.m.m} (99%) diff --git a/external/intan/read_Intan_RHD2000_file_Brainstorm_2022.m b/external/intan/read_Intan_RHD2000_bst_2022.m.m similarity index 99% rename from external/intan/read_Intan_RHD2000_file_Brainstorm_2022.m rename to external/intan/read_Intan_RHD2000_bst_2022.m.m index 9b97045378..f11ac0330c 100644 --- a/external/intan/read_Intan_RHD2000_file_Brainstorm_2022.m +++ b/external/intan/read_Intan_RHD2000_bst_2022.m.m @@ -1,4 +1,4 @@ -function newHeader = read_Intan_RHD2000_file(filename,loadData,loadEvents,iSamplesStart,nSamplesToLoad,precision) +function newHeader = read_Intan_RHD2000_bst_2022(filename,loadData,loadEvents,iSamplesStart,nSamplesToLoad,precision) % Modified for Brainstorm % Author: Konstantinos Nasiotis 2018, 2022 diff --git a/external/intan/read_Intan_RHS2000_file.m b/external/intan/read_Intan_RHS2000_bst_2018.m.m similarity index 99% rename from external/intan/read_Intan_RHS2000_file.m rename to external/intan/read_Intan_RHS2000_bst_2018.m.m index b1990833f4..0d8be69057 100644 --- a/external/intan/read_Intan_RHS2000_file.m +++ b/external/intan/read_Intan_RHS2000_bst_2018.m.m @@ -1,4 +1,4 @@ -function newHeader = read_Intan_RHS2000_file(filename, loadData, loadEvents, iSamplesStart, nSamplesToLoad) +function newHeader = read_Intan_RHS2000_bst_2018(filename, loadData, loadEvents, iSamplesStart, nSamplesToLoad) % Modified for Brainstorm % Author: Konstantinos Nasiotis, 2018 From 0c3327da32292d40eec2f97b1d5b8b8ded7f7971 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Tue, 29 Mar 2022 17:56:55 -0400 Subject: [PATCH 21/43] Typos on extensions --- ...d_Intan_RHD2000_bst_2022.m.m => read_Intan_RHD2000_bst_2022.m} | 0 ...d_Intan_RHS2000_bst_2018.m.m => read_Intan_RHS2000_bst_2018.m} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename external/intan/{read_Intan_RHD2000_bst_2022.m.m => read_Intan_RHD2000_bst_2022.m} (100%) rename external/intan/{read_Intan_RHS2000_bst_2018.m.m => read_Intan_RHS2000_bst_2018.m} (100%) diff --git a/external/intan/read_Intan_RHD2000_bst_2022.m.m b/external/intan/read_Intan_RHD2000_bst_2022.m similarity index 100% rename from external/intan/read_Intan_RHD2000_bst_2022.m.m rename to external/intan/read_Intan_RHD2000_bst_2022.m diff --git a/external/intan/read_Intan_RHS2000_bst_2018.m.m b/external/intan/read_Intan_RHS2000_bst_2018.m similarity index 100% rename from external/intan/read_Intan_RHS2000_bst_2018.m.m rename to external/intan/read_Intan_RHS2000_bst_2018.m From 478346ed8b7973abafe7a0dbab45b3aa32896578 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Tue, 5 Apr 2022 15:48:14 -0400 Subject: [PATCH 22/43] Update in_fopen.m Plexon fopen cases were deleted by mistake while merging --- toolbox/io/in_fopen.m | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) 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'} From 52bc2ae8e6cdf1aacd3aba9394e8d372c79a4678 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Thu, 7 Apr 2022 19:38:49 -0400 Subject: [PATCH 23/43] Update process_spikesorting_kilosort.m Added check for GPU training. Updated bst_progress calls. --- .../functions/process_spikesorting_kilosort.m | 57 ++++++++++--------- 1 file changed, 29 insertions(+), 28 deletions(-) diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index 2dc865577b..d34d32add2 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -93,7 +93,7 @@ bst_report('Error', sProcess, sInputs, '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.'); return; @@ -104,26 +104,12 @@ [isInstalled, errMsg] = bst_plugin('Install', 'kilosort'); if ~isInstalled error(errMsg); - end - - - %% Prepare parallel pool - try - poolobj = gcp('nocreate'); - if isempty(poolobj) - bst_progress('text', 'Starting parallel pool'); - parpool; - end - catch - poolobj = []; - end - + end %% Initialize KiloSort Parameters (This initially is a copy of StandardConfig_MOVEME) KilosortStandardConfig(); ops.GPU = sProcess.options.GPU.Value; - %% Compute on each raw input independently for i = 1:length(sInputs) [fPath, fBase] = bst_fileparts(sInputs(i).FileName); @@ -157,7 +143,12 @@ try rmdir(outputPath, 's'); catch - error('Couldnt remove spikes folder. Make sure the current directory is not that folder or that Klusters is not open.') + cd .. + try + rmdir(outputPath, 's'); + catch + error('Couldnt remove spikes folder. Make sure the current directory is not that folder or that Klusters is not open.') + end end end @@ -255,8 +246,10 @@ 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', 'Kilosort spike sorting'); + isProgress = bst_progress('isVisible'); + if isProgress + bst_progress('start', 'Kilosort', 'Spike-sorting'); + end % Some residual parameters that need the outputPath and the converted Raw signal ops.fbinary = converted_raw_File; % will be created for 'openEphys' @@ -274,9 +267,22 @@ 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) - + try + rez = fitTemplates(rez, DATA, uproj); % fit templates iteratively + catch + if sProcess.options.GPU.Value + % ~\.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, sInputs, '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, sInputs, 'Error with Kilosort while training on the CPU'); + return; + end + end + + rez = fullMPMU(rez, DATA);% extract final spike times (overlapping extraction) %% save matlab results file save(fullfile(ops.root, 'rez.mat'), 'rez', '-v7.3'); @@ -356,13 +362,8 @@ end %%%%%%%%%%%%%%%%%%%%%% Prepare to exit %%%%%%%%%%%%%%%%%%%%%%% - % Turn off parallel processing and return to the initial directory - if ~isempty(poolobj) - delete(poolobj); - end - isProgress = bst_progress('isVisible'); - if ~isProgress + if isProgress bst_progress('stop'); end From 8520e1e3536d250f4bec63c7027df4ff2330d950 Mon Sep 17 00:00:00 2001 From: Konstantinos Date: Thu, 7 Apr 2022 21:08:35 -0400 Subject: [PATCH 24/43] Update progress-bar popups --- toolbox/io/in_fopen_tdt.m | 4 +- .../functions/process_convert_raw_to_lfp.m | 41 ++++++++++++-- .../functions/process_spikesorting_kilosort.m | 12 ++--- .../process_spikesorting_ultramegasort2000.m | 54 ++++++++++++------- .../functions/process_spikesorting_waveclus.m | 17 ++++-- 5 files changed, 90 insertions(+), 38 deletions(-) diff --git a/toolbox/io/in_fopen_tdt.m b/toolbox/io/in_fopen_tdt.m index 8299a3a53c..1100c0909f 100644 --- a/toolbox/io/in_fopen_tdt.m +++ b/toolbox/io/in_fopen_tdt.m @@ -48,7 +48,7 @@ %% ===== READ DATA HEADERS ===== isProgress = bst_progress('isVisible'); -if ~isProgress +if isProgress bst_progress('start', 'TDT', 'Reading headers...'); end @@ -306,7 +306,7 @@ sFile = import_events(sFile, [], events); isProgress = bst_progress('isVisible'); -if ~isProgress +if isProgress bst_progress('stop'); end diff --git a/toolbox/process/functions/process_convert_raw_to_lfp.m b/toolbox/process/functions/process_convert_raw_to_lfp.m index 74da300882..c625a17a2c 100644 --- a/toolbox/process/functions/process_convert_raw_to_lfp.m +++ b/toolbox/process/functions/process_convert_raw_to_lfp.m @@ -121,11 +121,17 @@ 'Make sure you remove the DC offset before resampling; EEGLAB function does not work well when the signals are not centered.']); end - % Prepare parallel pool, if requested + + + %% Prepare parallel pool, if requested if sProcess.options.paral.Value try poolobj = gcp('nocreate'); if isempty(poolobj) + isProgress = bst_progress('isVisible'); + if isProgress + bst_progress('start', 'Convert RAW file to LFP', 'Starting parallel pool'); + end parpool; end catch @@ -135,9 +141,16 @@ else poolobj = []; end + %% Check if the files are separated per channel. If not do it now. % These files will be converted to LFP right after + + isProgress = bst_progress('isVisible'); + if isProgress + bst_progress('start', 'Convert RAW file to LFP', 'Demultiplexing raw file'); + end + 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); @@ -211,8 +224,17 @@ % The sFileOut is what will be the final [sFileOut, errMsg] = out_fopen(RawFileOut, RawFileFormat, sFileTemplate, ChannelMat); + %% Initialize progress bar + isProgress = bst_progress('isVisible'); + if isProgress + if sProcess.options.paral.Value + bst_progress('start', 'Raw2LFP', 'Converting raw signals to LFP...'); + else + bst_progress('start', 'Raw2LFP', 'Converting raw signals to LFP...', 0, (sProcess.options.paral.Value == 0) * nChannels); + end + end + %% 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 @@ -221,7 +243,9 @@ else for iChannel = 1:nChannels LFP(iChannel,:) = BayesianSpikeRemoval(sFiles_temp_mat{iChannel}, filterBounds, sMat.F, ChannelMat, cleanChannelNames, notchFilterFreqs, LFP_fs); - bst_progress('inc', 1); + if isProgress + bst_progress('inc', 1); + end end end else @@ -232,12 +256,14 @@ else for iChannel = 1:nChannels LFP(iChannel,:) = filter_and_downsample(sFiles_temp_mat{iChannel}, Fs, filterBounds, notchFilterFreqs, LFP_fs); - bst_progress('inc', 1); + if isProgress + bst_progress('inc', 1); + end end end end - % WRITE OUT + %% WRITE OUT sFileOut = out_fwrite(sFileOut, ChannelMatOut, [], [], [], LFP); % Import the RAW file in the database viewer and open it immediately @@ -254,6 +280,11 @@ delete(RawFile); db_reload_studies(iStudy); end + + isProgress = bst_progress('isVisible'); + if isProgress + bst_progress('stop'); + end end diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index d34d32add2..c68b5b73b0 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -137,23 +137,17 @@ %% %%%%%%%%%%%%%%%%%%% 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 - cd .. - try - rmdir(outputPath, 's'); - catch - error('Couldnt remove spikes folder. Make sure the current directory is not that folder or that Klusters is not open.') - end + error('Couldnt remove spikes folder. Make sure the current directory is not that folder or that Klusters is not open.') end end - mkdir(outputPath); - + %% Prepare the ChannelMat File % This is a file that just contains information for the location of % the electrodes. diff --git a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m index 77c7d64830..c7dff49a1d 100644 --- a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m +++ b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m @@ -108,6 +108,25 @@ error(errMsg); end + % Prepare parallel pool, if requested + if sProcess.options.paral.Value + try + poolobj = gcp('nocreate'); + if isempty(poolobj) + isProgress = bst_progress('isVisible'); + if isProgress + bst_progress('start', 'UltraMegaSort2000', 'Starting parallel pool'); + end + parpool; + end + catch + sProcess.options.paral.Value = 0; + poolobj = []; + end + else + poolobj = []; + end + % Compute on each raw input independently for i = 1:length(sInputs) [fPath, fBase] = bst_fileparts(sInputs(i).FileName); @@ -126,27 +145,16 @@ 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 = []; - end - %%%%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% outputPath = bst_fullfile(ProtocolInfo.STUDIES, fPath, [fBase '_ums2k_spikes']); % Clear if directory already exists if exist(outputPath, 'dir') == 7 - rmdir(outputPath, 's'); + try + rmdir(outputPath, 's'); + catch + error('Couldnt remove spikes folder. Make sure the current directory is not that folder.') + end end mkdir(outputPath); @@ -177,12 +185,18 @@ else for ielectrode = 1:numChannels do_UltraMegaSorting(sFiles{ielectrode}, sFile, sProcess.options.lowpass, sProcess.options.highpass, Fs); - bst_progress('inc', 1); + if isProgress + bst_progress('inc', 1); + end end end %%%%%%%%%%%%%%%%%%%%% Create Brainstorm Events %%%%%%%%%%%%%%%%%%% - bst_progress('text', 'Saving events file...'); + isProgress = bst_progress('isVisible'); + if isProgress + bst_progress('start', 'UltraMegaSort2000', 'Gathering spiking events...'); + end + cd(previous_directory); % Delete existing spike events @@ -220,6 +234,10 @@ DataMat.Spikes(iSpike).Name = ChannelMat.Channel(iSpike).Name; DataMat.Spikes(iSpike).Mod = 0; end + isProgress = bst_progress('isVisible'); + if isProgress + bst_progress('start', 'UltraMegaSort2000', 'Saving events file...'); + end % Save events file for backup SaveBrainstormEvents(DataMat, 'events_UNSUPERVISED.mat'); % Add history field diff --git a/toolbox/process/functions/process_spikesorting_waveclus.m b/toolbox/process/functions/process_spikesorting_waveclus.m index f652525ff8..b8e569d582 100644 --- a/toolbox/process/functions/process_spikesorting_waveclus.m +++ b/toolbox/process/functions/process_spikesorting_waveclus.m @@ -107,7 +107,10 @@ try poolobj = gcp('nocreate'); if isempty(poolobj) - bst_progress('text', 'Starting parallel pool'); + isProgress = bst_progress('isVisible'); + if isProgress + bst_progress('start', 'WaveClus', 'Starting parallel pool'); + end parpool; end catch @@ -139,7 +142,11 @@ % Clear if directory already exists if exist(outputPath, 'dir') == 7 - rmdir(outputPath, 's'); + try + rmdir(outputPath, 's'); + catch + error('Couldnt remove spikes folder. Make sure the current directory is not that folder.') + end end mkdir(outputPath); @@ -166,7 +173,9 @@ if ismember(upper(ChannelMat.Channel(ielectrode).Type), {'EEG', 'SEEG'}) Get_spikes(sFiles{ielectrode}); end - bst_progress('inc', 1); + if isProgress + bst_progress('inc', 1); + end end end @@ -257,7 +266,7 @@ end isProgress = bst_progress('isVisible'); - if ~isProgress + if isProgress bst_progress('stop'); end end From 9644cd9f192db94b8db678ad78235d49b4d62593 Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 18 Apr 2022 17:23:58 +0200 Subject: [PATCH 25/43] Update bst_plugin.m --- toolbox/core/bst_plugin.m | 1 - 1 file changed, 1 deletion(-) diff --git a/toolbox/core/bst_plugin.m b/toolbox/core/bst_plugin.m index a96ea96ca3..c52dbc61e9 100644 --- a/toolbox/core/bst_plugin.m +++ b/toolbox/core/bst_plugin.m @@ -2287,7 +2287,6 @@ function MenuUpdate(jPlugs) % Is installed? PlugRef = GetSupported(PlugName); Plug = GetInstalled(PlugName); - if ~isempty(Plug) isInstalled = 1; elseif ~isempty(PlugRef) From f11d1a0ca81cb4f3c20fd300da5ff7344309fb3f Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 18 Apr 2022 17:25:37 +0200 Subject: [PATCH 26/43] Fixed typo --- toolbox/gui/figure_timefreq.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/toolbox/gui/figure_timefreq.m b/toolbox/gui/figure_timefreq.m index f5d0bc00fa..03364841b2 100644 --- a/toolbox/gui/figure_timefreq.m +++ b/toolbox/gui/figure_timefreq.m @@ -1384,7 +1384,7 @@ function ConfigureAxes(hAxes, Time, FullTimeVector, Freqs, TfInfo, MinMaxVal, Lo elseif ~isempty(strfind(lower(TfInfo.FileName), 'rasterplot')) xlabel(hAxes, 'Time (s)'); ylabel(hAxes, 'Trials'); - elseif ~isempty(strfind(lower(TfInfo.FileName), 'spiking_phase_locking`')) + elseif ~isempty(strfind(lower(TfInfo.FileName), 'spiking_phase_locking')) xlabel(hAxes, ' '); ylabel(hAxes, ' '); else From ecfae56d251612e5abe5419011146dda8b6f5b9f Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 18 Apr 2022 17:41:34 +0200 Subject: [PATCH 27/43] Updated progress bar management --- toolbox/io/in_fopen_plexon.m | 37 ++++++++---------------------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/toolbox/io/in_fopen_plexon.m b/toolbox/io/in_fopen_plexon.m index b1a27122d7..67d71cbc5d 100644 --- a/toolbox/io/in_fopen_plexon.m +++ b/toolbox/io/in_fopen_plexon.m @@ -54,11 +54,6 @@ %% ===== READ DATA HEADERS ===== -isProgress = bst_progress('isVisible'); -if isProgress - bst_progress('start', 'Plexon importer', 'Reading header'); -end - hdr.chan_headers = {}; hdr.chan_files = {}; hdr.extension = plexonFormat; @@ -75,7 +70,7 @@ channelsWithTimeseriesNames = all_Channel_names(channels_with_timetraces); %% ===== CREATE CHANNEL FILE ===== -bst_progress('text', 'Creating channel file'); +bst_progress('text', 'Plexon: Creating channel file'); all_signalTypesWithoutNumbers = regexprep(all_Channel_names,'[\d"]','')'; signalTypesWithoutNumbers = regexprep(channelsWithTimeseriesNames,'[\d"]','')'; @@ -161,11 +156,6 @@ names = cellstr(names); % Convert to cell so it can be used in regexprep iPresentEvents = find(logical(evcounts)); -isProgress = bst_progress('isVisible'); -if isProgress - bst_progress('start', 'Plexon importer', 'Gathering acquisition events', 0, length(iPresentEvents)); -end - % Read the events if ~isempty(iPresentEvents) @@ -179,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 @@ -192,8 +183,6 @@ events(iEvt).select = 1; events(iEvt).channels = cell(1, size(events(iEvt).times, 2)); events(iEvt).notes = cell(1, size(events(iEvt).times, 2)); - - bst_progress('inc', 1); end % Import this list sFile = import_events(sFile, [], events); @@ -202,14 +191,9 @@ %% Read the Spikes events 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 - - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('start', 'Plexon importer', 'Gathering spiking events', 0, nUnique_events); - end - + % Initialize list of events events = repmat(db_template('event'), 1, nUnique_events); iEnteredEvent = 1; @@ -222,6 +206,9 @@ 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 @@ -241,9 +228,6 @@ events(iEnteredEvent).channels = repmat({{all_Channel_names{iChannels_selected(iChannel)}}}, 1, size(events(iEnteredEvent).times, 2)); iEnteredEvent = iEnteredEvent + 1; - - bst_progress('inc', 1); - end end end @@ -252,9 +236,4 @@ sFile = import_events(sFile, [], events); end -isProgress = bst_progress('isVisible'); -if isProgress - bst_progress('stop'); end - -end \ No newline at end of file From 2f0c36f6b5d689389f6538c02986b0e4ac950377 Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 18 Apr 2022 17:44:15 +0200 Subject: [PATCH 28/43] Updated progress bar management --- toolbox/io/in_fopen_tdt.m | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/toolbox/io/in_fopen_tdt.m b/toolbox/io/in_fopen_tdt.m index 1100c0909f..b6172a4f23 100644 --- a/toolbox/io/in_fopen_tdt.m +++ b/toolbox/io/in_fopen_tdt.m @@ -47,10 +47,7 @@ Comment = DataFolder; %% ===== READ DATA HEADERS ===== -isProgress = bst_progress('isVisible'); -if isProgress - bst_progress('start', 'TDT', 'Reading headers...'); -end +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 @@ -170,7 +167,7 @@ %% Check for acquisition events -bst_progress('text', '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 @@ -229,7 +226,7 @@ check_for_spikes = 1; if check_for_spikes - bst_progress('text', 'Collecting 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 @@ -305,9 +302,4 @@ % Import this list sFile = import_events(sFile, [], events); -isProgress = bst_progress('isVisible'); -if isProgress - bst_progress('stop'); end - -end \ No newline at end of file From 6130c392228dc25e3b0ce8a72074a5cf30c24bb6 Mon Sep 17 00:00:00 2001 From: Francois Date: Mon, 18 Apr 2022 17:47:49 +0200 Subject: [PATCH 29/43] Updated error management --- toolbox/io/in_fopen_tdt.m | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/toolbox/io/in_fopen_tdt.m b/toolbox/io/in_fopen_tdt.m index b6172a4f23..c2ace439ee 100644 --- a/toolbox/io/in_fopen_tdt.m +++ b/toolbox/io/in_fopen_tdt.m @@ -106,11 +106,11 @@ LFP_label_exists = 1; if isempty(data_new.streams) - bst_error('The selected stream is empty') + error('The selected stream is empty'); end if isempty(indx) - bst_error('No stream was selected') + error('No stream was selected'); end end From 94872aa8c320a0673ae0a360a2b96e94ff61612a Mon Sep 17 00:00:00 2001 From: ftadel Date: Thu, 21 Apr 2022 16:19:39 +0200 Subject: [PATCH 30/43] Created spike file type --- toolbox/core/bst_get.m | 4 ++-- toolbox/db/db_add.m | 9 +++++++-- toolbox/db/private/db_parse_study.m | 4 ++-- toolbox/io/file_fullpath.m | 4 ++-- toolbox/io/file_gettype.m | 10 +++++----- toolbox/process/bst_process.m | 2 +- toolbox/tree/node_create_study.m | 6 ++++-- toolbox/tree/node_delete.m | 21 ++++++++++++++++++++- toolbox/tree/node_rename.m | 4 ++-- toolbox/tree/tree_callbacks.m | 24 +++++++++++++----------- 10 files changed, 58 insertions(+), 30 deletions(-) diff --git a/toolbox/core/bst_get.m b/toolbox/core/bst_get.m index 61c56e04d2..f94ac0ceb6 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); 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/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/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/process/bst_process.m b/toolbox/process/bst_process.m index a008ca2896..46d0d5712b 100644 --- a/toolbox/process/bst_process.m +++ b/toolbox/process/bst_process.m @@ -1523,7 +1523,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/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..057999056a 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' + bst_process('CallProcess', 'process_spikesorting_supervised', 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)bst_process('CallProcess', 'process_spikesorting_supervised', 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')) From 424ea5a6e654f384226d410d24b0ae483322cf17 Mon Sep 17 00:00:00 2001 From: ftadel Date: Thu, 21 Apr 2022 16:36:59 +0200 Subject: [PATCH 31/43] Editing spike sorter options: use plugin manager --- toolbox/gui/panel_spikesorting_options.m | 94 ++++++++++++------------ 1 file changed, 47 insertions(+), 47 deletions(-) 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); From 6811cb95e1bbc0bfd0179b509fdc53f4d09d853b Mon Sep 17 00:00:00 2001 From: ftadel Date: Thu, 21 Apr 2022 20:06:26 +0200 Subject: [PATCH 32/43] Created spike file type (fix) --- toolbox/db/db_add_data.m | 2 +- toolbox/io/file_short.m | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) 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/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 From c151fbc88a73fc0fa0e435e0f87230d9ca9cd2b7 Mon Sep 17 00:00:00 2001 From: ftadel Date: Thu, 21 Apr 2022 20:06:53 +0200 Subject: [PATCH 33/43] Reformatting spike sorting processes --- toolbox/io/in_spikesorting_rawelectrodes.m | 11 +- toolbox/process/bst_process.m | 6 +- .../functions/process_spikesorting_kilosort.m | 150 ++++++------- .../process_spikesorting_ultramegasort2000.m | 206 ++++++++---------- .../functions/process_spikesorting_waveclus.m | 127 ++++------- 5 files changed, 208 insertions(+), 292 deletions(-) diff --git a/toolbox/io/in_spikesorting_rawelectrodes.m b/toolbox/io/in_spikesorting_rawelectrodes.m index 5805179de4..c1d788139f 100644 --- a/toolbox/io/in_spikesorting_rawelectrodes.m +++ b/toolbox/io/in_spikesorting_rawelectrodes.m @@ -98,10 +98,7 @@ num_segments = ceil(total_samples / max_samples); num_samples_per_segment = ceil(total_samples / num_segments); -isProgress = bst_progress('isVisible'); -if ~isProgress - bst_progress('start', 'Spike-sorting', 'Demultiplexing raw file...', 0, (parallel == 0) * num_segments * numChannels); -end +bst_progress('start', 'Spike-sorting', 'Demultiplexing raw file...', 0, (parallel == 0) * num_segments * numChannels); sFiles = {}; for iChannel = 1:numChannels @@ -157,15 +154,13 @@ %% Convert binary files per channel to Matlab files -isProgress = bst_progress('isVisible'); -if ~isProgress - bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...', 0, (parallel == 0) * numChannels); -end if parallel + bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...'); parfor iChannel = 1:numChannels convert2mat(sFiles{iChannel}, sr, precision); end else + bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...', 0, (parallel == 0) * numChannels); for iChannel = 1:numChannels convert2mat(sFiles{iChannel}, sr, precision); bst_progress('inc', 1); diff --git a/toolbox/process/bst_process.m b/toolbox/process/bst_process.m index 46d0d5712b..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 diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index c68b5b73b0..450380844e 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -28,7 +28,9 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018, 2022; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018-2022 +% Martin Cousineau, 2018 +% Francois Tadel, 2022 eval(macro_method); end @@ -57,13 +59,18 @@ sProcess.options.binsize.Comment = 'Maximum RAM to use: '; sProcess.options.binsize.Type = 'value'; sProcess.options.binsize.Value = {2, 'GB', 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 @@ -77,7 +84,6 @@ %% ===== RUN ===== function OutputFiles = Run(sProcess, sInputs) %#ok OutputFiles = {}; - ProtocolInfo = bst_get('ProtocolInfo'); % Not available in the compiled version if bst_iscompiled() @@ -99,20 +105,19 @@ return; end - - %% Load plugin + % Load plugin [isInstalled, errMsg] = bst_plugin('Install', 'kilosort'); if ~isInstalled error(errMsg); - end + end - %% Initialize KiloSort Parameters (This initially is a copy of StandardConfig_MOVEME) + % Initialize KiloSort Parameters (This initially is a copy of StandardConfig_MOVEME) KilosortStandardConfig(); ops.GPU = sProcess.options.GPU.Value; - %% Compute on each raw input independently + % Compute on each raw input independently for i = 1:length(sInputs) - [fPath, fBase] = bst_fileparts(sInputs(i).FileName); + [fPath, fBase] = bst_fileparts(file_fullpath(sInputs(i).FileName)); % Remove "data_0raw" or "data_" tag if (length(fBase) > 10 && strcmp(fBase(1:10), 'data_0raw_')) fBase = fBase(11:end); @@ -120,12 +125,12 @@ fBase = fBase(6:end); end + % Load input files DataMat = in_bst_data(sInputs(i).FileName, 'F'); + sFile = DataMat.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 + % 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') @@ -133,11 +138,8 @@ end end - sFile = DataMat.F; - - %% %%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% - outputPath = bst_fullfile(ProtocolInfo.STUDIES, fPath, [fBase '_kilosort_spikes']); - + %%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% + outputPath = bst_fullfile(fPath, [fBase '_kilosort_spikes']); % Clear if directory already exists if exist(outputPath, 'dir') == 7 try @@ -148,34 +150,29 @@ end mkdir(outputPath); - %% Prepare the ChannelMat File - % This is a file that just contains information for the location of - % the electrodes. - + % 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 + % Get the channels in the montage % First check if any montages have been assigned [Channels, Montages, channelsMontage,montageOccurences] = deal_with_channels_and_groups(ChannelMat); - %% Adjust the possible clusters based on the number of channels - + % 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 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 @@ -200,7 +197,7 @@ xcoords = converted_coordinates(:,1); ycoords = converted_coordinates(:,2); else - xcoords = [1:length(Channels)]'; + xcoords = (1:length(Channels))'; ycoords = ones(length(Channels),1); end @@ -211,14 +208,14 @@ 'chanMap','connected', 'xcoords', 'ycoords', 'kcoords', 'chanMap0ind', 'fs') - %% Width of the spike-waveforms - NEEDS TO BE EVEN + % 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 + % 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) @@ -228,7 +225,7 @@ 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). + % 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_bst(ChannelMat, fs, xmlFile, ops) @@ -236,14 +233,12 @@ previous_directory = pwd; cd(outputPath); - %% Convert to the right input for KiloSort + % Convert to the right input for KiloSort converted_raw_File = in_spikesorting_convertforkilosort(sInputs(i), sProcess.options.binsize.Value{1} * 1e9); % This converts into int16. - %% %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('start', 'Kilosort', 'Spike-sorting'); - end + + %%%%%%%%%%%%%%%%%%%%%%% Start the 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' @@ -278,31 +273,29 @@ rez = fullMPMU(rez, DATA);% extract final spike times (overlapping extraction) - %% save matlab results file + %%save matlab results file save(fullfile(ops.root, 'rez.mat'), 'rez', '-v7.3'); % remove temporary file delete(ops.fproc); - %% Now convert the rez.mat and the .xml to Neuroscope format so it can be read from Klusters + % 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 %%%%%%%%%%%%%%%%%%% - + %%%%%%%%%%%%%%%%%%% 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); + convertKilosort2BrainstormEvents(sFile, ChannelMat, fPath, rez); cd(previous_directory); @@ -330,7 +323,7 @@ % ===== SAVE LINK FILE ===== % Build output filename - NewBstFile = bst_fullfile(ProtocolInfo.STUDIES, fPath, ['data_0ephys_' fBase '.mat']); + NewBstFile = bst_fullfile(fPath, ['data_0ephys_' fBase '.mat']); % Build output structure DataMat_spikesorter = struct(); DataMat_spikesorter.Comment = 'KiloSort Spike Sorting'; @@ -345,7 +338,7 @@ % Save file on hard drive bst_save(NewBstFile, DataMat_spikesorter, 'v6'); % Add file to database - sOutputStudy = db_add_data(sInputs(i).iStudy, NewBstFile, DataMat_spikesorter); + db_add_data(sInputs(i).iStudy, file_short(NewBstFile), DataMat_spikesorter); % Return new file OutputFiles{end+1} = NewBstFile; @@ -353,20 +346,12 @@ % Update links db_links('Study', sInputs(i).iStudy); panel_protocols('UpdateNode', 'Study', sInputs(i).iStudy); - end - - %%%%%%%%%%%%%%%%%%%%%% Prepare to exit %%%%%%%%%%%%%%%%%%%%%%% - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('stop'); - end - - cd(previous_directory); - + end end +%% ===== CONVERT EVENTS ===== function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) events_spikes = struct(); @@ -436,7 +421,7 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) save(fullfile(parentPath,'events_UNSUPERVISED.mat'),'events') - %% Assign the unsupervised spike sorted events to the link to raw file + % 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'); @@ -444,11 +429,12 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) end +%% ===== 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; @@ -460,12 +446,11 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) res = load(bst_fullfile(SpikeSortedMat.Parent, [study '.res.' sMontage])); fet = dlmread(bst_fullfile(SpikeSortedMat.Parent, [study '.fet.' sMontage])); - - %% Get the channels that belong in the selected montage + % Get the channels that belong in the selected montage [Channels, Montages, channelsMontage,montageOccurences] = deal_with_channels_and_groups(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. % Brainstorm assign each spike to a SINGLE NEURON on each electrode. This @@ -518,6 +503,8 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) end end + +%% ===== COPY KILOSORT CONFIG ===== function copyKilosortConfig(defaultFile, outputFile) if exist(outputFile, 'file') == 2 delete(outputFile); @@ -540,6 +527,7 @@ function copyKilosortConfig(defaultFile, outputFile) end +%% ===== CREATE XML ===== function createXML_bst(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. @@ -733,36 +721,34 @@ function createXML_bst(ChannelMat, Fs, xmlFile, ops) s = cat(1,s, chunk5); -%% Output +% 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. - -[nrows,ncols]= size(charcell); - -fid = fopen(filename, 'w'); - -for row=1:nrows - fprintf(fid, '%s \n', charcell{row,:}); -end - -fclose(fid); + %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. + + [nrows,ncols]= size(charcell); + + fid = fopen(filename, 'w'); + + for row=1:nrows + fprintf(fid, '%s \n', charcell{row,:}); + end + + fclose(fid); end +%% ===== GET MONTAGE ===== +% Get the channels in the montage function [Channels, Montages, channelsMontage,montageOccurences] = deal_with_channels_and_groups(ChannelMat) - - %% Get the channels in the montage % First check if any montages have been assigned - Channels = ChannelMat.Channel; - allMontages = {Channels.Group}; nEmptyMontage = length(find(cellfun(@isempty,allMontages))); diff --git a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m index c7dff49a1d..395f988d4d 100644 --- a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m +++ b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m @@ -28,7 +28,9 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018-2019, 2022; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018-2022 +% Martin Cousineau, 2018 +% Francois Tadel, 2022 eval(macro_method); end @@ -54,12 +56,12 @@ 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'; + sProcess.options.parallel.Comment = 'Parallel processing'; + sProcess.options.parallel.Type = 'checkbox'; + sProcess.options.parallel.Value = 0; + % 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 +70,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 @@ -89,47 +95,32 @@ %% ===== RUN ===== function OutputFiles = Run(sProcess, sInputs) %#ok OutputFiles = {}; - ProtocolInfo = bst_get('ProtocolInfo'); - + % 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.'); return end - if sProcess.options.binsize.Value{1} <= 0 - bst_report('Error', sProcess, sInputs, 'Invalid maximum amount of RAM specified.'); - return - end - - - %% Load plugin + % Load plugin [isInstalled, errMsg] = bst_plugin('Install', 'ultramegasort2000'); if ~isInstalled error(errMsg); end - - % Prepare parallel pool, if requested - if sProcess.options.paral.Value - try - poolobj = gcp('nocreate'); - if isempty(poolobj) - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('start', 'UltraMegaSort2000', 'Starting parallel pool'); - end - parpool; - end - catch - sProcess.options.paral.Value = 0; - poolobj = []; - end - else - poolobj = []; - end - + + % Get option: bin size + BinSize = sProcess.options.binsize.Value{1}; + if (BinSize <= 0) + bst_report('Error', sProcess, sInputs, 'Invalid maximum amount of RAM specified.'); + return + end + % Get other options + isParallel = sProcess.options.parallel.Value; + LowPass = sProcess.options.lowpass.Value{1}(1); + HighPass = sProcess.options.highpass.Value{1}(1); + % Compute on each raw input independently for i = 1:length(sInputs) - [fPath, fBase] = bst_fileparts(sInputs(i).FileName); + [fPath, fBase] = bst_fileparts(file_fullpath(sInputs(i).FileName)); % Remove "data_0raw" or "data_" tag if (length(fBase) > 10 && strcmp(fBase(1:10), 'data_0raw_')) fBase = fBase(11:end); @@ -137,16 +128,26 @@ fBase = fBase(6:end); end + % Load input files DataMat = in_bst_data(sInputs(i).FileName, 'F'); sFile = DataMat.F; + % Check filtering frequencies + nyq = floor(sFile.prop.sfreq/2); + if (LowPass >= nyq) + bst_report('Error', sProcess, sInputs, ['Higher cutoff frequency must be lower than Nyquist frequency (' num2str(nyq) ' Hz).']); + return; + elseif (HighPass >= LowPass) + bst_report('Error', sProcess, sInputs, 'Higher cutoff frequency must be lower lower cutoff frequency.'); + return; + end + % Load channel file 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); + % Demultiplex channels + sFiles = in_spikesorting_rawelectrodes(sInputs(i), BinSize * 1e9, isParallel); %%%%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% - outputPath = bst_fullfile(ProtocolInfo.STUDIES, fPath, [fBase '_ums2k_spikes']); + outputPath = bst_fullfile(fPath, [fBase '_ums2k_spikes']); % Clear if directory already exists if exist(outputPath, 'dir') == 7 @@ -158,53 +159,33 @@ end mkdir(outputPath); - %% %%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% - isProgress = bst_progress('isVisible'); - if sProcess.options.paral.Value - if isProgress - bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); - end - else - if isProgress - bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); - end - end - - %% UltraMegaSort2000 needs manual filtering of the raw files - - Fs = sFile.prop.sfreq; - - % The Get_spikes saves the _spikes files at the current directory. + %%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% previous_directory = pwd; cd(outputPath); - - if sProcess.options.paral.Value + if isParallel + bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); parfor ielectrode = 1:numChannels - do_UltraMegaSorting(sFiles{ielectrode}, sFile, sProcess.options.lowpass, sProcess.options.highpass, Fs); + do_UltraMegaSorting(sFiles{ielectrode}, sFile, LowPass, HighPass, sFile.prop.sfreq); end else + bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); for ielectrode = 1:numChannels - do_UltraMegaSorting(sFiles{ielectrode}, sFile, sProcess.options.lowpass, sProcess.options.highpass, Fs); - if isProgress - bst_progress('inc', 1); - end + do_UltraMegaSorting(sFiles{ielectrode}, sFile, LowPass, HighPass, sFile.prop.sfreq); + bst_progress('inc', 1); end end - + % Restore current folder + cd(previous_directory); + %%%%%%%%%%%%%%%%%%%%% Create Brainstorm Events %%%%%%%%%%%%%%%%%%% - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('start', 'UltraMegaSort2000', 'Gathering spiking events...'); - end + bst_progress('start', 'UltraMegaSort2000', 'Gathering spiking events...'); - cd(previous_directory); - % Delete existing spike events process_spikesorting_supervised('DeleteSpikeEvents', sInputs(i).FileName); - % ===== SAVE LINK FILE ===== + % ===== SAVE SPIKE FILE ===== % Build output filename - NewBstFilePrefix = bst_fullfile(ProtocolInfo.STUDIES, fPath, ['data_0ephys_ums2k_' fBase]); + NewBstFilePrefix = bst_fullfile(fPath, ['data_0ephys_ums2k_' fBase]); NewBstFile = [NewBstFilePrefix '.mat']; iFile = 1; commentSuffix = ''; @@ -216,7 +197,7 @@ % Build output structure DataMat = struct(); DataMat.Comment = ['UltraMegaSort2000 Spike Sorting' commentSuffix]; - DataMat.DataType = 'raw';%'ephys'; + DataMat.DataType = 'raw'; DataMat.Device = 'ultramegasort2000'; DataMat.Name = NewBstFile; DataMat.Parent = outputPath; @@ -234,18 +215,16 @@ DataMat.Spikes(iSpike).Name = ChannelMat.Channel(iSpike).Name; DataMat.Spikes(iSpike).Mod = 0; end - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('start', 'UltraMegaSort2000', 'Saving events file...'); - 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); + db_add_data(sInputs(i).iStudy, file_short(NewBstFile), DataMat); % Return new file OutputFiles{end+1} = NewBstFile; @@ -253,35 +232,22 @@ % 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); - end - end - - if isProgress - bst_progress('stop'); - end - + end 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) @@ -297,10 +263,10 @@ function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) for iElectrode = 1:numElectrodes newEvents = process_spikesorting_supervised(... 'CreateSpikeEvents', ... - sFile.RawFile, ... - sFile.Device, ... - bst_fullfile(sFile.Parent, sFile.Spikes(iElectrode).File), ... - sFile.Spikes(iElectrode).Name, ... + SpikeMat.RawFile, ... + SpikeMat.Device, ... + bst_fullfile(SpikeMat.Parent, SpikeMat.Spikes(iElectrode).File), ... + SpikeMat.Spikes(iElectrode).Name, ... 1, eventNamePrefix); if iNewEvent == 0 @@ -313,33 +279,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 b8e569d582..be2a0d4412 100644 --- a/toolbox/process/functions/process_spikesorting_waveclus.m +++ b/toolbox/process/functions/process_spikesorting_waveclus.m @@ -28,7 +28,9 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018-2019, 2022; Martin Cousineau, 2018 +% Authors: Konstantinos Nasiotis, 2018-2022 +% Martin Cousineau, 2018 +% Francois Tadel, 2022 eval(macro_method); end @@ -51,24 +53,28 @@ sProcess.options.spikesorter.Type = 'text'; sProcess.options.spikesorter.Value = 'waveclus'; sProcess.options.spikesorter.Hidden = 1; + % Procesing options 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'; + sProcess.options.parallel.Comment = 'Parallel processing'; + sProcess.options.parallel.Type = 'checkbox'; + sProcess.options.parallel.Value = 0; + 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 @@ -82,63 +88,44 @@ %% ===== RUN ===== function OutputFiles = Run(sProcess, sInputs) %#ok OutputFiles = {}; - ProtocolInfo = bst_get('ProtocolInfo'); % 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.'); return end - - if sProcess.options.binsize.Value{1} <= 0 - bst_report('Error', sProcess, sInputs, 'Invalid maximum amount of RAM specified.'); - return - end - - - %% Load plugin + % Load plugin [isInstalled, errMsg] = bst_plugin('Install', 'waveclus'); if ~isInstalled error(errMsg); end - - % Prepare parallel pool, if requested - if sProcess.options.paral.Value - try - poolobj = gcp('nocreate'); - if isempty(poolobj) - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('start', 'WaveClus', 'Starting parallel pool'); - end - parpool; - end - catch - sProcess.options.paral.Value = 0; - poolobj = []; - end - else - poolobj = []; + + % Get option: bin size + BinSize = sProcess.options.binsize.Value{1}; + if (BinSize <= 0) + bst_report('Error', sProcess, sInputs, 'Invalid maximum amount of RAM specified.'); + return end + % Get other options + isParallel = sProcess.options.parallel.Value; % Compute on each raw input independently for i = 1:length(sInputs) - [fPath, fBase] = bst_fileparts(sInputs(i).FileName); + [fPath, fBase] = bst_fileparts(file_fullpath(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 - + + % Load input files 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); + sFiles = in_spikesorting_rawelectrodes(sInputs(i), BinSize * 1e9, isParallel); %%%%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% - outputPath = bst_fullfile(ProtocolInfo.STUDIES, fPath, [fBase '_waveclus_spikes']); + outputPath = bst_fullfile(fPath, [fBase '_waveclus_spikes']); % Clear if directory already exists if exist(outputPath, 'dir') == 7 @@ -150,40 +137,32 @@ end mkdir(outputPath); - %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% - isProgress = bst_progress('isVisible'); - if sProcess.options.paral.Value && isProgress - bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); - elseif isProgress - bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); - end - - % The Get_spikes saves the _spikes files at the current directory. + %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% + % The function Get_spikes saves the _spikes files at the current directory previous_directory = pwd; cd(outputPath); - - if sProcess.options.paral.Value + 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(sFiles{ielectrode}); end end 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(sFiles{ielectrode}); end - if isProgress - bst_progress('inc', 1); - end + bst_progress('inc', 1); end end %%%%%%%%%%%%%%%%%%%%%% Do the clustering %%%%%%%%%%%%%%%%%%%%%%%%%% - bst_progress('text', 'Clustering detected spikes...'); + 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 + if isParallel parallel = true; else parallel = false; @@ -193,7 +172,6 @@ else make_plots = false; end - % Do the clustering Do_clustering(1:numChannels, 'parallel', parallel, 'make_plots', make_plots); @@ -206,7 +184,7 @@ % ===== SAVE LINK FILE ===== % Build output filename - NewBstFilePrefix = bst_fullfile(ProtocolInfo.STUDIES, fPath, ['data_0ephys_wclus_' fBase]); + NewBstFilePrefix = bst_fullfile(fPath, ['data_0ephys_wclus_' fBase]); NewBstFile = [NewBstFilePrefix '.mat']; iFile = 1; commentSuffix = ''; @@ -218,15 +196,12 @@ % Build output structure DataMat = struct(); DataMat.Comment = ['WaveClus Spike Sorting' commentSuffix]; - DataMat.DataType = 'raw';%'ephys'; + DataMat.DataType = 'raw'; 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) @@ -246,7 +221,7 @@ % Save file on hard drive bst_save(NewBstFile, DataMat, 'v6'); % Add file to database - sOutputStudy = db_add_data(sInputs(i).iStudy, NewBstFile, DataMat); + db_add_data(sInputs(i).iStudy, file_short(NewBstFile), DataMat); % Return new file OutputFiles{end+1} = NewBstFile; @@ -254,23 +229,11 @@ % 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); - end - end - - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('stop'); - end + end end + +%% ===== SAVE BRAINSTORM EVENTS ===== function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) if nargin < 3 eventNamePrefix = ''; From e1db819c3ee953d32d33101297f46c261c3f4713 Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 22 Apr 2022 13:16:59 +0200 Subject: [PATCH 34/43] Reformatted spike sorting processes --- .../io/in_spikesorting_convertforkilosort.m | 128 ----- ...ting_rawelectrodes.m => out_demultiplex.m} | 131 ++--- .../functions/process_convert_raw_to_lfp.m | 4 +- .../functions/process_spikesorting_kilosort.m | 501 ++++++++++-------- .../process_spikesorting_ultramegasort2000.m | 29 +- .../functions/process_spikesorting_waveclus.m | 34 +- 6 files changed, 375 insertions(+), 452 deletions(-) delete mode 100644 toolbox/io/in_spikesorting_convertforkilosort.m rename toolbox/io/{in_spikesorting_rawelectrodes.m => out_demultiplex.m} (53%) diff --git a/toolbox/io/in_spikesorting_convertforkilosort.m b/toolbox/io/in_spikesorting_convertforkilosort.m deleted file mode 100644 index b6f79a9190..0000000000 --- a/toolbox/io/in_spikesorting_convertforkilosort.m +++ /dev/null @@ -1,128 +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, 2022; 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; % 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); - -converted_raw_File = bst_fullfile(parentPath, ['raw_data_no_header_' sInput.Condition(5:end) '.dat']); - -isProgress = bst_progress('isVisible'); -if ~isProgress - bst_progress('start', 'Spike-sorting', 'Converting to KiloSort Input...', 0, ceil((fileSamples(2)-fileSamples(1))/max_samples)); -end - -if exist(converted_raw_File, 'file') == 2 - disp('File already converted to kilosort input') - 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'); - -num_segments = ceil(total_samples / max_samples); -num_samples_per_segment = ceil(total_samples / num_segments); - -isProgress = bst_progress('isVisible'); -if ~isProgress - bst_progress('show'); -end -bst_progress('start', 'Kilosort spike sorting', 'Converting to int16 .dat file', 0, num_segments); - - -sampleBounds_all = cell(num_segments,1); -sampleBounds = [0,0]; -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 - - 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 - - fwrite(fid, F, 'int16'); - - bst_progress('inc', 1); - sampleBounds_all{iSegment} = sampleBounds; % This is here for an easy check that there is no overlap between segments - - clear F -end -fclose(fid); - -isProgress = bst_progress('isVisible'); -if ~isProgress - bst_progress('stop'); -end - - diff --git a/toolbox/io/in_spikesorting_rawelectrodes.m b/toolbox/io/out_demultiplex.m similarity index 53% rename from toolbox/io/in_spikesorting_rawelectrodes.m rename to toolbox/io/out_demultiplex.m index c1d788139f..4db5c77e4e 100644 --- a/toolbox/io/in_spikesorting_rawelectrodes.m +++ b/toolbox/io/out_demultiplex.m @@ -1,8 +1,5 @@ -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) +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: @@ -22,68 +19,43 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Authors: Konstantinos Nasiotis, 2018, 2022; 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); +% Authors: Konstantinos Nasiotis, 2018-2022 +% Martin Cousineau, 2018 +% Francois Tadel, 2022 -% Make sure the temporary directory exist, otherwise create it -if ~exist(parentPath, 'dir') - mkdir(parentPath); +% If the output directory doesn't exist: create it +if ~exist(OutputDir, 'dir') + mkdir(OutputDir); end - -% Check whether the electrode files already exist -ChannelMat = in_bst_channel(sInput.ChannelFile); +% Load channel file +ChannelMat = in_bst_channel(ChannelFile); numChannels = length(ChannelMat.Channel); - -% New channelNames - Without any special characters. +% Channel names: Remove 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 +% 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) return; -else - % Clear any remaining intermediate file - for iFile = 1:length(sFiles) - delete(sFiles{iFile}); - end +% If some files already exist: delete all intermediate existing file, before generating them again +elseif any(isFileOk) + delete(outFiles{isFileOk}); end - -% Otherwise, generate all of them again. -DataMat = in_bst_data(sInput.FileName, 'F'); +% 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 -ImportOptions = db_template('ImportOptions'); if ismember(sFile.format, {'EEG-AXION', 'EEG-BLACKROCK', 'EEG-INTAN', 'EEG-PLEXON'}) precision = 'single'; nBytes = 4; @@ -93,34 +65,13 @@ 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); -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 - - -%% 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 -sampleBounds_all = cell(num_segments,1); -sampleBounds = [0,0]; +% 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 @@ -128,54 +79,56 @@ 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([sFiles{iChannel} '.bin'], 'a'); + 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([sFiles{iChannel} '.bin'], 'a'); + fid = fopen([outFiles{iChannel} '.bin'], 'a'); fwrite(fid, electrode_data, precision); fclose(fid); bst_progress('inc', 1); end end - clear F - sampleBounds_all{iSegment} = sampleBounds; % This is here for an easy check that there is no overlap between segments end - -%% Convert binary files per channel to Matlab files +% Convert binary files per channel to Matlab files if parallel bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...'); parfor iChannel = 1:numChannels - convert2mat(sFiles{iChannel}, sr, precision); + convert2mat(outFiles{iChannel}, sr, precision); end else - bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...', 0, (parallel == 0) * numChannels); + bst_progress('start', 'Spike-sorting', 'Converting demultiplexed files...', 0, numChannels); for iChannel = 1:numChannels - convert2mat(sFiles{iChannel}, sr, precision); + convert2mat(outFiles{iChannel}, sr, precision); bst_progress('inc', 1); end end -sFiles = cellfun(@(x) [x '.mat'], sFiles, 'UniformOutput', 0); +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'); - file_delete([chanFile '.bin'], 1 ,3); + % Delete .bin file + delete([chanFile '.bin']); end diff --git a/toolbox/process/functions/process_convert_raw_to_lfp.m b/toolbox/process/functions/process_convert_raw_to_lfp.m index c625a17a2c..6c2e254aa5 100644 --- a/toolbox/process/functions/process_convert_raw_to_lfp.m +++ b/toolbox/process/functions/process_convert_raw_to_lfp.m @@ -74,12 +74,10 @@ 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 - + 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 - %% ===== FORMAT COMMENT ===== function Comment = FormatComment(sProcess) %#ok Comment = sProcess.Comment; diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index 450380844e..076f6b5730 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -50,15 +50,22 @@ 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 = '
'; @@ -111,12 +118,16 @@ error(errMsg); end + % 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; % Compute on each raw input independently for i = 1:length(sInputs) + bst_progress('text', 'Kilosort: Reading input files...'); [fPath, fBase] = bst_fileparts(file_fullpath(sInputs(i).FileName)); % Remove "data_0raw" or "data_" tag if (length(fBase) > 10 && strcmp(fBase(1:10), 'data_0raw_')) @@ -140,14 +151,21 @@ %%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% outputPath = bst_fullfile(fPath, [fBase '_kilosort_spikes']); - % Clear if directory already exists + 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('Couldnt remove spikes folder. Make sure the current directory is not that folder or that Klusters is not open.') + error(['Could not remove spikes folder: ' 10 outputPath 10 ' Make sure this folder is not open in another program (e.g. Klusters).']) end end + % Create output folder mkdir(outputPath); % Prepare the ChannelMat File @@ -159,7 +177,7 @@ % Get the channels in the montage % First check if any montages have been assigned - [Channels, Montages, channelsMontage,montageOccurences] = deal_with_channels_and_groups(ChannelMat); + [Channels, Montages, channelsMontage,montageOccurences] = ParseMontage(ChannelMat); % Adjust the possible clusters based on the number of channels doubleChannels = 2*max(montageOccurences); % Each Montage will be treated as its own entity. @@ -228,13 +246,12 @@ % 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_bst(ChannelMat, fs, xmlFile, ops) + CreateXML(ChannelMat, fs, xmlFile, ops); - previous_directory = pwd; cd(outputPath); % Convert to the right input for KiloSort - converted_raw_File = in_spikesorting_convertforkilosort(sInputs(i), sProcess.options.binsize.Value{1} * 1e9); % This converts into int16. + converted_raw_File = ConvertForKilosort(sInputs(i), BinSize * 1e9, UseSsp); % This converts into int16. %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% @@ -259,7 +276,7 @@ try rez = fitTemplates(rez, DATA, uproj); % fit templates iteratively catch - if sProcess.options.GPU.Value + 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. @@ -295,7 +312,7 @@ process_spikesorting_supervised('DeleteSpikeEvents', sInputs(i).FileName); sFile.RawFile = sInputs(i).FileName; - convertKilosort2BrainstormEvents(sFile, ChannelMat, fPath, rez); + ImportKilosortEvents(sFile, ChannelMat, fPath, rez); cd(previous_directory); @@ -321,12 +338,20 @@ end end - % ===== SAVE LINK FILE ===== + % ===== SAVE SPIKE FILE ===== % Build output filename - NewBstFile = bst_fullfile(fPath, ['data_0ephys_' fBase '.mat']); + 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'; + DataMat_spikesorter.Comment = ['KiloSort Spike Sorting' commentSuffix]; DataMat_spikesorter.DataType = 'raw';%'ephys'; DataMat_spikesorter.Device = 'KiloSort'; DataMat_spikesorter.Parent = outputPath; @@ -350,12 +375,68 @@ 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 -%% ===== CONVERT EVENTS ===== -function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) - events_spikes = struct(); - +%% ===== 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, @@ -375,15 +456,14 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) % 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'); 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 @@ -425,7 +505,6 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) DataMat.F.events = events; [folder, filename_link2Raw, extension] = bst_fileparts(sFile.RawFile); bst_save(bst_fullfile(parentPath, [filename_link2Raw extension]), DataMat, 'v6'); - end @@ -443,15 +522,14 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) [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])); % Get the channels that belong in the selected montage - [Channels, Montages, channelsMontage,montageOccurences] = deal_with_channels_and_groups(ChannelMat); + [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) @@ -505,6 +583,7 @@ function convertKilosort2BrainstormEvents(sFile, ChannelMat, parentPath, rez) %% ===== COPY KILOSORT CONFIG ===== +% Called by bst_plugin after installing the kilosort plugin function copyKilosortConfig(defaultFile, outputFile) if exist(outputFile, 'file') == 2 delete(outputFile); @@ -528,201 +607,193 @@ function copyKilosortConfig(defaultFile, outputFile) %% ===== CREATE XML ===== -function createXML_bst(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 +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 - 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 + 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 - 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 + montages = unique({temp_ChannelsMat.Group},'stable'); + montages = montages(find(~cellfun(@isempty, montages))); - for iChannel = 1:length(ChannelsInMontage{iMontage}) - ChannelsInMontage{iMontage,2} = [ChannelsInMontage{iMontage,2} find(strcmp({ChannelMat.Channel.Name}, ChannelsInMontage{iMontage}(iChannel).Name))]; + 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 - 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); - -%add channel count here - -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]); + 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,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]); + + 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,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); + + 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 -end - -s = cat(1,s, chunk5); -% Output -charcelltotext(s, xmlFile); + s = cat(1,s, chunk5); + + % Output + charcelltotext(s, xmlFile); end @@ -731,22 +802,17 @@ 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. - - [nrows,ncols]= size(charcell); - fid = fopen(filename, 'w'); - - for row=1:nrows + 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] = deal_with_channels_and_groups(ChannelMat) +function [Channels, Montages, channelsMontage, montageOccurences] = ParseMontage(ChannelMat) % First check if any montages have been assigned Channels = ChannelMat.Channel; allMontages = {Channels.Group}; @@ -755,8 +821,7 @@ function charcelltotext(charcell,filename) 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 + % 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 diff --git a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m index 395f988d4d..ede97a36a5 100644 --- a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m +++ b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m @@ -50,15 +50,22 @@ 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}; + % 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 = '
'; @@ -115,8 +122,12 @@ 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); + % Get protocol info + ProtocolInfo = bst_get('ProtocolInfo'); + BrainstormTmpDir = bst_get('BrainstormTmpDir'); % Compute on each raw input independently for i = 1:length(sInputs) @@ -144,23 +155,29 @@ ChannelMat = in_bst_channel(sInputs(i).ChannelFile); numChannels = length(ChannelMat.Channel); % Demultiplex channels - sFiles = in_spikesorting_rawelectrodes(sInputs(i), BinSize * 1e9, isParallel); + demultiplexDir = bst_fullfile(BrainstormTmpDir, 'Unsupervised_Spike_Sorting', ProtocolInfo.Comment, sInputs(i).FileName); + sFiles = out_demultiplex(sInputs(i).FileName, sInputs(i).ChannelFile, demultiplexDir, UseSsp, BinSize * 1e9, isParallel); - %%%%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% + %%%%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% outputPath = bst_fullfile(fPath, [fBase '_ums2k_spikes']); - - % Clear if directory already exists + 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('Couldnt remove spikes folder. Make sure the current directory is not that folder.') + error(['Could not remove spikes folder: ' 10 outputPath 10 ' Make sure this folder is not open in another program.']) end end + % Create output folder mkdir(outputPath); %%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% - previous_directory = pwd; cd(outputPath); if isParallel bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); diff --git a/toolbox/process/functions/process_spikesorting_waveclus.m b/toolbox/process/functions/process_spikesorting_waveclus.m index be2a0d4412..18d3856533 100644 --- a/toolbox/process/functions/process_spikesorting_waveclus.m +++ b/toolbox/process/functions/process_spikesorting_waveclus.m @@ -50,16 +50,23 @@ 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; - % Procesing options + % RAM limitation sProcess.options.binsize.Comment = 'Maximum RAM to use: '; sProcess.options.binsize.Type = 'value'; sProcess.options.binsize.Value = {2, 'GB', 1}; + % 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; @@ -108,6 +115,10 @@ end % Get other options isParallel = sProcess.options.parallel.Value; + UseSsp = sProcess.options.usessp.Value; + % Get protocol info + ProtocolInfo = bst_get('ProtocolInfo'); + BrainstormTmpDir = bst_get('BrainstormTmpDir'); % Compute on each raw input independently for i = 1:length(sInputs) @@ -122,24 +133,31 @@ % Load input files ChannelMat = in_bst_channel(sInputs(i).ChannelFile); numChannels = length(ChannelMat.Channel); - sFiles = in_spikesorting_rawelectrodes(sInputs(i), BinSize * 1e9, isParallel); + % Demultiplex channels + demultiplexDir = bst_fullfile(BrainstormTmpDir, 'Unsupervised_Spike_Sorting', ProtocolInfo.Comment, sInputs(i).FileName); + sFiles = out_demultiplex(sInputs(i).FileName, sInputs(i).ChannelFile, demultiplexDir, UseSsp, BinSize * 1e9, isParallel); - %%%%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% + %%%%%%%%%%%%%%%%%%%%% Prepare output folder %%%%%%%%%%%%%%%%%%%%%% outputPath = bst_fullfile(fPath, [fBase '_waveclus_spikes']); - - % Clear if directory already exists + 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('Couldnt remove spikes folder. Make sure the current directory is not that folder.') + error(['Could not remove spikes folder: ' 10 outputPath 10 ' Make sure this folder is not open in another program.']) end end + % Create output folder mkdir(outputPath); %%%%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% % The function Get_spikes saves the _spikes files at the current directory - previous_directory = pwd; cd(outputPath); if isParallel bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); @@ -182,7 +200,7 @@ % Delete existing spike events process_spikesorting_supervised('DeleteSpikeEvents', sInputs(i).FileName); - % ===== SAVE LINK FILE ===== + % ===== SAVE SPIKE FILE ===== % Build output filename NewBstFilePrefix = bst_fullfile(fPath, ['data_0ephys_wclus_' fBase]); NewBstFile = [NewBstFilePrefix '.mat']; From ea76641bad1943ba6c3a115b162837cca2177f3b Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 22 Apr 2022 18:21:00 +0200 Subject: [PATCH 35/43] Convert processes category from Custom to File --- toolbox/io/out_demultiplex.m | 3 + .../functions/process_spikesorting_kilosort.m | 481 +++++++++--------- .../process_spikesorting_ultramegasort2000.m | 237 +++++---- .../functions/process_spikesorting_waveclus.m | 250 +++++---- 4 files changed, 485 insertions(+), 486 deletions(-) diff --git a/toolbox/io/out_demultiplex.m b/toolbox/io/out_demultiplex.m index 4db5c77e4e..880c1c0442 100644 --- a/toolbox/io/out_demultiplex.m +++ b/toolbox/io/out_demultiplex.m @@ -39,6 +39,8 @@ % 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 + 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) @@ -116,6 +118,7 @@ end end +% Add the .mat extension to the file names outFiles = cellfun(@(x) [x '.mat'], outFiles, 'UniformOutput', 0); end diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index 076f6b5730..f39463b124 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: @@ -37,10 +35,10 @@ %% ===== 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'; @@ -83,41 +81,42 @@ %% ===== 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 = {}; + % ===== 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 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 - 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; @@ -125,253 +124,253 @@ KilosortStandardConfig(); ops.GPU = sProcess.options.GPU.Value; - % Compute on each raw input independently - for i = 1:length(sInputs) - bst_progress('text', 'Kilosort: Reading input files...'); - [fPath, fBase] = bst_fileparts(file_fullpath(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 - - % Load input files - DataMat = in_bst_data(sInputs(i).FileName, 'F'); - sFile = DataMat.F; - ChannelMat = in_bst_channel(sInputs(i).ChannelFile); + % 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 + + % ===== 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 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 + % 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 + + % ===== 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 - % Create output folder - 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; - - % Get the channels in the montage - % First check if any montages have been assigned - [Channels, Montages, channelsMontage,montageOccurences] = ParseMontage(ChannelMat); - - % 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; + % 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 - - 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 + end + % Create output folder + mkdir(outputPath); + - % APPLY TRANSORMATION TO A FLAT SURFACE (X-Y COORDINATES: IGNORE Z) - converted_coordinates = zeros(length(Channels),3); + % ===== 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); + + % 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) - 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); + if strcmp(Channels(iChannel).Group, Montages{iMontage}) + channelsCoords(iChannel,1:3) = Channels(iChannel).Loc; + end 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 - 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; + % 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 - 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. + + 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(sInputs(i), BinSize * 1e9, UseSsp); % This converts into int16. - + 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. + - %%%%%%%%%%%%%%%%%%%%%%% Start the 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 - - % 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 - 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, sInputs, '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, sInputs, 'Error with Kilosort while training on the CPU'); - return; - end + % ===== 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 - - 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); - - % 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; - ImportKilosortEvents(sFile, ChannelMat, fPath, rez); - - cd(previous_directory); + end - % 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 - 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 + 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); + + % 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) + + % Restore current folder + cd(previous_directory); + + + % ===== IMPORT EVENTS ===== + bst_progress('text', 'Saving events file...'); + + % Delete existing spike events + process_spikesorting_supervised('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 - - % ===== 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) ')']; + 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 - % 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 = sInputs(i).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(sInputs(i).iStudy, file_short(NewBstFile), DataMat_spikesorter); - % 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 + 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 diff --git a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m index ede97a36a5..e5da64e092 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: @@ -40,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'; @@ -100,12 +98,13 @@ %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInput) %#ok OutputFiles = {}; + % ===== 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 % Load plugin @@ -114,10 +113,11 @@ error(errMsg); end + % ===== OPTIONS ===== % Get option: bin size BinSize = sProcess.options.binsize.Value{1}; if (BinSize <= 0) - bst_report('Error', sProcess, sInputs, 'Invalid maximum amount of RAM specified.'); + bst_report('Error', sProcess, sInput, 'Invalid maximum amount of RAM specified.'); return end % Get other options @@ -125,131 +125,130 @@ UseSsp = sProcess.options.usessp.Value; LowPass = sProcess.options.lowpass.Value{1}(1); HighPass = sProcess.options.highpass.Value{1}(1); + + % ===== LOAD INPUTS ===== % Get protocol info ProtocolInfo = bst_get('ProtocolInfo'); BrainstormTmpDir = bst_get('BrainstormTmpDir'); - - % Compute on each raw input independently - for i = 1:length(sInputs) - [fPath, fBase] = bst_fileparts(file_fullpath(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); + % 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 + 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 - - % Load input files - DataMat = in_bst_data(sInputs(i).FileName, 'F'); - sFile = DataMat.F; - % Check filtering frequencies - nyq = floor(sFile.prop.sfreq/2); - if (LowPass >= nyq) - bst_report('Error', sProcess, sInputs, ['Higher cutoff frequency must be lower than Nyquist frequency (' num2str(nyq) ' Hz).']); - return; - elseif (HighPass >= LowPass) - bst_report('Error', sProcess, sInputs, 'Higher cutoff frequency must be lower lower cutoff frequency.'); - return; + % 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 - % Load channel file - ChannelMat = in_bst_channel(sInputs(i).ChannelFile); - numChannels = length(ChannelMat.Channel); - % Demultiplex channels - demultiplexDir = bst_fullfile(BrainstormTmpDir, 'Unsupervised_Spike_Sorting', ProtocolInfo.Comment, sInputs(i).FileName); - sFiles = out_demultiplex(sInputs(i).FileName, sInputs(i).ChannelFile, demultiplexDir, UseSsp, BinSize * 1e9, isParallel); - - %%%%%%%%%%%%%%%%%%%%% Prepare 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 - % 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 + end + % Create output folder + mkdir(outputPath); + + + % ===== 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 - % Create output folder - mkdir(outputPath); - - %%%%%%%%%%%%%%%%%%%%% Start the spike sorting %%%%%%%%%%%%%%%%%%% - cd(outputPath); - if isParallel - bst_progress('start', 'Spike-sorting', 'Extracting spikes...'); - parfor ielectrode = 1:numChannels - do_UltraMegaSorting(sFiles{ielectrode}, sFile, LowPass, HighPass, sFile.prop.sfreq); - end - else - bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); - for ielectrode = 1:numChannels - do_UltraMegaSorting(sFiles{ielectrode}, sFile, LowPass, HighPass, sFile.prop.sfreq); - bst_progress('inc', 1); - end + 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 - % Restore current folder - cd(previous_directory); + end + % Restore current folder + cd(previous_directory); - %%%%%%%%%%%%%%%%%%%%% Create Brainstorm Events %%%%%%%%%%%%%%%%%%% - bst_progress('start', 'UltraMegaSort2000', 'Gathering spiking events...'); + + % ===== IMPORT EVENTS ===== + bst_progress('start', 'UltraMegaSort2000', 'Gathering spiking events...'); + % Delete existing spike events + process_spikesorting_supervised('DeleteSpikeEvents', sInput.FileName); - % Delete existing spike events - process_spikesorting_supervised('DeleteSpikeEvents', sInputs(i).FileName); - - % ===== SAVE SPIKE FILE ===== - % 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 = 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; + % 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 - - % Save events file for backup - SaveBrainstormEvents(DataMat, 'events_UNSUPERVISED.mat'); + DataMat.Spikes(iSpike).Name = ChannelMat.Channel(iSpike).Name; + DataMat.Spikes(iSpike).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 - db_add_data(sInputs(i).iStudy, file_short(NewBstFile), DataMat); - % Return new file - OutputFiles{end+1} = 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 + db_add_data(sInput.iStudy, file_short(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 + % ===== UPDATE DATABASE ===== + % Update links + db_links('Study', sInput.iStudy); + panel_protocols('UpdateNode', 'Study', sInput.iStudy); end diff --git a/toolbox/process/functions/process_spikesorting_waveclus.m b/toolbox/process/functions/process_spikesorting_waveclus.m index 18d3856533..5b783be14f 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: @@ -40,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'; @@ -93,12 +91,13 @@ %% ===== RUN ===== -function OutputFiles = Run(sProcess, sInputs) %#ok +function OutputFiles = Run(sProcess, sInput) %#ok OutputFiles = {}; + % ===== 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 % Load plugin @@ -107,147 +106,146 @@ error(errMsg); end + % ===== OPTIONS ===== % Get option: bin size BinSize = sProcess.options.binsize.Value{1}; if (BinSize <= 0) - bst_report('Error', sProcess, sInputs, 'Invalid maximum amount of RAM specified.'); + 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; + + % ===== 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); - % Compute on each raw input independently - for i = 1:length(sInputs) - [fPath, fBase] = bst_fileparts(file_fullpath(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); + % ===== 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 - - % Load input files - ChannelMat = in_bst_channel(sInputs(i).ChannelFile); - numChannels = length(ChannelMat.Channel); - % Demultiplex channels - demultiplexDir = bst_fullfile(BrainstormTmpDir, 'Unsupervised_Spike_Sorting', ProtocolInfo.Comment, sInputs(i).FileName); - sFiles = out_demultiplex(sInputs(i).FileName, sInputs(i).ChannelFile, demultiplexDir, UseSsp, BinSize * 1e9, isParallel); - - %%%%%%%%%%%%%%%%%%%%% Prepare 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 - rmdir(outputPath, 's'); - catch - error(['Could not remove spikes folder: ' 10 outputPath 10 ' Make sure this folder is not open in another program.']) - 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.']) end - % Create output folder - mkdir(outputPath); - - %%%%%%%%%%%%%%%%%%%%%%% Start the 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(sFiles{ielectrode}); - end + end + % Create output folder + mkdir(outputPath); + + % ===== 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 - bst_progress('start', 'Spike-sorting', 'Extracting spikes...', 0, numChannels); - for ielectrode = 1:numChannels - if ismember(upper(ChannelMat.Channel(ielectrode).Type), {'EEG', 'SEEG'}) - Get_spikes(sFiles{ielectrode}); - end - bst_progress('inc', 1); + end + 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 + 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 isParallel - 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 SPIKE FILE ===== - % Build output filename - NewBstFilePrefix = bst_fullfile(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'; - DataMat.Device = 'waveclus'; - DataMat.Name = NewBstFile; - DataMat.Parent = outputPath; - DataMat.RawFile = sInputs(i).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; + + % ===== 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 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); + % Restore current folder + cd(previous_directory); + + + % ===== IMPORT EVENTS ===== + bst_progress('text', 'Saving events file...'); + % Delete existing spike events + process_spikesorting_supervised('DeleteSpikeEvents', sInput.FileName); + + % Build output filename + NewBstFilePrefix = bst_fullfile(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'; + 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 - % 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(sInputs(i).iStudy, file_short(NewBstFile), DataMat); - % Return new file - OutputFiles{end+1} = NewBstFile; + 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 + db_add_data(sInput.iStudy, file_short(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 + % ===== UPDATE DATABASE ===== + % Update links + db_links('Study', sInput.iStudy); + panel_protocols('UpdateNode', 'Study', sInput.iStudy); end From b02cd89831b3a67ec3ef052a92b5a06012466a60 Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 22 Apr 2022 20:29:25 +0200 Subject: [PATCH 36/43] Fix ASCII channel file import --- toolbox/core/bst_get.m | 12 ++++++------ toolbox/io/import_channel.m | 15 ++++++--------- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/toolbox/core/bst_get.m b/toolbox/core/bst_get.m index f94ac0ceb6..841f9b43f2 100644 --- a/toolbox/core/bst_get.m +++ b/toolbox/core/bst_get.m @@ -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/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'; From 35fd2cd60d851f5e3c8edc0df5c2e932bf08912a Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 22 Apr 2022 20:36:25 +0200 Subject: [PATCH 37/43] Convert to lfp: Re-coding --- toolbox/io/out_demultiplex.m | 1 + .../functions/process_convert_raw_to_lfp.m | 467 +++++++----------- 2 files changed, 187 insertions(+), 281 deletions(-) diff --git a/toolbox/io/out_demultiplex.m b/toolbox/io/out_demultiplex.m index 880c1c0442..c66f311301 100644 --- a/toolbox/io/out_demultiplex.m +++ b/toolbox/io/out_demultiplex.m @@ -40,6 +40,7 @@ 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 diff --git a/toolbox/process/functions/process_convert_raw_to_lfp.m b/toolbox/process/functions/process_convert_raw_to_lfp.m index 6c2e254aa5..1426891046 100644 --- a/toolbox/process/functions/process_convert_raw_to_lfp.m +++ b/toolbox/process/functions/process_convert_raw_to_lfp.m @@ -23,17 +23,17 @@ % =============================================================================@ % % 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.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/RawToLFP'; @@ -45,292 +45,212 @@ 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.LFP_fs.Comment = 'Sampling rate of the LFP signals'; + % === 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}; - - 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):'; + % 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 + % 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 - LFP_fs = sProcess.options.LFP_fs.Value{1}; - - % 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.']); - end - - - - %% Prepare parallel pool, if requested - if sProcess.options.paral.Value - try - poolobj = gcp('nocreate'); - if isempty(poolobj) - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('start', 'Convert RAW file to LFP', 'Starting parallel pool'); - end - parpool; - end - catch - sProcess.options.paral.Value = 0; - poolobj = []; - end - else - poolobj = []; - end - - - %% Check if the files are separated per channel. If not do it now. - % These files will be converted to LFP right after - - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('start', 'Convert RAW file to LFP', 'Demultiplexing raw file'); - end - - 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 = sMat.F.prop.sfreq; % This is the original sampling rate - % Inialize LFP matrix - LFP = zeros(length(sFiles_temp_mat), length(downsample(sMat.Time, round(Fs/LFP_fs)))); % This shouldn't create a memory problem - - %% Make a check if the requested sampling rate is higher than the original sampling rate - - if Fs < LFP_fs - bst_report('Error', sProcess, sInputs(iInput), 'The requested LFP sampling rate is higher than the RAW signal sampling rate. No need to use this function'); + % ===== 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 - - %% Initialize - % Prepare output file - ProtocolInfo = bst_get('ProtocolInfo'); - newCondition = [sInput.Condition, '_LFP']; - - if mod(Fs,LFP_fs) ~= 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(LFP_fs) ' Hz']) + % Install plugin + [isInstalled, errMsg] = bst_plugin('Install', 'derivelfp'); + if ~isInstalled + bst_report('Error', sProcess, [], errMsg); + return; 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 = LFP_fs; -% sFileTemplate.prop.times = round(sFileTemplate.prop.times(1) * NewFreq) / NewFreq + [0, size(LFP,2)-1] ./ NewFreq; - sFileTemplate.prop.times = [sFileTemplate.prop.times(1) (size(LFP,2)-1) ./ LFP_fs]; - sFileTemplate.header.sfreq = LFP_fs; - sFileTemplate.header.nsamples = size(LFP,2); - - % Update file - sFileTemplate.CommentTag = sprintf('resample(%dHz)', round(LFP_fs)); - sFileTemplate.HistoryComment = sprintf('Filter [%0.1f-%0.1f]Hz - Resample from %0.2f Hz to %0.2f Hz (%s)', filterBounds(1), filterBounds(2), Fs, LFP_fs, method); - sFileTemplate.despikeLFP = sProcess.options.despikeLFP.Value; - - % Convert events to new sampling rate -% newTimeVector = panel_time('GetRawTimeVector', sFileTemplate); - newTimeVector = downsample(sMat.Time, round(Fs/1000)); - sFileTemplate.events = panel_record('ChangeTimeVector', sFileTemplate.events, Fs, newTimeVector); + end - %% 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); - - %% Initialize progress bar - isProgress = bst_progress('isVisible'); - if isProgress - if sProcess.options.paral.Value - bst_progress('start', 'Raw2LFP', 'Converting raw signals to LFP...'); - else - bst_progress('start', 'Raw2LFP', 'Converting raw signals to LFP...', 0, (sProcess.options.paral.Value == 0) * nChannels); - end + % ===== 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 = [0, (nTimeOut-1) ./ LFP_fs]; + 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 - - %% Filter and derive LFP - 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, LFP_fs); - end - else - for iChannel = 1:nChannels - LFP(iChannel,:) = BayesianSpikeRemoval(sFiles_temp_mat{iChannel}, filterBounds, sMat.F, ChannelMat, cleanChannelNames, notchFilterFreqs, LFP_fs); - if isProgress - bst_progress('inc', 1); - end - end - end - else - if sProcess.options.paral.Value - parfor iChannel = 1:nChannels - LFP(iChannel,:) = filter_and_downsample(sFiles_temp_mat{iChannel}, Fs, filterBounds, notchFilterFreqs, LFP_fs); - end - else - for iChannel = 1:nChannels - LFP(iChannel,:) = filter_and_downsample(sFiles_temp_mat{iChannel}, Fs, filterBounds, notchFilterFreqs, LFP_fs); - if isProgress - bst_progress('inc', 1); - end - 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); - - % 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); end - - isProgress = bst_progress('isVisible'); - if isProgress - bst_progress('stop'); + + % ===== 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, LFP_fs) - 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, time_out] = process_resample('Compute', data, linspace(0, length(data)/sMat.sr, length(data)), LFP_fs); + % 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); + 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, LFP_fs) - - 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) % Assume that a spike lasts 3ms - nSegment = round(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'); @@ -343,8 +263,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 @@ -355,40 +274,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/LFP_fs)); % The file now has a different sampling rate (fs/30) = 1000Hz - [data_derived, time_out] = process_resample('Compute', data_derived, linspace(0, length(data_derived)/Fs, length(data_derived)), LFP_fs); - end - - From fd370189900e4eaebfb4797d490bb4166808b52b Mon Sep 17 00:00:00 2001 From: ftadel Date: Mon, 25 Apr 2022 12:10:00 +0200 Subject: [PATCH 38/43] Removed process_spikesorting_supervised This was not a "process", but a collection of interactive and shared functions. It was all moved to panel_spikes.m --- toolbox/core/bst_memory.m | 2 +- toolbox/gui/panel_spikes.m | 556 +++++++++++++++- toolbox/io/in_fopen_blackrock.m | 2 +- toolbox/io/in_fopen_plexon.m | 2 +- .../functions/process_convert_raw_to_lfp.m | 2 +- .../functions/process_noise_correlation.m | 2 +- .../functions/process_psth_per_electrode.m | 6 +- .../functions/process_psth_per_neuron.m | 2 +- .../functions/process_rasterplot_per_neuron.m | 2 +- .../functions/process_spike_field_coherence.m | 4 +- .../process_spike_triggered_average.m | 6 +- .../functions/process_spikesorting_kilosort.m | 8 +- .../process_spikesorting_supervised.m | 596 ------------------ .../process_spikesorting_ultramegasort2000.m | 6 +- .../functions/process_spikesorting_waveclus.m | 6 +- .../functions/process_spiking_phase_locking.m | 2 +- toolbox/process/panel_process_select.m | 2 +- toolbox/tree/tree_callbacks.m | 4 +- 18 files changed, 579 insertions(+), 631 deletions(-) delete mode 100644 toolbox/process/functions/process_spikesorting_supervised.m 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/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/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 67d71cbc5d..52f94dd2a9 100644 --- a/toolbox/io/in_fopen_plexon.m +++ b/toolbox/io/in_fopen_plexon.m @@ -198,7 +198,7 @@ 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 diff --git a/toolbox/process/functions/process_convert_raw_to_lfp.m b/toolbox/process/functions/process_convert_raw_to_lfp.m index 1426891046..5cba549a8d 100644 --- a/toolbox/process/functions/process_convert_raw_to_lfp.m +++ b/toolbox/process/functions/process_convert_raw_to_lfp.m @@ -253,7 +253,7 @@ 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 diff --git a/toolbox/process/functions/process_noise_correlation.m b/toolbox/process/functions/process_noise_correlation.m index f04c1c6842..90f331f232 100644 --- a/toolbox/process/functions/process_noise_correlation.m +++ b/toolbox/process/functions/process_noise_correlation.m @@ -120,7 +120,7 @@ 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)) + if panel_spikes('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; end end diff --git a/toolbox/process/functions/process_psth_per_electrode.m b/toolbox/process/functions/process_psth_per_electrode.m index 729df74b1a..1e27561ab2 100644 --- a/toolbox/process/functions/process_psth_per_electrode.m +++ b/toolbox/process/functions/process_psth_per_electrode.m @@ -126,9 +126,9 @@ 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)) + if panel_spikes('IsSpikeEvent', trial.Events(ievent).label) ... + && panel_spikes('IsFirstNeuron', trial.Events(ievent).label) ... + && strcmp(ChannelMat.Channel(ielectrode).Name, panel_spikes('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 diff --git a/toolbox/process/functions/process_psth_per_neuron.m b/toolbox/process/functions/process_psth_per_neuron.m index 0157fc7d15..9950c00c14 100644 --- a/toolbox/process/functions/process_psth_per_neuron.m +++ b/toolbox/process/functions/process_psth_per_neuron.m @@ -129,7 +129,7 @@ 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) + if panel_spikes('IsSpikeEvent', ALL_TRIALS_files(iFile).Events(iEvent).label) labelsForDropDownMenu{end+1} = ALL_TRIALS_files(iFile).Events(iEvent).label; end end diff --git a/toolbox/process/functions/process_rasterplot_per_neuron.m b/toolbox/process/functions/process_rasterplot_per_neuron.m index 697ecfffbe..90d9b45e54 100644 --- a/toolbox/process/functions/process_rasterplot_per_neuron.m +++ b/toolbox/process/functions/process_rasterplot_per_neuron.m @@ -115,7 +115,7 @@ 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) + if panel_spikes('IsSpikeEvent', ALL_TRIALS_files(iFile).Events(iEvent).label) labelsForDropDownMenu{end+1} = ALL_TRIALS_files(iFile).Events(iEvent).label; end end diff --git a/toolbox/process/functions/process_spike_field_coherence.m b/toolbox/process/functions/process_spike_field_coherence.m index 8e6b7f57b4..6f500c3733 100644 --- a/toolbox/process/functions/process_spike_field_coherence.m +++ b/toolbox/process/functions/process_spike_field_coherence.m @@ -208,7 +208,7 @@ 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) + if panel_spikes('IsSpikeEvent', everything(iFile).FFTs_single_trial(iNeuron).label) labelsForDropDownMenu{end+1} = everything(iFile).FFTs_single_trial(iNeuron).label; end end @@ -335,7 +335,7 @@ % Important Variable here! 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)); diff --git a/toolbox/process/functions/process_spike_triggered_average.m b/toolbox/process/functions/process_spike_triggered_average.m index a8d8501021..5efa9d6fb1 100644 --- a/toolbox/process/functions/process_spike_triggered_average.m +++ b/toolbox/process/functions/process_spike_triggered_average.m @@ -251,8 +251,8 @@ %% Get meaningful label from neuron name - better_label = process_spikesorting_supervised('GetChannelOfSpikeEvent', labelsForDropDownMenu{iNeuron}); - neuron = process_spikesorting_supervised('GetNeuronOfSpikeEvent', labelsForDropDownMenu{iNeuron}); + better_label = panel_spikes('GetChannelOfSpikeEvent', labelsForDropDownMenu{iNeuron}); + neuron = panel_spikes('GetNeuronOfSpikeEvent', labelsForDropDownMenu{iNeuron}); if ~isempty(neuron) better_label = [better_label ' #' num2str(neuron)]; end @@ -322,7 +322,7 @@ % Important Variable here! 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); for ielectrode = 1: nChannels %selectedChannels diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index f39463b124..95cc80f7f4 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -311,7 +311,7 @@ bst_progress('text', 'Saving events file...'); % Delete existing spike events - process_spikesorting_supervised('DeleteSpikeEvents', sInput.FileName); + panel_spikes('DeleteSpikeEvents', sInput.FileName); % Add events to file sFile.RawFile = sInput.FileName; ImportKilosortEvents(sFile, ChannelMat, fPath, rez); @@ -456,7 +456,7 @@ function ImportKilosortEvents(sFile, ChannelMat, parentPath, rez) % 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'; - spikeEventPrefix = process_spikesorting_supervised('GetSpikesEventPrefix'); + spikeEventPrefix = panel_spikes('GetSpikesEventPrefix'); index = 0; events_spikes = struct(); @@ -487,7 +487,7 @@ function ImportKilosortEvents(sFile, ChannelMat, parentPath, rez) 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 @@ -551,7 +551,7 @@ function ImportKilosortEvents(sFile, ChannelMat, parentPath, rez) 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 diff --git a/toolbox/process/functions/process_spikesorting_supervised.m b/toolbox/process/functions/process_spikesorting_supervised.m deleted file mode 100644 index 7f2e42d68e..0000000000 --- a/toolbox/process/functions/process_spikesorting_supervised.m +++ /dev/null @@ -1,596 +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 + 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 - -function prefix = GetSpikesEventPrefix(varargin) - if length(varargin) < 1 - prefix = 'Spikes Channel'; - else - prefix = {'Spikes Channel', 'Spikes Noise'}; - end -end - -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 - -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 e5da64e092..eb9c17f9af 100644 --- a/toolbox/process/functions/process_spikesorting_ultramegasort2000.m +++ b/toolbox/process/functions/process_spikesorting_ultramegasort2000.m @@ -199,7 +199,7 @@ % ===== IMPORT EVENTS ===== bst_progress('start', 'UltraMegaSort2000', 'Gathering spiking events...'); % Delete existing spike events - process_spikesorting_supervised('DeleteSpikeEvents', sInput.FileName); + panel_spikes('DeleteSpikeEvents', sInput.FileName); % Build output filename NewBstFilePrefix = bst_fullfile(fPath, ['data_0ephys_ums2k_' fBase]); @@ -266,7 +266,7 @@ function SaveBrainstormEvents(SpikeMat, outputFile, eventNamePrefix) 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 @@ -277,7 +277,7 @@ function SaveBrainstormEvents(SpikeMat, outputFile, eventNamePrefix) end for iElectrode = 1:numElectrodes - newEvents = process_spikesorting_supervised(... + newEvents = panel_spikes(... 'CreateSpikeEvents', ... SpikeMat.RawFile, ... SpikeMat.Device, ... diff --git a/toolbox/process/functions/process_spikesorting_waveclus.m b/toolbox/process/functions/process_spikesorting_waveclus.m index 5b783be14f..698a9d8dca 100644 --- a/toolbox/process/functions/process_spikesorting_waveclus.m +++ b/toolbox/process/functions/process_spikesorting_waveclus.m @@ -198,7 +198,7 @@ % ===== IMPORT EVENTS ===== bst_progress('text', 'Saving events file...'); % Delete existing spike events - process_spikesorting_supervised('DeleteSpikeEvents', sInput.FileName); + panel_spikes('DeleteSpikeEvents', sInput.FileName); % Build output filename NewBstFilePrefix = bst_fullfile(fPath, ['data_0ephys_wclus_' fBase]); @@ -263,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 @@ -274,7 +274,7 @@ function SaveBrainstormEvents(sFile, outputFile, eventNamePrefix) end for iElectrode = 1:numElectrodes - newEvents = process_spikesorting_supervised(... + newEvents = panel_spikes(... 'CreateSpikeEvents', ... sFile.RawFile, ... sFile.Device, ... diff --git a/toolbox/process/functions/process_spiking_phase_locking.m b/toolbox/process/functions/process_spiking_phase_locking.m index 519d3d628d..16a4ec7498 100644 --- a/toolbox/process/functions/process_spiking_phase_locking.m +++ b/toolbox/process/functions/process_spiking_phase_locking.m @@ -135,7 +135,7 @@ 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) + if panel_spikes('IsSpikeEvent', ALL_TRIALS_files(iFile).Events(iEvent).label) neuronLabels{end+1} = ALL_TRIALS_files(iFile).Events(iEvent).label; 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/tree/tree_callbacks.m b/toolbox/tree/tree_callbacks.m index 057999056a..e5592b178e 100644 --- a/toolbox/tree/tree_callbacks.m +++ b/toolbox/tree/tree_callbacks.m @@ -305,7 +305,7 @@ % ===== SPIKES ===== case 'spike' - bst_process('CallProcess', 'process_spikesorting_supervised', filenameRelative, []); + panel_spikes('OpenSpikeFile', filenameRelative); % ===== TIME-FREQUENCY ===== case {'timefreq', 'ptimefreq'} @@ -1832,7 +1832,7 @@ %% ===== POPUP: SPIKE ===== case 'spike' - gui_component('MenuItem', jPopup, [], 'Supervised spike sorting', IconLoader.ICON_SPIKE_SORTING, [], @(h,ev)bst_process('CallProcess', 'process_spikesorting_supervised', filenameRelative, [])); + gui_component('MenuItem', jPopup, [], 'Supervised spike sorting', IconLoader.ICON_SPIKE_SORTING, [], @(h,ev)panel_spikes('OpenSpikeFile', filenameRelative)); %% ===== POPUP: TIME-FREQ ===== case {'timefreq', 'ptimefreq'} From ae9cd964dfa8decd874ec1544dcfe43690aed2d7 Mon Sep 17 00:00:00 2001 From: ftadel Date: Tue, 26 Apr 2022 17:32:04 +0200 Subject: [PATCH 39/43] Reformatted all the advanced functions --- .../functions/process_convert_raw_to_lfp.m | 2 +- .../functions/process_noise_correlation.m | 195 ++++------ .../functions/process_psth_per_electrode.m | 210 ++++------- .../functions/process_psth_per_neuron.m | 206 ++++------- .../functions/process_rasterplot_per_neuron.m | 207 ++++------- .../functions/process_spike_field_coherence.m | 324 ++++++----------- .../process_spike_triggered_average.m | 339 ++++++------------ .../functions/process_spiking_phase_locking.m | 323 +++++++---------- .../process/functions/process_tuning_curves.m | 183 +++++----- 9 files changed, 737 insertions(+), 1252 deletions(-) diff --git a/toolbox/process/functions/process_convert_raw_to_lfp.m b/toolbox/process/functions/process_convert_raw_to_lfp.m index 5cba549a8d..5361bc2c55 100644 --- a/toolbox/process/functions/process_convert_raw_to_lfp.m +++ b/toolbox/process/functions/process_convert_raw_to_lfp.m @@ -35,7 +35,7 @@ sProcess.Comment = 'Convert Raw to LFP'; 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'}; diff --git a/toolbox/process/functions/process_noise_correlation.m b/toolbox/process/functions/process_noise_correlation.m index 90f331f232..044e7e262d 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 panel_spikes('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(neuronLabels) + 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_electrode.m b/toolbox/process/functions/process_psth_per_electrode.m index 1e27561ab2..434f43aebe 100644 --- a/toolbox/process/functions/process_psth_per_electrode.m +++ b/toolbox/process/functions/process_psth_per_electrode.m @@ -5,9 +5,6 @@ % 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: @@ -27,30 +24,27 @@ % For more information type "brainstorm license" at command prompt. % =============================================================================@ % -% Author: Konstantinos Nasiotis, 2018-2019; +% 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 = 'PSTH Per Electrode'; + sProcess.Comment = 'PSTH per electrode'; sProcess.FileTag = 'raster'; - sProcess.Category = 'custom'; + sProcess.Category = 'File'; sProcess.SubGroup = 'Electrophysiology'; - sProcess.Index = 1505; - sProcess.Description = 'https://neuroimage.usc.edu/brainstorm/e-phys/functions#Raster_Plots'; + 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: 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'; @@ -59,153 +53,93 @@ %% ===== 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) % 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 - + % ==== 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, sInputs, 'Positive bin size required.'); + bst_report('Error', sProcess, sInput, '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 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(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) + 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)) - % Bin ONLY THE FIRST NEURON'S SPIKES if there are multiple neurons! - if panel_spikes('IsSpikeEvent', trial.Events(ievent).label) ... - && panel_spikes('IsFirstNeuron', trial.Events(ievent).label) ... - && strcmp(ChannelMat.Channel(ielectrode).Name, panel_spikes('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 + 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 - - - % 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 + % 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; - - % Display report to user - bst_report('Info', sProcess, sInputs, 'Success'); - disp('BST> process_timefreq: Success'); + % 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_neuron.m b/toolbox/process/functions/process_psth_per_neuron.m index 9950c00c14..076fec1da3 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 panel_spikes('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(neuronLabels) + 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 90d9b45e54..e83a32254f 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 panel_spikes('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(neuronLabels) + 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 6f500c3733..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 panel_spikes('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) 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 5efa9d6fb1..14de1da56b 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,321 +21,220 @@ % =============================================================================@ % % 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 = []; + % Get options + isParallel = sProcess.options.parallel.Value; + TimeWindow = sProcess.options.timewindow.Value{1}; + + % ===== LOAD INPUTS ===== + % Loads all the data outside of the parfor, so it doesn't fail + nTrials = length(sInputs); + 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 end - - tfOPTIONS.TimeVector = in_bst(sInputs(1).FileName, 'Time'); - - if sProcess.options.timewindow.Value{1}(1)>=0 || sProcess.options.timewindow.Value{1}(2)<=0 + % Check time window + if TimeWindow(1)>=0 || TimeWindow(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'); + 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 - - - % === OUTPUT STUDY === - % Get output study - [tmp, iStudy] = bst_process('GetOutputStudy', sProcess, sInputs); - tfOPTIONS.iTargetStudy = iStudy; - - % Get channel file - sChannel = bst_get('ChannelForStudy', iStudy); + % Sampling frequency + sampling_rate = round(abs(1. / (DataMats{1}.Time(2) - DataMats{1}.Time(1)))); % Load channel file - ChannelMat = in_bst_channel(sChannel.FileName); + ChannelMat = in_bst_channel(sInputs(1).ChannelFile); % === 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 - - - 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 = []; - 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); - end - - - % Optimize this - if ~isempty(poolobj) + % 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, % and a 2D image with the other two dimensions will appear, showing the % 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).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; + all_labels = cell(length(LFP_trials{iFile}), nTrials); + 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)); + + iEvents = zeros(nTrials,1); + for iFile = 1:nTrials + 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:nTrials + 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))); +% std_single_neuron = sqrt(std_single_neuron./(divideBy - size(all_labels,2))); - - %% Get meaningful label from neuron name - better_label = panel_spikes('GetChannelOfSpikeEvent', labelsForDropDownMenu{iNeuron}); - neuron = panel_spikes('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}; - % Prepare output file structure - 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(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; + % ===== SAVE FILE ===== + % Prepare output file structure + 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 = divideBy; + FileMat.History = DataMats{1}.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) panel_spikes('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 +242,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_spiking_phase_locking.m b/toolbox/process/functions/process_spiking_phase_locking.m index 16a4ec7498..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,199 +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 panel_spikes('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; + + %% ===== 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; - centerOfBins = EDGES(1:end-1) + (pi/180*sProcess.options.phaseBin.Value{1})/2; - - progressPos = bst_progress('set',0); - bst_progress('text', 'Accumulating spiking phases for each neuron...'); 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)); - - angle_filtered_F = zeros(size(filtered_F)); - for iChannel = 1:size(filtered_F,1) - angle_filtered_F(iChannel,:) = angle(hilbert(filtered_F(iChannel,:))); - end + % 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 + for iNeuron = 1:length(neuronLabels) + iEvent_Neuron = find(ismember({events.label},neuronLabels{iNeuron})); - 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).timesDataMat.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),'*') + % =============================================================== - % Get the index of the closest timeBin - [temp, iClosest] = histc(events(iEvent_Neuron).times,DataMat.Time); + % 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,edges] = histcounts(angle_filtered_F(:,iClosest)',EDGES); -%% % ADD A TEST HERE FOR VERIFICATION THE CODE WORKS -% iClosest = angle_filtered_F(1,:)<0 & angle_filtered_F(1,:)>-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; - end - end + 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; + 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)); @@ -256,67 +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 = []; - - FileMat.neurons.phase.pValues = pValues; - FileMat.neurons.phase.preferredPhase = preferredPhase; - FileMat.neurons.phase.total_spikes = total_spikes; + 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) @@ -328,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 db571358f4..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,124 +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 + 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 From d55a3b8ac67ea59be2ff4951357afaf7ad729c44 Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 29 Apr 2022 16:29:41 +0200 Subject: [PATCH 40/43] Reporting last changes from @pompolas PR #13 https://github.com/mpompolas/brainstorm3/pull/13 --- toolbox/process/functions/process_convert_raw_to_lfp.m | 9 +++++---- toolbox/process/functions/process_noise_correlation.m | 2 +- ...psth_per_electrode.m => process_psth_per_channel.m} | 10 +++++----- toolbox/process/functions/process_psth_per_neuron.m | 2 +- .../process/functions/process_rasterplot_per_neuron.m | 2 +- .../functions/process_spike_triggered_average.m | 4 ++-- .../process/functions/process_spikesorting_kilosort.m | 7 ++++++- 7 files changed, 21 insertions(+), 15 deletions(-) rename toolbox/process/functions/{process_psth_per_electrode.m => process_psth_per_channel.m} (94%) diff --git a/toolbox/process/functions/process_convert_raw_to_lfp.m b/toolbox/process/functions/process_convert_raw_to_lfp.m index 5361bc2c55..5644cb8d43 100644 --- a/toolbox/process/functions/process_convert_raw_to_lfp.m +++ b/toolbox/process/functions/process_convert_raw_to_lfp.m @@ -172,7 +172,7 @@ % Template structure for the creation of the output raw file sFileTemplate = sFileIn; sFileTemplate.prop.sfreq = LFP_fs; - sFileTemplate.prop.times = [0, (nTimeOut-1) ./ LFP_fs]; + sFileTemplate.prop.times = [newTimeVector(1), newTimeVector(end)]; sFileTemplate.header.sfreq = LFP_fs; sFileTemplate.header.nsamples = nTimeOut; % Convert events to new sampling rate @@ -231,7 +231,8 @@ % Get channel name from electrode file name [tmp, ChannelName] = fileparts(ElecFile); ChannelName = strrep(ChannelName, 'raw_elec_', ''); - data = BayesianSpikeRemoval(ChannelName, data, sr, sFileIn, ChannelMat, cleanChannelNames); + 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); @@ -242,7 +243,7 @@ %% ===== BAYESIAN SPIKE REMOVAL ===== % Reference: https://www.ncbi.nlm.nih.gov/pubmed/21068271 -function data_derived = BayesianSpikeRemoval(ChannelName, data, Fs, sFile, ChannelMat, cleanChannelNames) +function data_derived = BayesianSpikeRemoval(ChannelName, data, Fs, sFile, ChannelMat, cleanChannelNames, BandPass) % Assume that a spike lasts 3ms nSegment = round(Fs * 0.003); Bs = eye(nSegment); % 60x60 @@ -275,7 +276,7 @@ % from spktimes to obtain the start times of the spikes if mod(length(data),2)~=0 - data_temp = [data;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); diff --git a/toolbox/process/functions/process_noise_correlation.m b/toolbox/process/functions/process_noise_correlation.m index 044e7e262d..33f3c723a4 100644 --- a/toolbox/process/functions/process_noise_correlation.m +++ b/toolbox/process/functions/process_noise_correlation.m @@ -78,7 +78,7 @@ end end % If no neuron was found - if isempty(neuronLabels) + if isempty(uniqueNeurons) bst_report('Error', sProcess, sCurrentInputs(1), 'No neurons/spiking events detected.'); return; end diff --git a/toolbox/process/functions/process_psth_per_electrode.m b/toolbox/process/functions/process_psth_per_channel.m similarity index 94% rename from toolbox/process/functions/process_psth_per_electrode.m rename to toolbox/process/functions/process_psth_per_channel.m index 434f43aebe..5d692fc0e1 100644 --- a/toolbox/process/functions/process_psth_per_electrode.m +++ b/toolbox/process/functions/process_psth_per_channel.m @@ -1,8 +1,8 @@ -function varargout = process_psth_per_electrode( varargin ) -% PROCESS_PSTH_PER_ELECTRODE: Computes the PSTH per electrode. +function varargout = process_psth_per_channel( varargin ) +% PROCESS_PSTH_PER_CHANNEL: Computes the PSTH per channel. -% 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 +% 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. @@ -34,7 +34,7 @@ %% ===== GET DESCRIPTION ===== function sProcess = GetDescription() % Description the process - sProcess.Comment = 'PSTH per electrode'; + sProcess.Comment = 'PSTH per channel'; sProcess.FileTag = 'raster'; sProcess.Category = 'File'; sProcess.SubGroup = 'Electrophysiology'; diff --git a/toolbox/process/functions/process_psth_per_neuron.m b/toolbox/process/functions/process_psth_per_neuron.m index 076fec1da3..a54623cc7d 100644 --- a/toolbox/process/functions/process_psth_per_neuron.m +++ b/toolbox/process/functions/process_psth_per_neuron.m @@ -92,7 +92,7 @@ end end % If no neuron was found - if isempty(neuronLabels) + if isempty(labelsNeurons) bst_report('Error', sProcess, sCurrentInputs(1), 'No neurons/spiking events detected.'); return; end diff --git a/toolbox/process/functions/process_rasterplot_per_neuron.m b/toolbox/process/functions/process_rasterplot_per_neuron.m index e83a32254f..ee6ec97187 100644 --- a/toolbox/process/functions/process_rasterplot_per_neuron.m +++ b/toolbox/process/functions/process_rasterplot_per_neuron.m @@ -82,7 +82,7 @@ end end % If no neuron was found - if isempty(neuronLabels) + if isempty(labelsNeurons) bst_report('Error', sProcess, sCurrentInputs(1), 'No neurons/spiking events detected.'); return; end diff --git a/toolbox/process/functions/process_spike_triggered_average.m b/toolbox/process/functions/process_spike_triggered_average.m index 14de1da56b..5e29d43088 100644 --- a/toolbox/process/functions/process_spike_triggered_average.m +++ b/toolbox/process/functions/process_spike_triggered_average.m @@ -165,7 +165,7 @@ end end % Divide by total number of averages - STA_single_neuron = (STA_single_neuron./divideBy)'; + 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 @@ -179,7 +179,7 @@ % ===== SAVE FILE ===== % Prepare output file structure FileMat = db_template('datamat'); - FileMat.F = STA_single_neuron'; + 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 ')']; diff --git a/toolbox/process/functions/process_spikesorting_kilosort.m b/toolbox/process/functions/process_spikesorting_kilosort.m index 95cc80f7f4..18c051db76 100644 --- a/toolbox/process/functions/process_spikesorting_kilosort.m +++ b/toolbox/process/functions/process_spikesorting_kilosort.m @@ -496,7 +496,12 @@ function ImportKilosortEvents(sFile, ChannelMat, parentPath, rez) index = index + 1; end end - events = [events events_spikes]; + + if ~isempty(existingEvents) + events = [events events_spikes]; + else + events = events_spikes; + end save(fullfile(parentPath,'events_UNSUPERVISED.mat'),'events') From f43d81e0214c0dcf36bd4436dd812e15540cdd8d Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 29 Apr 2022 16:32:51 +0200 Subject: [PATCH 41/43] Added tutorial script --- toolbox/script/tutorial_ephys.m | 256 ++++++++++++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 toolbox/script/tutorial_ephys.m diff --git a/toolbox/script/tutorial_ephys.m b/toolbox/script/tutorial_ephys.m new file mode 100644 index 0000000000..2a1aa89437 --- /dev/null +++ b/toolbox/script/tutorial_ephys.m @@ -0,0 +1,256 @@ +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'); +% Process: Snapshot: Sensors/MRI registration +bst_process('CallProcess', 'process_snapshot', sFilesRaw, [], ... + 'type', 'registration', ... % Sensors/MRI registration + 'modality', 6, ... % SEEG + 'orient', 1); % left + +% 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); + + + From e1637e19e0e6e3d694d4c8c5c2637274c88314b1 Mon Sep 17 00:00:00 2001 From: ftadel Date: Fri, 29 Apr 2022 17:12:57 +0200 Subject: [PATCH 42/43] Bugfix: Errors in reformatting process_spike_triggered_average --- .../process_spike_triggered_average.m | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/toolbox/process/functions/process_spike_triggered_average.m b/toolbox/process/functions/process_spike_triggered_average.m index 5e29d43088..28d187d4ac 100644 --- a/toolbox/process/functions/process_spike_triggered_average.m +++ b/toolbox/process/functions/process_spike_triggered_average.m @@ -65,7 +65,8 @@ % Get options isParallel = sProcess.options.parallel.Value; TimeWindow = sProcess.options.timewindow.Value{1}; - + + % ===== LOAD INPUTS ===== % Loads all the data outside of the parfor, so it doesn't fail nTrials = length(sInputs); @@ -91,7 +92,7 @@ sampling_rate = round(abs(1. / (DataMats{1}.Time(2) - DataMats{1}.Time(1)))); % Load channel file ChannelMat = in_bst_channel(sInputs(1).ChannelFile); - + % === START COMPUTATION === % Input time window @@ -107,8 +108,8 @@ LFP_trials{iFile} = get_LFPs(DataMats{iFile}, ChannelMat, TimeWindow, time_segmentAroundSpikes, sampling_rate); end end - - + + % ===== COMPUTE SPIKE TRIGGERED AVERAGE ===== % The Spike Triggered Average should be a 3d matrix % Number of neurons x Frequencies x Electrodes @@ -116,12 +117,12 @@ % and a 2D image with the other two dimensions will appear, showing the % 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 = {}; labelsNeurons = {}; % Unique neuron labels (each trial might have different number of neurons). We need everything that appears. for iFile = 1:nTrials - all_labels = cell(length(LFP_trials{iFile}), nTrials); for iNeuron = 1:length(LFP_trials{iFile}) all_labels{iNeuron,iFile} = LFP_trials{iFile}(iNeuron).label; labelsNeurons{end+1} = LFP_trials{iFile}(iNeuron).label; @@ -137,9 +138,9 @@ logicalEvents(ii,jj) = strcmp(all_labels{ii,jj}, labelsNeurons{iNeuron}); end end - - iEvents = zeros(nTrials,1); - for iFile = 1:nTrials + + iEvents = zeros(size(all_labels,2),1); + for iFile = 1:size(all_labels,2) temp = find(logicalEvents(:,iFile)); if ~isempty(temp) iEvents(iFile) = temp; @@ -147,12 +148,12 @@ 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)); divideBy = 0; - for iFile = 1:nTrials + 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; @@ -165,9 +166,10 @@ 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))); + 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 = panel_spikes('GetChannelOfSpikeEvent', labelsNeurons{iNeuron}); neuron = panel_spikes('GetNeuronOfSpikeEvent', labelsNeurons{iNeuron}); @@ -179,16 +181,16 @@ % ===== SAVE FILE ===== % Prepare output file structure FileMat = db_template('datamat'); - FileMat.F = STA_single_neuron; + 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.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 = divideBy; + FileMat.nAvg = 1; FileMat.History = DataMats{1}.History; - + % Add history field FileMat = bst_history('add', FileMat, 'compute', ['Spike Triggered Average: [' num2str(TimeWindow(1)) ', ' num2str(TimeWindow(2)) '] ms']); for iFile = 1:length(sInputs) From 9779e6b834b0648af58979378b07d3d70f2a9acb Mon Sep 17 00:00:00 2001 From: ftadel Date: Sat, 30 Apr 2022 10:33:10 +0200 Subject: [PATCH 43/43] Removed double registration display --- toolbox/script/tutorial_ephys.m | 5 ----- 1 file changed, 5 deletions(-) diff --git a/toolbox/script/tutorial_ephys.m b/toolbox/script/tutorial_ephys.m index 2a1aa89437..612ebb02b7 100644 --- a/toolbox/script/tutorial_ephys.m +++ b/toolbox/script/tutorial_ephys.m @@ -103,11 +103,6 @@ function tutorial_ephys(tutorial_dir) bst_process('CallProcess', 'process_channel_settype', sFilesRaw, [], ... 'sensortypes', '', ... 'newtype', 'SEEG'); -% Process: Snapshot: Sensors/MRI registration -bst_process('CallProcess', 'process_snapshot', sFilesRaw, [], ... - 'type', 'registration', ... % Sensors/MRI registration - 'modality', 6, ... % SEEG - 'orient', 1); % left % Display anatomy and sensors hFig = view_channels_3d(sFilesRaw.ChannelFile, 'SEEG', 'scalp', 1);