Adaptive staircase

Hi, I am building a SNR adaptive staircase whereby participants have to select the stimulus they heard (randomly chosen & then mixed with noise at different dB) using the following snippet of code: (I have bolded the staircase-relevant parts of code, the rest is just initialising the stimuli etc)

n_reversals_total = 20
threshold = []
def getstaircase(startVal):
    return data.staircase.StairHandler(startVal, stepSizes=[2],
                                maxVal=10, minVal=-10, nDown=1, nUp=1,
                                nTrials=8, nReversals=n_reversals_total, stepType='lin')
    
#Check for key press
event.waitKeys()

Parameters_file = pd.read_csv('Parameters.csv')
Pair_list = Parameters_file.ix[1]
syllable_list = Parameters_file.ix[0]
corr_Ans_list = Parameters_file.ix[2]

Block1 = getstaircase(-10)
thisExp.addLoop(Block1)  # add the loop to the experiment
#condition = Block1.currentStaircase.condition
#thisBlock1 = Block1.trialList[0]  # so we can initialise stimuli with some values
# abbreviate parameter names if possible (e.g. rgb = thisBlock1.rgb)
#if thisBlock1 != None:
    #for paramName in thisBlock1:
        #exec('{} = thisBlock1[paramName]'.format(paramName))

for increment in Block1:
    currentloop = Block1
    # abbreviate parameter names if possible (e.g. rgb = thisBlock1.rgb)b
    #if thisBlock1 != None:
        #for paramName in thisBlock1:
            #exec('{} = thisBlock1[paramName]'.format(paramName))
    # ------Prepare to start Routine "Trial"-------
    continueRoutine = True
    # update component parameters for each repeat
    trial_index = randint(6)
    Pair = Pair_list[trial_index]
    corrAns = corr_Ans_list[trial_index]
    
    key_resp.keys = []
    key_resp.rt = []
    _key_resp_allKeys = []
    text.setText(Pair
)
    shuffled_syllable = syllable_list[trial_index] 
    level = Block1.calculateNextIntensity()
    
    path = '/Desktop/SNR_list'
    filename = os.path.join(path, str(level) +'.mat')
    Stimulus = scipy.io.loadmat(filename)
    Stimulus = Stimulus + shuffled_syllable
    
    sound_1.setSound(Stimulus, hamming=True)
    sound_1.setVolume(1, log=False)
    # keep track of which components have finished
    TrialComponents = [key_resp, text, sound_1]
    for thisComponent in TrialComponents:
        thisComponent.tStart = None
        thisComponent.tStop = None
        thisComponent.tStartRefresh = None
        thisComponent.tStopRefresh = None
        if hasattr(thisComponent, 'status'):
            thisComponent.status = NOT_STARTED
    # reset timers
    t = 0
    _timeToFirstFrame = win.getFutureFlipTime(clock="now")
    TrialClock.reset(-_timeToFirstFrame)  # t0 is time of first possible flip
    frameN = -1
    
    # -------Run Routine "Trial"-------
    while continueRoutine:
        
        # get current time
        t = TrialClock.getTime()
        tThisFlip = win.getFutureFlipTime(clock=TrialClock)
        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
        # update/draw components on each frame
        
        # *key_resp* updates
        waitOnFlip = False
        if key_resp.status == NOT_STARTED and tThisFlip >= 0.1-frameTolerance:
            
            # keep track of start time/frame for later
            key_resp.frameNStart = frameN  # exact frame index
            key_resp.tStart = t  # local t and not account for scr refresh
            key_resp.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(key_resp, 'tStartRefresh')  # time at next scr refresh
            key_resp.status = STARTED
            
            # keyboard checking is just starting
            waitOnFlip = True
            win.callOnFlip(key_resp.clock.reset)  # t=0 on next screen flip
            win.callOnFlip(key_resp.clearEvents, eventType='keyboard')  # clear events on next screen flip
            
        if key_resp.status == STARTED:
            # is it time to stop? (based on global clock, using actual start)
            if tThisFlipGlobal > key_resp.tStartRefresh + 2.5-frameTolerance:
                # keep track of stop time/frame for later
                key_resp.tStop = t  # not accounting for scr refresh
                key_resp.frameNStop = frameN  # exact frame index
                win.timeOnFlip(key_resp, 'tStopRefresh')  # time at next scr refresh
                key_resp.status = FINISHED
                
        if key_resp.status == STARTED and not waitOnFlip:
            theseKeys = key_resp.getKeys(keyList=['left', 'right'], waitRelease=False)
            _key_resp_allKeys.extend(theseKeys)
            
            if len(_key_resp_allKeys):
                key_resp.keys = _key_resp_allKeys[-1].name  # just the last key pressed
                key_resp.rt = _key_resp_allKeys[-1].rt
                # was this correct?
                if (key_resp.keys == str(corrAns)) or (key_resp.keys == corrAns):
                    key_resp.corr = 1
                else:
                    key_resp.corr = 0
                # a response ends the routine
                continueRoutine = False
        
        # *text* updates
        if text.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            
            # keep track of start time/frame for later
            text.frameNStart = frameN  # exact frame index
            text.tStart = t  # local t and not account for scr refresh
            text.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(text, 'tStartRefresh')  # time at next scr refresh
            text.setAutoDraw(True)
        if text.status == STARTED:
            # is it time to stop? (based on global clock, using actual start)
            if tThisFlipGlobal > text.tStartRefresh + 2.5-frameTolerance:
                # keep track of stop time/frame for later
                text.tStop = t  # not accounting for scr refresh
                text.frameNStop = frameN  # exact frame index
                win.timeOnFlip(text, 'tStopRefresh')  # time at next scr refresh
                text.setAutoDraw(False)
                
        # start/stop sound_1
        if sound_1.status == NOT_STARTED and tThisFlip >= 0.1-frameTolerance:
            # keep track of start time/frame for later
            sound_1.frameNStart = frameN  # exact frame index
            sound_1.tStart = t  # local t and not account for scr refresh
            sound_1.tStartRefresh = tThisFlipGlobal  # on global time
            sound_1.play(when=win)  # sync with win flip
        
        # check for quit (typically the Esc key)
        if endExpNow or defaultKeyboard.getKeys(keyList=["escape"]):
            core.quit()
        
        # check if all components have finished
        if not continueRoutine:  # a component has requested a forced-end of Routine
            break
        continueRoutine = False  # will revert to True if at least one component still running
        for thisComponent in TrialComponents:
            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
                continueRoutine = True
                break  # at least one component has not yet finished
        
        # refresh the screen
        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
            win.flip()
    
    # -------Ending Routine "Trial" & adding results to staircase-------
    for thisComponent in TrialComponents:
        if hasattr(thisComponent, "setAutoDraw"):
            thisComponent.setAutoDraw(False)
    sound_1.stop() 
    Block1.addOtherData('sound_1.started', sound_1.tStartRefresh)
    Block1.addOtherData('sound_1.stopped', sound_1.tStopRefresh)
    # check response 
    thisResponse = None
    while thisResponse is None:
        allKeys = key_resp.keys
        for thisKey in allKeys: 
            if thisKey in ['', [], None]:  # No response was made
                thisKey = None
            elif (thisKey == str(corrAns)) or (thisKey == corrAns):
                thisResponse = 1
            elif (thisKey != str(corrAns)) or (thisKey != corrAns):
                thisResponse = 0
        Block1.addResponse(thisResponse)
        Block1.addOtherData('key_resp.rt',key_resp.rt)
        Block1.addOtherData('key_resp.started',key_resp.tStartRefresh)
        Block1.addOtherData('key_resp.stopped',key_resp.tStopRefresh)
        Block1.addOtherData('Syllable_played', shuffled_syllable)
        Block1.addOtherData('SNR_played', SNR)
        Block1.addOtherData('Reversal_intensities', staircase.reversalIntensities)
                
    # store data for Block1 (TrialHandler)
    threshold += np.average(Block1.reversalIntensities)
    # the Routine "Trial" was not non-slip safe, so reset the non-slip timer
    routineTimer.reset()

However, psychopy tells me I have an error in the function calculateNextIntensity in the following code. I am assuming that this is not actually an error in the function but a consequence of something going wrong with my staircase code but cannot figure out what it is. Thank you!

 if self.data[-1] == 1:  # last answer correct
                # got it right
                if self.currentDirection == 'up':
                    reversal = True

The exact error message is:

Traceback (most recent call last):
  File "/Users/emilia/Desktop/Staircase/staircase_trial.py", line 1460, in <module>
    level = Block1.calculateNextIntensity()
  File "/Applications/PsychoPy3.app/Contents/Resources/lib/python3.6/psychopy/data/staircase.py", line 282, in calculateNextIntensity
    if self.data[-1] == 1:  # last answer correct
IndexError: list index out of range

Hi there,

You appear to be calling level = Block1.calculateNextIntensity() before adding any data to the staircase using Block1.addResponse(). Therefore, data[-1] is empty and returns an index out of range error. At least I think that is the problem here :slight_smile: .

You don’t actually need to tell the staircase object to calculate the next intensity, it’s done automatically when you call addResponse(). And setting a startVal initially sets the first intensity level, so there’s no need to call calculateNextIntenisty() at the beginning, either.

I used this guide recently to get the staircase routines going, if you might find it useful.

Cheers,

Martin.

Dear Martin,

Thank you for your response! I’ve changed my code accordingly. I currently have a blank screen for 0.5 s after each staircase step which I’ve added after I add all the responses to my staircase, however, I keep getting syntax errors with lines such as 'routineTimer.reset() and such. Would this be the cause of this issue? I’ve included the new staircase chunk of code below. Sorry I’m a bit of a newbie to pschopy & would greatly appreciate any help! :slight_smile:

Block1 = data.staircase.StairHandler(startVal=0, stepSizes=[2],
                                maxVal=10, minVal=-10, nDown=1, nUp=1,
                                nTrials=10, nReversals=20, stepType='lin',
                                name='Block1')
thisExp.addLoop(Block1)  # add the loop to the experiment
level = thisBlock1 = 0
event.waitKeys()

# Use pandas to read variables. 
Parameters_file = pd.read_csv('Parameters.csv', header=0)
Pair_list = Parameters_file['Pair']
syllable_list = Parameters_file['Stimulus']
corr_Ans_list = Parameters_file['corrAns']

for thisBlock1 in Block1:
    currentloop = Block1
    level = thisBlock1
            
    # ------Prepare to start Routine "Trial"-------
    continueRoutine = True
    
    # update component parameters for each repeat
    trial_index = randint(5)
    Pair = Pair_list[trial_index]
    corrAns = corr_Ans_list[trial_index]
    
    key_resp_7.keys = []
    key_resp_7.rt = []
    _key_resp_allKeys = []
    text_4.setText(Pair
)
    shuffled_syllable = syllable_list[trial_index] 
    Stimulus2 = shuffled_syllable
    
    filename = os.path.join('SNR_list', str(level) +'.mat')
    Stimulus = scipy.io.loadmat(filename, appendmat=False)
    
    sound_2.setSound(Stimulus, hamming=True)
    sound_2.setVolume(1, log=False)
    sound_4.setSound(Stimulus2, hamming=True)
    sound_4.setVolume(1, log=False)
    
    # keep track of which components have finished
    TrialComponents = [key_resp_7, text_4, sound_2, sound_4] 
    for thisComponent in TrialComponents:
        thisComponent.tStart = None
        thisComponent.tStop = None
        thisComponent.tStartRefresh = None
        thisComponent.tStopRefresh = None
        if hasattr(thisComponent, 'status'):
            thisComponent.status = NOT_STARTED
    # reset timers
    t = 0
    _timeToFirstFrame = win.getFutureFlipTime(clock="now")
    TrialClock.reset(-_timeToFirstFrame)  # t0 is time of first possible flip
    frameN = -1
    
    # -------Run Routine "Trial"-------
    while continueRoutine:
        
        # get current time
        t = TrialClock.getTime()
        tThisFlip = win.getFutureFlipTime(clock=TrialClock)
        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
        # update/draw components on each frame
        
        # *key_resp* updates
        waitOnFlip = False
        if key_resp_7.status == NOT_STARTED and tThisFlip >= 0.1-frameTolerance:
            
            # keep track of start time/frame for later
            key_resp_7.frameNStart = frameN  # exact frame index
            key_resp_7.tStart = t  # local t and not account for scr refresh
            key_resp_7.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(key_resp_7, 'tStartRefresh')  # time at next scr refresh
            key_resp_7.status = STARTED
            
            # keyboard checking is just starting
            waitOnFlip = True
            win.callOnFlip(key_resp_7.clock.reset)  # t=0 on next screen flip
            win.callOnFlip(key_resp_7.clearEvents, eventType='keyboard')  # clear events on next screen flip
            
        if key_resp_7.status == STARTED:
            # is it time to stop? (based on global clock, using actual start)
            if tThisFlipGlobal > key_resp_7.tStartRefresh + 2.5-frameTolerance:
                # keep track of stop time/frame for later
                key_resp_7.tStop = t  # not accounting for scr refresh
                key_resp_7.frameNStop = frameN  # exact frame index
                win.timeOnFlip(key_resp_7, 'tStopRefresh')  # time at next scr refresh
                key_resp_7.status = FINISHED
                
        if key_resp_7.status == STARTED and not waitOnFlip:
            theseKeys = key_resp_7.getKeys(keyList=['left', 'right'], waitRelease=False)
            _key_resp_allKeys.extend(theseKeys)
            
            if len(_key_resp_allKeys):
                key_resp_7.keys = _key_resp_allKeys[-1].name  # just the last key pressed
                key_resp_7.rt = _key_resp_allKeys[-1].rt
                # was this correct?
                if (key_resp_7.keys == str(corrAns)) or (key_resp_7.keys == corrAns):
                    key_resp_7.corr = 1
                else:
                    key_resp_7.corr = 0
                # a response ends the routine
                continueRoutine = False
        
        # *text* updates
        if text_4.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            
            # keep track of start time/frame for later
            text_4.frameNStart = frameN  # exact frame index
            text_4.tStart = t  # local t and not account for scr refresh
            text_4.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(text_4, 'tStartRefresh')  # time at next scr refresh
            text_4.setAutoDraw(True)
        if text_4.status == STARTED:
            # is it time to stop? (based on global clock, using actual start)
            if tThisFlipGlobal > text_4.tStartRefresh + 2.5-frameTolerance:
                # keep track of stop time/frame for later
                text_4.tStop = t  # not accounting for scr refresh
                text_4.frameNStop = frameN  # exact frame index
                win.timeOnFlip(text_4, 'tStopRefresh')  # time at next scr refresh
                text_4.setAutoDraw(False)
                
        # start/stop sound_1
        if sound_2.status == NOT_STARTED and tThisFlip >= 0.1-frameTolerance:
            # keep track of start time/frame for later
            sound_2.frameNStart = frameN  # exact frame index
            sound_2.tStart = t  # local t and not account for scr refresh
            sound_2.tStartRefresh = tThisFlipGlobal  # on global time
            sound_2.play(when=win)  # sync with win flip
        
        # check for quit (typically the Esc key)
        if endExpNow or defaultKeyboard.getKeys(keyList=["escape"]):
            core.quit()
        
        # check if all components have finished
        if not continueRoutine:  # a component has requested a forced-end of Routine
            break
        continueRoutine = False  # will revert to True if at least one component still running
        for thisComponent in TrialComponents:
            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
                continueRoutine = True
                break  # at least one component has not yet finished
        
        # refresh the screen
        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
            win.flip()
    
    # -------Ending Routine "Trial" & adding results to staircase-------
    for thisComponent in TrialComponents:
        if hasattr(thisComponent, "setAutoDraw"):
            thisComponent.setAutoDraw(False) 
    
    # check response 
    thisResp = None
    while thisResp == None:
        allKeys = event.waitKeys()
        for thisKey in allKeys: 
            if thisKey in ['', [], None]:  # No response was made
                thisKey = None
            elif (thisKey == str(corrAns)) or (thisKey == corrAns):
                thisResp = 1
            elif (thisKey != str(corrAns)) or (thisKey != corrAns):
                thisResp = -1
            elif thisKey in ['q', 'escape']:
                core.quit()  # abort experiment
        event.clearEvents()
        
    # store data for Block1 (StairHandler)
    Block1.addResponse(thisResp)
    Block1.addOtherData('key_resp_7.rt',key_resp_7.rt)
    Block1.addOtherData('key_resp_7.started',key_resp_7.tStartRefresh)
    Block1.addOtherData('key_resp_7.stopped',key_resp_7.tStopRefresh)
    Block1.addOtherData('Syllable_played', shuffled_syllable)
    Block1.addOtherData('SNR_played', SNR)
    Block1.addOtherData('Reversal_intensities', Block1.reversalIntensities)
    Block1.addOtherData('text_4.started', text_4.tStartRefresh)
    Block1.addOtherData('text_4.stopped', text_4.tStopRefresh)
    sound_2.stop() # ensure the sound has stopped at the end of the routine
    Block1.addOtherData('sound_2.started', sound_2.tStartRefresh)
    Block1.addOtherData('sound_2.stopped', sound_2.tStopRefresh)
    Block1.addOtherData('Reversal_intensity_average', np.average(Block1.reversalIntensities)
    # the Routine "Trial" was not non-slip safe, so reset the non-slip timer
    routineTimer.reset()
    

    
    # ------Prepare to start Routine "Blank500"-------
    continueRoutine = True
    routineTimer.add(0.500000)
    # update component parameters for each repeat
    # keep track of which components have finished
    Blank500Components = [text_3]
    for thisComponent in Blank500Components:
        thisComponent.tStart = None
        thisComponent.tStop = None
        thisComponent.tStartRefresh = None
        thisComponent.tStopRefresh = None
        if hasattr(thisComponent, 'status'):
            thisComponent.status = NOT_STARTED
    # reset timers
    t = 0
    _timeToFirstFrame = win.getFutureFlipTime(clock="now")
    Blank500Clock.reset(-_timeToFirstFrame)  # t0 is time of first possible flip
    frameN = -1
    
    # -------Run Routine "Blank500"-------
    while continueRoutine and routineTimer.getTime() > 0:
        # get current time
        t = Blank500Clock.getTime()
        tThisFlip = win.getFutureFlipTime(clock=Blank500Clock)
        tThisFlipGlobal = win.getFutureFlipTime(clock=None)
        frameN = frameN + 1  # number of completed frames (so 0 is the first frame)
        # update/draw components on each frame
        
        # *text_3* updates
        if text_3.status == NOT_STARTED and tThisFlip >= 0.0-frameTolerance:
            # keep track of start time/frame for later
            text_3.frameNStart = frameN  # exact frame index
            text_3.tStart = t  # local t and not account for scr refresh
            text_3.tStartRefresh = tThisFlipGlobal  # on global time
            win.timeOnFlip(text_3, 'tStartRefresh')  # time at next scr refresh
            text_3.setAutoDraw(True)
        if text_3.status == STARTED:
            # is it time to stop? (based on global clock, using actual start)
            if tThisFlipGlobal > text_3.tStartRefresh + 0.5-frameTolerance:
                # keep track of stop time/frame for later
                text_3.tStop = t  # not accounting for scr refresh
                text_3.frameNStop = frameN  # exact frame index
                win.timeOnFlip(text_3, 'tStopRefresh')  # time at next scr refresh
                text_3.setAutoDraw(False)
        
        # check for quit (typically the Esc key)
        if endExpNow or defaultKeyboard.getKeys(keyList=["escape"]):
            core.quit()
        
        # check if all components have finished
        if not continueRoutine:  # a component has requested a forced-end of Routine
            break
        continueRoutine = False  # will revert to True if at least one component still running
        for thisComponent in Blank500Components:
            if hasattr(thisComponent, "status") and thisComponent.status != FINISHED:
                continueRoutine = True
                break  # at least one component has not yet finished
        
        # refresh the screen
        if continueRoutine:  # don't flip if this routine is over or we'll get a blank screen
            win.flip()
    
    # -------Ending Routine "Blank500"-------
    for thisComponent in Blank500Components:
        if hasattr(thisComponent, "setAutoDraw"):
            thisComponent.setAutoDraw(False)
    thisExp.nextEntry()

win.flip()
thisExp.saveAsWideText(filename+'.csv')
thisExp.saveAsPickle(filename)

No problem!

I’m afraid I don’t have the time to check all of your code, but for routineTimer.reset() it looks like you haven’t completed the parenthesis on the line above it:

Block1.addOtherData('Reversal_intensity_average', np.average(Block1.reversalIntensities)

This should be:

Block1.addOtherData('Reversal_intensity_average', np.average(Block1.reversalIntensities))

When Python throws those kinds of errors, it can be worth looking at the previous line.

If you’re encountering a bunch of these types of errors, you might benefit from using an IDE (basically a graphical coding interface) like Spyder. It has automated code checking, and can be a bit easier to use that PsychoPy’s scripting interface. If you want to do that, it’s easiest to install via the “Anaconda” distribution, which has Spyder packaged, but you need to do a tiny bit of terminal work to get PsychoPy to be recognised by it.

Instructions for that from the PsychoPy Developers are here

Cheers,

Martin

Thank you so much - that was indeed the issue!! :slight_smile: