psychopy.org | Reference | Downloads | Github

How to use questhandler to measure threshold?

I am new in psychopy, trying to use the questhandler. I tried to follow the instruction on line http://www.psychopy.org/api/data.html#questhandler but I can not fully understand it. I try to make some changes on the JND_staircase_exp.py demo, when I input this:“staircase = data.QuestHandler(staircase._nextIntensity, 0.2,
pThreshold=0.63, gamma=0.01, nTrials=20, minVal=0, maxVal=1)” it said “NameError: name ‘staircase’ is not defined”
I don’t know why.
Can anybody help me?Or do you have a demo of QUEST?
Thanks

That API example doesn’t make too much sense to me, as the first function parameter refers to an attribute of the object staircase, when staircase is what the function should return. i.e. staircase only gets defined after the function ends, and so can’t be used in its own creation.

I suggest you just replace that first parameter in the function call with an explicitly named startVal value, e.g.

staircase = data.QuestHandler(startVal = 0.5, startValSd = 0.2,
pThreshold = 0.63, gamma = 0.01, nTrials = 20, minVal = 0, maxVal = 1)

(Of course, I have no idea of whether 0.5 is a valid start value for your particular situation.)

i.e. in this case, if the demo code works, trust that over the API page, which seems to contain an error.

2 Likes

Yep, looks like the API example is wrong. I’ve submitted a PR on github to fix it using your values Michael!

3 Likes

Thank you guys!I was really confused. Can I have a look of your code?I still don’t know how to make questhandler work.

Here is an example that works for me. Run this, and press left or right if the lines are tilted counter-clockwise or clockwise, respectively:

from psychopy import core, visual, event, monitors, data, gui
import numpy as np

screen = monitors.Monitor('testMonitor')
screen.setSizePix([1680, 1050])
screen.setWidth(47.475)
screen.setDistance(57)
win = visual.Window(allowGUI=True, units='deg', monitor=screen)


grating = visual.GratingStim(win, tex='sin', mask='gauss', contrast=1,
                             sf=3, size=3)

staircase = data.QuestHandler(0.5, 0.2, pThreshold=0.63, gamma=0.01,
                              minVal=0, maxVal=1, ntrials=10)

orientations = [-45, 45]
responses = ['left', 'right']

for contrast in staircase:
    keys = []
    # randomise orientation for this trial
    grating.ori = np.random.choice(orientations)
    # update the difficulty (the contrast)
    grating.contrast = contrast
    # before the trial: wait 500ms
    core.wait(0.5)
    # start the trial: draw grating
    grating.draw()
    win.flip()
    # leave grating on screen for 0.2 seconds
    core.wait(0.2)
    win.flip()
    # wait for response
    while not keys:
        keys = event.getKeys(keyList=['left', 'right'])
    # check if it's the correct response:
    if responses[orientations.index(grating.ori)] in keys:
        response = 1
    else:
        response = 0
    # inform QUEST of the response, needed to calculate next level
    staircase.addResponse(response)

print('The threshold is {s}'.format(s=staircase.mean()))

It should get harder. It only runs for 10 trials so of course this isn’t that reliable, but I just wanted to illustrate it.

Thank you, I can run your demo~But I dont get it,the threshold of contrast should be what you want to measure,but subject have to judge orientation,how can this response influence contrast in staircase?

This was just a simple example, and if you’re interested in an actual contrast detection threshold or contrast discrimination thresholds / just noticeable differences, then you should change the task to suit your needs.

QUEST simply gives you a “difficulty level”, and you can then do with that level whatever you like. What is it you want to measure?

As far as I can tell you’re using it correctly. The main issue you have is when determining if correct or not.

You set target side as either -1 or 1, so your code for checking the response should be:

while thisResp == None:
        allKeys = event.waitKeys()
        for thisKey in allKeys:
            if thisKey == 'left':
                if targetSide > 0:
                    thisResp = 0  # incorrect
                else:
                    thisResp = 1
            elif thisKey == 'right':
                if this < 0:
                    thisResp = 0  # incorrect
                else:
                    thisResp = 1  # correct

You were comparing this (your contrast) (which, by the way I’d change to be more explicit - call it something like thisContrast) to 0.3. I think that’s just a bug.

When I change it, it at least changes the contrast depending on my response.

EDIT: I posted this before you withdrew your post, apologies if it’s based on outdated code

this is something I wrote,it can run,but won’t change correctly according to my response.

sorry I was wrong before,I want to let the object to compare target with foil (the contrast of target will change while the contrast of foil staystill like 0.3)and the position of two stimulus are random. I want them to press’left’ if they feel the target is lower in contrast,‘right“otherwise.

Ah, OK, I understand. I see a few issues.

In that case, you need to think about what you’re passing to the actual QUEST function. Like I said, QUEST works with “difficulty” values, but currently you are using the absolute contrast value. Since people are comparing them, what you should really be doing is calculate the difference between the target contrast and the reference contrast.

So, for your QUEST creation you need to change your minVal and maxVal, since they’ll be added to the reference conrast of 0.3:

staircase = data.QuestHandler(0.3, 0.2, delta=0.01, beta=3.5, stopInterval=0.01,
                              grain=0.01, pThreshold=0.63, gamma=0.1, minVal=0, maxVal=0.3, nTrials=20)

Then for your target presentation you should add a random choice between contrast increase and decrease:

targetDirection = random.choice([1, -1])

target.setContrast(0.3 + targetDirection * this)

Lastly you need to make sure you’re still recording the response correctly, so change your if statements to:

    # get response
    thisResp = None
    while thisResp == None:
        allKeys = event.waitKeys()
        for thisKey in allKeys:
            if thisKey == 'left':
                if target.contrast > foil.contrast:
                    thisResp = 0  # incorrect
                else:
                    thisResp = 1
            elif thisKey == 'right':
                if target.contrast < foil.contrast:
                    thisResp = 0  # incorrect
                else:
                    thisResp = 1  # correct
            elif thisKey in ['q', 'escape']:
                core.quit()  # abort experiment

Finally, you print the mean of the last 6 reversals, but QuestHandler actually doesn’t have reversals. Instead, try staircase.mean().

1 Like

You are so right about the questhandler,now I understand the ‘difficulty’. You are sooooo kind,thank you again!!

1 Like