-
Notifications
You must be signed in to change notification settings - Fork 1
/
WorkingMemoryCapacity.m
1038 lines (849 loc) · 41.5 KB
/
WorkingMemoryCapacity.m
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
% PURPOSE: An implementation of a visual working memory capacity experiment
% by Vogel & Machizawa (2004). The research article can be found
% at <https://www.nature.com/articles/nature02447>.
%
% CONTEXT: Final exam of the course "Programming for Behavioral and
% Neurosciences" at Justus Liebig University Giessen
% <https://www.uni-giessen.de>
%
% AUTHOR: 2023 Marvin Theiss
%
% ASSUMPTIONS & LIMITATIONS:
% Psychtoolbox (<http://psychtoolbox.org>) needs to be installed.
% For system requirements regarding the use of Psychtoolbox, please
% check <http://psychtoolbox.org/requirements.html>.
%
% LICENSE: MIT License
%
% Copyright (c) 2023 Marvin Theiss
%
% Permission is hereby granted, free of charge, to any person obtaining a copy
% of this software and associated documentation files (the "Software"), to deal
% in the Software without restriction, including without limitation the rights
% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
% copies of the Software, and to permit persons to whom the Software is
% furnished to do so, subject to the following conditions:
%
% The above copyright notice and this permission notice shall be included in all
% copies or substantial portions of the Software.
%
% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
% SOFTWARE.
%----------------------------------------------------------------------
% CLEAN UP & CONFIGURE PTB
%----------------------------------------------------------------------
sca
close all
clear
clc
% Perform basic configuration of PTB parameters
% NOTE: Type "help configurePsych" into the command window for details on
% how to modify this command to fit your needs.
Config = configurePsych();
% Seed the random number generator
rng('shuffle')
%----------------------------------------------------------------------
% EXPERIMENTAL SETUP
%----------------------------------------------------------------------
%------------------------------------------------------------------
% CONFIGURATION OF EXPERIMENT
%------------------------------------------------------------------
% Set number of colored squares per hemifield
% NOTE: Parameters used by Vogel & Machizawa (2004): 1, 2, 3, 4, 6, 8, 10
nSquares = 4;
% Number of trials (excluding practice trials)
% NOTE: Vogel & Machizawa (2004) conducted 240 trials in each experiment
nTrials = 240;
% Number of practice trials
nPracticeTrials = 10;
% Set range to be used for the SOA (values are the ones used by Vogel &
% Machizawa (2004))
Duration.stimOnsetAsyncMinSecs = 0.3; % in secs
Duration.stimOnsetAsyncMaxSecs = 0.4; % in secs
% Set remaining timing parameters for the experiment (again, values are the
% ones used by Vogel & Machizawa (2004))
Duration.arrowSecs = 0.2; % in secs
Duration.memoryArraySecs = 0.1; % in secs
Duration.retentionIntervalSecs = 0.9; % in secs
Duration.testArraySecs = 2; % in secs
% (Orthogonal) distance from eye to screen in mm
% NOTE: This depends heavily on the setup (chair, desk, laptop vs. external
% monitor, etc.) that's being used. With my setup, I measured the
% following distances (using a height-adjustable desk and desk chair that
% are properly adjusted to me):
% - w/ laptop screen (MacBook Pro 16"): 550 mm
% - w/ external monitor (Dell U4021QW 40" attached to Ergotron HX): 650 mm
viewingDistanceMM = 550; % in mm
% 'Progress.thresholdPct' can be modified to control how often the
% participant is informed about his/her progress
% EXAMPLE: By setting 'Progress.thresholdPct' to 20, the participant is
% informed about his/her progress after 20 %, 40 %, 60 % and 80 % of all
% trials.
% NOTE: While this script does work for arbitrary numbers between 1 and 100,
% the value of 'Progress.thresholdPct' should be chosen reasonably.
% Sensible choices would be 5 %, 10 %, 20 % or 25 %.
Progress.thresholdPct = 10; % in pct
% Set text size, font, and color
% NOTE: This script was developed on a 16" laptop. For laptops of
% different sizes, 'txtSize' will most likely need some adjustment.
txtSize = 40;
txtFont = 'Helvetica';
txtColor = 0;
% If the PTB window is opened in 'debugMode' (i.e., the window only covers
% 25 % of the screen), we scale down the text size accordingly
if Config.debugMode
txtSize = round(0.5 * txtSize);
end
%------------------------------------------------------------------
% KEYBOARD SETTINGS
%------------------------------------------------------------------
% NOTE: The command "KbName('UnifyKeyNames')" is automatically executed by
% the command 'PsychDefaultSetup(2)', which is issued within the function
% 'configurePsych'. The latter is called at the very beginning of this
% script.
% Set keys
% NOTE: The space bar will be used by participants to navigate through
% instructions. To indicate a decision, participants will press either 'J'
% (indicating identical arrays) or 'F' (indicating different arrays).
% Finally, the escape key can be used to prematurely end the experiment.
Key.space = KbName('SPACE');
Key.identical = KbName('J');
Key.different = KbName('F');
Key.escape = KbName('ESCAPE');
%------------------------------------------------------------------
% INSTRUCTION/ERROR MESSAGES
%------------------------------------------------------------------
% Message to be displayed at the very beginning of the experiment
Msg.instructions = ['Hi there!\n\n'...
'This experiment will test your visual working memory capacity.\n'...
'Throughout the experiment, you will have to fixate a fixation cross\n' ...
'at the center of the screen. At the beginning of each trial, an\n' ...
'arrow will briefly appear just above this fixation cross, pointing\n' ...
'left or right. Immediately after, %d colored squares will appear left\n' ...
'and right to the fixation cross (i.e., %d squares in total). These\n' ...
'squares will only be flashed very briefly! It is your task to remember\n' ...
'the colors of the squares that the arrow pointed towards (and only\n' ...
'these colors). After a short period of just the fixation cross\n' ...
'being visible on screen, the squares will reappear. You have to judge\n' ...
'whether the colors of the squares in the hemifield indicated by the\n' ...
'arrow are identical or not. Press ''J'' if you think they are. If you think\n' ...
'one square has changed color, press ''F''. You have to answer before\n' ...
'the next trial starts (i.e., you have %.1f seconds to answer).\n\n' ...
'Note: Each trial starts and ends automatically, you can only answer once,\n' ...
'and you will not receive feedback whether your answer was correct.\n\n' ...
'Press space to start with some practice trials.'];
% Message to be displayed after all practice trials have been completed
Msg.practiceCompleted = [ ...
'Great job! You have completed all practice trials! Before we begin,\n' ...
'let''s go over some important details for the experiment (again):\n\n' ...
'1.) Every trial starts and ends automatically. You have to respond\n' ...
'while the test array is still on screen.\n\n' ...
'2.) You cannot change your answer once you have responded.\n\n' ...
'3.) Only remember the colors of the squares that the arrow pointed towards.\n\n' ...
'4.) Always keep fixating at the fixation cross centered on screen.\n\n' ...
'Also, you will regularly be informed about your progress throughout\n' ...
'the experiment. Feel free to use these opportunities to take a break!\n\n' ...
'Finally, remember the answer keys:\n' ...
'For identical colors, press ''J''.\n' ...
'If a color has changed, press ''F''.\n\n' ...
'When you''re ready, press space to start the first block of trials.'];
% Message to be displayed to inform the participant about his/her progress
Msg.progress = ['You have completed %d %% of all trials.\n\n' ...
'Press space to start the next block of trials.'];
% Thank-you-message presented at the end of the experiment
Msg.thankYou = ['You have completed the experiment!\n' ...
'Thank you for participating!\n\n' ...
'This window will close automatically in: %d'];
% Error message that is printed to the command window if the participant
% does not provide any information through the dialog box
Msg.errorNoInput = ['No participant information was entered into the ' ...
'dialog box. Please start the experiment again.'];
% Error message that is printed to the command window if an invalid
% participant ID was entered into the dialog box that is opened at the
% beginning of the experiment
Msg.errorInvalidID = ...
'Participant ID is not valid, expected an integer between 1 and 999!';
% Error message that is printed to the command window if an invalid sex
% was entered into the dialog box that is opened at the beginning of the
% experiment
Msg.errorInvalidSex = ...
'Participant sex is not valid, expected one of m, w, d!';
% Error message that is printed to the command window if an invalid
% year of birth was entered into the dialog box that is opened at the
% beginning of the experiment
Msg.errorInvalidYoB = ...
'Participant''s year of birth is not valid, expected a positive integer!';
% Error message that is printed to the command window if a participant ID
% is chosen that already exists and the entered participant data does not
% match previously entered data
Msg.errorInvalidParticipantData = ['Participant data does not match ' ...
'previously entered data. Please enter the same data or choose a ' ...
'different ID!'];
% Error message that is printed to the command window if participant ends
% the experiment prematurely
Msg.errorExptAborted = ['The participant has ended the experiment ' ...
'prematurely.\n' ...
'A total of %d trials out of %d trials have been completed.\n' ...
'All data collected so far was saved.'];
% Error message that is printed to the command window if participant ends
% the experiment during or right after the practice trials
Msg.errorExptAbortedDuringPractice = ...
'The participant has ended the experiment during practice.';
%------------------------------------------------------------------
% STIMULI SETUP & PREPARATION
%------------------------------------------------------------------
% Define background color
% NOTE: Vogel & Machizawa (2004) used a gray background with a luminance of
% 8.2 cd/m^2.
backgroundColor = 0.6;
% Define colors used for the squares
nColors = 7;
colorArray = cell(nColors, 1);
% NOTE: Qualitatively, these colors are the ones used by Vogel & Machizawa
% (2004).
colorArray{1} = [1, 0, 0]; % red
colorArray{2} = [0, 0, 1]; % blue
colorArray{3} = [0.561, 0, 1]; % violet
colorArray{4} = [0, 1, 0]; % green
colorArray{5} = [1, 1, 0]; % yellow
colorArray{6} = [0, 0, 0]; % black
colorArray{7} = [1, 1, 1]; % white
% Store color names for analysis purposes
colorNames = ["red"; "blue"; "violet"; "green"; ...
"yellow"; "black"; "white"];
% NOTE: Vogel & Machizawa (2004) used squares with a size of 0.65° x 0.65°.
squareSizeVA = 0.65; % in degrees of visual angle
% Convert size of squares from degrees of visual angle to pixels
% NOTE: What comes next is a simplification as we're assuming that each
% square is centered on the screen! Technically, as objects of a fixed
% size presented on the screen move further into the periphery, their
% visual angle decreases. Conversely, if we want the visual angle to
% remain constant at 0.65°, we would have to alter the size of the squares
% in pixels.
% TODO: Modify the 'visualAngleToSize' function and adjust this code to
% account for the above effect!
squareSize = round(visualAngleToSize( ...
squareSizeVA, viewingDistanceMM) * Config.pixelsPerMM); % in pixels
squareCoords = [0, 0, squareSize, squareSize]; % in pixels
% Set width and height of the rectangular regions left and right to the
% fixation cross in which the colored squares will be presented.
% NOTE: These are the values used by Vogel & Machizawa (2004).
rectRegionSizeVA = [4, 7.3]; % in degrees of visual angle
% Convert from degrees of visual angle to pixels
rectRegionSize = round(visualAngleToSize( ...
rectRegionSizeVA, viewingDistanceMM) * Config.pixelsPerMM); % in pixels
% We need to subtract 2 * ('squareSize' / 2) from 'rectRegionSize' to
% obtain the size of the rectangular region of valid locations of the
% individual squares' centers. Otherwise, if a square were centered right
% on the edge of the rectangular region, it would extend outside of the
% latter.
validCenterPosSize = rectRegionSize - squareSize;
% The rectangular regions are centered 3° of visual angle to the left and
% right of the central fixation cross in the experiment by Vogel &
% Machizawa (2004).
rectRegionHorzShiftVA = 3; % in degrees of visual angle
% Convert from degrees of visual angle to pixels
rectRegionHorzShift = round(visualAngleToSize( ...
rectRegionHorzShiftVA, viewingDistanceMM) * Config.pixelsPerMM); % in pixels
% Compute center of the rectangular regions (left & right)
rectRegionCenterLeft = [Config.xCenter - rectRegionHorzShift, ...
Config.yCenter];
rectRegionCenterRight = [Config.xCenter + rectRegionHorzShift, ...
Config.yCenter];
% Compute boundaries of the rectangular regions
rectRegionLeft = CenterRectOnPoint([0, 0, validCenterPosSize], ...
rectRegionCenterLeft(1), rectRegionCenterLeft(2));
rectRegionRight = CenterRectOnPoint([0, 0, validCenterPosSize], ...
rectRegionCenterRight(1), rectRegionCenterRight(2));
% Each square needs to be at least 2° of visual angle away from every other
% square (measured center to center)
minDistanceVA = 2; % in degrees of visual angle
% Convert distance from degrees of visual angle to pixels
minDistance = round(visualAngleToSize( ...
minDistanceVA, viewingDistanceMM) * Config.pixelsPerMM); % in pixels
% Clean up workspace
clear minDistanceVA rectRegionCenterLeft rectRegionCenterRight ...
rectRegionHorzShift rectRegionHorzShiftVA rectRegionSize ...
validCenterPosSize rectRegionSizeVA squareSize squareSizeVA
%------------------------------------------------------------------
% POSITIONING AND SIZE OF FIXATION CROSS & ARROW(S)
%------------------------------------------------------------------
% Set length and vertical displacement of arrow in degrees of visual angle
arrLengthVA = 1; % in degrees of visual angle
arrVertDisplacementVA = 0.75; % in degrees of visual angle
% Convert length of arrow from degrees of visual angle to pixels
Arrow.length = round(visualAngleToSize( ...
arrLengthVA, viewingDistanceMM) * Config.pixelsPerMM); % in pixels
% Set length of the arrowhead relative to length of the shaft of the arrow
Arrow.headLength = floor(Arrow.length / 3); % in pixels
% Convert vertical displacement from degrees of visual angle to pixels
arrVertDisplacementPixels = round(visualAngleToSize( ...
arrVertDisplacementVA, viewingDistanceMM) * Config.pixelsPerMM); % in pixels
% Compute center of arrow
% NOTE: The fixation cross will be placed at the center of the screen.
% The arrow will be presented slightly above the fixation cross.
Arrow.center = Config.center - [0, arrVertDisplacementPixels];
% Set width of arrow and angle between shaft and arrowhead
Arrow.width = 2; % in pixels
Arrow.angle = 40; % in degrees
% Size and thickness of the fixation cross
fixCrossVA = 0.65; % in degrees of visual angle
FixCross.size = round(visualAngleToSize( ...
fixCrossVA, viewingDistanceMM) * Config.pixelsPerMM); % in pixels
FixCross.width = 2; % in pixels
% Clean up workspace
clear arrLengthVA arrVertDisplacementPixels arrVertDisplacementVA ...
fixCrossVA
%------------------------------------------------------------------
% TRIAL STRUCTURE & SETUP
%------------------------------------------------------------------
% Number of items per array
Items = nSquares * ones(nTrials, 1);
% NOTE: 'Order' will be used to randomize the order of trials
Order = randperm(nTrials)';
% Randomize stimulus onset asynchrony
StimOnsetAsyncSecs = Duration.stimOnsetAsyncMinSecs + ( ...
Duration.stimOnsetAsyncMaxSecs - Duration.stimOnsetAsyncMinSecs ...
) .* rand(nTrials, 1);
% Little helper variable to set up trial conditions
quartiles = floor(quantile(1:nTrials, [0.25, 0.5, 0.75]));
% In half of the trials, participants will have to remember the squares in
% the left hemifield. In the other half of the trials, they will have to
% remember the squares in the right hemifield.
Hemifield = strings(nTrials, 1);
Hemifield(1:quartiles(2)) = "left";
Hemifield(quartiles(2)+1:end) = "right";
% In each of the two conditions (left hemifield vs. right hemifield), the
% memory array will be identical in exactly half of all trials of that
% condition.
IdenticalArrays = true(nTrials, 1);
IdenticalArrays( ...
[quartiles(1)+1:quartiles(2), quartiles(3)+1:end] ...
) = false;
% For each trial, we randomly choose the squares' colors from the 7 colors
% specified in the 'colorArray' cell array.
% NOTE: Following Vogel & Machizawa (2004), we let no color appear more
% than twice in a single memory array.
ColorsLeft = NaN(nTrials, nSquares); % array colors (left hemifield)
ColorsRight = NaN(nTrials, nSquares); % array colors (right hemifield)
colorCodes = repelem(1:nColors, 2); % colors to choose from
for iTrial = 1:nTrials
% Memory array for left hemifield
selectedColors = randperm(length(colorCodes), nSquares);
ColorsLeft(iTrial, :) = colorCodes(selectedColors);
% Memory array for right hemifield
selectedColors = randperm(length(colorCodes), nSquares);
ColorsRight(iTrial, :) = colorCodes(selectedColors);
end
% For each trial, we randomize the center coordinates for all squares
xLeftRange = rectRegionLeft([1, 3]); % range of valid values for x-coordinates in left hemifield
xRightRange = rectRegionRight([1, 3]); % range of valid values for x-coordinates in right hemifield
yRange = rectRegionLeft([2, 4]); % range of valid values for y-coordinates in either hemifield
XPosLeft = NaN(nTrials, nSquares); % x-coordinates of squares in left hemifield
YPosLeft = NaN(nTrials, nSquares); % x-coordinates of squares in left hemifield
XPosRight = NaN(nTrials, nSquares); % x-coordinates of squares in right hemifield
YPosRight = NaN(nTrials, nSquares); % y-coordinates of squares in right hemifield
printFreqPct = 10; % in pct
printFreq = round(nTrials * printFreqPct / 100);
disp("Starting to randomize positions!");
% NOTE: We're using a brute-force-approach to randomize the center
% coordinates, i.e., we simply generate 'nSquares' (pseudo-)random
% coordinates and then check if these satisfy the condition that all
% centers are at least 'minDistance' pixels apart from each other. If not,
% we generate a new set of (pseudo-)random coordinates and check again, and
% so on.
%
% NOTE: This approach works for 'nSquares' = 1, ..., 6. In detail:
% - The for-loop below executes almost instantly if 'nSquares' < 6.
% - If 'nSquares' = 6, the for-loop still executes in a reasonable amount
% of time (approx. 30 secs for 240 trials; this may vary from system to
% system).
% - If 'nSquares' > 6, the execution time explodes and the approach is no
% longer feasable.
%
% TODO: Develop systematic approach to fix this issue for 'nSquares' > 6.
for iTrial = 1:nTrials
% Randomize coordinates for squares in left hemifield
isValidCoords = false;
while ~isValidCoords
xLeft = randi(xLeftRange, nSquares, 1);
yLeft = randi(yRange, nSquares, 1);
isValidCoords = min(pdist([xLeft, yLeft])) >= minDistance;
end
XPosLeft(iTrial, :) = xLeft;
YPosLeft(iTrial, :) = yLeft;
% Randomize coordinates for squares in right hemifield
isValidCoords = false;
while ~isValidCoords
xRight = randi(xRightRange, nSquares, 1);
yRight = randi(yRange, nSquares, 1);
isValidCoords = min(pdist([xRight, yRight])) >= minDistance;
end
XPosRight(iTrial, :) = xRight;
YPosRight(iTrial, :) = yRight;
% Report progress
if mod(iTrial, printFreq) == 0 && iTrial < nTrials
trialsComputed = round(iTrial / nTrials * 100); % in pct
fprintf("Randomized positions for %d %% of trials ...\n", ...
trialsComputed);
elseif iTrial == nTrials
disp("Randomization finished!");
end
end
% For those trials in which memory and test array do not match, we randomly
% select a square that changes color.
OddSquare = randi(nSquares, nTrials, 1);
OddSquare(IdenticalArrays) = NaN;
% Select the color that the square changes to in non-identical trials
OddColor = NaN(nTrials, 1);
for iTrial = 1:nTrials
% Only select an odd color in non-identical trials
if ~IdenticalArrays(iTrial)
% Get initial colors of squares in appropriate hemifield
if strcmp(Hemifield(iTrial), "left")
initColors = ColorsLeft(iTrial, :);
else
initColors = ColorsRight(iTrial, :);
end
% Get initial color of square that ought to change color
initColorOddSquare = initColors(OddSquare(iTrial));
% Count occurences of colors in memory array
colorCounts = repelem(histcounts(initColors, ...
'BinMethod', 'integers', 'BinLimits', [1, nColors]), 2);
% Select a new color at random
% NOTE: The new color (obviously) needs to be different from the
% initial color of the square. Additionally, it cannot be any
% color that's already present twice in the memory array!
possibleColors = colorCodes( ...
colorCodes ~= initColorOddSquare & colorCounts < 2);
OddColor(iTrial) = possibleColors(randi(length(possibleColors)));
end
end
% Column to store the participant's responses
% NOTE: We initialize the response column as a vector of missing values and
% we later assign empty strings if the participant does not give a valid
% response while the test array is on screen. That way, we can
% differentiate between trials that have not been completed and those where
% no valid response was given if the experiment is aborted before all
% trials are completed.
Response = string(NaN(nTrials, 1));
% Combine all variables into a single table
trials = table(Items, Order, StimOnsetAsyncSecs, Hemifield, ...
IdenticalArrays, ColorsLeft, ColorsRight, XPosLeft, YPosLeft, ...
XPosRight, YPosRight, OddSquare, OddColor, Response);
% Randomize order of trials
trials = sortrows(trials, 'Order');
% Now that everything is set up, we add practice trials!
% First, we compute the total number of trials.
nTotalTrials = nTrials + nPracticeTrials;
% Then, we randomly select trials to (also) use as practice trials ...
practiceTrials = randperm(nTrials, nPracticeTrials);
% ... and add them to the 'trials' table
trials(nTrials+1:nTotalTrials, :) = trials(practiceTrials, :);
% Finally, we assign an 'Order' of 0 to identify the practice trials later
% on and we move the practice trials to the top of the 'trials' table.
trials.Order(nTrials+1:nTotalTrials) = 0;
trials = sortrows(trials, 'Order');
% Clean up workspace
clear colorCodes colorCounts ColorsLeft ColorsRight Hemifield ...
IdenticalArrays initColorOddSquare initColors isValidCoords Items ...
minDistance nColors OddColor OddSquare Order possibleColors ...
practiceTrials printFreq printFreqPct quartiles rectRegionLeft ...
rectRegionRight Response selectedColors StimOnsetAsyncSecs ...
trialsComputed xLeft xLeftRange XPosLeft XPosRight xRight ...
xRightRange yLeft YPosLeft YPosRight yRange yRight
%----------------------------------------------------------------------
% RECORD PARTICIPANT DATA
%----------------------------------------------------------------------
% Record some basic data of our participant using a dialog box
prompt = {'Participant ID (1 - 999):', ...
'Please enter your sex (m/w/d):', ...
'Please enter your year of birth:'};
dlgtitle = 'Participant Data';
dims = [1, 40];
answer = inputdlg(prompt, dlgtitle, dims);
% Check if the participant closed the dialog box or clicked on 'Cancel'
if isempty(answer)
error(Msg.errorNoInput);
end
% Store input in struct 'Participant'
Participant.id = str2double(answer{1});
Participant.sex = upper(answer{2});
Participant.yob = str2double(answer{3});
% Ensure that the data provided by the participant is valid
% a) Check participant ID (expected to be integer between 1 and 999)
assert(isnumeric(Participant.id) && isreal(Participant.id) && ...
isfinite(Participant.id) && mod(Participant.id, 1) == 0 && ...
1 <= Participant.id && Participant.id <= 999, Msg.errorInvalidID);
% b) Check participant sex (expected to be one of 'M', 'W', 'D')
assert(ismember(Participant.sex, {'M', 'W', 'D'}), ...
Msg.errorInvalidSex);
% c) Check participant's year of birth (expected to be positive integer)
assert(isnumeric(Participant.yob) && isreal(Participant.yob) && ...
isfinite(Participant.yob) && mod(Participant.yob, 1) == 0 && ...
Participant.yob > 0, Msg.errorInvalidYoB);
% Convert ID to nicely formatted string (e.g., the number 1 will be
% converted to the string "001") and store participant data in table
Participant.id = string(sprintf('%03d', Participant.id));
participantData = table( ...
Participant.id, string(Participant.sex), Participant.yob, ...
'VariableNames', ["ID", "Sex", "YearOfBirth"]);
% Make sure that subdirectoy 'data/participants' exists
% NOTE: This will also work if the directory 'data' does not yet exist.
if ~isfolder(fullfile('data', 'participants'))
mkdir(fullfile('data', 'participants'))
end
% Construct filename
filename = fullfile('data', 'participants', Participant.id + ".csv");
% If participant ID already exists, make sure that entered participant data
% coincides with previously entered data, else create a new file
if isfile(filename)
prevParticipantData = readtable(filename);
identicalSex = strcmp(participantData.Sex, prevParticipantData.Sex);
identicalYoB = ...
participantData.YearOfBirth == prevParticipantData.YearOfBirth;
if ~(identicalSex && identicalYoB)
error(Msg.errorInvalidParticipantData);
end
else
writetable(participantData, filename, 'Delimiter', ',');
end
% Create filename to store the results of the experiment
t = datetime("now", "Format", "yyyy-MM-dd");
filePattern = Participant.id + sprintf('_%02d-ITEMS_', nSquares) ...
+ string(t) + "_v%d";
filePattern = fullfile("data", filePattern + ".csv");
% Clean up workspace
clear answer dims dlgtitle identicalSex identicalYoB ...
prevParticipantData prompt t
%----------------------------------------------------------------------
% OPEN PTB WINDOW
%----------------------------------------------------------------------
try
% Skip sync tests depending on configuration of 'Config.skipTest'
Screen('Preference', 'SkipSyncTests', Config.skipTest);
% Enable listening for keyboard input & suppress any output of key
% presses to MATLAB windows (e.g., this script)
% NOTE: Inside the function 'endExperiment', the command
% 'ListenChar(0)' is issued to turn off character listening and
% re-enable keyboard input!
ListenChar(2);
% Open new PTB window with gray background
[windowPtr, windowRect] = PsychImaging('OpenWindow', ...
Config.screenNumber, backgroundColor, Config.winRect);
% Hide cursor
HideCursor(Config.screenNumber);
% Set text size & font
Screen('TextSize', windowPtr, txtSize);
Screen('TextFont', windowPtr, txtFont);
% Enable antialiasing
Screen('BlendFunction', ...
windowPtr, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
%------------------------------------------------------------------
% OBTAIN TIMING INFORMATION
%------------------------------------------------------------------
% Query frame duration
Config.ifi = Screen('GetFlipInterval', windowPtr);
% Set priority to maximum priority
Priority(MaxPriority(windowPtr));
% Convert presentation durations from seconds to number of frames
Duration.arrowFrames = round( ...
Duration.arrowSecs / Config.ifi); % in frames
Duration.memoryArrayFrames = round( ...
Duration.memoryArraySecs / Config.ifi); % in frames
Duration.retentionIntervalFrames = round( ...
Duration.retentionIntervalSecs / Config.ifi); % in frames
Duration.testArrayFrames = round( ...
Duration.testArraySecs / Config.ifi); % in frames
%----------------------------------------------------------------------
% START OF EXPERIMENT
%----------------------------------------------------------------------
% Compute the number of trials that correspond to multiples of
% 'Progress.thresholdPct' % of all trials
Progress.nSteps = round(100 / Progress.thresholdPct);
Progress.stepArray = round(linspace(0, nTrials, Progress.nSteps + 1));
% Drop first entry (which is always equal to 0 by design)
Progress.stepArray = Progress.stepArray(2:end);
% Present general instructions to participant
DrawFormattedText(windowPtr, sprintf(Msg.instructions, ...
nSquares, 2*nSquares, Duration.testArraySecs), ...
'center', 'center', txtColor);
Screen('Flip', windowPtr);
% Wait for participant to start the first practice trial
KbReleaseWait(Config.keyboard);
while true
[~, ~, keyCode] = KbCheck(Config.keyboard);
% Exit while-loop and continue if space bar has been pressed, else
% continue checking the keyboard for input
if keyCode(Key.space)
break
end
end
%------------------------------------------------------------------
% LOOP OVER INDIVIDUAL TRIALS
%------------------------------------------------------------------
for iTrial = 1:nTotalTrials
% STEP 0: Check for end of practice trials and/or report progress
% to participant
% 0.1 Check whether it's the end of practice trials
if iTrial == nPracticeTrials+1
% Present further instructions
DrawFormattedText(windowPtr, Msg.practiceCompleted, ...
'center', 'center', txtColor);
Screen('Flip', windowPtr);
% Increase text size by 50 % for rest of experiment
Screen('TextSize', windowPtr, round(1.5 * txtSize));
% Wait for participant to press the space bar to start the
% first block of trials or press the escape key to end the
% experiment after practice
KbReleaseWait(Config.keyboard);
while true
[~, ~, keyCode] = KbCheck(Config.keyboard);
if keyCode(Key.space)
break
elseif keyCode(Key.escape)
error(Msg.errorExptAbortedDuringPractice);
end
end
end
% 0.2 Report progress to participant during experiment
if ismember(iTrial - (nPracticeTrials+1), Progress.stepArray)
% Compute progress
Progress.completed = round((iTrial - (nPracticeTrials+1)) / ...
nTrials * 100); % in pct
% Display progress to participant
DrawFormattedText(windowPtr, ...
sprintf(Msg.progress, Progress.completed), ...
'center', 'center', txtColor);
Screen('Flip', windowPtr);
% Wait for participant to press the space bar to start the
% next block of trials or press the escape key to end the
% experiment prematurely
KbReleaseWait(Config.keyboard);
while true
[~, ~, keyCode] = KbCheck(Config.keyboard);
if keyCode(Key.space)
break
elseif keyCode(Key.escape)
% Throw error containing number of completed (out of
% total) trials
error(Msg.errorExptAborted, ...
iTrial - (nPracticeTrials+1), nTrials);
end
end
end
% STEP 1: Prepare for trial
% 1.1 Query stimulus onset asynchrony (in secs) and convert to
% number of frames
stimOnsetAsyncSecs = trials.StimOnsetAsyncSecs(iTrial);
stimOnsetAsyncFrames = round(stimOnsetAsyncSecs / Config.ifi);
% 1.2 Query colors of squares in memory array
allColorCodes = [trials.ColorsLeft(iTrial, :), ...
trials.ColorsRight(iTrial, :)];
allColorsMemory = NaN(3, 2*nSquares);
for iSquare = 1:2*nSquares
allColorsMemory(:, iSquare) = colorArray{allColorCodes(iSquare)};
end
allColorsTest = allColorsMemory;
% 1.3 Query positioning of squares
allSquares = repmat(squareCoords', 1, 2*nSquares);
% NOTE: We make use of the fact that 'CenterRectOnPoint' is a
% vectorized function to avoid a for-loop.
allSquares = CenterRectOnPoint(allSquares, ...
[trials.XPosLeft(iTrial, :), trials.XPosRight(iTrial, :)], ...
[trials.YPosLeft(iTrial, :), trials.YPosRight(iTrial, :)]);
% 1.4 Modify colors for test array if applicable
if ~trials.IdenticalArrays(iTrial)
% Query new color
oddColorCode = trials.OddColor(iTrial);
oddColor = colorArray{oddColorCode};
% Query square that ought to change color
oddSquare = trials.OddSquare(iTrial);
% The next step accounts for the fact that we have combined
% the colors of all squares into a single matrix in which the
% first 'nSquares' columns correspond to squares in the left
% hemifield and the second 'nSquares' columns correspond to
% squares in the right hemifield.
if strcmp(trials.Hemifield(iTrial), "right")
oddSquare = oddSquare + nSquares;
end
% Modify color of appropriate square
allColorsTest(:, oddSquare) = oddColor;
end
% 1.5 Set logical used for collection of response and clear
% response from previous trial
isResponseGiven = false;
response = "";
% STEP 2: Display fixation cross
% 2.1 Draw fixation cross at the center of the screen
% NOTE: Type "help drawFixationCross" into the command window for
% further information.
drawFixationCross(windowPtr, FixCross.size, FixCross.width, ...
Config.center, txtColor);
% 2.2 Flip fixation cross to screen
% NOTE: We set 'dontclear' (fourth argument) to 1 for
% incremental drawing (since we also want the fixation cross
% to be displayed when the arrow is presented next).
[~, stimulusOnsetTime] = Screen('Flip', windowPtr, [], 1);
% STEP 3: Display arrow indicating left vs. right hemifield
% 3.1 Draw arrow
% NOTE: Type "help drawArrow" into the command window for further
% information.
drawArrow(windowPtr, Arrow.length, Arrow.headLength, ...
Arrow.width, Arrow.angle, Arrow.center, ...
trials.Hemifield(iTrial));
% 3.2 Flip arrow to screen
[~, stimulusOnsetTime] = Screen('Flip', windowPtr, ...
stimulusOnsetTime + (stimOnsetAsyncFrames-0.5) * Config.ifi);
% STEP 4: Display memory array
% 4.1 Draw memory array (and fixation cross)
drawFixationCross(windowPtr, FixCross.size, FixCross.width, ...
Config.center, txtColor);
Screen('FillRect', windowPtr, allColorsMemory, allSquares);
% 4.2 Flip memory array (and fixation cross) to screen
[~, stimulusOnsetTime] = Screen('Flip', windowPtr, ...
stimulusOnsetTime + (Duration.arrowFrames-0.5) * Config.ifi);
% STEP 5: Retention interval
% 5.1 Draw fixation cross
drawFixationCross(windowPtr, FixCross.size, FixCross.width, ...
Config.center, txtColor);
% 5.2 Erase memory array and flip fixation cross to screen
% NOTE: We're again using incremental drawing here!
[~, stimulusOnsetTime] = Screen('Flip', windowPtr, ...
stimulusOnsetTime + ...
(Duration.memoryArrayFrames-0.5) * Config.ifi, 1);
% STEP 6: Display test array and check for response
% 6.1 Draw test array (and fixation cross)
drawFixationCross(windowPtr, FixCross.size, FixCross.width, ...
Config.center, txtColor);
Screen('FillRect', windowPtr, allColorsTest, allSquares);
% 6.2 Flip test array to screen
[~, stimulusOnsetTime] = Screen('Flip', windowPtr, ...
stimulusOnsetTime + ...
(Duration.retentionIntervalFrames-0.5) * Config.ifi);
% STEP 7: Collect response
% 7.1: Keep checking for response while test array is on screen
while true
% NOTE: 'secs' is the time of the status check as returned by
% 'GetSecs'. Hence, we can simply use this value instead of
% issuing an additional call to the 'GetSecs' function!
[~, secs, keyCode] = KbCheck(Config.keyboard);
% Once the test array has been on screen for 2,000 ms, we wipe
% the screen and stop checking for a response
timeElapsed = secs - stimulusOnsetTime;
if timeElapsed >= Duration.testArraySecs
drawFixationCross(windowPtr, FixCross.size, ...
FixCross.width, Config.center, txtColor);
Screen('Flip', windowPtr);
break
end
% Check if experiment is to be aborted
if keyCode(Key.escape)
if trials.Order(iTrial) == 0 % practice trials
error(Msg.errorExptAbortedDuringPractice);
else % "non-practice" trials
error(Msg.errorExptAborted, ...
iTrial - (nPracticeTrials+1), nTrials);
end
end
if ~isResponseGiven
% Check if a valid response was given
if keyCode(Key.identical)
response = "identical";
isResponseGiven = true;
elseif keyCode(Key.different)
response = "different";
isResponseGiven = true;
end
end
end
% 7.2 Store participant's response
trials.Response(iTrial) = response;
end
% Clean up workspace
clear allColorCodes allColorsMemory allColorsTest allSquares ...
iSquare isResponseGiven iTrial keyCode nPracticeTrials ...
nTotalTrials nTrials oddColor oddColorCode oddSquare response ...
squareCoords stimOnsetAsyncFrames stimOnsetAsyncSecs ...
stimulusOnsetTime timeElapsed
%----------------------------------------------------------------------
% END OF EXPERIMENT
%----------------------------------------------------------------------
% Wipe screen
Screen('Flip', windowPtr);
WaitSecs(0.5);
% Present thank-you-message to participant
for secs = 10:-1:1
DrawFormattedText(windowPtr, sprintf(Msg.thankYou, secs), ...
'center', 'center', txtColor);
Screen('Flip', windowPtr);
WaitSecs(1);
end
% Wipe screen again before shutting down
Screen('Flip', windowPtr);
WaitSecs(0.5);
% Clean up workspace
clear ans secs
%------------------------------------------------------------------
% SAVE DATA & SHUT DOWN
%------------------------------------------------------------------
% Make sure that filename is unique
counter = 1;
while true
filename = sprintf(filePattern, counter);
if ~isfile(filename)
break
else
counter = counter + 1;
end
end
% Save data
writetable(trials, filename, 'Delimiter', ',');
% Turn off character listening, re-enable keyboard input and close all
% open screens
endExperiment();
% Clean up workspace
clear counter filePattern
%----------------------------------------------------------------------
% ERROR HANDLING
%----------------------------------------------------------------------
catch errorMessage
% Indicate that the collected data is incomplete
filePattern = strrep(filePattern, 'data/', 'data/INCOMPLETE_');
% Make sure that filename is unique