diff --git a/README.md b/README.md index 1cf7da8..3a249e2 100644 --- a/README.md +++ b/README.md @@ -154,13 +154,9 @@ git submodule foreach --recursive 'git submodule update' Download the code. Unzip. And add to the matlab path. -Pick a specific version: +Pick a specific version from [here](https://github.com/cpp-lln-lab/CPP_PTB/releases). -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 @@ -187,7 +183,6 @@ In practice, we use the following regular expression for function names: > > - 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) > - ... diff --git a/docs/00_index.md b/docs/00-index.md similarity index 99% rename from docs/00_index.md rename to docs/00-index.md index e0ceec6..33c377c 100644 --- a/docs/00_index.md +++ b/docs/00-index.md @@ -193,7 +193,7 @@ let PTB find and use the default device. ## 3. functions descriptions The main functions of the toolbox are described -[here](./10_functions_description.md). +[here](./10-functions-description.md). ## 4. Annexes diff --git a/docs/10_functions_description.md b/docs/10-functions-description.md similarity index 67% rename from docs/10_functions_description.md rename to docs/10-functions-description.md index f0bfa39..fb562ce 100644 --- a/docs/10_functions_description.md +++ b/docs/10-functions-description.md @@ -1,42 +1,39 @@ # 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) - - 6.4. [setUpRand](#setUprand) - - - - -## 1. General functions - -### 1.1. initPTB + + +- [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. @@ -57,34 +54,34 @@ any other functions of CPP_PTB. - hides cursor - sound -### 1.2. cleanUp +### 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 +### getExperimentStart Wrapper function that will show a fixation cross and collect a start timestamp in `cfg.experimentStart` -### 1.4. getExperimentEnd +### 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 +### 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 +### 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 +### 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 @@ -106,12 +103,12 @@ There are several actions to perform: cpp-lln-lab/CPP_BIDS, in the output folder and shut the connection between the stimulation computer and the EyeLink computer -### 1.8. standByScreen +### standByScreen It shows a basic one-page instruction stored in `cfg.task.instruction` and wait for `space` stroke. -### 1.9. waitForTrigger +### waitForTrigger Counts a certain number of triggers coming from the mri/scanner before returning. Requires number of triggers to wait for. @@ -119,27 +116,27 @@ 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 +### waitFor A generic function that you can use to for a certain amount of time or a number of triggers -### 1.11. readAndFilterLogfile +### 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 +## Keyboard functions: response collection and aborting experiment -### 2.1. testKeyboards +### 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 +### getResponse It is wrapper function to use `KbQueue` which is definitely what you should use to collect responses. @@ -172,7 +169,7 @@ In brief, there are several actions you can execute with this function. again. - release: closes the buffer for good. -### 2.3. pressSpaceForme +### pressSpaceForme Use that to stop your script and only restart when the space bar is pressed. @@ -184,39 +181,39 @@ some set up before giving the green light. A simple function that will quit your experiment if you press the key you have defined in `cfg.keyboard.escapeKey`. -## 3. Fixations +## Fixations -### 3.1. drawFixationCross +### drawFixationCross Define the parameters of the fixation cross in `cfg` and `expParameters` and this does the rest. -## 4. Drawing dots +## Drawing dots -## 5. Drawing apertures +## Drawing apertures -## 6. Randomization +## Randomization Functions that can be used to create random stimuli sequences. -### 6.1. shuffle +### 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 +### 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 +### 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 +### 6.4. 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! +Set up the randomizers for uniform and normal distributions. It is of great +importance to do this before anything else! 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/src/aperture/apertureTexture.m b/src/aperture/apertureTexture.m index 87a4f03..90b9307 100644 --- a/src/aperture/apertureTexture.m +++ b/src/aperture/apertureTexture.m @@ -106,6 +106,8 @@ case 'circle' + Screen('Fillrect', cfg.aperture.texture, cfg.color.background); + diameter = cfg.aperture.widthPix; if isfield(cfg.aperture, 'xPosPix') diff --git a/src/dot/computeCartCoord.m b/src/dot/computeCartCoord.m index 063944d..a2c6f40 100644 --- a/src/dot/computeCartCoord.m +++ b/src/dot/computeCartCoord.m @@ -1,7 +1,7 @@ -function cartesianCoordinates = computeCartCoord(positions, cfg) +function cartesianCoordinates = computeCartCoord(positions, dotMatrixWidth) + cartesianCoordinates = ... - [positions(:, 1) - cfg.dot.matrixWidth / 2, ... % x coordinate - positions(:, 2) - cfg.dot.matrixWidth / 2]; % 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..1879f61 100644 --- a/src/dot/computeRadialMotionDirection.m +++ b/src/dot/computeRadialMotionDirection.m @@ -1,8 +1,8 @@ -function angleMotion = computeRadialMotionDirection(cfg, dots) +function angleMotion = computeRadialMotionDirection(positions, dotMatrixWidth, dots) - cartesianCoordinates = computeCartCoord(dots.positions, cfg); + positions = computeCartCoord(positions, dotMatrixWidth); - [angleMotion, ~] = cart2pol(cartesianCoordinates(:, 1), cartesianCoordinates(:, 2)); + [angleMotion, ~] = cart2pol(positions(:, 1), positions(:, 2)); angleMotion = angleMotion / pi * 180; if dots.direction == -666 diff --git a/src/dot/dotMotionSimulation.m b/src/dot/dotMotionSimulation.m new file mode 100644 index 0000000..f160fad --- /dev/null +++ b/src/dot/dotMotionSimulation.m @@ -0,0 +1,107 @@ +function 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 c775a03..50e287b 100644 --- a/src/dot/dotTexture.m +++ b/src/dot/dotTexture.m @@ -4,18 +4,22 @@ 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, ... + [xCenter yCenter], ... dotType); case 'draw' diff --git a/src/dot/generateNewDotPositions.m b/src/dot/generateNewDotPositions.m index 92bf28f..d59bb04 100644 --- a/src/dot/generateNewDotPositions.m +++ b/src/dot/generateNewDotPositions.m @@ -1,5 +1,5 @@ -function newPositions = generateNewDotPositions(cfg, dotNumber) +function newPositions = generateNewDotPositions(dotMatrixWidth, nbDots) - newPositions = rand(dotNumber, 2) * cfg.dot.matrixWidth; + newPositions = rand(nbDots, 2) * dotMatrixWidth; end diff --git a/src/dot/initDots.m b/src/dot/initDots.m index 2c247cd..94077b0 100644 --- a/src/dot/initDots.m +++ b/src/dot/initDots.m @@ -24,47 +24,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.speed(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 7c2c750..d30e7a9 100644 --- a/src/dot/reseedDots.m +++ b/src/dot/reseedDots.m @@ -5,7 +5,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: @@ -27,14 +27,13 @@ % and change its lifetime to 1 if any(N) - dots.positions(N, :) = generateNewDotPositions(cfg, sum(N)); + isSignal = dots.isSignal(N); - dots = setDotDirection(cfg, dots); + [positions, speeds, time] = seedDots(dots, cfg, isSignal); - [horVector, vertVector] = decomposeMotion(dots.directionAllDots); - dots.speeds = [horVector, vertVector] * dots.speedPixPerFrame; - - 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..c219ea9 --- /dev/null +++ b/src/dot/seedDots.m @@ -0,0 +1,28 @@ +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 duraion 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..3d093b4 100644 --- a/src/dot/setDotDirection.m +++ b/src/dot/setDotDirection.m @@ -1,4 +1,4 @@ -function dots = setDotDirection(cfg, dots) +function directionAllDots = setDotDirection(positions, cfg, dots, isSignal) % dots = setDotDirection(cfg, dots) % % creates some new direction for the dots @@ -10,29 +10,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(isSignal) = 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) = angleMotion; + end - % Random direction for the non coherent dots + %% Random direction for the non coherent dots + directionAllDots(~isSignal) = rand(sum(~isSignal), 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/utils/degToPix.m b/src/utils/degToPix.m index 9780d02..bb5fac5 100644 --- a/src/utils/degToPix.m +++ b/src/utils/degToPix.m @@ -4,6 +4,19 @@ % 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 diff --git a/src/utils/pixToDeg.m b/src/utils/pixToDeg.m new file mode 100644 index 0000000..a55978c --- /dev/null +++ b/src/utils/pixToDeg.m @@ -0,0 +1,29 @@ +% 2020 CPP BIDS SPM-pipeline developpers + +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'], ... + floor(pix / cfg.screen.ppd)); %#ok + +end diff --git a/tests/test_computeRadialMotionDirection.m b/tests/test_computeRadialMotionDirection.m index 38083d6..86f65fe 100644 --- a/tests/test_computeRadialMotionDirection.m +++ b/tests/test_computeRadialMotionDirection.m @@ -10,9 +10,7 @@ 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; @@ -24,7 +22,7 @@ function test_computeRadialMotionDirectionBasic() 0, 0; ... 100 / 2, 0]; - % direction = computeRadialMotionDirection(cfg, positions, direction); + angleMotion = computeRadialMotionDirection(dots.positions, cfg.dot.matrixWidth, dots); expectedDirection = [ 0; ... right @@ -36,6 +34,6 @@ function test_computeRadialMotionDirectionBasic() %% test - % assertEqual(expectedDirection, direction); + assertEqual(angleMotion, expectedDirection); end diff --git a/tests/test_dotMotionSimulation.m b/tests/test_dotMotionSimulation.m new file mode 100644 index 0000000..83e715e --- /dev/null +++ b/tests/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.speed = 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.speed = 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/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..75a50d8 100644 --- a/tests/test_initDots.m +++ b/tests/test_initDots.m @@ -27,7 +27,6 @@ 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 @@ -47,8 +46,7 @@ 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'); @@ -84,8 +82,7 @@ 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); 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 6f3da93..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 + 300, 10 % OK + 750, 1010 % out of frame + -1040, 50 % out of frame + 300, 300 % OK + 500, 500 % on the fixation cross ]; - dots.time = [ ... - 6; ... OK - 4; ... OK - 56; ... OK - 300; ... % exceeded its life time - 50]; % OK + 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_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