-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.py
1546 lines (1377 loc) · 65.6 KB
/
main.py
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
# the default is the observational version (set condition to s to get the strategic version)
# the default is the dots version (set task to s to get the squircles version)
# IMPORT EXTERNAL PACKAGES
from __future__ import absolute_import, division
import random
import os
import math
import numpy as np # whole numpy lib is available, prepend 'np.'
from psychopy import gui, visual, core, data, event, logging, misc, clock
from psychopy.hardware import keyboard
import serial # serial port for eeg triggering
print('Reminder: Press Q to quit.') # press Q and experiment will quit on next win flip
# SESSION INFORMATION
# Pop up asking for participant number, session, age, and gender
expInfo = {'participant nr': '', 'session (1/2)': '', 'condition (s/ns)': '', 'task (s/d)': '', 'age': '',
'gender (f/m/o)': '', 'handedness (l/r/b)': '',
'send triggers': 'True'} # default is sending triggers; set this to false if you do not want to do this as EEG experiment
expName = 'Confidence Matching EEG'
dlg = gui.DlgFromDict(dictionary=expInfo, sortKeys=False, title=expName)
if not dlg.OK:
core.quit() # pressed cancel
# SET UP EEG TRIGGERS
triggers = dict(
exp_start=1,
block_start=2,
fixation_cross=3,
stimulus_left_correct=4,
stimulus_right_correct=5,
response_left=6,
response_right=7,
highlight_box=8,
confidence_rating=9,
partner_marker_left=10,
partner_marker_right=11,
higher_conf_box=12,
feedback_correct=13,
feedback_incorrect=14,
exp_end=15
)
send_triggers = expInfo['send triggers']
if send_triggers:
# triggers are actually sent over a serial port, not parallel port
IOport = serial.Serial('COM4', 115200, timeout=0.001) # port COM4, baud rate = 115200, timeout of 1ms
def send_trigger(code):
"""
code: expects an integer code (up to a maximum of 127, because of the serial port being weird)to send to the EEG)
"""
IOport.write(str.encode(
chr(code))) # this expects bytes. therefore we need to convert the integers we are using as trigger codes
IOport.flush()
else:
def send_trigger(code):
print('sending trigger: ' + str(code))
# SET EXPERIMENT VARIABLES
# variables in gv are just used to structure the task
initial_stair_value = 4.2
gv = dict(
n_practice_trials=100, # MAKE TRIAL COUNT 100 HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
n_confidence_practice_trials=5, # make trial count 5 here
n_blocks_per_partner=5, # make block count 5 here
n_trials_per_block=30, # MAKE TRIAL COUNT 30 HERE !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
stimulus_period=0.3,
fixation_period=1,
wait_period=0.5, # wait time after response before next fixation cross
staircase_breaks=[5, 10],
# number of trials after which staircasing values change (takes only two values at the moment)
staircase_step_sizes=[0.4, 0.2, 0.1],
# staircase step sizes in log space (takes only 3 values at the moment in accordance with the 2 staircase_breaks values)
next_stair_value=initial_stair_value,
# initial staircasing value is 4.2 (as in Rouault, M., Seow, T. Gillan, C. M., & Fleming, S. M. (2018))
stair_values_list=[initial_stair_value],
dot_difference_limits=[1, 100], # minimum and maximum dots difference
next_dot_count_low=200, # fixed low dot count
next_dot_count_high=200 + int(round((math.e ** initial_stair_value), 0)),
# initial high dot count based on initial stair value
squircles_n_circles=8, # number of circles for the squircles stimuli
squircles_color_sd=0.1, # colour variance for the squircles stimuli
next_squircle_difference=int(round((math.e ** initial_stair_value), 0)) / 1000,
# initial squircle colour difference
previous_trial_correct=False
)
# variables in info will be saved as participant data
info = dict(
expName=expName,
curec_ID='R67369/RE001',
session=expInfo['session (1/2)'],
condition=expInfo['condition (s/ns)'],
task=expInfo['task (s/d)'],
date=data.getDateStr(),
end_date=None,
participant=expInfo['participant nr'],
age=expInfo['age'],
gender=expInfo['gender (f/m/o)'],
confidence_slider_on=None,
staircasing_on=None,
partner=None,
partner_colour=None,
trial_count=0,
block_count=0,
block_with_partner=0,
trial_in_block=0,
dot_count_low=None,
dot_count_high=None,
squircle_value_low=None,
squircle_value_high=None,
stair_value=None,
correct_response=None,
participant_response=None,
participant_correct=None,
participant_confidence=None,
partner_response=None,
partner_correct=None,
partner_confidence=None,
joint_correct=None,
participant_chosen=None,
partner_chosen=None,
decision_rt=None,
confidence_rt=None,
trial_score=None,
final_score=None,
reward=None,
p1_likeability=None,
p1_confidence=None,
p1_accuracy=None,
p1_team_work=None,
p2_likeability=None,
p2_confidence=None,
p2_accuracy=None,
p2_team_work=None
)
# LOGGING
log_vars = list(info.keys())
if not os.path.exists('data'):
os.mkdir('data')
filename = os.path.join('data', '%s_%s' % (info['participant'], info['date']))
## Save a log file for detail verbose info
# logFile = logging.LogFile(filename + '.log', level=logging.EXP)
# logging.console.setLevel(0) # this outputs to the screen, not a file
datafile = open(filename + '.csv', 'w')
datafile.write(','.join(log_vars) + '\n')
datafile.flush()
# SET UP WINDOW
win = visual.Window(
gammaErrorPolicy='ignore',
fullscr=True, screen=0,
allowGUI=True, allowStencil=False,
monitor='testMonitor', color='black',
blendMode='avg', useFBO=True, units='pix') # pix is ca 1200 by 1000
# INSTRUCTIONS
th = 35
button = visual.Rect(
win=win,
units="pix",
width=160,
height=60,
pos=(0, -400),
fillColor=[-1, 1, -1],
lineColor=[-1, .8, -1]
)
button_txt = visual.TextStim(win=win, text='NEXT', height=th, pos=button.pos, color=[-1, -1, -1], bold=True)
choice_txt = visual.TextStim(win=win, text='Which box contained more dots?', height=30, pos=[0, -80], color='white')
confidence_txt = visual.TextStim(win=win, text='Indicate your confidence with the slider below', height=30,
pos=[0, -80], color='white')
welcome_txt = visual.TextStim(win=win, text='Welcome to this experiment!', height=70, pos=[0, 0], color='white')
welcome2_txt = visual.TextStim(win=win, text='In this experiment, you will be playing a game with dots!', height=50,
pos=[0, 0], color='white')
welcome2_squircles_txt = visual.TextStim(win=win,
text='In this experiment, you will be playing a game of comparing colours!',
height=50,
pos=[0, 0], color='white')
prompt_txt = visual.TextStim(win=win,
text='Which box contains more dots?',
height=50,
pos=[0, 50], color='white')
prompt_below_txt = visual.TextStim(win=win,
text='(left click for left box, right click for right box)',
height=30,
pos=[0, -100], color='white')
prompt_squircles_txt = visual.TextStim(win=win,
text='Which circle is more red?',
height=50,
pos=[0, 50], color='white')
prompt_squircles_below_txt = visual.TextStim(win=win,
text='(left click for left circle, right click for right circle)',
height=30,
pos=[0, -100], color='white')
practice_instructions_txt = visual.TextStim(win=win,
text='During this game, you will see two boxes containing dots briefly '
'flash on either side of the centre of the screen (marked with a "+" sign). '
'Your task is to decide whether the left or right box contains more dots. '
'If you think the left box contained more dots, you respond with a '
'left mouse-click. If you think the right box contained more dots, you '
'respond with a right mouse-click. The task will start off quite '
'easy but will become harder as you progress. \n \n Press the "next" '
'button to start the game. The first phase of the game can take up to 8 '
'minutes. You should reach a stable performance level to continue to the '
'next phase.', height=th, pos=[0, 0], wrapWidth=1000, color='white')
practice_instructions_squircles_txt = visual.TextStim(win=win,
text='During this game, you will see 2 circles of coloured patches that will'
' briefly flash on either side of the centre of the screen (marked '
'with a "+" sign). The patches will have colours ranging from red to '
'blue. Your task is to decide which of the two circles of coloured '
'patches is more red on average, compared to the other one. If you '
'think the left circle was more red, you respond with a left '
'mouse-click. If you think the right circle was more red, you respond '
'with a right mouse-click. The task will start off quite easy but will '
'become harder as you progress \n \n Press the "next" button to start '
'the game. The first phase of the game can take up to 8 '
'minutes. You should reach a stable performance level to continue to the '
'next phase.', height=th, pos=[0, 0], wrapWidth=1000,
color='white')
continue_txt = visual.TextStim(win=win, text='Great, you are now ready to continue!', height=60, pos=[0, 0],
wrapWidth=1000,
color='white')
halfway_txt = visual.TextStim(win=win,
text='Great! You are half-way through. \n \n For the second half you will be paired with a different partner.',
height=60, pos=[0, 0], wrapWidth=1000,
color='white')
confidence_instructions_txt = visual.TextStim(win=win,
text='From now on, you will indicate your confidence in your decisions. \n \n'
'After responding with a left or right click, you will now '
'indicate your confidence in the decision on a sliding '
'scale. Hover over the slider bar to change the slider '
'position - towards the middle f you aren\'t sure of your '
'answer, and towards the left/right side if you\'re confident '
'that the respective side is the correct answer - then click'
' on the slider bar to register your confidence rating. '
'\n \n Press the "next" button to do a few trials with the confidence '
'slider.', height=th, pos=[0, 0], wrapWidth=1000,
color='white')
confidence_feedback_instructions_txt = visual.TextStim(win=win,
text='From now on, you will indicate your confidence in '
'your decisions and get feedback if your choice was correct or incorrect. \n \n'
'After responding with a left or right click, you '
'will now indicate your confidence in the decision '
'on a sliding scale. Hover over the slider bar to '
'change the slider position - towards the middle '
'if you aren\'t sure of your answer, and towards '
'the left/right side if you\'re confident that the '
'respective side is the correct answer - then '
'click on the slider bar to register your confidence '
'rating. After you respond, you will get feedback '
'about whether the side you picked was correct or '
'incorrect \n \n '
'Press the "next" button to do a few trials with '
'the confidence slider.', height=th, pos=[0, 0],
wrapWidth=1000,
color='white')
partner1_observe_instructions_txt = visual.TextStim(win=win,
text='For the rest of the experiment, you\'ll be doing the task you '
'just practiced. However, from now on you will see the response'
' of another past participant for the stimulus. Your '
'partner did about as well as you did in the practice trials. \n \n'
'You will earn points depending on whether your response is '
'correct and how accurate your confidence judgment was. At the '
'end of the game, we will take all trials where you indicated, '
'for example, a confidence of 70-80%% and check if in fact '
'70-80%% of those trials were correct. The closer your confidence '
'judgements align with your actual accuracy, the higher your '
'cash bonus (up to £5) will be at the end of the experiment.'
'\n \nThere will be %i blocks of %i trials, which should take '
'approximately 30 minutes to complete altogether. Press "next" '
'to continue to the first block.'
% (
gv['n_blocks_per_partner'] * 2,
gv['n_trials_per_block']),
height=th, pos=[0, 0],
wrapWidth=1000, color='white')
partner2_observe_instructions_txt = visual.TextStim(win=win,
text='Based on your responses, we will select another participant '
'who did about as well as you did on the task. Different '
'partner, same rules: After responding with a left or right '
'click and indicating your confidence on the slider, you will '
'see the your partner\'s response for the same stimulus. \n \n You '
'will earn points depending on whether your response is correct'
' and how accurate your confidence judgment was. At the end of '
'the game, we will take all trials where you indicated, for '
'example, a confidence of 70-80%% and check if in fact 70-80%% '
'of those trials were correct. The closer your confidence '
'judgements align with your actual accuracy, the higher your '
'cash bonus (up to £5) will be at the end of the experiment. '
'\n \nPress the "next" button to do %i more blocks of the game.'
% (gv['n_blocks_per_partner']), height=th, pos=[0, 0],
wrapWidth=1000, color='white')
partner1_strategic_instructions_txt = visual.TextStim(win=win,
text='For the rest of the experiment, you\'ll be doing the task you '
'just practiced. However, from now on you will see the response '
'of another past participant for the same stimulus. Your partner '
'did about as well as you did in the practice trials. The '
'decision that is reported with higher confidence will be selected '
'as your joint decision. \n \nCritically, your feedback and cash bonus '
'will be based on the joint decision. Every correct joint decision '
'will earn you 2p. Therefore, if you are sure you got it right, '
'report high confidence such that your decision counts. '
'However, if you are not very sure, it might be best to report '
'low confidence and thus let your partner\'s decision be chosen.'
'\n \nThere will be %i blocks of %i trials, which should take '
'approximately 30 minutes to complete altogether. Press "next" '
'to continue to the first block.'
% (gv['n_blocks_per_partner'] * 2,
gv['n_trials_per_block']),
height=th, pos=[0, 0],
wrapWidth=1000, color='white')
partner2_strategic_instructions_txt = visual.TextStim(win=win,
text='Based on your responses, we will select another participant '
'who did about as well as you did on the task. Different '
'partner, same rules: After responding with a left or right '
'click and indicating your confidence on the slider, you will '
'see the your partner\'s response for the same stimulus. '
'Your partner did about as well as you did in the practice trials. '
'The decision that is reported with higher confidence will be selected '
'as your joint decision. \n \n Critically, your feedback and cash bonus '
'will be based on the joint decision. Therefore, if you are sure '
'you got it right, report high confidence such that your decision '
'counts. However, if you are not very sure, it might be best to '
'report low confidence and thus let your partner\'s decision be chosen. '
'\n \nPress the "next" button to do %i more blocks of the game.'
% (gv['n_blocks_per_partner']), height=th, pos=[0, 0],
wrapWidth=1000, color='white')
thanks_txt = visual.TextStim(win=win, text='Thank you for completing the study!', height=70, pos=[0, 0], color='white')
qtxt1 = visual.TextStim(win=win, text='What did you think about your partner\n .', height=55, pos=[0, 0], color='white')
qtxt2 = visual.TextStim(win=win, text='What did you think about your partner\n . .', height=55, pos=[0, 0],
color='white')
qtxt3 = visual.TextStim(win=win, text='What did you think about your partner\n . . .', height=55, pos=[0, 0],
color='white')
qtxt4 = visual.TextStim(win=win, text='What did you think about your partner\n . . . ?', height=55, pos=[0, 0],
color='white')
######################################################################################################################################################
############################################################# FUNCTIONS #######################################################################
######################################################################################################################################################
# HELPER FUNCTIONS
# 20x20 matrix with 0s (no dots) and 1s (dots) in random locations. number of 1s = number of dots
def dots_matrix(shape, ones):
o = np.ones(ones, dtype=int)
z = np.zeros(np.product(shape) - ones, dtype=int)
board = np.concatenate([o, z])
np.random.shuffle(board)
return board.reshape(shape)
# generate colour samples for the squircles
def generate_colour_samples(colour_mean, colour_sd, n_circles):
# create a list of n numbers between a and b
def random_list(n, a, b):
list = []
for i in range(n):
list.append(random.random() * (b - a) + a)
return list
# create a list of random numbers between 1 and 10
list = random_list(n_circles, 1, 10)
# compute mean, sd and the interval range [min, max] of list
def descriptives_list(list):
leng = len(list)
a = float('inf')
b = float('-inf')
sum = 0
for i in range(leng):
sum += float(list[i])
a = min(a, list[i])
b = max(b, list[i])
mean = sum / leng
sum = 0
for i in range(leng):
sum += (list[i] - mean) * (list[i] - mean)
sd = math.sqrt(sum / (leng - 1))
return {
'mean': mean,
'sd': sd,
'range': [a, b]
}
# transform list to have an exact mean and sd
def force_descriptives(list, mean, sd):
old_descriptives = descriptives_list(list)
old_mean = old_descriptives['mean']
old_sd = old_descriptives['sd']
new_list = []
leng = len(list)
for i in range(leng):
new_list.append(sd * (list[i] - old_mean) / old_sd + mean)
return new_list
new_list = force_descriptives(list, colour_mean, colour_sd)
random.shuffle(new_list)
return new_list
# turn colour sample values into RGB values from blue to red
def generate_rgb_values(value):
colour_value = [value * 255, 0, (1 - value) * 255]
return colour_value
# add this in to allow exiting the experiment when we are in full screen mode
def exit_q(key_list=None):
# this just checks if anything has been pressed - it doesn't wait
if key_list is None:
key_list = ['q']
keys = event.getKeys(keyList=key_list)
res = len(keys) > 0
if res:
if 'q' in keys:
trig = triggers['exp_end']
send_trigger(trig)
win.close()
core.quit()
return res
# returns 0 if incorrect and certain, and 1 if correct and certain
def reverse_brier_score(confidence, outcome):
if outcome:
o = 0
else:
o = 1
f = confidence / 100
return (f - o) ** 2
# function for sampling from normal distribution with given min, max, and skew
# standard Normal variate using Box-Muller transform
def randn_bm(min, max, skew):
u = 0
v = 0
while u == 0:
u = random.random()
while v == 0:
v = random.random()
num = math.sqrt(-2.0 * math.log(u)) * math.cos(2.0 * math.pi * v)
num = num / 10.0 + 0.5
if num > 1 or num < 0:
num = randn_bm(min, max, skew)
num = math.pow(num, skew)
num *= max - min
num += min
return num
# loading partner
def load_partner():
visual.TextStim(win=win, text='selecting your partner \n \n .', height=55, pos=[0, 0], color='white').draw()
core.wait(0.7)
win.flip()
visual.TextStim(win=win, text='selecting your partner \n \n . .', height=55, pos=[0, 0], color='white').draw()
core.wait(0.7)
win.flip()
visual.TextStim(win=win, text='selecting your partner \n \n . . .', height=55, pos=[0, 0], color='white').draw()
core.wait(0.7)
win.flip()
visual.TextStim(win=win, text='checking for similar performance levels \n \n .', height=55, pos=[0, 0],
color='white').draw()
core.wait(0.7)
win.flip()
visual.TextStim(win=win, text='checking for similar performance levels \n \n . .', height=55, pos=[0, 0],
color='white').draw()
core.wait(0.7)
win.flip()
visual.TextStim(win=win, text='checking for similar performance levels \n \n . . .', height=55, pos=[0, 0],
color='white').draw()
core.wait(0.7)
win.flip()
visual.TextStim(win=win, text='loading the game \n \n .', height=55, pos=[0, 0], color='white').draw()
core.wait(0.7)
win.flip()
visual.TextStim(win=win, text='loading the game \n \n . .', height=55, pos=[0, 0], color='white').draw()
core.wait(0.7)
win.flip()
visual.TextStim(win=win, text='loading the game \n \n . . .', height=55, pos=[0, 0], color='white').draw()
core.wait(0.7)
win.flip()
# questionnaire items
def questionnaire_item(item_text='item text', tick1='\n1\n not at all', tick10='\n10\n very much'):
rating = visual.RatingScale(win=win, pos=(0, -100), low=1, high=10, stretch=1.4,
marker='circle', markerColor=(0.92, 0.74, 0.40), showAccept=False, singleClick=True,
tickMarks=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
labels=[tick1, '2', '3', '4', '5', '6', '7', '8', '9', tick10])
item = visual.TextStim(win, text=item_text, pos=(0, 150), height=th + 10, color='white')
while rating.noResponse:
item.draw()
rating.draw()
win.flip()
curr_rating = rating.getRating()
win.flip()
# return results
return curr_rating
# DRAW DOTS FUNCTION
def do_trial(win, mouse, gv, info, triggers):
# CLOCK
trial_clock = core.Clock()
# STIMULI
fixation = visual.ShapeStim(
win,
vertices=((0, -2), (0, 2), (0, 0), (-2, 0), (2, 0)),
lineWidth=50,
pos=(0, 100),
closeShape=False,
lineColor="white"
)
circle_radius_pix = [1, 1]
dot = visual.Circle(
win=win,
radius=circle_radius_pix,
units="pix",
edges=300,
fillColor="white",
)
rect_right = visual.Rect(
win=win,
units="pix",
width=20 * 12,
height=20 * 12,
pos=(210, 105), # check which variables should correspond to the correct position
colorSpace='rgb255',
fillColor=None,
lineColor=(255, 255, 255)
)
rect_left = visual.Rect(
win=win,
units="pix",
width=20 * 12,
height=20 * 12,
pos=(-210, 105), # check which variables should correspond to the correct position
colorSpace='rgb255',
fillColor=None,
lineColor=(255, 255, 255)
)
slider_marker = visual.ShapeStim(
win=win,
vertices=((0, -25), (0, 25)),
lineWidth=8,
pos=(0, 0),
closeShape=False,
colorSpace='rgb255',
lineColor=(245, 222, 179),
)
partner_marker = visual.ShapeStim(
win=win,
vertices=((0, -25), (0, 25)),
lineWidth=8,
pos=(0, 0),
closeShape=False,
lineColor=info['partner_colour'],
)
higher_conf_box = visual.Rect(
win=win,
units="pix",
width=20,
height=60,
pos=(0, 0),
lineWidth=4,
fillColor=None,
lineColor="white"
)
slider_cover = visual.Rect(
win=win,
units="pix",
width=400,
height=60,
pos=(0, 0),
opacity=0.6,
fillColor='white',
lineColor='white'
)
# HIDE THE MOUSE
win.mouseVisible = False
# DRAW THE FIXATION CROSS
trig = triggers['fixation_cross']
fixation.draw()
win.flip()
send_trigger(trig)
exit_q()
core.wait(gv['fixation_period'])
# determine the correct response and draw the left/right stimulus correspondingly
response_options = ["left", "right"]
correct_response = random.choice(response_options)
info['correct_response'] = correct_response
# SQUIRCLES TASK
if info['task'] == 's':
choice_txt.text = 'Which circle was more red?'
# CREATE AND DRAW THE SQUIRCLE STIMULI
difference = gv['next_squircle_difference']
# determine squircle parameters
colour_mean_low = round(random.uniform(0, (0.9 - difference)),
2) # 0.9 instead of 1.0 because of the variance around the mean
colour_mean_high = colour_mean_low + difference # this will be max. .09 (i.e. even with sd won't go above 1)
info['squircle_value_low'] = colour_mean_low
info['squircle_value_high'] = colour_mean_high
if correct_response == "left":
trig = triggers['stimulus_left_correct']
left_squircle_colours = generate_colour_samples(colour_mean_high, gv['squircles_color_sd'], gv[
'squircles_n_circles']) # one number for each circle with defined mean and sd for the numbers
right_squircle_colours = generate_colour_samples(colour_mean_low, float(gv['squircles_color_sd']),
int(gv['squircles_n_circles']))
else:
trig = triggers['stimulus_right_correct']
left_squircle_colours = generate_colour_samples(colour_mean_low, gv['squircles_color_sd'],
gv['squircles_n_circles'])
right_squircle_colours = generate_colour_samples(colour_mean_high, gv['squircles_color_sd'],
gv['squircles_n_circles'])
circle = visual.Circle(
win=win,
units="pix",
colorSpace='rgb255',
fillColor=(255, 255, 255),
lineColor=(0, 0, 0),
edges=128,
radius=22
)
f = 360 / gv['squircles_n_circles']
thetas = [x * f for x in range(360)]
# right stimulus
for i in range(gv['squircles_n_circles']):
[pos_x, pos_y] = misc.pol2cart(
thetas[i],
80
)
circle.pos = [pos_x + rect_right.pos[0], pos_y + rect_right.pos[1]]
fill_colour = generate_rgb_values(right_squircle_colours[i])
circle.fillColor = (fill_colour[0], fill_colour[1], fill_colour[2])
circle.draw()
# left stimulus
for i in range(gv['squircles_n_circles']):
[pos_x, pos_y] = misc.pol2cart(
thetas[i],
75
)
circle.pos = [pos_x + rect_left.pos[0], pos_y + rect_left.pos[1]]
fill_colour = generate_rgb_values(left_squircle_colours[i])
circle.fillColor = (fill_colour[0], fill_colour[1], fill_colour[2])
circle.draw()
# DOTS TASK
else:
# CREATE AND DRAW THE DOTS GRID
dot_count_high = gv['next_dot_count_high']
dot_count_low = gv['next_dot_count_low']
if correct_response == "left":
trig = triggers['stimulus_left_correct']
dots_matrix_left = dots_matrix([20, 20], dot_count_high)
dots_matrix_right = dots_matrix([20, 20], dot_count_low)
else:
trig = triggers['stimulus_right_correct']
dots_matrix_right = dots_matrix([20, 20], dot_count_high)
dots_matrix_left = dots_matrix([20, 20], dot_count_low)
info['dot_count_low'] = dot_count_low
info['dot_count_high'] = dot_count_high
# draw the dots grids based on the matrices
offsets = range(0, 19, 1)
row_count = 0
# right stimulus
for y_offset in offsets:
for x_offset in offsets:
if dots_matrix_right[y_offset, x_offset] == 1:
for stimulus in [dot]:
stimulus.pos = [x_offset * 12 + 100, y_offset * 12]
stimulus.draw()
row_count = row_count + 1
# left stimulus
for y_offset in offsets:
for x_offset in offsets:
if dots_matrix_left[y_offset, x_offset] == 1:
for stimulus in [dot]:
stimulus.pos = [x_offset * (-12) - 100, y_offset * 12]
stimulus.draw()
row_count = row_count + 1
# show stimulus for stimulus period
rect_right.draw()
rect_left.draw()
win.flip()
send_trigger(trig)
trial_clock.reset() # set clock to 0 at stimulus presentation
exit_q()
core.wait(gv['stimulus_period'])
# remove stimulus but keep the rectangles
rect_right.draw()
rect_left.draw()
# choice_txt.draw() # leave this out to not have a visual change on screen post stimulus presentation
win.flip()
exit_q()
# PARTICIPANT CHOICE LEFT/RIGHT
choice = None
# wait for mouse click
buttons = mouse.getPressed()
while buttons == [0, 0, 0]:
buttons = mouse.getPressed()
# left click
if buttons == [1, 0, 0]:
trig = triggers ['response_left']
send_trigger(trig)
# save participant choice
choice = "left"
rect_left.lineColor = (245, 222, 179)
rect_left.lineWidth = 6
slider_cover.pos = (200, -300)
# right click
if buttons == [0, 0, 1]:
trig = triggers['response_right']
send_trigger(trig)
# save participant choice
choice = "right"
rect_right.lineColor = (245, 222, 179)
rect_right.lineWidth = 6
slider_cover.pos = (-200, -300)
info['decision_rt'] = trial_clock.getTime()
info['participant_response'] = choice
if choice == correct_response:
participant_correct = True
else:
participant_correct = False
info['participant_correct'] = participant_correct
rect_right.draw()
rect_left.draw()
core.wait(0.8) # do not have visual changes on screen for 800ms post response
trig = triggers['highlight_box']
win.flip()
send_trigger(trig)
exit_q()
# STAIRCASING
# two-down one-up staircase procedure with equal step-sizes for steps up and down
stair_value = gv['next_stair_value']
info['stair_value'] = stair_value
if info['staircasing_on']:
if not participant_correct: # incorrect, make it easier
if info['trial_in_block'] <= gv['staircase_breaks'][0]:
stair_value = stair_value + gv['staircase_step_sizes'][
0] # changing by + 0.4 in log space for the first 5 trials
elif gv['staircase_breaks'][0] < info['trial_in_block'] <= gv['staircase_breaks'][1]:
stair_value = stair_value + gv['staircase_step_sizes'][
1] # changing by + 0.2 in log space for the next 5 trials
else:
stair_value = stair_value + gv['staircase_step_sizes'][
2] # changing by + 0.1 in log space for the rest of the task
elif participant_correct & gv['previous_trial_correct']: # correct two times in a row, make it harder
if info['trial_in_block'] <= gv['staircase_breaks'][0]:
stair_value = stair_value - gv['staircase_step_sizes'][
0] # changing by - 0.4 in log space for the first 5 trials
elif gv['staircase_breaks'][0] < info['trial_in_block'] <= gv['staircase_breaks'][1]:
stair_value = stair_value - gv['staircase_step_sizes'][
1] # changing by - 0.2 in log space for the next 5 trials
else:
stair_value = stair_value - gv['staircase_step_sizes'][
2] # changing by- 0.1 in log space for the rest of the task
else: # correct only once in a row, keep it the same
stair_value = stair_value
gv['stair_values_list'].append(stair_value)
gv['next_stair_value'] = stair_value
gv['previous_trial_correct'] = participant_correct # only update this variable after we have used it!
else:
min_stair_value = min(gv['stair_values_list'])
stair_value = round((gv['next_stair_value'] + min_stair_value) / 2,
2) # final stair value is the average of the last stair value and the lowest stair value that the participant reached in the practice phase (this is in case participants just start slacking in the end and perform worse than they could)
# SQUIRCLES TASK
if info['task'] == 's':
gv['next_squircle_difference'] = int(round((math.e ** stair_value), 0)) / 1000
# DOTS TASK
else:
next_dot_count_low = info['dot_count_low'] # stays the same
next_dot_count_high = next_dot_count_low + int(round((math.e ** stair_value), 0))
# limits for dots difference at 1 and 100
if next_dot_count_high < (next_dot_count_low + gv['dot_difference_limits'][0]):
next_dot_count_high = next_dot_count_low + gv['dot_difference_limits'][0]
elif next_dot_count_high > (next_dot_count_low + gv['dot_difference_limits'][1]):
next_dot_count_high = next_dot_count_low + gv['dot_difference_limits'][1]
gv['next_dot_count_low'] = next_dot_count_low
gv['next_dot_count_high'] = next_dot_count_high
# CONFIDENCE SLIDER
if info['confidence_slider_on']:
# ENABLE MOUSE
win.mouseVisible = True
slider = visual.Slider(win, ticks=(1, 2, 3, 4, 5, 6, 7),
labels=["certainly\nLEFT", "probably\nLEFT", "maybe\nLEFT", "",
"maybe\nRIGHT", "probably\nRIGHT", "certainly\nRIGHT"],
pos=(0, -300),
size=(800, 50), units="pix", flip=True, style='slider', granularity=0, labelHeight=20)
slider.tickLines.sizes = (1, 30)
slider.draw()
slider_cover.draw()
rect_left.draw()
rect_right.draw()
confidence_txt.draw()
core.wait(0.3)
win.flip()
trial_clock.reset() # reset the trial clock to measure time until confidence rating
exit_q()
# ANIMATE THE CONFIDENCE SLIDER MARKER
slider_rating_txt = visual.TextStim(win=win, text='%', height=18,
pos=(0, 0), color='white')
trig = triggers['confidence_rating']
while not slider.rating:
# restrict slider marker to the range of slider
if mouse.getPos()[0] > (slider.size[0] / 2):
slider_marker.pos = ((slider.size[0] / 2), slider.pos[1])
slider_rating_txt.pos = ((slider.size[0] / 2), slider.pos[1] - 40)
elif mouse.getPos()[0] < -(slider.size[0] / 2):
slider_marker.pos = (-(slider.size[0] / 2), slider.pos[1])
slider_rating_txt.pos = (-(slider.size[0] / 2), slider.pos[1] - 40)
else:
slider_marker.pos = (mouse.getPos()[0], slider.pos[1])
slider_rating_txt.pos = (mouse.getPos()[0], slider.pos[1] - 40)
if choice == 'right':
participant_confidence = 100 - (slider.size[0] / 2 - slider_marker.pos[0]) / slider.size[0] * 100
else:
participant_confidence = (slider.size[0] / 2 - slider_marker.pos[0]) / slider.size[0] * 100
# don't allow clicks on confidence slider on the side that was not chosen initially
if participant_confidence < 50:
slider.readOnly = True
else:
slider.readOnly = False
participant_confidence = round(participant_confidence, 2)
slider_rating_txt.text = ' %i %%' % round(participant_confidence, 0)
slider.draw()
slider_cover.draw()
rect_left.draw()
rect_right.draw()
slider_marker.draw()
slider_rating_txt.draw()
confidence_txt.draw()
win.flip()
exit_q()
send_trigger(trig)
info['confidence_rt'] = trial_clock.getTime()
info['participant_confidence'] = participant_confidence
print(participant_confidence)
# PARTNER CHOICE AND CONFIDENCE RATING
if info['partner'] is not None:
info['trial_score'] = reverse_brier_score(participant_confidence, participant_correct)
partner_confidence = 999
while partner_confidence > 100 or partner_confidence < 50:
# p correct from normal distribution between 0.6 and 1 (mean 0.8)
pCorrect = randn_bm(0.6, 1, 1)
# pick correct response with p correct
random_v = random.random()
if random_v < pCorrect:
partner_choice = correct_response
info['partner_correct'] = True
else:
if correct_response == 'left':
partner_choice = 'right'
else:
partner_choice = 'left'
info['partner_correct'] = False
info['partner_response'] = partner_choice
# given p(correct), determine partner's confidence according to c + (x - .5) * s + e
# c is a constant do differentiate over- and under-confident partners (overconfident set to 0.6, underconfident set to 0.1)
# s is the slope which I set to 0.8
# e is random noise which I will sample from a normal distribution with m=0 and sd=.05
error = randn_bm(-0.05 * 3, 0.05 * 3, 1)
if info['partner'] == 'underconfident':
partner_confidence = 0.1 + (pCorrect - 0.5) * 0.8 + error
partner_confidence = partner_confidence * 50 + 50
elif info['partner'] == 'overconfident':
partner_confidence = 0.6 + (pCorrect - 0.5) * 0.8 + error
partner_confidence = partner_confidence * 50 + 50
else:
partner_confidence = None
if partner_choice == 'right':
partner_marker_position = -slider.size[0] / 2 + partner_confidence / 100 * slider.size[0]
trig = triggers['partner_marker_right']
else:
partner_marker_position = slider.size[0] / 2 - partner_confidence / 100 * slider.size[0]
trig = triggers['partner_marker_left']
partner_marker.pos = (partner_marker_position, slider.pos[1])
partner_confidence = round(partner_confidence, 2)
info['partner_confidence'] = partner_confidence
print(partner_confidence)
core.wait(0.6)
slider.draw()
slider_cover.draw()
rect_left.draw()
rect_right.draw()
slider_marker.draw()
slider_rating_txt.draw()
partner_marker.draw()
win.flip()
send_trigger(trig)
exit_q()
core.wait(1) # give participant time to see the partner marker
# STRATEGIC CONDITION
if info['condition'] == 's':
trig = triggers['higher_conf_box']
# highlight higher confidence decision
if partner_confidence > participant_confidence: # partner's decision is chosen
higher_conf_box.pos = (partner_marker_position, slider.pos[1])
higher_conf_box.lineColor = info['partner_colour']
info['participant_chosen'] = False
info['partner_chosen'] = True