diff --git a/.github/workflows/moxunit.yml b/.github/workflows/moxunit.yml index 16a9ee9..511d918 100644 --- a/.github/workflows/moxunit.yml +++ b/.github/workflows/moxunit.yml @@ -19,7 +19,7 @@ jobs: uses: joergbrech/moxunit-action@v1.1 with: tests: tests - src: src + src: subfun with_coverage: true cover_xml_file: coverage.xml - name: Code coverage diff --git a/README.md b/README.md index fda12ab..187ba53 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,34 @@ +[![](https://img.shields.io/badge/Octave-CI-blue?logo=Octave&logoColor=white)](https://github.com/cpp-lln-lab/localizer_visual_motion/actions) +![](https://github.com/cpp-lln-lab/localizer_visual_motion/workflows/CI/badge.svg) + +[![Build Status](https://travis-ci.com/cpp-lln-lab/localizer_visual_motion.svg?branch=master)](https://travis-ci.com/cpp-lln-lab/localizer_visual_motion) + + +* 1. [Requirements](#Requirements) +* 2. [Installation](#Installation) +* 3. [Structure and function details](#Structureandfunctiondetails) + * 3.1. [visualLocTranslational](#visualLocTranslational) + * 3.2. [setParameters](#setParameters) + * 3.3. [subfun/doDotMo](#subfundoDotMo) + * 3.3.1. [Input:](#Input:) + * 3.3.2. [Output:](#Output:) + * 3.4. [subfun/expDesign](#subfunexpDesign) + * 3.4.1. [EVENTS](#EVENTS) + * 3.4.2. [TARGETS:](#TARGETS:) + * 3.4.3. [Input:](#Input:-1) + * 3.4.4. [Output:](#Output:-1) + + + + # fMRI localizers for visual motion # Translational Motion -## Requirements +## 1. Requirements Make sure that the following toolboxes are installed and added to the matlab / octave path. @@ -16,7 +42,7 @@ For instructions see the following links: | [Matlab](https://www.mathworks.com/products/matlab.html) | >=2017 | | or [octave](https://www.gnu.org/software/octave/) | >=4.? | -## Installation +## 2. Installation The CPP_BIDS and CPP_PTB dependencies are already set up as submodule to this repository. You can install it all with git by doing. @@ -25,9 +51,9 @@ You can install it all with git by doing. git clone --recurse-submodules https://github.com/cpp-lln-lab/localizer_visual_motion.git ``` -## Structure and function details +## 3. Structure and function details -### visualLocTranslational +### 3.1. visualLocTranslational Running this script will show blocks of motion dots (soon also moving gratings) and static dots. Motion blocks will show dots(/gratings) moving in one of four directions (up-, down-, left-, and right-ward) @@ -35,7 +61,7 @@ By default it is run in `Debug mode` meaning that it does not run care about sub Any details of the experiment can be changed in `setParameters.m` (e.g., experiment mode, motion stimuli details, exp. design, etc.) -### setParameters +### 3.2. setParameters `setParameters.m` is the core engine of the experiment. It contains the following tweakable sections: @@ -51,34 +77,34 @@ Any details of the experiment can be changed in `setParameters.m` (e.g., experim - Instructions - Task #1 parameters -### subfun/doDotMo +### 3.3. subfun/doDotMo -#### Input: +#### 3.3.1. Input: - `cfg`: PTB/machine configurations returned by `setParameters` and `initPTB` - `expParameters`: parameters returned by `setParameters` - `logFile`: structure that stores the experiment logfile to be saved -#### Output: +#### 3.3.2. Output: - Event `onset` - Event `duration` The dots are drawn on a square that contains the round aperture, then any dots outside of the aperture is turned into a NaN so effectively the actual number of dots on the screen at any given time is not the one that you input but a smaller number (nDots / Area of aperture) on average. -### subfun/expDesign +### 3.4. subfun/expDesign 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. -#### EVENTS +#### 3.4.1. EVENTS The `numEventsPerBlock` should be a multiple of the number of "base" listed in the `motionDirections` and `staticDirections` (4 at the moment). -#### TARGETS: +#### 3.4.2. 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 -#### Input: +#### 3.4.3. Input: - `expParameters`: parameters returned by `setParameters` - `displayFigs`: a boolean to decide whether to show the basic design matrix of the design -#### Output: +#### 3.4.4. Output: - `expParameters.designBlockNames` is a cell array `(nr_blocks, 1)` with the name for each block - `expParameters.designDirections` is an array `(nr_blocks, numEventsPerBlock)` with the direction to present in a given block - `0 90 180 270` indicate the angle diff --git a/initEnv.m b/initEnv.m index bd40b17..ec6eefe 100644 --- a/initEnv.m +++ b/initEnv.m @@ -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 @@ -72,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) @@ -83,6 +75,25 @@ 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() diff --git a/lib/CPP_BIDS b/lib/CPP_BIDS index c23210f..febf3e6 160000 --- a/lib/CPP_BIDS +++ b/lib/CPP_BIDS @@ -1 +1 @@ -Subproject commit c23210f5b35faa1505ca2cc35c92c3168564f98a +Subproject commit febf3e648d088544421c4270f5a248eae1b8a8da diff --git a/lib/CPP_PTB b/lib/CPP_PTB index 7861863..e7be247 160000 --- a/lib/CPP_PTB +++ b/lib/CPP_PTB @@ -1 +1 @@ -Subproject commit 78618636295d4e52036d04c210f6ec5c6c4e68eb +Subproject commit e7be247a039cfe5ed95122d5045328f770023935 diff --git a/miss_hit.cfg b/miss_hit.cfg index 1ea7cd0..5a5f311 100644 --- a/miss_hit.cfg +++ b/miss_hit.cfg @@ -1,5 +1,11 @@ +# style guide (https://florianschanda.github.io/miss_hit/style_checker.html) line_length: 100 regex_function_name: "[a-z]+(([A-Z]){1}[A-Za-z]+)*" suppress_rule: "copyright_notice" exclude_dir: "lib" -exclude_dir: "Visual-loc_radial" \ No newline at end of file + +# 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/setParameters.m b/setParameters.m index 0b99eef..48dada5 100644 --- a/setParameters.m +++ b/setParameters.m @@ -39,6 +39,7 @@ % cfg.design.motionType = 'translation'; % cfg.design.motionType = 'radial'; cfg.design.motionType = 'translation'; + cfg.design.motionDirections = [0 0 180 180]; cfg.design.names = {'static'; 'motion'}; cfg.design.nbRepetitions = 10; cfg.design.nbEventsPerBlock = 12; % DO NOT CHANGE diff --git a/subfun/expDesign.m b/subfun/expDesign.m index 14a13b0..912915e 100644 --- a/subfun/expDesign.m +++ b/subfun/expDesign.m @@ -22,10 +22,10 @@ % TARGETS % % Pseudorandomization rules: - % (1) If there are 2 targets per block we make sure that they are at least 2 + % (1) If there are more than 1 target 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 + % (3) Targets can not be present more than NB_REPETITIONS - 1 times in the same event % position across blocks. % % Input: @@ -57,16 +57,20 @@ % Set variables here for a dummy test of this function if nargin < 1 || isempty(cfg) - error('give me something to work with') + error('give me something to work with'); end - - fprintf('\n\nCreating design.\n\n') + + 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); + if mod(NB_REPETITIONS, MAX_TARGET_PER_BLOCK) ~= 0 + error('number of repetitions must be a multiple of max number of targets'); + end + + 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); @@ -85,33 +89,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); - - case 2 - - targetDifference = 0; - - while abs(targetDifference) <= 2 - chosenTarget = randsample(2:NB_EVENTS_PER_BLOCK - 1, tmpTarget, false); - targetDifference = diff(chosenTarget); - end + nbTarget = numTargetsForEachBlock(iBlock); - end + chosenPosition = setTargetPositionInSequence( ... + NB_EVENTS_PER_BLOCK, ... + nbTarget, ... + [1 NB_EVENTS_PER_BLOCK]); - 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 @@ -150,26 +140,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 @@ -183,15 +164,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 @@ -202,25 +176,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_exDesign.m b/tests/test_exDesign.m deleted file mode 100644 index f5bdceb..0000000 --- a/tests/test_exDesign.m +++ /dev/null @@ -1,25 +0,0 @@ -function test_suite = test_exDesign %#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_checkCfgDefault() - - initEnv(); - - % cfg.design.motionType = 'translation'; - cfg.design.motionType = 'translation'; - cfg.design.names = {'static'; 'motion'}; - cfg.design.nbRepetitions = 10; - cfg.design.nbEventsPerBlock = 12; - cfg.dot.speedPixPerFrame = 4; - cfg.target.maxNbPerBlock = 1; - displayFigs = 1; - - [cfg] = expDesign(cfg, displayFigs) - - -end \ 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