Why is my event.getKeys() memory buffer not clearing?

Background:

I have a task which plays a video using VLCMovieStim and, every frame, collects ratings on a continuous bipolar scale. To give an idea of what it looks like.
enter image description here
event.getKeys() tracks when either the 2 or 3 buttons are pressed, but not yet released. When these conditions are met, a variable keys changes value. For every frame that keys takes a ‘LEFT’ or ‘RIGHT’ value because of the key presses, the scale will increment 1% in the respective direction. When keys are released, event.clearEvents() clears the memory buffer, effectively changing the value of keys back to ‘STOP’.

Issue:

When the rating scale oscillates enough times, it will break. From wherever the scale currently sits, it progresses to one of the two poles and it stays at that terminal position, no matter what button states exist afterward. print() functions have demonstrated that the value of keys does successfully reset with each frame, but it seems once it gets to the conditionals, it feels that the conditions to change keys to either ‘LEFT’ or ‘RIGHT’ have been met, despite no buttons being pressed. It seems like although I’m changing the value of keys I’m not actually clearing the event memory, but I don’t know why that’s happening. I’m assuming I must have a flaw in my logic somewhere, but after about a week of workshopping this, I’m at a loss:

kb = keyboard.Keyboard()
        if kb.status == STARTED:
            keys = 'STOP'
            # When the left key is pressed and not released, keys = LEFT
            # If either boolean is 'True', simply holding down the button will not yield continuous incrementing in the rating
            if len(kb.getKeys([keyLeft], waitRelease = True, clear = True)) == 0 or len(kb.getKeys([keyRight], waitRelease = True, clear = True)) == 0:
                keys = 'STOP'
                if len(kb.getKeys([keyLeft], waitRelease = False, clear = False)) > 0:
                    keys = 'LEFT'
                    if len(kb.getKeys([keyLeft], waitRelease = True, clear = False)) > 0:
                        kb.clearEvents()
                elif len(kb.getKeys([keyRight], waitRelease = False, clear = False)) > 0:
                    keys = 'RIGHT'
                    if len(kb.getKeys([keyRight], waitRelease = True, clear = False)) > 0:
                        kb.clearEvents()
            if keys == 'LEFT':
                y1 -= round(0.005,3)
                y1 = round(y1,3)
                y3 = ((y1)/2)
                if y1 < 0:
                    keyCount += 1
                if keyCount > 100:
                    keyCount = 100
                if y1 >0:
                    keyCount -= 1
                if y1 == 0.000:
                    keyCount = 0
                if y1 <= -0.5:
                    y1 = -0.5
                    keyCount = 100
            if keys == 'RIGHT':
                y1 += round(0.005,3)
                y1 = round(y1,3)
                y3 = ((y1)/2)
                if y1 > 0:
                    keyCount += 1
                if keyCount > 100:
                    keyCount = 100
                if y1 <0:
                    keyCount -= 1
                if y1 == 0.000:
                    keyCount = 0
                if y1 >= 0.5:
                    y1 = 0.5
                    keyCount = 100 

For the sake of sharing replicable code and not spamming this post, I’ve isolated the necessary code and made it available, along with necessary stimuli, here.

Thanks in advance for your time!

Edits: to clarify wording and update script after I’ve tried other solutions

Follow up:
I’d used print() arguments in the past for this problem with no luck, but I had some success just now when I added print() arguments following if keys = ' * '. They ask Python to print: 1) Statement IDs (so I know which statement output I’m looking at), 2) keys value, 3) y1 value, 4) length of kb.getKeys(), and 5) value of kb.getKeys(). There should be no cases wherein the value of getKeys is greater than 1, even if more than 1 key pressed at a time. However, this excerpt from the output right when the task broke shows it, for some reason, has a length of 2 and then it goes off the rails after that.

# Text output from the print function on a frame by frame basis...
# ...
1 LEFT -0.135 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B4F1C0F0>]
LEFT
1 LEFT -0.14 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B4F1CC18>]
LEFT
1 LEFT -0.145 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B4F1CBE0>]
LEFT
1 LEFT -0.15 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B4F7D358>]
LEFT
1 LEFT -0.155 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F78E10>]
LEFT
1 LEFT -0.16 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F78A20>]
LEFT
1 LEFT -0.165 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F78978>]
LEFT
1 LEFT -0.17 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B4F1CC18>]
LEFT
# ... and right here, the length is all of a sudden 2 rather than 1...
1 LEFT -0.175 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84940>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84FD0>]
LEFT
1 LEFT -0.18 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B4F1C0F0>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B4F7DE48>]
LEFT
1 LEFT -0.185 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B4F1CC18>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F844A8>]
LEFT
1 LEFT -0.19 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B4F7DE48>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84208>]
LEFT
1 LEFT -0.195 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F782E8>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84908>]
LEFT
1 LEFT -0.2 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84B70>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84C88>]
LEFT
1 LEFT -0.205 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B4F7DE48>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84080>]
LEFT
1 LEFT -0.21 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F782E8>]
LEFT
1 LEFT -0.215 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84B70>]
LEFT
1 LEFT -0.22 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84C88>]
LEFT
1 LEFT -0.225 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F787B8>]
LEFT
1 LEFT -0.23 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F78A58>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F840B8>]
LEFT
1 LEFT -0.235 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F78FD0>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84B70>]
LEFT
1 LEFT -0.24 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F782E8>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F842E8>]
LEFT
1 LEFT -0.245 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F78A58>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84978>]
LEFT
1 LEFT -0.25 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84BE0>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F90668>]
LEFT
1 LEFT -0.255 2 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F842E8>, <psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F90C88>]
LEFT
# ... and even after the length returns to 1, I no longer have control of the task.
# Notice the value of y1 increases in magnitude.
1 LEFT -0.26 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84978>]
LEFT
1 LEFT -0.265 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F84BE0>]
LEFT
1 LEFT -0.27 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F78978>]
LEFT
1 LEFT -0.275 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F78A20>]
LEFT
1 LEFT -0.28 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F90D68>]
LEFT
1 LEFT -0.285 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F78978>]
LEFT
1 LEFT -0.29 1 [<psychopy.hardware.keyboard.KeyPress object at 0x000001F1B5F90EB8>]
LEFT
# ...

I think I can probably figure this out, but I figured I would share my progress for posterity’s sake, should anyone else run into a similar problem.

EDIT: I thought simply specifying if len(kb.getKeys[*], ... == 1 might work, in that psychopy would simply ignore the frames will a length longer than 1, but the problem reoccurred, with output looking otherwise “normal” (i.e., length of 1). So it seems like length might not actually be the issue.

EDIT EDIT: Contrary to my comment, totally at a loss for a satisfying solution. I thought maybe the kb.getKeys cache was not resetting every frame due to clear = False, so I respecified the conditions for 'STOP' before and after, which had no effect. I also tried replacing 'keys' with different arrays whose lengths determined what actions would follow, which also did not work. I also tried removing 'keys' entirely and just putting the associated actions following the conditionals, assuming perhaps there was an error in redefining the keys variable so many times.

1 Like

Hi There,

Looking at your code there is a fair bit going on, so before delving in to deep, could I ask if you have tried using the slider component to obtain ratings instead? Something like this might work for what you need and be slightly simpler?
custom_slider.psyexp (12.2 KB)

Hope this helps,
Becca

1 Like

Hi @Wjpmitchell3,
I think you should check for keys only once per frame, but using waitRelease=False, reset=False, and reset the buffer only when the key you get has a .duration that is not None (that means it has been released).
To figure this out I’ve prepared the following short script that you can copy into PsychoPy Coder and run. Pressing and holding the up arrow extends the white box, while holing the down arrow shortens it. Pressing 'q' quits.

from psychopy import visual, core
from psychopy.hardware import keyboard


window = visual.Window(monitor='testMonitor')
box = visual.rect.Rect(window, size=(0.1, 0.1), fillColor='white',
                       units='norm')
kb = keyboard.Keyboard()

box.draw()
window.flip()

min_width = 0.02
max_width = 0.9
accepted_keys = ['up', 'down', 'q']

go_on = True
while go_on:
    keys = kb.getKeys(accepted_keys, waitRelease=False, clear=False)

    if 'q' in keys:
        go_on = False

    if 'up' in keys:
        box.width += 0.02
    elif 'down' in keys:
        box.width -= 0.02

    if box.width < min_width:
        box.width = min_width
    if box.width > max_width:
        box.width = max_width
    
    for key in keys:
        if key.duration is not None:
            # key has been released, we need to clean the buffer
            kb.getKeys([key.name], clear=True)

    box.draw()
    window.flip()

core.quit()

To adapt this to a procedure created in Builder you would have to remove the while loop and the lines where the box is drawn and the window is flipped. Let me know if you need any help with that.

1 Like

Hey everyone, I appreciate the assistance.

@Becca, I really appreciate you taking the time to make that in the builder. It looks great. Unfortunately, for our purposes, though, I cannot use the builder scale, as it demands mouse access and we’re going to have people completing this task while undergoing an MRI, so no mouse access, and we need their ratings on a second-by-second scale for over an hour.

@mmagnuski I appreciate you preparing that. It looks like it could certainly work. For our purposes, participants will only have access to two buttons, so relying on a third to make the scale stop isn’t possible, which is why I have to source event.getKeys twice with release= True, clear = True.

Prior to seeing either of these comments, I came up with my own solution. Sorry I hadn’t seen them earlier. I simply altered the conditional logic to specify that keys can only take a directional value if only one of the keys is currently being pressed. The issue always seems to generate when changing directions, so by limiting one’s ability to do that quickly, I seem to have eliminated the problem. For additional insurance, I specified that keys cannot take a directional value if the scale is currently at either pole. I’ve been trying to break the script for a few hours now and haven’t had luck. Of course, I won’t know for sure until it reoccurs, but fingers crossed. Thanks so much again both of you for your time. I really appreciate it.

        if kb.status == STARTED:
           
            # At the start of the loop, we check whether the scale is at either of its poles, 
            # and if so, we reset the keys variable and clear the event log.
            if abs(y1) >= 0.5:
                keys = 'STOP'
                kb.clearEvents()

            # If the left button is being pressed, and the right button is not being 
            # pressed, and the scale is not currently at the left pole, change the 
            # value of keys to LEFT. These additional conditionals are crucial for me.
            if len(kb.getKeys([keyLeft], waitRelease = False, clear = False)) == 1 and len(kb.getKeys([keyRight], waitRelease = False, clear = False)) != 1 and y1 > -0.5:
                keys = 'LEFT'

                # Once the key is released, clear the memory buffer and change 
                # the value of keys to STOP
                if len(kb.getKeys([keyLeft], waitRelease = True, clear = False)) > 0:
                     keys = 'STOP'
                     kb.clearEvents()

            # Just as above, except for the right key. 
            elif len(kb.getKeys([keyRight], waitRelease = False, clear = False)) == 1 and len(kb.getKeys([keyLeft], waitRelease = False, clear = False)) != 1 and y1 < 0.5 :
                keys = 'RIGHT'
                if len(kb.getKeys([keyRight], waitRelease = True, clear = False)) > 0:
                        keys = 'STOP'
                        kb.clearEvents()
1 Like

@Wjpmitchell3, good you found your own solution. Just a clarification to your response:

It looks like it could certainly work. For our purposes, participants will only have access to two buttons, so relying on a third to make the scale stop isn’t possible, which is why I have to source event.getKeys twice with release= True, clear = True .

The 'q' button in my example ends the procedure, it does not stop the scale. When you stop pressing a button the box stops changing. :slight_smile: So you certainly don’t have to use getKeys twice. But your approach is just as good!

1 Like

Oh man, I’m sorry. I must have looked too quickly. Sorry about that. Thanks for that clarification. @mmagnuski

No problem! :slight_smile: