Delay detection from both event.getKeys and key_resp.getKeys

Dear all,

I am writing the code for 6 blocks object nback (0-1-2-0-1-2). Each image stimulus will show up 0.5 secs following 1.5 secs of black screen and there are 16 image stimuli in each n-back block. I am creating the log file record the key press response, stimulus order etc,. I attached the code below. Right now I met a issue that If I press the key for this image stimuli, the response will only be detected in the following stimuli. For example if the current image is my target and I press the key. After I check the log file later. This response was counted for next stimuli. That means my if statement did not detect the key was pressed in the stimuli I pressed the key. I have tried both event.getKeys and also the new keyboard class. Both have one loop delay. My key press event only could be detected until next iteration but not the current iteration I press the key. Feel free to try my code(I also uploaded the test images). Now I only activate the first 0 back block for testing. My environment is Win10+Psychopy 2023.1.2… Much appreciation about your help.

import csv
import random
from datetime import datetime
from psychopy import visual, core, event
from psychopy.hardware import keyboard

# Define the window and stimuli
win = visual.Window(size=(800, 600), fullscr=False, color=[-1,-1,-1])
images = [visual.ImageStim(win, image='image1.jpg'),
          visual.ImageStim(win, image='image2.jpg'),
          visual.ImageStim(win, image='image3.jpg'),
          visual.ImageStim(win, image='image4.jpg'),
          visual.ImageStim(win, image='image5.jpg'),
          visual.ImageStim(win, image='image6.jpg')]


def generate_n_back_stimuli(images, n, num_stimuli, num_n_back):
    # Generate a list of stimuli with no n-back situations
      
    stimuli = [random.choice(images) for _ in range(0, num_stimuli)]

    # Generate a list of indices for the n-back situations
    n_back_indices = random.sample(range(2, num_stimuli-2), num_n_back)

    # Replace the stimuli at the n-back indices with n-back situations
    if n!=0:
        for i in n_back_indices:
            stimuli[i] = stimuli[i-n]
    else:
        for i in n_back_indices:
            stimuli[i] = visual.ImageStim(win, image='image1.bmp')
    
    return stimuli




# Define the n-back tasks
n_back_tasks = [
    {'n': 0, 'stimuli': generate_n_back_stimuli(images, 0, 16, 5)},
    #{'n': 1, 'stimuli': generate_n_back_stimuli(images, 1, 16, 5)},
    #{'n': 2, 'stimuli': generate_n_back_stimuli(images, 2, 16, 5)},
    #{'n': 0, 'stimuli': generate_n_back_stimuli(images, 0, 16, 5)},
    #{'n': 1, 'stimuli': generate_n_back_stimuli(images, 1, 16, 5)},
    #{'n': 2, 'stimuli': generate_n_back_stimuli(images, 2, 16, 5)},
]

"""
n_back_tasks = [
    {'n': 0, 'stimuli': random.sample(images*2, 16)},
    {'n': 1, 'stimuli': random.sample(images*2, 16)},
    {'n': 2, 'stimuli': random.sample(images*2, 16)},
    {'n': 0, 'stimuli': random.sample(images*2, 16)},
    {'n': 1, 'stimuli': random.sample(images*2, 16)},
    {'n': 2, 'stimuli': random.sample(images*2, 16)},
]
"""

# Define the duration of each n-back task and rest
task_duration = 32.0
stimuli_duration = 0.5
black_screen_duration = 1.5
rest_duration = 5.0
key_resp = keyboard.Keyboard()
key_resp.keys = []
key_resp.rt = []

# Define the keyboard keys
key_1 = '1'
key_5 = '5'

# Define the log file
log_file = f'subject_{datetime.now().strftime("%Y%m%d_%H%M%S")}.csv'

# Wait for the key 5 to be pressed to start the loop
start_text = visual.TextStim(win, text='Welcome to Task', pos=(0, 0),height=0.25)
start_text.draw()
win.flip()
while True:
    keys = event.getKeys(keyList=[key_5])
    if key_5 in keys:
        break

with open(log_file, 'w', newline='') as file:
    writer = csv.writer(file)
    writer.writerow(['Task Type', 'Stimulus', 'N-Back', 'Stimulus Image', 'Response', 'Accuracy', 'RT', 'Omission Error', 'Commission Error'])

    # Run the n-back tasks
    correct_responses = 0
    total_responses = 0
    total_omission_errors = 0
    total_commission_errors = 0
    for i, task in enumerate(n_back_tasks):
        # Show the task type
        task_type = visual.TextStim(win, text=f'{task["n"]}-back task', pos=(0, 0),height=0.25)
        task_type.draw()
        win.flip()
        core.wait(rest_duration)

        # Show the stimuli
        for j, stimulus in enumerate(task['stimuli']):
            
            # Show the stimulus for 0.5 seconds
            win.color = [-1, -1, -1]
            stimulus.draw()
            win.flip()
            start_time = core.getTime()
            key_resp.clock.reset()
            
            keys=key_resp.getKeys(keyList=['1'], waitRelease=True)
            core.wait(stimuli_duration)
            
            
            #keys = event.getKeys(keyList=[key_1])
            
            
            
            
            #Show a black screen for 1.5 seconds
            
            win.color = [-1, -1, -1]
            win.flip()
            #kb.clock.reset()
            #keys=kb.getKeys([key_1])
            core.wait(black_screen_duration)
            #core.checkPygletDuringWait=True
            #keys = event.getKeys(keyList=[key_1], waitRelease=True)
            #keys = event.waitKeys(keyList=[key_1], maxWait=stimuli_duration)
            #keys = event.waitKeys(keyList=[key_1], maxWait=stimuli_duration+black_screen_duration)
            #keys = event.getKeys(keyList=[key_1])
                                    
            # Check if the subject pressed the key 1
            if keys is not None and key_1 in keys:
                total_responses += 1
                response = '1'
                rt=keys[0].rt
                #rt = core.getTime() - start_time
                if task['n'] == 0: 
                    if stimulus.image == 'image1.bmp':
                        accuracy = 'Correct'
                        correct_responses += 1
                    elif stimulus.image != 'image1.bmp':
                        accuracy = 'Incorrect'
                        total_commission_errors += 1
                elif j >= task['n'] and stimulus.image == task['stimuli'][j-task['n']].image:
                    accuracy = 'Correct'
                    correct_responses += 1
                else:
                    accuracy = 'Incorrect'
                    total_commission_errors += 1
            else:
                response = ''
                rt = 2
                if task['n'] == 0:
                    if stimulus.image != 'image1.bmp':
                        accuracy = 'Correct'
                        correct_responses += 1
                    elif stimulus.image == 'image1.bmp':
                        accuracy = 'Incorrect'
                        total_omission_errors += 1
                elif j >= task['n'] and stimulus.image != task['stimuli'][j-task['n']].image:
                    accuracy = 'Correct'
                    correct_responses += 1
                else:
                    accuracy = 'Incorrect'
                    total_omission_errors += 1
                
        
            #duration2=(stimuli_duration+black_screen_duration)-(2*stimuli_duration)
            #core.wait(black_screen_duration)
            #core.wait(duration2)
            
            
            
            #core.wait(stimuli_duration)
            # Calculate the reaction time
            #rt = core.getTime() - start_time

            # Show a black screen for 1.5 seconds
            #win.color = [-1, -1, -1]
            #win.flip()
            #core.wait(black_screen_duration)

            # Log the data
            writer.writerow([f'{task["n"]}-back', j+1, task['n'], stimulus.image, response, accuracy, rt, total_omission_errors, total_commission_errors])

        # Rest between tasks
        rest = visual.TextStim(win, text='Rest', pos=(0, 0),height=0.25)
        rest.draw()
        win.flip()
        core.wait(rest_duration)

    # Calculate the percentage of correct responses
    if total_responses > 0:
        score = correct_responses / total_responses * 100
    else:
        score = 0

    # Calculate the percentage of omission errors and commission errors
    if total_responses > 0:
        omission_error_rate = total_omission_errors / total_responses * 100
        commission_error_rate = total_commission_errors / total_responses * 100
    else:
        omission_error_rate = 0
        commission_error_rate = 0

    # Output the results
    print(f'Score: {score:.2f}%')
    print(f'Omission Error Rate: {omission_error_rate:.2f}%')
    print(f'Commission Error Rate: {commission_error_rate:.2f}%')

image5
image4

I think it’s probably this. You’re only checking for the key-press on one single frame. The getKeys executes instantaneously and it only reads evens that happened before it’s called, not those that happen during the core.wait period that follows. The easy solution is just to reverse them.

core.wait(stimuli_duration)
keys=key_resp.getKeys(keyList=['1'], waitRelease=True)

This will pick up any key-press that happened during the wait period.

Alternately, you could do something like this:

key_resp.waitKeys(maxWait=stimuli_duration, keyList=['1'], waitRelease=True)

This will end either when a key is pressed or when stimuli_duration has passed, whichever comes first.

Hi Jonathan,

Thank you for your suggestions and checking my code. Unfortunately the issue remained. There is still one iteration delay after I press the key. I am thinking o the parallel approach to do stimuli presentation and key detection in parallel so they won’t affect each other and there probably won’t be issue from the locations of getKeys in the code. Let me know if you or anyone in this form are familiar with this parallels approach. Thank you.

“In parallel” is sort of a tricky concept, but you could try something like this:

for j, stimulus in enumerate(task['stimuli']):
            
            # Show the stimulus for 0.5 seconds
            win.color = [-1, -1, -1]
            stimulus.draw()
            win.flip()
            start_time = core.getTime()
            key_resp.clock.reset()
            
            stimClock = core.Clock()
            keys=key_resp.getKeys(keyList=['1'], waitRelease=True)

            while stimClock.getTime() < stimuli_duration:
                if len(keys) == 0:
                    keys=key_resp.getKeys(keyList=['1'], waitRelease=True)
                

This should check key_resp until stimuli_duration has elapsed, but doesn’t check once a key has been detected so it won’t overwrite the first response.

Hi Jonathan,

Thank you very much for your reply. I think it works and detects the response in the current cycle now.