""" Jankovic et al Experiment 1: TO RUN, REMOVE COMMENT SYMBOLS "#" FROM LINES 632-634 Examining alerting effect in compound task using Maljkovic & Nakayama (1994) PoP tasky Conditions: Colour Repeat x Flash Presence (2x2) Alerting stimulus: brightening of the screen background for 48 ms; 100 ms flash --> display SOA @author: nadja """ from psychopy import visual, event, core, misc, gui import random from datetime import datetime from win32api import GetSystemMetrics ##define an error that we can raise (from Bert) class ExpError(Exception): def __init__(self, value): self.value=value def __str__(self): return self.value # Class to define the Maljkovic & Nakayama diamond shaped items with either L or R corner cut off class DiamondItem(object): def __init__(self, Display, Trial, CutPos, Colour, ItemCenter = [0,0], isTarget = False): super(DiamondItem,self).__init__() self.Trial = Trial self.Display = Display self.isTarget = isTarget self.CutPos = CutPos self.ItemCenter = ItemCenter self.Colour = Colour self.Shape = visual.ShapeStim( win = self.Display.Trial.Block.Experiment.win, lineColor = [0,0,0], fillColor = [0,0,0], fillColorSpace = 'rgb', lineColorSpace = 'rgb', ##N&M: 1.0 x 1.0 deg of vis angle, with .14 deg cutoff (at 57 cm away, will be 1 cm by 1 cm, with .14 cm cutoff) ## On Lab screens: 1.0 cm x 1.0 cm, with .15 cut off (at 57 cm away) vertices = [(20,0),(0,20),(-10,10),(-10,-10),(0,-20)], pos = self.ItemCenter, closeShape = True ) ## Change shape colour based on Colour property. Only 2 options: Red or Green try: if self.Colour == "Red": self.Shape.lineColor = [1,-1,-1] self.Shape.fillColor = [1,-1,-1] elif self.Colour == "Green": self.Shape.fillColor = [-1,-.09,-1] self.Shape.lineColor = [-1,-.09,-1] elif self.Colour == None: pass except: raise ExpError("Error in setting DiamondItem's self.Colour. Acceptable arguments: 'Red', 'Green', None.") ## Change orientation based on Left or Right cut position. In degrees. try: if self.CutPos == "Left": self.Shape.ori = 0 elif self.CutPos == "Right": self.Shape.ori = 180 except: raise ExpError("Error in setting DiamondItem's self.CutPos. Acceptable Arguments: 'Left' or 'Right'") #draws the diamond object to the window def drawItem(self): self.Shape.draw() def targetPosition(self): if self.isTarget: return self.ItemCenter else: return None # defining the Display or the Stimulus Array class: 12 locations around an imaginary annulus. The target and two distractors populate 3 of those locations, and those 3 locations are always equidistant from each other (looks like 3 vertices of an equilateral triangle) class Display(object): def __init__(self, Trial, TargetColour, SetSize, TargetShapeID): super(Display,self).__init__() self.Trial = Trial self.TargetColour = TargetColour self.SetSize = SetSize self.TargetShapeID = TargetShapeID #TargetColour passed from Trial class - always set distractors to opposite colour if self.TargetColour == "Red": self.DistractorColour = "Green" elif self.TargetColour == "Green": self.DistractorColour = "Red" else: self.DistractorColour = None def createDisplay(self): Num_Positions = 12 ##hardcoded, but what Maljkovic & Nakayama (1994) used - 3 items, 12 possible positions around the array, always equidistant. Coded to draw on every 4th position. Position_List = range(Num_Positions) ##generates a list from which we will pick a multiplier for our angles Position_Multiplier = random.choice(Position_List) ##randomly chooses from list of [0,1,2,3,4,5,6,7,8,9,10,11]. We will multiply this by 360/Num_Positions, which is 30. Angles_List = [] ## N&M: ellipse: major axis (10.0), minor axis (8.1) - so, choosing 5 cm radius for current exp ## circular array: Radius = 5 deg of visual angle (diameter = 10) Array_Radius = 184 ## 184 pixels on Spalek Main Lab computers = 5 cm = 5 deg vis angle at 57 cm away for i in range(self.SetSize): angle = (i*360/self.SetSize) + (Position_Multiplier * (360/Num_Positions)) ## n * 30 deg added to all calculated angles. In effect, this changes the orientation of the display while keeping items equidistant Angles_List.append(angle) random.shuffle(Angles_List) self.Display_Items_List = [] for i in range(len(Angles_List)): ## Chooses first iteration to be the Target: if i == 0: [surr_x, surr_y] = misc.pol2cart( Angles_List[i], Array_Radius ) target = DiamondItem(Display = self, Trial = self.Trial, CutPos = self.Trial.TargetShapeID, Colour = self.Trial.TargetColour, ItemCenter = [surr_x, surr_y], isTarget = True) self.Display_Items_List.append(target) ## Choose following iterations to be the distractors: else: [surr_x, surr_y] = misc.pol2cart( Angles_List[i], Array_Radius ) ## randomly chooses the cut position for the distractors: Distractor_ShapeID = random.choice(["Right","Left"]) distractor = DiamondItem(Display = self, Trial = self.Trial, CutPos = Distractor_ShapeID, Colour = self.DistractorColour, ItemCenter = [surr_x, surr_y]) self.Display_Items_List.append(distractor) #draw the display to the window def drawDisplay(self): for i in range(len(self.Display_Items_List)): self.Display_Items_List[i].drawItem() class Trial(object): TrialNo = 0 def __init__(self, Experiment, CounterBalanceOpt, Block, Repeat, PreviousTargetColour, TargetColour, TargetShapeID, Flash, SetSize, Catch = False): super(Trial, self).__init__() self.Experiment = Experiment self.CounterBalanceOpt = CounterBalanceOpt self.Block = Block self.Repeat = Repeat self.PreviousTargetColour = PreviousTargetColour self.TargetColour = TargetColour self.TargetShapeID = TargetShapeID self.Flash = Flash self.SetSize = SetSize self.Catch = Catch self.FixCross = visual.ShapeStim( win = self.Block.Experiment.win, vertices=([0,-5], [0,5], [0,0], [-5,0], [5,0]), lineWidth=2, units="pix", closeShape=False, lineColor=[-1,-1,-1], # black autoDraw= True #set to False at end of trial (or else will have a fix cross during inter-block screens) ) # Must instantiate the trial display, and call its createDisplay() method def createTrialDisplay(self): self.TrialDisplay = Display(Trial = self, TargetColour = self.TargetColour, SetSize = self.SetSize, TargetShapeID = self.TargetShapeID) self.TrialDisplay.createDisplay() def runTrial(self): Flash_Duration = .044 ## want to approximate flash lasting 50 msec. Best we can do is 48 msec. To be safe from adding an extra frame, subtract from 48 half of the refresh rate Flash_Jitter = random.randint(8,12)/10.0 ## delivers a random floating point number between 1.9 and 2.4 (to jitter the period before the start of the trial and the Flash. ISI_Duration = .044 ##ISI duration of "50" msec (coded as 48; will be 56 with incorporated win flips) self.FixCross.draw() ## self.FixCross set to autoDraw True timer = core.Clock() self.Block.Experiment.win.flip() timer.reset() core.wait(Flash_Jitter) ## screen background changes to white for Flash stimulus if self.Flash == "Flash": self.Block.Experiment.win.color = [1,1,1] self.Block.Experiment.win.flip() timer.reset() while timer.getTime() < Flash_Duration: pass self.Block.Experiment.win.color = [0,0,0] self.Block.Experiment.win.flip() ## screen remains gray (with fixcross) for same duration else: timer.reset() while timer.getTime() < Flash_Duration: pass ## ISI between the Flash (or absence of the flash) and the display onset timer.reset() while timer.getTime() < ISI_Duration: pass ## Present the stimulus display self.TrialDisplay.drawDisplay() self.Block.Experiment.win.flip() timer.reset() # Collect response to display and record time self.keys = event.waitKeys( maxWait = 2, keyList = ['z', 'm'], timeStamped = timer ) self.FixCross.autoDraw = False def Response(self): if self.keys: KeyPress = self.keys[0][0] ## to get the actual key pressed: index 0 of self.keys tuple else: KeyPress = "None" return KeyPress def Acc(self): if self.TargetShapeID == "Left": if self.keys: if self.keys[0][0] == "z": return 1 elif self.keys[0][0] == "m": return 0 else: return "None" if self.TargetShapeID == "Right": if self.keys: if self.keys[0][0] == "z": return 0 elif self.keys[0][0] == "m": return 1 else: return "None" def RT(self): if self.keys: ReacTime = self.keys[0][1] else: ReacTime = -1 return ReacTime ## str function to write to the datafile def __str__(self): ## Add 1 to both blockNumber and TrialNo, because these start at 0 return str(self.Block.blockNumber()+1)+","\ +str(self.TrialNo+1)+","\ +str(self.Repeat)+","\ +str(self.Flash)+","\ +str(self.TargetColour)+","\ +str(self.PreviousTargetColour)+","\ +str(self.TargetShapeID)+","\ +str(self.Response())+"," \ +str(self.Acc()) + ","\ +str(self.RT()) ## for the first line of the datafile. def DataHeader(self): return "BlockNo,TrialNo,Repeat,Flash,TargetColour,PreviousTargetColour,TargetShapeID,Response,Acc,RT" def saveTrial(self): try: ## saves the trial's DataHeader() and __str__() if self.Block.blockNumber() == 0 and self.TrialNo == 0: ## this is the first trial of the first block: write the dataheader to the outputfile self.Block.Experiment.DataFile.write(self.DataHeader()+'\n') ## write the trial's data to the file: self.Block.Experiment.DataFile.write(self.__str__()+'\n') except: raise ExpError('Trial object savetrial() method: unable to save trial') class Block(object): def __init__(self, Experiment, CounterBalanceOpt, TrialsPerBlock, isPracBlock = False): super(Block, self).__init__() self.Experiment = Experiment self.CounterBalanceOpt = CounterBalanceOpt self.TrialsPerBlock = TrialsPerBlock self.isPracBlock = isPracBlock self.Trial_Types = [] self.Trials = [] ##Trial Type Codes: 2x2x2 = 8 trial types ## 1) NoRepeat/Repeat = self.Trial_Types[0] ## 2) NoFlash/Flash = self.Trial_Types[1] ## 3) CutLeft/CutRight = self.Trial_Types[2] ## Populates self.Trial_Types list def buildTrialTypes(self): ##List of lists: 8 types of trials Types_List = [["NoRepeat","NoFlash","Left"],["NoRepeat","NoFlash","Right"],["NoRepeat","Flash","Left"],["NoRepeat","Flash","Right"],["Repeat","NoFlash","Left"],["Repeat","NoFlash","Right"],["Repeat","Flash","Left"],["Repeat","Flash","Right"]] TrialsPerType = int(self.TrialsPerBlock/(len(Types_List))) ##96 Trials per block / 8 Trial types = 12 trials per type for i in range(TrialsPerType): for j in range(len(Types_List)): self.Trial_Types.append(Types_List[j]) random.shuffle(self.Trial_Types) ## Method to check the levels of colours in each block. Uses self.Trial_Types list def checkColours(self): checklist = [] for i in range(len(self.Trial_Types)): if i == 0: chk = "red" ## assign first colour as red, just to check ratio of colours (will not necessarily be actual colour assigned to that particular trial) checklist.append(chk) else: try: if self.Trial_Types[i][0] == "Repeat": ## if a repeat trial, assign chk colour to chk-1 colour chk = checklist[i-1] checklist.append(chk) elif self.Trial_Types[i][0] == "NoRepeat": ## if a no-repeat trial, assign chk to opposite of chk-1 if checklist[i-1] == "red": chk = "green" elif checklist[i-1] == "green": chk = "red" checklist.append(chk) except: raise ExpError("Error in Block.checkcolours(). Could not assign colours based on Trial Types.") NumReds = 0 NumGreens = 0 for j in range(len(checklist)): if checklist[j] == "red": NumReds += 1 elif checklist[j] == "green": NumGreens+= 1 Colour_CheckList = [NumReds, NumGreens] return Colour_CheckList ## Method to balance the relative colours obtained from checkColours(). # Right now, it just takes the first self.Trial_Types list generated that fits within the acceptable range: 48% < NumReds < 52%. def balanceColours(self): NumLoops = 100 for i in range(NumLoops): colours = self.checkColours() if colours[0] > (self.TrialsPerBlock*.48) and colours[0] < (self.TrialsPerBlock*.52): pass else: random.shuffle(self.Trial_Types) ## Method to create trial objects based on their trial type. Certain parameters (colour) to be set in setColours() method def setTrials(self): for i in range(len(self.Trial_Types)): ## First Trial: not a repeat or a no-repeat. Set TargetColour randomly. if i == 0: colours = ["Red","Green"] random.shuffle(colours) self.Trials.append(Trial( Experiment = self.Experiment, Block = self, CounterBalanceOpt = self.CounterBalanceOpt, Repeat = None, PreviousTargetColour = None, TargetColour = colours[0], Flash = self.Trial_Types[i][1], TargetShapeID = self.Trial_Types[i][2], SetSize = 3)) ## Remaining trials: Create trials, leaving the colours to be set in block's setColours() method. else: self.Trials.append(Trial( Experiment = self.Experiment, Block = self, CounterBalanceOpt = self.CounterBalanceOpt, Repeat = self.Trial_Types[i][0], PreviousTargetColour = None, TargetColour = None, Flash = self.Trial_Types[i][1], TargetShapeID = self.Trial_Types[i][2], SetSize = 3)) ## Gets the colour of the Target in the previous Trial, and based on whether it is a Repeat or a NoRepeat trial, it sets the current Target colour accordingly def setColours(self): for i in range(len(self.Trials)): ## First trial in block, so we pass (we 'flip a coin' to choose the first colour in setTrials() if i == 0: pass else: try: prevTC = self.Trials[i-1].TargetColour self.Trials[i].PreviousTargetColour = prevTC if self.Trials[i].Repeat == "Repeat": self.Trials[i].TargetColour = prevTC elif self.Trials[i].Repeat == "NoRepeat": if prevTC == "Red": self.Trials[i].TargetColour = "Green" elif prevTC == "Green": self.Trials[i].TargetColour = "Red" except: raise ExpError("Error in Block.setColours. Cannot set target colours based on previous target colour") ## Create the display during the Block's init AFTER you have created your trial objects, by calling the trial's createTrialDisplay() method for t in self.Trials: t.createTrialDisplay() #If isPracTrial == True, then this block in just for practice, and the data will not be recorded. def createPracTrials(self): for pt in range(self.TrialsPerBlock): clr = ["Red","Green"] random.shuffle(clr) flsh = ["Flash","NoFlash"] random.shuffle(flsh) trgID = ["Left","Right"] random.shuffle(trgID) self.Trials.append(Trial( Experiment = self.Experiment, Block = self, CounterBalanceOpt = self.CounterBalanceOpt, Repeat = None, PreviousTargetColour = None, TargetColour = clr[0], Flash = flsh[0], TargetShapeID = trgID[0], SetSize = 3)) for t in self.Trials: t.createTrialDisplay() def runTrials(self): trialcounter = 0 for t in self.Trials: t.TrialNo = trialcounter t.runTrial() trialcounter+=1 if self.isPracBlock == False: self.saveTrials() def saveTrials(self): for t in self.Trials: t.saveTrial() def blockNumber(self): # ## find this block in parent experiment's collection: try: idx=self.Experiment.Blocks.index(self) ## built in pythod method: list.index() return idx except: raise ExpError('Block blockNumber() method: unable to index block object') def initBlock(self): if self.isPracBlock == False: self.buildTrialTypes() self.balanceColours() self.setTrials() self.setColours() elif self.isPracBlock == True: self.createPracTrials() class Experiment(object): def __init__(self, NumBlocks, TrialsPerBlock, NumPracTrials = 12, screen = 0, datafile = ""): super(Experiment, self).__init__() self.NumBlocks = NumBlocks self.TrialsPerBlock = TrialsPerBlock self.Blocks = [] expGUI = gui.Dlg(title="Experiment", screen = 0) expGUI.addField("ParticipantID:" , "0") expGUI.addField("Session:", "1") configinfo = expGUI.show() if expGUI.OK: self.ParticipantID = configinfo[0] self.SessionNo = configinfo[1] else: print ("Experiment.init(): Cancelled by user") core.quit() demoGUI = gui.Dlg(title = "Demographic Information", screen = 0) demoGUI.addField("Age:", "0") demoGUI.addField("Gender:", choices = ["Female", "Male", "Other"]) demoGUI.addField("Dominant Hand:", choices = ["Right", "Left"]) demoinfo = demoGUI.show() if demoGUI.OK: self.Age = demoinfo[0] self.Gender = demoinfo[1] self.Hand = demoinfo[2] self.DemoHeader = "Age, Gender, DominantHand, \n" self.DemoEntered = str(self.Age) + "," + str(self.Gender) + "," + str(self.Hand) + "\n" + "\n" ## If nothing passed as datafile argument, then a new .csv file will be opened to write to if datafile == "": d = datetime.now() ## example filename = 20181112188_s999_1.csv filename = str(d.year)+str(d.month)+str(d.day)+str(d.hour)+str(d.minute)+"_"+"s"+str(self.ParticipantID)+"_"+str(self.SessionNo)+".csv" self.DataFile = open(filename, 'w') ## Write demographic information to file self.DataFile.write(self.DemoHeader) self.DataFile.write(self.DemoEntered) ## If filename is passed to datafile argument, then the file will be opened to append to else: self.DataFile = open(datafile, 'a') self.win = visual.Window( size = [GetSystemMetrics(0), GetSystemMetrics(1)], ## How to fit to all screens. from win32api import GetSystemMetrics ; width = GetSystemMetics(0), height = GetSystemMetrics(1) units = 'pix', fullscr = True, color = [0,0,0], ## grey allowGUI = False, ## hide mouse waitBlanking = True ) def drawInstructions(self): ## Create and draw Instructions screen from file. FILE MUST BE IN SAME FOLDER AS THE PROGRAM! try: self.InstructionsSlide = visual.ImageStim( win = self.win, image = "Instructions.bmp" ) ## Display error text instead of 'Instructions.bmp' if instructions file not found except IOError: self.InstructionsSlide = visual.TextStim( win = self.win, color = (1,1,1), text = "Oops! Unable to display Instructions. Press space to continue", font = "Calibri", height = 40) self.InstructionsSlide.draw() ## Create BlockScreen ("Block # / press space to continue") in between each block. def drawBlockScreen(self, BlockNo): self.BlockScreen = visual.TextStim( win = self.win, color = (1,1,1), pos = [0, 30], text = "Block " + str(BlockNo + 1), font = "Calibri", height = 40) self.pressSpace = visual.TextStim( win = self.win, color = (1,1,1), pos = [0, -30], text = "(press the spacebar to continue)", font = "Calibri", height = 30) self.BlockScreen.draw() self.pressSpace.draw() def drawPracStartScreen(self): self.PracStartScreen = visual.TextStim( win = self.win, color = (1,1,1), pos = [0, 30], text = "About to start the practice trials.", font = "Calibri", height = 30) self.pressSpaceBegin = visual.TextStim( win = self.win, color = (1,1,1), pos = [0, -30], text = "(press the spacebar to begin)", font = "Calibri", height = 25) self.PracStartScreen.draw() self.pressSpaceBegin.draw() def drawPracEndScreen(self): self.PracEndScreen = visual.TextStim( win = self.win, color = (1,1,1), pos = [0, 30], text = "That's the end of the practice trials. Ready to begin the experiment?", font = "Calibri", height = 30) self.pressSpaceExp = visual.TextStim( win = self.win, color = (1,1,1), pos = [0, -40], text = "(press the spacebar to start the experiment)", font = "Calibri", height = 25) self.PracEndScreen.draw() self.pressSpaceExp.draw() def drawDebrief(self): ## Create debrief slide imagestim object try: self.DebriefSlide = visual.ImageStim( win = self.win, image = "Debrief.bmp" ## Debrief.bmp MUST BE IN THE SAME FOLDER AS THIS PROGRAM ) ## Display textstim error message instead of 'Debrief.bmp' if file not found except IOError: self.DebriefSlide = visual.TextStim( win = self.win, color = (1,1,1), text = "Oops! Unable to display Debrief. Press spacebar to exit", font = "Calibri", height = 40) self.DebriefSlide.draw() def createPracBlock(self): self.PracBlock = Block(Experiment = self, CounterBalanceOpt = None, TrialsPerBlock = 12, isPracBlock = True) self.PracBlock.initBlock() def createBlocks(self): for i in range(self.NumBlocks): self.Blocks.append(Block(Experiment = self, CounterBalanceOpt = None, TrialsPerBlock = self.TrialsPerBlock)) random.shuffle(self.Blocks) def run(self): self.drawInstructions() self.win.flip() self.createPracBlock() self.createBlocks() event.waitKeys(keyList =['space']) self.win.flip() self.drawPracStartScreen() self.win.flip() event.waitKeys(keyList =['space']) self.PracBlock.runTrials() self.win.flip() self.drawPracEndScreen() self.win.flip() event.waitKeys(keyList=['space']) self.win.flip() self.runAllBlocks() self.drawDebrief() self.win.flip() event.waitKeys() def runAllBlocks(self): for b in range(len(self.Blocks)): self.drawBlockScreen(BlockNo = b) ## draws screen that displays block number & asks for keypress (spacebar) to continue self.win.flip() self.Blocks[b].initBlock() ## initialize the block while BlockScreen is being displayed event.waitKeys(keyList = ['space']) self.win.flip() self.runBlock(BlockNo = b) ## run a single block of trials ## Run one block of trials def runBlock(self, BlockNo=0): self.Blocks[BlockNo].runTrials() ## closes the datafile and the window; DO NOT FORGET TO CALL AT END OF EXPERIMENT def cleanup(self): self.DataFile.close() self.win.close() ## ~ ~ ~ * ~ ~ ~ *~ ~ ~ * ~ ~ ~ *~ ~ ~ * ~ ~ ~ *~ ~ ~ * ~ ~ ~ * CREATE & RUN EXPERIMENT BELOW ~ ~ ~ * ~ ~ ~ *~ ~ ~ * ~ ~ ~ *~ ~ ~ * ~ ~ ~ *~ ~ ~ * ~ ~ ~ *## ## To test, run this bit - there is no crash-out option mid-experiment for this one, so just run one block of a few trials. #test = Experiment(NumBlocks = 1, TrialsPerBlock = 16) #test.run() #test.cleanup() ## RUN THIS FOR MAIN EXP: Number of Blocks = 8, Trials Per Block = 96, No Counterbalancing Option. Total Number of trials = 768. Each experimental condition = 192 trials (Flash/NoFlash x Repeat/NoRepeat) #EXP = Experiment(NumBlocks = 8, TrialsPerBlock = 96) #EXP.run() #EXP.cleanup()