diff --git a/.travis.yml b/.travis.yml index 1ff89b8..eb1a738 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,27 +5,10 @@ dist: bionic cache: apt: true # only works with Pro version -env: - global: - - OCTFLAGS="--no-gui --no-window-system --silent" - before_install: - - travis_retry sudo apt-get -y -qq update - - travis_retry sudo apt-get -y install octave - - travis_retry sudo apt-get -y install liboctave-dev - cd .. && git clone https://github.com/florianschanda/miss_hit.git && export PATH=$PATH:`pwd`/miss_hit && cd CPP_PTB -install: - - octave $OCTFLAGS --eval "addpath (pwd); savepath ();" - -before_script: - # Change current directory - - cd tests - jobs: include: - - stage: "Tests and linter" - name: "Unit Tests" # names the first job - script: octave $OCTFLAGS --eval "results = runTests; assert(all(~[results.Failed]))" - - script: cd .. && mh_style `pwd` + - script: mh_style `pwd` name: "miss_hit linter" # names the second job \ No newline at end of file diff --git a/apertureTexture.m b/apertureTexture.m deleted file mode 100644 index f6bba2b..0000000 --- a/apertureTexture.m +++ /dev/null @@ -1,59 +0,0 @@ -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)); - - otherwise - - error('unknown aperture type: %s.', cfg.aperture.type); - - 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 diff --git a/drawFixation.m b/drawFixation.m index ce9d3f0..e9897ae 100644 --- a/drawFixation.m +++ b/drawFixation.m @@ -1,16 +1,69 @@ function drawFixation(cfg) % Define the parameters of the fixation cross in `cfg` and `expParameters` - if strcmp(cfg.fixation.type, 'cross') + switch cfg.fixation.type + case 'cross' - smooth = 1; + 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); + + 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)); + + % Draw fixation + Screen('FillOval', ... + cfg.screen.win, ... + cfg.color.foreground, ... + CenterRect( ... + [0 0 repmat(cfg.fixation.widthPix, 1, 2)], ... + cfg.screen.winRect)); + + case 'bestFixation' + + % Code adapted from: + % What is the best fixation target? + % DOI 10.1016/j.visres.2012.10.012 + + % 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)); + + Screen('FillOval', ... + 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)]); + + Screen('FillOval', ... + cfg.screen.win, ... + cfg.color.black, ... + cfg.fixation.innerOval, ... + cfg.fixation.widthPix / 3); - 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 diff --git a/initFixation.m b/initFixation.m index e85d7b3..ae1cdce 100644 --- a/initFixation.m +++ b/initFixation.m @@ -1,18 +1,38 @@ 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]; + % 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 + xLine = [-cfg.fixation.widthPix cfg.fixation.widthPix 0 0] / 2; + yLine = [0 0 -cfg.fixation.widthPix cfg.fixation.widthPix] / 2; + + cfg.fixation.xCoords = xLine + cfg.fixation.xDisplacementPix; + cfg.fixation.yCoords = yLine + cfg.fixation.yDisplacementPix; + + cfg.fixation.allCoords = [cfg.fixation.xCoords; cfg.fixation.yCoords]; + + switch cfg.fixation.type + + case 'bestFixation' + + % Code adapted from: + % What is the best fixation target? + % 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.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]; end diff --git a/setDefaultsPTB.m b/setDefaultsPTB.m index 5a172d6..386f3c1 100644 --- a/setDefaultsPTB.m +++ b/setDefaultsPTB.m @@ -35,7 +35,7 @@ fieldsToSet.fixation.xDisplacement = 0; fieldsToSet.fixation.yDisplacement = 0; fieldsToSet.fixation.color = [255 255 255]; - fieldsToSet.fixation.width = 1; + fieldsToSet.fixation.width = 1; % degrees of visual angle fieldsToSet.fixation.lineWidthPix = 5; % define visual apperture field diff --git a/src/aperture/apertureTexture.m b/src/aperture/apertureTexture.m new file mode 100644 index 0000000..d3c8f2b --- /dev/null +++ b/src/aperture/apertureTexture.m @@ -0,0 +1,127 @@ +function [cfg, thisEvent] = apertureTexture(action, cfg, thisEvent) + + transparent = [0 0 0 0]; + + switch action + + case 'init' + + switch cfg.aperture.type + + case 'circle' + % 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); + + case 'ring' + + % Set parameters for rings + if strcmp(cfg.aperture.type, 'ring') + % scale of outer ring (exceeding screen until + % inner ring reaches window boarder) + cfg.ring.maxEcc = ... + cfg.screen.FOV / 2 + ... + cfg.aperture.width + ... + log(cfg.screen.FOV / 2 + 1) ; + % ring.CsFuncFact is used to expand with log increasing speed so + % that ring is at ring.maxEcc at end of cycle + cfg.ring.csFuncFact = ... + 1 / ... + ((cfg.ring.maxEcc + exp(1)) * ... + log(cfg.ring.maxEcc + exp(1)) - ... + (cfg.ring.maxEcc + exp(1))) ; + end + end + + cfg.aperture.texture = Screen('MakeTexture', cfg.screen.win, ... + cfg.color.background(1) * ones(cfg.screen.winRect([4 3]))); + + case 'make' + + xCenter = cfg.screen.center(1); + yCenter = cfg.screen.center(2); + + switch cfg.aperture.type + + case 'none' + + Screen('Fillrect', cfg.aperture.texture, transparent); + + case 'circle' + + diameter = cfg.aperture.widthPix; + + if isfield(cfg.aperture, 'xPosPix') + xCenter = cfg.screen.center(1) + cfg.aperture.xPosPix; + end + if isfield(cfg.aperture, 'yPosPix') + yCenter = cfg.screen.center(2) + cfg.aperture.yPosPix; + end + + Screen('FillOval', cfg.aperture.texture, transparent, ... + CenterRectOnPoint([0 0 repmat(diameter, 1, 2)], ... + xCenter, yCenter)); + + case 'ring' + + % expansion speed is log over eccentricity + [cfg] = eccenLogSpeed(cfg, thisEvent.time); + + 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)); + + Screen('FillOval', cfg.aperture.texture, [cfg.color.background 255], ... + CenterRectOnPoint( ... + [0 0 repmat(cfg.ring.innerRimPix, 1, 2)], ... + xCenter, yCenter)); + + case 'wedge' + + cycleDuration = cfg.mri.repetitionTime * cfg.volsPerCycle; + + % Update angle for rotation of background and for apperture for wedge + switch cfg.direction + + case '+' + thisEvent.angle = 90 - ... + cfg.aperture.width / 2 + ... + (thisEvent.time / cycleDuration) * 360; + case '-' + thisEvent.angle = 90 - ... + cfg.aperture.width / 2 - ... + (thisEvent.time / cycleDuration) * 360; + + end + + Screen('Fillrect', cfg.aperture.texture, cfg.color.background); + + Screen('FillArc', cfg.aperture.texture, transparent, ... + CenterRect( ... + [0 0 repmat(cfg.stimRect(4), 1, 2)], ... + cfg.screen.winRect), ... + thisEvent.angle, ... % start angle + cfg.aperture.width); % arc angle + + otherwise + + error('unknown aperture type: %s.', cfg.aperture.type); + + 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 diff --git a/src/aperture/eccenLogSpeed.m b/src/aperture/eccenLogSpeed.m new file mode 100644 index 0000000..df00b83 --- /dev/null +++ b/src/aperture/eccenLogSpeed.m @@ -0,0 +1,61 @@ +function [cfg] = eccenLogSpeed(cfg, time) + % vary CurrScale so that expansion speed is log over eccentricity + % cf. Tootell 1997; Swisher 2007; Warnking 2002 etc + + TR = cfg.mri.repetitionTime; + cycleDuration = TR * cfg.volsPerCycle; + + % CurrScale only influences ring + if strcmp(cfg.aperture.type, 'ring') + + csFuncFact = cfg.ring.csFuncFact; + ringWidthVA = cfg.ring.ringWidthVA; + maxEcc = cfg.ring.maxEcc; + + switch cfg.direction + case '+' + % current visual angle linear in time + outerRimVA = 0 + mod(time, cycleDuration) / cycleDuration * maxEcc; + % ensure some foveal stimulation at beginning (which is hidden by + % fixation cross otherwise) + if outerRimVA < cfg.fixation.size + outerRimVA = cfg.fixation.size + .1; + end + case '-' + outerRimVA = maxEcc - mod(time, cycleDuration) / cycleDuration * maxEcc; + if outerRimVA > maxEcc + outerRimVA = maxEcc; + end + end + + % near-exp visual angle + newOuterRimVA = ((outerRimVA + exp(1)) * log(outerRimVA + exp(1)) - ... + (outerRimVA + exp(1))) * maxEcc * csFuncFact; + outerRimPix = newOuterRimVA * cfg.screen.ppd; % in pixel + + % width of apperture changes logarithmically with eccentricity of inner ring + oldScaleInnerVA = outerRimVA - ringWidthVA; + if oldScaleInnerVA < 0 + oldScaleInnerVA = 0; + end + + % growing with inner ring ecc + ringWidthVA = cfg.aperture.width + log(oldScaleInnerVA + 1); + innerRimVA = newOuterRimVA - ringWidthVA; + + if innerRimVA < 0 + innerRimVA = 0; + end + + % in pixel + innerRimPix = innerRimVA * cfg.screen.ppd; + + % update cfg that we are about to return + cfg.ring.outerRimPix = outerRimPix; + cfg.ring.innerRimPix = innerRimPix; + cfg.ring.ring_outer_rim = newOuterRimVA; + cfg.ring.ring_inner_rim = innerRimVA; + + end + +end diff --git a/src/dot/computeCartCoord.m b/src/dot/computeCartCoord.m index cfe7a64..8aacd65 100644 --- a/src/dot/computeCartCoord.m +++ b/src/dot/computeCartCoord.m @@ -1,7 +1,7 @@ function cartesianCoordinates = computeCartCoord(positions, cfg) cartesianCoordinates = ... - [positions(:,1) + cfg.dot.matrixWidth, ... % x coordinate - positions(:,2) + cfg.dot.matrixWidth]; % y coordinate - -% cartesianCoordinates = positions; -end \ No newline at end of file + [positions(:, 1) + cfg.dot.matrixWidth, ... % x coordinate + positions(:, 2) + cfg.dot.matrixWidth]; % y coordinate + + % cartesianCoordinates = positions; +end diff --git a/src/dot/computeRadialMotionDirection.m b/src/dot/computeRadialMotionDirection.m index f655b29..ef47ed8 100644 --- a/src/dot/computeRadialMotionDirection.m +++ b/src/dot/computeRadialMotionDirection.m @@ -1,13 +1,12 @@ function angleMotion = computeRadialMotionDirection(cfg, dots) - + cartesianCoordinates = computeCartCoord(dots.positions, cfg); - + [angleMotion, ~] = cart2pol(cartesianCoordinates(:, 1), cartesianCoordinates(:, 2)); angleMotion = angleMotion / pi * 180; - + if dots.direction == -666 angleMotion = angleMotion - 180; end - -end +end diff --git a/src/dot/generateNewDotPositions.m b/src/dot/generateNewDotPositions.m index a60f739..92bf28f 100644 --- a/src/dot/generateNewDotPositions.m +++ b/src/dot/generateNewDotPositions.m @@ -1,5 +1,5 @@ function newPositions = generateNewDotPositions(cfg, dotNumber) - + newPositions = rand(dotNumber, 2) * cfg.dot.matrixWidth; - -end \ No newline at end of file + +end diff --git a/src/dot/initDots.m b/src/dot/initDots.m index 8529285..80efb43 100644 --- a/src/dot/initDots.m +++ b/src/dot/initDots.m @@ -64,5 +64,3 @@ dots.speedPixPerFrame = speedPixPerFrame; end - - diff --git a/src/dot/reseedDots.m b/src/dot/reseedDots.m index fe2a128..19dfa5e 100644 --- a/src/dot/reseedDots.m +++ b/src/dot/reseedDots.m @@ -1,22 +1,20 @@ function dots = reseedDots(dots, cfg) - + fixationWidthPix = 0; if isfield(cfg.fixation, 'widthPix') fixationWidthPix = cfg.fixation.widthPix; end - + cartesianCoordinates = computeCartCoord(dots.positions, cfg); [~, radius] = cart2pol(cartesianCoordinates(:, 1), cartesianCoordinates(:, 2)); - - % Create a logical vector to detect any dot that has: % - an xy position inferior to 0 % - an xy position superior to winWidth % - has exceeded its liftime % - is on the fixation cross % - has been been picked to be killed - + N = any([ ... dots.positions > cfg.dot.matrixWidth, ... dots.positions < 0, ... @@ -24,20 +22,20 @@ 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); - + [horVector, vertVector] = decomposeMotion(dots.directionAllDots); dots.speeds = [horVector, vertVector] * dots.speedPixPerFrame; - + dots.time(N, 1) = 1; - + end - -end \ No newline at end of file + +end diff --git a/src/dot/setDotDirection.m b/src/dot/setDotDirection.m index a5e53c2..0d46dd2 100644 --- a/src/dot/setDotDirection.m +++ b/src/dot/setDotDirection.m @@ -4,7 +4,7 @@ % Coherent dots directionAllDots(dots.isSignal) = dots.direction; - + if strcmp(cfg.design.motionType, 'radial') angleMotion = computeRadialMotionDirection(cfg, dots); directionAllDots(dots.isSignal) = angleMotion; @@ -13,7 +13,7 @@ % Random direction for the non coherent dots directionAllDots(~dots.isSignal) = rand(sum(~dots.isSignal), 1) * 360; directionAllDots = rem(directionAllDots, 360); - + dots.directionAllDots = directionAllDots; -end \ No newline at end of file +end diff --git a/src/dot/updateDots.m b/src/dot/updateDots.m index ddeb970..329a81a 100644 --- a/src/dot/updateDots.m +++ b/src/dot/updateDots.m @@ -9,7 +9,7 @@ error(errorStruct); end - + % Add one frame to the dot lifetime to each dot dots.time = dots.time + 1; diff --git a/tests/test_reseedDots.m b/tests/test_reseedDots.m index 5935b81..c8ee8d8 100644 --- a/tests/test_reseedDots.m +++ b/tests/test_reseedDots.m @@ -7,39 +7,39 @@ end function test_reseedDotsBasic() - + dots.lifeTime = 100; cfg.screen.winWidth = 2000; cfg.dot.number = 5; cfg.dot.sizePix = 20; cfg.dot.proportionKilledPerFrame = 0; - + cfg.fixation.widthPix = 20; - + dots.positions = [ ... 694, 100; % OK 490, 2043; % out of frame - -104, 392; % out of frame + -104, 392; % out of frame 492, 402; % OK 1000, 1000; % on the fixation cross ]; - - dots.time = [... + + dots.time = [ ... 6; ... OK 4 ; ... OK 56; ... OK 300; ... % exceeded its life time 50]; % OK - + dots = reseedDots(dots, cfg); - - reseeded = [... + + reseeded = [ ... 6; 1; 1; 1; 1]; - - assertEqual(reseeded, dots.time) - -end \ No newline at end of file + + assertEqual(reseeded, dots.time); + +end