Help for Coding for a Bayesian Task in determing spatial frequency threshold for peripheral vision

Here is my problem-

I have to measure high contrast spatial frequency cutoff value for peripheral vision.

Now the stimuli will be presented in the following meridians-

temporal (T), superior-temporal (ST), superior (S), superior-nasal
(SN), nasal (N), inferior-nasal (IN), inferior (I), and inferior-
temporal (IT).

The orientations of gratings mentioned in the
following sections are all relative to the visual field meridian tested,
with 0 deg denoting gratings parallel with the tested meridian
(i.e., oriented radially in the visual field), 90-deg gratings per-
pendicular to the meridian, and 45- and j45-deg gratings with
an oblique orientation relative to the meridian.

All the measurements are done at 14 degree eccentricities.

The Sinusoidal Gabor Gratings are involved in a Gaussisn window whose Gaussian filter had a standard deviation of 1.6 degrees.

The contrast is 100% and the stimuli has no temporal frequency.

It is a 4- alternative forced choice paradigm with the participants pressing 6 for 0 degrees, 9 for +45 degrees, 8 for 90 degrees and 7 for -45 degrees on the keypad. The threshold is that spatial frequency at which 62.5% of the responses are correct.

I want to use a Bayesian adaptive method to analyze and determine the threshold but couldn’t find a satisfactory code for this approach. Can someone help me out with the code?

Hi There,

I think you want a QUEST staircase for this, and I am attaching a basic PsychoPy builder file to demonstrate how one QUEST could be specified. This task requires the participant to use the left and right arrow keys on the keyboard to indicate the orientation of a grating, and estimates the orientation threshold.

In the spreadsheet specifying the QUEST parameters pThreshold is set to 0.625 - which I believe should derive the estimate for 65.5% performance.

Note that this is a 2AFC not a 4AFC - so you will want to adapt the procedure and parameters according to this protocol, but hopefully it gives some indication of the direction you could take!

Becca

readme.md (2.8 KB)

questTemplate4.xlsx (20.3 KB)

quest_staircase.psyexp (16.3 KB)

Hey thanks for your reply. The thing is I am completely new to using Psychopy and so I am extremely sorry for not understanding your reply.

Anyway here is the basic code that I have-

# gabor_meridian_keyed_final_vK_PSI_vNoExternal.py

Version using only PsychoPy for Bayesian (Psi) threshold

from psychopy import visual, core, event, monitors, data
import numpy as np
import pandas as pd
import os, random, time
import matplotlib.pyplot as plt

----------------------------

Monitor Configuration

----------------------------

myMonitor = monitors.Monitor(“myMonitor”)
myMonitor.setWidth(34) # screen width in cm
myMonitor.setDistance(57) # viewing distance in cm
myMonitor.setSizePix((1920, 1000)) # resolution

----------------------------

Window Setup

----------------------------

win = visual.Window(
size=(1360, 768),
fullscr=True,
monitor=myMonitor,
color=[0, 0, 0],
units=‘deg’,
allowGUI=False,
waitBlanking=False,
checkTiming=False
)

----------------------------

Stimulus Setup

----------------------------

ecc = 14
fixation = visual.TextStim(win, text=‘+’, color=‘white’, height=0.6)
gabor = visual.GratingStim(
win=win,
mask=‘gauss’,
sf=2,
size=4,
ori=0,
contrast=1.0,
units=‘deg’
)

feedback_correct = visual.TextStim(win, text=‘✓’, color=‘green’, height=0.8)
feedback_wrong = visual.TextStim(win, text=‘✗’, color=‘red’, height=0.8)

----------------------------

Experiment Parameters

----------------------------

spatial_freqs = np.geomspace(1, 20, 8)
orientations = [45, -45]
response_keys = {‘z’: -45, ‘m’: 45}
n_trials = 50
results =

----------------------------

PsiHandler (Bayesian adaptive)

----------------------------

psi_handler = data.PsiHandler(
stimRange=(1, 20), # spatial frequency range
nTrials=n_trials,
threshold=0.625,
slope=3.5,
guessRate=0.5, # 2AFC task
lapseRate=0.02,
startVal=5.0
)

----------------------------

Trial Loop

----------------------------

print(" Starting experiment… Press ESC to quit.\n")
clock = core.Clock()

try:
for trial in range(n_trials):
sf = psi_handler.next()
ori_rel = random.choice(orientations)
ori_abs = 0 + ori_rel

    # Horizontal meridian
    angle = np.deg2rad(0)
    gabor.pos = (ecc \* np.cos(angle), ecc \* np.sin(angle))
    gabor.sf = sf
    gabor.ori = ori_abs

    # Fixation
    fixation.draw()
    win.flip()
    core.wait(0.5)

    # Stimulus
    gabor.draw()
    win.flip()
    core.wait(0.25)

    # ISI
    win.flip()
    core.wait(0.5)

    # Response
    keys = event.waitKeys(keyList=list(response_keys.keys()) + \['escape'\])
    if 'escape' in keys:
        raise KeyboardInterrupt

    pressed_key = keys\[0\]
    response_ori = response_keys.get(pressed_key, None)
    correct = int(response_ori == ori_rel)

    # Feedback
    if correct:
        feedback_correct.draw()
    else:
        feedback_wrong.draw()
    win.flip()
    core.wait(0.3)

    # Record data
    results.append({
        'trial': trial + 1,
        'sf': sf,
        'ori_rel': ori_rel,
        'pressed_key': pressed_key,
        'correct': correct
    })

    psi_handler.addData(correct)

except KeyboardInterrupt:
print(“\n Experiment safely exited by user.\n”)

finally:
win.close()
core.quit()

----------------------------

Save data

----------------------------

df = pd.DataFrame(results)
timestamp = time.strftime(“%Y-%m-%d_%Hh%M.%S”, time.localtime())
filename = f"gabor_PSI_results_{timestamp}.xlsx"
try:
df.to_excel(filename, index=False)
print(f" Data saved to {filename}“)
except PermissionError:
alt_filename = f"gabor_PSI_results_{timestamp}_backup.xlsx”
df.to_excel(alt_filename, index=False)
print(f"️ File was open; saved instead to {alt_filename}")

----------------------------

Plot Bayesian Psi curve (using PsychoPy PsiHandler estimates)

----------------------------

x = np.linspace(1, 20, 100)
y = psi_handler.mean # mean threshold estimate

For a simple plot, we can show the sequence of stimuli & responses

plt.figure()
plt.title(“Bayesian Psi Curve (Estimated Threshold)”)
plt.xlabel(“Trial”)
plt.ylabel(“Spatial Frequency (cpd)”)
plt.plot([r[‘trial’] for r in results], [r[‘sf’] for r in results], ‘o-’, label=‘Stimulus SF’)
plt.axhline(y, color=‘red’, linestyle=‘–’, label=f’Estimated threshold ≈ {y:.2f} cpd’)
plt.legend()
plt.show()

I used AI to draft this code since I have little experience in coding Python. So if you could elaborate how to integrate QUEST code using into this, it would be helpful. I have installed numpy scipy pandas and matplotlib Python libraries of 3.10.4 version of Python since thats what Psychopy uses.

Thank you

Tarun.

Hi Tarun,

I am going to reply to your email here rather than via email.

Your paradigm is not in my area of expertise (Bayesian, Gabor gratings, staircases), so I do not have an existing demo I can share with you. @Becca has given you a starting point, and I would recommend that you start from her Builder approach rather than using Coder.

If you would like someone to create the experiment for you, or spend a significant length of time helping you, then we do offer consultancy at £70 (+VAT) per hour. Consultancy Services — PsychoPy v2025.2.1

Best wishes,

Wakefield

Hey thanks for the advice. I will try to integrate this on my own. But can I post in this forum if I have any doubts?

You can absolutely post on this forum, but you will get more help here if you ask for help with specific issues rather than the whole task.

Here is my problem-

I am not able to randomly vary my stimulus position, orientation and spatial frequency.

This is the loop I constructed…

The Meridians 1 file.

The Orientations file..

Save Capture 4

This is the main excel file I uploaded for the staircase..

I am stuck here..

Can any one of you solve this?

I’m not sure why you have 3 loops for a trial, but I don’t do staircase experiments.

But psychopy has a loop type condition for staircases.

If I am understanding this experiment correctly, you just need one single csv to control all your stimuli, but I don’t work with staircases so I may be wrong. This is latter staircase is what @Becca is referring to. I suggest you go back and read what they had provided.

Issac

Hi There,

Yes no need to apologise at all for not understanding my response - if you are totally new to PsychoPy I would 100% recommend start with Builder rather than coder. The steps I would suggest to get started:

  1. Download the files I shared in my last post to a single folder.
  2. Open the .psyexp file in PsychoPy Builder and try a run through of the task.
  3. Check the data file to ensure it provides what you need. Specifically, search the value “intensity” in the csv and plot it, you should see that the values converge towards a threshold like the below:

This is a good sanity check “yes, the staircase is staircasing” - lovely.

From there you can consider how to adapt the task to your needs e.g. what do we need to consider to make this a 4AFC rather than a 2AFC (I think this is where you are at based on your response - so this is good so far and apologies if the above it extra info!)

Your question is about randomising orientations, stimulus position and so on. In your QUEST spreadsheet you can also add custom parameters. So see how in this spreadsheet I added the column x_position - in one staircase the stimulus is set to be on the left, on the other the right:

So I have one staircase here where stimuli will be on the left, and another where they will be on the right - this is in the same spreadsheet as the quest params. Then in my stimulus, I set the x coordinate to be x_position and that field to set every repeat.

I am attaching this updates .psyexp and spreadsheet to hopefully help.

Becca :slight_smile:

questTemplate_randomise_params.xlsx (20.4 KB)

quest_staircase_randomise_conditions.psyexp (29.2 KB)

Thank you so much of simplifying. So what i understand from here is that I need to create 8 staircases ( 1 each for the 8 different meridians like Temporal, Nasal, Superior, Inferior, Superotemporal, Superonasal, Inferotemporal, Inferonasal).

My main worry is The next stimulus appears before the subject can press the keypad and randomising the orientation and spatial frequency for each meridian.

If I could understand I guess most of my problem would be solved.

Thanks

Tarun.

For your issue regarding stimuli presentation, set your stimulus (or stimuli) presentation duration to blank (i.e “forever”). Then go to your keyboard response and ensure the “Force end of Routine” button is checked.

Your randomizing concern is answered via making sure your excel sheet is setup properly (see @Becca ‘s nicely laid out csv example above).

Issac

I think my only doubt should I consider each of the 8 meridians as one staircase? It’s for creating the right Excel file. So there are 8 staircases and I have to randomize the orientations and spatial frequency, right?

If you want a separate threshold for each meridian you give it its own staircase - for randomised parameters I would use a code component.

Hi there, thanks for the reply. While I am working on peripheral vision setup, my study also includes a central vision setup as well.

This is the setup I planned for my central vision setup.

No.of trials- 50

 Stimulus Type- Sinusoidal Gabor Grating enveloped in Gaussian window.

 Stimulus Size- 4 degree in diameter.

 Orientation – 4 orientations (0 degrees-horizontal, 90 degreesvertical, -45 degrees- left oblique, 45 degrees – right oblique relative to the meridian of the visual field.

 Stimulus Period- 500 milliseconds

 Interstimulus Period- 750 milliseconds.

 For Fixation- A high contrast Maltese cross will be provided at the centre during the interstimulus period.

Spatial frequency- 0.5-30 cpd

And the orientations and spatial frequency will be varied at random.

Subject has to press the correct key on the keypad according to the orientation with this being a 4 AFC task

For this I need to setup a single staircase right? What modifications do i need to make in the excel file? Thank you