Task-Switching in Builder view

If this template helps then use it. If not then just delete and start from scratch.

MacOS X
PsychoPy version 3.0.0
Standard Standalone

Hello,

I am currently trying to build an experiment in which participants complete two tasks. The primary task involves responding to a math problem via key press. I have managed to get this part working. However, during every trial, we want there to be a prompt for participants to choose whether or not to complete a secondary task (during the primary task). This prompt should appear in 20% of the trials, and if the participant elects to attempt this task, a different stimuli should appear on the screen, taking the place of the math problem. Once this secondary task is completed, the primary task (math problems) continues as normal.

For now, I am first seeking help on getting this prompt to popup during the primary task.

I have tried following the steps in this thread: Presentation of a sound in a random trial - #2 by Michael but I have not been successful. The text simply does not appear during any of the trials.
I am attaching screenshots of my builder view, as well as the code component I have been trying to work with.

Would anyone be able to give me some advice on how to set this up? Any and all help is appreciated. Please let me know if more information is needed!

Hi,

Thanks for all of the clear information, but please paste in code as text into your message. We can’t cut and paste from your code if it is in a screen shot, and please:

Having said that, you are on the right track with your code but note that you are running it conditionally (only on the first trial), so it will have no effect on subsequent trials. 80% of the time, the stimulus would be set to be invisible, and remain so for the rest of the experiment.

But there is a larger problem here. Rather than do this within one routine, you probably want to split things across routines (i.e. construct your trial from multiple routines), and make the execution of those routines conditional on your popup_prompt_rng variable.

The reason for this is that you can’t have two keyboard components running at the same time, as you have currently. There is only one physical keyboard, so they will be competing and conflicting.

So put your popup message and keyboard on a separate routine, with the message text set to always be visible. Insert a code component on that routine, and generate your popup_prompt_rng variable.

Then add this:

# only run this routine on 1/5 trials:
if popup_prompt_rng != 1:
    continueRoutine = False

If you need the stimuli (e.g. the math problem) from the previous routine to carry over to this one (so the participant doesn’t notice the change), you can do that. Just insert another text component, and carry over its content from the previous routine.

EDIT: actually, looking at your screen shots again, maybe this wouldn’t work for you, as you want that prompt text to appear early in the trial. So just remove the conditional line about .thisN == 0: from your code, and adjust things so that you can just use one keyboard component, to avoid the conflict between them overlapping.

Thank you for your help! I went with your first suggestion, as it seems to fit our experiment better.

I was able to get the popup to work, but unfortunately I have a few more questions.
For this task, we want participants to be able to choose to respond to either the popup or the math problem. On trials with the popup, would it be possible to have it set so that they can ignore the popup and continue with the primary task on screen (math problem)? Because you mentioned being unable to have more than one keyboard component, I’m unsure how to set this up.

I’ve also gone ahead and attempted to insert the secondary task, which is contingent on participant response to the popup (pressing Y), using this code in the “Begin Routine” tab

if popup_key != 'y':
    continueRoutine = False

in a separate routine with the new stimulus and keyboard component. I have also included a points counter for correct and incorrect answers. The primary task is worth 3 points, and the secondary is worth 5.

gainsThisBlock=0
#counting points
if key_resp_3.corr ==1:
   gainsThisBlock=gainsThisBlock+3
else:
   gainsThisBlock=gainsThisBlock-3

gainsTotal=gainsThisBlock + gainsTotal 

is the code I have for the points counters. The above is in the End Routine tab for the two different tasks (with different point values for each).
However, the points counter I have still seems to continue even if the popup wasn’t shown, resulting in a deduction of points for the secondary task.

I really appreciate all the help, as I’m still very new to coding in general. Please let me know if I left out something that may help you understand what the issue is.

OK, there are multiple questions in this post, but not enough information to answer any of them. How about you focus on just one thing at a time, and give all of the information required to answer it. e.g. in the post above, with the first code snippet it is not clear if that is just for information or if there is a problem with it?

e.g. also:

Because you’ve presumably changed things now, it is hard to give an answer here without seeing all of the details of the new setup (relevant screenshots and code snippets).

Apologies for the barrage of questions, I’ll go one at a time.

We want participants to be able to either switch to the secondary task (by pressing y), or to continue with the primary task (the math problem, by pressing C or I). In your first response, you mentioned not being able to have more than one keyboard component in the same routine. If this is the case, is it still possible to have participants respond to one and not the other?

Attached is a screenshot of my builder view, as well as the code in the Begin Routine tab of the “popup_code” code component.

popup_prompt_rng = randint(low = 0, high = 5) 
#1/5 trials
if popup_prompt_rng != 1:
    continueRoutine = False

Don’t get caught up about having multiple keyboard components: consider that the participant only has one physical keyboard in front of them. What is important is how you respond to the keys they choose to press and when. From reading your messages above, I think what you want is an arrangement like this:

  • Routine 1 contains the stimulus for the maths problem and a text prompt that is visible in only one in five trials. There is one keyboard component here.
  • Routine 2 contains the secondary task. This routine will run only if this is a one-in-five trial and the participant chose to do the secondary task.

So I would suggest that the first routine contains the following code:

Begin routine:

popup_prompt_rng = randint(low = 0, high = 6)

if popup_prompt_rng == 1:
    popup_message.opacity = 1
else:
    popup_message.opacity = 0

Each frame:

run_secondary_task = False
response = popup_key.keys # maybe .keys()?

# check if subject wants to do secondary task,
# but only on relevant trials:
if 'y' in response and popup_prompt_rng == 1:
    run_secondary_task = True
    continueRoutine = False # quit this task immediately
else: 
    # actually I'm not sure what else to 
    # expect here from the primary task.
    # Something about keeping score from
    # 'C' and 'I' responses?

Then in the next routine, containing the optional secondary task, put this in its Begin routine tab:

if not run_secondary_task:
    continueRoutine = False

I’m sorry, but I’m not sure I understand. In your suggestion for Routine 1, what would be the keyboard component for, the math problem or to respond to the popup? If I leave it for the math problem, I get an error message since popup_key is not defined, but if I change the component to be for a response to the popup, the math problem can no longer be responded to.

I’ll describe the experiment I’m trying to create again, in case my original description was hard to follow.

In this experiment, there are two tasks. The primary task is responding to whether math problems are correct (‘C’ for correct, ‘I’ for incorrect). For each correct response, participants receive 3 points and lose 3 for an incorrect response. I have a conditions file set up for correct/incorrect responses.
On 20% of trials, they will have a chance to switch to a secondary task. They will elect to do so when a popup appears on their screen (by pressing Y). They may also choose not to switch, in which case they will just respond to the math problem that is still on screen. Should they choose to switch (by pressing Y), they will be taken to a secondary task, a word completion task. They gain or lose 5 points here, depending on correct/incorrect responses (also via condition file). There will be 5 blocks of about 25 trials.

Again, I am very thankful for all your help thus far!

As above, the subject is able to respond with any of the three keys, correct? So you only need one keyboard component (and in fact, you can only have one, as otherwise they compete and conflict with each other). Just specify in that component that the valid keys are 'y', 'c', and 'i'.

It is in your code that you decide how to deal with those responses. i.e. if they push 'y', but this isn’t one of the nominated trials (i.e. popup_prompt_rng does not equal 1), then in the code above, that response will just be disregarded. Otherwise, you deal with the 'c', and 'i' responses in the else clause of that code.

Make sense?

I see, so instead of checking for accuracy on the math problems in the keyboard component (via “store correct” etc.), I would do it in the else clause in the code.

I have followed your suggestions above, but now the popup text is visible on every trial, and while pressing y ends the routine, it does not progress to the secondary task. It instead goes on to the next math problem.
In the Begin Routine tab of Routine 1:

popup_prompt_rng = randint(low = 0, high = 6)

if popup_prompt_rng == 1:
    popup_message.opacity = 1
else:
    popup_message.opacity = 0

In the Each Frame tab of Routine 1:

run_secondary_task = False
response = trialkey.keys #.keys() did not work

# check if subject wants to do secondary task, # but only on relevant trials: 
if 'y' in response and popup_prompt_rng == 1:
    run_secondary_task = True
    continueRoutine = False # quit this task immediately
else:
    continueRoutine = True 
#Point counter in End Routine, otherwise an answer would be incorrect on every frame, resulting in wildly negative values

In the Begin Routine tab of the secondary task:

if not run_secondary_task:     
    continueRoutine = False

I am assuming I need to update the Opacity field of my popup_message but I am not sure with what.

Actually, it can be tricky having this code and the keyboard component both running. i.e. if the component detects a 'y' response and is set to accept only one response, it would decide that the response is incorrect.

So yes, I’d be tempted to delete the keyboard component entirely and handle everything in code:

Each frame: something like

# get just a single key from the keyboard queue:
response = event.getKeys()

if response:
    response = response[0]
    # check if subject wants to do secondary task,
    # but only on relevant trials:
    if response == 'y' and popup_prompt_rng == 1:
        run_secondary_task = True
        # store the answer in the data:
        thisExp.addData('response', response)
        continueRoutine = False # quit this task immediately
    # otherwise, check if this is a response to the task:
    elif response == 'c' or response == 'i': 
        run_secondary_task = False
        # store the answer in the data:
        thisExp.addData('response', response)
        if response == your_correct_answer_variable_name:
            # do whatever happens for a correct response
        else:
            # do whatever happens for an incorrect response

Is that opacity field set to “constant” (it should be)?

If it is, then try inserting some (temporary) debugging print() statements in the opacity code, e.g.

popup_prompt_rng = randint(low = 0, high = 6)

print(popup_prompt_rng)

if popup_prompt_rng == 1:
    print('making visible')
    popup_message.opacity = 1
else:
    print('making invisible')
    popup_message.opacity = 0

No, this should be handled entirely in the code above (which is why we need to check that field is set to “constant”, otherwise the component will be updating that value itself, which will undo the effect of setting it in code).

We probably need to see a screenshot of your current flow panel to understand that, but to start with:

  • The two routines should be consecutive, and embedded within the same loop.
  • The second routine needs its own code component, in order to run its own “begin routine” custom code.
  • Also consider some (temporary) debugging code there too:
print('We got to here...')
if not run_secondary_task: 
    print('But we will not continue.')
    continueRoutine = False

First off, thanks for your ongoing patience!

The code you suggested returns a “list index out of range” error. Removing the [0] resolves this error, but as you would probably expect, no key presses do anything.

I fixed this by having Letter height set to “every repeat”. I had opacity set to that as well, but letter height was set to constant. It seems to work fine now.

I am attaching a screenshot of my view. I have my routines ordered as you describe above, but because I keep getting an error, I have not been able to debug just yet.

I edited the code above to fix this, it would be safer to use that approach, just in case someone mashes a couple of keys.

Not sure why that is being used. but glad to hear that the visibility is working at least.

What is the function of the loop around the second_task routine?

We need to see the full text of this error.

I have edited my code to match this, but there is no response to pressing c or i. I checked the output file, and it is recording responses properly. I am attaching my full code (in the Each Frame tab) here. I tried to stay as close to the code you shared so as not to cause any more errors.

# get just a single key from the keyboard queue:
response = event.getKeys()

if response:
    response = response[0]
    # check if subject wants to do secondary task,
    # but only on relevant trials:
    if response == 'y' and popup_prompt_rng == 1:
        run_secondary_task = True
        # store the answer in the data:
        thisExp.addData('response', response)
        continueRoutine = False # quit this task immediately
    # otherwise, check if this is a response to the task:
    elif response == 'c' or response == 'i': 
        run_secondary_task = False
        # store the answer in the data:
        thisExp.addData('response', response)
        if response == correct: #correct refers to the correct answer to the math problem (either c or i)
            gainsThisBlock=gainsThisBlock+3 #3 points for a correct answer
        else:
            gainsThisBlock=gainsThisBlock-3 # minus 3 points for an incorrect answer 

That loop is for the conditions file tied to that task. There are several word stimuli that can be chosen from.

I was able to get the secondary task working after going back and messing with some of the settings. I’m just unsure about the first task now (as above).

That was very useful to put everything together like that. So it seems that the logic of the code is basically running OK, given that the responses are being recorded properly. I guess the only thing that is missing is that you also need to end the routine in response to the c or i keys, so that the next maths problem can be presented .

ie add continueRoutine = False as the last line within the elif clause above.

For convenience, have also edited below to store whether or not the response was correct, and have also added some defensive coding in case the subject accidentally engages the caps lock key (i.e. we force the response to be lower case):

# check the keyboard queue for events:
response = event.getKeys()

if response: # there was at least one, so
    # get just the last if there were several pushed, 
    # and enforce lower case:
    response = response[0].lower() 

    # check if subject wants to do secondary task,
    # but only on relevant trials:
    if response == 'y' and popup_prompt_rng == 1:
        run_secondary_task = True
        # store the answer in the data:
        thisExp.addData('response', response)
        thisExp.addData('response_correct', 'N/A')
        continueRoutine = False # quit this task immediately

    # otherwise, check if this is a response to the task:
    elif response == 'c' or response == 'i': 
        run_secondary_task = False
        thisExp.addData('response', response)

        # amend the score accordingly:
        if response == correct: # correct refers to the correct answer to the math problem (either c or i)
            gainsThisBlock = gainsThisBlock + 3 # 3 points for a correct answer
            thisExp.addData('response_correct', 'True')
        else:
            gainsThisBlock = gainsThisBlock - 3 # minus 3 points for an incorrect answer
            thisExp.addData('response_correct', 'False')
        
        # go on to the next problem:
        continueRoutine = False 

This worked perfectly for the primary task, thank you!

Because our secondary task consists of multiple key presses (i.e. a word completion task in which two letters are missing from a word), I am assuming that this would be executed much more easily by ditching the keyboard component as was done for the primary task.
The code below (in the Each Frame tab of the secondary task) works for stimuli with only one key press:

response2 = event.getKeys()

if response2: # must allow for multiple key presses, currently only works for 1
    response2 = response2[0].lower() 

# amend the score accordingly:
    if response2 == corrAns: # corrAns refers to the correct answer to the missing letters
        gainsThisBlock = gainsThisBlock + 5 # 5 points for a correct answer
        thisExp.addData('response2_correct', 'True')
        continueRoutine = False
    else:
        gainsThisBlock = gainsThisBlock - 5 # minus 5 points for an incorrect answer
        thisExp.addData('response2_correct', 'False')
        
         #go on to the next problem: 
        continueRoutine = False 

I suspect I would have to further modify

if response2: # must allow for multiple key presses, currently only works for 1
    response2 = response2[0].lower()

but removing the [0] returns an error. Is it still possible to allow for more than one key press, as well as to check for a correct answer (from a conditions file) involving multiple key presses?

The way that keyboard checking happens is that we generally check faster than people can type, so (usually) we would detect each keypress on separate runs through the code (i.e. during separate screen refreshes). This means that we need to maintain a counter across screen refreshes, so we know if we have detected the first key press (in which case, we want to keep on checking), or the second (in which case we see if the response was correct, and then end the trial).

What does your corrAns variable contain? i.e. is it a string of two letters (like 'ab') or a list of two separate letters (like ['a', 'b'])? And are there any other constraints?

And how do you score correctness? Do both letters have to be identified, or is one letter sufficient for half a score? Does the order of responses matter? i.e. would 'a' then 'b' be equivalent to 'b' then 'a', or not?

It has a string of letters, no other constraints. Would it also be possible to have some strings with more or less than two letters?

All letters must be identified for full points.

Yes, if the answer is ab, then ba would be incorrect.

Yes, we can check the length of your corrAns variable on each trial and then use that to determine how many responses we need to count for the trial to be completed. e.g.

In the Begin routine tab, define some variables that will be needed in the Each frame tab:

num_required = len(corrAns)
num_received = 0
answer = ''

In the Each frame code, each time a key press is detected, you increment num_received and add the received letter to answer. If num_received == num_required, then you check if answer is equal to corrAns, and take action as appropriate.

Again, sprinkle in lots of (temporary) print() statements, so you can see if the logic is flowing as required, and check on what the variables look like.

I’m working on a funding application tonight, so you won’t get much more response from me for a while. Give me a prod in a day or so (if you haven’t figured it out independently).

Sorry to bother you again, but I’m still having trouble implementing this last part:

I’ve tried reviewing other forum posts similar to this (Record string response from keyboard and use Enter key to continue loop - #3 by movivi), but I have been unsuccessful so far.

I have this in my Each Frame tab in the secondary task routine:

# check the keyboard queue for events:
wp_response = event.getKeys()

if wp_response: # must allow for multiple key presses
    wp_response = wp_response.lower() 
    for key in keys:
        num_received = []
        num_received += answer
        if len(num_received) == num_required:

# amend the score accordingly:
            if answer == corrAns: # corrAns refers to the correct answer to the word problem, can be any key
                gainsThisBlock = gainsThisBlock + 5 # 5 points for a correct answer
                thisExp.addData('wordproblem_correct', 'True')
                continueRoutine = False
            else:
                gainsThisBlock = gainsThisBlock - 5 # minus 5 points for an incorrect answer
                thisExp.addData('wordproblem_correct', 'False')
        
         #go on to the next problem: 
                continueRoutine = False 

When I run this, I get the following error:
wp_response = wp_response.lower() AttributeError: 'list' object has no attribute 'lower'
I’m assuming this is due to my logic being off (or just completely wrong), but I’m not sure where it’s going wrong.

As an aside, would it be possible to record the time it takes for each key press (and the total time) for this task? I know this

thisExp.addData('response_rt', t)

works for my primary task, but I’m not sure if this is possible for trials with multiple key presses.

As always, thanks for your help!

# check the keyboard queue for events:
wp_response = event.getKeys()

if wp_response: # must allow for multiple key presses
    for key in wp_response:
        key = key.lower()
        num_received = num_received + 1
        answer = answer + key

    if num_received == num_required:
        # amend the score accordingly:
        if answer == corrAns: # corrAns refers to the correct answer to the word problem, can be any key
            gainsThisBlock = gainsThisBlock + 5 # 5 points for a correct answer
            thisExp.addData('wordproblem_correct', 'True')
        else:
            gainsThisBlock = gainsThisBlock - 5 # minus 5 points for an incorrect answer
            thisExp.addData('wordproblem_correct', 'False')
        
        #go on to the next problem: 
        continueRoutine = False 

No, this is just because the .lower() function can only operate on strings of characters (like 'Hello', not a list object, even if that list contains strings (like ['Hello', 'bye'] ). A lot of Python is about getting used to thinking about what sort of object a variable name refers to, and hence what you can do with it. e.g. 2 + 2 == 4 while '2' + '2' == '22', because you can add integers and you can add strings, but what addition means is different for each of these sorts of objects. Meanwhile, '2'.lower() is valid (as '2' is a string, even though it isn’t a letter, while 2.lower() is not a valid operation.

When in doubt, try print(some_variable) and print(type(some_variable)) to see if your variable represents what you think it does.

So anyway, have amended the code above to (hopefully) take care of some of those issues (being more careful to distinguish between numbers, responses, and so on). The basic structure was correct, just some of the variables weren’t being handled correctly (although the second if statement did need to be de-indented so that it didn’t run in response to every letter if more than one was received). Try this and see if it works. Then we can look at the time recording once this is working.