function [dataout] = ft_eventtiminganalysis(cfg, data) % FT_EVENTTIMINGANALYSIS computes a model of single trial event- related activity, % by estimating per trial the latency (and amplitude) of event-related signal % components. % % Use as % [dataout] = ft_eventtiminganalysis(cfg, data) % where data is single-channel raw data as obtained by FT_PREPROCESSING % and cfg is a configuration structure according to % % cfg.method = method for estimating event-related activity % 'aseo', analysis of single-trial ERP and ongoing % activity (according to Xu et al, 2009) % 'gbve', graph-based variability estimation % (according to Gramfort et al, IEEE TBME 2009) % cfg.channel = Nx1 cell-array with selection of channels (default = 'all'), % see FT_CHANNELSELECTION for details % cfg.trials = 'all' or a selection given as a 1xN vector (default = 'all') % cfg.output = 'model', or 'residual', which returns the modelled data, % or the residuals. % % Method specific options are specified in the appropriate substructure. % % For the ASEO method, the following options can be specified: % cfg.aseo.noiseEstimate = 'non-parametric' or 'parametric', estimate noise % using parametric or non-parametric (default) method % cfg.aseo.tapsmofrq = value, smoothing parameter of noise for % nonparametric estimation (default = 5) % cfg.aseo.jitter = value, time jitter in initial timewindow % estimate (in seconds). default 0.050 seconds % cfg.aseo.numiteration = value, number of iteration (default = 1) % cfg.aseo.initlatency = Nx2 matrix, initial set of latencies in seconds of event- % related components, give as [comp1start, comp1end; % comp2start, comp2end] (default not % specified). For multiple channels it should % be a cell-array, one matrix per channel % Alternatively, rather than specifying a (set of latencies), one can also % specify: % % cfg.aseo.initcomp = vector, initial estimate of the waveform % components. For multiple channels it should % be a cell-array, one matrix per channel. % % For the GBVE method, the following options can be specified: % cfg.gbve.sigma = vector, range of sigma values to explore in % cross-validation loop (default: 0.01:0.01:0.2) % cfg.gbve.distance = scalar, distance metric to use as % evaluation criterion, see plugin code for % more informatoin % cfg.gbve.alpha = vector, range of alpha values to explor in % cross-validation loop (default: [0 0.001 0.01 0.1]) % cfg.gbve.exponent = scalar, see plugin code for information % cfg.gbve.use_maximum = boolean, (default: 1) consider the positive going peak % cfg.gbve.show_pca = boolean, see plugin code (default 0) % cfg.gbve.show_trial_number = boolean, see plugin code (default 0) % cfg.gbve.verbose = boolean (default: 1) % cfg.gbve.disp_log = boolean, see plugin code (default 0) % cfg.gbve.latency = vector [min max], latency range in s % (default: [-inf inf]) % cfg.gbve.xwin = scalar smoothing parameter for moving % average smoothing (default: 1), see % eeglab's movav function for more % information. % % 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 FT_SINGLETRIALANALYSIS_ASEO % Copyright (C) 2018-2019, Jan-Mathijs Schoffelen DCCN % % This file is part of FieldTrip, see http://www.fieldtriptoolbox.org % 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 . % % $Id$ ft_revision = '$Id$'; ft_nargin = nargin; ft_nargout = nargout; % do the general setup of the function ft_defaults ft_preamble init ft_preamble debug ft_preamble loadvar data ft_preamble provenance data % the ft_abort variable is set to true or false in ft_preamble_init if ft_abort % do not continue function execution in case the outputfile is present and the user indicated to keep it return end % ensure that the input data is valid for this function data = ft_checkdata(data, 'datatype', {'raw+comp', 'raw'}, 'feedback', 'yes', 'hassampleinfo', 'yes'); % check if the input cfg is valid for this function cfg = ft_checkconfig(cfg, 'forbidden', {'channels', 'trial'}); % prevent accidental typos, see issue 1729 cfg = ft_checkconfig(cfg, 'required', {'method'}); % set the defaults cfg.trials = ft_getopt(cfg, 'trials', 'all', 1); % all trials as default cfg.channel = ft_getopt(cfg, 'channel', 'all'); cfg.output = ft_getopt(cfg, 'output', 'model'); % ensure that the options are valid cfg = ft_checkopt(cfg, 'method', 'char', {'aseo' 'gbve'}); %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % the actual computation is done in the middle part %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% % select trials of interest tmpcfg = keepfields(cfg, {'trials' 'channel' 'showcallinfo', 'trackcallinfo', 'trackusage', 'trackdatainfo', 'trackmeminfo', 'tracktimeinfo', 'checksize'}); data = ft_selectdata(tmpcfg, data); % restore the provenance information [cfg, data] = rollback_provenance(cfg, data); % some error checks if isfield(data, 'trial') && numel(data.trial)==0, ft_error('no trials were selected'); end if numel(data.label)==0, ft_error('no channels were selected'); end switch cfg.method case 'aseo' % define general variables that are used locally fsample = data.fsample; % Sampling Frequency in Hz nchan = numel(data.label); nsample = numel(data.time{1}); %FIXME ASSUMING FIXED TIME AXIS ACROSS ALL TRIALS % setting a bunch of options, to be passed on to the lower level function if ~isfield(cfg, 'aseo'), cfg.aseo = []; end cfg.aseo.thresholdAmpH = ft_getopt(cfg.aseo, 'thresholdAmpH', 0.5); cfg.aseo.thresholdAmpL = ft_getopt(cfg.aseo, 'thresholdAmpL', 0.1); cfg.aseo.thresholdCorr = ft_getopt(cfg.aseo, 'thresholdCorr', 0.2); cfg.aseo.maxOrderAR = ft_getopt(cfg.aseo, 'maxOrderAR', 5); cfg.aseo.noiseEstimate = ft_getopt(cfg.aseo, 'noiseEstimate', 'nonparametric'); cfg.aseo.numiteration = ft_getopt(cfg.aseo, 'numiteration', 1); cfg.aseo.tapsmofrq = ft_getopt(cfg.aseo, 'tapsmofrq', 5); cfg.aseo.fsample = fsample; cfg.aseo.nsample = nsample; cfg.aseo.pad = ft_getopt(cfg.aseo, 'pad', (2.*nsample)/fsample); % deal with the different ways with which the initial waveforms can be defined initlatency = ft_getopt(cfg.aseo, 'initlatency', {}); initcomp = ft_getopt(cfg.aseo, 'initcomp', {}); jitter = ft_getopt(cfg.aseo, 'jitter', 0.050); % half temporal width of shift in s if isempty(initlatency) && isempty(initcomp) ft_error('for the ASEO method you should supply either an initial estimate of the waveform component, or a set of latencies'); elseif ~isempty(initlatency) % this takes precedence, and should contain per channel the begin and % end points of the subwindows in time, based on which the initial % subcomponents are estimated % ensure it to be a cell-array if the input is a matrix if ~iscell(initlatency) initlatency = repmat({initlatency},[1 nchan]); end make_init = true; elseif ~isempty(initcomp) % ensure it to be a cell-array if the input is a matrix if ~iscell(initcomp) initcomp = repmat({initcomp}, [1 nchan]); end make_init = false; end if make_init assert(numel(initlatency)==nchan); for k = 1:nchan % preprocessing data tmp = cellrowselect(data.trial,k); chandat = cat(1,tmp{:}); chandat = ft_preproc_baselinecorrect(chandat, nearest(data.time{1}, -inf), nearest(data.time{1}, 0)); avgdat = nanmean(chandat, 1); % set the initial ERP waveforms according to the preset parameters ncomp = size(initlatency{k},1); initcomp{k} = zeros(nsample, ncomp); for m = 1:ncomp begsmp = nearest(data.time{1},initlatency{k}(m, 1)); endsmp = nearest(data.time{1},initlatency{k}(m, 2)); if begsmp<1, begsmp = 1; end if endsmp>nsample, endsmp = nsample; end tmp = avgdat(begsmp:endsmp)'; initcomp{k}(begsmp:endsmp, m) = tmp; end initcomp{k} = initcomp{k} - repmat(mean(initcomp{k}),nsample,1); end else assert(numel(initcomp)==nchan); end if ~iscell(jitter) jitter = repmat({jitter}, [1 nchan]); end for k = 1:numel(jitter) if ~isempty(jitter{k}) if size(jitter{k},1)~=size(initcomp{k},2), jitter{k} = repmat(jitter{k}(1,:),[size(initcomp{k},2) 1]); end end end % initialize the output data dataout = removefields(data, 'cfg'); for k = 1:numel(data.trial) dataout.trial{k}(:) = nan; end % initialize the struct that will contain the output parameters params = struct([]); % do the actual computations for k = 1:nchan % preprocessing data tmp = cellrowselect(data.trial,k); chandat = cat(1,tmp{:}); % baseline correction chandat = ft_preproc_baselinecorrect(chandat, nearest(data.time{1}, -inf), nearest(data.time{1}, 0)); % do zero-padding and FFT to the signal and initial waveforms npad = cfg.aseo.pad*fsample; % length of data + zero-padding number nfft = 2.^(ceil(log2(npad)))*2; initcomp_fft = fft(initcomp{k}, nfft); % Fourier transform of the initial waveform chandat_fft = fft(chandat', nfft); % Fourier transform of the signal cfg.aseo.jitter = jitter{k}; output = ft_singletrialanalysis_aseo(cfg, chandat_fft, initcomp_fft); params(k).latency = output(end).lat_est./fsample; params(k).amplitude = output(end).amp_est; params(k).components = output(end).erp_est; params(k).rejectflag = output(end).rejectflag; params(k).noise = output(end).noise; for m = 1:numel(data.trial) if output(end).rejectflag(m)==0 switch cfg.output case 'model' dataout.trial{m}(k,:) = data.trial{m}(k,:)-output(end).residual(:,m)'; case 'residual' dataout.trial{m}(k,:) = output(end).residual(:,m)'; end end end end case 'gbve' ft_hastoolbox('lagextraction', 1); ft_hastoolbox('eeglab', 1); % because the low-level code might use a specific moving average function from EEGLAB ft_hastoolbox('cellfunction', 1); if ~isfield(cfg, 'gbve'), cfg.gbve = []; end cfg.gbve.NORMALIZE_DATA = ft_getopt(cfg.gbve, 'NORMALIZE_DATA', true); cfg.gbve.CENTER_DATA = ft_getopt(cfg.gbve, 'CENTER_DATA', false); cfg.gbve.USE_ADAPTIVE_SIGMA = ft_getopt(cfg.gbve, 'USE_ADAPTIVE_SIGMA', false); cfg.gbve.sigma = ft_getopt(cfg.gbve, 'sigma', 0.01:0.01:0.2); cfg.gbve.distance = ft_getopt(cfg.gbve, 'distance', 'corr2'); cfg.gbve.alpha = ft_getopt(cfg.gbve, 'alpha', [0 0.001 0.01 0.1]); cfg.gbve.exponent = ft_getopt(cfg.gbve, 'exponent', 1); cfg.gbve.use_maximum = ft_getopt(cfg.gbve, 'use_maximum', 1); % consider the positive going peak cfg.gbve.show_pca = ft_getopt(cfg.gbve, 'show_pca', false); cfg.gbve.show_trial_number = ft_getopt(cfg.gbve, 'show_trial_number', false); cfg.gbve.verbose = ft_getopt(cfg.gbve, 'verbose', true); cfg.gbve.disp_log = ft_getopt(cfg.gbve, 'disp_log', false); cfg.gbve.latency = ft_getopt(cfg.gbve, 'latency', [-inf inf]); cfg.gbve.xwin = ft_getopt(cfg.gbve, 'xwin', 1); % default is a bit of smoothing cfg.gbve.nfold = ft_getopt(cfg.gbve, 'nfold', 5); nchan = numel(data.label); ntrl = numel(data.trial); tmin = nearest(data.time{1}, cfg.gbve.latency(1)); tmax = nearest(data.time{1}, cfg.gbve.latency(2)); % initialize the struct that will contain the output parameters dataout = removefields(data, 'cfg'); params = struct([]); for k = 1:nchan % preprocessing data options = cfg.gbve; fprintf('--- Processing channel %d\n',k); tmp = cellrowselect(data.trial,k); chandat = cat(1,tmp{:}); points = chandat(:,tmin:tmax); % perform a loop across alpha values, cross validation alphas = options.alpha; if length(alphas) > 1 % Use Cross validation error if multiple alphas are specified best_CVerr = -Inf; K = cfg.gbve.nfold; disp(['--- Running K Cross Validation (K = ',num2str(K),')']); block_idx = fix(linspace(1, ntrl, K+1)); % K cross validation for jj=1:length(alphas) options.alpha = alphas(jj); CVerr = 0; for kk = 1:K bidx = block_idx(kk):block_idx(kk+1); idx = 1:ntrl; idx(bidx) = []; data_k = chandat(idx,:); points_k = points(idx,:); [order,lags] = extractlag(points_k,options); data_reordered = data_k(order,:); lags = lags + tmin; [data_aligned, dum] = perform_realign(data_reordered, data.time{1}, lags); data_aligned(~isfinite(data_aligned)) = nan; ep_evoked = nanmean(data_aligned); ep_evoked = ep_evoked ./ norm(ep_evoked); data_k = chandat(bidx,:); data_norm = sqrt(sum(data_k.^2,2)); data_k = diag(1./data_norm)*data_k; data_k(data_norm==0,:) = 0; for pp=1:length(bidx) c = xcorr(ep_evoked,data_k(pp,:)); CVerr = CVerr + max(c(:)); end end CVerr = CVerr/ntrl; if CVerr > best_CVerr best_CVerr = CVerr; best_alpha = alphas(jj); end end options.alpha = best_alpha; end if options.use_maximum [order, lags] = extractlag( points, options ); else [order, lags] = extractlag( -points, options ); end disp(['---------- Using alpha = ',num2str(options.alpha)]); data_reordered = chandat(order,:); lags = lags + tmin; [data_aligned] = perform_realign(data_reordered, data.time{1}, lags ); data_aligned(~isfinite(data_aligned)) = nan; [dum, order_inv] = sort(order); lags_no_order = lags(order_inv); data_aligned = data_aligned(order_inv,:); params(k).latency = data.time{1}(lags_no_order)'; switch cfg.output case 'model' tmp = mat2cell(data_aligned, ones(1,size(data_aligned,1)), size(data_aligned,2))'; dataout.trial = cellrowassign(dataout.trial, tmp, k); case 'residual' % to be done error('not yet implemented'); end end otherwise ft_error('unsupported method'); end % switch method dataout.params = params; dataout.cfg = cfg; % do the general cleanup and bookkeeping at the end of the function ft_postamble debug ft_postamble previous data ft_postamble provenance dataout ft_postamble history dataout ft_postamble savevar dataout