/
ft_steadystatesimulation.m
399 lines (353 loc) · 17.3 KB
/
ft_steadystatesimulation.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
function [data] = ft_steadystatesimulation(cfg)
% FT_STEADYSTATESIMULATION creates a simulated EEG/MEG dataset. This function
% allows to simulate the effect of several independent stimulus trains. These can
% be presented as a periodic sequence, or as single (or few) transient stimuli.
% This function creates a single block of data. You can call it repeatedly and use
% FT_APPENDDATA to combine different blocks.
%
% Use as
% data = ft_steadystatesimulation(cfg)
% where cfg is a configuration structure that should contain
% cfg.fsample = scalar, sampling frequency in Hz (default = 512)
% cfg.duration = scalar, trial length in seconds (default = 4.56)
% cfg.baseline = scalar, baseline length in seconds (default = 0)
% cfg.ntrials = integer N, number of trials (default = 320)
% cfg.iti = scalar, inter-trial interval in seconds (default = 1)
% cfg.randomseed = 'yes' or a number or vector with the seed value (default = 'yes')
%
% Each trial can contain multiple nested experimental manipulations
% cfg.level1.condition = scalar, or vector of length L1 (default = 1)
% cfg.level1.gain = scalar, or vector of length L1 (default = 1)
% cfg.level2.condition = scalar, or vector of length L2 (default = 1)
% cfg.level2.gain = scalar, or vector of length L2 (default = 1)
% cfg.level3.condition = scalar, or vector of length L3 (default = 1)
% cfg.level3.gain = scalar, or vector of length L3 (default = 1)
% If you don't need level 2 and up, specify the condition and gain as empty.
% Idem for level 3 and up.
%
% Stimuli are created at the lowest experimental level, and are modulated according to the product of the gain of all levels.
% Each trial can contain one or multiple stimuli.
% The behavior of each stimuli is specified with
% cfg.stimulus1.mode = 'periodic', 'transient' or 'off' (default = 'periodic')
% cfg.stimulus2.mode = 'periodic', 'transient' or 'off' (default = 'transient')
%
% If the stimulus is periodic (below as example for stimulus1), the following options apply
% cfg.stimulus1.number = does not apply for periodic stimuli
% cfg.stimulus1.onset = in seconds, first stimulus relative to the start of the trial (default = 0)
% cfg.stimulus1.onsetjitter = in seconds, max jitter that is added to the onset (default = 0)
% cfg.stimulus1.isi = in seconds, i.e. for 10Hz you would specify 0.1 seconds as the interstimulus interval (default = 0.1176)
% cfg.stimulus1.isijitter = in seconds, max jitter relative to the previous stimulus (default = 0)
% cfg.stimulus2.condition = does not apply for periodic stimuli
% cfg.stimulus2.gain = does not apply for periodic stimuli
% cfg.stimulus1.kernelshape = 'sine'
% cfg.stimulus1.kernelduration = in seconds (default = isi)
%
% If the stimulus is transient (below as example for stimulus2), the following options apply
% cfg.stimulus2.number = scalar M, how many transients are to be presented per trial (default = 4)
% cfg.stimulus2.onset = in seconds, first stimulus relative to the start of the trial (default = 0.7)
% cfg.stimulus2.onsetjitter = in seconds, max jitter that is added to the onset (default = 0.2)
% cfg.stimulus2.isi = in seconds as the interstimulus interval (default = 0.7)
% cfg.stimulus2.isijitter = in seconds, max jitter relative to the previous stimulus (default = 0.2)
% cfg.stimulus2.condition = 1xM vector with condition codes for each transient within a trial (default = [1 1 2 2])
% cfg.stimulus2.gain = 1xM vector with gain for each condition for each transient within a trial (default = [1 1 1 1])
% cfg.stimulus2.kernelshape = 'hanning'
% cfg.stimulus2.kernelduration = in seconds (default = 0.75*isi)
%
% RANDOMIZATIONS:
% - The onsetjitter is randomized between 0 and the value given, and is always added to the onset.
% - The isijitter is randomized between 0 and the value given, and is always added to the interstimulus interval (isi).
% - For periodic stimuli, which are constant within a trial, the condition code and gain are shuffled over all trials.
% - For transient stimuli, the condition code and gain are shuffled within each trial.
%
% Using the default settings, we model a peripherally presented flickering stimulus
% that appears at different excentricities together with a centrally presented
% transient stimulus that appears 4x per trial. To simulate the experiment described
% at , you have to call this 4 times with a different cfg.configuration and
% cfg.gain to model the task load and use FT_APPENDDATA to concatenate the trials. In
% this case cfg.condition models the factor "task load" (2 levels, low and high),
% cfg.stimulus1.condition models the factor "excentricity" (4 levels), and
% cfg.stimulation2.condition models the factor "stimulus type" (2 levels, non-target
% or target).
%
% See also FT_DIPOLESIMULATION, FT_TIMELOCKSIMULATION, FT_FREQSIMULATION,
% FT_CONNECTIVITYSIMULATION, FT_APPENDDATA
% Copyright (C) 2017, Stefan Wiens, Malina Szychowska, Robert Oostenveld
%
% 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 <http://www.gnu.org/licenses/>.
%
% $Id$
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% the initial part deals with parsing the input options and data
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% these are used by the ft_preamble/ft_postamble function and scripts
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 provenance
ft_preamble randomseed
% 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
% get the options or set the defaults
cfg.level1 = ft_getopt(cfg, 'level1');
cfg.level2 = ft_getopt(cfg, 'level2');
cfg.level3 = ft_getopt(cfg, 'level3');
cfg.level1.condition = ft_getopt(cfg.level1, 'condition', 1);
cfg.level2.condition = ft_getopt(cfg.level2, 'condition', []);
cfg.level3.condition = ft_getopt(cfg.level3, 'condition', []);
cfg.level1.gain = ft_getopt(cfg.level1, 'gain', 1);
cfg.level2.gain = ft_getopt(cfg.level2, 'gain', []);
cfg.level3.gain = ft_getopt(cfg.level3, 'gain', []);
cfg.ntrials = ft_getopt(cfg, 'ntrials', 320);
cfg.fsample = ft_getopt(cfg, 'fsample', 512);
cfg.duration = ft_getopt(cfg, 'duration', 4.5);
cfg.baseline = ft_getopt(cfg, 'baseline', 0);
cfg.iti = ft_getopt(cfg, 'iti', 1);
cfg.stimulus1 = ft_getopt(cfg, 'stimulus1');
cfg.stimulus2 = ft_getopt(cfg, 'stimulus2');
cfg.stimulus1.mode = ft_getopt(cfg.stimulus1, 'mode', 'periodic');
cfg.stimulus1.number = ft_getopt(cfg.stimulus1, 'number', 0);
cfg.stimulus1.onset = ft_getopt(cfg.stimulus1, 'onset', 0);
cfg.stimulus1.onsetjitter = ft_getopt(cfg.stimulus1, 'onsetjitter', 0);
cfg.stimulus1.isi = ft_getopt(cfg.stimulus1, 'isi', 0.1176);
cfg.stimulus1.isijitter = ft_getopt(cfg.stimulus1, 'isijitter', 0);
cfg.stimulus1.condition = ft_getopt(cfg.stimulus1, 'condition');
cfg.stimulus1.gain = ft_getopt(cfg.stimulus1, 'gain');
cfg.stimulus1.kernelshape = ft_getopt(cfg.stimulus1, 'kernelshape', 'sine');
cfg.stimulus1.kernelduration = ft_getopt(cfg.stimulus1, 'kernelduration', cfg.stimulus1.isi);
cfg.stimulus2.mode = ft_getopt(cfg.stimulus2, 'mode', 'transient');
cfg.stimulus2.number = ft_getopt(cfg.stimulus2, 'number', 4);
cfg.stimulus2.onset = ft_getopt(cfg.stimulus2, 'onset', 0.7);
cfg.stimulus2.onsetjitter = ft_getopt(cfg.stimulus2, 'onsetjitter', 0);
cfg.stimulus2.isi = ft_getopt(cfg.stimulus2, 'isi', 0.7);
cfg.stimulus2.isijitter = ft_getopt(cfg.stimulus2, 'isijitter', 0);
cfg.stimulus2.condition = ft_getopt(cfg.stimulus2, 'condition', [1 2 3 4]);
cfg.stimulus2.gain = ft_getopt(cfg.stimulus2, 'gain', [1 1 1 1]);
cfg.stimulus2.kernelshape = ft_getopt(cfg.stimulus2, 'kernelshape', 'hanning');
cfg.stimulus2.kernelduration = ft_getopt(cfg.stimulus2, 'kernelduration', 0.75*cfg.stimulus2.isi);
ncondition1 = length(cfg.level1.condition);
ncondition2 = length(cfg.level2.condition);
ncondition3 = length(cfg.level3.condition);
switch cfg.stimulus1.mode
case 'off'
cfg.stimulus1.number = 0;
cfg.stimulus1.condition = [];
cfg.stimulus1.gain = [];
case 'transient'
assert(numel(cfg.stimulus1.condition)==cfg.stimulus1.number);
assert(numel(cfg.stimulus1.gain)==cfg.stimulus1.number);
end
switch cfg.stimulus2.mode
case 'off'
cfg.stimulus2.number = 0;
cfg.stimulus2.condition = [];
cfg.stimulus2.gain = [];
case 'transient'
assert(numel(cfg.stimulus2.condition)==cfg.stimulus2.number);
assert(numel(cfg.stimulus2.gain)==cfg.stimulus2.number);
end
% they cannot be the same, as that would confuse the trialinfo
assert(~isequal(cfg.stimulus1.mode, cfg.stimulus2.mode));
% determine which levels are being used
level1 = ncondition1>0;
level2 = ncondition2>0;
level3 = ncondition3>0;
% count the number of blocks and number of trials per block
nblock = max(ncondition1,1) * max(ncondition2,1) * max(ncondition3,1);
ntrial = cfg.ntrials / nblock;
assert(ntrial==round(ntrial), 'number of trials inconsistent');
% the time (followinf onset) and baseline are the same for each trial
baseline = (-cfg.baseline):(1/cfg.fsample):0;
baseline(end) = []; % remove the last sample
time = 0:(1/cfg.fsample):cfg.duration;
% construct a sine-wave kernel, feval fails on this one
sine = @(n) sin(2*pi*((1:n)-1)/n);
% construct the kernels for the response to each stimulus
if strcmp(cfg.stimulus1.kernelshape, 'sine')
kernel1 = sine(round(cfg.stimulus1.kernelduration*cfg.fsample));
else
kernel1 = feval(cfg.stimulus1.kernelshape, round(cfg.stimulus1.kernelduration*cfg.fsample));
end
if strcmp(cfg.stimulus2.kernelshape, 'sine')
kernel2 = sine(round(cfg.stimulus2.kernelduration*cfg.fsample));
else
kernel2 = feval(cfg.stimulus2.kernelshape, round(cfg.stimulus2.kernelduration*cfg.fsample));
end
% these should be row-vectors
kernel1 = kernel1(:)';
kernel2 = kernel2(:)';
% start with empty data
data = [];
% this keeps track of the trials
k = 1;
shuffle = randperm(ncondition3);
tmpcondition3 = cfg.level3.condition(shuffle);
tmpgain3 = cfg.level3.gain(shuffle);
for cond3=1:max(ncondition3, 1)
if level3
trialgain = tmpgain3(cond3);
else
trialgain = 1;
end
shuffle = randperm(ncondition2);
tmpcondition2 = cfg.level2.condition(shuffle);
tmpgain2 = cfg.level2.gain(shuffle);
for cond2=1:max(ncondition2, 1)
if level2
trialgain = trialgain * tmpgain2(cond2);
end
shuffle = randperm(ncondition1);
tmpcondition1 = cfg.level1.condition(shuffle);
tmpgain1 = cfg.level1.gain(shuffle);
for cond1=1:max(ncondition1, 1)
if level1
trialgain = trialgain * tmpgain1(cond1);
end
for trial=1:ntrial
trialinfo = k; % start with the trial number
varname = {'trial'};
if level1
trialinfo = [trialinfo tmpcondition1(cond1)];
varname(end+1) = {'l1_cond'};
end
if level2>0
trialinfo = [trialinfo tmpcondition2(cond2)];
varname(end+1) = {'l2_cond'};
end
if level3>0
trialinfo = [trialinfo tmpcondition3(cond3)];
varname(end+1) = {'l3_cond'};
end
dat = [];
stimcfg = cfg.stimulus1;
signal = zeros(size(time)); % the baseline will be added later
switch stimcfg.mode
case 'periodic'
sample = nearest(time, stimcfg.onset + rand(1)*stimcfg.onsetjitter);
for i=1:ceil((cfg.duration-stimcfg.onset)/stimcfg.isi)
sample = sample + round(cfg.fsample*(stimcfg.isi + stimcfg.isijitter*rand(1)));
if sample>length(signal)
% this periodic stimulus falls outside the trial
sample = nan;
else
signal(sample) = 1;
end
trialinfo = [trialinfo sample+length(baseline)];
varname(end+1) = {sprintf('p%d_sample', i)};
end % while
case 'transient'
shuffle = randperm(numel(stimcfg.gain));
sample = nearest(time, stimcfg.onset + rand(1)*stimcfg.onsetjitter);
transientgain = stimcfg.gain(shuffle);
transientcondition = stimcfg.condition(shuffle);
for i=1:stimcfg.number
sample = sample + round(cfg.fsample*(stimcfg.isi + stimcfg.isijitter*rand(1)));
if sample>length(signal)
warning('transient stimulus falls outside of trial');
sample = nan;
else
signal(sample) = transientgain(i);
end
trialinfo = [trialinfo transientcondition(i)];
varname(end+1) = {sprintf('t%d_cond', i)};
trialinfo = [trialinfo sample+length(baseline)];
varname(end+1) = {sprintf('t%d_sample', i)};
end % while
end % switch mode
dat(2,:) = [zeros(size(baseline)) signal*trialgain ];
dat(3,:) = [zeros(size(baseline)) erpconvolve(signal*trialgain, kernel1)];
stimcfg = cfg.stimulus2;
signal = zeros(size(time)); % the baseline will be added later
switch stimcfg.mode
case 'periodic'
sample = nearest(time, stimcfg.onset + rand(1)*stimcfg.onsetjitter);
for i=1:ceil((cfg.duration-stimcfg.onset)/stimcfg.isi)
sample = sample + round(cfg.fsample*(stimcfg.isi + stimcfg.isijitter*rand(1)));
if sample>length(signal)
% this periodic stimulus falls outside the trial
sample = nan;
else
signal(sample) = 1;
end
trialinfo = [trialinfo sample+length(baseline)];
varname(end+1) = {sprintf('p%d_sample', i)};
end % while
case 'transient'
shuffle = randperm(numel(stimcfg.gain));
sample = nearest(time, stimcfg.onset + rand(1)*stimcfg.onsetjitter);
transientgain = stimcfg.gain(shuffle);
transientcondition = stimcfg.condition(shuffle);
for i=1:stimcfg.number
sample = sample + round(cfg.fsample*(stimcfg.isi + stimcfg.isijitter*rand(1)));
if sample>length(signal)
warning('transient stimulus falls outside of trial');
sample = nan;
else
signal(sample) = transientgain(i);
end
trialinfo = [trialinfo transientcondition(i)];
varname(end+1) = {sprintf('t%d_cond', i)};
trialinfo = [trialinfo sample+length(baseline)];
varname(end+1) = {sprintf('t%d_sample', i)};
end % while
end % switch mode
dat(4,:) = [zeros(size(baseline)) signal*trialgain ];
dat(5,:) = [zeros(size(baseline)) erpconvolve(signal*trialgain, kernel2)];
% the first channel contains the sum of the two convolved signals
dat(1,:) = sum(dat([3 5],:), 1);
data.trial{k} = dat;
data.time{k} = [baseline time];
data.trialinfo(k,:) = trialinfo;
k = k+1;
end % for each trial in each block
end % for all conditions in level 1
end % for all conditions in level 2
end % for all conditions in level 3
data.trialinfo = array2table(data.trialinfo, 'VariableNames', varname);
data.fsample = cfg.fsample;
data.label = {
'combined'
'stickfunction1'
'convolved1'
'stickfunction2'
'convolved2'
};
% construct the sample information, start with consecutive segments
nsample = length(baseline) + length(time);
data.sampleinfo(:,1) = ((1:ntrial)-1)*nsample + 1;
data.sampleinfo(:,2) = ((1:ntrial) )*nsample;
% add the inter-trial-interval
data.sampleinfo(:,1) = data.sampleinfo(:,1) + transpose((1:ntrial)-1)*round(cfg.iti*cfg.fsample);
data.sampleinfo(:,2) = data.sampleinfo(:,2) + transpose((1:ntrial)-1)*round(cfg.iti*cfg.fsample);
% do the general cleanup and bookkeeping at the end of the function
ft_postamble debug
ft_postamble randomseed
ft_postamble provenance data
ft_postamble history data
ft_postamble savevar data
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% SUBFUNCTION
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function dat = erpconvolve(stickfunction, kernel)
dat = convn(stickfunction, kernel, 'full');
dat = dat(1:length(stickfunction));