diff --git a/lib/CPP_BIDS b/lib/CPP_BIDS index 7d7c052..a17e12a 160000 --- a/lib/CPP_BIDS +++ b/lib/CPP_BIDS @@ -1 +1 @@ -Subproject commit 7d7c052f22d0288d4dd469ae97a81988fba390d3 +Subproject commit a17e12a53c0063e36fb017b7a545d605e2c2ef8e diff --git a/lib/CPP_PTB b/lib/CPP_PTB index 46f990f..a8bc6b5 160000 --- a/lib/CPP_PTB +++ b/lib/CPP_PTB @@ -1 +1 @@ -Subproject commit 46f990f18fe320984f71ae6ec1b02c12f604ad40 +Subproject commit a8bc6b5a1457ad78b496fa89037c07f0e548e0c8 diff --git a/setParameters.m b/setParameters.m index 93aa334..b4b031d 100644 --- a/setParameters.m +++ b/setParameters.m @@ -11,6 +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 @@ -32,10 +33,14 @@ cfg = setMRI(cfg); %% Experiment Design - cfg.names = {'static', 'motion'}; - cfg.possibleDirections = [-1 1]; % 1 motion , -1 static - cfg.numBlocks = size(cfg.possibleDirections, 2); - cfg.numRepetitions = 1; % AT THE MOMENT IT IS NOT SET IN THE MAIN SCRIPT + + cfg.design.names = {'static'; 'motion'}; + cfg.design.nbRepetitions = 4; + cfg.design.nbEventsPerBlock = 12; % DO NOT CHANGE + + %% Timing + + % Time between blocs in secs cfg.IBI = .5; % 8; % Time between events in secs cfg.ISI = 0.5; @@ -44,25 +49,20 @@ % Number of seconds after the end all the stimuli before ending the run cfg.endDelay = .1; - %% Visual Stimulation - - % Number of events per block (should not be changed) - cfg.numEventsPerBlock = 12; cfg.eventDuration = 1; % second + + %% Visual Stimulation - % speed in visual angles / second + % Speed in visual angles / second cfg.dot.speed = 15; % Coherence Level (0-1) cfg.dot.coherence = 1; - % nb dots per visual angle square. - cfg.dot.density = .25; - + % Number of dots per visual angle square. + cfg.dot.density = .1; % Dot life time in seconds cfg.dot.lifeTime = 10; - % proportion of dots killed per frame - cfg.dot.proportionKilledPerFrame = .05; - + cfg.dot.proportionKilledPerFrame = 0; % Dot Size (dot width) in visual angles. cfg.dot.size = 1; cfg.dot.color = cfg.color.white; @@ -92,6 +92,7 @@ cfg.target.duration = 0.05; % In secs cfg.extraColumns = {'direction', 'speed', 'target', 'event', 'block'}; + end function cfg = setKeyboards(cfg) diff --git a/subfun/doDotMo.m b/subfun/doDotMo.m index 1ba4ce3..487741f 100644 --- a/subfun/doDotMo.m +++ b/subfun/doDotMo.m @@ -16,7 +16,7 @@ %% Get parameters - dots = initializeDots(cfg, thisEvent); + dots = initDots(cfg, thisEvent); % Set for how many frames this event will last framesLeft = floor(cfg.eventDuration / cfg.screen.ifi); diff --git a/subfun/expDesign.m b/subfun/expDesign.m index 2d77451..fec3ffd 100644 --- a/subfun/expDesign.m +++ b/subfun/expDesign.m @@ -1,167 +1,291 @@ 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. - % - % It can be run as a stand alone without inputs to display a visual example of possible design. + % The conditions are consecutive static and motion blocks (Gives better results than randomised). % % EVENTS - % The numEventsPerBlock should be a multiple of the number of "base" - % listed in the motionDirections and staticDirections (4 at the moment). + % The numEventsPerBlock should be a multiple of the number of "base" + % listed in the motionDirections and staticDirections (4 at the moment). + % + % Pseudorandomization rules: + % (1) Directions are all present in random orders in `numEventsPerBlock/nDirections` + % consecutive chunks. This evenly distribute the directions across the + % block. + % (2) No same consecutive direction % % - % TARGETS: - % 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 + % TARGETS + % + % Pseudorandomization rules: + % (1) If there are 2 targets per block we make sure that they are at least 2 + % events apart. + % (2) Targets cannot be on the first or last event of a block. + % (3) Targets can not be present more than 2 times in the same event + % position across blocks. % % Input: - % - ExpParameters: parameters returned by SetParameters - % - displayFigs: a boolean to decide whether to show the basic design - % matrix of the design + % - cfg: parameters returned by setParameters + % - displayFigs: a boolean to decide whether to show the basic design + % matrix of the design % % Output: - % - ExpParameters.designBlockNames = cell array (nr_blocks, 1) with the - % name for each block + % - ExpParameters.designBlockNames = cell array (nr_blocks, 1) with the + % name for each block % - % - ExpParameters.designDirections = array (nr_blocks, numEventsPerBlock) - % with the direction to present in a given block - % - 0 90 180 270 indicate the angle - % - -1 indicates static + % - cfg.designDirections = array (nr_blocks, numEventsPerBlock) + % with the direction to present in a given block + % - 0 90 180 270 indicate the angle + % - -1 indicates static % - % - ExpParameters.designSpeeds = array (nr_blocks, numEventsPerBlock) * speedEvent + % - cfg.designSpeeds = array (nr_blocks, numEventsPerBlock) * speedEvent; % - % - ExpParameters.designFixationTargets = array (nr_blocks, numEventsPerBlock) - % showing for each event if it should be accompanied by a target + % - cfg.designFixationTargets = array (nr_blocks, numEventsPerBlock) + % showing for each event if it should be accompanied by a target % - - % Set directions for static and motion condition - motionDirections = [0 90 180 270]; - staticDirections = [0 90 180 270]; - % staticDirections = [-1 -1 -1 -1]; - + + %% 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.names = {'static', 'motion'}; - cfg.numRepetitions = 4; - cfg.dot.speed = 4; - cfg.numEventsPerBlock = 12; + cfg.design.names = {'static'; 'motion'}; + cfg.design.nbRepetitions = 4; + cfg.design.nbEventsPerBlock = 12; + cfg.dot.speedPixPerFrame = 4; cfg.target.maxNbPerBlock = 2; + displayFigs = 1; end - - % Set to 1 for a visualtion of the trials design order - if nargin < 2 || isempty(displayFigs) - displayFigs = 0; - end - - % Get the parameters - names = cfg.names; - numRepetitions = cfg.numRepetitions; - dotsSpeed = cfg.dot.speedPixPerFrame; - numEventsPerBlock = cfg.numEventsPerBlock; - maxNumFixTargPerBlock = cfg.target.maxNbPerBlock; - - if mod(numEventsPerBlock, length(motionDirections)) ~= 0 - warning('Number of events/block not a multiple of number of motion/static direction'); + + [NB_BLOCKS, NB_REPETITIONS, NB_EVENTS_PER_BLOCK, MAX_TARGET_PER_BLOCK] = getInput(cfg); + [~, staticIndex, motionIndex] = assignConditions(cfg); + + RANGE_TARGETS = [1 MAX_TARGET_PER_BLOCK]; + targetPerCondition = repmat(RANGE_TARGETS, 1, NB_REPETITIONS/2); + + numTargetsForEachBlock = zeros(1, NB_BLOCKS); + numTargetsForEachBlock(staticIndex) = shuffle(targetPerCondition); + numTargetsForEachBlock(motionIndex) = 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 - - %% Adapt some variables according to input - + + %% 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) + +end + +function cfg = setDirections(cfg) + + % CONSTANTS % Set directions for static and motion condition - motionDirections = repmat(motionDirections, 1, numEventsPerBlock / length(motionDirections)); - staticDirections = repmat(staticDirections, 1, numEventsPerBlock / length(staticDirections)); - - % Assign the conditions - condition = repmat(names, 1, numRepetitions); - nrBlocks = length(condition); + MOTION_DIRECTIONS = [0 90 180 270]; + STATIC_DIRECTIONS = [-1 -1 -1 -1]; + + [NB_BLOCKS, NB_REPETITIONS, NB_EVENTS_PER_BLOCK] = getInput(cfg); + + [~, staticIndex, motionIndex] = 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) + break + end + end + + % Set motion direction and static order + directions(motionIndex(iMotionBlock), :) = tmp; + directions(staticIndex(iMotionBlock), :) = static_directions; + + end + + cfg.design.directions = directions; + +end + +function [nbBlocks, nbRepet, nbEventsBlock, maxTargBlock] = getInput(cfg) + nbRepet = cfg.design.nbRepetitions; + nbEventsBlock = cfg.design.nbEventsPerBlock; + maxTargBlock = cfg.target.maxNbPerBlock; + nbBlocks = length(cfg.design.names) * nbRepet; +end + +function [condition, staticIndex, motionIndex] = 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')); - - % Assign the targets for each condition - rangeTargets = [1 maxNumFixTargPerBlock]; - % Get random number of targets for one condition - targetPerCondition = randi(rangeTargets, 1, numRepetitions); - % Assign the number of targets for each condition after shuffling - numTargets = zeros(1, nrBlocks); - numTargets(staticIndex) = Shuffle(targetPerCondition); - numTargets(motionIndex) = Shuffle(targetPerCondition); - - %% Give the blocks the names with condition - - cfg.design.blockNames = cell(nrBlocks, 1); - cfg.design.directions = zeros(nrBlocks, numEventsPerBlock); - cfg.design.speeds = ones(nrBlocks, numEventsPerBlock) * dotsSpeed; - cfg.design.fixationTargets = zeros(nrBlocks, numEventsPerBlock); - - for iMotionBlock = 1:numRepetitions - - cfg.design.directions(motionIndex(iMotionBlock), :) = Shuffle(motionDirections); - cfg.design.directions(staticIndex(iMotionBlock), :) = Shuffle(staticDirections); - - end - - for iBlock = 1:nrBlocks - - % Set block name - switch condition{iBlock} - case 'static' - thisBlockName = {'static'}; - case 'motion' - thisBlockName = {'motion'}; - end - cfg.design.blockNames(iBlock) = thisBlockName; - - % 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 - - chosenTarget = []; - - tmpTarget = numTargets(iBlock); - - switch tmpTarget - - case 1 - - chosenTarget = randsample(2:numEventsPerBlock - 1, tmpTarget, false); - - case 2 - - targetDifference = 0; - - while targetDifference <= 2 - chosenTarget = randsample(2:numEventsPerBlock - 1, tmpTarget, false); - targetDifference = (max(chosenTarget) - min(chosenTarget)); - end - - end - - cfg.design.fixationTargets(iBlock, chosenTarget) = 1; - + +end + +function shuffled = shuffle(unshuffled) + % in case PTB is not in the path + try + shuffled = Shuffle(unshuffled); + catch + shuffled = unshuffled(randperm(length(unshuffled))); end +end +function diplayDesign(cfg, displayFigs) + %% Visualize the design matrix if displayFigs - - uniqueNames = unique(cfg.design.blockNames) ; - - Ind = zeros(length(cfg.design.blockNames), length(uniqueNames)) ; - - for i = 1:length(uniqueNames) - CondInd(:, i) = find( ... - strcmp(cfg.design.blockNames, uniqueNames{i})) ; %#ok<*AGROW> - Ind(CondInd(:, i), i) = 1 ; + + 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)); + + subplot(2, 2, iMotion); + hist(position); + scaleAxes(); + labelAxesFreq(); + title(num2str(MOTION_DIRECTIONS(iMotion))); + end - - imagesc(Ind); - - set(gca, ... - 'XTick', 1:length(uniqueNames), ... - 'XTickLabel', uniqueNames); + end + +end + +function labelAxesBlock() + % an old viking saying because they really cared about their axes + ylabel('Block seq.', 'Fontsize', 8); + xlabel('Events', 'Fontsize', 8); +end + +function labelAxesFreq() + % an old viking saying because they really cared about their axes + ylabel('Number of targets', 'Fontsize', 8); + xlabel('Events', 'Fontsize', 8); +end + +function scaleAxes() + xlim([1 12]); + ylim([0 5]); +end diff --git a/visualLocTanslational.m b/visualLocTanslational.m index e14fd4c..0c17f6a 100644 --- a/visualLocTanslational.m +++ b/visualLocTanslational.m @@ -48,9 +48,7 @@ [el] = eyeTracker('Calibration', cfg); - % % % REFACTOR THIS FUNCTION [cfg] = expDesign(cfg); - % % % % Prepare for the output logfiles with all logFile.extraColumns = cfg.extraColumns; @@ -78,20 +76,20 @@ %% For Each Block - for iBlock = 1:cfg.numBlocks + for iBlock = 1:cfg.design.nbBlocks fprintf('\n - Running Block %.0f \n', iBlock); eyeTracker('StartRecording', cfg); % For each event in the block - for iEvent = 1:cfg.numEventsPerBlock + for iEvent = 1:cfg.design.nbEventsPerBlock % Check for experiment abortion from operator checkAbort(cfg, cfg.keyboard.keyboard); % set direction, speed of that event and if it is a target - thisEvent.trial_type = 'dummy'; + thisEvent.trial_type = cfg.design.blockNames{iBlock}; thisEvent.direction = cfg.design.directions(iBlock, iEvent); thisEvent.speed = cfg.design.speeds(iBlock, iEvent); thisEvent.target = cfg.design.fixationTargets(iBlock, iEvent);