diff --git a/.github/workflows/moxunit.yml b/.github/workflows/moxunit.yml new file mode 100644 index 0000000..511d918 --- /dev/null +++ b/.github/workflows/moxunit.yml @@ -0,0 +1,32 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + branches: '*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 1 + - name: MOxUnit Action + uses: joergbrech/moxunit-action@v1.1 + with: + tests: tests + src: subfun + with_coverage: true + cover_xml_file: coverage.xml + - name: Code coverage + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos + file: coverage.xml # optional + flags: unittests # optional + name: codecov-umbrella # optional + fail_ci_if_error: true # optional (default = false) diff --git a/audioLocTranslational.m b/audioLocTranslational.m index 41a867c..63e7262 100644 --- a/audioLocTranslational.m +++ b/audioLocTranslational.m @@ -40,14 +40,8 @@ [cfg] = initPTB(cfg); [el] = eyeTracker('Calibration', cfg); - - % % % REFACTOR THIS FUNCTION % % % - - [cfg] = expDesign(cfg); - cfg.design.blockNames - - % % % REFACTOR THIS FUNCTION % % % + [cfg] = expDesign(cfg); % Prepare for the output logfiles with all logFile.extraColumns = cfg.extraColumns; @@ -69,7 +63,7 @@ getResponse('start', cfg.keyboard.responseBox); - WaitSecs(cfg.timing.onsetDelay); + waitFor(cfg, cfg.timing.onsetDelay); %% For Each Block @@ -90,13 +84,19 @@ thisEvent.direction = cfg.design.directions(iBlock, iEvent); % thisEvent.speed = cfg.design.speeds(iBlock, iEvent); thisEvent.target = cfg.design.fixationTargets(iBlock, iEvent); + + % we wait for a trigger every 2 events + if cfg.pacedByTriggers.do && mod(iEvent, 2) == 1 + waitForTrigger( ... + cfg, ... + cfg.keyboard.responseBox, ... + cfg.pacedByTriggers.quietMode, ... + cfg.pacedByTriggers.nbTriggers); + end % % % REFACTOR THIS FUNCTION % % % - % play the sounds and collect onset and duration of the event - [onset, duration] = doAudMot(cfg, thisEvent); - - % % % REFACTOR THIS FUNCTION % % % + [onset, duration] = doAuditoryMotion(cfg, thisEvent); thisEvent.event = iEvent; thisEvent.block = iBlock; @@ -122,13 +122,13 @@ saveResponsesAndTriggers(responseEvents, cfg, logFile, triggerString); % wait for the inter-stimulus interval - WaitSecs(cfg.timing.ISI); + waitFor(cfg, cfg.timing.ISI); end eyeTracker('StopRecordings', cfg); - WaitSecs(cfg.timing.IBI); + waitFor(cfg, cfg.timing.IBI); % trigger monitoring triggerEvents = getResponse('check', cfg.keyboard.responseBox, cfg, ... @@ -140,7 +140,7 @@ end % End of the run for the BOLD to go down - WaitSecs(cfg.timing.endDelay); + waitFor(cfg, cfg.timing.endDelay); cfg = getExperimentEnd(cfg); diff --git a/initEnv.m b/initEnv.m index ff9214e..ec6eefe 100644 --- a/initEnv.m +++ b/initEnv.m @@ -8,7 +8,7 @@ % - struct % - statistics % -% MATLAB > R2017a +% MATLAB >= R2015b % % 2 - Add project to the O/M path @@ -17,6 +17,8 @@ octaveVersion = '4.0.3'; matlabVersion = '8.6.0'; + installlist = {'io', 'statistics', 'image'}; + if isOctave % Exit if min version is not satisfied @@ -24,27 +26,19 @@ error('Minimum required Octave version: %s', octaveVersion); end - installlist = {'statistics', 'image'}; for ii = 1:length(installlist) + + packageName = installlist{ii}; + try % Try loading Octave packages - disp(['loading ' installlist{ii}]); - pkg('load', installlist{ii}); + disp(['loading ' packageName]); + pkg('load', packageName); catch - errorcount = 1; - while errorcount % Attempt twice in case installation fails - try - pkg('install', '-forge', installlist{ii}); - pkg('load', installlist{ii}); - errorcount = 0; - catch err - errorcount = errorcount + 1; - if errorcount > 2 - error(err.message); - end - end - end + + tryInstallFromForge(packageName); + end end @@ -58,7 +52,9 @@ % If external dir is empty throw an exception % and ask user to update submodules. - if numel(dir('lib')) <= 2 % Means that the external is empty + libDirectory = fullfile(fileparts(mfilename('fullpath')), 'lib'); + + if numel(dir(libDirectory)) <= 2 % Means that the external is empty error(['Git submodules are not cloned!', ... 'Try this in your terminal:', ... ' git submodule update --recursive ']); @@ -70,10 +66,8 @@ end -%% -%% Return: true if the environment is Octave. -%% function retval = isOctave + % Return: true if the environment is Octave. persistent cacheval % speeds up repeated calls if isempty (cacheval) @@ -81,14 +75,32 @@ end retval = cacheval; + +end + +function tryInstallFromForge(packageName) + + errorcount = 1; + while errorcount % Attempt twice in case installation fails + try + pkg('install', '-forge', packageName); + pkg('load', packageName); + errorcount = 0; + catch err + errorcount = errorcount + 1; + if errorcount > 2 + error(err.message); + end + end + end + end function addDependencies() pth = fileparts(mfilename('fullpath')); addpath(genpath(fullfile(pth, 'lib', 'CPP_BIDS', 'src'))); - addpath(fullfile(pth, 'lib', 'CPP_PTB')); + addpath(genpath(fullfile(pth, 'lib', 'CPP_PTB', 'src'))); addpath(fullfile(pth, 'subfun')); - addpath(fullfile(pth, 'input')); end diff --git a/lib/CPP_BIDS b/lib/CPP_BIDS index c6659f7..962c947 160000 --- a/lib/CPP_BIDS +++ b/lib/CPP_BIDS @@ -1 +1 @@ -Subproject commit c6659f74e995ab870336bc266fe849c554a9f044 +Subproject commit 962c947fe38094da9561eeba5daa44993505f2c0 diff --git a/lib/CPP_PTB b/lib/CPP_PTB index ea8369a..e7be247 160000 --- a/lib/CPP_PTB +++ b/lib/CPP_PTB @@ -1 +1 @@ -Subproject commit ea8369a359c52726ff17b8627a1b3d57c5d96075 +Subproject commit e7be247a039cfe5ed95122d5045328f770023935 diff --git a/setParameters.m b/setParameters.m index 5ea568d..42ed4a6 100644 --- a/setParameters.m +++ b/setParameters.m @@ -31,6 +31,8 @@ % MRI settings cfg = setMRI(cfg); + + cfg.pacedByTriggers.do = true; %% Experiment Design @@ -38,9 +40,8 @@ % cfg.design.motionType = 'radial'; cfg.design.motionType = 'translation'; cfg.design.names = {'static'; 'motion'}; - cfg.design.possibleDirections = [-1 1]; % 1 motion , -1 static %NOT IN USE AT THE MOMENT -% cfg.design.nbBlocks = size(cfg.design.names, 2); % TO CHECK - cfg.design.nbRepetitions = 14; % AT THE MOMENT IT IS NOT SET IN THE MAIN SCRIPT + cfg.design.motionDirections = [-1 -1 1 1]; + cfg.design.nbRepetitions = 14; cfg.design.nbEventsPerBlock = 12; %% Timing @@ -51,15 +52,35 @@ % IBI % block length = (cfg.eventDuration + cfg.ISI) * cfg.design.nbEventsPerBlock + cfg.timing.eventDuration = 0.850; % second + % Time between blocs in secs - cfg.timing.IBI = 1.8; % 8; + cfg.timing.IBI = 1.8; % Time between events in secs cfg.timing.ISI = 0; % Number of seconds before the motion stimuli are presented - cfg.timing.onsetDelay = .1; + cfg.timing.onsetDelay = 0; % Number of seconds after the end all the stimuli before ending the run cfg.timing.endDelay = 3.6; + % reexpress those in terms of repetition time + if cfg.pacedByTriggers.do + + cfg.pacedByTriggers.quietMode = true; + cfg.pacedByTriggers.nbTriggers = 1; + + cfg.timing.eventDuration = cfg.mri.repetitionTime / 2 - 0.04; % second + + % Time between blocs in secs + cfg.timing.IBI = 1; + % Time between events in secs + cfg.timing.ISI = 0; + % Number of seconds before the motion stimuli are presented + cfg.timing.onsetDelay = 0; + % Number of seconds after the end all the stimuli before ending the run + cfg.timing.endDelay = 2; + end + %% Auditory Stimulation cfg.audio.channels = 2; @@ -80,7 +101,7 @@ cfg.fixation.xDisplacement = 0; cfg.fixation.yDisplacement = 0; - cfg.target.maxNbPerBlock = 0; + cfg.target.maxNbPerBlock = 2; cfg.target.duration = 0.5; % In secs cfg.extraColumns = {'direction', 'speed', 'target', 'event', 'block', 'keyName'}; diff --git a/subfun/doAudMot.m b/subfun/doAuditoryMotion.m similarity index 72% rename from subfun/doAudMot.m rename to subfun/doAuditoryMotion.m index 09da146..cd3c364 100644 --- a/subfun/doAudMot.m +++ b/subfun/doAuditoryMotion.m @@ -1,4 +1,4 @@ -function [onset, duration] = doAudMot(cfg, thisEvent) +function [onset, duration] = doAuditoryMotion(cfg, thisEvent) % Play the auditopry stimulation of moving in 4 directions or static noise bursts % @@ -11,48 +11,36 @@ % - thisEvent: structure that the parameters regarding the event to present % % Output: - % - + % - onset in machine time + % - duration in seconds % %% Get parameters - sound = []; - direction = thisEvent.direction(1); isTarget = thisEvent.target(1); targetDuration = cfg.target.duration; soundData = cfg.soundData; - % if isTarget == 0 + switch direction + case -1 + fieldName = 'S'; + case 90 + fieldName = 'U'; + case 270 + fieldName = 'D'; + case 0 + fieldName = 'R'; + case 180 + fieldName = 'L'; + end - if direction == -1 - sound = soundData.S; - elseif direction == 90 - sound = soundData.U; - elseif direction == 270 - sound = soundData.D; - elseif direction == 0 - sound = soundData.R; - elseif direction == 180 - sound = soundData.L; + if isTarget == 1 + fieldName = [fieldName '_T']; end - % elseif isTarget == 1 - % - % if direction == -1 - % sound = soundData.S_T; - % elseif direction == 90 - % sound = soundData.U_T; - % elseif direction == 270 - % sound = soundData.D_T; - % elseif direction == 0 - % sound = soundData.R_T; - % elseif direction == 180 - % sound = soundData.L_T; - % end - % - % end + sound = soundData.(fieldName); % Start the sound presentation PsychPortAudio('FillBuffer', cfg.audio.pahandle, sound); diff --git a/subfun/expDesign.m b/subfun/expDesign.m index ae39615..091f170 100644 --- a/subfun/expDesign.m +++ b/subfun/expDesign.m @@ -57,23 +57,16 @@ % Set variables here for a dummy test of this function if nargin < 1 || isempty(cfg) - % cfg.design.motionType = 'translation'; - cfg.design.motionType = 'translation'; - cfg.design.names = {'static'; 'motion'}; - cfg.design.nbRepetitions = 16; - cfg.design.nbEventsPerBlock = 12; - cfg.target.maxNbPerBlock = 0; - displayFigs = 1; + error('give me something to work with'); end fprintf('\n\nCreating design.\n\n') - [NB_BLOCKS, NB_REPETITIONS, NB_EVENTS_PER_BLOCK, MAX_TARGET_PER_BLOCK] = getInput(cfg); [~, STATIC_INDEX, MOTION_INDEX] = assignConditions(cfg); - RANGE_TARGETS = [1 MAX_TARGET_PER_BLOCK]; - targetPerCondition = repmat(RANGE_TARGETS, 1, NB_REPETITIONS / 2); + RANGE_TARGETS = 1:MAX_TARGET_PER_BLOCK; + targetPerCondition = repmat(RANGE_TARGETS, 1, NB_REPETITIONS / MAX_TARGET_PER_BLOCK); numTargetsForEachBlock = zeros(1, NB_BLOCKS); numTargetsForEachBlock(STATIC_INDEX) = shuffle(targetPerCondition); @@ -92,33 +85,19 @@ % - 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); + nbTarget = numTargetsForEachBlock(iBlock); - case 2 + chosenPosition = setTargetPositionInSequence( ... + NB_EVENTS_PER_BLOCK, ... + nbTarget, ... + [1 NB_EVENTS_PER_BLOCK]); - 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; + fixationTargets(iBlock, chosenPosition) = 1; end % Check rule 3 - if max(sum(fixationTargets)) < 3 + if max(sum(fixationTargets)) < NB_REPETITIONS - 1 break end @@ -154,26 +133,17 @@ directions = zeros(NB_BLOCKS, NB_EVENTS_PER_BLOCK); % Create a vector for the static condition + NB_REPEATS_BASE_VECTOR = NB_EVENTS_PER_BLOCK / length(STATIC_DIRECTIONS); + static_directions = repmat( ... STATIC_DIRECTIONS, ... - 1, NB_EVENTS_PER_BLOCK / length(STATIC_DIRECTIONS)); + 1, NB_REPEATS_BASE_VECTOR); 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(MOTION_INDEX(iMotionBlock), :) = tmp; + directions(MOTION_INDEX(iMotionBlock), :) = ... + repeatShuffleConditions(MOTION_DIRECTIONS, NB_REPEATS_BASE_VECTOR); directions(STATIC_INDEX(iMotionBlock), :) = static_directions; end @@ -187,15 +157,8 @@ % CONSTANTS % Set directions for static and motion condition - STATIC_DIRECTIONS = [-1 -1 -1 -1]; - - switch cfg.design.motionType - case 'translation' - MOTION_DIRECTIONS = [0 0 180 180]; - case 'radial' - STATIC_DIRECTIONS = [666 -666 666 -666]; - MOTION_DIRECTIONS = [666 -666 666 -666]; - end + MOTION_DIRECTIONS = cfg.design.motionDirections; + STATIC_DIRECTIONS = repmat(-1, size(MOTION_DIRECTIONS)); end @@ -206,25 +169,16 @@ nbBlocks = length(cfg.design.names) * nbRepet; end -function [condition, STATIC_INDEX, MOTION_INDEX] = assignConditions(cfg) +function [conditionNamesVector, STATIC_INDEX, MOTION_INDEX] = assignConditions(cfg) [~, nbRepet] = getInput(cfg); - condition = repmat(cfg.design.names, nbRepet, 1); + conditionNamesVector = repmat(cfg.design.names, nbRepet, 1); % Get the index of each condition - STATIC_INDEX = find(strcmp(condition, 'static')); - MOTION_INDEX = find(strcmp(condition, 'motion')); - -end + STATIC_INDEX = find(strcmp(conditionNamesVector, 'static')); + MOTION_INDEX = find(strcmp(conditionNamesVector, 'motion')); -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) diff --git a/tests/miss_hit.cfg b/tests/miss_hit.cfg new file mode 100644 index 0000000..f59bdb0 --- /dev/null +++ b/tests/miss_hit.cfg @@ -0,0 +1,10 @@ +# style guide (https://florianschanda.github.io/miss_hit/style_checker.html) +line_length: 100 +regex_function_name: "((test_[a-z]+)|[a-z]+)(([A-Z]){1}[A-Za-z]+)*" +suppress_rule: "copyright_notice" + +# metrics limit for the code quality (https://florianschanda.github.io/miss_hit/metrics.html) +metric "cnest": limit 4 +metric "file_length": limit 500 +metric "cyc": limit 15 +metric "parameters": limit 5 \ No newline at end of file diff --git a/tests/test_expDesign.m b/tests/test_expDesign.m new file mode 100644 index 0000000..a224c55 --- /dev/null +++ b/tests/test_expDesign.m @@ -0,0 +1,75 @@ +function test_suite = test_expDesign %#ok<*STOUT> + try % assignment of 'localfunctions' is necessary in Matlab >= 2016 + test_functions = localfunctions(); %#ok<*NASGU> + catch % no problem; early Matlab versions can use initTestSuite fine + end + initTestSuite; +end + +function test_exDesignBasic() + + initEnv(); + + cfg.design.motionType = 'translation'; + cfg.design.motionDirections = [0 0 180 180]; + cfg.design.names = {'static'; 'motion'}; + cfg.design.nbRepetitions = 6; + cfg.design.nbEventsPerBlock = 12; + cfg.dot.speedPixPerFrame = 4; + cfg.target.maxNbPerBlock = 3; + displayFigs = 0; + + [cfg] = expDesign(cfg, displayFigs); + + % make sure we don't have the same directions one after the other + directions = cfg.design.directions(strcmp(cfg.design.blockNames, 'motion'), :); + repeatedDirections = all(diff(directions, [], 2) == 0); + assertTrue(all(repeatedDirections == 0)); + + % make sure that we have the right number of blocks of the right length + assertTrue(all(size(cfg.design.directions) == [ ... + cfg.design.nbRepetitions * numel(cfg.design.names), ... + cfg.design.nbEventsPerBlock])); + + % check that we do not have more than the required number of targets per + % block + assertTrue(all(sum(cfg.design.fixationTargets, 2) <= cfg.target.maxNbPerBlock)); + + % make sure that targets are not presented too often in the same position + assertTrue(all(sum(cfg.design.fixationTargets) < cfg.design.nbRepetitions - 1)); + +end + +function test_exDesignBasicOtherSetUp() + + initEnv(); + + cfg.design.motionType = 'translation'; + cfg.design.motionDirections = [0 90 180 270]; + cfg.design.names = {'static'; 'motion'}; + cfg.design.nbRepetitions = 9; + cfg.design.nbEventsPerBlock = 8; + cfg.dot.speedPixPerFrame = 4; + cfg.target.maxNbPerBlock = 3; + displayFigs = 0; + + [cfg] = expDesign(cfg, displayFigs); + + % make sure we don't have the same directions one after the other + directions = cfg.design.directions(strcmp(cfg.design.blockNames, 'motion'), :); + repeatedDirections = all(diff(directions, [], 2) == 0); + assertTrue(all(repeatedDirections == 0)); + + % make sure that we have the right number of blocks of the right length + assertTrue(all(size(cfg.design.directions) == [ ... + cfg.design.nbRepetitions * numel(cfg.design.names), ... + cfg.design.nbEventsPerBlock])); + + % check that we do not have more than the required number of targets per + % block + assertTrue(all(sum(cfg.design.fixationTargets, 2) <= cfg.target.maxNbPerBlock)); + + % make sure that targets are not presented too often in the same position + assertTrue(all(sum(cfg.design.fixationTargets) < cfg.design.nbRepetitions - 1)); + +end