"""An experiment to cause eye saccades (movements) or smooth pursuits Author - Michael Tan (mtan30@uic.edu) Adapted from: https://github.com/colinquirk/PsychopyChangeDetection Note: this code relies on the Colin Quirk templateexperiments module. You can get it from https://github.com/colinquirk/templateexperiments and either put it in the same folder as this code or give the path to psychopy in the preferences. """ from __future__ import division from __future__ import print_function import os import sys import errno import math import warnings import json import random import numpy as np import psychopy.core import psychopy.event import psychopy.visual import psychopy.hardware.joystick import psychopy.sound import psychopy # noqa:F401 import eyelinker import template distance_to_monitor = 90 # Things you probably don't need to change, but can if you want to exp_name = 'SaccadePursuit' data_fields = [ 'Subject', 'Condition', 'Block', 'Trial', 'Timestamp', 'TrialType', 'Duration', 'Frequency', 'Locations', ] def warning_on_one_line(message, category, filename, lineno, file=None, line=None): return ' %s:%s: %s:%s' % (filename, lineno, category.__name__, message) # This is the logic that runs the experiment # Change anything below this comment at your own risk class SPtask(template.BaseExperiment): """The class that runs the Saccade Pursuit experiment. Parameters: Additional keyword arguments are sent to template.BaseExperiment(). Methods: """ def __init__(self, pursuit_time, stim_color, pursuit_frequencies, saccade_distance, saccade_time, stimulus_size, saccade_fixation_color, fixation_size, pursuit_distance, necker_time, necker_color, necker_scale, necker_bg_color, necker_file, rivalry_file1, rivalry_file2, fixation_trial_time, rivalry_time, rivalry_scale,rivalry_border_color, rivalry_border_width, rivalry_distance, new_trial_sound, questionaire_dict=None, questionaire_order=None, **kwargs): self.new_trial_sound = new_trial_sound # Pursuit self.pursuit_time = pursuit_time self.pursuit_distance = pursuit_distance self.pursuit_frequencies = pursuit_frequencies # Saccade self.stim_color = stim_color self.saccade_time = saccade_time self.stimulus_size = stimulus_size self.saccade_distance = saccade_distance self.saccade_fixation_color = saccade_fixation_color # Fixation self.fixation_size = fixation_size self.fixation_trial_time = fixation_trial_time # Necker self.necker_time = necker_time self.necker_color = necker_color self.necker_scale = necker_scale self.necker_bg_color = necker_bg_color self.necker_file = necker_file # Rivalry self.rivalry_time = rivalry_time self.rivalry_scale = rivalry_scale self.rivalry_file1 = rivalry_file1 self.rivalry_file2 = rivalry_file2 self.rivalry_border_color = rivalry_border_color self.rivalry_border_width = rivalry_border_width self.rivalry_distance = rivalry_distance self.questionaire_dict = questionaire_dict super(SPtask, self).__init__(**kwargs) def chdir(self): """Changes the directory to where the data will be saved. """ try: os.makedirs(self.data_directory) except OSError as e: if e.errno != errno.EEXIST: raise os.chdir(self.data_directory) def make_block(self, condition, numTrials): """Makes a block of trials. Returns a shuffled list of trials created by self.make_trial. """ trial_list = [] saccade_locations = list(range(-self.saccade_distance,0,1)) for x in range(self.saccade_distance): saccade_locations.append(x+1) random.shuffle(saccade_locations) counter = 0 for num_trials in range(numTrials): if condition == 'Pursuit': counter1 = 0 for freq in self.pursuit_frequencies: trial = self.make_trial(self.pursuit_time[counter1], freq, condition) trial_list.append(trial) counter1 += 1 elif condition == 'Necker': trial = self.make_trial(self.necker_time, 1, condition) trial_list.append(trial) elif condition == 'Fixation': trial = self.make_trial(self.fixation_trial_time,1,condition) trial_list.append(trial) elif condition == 'Rivalry': trial = self.make_trial(self.rivalry_time,1,condition) trial_list.append(trial) else: for loc in saccade_locations: trial = self.make_trial(round(random.uniform(1,self.saccade_time),3), loc, condition) trial_list.append(trial) counter += 1 if not condition=='Pursuit': random.shuffle(trial_list) return trial_list def make_trial(self, trial_time, pursuit_frequency, trial_type): """Creates a single trial dict. A helper function for self.make_block. Returns the trial dict. Parameters: set_size -- The number of stimuli for this trial. trial_type -- Whether this trial is same or different. """ locs = [pursuit_frequency,0] trial = { 'locations': locs, 'trial_time': trial_time, 'pursuit_frequency': pursuit_frequency, 'trial_type': trial_type } return trial def display_break(self): """Displays a break screen in between blocks. """ break_text = 'Please take a short break. Press any key to continue.' self.display_text_screen(text=break_text, bg_color=[0, 0, 0]) def display_fixation(self, wait_time, necker_bg_color=[-1,-1,-1]): """Displays a fixation cross. A helper function for self.run_trial. Parameters: wait_time -- The amount of time the fixation should be displayed for. """ self.experiment_window.color = necker_bg_color stim = psychopy.visual.TextStim( self.experiment_window, text='+', color=self.stim_color, height=self.fixation_size, units='deg') for frameN in range(int(round(wait_time*60))): stim.draw() self.experiment_window.flip() def display_saccade(self, coordinates, stim_time): """Displays the stimuli. A helper function for self.run_saccade_trial. Parameters: coordinates -- A list of coordinates (list of x and y value) describing where the stimuli should be displayed. colors -- A list of colors describing what should be drawn at each coordinate. """ color = self.stim_color stim = psychopy.visual.Circle( self.experiment_window, radius=self.stimulus_size/2, pos=coordinates, fillColor=color, lineColor=color, units='deg') fixation = psychopy.visual.TextStim( self.experiment_window, text='+', color=self.saccade_fixation_color, height=self.fixation_size, units='deg') stimList = [] temp = list(range(-self.saccade_distance,self.saccade_distance,1)) for circleN in temp: if circleN < 0: tempCoord = circleN else: tempCoord = circleN+1 tempStim = psychopy.visual.Circle( self.experiment_window, radius=self.stimulus_size/2, pos=[tempCoord,0], fillColor=self.saccade_dot_color, lineColor=self.saccade_dot_color, units='deg') stimList.append(tempStim) for frameN in range(int(round(stim_time*60))): for s in stimList: s.draw() fixation.draw() stim.draw() self.experiment_window.flip() def display_saccade_fixation(self, stim_time): """Displays the stimuli. A helper function for self.run_saccade_trial. Parameters: coordinates -- A list of coordinates (list of x and y value) describing where the stimuli should be displayed. colors -- A list of colors describing what should be drawn at each coordinate. """ fixation = psychopy.visual.TextStim( self.experiment_window, text='+', color=self.stim_color, height=self.fixation_size, units='deg') stimList = [] temp = list(range(-self.saccade_distance,self.saccade_distance,1)) for circleN in temp: if circleN < 0: tempCoord = circleN else: tempCoord = circleN+1 tempStim = psychopy.visual.Circle( self.experiment_window, radius=self.stimulus_size/2, pos=[tempCoord,0], fillColor=self.saccade_dot_color, lineColor=self.saccade_dot_color, units='deg') stimList.append(tempStim) for frameN in range(int(round(stim_time*60))): for s in stimList: s.draw() fixation.draw() self.experiment_window.flip() def display_pursuit(self, stim_frequency, stim_time): """Displays a pursuit stimululus. A helper function for self.run_pursuit_trial. Parameters: coordinates -- A list of coordinates (list of x and y value) describing where the start point of the stimulus. color -- A color describing what should be drawn. """ color = self.stim_color Xposition = [0] num_frames_per_second = 60 counter = 0 if not isinstance(stim_frequency, list): stim_frequency = [stim_frequency] if not isinstance(stim_time, list): stim_time = [stim_time] for freq in stim_frequency: stim_frames = int(round(stim_time[counter]*num_frames_per_second)) for time in range(stim_frames): Xposition.append(math.sin(freq*math.radians((time+1)*360/num_frames_per_second))*self.pursuit_distance) counter += 1 stim = psychopy.visual.Circle( self.experiment_window, radius=self.stimulus_size/2, pos=(0,0), fillColor=color, lineColor=color, units='deg') # Draw circle for 0.5s prior to moving for frameN in range(30): stim.draw() self.experiment_window.flip() for Xpos in Xposition: stim.pos = (Xpos,0) stim.draw() self.experiment_window.flip() # Draw circle for 0.5s after moving for frameN in range(30): stim.draw() self.experiment_window.flip() def display_necker(self, stim_time, tracker): self.experiment_window.color = self.necker_bg_color stim = psychopy.visual.ImageStim( self.experiment_window, image=self.necker_file) stim.size *= self.necker_scale stim.setColor(self.necker_color, 'rgb255') responses = [] for frameN in range(stim_time*60): response = psychopy.event.getKeys() if response: if response[0]=='1' or response[0]=='3': direction = 'Left' elif response[0]=='2' or response[0]=='4': direction = 'Right' else: direction = 'Center' responses.append((response[0],direction,psychopy.core.getAbsTime())) if tracker: #print(response[0]) tracker.send_message(['Direction: %s, Key: %s' % (direction, response[0])]) stim.draw() self.experiment_window.flip() #print(responses) return responses def display_rivalry(self, stim_time, tracker): color = self.rivalry_border_color lineSize = self.rivalry_border_width imDist = self.rivalry_distance stim1 = psychopy.visual.ImageStim( self.experiment_window, image=self.rivalry_file1, pos=(-imDist,0)) stim1.size *= self.rivalry_scale stim2 = psychopy.visual.ImageStim( self.experiment_window, image=self.rivalry_file2, pos=(imDist,0)) stim2.size *= self.rivalry_scale borderScale = stim1.size[0]*0.4 borderWidth1 = stim1.size[0]+borderScale borderHeight1 = stim1.size[1]+borderScale borderWidth2 = stim2.size[0]+borderScale borderHeight2 = stim2.size[1]+borderScale stimBorder1 = psychopy.visual.Rect( self.experiment_window, width=borderWidth1, height=borderHeight1, pos=stim1.pos, lineWidth=lineSize, lineColor=color, units='deg') stimBorder2 = psychopy.visual.Rect( self.experiment_window, width=borderWidth2, height=borderHeight2, pos=stim2.pos, lineWidth=lineSize, lineColor=color, units='deg') stimFix1_top = psychopy.visual.Line( self.experiment_window, start=(stim1.pos[0],stim1.pos[1]+borderHeight1/2), end=(stim1.pos[0],stim1.pos[1]+stim1.size[1]/1.9), units='deg', lineColor=color, lineWidth=lineSize) stimFix1_left = psychopy.visual.Line( self.experiment_window, start=(stim1.pos[0]-borderWidth1/2,stim1.pos[1]), end=(stim1.pos[0]-stim1.size[0]/1.9,stim1.pos[1]), units='deg', lineColor=color, lineWidth=lineSize) stimFix2_bottom = psychopy.visual.Line( self.experiment_window, start=(stim2.pos[0],stim2.pos[1]-borderHeight2/2), end=(stim2.pos[0],stim2.pos[1]-stim2.size[1]/1.9), units='deg', lineColor=color, lineWidth=lineSize) stimFix2_right = psychopy.visual.Line( self.experiment_window, start=(stim2.pos[0]+borderWidth2/2,stim2.pos[1]), end=(stim2.pos[0]+stim2.size[0]/1.9,stim2.pos[1]), units='deg', lineColor=color, lineWidth=lineSize) responses = [] for frameN in range(stim_time*60): response = psychopy.event.getKeys() if response: if response[0]=='1' or response[0]=='3': direction = 'Left' elif response[0]=='2' or response[0]=='4': direction = 'Right' else: direction = 'Center' responses.append((response[0],direction,psychopy.core.getAbsTime())) if tracker: tracker.send_message(['Direction: %s' % direction]) stim1.draw() stim2.draw() stimBorder1.draw() stimBorder2.draw() stimFix1_top.draw() stimFix1_left.draw() stimFix2_bottom.draw() stimFix2_right.draw() self.experiment_window.flip() return responses def send_data(self, data): """Updates the experiment data with the information from the last trial. This function is seperated from run_trial to allow additional information to be added afterwards. Parameters: data -- A dict where keys exist in data_fields and values are to be saved. """ self.update_experiment_data([data]) def run_trial(self, trial, block_num, trial_num, tracker=[]): freq = [0] if trial['trial_type']=='Necker': freq = self.display_necker(trial['trial_time'], tracker) elif trial['trial_type']=='Pursuit': self.display_pursuit(trial['pursuit_frequency'],trial['trial_time']) psychopy.sound.Sound(self.new_trial_sound).play() freq = trial['pursuit_frequency'] elif trial['trial_type']=='Fixation': self.display_fixation(trial['trial_time']) elif trial['trial_type']=='Rivalry': freq = self.display_rivalry(trial['trial_time'], tracker) else: self.display_saccade(trial['locations'], trial['trial_time']) psychopy.sound.Sound(self.new_trial_sound).play() data = { 'Subject': self.experiment_info['Subject Identifier'], 'Block': block_num, 'Trial': trial_num, 'Timestamp': psychopy.core.getAbsTime(), 'TrialType': trial['trial_type'], 'Duration': trial['trial_time'], 'Frequency': freq, 'Locations': trial['locations'] } return data # If you call this script directly, directs you to correct file if __name__ == '__main__': warnings.showwarning( 'Please run SaccadePursuitEyeTracking.py', Warning,'SaccadePursuit.py',414) warnings.formatwarning = warning_on_one_line