diff --git a/.github/workflows/check_md_links.yml b/.github/workflows/check_md_links.yml new file mode 100644 index 0000000..c54aac1 --- /dev/null +++ b/.github/workflows/check_md_links.yml @@ -0,0 +1,18 @@ +name: Check Markdown links + +# checking for any dead links in markdown files + +on: + push: + branches: + - master + - dev + pull_request: + branches: '*' + +jobs: + markdown-link-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: gaurav-nelson/github-action-markdown-link-check@v1 \ No newline at end of file diff --git a/.github/workflows/miss_hit.yml b/.github/workflows/miss_hit.yml new file mode 100644 index 0000000..d4cb7ae --- /dev/null +++ b/.github/workflows/miss_hit.yml @@ -0,0 +1,39 @@ +name: miss_hit + +on: + push: + branches: + - master + - dev + pull_request: + branches: '*' + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + + - uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 1 + + - name: Set up Python 3.6 + uses: actions/setup-python@v2 + with: + python-version: 3.6 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip setuptools + pip3 install install miss_hit + + - name: Miss_hit code quality + run: | + mh_metric . --ci + + - name: Miss_hit code style + run: | + mh_style . diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6e0d1ce --- /dev/null +++ b/.prettierrc @@ -0,0 +1,13 @@ +{ + "parser": "markdown", + "proseWrap": "always", + "tabWidth": 2, + "overrides": [ + { + "files": "*.md", + "options": { + "tabWidth": 4 + } + } + ] +} diff --git a/.remarkrc b/.remarkrc new file mode 100644 index 0000000..201ce70 --- /dev/null +++ b/.remarkrc @@ -0,0 +1,11 @@ +{ + "plugins": [ + "preset-lint-consistent", + "preset-lint-markdown-style-guide", + "preset-lint-recommended", + ["lint-no-duplicate-headings", false], + ["lint-list-item-indent", "tab-size"], + ["lint-maximum-line-length", true], + ["lint-maximum-heading-length", false] + ] +} diff --git a/.travis.yml b/.travis.yml index c05ae32..17da21f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,22 +6,26 @@ dist: bionic # Language and version -language: python -python: - - "3.6" # current default Python on Travis CI +language: node_js +node_js: + - "10" cache: apt: true # only works with Pro version + directories: + - node_modules # NPM packages for the remark markdown linter -# Install the miss_hit linter -before_install: - - pip3 install miss_hit +branches: + only: # only run the CI for those branches + - master + - dev # Lists all the tasks we will do jobs: include: - - name: "miss_hit: checking code quality" - script: mh_metric . --ci - - name: "miss_hit: checking code style" - script: mh_style . + - name: "Check markdown" + before_script: + - npm install `cat npm-requirements.txt` + script: + - remark . --frail \ No newline at end of file diff --git a/README.md b/README.md index 869144b..adb0413 100644 --- a/README.md +++ b/README.md @@ -6,52 +6,65 @@ [![codecov](https://codecov.io/gh/cpp-lln-lab/CPP_PTB/branch/master/graph/badge.svg)](https://codecov.io/gh/cpp-lln-lab/CPP_PTB) + [![All Contributors](https://img.shields.io/badge/all_contributors-3-orange.svg?style=flat-square)](#contributors-) + # CPP_PTB + + -- [CPP_PTB](#cpp_ptb) - - [Requirements](#requirements) - - [Documentation](#documentation) - - [Content](#content) - - [How to install](#how-to-install) - - [Download with git](#download-with-git) - - [Add as a submodule](#add-as-a-submodule) - - [Example for submodule usage](#example-for-submodule-usage) - - [Direct download](#direct-download) - - [Add CPP_PTB globally to the matlab path](#add-cpp_ptb-globally-to-the-matlab-path) - - [Code style guide](#code-style-guide) - - [Unit tests](#unit-tests) - - [Contributors ✨](#contributors-) +- [CPP_PTB](#cpp_ptb) + - [Requirements](#requirements) + - [Documentation](#documentation) + - [Content](#content) + - [How to install](#how-to-install) + - [Download with git](#download-with-git) + - [Add as a submodule](#add-as-a-submodule) + - [Example for submodule usage](#example-for-submodule-usage) + - [Direct download](#direct-download) + - [Add CPP_PTB globally to the matlab path](#add-cpp_ptb-globally-to-the-matlab-path) + - [Code style guide](#code-style-guide) + - [Unit tests](#unit-tests) + - [Contributors ✨](#contributors-) + -This is the Crossmodal Perception and Plasticity lab (CPP) PsychToolBox (PTB) toolbox. +This is the Crossmodal Perception and Plasticity lab (CPP) PsychToolBox (PTB) +toolbox. -Those functions are mostly wrappers around some PTB functions to facilitate their use and their reuse (#DontRepeatYourself) +Those functions are mostly wrappers around some PTB functions to facilitate +their use and their reuse (#DontRepeatYourself) ## Requirements -Make sure that the following toolboxes are installed and added to the matlab / octave path. +Make sure that the following toolboxes are installed and added to the matlab / +octave path. For instructions see the following links: + + | Requirements | Used version | -|----------------------------------------------------------|--------------| +| -------------------------------------------------------- | ------------ | | [PsychToolBox](http://psychtoolbox.org/) | >=3.0.14 | | [Matlab](https://www.mathworks.com/products/matlab.html) | >=2015b | | or [Octave](https://www.gnu.org/software/octave/) | 4.? | + + Tested: + - matlab 2015b or octave 4.2.2 and PTB 3.0.14. ## Documentation -All the documentation is accessible [here](./docs/00_index.md). +All the documentation is accessible [here](./docs/00-index.md). ## Content @@ -75,7 +88,7 @@ All the documentation is accessible [here](./docs/00_index.md). ### Download with git -``` bash +```bash cd fullpath_to_directory_where_to_install # use git to download the code git clone https://github.com/cpp-lln-lab/CPP_PTB.git @@ -84,12 +97,15 @@ cd CPP_PTB ``` Then get the latest commit to stay up to date: + ```bash # from the directory where you downloaded the code git pull origin master ``` -To work with a specific version, create a branch at a specific version tag number +To work with a specific version, create a branch at a specific version tag +number + ```bash # creating and checking out a branch that will be called version1 at the version tag v1.0.0 git checkout -b version1 v1.0.0 @@ -99,14 +115,15 @@ git checkout -b version1 v1.0.0 Add it as a submodule in the repo you are working on. -``` bash +```bash cd fullpath_to_directory_where_to_install # use git to download the code git submodule add https://github.com/cpp-lln-lab/CPP_PTB.git ``` -To get the latest commit you then need to update the submodule with the information -on its remote repository and then merge those locally. +To get the latest commit you then need to update the submodule with the +information on its remote repository and then merge those locally. + ```bash git submodule update --remote --merge ``` @@ -115,10 +132,14 @@ Remember that updates to submodules need to be committed as well. #### Example for submodule usage -So say you want to clone a repo that has some nested submodules, then you would type this to get the content of all the submodules at once (here with my experiment repo): -``` bash +So say you want to clone a repo that has some nested submodules, then you would +type this to get the content of all the submodules at once (here with my +experiment repo): + +```bash git clone --recurse-submodules https://github.com/user_name/yourExperiment.git ``` + This would be the way to do it "by hand" ```bash @@ -141,19 +162,20 @@ git submodule foreach --recursive 'git submodule update' Download the code. Unzip. And add to the matlab path. -Pick a specific version: - -https://github.com/cpp-lln-lab/CPP_PTB/releases +Pick a specific version from +[here](https://github.com/cpp-lln-lab/CPP_PTB/releases). -Or take the latest commit (NOT RECOMMENDED): - -https://github.com/cpp-lln-lab/CPP_PTB/archive/master.zip +Or take +[the latest commit](https://github.com/cpp-lln-lab/CPP_PTB/archive/master.zip) - +NOT RECOMMENDED. ### Add CPP_PTB globally to the matlab path -This is NOT RECOMMENDED as this might create conflicts if you use different versions of CPP_PTB as sub-modules. +This is NOT RECOMMENDED as this might create conflicts if you use different +versions of CPP_PTB as sub-modules. -Also note that this might not work at all if you have not set a command line alias to start Matlab from a terminal window by just typing `matlab`. :wink: +Also note that this might not work at all if you have not set a command line +alias to start Matlab from a terminal window by just typing `matlab`. :wink: ```bash # from within the CPP_PTB folder @@ -162,34 +184,49 @@ matlab -nojvm -nosplash -r "addpath(genpath(fullfile(pwd, 'src'))); savepath(); ## Code style guide -We use the `camelCase` to more easily differentiates our functions from the ones from PTB that use a `PascalCase`. +We use the `camelCase` to more easily differentiates our functions from the ones +from PTB that use a `PascalCase`. -In practice, we use the following regular expression for function names: `[a-z]+(([A-Z]|[0-9]){1}[a-z]+)*`. +In practice, we use the following regular expression for function names: +`[a-z]+(([A-Z]|[0-9]){1}[a-z]+)*`. > Regular expressions look scary but are SUPER useful to sort through filenames: -> - A quick [intro to regular expression](https://www.rexegg.com/) -> - And many websites allow you to "design and test" your regular expression: -> - [regexr](https://regexr.com/) -> - [regexper](https://regexper.com/#%5Ba-z%5D%2B%28%28%5BA-Z%5D%7C%5B0-9%5D%29%7B1%7D%5Ba-z%5D%2B%29) -> - ... - -We keep the McCabe complexity below 15 as reported by the [check_my_code function](https://github.com/Remi-Gau/check_my_code) or the [MISS_HIT code checker](https://florianschanda.github.io/miss_hit). A couple of code quality metrics are also checked automatically by MISS_HIT (avoiding functions with too many nested `if` blocks). - -We use the [MISS_HIT linter](https://florianschanda.github.io/miss_hit/style_checker.html) to automatically fix some linting issues. - -The code style and quality is also checked during the [continuous integration](./.travis.yml). +> +> - A quick [intro to regular expression](https://www.rexegg.com/) +> +> - And many websites allow you to "design and test" your regular expression: +> - [regexper](https://regexper.com/#%5Ba-z%5D%2B%28%28%5BA-Z%5D%7C%5B0-9%5D%29%7B1%7D%5Ba-z%5D%2B%29) +> - ... + +We keep the McCabe complexity below 15 as reported by the +[check_my_code function](https://github.com/Remi-Gau/check_my_code) or the +[MISS_HIT code checker](https://florianschanda.github.io/miss_hit). A couple of +code quality metrics are also checked automatically by MISS_HIT (avoiding +functions with too many nested `if` blocks). + +We use the +[MISS_HIT linter](https://florianschanda.github.io/miss_hit/style_checker.html) +to automatically fix some linting issues. + +The code style and quality is also checked during the +[continuous integration](./.travis.yml). ## Unit tests -Unit tests are run with the mox unit toolbox and automated with github action on Octave. +Unit tests are run with the mox unit toolbox and automated with github action on +Octave. ## Contributors ✨ -Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): +Thanks goes to these wonderful people +([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + @@ -199,7 +236,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d

Remi Gau

💻 🎨 📖 🐛 📓 🤔 🚇 🚧 ⚠️ 💬
+ + -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! +This project follows the +[all-contributors](https://github.com/all-contributors/all-contributors) +specification. Contributions of any kind welcome! diff --git a/demos/CPP_checkAbortDemo.m b/demos/CPP_checkAbortDemo.m index 4d79b65..8ca7dee 100644 --- a/demos/CPP_checkAbortDemo.m +++ b/demos/CPP_checkAbortDemo.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + % add parent directory to the path (to make sure we can access the CPP_PTB % functions) addpath(fullfile(pwd, '..')); diff --git a/demos/CPP_getResponseDemo.m b/demos/CPP_getResponseDemo.m index 6c4dc66..2969827 100644 --- a/demos/CPP_getResponseDemo.m +++ b/demos/CPP_getResponseDemo.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + %% Demo showing how to use the getResponse function % This small script shows how to use the getReponse function @@ -103,9 +105,9 @@ end fprintf('\n %s was %s at time %.3f seconds\n', ... - responseEvents(iEvent).keyName, ... - eventType, ... - responseEvents(iEvent).onset - startSecs); + responseEvents(iEvent).keyName, ... + eventType, ... + responseEvents(iEvent).onset - startSecs); end diff --git a/demos/CPP_pressSpaceForMeDemo.m b/demos/CPP_pressSpaceForMeDemo.m index 636ad77..d7470a8 100644 --- a/demos/CPP_pressSpaceForMeDemo.m +++ b/demos/CPP_pressSpaceForMeDemo.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + % add parent directory to the path (to make sure we can access the CPP_PTB % functions) addpath(fullfile(pwd, '..')); diff --git a/demos/CPP_waitForTriggerDemo.m b/demos/CPP_waitForTriggerDemo.m index abbbcdc..a7db9b2 100644 --- a/demos/CPP_waitForTriggerDemo.m +++ b/demos/CPP_waitForTriggerDemo.m @@ -1,21 +1,19 @@ +% (C) Copyright 2020 CPP_PTB developers + +% add parent/src directory to the path (to make sure we can access the CPP_PTB functions) addpath(genpath(fullfile(pwd, '..', 'src'))); -%% cfg.testingDevice = 'mri'; -cfg.mri.triggerNb = 2; - -cfg.mri.triggerKey = 'space'; +cfg.mri.triggerNb = 5; +cfg.mri.triggerKey = 't'; KbName('UnifyKeyNames'); -%% -% waitForTrigger(cfg); +quietMode = false; + +fprintf(1, 'Press the letter %s %i times, please.\n', cfg.mri.triggerKey, cfg.mri.triggerNb); -%% -quietMode = true; -% waitForTrigger(cfg, [], quietMode); +lastTriggerTimeStamp = waitForTrigger(cfg, [], quietMode, cfg.mri.triggerNb); -%% -nbTriggersToWait = 1; -waitForTrigger(cfg, [], quietMode, nbTriggersToWait); +fprintf(1, 'Thank you. The time stamp of the last trigger was %f.\n', lastTriggerTimeStamp); diff --git a/demos/miss_hit.cfg b/demos/miss_hit.cfg new file mode 100644 index 0000000..0333c2c --- /dev/null +++ b/demos/miss_hit.cfg @@ -0,0 +1,12 @@ +# style guide (https://florianschanda.github.io/miss_hit/style_checker.html) +line_length: 100 +regex_function_name: "CPP_[a-z]+(([A-Z]){1}[A-Za-z]+)*" +copyright_entity: "Sam Schwarzkopf" +copyright_entity: "Agah Karakuzu" +copyright_entity: "CPP_PTB developers" + +# 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/dev/devSandbox.m b/dev/devSandbox.m index 637830a..48dd61e 100644 --- a/dev/devSandbox.m +++ b/dev/devSandbox.m @@ -256,11 +256,11 @@ InitializePsychSound(1); cfg.audio.pahandle = PsychPortAudio('Open', ... - [], ... - [], ... - [], ... - cfg.audio.fs, ... - cfg.audio.channels); + [], ... + [], ... + [], ... + cfg.audio.fs, ... + cfg.audio.channels); end end diff --git a/docs/00_index.md b/docs/00-index.md similarity index 62% rename from docs/00_index.md rename to docs/00-index.md index 389d190..9235f06 100644 --- a/docs/00_index.md +++ b/docs/00-index.md @@ -1,32 +1,42 @@ # CPP_PTB documentation - -* 1. [the CFG structure](#theCFGstructure) -* 2. [Setting up keyboards](#Settingupkeyboards) -* 3. [functions descriptions](#functionsdescriptions) -* 4. [Annexes](#Annexes) - * 4.1. [Experiment template [ WIP ]](#ExperimenttemplateWIP) - * 4.2. [devSandbox (stand-alone)](#devSandboxstand-alone) + - - + -## 1. the CFG structure +- [CPP_PTB documentation](#cpp_ptb-documentation) + - [the CFG structure](#the-cfg-structure) + - [Setting up keyboards](#setting-up-keyboards) + - [functions descriptions](#functions-descriptions) + - [Annexes](#annexes) + - [Experiment template [ WIP ]](#experiment-template--wip-) + - [devSandbox (stand-alone)](#devsandbox-stand-alone) -The `cfg` structure is where most of the information about your experiment will be defined. + + + + +## the CFG structure + +The `cfg` structure is where most of the information about your experiment will +be defined. Below we try to outline what it contains. -Some of those fields you can set yourself while some others will be created and filled after running -`setDefaultsPTB.m` and `initPTB.m`. +Some of those fields you can set yourself while some others will be created and +filled after running `setDefaultsPTB.m` and `initPTB.m`. -- `setDefaultsPTB.m` sets some default values for things about your experiment that that do not "depend" on your system or that PTB cannot "know". For example the width of the screen in cm or the dimensions of the fixation cross you want to use... -- `initPTB.m` will fill in the fields that ARE system dependent like the screen refresh rate, the reference of the window that PTB opened and where to flip stimulus to. +- `setDefaultsPTB.m` sets some default values for things about your experiment + that that do not "depend" on your system or that PTB cannot "know". For + example the width of the screen in cm or the dimensions of the fixation + cross you want to use... -If no value is provided here, it means that there is no set default (or that the `initPTB` takes care of it). +- `initPTB.m` will fill in the fields that ARE system dependent like the + screen refresh rate, the reference of the window that PTB opened and where + to flip stimulus to. + +If no value is provided here, it means that there is no set default (or that the +`initPTB` takes care of it). ```matlab %% -------------------------------------------------------------------------- %% @@ -37,22 +47,22 @@ cfg.testingDevice = 'pc'; % could be 'mri', 'eeg', 'meg' %% -------------------------------------------------------------------------- %% % cfg.keyboard -cfg.keyboard.keyboard = []; % device index for the main keyboard +cfg.keyboard.keyboard = []; % device index for the main keyboard (that of the experimenter) -cfg.keyboard.responseBox = []; % device index used by the participants -cfg.keyboard.responseKey = {}; % list the keys that PTB should "listen" to when -% using KbQueue to collect responses ; if empty PTB will listen to +cfg.keyboard.responseBox = []; % device index used by the participants +cfg.keyboard.responseKey = {}; % list the keys that PTB should "listen" to when +% using KbQueue to collect responses ; if empty PTB will listen to all key presses cfg.keyboard.escapeKey = 'ESCAPE'; % key to press to escape %% -------------------------------------------------------------------------- %% % cfg.debug -cfg.debug.do = true; % if true this will make less PTB tolerant with +cfg.debug.do = true; % if true this will make less PTB tolerant with bad synchronisation -cfg.debug.transpWin = true; % makes the stimulus windows semi-transparent: +cfg.debug.transpWin = true; % makes the stimulus windows semi-transparent: useful when designing your experiment -cfg.debug.smallWin = true; % open a small window and not a full screen window ; +cfg.debug.smallWin = true; % open a small window and not a full screen window ; can be useful for debugging %% -------------------------------------------------------------------------- %% @@ -75,7 +85,7 @@ cfg.screen.resolution = {[], [], []}; %% -------------------------------------------------------------------------- %% -% fixation +% fixation cfg.fixation.type = 'cross'; % can also be 'dot' or 'bestFixation' cfg.fixation.xDisplacement = 0; % horizontal offset from window center cfg.fixation.yDisplacement = 0; % vertical offset from window center @@ -85,16 +95,16 @@ cfg.fixation.lineWidthPix = 5; % width of the lines in pixel %% -------------------------------------------------------------------------- %% % aperture -% mostly relevant for retinotopy scripts but can be reused for other types of +% mostly relevant for retinotopy scripts but can be reused for other types of % experiments where an aperture is needed -cfg.aperture.type = 'none'; % 'bar', 'wedge', 'ring', 'circle' +cfg.aperture.type = 'none'; % 'bar', 'wedge', 'ring', 'circle' %% -------------------------------------------------------------------------- %% % cfg.audio cfg.audio.do = false; % set to true if you are going to play some sounds cfg.audio.requestedLatency = 3; -cfg.audio.fs 44800; % sampling frequency +cfg.audio.fs 44800; % sampling frequency cfg.audio.channels = 2; % number of auditory channels cfg.audio.initVolume = 1; cfg.audio.repeat = 1; @@ -125,80 +135,97 @@ cfg.bids.mri.repetitionTime % all the following will be initialised by initPTB cfg.screen.idx % screen index cfg.screen.win % window index -cfg.screen.winRect % rectangle definition of the window +cfg.screen.winRect % rectangle definition of the window cfg.screen.winWidth cfg.screen.winHeight cfg.screen.center % [x y] ; pixel coordinate of the window center cfg.screen.FOV % width of the field of view in degrees of visual angle cfg.screen.ppd % pixel per degree -cfg.screen.ifi % inter frame interval +cfg.screen.ifi % inter frame interval cfg.screen.monRefresh % monitor refresh rate ; 1 / ifi %% -------------------------------------------------------------------------- %% % cfg.audio cfg.audio.requestOffsetTime = 1; cfg.audio.reqsSampleOffset -cfg.audio.pushSize +cfg.audio.pushSize cfg.audio.playbackMode = 1; cfg.audio.devIdx = []; cfg.audio.pahandle %% -------------------------------------------------------------------------- %% % operating system information collected by initPTB -cfg.software.os +cfg.software.os cfg.software.name = 'Psychtoolbox'; cfg.software.RRID = 'SCR_002881'; cfg.software.version % psychtoolbox version cfg.software.runsOn % matlab or octave and version number ``` -## 2. Setting up keyboards +## Setting up keyboards -To select a specific keyboard to be used by the experimenter or the participant, you need to know -the value assigned by PTB to each keyboard device. +To select a specific keyboard to be used by the experimenter or the participant, +you need to know the value assigned by PTB to each keyboard device. To know this copy-paste this on the command window: -``` matlab +```matlab [keyboardNumbers, keyboardNames] = GetKeyboardIndices; disp(keyboardNumbers); disp(keyboardNames); ``` -You can then assign a specific device number to the main keyboard or the response box in the `cfg` structure +You can then assign a specific device number to the main keyboard or the +response box in the `cfg` structure -- `cfg.keyboard.responseBox` would be the device number of the device used by the participant to give his/her -response: like the button box in the scanner or a separate keyboard for a behavioral experiment -- `cfg.keyboard.keyboard` would be the device number of the keyboard on which the experimenter will type or -press the keys necessary to start or abort the experiment. +- `cfg.keyboard.responseBox` would be the device number of the device used by + the participant to give his/her response: like the button box in the scanner + or a separate keyboard for a behavioral experiment -`cfg.keyboard.responseBox` and `cfg.keyboard.keyboard` can be different or the same. +- `cfg.keyboard.keyboard` would be the device number of the keyboard on which + the experimenter will type or press the keys necessary to start or abort the + experiment. -Using empty vectors (ie `[]`) or a negative value for those means that you will let PTB find and use the default device. +`cfg.keyboard.responseBox` and `cfg.keyboard.keyboard` can be different or the +same. -## 3. functions descriptions +Using empty vectors (ie `[]`) or a negative value for those means that you will +let PTB find and use the default device. -The main functions of the toolbox are described [here](./10_functions_descriptions.md). +## functions descriptions -## 4. Annexes +The main functions of the toolbox are described +[here](./10-functions-description.md). -### 4.1. Experiment template [ WIP ] +## Annexes + +### Experiment template {WIP} Will be moved to a different repository -### 4.2. devSandbox (stand-alone) +### devSandbox (stand-alone) -Will be moved to a different repository +Will be moved to a different repository. -This script is a stand-alone function that can be useful as a sandbox to develop the PTB audio/visual stimulation of your experiment. No input/output required. +This script is a stand-alone function that can be useful as a sandbox to develop +the PTB audio/visual stimulation of your experiment. No input/output required. -Here, a tutorial from https://peterscarfe.com/contrastgratingdemo.html is provided for illustrative purpose (notice that some variable names are updated to our code style). For your use, you will delete that part. +Here, [a tutorial](https://peterscarfe.com/contrastgratingdemo.html) is +provided for illustrative purpose (notice that some variable names are updated +to our code style). For your use, you will delete that part. It is composed of two parts: - - a fixed structure that will initialize and close PTB in 'debug mode' + +- a fixed structure that will initialize and close PTB in 'debug mode' (`PsychDebugWindowConfiguration`, `SkipSyncTests`) - - the actual sandbox where to set your dynamic variables (the stimulation - parameters) and the 'playground' where to develop the stimulation code - When you are happy with it, ideally you will move the vars in `setParameters.m` and the stimulation code in a separate function in `my-experiment-folder/subfun`. The code style and variable names are the same used in `cpp-lln-lab/CPP_PTB` github repo, therefore it should be easy to move everything in your experiment scripts (see the template that is annexed in `cpp-lln-lab/CPP_PTB`). +- the actual sandbox where to set your dynamic variables (the stimulation + parameters) and the 'playground' where to develop the stimulation code + +When you are happy with it, ideally you will move the vars in `setParameters.m` +and the stimulation code in a separate function in +`my-experiment-folder/subfun`. The code style and variable names are the same +used in `cpp-lln-lab/CPP_PTB` github repo, therefore it should be easy to move +everything in your experiment scripts (see the template that is annexed in +`cpp-lln-lab/CPP_PTB`). diff --git a/docs/10-functions-description.md b/docs/10-functions-description.md new file mode 100644 index 0000000..7f8747f --- /dev/null +++ b/docs/10-functions-description.md @@ -0,0 +1,241 @@ +# functions description + + + + + +- [functions description](#functions-description) + - [General functions](#general-functions) + - [initPTB](#initptb) + - [cleanUp](#cleanup) + - [getExperimentStart](#getexperimentstart) + - [getExperimentEnd](#getexperimentend) + - [degToPix](#degtopix) + - [computeFOV](#computefov) + - [eyeTracker](#eyetracker) + - [standByScreen](#standbyscreen) + - [waitForTrigger](#waitfortrigger) + - [waitFor](#waitfor) + - [readAndFilterLogfile](#readandfilterlogfile) + - [Keyboard functions: response collection and aborting experiment](#keyboard-functions-response-collection-and-aborting-experiment) + - [testKeyboards](#testkeyboards) + - [getResponse](#getresponse) + - [pressSpaceForme](#pressspaceforme) + - [checkAbort](#checkabort) + - [Fixations](#fixations) + - [drawFixationCross](#drawfixationcross) + - [Drawing dots](#drawing-dots) + - [Drawing apertures](#drawing-apertures) + - [Randomization](#randomization) + - [shuffle](#shuffle) + - [setTargetPositionInSequence](#settargetpositioninsequence) + - [repeatShuffleConditions](#repeatshuffleconditions) + + + + + +## General functions + +### initPTB + +This will initialize PsychToolBox. + +It is pretty much necessary to use this function to set up the stage for using +any other functions of CPP_PTB. + +- checks OS and PTB version + +- set some defaults + +- set the screen details + - the window opened takes the whole screen by default + - set in debug mode with window transparency if necessary + - can skip synch test if you ask for it (nicely) + - gets the flip interval + - computes the pixel per degree of visual angle + +- set fixation cross details + +- set font details + +- keyboard + +- hides cursor + +- sound + +### cleanUp + +A wrapper function to close all windows, ports, show mouse cursor, close +keyboard queues and give you back access to the keyboards. + +### getExperimentStart + +Wrapper function that will show a fixation cross and collect a start timestamp +in `cfg.experimentStart` + +### getExperimentEnd + +Wrapper function that will show a fixation cross and display in the console the +whole experiment's duration in minutes and seconds + +### degToPix + +For a given field value in degrees of visual angle in the input `structure`, +this computes its value in pixel using the pixel per degree value of the `cfg` +structure and returns a structure with an additional field with Pix suffix +holding that new value. + +### computeFOV + +Gives you the width of the field on view in degress of visual angle based on the +screen width and distance to the screen in cm (taken from the `cfg`) + +### eyeTracker + +This will handle the Eye Tracker (EyeLink set up) and can be called to +initialize the connection and start the calibration, start/stop eye(s) movement +recordings and save the `*.edf` file (named with BIDS specification from +cpp-lln-lab/CPP_BIDS). + +There are several actions to perform: + +- Calibration: to initialize EyeLink and run calibration + + - 'default calibration' (default) will run a calibration with 6 points + + - 'custom calibration' (cfg.eyeTracker.defaultCalibration = 'false') will + run a calibration with 6 points but the experimenter can choose their + position on the screen + +- StartRecording: to start eye movements recording + +- Message: will add a tag (e.g. 'Block_n1') in the ET output file, the tag is + a string and it is input from `varargin` + +- StopRecordings: to stop eye movements recornding + +- Shutdown: to save the `.edf` file with BIDS compliant name, from + cpp-lln-lab/CPP_BIDS, in the output folder and shut the connection between + the stimulation computer and the EyeLink computer + +### standByScreen + +It shows a basic one-page instruction stored in `cfg.task.instruction` and wait +for `space` stroke. + +### waitForTrigger + +Counts a certain number of triggers coming from the mri/scanner before +returning. Requires number of triggers to wait for. + +This can also be used if you want to let the scanner pace the experiment and you +want to synch stimulus presentation to the scanner trigger. + +### waitFor + +A generic function that you can use to for a certain amount of time or a number +of triggers + +### readAndFilterLogfile + +Displays in the command window part of the `*events.tsv` file filtered by an +element (e.g. 'trigger'). It can take the last output produced (through `cfg`) +or any output BIDS compatible (through file path). + +## Keyboard functions: response collection and aborting experiment + +### testKeyboards + +Checks that the keyboards asked for are properly connected. + +If no key is pressed on the correct keyboard after the timeOut time this exits +with an error. + +### getResponse + +It is wrapper function to use `KbQueue` which is definitely what you should use +to collect responses. + +You can easily collect responses while running some other code at the same time. + +It will only take responses from one device which can simply be the "main +keyboard" (the default device that PTB will find) or another keyboard connected +to the computer or the response box that the participant is using. + +You can use it in a way so that it only takes responses from certain keys and +ignore others (like the triggers from an MRI scanner). + +If you want to know more on how to use it check its help section and the +`CPP_getResponseDemo.m`. + +In brief, there are several actions you can execute with this function. + +- init: initialize the buffer for key presses on a given device (you can also + specify the keys of interest that should be listened to). + +- start: start listening to the key presses (carefully insert into your + script - where do you want to start buffering the responses). + +- check: till that point, it will check the buffer for all key presses. - It + only reports presses on the keys of interest mentioned at initialization. - + It **can** also check for presses on the escape key and abort if the escape + key is part of the keys of interest. + +- flush: empties the buffer of key presses in case you want to discard any + previous key presses. + +- stop: stops buffering key presses. You can still restart by calling "start" + again. + +- release: closes the buffer for good. + +### pressSpaceForme + +Use that to stop your script and only restart when the space bar is pressed. + +This can be useful if as an experimenter you want to have one final check on +some set up before giving the green light. + +### checkAbort + +A simple function that will quit your experiment if you press the key you have +defined in `cfg.keyboard.escapeKey`. + +## Fixations + +### drawFixationCross + +Define the parameters of the fixation cross in `cfg` and `expParameters` and +this does the rest. + +## Drawing dots + +## Drawing apertures + +## Randomization + +Functions that can be used to create random stimuli sequences. + +### shuffle + +Is just there to replace the Shuffle function from PTB in case it is not in the +path. Can be useful for testing or for continuous integration. + +### setTargetPositionInSequence + +For a sequence of length `seqLength` where we want to insert `nbTarget` targets, +this will return `nbTarget` random position in that sequence and make sure that +they are not in consecutive positions. + +### repeatShuffleConditions + +Given `baseConditionVector`, a vector of conditions (coded as numbers), this +will create a longer vector made of `nbRepeats` of this base vector and make +sure that a given condition is not repeated one after the other. + +### 6.4. setUpRand + +Set up the randomizers for uniform and normal distributions. It is of great +importance to do this before anything else! diff --git a/docs/10_functions_description.md b/docs/10_functions_description.md deleted file mode 100644 index a7318f8..0000000 --- a/docs/10_functions_description.md +++ /dev/null @@ -1,203 +0,0 @@ -# functions description - - -* 1. [ General functions](#Generalfunctions) - * 1.1. [initPTB](#initPTB) - * 1.2. [cleanUp](#cleanUp) - * 1.3. [getExperimentStart](#getExperimentStart) - * 1.4. [getExperimentEnd](#getExperimentEnd) - * 1.5. [degToPix](#degToPix) - * 1.6. [computeFOV](#computeFOV) - * 1.7. [eyeTracker](#eyeTracker) - * 1.8. [standByScreen](#standByScreen) - * 1.9. [waitForTrigger](#waitForTrigger) - * 1.10. [waitFor](#waitFor) - * 1.11. [readAndFilterLogfile](#readAndFilterLogfile) -* 2. [Keyboard functions: response collection and aborting experiment](#Keyboardfunctions:responsecollectionandabortingexperiment) - * 2.1. [testKeyboards](#testKeyboards) - * 2.2. [getResponse](#getResponse) - * 2.3. [pressSpaceForme](#pressSpaceForme) -* 3. [Fixations](#Fixations) - * 3.1. [drawFixationCross](#drawFixationCross) -* 4. [Drawing dots](#Drawingdots) -* 5. [Drawing apertures](#Drawingapertures) -* 6. [Randomization](#Randomization) - * 6.1. [shuffle](#shuffle) - * 6.2. [setTargetPositionInSequence](#setTargetPositionInSequence) - * 6.3. [repeatShuffleConditions](#repeatShuffleConditions) - - - - -## 1. General functions - -### 1.1. initPTB - -This will initialize PsychToolBox. - -It is pretty much necessary to use this function to set up the stage for using -any other functions of CPP_PTB. - -- checks OS and PTB version -- set some defaults -- set the screen details - - the window opened takes the whole screen by default - - set in debug mode with window transparency if necessary - - can skip synch test if you ask for it (nicely) - - gets the flip interval - - computes the pixel per degree of visual angle -- set fixation cross details -- set font details -- keyboard -- hides cursor -- sound - -### 1.2. cleanUp - -A wrapper function to close all windows, ports, show mouse cursor, close keyboard -queues and give you back access to the keyboards. - -### 1.3. getExperimentStart - -Wrapper function that will show a fixation cross and collect a start timestamp -in `cfg.experimentStart` - -### 1.4. getExperimentEnd - -Wrapper function that will show a fixation cross and display in the console -the whole experiment's duration in minutes and seconds - -### 1.5. degToPix - -For a given field value in degrees of visual angle in the input `structure`, -this computes its value in pixel using the pixel per degree value of the `cfg` -structure and returns a structure with an additional field with Pix suffix holding that new value. - -### 1.6. computeFOV - -Gives you the width of the field on view in degress of visual angle based on -the screen width and distance to the screen in cm (taken from the `cfg`) - -### 1.7. eyeTracker - -This will handle the Eye Tracker (EyeLink set up) and can be called to initialize -the connection and start the calibration, start/stop eye(s) movement recordings -and save the `*.edf` file (named with BIDS specification from cpp-lln-lab/CPP_BIDS). - -There are several actions to perform: - -- Calibration: to initialize EyeLink and run calibration - - 'default calibration' (default) will run a calibration with 6 points - - 'custom calibration' (cfg.eyeTracker.defaultCalibration = 'false') will run -a calibration with 6 points but the experimenter can choose their position on the screen -- StartRecording: to start eye movements recording -- StopRecordings: to stop eye movements recornding -- Shutdown: to save the `.edf` file with BIDS compliant name, from cpp-lln-lab/CPP_BIDS, -in the output folder and shut the connection between the stimulation computer and the EyeLink computer - -### 1.8. standByScreen - -It shows a basic one-page instruction stored in `cfg.task.instruction` and wait for `space` stroke. - -### 1.9. waitForTrigger - -Counts a certain number of triggers coming from the mri/scanner before returning. -Requires number of triggers to wait for. - -This can also be used if you want to let the scanner pace the experiment and you -want to synch stimulus presentation to the scanner trigger. - -### 1.10. waitFor - -A generic function that you can use to for a certain amount of time or a number of triggers - -### 1.11. readAndFilterLogfile - -Displays in the command window part of the `*events.tsv` file filtered by an element -(e.g. 'trigger'). It can take the last output produced (through `cfg`) or any -output BIDS compatible (through file path). - -## 2. Keyboard functions: response collection and aborting experiment - -### 2.1. testKeyboards - -Checks that the keyboards asked for are properly connected. - -If no key is pressed on the correct keyboard after the timeOut time this exits with an error. - -### 2.2. getResponse - -It is wrapper function to use `KbQueue` which is definitely what you should use -to collect responses. - -You can easily collect responses while running some other code at the same time. - -It will only take responses from one device which can simply be the "main keyboard" -(the default device that PTB will find) or another keyboard connected to the computer -or the response box that the participant is using. - -You can use it in a way so that it only takes responses from certain keys and ignore others (like -the triggers from an MRI scanner). - -If you want to know more on how to use it check its help section and the `CPP_getResponseDemo.m`. - -In brief, there are several actions you can execute with this function. - -- init: initialize the buffer for key presses on a given device (you can also -specify the keys of interest that should be listened to). -- start: start listening to the key presses (carefully insert into your script - -where do you want to start buffering the responses). -- check: till that point, it will check the buffer for all key presses. - - It only reports presses on the keys of interest mentioned at initialization. - - It **can** also check for presses on the escape key and abort if the escape -key is part of the keys of interest. -- flush: empties the buffer of key presses in case you want to discard any -previous key presses. -- stop: stops buffering key presses. You can still restart by calling "start" again. -- release: closes the buffer for good. - -### 2.3. pressSpaceForme - -Use that to stop your script and only restart when the space bar is pressed. - -This can be useful if as an experimenter you want to have one final check on -some set up before giving the green light. - -### checkAbort - -A simple function that will quit your experiment if you press the key you have -defined in `cfg.keyboard.escapeKey`. - -## 3. Fixations - -### 3.1. drawFixationCross - -Define the parameters of the fixation cross in `cfg` and `expParameters` and this does the rest. - -## 4. Drawing dots - -## 5. Drawing apertures - -## 6. Randomization - -Functions that can be used to create random stimuli sequences. - -### 6.1. shuffle - -Is just there to replace the Shuffle function from PTB in case it is not in the -path. Can be useful for testing or for continuous integration. - -### 6.2. setTargetPositionInSequence - -For a sequence of length `seqLength` where we want to insert `nbTarget` targets, -this will return `nbTarget` random position in that sequence and make sure that -they are not in consecutive positions. - -### 6.3. repeatShuffleConditions - -Given `baseConditionVector`, a vector of conditions (coded as numbers), this will -create a longer vector made of `nbRepeats` of this base vector and make sure that -a given condition is not repeated one after the other. diff --git a/manualTests/test_dotMotionSimulation.m b/manualTests/test_dotMotionSimulation.m new file mode 100644 index 0000000..8140072 --- /dev/null +++ b/manualTests/test_dotMotionSimulation.m @@ -0,0 +1,48 @@ +function test_suite = test_dotMotionSimulation %#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_dotMotionSimulationStatic() + + nbEvents = 1; + doPlot = false; + + thisEvent.direction = -1; % degrees + thisEvent.speedPix = 1; % pix per frame + + cfg.design.motionType = 'translation'; + cfg.dot.coherence = 1; % proportion + cfg.dot.lifeTime = .5; % in seconds + cfg.dot.matrixWidth = 250; % in pixels + cfg.dot.proportionKilledPerFrame = 0; + cfg.timing.eventDuration = 1.8; % in seconds + + relativeDensityContrast = dotMotionSimulation(cfg, thisEvent, nbEvents, doPlot); + +end + +function test_dotMotionSimulationTranslation() + % ensure that dog homogenity is not too low when we kill dots often enough + + nbEvents = 500; + doPlot = false; + + thisEvent.direction = 0; % degrees + thisEvent.speedPix = 1; % pix per frame + + cfg.design.motionType = 'translation'; + cfg.dot.coherence = 1; % proportion + cfg.dot.lifeTime = .1; % in seconds + cfg.dot.matrixWidth = 250; % in pixels + cfg.dot.proportionKilledPerFrame = 0; + cfg.timing.eventDuration = 1.8; % in seconds + + relativeDensityContrast = dotMotionSimulation(cfg, thisEvent, nbEvents, doPlot); + + assertLessThan(relativeDensityContrast, 0.5); + +end diff --git a/manualTests/test_getResponse.m b/manualTests/test_getResponse.m index 78a3bad..cea8d4a 100644 --- a/manualTests/test_getResponse.m +++ b/manualTests/test_getResponse.m @@ -54,7 +54,7 @@ for iEvent = 1:size(responseEvents, 1) fprintf(' %s was pressed\n ', ... - responseEvents(iEvent).keyName); + responseEvents(iEvent).keyName); if ~any(strcmp({responseEvents(iEvent).keyName}, cfg.keyboard.responseKey)) errorRestrictedKeysGetReponse(); diff --git a/manualTests/test_radialMotion.m b/manualTests/test_radialMotion.m new file mode 100644 index 0000000..6cb305a --- /dev/null +++ b/manualTests/test_radialMotion.m @@ -0,0 +1,21 @@ +% ensure that dot density contrast is not too low when we kill dots often enough + +% There is an actual official unit test in the tests folder so this here is more +% to have the visualization turned on. + +nbEvents = 100; +doPlot = true; + +thisEvent.direction = 0; % degrees +thisEvent.speed = 1; % pix per frame + +cfg.design.motionType = 'radial'; + +cfg.dot.coherence = 1; % proportion +cfg.dot.lifeTime = .1; % in seconds +cfg.dot.matrixWidth = 250; % in pixels +cfg.dot.proportionKilledPerFrame = 0; + +cfg.timing.eventDuration = 1.8; % in seconds + +relativeDensityContrast = dotMotionSimulation(cfg, thisEvent, nbEvents, doPlot); diff --git a/miss_hit.cfg b/miss_hit.cfg index 42f49fe..99d85d7 100644 --- a/miss_hit.cfg +++ b/miss_hit.cfg @@ -1,7 +1,9 @@ # 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" +copyright_entity: "Sam Schwarzkopf" +copyright_entity: "Agah Karakuzu" +copyright_entity: "CPP_PTB developers" # metrics limit for the code quality (https://florianschanda.github.io/miss_hit/metrics.html) metric "cnest": limit 4 diff --git a/npm-requirements.txt b/npm-requirements.txt new file mode 100644 index 0000000..061f9ad --- /dev/null +++ b/npm-requirements.txt @@ -0,0 +1,6 @@ +remark-cli@5.0.0 +remark-lint@6.0.2 +remark-preset-lint-recommended@3.0.2 +remark-preset-lint-markdown-style-guide@2.1.2 +remark-preset-lint-consistent + diff --git a/src/aperture/apertureTexture.m b/src/aperture/apertureTexture.m index 49086ee..c406887 100644 --- a/src/aperture/apertureTexture.m +++ b/src/aperture/apertureTexture.m @@ -1,3 +1,6 @@ +% (C) Copyright 2010-2020 Sam Schwarzkopf +% (C) Copyright 2020 CPP_PTB developers + function [cfg, thisEvent] = apertureTexture(action, cfg, thisEvent) % [cfg, thisEvent] = apertureTexture(action, cfg, thisEvent) % @@ -10,7 +13,8 @@ cfg = apertureInit(cfg); cfg.aperture.texture = Screen('MakeTexture', cfg.screen.win, ... - cfg.color.background(1) * ones(cfg.screen.winRect([3 3]))); + cfg.color.background(1) * ... + ones(cfg.screen.winRect([3 3]))); case 'make' @@ -29,9 +33,9 @@ end Screen('DrawTexture', cfg.screen.win, cfg.aperture.texture, ... - cfg.screen.winRect, ... - CenterRect(cfg.screen.winRect * scalingFactor, cfg.screen.winRect), ... - rotationAngle); + cfg.screen.winRect, ... + CenterRect(cfg.screen.winRect * scalingFactor, cfg.screen.winRect), ... + rotationAngle); end @@ -64,8 +68,8 @@ cfg.ring.csFuncFact = ... 1 / ... ((cfg.ring.maxEcc + exp(1)) * ... - log(cfg.ring.maxEcc + exp(1)) - ... - (cfg.ring.maxEcc + exp(1))); + log(cfg.ring.maxEcc + exp(1)) - ... + (cfg.ring.maxEcc + exp(1))); end case 'bar' @@ -105,6 +109,8 @@ case 'circle' + Screen('Fillrect', cfg.aperture.texture, cfg.color.background); + diameter = cfg.aperture.widthPix; if isfield(cfg.aperture, 'xPosPix') @@ -115,8 +121,8 @@ end Screen('FillOval', cfg.aperture.texture, TRANSPARENT, ... - CenterRectOnPoint([0, 0, repmat(diameter, 1, 2)], ... - xCenter, yCenter)); + CenterRectOnPoint([0, 0, repmat(diameter, 1, 2)], ... + xCenter, yCenter)); case 'ring' @@ -126,14 +132,14 @@ Screen('Fillrect', cfg.aperture.texture, cfg.color.background); Screen('FillOval', cfg.aperture.texture, TRANSPARENT, ... - CenterRectOnPoint( ... - [0, 0, repmat(cfg.ring.outerRimPix, 1, 2)], ... - xCenter, yCenter)); + CenterRectOnPoint( ... + [0, 0, repmat(cfg.ring.outerRimPix, 1, 2)], ... + xCenter, yCenter)); Screen('FillOval', cfg.aperture.texture, [cfg.color.background 255], ... - CenterRectOnPoint( ... - [0, 0, repmat(cfg.ring.innerRimPix, 1, 2)], ... - xCenter, yCenter)); + CenterRectOnPoint( ... + [0, 0, repmat(cfg.ring.innerRimPix, 1, 2)], ... + xCenter, yCenter)); case 'wedge' @@ -156,11 +162,11 @@ Screen('Fillrect', cfg.aperture.texture, cfg.color.background); Screen('FillArc', cfg.aperture.texture, TRANSPARENT, ... - CenterRect( ... - cfg.destinationRect, ... - cfg.screen.winRect), ... - thisEvent.angle, ... % start angle - cfg.aperture.width); % arc angle + CenterRect( ... + cfg.destinationRect, ... + cfg.screen.winRect), ... + thisEvent.angle, ... % start angle + cfg.aperture.width); % arc angle case 'bar' @@ -169,25 +175,25 @@ % We let the stimulus through Screen('FillOval', cfg.aperture.texture, TRANSPARENT, ... - CenterRect( ... - [0, 0, repmat(cfg.screen.winRect(4), 1, 2)], ... - cfg.screen.winRect)); + CenterRect( ... + [0, 0, repmat(cfg.screen.winRect(4), 1, 2)], ... + cfg.screen.winRect)); % Then we add the position of the bar aperture % which one is the right and which one is the left?? Screen('FillRect', cfg.aperture.texture, cfg.color.background, ... - [0, ... - 0, ... - thisEvent.barPosPix - cfg.aperture.barWidthPix / 2, ... - cfg.screen.winRect(4)]); + [0, ... + 0, ... + thisEvent.barPosPix - cfg.aperture.barWidthPix / 2, ... + cfg.screen.winRect(4)]); Screen('FillRect', cfg.aperture.texture, cfg.color.background, ... - [thisEvent.barPosPix + cfg.aperture.barWidthPix / 2, ... - 0, ... - cfg.screen.winRect(3), ... - cfg.screen.winRect(4)]); + [thisEvent.barPosPix + cfg.aperture.barWidthPix / 2, ... + 0, ... + cfg.screen.winRect(3), ... + cfg.screen.winRect(4)]); otherwise diff --git a/src/aperture/eccenLogSpeed.m b/src/aperture/eccenLogSpeed.m index df00b83..02ddb2f 100644 --- a/src/aperture/eccenLogSpeed.m +++ b/src/aperture/eccenLogSpeed.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function [cfg] = eccenLogSpeed(cfg, time) % vary CurrScale so that expansion speed is log over eccentricity % cf. Tootell 1997; Swisher 2007; Warnking 2002 etc @@ -30,7 +32,7 @@ % near-exp visual angle newOuterRimVA = ((outerRimVA + exp(1)) * log(outerRimVA + exp(1)) - ... - (outerRimVA + exp(1))) * maxEcc * csFuncFact; + (outerRimVA + exp(1))) * maxEcc * csFuncFact; outerRimPix = newOuterRimVA * cfg.screen.ppd; % in pixel % width of apperture changes logarithmically with eccentricity of inner ring diff --git a/src/aperture/getApertureName.m b/src/aperture/getApertureName.m new file mode 100644 index 0000000..e6dc8c0 --- /dev/null +++ b/src/aperture/getApertureName.m @@ -0,0 +1,16 @@ +% (C) Copyright 2020 CPP_PTB developers + +function apertureName = getApertureName(cfg, apertures, iApert) + + switch cfg.aperture + case 'Bar' + apertureName = sprintf('bar_angle-%i_position-%02.2f.tif', ... + apertures.barAngle(iApert), ... + apertures.barPostion(iApert)); + case 'Wedge' + apertureName = sprintf('wedge_nb-%i.tif', iApert); + case 'Ring' + apertureName = sprintf('ring_nb-%i.tif', iApert); + end + +end diff --git a/src/aperture/saveAperture.m b/src/aperture/saveAperture.m new file mode 100644 index 0000000..d5e116b --- /dev/null +++ b/src/aperture/saveAperture.m @@ -0,0 +1,36 @@ +% (C) Copyright 2010-2020 Sam Schwarzkopf +% (C) Copyright 2020 CPP_PTB developers + +if SaveAps + if SaveAps == 1 + ApFrm = zeros(100, 100, Parameters.Volumes_per_Trial * length(Parameters.Conditions)); + elseif SaveAps == 2 + ApFrm = zeros(640, 480, 3); + sf = 0; + end + SavWin = Screen('MakeTexture', Win, 127 * ones(StimRect([3 3]))); +end + +% If saving movie +if SaveAps == 1 && PrevVolume ~= CurrVolume + PrevVolume = CurrVolume; + CurApImg = Screen('GetImage', Win); + CurApImg = rgb2gray(CurApImg); + CurApImg = imresize(CurApImg, [Rect(4) Rect(3)]); + Fxy = round(CenterRect(FrameRect, Rect) + ... + [Parameters.Image_Position Parameters.Image_Position]); + CurApImg = CurApImg(Fxy(2):Fxy(4), Fxy(1):Fxy(3)); + CurApImg = double(abs(double(CurApImg) - 127) > 1); + CurApImg = imresize(CurApImg, [100 100]); + ApFrm(:, :, Parameters.Volumes_per_Trial * (Trial - 1) + CurrVolume) = CurApImg; +elseif SaveAps == 2 + CurApImg = Screen('GetImage', Win); + CurApImg = imresize(CurApImg, [640 480]); + sf = sf + 1; + ApFrm(:, :, :, sf) = CurApImg; +end + +if SaveAps == 2 + ApFrm = uint8(ApFrm); + save('Stimulus_movie', 'ApFrm'); +end diff --git a/src/aperture/saveApertures.m b/src/aperture/saveApertures.m new file mode 100644 index 0000000..ddb79a3 --- /dev/null +++ b/src/aperture/saveApertures.m @@ -0,0 +1,45 @@ +% (C) Copyright 2010-2020 Sam Schwarzkopf +% (C) Copyright 2020 CPP_PTB developers + +function saveApertures(saveAps, cfg, apertures) + + if saveAps + + matFile = fullfile( ... + cfg.outputDir, ... + strrep(cfg.fileName.events, '.tsv', '_AperturesPRF.mat')); + if IsOctave + save(matFile, '-mat7-binary'); + else + save(matFile, '-v7.3'); + end + + for iApert = 1:size(apertures.Frames, 3) + + tmp = apertures.Frames(:, :, iApert); + + % We skip the all nan frames and print the others + if ~all(isnan(tmp(:))) + + close all; + + imagesc(apertures.Frames(:, :, iApert)); + + colormap gray; + + box off; + axis off; + axis square; + + apertureName = getApertureName(cfg, apertures, iApert); + + print(gcf, ... + fullfile(cfg.aperture.outputDir, [ApertureName '.tif']), ... + '-dtiff'); + end + + end + + end + +end diff --git a/src/aperture/smoothOval.m b/src/aperture/smoothOval.m new file mode 100644 index 0000000..9ae34f3 --- /dev/null +++ b/src/aperture/smoothOval.m @@ -0,0 +1,16 @@ +% (C) Copyright 2010-2020 Sam Schwarzkopf +% (C) Copyright 2020 CPP_PTB developers + +function smoothOval(win, color, rect, fringe) + % SmoothOval(WindowPtr, Color, Rect, Fringe) + % + % Draws a filled oval (using the PTB parameters) with a transparent fringe. + % + + alphas = linspace(0, 255, fringe); + + for f = 0:fringe - 1 + Screen('FillOval', win, ... + [color alphas(f + 1)], ... + [rect(1) + f rect(2) + f rect(3) - f rect(4) - f]); + end diff --git a/src/aperture/smoothRect.m b/src/aperture/smoothRect.m new file mode 100644 index 0000000..96b4542 --- /dev/null +++ b/src/aperture/smoothRect.m @@ -0,0 +1,16 @@ +% (C) Copyright 2010-2020 Sam Schwarzkopf +% (C) Copyright 2020 CPP_PTB developers + +function smoothRect(win, color, rect, fringe) + % SmoothRect(WindowPtr, Color, Rect, Fringe) + % + % Draws a filled rect (using the PTB parameters) with a transparent fringe. + % + + alphas = linspace(0, 255, fringe); + + for f = 0:fringe - 1 + Screen('FillRect', win, ... + [color alphas(f + 1)], ... + [rect(1) + f rect(2) + f rect(3) - f rect(4) - f]); + end diff --git a/src/dot/computeCartCoord.m b/src/dot/computeCartCoord.m index 8aacd65..c2c2bd5 100644 --- a/src/dot/computeCartCoord.m +++ b/src/dot/computeCartCoord.m @@ -1,7 +1,9 @@ -function cartesianCoordinates = computeCartCoord(positions, cfg) +% (C) Copyright 2020 CPP_PTB developers + +function cartesianCoordinates = computeCartCoord(positions, dotMatrixWidth) + cartesianCoordinates = ... - [positions(:, 1) + cfg.dot.matrixWidth, ... % x coordinate - positions(:, 2) + cfg.dot.matrixWidth]; % y coordinate + [positions(:, 1) - dotMatrixWidth / 2, ... % x coordinate + positions(:, 2) - dotMatrixWidth / 2]; % y coordinate - % cartesianCoordinates = positions; end diff --git a/src/dot/computeRadialMotionDirection.m b/src/dot/computeRadialMotionDirection.m index ef47ed8..718ece2 100644 --- a/src/dot/computeRadialMotionDirection.m +++ b/src/dot/computeRadialMotionDirection.m @@ -1,8 +1,10 @@ -function angleMotion = computeRadialMotionDirection(cfg, dots) +% (C) Copyright 2020 CPP_PTB developers - cartesianCoordinates = computeCartCoord(dots.positions, cfg); +function angleMotion = computeRadialMotionDirection(positions, dotMatrixWidth, dots) - [angleMotion, ~] = cart2pol(cartesianCoordinates(:, 1), cartesianCoordinates(:, 2)); + positions = computeCartCoord(positions, dotMatrixWidth); + + [angleMotion, ~] = cart2pol(positions(:, 1), positions(:, 2)); angleMotion = angleMotion / pi * 180; if dots.direction == -666 diff --git a/src/dot/decomposeMotion.m b/src/dot/decomposeMotion.m index ba58da7..6f235e2 100644 --- a/src/dot/decomposeMotion.m +++ b/src/dot/decomposeMotion.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function [horVector, vertVector] = decomposeMotion(angleMotion) % [horVector, vertVector] = decomposeMotion(angleMotion) % diff --git a/src/dot/dotMotionSimulation.m b/src/dot/dotMotionSimulation.m new file mode 100644 index 0000000..b11867e --- /dev/null +++ b/src/dot/dotMotionSimulation.m @@ -0,0 +1,111 @@ +% (C) Copyright 2020 CPP_PTB developers + +function relativeDensityContrast = dotMotionSimulation(cfg, thisEvent, nbEvents, doPlot) + % relativeDensityContrast = dotMotionSimulation(cfg, thisEvent, nbEvents, doPlot) + % + % to simulate where the dots are more dense on the screen + % relativeDensityContrast : hard to get it below 0.10 + + close all; + + if nargin < 4 + doPlot = 1; + end + + if nargin < 3 + nbEvents = 100; + end + + if nargin < 2 + thisEvent.direction = 0; % degrees + thisEvent.speed = 1; % pix per frame + end + + if nargin < 1 + + cfg.design.motionType = 'translation'; + + cfg.dot.coherence = 1; % proportion + + cfg.dot.lifeTime = Inf; % in seconds + + cfg.dot.matrixWidth = 250; % in pixels + + cfg.dot.proportionKilledPerFrame = 0; + + cfg.timing.eventDuration = 1.8; % in seconds + + end + + % interframe interval + cfg.screen.ifi = 0.016; % in seconds + + % size of the fixation is 1% of screen width + cfg.fixation.widthPix = ceil(cfg.dot.matrixWidth * 1 / 100); + + % dot size + cfg.dot.sizePix = 1; + + if ~isfield(cfg.dot, 'number') + % We fill 25% of the screen with dots + cfg.dot.number = round(cfg.dot.matrixWidth^2 * 25 / 100); + end + + fprintf(1, '\n\nDot motion simulation:'); + + nbFrames = ceil(cfg.timing.eventDuration / cfg.screen.ifi); + + % to keep track of where the dots have been + dotDensity = zeros(cfg.dot.matrixWidth); + + for iEvent = 1:nbEvents + + [dots] = initDots(cfg, thisEvent); + dotDensity = updateDotDensity(dotDensity, dots); + + for iFrame = 1:nbFrames + + [dots] = updateDots(dots, cfg); + + dotDensity = updateDotDensity(dotDensity, dots); + + end + + end + + %% Post sim + % trim the edges (to avoid super high/low values + dotDensity = dotDensity(2:end - 1, 2:end - 1); + + % computes the maximum difference in dot density over the all screen + % to be used for unit test + relativeDensityContrast = (max(dotDensity(:)) - min(dotDensity(:))) / max(dotDensity(:)); + + if doPlot + imagesc(dotDensity); + axis square; + title('dot density'); + end + + fprintf(1, '\n'); + +end + +function dotDensity = updateDotDensity(dotDensity, dots) + + x = round(dots.positions(:, 1)); + x = avoidEdgeValues(x, size(dotDensity, 2)); + + y = round(dots.positions(:, 2)); + y = avoidEdgeValues(y, size(dotDensity, 1)); + + ind = sub2ind(size(dotDensity), y, x); + + dotDensity(ind) = dotDensity(ind) + 1; + +end + +function x = avoidEdgeValues(x, dim) + x(x < 1) = 1; + x(x > dim) = dim; +end diff --git a/src/dot/dotTexture.m b/src/dot/dotTexture.m index 1d5ff2c..cf23ce9 100644 --- a/src/dot/dotTexture.m +++ b/src/dot/dotTexture.m @@ -1,22 +1,28 @@ +% (C) Copyright 2020 CPP_PTB developers + function cfg = dotTexture(action, cfg, thisEvent) switch action case 'init' cfg.dot.texture = Screen('MakeTexture', cfg.screen.win, ... - cfg.color.background(1) * ones(cfg.screen.winRect([4 3]))); + cfg.color.background(1) * ... + ones(cfg.screen.winRect([4 3]))); case 'make' dotType = 2; + xCenter = cfg.screen.center(1) + thisEvent.dotCenterXPosPix; + yCenter = cfg.screen.center(2); + Screen('FillRect', cfg.dot.texture, cfg.color.background); Screen('DrawDots', cfg.dot.texture, ... - thisEvent.dot.positions, ... - cfg.dot.sizePix, ... - cfg.dot.color, ... - cfg.screen.center, ... - dotType); + thisEvent.dot.positions, ... + cfg.dot.sizePix, ... + cfg.dot.color, ... + [xCenter yCenter], ... + dotType); case 'draw' diff --git a/src/dot/generateNewDotPositions.m b/src/dot/generateNewDotPositions.m index 92bf28f..1858a98 100644 --- a/src/dot/generateNewDotPositions.m +++ b/src/dot/generateNewDotPositions.m @@ -1,5 +1,7 @@ -function newPositions = generateNewDotPositions(cfg, dotNumber) +% (C) Copyright 2020 CPP_PTB developers - newPositions = rand(dotNumber, 2) * cfg.dot.matrixWidth; +function newPositions = generateNewDotPositions(dotMatrixWidth, nbDots) + + newPositions = rand(nbDots, 2) * dotMatrixWidth; end diff --git a/src/dot/initDots.m b/src/dot/initDots.m index 2c247cd..32175b5 100644 --- a/src/dot/initDots.m +++ b/src/dot/initDots.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function [dots] = initDots(cfg, thisEvent) % [dots] = initDots(cfg, thisEvent) % @@ -24,47 +26,25 @@ dots.direction = thisEvent.direction(1); - speedPixPerFrame = thisEvent.speed(1); - - lifeTime = cfg.dot.lifeTime; - % decide which dots are signal dots (1) and those are noise dots (0) dots.isSignal = rand(cfg.dot.number, 1) < cfg.dot.coherence; + dots.speedPixPerFrame = thisEvent.speedPix(1); + lifeTime = cfg.dot.lifeTime; + % for static dots if dots.direction == -1 - speedPixPerFrame = 0; - lifeTime = Inf; dots.isSignal = true(cfg.dot.number, 1); + dots.speedPixPerFrame = 0; + lifeTime = Inf; end - %% Set an array of dot positions [xposition, yposition] - % These can never be bigger than 1 or lower than 0 - % [0,0] is the top / left of the square - % [1,1] is the bottom / right of the square - dots.positions = generateNewDotPositions(cfg, cfg.dot.number); - - %% Set vertical and horizontal speed for all dots - dots = setDotDirection(cfg, dots); - - [horVector, vertVector] = decomposeMotion(dots.directionAllDots); - speeds = [horVector, vertVector]; - - % we were working with unit vectors. we now switch to pixels - speeds = speeds * speedPixPerFrame; - - %% Create a vector to update to dotlife time of each dot - % Not all set to 1 so the dots will die at different times - % The maximum value is the duraion of the event in frames - time = floor(rand(cfg.dot.number, 1) * cfg.timing.eventDuration / cfg.screen.ifi); + % set position and directions fo the dots + [dots.positions, dots.speeds, dots.time] = ... + seedDots(dots, cfg, dots.isSignal); %% Convert from seconds to frames lifeTime = ceil(lifeTime / cfg.screen.ifi); - - %% dots.lifeTime = lifeTime; - dots.time = time; - dots.speeds = speeds; - dots.speedPixPerFrame = speedPixPerFrame; end diff --git a/src/dot/reseedDots.m b/src/dot/reseedDots.m index b6bd6e7..38eb3f0 100644 --- a/src/dot/reseedDots.m +++ b/src/dot/reseedDots.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function dots = reseedDots(dots, cfg) fixationWidthPix = 0; @@ -5,7 +7,7 @@ fixationWidthPix = cfg.fixation.widthPix; end - cartesianCoordinates = computeCartCoord(dots.positions, cfg); + cartesianCoordinates = computeCartCoord(dots.positions, cfg.dot.matrixWidth); [~, radius] = cart2pol(cartesianCoordinates(:, 1), cartesianCoordinates(:, 2)); % Create a logical vector to detect any dot that has: @@ -16,25 +18,24 @@ % - has been been picked to be killed N = any([ ... - dots.positions > cfg.dot.matrixWidth, ... - dots.positions < 0, ... - dots.time > dots.lifeTime, ... - radius - cfg.dot.sizePix < fixationWidthPix / 2, ... - rand(cfg.dot.number, 1) < cfg.dot.proportionKilledPerFrame ... - ], 2); + dots.positions > cfg.dot.matrixWidth, ... + dots.positions < 0, ... + dots.time > dots.lifeTime, ... + radius - cfg.dot.sizePix < fixationWidthPix / 2, ... + rand(cfg.dot.number, 1) < cfg.dot.proportionKilledPerFrame ... + ], 2); % If there is any such dot we relocate it to a new random position % and change its lifetime to 1 if any(N) - dots.positions(N, :) = generateNewDotPositions(cfg, sum(N)); - - dots = setDotDirection(cfg, dots); + isSignal = dots.isSignal(N); - [horVector, vertVector] = decomposeMotion(dots.directionAllDots); - dots.speeds = [horVector, vertVector] * dots.speedPixPerFrame; + [positions, speeds, time] = seedDots(dots, cfg, isSignal); - dots.time(N, 1) = 1; + dots.positions(N, :) = positions; + dots.speeds(N, :) = speeds; + dots.time(N, 1) = time; end diff --git a/src/dot/seedDots.m b/src/dot/seedDots.m new file mode 100644 index 0000000..05e7630 --- /dev/null +++ b/src/dot/seedDots.m @@ -0,0 +1,30 @@ +% (C) Copyright 2020 CPP_PTB developers + +function [positions, speeds, time] = seedDots(varargin) + + [dots, cfg, isSignal] = deal(varargin{:}); + + nbDots = numel(isSignal); + + %% Set an array of dot positions [xposition, yposition] + % These can never be bigger than 1 or lower than 0 + % [0,0] is the top / left of the square + % [1,1] is the bottom / right of the square + positions = generateNewDotPositions(cfg.dot.matrixWidth, nbDots); + + %% Set vertical and horizontal speed for all dots + directionAllDots = setDotDirection(positions, cfg, dots, isSignal); + [horVector, vertVector] = decomposeMotion(directionAllDots); + + if strcmp(cfg.design.motionType, 'radial') + vertVector = vertVector * -1; + end + % we were working with unit vectors. we now switch to pixels + speeds = [horVector, vertVector] * dots.speedPixPerFrame; + + %% Create a vector to update to dotlife time of each dot + % Not all set to 1 so the dots will die at different times + % The maximum value is the duration of the event in frames + time = floor(rand(nbDots, 1) * cfg.timing.eventDuration / cfg.screen.ifi); + +end diff --git a/src/dot/setDotDirection.m b/src/dot/setDotDirection.m index 0df6fe0..141a1d4 100644 --- a/src/dot/setDotDirection.m +++ b/src/dot/setDotDirection.m @@ -1,5 +1,7 @@ -function dots = setDotDirection(cfg, dots) - % dots = setDotDirection(cfg, dots) +% (C) Copyright 2020 CPP_PTB developers + +function directionAllDots = setDotDirection(positions, cfg, dots, isSignal) + % directionAllDots = setDotDirection(positions, cfg, dots, isSignal) % % creates some new direction for the dots % @@ -10,29 +12,34 @@ % % all directions are in end expressed between 0 and 360 - directionAllDots = nan(cfg.dot.number, 1); + directionAllDots = dots.direction; - % Coherent dots + % when we initialiaze the direction for all the dots + % after that dots.direction will be a vector + if numel(directionAllDots) == 1 - if numel(dots.direction) == 1 - dots.direction = ones(sum(dots.isSignal), 1) * dots.direction; - elseif numel(dots.direction) ~= sum(dots.isSignal) - error(['dots.direction must have one element' ... - 'or as many element as there are coherent dots']); - end + directionAllDots = ones(sum(isSignal), 1) * dots.direction; - directionAllDots(dots.isSignal) = dots.direction; + end + %% Coherent dots if strcmp(cfg.design.motionType, 'radial') - angleMotion = computeRadialMotionDirection(cfg, dots); - directionAllDots(dots.isSignal) = angleMotion; + + angleMotion = computeRadialMotionDirection(positions, cfg.dot.matrixWidth, dots); + + directionAllDots(isSignal == 1) = angleMotion; + end - % Random direction for the non coherent dots + %% Random direction for the non coherent dots + directionAllDots(isSignal ~= 1) = rand(sum(isSignal ~= 1), 1) * 360; - directionAllDots(~dots.isSignal) = rand(sum(~dots.isSignal), 1) * 360; - directionAllDots = rem(directionAllDots, 360); + %% Express the direction in the 0 to 360 range + directionAllDots = mod(directionAllDots, 360); - dots.directionAllDots = directionAllDots; + % ensure we return a colum vector + if size(directionAllDots, 1) == 1 + directionAllDots = directionAllDots'; + end end diff --git a/src/dot/updateDots.m b/src/dot/updateDots.m index 329a81a..14c0b1f 100644 --- a/src/dot/updateDots.m +++ b/src/dot/updateDots.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function [dots] = updateDots(dots, cfg) % Move the selected dots diff --git a/src/drawFieldOfVIew.m b/src/drawFieldOfVIew.m index 7a15385..f3c314b 100644 --- a/src/drawFieldOfVIew.m +++ b/src/drawFieldOfVIew.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function drawFieldOfVIew(cfg) % drawFieldOfVIew(cfg) % @@ -15,14 +17,14 @@ function drawFieldOfVIew(cfg) fov = cfg.screen.effectiveFieldOfView; fov = CenterRect( ... - [0, 0, fov(1), fov(2)], ... - cfg.screen.winRect); + [0, 0, fov(1), fov(2)], ... + cfg.screen.winRect); Screen('FrameRect', ... - cfg.screen.win, ... - RED, ... - fov, ... - penWidth); + cfg.screen.win, ... + RED, ... + fov, ... + penWidth); end end diff --git a/src/errors/errorAbort.m b/src/errors/errorAbort.m index c67f7f9..6464944 100644 --- a/src/errors/errorAbort.m +++ b/src/errors/errorAbort.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function errorAbort errorStruct.message = 'Escape key press detected: aborting experiment.'; errorStruct.identifier = 'checkAbort:abortRequested'; diff --git a/src/errors/errorAbortGetReponse.m b/src/errors/errorAbortGetReponse.m index 78ec208..b65fae3 100644 --- a/src/errors/errorAbortGetReponse.m +++ b/src/errors/errorAbortGetReponse.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function errorAbortGetReponse errorStruct.message = 'Escape key press detected by getResponse: aborting experiment.'; diff --git a/src/errors/errorDistanceToScreen.m b/src/errors/errorDistanceToScreen.m index 1e17135..3778c0d 100644 --- a/src/errors/errorDistanceToScreen.m +++ b/src/errors/errorDistanceToScreen.m @@ -1,9 +1,11 @@ +% (C) Copyright 2020 CPP_PTB developers + function errorDistanceToScreen(cfg) errorStruct.message = sprintf([ - 'Distance to monitor seems too small.\n' ... - 'It should be in centimeters.\n' ... - 'cfg.screen.monitorDistance = %f'], cfg.screen.monitorDistance); + 'Distance to monitor seems too small.\n' ... + 'It should be in centimeters.\n' ... + 'cfg.screen.monitorDistance = %f'], cfg.screen.monitorDistance); errorStruct.identifier = 'computeFOV:wrongDistanceToScreen'; diff --git a/src/errors/errorRestrictedKeysGetReponse.m b/src/errors/errorRestrictedKeysGetReponse.m index b0c0a5a..101a735 100644 --- a/src/errors/errorRestrictedKeysGetReponse.m +++ b/src/errors/errorRestrictedKeysGetReponse.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function errorRestrictedKeysGetReponse errorStruct.message = 'getResponse reported a key press on a restricted key'; diff --git a/src/eyeTracker.m b/src/eyeTracker.m index f8e3a9c..7378bd3 100755 --- a/src/eyeTracker.m +++ b/src/eyeTracker.m @@ -1,5 +1,7 @@ -function [el, cfg] = eyeTracker(input, cfg) - % [el] = eyeTracker(input, cfg) +% (C) Copyright 2020 CPP_PTB developers + +function [el, cfg] = eyeTracker(input, cfg, varargin) + % [el] = eyeTracker(input, cfg, varargin) % % Wrapper function that deals with all the necessery actions to implement % Eye Tracker recording with eyelink. @@ -12,6 +14,8 @@ % -- 'custom calibration' (cfg.eyeTracker.defaultCalibration = 'false') will run a % calibration with 6 points but the experimenter can choose their position on the screen % - StartRecording: to start eye movements recording + % - Message: will add a tag (e.g. 'Block_n1') in the ET output file, the tag is a string and it + % is input from `varargin` % - StopRecordings: to stop eye movements recornding % - Shutdown: to save the `.edf` file with BIDS compliant name, from cpp-lln-lab/CPP_BIDS, in % the output folder and shut the connection between the stimulation computer and the EyeLink @@ -99,24 +103,28 @@ % [width, height]=Screen('WindowSize', screenNumber); + % TODO - update those values with the content set up by + % CPP_PTB in the cfg + % fieldsToSet.eyeTracker.CalibrationPosition = ''; + Eyelink('Command', 'calibration_samples = 6'); Eyelink('Command', 'calibration_sequence = 0,1,2,3,4,5'); Eyelink('Command', 'calibration_targets = %d,%d %d,%d %d,%d %d,%d %d,%d', ... - 640, 512, ... % width/2,height/2 - 640, 102, ... % width/2,height*0.1 - 640, 614, ... % width/2,height*0.6 - 128, 341, ... % width*0.1,height*1/3 - 1152, 341); % width-width*0.1,height*1/3 + 640, 512, ... % width/2,height/2 + 640, 102, ... % width/2,height*0.1 + 640, 614, ... % width/2,height*0.6 + 128, 341, ... % width*0.1,height*1/3 + 1152, 341); % width-width*0.1,height*1/3 % Validation target locations Eyelink('Command', 'validation_samples = 5'); Eyelink('Command', 'validation_sequence = 0,1,2,3,4,5'); Eyelink('Command', 'validation_targets = %d,%d %d,%d %d,%d %d,%d %d,%d', ... - 640, 512, ... % width/2,height/2 - 640, 102, ... % width/2,height*0.1 - 640, 614, ... % width/2,height*0.6 - 128, 341, ... % width*0.1,height*1/3 - 1152, 341); % width-width*0.1,height*1/3 + 640, 512, ... % width/2,height/2 + 640, 102, ... % width/2,height*0.1 + 640, 614, ... % width/2,height*0.6 + 128, 341, ... % width*0.1,height*1/3 + 1152, 341); % width-width*0.1,height*1/3 end @@ -128,6 +136,15 @@ % Enter Eyetracker camera setup mode, calibration and validation. EyelinkDoTrackerSetup(el); + % TODO - update content of cfg after initializing and + % calibration + % fieldsToSet.eyeTracker.SamplingFrequency = []; + % fieldsToSet.eyeTracker.Manufacturer = ''; + % fieldsToSet.eyeTracker.ManufacturersModelName = ''; + % fieldsToSet.eyeTracker.SoftwareVersions = ''; + % fieldsToSet.eyeTracker.MaximalCalibrationError = []; + % fieldsToSet.eyeTracker.AverageCalibrationError = []; + % Go back to default screen background color. Screen('FillRect', cfg.screen.win, cfg.color.background); Screen('Flip', cfg.screen.win); @@ -157,6 +174,15 @@ % Mark the beginning of the trial, here start the stimulation of the experiment. Eyelink('Message', 'start_recording'); + case 'Message' + + %% Add tag during the recording (e.g. trial_type) + + message = varargin{1}; + + % EyeLink Stop recording the block. + Eyelink('Message', message); + case 'StopRecordings' %% Stop recording of eye-movements @@ -177,9 +203,9 @@ % Set the edf file path + name. edfFileName = fullfile( ... - cfg.dir.outputSubject, ... - cfg.fileName.modality, ... - cfg.fileName.eyetracker); + cfg.dir.outputSubject, ... + cfg.fileName.modality, ... + cfg.fileName.eyetracker); Eyelink('Command', 'set_idle_mode'); @@ -203,8 +229,8 @@ if exist(edfFileName, 'file') == 2 fprintf('Data file ''%s'' can be found in ''%s''\n', ... - cfg.fileName.eyetracker, ... - fullfile(cfg.dir.outputSubject, 'eyetracker')); + cfg.fileName.eyetracker, ... + fullfile(cfg.dir.outputSubject, 'eyetracker')); end diff --git a/src/fixation/drawFixation.m b/src/fixation/drawFixation.m index e9897ae..19907f5 100644 --- a/src/fixation/drawFixation.m +++ b/src/fixation/drawFixation.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function drawFixation(cfg) % Define the parameters of the fixation cross in `cfg` and `expParameters` @@ -7,29 +9,29 @@ function drawFixation(cfg) smooth = 1; Screen('DrawLines', ... - cfg.screen.win, ... - cfg.fixation.allCoords, ... - cfg.fixation.lineWidthPix, ... - cfg.fixation.color, ... - [cfg.screen.center(1) cfg.screen.center(2)], smooth); + cfg.screen.win, ... + cfg.fixation.allCoords, ... + cfg.fixation.lineWidthPix, ... + cfg.fixation.color, ... + [cfg.screen.center(1) cfg.screen.center(2)], smooth); case 'dot' % Draw gap around fixation of 20% the size Screen('FillOval', ... - cfg.screen.win, ... - cfg.color.background, ... - CenterRect( ... - [0 0 repmat(1.2 * cfg.fixation.widthPix, 1, 2)], ... - cfg.screen.winRect)); + cfg.screen.win, ... + cfg.color.background, ... + CenterRect( ... + [0 0 repmat(1.2 * cfg.fixation.widthPix, 1, 2)], ... + cfg.screen.winRect)); % Draw fixation Screen('FillOval', ... - cfg.screen.win, ... - cfg.color.foreground, ... - CenterRect( ... - [0 0 repmat(cfg.fixation.widthPix, 1, 2)], ... - cfg.screen.winRect)); + cfg.screen.win, ... + cfg.color.foreground, ... + CenterRect( ... + [0 0 repmat(cfg.fixation.widthPix, 1, 2)], ... + cfg.screen.winRect)); case 'bestFixation' @@ -39,30 +41,30 @@ function drawFixation(cfg) % Draw gap around fixation of 20% the size Screen('FillOval', ... - cfg.screen.win, ... - cfg.color.background, ... - CenterRect( ... - [0 0 repmat(1.5 * cfg.fixation.widthPix, 1, 2)], ... - cfg.screen.winRect)); + cfg.screen.win, ... + cfg.color.background, ... + CenterRect( ... + [0 0 repmat(1.5 * cfg.fixation.widthPix, 1, 2)], ... + cfg.screen.winRect)); Screen('FillOval', ... - cfg.screen.win, ... - cfg.color.black, ... - cfg.fixation.outerOval, ... - cfg.fixation.widthPix); + cfg.screen.win, ... + cfg.color.black, ... + cfg.fixation.outerOval, ... + cfg.fixation.widthPix); Screen('DrawLines', ... - cfg.screen.win, ... - cfg.fixation.allCoords, ... - cfg.fixation.widthPix / 3, ... - cfg.color.white, ... - [cfg.screen.center(1) cfg.screen.center(2)]); + cfg.screen.win, ... + cfg.fixation.allCoords, ... + cfg.fixation.widthPix / 3, ... + cfg.color.white, ... + [cfg.screen.center(1) cfg.screen.center(2)]); Screen('FillOval', ... - cfg.screen.win, ... - cfg.color.black, ... - cfg.fixation.innerOval, ... - cfg.fixation.widthPix / 3); + cfg.screen.win, ... + cfg.color.black, ... + cfg.fixation.innerOval, ... + cfg.fixation.widthPix / 3); end diff --git a/src/fixation/initFixation.m b/src/fixation/initFixation.m index f5ed33e..2c21617 100644 --- a/src/fixation/initFixation.m +++ b/src/fixation/initFixation.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function cfg = initFixation(cfg) % cfg = initFixation(cfg) % @@ -42,16 +44,16 @@ % DOI 10.1016/j.visres.2012.10.012 cfg.fixation.outerOval = [ ... - cfg.screen.center(1) - cfg.fixation.widthPix / 2, ... - cfg.screen.center(2) - cfg.fixation.widthPix / 2, ... - cfg.screen.center(1) + cfg.fixation.widthPix / 2, ... - cfg.screen.center(2) + cfg.fixation.widthPix / 2]; + cfg.screen.center(1) - cfg.fixation.widthPix / 2, ... + cfg.screen.center(2) - cfg.fixation.widthPix / 2, ... + cfg.screen.center(1) + cfg.fixation.widthPix / 2, ... + cfg.screen.center(2) + cfg.fixation.widthPix / 2]; cfg.fixation.innerOval = [ ... - cfg.screen.center(1) - cfg.fixation.widthPix / 6, ... - cfg.screen.center(2) - cfg.fixation.widthPix / 6, ... - cfg.screen.center(1) + cfg.fixation.widthPix / 6, ... - cfg.screen.center(2) + cfg.fixation.widthPix / 6]; + cfg.screen.center(1) - cfg.fixation.widthPix / 6, ... + cfg.screen.center(2) - cfg.fixation.widthPix / 6, ... + cfg.screen.center(1) + cfg.fixation.widthPix / 6, ... + cfg.screen.center(2) + cfg.fixation.widthPix / 6]; end diff --git a/src/getExperimentEnd.m b/src/getExperimentEnd.m index 19d0ea7..4e74e96 100644 --- a/src/getExperimentEnd.m +++ b/src/getExperimentEnd.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function cfg = getExperimentEnd(cfg) drawFixation(cfg); @@ -8,8 +10,8 @@ ExpmtDurMin = floor(ExpmtDur / 60); ExpmtDurSec = mod(ExpmtDur, 60); disp(['Experiment lasted ', ... - num2str(ExpmtDurMin), ' minutes ', ... - num2str(ExpmtDurSec), ' seconds']); + num2str(ExpmtDurMin), ' minutes ', ... + num2str(ExpmtDurSec), ' seconds']); disp(' '); end diff --git a/src/getExperimentStart.m b/src/getExperimentStart.m index cdd451b..231bd45 100644 --- a/src/getExperimentStart.m +++ b/src/getExperimentStart.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function cfg = getExperimentStart(cfg) % cfg = getExperimentStart(cfg) % diff --git a/src/initPTB.m b/src/initPTB.m index fc38635..ba29173 100644 --- a/src/initPTB.m +++ b/src/initPTB.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function [cfg] = initPTB(cfg) % [cfg] = initPTB(cfg) % @@ -30,6 +32,9 @@ % For octave: to avoid displaying messenging one screen at a time more off; + % Resets the seed of the random number generator + setUpRand(); + % check for OpenGL compatibility, abort otherwise: AssertOpenGL; @@ -57,7 +62,7 @@ if isfield(cfg.screen, 'resolution') [newWidth, newHeight, newHz] = deal(cfg.screen.resolution{:}); cfg.screen.oldResolution = Screen('Resolution', cfg.screen.idx, ... - newWidth, newHeight, newHz); + newWidth, newHeight, newHz); end cfg = openWindow(cfg); @@ -107,9 +112,9 @@ [~, versionStruc] = PsychtoolboxVersion; cfg.software.version = sprintf('%i.%i.%i', ... - versionStruc.major, ... - versionStruc.minor, ... - versionStruc.point); + versionStruc.major, ... + versionStruc.minor, ... + versionStruc.point); runsOn = 'Matlab - '; if IsOctave @@ -163,9 +168,6 @@ function initDebug(cfg) InitializePsychSound(1); - cfg.audio.devIdx = []; - cfg.audio.playbackMode = 1; - if isfield(cfg.audio, 'useDevice') % get audio device list @@ -173,9 +175,10 @@ function initDebug(cfg) % find output device to use idx = find( ... - audioDev.NrInputChannels == cfg.audio.inputChannels && ... - audioDev.NrOutputChannels == cfg.audio.channels && ... - ~cellfun(@isempty, regexp({audioDev.HostAudioAPIName}, cfg.audio.deviceName))); + audioDev.NrInputChannels == cfg.audio.inputChannels && ... + audioDev.NrOutputChannels == cfg.audio.channels && ... + ~cellfun(@isempty, regexp({audioDev.HostAudioAPIName}, ... + cfg.audio.deviceName))); % save device ID cfg.audio.devIdx = audioDev(idx).DeviceIndex; @@ -186,11 +189,11 @@ function initDebug(cfg) end cfg.audio.pahandle = PsychPortAudio('Open', ... - cfg.audio.devIdx, ... - cfg.audio.playbackMode, ... - cfg.audio.requestedLatency, ... - cfg.audio.fs, ... - cfg.audio.channels); + cfg.audio.devIdx, ... + cfg.audio.playbackMode, ... + cfg.audio.requestedLatency, ... + cfg.audio.fs, ... + cfg.audio.channels); % set initial PTB volume for safety (participants can adjust this manually % at the begining of the experiment) @@ -209,7 +212,7 @@ function initDebug(cfg) if cfg.debug.smallWin [cfg.screen.win, cfg.screen.winRect] = ... Screen('OpenWindow', cfg.screen.idx, cfg.color.background, ... - [0, 0, 480, 270]); + [0, 0, 480, 270]); else [cfg.screen.win, cfg.screen.winRect] = ... Screen('OpenWindow', cfg.screen.idx, cfg.color.background); diff --git a/src/isOctave.m b/src/isOctave.m index f4a11ec..72d116b 100644 --- a/src/isOctave.m +++ b/src/isOctave.m @@ -1,3 +1,6 @@ +% (C) Copyright 2010-2020 Agah Karakuzu +% (C) Copyright 2020 CPP_PTB developers + function retval = isOctave % Return: true if the environment is Octave. % mostly used to testing when PTB is not in the path diff --git a/src/keyboard/checkAbort.m b/src/keyboard/checkAbort.m index 7d987c3..ed2e0df 100644 --- a/src/keyboard/checkAbort.m +++ b/src/keyboard/checkAbort.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function checkAbort(cfg, deviceNumber) % checkAbort(cfg, deviceNumber) % diff --git a/src/keyboard/checkAbortGetResponse.m b/src/keyboard/checkAbortGetResponse.m index e19cfea..15800fc 100644 --- a/src/keyboard/checkAbortGetResponse.m +++ b/src/keyboard/checkAbortGetResponse.m @@ -1,9 +1,11 @@ +% (C) Copyright 2020 CPP_PTB developers + function checkAbortGetResponse(responseEvents, cfg) if isfield(responseEvents, 'keyName') > 0 && ... any( ... - strcmpi({responseEvents(:).keyName}, cfg.keyboard.escapeKey) ... - ) + strcmpi({responseEvents(:).keyName}, cfg.keyboard.escapeKey) ... + ) errorAbortGetReponse; end end diff --git a/src/keyboard/collectAndSaveResponses.m b/src/keyboard/collectAndSaveResponses.m index 222ad85..45bf36a 100644 --- a/src/keyboard/collectAndSaveResponses.m +++ b/src/keyboard/collectAndSaveResponses.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function responseEvents = collectAndSaveResponses(cfg, logFile, experimentStart) responseEvents = getResponse('check', cfg.keyboard.responseBox, cfg); diff --git a/src/keyboard/getResponse.m b/src/keyboard/getResponse.m index 0f103dd..11aee4e 100644 --- a/src/keyboard/getResponse.m +++ b/src/keyboard/getResponse.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function responseEvents = getResponse(action, deviceNumber, cfg, getOnlyPress) % responseEvents = getResponse(action, deviceNumber, cfg, getOnlyPress) % @@ -60,8 +62,8 @@ if nargin < 3 cfg = struct( ... - 'keyboard', struct('responseKey', {}) ... - ); + 'keyboard', struct('responseKey', {}) ... + ); end if nargin < 4 @@ -201,7 +203,7 @@ function talkToMe(action, cfg) end - if verbose + if verbose > 2 fprintf('\n %s\n\n', msg); end diff --git a/src/keyboard/pressSpaceForMe.m b/src/keyboard/pressSpaceForMe.m index b6c209d..feef001 100644 --- a/src/keyboard/pressSpaceForMe.m +++ b/src/keyboard/pressSpaceForMe.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function pressSpaceForMe() % pressSpaceForMe() % diff --git a/src/keyboard/testKeyboards.m b/src/keyboard/testKeyboards.m index 6ce8e7a..4aa175d 100644 --- a/src/keyboard/testKeyboards.m +++ b/src/keyboard/testKeyboards.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function testKeyboards(cfg) % testKeyboards(cfg) % diff --git a/src/randomization/repeatShuffleConditions.m b/src/randomization/repeatShuffleConditions.m index c4a2ba7..a769021 100644 --- a/src/randomization/repeatShuffleConditions.m +++ b/src/randomization/repeatShuffleConditions.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function shuffledRepeats = repeatShuffleConditions(baseConditionVector, nbRepeats) % shuffledRepeats = repeatShuffleConditions(baseConditionVector, nbRepeats) % diff --git a/src/randomization/setTargetPositionInSequence.m b/src/randomization/setTargetPositionInSequence.m index 2f7a9bf..7fb0f96 100644 --- a/src/randomization/setTargetPositionInSequence.m +++ b/src/randomization/setTargetPositionInSequence.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function chosenPositions = setTargetPositionInSequence(seqLength, nbTarget, forbiddenPos) % chosenPositions = setTargetPositionInSequence(seqLength, nbTarget, forbiddenPos) % diff --git a/src/randomization/shuffle.m b/src/randomization/shuffle.m index 3aa2980..33326ca 100644 --- a/src/randomization/shuffle.m +++ b/src/randomization/shuffle.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function shuffled = shuffle(unshuffled) % in case PTB is not in the path % mostly for unit test diff --git a/src/readAndFilterLogfile.m b/src/readAndFilterLogfile.m index 4592f30..9049b44 100644 --- a/src/readAndFilterLogfile.m +++ b/src/readAndFilterLogfile.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function outputFiltered = readAndFilterLogfile(columnName, filterBy, varargin) % outputFiltered = readOutputFilter(filterHeader, filterContent, varargin) % @@ -26,8 +28,8 @@ tsvFile = varargin{1}; elseif isstruct(varargin{1}) tsvFile = fullfile(varargin{1}.dir.outputSubject, ... - varargin{1}.fileName.modality, ... - varargin{1}.fileName.events); + varargin{1}.fileName.modality, ... + varargin{1}.fileName.events); end % Check if the file exists diff --git a/src/screen/farewellScreen.m b/src/screen/farewellScreen.m index 37e0ad2..abbe0ab 100644 --- a/src/screen/farewellScreen.m +++ b/src/screen/farewellScreen.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function farewellScreen(cfg) Screen('FillRect', cfg.screen.win, cfg.color.background, cfg.screen.winRect); diff --git a/src/screen/standByScreen.m b/src/screen/standByScreen.m index 8df6d23..41610ee 100644 --- a/src/screen/standByScreen.m +++ b/src/screen/standByScreen.m @@ -1,10 +1,12 @@ +% (C) Copyright 2020 CPP_PTB developers + function standByScreen(cfg) Screen('FillRect', cfg.screen.win, cfg.color.background, cfg.screen.winRect); DrawFormattedText(cfg.screen.win, ... - cfg.task.instruction, ... - 'center', 'center', cfg.text.color); + cfg.task.instruction, ... + 'center', 'center', cfg.text.color); Screen('Flip', cfg.screen.win); diff --git a/src/utils/checkPtbVersion.m b/src/utils/checkPtbVersion.m index 7a42ab7..6acd23d 100644 --- a/src/utils/checkPtbVersion.m +++ b/src/utils/checkPtbVersion.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function checkPtbVersion() % Checks that the right dependencies are installed. @@ -15,22 +17,22 @@ function checkPtbVersion() [~, versionStruc] = PsychtoolboxVersion; fprintf(' Using PTB %i.%i.%i\n', ... - versionStruc.major, ... - versionStruc.minor, ... - versionStruc.point); + versionStruc.major, ... + versionStruc.minor, ... + versionStruc.point); if any([ ... versionStruc.major < PTB.major, ... versionStruc.minor < PTB.minor, ... versionStruc.point < PTB.point ... - ]) + ]) str = sprintf('%s %i.%i.%i %s.\n%s', ... - 'The current version PTB version is not', ... - PTB.major, ... - PTB.minor, ... - PTB.point, ... - 'In case of problems (e.g json file related) consider updating.'); + 'The current version PTB version is not', ... + PTB.major, ... + PTB.minor, ... + PTB.point, ... + 'In case of problems (e.g json file related) consider updating.'); warning(str); %#ok<*SPWRN> end catch diff --git a/src/utils/cleanUp.m b/src/utils/cleanUp.m index aeb7d6b..261de89 100755 --- a/src/utils/cleanUp.m +++ b/src/utils/cleanUp.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function cleanUp() % cleanUp() % diff --git a/src/utils/computeFOV.m b/src/utils/computeFOV.m index e825a43..0526244 100644 --- a/src/utils/computeFOV.m +++ b/src/utils/computeFOV.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function FOV = computeFOV(cfg) % FOV = computeFOV(cfg) % diff --git a/src/utils/degToPix.m b/src/utils/degToPix.m index 90d6072..a2a1c09 100644 --- a/src/utils/degToPix.m +++ b/src/utils/degToPix.m @@ -1,13 +1,28 @@ +% (C) Copyright 2020 CPP_PTB developers + function structure = degToPix(fieldName, structure, cfg) % structure = degToPix(fieldName, structure, cfg) % % For a given field value in degrees of visual angle in the structure, % this computes its value in pixel using the pixel per degree value of the cfg structure % and returns a structure with an additional field with Pix suffix holding that new value. + % + % + % USAGE: + % ------ + % fixation.width = 2; + % cfg.screen.ppd = 10; + % + % fixation = degToPix('width', fixation, cfg); + % + % Returns: + % ------- + % fixation.widthPix = 20; + % deg = getfield(structure, fieldName); %#ok structure = setfield(structure, [fieldName 'Pix'], ... - floor(deg * cfg.screen.ppd)); %#ok + deg * cfg.screen.ppd); %#ok end diff --git a/src/utils/makeGif.m b/src/utils/makeGif.m new file mode 100644 index 0000000..ef5a729 --- /dev/null +++ b/src/utils/makeGif.m @@ -0,0 +1,39 @@ +% (C) Copyright 2020 CPP_PTB developers + +close all; +clear; +clc; + +output_folder = fullfile(pwd, 'ouputs'); +screen_capture_folder = fullfile(output_folder, 'screen_capture'); +screen_capture_filename = fullfile(screen_capture_folder, ... + 'EMCL_kaks_frame-'); + +gif_file = fullfile(screen_capture_folder, ... + 'EMCL_kaks_frame.gif'); + +FigDim = [100, 100, 1000, 1500]; +Visibility = 'on'; + +for tif_img = 6:26 % 128 + + filename = fullfile([screen_capture_filename sprintf('%04.0f', tif_img) '.tif']); + + h = figure('name', 'test', ... + 'Position', FigDim, 'Color', [1 1 1], ... + 'Visible', Visibility); + + imshow(imread(filename)); + + frame = getframe(h); + im = frame2im(frame); + [imind, cm] = rgb2ind(im, 256); + + if tif_img == 1 + imwrite(imind, cm, gif_file, 'gif', 'Loopcount', inf); + else + imwrite(imind, cm, gif_file, 'gif', 'WriteMode', 'append'); + end + + close all; +end diff --git a/src/utils/pixToDeg.m b/src/utils/pixToDeg.m new file mode 100644 index 0000000..728d558 --- /dev/null +++ b/src/utils/pixToDeg.m @@ -0,0 +1,29 @@ +% (C) Copyright 2020 CPP_PTB developers + +function structure = pixToDeg(fieldName, structure, cfg) + % structure = pixToDeg(fieldName, structure, cfg) + % + % For a given field value in pixel in the structure, + % this computes its value in degrees of viual angle using the pixel per + % degree value of the cfg structure and returns a structure with an + % additional field holding that new value and with a fieldname with any + % 'Pix' suffix removed and replaced with the 'DegVA' suffix . + % + % USAGE: + % ------ + % fixation.widthPix = 20; + % cfg.screen.ppd = 10; + % + % fixation = degToPix('widthPix', fixation, cfg); + % + % Returns: + % ------- + % fixation.widthDegVA = 2; + % + + pix = getfield(structure, fieldName); %#ok + + structure = setfield(structure, [strrep(fieldName, 'Pix', '') 'DegVA'], ... + pix / cfg.screen.ppd); %#ok + +end diff --git a/src/utils/printCreditsCppPtb.m b/src/utils/printCreditsCppPtb.m index e39e0ac..9c8c7bb 100644 --- a/src/utils/printCreditsCppPtb.m +++ b/src/utils/printCreditsCppPtb.m @@ -1,16 +1,18 @@ +% (C) Copyright 2020 CPP_PTB developers + function printCreditsCppPtb() try version = fileread(fullfile(fileparts(mfilename('fullpath')), ... - '..', '..', 'version.txt')); + '..', '..', 'version.txt')); catch version = 'v1.0.0'; end contributors = { ... - 'Rémi Gau', ... - 'Marco Barilari', ... - 'Ceren Battal'}; + 'Rémi Gau', ... + 'Marco Barilari', ... + 'Ceren Battal'}; % DOI_URL = 'https://doi.org/10.5281/zenodo.3554331.'; diff --git a/src/utils/printScreen.m b/src/utils/printScreen.m new file mode 100644 index 0000000..071ffc0 --- /dev/null +++ b/src/utils/printScreen.m @@ -0,0 +1,16 @@ +% (C) Copyright 2020 CPP_PTB developers + +function frame = printScreen(win, filename, frame) + + image_array = Screen('GetImage', win); + + imagesc(image_array); + box off; + axis off; + set(gca, 'position', [0 0 1 1], 'units', 'normalized'); + + print(gcf, [filename sprintf('%04.0f', frame) '.jpeg'], '-djpeg'); + + frame = frame + 1; + +end diff --git a/src/utils/setDefaults.m b/src/utils/setDefaults.m index 8f34929..e9238dc 100644 --- a/src/utils/setDefaults.m +++ b/src/utils/setDefaults.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function structure = setDefaults(structure, fieldsToSet) % structure = setDefaults(structure, fieldsToSet) % @@ -16,16 +18,16 @@ structure.(names{i}) = ... setDefaults( ... - structure.(names{i}), ... - fieldsToSet.(names{i}) ... - ); + structure.(names{i}), ... + fieldsToSet.(names{i}) ... + ); else structure = setFieldToIfNotPresent( ... - structure, ... - names{i}, ... - thisField); + structure, ... + names{i}, ... + thisField); end end diff --git a/src/utils/setDefaultsPTB.m b/src/utils/setDefaultsPTB.m index 856f8f9..d0cd10e 100644 --- a/src/utils/setDefaultsPTB.m +++ b/src/utils/setDefaultsPTB.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function cfg = setDefaultsPTB(cfg) % cfg = setDefaultsPTB(cfg) % @@ -46,6 +48,9 @@ if isfield(cfg, 'audio') && cfg.audio.do + fieldsToSet.audio.devIdx = []; + fieldsToSet.audio.playbackMode = 1; + fieldsToSet.audio.fs = 44800; fieldsToSet.audio.channels = 2; fieldsToSet.audio.initVolume = 1; @@ -63,7 +68,7 @@ end - if isfield(cfg, 'eyeTracker') + if isfield(cfg, 'eyeTracker') && cfg.eyeTracker.do % Calibration environment fieldsToSet.eyeTracker.defaultCalibration = true; diff --git a/src/utils/setUpRand.m b/src/utils/setUpRand.m new file mode 100644 index 0000000..6311869 --- /dev/null +++ b/src/utils/setUpRand.m @@ -0,0 +1,42 @@ +% (C) Copyright 2010-2020 Sam Schwarzkopf +% (C) Copyright 2020 CPP_PTB developers + +function setUpRand() + % setUpRand() + % + % Resets the seed of the random number generator. Will "adapt" depending on the matlab/octave + % version. + % It is of great importance to do this before anything else! + % + % For an alternative from PTB see `ClockRandSeed` + + seed = sum(100 * clock); + + try + % Use the reccomended method in modern Matlab + RandStream.setGlobalStream(RandStream('mt19937ar', 'seed', seed)); + disp('Using modern randomizer...'); + catch + + try + % Use the recommended method in Matlab R2012a. + rng('shuffle'); + disp('Using less modern randomizer...'); + catch + + try + % Use worse methods for Octave or old versions of Matlab (e.g. 7.1.0.246 (R14) SP3). + rand('twister', seed); + randn('twister', seed); + disp('Using Octave or outdated randomizer...'); + catch + % For very old Matlab versions these are the only methods you can use. + % These are supposed to be flawed although you will probably not + % notice any effect of this for most situations. + rand('state', seed); + randn('state', seed); + disp('Using "flawed" randomizer...'); + end + end + + end diff --git a/src/waitFor.m b/src/waitFor.m index 0657f53..b612608 100644 --- a/src/waitFor.m +++ b/src/waitFor.m @@ -1,3 +1,5 @@ +% (C) Copyright 2020 CPP_PTB developers + function waitFor(cfg, timeToWait) % waitFor(cfg, timeToWait) % @@ -5,10 +7,10 @@ function waitFor(cfg, timeToWait) if cfg.pacedByTriggers.do waitForTrigger( ... - cfg, ... - cfg.keyboard.responseBox, ... - cfg.pacedByTriggers.quietMode, ... - timeToWait); + cfg, ... + cfg.keyboard.responseBox, ... + cfg.pacedByTriggers.quietMode, ... + timeToWait); else WaitSecs(timeToWait); end diff --git a/src/waitForTrigger.m b/src/waitForTrigger.m index 0a936ff..a8c8bac 100644 --- a/src/waitForTrigger.m +++ b/src/waitForTrigger.m @@ -1,4 +1,6 @@ -function waitForTrigger(varargin) +% (C) Copyright 2020 CPP_PTB developers + +function lastTriggerTimeStamp = waitForTrigger(varargin) % waitForTrigger(cfg, deviceNumber, quietMode, nbTriggersToWait) % % Counts a certain number of triggers coming from the scanner before returning. @@ -19,14 +21,14 @@ function waitForTrigger(varargin) % triggers coming from the scanner in a real case scenario. % % INPUTS - % - varargin{1} = cfg + % - varargin{1} = cfg % % - varargin{2} = deviceNumber % % - varargin{3} = quietMode: a boolean to make sure nothing is printed on the screen or % the prompt % - % - nbTriggersToWait + % - nvarargin{4} = nbTriggersToWait [cfg, nbTriggersToWait, deviceNumber, quietMode] = checkInputs(varargin); @@ -35,7 +37,7 @@ function waitForTrigger(varargin) if strcmpi(cfg.testingDevice, 'mri') msg = ['Experiment starting in ', ... - num2str(nbTriggersToWait - triggerCounter), '...']; + num2str(nbTriggersToWait - triggerCounter), '...']; talkToMe(cfg, msg, quietMode); @@ -43,7 +45,7 @@ function waitForTrigger(varargin) keyCode = []; %#ok - [~, keyCode] = KbPressWait(deviceNumber); + [~, lastTriggerTimeStamp, keyCode] = KbCheck(deviceNumber); if strcmp(KbName(keyCode), cfg.mri.triggerKey) @@ -105,7 +107,7 @@ function talkToMe(cfg, msg, quietMode) if isfield(cfg, 'screen') && isfield(cfg.screen, 'win') DrawFormattedText(cfg.screen.win, msg, ... - 'center', 'center', cfg.text.color); + 'center', 'center', cfg.text.color); Screen('Flip', cfg.screen.win); diff --git a/tests/test_checkAbortGetResponse.m b/tests/test_checkAbortGetResponse.m index b50c0ab..bbce61c 100644 --- a/tests/test_checkAbortGetResponse.m +++ b/tests/test_checkAbortGetResponse.m @@ -15,6 +15,6 @@ function test_checkAbortGetResponseBasic() cfg.keyboard.escapeKey = 'ESCAPE'; assertExceptionThrown(@()checkAbortGetResponse(responseEvents, cfg), ... - 'getResponse:abortRequested'); + 'getResponse:abortRequested'); end diff --git a/tests/test_computeFOV.m b/tests/test_computeFOV.m index cc54b09..a151e42 100644 --- a/tests/test_computeFOV.m +++ b/tests/test_computeFOV.m @@ -24,6 +24,6 @@ function test_computeFOVError() cfg.screen.monitorDistance = 1.2; % error as distance is most likely in meter assertExceptionThrown(@()computeFOV(cfg), ... - 'computeFOV:wrongDistanceToScreen'); + 'computeFOV:wrongDistanceToScreen'); end diff --git a/tests/test_computeRadialMotionDirection.m b/tests/test_computeRadialMotionDirection.m index f568037..86f65fe 100644 --- a/tests/test_computeRadialMotionDirection.m +++ b/tests/test_computeRadialMotionDirection.m @@ -10,32 +10,30 @@ function test_computeRadialMotionDirectionBasic() %% set up - cfg.design.motionType = 'radial'; - cfg.dot.matrixWidth = 50; % in pixels - cfg.screen.winWidth = 100; % in pixels + cfg.dot.matrixWidth = 100; % in pixels cfg.timing.eventDuration = 2; dots.direction = 666; dots.positions = [ - 100, 100 / 2; ... % middle of right side - 100, 100; ... % top right corner - 100 / 2, 100; ... - 0, 100 / 2; ... - 0, 0; ... - 100 / 2, 0]; + 100, 100 / 2; ... % middle of right side + 100, 100; ... % top right corner + 100 / 2, 100; ... + 0, 100 / 2; ... + 0, 0; ... + 100 / 2, 0]; - % direction = computeRadialMotionDirection(cfg, positions, direction); + angleMotion = computeRadialMotionDirection(dots.positions, cfg.dot.matrixWidth, dots); expectedDirection = [ - 0; ... right - 45; ... up-right - 90; ... up - 180; ... left - -135; ... down left - -90]; % down + 0; ... right + 45; ... up-right + 90; ... up + 180; ... left + -135; ... down left + -90]; % down %% test - % assertEqual(expectedDirection, direction); + assertEqual(angleMotion, expectedDirection); end diff --git a/tests/test_generateNewDotPositions.m b/tests/test_generateNewDotPositions.m index f6a2721..d47e285 100644 --- a/tests/test_generateNewDotPositions.m +++ b/tests/test_generateNewDotPositions.m @@ -8,10 +8,10 @@ function test_generateNewDotPositionsBasic() - cfg.dot.matrixWidth = 400; + dotMatrixWidth = 400; dotNumber = 200; - newPositions = generateNewDotPositions(cfg, dotNumber); + newPositions = generateNewDotPositions(dotMatrixWidth, dotNumber); assertEqual([200, 2], size(newPositions)); diff --git a/tests/test_initDots.m b/tests/test_initDots.m index 7f92d6e..3f0dce7 100644 --- a/tests/test_initDots.m +++ b/tests/test_initDots.m @@ -27,18 +27,17 @@ function test_initDotsBasic() cfg.dot.coherence = 1; % proportion cfg.dot.lifeTime = 0.250; % in seconds cfg.dot.matrixWidth = 50; % in pixels - cfg.screen.winWidth = 2000; % in pixels cfg.timing.eventDuration = 1; % in seconds cfg.screen.ifi = 0.01; % in seconds thisEvent.direction = 0; - thisEvent.speed = 10; + thisEvent.speedPix = 10; [dots] = initDots(cfg, thisEvent); %% Undeterministic ouput assertTrue(all(dots.positions(:) >= 0)); - assertTrue(all(dots.positions(:) <= 2000)); + assertTrue(all(dots.positions(:) <= 50)); assertTrue(all(dots.time(:) >= 0)); assertTrue(all(dots.time(:) <= 1 / 0.01)); @@ -47,15 +46,14 @@ function test_initDotsBasic() expectedStructure.isSignal = ones(10, 1); expectedStructure.speeds = repmat([1 0], 10, 1) * 10; expectedStructure.speedPixPerFrame = 10; - expectedStructure.direction = zeros(10, 1); - expectedStructure.directionAllDots = zeros(10, 1); + expectedStructure.direction = 0; % remove undeterministic output dots = rmfield(dots, 'time'); dots = rmfield(dots, 'positions'); %% test - assertEqual(expectedStructure, dots); + assertEqual(dots, expectedStructure); end @@ -66,12 +64,11 @@ function test_initDotsStatic() cfg.dot.coherence = 1; % proportion cfg.dot.lifeTime = 0.250; % in seconds cfg.dot.matrixWidth = 50; % in pixels - cfg.screen.winWidth = 2000; % in pixels cfg.timing.eventDuration = 1; % in seconds cfg.screen.ifi = 0.01; % in seconds thisEvent.direction = -1; - thisEvent.speed = 10; + thisEvent.speedPix = 10; [dots] = initDots(cfg, thisEvent); @@ -84,38 +81,64 @@ function test_initDotsStatic() expectedStructure.isSignal = ones(10, 1); expectedStructure.speeds = zeros(10, 2); expectedStructure.speedPixPerFrame = 0; - expectedStructure.direction = -1 * ones(10, 1); - expectedStructure.directionAllDots = -1 * ones(10, 1); + expectedStructure.direction = -1; %% test - assertEqual(expectedStructure, dots); + assertEqual(dots, expectedStructure); end function test_initDotsRadial() + %% set up + + % % Dot life time in seconds + % cfg.dot.lifeTime + % % Number of dots + % cfg.dot.number + % Proportion of coherent dots. + % cfg.dot.coherence + % + % % Direction (an angle in degrees) + % thisEvent.direction + % % Speed expressed in pixels per frame + % thisEvent.speed + cfg.design.motionType = 'radial'; cfg.dot.number = 10; cfg.dot.coherence = 1; % proportion cfg.dot.lifeTime = 0.250; % in seconds cfg.dot.matrixWidth = 50; % in pixels - cfg.screen.winWidth = 2000; % in pixels cfg.timing.eventDuration = 1; % in seconds cfg.screen.ifi = 0.01; % in seconds - thisEvent.direction = 666; % outward motion - thisEvent.speed = 10; + thisEvent.direction = 666; + thisEvent.speedPix = 10; [dots] = initDots(cfg, thisEvent); - %% data to test against - XY = dots.positions - 2000 / 2; - angle = cart2pol(XY(:, 1), XY(:, 2)); - angle = angle / pi * 180; - [horVector, vertVector] = decomposeMotion(angle); - speeds = [horVector, vertVector] * 10; + %% Deterministic output : data to test against + expectedStructure.lifeTime = 25; + expectedStructure.isSignal = ones(10, 1); + expectedStructure.speedPixPerFrame = 10; + expectedStructure.direction = 666; + + directionAllDots = setDotDirection( ... + dots.positions, ... + cfg, ... + expectedStructure, ... + expectedStructure.isSignal); + [horVector, vertVector] = decomposeMotion(directionAllDots); + if strcmp(cfg.design.motionType, 'radial') + vertVector = vertVector * -1; + end + expectedStructure.speeds = [horVector, vertVector] * expectedStructure.speedPixPerFrame; + + % remove undeterministic output + dots = rmfield(dots, 'time'); + dots = rmfield(dots, 'positions'); %% test - % assertEqual(speeds, dots.speeds); + assertEqual(dots, expectedStructure); end diff --git a/tests/test_pixToDeg.m b/tests/test_pixToDeg.m new file mode 100644 index 0000000..847b233 --- /dev/null +++ b/tests/test_pixToDeg.m @@ -0,0 +1,21 @@ +function test_suite = test_pixToDeg %#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_pixToDegBasic() + + fixation.widthPix = 20; + cfg.screen.ppd = 10; + + fixation = pixToDeg('widthPix', fixation, cfg); + + expectedStruct.widthDegVA = 2; + expectedStruct.widthPix = 20; + + assertEqual(expectedStruct, fixation); + +end diff --git a/tests/test_reseedDots.m b/tests/test_reseedDots.m index 189e0bd..6097bf5 100644 --- a/tests/test_reseedDots.m +++ b/tests/test_reseedDots.m @@ -8,46 +8,44 @@ function test_reseedDotsBasic() - cfg.screen.winWidth = 2000; + dotNb = 5; - cfg.design.motionType = 'radial'; + cfg.design.motionType = 'translation'; + cfg.timing.eventDuration = 1; % in seconds + cfg.screen.ifi = 0.01; % in seconds - cfg.dot.matrixWidth = 50; % in pixels - cfg.dot.number = 5; + cfg.dot.matrixWidth = 1000; % in pixels + cfg.dot.number = dotNb; cfg.dot.sizePix = 20; cfg.dot.proportionKilledPerFrame = 0; - cfg.fixation.widthPix = 20; + cfg.fixation.widthPix = 5; dots.lifeTime = 100; dots.speedPixPerFrame = 3; dots.direction = 90; - dots.isSignal = true(5, 1); + dots.isSignal = true(dotNb, 1); + dots.speeds = ones(dotNb, 2); dots.positions = [ ... - 49, 1; % OK - 490, 2043; % out of frame - -104, 392; % out of frame - 492, 402; % OK - 1000, 1000; % on the fixation cross - ]; - - dots.time = [ ... - 6; ... OK - 4; ... OK - 56; ... OK - 300; ... % exceeded its life time - 50]; % OK + 300, 10 % OK + 750, 1010 % out of frame + -1040, 50 % out of frame + 300, 300 % OK + 500, 500 % on the fixation cross + ]; + + originalTime = [ ... + 6; ... OK + 4; ... OK + 56; ... OK + 300; ... % exceeded its life time + 50]; % OK + dots.time = originalTime; dots = reseedDots(dots, cfg); - reseeded = [ ... - 6; - 1; - 1; - 1; - 1]; - - assertEqual(reseeded, dots.time); + assertEqual(dots.time(1), originalTime(1)); + assertTrue(all(dots.time(2:end) ~= originalTime(2:end))); end diff --git a/tests/test_seedDots.m b/tests/test_seedDots.m new file mode 100644 index 0000000..782b60d --- /dev/null +++ b/tests/test_seedDots.m @@ -0,0 +1,37 @@ +function test_suite = test_seedDots %#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_seedDotsBasic() + + %% set up + + cfg.dot.matrixWidth = 400; + cfg.design.motionType = 'translation'; + cfg.timing.eventDuration = 1; % in seconds + cfg.screen.ifi = 0.01; % in seconds + + nbDots = 10; + isSignal = [true(5, 1); false(nbDots - 5, 1)]; + + dots.direction = 0; + dots.speedPixPerFrame = 10; + + [positions, speeds, time] = seedDots(dots, cfg, isSignal); + + %% Deterministic output + assertEqual(size(positions), [nbDots, 2]); + assertTrue(all(all([ ... + positions(:) <= cfg.dot.matrixWidth, ... + positions(:) >= 0]))); + + assertTrue(all(time(:) >= 0)); + assertTrue(all(time(:) <= 1 / 0.01)); + + assertEqual(speeds(1:5, :), repmat([10 0], 5, 1)); + +end diff --git a/tests/test_setDefaultsPTB.m b/tests/test_setDefaultsPTB.m index ae61b66..e0facb9 100644 --- a/tests/test_setDefaultsPTB.m +++ b/tests/test_setDefaultsPTB.m @@ -43,14 +43,16 @@ function test_setDefaultsPtbAudio() % test data expectedCfg = returnExpectedCFG(); expectedCfg.audio = struct( ... - 'do', true, ... - 'fs', 44800, ... - 'channels', 2, ... - 'initVolume', 1, ... - 'requestedLatency', 3, ... - 'repeat', 1, ... - 'startCue', 0, ... - 'waitForDevice', 1); + 'do', true, ... + 'devIdx', [], ... + 'playbackMode', 1, ... + 'fs', 44800, ... + 'channels', 2, ... + 'initVolume', 1, ... + 'requestedLatency', 3, ... + 'repeat', 1, ... + 'startCue', 0, ... + 'waitForDevice', 1); % test assertEqual(expectedCfg, cfg); @@ -60,11 +62,11 @@ function test_setDefaultsPtbAudio() function expectedCFG = returnExpectedCFG() expectedCFG = struct( ... - 'testingDevice', 'pc', ... - 'debug', struct('do', true, 'transpWin', true, 'smallWin', true), ... - 'color', struct( ... - 'background', [0 0 0]), ... - 'text', struct('font', 'Courier New', 'size', 18, 'style', 1)); + 'testingDevice', 'pc', ... + 'debug', struct('do', true, 'transpWin', true, 'smallWin', true), ... + 'color', struct( ... + 'background', [0 0 0]), ... + 'text', struct('font', 'Courier New', 'size', 18, 'style', 1)); expectedCFG.screen.monitorWidth = 42; expectedCFG.screen.monitorDistance = 134; diff --git a/tests/test_setDotDirection.m b/tests/test_setDotDirection.m index f68fdd1..12a80b4 100644 --- a/tests/test_setDotDirection.m +++ b/tests/test_setDotDirection.m @@ -6,21 +6,52 @@ initTestSuite; end -function test_setDotDirectionBasic() +function test_setDotDirectionInit() % create 5 coherent dots with direction == 362 (that should give 2 in the % end) - % also creates 955 additonal dots with random direction between 0 and 360 + % also creates additonal dots with random direction between 0 and 360 - cfg.dot.number = 1000; + nbDots = 10; + + cfg.dot.matrixWidth = 400; cfg.design.motionType = 'translation'; + dots.direction = 362; + dots.isSignal = [true(5, 1); false(nbDots - 5, 1)]; + + positions = generateNewDotPositions(cfg.dot.matrixWidth, numel(dots.isSignal)); + + directionAllDots = setDotDirection(positions, cfg, dots, dots.isSignal); + + assertEqual(directionAllDots(1:5), 2 * ones(5, 1)); + assertGreaterThan(directionAllDots, zeros(size(directionAllDots))); + assertLessThan(directionAllDots, 360 * ones(size(directionAllDots))); + +end + +function test_setDotDirectionReturn() + % make sure that if the directions are already set it only changes that of + % the noise dots + % input has 4 signal dots with set directions also has additonal noise dots + % with negative direction + + nbDots = 8; + + cfg.dot.matrixWidth = 400; + cfg.design.motionType = 'translation'; + + dots.direction = [ ... + [362; 2; -362; -2]; ... + -20 * ones(4, 1)]; + dots.isSignal = [true(4, 1); false(nbDots - 4, 1)]; - dots.isSignal = [true(5, 1); false(1000 - 5, 1)]; + positions = generateNewDotPositions(cfg.dot.matrixWidth, numel(dots.isSignal)); - dots = setDotDirection(cfg, dots); + directionAllDots = setDotDirection(positions, cfg, dots, dots.isSignal); - assertTrue(all(dots.directionAllDots(1:5) == 2 * ones(5, 1))); - assertTrue(all(dots.directionAllDots >= 0)); - assertTrue(all(dots.directionAllDots <= 360)); + assertEqual(directionAllDots(1:4), [2 2 358 358]'); + assertGreaterThan(directionAllDots, zeros(size(directionAllDots))); + assertLessThan(directionAllDots, 360 * ones(size(directionAllDots))); + assertTrue(all(directionAllDots(5:end) ~= -20 * ones(4, 1))); end