function [freq] = ft_freqanalysis(cfg, data)
% FT_FREQANALYSIS performs frequency and time-frequency analysis
% on time series data over multiple trials
%
% Use as
% [freq] = ft_freqanalysis(cfg, data)
%
% The input data should be organised in a structure as obtained from
% the FT_PREPROCESSING or the FT_MVARANALYSIS function. The configuration
% depends on the type of computation that you want to perform.
%
% The configuration should contain:
% cfg.method = different methods of calculating the spectra
% 'mtmfft', analyses an entire spectrum for the entire data
% length, implements multitaper frequency transformation
% 'mtmconvol', implements multitaper time-frequency
% transformation based on multiplication in the frequency
% domain.
% 'wavelet', implements wavelet time frequency
% transformation (using Morlet wavelets) based on
% multiplication in the frequency domain.
% 'tfr', implements wavelet time frequency transformation
% (using Morlet wavelets) based on convolution in the
% time domain.
% 'mvar', does a fourier transform on the coefficients of
% an estimated multivariate autoregressive model,
% obtained with FT_MVARANALYSIS. In this case, the
% output will contain a spectral transfer matrix,
% the cross-spectral density matrix, and the
% covariance matrix of the innovatio noise.
% cfg.output = 'pow' return the power-spectra
% 'powandcsd' return the power and the cross-spectra
% 'fourier' return the complex Fourier-spectra
% cfg.channel = Nx1 cell-array with selection of channels (default = 'all'),
% see FT_CHANNELSELECTION for details
% cfg.channelcmb = Mx2 cell-array with selection of channel pairs (default = {'all' 'all'}),
% see FT_CHANNELCOMBINATION for details
% cfg.trials = 'all' or a selection given as a 1xN vector (default = 'all')
% cfg.keeptrials = 'yes' or 'no', return individual trials or average (default = 'no')
% cfg.keeptapers = 'yes' or 'no', return individual tapers or average (default = 'no')
% cfg.pad = number or 'maxperlen', length in seconds to which the
% data can be padded out (default = 'maxperlen') The
% padding will determine your spectral resolution. If
% you want to compare spectra from data pieces of
% different lengths, you should use the same cfg.pad
% for both, in order to spectrally interpolate them to
% the same spectral resolution. Note that this will
% run very slow if you specify cfg.pad as maxperlen
% AND the number of samples turns out to have a large
% prime factor sum. This is because the FFTs will then
% be computed very inefficiently.
% cfg.padtype = string, type of padding (default 'zero', see
% ft_preproc_padding)
% cfg.polyremoval = number (default = 0), specifying the order of the
% polynome which is fitted and subtracted from the
% time domain data prior to the spectral analysis. For example, a
% value of 1 corresponds to a linear trend. The default is a mean
% subtraction, thus a value of 0. If no removal is requested,
% specify -1.
% see FT_PREPROC_POLYREMOVAL for details
%
%
% METHOD SPECIFIC OPTIONS AND DESCRIPTIONS
%
% MTMFFT
% MTMFFT performs frequency analysis on any time series
% trial data using the 'multitaper method' (MTM) based on discrete
% prolate spheroidal sequences (Slepian sequences) as tapers. Alternatively,
% you can use conventional tapers (e.g. Hanning).
% cfg.foilim = [begin end], frequency band of interest
% OR
% cfg.foi = vector 1 x numfoi, frequencies of interest
% cfg.tapsmofrq = number, the amount of spectral smoothing through
% multi-tapering. Note that 4 Hz smoothing means
% plus-minus 4 Hz, i.e. a 8 Hz smoothing box.
% cfg.taper = 'dpss', 'hanning' or many others, see WINDOW (default = 'dpss')
% For cfg.output='powandcsd', you should specify the channel combinations
% between which to compute the cross-spectra as cfg.channelcmb. Otherwise
% you should specify only the channels in cfg.channel.
%
% MTMCONVOL
% MTMCONVOL performs time-frequency analysis on any time series trial data
% using the 'multitaper method' (MTM) based on Slepian sequences as tapers. Alternatively,
% you can use conventional tapers (e.g. Hanning).
% cfg.tapsmofrq = vector 1 x numfoi, the amount of spectral smoothing through
% multi-tapering. Note that 4 Hz smoothing means
% plus-minus 4 Hz, i.e. a 8 Hz smoothing box.
% cfg.foi = vector 1 x numfoi, frequencies of interest
% cfg.taper = 'dpss', 'hanning' or many others, see WINDOW (default = 'dpss')
% For cfg.output='powandcsd', you should specify the channel combinations
% between which to compute the cross-spectra as cfg.channelcmb. Otherwise
% you should specify only the channels in cfg.channel.
% cfg.t_ftimwin = vector 1 x numfoi, length of time window (in seconds)
% cfg.toi = vector 1 x numtoi, the times on which the analysis windows
% should be centered (in seconds)
%
% WAVELET
% WAVELET performs time-frequency analysis on any time series trial data
% using the 'wavelet method' based on Morlet wavelets. Using mulitplication
% in the frequency domain instead of convolution in the time domain.
% cfg.foi = vector 1 x numfoi, frequencies of interest
% OR
% cfg.foilim = [begin end], frequency band of interest
% cfg.toi = vector 1 x numtoi, the times on which the analysis windows
% should be centered (in seconds)
% cfg.width = 'width', or number of cycles, of the wavelet (default = 7)
% cfg.gwidth = determines the length of the used wavelets in standard deviations
% of the implicit Gaussian kernel and should be choosen
% >= 3; (default = 3)
%
% The standard deviation in the frequency domain (sf) at frequency f0 is
% defined as: sf = f0/width
% The standard deviation in the temporal domain (st) at frequency f0 is
% defined as: st = 1/(2*pi*sf)
%
%
% TFR
% TFR performs time-frequency analysis on any time series trial data
% using the 'wavelet method' based on Morlet wavelets. Using convolution
% in the time domain instead of multiplication in the frequency domain.
% cfg.foi = vector 1 x numfoi, frequencies of interest
% OR
% cfg.foilim = [begin end], frequency band of interest
% cfg.width = 'width', or number of cycles, of the wavelet (default = 7)
% cfg.gwidth = determines the length of the used wavelets in standard deviations
% of the implicit Gaussian kernel and should be choosen
% >= 3; (default = 3)
%
%
%
% To facilitate data-handling and distributed computing you can use
% cfg.inputfile = ...
% cfg.outputfile = ...
% If you specify one of these (or both) the input data will be read from a *.mat
% file on disk and/or the output data will be written to a *.mat file. These mat
% files should contain only a single variable, corresponding with the
% input/output structure.
%
% See also
% Guidelines for use in an analysis pipeline:
% after FT_FREQANALYSIS you will have frequency or time-frequency
% representations (TFRs) of the data, represented as power-spectra,
% power and cross-spectra, or complex fourier-spectra, either for individual
% trials or an average over trials.
% This usually serves as input for one of the following functions:
% * FT_FREQDESCRIPTIVES to compute descriptive univariate statistics
% * FT_FREQSTATISTICS to perform parametric or non-parametric statistical tests
% * FT_FREQBASELINE to perform baseline normalization of the spectra
% * FT_FREQGRANDAVERAGE to compute the average spectra over multiple subjects or datasets
% * FT_CONNECTIVITYANALYSIS to compute various measures of connectivity
% Furthermore, the data can be visualised using the various plotting
% functions, including:
% * FT_SINGLEPLOTTFR to plot the TFR of a single channel or the average over multiple channels
% * FT_TOPOPLOTTFR to plot the topographic distribution over the head
% * FT_MULTIPLOTTFR to plot TFRs in a topographical layout
% Undocumented local options:
% cfg.method = 'hilbert'. Keeping this as undocumented as it does not make sense to use
% in ft_freqanalysis unless the user is doing his own filter-padding
% to remove edge-artifacts
% cfg.correctt_ftimwin (set to yes to try to determine new t_ftimwins based
% on correct cfg.foi)
% Copyright (C) 2003-2006, F.C. Donders Centre, Pascal Fries
% Copyright (C) 2004-2006, F.C. Donders Centre, Markus Siegel
% Copyright (C) 2007-2012, DCCN, The FieldTrip team
%
% This file is part of FieldTrip, see http://www.ru.nl/neuroimaging/fieldtrip
% for the documentation and details.
%
% FieldTrip is free software: you can redistribute it and/or modify
% it under the terms of the GNU General Public License as published by
% the Free Software Foundation, either version 3 of the License, or
% (at your option) any later version.
%
% FieldTrip is distributed in the hope that it will be useful,
% but WITHOUT ANY WARRANTY; without even the implied warranty of
% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
% GNU General Public License for more details.
%
% You should have received a copy of the GNU General Public License
% along with FieldTrip. If not, see .
revision = '$Id: ft_freqanalysis.m 10202 2015-02-11 14:15:07Z jansch $';
% do the general setup of the function
ft_defaults
ft_preamble init
ft_preamble provenance
ft_preamble trackconfig
ft_preamble debug
ft_preamble loadvar data
% the abort variable is set to true or false in ft_preamble_init
if abort
return
end
% ensure that the required options are present
cfg.feedback = ft_getopt(cfg, 'feedback', 'text');
cfg.inputlock = ft_getopt(cfg, 'inputlock', []); % this can be used as mutex when doing distributed computation
cfg.outputlock = ft_getopt(cfg, 'outputlock', []); % this can be used as mutex when doing distributed computation
cfg.trials = ft_getopt(cfg, 'trials', 'all', 1);
cfg.channel = ft_getopt(cfg, 'channel', 'all');
% check if the input data is valid for this function
data = ft_checkdata(data, 'datatype', {'raw', 'raw+comp', 'mvar'}, 'feedback', cfg.feedback, 'hassampleinfo', 'yes');
% check if the input cfg is valid for this function
cfg = ft_checkconfig(cfg, 'renamed', {'label', 'channel'});
cfg = ft_checkconfig(cfg, 'renamed', {'sgn', 'channel'});
cfg = ft_checkconfig(cfg, 'renamed', {'labelcmb', 'channelcmb'});
cfg = ft_checkconfig(cfg, 'renamed', {'sgncmb', 'channelcmb'});
cfg = ft_checkconfig(cfg, 'required', {'method'});
cfg = ft_checkconfig(cfg, 'renamedval', {'method', 'fft', 'mtmfft'});
cfg = ft_checkconfig(cfg, 'renamedval', {'method', 'convol', 'mtmconvol'});
cfg = ft_checkconfig(cfg, 'forbidden', {'latency'}); % see bug 1376 and 1076
cfg = ft_checkconfig(cfg, 'renamedval', {'method', 'wltconvol', 'wavelet'});
% select trials of interest
tmpcfg = [];
tmpcfg.trials = cfg.trials;
tmpcfg.channel = cfg.channel;
data = ft_selectdata(tmpcfg, data);
% restore the provenance information
[cfg, data] = rollback_provenance(cfg, data);
% some proper error handling
if isfield(data, 'trial') && numel(data.trial)==0
error('no trials were selected'); % this does not apply for MVAR data
end
if numel(data.label)==0
error('no channels were selected');
end
% switch over method and do some of the method specfic checks and defaulting
switch cfg.method
case 'mtmconvol'
cfg.taper = ft_getopt(cfg, 'taper', 'dpss');
if isequal(cfg.taper, 'dpss') && ~isfield(cfg, 'tapsmofrq')
error('you must specify a smoothing parameter with taper = dpss');
end
% check for foi above Nyquist
if isfield(cfg,'foi')
if any(cfg.foi > (data.fsample/2))
error('frequencies in cfg.foi are above Nyquist')
end
if isequal(cfg.taper, 'dpss') && not(isfield(cfg, 'tapsmofrq'))
error('you must specify a smoothing parameter with taper = dpss');
end
end
cfg = ft_checkconfig(cfg, 'required', 'toi');
if ischar(cfg.toi) && strcmp(cfg.toi, 'all')
% do an educated guess with respect to the requested time bins
begtim = min(cellfun(@min,data.time));
endtim = max(cellfun(@max,data.time));
cfg.toi = linspace(begtim,endtim,round((endtim-begtim)./mean(diff(data.time{1})))+1);
elseif ischar(cfg.toi)
error('cfg.toi should be either a numeric vector, or can be ''all''');
end
case 'mtmfft'
cfg.taper = ft_getopt(cfg, 'taper', 'dpss');
if isequal(cfg.taper, 'dpss') && not(isfield(cfg, 'tapsmofrq'))
error('you must specify a smoothing parameter with taper = dpss');
end
% check for foi above Nyquist
if isfield(cfg,'foi')
if any(cfg.foi > (data.fsample/2))
error('frequencies in cfg.foi are above Nyquist')
end
end
if isequal(cfg.taper, 'dpss') && not(isfield(cfg, 'tapsmofrq'))
error('you must specify a smoothing parameter with taper = dpss');
end
case 'wavelet'
cfg.width = ft_getopt(cfg, 'width', 7);
cfg.gwidth = ft_getopt(cfg, 'gwidth', 3);
case 'tfr'
cfg = ft_checkconfig(cfg, 'renamed', {'waveletwidth', 'width'});
cfg = ft_checkconfig(cfg, 'unused', {'downsample'});
cfg.width = ft_getopt(cfg, 'width', 7);
cfg.gwidth = ft_getopt(cfg, 'gwidth', 3);
case 'hilbert'
warning('method = hilbert requires user action to deal with filtering-artifacts')
if ~isfield(cfg, 'filttype'), cfg.filttype = 'but'; end
if ~isfield(cfg, 'filtorder'), cfg.filtorder = 4; end
if ~isfield(cfg, 'filtdir'), cfg.filtdir = 'twopass'; end
if ~isfield(cfg, 'width'), cfg.width = 1; end
case 'mvar'
if isfield(cfg, 'inputfile')
freq = feval(@ft_freqanalysis_mvar,cfg);
else
freq = feval(@ft_freqanalysis_mvar,cfg,data);
end
return
otherwise
error('specified cfg.method is not supported')
end
% set all the defaults
cfg.pad = ft_getopt(cfg, 'pad', 'maxperlen');
cfg.padtype = ft_getopt(cfg, 'padtype', 'zero');
cfg.output = ft_getopt(cfg, 'output', 'pow');
cfg.calcdof = ft_getopt(cfg, 'calcdof', 'no');
cfg.channel = ft_getopt(cfg, 'channel', 'all');
cfg.precision = ft_getopt(cfg, 'precision', 'double');
cfg.foi = ft_getopt(cfg, 'foi', []);
cfg.foilim = ft_getopt(cfg, 'foilim', []);
cfg.correctt_ftimwin = ft_getopt(cfg, 'correctt_ftimwin', 'no');
cfg.polyremoval = ft_getopt(cfg, 'polyremoval', 0);
% keeptrials and keeptapers should be conditional on cfg.output,
% cfg.output = 'fourier' should always output tapers
if strcmp(cfg.output, 'fourier')
cfg.keeptrials = ft_getopt(cfg, 'keeptrials', 'yes');
cfg.keeptapers = ft_getopt(cfg, 'keeptapers', 'yes');
if strcmp(cfg.keeptrials, 'no') || strcmp(cfg.keeptapers, 'no'),
error('cfg.output = ''fourier'' requires cfg.keeptrials = ''yes'' and cfg.keeptapers = ''yes''');
end
else
cfg.keeptrials = ft_getopt(cfg, 'keeptrials', 'no');
cfg.keeptapers = ft_getopt(cfg, 'keeptapers', 'no');
end
% set flags for keeping trials and/or tapers
if strcmp(cfg.keeptrials,'no') && strcmp(cfg.keeptapers,'no')
keeprpt = 1;
elseif strcmp(cfg.keeptrials,'yes') && strcmp(cfg.keeptapers,'no')
keeprpt = 2;
elseif strcmp(cfg.keeptrials,'no') && strcmp(cfg.keeptapers,'yes')
error('There is currently no support for keeping tapers WITHOUT KEEPING TRIALS.');
elseif strcmp(cfg.keeptrials,'yes') && strcmp(cfg.keeptapers,'yes')
keeprpt = 4;
end
if strcmp(cfg.keeptrials,'yes') && strcmp(cfg.keeptapers,'yes')
if ~strcmp(cfg.output, 'fourier'),
error('Keeping trials AND tapers is only possible with fourier as the output.');
end
end
% Set flags for output
if strcmp(cfg.output,'pow')
powflg = 1;
csdflg = 0;
fftflg = 0;
elseif strcmp(cfg.output,'powandcsd')
powflg = 1;
csdflg = 1;
fftflg = 0;
elseif strcmp(cfg.output,'fourier')
powflg = 0;
csdflg = 0;
fftflg = 1;
else
error('Unrecognized output required');
end
% prepare channel(cmb)
if ~isfield(cfg, 'channelcmb') && csdflg
%set the default for the channelcombination
cfg.channelcmb = {'all' 'all'};
elseif isfield(cfg, 'channelcmb') && ~csdflg
% no cross-spectrum needs to be computed, hence remove the combinations from cfg
cfg = rmfield(cfg, 'channelcmb');
end
if isfield(cfg, 'channelcmb')
% the channels in the data are already the subset according to cfg.channel
cfg.channelcmb = ft_channelcombination(cfg.channelcmb, data.label);
end
% determine the corresponding indices of all channels
chanind = match_str(data.label, cfg.channel);
nchan = size(chanind,1);
if csdflg
assert(nchan>1, 'CSD output requires multiple channels');
% determine the corresponding indices of all channel combinations
[dummy,chancmbind(:,1)] = match_str(cfg.channelcmb(:,1), data.label);
[dummy,chancmbind(:,2)] = match_str(cfg.channelcmb(:,2), data.label);
nchancmb = size(chancmbind,1);
chanind = unique([chanind(:); chancmbind(:)]);
nchan = length(chanind);
cutdatindcmb = zeros(size(chancmbind));
for ichan = 1:nchan
cutdatindcmb(chancmbind == chanind(ichan)) = ichan;
end
end
% determine trial characteristics
ntrials = numel(data.trial);
trllength = zeros(1, ntrials);
for itrial = 1:ntrials
trllength(itrial) = size(data.trial{itrial}, 2);
end
if strcmp(cfg.pad, 'maxperlen')
padding = max(trllength);
cfg.pad = padding/data.fsample;
else
padding = cfg.pad*data.fsample;
if padding1
powspctrm(:,:,hasdc_nyq,:) = powspctrm(:,:,hasdc_nyq,:)./2;
else
powspctrm(:,hasdc_nyq,:) = powspctrm(:,hasdc_nyq,:)./2;
end
end
freq.powspctrm = powspctrm;
end
if fftflg
% correct the 0 Hz or Nyqist bin if present, scaling with a factor of 2 is only appropriate for ~0 Hz
if ~isempty(hasdc_nyq)
if keeprpt>1
fourierspctrm(:,:,hasdc_nyq,:) = fourierspctrm(:,:,hasdc_nyq,:)./sqrt(2);
else
fourierspctrm(:,hasdc_nyq,:) = fourierspctrm(:,hasdc_nyq,:)./sqrt(2);
end
end
freq.fourierspctrm = fourierspctrm;
end
if csdflg
% correct the 0 Hz or Nyqist bin if present, scaling with a factor of 2 is only appropriate for ~0 Hz
if ~isempty(hasdc_nyq)
if keeprpt>1
crsspctrm(:,:,hasdc_nyq,:) = crsspctrm(:,:,hasdc_nyq,:)./2;
else
crsspctrm(:,hasdc_nyq,:) = crsspctrm(:,hasdc_nyq,:)./2;
end
end
freq.labelcmb = cfg.channelcmb;
freq.crsspctrm = crsspctrm;
end
if strcmp(cfg.calcdof, 'yes');
freq.dof = 2 .* dof;
end;
if strcmp(cfg.method, 'mtmfft') && (keeprpt == 2 || keeprpt == 4)
freq.cumsumcnt = trllength';
end
if exist('cumtapcnt', 'var') && (keeprpt == 2 || keeprpt == 4)
freq.cumtapcnt = cumtapcnt;
end
% backwards compatability of foilim
if ~isempty(cfg.foilim)
cfg = rmfield(cfg, 'foi');
else
cfg = rmfield(cfg, 'foilim');
end
% some fields from the input should always be copied over in the output
freq = copyfields(data, freq, {'grad', 'elec', 'topo', 'topolabel', 'unmixing'});
if isfield(data, 'trialinfo') && strcmp(cfg.keeptrials, 'yes')
% copy the trialinfo into the output, but not the sampleinfo
freq.trialinfo = data.trialinfo;
end
% do the general cleanup and bookkeeping at the end of the function
ft_postamble debug
ft_postamble trackconfig
ft_postamble provenance
ft_postamble previous data
ft_postamble history freq
ft_postamble savevar freq