Skip to content
94 changes: 62 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,44 +35,74 @@ We use the [MISS_HIT linter](https://florianschanda.github.io/miss_hit/style_che

## How to install

### Use the matlab package manager
### Download with git

``` 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
# move into the folder you have just created
cd CPP_PTB
# add the src folder to the matlab path and save the path
matlab -nojvm -nosplash -r "addpath(fullfile(pwd)); savepath ();"
```

This repository can be added as a dependencies by listing it in a [mpm-requirements.txt file](.mpm-requirements.txt)
as follows:
Then get the latest commit:
```bash
# from the directory where you downloaded the code
git pull origin master
```

CPP_PTB -u https://github.com/cpp-lln-lab/CPP_PTB.git
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 calle version1 at the version tag v0.0.1
git checkout -b version1 v0.0.1
```

You can then use the [matlab package manager](https://github.com/mobeets/mpm), to simply download the appropriate version of those dependencies and add them to your path by running a `getDependencies` function like the one below where you just need to replace `YOUR_EXPERIMENT_NAME` by the name of your experiment.
### Add as a submodule

```matlab
function getDependencies(action)
% Will install on your computer the matlab dependencies specified in the mpm-requirements.txt
% and add them to the matlab path. The path is never saved so you need to run getDependencies() when
% you start matlab.
%
% getDependencies('update') will force the update and overwrite previous version of the dependencies.
%
% getDependencies() If you only already have the appropriate version but just want to add them to the matlab path.

experimentName = YOUR_EXPERIMENT_NAME;

if nargin<1
action = '';
end

switch action
case 'update'
% install dependencies
mpm install -i mpm-requirements.txt -f -c YOUR_EXPERIMENT_NAME
end

% adds them to the path
mpm_folder = fileparts(which('mpm'));
addpath(genpath(fullfile(mpm_folder, 'mpm-packages', 'mpm-collections', experimentName)));

end
Add it as a submodule in the repo you are working on.

``` 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
# move into the folder you have just created
cd CPP_PTB
# add the src folder to the matlab path and save the path
matlab -nojvm -nosplash -r "addpath(fullfile(pwd))"
```

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
```

Remember that updates to submodules need to be commited as well.

**TO DO**
<!-- Submodules
pros: in principle, downloading the experiment you have the whole package plus the benefit to stay updated and use version control of this dependency. Can probably handle a use case in which one uses different version on different projects (e.g. older and newer projects).
cons: for pro users and not super clear how to use it at the moment. -->

### Direct download

Download the code. Unzip. And add to the matlab path.

Pick a specific version:

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

**TO DO**
<!-- Download a specific version and c/p it in a subfun folder
pros: the easiest solution to share the code and 'installing' it on the stimulation computer (usually not the one used to develop the code).
cons: extreme solution useful only at the very latest stage (i.e. one minute before acquiring your data); prone to be customized/modified (is it what we want?) -->

## Setting up keyboards

To select a specific keyboard to be used by the experimenter or the participant, you need to know
Expand Down
55 changes: 55 additions & 0 deletions apertureTexture.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
function cfg = apertureTexture(action, cfg, thisEvent)

transparent = [0 0 0 0];

switch action

case 'init'

% we take the screen height as maximum aperture width if not
% specified.
if ~isfield(cfg.aperture, 'width') || isempty(cfg.aperture.width)
cfg.aperture.width = cfg.screen.winRect(4) / cfg.screen.ppd;
end
cfg.aperture = degToPix('width', cfg.aperture, cfg);

cfg.aperture.texture = Screen('MakeTexture', cfg.screen.win, ...
cfg.color.background(1) * ones(cfg.screen.winRect([4 3])));

case 'make'

switch cfg.aperture.type

case 'none'

Screen('Fillrect', cfg.aperture.texture, transparent);

case 'circle'

diameter = cfg.aperture.widthPix;

xPos = cfg.screen.center(1);
yPos = cfg.screen.center(2);
if isfield(cfg.aperture, 'xPosPix')
xPos = cfg.screen.center(1) + cfg.aperture.xPosPix;
end
if isfield(cfg.aperture, 'yPosPix')
yPos = cfg.screen.center(2) + cfg.aperture.yPosPix;
end

Screen('FillOval', cfg.aperture.texture, transparent, ...
CenterRectOnPoint([0 0 repmat(diameter, 1, 2)], ...
xPos, yPos));

end

case 'draw'

Screen('DrawTexture', cfg.screen.win, cfg.aperture.texture);

% Screen('DrawTexture', cfg.screen.win, apertureTexture, ...
% cfg.screen.winRect, cfg.screen.winRect, current.apertureAngle - 90);

end

end
11 changes: 9 additions & 2 deletions computeFOV.m
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,15 @@
%
% computes the number of degrees of visual angle in the whole field of view
%
% δ = 2 arctan ( d / 2D )
%
% δ is the angular diameter, and d is the actual diameter of the object,
% and D is the distance to the object.
% The result obtained is in radians.
%

FOV = 2 * ...
(180 * (atan(cfg.screen.monitorWidth / (2 * cfg.screen.monitorDistance)) / pi));
FOV = ...
180 / pi * ...
2 * atan(cfg.screen.monitorWidth / (2 * cfg.screen.monitorDistance));

end
5 changes: 5 additions & 0 deletions decompMotion.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function [horVector, vertVector] = decompMotion(angleMotion)
% decompose angle of start motion into horizontal and vertical vector
horVector = cos(pi * angleMotion / 180);
vertVector = -sin(pi * angleMotion / 180);
end
2 changes: 1 addition & 1 deletion degToPix.m
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
deg = getfield(structure, fieldName); %#ok<GFLD>

structure = setfield(structure, [fieldName 'Pix'], ...
floor(cfg.screen.ppd * deg)) ; %#ok<SFLD>
floor(deg * cfg.screen.ppd)) ; %#ok<SFLD>

end
27 changes: 27 additions & 0 deletions dotTexture.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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])));

case 'make'

dotType = 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);

case 'draw'

Screen('DrawTexture', cfg.screen.win, cfg.dot.texture);

end

end
16 changes: 16 additions & 0 deletions drawFixation.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
function drawFixation(cfg)
% Define the parameters of the fixation cross in `cfg` and `expParameters`

if strcmp(cfg.fixation.type, 'cross')

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);
end

end
11 changes: 0 additions & 11 deletions drawFixationCross.m

This file was deleted.

8 changes: 8 additions & 0 deletions farewellScreen.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
function farewellScreen(cfg)

Screen('FillRect', cfg.screen.win, cfg.color.background, cfg.screen.winRect);
DrawFormattedText(cfg.screen.win, 'Thank you!', 'center', 'center', cfg.text.color);
Screen('Flip', cfg.screen.win);
WaitSecs(cfg.mri.repetitionTime * 2);

end
15 changes: 15 additions & 0 deletions getExperimentEnd.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
function cfg = getExperimentEnd(cfg)

drawFixation(cfg);
endExpmt = Screen('Flip', cfg.screen.win);

disp(' ');
ExpmtDur = endExpmt - cfg.experimentStart;
ExpmtDurMin = floor(ExpmtDur / 60);
ExpmtDurSec = mod(ExpmtDur, 60);
disp(['Experiment lasted ', ...
num2str(ExpmtDurMin), ' minutes ', ...
num2str(ExpmtDurSec), ' seconds']);
disp(' ');

end
6 changes: 6 additions & 0 deletions getExperimentStart.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
function cfg = getExperimentStart(cfg)
% Show the fixation cross
drawFixation(cfg);
vbl = Screen('Flip', cfg.screen.win);
cfg.experimentStart = vbl;
end
19 changes: 19 additions & 0 deletions initFixation.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
function cfg = initFixation(cfg)

if strcmp(cfg.fixation.type, 'cross')

% Convert some values from degrees to pixels
cfg.fixation = degToPix('width', cfg.fixation, cfg);
cfg.fixation = degToPix('xDisplacement', cfg.fixation, cfg);
cfg.fixation = degToPix('yDisplacement', cfg.fixation, cfg);

% Prepare fixation cross
cfg.fixation.xCoords = [-cfg.fixation.widthPix cfg.fixation.widthPix 0 0] / 2 + ...
cfg.fixation.xDisplacementPix;
cfg.fixation.yCoords = [0 0 -cfg.fixation.widthPix cfg.fixation.widthPix] / 2 + ...
cfg.fixation.yDisplacementPix;
cfg.fixation.allCoords = [cfg.fixation.xCoords; cfg.fixation.yCoords];

end

end
6 changes: 5 additions & 1 deletion initPTB.m
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@
% monitor width
% This assumes that the window fills the whole screen
cfg.screen.FOV = computeFOV(cfg);
cfg.screen.ppd = cfg.screen.winRect(3) / cfg.screen.FOV;
cfg.screen.ppd = cfg.screen.winWidth / cfg.screen.FOV;

% Initialize visual parmaters for fixation cross or dot
cfg = initFixation(cfg);

%% Select specific text font, style and size
initText(cfg);
Expand Down Expand Up @@ -179,6 +182,7 @@ function initDebug(cfg)

% Enable alpha-blending, set it to a blend equation useable for linear
% superposition with alpha-weighted source.
% Required for drwing smooth lines and screen('DrawDots')
Screen('BlendFunction', cfg.screen.win, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

end
Expand Down
49 changes: 49 additions & 0 deletions initializeDots.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
function [dots] = initializeDots(cfg, thisEvent)

direction = thisEvent.direction(1);

dots.lifeTime = cfg.dot.lifeTime;

speedPixPerFrame = thisEvent.speed(1);

% decide which dots are signal dots (1) and those are noise dots (0)
dots.isSignal = rand(cfg.dot.number, 1) < cfg.dot.coherence;

% for static dots
if direction == -1
speedPixPerFrame = 0;
dots.lifeTime = cfg.eventDuration;
dots.isSignal = ones(cfg.dot.number, 1);
end

% Convert from seconds to frames
dots.lifeTime = ceil(dots.lifeTime / cfg.screen.ifi);

% 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 = rand(cfg.dot.number, 2) * cfg.screen.winWidth;

% Set a N x 2 matrix that speed in X and Y
dots.speeds = nan(cfg.dot.number, 2);

% Coherent dots
[horVector, vertVector] = decompMotion(direction);
dots.speeds(dots.isSignal, :) = ...
repmat([horVector, vertVector], sum(dots.isSignal), 1);

% If not 100% coherence, we get new random direction for the other dots
direction = rand(sum(~dots.isSignal), 1) * 360;
[horVector, vertVector] = decompMotion(direction);
dots.speeds(~dots.isSignal, :) = [horVector, vertVector];

% So far we were working wiht unit vectors convert that speed in pixels per
% frame
dots.speeds = dots.speeds * speedPixPerFrame;

% Create a vector to update to dotlife time of each dot
% Not all set to one so the dots will die at different times
dots.time = floor(rand(cfg.dot.number, 1) * cfg.eventDuration / cfg.screen.ifi);

end
11 changes: 11 additions & 0 deletions setDefaultsPTB.m
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@
fieldsToSet.screen.monitorWidth = 42;
fieldsToSet.screen.monitorDistance = 134;

% fixation cross or dot
fieldsToSet.fixation.type = 'cross';
fieldsToSet.fixation.xDisplacement = 0;
fieldsToSet.fixation.yDisplacement = 0;
fieldsToSet.fixation.color = [255 255 255];
fieldsToSet.fixation.width = 1;
fieldsToSet.fixation.lineWidthPix = 5;

% define visual apperture field
fieldsToSet.aperture.type = 'none';

if isfield(cfg, 'audio') && cfg.audio.do

fieldsToSet.audio.fs = 44800;
Expand Down
Loading