Variable timings of stimulus presentation based on response times in a 2-back test

OS: macOS Sequoia 15.3.2
PsychoPy version: 2024.2.4 Py 3.10
Standard Standalone Installation? (y/n): y
Do you want it to also run online? (y/n): y

What are you trying to achieve?:
Update on- and offset timing based on click responses within a routine

What did you try to make it work?:

Hi everyone,

I am working on mnemonic discrimination task using image stimuli in a 2-back format. The routine consists of a sequence of 4 images including 2 original and 2 repetitions. For repetitions 1 and 2 participants are supposed to give an old/new answer based on mouse clicks (either on a button ā€˜old’ or on the image itself whereupon feedback is shown).

What is a trial?: A trial consists of sequence of four stimuli, 2 original images followed by 2 repetitions of those images. I am showing the original images for 5s each with a 1s break inbetween. The first and second repeated images are either exact repetitions of the first and second original image or are very similar lures. The repetitions are each shown for 15s and involve the participant to respond via a mouse click whether the repetitions is indeed the old stimulus shown before or a similar but new one. If there is no click the events are thus:

OrigImg1(5s) - 1s - OrigImg2(5s) - 1s - RepImg1(15s) - 1s - RepImg2(15s).

However, if a participant makes a response on RepImg1, I don’t want them to wait the full 15s. Upon the response they should see the feedback for 0.75s, have the 1s break and then should already see RepImg2. This means that offset of RepImg1 and onset of RepImg2 have to be updated based on the click time.

One way of achieving this is probably to leave the start/stop tabs in the builder component blank and condition the on and offsets via setAutoDraw to either a response click or elapsed time. However, I was wondering whether it would in principle work to update onsets and offsets where the logic would be something like if click, then offset1 = timeOfClick + 0.75 as was similarly suggested here using keypresses: Variable feedback timing and total trial time question - #6 by jderrfuss

My approach: Define default onsets and offsets in the Begin routine tab and update them in Each frame if a response was made.

Begin routine:

# initialize dynamic onset variables
def_onset_1 = stim_dur*2+2
def_onset_2 = stim_dur*2+3+rep_dur
def_onset_fb1 = stim_dur*2+2+rep_dur
def_onset_fb2 = stim_dur*2+3+rep_dur*2
onset_1 = def_onset_1 # start with defaults
onset_2 = def_onset_2
onset_fb1 = def_onset_fb1
onset_fb2 = def_onset_fb2

# initialize dynamic offset variables
def_offset_1 = stim_dur*2+2+rep_dur+0.75
def_offset_2 = stim_dur*2+3+rep_dur*2+0.75
def_offset_mouse1 = stim_dur*2+2+rep_dur
def_offset_mouse2 = stim_dur*2+2+rep_dur*2
offset_1 = def_offset_1 # start with defaults
offset_2 = def_offset_2
offset_mouse1 = def_offset_mouse1
offset_mouse2 = def_offset_mouse2

# initialize clicks: if no click, use default timings 
click1 = def_offset_1
click2 = def_offset_2

Specifically, based on the timing of the first response, I want participants to get feedback right away (for 0.75 s), have a 1-second delay and then immediately present the second repetition (not waiting for the default offset). I was trying this:

Each frame:

if len(mouseRepeat1.clicked_name) > 0:
    click1 = mouseRepeat1.getPressed(getTime=True)[0][1] 
    onset_fb1 = click1  
    offset_mouse1 = click1  
    offset_1 = click1 + 0.75 
    onset_2 = click1 + 1.75  

    if mouseRepeat1.clicked_name[0] == 'imageButton1':
        frameButton1.opacity = 1
        feedbackCorrect1.opacity = 1 if repeat1 == stim1 else 0
        feedbackIncorrect1.opacity = 1 if repeat1 != stim1 else 0
    elif mouseRepeat1.clicked_name[0] == 'imageRepeat1':
        imageFrame1.opacity = 1
        feedbackCorrect1.opacity = 1 if repeat1 != stim1 else 0
        feedbackIncorrect1.opacity = 1 if repeat1 == stim1 else 0

if len(mouseRepeat2.clicked_name) > 0:
    click2 = mouseRepeat2.getPressed(getTime=True)[0][1]
    onset_fb2 = click2  
    offset_mouse2 = click2  
    offset_2 = click2 + 0.75 

    if mouseRepeat2.clicked_name[0] == 'imageButton2':
        frameButton2.opacity = 1
        feedbackCorrect2.opacity = 1 if repeat2 == stim2 else 0
        feedbackIncorrect2.opacity = 1 if repeat2 != stim2 else 0
    elif mouseRepeat2.clicked_name[0] == 'imageRepeat2':
        imageFrame2.opacity = 1
        feedbackCorrect2.opacity = 1 if repeat2 != stim2 else 0
        feedbackIncorrect2.opacity = 1 if repeat2 == stim2 else 0

# make sure mouse cursor is only visible when needed:
if onset_1 <= myClock.getTime() <= click1: 
    if onset1 == 1:
        onset1 = onset1 + 1
        mouseRepeat1.setPos([0,0])
    win.mouseVisible = True
elif onset_2 < myClock.getTime() <= click2:
    if onset1 == 2:
        onset1 = onset1 + 1
        mouseRepeat2.setPos([0,0])
    win.mouseVisible = True
else:
    win.mouseVisible = False

What specifically went wrong when you tried that?:
The code works for updating the feedback onsets as they are directly triggered by the timing of the clicks (click1 and click2). However, something like onset_2 = click1 + 1.75 does not result in ā€œwaitingā€ 1.75 s since click1 but also sets onset_2 to the click1-timepoint. What can I do? I’d be happy for any advice!

Hi @VTKoenigsmark,

Sorry, I have to admit that I don’t really understand your experimental paradigm yet. Do you perhaps have a figure that illustrates the events in a trial? (No worries if not - I’ll ask some additional questions in that case.)

Best,
Jan

Hi @jderrfuss,

Thanks for responding so quick and sorry for not being explicit enough on what constitutes a trial. I have now tried to make it more clear in the original post. Let me know if you need more details!

Cheers,
Varg

Hi @VTKoenigsmark,

Please correct me if I’m misunderstanding the design, but might something like this work?:

In this design, your first two images would be presented for a fixed duration of five seconds. For the repetitions on the other hand you would add a mouse component and use ā€œEnd Routine on pressā€ to end the routine when a response is given. This ensures that participants wouldn’t have to wait for the full 15 s. The inter-trial interval could be created by delaying the onset of the pictures by 1 s (not included in the screenshot above).

You could potentially make your code a bit more efficient by doing this and using ā€œSelected rowsā€ in imgLoop and repLoop:

However, it would also complicate things somewhat in the sense that you would need to use variables to update the selected rows on each iteration.

Hope this helps!

Jan

2 Likes

Thanks a lot @jderrfuss! This would probably work but I’d have to use more routines. Right now, I have all images and reps in a single routine, like this:

I’m curious now, is there any chance that I could in principle achieve this within a routine?

Again, thanks for your support!
Varg

Hi @VTKoenigsmark,

It’s probably possible, but my impression is that the one-routine approach complicates things because you have to handle several aspects manually (i.e., by writing dedicated code), whereas my suggested solution takes care of them automatically (i.e., by leveraging the built-in capabilities of the Builder routines and components).

Best wishes,
Jan

1 Like