forked from thartbm/PyVMEC
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathExp.py
1859 lines (1675 loc) · 98.8 KB
/
Exp.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
# with functions that run a trial sequence as passed to it, and stores the data appropriately
from psychopy.visual import Window, Circle, ShapeStim, TextStim, ImageStim
from psychopy import event, core
from psychopy.visual import shape
from os import path, listdir
from json import dump
import pyautogui
#import pygame as pg
#from pygame import QUIT, quit, KEYDOWN, K_SPACE, K_ESCAPE
#from pygame import event as pev
from numpy import sqrt, arctan2, cos, sin, linalg, dot, ndarray, array, diff, mean, arange, pi, dot
import csv
import math
from pandas import concat, DataFrame
from random import choice, seed, shuffle
from Tkinter import Tk
from copy import deepcopy
import sys
import screeninfo
try:
from ctypes import *
except:
pass
from time import time
root = Tk()
def addWorkSpaceLimits(screen, cfg = {}):
s = screeninfo.get_monitors()
# why all this complicated code?
#if (len(s) == 1 and screen == 0):
# screen_width = root.winfo_screenwidth()
# screen_height = root.winfo_screenheight()
#elif(len(s) > 1 and screen == 0):
# screen_width = int(str(s[0]).strip('monitor(').partition('x')[0])
# screen_height = int(str(s[0]).strip('monitor(').partition('x')[2].partition('+')[0])
# cfg['main_screen_dimensions'] = [int(str(s[1]).strip('monitor(').partition('x')[0]), int(str(s[1]).strip('monitor(').partition('x')[2].partition('+')[0])]
#else:
# screen_width = int(str(s[1]).strip('monitor(').partition('x')[0])
# screen_height = int(str(s[1]).strip('monitor(').partition('x')[2].partition('+')[0])
# cfg['main_screen_dimensions'] = [int(str(s[0]).strip('monitor(').partition('x')[0]), int(str(s[0]).strip('monitor(').partition('x')[2].partition('+')[0])]
screen_width = s[screen].width
screen_height = s[screen].height
cfg['main_screen_dimensions'] = [screen_width, screen_height]
cfg['main_screen_offset'] = [s[screen].x, s[screen].y]
trimmed_width = int((float(2)/float(3))*float(screen_width))
trimmed_height = int((float(2)/float(3))*float(screen_height))
if (trimmed_height*2 < trimmed_width):
trimmed_width = trimmed_height*2
else:
trimmed_height = trimmed_width/2
cfg['active_width'] = trimmed_width
cfg['active_height'] = trimmed_height
cfg['circle_radius'] = trimmed_height*0.025
cfg['icon_diameter'] = trimmed_height*0.075
# where is the home position calculated?
cfg['home_pos'] = [0, -0.35 * trimmed_height]
# active height is 1 normalized screen unit: this is what should be stored in the cfg
# we don't need the active width all that much... it's just 2 normalized screen units
# circle_radius and icon diameter are maybe not workspace limits
cfg['screen_dimensions'] = [screen_width, screen_height]
cfg['winType'] = 'pyglet'
return cfg
# this try/except:
# 1: has to add the psychopy mouse object in the except statement
# 2: should be in a function to add a mouse object to the overall cfg
# 3: be called once, with no testing of which mouse object to use at any later point
# there is no window object yet? how does this make sense?
#cfg['psyMouse'] = event.Mouse(visible = False, newPos = None, win = cfg['win'])
#
#try:
# class myMouse:
# Xlib = CDLL("libX11.so.6")
# display = Xlib.XOpenDisplay(None)
# if display == 0: sys.exit(2)
# w = Xlib.XRootWindow(display, c_int(0))
# (root_id, child_id) = (c_uint32(), c_uint32())
# (root_x, root_y, win_x, win_y) = (c_int(), c_int(), c_int(), c_int())
# mask = c_uint()
#
# width = cfg['main_screen_dimensions'][0]
# height = cfg['main_screen_dimensions'][1]
#
# x_offset = cfg['main_screen_offset'][0]
# y_offset = cfg['main_screen_offset'][1]
#
# def Pos(self):
# ret = self.Xlib.XQueryPointer(self.display, c_uint32(self.w), byref(self.root_id), byref(self.child_id), byref(self.root_x), byref(self.root_y), byref(self.win_x), byref(self.win_y), byref(self.mask))
# if ret == 0: sys.exit(1)
# return [self.root_x.value - (self.width/2) - x_offset, -1 * (self.root_y.value - (self.height/2) - y_offset), time()] # c_int can't be used for regular Python math, their values are ints - and we return the current time
#except:
# # create an identically named object with identical behavior based on PsychoPy?
# print ('not using xLib')
#
# class myMouse:
#
# def Pos(self):
# #print('PsychoPy mouse')
# [X,Y] = cfg['psyMouse'].getPos()
# return [X,Y,time()]
#
#cfg['mouse'] = myMouse()
# function not used?
#def moveMouse(x,y):
# myMouse.setPos([x,y])
# myWin.winHandle._mouse_x = x # hack to change pyglet window
# myWin.winHandle._mouse_y = y
def myRounder(x, base):
return int(base * round(float(x)/base))
def vector_rotate(node, center, angle):
vector_X = center[0] + (node[0] - center[0])*math.cos(math.radians(angle)) - (node[1] - center[1])*math.sin(math.radians(angle))
vector_Y = center[1] + (node[0] - center[0])*math.sin(math.radians(angle)) + (node[1] - center[1])*math.cos(math.radians(angle))
return [vector_X, vector_Y]
# what is this for???
def task_namer(given_task, function):
if (function == True):
if (given_task == "cursor"):
return "Cursor"
if (given_task == "no_cursor"):
return "No Cursor"
if (given_task == "pause"):
return "Pause Task"
if (given_task == "error_clamp"):
return "Error Clamp"
elif (function == False):
if (given_task == "Cursor"):
return "cursor"
if (given_task == "No Cursor"):
return "no_cursor"
if (given_task == "Pause Task"):
return "pause"
if (given_task == "Error Clamp"):
return "error_clamp"
def task_num(given_task, function):
if (function == True):
if (given_task == "cursor"):
return 0
if (given_task == "no_cursor"):
return 1
if (given_task == "error_clamp"):
return 2
if (given_task == "pause"):
return 3
elif (function == False):
if (given_task == 0):
return "cursor"
if (given_task == 1):
return "no_cursor"
if (given_task == 2):
return "error_clamp"
if (given_task == 3):
return "pause"
def rotation_num(rotation_type, function):
if (function == True):
if (rotation_type == 'abrupt'):
return 0
elif (rotation_type == 'gradual'):
return 1
if (function == False):
if (rotation_type == 0):
return 'abrupt'
if (rotation_type == 1):
return 'gradual'
# this doesn't need to be a function as this code should be called only once: at the start of the experiment
def setParticipantSeed(participant):
seed(sum([ord(c) for c in participant]))
def shuffleTargets4task(targets, blocks):
taskTargets = []
for block in range(blocks):
shuffle(targets)
taskTargets = taskTargets + targets
return(taskTargets)
# do we use this?
def rotation_direction_num(rotation_direction, function):
if (function == True):
if (rotation_direction == 'Counter-clockwise'):
return 0
elif (rotation_direction == 'Clockwise'):
return 1
if (function == False):
if (rotation_direction == 0):
return 'Counter-clockwise'
if (rotation_direction == 1):
return 'Clockwise'
# I could be wrong, but it seems to me that all of the following stuff can be done without (big) libraries/modules
# so we can reduce the annoyance of the many files that need to be copied every time someone uses this in class...
def cart2pol(coord=[]):
rho = sqrt(coord[0]**2 + coord[1]**2)
phi = arctan2(coord[1], coord[0])
return [rho, phi]
def pol2cart(rho, phi):
x = rho * cos(phi)
y = rho * sin(phi)
return(x, y)
def get_dist(select_pos, target_pos):
vector = [target_pos[0] - select_pos[0], target_pos[1] - select_pos[1]]
return linalg.norm(vector)
# is this better than just doing pythagoras in 1 line?
def get_vect(select_pos, target_pos):
vector = [target_pos[0] - select_pos[0], target_pos[1] - select_pos[1]]
return vector
# replace with target_pos - select_pos ? # hmmm... doesn't work in Python
def get_uvect(vector): # what is sthis for?
uvect = vector/linalg.norm(vector)
return uvect
def get_vector_projection(moving_vect, static_vect):
static_uvect = get_uvect(static_vect)
scalar_proj = dot(moving_vect, static_uvect)
return scalar_proj*static_uvect
def get_clamped_vector(moving_vect, static_vect):
moving_magnitude = linalg.norm(moving_vect)
static_uvect = get_uvect(static_vect)
clamped_vector = moving_magnitude*static_uvect
return clamped_vector
def vector_projection(moving_vect, static_vect):
static_uvect = get_uvect(static_vect)
scalar_proj = dot(moving_vect, static_uvect)/(linalg.norm(static_vect))
return list(scalar_proj*static_uvect)
def angle_split(min_angle, max_angle, num_splits):
angles = []
for i in range(0, num_splits):
if (num_splits%2 == 1):
new_angle = min_angle + int(math.ceil(((float(max_angle) - float(min_angle))/(float(num_splits) - 1))))*i
else:
new_angle = min_angle + int(math.floor(((float(max_angle) - float(min_angle))/(float(num_splits) - 1))))*i
if (i == num_splits - 1):
new_angle = max_angle
angles.append(new_angle)
return angles
#################### PAUSE TASK ####################################
# great function, but it's not used...
#def pause_experiment(cfg={}):
# print('running pause task as task')
# myWin = cfg['win']
# instruction = cfg['pause_instruction']
# counter_text = TextStim(myWin, text=str(cfg['pausetime']), pos=(0, 40), color=( 1, 1, 1))
# instruction_text = TextStim(myWin, text=instruction, pos=(0,0), color=( 1, 1, 1))
# end_text = TextStim(myWin, text="Press space to continue", pos=(0,-40), color=( 1, 1, 1))
# while ((core.getTime() - cfg['time']) < cfg['pausetime']):
# counter_text.setText("{:0.0f}".format((cfg['pausetime'] - (core.getTime() - cfg['time']))))
# instruction_text.draw()
# counter_text.draw()
# myWin.flip()
# if (cfg['pause_button_wait'] == True):
# instruction_text.draw()
# if (cfg['pausetime'] > 0):
# counter_text.draw()
# end_text.draw()
# myWin.flip()
# event.waitKeys(keyList=['space'])
def trial_runner(cfg={}):
try:
myWin=cfg['win'] # excellent, all the copies in running[] are not used? oh no... cfg is now running
if (cfg['trial_type'] == 'pause'): # why does a TRIAL runner have code for a pause TASK?
# I'm assuming this is not used...
instruction = cfg['pause_instruction']
counter_text = TextStim(myWin, text=str(cfg['pausetime']), flipVert=cfg['flip_text'], pos=(0, 40*cfg['flipscreen']), color=( 1, 1, 1))
instruction_text = TextStim(myWin, text=instruction, pos=(0,0), flipVert=cfg['flip_text'], color=( 1, 1, 1))
end_text = TextStim(myWin, text="Press space to continue", pos=(0,-40*cfg['flipscreen']), flipVert=cfg['flip_text'], color=( 1, 1, 1))
# pyautogui.moveTo(root.winfo_screenwidth() - 50, root.winfo_screenheight() - 50)
#pyautogui.click() # does this need some coordinates?
#counter_text.setText("{:0.0f}".format((cfg['pausetime'] - (core.getTime() - cfg['time']))))
while ((core.getTime() - cfg['time']) < cfg['pausetime']):
counter_text.setText("{:0.0f}".format((cfg['pausetime'] - (core.getTime() - cfg['time']))))
instruction_text.draw()
counter_text.draw()
myWin.flip()
if (cfg['pause_button_wait'] == True):
instruction_text.draw()
if (cfg['pausetime'] != 0):
counter_text.draw()
end_text.draw()
myWin.flip()
event.waitKeys(keyList=['space'])
#### IF USING PYGAME ####
# pev.clear()
# while True:
# event = pev.wait()
# if event.type == QUIT:
# quit()
# sys.exit()
# elif event.type == KEYDOWN:
# if event.key == K_SPACE:
# return None
return None
end_X = cfg['starting_pos'][0] + (cfg['target_distance'] * math.cos(math.radians(cfg['target_angle'])))
end_Y = (cfg['starting_pos'][1] + ((cfg['target_distance'] * math.sin(math.radians(cfg['target_angle']))))) * cfg['flipscreen'] #- cfg['active_height']/2)*cfg['flipscreen']
### Creates Mouse object
#print('creating mouse object for EVERY TRIAL (!), and long after the workspace setup is done???')
# oh wait... it seems that the trial_runner doesn't run a trial, but a whole task
# still
#if (cfg['poll_type'] == 'psychopy'):
# myMouse = cfg['mouse']
# ### Gets current CPU Time
# myTime = cfg['time']
#elif (cfg['poll_type'] == 'x11'):
# myMouse = cfg['x11_mouse']
# ### Gets current CPU Time....
# myTime = myMouse.Pos()[2]
# WHY?! why are we getting time samples asynchronously from the mouse samples? it makes no sense...
# only do this when also getting the mouse position at the same time... this completely undoes the only advantage of the X11 system: more accurately timed samples
# myMouse should not be used:
# cfg['mouse'] should be used....
pos = cfg['mouse'].Pos() # now, position and time are sampled at the same time...
# set distance criterion for reaching target depending on if icons are used or not
dist_criterion = cfg['circle_radius']
if (cfg['custom_stim_enable'] == False):
### Creates cursor circle Object
myCircle = cfg['cursor_circle']
testCircle = cfg['test_circle']
### Creates a Target circle
endCircle = cfg['end_circle']
elif (cfg['custom_stim_enable'] == True):
rng = choice(cfg['custom_stim'])
#print rng
myCircle = rng[0]
endCircle = rng[1]
dist_criterion = 2 * dist_criterion
### Creates a circle object to be used as starting point
startCircle = cfg['start_circle']
### Define Parameters here
startPos = cfg['starting_pos'] # start circle should be at starting pos, but this is never explicitly set nor implicitly assumed in this function (trial_runner which is a task_runner)
#print('start pos:')
#print(startPos)
# which if these two are we using?
arrow=cfg['arrow_stim']
arrowFill=cfg['arrowFill_stim']
endPos=[end_X, end_Y] # doesn't it make more sense to put this right at after calculating end_X and end_Y?
### Instantiating Checking Variables Here
phase_1 = False
phase_2 = False
show_target = False
show_home = True
show_cursor = True
show_arrow = False
show_arrowFill = False
nc_check_1 = False
timerSet = False
stabilize = False
screen_edge = (root.winfo_screenwidth()/2) - (cfg['screen_on']*cfg['screen_dimensions'][0])
# what is 'screen_egde' for?
### These variables record timestamps and mouse positions (Used to calculate mouse velocity)
prev_timestamp = 0
prev_X = 0
prev_Y = 0
prev_X_cursor = 0
prev_Y_cursor = 0
velocity = 0
pixels_per_sample = 0
pos_buffer = 0
### Instantiating return dictionary and arrays within it
timePos_dict = {}
timeArray = []
mouseposXArray = []
mouseposYArray = []
cursorposXArray = []
cursorposYArray = []
### target circle position
endCircle.setPos(endPos)
### starting circle
startCircle.setPos(startPos)
arrow.setPos(startPos)
arrowFill.setPos(startPos)
### Rotation direction
except Exception as e:
print "error in Block 1" # what is block 1?
print e
if cfg['rotation_angle_direction'] == 'Counter-clockwise': # shouldn't we just take the sign of the rotation?
rot_dir = 1
elif cfg['rotation_angle_direction'] == 'Clockwise':
rot_dir = -1
# is this while loop creating frames and storing samples until the trial is finished?
# why is there a limit of 2 minutes here? what happens if someone does not get to the target in 2 minutes? do they go to the next trial... it seems better to me to exit the experiment without saving data, so we can 'continue run' and don't waste lots of disk space
#print('starting sample/frame loop')
while (core.getTime() - cfg['time']) < 120:
try:
# why is this try statement here? we're immediately moving on to the next one anyway...
### ESCAPE ### USING PYGAME ####
# event = pev.wait()
# if event.type == KEYDOWN:
# if event.key == K_ESCAPE:
# myWin.close()
# return 'escaped'
### ESCAPE ### USING PYGLET ####
try:
if event.getKeys(keyList=['escape']):
myWin.close()
return 'escaped'
### mouse Position
#print('getting mouse position:')
##print('deciding on mouse object again?')
#if (cfg['poll_type'] == 'psychopy'):
# mousePos = [myMouse.getPos()[0], myMouse.getPos()[1]*cfg['flipscreen']]
# current_pos = mousePos
# current_timestamp = core.getTime() - myTime
#elif (cfg['poll_type'] == 'x11'):
# # whyyy?
# # the mouse object is called 3 separate times to get the X coordinate, the Y coordinate and the time?
# # one call is better!
# # and it ensures that the time sample is better matched to the position coordinates...
# mousePos = [myMouse.Pos()[0], myMouse.Pos()[1]*cfg['flipscreen']] # screen flipping should be done in the mouse object?
# current_pos = mousePos
# current_timestamp = myMouse.Pos()[2] - myTime
mousePos = cfg['mouse'].Pos()
current_pos = [mousePos[0], mousePos[1]]
current_timestamp = mousePos[2]
#print(mousePos)
########################## SPECIAL CURSOR CONFIGURATIONS #####################
if (prev_timestamp != 0):
change_in_time = current_timestamp - prev_timestamp
velocity = (linalg.norm([current_pos[0] - prev_X, current_pos[1] - prev_Y]))/change_in_time # this is not velocity, but distance
pixels_per_sample = velocity*change_in_time # this is velocity
# Julius' previous version (doesn't use start position, but position, cold calculated on every sample)
#rotated_X, rotated_Y = vector_rotate(mousePos, [0 + (cfg['screen_on']*(cfg['screen_dimensions'][0]/2)), -cfg['active_height']/2], cfg['current_rotation_angle'])
# Marius' previous version (does use start position, but doesn't work on flipped screens)
#rotated_X, rotated_Y = vector_rotate(mousePos, startPos, cfg['current_rotation_angle'])
# trying to implement properly with rotation matrix
# (although the matrix should be calculated once for a trial, not on every frame...)
#print(pi)
theta = (cfg['current_rotation_angle']/180.)*pi
#print(theta)
R = array([[cos(theta),-1*sin(theta)],[sin(theta),cos(theta)]])
#print(R)
rotpos = dot(R,array([mousePos[0]-startPos[0],mousePos[1]-startPos[1]]))
rotated_X = rotpos[0]+startPos[0]
rotated_Y = rotpos[1]+startPos[1]
#print(rotated_X, rotated_Y)
if (cfg['trial_type'] == 'cursor'):
if (cfg['current_rotation_angle'] == 0):
circle_pos = current_pos
else:
circle_pos = [rotated_X, rotated_Y] # we don't have to if-else this, we can just use the rotated position all the time, right?
elif (cfg['trial_type'] == 'no_cursor'):
if (cfg['current_rotation_angle'] == 0):
circle_pos = current_pos
else:
circle_pos = [rotated_X, rotated_Y] # SAME HERE?
elif (cfg['trial_type'] == 'error_clamp'):
circle_pos = current_pos
#vector_proj_array = get_clamped_vector(get_vect(startPos, mousePos), get_vect(startPos, endPos))
#vector_proj = ndarray.tolist(vector_proj_array)
#rotated_X_clamped, rotated_Y_clamped = vector_rotate([vector_proj[0] + (cfg['screen_on']*(cfg['screen_dimensions'][0]/2)), vector_proj[1] - startPos[1]], startPos, cfg['current_rotation_angle'])
home_dist = get_dist(current_pos, startPos)
target_dist = get_dist(current_pos, endPos)
cursor_angle = cfg['target_angle'] + cfg['current_rotation_angle']
#if (target_dist > cfg['target_distance']) :
# cursor_angle = cursor_angle + 180
rotated_X_clamped = (math.cos(math.radians(cursor_angle)) * home_dist) + startPos[0]
rotated_Y_clamped = ((math.sin(math.radians(cursor_angle)) * home_dist) + startPos[1]) * cfg['flipscreen']
# we still have to multiply by flipscreen?
# cursor_direction_vector = vector_projection(get_vect(startPos, mousePos), get_vect(startPos, endPos))
# clamped_X_vector = vector_proj[0]
# clamped_Y_vector = vector_proj[1]
# if (phase_1 == False):
# active_X = circle_pos[0]
# active_Y = circle_pos[1]
# else:
## if (active_Y < startPos[1] - 20 and clamped_Y_vector < 0):
## active_X = active_X - clamped_X_vector
## active_Y = active_Y - clamped_Y_vector
## else:
## active_X = prev_X_cursor + clamped_X_vector
## active_Y = prev_Y_cursor + clamped_Y_vector
# active_X = vector_proj[0]
# active_Y = vector_proj[1]
# circle_pos_clamped = [vector_proj[0] + (cfg['screen_on']*(cfg['screen_dimensions'][0]/2)), vector_proj[1] - cfg['active_height']/2]
# whyyy?
# we are calculating this position when we don't even know if it's needed?
# only do this for clamped (&& rotation !=0) trials?
circle_pos_clamped = [rotated_X_clamped, rotated_Y_clamped]
#print('mouse position:')
#print(mousePos)
except e:
print('error in main mouse/cursor position block:')
print(e)
pass
########################### SET CURSOR POSITIONS #############################
try:
try:
if (cfg['trial_type'] == 'error_clamp' and phase_1 == True and phase_2 == False and stabilize == True):
circle_pos = circle_pos_clamped
if (cfg['trial_type'] == 'error_clamp' and phase_1 == True and phase_2 == True and stabilize == True):
# wut?
# if you are not on the zeroeth screen, we subtract half the window width?
# that does not correct for the size in the vitual desktop... and this should be done in the X11 mouse object (but not the PsychoPy mouse object, as PsychoPy does it for you)
# circle_pos = [circle_pos[0] - cfg['screen_on']*(cfg['screen_dimensions'][0]/2), circle_pos[1]]
circle_pos = [circle_pos[0], circle_pos[1]] # circle pos = circle pos?
elif (cfg['trial_type'] == 'error_clamp' and phase_1 == True and stabilize == False):
circle_pos = startPos
stabilize = True
if cfg['trial_type'] != 'error_clamp' or (cfg['trial_type'] == 'error_clamp' and phase_1 == False):
#circle_pos = [circle_pos[0] - cfg['screen_on']*(cfg['screen_dimensions'][0]/2), circle_pos[1]]
circle_pos = [circle_pos[0], circle_pos[1]]
if (cfg['screen_on'] == 1 and mousePos[0] <= -screen_edge):
# what is this for?
# does this put the cursor in the lower corner at the start of the experiment? whyyyy?
circle_pos[0] = circle_pos[0] # (-((root.winfo_screenwidth - cfg['screen_dimensions'][0])/2)) + 50
myCircle.setPos(circle_pos)
# should we not set the cursor position for the
# testCircle.setPos([circle_pos[0] +cfg['screen_dimensions'][0]/2, circle_pos[1]])
########################### SPECIAL ARROW CONDITIONS #########################
if (cfg['trial_type'] == 'no_cursor' or (cfg['trial_type'] == 'cursor' and cfg['terminal_feedback'] == True) or (cfg['trial_type'] == 'error_clamp')):
# everything should be calculated relative to the home position, so that one of the first things to do on every sample is calculated the relative position of stuff
# where does current_pos come from... don't we need circle_pos?
relPos = [circle_pos[0] - startPos[0], circle_pos[1] - startPos[1]]
orientation = -myRounder(math.degrees(cart2pol(relPos)[1]),45)
# ori is in degrees, and like a clock: 0 is upward, positive is clockwise
arrow.ori = orientation
arrowFill.ori = orientation
# previous section:
#arrow.ori = -myRounder(math.degrees(cart2pol([current_pos[0] - cfg['screen_on']*(cfg['screen_dimensions'][0]/2),current_pos[1] + cfg['active_height']/2])[1]), 45)
#arrowFill.ori = -myRounder(math.degrees(cart2pol([current_pos[0] - cfg['screen_on']*(cfg['screen_dimensions'][0]/2),current_pos[1] + cfg['active_height']/2])[1]), 45)
except:
pass # huh? we should do *something* when catching an error... why else do we have try/excepts?
################################ SHOW OBJECTS ################################
try:
if (pos_buffer == 0):
pos_buffer = pos_buffer + 1
if (show_home == True):
startCircle.draw() # home position
if (show_target == True):
endCircle.draw() # target position
if (show_arrow == True):
#print('drawing black and gray arrows on top of each other?')
arrow.draw()
arrowFill.draw()
if (show_cursor == True):
myCircle.draw() # cursor?
# testCircle.draw()
except:
pass # don't just pass this... what has failed?
except:
pass # what has failed?
except:
pass # what has failed?
################################ PHASE 1 #####################################
# phase 1 is getting to the home position (usually over very soon)
try:
if (phase_1 == False):
if (cfg['trial_type'] == 'cursor'):
if (get_dist(circle_pos, startPos) < dist_criterion and velocity < 35):
phase_1 = True
show_home = False
show_target = True
if (cfg['terminal_feedback'] == True):
show_cursor = False
elif (cfg['trial_type'] == 'no_cursor'):
if (get_dist(circle_pos, startPos) < dist_criterion and velocity < 35):
phase_1 = True
show_target = True
show_home = False
show_cursor = False
elif (cfg['trial_type'] == 'error_clamp'):
if (get_dist(circle_pos, startPos) < dist_criterion and velocity < 35):
phase_1 = True
show_target = True
show_home = False
if (cfg['terminal_feedback'] == True):
show_cursor = False
################################ PHASE 2 #####################################
# phase 2 is getting to the target
if (phase_1 == True and phase_2 == False):
if (cfg['trial_type'] == 'cursor'):
if (get_dist(circle_pos, endPos) < dist_criterion and velocity < 35 and cfg['terminal_feedback'] == False):
phase_2 = True
show_home = True
show_target = False
#if (cfg['terminal_feedback'] == True and (get_dist(circle_pos, startPos) >= cfg['terminal_multiplier']*get_dist(startPos, endPos)) and phase_1 == True):
if (cfg['terminal_feedback'] == True and (get_dist(circle_pos, startPos) >= cfg['target_distance']) and phase_1 == True):
#timer = core.getTime() # this time is not what we need...
terminal_start_time = time()
phase_2 = True
show_home = True
show_target = False # why is the target switched of now? owww... because the terminal feedback is handled by it's own loop, not in the main loop... not good
#show_target = True # it should be switched of with the feedback
# no, the feedback should be just beyond the target, according to the terminal_multiplier, and this should be shown whenever people reach as far as the target is...
# end_point = circle_pos
relPos = [circle_pos[0] - startPos[0], circle_pos[1] - startPos[1]]
#print(relPos)
#print(cart2pol(relPos))
terminal_feedback_angle = math.degrees(cart2pol(relPos)[1])
terminal_distance = cfg['target_distance'] * cfg['terminal_multiplier']
terminal_X = (math.cos(math.radians(terminal_feedback_angle)) * terminal_distance) + startPos[0]
terminal_Y = ((math.sin(math.radians(terminal_feedback_angle)) * terminal_distance) + startPos[1]) * cfg['flipscreen'] # cfg['flipscreen'] is actually running[i]['flipscreen'], and *always* 1
myCircle.pos = [terminal_X, terminal_Y]
# the rest of the experiment should continue working while displaying this... that is... why do we go into another while loop?
end_point = circle_pos # this is for determining where a reach ended, and how far people have moved from it (for showing arrow feedback)
#print('time:')
#print(current_timestamp)
#print(terminal_start_time)
#print(current_timestamp - terminal_start_time)
show_terminal = (current_timestamp - terminal_start_time) < cfg['terminal_feedback_time']
#print(show_terminal)
while (show_terminal):
# show feedback:
endCircle.draw()
myCircle.draw()
myWin.flip()
#print('flipped window, still in terminal feedback loop')
# collect data:
mousePos = cfg['mouse'].Pos()
current_pos = [mousePos[0], mousePos[1]]
current_timestamp = mousePos[2]
#print('got mouse position and time, still in terminal feedback loop')
timeArray.append(current_timestamp) # what is myTime?
mouseposXArray.append(current_pos[0])
mouseposYArray.append(current_pos[1] - startPos[1])
cursorposXArray.append(rotated_X)
cursorposYArray.append(rotated_Y - startPos[1])
#print('stored data in vectors, still in terminal feedback loop')
show_terminal = (current_timestamp - terminal_start_time) < cfg['terminal_feedback_time']
#print(show_terminal)
#print('show terminal loop ended...')
#print((current_timestamp - terminal_start_time) < cfg['terminal_feedback_time'])
if (cfg['trial_type'] == 'no_cursor'):
##### STOP WATCH ####
# if (pixels_per_sample <= 1 and timerSet == False):
# timer_timestamp = current_timestamp
# timerSet = True
# stop_time = current_timestamp - timer_timestamp
# print(cursor_angle)
if (pixels_per_sample > 1 and timerSet == True):
timerSet = False
stop_time = 0
if (get_dist(circle_pos, startPos) > cfg['circle_radius'] and nc_check_1 == False):
nc_check_1 = True
if (get_dist(circle_pos, startPos) > get_dist(startPos, endPos)/2):
if current_timestamp > .5:
idx = (array(timeArray) > (current_timestamp - .5)).nonzero()[0]
avgspeed = mean(sqrt(diff(array(mouseposXArray)[idx])**2 + diff(array(mouseposYArray)[idx])**2) / diff(array(timeArray)[idx]))
if avgspeed < 3:
phase_2 = True
show_target = False
show_home = True
end_point = circle_pos
if (cfg['trial_type'] == 'error_clamp'):
if cfg['terminal_feedback'] == False and abs(cfg['current_rotation_angle']) == 0:
if (get_dist(circle_pos, endPos) < cfg['circle_radius'] and velocity < 35):
end_point = circle_pos
phase_2 = True
show_home = True
show_cursor = False
show_target = False
elif cfg['terminal_feedback'] == False and abs(cfg['current_rotation_angle']) > 0:
if (get_dist(circle_pos, startPos) >= get_dist(startPos, endPos) and velocity < 35):
#print('putting end_pos at circle_pos, instead of just beyond the target?') # why is the terminal multiplier not used? that is the only thing it should be used for...
end_point = circle_pos
phase_2 = True
show_home = True
show_cursor = False
show_target = False
#elif (cfg['terminal_feedback'] == True and (get_dist(circle_pos, startPos) >= cfg['terminal_multiplier']*get_dist(startPos, endPos)) and phase_1 == True):
# why is the logic for terminal feedback implemented twice?
elif (cfg['terminal_feedback'] == True and (get_dist(circle_pos, startPos) >= cfg['target_distance']) and phase_1 == True):
timer = core.getTime()
phase_2 = True
show_home = True
show_target = False
relPos = [circle_pos[0] - startPos[0], circle_pos[1] - startPos[1]]
#print(relPos)
#print(cart2pol(relPos))
terminal_feedback_angle = math.degrees(cart2pol(relPos)[1])
terminal_distance = cfg['target_distance'] * cfg['terminal_multiplier']
terminal_X = (math.cos(math.radians(terminal_feedback_angle)) * terminal_distance) + startPos[0]
terminal_Y = ((math.sin(math.radians(terminal_feedback_angle)) * terminal_distance) + startPos[1]) * cfg['flipscreen']
myCircle.pos = [terminal_X, terminal_Y]
end_point = circle_pos
#print('time:')
#print(current_timestamp)
#print(terminal_start_time)
#print(current_timestamp - terminal_start_time)
show_terminal = (current_timestamp - terminal_start_time) < cfg['terminal_feedback_time']
#print(show_terminal)
while (show_terminal):
# show feedback:
endCircle.draw()
myCircle.draw()
myWin.flip()
#print('flipped window, still in terminal feedback loop')
# collect data:
mousePos = cfg['mouse'].Pos()
current_pos = [mousePos[0], mousePos[1]]
current_timestamp = mousePos[2]
#print('got mouse position and time, still in terminal feedback loop')
timeArray.append(current_timestamp) # what is myTime?
mouseposXArray.append(current_pos[0])
mouseposYArray.append(current_pos[1] - startPos[1])
cursorposXArray.append(rotated_X)
cursorposYArray.append(rotated_Y - startPos[1])
#print('stored data in vectors, still in terminal feedback loop')
show_terminal = (current_timestamp - terminal_start_time) < cfg['terminal_feedback_time']
#print(show_terminal)
#print('show terminal loop ended...')
#print((current_timestamp - terminal_start_time) < cfg['terminal_feedback_time'])
#while ((core.getTime() - timer) > cfg['terminal_feedback_time']):
# myCircle.draw()
# if (cfg['poll_type'] == 'psychopy'):
# timeArray.append(core.getTime() - myTime)
# mouseposXArray.append(myMouse.getPos()[0])
# mouseposYArray.append(myMouse.getPos()[1] + startPos[1])
# elif (cfg['poll_type'] == 'x11'):
# timeArray.append(myMouse.Pos()[2] - myTime)
# mouseposXArray.append(myMouse.Pos()[0])
# mouseposYArray.append(myMouse.Pos()[1] + startPos[1])
# cursorposXArray.append(rotated_X)
# cursorposYArray.append(rotated_Y + startPos[1])
# #print('one flip statement') #### ??????????
# myWin.flip()
############################ DATA COLLECTION #################################
prev_timestamp = current_timestamp
prev_X = current_pos[0]
prev_Y = current_pos[1]
prev_X_cursor = circle_pos[0]
prev_Y_cursor = circle_pos[1]
# print 'mouse position: ', mousePos, 'circle position: ', circle_pos, 'screen_edge', -screen_edge
if (phase_1 == True and phase_2 == True and cfg['return_movement'] == False):
pass
else:
if phase_1 == True:
timeArray.append(current_timestamp)
mouseposXArray.append(current_pos[0]) #- (cfg['screen_on']*(cfg['screen_dimensions'][0]/2)))
mouseposYArray.append(current_pos[1]*cfg['flipscreen'] - startPos[1])
cursorposXArray.append(circle_pos[0])
cursorposYArray.append(circle_pos[1]*cfg['flipscreen'] - startPos[1])
#print('another flip statement?')
myWin.flip()
################################ PHASE 3 #####################################
# phase 3 is getting back to the home position
if (phase_1 == True and phase_2 == True):
# if ((cfg['trial_type'] == 'no_cursor' or (cfg['trial_type'] == 'cursor' and cfg['terminal_feedback'] == True) or (cfg['trial_type'] == 'error_clamp')) and get_dist(circle_pos, startPos) <= get_dist(startPos,endPos)/2):
# show_arrow = True
# show_arrowFill = True
#
# if ((cfg['trial_type'] == 'no_cursor' or (cfg['trial_type'] == 'cursor' and cfg['terminal_feedback'] == True) or (cfg['trial_type'] == 'error_clamp')) and get_dist(circle_pos, startPos) > get_dist(startPos, endPos)/2):
# show_arrow = False
# show_arrowFill = False
if ((cfg['trial_type'] == 'no_cursor' or (cfg['trial_type'] == 'cursor' and cfg['terminal_feedback'] == True) or (cfg['trial_type'] == 'error_clamp')) and get_dist(circle_pos, end_point) >= get_dist(startPos,endPos)/10):
# where is the angle for the arrow calculated?
# why isn't the arrow drawn now?
# everything regarding the arrow could be done in this if statement, right?
show_arrow = True
show_arrowFill = True
if ((cfg['trial_type'] == 'no_cursor' or (cfg['trial_type'] == 'cursor' and cfg['terminal_feedback'] == True) or (cfg['trial_type'] == 'error_clamp')) and get_dist(circle_pos, startPos) > 3*get_dist(startPos, endPos)/20):
show_cursor = False
if ((cfg['trial_type'] == 'no_cursor' or (cfg['trial_type'] == 'cursor' and cfg['terminal_feedback'] == True) or (cfg['trial_type'] == 'error_clamp')) and get_dist(circle_pos, startPos) <= 3*get_dist(startPos, endPos)/20):
show_cursor = True
if (cfg['trial_type'] == 'cursor' and get_dist(circle_pos, startPos) < dist_criterion and velocity < 35 and cfg['terminal_feedback'] == False):
timePos_dict['task_num'] = cfg['task_num']
timePos_dict['task_name'] = cfg['task_name']
timePos_dict['trial_num'] = cfg['trial_num']
timePos_dict['trial_type'] = cfg['trial_type']
timePos_dict['targetangle_deg'] = cfg['target_angle']
timePos_dict['rotation_angle'] = rot_dir*cfg['current_rotation_angle']
timePos_dict['homex_px'] = startPos[0]
timePos_dict['homey_px'] = startPos[1]*cfg['flipscreen'] - startPos[1]
timePos_dict['targetx_px'] = endPos[0]
timePos_dict['targety_px'] = endPos[1]*cfg['flipscreen'] - startPos[1]
timePos_dict['time_s'] = timeArray
timePos_dict['mousex_px'] = mouseposXArray
timePos_dict['mousey_px'] = mouseposYArray
timePos_dict['cursorx_px'] = cursorposXArray
timePos_dict['cursory_px'] = cursorposYArray
timePos_dict['terminalfeedback_bool'] = cfg['terminal_feedback']
timePos_dict['targetdistance_percmax'] = int(cfg['target_distance_ratio']*100)
return timePos_dict
elif ((cfg['trial_type'] == 'no_cursor' or cfg['trial_type'] == 'error_clamp' or (cfg['trial_type'] == 'cursor' and cfg['terminal_feedback'] == True)) and get_dist(circle_pos, startPos) <= 3*get_dist(startPos, endPos)/20):
show_cursor = True
if (get_dist(circle_pos, startPos) < cfg['circle_radius']):
# back at the home? no not yet... record data, and return it? why not just save it here instead of throwing around all that data from one function to another?
timePos_dict['task_num'] = cfg['task_num']
timePos_dict['task_name'] = cfg['task_name']
timePos_dict['trial_num'] = cfg['trial_num']
timePos_dict['trial_type'] = cfg['trial_type']
timePos_dict['targetangle_deg'] = cfg['target_angle']
timePos_dict['rotation_angle'] = rot_dir*cfg['current_rotation_angle']
timePos_dict['homex_px'] = startPos[0]
#print('start and end position should already be relative to home position, and stay that way?')
timePos_dict['homey_px'] = startPos[1]*cfg['flipscreen'] - startPos[1] #+ cfg['active_height']/2
timePos_dict['targetx_px'] = endPos[0]
timePos_dict['targety_px'] = endPos[1]*cfg['flipscreen'] - startPos[1] #+ cfg['active_height']/2
timePos_dict['time_s'] = timeArray
timePos_dict['mousex_px'] = mouseposXArray
timePos_dict['mousey_px'] = mouseposYArray
timePos_dict['cursorx_px'] = cursorposXArray
timePos_dict['cursory_px'] = cursorposYArray
timePos_dict['terminalfeedback_bool'] = cfg['terminal_feedback']
timePos_dict['targetdistance_percmax'] = int(cfg['target_distance_ratio']*100)
return timePos_dict
except:
pass # what went wrong if an exception occurred? why are we catching it? what should we do as backup?
def generate_rotation_list(initial, final, trials):
rotation_list = ndarray.tolist(((((1.0*final)-(1.0*initial))/trials)*arange(trials)) + initial)
return rotation_list
############################# RUN EXPERIMENT V2 ###############################
def run_experiment_2(fulls, participant, experiment = {}):
end_exp = DataFrame({})
task_save = DataFrame({})
running = deepcopy(experiment['experiment']) # why copy this? set up a window, a mouse object and add those, plus a task-index to your cfg, then simply loop through the tasks, and throw that to a run-task function?
settings = deepcopy(experiment['settings']) # same here...
# participant_state = deepcopy(experiment['participant'][participant]['state'])
cfg = {}
#### Generate seed ####
participant_seed = participant + settings['experiment_folder'] # where is this used? seeding the random-number generator *once* is sufficient, and cleaner
# the next if/else and two try/except statements should be combined in one function: 'createWorkspace' or something...
if experiment['settings']['flipscreen'] == True:
view_scale = [1, -1]
else:
view_scale = [1, 1]
try:
addWorkSpaceLimits(experiment['settings']['screen'], cfg)
except:
print "Exception adding workspace limits"
try:
Win = Window(cfg['screen_dimensions'],
winType=cfg['winType'],
colorSpace='rgb',
fullscr=fulls,
name='MousePosition', # why is it called that?
color=(-1, -1, -1),
units='pix',
screen=experiment['settings']['screen'],
viewScale=view_scale)
# Win._setCurrent()
# I would have expected a line like this:
# cfg['win'] = Window(...) with the cfg being the only location to have the Window object
# is this Window object created for nothing? It doesn't seem to be stored anywhere? Delete?
cfg['win'] = Win
except:
print "Exception creating Window"
# now set up the mouse object?
cfg['psyMouse'] = event.Mouse(visible = False, newPos = None, win = cfg['win'])
try:
class myMouse:
Xlib = CDLL("libX11.so.6")
display = Xlib.XOpenDisplay(None)
if display == 0: sys.exit(2)
w = Xlib.XRootWindow(display, c_int(0))
(root_id, child_id) = (c_uint32(), c_uint32())
(root_x, root_y, win_x, win_y) = (c_int(), c_int(), c_int(), c_int())
mask = c_uint()
width = cfg['main_screen_dimensions'][0]
height = cfg['main_screen_dimensions'][1]
x_offset = cfg['main_screen_offset'][0]
y_offset = cfg['main_screen_offset'][1]
def Pos(self):
ret = self.Xlib.XQueryPointer(self.display, c_uint32(self.w), byref(self.root_id), byref(self.child_id), byref(self.root_x), byref(self.root_y), byref(self.win_x), byref(self.win_y), byref(self.mask))
if ret == 0: sys.exit(1)
return [self.root_x.value - (self.width/2) - self.x_offset, -1 * (self.root_y.value - (self.height/2) - self.y_offset), time()] # c_int can't be used for regular Python math, their values are ints - and we return the current time
except:
# create an identically named object with identical behavior based on PsychoPy?
print ('not using xLib')
class myMouse:
def Pos(self):
#print('PsychoPy mouse')
[X,Y] = cfg['psyMouse'].getPos()
return [X,Y,time()]
cfg['mouse'] = myMouse()
### Configure visual feedback settings here
# it would also make sense to put this in a function, so that it's a separate unit
# I'd also put the creation of image stim objects for the icons in a separate function as well
try:
#print('creating two arrows?')
# is this black arrow used?
arrowFillVert = [(-1 , 1), (-1, -1),(-0.5, 0)]
arrowFill = ShapeStim(win=Win,
vertices=arrowFillVert,