diff --git a/initEnv.m b/initEnv.m index 90560f8..e135df6 100644 --- a/initEnv.m +++ b/initEnv.m @@ -88,6 +88,7 @@ function addDependencies() pth = fileparts(mfilename('fullpath')); addpath(fullfile(pth, 'lib', 'CPP_BIDS', 'src')); addpath(fullfile(pth, 'lib', 'CPP_PTB')); + addpath(genpath(fullfile(pth, 'lib', 'CPP_PTB', 'src'))); addpath(fullfile(pth, 'subfun')); end diff --git a/lib/CPP_BIDS b/lib/CPP_BIDS index a17e12a..d2a4c43 160000 --- a/lib/CPP_BIDS +++ b/lib/CPP_BIDS @@ -1 +1 @@ -Subproject commit a17e12a53c0063e36fb017b7a545d605e2c2ef8e +Subproject commit d2a4c43725ec69a87afccd4a0ec39d800c5968a8 diff --git a/lib/CPP_PTB b/lib/CPP_PTB index a8bc6b5..a0c8874 160000 --- a/lib/CPP_PTB +++ b/lib/CPP_PTB @@ -1 +1 @@ -Subproject commit a8bc6b5a1457ad78b496fa89037c07f0e548e0c8 +Subproject commit a0c8874fd7894a748758624d94d7b74f7e5c0252 diff --git a/setParameters.m b/setParameters.m index b4b031d..413ac19 100644 --- a/setParameters.m +++ b/setParameters.m @@ -11,7 +11,7 @@ 'output'); %% Debug mode settings - + cfg.debug.do = true; % To test the script out of the scanner, skip PTB sync cfg.debug.smallWin = false; % To test on a part of the screen, change to 1 cfg.debug.transpWin = true; % To test with trasparent full size screen @@ -34,22 +34,31 @@ %% Experiment Design +% cfg.design.motionType = 'translation'; +% cfg.design.motionType = 'radial'; + cfg.design.motionType = 'translation'; cfg.design.names = {'static'; 'motion'}; cfg.design.nbRepetitions = 4; cfg.design.nbEventsPerBlock = 12; % DO NOT CHANGE %% Timing + % FOR 7T: if you want to create localizers on the fly, the following must be + % multiples of the scanneryour sequence TR + % + % IBI + % block length = (cfg.eventDuration + cfg.ISI) * cfg.design.nbEventsPerBlock + % Time between blocs in secs - cfg.IBI = .5; % 8; + cfg.IBI = 1.8*3; % 8; % Time between events in secs - cfg.ISI = 0.5; + cfg.ISI = 0.1; % Number of seconds before the motion stimuli are presented cfg.onsetDelay = .1; % Number of seconds after the end all the stimuli before ending the run cfg.endDelay = .1; - cfg.eventDuration = 1; % second + cfg.eventDuration = 0.8; % second %% Visual Stimulation @@ -91,13 +100,13 @@ cfg.target.maxNbPerBlock = 2; cfg.target.duration = 0.05; % In secs - cfg.extraColumns = {'direction', 'speed', 'target', 'event', 'block'}; - + cfg.extraColumns = {'direction', 'speed', 'target', 'event', 'block', 'keyName'}; + end function cfg = setKeyboards(cfg) cfg.keyboard.escapeKey = 'ESCAPE'; - cfg.keyboard.responseKey = {'space'}; + cfg.keyboard.responseKey = {'space', 't'}; cfg.keyboard.keyboard = []; cfg.keyboard.responseBox = []; @@ -112,7 +121,7 @@ cfg.mri.triggerKey = 't'; cfg.mri.triggerNb = 4; - cfg.mri.repetitionTime = 2; + cfg.mri.repetitionTime = 1.8; cfg.bids.MRI.Instructions = 'Detect the RED fixation cross'; cfg.bids.MRI.TaskDescription = []; diff --git a/subfun/doDotMo.m b/subfun/doDotMo.m index 487741f..29b2067 100644 --- a/subfun/doDotMo.m +++ b/subfun/doDotMo.m @@ -33,7 +33,7 @@ % We assumed that zero is at the top left, but we want it to be % in the center, so shift the dots up and left, which just means % adding half of the screen width in pixel to both the x and y direction. - thisEvent.dot.positions = (dots.positions - cfg.screen.winWidth / 2)'; + thisEvent.dot.positions = (dots.positions - cfg.dot.matrixWidth / 2)'; %% make textures dotTexture('make', cfg, thisEvent); diff --git a/subfun/expDesign.m b/subfun/expDesign.m index fec3ffd..4e4d0c9 100644 --- a/subfun/expDesign.m +++ b/subfun/expDesign.m @@ -1,11 +1,16 @@ function [cfg] = expDesign(cfg, displayFigs) % Creates the sequence of blocks and the events in them % - % The conditions are consecutive static and motion blocks (Gives better results than randomised). + % The conditions are consecutive static and motion blocks + % (Gives better results than randomised). + % + % Style guide: constants are in SNAKE_UPPER_CASE % % EVENTS % The numEventsPerBlock should be a multiple of the number of "base" - % listed in the motionDirections and staticDirections (4 at the moment). + % listed in the MOTION_DIRECTIONS and STATIC_DIRECTIONS (4 at the moment). + % MOTION_DIRECTIONS = [0 90 180 270]; + % STATIC_DIRECTIONS = [-1 -1 -1 -1]; % % Pseudorandomization rules: % (1) Directions are all present in random orders in `numEventsPerBlock/nDirections` @@ -42,17 +47,18 @@ % - cfg.designFixationTargets = array (nr_blocks, numEventsPerBlock) % showing for each event if it should be accompanied by a target % - - + %% Check inputs - + % Set to 1 for a visualtion of the trials design order if nargin < 2 || isempty(displayFigs) displayFigs = 0; end - + % Set variables here for a dummy test of this function if nargin < 1 || isempty(cfg) +% cfg.design.motionType = 'translation'; + cfg.design.motionType = 'radial'; cfg.design.names = {'static'; 'motion'}; cfg.design.nbRepetitions = 4; cfg.design.nbEventsPerBlock = 12; @@ -60,124 +66,137 @@ cfg.target.maxNbPerBlock = 2; displayFigs = 1; end - + [NB_BLOCKS, NB_REPETITIONS, NB_EVENTS_PER_BLOCK, MAX_TARGET_PER_BLOCK] = getInput(cfg); - [~, staticIndex, motionIndex] = assignConditions(cfg); - + [~, STATIC_INDEX, MOTION_INDEX] = assignConditions(cfg); + RANGE_TARGETS = [1 MAX_TARGET_PER_BLOCK]; - targetPerCondition = repmat(RANGE_TARGETS, 1, NB_REPETITIONS/2); - + targetPerCondition = repmat(RANGE_TARGETS, 1, NB_REPETITIONS / 2); + numTargetsForEachBlock = zeros(1, NB_BLOCKS); - numTargetsForEachBlock(staticIndex) = shuffle(targetPerCondition); - numTargetsForEachBlock(motionIndex) = shuffle(targetPerCondition); - + numTargetsForEachBlock(STATIC_INDEX) = shuffle(targetPerCondition); + numTargetsForEachBlock(MOTION_INDEX) = shuffle(targetPerCondition); + %% Give the blocks the names with condition and design the task in each event while 1 - + fixationTargets = zeros(NB_BLOCKS, NB_EVENTS_PER_BLOCK); - + for iBlock = 1:NB_BLOCKS - + % Set target % - if there are 2 targets per block we make sure that they are at least % 2 events apart % - targets cannot be on the first or last event of a block % - no more than 2 target in the same event order - + chosenTarget = []; - + tmpTarget = numTargetsForEachBlock(iBlock); - + switch tmpTarget - + case 1 - + chosenTarget = randsample(2:NB_EVENTS_PER_BLOCK - 1, tmpTarget, false); - + case 2 - + targetDifference = 0; - + while any(targetDifference <= 2) chosenTarget = randsample(2:NB_EVENTS_PER_BLOCK - 1, tmpTarget, false); targetDifference = diff(chosenTarget); end - + end - + fixationTargets(iBlock, chosenTarget) = 1; - + end - + % Check rule 3 if max(sum(fixationTargets)) < 3 break end - + end - + %% Now we do the easy stuff cfg.design.blockNames = assignConditions(cfg); - + cfg.design.nbBlocks = NB_BLOCKS; - + cfg = setDirections(cfg); - + speeds = ones(NB_BLOCKS, NB_EVENTS_PER_BLOCK) * cfg.dot.speedPixPerFrame; cfg.design.speeds = speeds; - + cfg.design.fixationTargets = fixationTargets; - %% Plot - diplayDesign(cfg, displayFigs) - + diplayDesign(cfg, displayFigs); + end function cfg = setDirections(cfg) - - % CONSTANTS - % Set directions for static and motion condition - MOTION_DIRECTIONS = [0 90 180 270]; - STATIC_DIRECTIONS = [-1 -1 -1 -1]; - + + [MOTION_DIRECTIONS, STATIC_DIRECTIONS] = getDirectionBaseVectors(cfg); + [NB_BLOCKS, NB_REPETITIONS, NB_EVENTS_PER_BLOCK] = getInput(cfg); - - [~, staticIndex, motionIndex] = assignConditions(cfg); - + + [~, STATIC_INDEX, MOTION_INDEX] = assignConditions(cfg); + if mod(NB_EVENTS_PER_BLOCK, length(MOTION_DIRECTIONS)) ~= 0 error('Number of events/block not a multiple of number of motion/static direction'); end - + % initialize directions = zeros(NB_BLOCKS, NB_EVENTS_PER_BLOCK); - + % Create a vector for the static condition static_directions = repmat( ... STATIC_DIRECTIONS, ... 1, NB_EVENTS_PER_BLOCK / length(STATIC_DIRECTIONS)); - + for iMotionBlock = 1:NB_REPETITIONS - + % Check that we never have twice the same direction while 1 tmp = [ ... shuffle(MOTION_DIRECTIONS), ... shuffle(MOTION_DIRECTIONS), ... shuffle(MOTION_DIRECTIONS)]; - - if ~any(diff(tmp,[],2)==0) + + if ~any(diff(tmp, [], 2) == 0) break end end - + % Set motion direction and static order - directions(motionIndex(iMotionBlock), :) = tmp; - directions(staticIndex(iMotionBlock), :) = static_directions; - + directions(MOTION_INDEX(iMotionBlock), :) = tmp; + directions(STATIC_INDEX(iMotionBlock), :) = static_directions; + end - + cfg.design.directions = directions; + +end + +function [MOTION_DIRECTIONS, STATIC_DIRECTIONS] = getDirectionBaseVectors(cfg) + + % CONSTANTS + % Set directions for static and motion condition + + STATIC_DIRECTIONS = [-1 -1 -1 -1]; + + switch cfg.design.motionType + case 'translation' + MOTION_DIRECTIONS = [0 90 180 270]; + case 'radial' + STATIC_DIRECTIONS = [666 -666 666 -666]; + MOTION_DIRECTIONS = [666 -666 666 -666]; + end end @@ -188,16 +207,16 @@ nbBlocks = length(cfg.design.names) * nbRepet; end -function [condition, staticIndex, motionIndex] = assignConditions(cfg) - +function [condition, STATIC_INDEX, MOTION_INDEX] = assignConditions(cfg) + [~, nbRepet] = getInput(cfg); - + condition = repmat(cfg.design.names, nbRepet, 1); - + % Get the index of each condition - staticIndex = find(strcmp(condition, 'static')); - motionIndex = find(strcmp(condition, 'motion')); - + STATIC_INDEX = find(strcmp(condition, 'static')); + MOTION_INDEX = find(strcmp(condition, 'motion')); + end function shuffled = shuffle(unshuffled) @@ -210,67 +229,66 @@ end function diplayDesign(cfg, displayFigs) - + %% Visualize the design matrix if displayFigs - - close all + + close all; figure(1); - + % Shows blocks (static and motion) and events (motion direction) order directions = cfg.design.directions; directions(directions == -1) = -90; - + subplot(3, 1, 1); imagesc(directions); - + labelAxesBlock(); - + caxis([-90 - 37, 270 + 37]); myColorMap = lines(5); colormap(myColorMap); - + title('Block (static and motion) & Events (motion direction)'); - + % Shows the fixation targets design in each event (1 or 0) fixationTargets = cfg.design.fixationTargets; - + subplot(3, 1, 2); imagesc(fixationTargets); labelAxesBlock(); title('Fixation Targets design'); colormap(gray); - + % Shows the fixation targets position distribution in the block across % the experimet [~, itargetPosition] = find(fixationTargets == 1); - + subplot(3, 1, 3); hist(itargetPosition); labelAxesFreq(); title('Fixation Targets position distribution'); - - + figure(2); - MOTION_DIRECTIONS = [0 90 180 270]; - - for iMotion = 1:length(MOTION_DIRECTIONS) - - [~, position] = find(directions == MOTION_DIRECTIONS(iMotion)); - + [motionDirections] = getDirectionBaseVectors(cfg); + motionDirections = unique(motionDirections); + + for iMotion = 1:length(motionDirections) + + [~, position] = find(directions == motionDirections(iMotion)); + subplot(2, 2, iMotion); hist(position); scaleAxes(); labelAxesFreq(); - title(num2str(MOTION_DIRECTIONS(iMotion))); - + title(num2str(motionDirections(iMotion))); + end - end - + end function labelAxesBlock() diff --git a/visualLocTanslational.m b/visualLocTanslational.m index 0c17f6a..d50228a 100644 --- a/visualLocTanslational.m +++ b/visualLocTanslational.m @@ -33,6 +33,8 @@ %% Init the experiment [cfg] = initPTB(cfg); + cfg.dot.matrixWidth = cfg.screen.winHeight; + % Convert some values from degrees to pixels cfg.dot = degToPix('size', cfg.dot, cfg); cfg.dot = degToPix('speed', cfg.dot, cfg); @@ -44,7 +46,8 @@ % dots are displayed on a square with a length in visual angle equal to the % field of view - cfg.dot.number = round(cfg.dot.density * (cfg.screen.winWidth / cfg.screen.ppd)^2); + cfg.dot.number = round(cfg.dot.density * ... + (cfg.dot.matrixWidth / cfg.screen.ppd)^2); [el] = eyeTracker('Calibration', cfg); @@ -53,7 +56,7 @@ % Prepare for the output logfiles with all logFile.extraColumns = cfg.extraColumns; logFile = saveEventsFile('open', cfg, logFile); - + % prepare textures cfg = apertureTexture('init', cfg); cfg = dotTexture('init', cfg); @@ -99,6 +102,7 @@ thisEvent.event = iEvent; thisEvent.block = iBlock; + thisEvent.keyName = 'n/a'; thisEvent.duration = duration; thisEvent.onset = onset - cfg.experimentStart; @@ -113,32 +117,64 @@ % collect the responses and appends to the event structure for % saving in the tsv file - responseEvents = collectAndSaveResponses(cfg, logFile, cfg.experimentStart); - - responseEvents = getResponse('check', cfg.keyboard.responseBox, cfg, getOnlyPress); - + responseEvents = getResponse('check', cfg.keyboard.responseBox, cfg, ... + getOnlyPress); + if isfield(responseEvents(1), 'onset') && ~isempty(responseEvents(1).onset) - + for iResp = 1:size(responseEvents, 1) - - responseEvents(iResp).event = iEvent; - responseEvents(iResp).block = iBlock; + responseEvents(iResp).onset = ... + responseEvents(iResp).onset - cfg.experimentStart; + responseEvents(iResp).event = 'n/a'; + responseEvents(iResp).block = 'n/a'; + responseEvents(iResp).direction = 'n/a'; + responseEvents(iResp).speed = 'n/a'; + responseEvents(iResp).target = 'n/a'; + if strcmp(responseEvents(iResp).keyName, 't') + responseEvents(iResp).trial_type = 'trigger'; + end end - + + responseEvents(1).fileID = logFile.fileID; + responseEvents(1).extraColumns = logFile.extraColumns; saveEventsFile('save', cfg, responseEvents); - + end - + % wait for the inter-stimulus interval WaitSecs(cfg.ISI); - getResponse('flush', cfg.keyboard.responseBox); - end - + eyeTracker('StopRecordings', cfg); - + WaitSecs(cfg.IBI); + + % trigger monitoring + triggerEvents = getResponse('check', cfg.keyboard.responseBox, cfg, ... + getOnlyPress); + + if isfield(triggerEvents(1), 'onset') && ~isempty(triggerEvents(1).onset) + + for iResp = 1:size(triggerEvents, 1) + triggerEvents(iResp).onset = ... + triggerEvents(iResp).onset - cfg.experimentStart; + triggerEvents(iResp).event = 'n/a'; + triggerEvents(iResp).block = 'n/a'; + triggerEvents(iResp).direction = 'n/a'; + triggerEvents(iResp).speed = 'n/a'; + triggerEvents(iResp).target = 'n/a'; + if strcmp(triggerEvents(iResp).keyName, 't') + triggerEvents(iResp).trial_type = 'trigger-baseline'; + end + end + + triggerEvents(1).fileID = logFile.fileID; + triggerEvents(1).extraColumns = logFile.extraColumns; + saveEventsFile('save', cfg, triggerEvents); + + end + end @@ -154,16 +190,8 @@ getResponse('release', cfg.keyboard.responseBox); eyeTracker('Shutdown', cfg); - - % save the whole workspace - matFile = fullfile( ... - cfg.dir.output, ... - strrep(cfg.fileName.events, 'tsv', 'mat')); - if IsOctave - save(matFile, '-mat7-binary'); - else - save(matFile, '-v7.3'); - end + + createBoldJson(cfg, cfg) farewellScreen(cfg);