Experiment (sometimes) breaks down on about the 430th of 440 trials

Hi all,

I’m doing an experiment in Psychopy where an image is presented for a very brief time (about 26 ms). This happens for 440 trials. 50% of the trials are masked with a set of four different masks also presented for 26 ms each. After each image, subjects have to state their awareness about the image they have just seen.

This all works pretty well, only that for 2 out of 5 participants, the script crashed at about the 430th trial. There is no error message in the psychopy output, but the window in which the stimuli are presented becomes unresponsive.

I really have no clue why it works at a time and at the other it crashes, so I would welcome every hint.

The relevant code can be found below (I’m sorry for the amount of code, im unsure which part could cause the troubles i’m experiencing).

Thank you in advance,
Mathis

#image stimuli and masks are preloaded
images = [] # need to start with an empty list

filenames = expFile["filename"]
filenames_path = path_images + expFile["filename"]

for file in filenames_path: #create an image stimulus from each file, and store it in the list
    images.append(visual.ImageStim(win=win, image=file)) 

#bind filename of each image to the prepared image stimulus to be able to decide on masking later
dict2 = {}
for i in range (len(filenames)):
    dict2.update({filenames[i]:images[i]})

#the same procedure for masks (omitted here)

#decide, which trials are going to be masked
stimList = expFile['filename']
memorable = expFile['memo'] # 0 = memorable, 1 = not
sceneCategory = expFile['sceneCat']
category = expFile['category']
print(stimList)
#shuffle stimuli while holding association with memorability-info upright
allinfo = zip(stimList, memorable, sceneCategory, category) #put info together; die zip-Fkt. so zu nutzen geht nur in Python 2.7!!!
random.shuffle(allinfo) #shuffle
stimList, memorable, sceneCategory, category = zip(*allinfo) #decompose again
print("cut")
print(stimList)

#Alle Bilder nach high/low mem. und indoor/outdoor aufteilen
high_i = []
high_o = []
low_i = []
low_o = []
for i in range (len(stimList)):
    if memorable[i] == "high":
        if sceneCategory[i] == "indoor":
            high_i.append(stimList[i])
        if sceneCategory[i] == "outdoor":
            high_o.append(stimList[i])
    if memorable[i] == "low":
        if sceneCategory[i] == "indoor":
            low_i.append(stimList[i])
        if sceneCategory[i] == "outdoor":
            low_o.append(stimList[i])

#Sicherheitsalber nochmal durchmischen
conditionsList = [high_i, high_o, low_i, low_o]
for i in range (len(conditionsList)):
    random.shuffle(conditionsList[i])

#Und dann von jedem Viertel die Hälfte nehmen --> Dann sollten es je 50% von indoor/outdoor und 50% von high/low mem. sein
maskingTrue = []
for i in range (len(conditionsList)):
    for j in range (len(stimList)/8):
        maskingTrue.append(conditionsList[i][j])

# create the process that will run in the background polling devices
io=launchHubServer()

# some default devices have been created that can now be used
display = io.devices.display
keyboard_io = io.devices.keyboard
mouse=io.devices.mouse
          
# Hide the 'system mouse cursor'.
mouse.setSystemCursorVisibility(False)

# Ensure that relative paths start from the same directory as this script
_thisDir = os.path.dirname(os.path.abspath(__file__))
os.chdir(_thisDir)

# Store info about the experiment session
expName = 'pm'
expInfo = {'participant':'', 'session':'001', 'age':'', 'gender':'', 'nationality':'', 'occupation':''}
dlg = gui.DlgFromDict(dictionary=expInfo, title=expName)
if dlg.OK == False: core.quit()  # user pressed cancel
expInfo['date'] = data.getDateStr()  # add a simple timestamp
expInfo['expName'] = expName
subNum = expInfo['participant']
age = expInfo['age']
gender = expInfo['gender']
nation = expInfo['nationality']
occupation = expInfo['occupation']

# Data file name stem = absolute path + name; later add .psyexp, .csv, .log, etc
filename = _thisDir + os.sep + 'data' + os.sep + '%s_%s_' %(expInfo['participant'], expName)
print(filename)
filename_winner = _thisDir + os.sep + 'data' + os.sep + file_winner

# Pruefen, ob Dateiname bereits existiert, damit keine Daten ueberschrieben werden
if os.path.exists(filename + 'out' + '.csv') == True: #'out' und '.csv' dazu weil das in Zeile 223 dazudefiniert wird, wenn die Datei wirklich erstellt wird
    sys.exit("Filename " + filename + 'out' + '.csv' + " already exists!")

#Instruktion bereitstellen
def presentInstruction(instruction):
    
    text = visual.TextStim(win,
        text=instruction,
        color=[-1, -1, -1],
        font='Arial',
        height=30,
        wrapWidth=int(0.8*dispsize[0])
        )
    text.draw()
    win.flip()
    event.waitKeys(keyList=['return'])

#Fixationskreuz mit variabler waiting time erstellen
def drawFixation(win):
    fixation = visual.ShapeStim(win, 
        vertices=((0, -0.5), (0, 0.5), (0,0), (-0.5,0), (0.5, 0)),
        lineWidth=5,
        closeShape=False,
        lineColor='black'
    )
    fixation.draw()
    win.flip()
    # draw a waiting time between 900 and 2100 ms
    waitTime = 0.001*random.randint(500, 900)
    core.wait(waitTime)
    # check for quit (the Esc key)
    if event.getKeys(keyList=["escape"]):
        core.quit()

#Praesentation eines 'Durchgangs': Bild, mask, blank intervall
def presentImage(win,stimulus, maskPic1, maskPic2, maskPic3, maskPic4, fillerPic):
    '''#Is the drawing of the images to the screen here at a place where timing isn't important!?'''
    #Bild, mask und blank intervall bereit stellen
    image = stimulus
        
    mask1 = maskPic1
        
    mask2 = maskPic2
        
    mask3 = maskPic3
    
    mask4 = maskPic4
        
    filler = fillerPic
        
    c = 0 #c initalisieren, damit Programm auch weiterlauft, wenn keine Reaktion erfolgt
    io.clearEvents() #damit auch nur gemessen wird, was wir messen wollen
    #Loop ueber die gewuenschte Anzahl an Frames fuer genaues Timing
    for frameN in xrange(presentationFrames): #ohne das zusaetzliche flip unten muesste hier 3 stehen, weil dann bei beginn des dritten Frames (Zaehler auf 2) abgebrochen wuerde und damit 2 Frames komplett dargestellt sind
        image.draw()
        win.flip()
        if frameN == 0:
             t1 = core.getTime() #zeit bei stimulus-onset
    win.flip() #damit das letzte (zweite) Frame auch komplett abgewartet wird, so werden zwei frames gezeigt: das, wo der zaehler bei 0 und bei 1 ist
    for frameN in xrange(presentationFrames):
        mask1.draw()
        win.flip()
    win.flip()
    for frameN in xrange(presentationFrames):
        mask2.draw()
        win.flip()
    win.flip()
    for frameN in xrange(presentationFrames):
        mask3.draw()
        win.flip()
    win.flip()
    for frameN in xrange(presentationFrames):
        mask4.draw()
        win.flip()
    win.flip()
    '''#core.wait(1.5): damit anstelle des fillers werden die RT nicht richtig erfasst, warum?!'''
    waitTime = random.randint(waitIntervalFiller[0], waitIntervalFiller[1])
    for frameN in xrange(waitTime):
        filler.draw()
        win.flip()
    win.flip()

    a = 0#to only capture first release/reaction
    for event in keyboard_io.getReleases(keys=[target_key, distractor_key]):
        if a==0:
            c = event.time - t1 #differenz der zeit aus stimulus-onset und release der spacebar = Reaction Time
            release = event.key
            a += 1
        
    if c != 0:
        return (release, c)
    else:
        return (99, c)

def awarenessScale():
    ratingScale = visual.RatingScale(
                    win, 
                    acceptKeys='space', 
                    low= 1, 
                    high= 4, 
                    scale='1 = keine Wahrnehmung, \n'
                    '2 = schwache Wahrnehmung,\n'
                    '3 = fast klare Wahrnehmung,\n'
                    '4 = klare Wahrnehmung\n\n\n',
                    acceptPreText=''
                )
    item = visual.TextStim(
                    win, 
                    text='Wie deutlich war Ihre Wahrnehmung der Szene?\n\nWählen sie mittels der Ziffern auf der Tastatur eine Zahl aus und bestätigen sie mit der Leertaste. \n\nEingaben können vor der Bestätigung korrigiert werden.', 
                    color='black',
                    font = "Arial",
                    height = 30,
                    wrapWidth=int(0.8*dispsize[0]),
                    pos=(0,200)
                )
    while ratingScale.noResponse:
        item.draw()
        ratingScale.draw()
        win.flip()
    rating = ratingScale.getRating()
    choiceHistory = ratingScale.getHistory()
    return rating

def Feedback(win, message, farbe, wait):
    FB = visual.TextStim(
            win,
            text=message,
            color=farbe,
            font="Arial",
            height=30,
            wrapWidth=int(0.8*dispsize[0])
        )
    FB.draw()
    win.flip()
    core.wait(wait)

#Moeglichkeit fuer Pause einfuegen
def takeBreak(text):
    takeBreak = visual.TextStim(
            win,
            text=text,
            color="black",
            font="Arial",
            height=30,
            wrapWidth=int(0.8*dispsize[0])
        )
    takeBreak.draw()
    win.flip()
    event.waitKeys(maxWait=180, keyList=['return'])

#Keys held down properly?
def checkkeys(win, message, farbe):
    #io.clearEvents()
    if not keyboard_pyglet[key.LCTRL] & keyboard_pyglet[key.RCTRL]: #hier gehen target_key und distractor_key leider nicht'''
        FB = visual.TextStim(
            win,
            text=message,
            color=farbe,
            font="Arial",
            height=30,
            wrapWidth=int(0.8*dispsize[0])
        )
        reminder1.draw()
        reminder2.draw()
        FB.draw()
        win.flip()
        event.waitKeys(keyList=[target_key, distractor_key])

#normaltrial
def normalTrial(rounds):
    presentInstruction(instruction_experiment)
    #import stimuli and info about memorability
    stimList = expFile['filename']
    memorable = expFile['memo'] # 0 = memorable, 1 = not
    sceneCategory = expFile['sceneCat']
    category = expFile['category']
    hitRate = expFile['hitRate']
    #print(stimList)
    #shuffle stimuli while holding association with memorability-info upright
    allinfo = zip(stimList, memorable, sceneCategory, category, hitRate) #put info together; die zip-Fkt. so zu nutzen geht nur in Python 2.7!!!
    random.shuffle(allinfo) #shuffle
    stimList, memorable, sceneCategory, category, hitRate = zip(*allinfo) #decompose again
    #print("cut")
    #print(stimList)
    fails = [] #to collect fails so that after three fails in a row an instructions is displayed to held
    who_wins = [] #to put all RT for correct trials in to later determine who was best
    '''Unterschied zu stimlist[:], memorable[:] ??? Letzteres gibt Warnung: See the caveats in the documentation: http://pandas.pydata.org/pandas-docs/stable/indexing.html#indexing-view-versus-copy'''
    #write output file 
    outputFile = open(filename+'out'+'.csv','w')
    outputFile.write("subject,age,gender,nation,occupation,filename,memo,RT,resp,respCorrect,category,sceneCat,masked,awareness,hitRate,mask1,mask2,mask3,mask4")
    outputFile.write("\n")
    outputFile_winner = open(filename_winner + '.csv', 'a') #to append to the end of the file so as not to overwrite
    #outputFile_winner.write("subject, meanRTcorr")
    #outputFile_winner.write("\n")
    for i in range(rounds):#len(stimList)):
        checkkeys(win, "Halte (" + target_key +") und (" + distractor_key + ") mit deinen Zeigefingern gedrückt!", "black")#Check if key is being held down
        drawFixation(win)
        toPresent = stimList[i] #wenn jedes mal wieder mit random.choice von gesamter Liste aufgerufen wird sind Dopplungen mgl, deshalb oben ueber shuffle
        random.shuffle(masks2)
        mask1 = dict3[masks2[0]]
        mask2 = dict3[masks2[1]]
        mask3 = dict3[masks2[2]]
        mask4 = dict3[masks2[3]]
        #random.shuffle(maskList)
        #mask1, mask2, mask3, mask4 = maskList[0:4]
        #Maskierung zuweisen und dann den dem Filename zugeordneten Image Stimulus abrufen
        if dic[toPresent] == 1:
            resp, RT = presentImage(win,dict2[stimList[i]], mask1, mask2, mask3, mask4, fillerPic) #speichert die gedrueckte Taste und die Reaktionszeit in getrennten Variablen
            masked = 1
        else:
            resp, RT = presentImage(win,dict2[stimList[i]], fillerPic, fillerPic, fillerPic, fillerPic, fillerPic)
            masked = 0
        droppedFrames = win.nDroppedFrames
        #vorausgesetzt, es gab eine Reaktion, ueberpruefen ob korrekte Taste gedrueckt
        if resp != 99:
            if ((sceneCategory[i]==target) & (resp==target_key)) | ((sceneCategory[i]==distractor) & (resp==distractor_key)):
                respCorrect = 1
                Feedback(win, "Richtig!", "green", 0.2)
                fails.append(0)
                who_wins.append(RT)
            else:
                respCorrect = 0
                Feedback(win, "Falsch!", "red", 0.2)
                fails.append(1)
        else:
            respCorrect = 0
            Feedback(win, "Bitte reagieren!", "black", 0.7)
            fails.append(1)
        if RT > maxRT:
            Feedback(win, "Denk daran, so schnell wie möglich zu reagieren!", "black", 0.7)
        rating = awarenessScale()
        #Weiter oben leere Liste erzeugt, in diese wird geschrieben, ob korrekt geantowrtet oder nicht. Bei drei inkorrekten Antworten in Folge([1,1,1]), Erinnerung an Instruktionen.
        if len(fails) >= 3:
            if fails[i-2:i+1] == [1, 1, 1]:
                if not i + 1 == rounds: #damit die Meldung nicht angezeigt wird, obwohl danach gar kein Bild mehr folgt
                    presentInstruction(instructionFails)
                    fails[i-2:i+1] = [0, 0, 0] #damit nicht beim 4. mal hintereinander direkt wieder die Meldung angezeigt wird, ginge auch mit del fails[:] --> loesche jeden index von fails
        #Pause nach 1/4, 2/4 und 3/4, angepasst an nullbasierte Iteration von i
        if i == int(0.25*rounds) - 1 or i == int(0.5*rounds) - 1 or i == int(0.75*rounds) -1:
            takeBreak("Mach eine kleine Pause! \nWenn du bereit bist, fotzufahren, drücke (Enter). \n\nNach spätestens drei Minuten geht es automatisch weiter.")
        #data = [j for i in zip(a,b) for j in i] --> merging lists
        #fill output file
        outputFile.write("{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{},{}\n".format(subNum,age,gender,nation,occupation,stimList[i],memorable[i],"{:.3f}".format(RT),resp,respCorrect,category[i],sceneCategory[i],masked,rating,hitRate[i],masks2[0],masks2[1],masks2[2],masks2[3]))
    meanRTcorr = np.mean(who_wins)
    outputFile_winner.write("{},{}\n".format(subNum, meanRTcorr))

#Fenster bereitstellen
win = visual.Window(
    size=dispsize, #fullscr auf True ueberschreibt size
    units="pix",
    fullscr=fullscreen,
    color=FGC
)

#call trials
normalTrial(rounds_experiment)
1 Like

Can you isolate the precise line which is causing the crash?

The best way to do this is to run the experiment in PyCharm instead of the PsychoPy coder. You can then set a breakpoint just before the crash (perhaps a conditional breakpoint so that it stops on trial 437-ish), then step forwards one line at a time until you provoke the crash.