"""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 # 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', ] gender_options = [ 'Male', 'Female', 'Other/Choose Not To Respond', ] # Add additional questions here questionaire_dict = { 'Age': 0, 'Gender': gender_options, } 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, questionaire_dict=questionaire_dict, **kwargs): # 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': for freq in self.pursuit_frequencies: trial = self.make_trial(self.pursuit_time, freq, condition) trial_list.append(trial) 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 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 space 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_fixation_color, lineColor=self.saccade_fixation_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_fixation_color, lineColor=self.saccade_fixation_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] 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' % direction]) 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']) 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']) 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